import { Suspense, lazy, useEffect } from 'react'; import { connectSSE } from './api/sse'; import { useAuthStore } from './stores/authStore'; import { useMailStore } from './stores/mailStore'; import { useSessionStore } from './stores/sessionStore'; import { useContactStore } from './stores/contactStore'; import { useUIStore } from './stores/uiStore'; import Sidebar from './components/Sidebar'; import MailList from './components/MailList'; import PermissionList from './components/PermissionList'; import ContactPanel from './components/ContactPanel'; import MailView from './components/MailView'; import ComposePage from './components/ComposePage'; import LoginPage from './components/LoginPage'; import SetupPage from './components/SetupPage'; import AccountPage from './components/AccountPage'; import AdminUsersPage from './components/AdminUsersPage'; // 日历懒加载。 // // 它传递依赖 lunar-javascript(node_modules 里 588KB)—— 全量打进主 chunk // 会让**登录页**也背上这份体积(实测主 bundle 447KB → 751KB, // gzip 133 → 239KB)。农历表是一大张查找数据,压不动也没必要提前拉: // 大多数会话根本不打开日历。 const CalendarView = lazy(() => import('./components/CalendarView')); import NarrowNav from './components/NarrowNav'; import NarrowStack from './components/NarrowStack'; import { useIsNarrow } from './hooks/useIsNarrow'; export default function App() { const phase = useAuthStore(s => s.phase); const bootstrap = useAuthStore(s => s.bootstrap); const user = useAuthStore(s => s.user); const viewMode = useUIStore(s => s.viewMode); const composing = useUIStore(s => s.composing); const resetUI = useUIStore(s => s.reset); const narrowPane = useUIStore(s => s.narrowPane); const narrow = useIsNarrow(); const fetchInbox = useMailStore(s => s.fetchInbox); const fetchSent = useMailStore(s => s.fetchSent); const dropMailSession = useMailStore(s => s.dropSession); const fetchSessions = useSessionStore(s => s.fetchSessions); const dropSessionIfCurrent = useSessionStore(s => s.dropSessionIfCurrent); const refreshRenameProposal = useSessionStore(s => s.refreshRenameProposal); const refreshBudget = useSessionStore(s => s.refreshBudget); const fetchContacts = useContactStore(s => s.fetchContacts); const removeSessionLocally = useContactStore(s => s.removeSessionLocally); // 启动时检测初始化状态 / 登录态 useEffect(() => { bootstrap(); }, []); // 登出后清理客户端状态,避免脏数据残留 useEffect(() => { if (phase === 'anonymous') { resetUI(); } }, [phase]); // 登录态就绪后拉取数据 + SSE useEffect(() => { if (phase !== 'authenticated') return; fetchInbox('all'); fetchSent(); fetchSessions(); fetchContacts(); return connectSSE((type, data) => { switch (type) { case 'new_mail': fetchInbox('all'); fetchSessions(); fetchContacts(); // 新来信可能带改名建议;Agent 发信也会消耗本任务的往返预算 refreshRenameProposal(); refreshBudget(); break; case 'session_update': // 别人(或另一个标签页)改了预算/别名 refreshBudget(); fetchInbox('all'); fetchSessions(); fetchContacts(); break; case 'permission_decision': fetchInbox('all'); fetchSessions(); fetchContacts(); break; case 'session_archived': { const id = typeof data.session_id === 'string' ? data.session_id : ''; if (!id) break; removeSessionLocally(id); dropMailSession(id); dropSessionIfCurrent(id); break; } } }); }, [phase]); // loading 阶段 if (phase === 'checking') { return (

加载中

); } // 未登录 if (phase === 'anonymous') { return ; } // 主区域(右栏):写信 / 账号 / 管理 / 邮件详情 const main = composing ? ( ) : viewMode === 'account' ? ( ) : viewMode === 'calendar' ? ( // 日历自己在内部分两栏(网格 + 当日日程/编辑器),因此走 main 分支 // 而不是复用外层的 list/main —— 它的「列表」是网格,不是条目清单。 }> ) : viewMode === 'admin' && user?.role === 'admin' ? ( ) : ( ); // 列表(中栏):收发件箱、授权、联系人视图有 const list = viewMode === 'contacts' ? ( ) : viewMode === 'permissions' ? ( ) : viewMode === 'inbox' || viewMode === 'sent' ? ( ) : null; // 账号/管理页没有列表栏,窄屏下要直接显示主区域, // 否则会出现一片空白(列表为 null 而 narrowPane 还停在 'list') const hasList = list !== null; // ---- 窄屏:详情页从右侧滑入盖住列表,不分栏 ---- if (narrow) { // 没有列表栏的视图(账号/管理/写信)直接铺满,不需要覆盖层: // 它们本来就是单页,套一层滑动只会让「进入账号页」也带动画,很怪 if (!hasList) { return (
{main}
); } return ( ); } // ---- 宽屏:三栏并排 ---- return (
{list} {main}
); } /** 懒加载 chunk 到达前的占位。占满主区域,避免布局跳动。 */ function PaneLoading() { return (

加载中

); } /** * 窄屏外壳:内容区 + 底部导航。 * * 没有抽屉式侧栏。它曾经存在,装的是六个与底部导航完全重复的入口, * 唯一独有的是退出登录(已移到「我的」页)。为一个按钮维护一套 * fixed 层级 + 遮罩的代价是:抽屉 `z-50` 铺满视口高度,把底部导航 * 最左那一项盖住点不到(实测 elementFromPoint 命中抽屉里的 SVG)。 */ function NarrowShell({ children }: { children: React.ReactNode }) { return (
{children}
); } /** 未登录时:先查 needs_setup,决定展示初始化向导还是登录页 */ function AnonymousRoute({ onBootDone }: { onBootDone: () => void }) { // bootstrap 已经在 App 里调过了,此处仅判断路由 // 若 bootstrap 已经把 phase 推到 anonymous,needs_setup 需独立查询 // 为简化,在 LoginPage 上方嵌套 SetupPage 的判断逻辑 return ; } import { setupStatus } from './api/client'; function LoginOrSetup({ onDone }: { onDone: () => void }) { const [needsSetup, setNeedsSetup] = useState(null); useEffect(() => { setupStatus() .then(r => setNeedsSetup(r.needs_setup)) .catch(() => setNeedsSetup(false)); }, []); if (needsSetup === null) { return (

加载中

); } if (needsSetup) { return ; } return ; } import { useState } from 'react';