- 新增 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
205 lines
9.6 KiB
TypeScript
205 lines
9.6 KiB
TypeScript
"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>
|
||
)
|
||
}
|