import { useEffect, useMemo, useState, type ComponentType, type ReactNode } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, Bell, Bot, Check, ChevronDown, LogOut, Mail, Repeat2, ShieldCheck, SlidersHorizontal, UserRound, } from "lucide-react"; import { Link } from "react-router-dom"; import { accountNotificationKeys, accountNotificationLabels, getAccountNotificationConsumption, getAccountSettings, updateAccountSettings, type AccountNotificationConsumption, type AccountAiPreferenceSettings, type AccountAnswerDepth, type AccountEvidenceDisplay, type AccountLanguage, type AccountNotificationEvidenceDisplay, type AccountNotificationKey, type AccountProfileSettings, type AccountReminderIntensity, type AccountSettings, type AccountSettingsSaveScope, } from "@/api/accountSettings"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { MobileBottomSheet } from "@/features/pmflow-os/components/FeedbackPrimitives"; import { MobileBottomNav } from "@/features/pmflow-os/components/MobileBottomNav"; import { PMFlowBrand } from "@/features/pmflow-os/components/chrome"; import { cn } from "@/lib/utils"; import { useAuthStore } from "@/stores/auth"; type MobileSheetKey = "profile" | "ai" | "notifications" | null; const answerDepthOptions: Array<{ value: AccountAnswerDepth; title: string; detail: string }> = [ { value: "concise", title: "简洁", detail: "只给结论和必要依据" }, { value: "balanced", title: "标准", detail: "结论、依据、下一步都保留" }, { value: "deep", title: "深入", detail: "展开证据链、风险和取舍" }, ]; const evidenceOptions: Array<{ value: AccountEvidenceDisplay; title: string; detail: string }> = [ { value: "summary", title: "摘要", detail: "只展示来源摘要" }, { value: "inline", title: "内联", detail: "关键结论旁展示证据" }, { value: "full", title: "完整", detail: "保留来源、片段和验证状态" }, ]; const languageOptions: Array<{ value: AccountLanguage; title: string; detail: string }> = [ { value: "zh-CN", title: "中文", detail: "界面、AI 回复和通知默认中文" }, { value: "en-US", title: "English", detail: "English replies when product context allows it" }, ]; const reminderIntensityOptions: Array<{ value: AccountReminderIntensity; title: string; detail: string }> = [ { value: "silent", title: "静默", detail: "不显示页面内提醒,业务对象和证据仍保留" }, { value: "normal", title: "普通", detail: "在对应页面显示标准提醒" }, { value: "prominent", title: "强提醒", detail: "用于高风险、阻塞或长时间停滞场景" }, ]; const notificationEvidenceOptions: Array<{ value: AccountNotificationEvidenceDisplay; title: string; detail: string }> = [ { value: "compact", title: "紧凑", detail: "提醒旁只显示关键证据摘要" }, { value: "expanded", title: "展开", detail: "提醒旁展开来源、状态和审计线索" }, ]; function getDisplayName(user: ReturnType["user"]) { return user?.name || user?.username || "张子墨"; } function getDisplayRole(user: ReturnType["user"]) { if (user?.role === "admin") return "管理员"; if (user?.role === "manager") return "项目负责人"; return user?.role || "产品负责人"; } function getAvatarSeed(user: ReturnType["user"], displayName: string) { return encodeURIComponent(user?.id || user?.username || displayName || "pmflow-account"); } function formatUpdatedAt(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) return "尚未保存"; return date.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }); } export function AccountSettingsPanel({ onLogout }: { onLogout: () => void }) { const queryClient = useQueryClient(); const user = useAuthStore((state) => state.user); const seed = useMemo( () => ({ displayName: getDisplayName(user), roleTitle: getDisplayRole(user), email: user?.email || "", }), [user] ); const settingsQuery = useQuery({ queryKey: ["account-settings", seed.displayName, seed.roleTitle, seed.email], queryFn: () => getAccountSettings(seed), }); const [draft, setDraft] = useState(null); const [activeSheet, setActiveSheet] = useState(null); useEffect(() => { if (settingsQuery.data) setDraft(settingsQuery.data); }, [settingsQuery.data]); const saveMutation = useMutation({ mutationFn: updateAccountSettings, onSuccess: (saved) => { setDraft(saved); queryClient.setQueryData(["account-settings", seed.displayName, seed.roleTitle, seed.email], saved); }, }); const isDirty = Boolean( draft && settingsQuery.data && JSON.stringify(draft) !== JSON.stringify(settingsQuery.data) ); const avatarSrc = `https://api.dicebear.com/7.x/avataaars/svg?seed=${getAvatarSeed(user, draft?.profile.displayName || seed.displayName)}`; const patchProfile = (patch: Partial) => { setDraft((current) => current ? { ...current, profile: { ...current.profile, ...patch } } : current); }; const patchAi = (patch: Partial) => { setDraft((current) => current ? { ...current, aiPreferences: { ...current.aiPreferences, ...patch } } : current); }; const patchNotification = (key: AccountNotificationKey, patch: Partial) => { setDraft((current) => current ? { ...current, notifications: { ...current.notifications, [key]: { ...current.notifications[key], ...patch }, }, } : current ); }; const save = () => { if (!draft) return; saveMutation.mutate(draft); }; const saveStatus: SaveStatus = saveMutation.isError ? "failed" : draft?.saveScope === "account" ? "account" : "local"; if (settingsQuery.isLoading || !draft) { return ; } return ( <> ); } function DesktopAccountSettings({ avatarSrc, draft, isDirty, isSaving, saveStatus, onLogout, onPatchAi, onPatchNotification, onPatchProfile, onSave, }: { avatarSrc: string; draft: AccountSettings; isDirty: boolean; isSaving: boolean; saveStatus: SaveStatus; onLogout: () => void; onPatchAi: (patch: Partial) => void; onPatchNotification: (key: AccountNotificationKey, patch: Partial) => void; onPatchProfile: (patch: Partial) => void; onSave: () => void; }) { return (

我的

个人资料与偏好

这些设置会影响 PMFlow 的默认语言、AI 回复形态、证据展示和提醒节奏。

返回个人空间
} title="个人资料" detail="用于 PMFlow 内的称呼、空间标识和通知展示。"> } title="AI 偏好" detail="控制默认回答深度和证据呈现,不影响运行时模型路由。"> } title="通知偏好" detail="优先覆盖需要人类拍板、执行回写、发布门禁和失败提醒。">
); } function MobileAccountSettings({ avatarSrc, draft, activeSheet, isDirty, isSaving, saveStatus, onOpenSheet, onLogout, onPatchAi, onPatchNotification, onPatchProfile, onSave, }: { avatarSrc: string; draft: AccountSettings; activeSheet: MobileSheetKey; isDirty: boolean; isSaving: boolean; saveStatus: SaveStatus; onOpenSheet: (key: MobileSheetKey) => void; onLogout: () => void; onPatchAi: (patch: Partial) => void; onPatchNotification: (key: AccountNotificationKey, patch: Partial) => void; onPatchProfile: (patch: Partial) => void; onSave: () => void; }) { return (
} title="个人资料" detail={`${draft.profile.displayName} · ${draft.profile.roleTitle}`} onClick={() => onOpenSheet("profile")} /> } title="AI 偏好" detail={`${getAnswerDepthTitle(draft.aiPreferences.answerDepth)} · ${getEvidenceTitle(draft.aiPreferences.evidenceDisplay)}`} onClick={() => onOpenSheet("ai")} /> } title="通知偏好" detail={`${getEnabledNotificationCount(draft)} 类提醒已开启 · ${getProminentNotificationCount(draft)} 类强提醒`} onClick={() => onOpenSheet("notifications")} /> } title="切换账号" detail="保留当前空间上下文" /> } title="退出登录" detail="草案与待确认事项保留" danger onClick={onLogout} />
onOpenSheet(null)} >
onOpenSheet(null)} >
onOpenSheet(null)} >
); } function ProfileSummary({ avatarSrc, draft, mobile = false }: { avatarSrc: string; draft: AccountSettings; mobile?: boolean }) { return (
{`${draft.profile.displayName}头像`}
{draft.profile.displayName}
{draft.profile.roleTitle} · 个人空间
{draft.profile.defaultLanguage === "zh-CN" ? "默认中文" : "Default English"}
上次保存:{formatUpdatedAt(draft.updatedAt)}
); } function SettingsSection({ icon, title, detail, children }: { icon: ReactNode; title: string; detail: string; children: ReactNode }) { return (
{icon}

{title}

{detail}

{children}
); } type SaveStatus = AccountSettingsSaveScope | "failed"; function getSaveStatusCopy(saveStatus: SaveStatus): { title: string; detail: string; wrap: string; iconWrap: string; titleClass: string; Icon: ComponentType<{ className?: string }>; } { if (saveStatus === "account") { return { title: "已保存到当前账号", detail: "这些偏好已和当前登录身份绑定,后续可由系统能力消费。", wrap: "border-[#cfe0ff] bg-[#eef4ff]", iconWrap: "bg-white text-[#2463EB]", titleClass: "text-[#2459d3]", Icon: ShieldCheck, }; } if (saveStatus === "failed") { return { title: "保存失败", detail: "当前偏好还没有写入本机暂存。请重试,或检查浏览器存储权限。", wrap: "border-rose-100 bg-rose-50", iconWrap: "bg-white text-rose-600", titleClass: "text-rose-700", Icon: AlertTriangle, }; } return { title: "仅本机暂存", detail: "当前 V1 不写云端账号设置;刷新本机浏览器会保留,换设备不会同步。", wrap: "border-[#e6edf7] bg-[#f8fbff]", iconWrap: "bg-white text-slate-500", titleClass: "text-slate-700", Icon: Check, }; } function getSaveButtonLabel({ isDirty, isSaving, saveStatus, }: { isDirty: boolean; isSaving: boolean; saveStatus: SaveStatus; }) { if (isSaving) return "保存中"; if (isDirty && saveStatus === "failed") return "重试保存"; if (isDirty) return "保存设置"; if (saveStatus === "account") return "已保存到当前账号"; if (saveStatus === "failed") return "保存失败"; return "仅本机暂存"; } function SaveStatusPanel({ saveStatus, mobile = false }: { saveStatus: SaveStatus; mobile?: boolean }) { const status = getSaveStatusCopy(saveStatus); return (
{status.title}
{status.detail}
); } function ProfileFields({ profile, mobile = false, onPatchProfile, }: { profile: AccountProfileSettings; mobile?: boolean; onPatchProfile: (patch: Partial) => void; }) { return (
onPatchProfile({ displayName: event.target.value })} className="h-11 rounded-lg border-[#dfe7f3] focus-visible:ring-[#2463EB]/30" /> onPatchProfile({ roleTitle: event.target.value })} className="h-11 rounded-lg border-[#dfe7f3] focus-visible:ring-[#2463EB]/30" />
onPatchProfile({ email: event.target.value })} className="h-11 rounded-lg border-[#dfe7f3] pl-9 focus-visible:ring-[#2463EB]/30" placeholder="用于通知摘要" />
onPatchProfile({ defaultLanguage: value })} />
); } function AiPreferenceFields({ aiPreferences, mobile = false, onPatchAi, }: { aiPreferences: AccountAiPreferenceSettings; mobile?: boolean; onPatchAi: (patch: Partial) => void; }) { return (
onPatchAi({ answerDepth: value })} /> onPatchAi({ evidenceDisplay: value })} />
); } function NotificationFields({ notifications, mobile = false, onPatchNotification, }: { notifications: AccountSettings["notifications"]; mobile?: boolean; onPatchNotification: (key: AccountNotificationKey, patch: Partial) => void; }) { const consumption = getAccountNotificationConsumption({ notifications, }); return (
{accountNotificationKeys.map((key) => { const item = notifications[key]; const label = accountNotificationLabels[key]; const reminderIntensity = item.reminderIntensity || (item.enabled ? "normal" : "silent"); const evidenceDisplay = item.evidenceDisplay || "compact"; return (
{label.title}
{label.detail}
onPatchNotification(key, { enabled, reminderIntensity: enabled ? (reminderIntensity === "silent" ? "normal" : reminderIntensity) : "silent", }) } className="mt-1 data-[state=checked]:bg-[#2463EB]" />
onPatchNotification(key, { reminderIntensity, enabled: reminderIntensity !== "silent", }) } /> onPatchNotification(key, { evidenceDisplay })} />
onPatchNotification(key, { desktop })} /> onPatchNotification(key, { emailDigest })} />
); })}
); } function NotificationConsumptionPreview({ items, mobile = false, }: { items: AccountNotificationConsumption[]; mobile?: boolean; }) { return (
页面内提醒消费说明

这些偏好只控制 PMFlow 页面里的提醒强度和证据展开方式:待确认、执行回写、发布门禁和失败提醒。当前不代表系统级桌面推送、邮件推送或真实通知中心已接入。

{items.map((item) => (
{item.productArea}
{item.trigger}
{getReminderIntensityTitle(item.reminderIntensity || (item.enabled ? "normal" : "silent"))} · {getNotificationEvidenceTitle(item.evidenceDisplay || "compact")}
{item.showProminentReminder ? "强提醒" : item.showInAppReminder ? "显示" : "静默"}
{item.detail}
))}
); } function ChannelToggle({ label, checked, disabled, onChange, }: { label: string; checked: boolean; disabled?: boolean; onChange: (checked: boolean) => void; }) { return ( ); } function SegmentedOptions({ options, value, onChange, }: { options: Array<{ value: T; title: string; detail: string }>; value: T; onChange: (value: T) => void; }) { return (
{options.map((option) => { const selected = value === option.value; return ( ); })}
); } function LabeledControl({ label, children }: { label: string; children: ReactNode }) { return (
{label} {children}
); } function ActionButton({ icon, label, detail, danger, onClick, }: { icon: ReactNode; label: string; detail: string; danger?: boolean; onClick?: () => void; }) { return ( ); } function MobileGroup({ title, children }: { title: string; children: ReactNode }) { return (

{title}

{children}
); } function MobileSettingRow({ icon, title, detail, danger, onClick, }: { icon: ReactNode; title: string; detail?: string; danger?: boolean; onClick?: () => void; }) { return ( ); } function AccountSettingsSkeleton() { return (
); } function getEnabledNotificationCount(settings: AccountSettings) { return accountNotificationKeys.filter((key) => settings.notifications[key].enabled).length; } function getProminentNotificationCount(settings: AccountSettings) { return accountNotificationKeys.filter((key) => settings.notifications[key].reminderIntensity === "prominent").length; } function getAnswerDepthTitle(value: AccountAnswerDepth) { return answerDepthOptions.find((option) => option.value === value)?.title || "标准"; } function getEvidenceTitle(value: AccountEvidenceDisplay) { return evidenceOptions.find((option) => option.value === value)?.title || "内联"; } function getReminderIntensityTitle(value: AccountReminderIntensity) { return reminderIntensityOptions.find((option) => option.value === value)?.title || "普通"; } function getNotificationEvidenceTitle(value: AccountNotificationEvidenceDisplay) { return notificationEvidenceOptions.find((option) => option.value === value)?.title || "紧凑"; }