import * as React from "react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useNavigate, useSearchParams } from "react-router-dom" import { ArrowLeft, BarChart3, Ban, Bell, BellOff, BookOpen, ChevronDown, ChevronUp, Clock3, Code2, Contact, Copy, HardDrive, Image, Info, KeyRound, LogOut, Mail, MailCheck, MailX, Moon, PanelLeftOpen, PencilLine, PlayCircle, Plus, RefreshCcw, Search, SendHorizontal, Settings, ShieldCheck, SlidersHorizontal, Sun, Trash2, Users, X } from "lucide-react" import { QRCodeSVG } from "qrcode.react" import { api, APIToken, ExternalImapAccount, ExternalImapAccountPayload, ExternalImapFolder, ExternalImapOAuthProvider, ExternalImapStorageMode, ExternalImapSyncRun, ExternalImapTlsMode, ForwardingSettings, ForwardingVerifiedEmail, MailLabel, MailRule, MailRuleAction, MailRuleCondition, Mailbox, MailboxApplyOptions, MailSignature, MailStats, PermissionLimits } from "@/lib/api" import { cn, formatBytes } from "@/lib/utils" import { applyTheme, getInitialTheme } from "@/lib/theme" import { DisplayMode, useDisplayMode } from "@/lib/display-mode" import { useMe } from "@/hooks/use-me" import { useLogout } from "@/hooks/use-logout" import { useIsMobile } from "@/hooks/use-mobile" import { validatePasswordConfirm } from "@/lib/validation" import { hasPermission } from "@/lib/permissions" import { Button } from "@/components/ui/button" import { PasswordInput } from "@/components/ui/password-input" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Separator } from "@/components/ui/separator" import { ScrollArea } from "@/components/ui/scroll-area" import { ConfirmDialog } from "@/components/confirm-dialog" import { useToast } from "@/hooks/use-toast" type Tab = "profile" | "mailboxes" | "contacts" | "cleanup" | "cleanupQueue" | "rules" | "blocked" | "stats" | "apiTokens" type AccountSettingsTab = "account" | "mail" | "clients" | "security" type PendingConfirm = { title: string; description?: string; confirmText: string; destructive?: boolean; onConfirm: () => void } const tabs: Record = { profile: { label: "账号设置", icon: }, mailboxes: { label: "邮箱管理", icon: }, contacts: { label: "联系人管理", icon: }, cleanup: { label: "邮件清理", icon: }, cleanupQueue: { label: "待清理邮件", icon: }, rules: { label: "收信规则", icon: }, blocked: { label: "被拦截邮件", icon: }, stats: { label: "数据统计", icon: }, apiTokens: { label: "开发者", icon: }, } const tabKeys = Object.keys(tabs) as Tab[] const accountSettingTabs: { key: AccountSettingsTab; label: string }[] = [ { key: "account", label: "账号" }, { key: "mail", label: "邮件" }, { key: "clients", label: "通知与客户端" }, { key: "security", label: "安全" }, ] export function ProfilePage() { const me = useMe() const qc = useQueryClient() const navigate = useNavigate() const [params, setParams] = useSearchParams() const { toast } = useToast() const passwordFormRef = React.useRef(null) const twoFactorFormRef = React.useRef(null) const [mailboxId, setMailboxId] = React.useState(() => localStorage.getItem("lanqin:selected-mailbox") || "") const [statsRangeDays, setStatsRangeDays] = React.useState(30) const [darkMode, setDarkMode] = React.useState(getInitialTheme) const [displayMode, setDisplayMode] = useDisplayMode() const [blockedMailboxId, setBlockedMailboxId] = React.useState("all") const [ruleDialogOpen, setRuleDialogOpen] = React.useState(false) const [mobileSidebarOpen, setMobileSidebarOpen] = React.useState(false) const [externalRunAccountId, setExternalRunAccountId] = React.useState("") const isMobile = useIsMobile() const themeMountedRef = React.useRef(false) const rawTab = params.get("tab") as Tab | null const rawAccountTab = params.get("accountTab") as AccountSettingsTab | null const user = me.data?.user const canAccessMail = hasPermission(user, "mail.access") const canReadMail = hasPermission(user, "mail.messages.read") const canOrganizeMail = hasPermission(user, "mail.messages.organize") const canManageLabels = hasPermission(user, "mail.labels.manage") const canManageContacts = hasPermission(user, "mail.contacts.manage") const canManageSignatures = hasPermission(user, "mail.signatures.manage") const canManageRules = hasPermission(user, "mail.rules.manage") const canManageBlocked = hasPermission(user, "mail.blocked_senders.manage") const canViewStats = hasPermission(user, "mail.stats.view") const canApplyMailbox = hasPermission(user, "mail.mailboxes.apply") const canConfigureMailboxApply = hasPermission(user, "admin.settings.update") const visibleTabKeys = tabKeys.filter((key) => { if (key === "profile") return true if (key === "mailboxes") return canAccessMail || canApplyMailbox if (key === "contacts") return canManageContacts if (key === "cleanup") return canOrganizeMail if (key === "cleanupQueue") return canOrganizeMail if (key === "rules") return canManageRules if (key === "blocked") return canManageBlocked if (key === "stats") return canViewStats if (key === "apiTokens") return true return false }) const tab: Tab = rawTab && visibleTabKeys.includes(rawTab) ? rawTab : "profile" const accountTab: AccountSettingsTab = rawAccountTab && accountSettingTabs.some((item) => item.key === rawAccountTab) ? rawAccountTab : "account" const mailboxes = useQuery({ queryKey: ["mailboxes", "mine"], queryFn: api.myMailboxes, enabled: canAccessMail }) const mailboxApplyOptions = useQuery({ queryKey: ["mailbox-apply-options"], queryFn: api.mailboxApplyOptions, enabled: canApplyMailbox }) const publicSettings = useQuery({ queryKey: ["public-settings"], queryFn: api.publicSettings }) const apiTokens = useQuery({ queryKey: ["api-tokens"], queryFn: api.apiTokens }) const contacts = useQuery({ queryKey: ["contacts"], queryFn: api.contacts, enabled: canManageContacts }) const signatures = useQuery({ queryKey: ["signatures"], queryFn: api.signatures, enabled: canManageSignatures }) const rules = useQuery({ queryKey: ["rules"], queryFn: api.rules, enabled: canManageRules }) const ruleForwarding = useQuery({ queryKey: ["forwarding-settings"], queryFn: api.forwardingSettings, enabled: canManageRules && canAccessMail }) const ruleVerifiedEmails = React.useMemo(() => ruleForwarding.data?.verifiedEmails.filter((item) => item.verified).map((item) => item.email) || [], [ruleForwarding.data?.verifiedEmails]) const blocked = useQuery({ queryKey: ["blocked-senders"], queryFn: api.blockedSenders, enabled: canManageBlocked }) const selectedMailbox = React.useMemo(() => mailboxes.data?.items.find((m) => m.id === mailboxId), [mailboxes.data?.items, mailboxId]) const activeMailboxId = selectedMailbox?.id || "" const externalImapEnabled = publicSettings.data?.externalImapEnabled ?? false const externalImapAccounts = useQuery({ queryKey: ["external-imap-accounts", activeMailboxId], queryFn: () => api.externalImapAccounts(activeMailboxId), enabled: !!activeMailboxId && canAccessMail && externalImapEnabled }) React.useEffect(() => { if (!externalRunAccountId) return if (externalImapAccounts.data?.items.some((item) => item.id === externalRunAccountId)) return setExternalRunAccountId("") }, [externalImapAccounts.data?.items, externalRunAccountId]) const selectedExternalRunAccount = externalImapAccounts.data?.items.find((item) => item.id === externalRunAccountId) const externalRunFolders = useQuery({ queryKey: ["external-imap-run-folders", externalRunAccountId], queryFn: () => api.externalFolders(externalRunAccountId), enabled: !!externalRunAccountId && !!selectedExternalRunAccount && canAccessMail && externalImapEnabled }) const externalSyncRuns = useQuery({ queryKey: ["external-imap-sync-runs", externalRunAccountId], queryFn: () => api.externalImapSyncRuns(externalRunAccountId), enabled: !!externalRunAccountId && !!selectedExternalRunAccount && canAccessMail && externalImapEnabled }) const labels = useQuery({ queryKey: ["labels", activeMailboxId], queryFn: () => api.labels(activeMailboxId), enabled: !!activeMailboxId && (canReadMail || canManageLabels || canManageRules) }) const stats = useQuery({ queryKey: ["mail-stats", activeMailboxId, statsRangeDays], queryFn: () => api.mailStats(activeMailboxId, statsRangeDays), enabled: !!activeMailboxId && canViewStats }) const profile = useMutation({ mutationFn: (form: FormData) => api.updateProfile({ displayName: String(form.get("displayName") || "") }), onSuccess: (data) => { qc.setQueryData(["me"], data); toast({ title: "个人资料已保存" }) }, onError: (error) => toast({ title: "保存失败", description: error.message }), }) const password = useMutation({ mutationFn: (form: FormData) => { const newPassword = String(form.get("newPassword") || "") validatePasswordConfirm(newPassword, String(form.get("confirmPassword") || ""), "两次输入的新密码不一致") return api.changePassword({ currentPassword: String(form.get("currentPassword") || ""), newPassword }) }, onSuccess: () => { passwordFormRef.current?.reset(); toast({ title: "密码已更新" }) }, onError: (error) => toast({ title: "修改失败", description: error.message }), }) const setupTwoFactor = useMutation({ mutationFn: api.setupTwoFactor, onSuccess: () => toast({ title: "双因素密钥已生成" }), onError: (error) => toast({ title: "生成失败", description: error.message }), }) const enableTwoFactor = useMutation({ mutationFn: (form: FormData) => api.enableTwoFactor(String(form.get("code") || "")), onSuccess: (data) => { qc.setQueryData(["me"], data); setupTwoFactor.reset(); twoFactorFormRef.current?.reset(); toast({ title: "双因素认证已启用" }) }, onError: (error) => toast({ title: "启用失败", description: error.message }), }) const disableTwoFactor = useMutation({ mutationFn: (form: FormData) => api.disableTwoFactor(String(form.get("code") || "")), onSuccess: (data) => { qc.setQueryData(["me"], data); twoFactorFormRef.current?.reset(); toast({ title: "双因素认证已关闭" }) }, onError: (error) => toast({ title: "关闭失败", description: error.message }), }) const createApiToken = useMutation({ mutationFn: (payload: { name: string; expiresAt?: string; scopes: string[] }) => api.createApiToken(payload), onSuccess: (res) => { qc.invalidateQueries({ queryKey: ["api-tokens"] }); toast({ title: "API 密钥已创建" }); return res }, onError: (error) => toast({ title: "创建失败", description: error.message }), }) const updateApiToken = useMutation({ mutationFn: ({ id, payload }: { id: string; payload: { name?: string; expiresAt?: string; disabled?: boolean; scopes?: string[] } }) => api.updateApiToken(id, payload), onSuccess: () => { qc.invalidateQueries({ queryKey: ["api-tokens"] }); toast({ title: "API 密钥已更新" }) }, onError: (error) => toast({ title: "更新失败", description: error.message }), }) const deleteApiToken = useMutation({ mutationFn: api.deleteApiToken, onSuccess: () => { qc.invalidateQueries({ queryKey: ["api-tokens"] }); toast({ title: "API 密钥已撤销" }) }, onError: (error) => toast({ title: "撤销失败", description: error.message }), }) const createContact = useMutation({ mutationFn: (form: FormData) => api.createContact({ name: String(form.get("name") || ""), email: String(form.get("email") || ""), note: String(form.get("note") || "") }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["contacts"] }); toast({ title: "联系人已保存" }) }, onError: (error) => toast({ title: "保存失败", description: error.message }), }) const deleteContact = useMutation({ mutationFn: api.deleteContact, onSuccess: () => { qc.invalidateQueries({ queryKey: ["contacts"] }); toast({ title: "联系人已删除" }) } }) const createSignature = useMutation({ mutationFn: (form: FormData) => api.createSignature({ mailboxId: String(form.get("mailboxId") || ""), name: String(form.get("name") || ""), content: String(form.get("content") || ""), isDefault: form.get("isDefault") === "on" }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["signatures"] }); qc.invalidateQueries({ queryKey: ["signature"] }); toast({ title: "签名已保存" }) }, onError: (error) => toast({ title: "保存失败", description: error.message }), }) const updateSignature = useMutation({ mutationFn: ({ id, form }: { id: string; form: FormData }) => api.updateSignature(id, { mailboxId: String(form.get("mailboxId") || ""), name: String(form.get("name") || ""), content: String(form.get("content") || ""), isDefault: form.get("isDefault") === "on" }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["signatures"] }); qc.invalidateQueries({ queryKey: ["signature"] }); toast({ title: "签名已更新" }) }, onError: (error) => toast({ title: "保存失败", description: error.message }), }) const setDefaultSignature = useMutation({ mutationFn: api.setDefaultSignature, onSuccess: () => { qc.invalidateQueries({ queryKey: ["signatures"] }); qc.invalidateQueries({ queryKey: ["signature"] }); toast({ title: "默认签名已更新" }) }, onError: (error) => toast({ title: "设置失败", description: error.message }), }) const deleteSignature = useMutation({ mutationFn: api.deleteSignature, onSuccess: () => { qc.invalidateQueries({ queryKey: ["signatures"] }); qc.invalidateQueries({ queryKey: ["signature"] }); toast({ title: "签名已删除" }) } }) const createRule = useMutation({ mutationFn: (payload: { mailboxId: string name: string matchMode: "all" | "any" conditions: MailRuleCondition[] actions: MailRuleAction[] applyToExisting: boolean stopProcessing: boolean enabled: boolean }) => api.createRule(payload), onSuccess: (rule) => { qc.invalidateQueries({ queryKey: ["rules"] }) qc.invalidateQueries({ queryKey: ["messages"] }) qc.invalidateQueries({ queryKey: ["mail-stats"] }) qc.invalidateQueries({ queryKey: ["labels"] }) setRuleDialogOpen(false) toast({ title: rule.appliedExistingCount ? `收件规则已保存,已应用 ${rule.appliedExistingCount} 封邮件` : "收件规则已保存" }) }, onError: (error) => toast({ title: "保存失败", description: error.message }), }) const deleteRule = useMutation({ mutationFn: api.deleteRule, onSuccess: () => { qc.invalidateQueries({ queryKey: ["rules"] }); toast({ title: "规则已删除" }) } }) const updateRule = useMutation({ mutationFn: ({ id, payload }: { id: string; payload: Partial }) => api.updateRule(id, payload), onSuccess: () => { qc.invalidateQueries({ queryKey: ["rules"] }); setRuleDialogOpen(false); toast({ title: "收件规则已更新" }) }, onError: (error) => toast({ title: "更新失败", description: error.message }), }) const moveRule = useMutation({ mutationFn: ({ id, direction }: { id: string; direction: "up" | "down" }) => api.moveRule(id, direction), onSuccess: () => qc.invalidateQueries({ queryKey: ["rules"] }), onError: (error) => toast({ title: "排序失败", description: error.message }), }) const applyRule = useMutation({ mutationFn: api.applyRule, onSuccess: (res) => { qc.invalidateQueries({ queryKey: ["messages"] }) qc.invalidateQueries({ queryKey: ["folders"] }) qc.invalidateQueries({ queryKey: ["mail-stats"] }) qc.invalidateQueries({ queryKey: ["labels"] }) toast({ title: `规则已应用到 ${res.affected} 封现有邮件` }) }, onError: (error) => toast({ title: "应用失败", description: error.message }), }) const createBlocked = useMutation({ mutationFn: (form: FormData) => api.createBlockedSender({ mailboxId: blockedMailboxId === "all" ? "" : blockedMailboxId, email: String(form.get("email") || ""), reason: String(form.get("reason") || "") }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["blocked-senders"] }); toast({ title: "拦截规则已保存" }) }, onError: (error) => toast({ title: "保存失败", description: error.message }), }) const deleteBlocked = useMutation({ mutationFn: api.deleteBlockedSender, onSuccess: () => { qc.invalidateQueries({ queryKey: ["blocked-senders"] }); toast({ title: "拦截规则已删除" }) } }) const createLabel = useMutation({ mutationFn: (form: FormData) => api.createLabel({ mailboxId: activeMailboxId, name: String(form.get("name") || ""), color: String(form.get("color") || "") }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["labels"] }); toast({ title: "标签已创建" }) }, onError: (error) => toast({ title: "创建失败", description: error.message }), }) const deleteLabel = useMutation({ mutationFn: (id: string) => api.deleteLabel(id, activeMailboxId), onSuccess: () => { qc.invalidateQueries({ queryKey: ["labels"] }); toast({ title: "标签已删除" }) }, onError: (error) => toast({ title: "删除失败", description: error.message }), }) const cleanup = useMutation({ mutationFn: (target: "empty-trash" | "empty-spam" | "archive-read-inbox") => api.cleanupMail({ mailboxId, target }), onSuccess: (res) => { qc.invalidateQueries({ queryKey: ["mail-stats"] }); qc.invalidateQueries({ queryKey: ["folders"] }); qc.invalidateQueries({ queryKey: ["messages"] }); toast({ title: `已处理 ${res.affected} 封邮件` }) }, onError: (error) => toast({ title: "清理失败", description: error.message }), }) const applyMailbox = useMutation({ mutationFn: api.applyMailbox, onSuccess: (mailbox) => { qc.invalidateQueries({ queryKey: ["mailboxes", "mine"] }) qc.invalidateQueries({ queryKey: ["mailbox-apply-options"] }) setMailboxId(mailbox.id) toast({ title: "邮箱已申请" }) }, onError: (error) => toast({ title: "申请失败", description: error.message }), }) const createExternalImap = useMutation({ mutationFn: api.createExternalImapAccount, onSuccess: () => { qc.invalidateQueries({ queryKey: ["external-imap-accounts"] }); qc.invalidateQueries({ queryKey: ["mail-external-accounts"] }); toast({ title: "外部 IMAP 已保存" }) }, onError: (error) => toast({ title: "保存失败", description: error.message }), }) const updateExternalImap = useMutation({ mutationFn: ({ id, payload }: { id: string; payload: ExternalImapAccountPayload }) => api.updateExternalImapAccount(id, payload), onSuccess: () => { qc.invalidateQueries({ queryKey: ["external-imap-accounts"] }); qc.invalidateQueries({ queryKey: ["mail-external-accounts"] }); toast({ title: "外部 IMAP 已更新" }) }, onError: (error) => toast({ title: "更新失败", description: error.message }), }) const deleteExternalImap = useMutation({ mutationFn: api.deleteExternalImapAccount, onSuccess: () => { qc.invalidateQueries({ queryKey: ["external-imap-accounts"] }); qc.invalidateQueries({ queryKey: ["mail-external-accounts"] }); toast({ title: "外部 IMAP 已删除" }) }, onError: (error) => toast({ title: "删除失败", description: error.message }), }) const testExternalImap = useMutation({ mutationFn: api.testExternalImapAccount, onSuccess: (res) => toast({ title: `连接成功,发现 ${res.folders} 个文件夹` }), onError: (error) => toast({ title: "连接失败", description: error.message }), }) const syncExternalImap = useMutation({ mutationFn: api.syncExternalImapAccount, onSuccess: (run) => { qc.invalidateQueries({ queryKey: ["external-imap-accounts"] }); qc.invalidateQueries({ queryKey: ["external-imap-sync-runs"] }); qc.invalidateQueries({ queryKey: ["folders"] }); qc.invalidateQueries({ queryKey: ["messages"] }); toast({ title: `同步完成:导入 ${run.imported},跳过 ${run.skipped}` }) }, onError: (error) => toast({ title: "同步失败", description: error.message }), }) const syncExternalImapFolder = useMutation({ mutationFn: ({ id, folder }: { id: string; folder: string }) => api.syncExternalImapFolder(id, folder), onSuccess: (run) => { qc.invalidateQueries({ queryKey: ["external-imap-accounts"] }); qc.invalidateQueries({ queryKey: ["external-imap-sync-runs"] }); qc.invalidateQueries({ queryKey: ["folders"] }); qc.invalidateQueries({ queryKey: ["messages"] }); toast({ title: `${run.folder || "文件夹"} 同步完成:导入 ${run.imported},跳过 ${run.skipped}` }) }, onError: (error) => toast({ title: "同步失败", description: error.message }), }) const startExternalOAuth = useMutation({ mutationFn: ({ provider, mailboxId, email, storageMode }: { provider: ExternalImapOAuthProvider; mailboxId: string; email: string; storageMode: ExternalImapStorageMode }) => api.startExternalImapOAuth(provider, { mailboxId, email, storageMode, syncReadState: true, enabled: true }), onSuccess: (res) => { window.location.href = res.url }, onError: (error) => toast({ title: "授权失败", description: error.message }), }) React.useEffect(() => { if (!mailboxes.isSuccess) return const items = mailboxes.data?.items || [] if (items.length === 0) { if (mailboxId) setMailboxId("") localStorage.removeItem("lanqin:selected-mailbox") return } if (!mailboxId || !items.some((m) => m.id === mailboxId)) setMailboxId(items[0].id) }, [mailboxId, mailboxes.isSuccess, mailboxes.data?.items]) React.useEffect(() => { if (mailboxId) localStorage.setItem("lanqin:selected-mailbox", mailboxId); else localStorage.removeItem("lanqin:selected-mailbox") }, [mailboxId]) React.useEffect(() => { applyTheme(darkMode, themeMountedRef.current); themeMountedRef.current = true }, [darkMode]) const logout = useLogout() async function copy(text: string) { await navigator.clipboard.writeText(text); toast({ title: "已复制" }) } function setTab(next: Tab) { const visibleNext = visibleTabKeys.includes(next) ? next : "profile" const nextParams = new URLSearchParams(params) if (visibleNext === "profile") nextParams.delete("tab") else { nextParams.set("tab", visibleNext) nextParams.delete("accountTab") } setParams(nextParams) setMobileSidebarOpen(false) } function setAccountTab(next: AccountSettingsTab) { const nextParams = new URLSearchParams(params) nextParams.delete("tab") if (next === "account") nextParams.delete("accountTab") else nextParams.set("accountTab", next) setParams(nextParams) } if (me.isLoading) return
加载中...
if (me.isError || !user) return
登录状态已失效
const sidebarContent = (
setDarkMode((v) => !v)} onBack={() => navigate("/")} />
) const pageTitle = tabs[tab].label const pageSubtitle = tab === "stats" ? "查看邮件收发趋势、分布情况和常用联系人。" : undefined const pageAction = tab === "stats" ? : tab === "apiTokens" ? : undefined return (
{isMobile ? (
管理导航
{sidebarContent}
{pageTitle}
{renderTab()}
) : (
{sidebarContent}
{renderTab()}
)}
) function renderTab() { if (tab === "profile") return ( createLabel.mutate(form)} onDeleteLabel={(id) => deleteLabel.mutate(id)} signatures={signatures.data?.items || []} signaturesLoading={signatures.isLoading} signaturesPending={createSignature.isPending || updateSignature.isPending || setDefaultSignature.isPending || deleteSignature.isPending} onCreateSignature={(form) => createSignature.mutate(form)} onUpdateSignature={(id, form) => updateSignature.mutate({ id, form })} onSetDefaultSignature={(id) => setDefaultSignature.mutate(id)} onDeleteSignature={(id) => deleteSignature.mutate(id)} clientHostname={publicSettings.data?.publicHostname} onCopy={copy} onSelectMailbox={setMailboxId} onOpenCleanup={() => setTab("cleanup")} /> ) if (tab === "mailboxes") return ( { if (!canAccessMail) return; setMailboxId(id); navigate("/") }} onApply={(payload) => applyMailbox.mutateAsync(payload).then(() => undefined)} onConfigureApply={() => navigate("/admin?section=settings&settingsTab=mail")} onCreateExternal={(payload) => createExternalImap.mutate(payload)} onStartExternalOAuth={(provider, payload) => startExternalOAuth.mutate({ provider, ...payload })} onUpdateExternal={(id, payload) => updateExternalImap.mutate({ id, payload })} onDeleteExternal={(id) => deleteExternalImap.mutate(id)} onTestExternal={(id) => testExternalImap.mutate(id)} onSyncExternal={(id) => syncExternalImap.mutate(id)} onSyncExternalFolder={(id, folder) => syncExternalImapFolder.mutate({ id, folder })} /> ) if (tab === "contacts") return createContact.mutate(form)} onDelete={(id) => deleteContact.mutate(id)} onCopy={copy} /> if (tab === "cleanup") return cleanup.mutate(target)} /> if (tab === "cleanupQueue") return if (tab === "rules") return createRule.mutate(payload)} onUpdate={(id, payload) => updateRule.mutate({ id, payload })} onToggle={(item) => updateRule.mutate({ id: item.id, payload: { enabled: !item.enabled } })} onMove={(id, direction) => moveRule.mutate({ id, direction })} onApply={(id) => applyRule.mutate(id)} onDelete={(id) => deleteRule.mutate(id)} pending={createRule.isPending || updateRule.isPending || moveRule.isPending || applyRule.isPending} /> if (tab === "blocked") return f.role === "spam")?.count || 0 : 0} onMailboxChange={setBlockedMailboxId} onCreate={(form) => createBlocked.mutate(form)} onDelete={(id) => deleteBlocked.mutate(id)} pending={createBlocked.isPending} /> if (tab === "stats") return stats.refetch()} /> if (tab === "apiTokens") return createApiToken.mutateAsync(payload)} onUpdate={(id, payload) => updateApiToken.mutate({ id, payload })} onDelete={(id) => deleteApiToken.mutate(id)} onCopy={copy} /> return null } } function contentFrameClass(tab: Tab) { return cn( "w-full", tab === "mailboxes" ? "pt-[34px]" : "pt-6", tab === "profile" || tab === "mailboxes" ? "mx-auto max-w-[896px]" : tab === "stats" ? "px-4 sm:px-6" : tab === "rules" || tab === "apiTokens" ? "mx-auto max-w-[896px] px-4 sm:px-0" : "mx-auto max-w-[1024px] px-4 sm:px-0", ) } function SettingsPageHeader({ title, subtitle, action, activeTab, onAccountTabChange }: { title: string; subtitle?: string; action?: React.ReactNode; activeTab?: AccountSettingsTab; onAccountTabChange: (tab: AccountSettingsTab) => void }) { return (

{title}

{subtitle &&

{subtitle}

}
{action &&
{action}
}
{activeTab && (
{accountSettingTabs.map((item) => ( ))}
)}
) } function StatsRangeTabs({ rangeDays, onRangeChange }: { rangeDays: number; onRangeChange: (days: number) => void }) { return (
{[ [7, "7天"], [30, "30天"], [90, "90天"], [365, "365天"], ].map(([value, label]) => ( ))}
) } type AccountSettingsSectionProps = { activeTab: AccountSettingsTab user: { id: string; loginName?: string; email: string; displayName: string; role: string; disabled: boolean; twoFactorEnabled: boolean; createdAt: string; limits?: PermissionLimits } profile: { mutate: (form: FormData) => void; isPending: boolean } password: { mutate: (form: FormData) => void; isPending: boolean } passwordFormRef: React.RefObject stats?: MailStats showStats: boolean displayMode: DisplayMode onDisplayModeChange: (mode: DisplayMode) => void twoFactorFormRef: React.RefObject setupTwoFactor: { data?: { secret: string; otpauthUrl: string }; mutate: () => void; reset: () => void; isPending: boolean } enableTwoFactor: { mutate: (form: FormData) => void; isPending: boolean } disableTwoFactor: { mutate: (form: FormData) => void; isPending: boolean } onCopy: (text: string) => void mailboxes: Mailbox[] selectedMailboxId: string selectedMailbox?: Mailbox labels: MailLabel[] labelsLoading: boolean labelsPending: boolean onCreateLabel: (form: FormData) => void onDeleteLabel: (id: string) => void signatures: MailSignature[] signaturesLoading: boolean signaturesPending: boolean onCreateSignature: (form: FormData) => void onUpdateSignature: (id: string, form: FormData) => void onSetDefaultSignature: (id: string) => void onDeleteSignature: (id: string) => void clientHostname?: string onSelectMailbox: (id: string) => void onOpenCleanup: () => void } function AccountSettingsSection(props: AccountSettingsSectionProps) { if (props.activeTab === "mail") { return ( ) } if (props.activeTab === "clients") { return } if (props.activeTab === "security") { return ( ) } return ( ) } function SettingsCard({ title, subtitle, action, children, className, contentClassName }: { title: string; subtitle?: string; action?: React.ReactNode; children: React.ReactNode; className?: string; contentClassName?: string }) { return (

{title}

{subtitle &&

{subtitle}

}
{action &&
{action}
}
{children}
) } function AccountTabSection({ user, stats, selectedMailbox, mailboxes, onOpenCleanup }: { user: AccountSettingsSectionProps["user"]; profile: AccountSettingsSectionProps["profile"]; stats?: MailStats; showStats: boolean; displayMode: DisplayMode; onDisplayModeChange: (mode: DisplayMode) => void; selectedMailbox?: Mailbox; mailboxes: Mailbox[]; onOpenCleanup: () => void }) { const accountName = user.loginName || user.email const quotaBytes = stats?.quotaBytes || (selectedMailbox?.quotaMb ? selectedMailbox.quotaMb * 1024 * 1024 : 0) const storageBytes = stats?.storageBytes || 0 const quotaPct = quotaBytes > 0 ? Math.min(100, Math.round((storageBytes / quotaBytes) * 100)) : 0 return (
邮件清理}>
{quotaBytes > 0 ? `${formatBytes(storageBytes)} / ${formatBytes(quotaBytes)}` : formatBytes(storageBytes)} {quotaBytes > 0 ? `${quotaPct}%` : "不限"}
0 ? quotaPct : 12}%` }} />
实时按当前账号配置计算}>
) } function InfoLine({ label, value }: { label: string; value: React.ReactNode }) { return (
{label}
{value}
) } function QuotaBox({ title, lines, highlight, className }: { title: string; lines: string[]; highlight?: string; className?: string }) { return (
{title}
{highlight && {highlight}}
{lines.map((line) =>
{line}
)}
) } function MailPreferencesSection({ labels, labelsLoading, labelsPending, onCreateLabel, onDeleteLabel, selectedMailbox, signatures, signaturesLoading, signaturesPending, mailboxes, onCreateSignature, onUpdateSignature, onSetDefaultSignature, onDeleteSignature, }: { labels: MailLabel[] labelsLoading: boolean labelsPending: boolean onCreateLabel: (form: FormData) => void onDeleteLabel: (id: string) => void selectedMailbox?: Mailbox signatures: MailSignature[] signaturesLoading: boolean signaturesPending: boolean mailboxes: Mailbox[] onCreateSignature: (form: FormData) => void onUpdateSignature: (id: string, form: FormData) => void onSetDefaultSignature: (id: string) => void onDeleteSignature: (id: string) => void }) { const [labelColor, setLabelColor] = React.useState("#3b82f6") const [signatureMailboxId, setSignatureMailboxId] = React.useState("all") const [signatureDefault, setSignatureDefault] = React.useState(false) const [editingSignature, setEditingSignature] = React.useState(null) const [pendingConfirm, setPendingConfirm] = React.useState(null) function submitLabel(event: React.FormEvent) { event.preventDefault() if (!selectedMailbox) return const form = new FormData(event.currentTarget) form.set("color", labelColor) onCreateLabel(form) event.currentTarget.reset() setLabelColor("#3b82f6") } function submitSignature(event: React.FormEvent) { event.preventDefault() const form = new FormData(event.currentTarget) form.set("mailboxId", signatureMailboxId === "all" ? "" : signatureMailboxId) form.set("isDefault", signatureDefault ? "on" : "") onCreateSignature(form) event.currentTarget.reset() setSignatureMailboxId("all") setSignatureDefault(false) } return (
setLabelColor(event.target.value)} className="h-10 w-12 cursor-pointer bg-background p-1" aria-label="标签颜色" />
{labels.map((label) => ( {label.name} ))} {!labelsLoading && labels.length === 0 && 暂无标签}
共 {signatures.length} 个签名}>