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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user