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
This commit is contained in:
SpecialX
2026-06-22 16:15:36 +08:00
parent 21c7e65fee
commit 5d42495480
29 changed files with 2445 additions and 1094 deletions

View File

@@ -1,6 +1,7 @@
"use client"
import * as React from "react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { School, Shield, Database, Bell } from "lucide-react"
@@ -12,23 +13,31 @@ 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)
// 模拟保存
await new Promise((r) => setTimeout(r, 800))
toast.success("设置已保存")
// 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"></h2>
<p className="text-muted-foreground"></p>
<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">
@@ -38,39 +47,39 @@ export function AdminSettingsView() {
<div className="flex items-center gap-2">
<School className="h-5 w-5 text-primary" />
<div>
<CardTitle className="text-base"></CardTitle>
<CardDescription></CardDescription>
<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"></Label>
<Input id="school-name" placeholder="请输入学校名称" defaultValue="Next_Edu 实验学校" />
<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"></Label>
<Input id="school-code" placeholder="请输入学校代码" />
<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"></Label>
<Input id="school-phone" placeholder="请输入联系电话" />
<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"></Label>
<Input id="school-email" type="email" placeholder="请输入联系邮箱" />
<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"></Label>
<Input id="school-address" placeholder="请输入学校地址" />
<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"></Label>
<Textarea id="school-desc" placeholder="请输入学校简介" rows={3} />
<Label htmlFor="school-desc">{t("schoolInfo.description2")}</Label>
<Textarea id="school-desc" name="schoolDescription" placeholder={t("schoolInfo.descriptionPlaceholder")} rows={3} />
</div>
</CardContent>
</Card>
@@ -81,43 +90,43 @@ export function AdminSettingsView() {
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-primary" />
<div>
<CardTitle className="text-base"></CardTitle>
<CardDescription></CardDescription>
<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"></Label>
<Input id="password-min-length" type="number" min={6} max={32} defaultValue={8} />
<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"></Label>
<Input id="session-timeout" type="number" min={5} max={1440} defaultValue={60} />
<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"></Label>
<p className="text-sm text-muted-foreground"></p>
<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" defaultChecked />
<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"></Label>
<p className="text-sm text-muted-foreground"></p>
<Label htmlFor="require-uppercase">{t("securityPolicy.requireUppercase")}</Label>
<p className="text-sm text-muted-foreground">{t("securityPolicy.requireUppercaseDesc")}</p>
</div>
<Switch id="require-uppercase" />
<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"></Label>
<p className="text-sm text-muted-foreground"></p>
<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" defaultChecked />
<Switch id="force-password-change" name="forcePasswordChange" defaultChecked />
</div>
</CardContent>
</Card>
@@ -128,20 +137,20 @@ export function AdminSettingsView() {
<div className="flex items-center gap-2">
<Database className="h-5 w-5 text-primary" />
<div>
<CardTitle className="text-base"></CardTitle>
<CardDescription></CardDescription>
<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">MB</Label>
<Input id="max-file-size" type="number" min={1} max={100} defaultValue={10} />
<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"></Label>
<Input id="allowed-types" placeholder="如jpg,png,pdf,docx" defaultValue="jpg,png,pdf,docx,xlsx,pptx" />
<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>
@@ -153,40 +162,40 @@ export function AdminSettingsView() {
<div className="flex items-center gap-2">
<Bell className="h-5 w-5 text-primary" />
<div>
<CardTitle className="text-base"></CardTitle>
<CardDescription></CardDescription>
<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"></Label>
<p className="text-sm text-muted-foreground"></p>
<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" defaultChecked />
<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"></Label>
<p className="text-sm text-muted-foreground"></p>
<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" defaultChecked />
<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"></Label>
<p className="text-sm text-muted-foreground"></p>
<Label htmlFor="notify-announcement">{t("notificationConfig.notifyAnnouncement")}</Label>
<p className="text-sm text-muted-foreground">{t("notificationConfig.notifyAnnouncementDesc")}</p>
</div>
<Switch id="notify-announcement" />
<Switch id="notify-announcement" name="notifyAnnouncement" />
</div>
</CardContent>
</Card>
<div className="flex justify-end gap-3">
<Button type="button" variant="outline"></Button>
<Button type="button" variant="outline">{t("reset")}</Button>
<Button type="submit" disabled={saving}>
{saving ? "保存中..." : "保存设置"}
{saving ? t("saving") : t("save")}
</Button>
</div>
</form>