import { App, Button, Form, Input, InputNumber, Select, Skeleton, Switch } from "antd"; import { AlertTriangle, AtSign, BadgeCheck, KeyRound, MailCheck, RefreshCw, RotateCcw, Save, Send, Server } from "lucide-react"; import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { useBlocker } from "react-router"; import { cn } from "@/lib/utils"; import { getAdminEmailSetting, updateAdminEmailSetting, type EmailSetting } from "@/services/api/wallet"; import { AdminStatusBadge, configuredSecretText, SettingsSectionCard } from "./admin-ui"; type EmailFormValues = Pick; export default function EmailSettingsPanel() { const { message, modal } = App.useApp(); const [setting, setSetting] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [saving, setSaving] = useState(false); const [dirty, setDirty] = useState(false); const [draftEnabled, setDraftEnabled] = 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 result = await getAdminEmailSetting(); if (requestVersion !== requestVersionRef.current) return; if (!isEmailSetting(result.setting)) throw new Error("服务端返回的邮件配置格式无效"); setSetting(result.setting); 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); } } }, [message], ); useEffect(() => { void load(true); return () => { requestVersionRef.current += 1; }; }, [load]); useEffect(() => { if (loading || !setting) return; form.setFieldsValue(toEmailFormValues(setting)); setDraftEnabled(setting.enabled); }, [form, loading, setting]); const blocker = useBlocker(dirty && !saving); useEffect(() => { const beforeUnload = (event: BeforeUnloadEvent) => { if (!dirty || saving) return; event.preventDefault(); }; window.addEventListener("beforeunload", beforeUnload); return () => window.removeEventListener("beforeunload", beforeUnload); }, [dirty, saving]); 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: "当前 SMTP 表单有尚未保存的调整,离开后这些内容会丢失。服务端正在使用的邮件配置不会改变。", 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-email-command-actions button"); const target = navigationTriggerRef.current?.isConnected ? navigationTriggerRef.current : fallback; target?.focus(); navigationTriggerRef.current = null; }); }, }); }, [blocker, modal]); const resetDraft = () => { if (!setting || saving) return; form.setFieldsValue(toEmailFormValues(setting)); form.setFields([]); setDraftEnabled(setting.enabled); setDirty(false); setSaveError(""); message.info("已撤销邮件服务的未保存调整"); }; const requestRefresh = () => { if (!dirty) { void load(false, true); return; } modal.confirm({ title: "放弃调整并重新读取?", content: "重新读取会丢弃当前 SMTP 表单中的未保存内容,并以服务端配置为准。", okText: "放弃并刷新", cancelText: "继续编辑", okButtonProps: { danger: true }, onOk: () => load(false, true), }); }; const save = async (values: EmailFormValues) => { const expected = normalizeEmailFormValues(values); setSaving(true); setSaveError(""); try { const result = await updateAdminEmailSetting(expected); if (!isEmailSetting(result.setting) || !emailResponseMatches(result.setting, expected)) throw new Error("服务端返回的邮件配置与本次保存内容不一致,请重新读取后核对"); setSetting(result.setting); form.setFieldsValue(toEmailFormValues(result.setting)); setDraftEnabled(result.setting.enabled); setDirty(false); message.success("注册邮件配置已保存"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "保存邮件配置失败"; setSaveError(`${errorMessage}。未自动重试,请重新读取当前配置后再决定是否保存。`); message.error(errorMessage); throw error; } finally { setSaving(false); } }; const submitSave = async () => { let values: EmailFormValues; try { values = await form.validateFields(); } catch { return; } const validationError = validateEmailDraft(values, setting); if (validationError) { message.error(validationError); return; } try { await save(values); } catch { // 保存错误已在 save 中就地提示。 } }; const toggleEnabled = (enabled: boolean) => { if (!setting || saving) return; form.setFieldValue("enabled", enabled); setDraftEnabled(enabled); setDirty(hasEmailChanges({ ...form.getFieldsValue(true), enabled }, setting)); setSaveError(""); }; if (loading && !setting) { return (
); } if (!setting) { return (

无法读取邮件服务配置

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

); } const currentValues = normalizeEmailFormValues(form.getFieldsValue(true)); const smtpReady = Boolean(currentValues.host && currentValues.port && currentValues.fromEmail); return (
{dirty ? "有未保存的邮件调整" : `注册验证码:${setting.enabled ? "已启用" : "未启用"}`}

{dirty ? "完成当前配置后保存生效。" : formatSettingTime(setting.updatedAt, "使用系统默认值")}

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

{title}

{description}

); } function toEmailFormValues(setting: EmailSetting): EmailFormValues { return { enabled: setting.enabled, host: setting.host, port: setting.port, username: setting.username, password: "", encryption: setting.encryption, fromEmail: setting.fromEmail, fromName: setting.fromName, }; } function normalizeEmailFormValues(values: Partial): EmailFormValues { return { enabled: Boolean(values.enabled), host: values.host?.trim() || "", port: Number(values.port) || 587, username: values.username?.trim() || "", password: values.password?.trim() || "", encryption: values.encryption === "tls" || values.encryption === "none" ? values.encryption : "starttls", fromEmail: values.fromEmail?.trim().toLowerCase() || "", fromName: values.fromName?.trim() || "影策", }; } function hasEmailChanges(values: Partial, setting: EmailSetting | null) { if (!setting) return false; const draft = normalizeEmailFormValues(values); const saved = normalizeEmailFormValues(toEmailFormValues(setting)); if (draft.password) return true; return (Object.keys(saved) as Array).some((key) => key !== "password" && draft[key] !== saved[key]); } function validateEmailDraft(values: EmailFormValues, setting: EmailSetting | null) { const draft = normalizeEmailFormValues(values); if (!draft.enabled) return ""; if (/\r|\n/.test(draft.fromName)) return "发件人名称不能包含换行"; if (!draft.host || draft.port < 1 || draft.port > 65535 || !draft.fromEmail) return "启用注册邮件前请完整填写 SMTP 主机、端口和发件邮箱"; if (!isValidEmail(draft.fromEmail)) return "发件邮箱格式不正确"; if (draft.username && !draft.password && !setting?.hasPassword) return "SMTP 用户名已填写,请同时填写密码或服务商授权码"; return ""; } function emailResponseMatches(setting: EmailSetting, expected: EmailFormValues) { const actual = normalizeEmailFormValues(toEmailFormValues(setting)); const fields: Array = ["enabled", "host", "port", "username", "encryption", "fromEmail", "fromName"]; if (expected.password && !setting.hasPassword) return false; return fields.every((key) => actual[key] === expected[key]); } function isEmailSetting(value: unknown): value is EmailSetting { if (!value || typeof value !== "object") return false; const setting = value as Partial; return ( typeof setting.enabled === "boolean" && typeof setting.host === "string" && typeof setting.port === "number" && typeof setting.username === "string" && ["starttls", "tls", "none"].includes(setting.encryption || "") && typeof setting.fromEmail === "string" && typeof setting.fromName === "string" && typeof setting.hasPassword === "boolean" ); } function isValidEmail(value: string) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); } 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 })}`; }