Files
NextEdu/src/modules/settings/components/admin-settings-view.tsx
SpecialX 5d42495480 feat(settings): 设置与个人信息模块审计重构 — i18n + 服务注入解耦 + Error Boundary + 流式渲染
- 新增 SettingsService 接口 + Context 注入,组件层不再直接 import users/messaging actions

- 新增 resolveRoleSettingsConfig 配置驱动角色路由,删除 parent/student/teacher-settings-view 冗余文件

- 新增 SettingsSectionErrorBoundary,每个 TabsContent + profile 角色概览区块均包裹

- 新增 ProfileStudentOverview/ProfileTeacherOverview 异步 Server Component + 骨架屏,支持流式渲染

- 抽取 buildStudentOverviewData 等纯函数到 lib/student-overview-data.ts,便于单元测试

- 新增 settings.json 翻译文件(zh-CN + en),所有组件改用 useTranslations/getTranslations

- 重构 profile/page.tsx:i18n 适配 + Suspense 分区加载 + 业务逻辑抽离

- 同步更新架构图 004/005
2026-06-22 16:15:36 +08:00

205 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import * as React from "react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { School, Shield, Database, Bell } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import { Input } from "@/shared/components/ui/input"
import { Label } from "@/shared/components/ui/label"
import { Textarea } from "@/shared/components/ui/textarea"
import { Switch } from "@/shared/components/ui/switch"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Separator } from "@/shared/components/ui/separator"
/**
* 管理员系统设置视图
*
* TODO: 当前为 mock 实现setTimeout 模拟保存),未接入真实数据层。
* 后续需新增 system_settings 表 + data-access + actions替换 mock 逻辑。
* 当前已适配 i18n文本均通过 settings.admin.* 翻译键获取。
*/
export function AdminSettingsView() {
const t = useTranslations("settings.admin")
const [saving, setSaving] = React.useState(false)
const handleSave = async (e: React.FormEvent) => {
e.preventDefault()
setSaving(true)
// TODO: 替换为真实 Server Action 调用
await new Promise<void>((resolve) => setTimeout(resolve, 800))
toast.success(t("saveSuccess"))
setSaving(false)
}
return (
<div className="flex h-full flex-col space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-muted-foreground">{t("description")}</p>
</div>
<form onSubmit={handleSave} className="space-y-6">
{/* 学校信息 */}
<Card className="shadow-none">
<CardHeader>
<div className="flex items-center gap-2">
<School className="h-5 w-5 text-primary" />
<div>
<CardTitle className="text-base">{t("schoolInfo.title")}</CardTitle>
<CardDescription>{t("schoolInfo.description")}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="school-name">{t("schoolInfo.name")}</Label>
<Input id="school-name" name="schoolName" placeholder={t("schoolInfo.namePlaceholder")} />
</div>
<div className="space-y-2">
<Label htmlFor="school-code">{t("schoolInfo.code")}</Label>
<Input id="school-code" name="schoolCode" placeholder={t("schoolInfo.codePlaceholder")} />
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="school-phone">{t("schoolInfo.phone")}</Label>
<Input id="school-phone" name="schoolPhone" placeholder={t("schoolInfo.phonePlaceholder")} />
</div>
<div className="space-y-2">
<Label htmlFor="school-email">{t("schoolInfo.email")}</Label>
<Input id="school-email" name="schoolEmail" type="email" placeholder={t("schoolInfo.emailPlaceholder")} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="school-address">{t("schoolInfo.address")}</Label>
<Input id="school-address" name="schoolAddress" placeholder={t("schoolInfo.addressPlaceholder")} />
</div>
<div className="space-y-2">
<Label htmlFor="school-desc">{t("schoolInfo.description2")}</Label>
<Textarea id="school-desc" name="schoolDescription" placeholder={t("schoolInfo.descriptionPlaceholder")} rows={3} />
</div>
</CardContent>
</Card>
{/* 安全策略 */}
<Card className="shadow-none">
<CardHeader>
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-primary" />
<div>
<CardTitle className="text-base">{t("securityPolicy.title")}</CardTitle>
<CardDescription>{t("securityPolicy.description")}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="password-min-length">{t("securityPolicy.passwordMinLength")}</Label>
<Input id="password-min-length" name="passwordMinLength" type="number" min={6} max={32} defaultValue={8} />
</div>
<div className="space-y-2">
<Label htmlFor="session-timeout">{t("securityPolicy.sessionTimeout")}</Label>
<Input id="session-timeout" name="sessionTimeout" type="number" min={5} max={1440} defaultValue={60} />
</div>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="require-special-char">{t("securityPolicy.requireSpecialChar")}</Label>
<p className="text-sm text-muted-foreground">{t("securityPolicy.requireSpecialCharDesc")}</p>
</div>
<Switch id="require-special-char" name="requireSpecialChar" defaultChecked />
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="require-uppercase">{t("securityPolicy.requireUppercase")}</Label>
<p className="text-sm text-muted-foreground">{t("securityPolicy.requireUppercaseDesc")}</p>
</div>
<Switch id="require-uppercase" name="requireUppercase" />
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="force-password-change">{t("securityPolicy.forcePasswordChange")}</Label>
<p className="text-sm text-muted-foreground">{t("securityPolicy.forcePasswordChangeDesc")}</p>
</div>
<Switch id="force-password-change" name="forcePasswordChange" defaultChecked />
</div>
</CardContent>
</Card>
{/* 文件上传 */}
<Card className="shadow-none">
<CardHeader>
<div className="flex items-center gap-2">
<Database className="h-5 w-5 text-primary" />
<div>
<CardTitle className="text-base">{t("fileUpload.title")}</CardTitle>
<CardDescription>{t("fileUpload.description")}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="max-file-size">{t("fileUpload.maxFileSize")}</Label>
<Input id="max-file-size" name="maxFileSize" type="number" min={1} max={100} defaultValue={10} />
</div>
<div className="space-y-2">
<Label htmlFor="allowed-types">{t("fileUpload.allowedTypes")}</Label>
<Input id="allowed-types" name="allowedTypes" placeholder={t("fileUpload.allowedTypesPlaceholder")} defaultValue="jpg,png,pdf,docx,xlsx,pptx" />
</div>
</div>
</CardContent>
</Card>
{/* 通知配置 */}
<Card className="shadow-none">
<CardHeader>
<div className="flex items-center gap-2">
<Bell className="h-5 w-5 text-primary" />
<div>
<CardTitle className="text-base">{t("notificationConfig.title")}</CardTitle>
<CardDescription>{t("notificationConfig.description")}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="notify-new-user">{t("notificationConfig.notifyNewUser")}</Label>
<p className="text-sm text-muted-foreground">{t("notificationConfig.notifyNewUserDesc")}</p>
</div>
<Switch id="notify-new-user" name="notifyNewUser" defaultChecked />
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="notify-schedule-change">{t("notificationConfig.notifyScheduleChange")}</Label>
<p className="text-sm text-muted-foreground">{t("notificationConfig.notifyScheduleChangeDesc")}</p>
</div>
<Switch id="notify-schedule-change" name="notifyScheduleChange" defaultChecked />
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="notify-announcement">{t("notificationConfig.notifyAnnouncement")}</Label>
<p className="text-sm text-muted-foreground">{t("notificationConfig.notifyAnnouncementDesc")}</p>
</div>
<Switch id="notify-announcement" name="notifyAnnouncement" />
</div>
</CardContent>
</Card>
<div className="flex justify-end gap-3">
<Button type="button" variant="outline">{t("reset")}</Button>
<Button type="submit" disabled={saving}>
{saving ? t("saving") : t("save")}
</Button>
</div>
</form>
</div>
)
}