import { App, Button, Form, Input, Select, Skeleton } from "antd"; import { Switch } from "@/pages/admin/ui/controls"; import { AlertTriangle, BadgeCheck, ChevronDown, FileText, Globe2, KeyRound, LockKeyhole, RefreshCw, RotateCcw, Save, ShieldCheck, Sparkles, UserPlus, UsersRound } from "lucide-react"; import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { useBlocker } from "react-router"; import { cn } from "@/lib/utils"; import { useAppearanceStore } from "@/stores/use-appearance-store"; import { getAdminLinuxDOSetting, getAdminRegistrationSetting, updateAdminLinuxDOSetting, updateAdminRegistrationSetting, type LinuxDOSetting, type RegistrationSetting } from "@/services/api/wallet"; import { AdminStatusBadge, configuredSecretText, SettingsSectionCard } from "./admin-ui"; const DEFAULT_AGREEMENT_TEMPLATE = `一、服务说明与接受 欢迎使用本平台提供的 AI 影视与内容创作服务。在注册或使用本平台各项功能前,请务必仔细阅读并理解本协议。当您勾选同意或点击注册、登录按钮,即表示您已自愿接受本协议所有条款的约束。 二、账号注册与安全 1. 用户在注册时应提供真实、准确、合法的个人信息,并自行妥善保管账号密码及相关凭据。 2. 任何通过该账号进行的行为均视为用户本人的行为,因保管不当造成的损失由用户自行承担。 三、内容合规与用户承诺 1. 用户使用本平台生成、上传或发布的文本、图片、视频、音频等所有内容,必须遵守国家相关法律法规及公序良俗。 2. 严禁利用本平台从事任何违反国家法律、侵害他人合法权益(包括但不限于肖像权、名誉权、知识产权、个人隐私等)或危害网络安全的活动。 四、知识产权与免责声明 1. 用户利用本平台创作产生的合法作品归用户所有或依法享有相应权利。 2. AI 模型生成的内容受算法模型与提示词影响,平台对其完整性、准确性不作绝对保证,用户应自行审慎评估和使用。 五、服务变更与终止 平台有权根据业务发展、技术升级或合规要求适时调整服务内容及本协议条款。修改后的条款一经发布即发生效力。`; type LinuxDOFormValues = Omit; export default function AccessSettingsPanel() { const { message, modal } = App.useApp(); const brandName = useAppearanceStore((state) => state.appearance.brandName) || "平台"; const [linuxdo, setLinuxdo] = useState(null); const [registration, setRegistration] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [savingLinuxDO, setSavingLinuxDO] = useState(false); const [savingRegistration, setSavingRegistration] = useState(false); const [savingAgreement, setSavingAgreement] = useState(false); const [dirty, setDirty] = useState(false); const [draftLinuxDOEnabled, setDraftLinuxDOEnabled] = useState(false); const [agreementTitle, setAgreementTitle] = useState(""); const [agreementContent, setAgreementContent] = useState(""); const [agreementDirty, setAgreementDirty] = useState(false); const [loadError, setLoadError] = useState(""); const [saveError, setSaveError] = useState(""); const [form] = Form.useForm(); const requestVersionRef = useRef(0); const navigationConfirmOpenRef = useRef(false); const navigationTriggerRef = useRef(null); const load = useCallback( async (initial = false, announce = false) => { const requestVersion = ++requestVersionRef.current; if (initial) setLoading(true); else setRefreshing(true); setLoadError(""); try { const [linuxdoData, registrationData] = await Promise.all([getAdminLinuxDOSetting(), getAdminRegistrationSetting()]); if (requestVersion !== requestVersionRef.current) return; setLinuxdo(linuxdoData.setting); setRegistration(registrationData.setting); setAgreementTitle(registrationData.setting.agreementTitle || ""); setAgreementContent(registrationData.setting.agreementContent || ""); setAgreementDirty(false); setDirty(false); setSaveError(""); if (announce) message.success("已重新读取当前登录与注册配置"); } catch (error) { if (requestVersion !== requestVersionRef.current) return; const errorMessage = error instanceof Error ? error.message : "读取登录与注册配置失败"; setLoadError(errorMessage); if (!initial) message.error(errorMessage); } finally { if (requestVersion === requestVersionRef.current) { setLoading(false); setRefreshing(false); } } }, [form, message], ); useEffect(() => { void load(true); return () => { requestVersionRef.current += 1; }; }, [load]); useEffect(() => { if (loading || !linuxdo || !registration) return; form.setFieldsValue(toLinuxDOFormValues(linuxdo)); setDraftLinuxDOEnabled(linuxdo.enabled); }, [form, linuxdo, loading, registration]); const unsaved = (dirty && !savingLinuxDO) || (agreementDirty && !savingAgreement); const blocker = useBlocker(unsaved); // 条款编辑区下方的字数与段落提示:与注册页的分段规则保持一致(按空行分段), // 让管理员在保存前就能看到条款会被拆成几段。 const agreementParagraphsCount = agreementContent.trim() ? agreementContent.trim().split(/\n\s*\n/).length : 0; useEffect(() => { const beforeUnload = (event: BeforeUnloadEvent) => { if (!unsaved) return; event.preventDefault(); }; window.addEventListener("beforeunload", beforeUnload); return () => window.removeEventListener("beforeunload", beforeUnload); }, [unsaved]); useEffect(() => { if (blocker.state !== "blocked" || navigationConfirmOpenRef.current) return; navigationConfirmOpenRef.current = true; navigationTriggerRef.current = document.activeElement instanceof HTMLElement && document.activeElement !== document.body ? document.activeElement : null; modal.confirm({ title: "放弃未保存的登录与注册调整?", content: "当前有尚未保存的表单调整,离开后这些内容会丢失。用户注册状态不受影响。", okText: "放弃并离开", cancelText: "继续编辑", okButtonProps: { danger: true }, onOk: () => { navigationConfirmOpenRef.current = false; navigationTriggerRef.current = null; blocker.proceed(); }, onCancel: () => { navigationConfirmOpenRef.current = false; blocker.reset(); window.requestAnimationFrame(() => { const fallback = document.querySelector(".admin-access-command-actions button"); const target = navigationTriggerRef.current?.isConnected ? navigationTriggerRef.current : fallback; target?.focus(); navigationTriggerRef.current = null; }); }, }); }, [blocker, modal]); const resetLinuxDODraft = () => { if (!linuxdo || savingLinuxDO) return; form.setFieldsValue(toLinuxDOFormValues(linuxdo)); form.setFields([]); setDraftLinuxDOEnabled(linuxdo.enabled); setDirty(false); setSaveError(""); message.info("已撤销 Linux.do 登录的未保存调整"); }; const saveAgreement = async () => { if (!registration || savingAgreement) return; setSavingAgreement(true); setSaveError(""); try { const data = await updateAdminRegistrationSetting({ enabled: registration.enabled, agreementTitle: agreementTitle.trim(), agreementContent: agreementContent.trim(), }); setRegistration(data.setting); setAgreementTitle(data.setting.agreementTitle || ""); setAgreementContent(data.setting.agreementContent || ""); setAgreementDirty(false); message.success("服务协议已保存,注册页将立即生效"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "保存服务协议失败"; setSaveError(`${errorMessage}。未自动重试,请重新读取当前配置后再决定是否保存。`); message.error(errorMessage); } finally { setSavingAgreement(false); } }; const resetAgreementDraft = () => { if (!registration || savingAgreement) return; setAgreementTitle(registration.agreementTitle || ""); setAgreementContent(registration.agreementContent || ""); setAgreementDirty(false); message.info("已撤销服务协议的未保存调整"); }; const requestRefresh = () => { if (!dirty && !agreementDirty) { void load(false, true); return; } modal.confirm({ title: "放弃调整并重新读取?", content: "重新读取会丢弃当前表单中的未保存内容(含服务协议),并以服务端配置为准。", okText: "放弃并刷新", cancelText: "继续编辑", okButtonProps: { danger: true }, onOk: () => load(false, true), }); }; const toggleRegistration = async (enabled: boolean) => { setSavingRegistration(true); try { const data = await updateAdminRegistrationSetting(enabled); setRegistration(data.setting); message.success(enabled ? "用户注册已开启" : "用户注册已关闭"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "更新注册设置失败"; message.error(errorMessage); throw error; } finally { setSavingRegistration(false); } }; const requestRegistrationChange = (enabled: boolean) => { if (!registration || enabled === registration.enabled || savingRegistration) return; void toggleRegistration(enabled).catch(() => undefined); }; const saveLinuxDO = async (values: LinuxDOFormValues) => { const expected = normalizeLinuxDOFormValues(values); setSavingLinuxDO(true); setSaveError(""); try { const result = await updateAdminLinuxDOSetting(expected); if (!linuxDOResponseMatches(result.setting, expected)) throw new Error("服务端返回的 Linux.do 配置与本次保存内容不一致,请重新读取后核对"); setLinuxdo(result.setting); setDirty(false); message.success("Linux.do 登录配置已保存"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "保存 Linux.do 配置失败"; setSaveError(`${errorMessage}。未自动重试,请重新读取当前配置后再决定是否保存。`); message.error(errorMessage); throw error; } finally { setSavingLinuxDO(false); } }; const submitLinuxDOSave = async () => { let values: LinuxDOFormValues; try { values = await form.validateFields(); } catch { return; } const validationError = validateLinuxDODraft(values, linuxdo); if (validationError) { message.error(validationError); return; } try { await saveLinuxDO(values); } catch { // 保存错误已在 saveLinuxDO 中就地提示。 } }; const toggleLinuxDO = (enabled: boolean) => { if (!linuxdo || savingLinuxDO) return; form.setFieldValue("enabled", enabled); setDraftLinuxDOEnabled(enabled); setDirty(hasLinuxDOChanges({ ...form.getFieldsValue(true), enabled }, linuxdo)); setSaveError(""); }; if (loading && (!linuxdo || !registration)) { return (
); } if (!linuxdo || !registration) { return (

无法读取登录与注册配置

{loadError || "当前没有可显示的配置,请稍后重试。"}

); } return (
{dirty ? "Linux.do 有未保存的调整" : "登录与注册设置"}

{dirty ? "完成接入信息后保存生效。" : `新用户注册${registration.enabled ? "已开放" : "已关闭"} · Linux.do ${linuxdo.enabled ? "已启用" : "未启用"}`}

{dirty ? ( ) : null}
{loadError || saveError ? (
) : null}
); } function FormSectionTitle({ icon, title, description }: { icon: ReactNode; title: string; description: string }) { return (
{icon}

{title}

{description}

); } function toLinuxDOFormValues(setting: LinuxDOSetting): LinuxDOFormValues { return { enabled: setting.enabled, clientId: setting.clientId, clientSecret: "", authorizationUrl: setting.authorizationUrl, tokenUrl: setting.tokenUrl, userInfoUrl: setting.userInfoUrl, redirectUrl: setting.redirectUrl, scopes: setting.scopes || [], clientAuthMethod: setting.clientAuthMethod, subjectField: setting.subjectField, usernameField: setting.usernameField, displayNameField: setting.displayNameField, emailField: setting.emailField, avatarField: setting.avatarField, }; } function normalizeLinuxDOFormValues(values: LinuxDOFormValues): LinuxDOFormValues { return { enabled: Boolean(values.enabled), clientId: values.clientId?.trim() || "", clientSecret: values.clientSecret?.trim() || "", authorizationUrl: values.authorizationUrl?.trim() || "", tokenUrl: values.tokenUrl?.trim() || "", userInfoUrl: values.userInfoUrl?.trim() || "", redirectUrl: values.redirectUrl?.trim() || "", scopes: [...new Set((values.scopes || []).map((value) => value.trim()).filter(Boolean))], clientAuthMethod: values.clientAuthMethod || "client_secret_post", subjectField: values.subjectField?.trim() || "id", usernameField: values.usernameField?.trim() || "username", displayNameField: values.displayNameField?.trim() || "name", emailField: values.emailField?.trim() || "email", avatarField: values.avatarField?.trim() || "avatar_url", }; } function hasLinuxDOChanges(values: LinuxDOFormValues, setting: LinuxDOSetting | null) { if (!setting) return false; const draft = normalizeLinuxDOFormValues(values); const saved = normalizeLinuxDOFormValues(toLinuxDOFormValues(setting)); if (draft.clientSecret) return true; return (Object.keys(saved) as Array).some((key) => key !== "clientSecret" && JSON.stringify(draft[key]) !== JSON.stringify(saved[key])); } function validateLinuxDODraft(values: LinuxDOFormValues, setting: LinuxDOSetting | null) { const draft = normalizeLinuxDOFormValues(values); if (!draft.enabled) return ""; if (!draft.clientId || (!draft.clientSecret && !setting?.hasClientSecret) || !draft.authorizationUrl || !draft.tokenUrl || !draft.userInfoUrl || !draft.redirectUrl) return "启用 Linux.do 登录前请完整填写 Client、端点和回调配置"; for (const value of [draft.authorizationUrl, draft.tokenUrl, draft.userInfoUrl]) if (!isValidURL(value, true)) return "Linux.do 授权、Token 和用户资料地址必须是有效的 HTTPS URL"; if (!isValidRedirectURL(draft.redirectUrl)) return "Linux.do 回调地址必须使用 HTTPS,本地回环地址可使用 HTTP"; return ""; } function linuxDOResponseMatches(setting: LinuxDOSetting, expected: LinuxDOFormValues) { const actual = normalizeLinuxDOFormValues(toLinuxDOFormValues(setting)); const fields: Array = ["enabled", "clientId", "authorizationUrl", "tokenUrl", "userInfoUrl", "redirectUrl", "scopes", "clientAuthMethod", "subjectField", "usernameField", "displayNameField", "emailField", "avatarField"]; if (expected.clientSecret && !setting.hasClientSecret) return false; return fields.every((key) => JSON.stringify(actual[key]) === JSON.stringify(expected[key])); } function isValidURL(value: string, requireHTTPS = false) { try { const parsed = new URL(value); return Boolean(parsed.host) && (!requireHTTPS || parsed.protocol === "https:"); } catch { return false; } } function isValidRedirectURL(value: string) { try { const parsed = new URL(value); if (parsed.protocol === "https:" && parsed.host) return true; return parsed.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(parsed.hostname); } catch { return false; } } function formatSettingTime(value: string | undefined, fallback: string) { if (!value) return fallback; const date = new Date(value); if (Number.isNaN(date.getTime()) || date.getFullYear() < 2000) return fallback; return `更新于 ${date.toLocaleString("zh-CN", { hour12: false })}`; }