import * as React from "react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import type { ImperativePanelHandle } from "react-resizable-panels" import { useNavigate, useSearchParams } from "react-router-dom" import { ArrowLeft, BarChart3, Ban, Contact, Copy, KeyRound, LogOut, Mail, MailCheck, MailX, Moon, PanelLeftClose, PanelLeftOpen, RefreshCcw, Settings, ShieldCheck, SlidersHorizontal, Sun, Trash2 } from "lucide-react" import { QRCodeSVG } from "qrcode.react" import { api, Mailbox, MailStats } from "@/lib/api" import { cn, formatBytes } from "@/lib/utils" import { applyTheme, getInitialTheme } from "@/lib/theme" import { useMe } from "@/hooks/use-me" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Separator } from "@/components/ui/separator" import { ScrollArea } from "@/components/ui/scroll-area" import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable" import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "@/components/ui/sidebar" import { useToast } from "@/hooks/use-toast" type Tab = "profile" | "mailboxes" | "contacts" | "cleanup" | "rules" | "blocked" | "stats" const tabs: Record = { profile: { label: "账户资料", icon: }, mailboxes: { label: "邮箱管理", icon: }, contacts: { label: "联系人管理", icon: }, cleanup: { label: "邮件清理", icon: }, rules: { label: "收件规则", icon: }, blocked: { label: "被拦截邮件", icon: }, stats: { label: "数据统计", icon: }, } const tabKeys = Object.keys(tabs) as Tab[] const actionLabels: Record = { archive: "移入归档", trash: "移入回收站", star: "添加星标", "mark-read": "标记已读" } 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 sidebarPanelRef = React.useRef(null) const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false) const [mailboxId, setMailboxId] = React.useState(() => localStorage.getItem("lanqin:selected-mailbox") || "") const [darkMode, setDarkMode] = React.useState(getInitialTheme) const [ruleMailboxId, setRuleMailboxId] = React.useState("all") const [ruleAction, setRuleAction] = React.useState("archive") const [blockedMailboxId, setBlockedMailboxId] = React.useState("all") const themeMountedRef = React.useRef(false) const rawTab = params.get("tab") as Tab | null const tab: Tab = rawTab && tabKeys.includes(rawTab) ? rawTab : "profile" const user = me.data?.user const mailboxes = useQuery({ queryKey: ["mailboxes", "mine"], queryFn: api.myMailboxes }) const contacts = useQuery({ queryKey: ["contacts"], queryFn: api.contacts }) const rules = useQuery({ queryKey: ["rules"], queryFn: api.rules }) const blocked = useQuery({ queryKey: ["blocked-senders"], queryFn: api.blockedSenders }) const selectedMailbox = React.useMemo(() => mailboxes.data?.items.find((m) => m.id === mailboxId), [mailboxes.data?.items, mailboxId]) const stats = useQuery({ queryKey: ["mail-stats", mailboxId], queryFn: () => api.mailStats(mailboxId), enabled: !!mailboxId }) 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") || "") if (newPassword !== String(form.get("confirmPassword") || "")) throw new Error("两次输入的新密码不一致") 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 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 createRule = useMutation({ mutationFn: (form: FormData) => api.createRule({ mailboxId: ruleMailboxId === "all" ? "" : ruleMailboxId, name: String(form.get("name") || ""), fromContains: String(form.get("fromContains") || ""), subjectContains: String(form.get("subjectContains") || ""), action: ruleAction, enabled: true }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["rules"] }); toast({ title: "收件规则已保存" }) }, onError: (error) => toast({ title: "保存失败", description: error.message }), }) const deleteRule = useMutation({ mutationFn: api.deleteRule, onSuccess: () => { qc.invalidateQueries({ queryKey: ["rules"] }); toast({ title: "规则已删除" }) } }) 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 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 }), }) React.useEffect(() => { const items = mailboxes.data?.items || [] if (items.length > 0 && (!mailboxId || !items.some((m) => m.id === mailboxId))) setMailboxId(items[0].id) }, [mailboxId, mailboxes.data?.items]) React.useEffect(() => { if (mailboxId) localStorage.setItem("lanqin:selected-mailbox", mailboxId) }, [mailboxId]) React.useEffect(() => { applyTheme(darkMode, themeMountedRef.current); themeMountedRef.current = true }, [darkMode]) async function logout() { await api.logout().catch(() => undefined); qc.clear(); navigate("/login", { replace: true }) } async function copy(text: string) { await navigator.clipboard.writeText(text); toast({ title: "已复制" }) } function setTab(next: Tab) { setParams(next === "profile" ? {} : { tab: next }) } function toggleSidebar() { sidebarCollapsed ? (sidebarPanelRef.current?.expand(14), setSidebarCollapsed(false)) : (sidebarPanelRef.current?.collapse(), setSidebarCollapsed(true)) } if (me.isLoading) return
加载中...
if (me.isError || !user) return
登录状态已失效
return (
setSidebarCollapsed(true)} onExpand={() => setSidebarCollapsed(false)}> setDarkMode((v) => !v)} onBack={() => navigate("/")} /> {!sidebarCollapsed && 个人中心} {tabKeys.map((key) => setTab(key)}>{tabs[key].icon}{!sidebarCollapsed && {tabs[key].label}})}
{tabs[tab].label}
{renderTab()}
) function renderTab() { if (tab === "mailboxes") return { setMailboxId(id); navigate("/") }} /> if (tab === "contacts") return createContact.mutate(form)} onDelete={(id) => deleteContact.mutate(id)} onCopy={copy} /> if (tab === "cleanup") return cleanup.mutate(target)} /> if (tab === "rules") return createRule.mutate(form)} onDelete={(id) => deleteRule.mutate(id)} pending={createRule.isPending} /> if (tab === "blocked") return f.role === "spam")?.count || 0} onMailboxChange={setBlockedMailboxId} onCreate={(form) => createBlocked.mutate(form)} onDelete={(id) => deleteBlocked.mutate(id)} pending={createBlocked.isPending} /> if (tab === "stats") return stats.refetch()} /> return } } function ProfileOverview({ user, profile, password, passwordFormRef, stats, twoFactorFormRef, setupTwoFactor, enableTwoFactor, disableTwoFactor, onCopy }: { user: { email: string; displayName: string; role: string; disabled: boolean; twoFactorEnabled: boolean; createdAt: string }; profile: { mutate: (form: FormData) => void; isPending: boolean }; password: { mutate: (form: FormData) => void; isPending: boolean }; passwordFormRef: React.RefObject; stats?: MailStats; 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 }) { return (
账户信息
{ e.preventDefault(); profile.mutate(new FormData(e.currentTarget)) }}>
角色
{user.role === "admin" ? "管理员" : "普通用户"}
账号状态 {user.disabled ? "已停用" : "正常"}
创建时间 {new Date(user.createdAt).toLocaleString()}
双因素认证
认证状态
{user.twoFactorEnabled ? "已启用" : "未启用"}
{!user.twoFactorEnabled && !setupTwoFactor.data && ( )} {!user.twoFactorEnabled && setupTwoFactor.data && (
{ e.preventDefault(); enableTwoFactor.mutate(new FormData(e.currentTarget)) }}>
)} {user.twoFactorEnabled && (
{ e.preventDefault(); disableTwoFactor.mutate(new FormData(e.currentTarget)) }}>
)}
修改密码
{ e.preventDefault(); password.mutate(new FormData(e.currentTarget)) }}>
) } function MailboxManagement({ mailboxes, selectedMailboxId, onSelect, onCopy, onOpen }: { mailboxes: Mailbox[]; selectedMailboxId: string; onSelect: (id: string) => void; onCopy: (text: string) => void; onOpen: (id: string) => void }) { return
{mailboxes.map((m) =>
{m.address}
{selectedMailboxId === m.id && 当前}
)}{mailboxes.length === 0 && }
} function ContactsSection({ items, loading, onCreate, onDelete, onCopy, pending }: { items: { id: string; name: string; email: string; note: string }[]; loading: boolean; onCreate: (form: FormData) => void; onDelete: (id: string) => void; onCopy: (text: string) => void; pending: boolean }) { return
新增联系人
{ e.preventDefault(); onCreate(new FormData(e.currentTarget)); e.currentTarget.reset() }}>
联系人列表{items.map((item) =>
{item.name}
{item.email}{item.note ? ` · ${item.note}` : ""}
)}{!loading && items.length === 0 && }
} function CleanupSection({ mailbox, stats, pending, onCleanup }: { mailbox?: Mailbox; stats?: MailStats; pending: boolean; onCleanup: (target: "empty-trash" | "empty-spam" | "archive-read-inbox") => void }) { return
清理当前邮箱} title="归档已读收件箱" disabled={!mailbox || pending} onClick={() => onCleanup("archive-read-inbox")} />} title="清空垃圾邮件" disabled={!mailbox || pending} onClick={() => onCleanup("empty-spam")} />} title="清空回收站" disabled={!mailbox || pending} onClick={() => onCleanup("empty-trash")} />
} function RulesSection({ items, mailboxes, mailboxId, action, onMailboxChange, onActionChange, onCreate, onDelete, pending }: { items: any[]; mailboxes: Mailbox[]; mailboxId: string; action: string; onMailboxChange: (value: string) => void; onActionChange: (value: string) => void; onCreate: (form: FormData) => void; onDelete: (id: string) => void; pending: boolean }) { return
新增收件规则
{ e.preventDefault(); onCreate(new FormData(e.currentTarget)); e.currentTarget.reset() }}>
规则列表{items.map((item) =>
{item.name}{actionLabels[item.action]}
{item.mailboxId ? mailboxes.find((m) => m.id === item.mailboxId)?.address : "全部邮箱"} · {item.fromContains ? `发件人包含 ${item.fromContains}` : ""} {item.subjectContains ? `主题包含 ${item.subjectContains}` : ""}
)}{items.length === 0 && }
} function BlockedSection({ items, mailboxes, mailboxId, spamCount, onMailboxChange, onCreate, onDelete, pending }: { items: any[]; mailboxes: Mailbox[]; mailboxId: string; spamCount: number; onMailboxChange: (value: string) => void; onCreate: (form: FormData) => void; onDelete: (id: string) => void; pending: boolean }) { return
新增拦截发件人
{ e.preventDefault(); onCreate(new FormData(e.currentTarget)); e.currentTarget.reset() }}>
被拦截邮件{items.map((item) =>
{item.email}
{item.mailboxId ? mailboxes.find((m) => m.id === item.mailboxId)?.address : "全部邮箱"}{item.reason ? ` · ${item.reason}` : ""}
)}{items.length === 0 && }
} function StatsSection({ stats, mailbox, onRefresh }: { stats?: MailStats; mailbox?: Mailbox; onRefresh: () => void }) { return
当前统计:{mailbox?.address || "未选择邮箱"}
文件夹分布{(stats?.byFolder || []).map((f) =>
{folderLabel(f.folder)}
{f.count} 封未读 {f.unread}{formatBytes(f.bytes)}
)}
} function StatsSummary({ stats }: { stats?: MailStats }) { const cards = [{ label: "总邮件", value: stats?.totalMessages || 0 }, { label: "未读", value: stats?.unreadMessages || 0 }, { label: "星标", value: stats?.starredMessages || 0 }, { label: "附件", value: stats?.attachmentCount || 0 }, { label: "容量", value: formatBytes(stats?.storageBytes || 0) }] return
{cards.map((c) =>
{c.value}
{c.label}
)}
} function CleanupButton({ icon, title, disabled, onClick }: { icon: React.ReactNode; title: string; disabled: boolean; onClick: () => void }) { return } function MailboxSelect({ value, mailboxes, onChange }: { value: string; mailboxes: Mailbox[]; onChange: (value: string) => void }) { return } function Field({ label, children }: { label: string; children: React.ReactNode }) { return
{children}
} function EmptyState({ text }: { text: string }) { return
{text}
} function folderLabel(folder: string) { return ({ Inbox: "收件箱", Sent: "已发送", Drafts: "草稿箱", Archive: "归档", Spam: "垃圾邮件", Trash: "回收站" } as Record)[folder] || folder } function AccountHeader({ collapsed, name, email, darkMode, onToggleTheme, onBack }: { collapsed: boolean; name: string; email?: string; darkMode: boolean; onToggleTheme: () => void; onBack: () => void }) { const displayName = cleanAccountName(name, email) if (collapsed) return
{accountInitial(displayName, email)}
return
{accountInitial(displayName, email)}
{displayName}
} function cleanAccountName(name: string, email?: string) { const value = name.trim(); if (!value || (email && value.toLowerCase() === email.toLowerCase())) return email?.split("@")[0] || "用户"; return value } function accountInitial(name: string, email?: string) { const source = cleanAccountName(name, email); const first = Array.from(source.trim())[0]; return (first || "蓝").toUpperCase() }