import { useEffect, useState, type ReactNode } from "react"; import { AnimatePresence, motion } from "motion/react"; import { Bell, BriefcaseBusiness, ChevronRight, CircleUserRound, HelpCircle, LogOut, Mail, MapPinned, Moon, Eye, EyeOff, Phone, Settings, Shield, Sun, User, X, } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@/components/ThemeProvider"; import { clearAuth, getCurrentUser, getProfileOverview, updateCurrentUserProfile, updateCurrentUserPassword, type ProfileOverview, type UpdateCurrentUserProfilePayload, type UpdateCurrentUserPasswordPayload, type UserProfile, } from "@/lib/auth"; type MenuItem = { key: "personal" | "notice" | "security" | "help"; icon: typeof User; label: string; color: string; bg: string; }; type EditableProfileForm = UpdateCurrentUserProfilePayload; const MENU_ITEMS: MenuItem[] = [ { key: "personal", icon: User, label: "个人资料", color: "text-blue-500 dark:text-blue-400", bg: "bg-blue-50 dark:bg-blue-500/10" }, { key: "notice", icon: Bell, label: "消息通知", color: "text-amber-500 dark:text-amber-400", bg: "bg-amber-50 dark:bg-amber-500/10" }, { key: "security", icon: Shield, label: "账号安全", color: "text-emerald-500 dark:text-emerald-400", bg: "bg-emerald-50 dark:bg-emerald-500/10" }, { key: "help", icon: HelpCircle, label: "帮助中心", color: "text-violet-500 dark:text-violet-400", bg: "bg-violet-50 dark:bg-violet-500/10" }, ]; const EMPTY_FORM: EditableProfileForm = { displayName: "", email: "", phone: "", }; const EMPTY_PASSWORD_FORM: SecurityForm = { oldPassword: "", newPassword: "", confirmPassword: "", }; type SecurityForm = UpdateCurrentUserPasswordPayload & { confirmPassword: string; }; function displayText(value?: string | null) { return value && value.trim() ? value : "无"; } function numericValue(value?: number | null) { return typeof value === "number" && Number.isFinite(value) ? value : 0; } function PageModal({ title, subtitle, onClose, children, footer, }: { title: string; subtitle?: string; onClose: () => void; children: ReactNode; footer?: ReactNode; }) { return ( <> {title} {subtitle ? {subtitle} : null} {children} {footer ? {footer} : null} > ); } export default function Profile() { const { theme, setTheme } = useTheme(); const navigate = useNavigate(); const [overview, setOverview] = useState(null); const [currentUser, setCurrentUser] = useState(null); const [profileDrawerOpen, setProfileDrawerOpen] = useState(false); const [securityDrawerOpen, setSecurityDrawerOpen] = useState(false); const [form, setForm] = useState(EMPTY_FORM); const [detailLoading, setDetailLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [securityForm, setSecurityForm] = useState(EMPTY_PASSWORD_FORM); const [securitySaving, setSecuritySaving] = useState(false); const [securityError, setSecurityError] = useState(""); const [securitySuccess, setSecuritySuccess] = useState(""); const [showOldPassword, setShowOldPassword] = useState(false); const [showNewPassword, setShowNewPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); useEffect(() => { let ignore = false; async function loadProfileData() { try { const me = await getCurrentUser(); const overviewData = await getProfileOverview().catch(() => null); if (ignore) { return; } setCurrentUser(me); setOverview(overviewData); } catch { if (ignore) { return; } setOverview(null); setCurrentUser(null); } } void loadProfileData(); return () => { ignore = true; }; }, []); const handleLogout = () => { clearAuth(); navigate("/login", { replace: true }); }; const handleNavigateToMonthlyOpportunity = () => { navigate("/opportunities"); }; const handleNavigateToMonthlyExpansion = () => { navigate("/expansion"); }; const handleOpenProfile = async () => { setDetailLoading(true); setError(""); try { const me = currentUser ?? await getCurrentUser(); const latestOverview = overview ?? await getProfileOverview().catch(() => null); setCurrentUser(me); setOverview(latestOverview); setForm({ userId: me.userId, username: me.username, displayName: me.displayName || "", email: me.email || "", phone: me.phone || "", pwdResetRequired: me.pwdResetRequired, isPlatformAdmin: me.isPlatformAdmin, orgId: undefined, }); setProfileDrawerOpen(true); } catch (loadError) { setError(loadError instanceof Error ? loadError.message : "获取个人资料失败"); setProfileDrawerOpen(true); } finally { setDetailLoading(false); } }; const handleCloseProfile = () => { if (saving) { return; } setProfileDrawerOpen(false); setError(""); }; const handleSave = async () => { if (saving) { return; } setSaving(true); setError(""); try { await updateCurrentUserProfile({ ...form, email: form.email || undefined, phone: form.phone || undefined, }); const me = await getCurrentUser(); const overviewData = await getProfileOverview().catch(() => overview); setCurrentUser(me); setOverview(overviewData); setForm({ userId: me.userId, username: me.username, displayName: me.displayName || "", email: me.email || "", phone: me.phone || "", pwdResetRequired: me.pwdResetRequired, isPlatformAdmin: me.isPlatformAdmin, orgId: undefined, }); sessionStorage.setItem("userProfile", JSON.stringify(me)); setProfileDrawerOpen(false); } catch (saveError) { setError(saveError instanceof Error ? saveError.message : "保存失败"); } finally { setSaving(false); } }; const handleOpenSecurity = () => { setSecurityForm(EMPTY_PASSWORD_FORM); setSecurityError(""); setSecuritySuccess(""); setShowOldPassword(false); setShowNewPassword(false); setShowConfirmPassword(false); setSecurityDrawerOpen(true); }; const handleCloseSecurity = () => { if (securitySaving) { return; } setSecurityDrawerOpen(false); setSecurityError(""); setSecuritySuccess(""); }; const handleUpdatePassword = async () => { if (securitySaving) { return; } if (!securityForm.oldPassword || !securityForm.newPassword || !securityForm.confirmPassword) { setSecurityError("请完整填写旧密码、新密码和确认密码"); setSecuritySuccess(""); return; } if (securityForm.newPassword !== securityForm.confirmPassword) { setSecurityError("两次输入的新密码不一致"); setSecuritySuccess(""); return; } if (securityForm.newPassword.length < 6) { setSecurityError("新密码长度不能少于6位"); setSecuritySuccess(""); return; } setSecuritySaving(true); setSecurityError(""); setSecuritySuccess(""); try { await updateCurrentUserPassword({ oldPassword: securityForm.oldPassword, newPassword: securityForm.newPassword, }); clearAuth(); navigate("/login", { replace: true }); } catch (saveError) { setSecurityError(saveError instanceof Error ? saveError.message : "密码修改失败"); } finally { setSecuritySaving(false); } }; const roleNames = displayText(overview?.jobTitle); const orgNames = displayText(overview?.deptName); const displayName = displayText(currentUser?.displayName || overview?.realName); const avatarText = displayName === "无" ? "无" : displayName.slice(0, 1); const currentEmail = displayText(currentUser?.email); const currentPhone = displayText(currentUser?.phone); return ( 我的 {avatarText} {displayName} 部门:{orgNames} 岗位:{roleNames} void handleOpenProfile()} className="self-end rounded-full bg-slate-50 p-2 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600 dark:bg-slate-800 dark:text-slate-500 dark:hover:bg-slate-700 dark:hover:text-slate-300 sm:self-start" > {numericValue(overview?.monthlyOpportunityCount)} 本月商机 {numericValue(overview?.monthlyExpansionCount)} 本月拓展 {numericValue(overview?.averageScore)} 平均分 setTheme(theme === "dark" ? "light" : "dark")} className="flex w-full items-center justify-between p-4 transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/50 md:hidden" > {theme === "dark" ? : } {theme === "dark" ? "切换亮色模式" : "切换暗色模式"} {MENU_ITEMS.map((item) => ( void handleOpenProfile() : item.key === "security" ? handleOpenSecurity : undefined } className="flex w-full items-center justify-between p-4 text-left transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/50" > {item.label} ))} 退出登录 {profileDrawerOpen ? ( 取消 void handleSave()} disabled={saving || detailLoading} className="rounded-xl bg-violet-600 px-4 py-3 text-sm font-medium text-white shadow-sm transition-colors hover:bg-violet-700 disabled:cursor-not-allowed disabled:opacity-60" > {saving ? "保存中..." : "保存资料"} )} > {error && !currentUser ? ( {error} ) : ( {(form.displayName || currentUser?.displayName || "无").slice(0, 1)} {displayText(currentUser?.displayName)} 部门:{orgNames} 岗位:{roleNames} 账号:{displayText(currentUser?.username)} 显示名称 setForm((current) => ({ ...current, displayName: event.target.value }))} className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50" /> 用户名 {displayText(currentUser?.username)} 手机号 setForm((current) => ({ ...current, phone: event.target.value }))} className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50" /> 邮箱 setForm((current) => ({ ...current, email: event.target.value }))} className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50" /> 岗位信息 岗位:{roleNames} 状态:{displayText(overview?.accountStatus)} 组织信息 部门:{orgNames} 入职天数:{numericValue(overview?.onboardingDays)} 当前手机号:{currentPhone} 当前邮箱:{currentEmail} {detailLoading ? ( 正在加载个人资料... ) : null} {error ? ( {error} ) : null} )} ) : null} {securityDrawerOpen ? ( 关闭 void handleUpdatePassword()} disabled={securitySaving} className="rounded-xl bg-emerald-600 px-4 py-3 text-sm font-medium text-white shadow-sm transition-colors hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-60" > {securitySaving ? "提交中..." : "更新密码"} )} > 为了账号安全,请先输入当前密码,再设置新密码。 当前密码 setSecurityForm((current) => ({ ...current, oldPassword: event.target.value }))} className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 pr-12 text-sm outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 dark:border-slate-800 dark:bg-slate-900/50" /> setShowOldPassword((current) => !current)} className="absolute inset-y-0 right-0 flex w-12 items-center justify-center text-slate-400 transition-colors hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300" aria-label={showOldPassword ? "隐藏当前密码" : "显示当前密码"} > {showOldPassword ? : } 新密码 setSecurityForm((current) => ({ ...current, newPassword: event.target.value }))} className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 pr-12 text-sm outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 dark:border-slate-800 dark:bg-slate-900/50" /> setShowNewPassword((current) => !current)} className="absolute inset-y-0 right-0 flex w-12 items-center justify-center text-slate-400 transition-colors hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300" aria-label={showNewPassword ? "隐藏新密码" : "显示新密码"} > {showNewPassword ? : } 确认新密码 setSecurityForm((current) => ({ ...current, confirmPassword: event.target.value }))} className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 pr-12 text-sm outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 dark:border-slate-800 dark:bg-slate-900/50" /> setShowConfirmPassword((current) => !current)} className="absolute inset-y-0 right-0 flex w-12 items-center justify-center text-slate-400 transition-colors hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300" aria-label={showConfirmPassword ? "隐藏确认密码" : "显示确认密码"} > {showConfirmPassword ? : } {securityError ? ( {securityError} ) : null} {securitySuccess ? ( {securitySuccess} ) : null} ) : null} ); }
{subtitle}
部门:{orgNames}
岗位:{roleNames}
{numericValue(overview?.monthlyOpportunityCount)}
本月商机
{numericValue(overview?.monthlyExpansionCount)}
本月拓展
{numericValue(overview?.averageScore)}
平均分
状态:{displayText(overview?.accountStatus)}
入职天数:{numericValue(overview?.onboardingDays)}
为了账号安全,请先输入当前密码,再设置新密码。