Files
NextEdu/src/modules/settings/components/admin-settings-view.tsx
SpecialX f3c223d914 feat(settings,questions,school,textbooks): add brand config, question components, school dialogs, textbooks hooks
settings:

- Add actions-brand, brand-config, data-access-brand for brand management

- Add admin-file-upload-card, admin-notification-config-card, admin-school-info-card, admin-security-policy-card

- Add ai-provider-delete-dialog, ai-provider-selector, brand-config-card

- Add security-recent-logins-section, security-two-factor-section

- Add config/profile-overview-config, data-access-profile-overview, lib/system-settings-utils

questions:

- Add batch-operations, import-export-buttons, knowledge-point-selector, options-editor

- Add question-bank-results-client, question-cascade-filter, question-content-renderer, utils

school:

- Add grade-delete-dialog, grade-form-dialog, grade-list-toolbar, grade-overview-cards

- Add use-grade-data hook

textbooks:

- Add textbook-form-fields component

- Add use-kp-create, use-kp-delete, use-kp-update hooks
2026-07-03 10:26:00 +08:00

195 lines
5.8 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 { Loader2 } from "lucide-react"
import {
getAdminSystemSettingsAction,
saveAdminSystemSettingsAction,
} from "@/modules/settings/actions-system-settings"
import { Button } from "@/shared/components/ui/button"
import { SchoolInfoCard, type SchoolInfoValues } from "@/modules/settings/components/admin-school-info-card"
import { SecurityPolicyCard, type SecurityPolicyValues } from "@/modules/settings/components/admin-security-policy-card"
import { FileUploadCard, type FileUploadValues } from "@/modules/settings/components/admin-file-upload-card"
import { NotificationConfigCard, type NotificationConfigValues } from "@/modules/settings/components/admin-notification-config-card"
import { BrandConfigCard } from "@/modules/settings/components/brand-config-card"
interface AdminSettingsFormValues {
schoolInfo: SchoolInfoValues
securityPolicy: SecurityPolicyValues
fileUpload: FileUploadValues
notificationConfig: NotificationConfigValues
}
const DEFAULT_VALUES: AdminSettingsFormValues = {
schoolInfo: {
schoolName: "",
schoolCode: "",
schoolPhone: "",
schoolEmail: "",
schoolAddress: "",
schoolDescription: "",
},
securityPolicy: {
passwordMinLength: 8,
sessionTimeout: 60,
requireSpecialChar: true,
requireUppercase: false,
forcePasswordChange: true,
},
fileUpload: {
maxFileSize: 10,
allowedTypes: "jpg,png,pdf,docx,xlsx,pptx",
},
notificationConfig: {
notifyNewUser: true,
notifyScheduleChange: true,
notifyAnnouncement: false,
},
}
/**
* 管理员系统设置视图
*
* 通过 Server Actions 加载和保存系统设置,数据持久化到 system_settings 表。
* 4 个 Card学校信息 / 安全策略 / 文件上传 / 通知配置。
* 所有文本通过 settings.admin.* i18n 键获取。
*/
export function AdminSettingsView(): React.ReactElement {
const t = useTranslations("settings.admin")
const [values, setValues] = React.useState<AdminSettingsFormValues>(DEFAULT_VALUES)
const [loadedValues, setLoadedValues] = React.useState<AdminSettingsFormValues>(DEFAULT_VALUES)
const [loading, setLoading] = React.useState(true)
const [saving, setSaving] = React.useState(false)
React.useEffect(() => {
let cancelled = false
async function load(): Promise<void> {
try {
const result = await getAdminSystemSettingsAction()
if (!cancelled && result.success && result.data) {
setValues(result.data)
setLoadedValues(result.data)
}
} catch {
// 加载失败时使用默认值
} finally {
if (!cancelled) setLoading(false)
}
}
void load()
return () => {
cancelled = true
}
}, [])
// dirty 检测:当前值与加载值不一致时为 dirty
const isDirty = React.useMemo(
() => JSON.stringify(values) !== JSON.stringify(loadedValues),
[values, loadedValues],
)
const handleSave = async (e: React.FormEvent): Promise<void> => {
e.preventDefault()
if (!isDirty) return
setSaving(true)
try {
const result = await saveAdminSystemSettingsAction(values)
if (result.success) {
toast.success(t("saveSuccess"))
setLoadedValues(values)
} else {
toast.error(result.message || t("saveFailure"))
}
} catch {
toast.error(t("saveFailure"))
} finally {
setSaving(false)
}
}
const handleReset = (): void => {
setValues(loadedValues)
}
const updateSchoolInfo = (key: keyof SchoolInfoValues, value: string): void => {
setValues((prev) => ({ ...prev, schoolInfo: { ...prev.schoolInfo, [key]: value } }))
}
const updateSecurityPolicy = (
key: keyof SecurityPolicyValues,
value: number | boolean
): void => {
setValues((prev) => ({ ...prev, securityPolicy: { ...prev.securityPolicy, [key]: value } }))
}
const updateFileUpload = (
key: keyof FileUploadValues,
value: number | string
): void => {
setValues((prev) => ({ ...prev, fileUpload: { ...prev.fileUpload, [key]: value } }))
}
const updateNotificationConfig = (
key: keyof NotificationConfigValues,
value: boolean
): void => {
setValues((prev) => ({ ...prev, notificationConfig: { ...prev.notificationConfig, [key]: value } }))
}
if (loading) {
return (
<div className="flex h-full items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)
}
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">
<SchoolInfoCard
values={values.schoolInfo}
onChange={updateSchoolInfo}
/>
<SecurityPolicyCard
values={values.securityPolicy}
onChange={updateSecurityPolicy}
/>
<FileUploadCard
values={values.fileUpload}
onChange={updateFileUpload}
/>
<NotificationConfigCard
values={values.notificationConfig}
onChange={updateNotificationConfig}
/>
<div className="flex justify-end gap-3">
<Button
type="button"
variant="outline"
onClick={handleReset}
disabled={!isDirty || saving}
>
{t("reset")}
</Button>
<Button type="submit" disabled={saving || !isDirty}>
{saving ? t("saving") : t("save")}
</Button>
</div>
</form>
{/* audit-P2-6: 品牌配置(独立表单 + 独立保存,不嵌入主表单以防嵌套 form */}
<BrandConfigCard />
</div>
)
}