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
195 lines
5.8 KiB
TypeScript
195 lines
5.8 KiB
TypeScript
"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>
|
||
)
|
||
}
|