diff --git a/web/index.html b/web/index.html index 50efb42..fd4b893 100644 --- a/web/index.html +++ b/web/index.html @@ -2,8 +2,37 @@ - + AgentMail +
diff --git a/web/src/App.tsx b/web/src/App.tsx index 830ea8a..f05a21a 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { Suspense, lazy, useEffect } from 'react'; import { connectSSE } from './api/sse'; import { useAuthStore } from './stores/authStore'; import { useMailStore } from './stores/mailStore'; @@ -7,6 +7,7 @@ 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'; @@ -14,6 +15,13 @@ 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'; @@ -115,16 +123,24 @@ export default function App() { ) : viewMode === 'account' ? ( + ) : viewMode === 'calendar' ? ( + // 日历自己在内部分两栏(网格 + 当日日程/编辑器),因此走 main 分支 + // 而不是复用外层的 list/main —— 它的「列表」是网格,不是条目清单。 + }> + + ) : viewMode === 'admin' && user?.role === 'admin' ? ( ) : ( ); - // 列表(中栏):仅收发件箱与联系人视图有 + // 列表(中栏):收发件箱、授权、联系人视图有 const list = viewMode === 'contacts' ? ( + ) : viewMode === 'permissions' ? ( + ) : viewMode === 'inbox' || viewMode === 'sent' ? ( ) : null; @@ -161,6 +177,15 @@ export default function App() { ); } +/** 懒加载 chunk 到达前的占位。占满主区域,避免布局跳动。 */ +function PaneLoading() { + return ( +
+

加载中

+
+ ); +} + /** * 窄屏外壳:内容区 + 底部导航。 * diff --git a/web/src/components/AccountPage.tsx b/web/src/components/AccountPage.tsx index 170d3d4..9fb47ab 100644 --- a/web/src/components/AccountPage.tsx +++ b/web/src/components/AccountPage.tsx @@ -3,6 +3,7 @@ import { useAuthStore } from '../stores/authStore'; import * as api from '../api/client'; import { LockIcon, LogoutIcon } from './icons'; import KeyPanel from './KeyPanel'; +import ThemePicker from './ThemePicker'; /** 当前用户个人中心:查看资料、修改密码、管理客户端连接密钥 */ export default function AccountPage() { @@ -211,6 +212,12 @@ export default function AccountPage() { /> + {/* 外观。放在密钥之后、退出之前:它是一个高频且完全可逆的偏好, + 与「账号自身」的密码/密钥属于不同性质,但同样是「我的设置」。 */} +
+ +
+ {/* 退出登录。 放在这里而不是导航里:它是一个低频且不可逆的动作, 与密码、密钥同属「账号自身」。窄屏下这也是唯一的退出口: diff --git a/web/src/components/NarrowNav.tsx b/web/src/components/NarrowNav.tsx index f1c3ab3..ad1bd3a 100644 --- a/web/src/components/NarrowNav.tsx +++ b/web/src/components/NarrowNav.tsx @@ -2,8 +2,18 @@ import { useUIStore, type ViewMode } from '../stores/uiStore'; import { useMailStore } from '../stores/mailStore'; import { useContactStore } from '../stores/contactStore'; import { useAuthStore } from '../stores/authStore'; -import { InboxIcon, SentIcon, ContactsIcon, ComposeIcon, UsersIcon, PersonIcon } from './icons'; +import { + InboxIcon, + SentIcon, + ContactsIcon, + ComposeIcon, + UsersIcon, + PersonIcon, + ShieldIcon, + CalendarIcon +} from './icons'; import { ConnectionIndicator } from './ConnectionIndicator'; +import { countPendingPermissions, splitByPermission } from '../lib/mailGroups'; /** * 窄屏底部导航。 @@ -22,7 +32,9 @@ const items: { adminOnly?: boolean; }[] = [ { short: '收件', mode: 'inbox', Icon: InboxIcon }, + { short: '授权', mode: 'permissions', Icon: ShieldIcon }, { short: '发件', mode: 'sent', Icon: SentIcon }, + { short: '日历', mode: 'calendar', Icon: CalendarIcon }, { short: '联系人', mode: 'contacts', Icon: ContactsIcon }, { short: '管理', mode: 'admin', Icon: UsersIcon, adminOnly: true } ]; @@ -35,7 +47,10 @@ export default function NarrowNav() { const narrowPane = useUIStore(s => s.narrowPane); const inbox = useMailStore(s => s.inbox); - const unread = inbox.filter(m => m.status === 'unread').length; + // 与 Sidebar 同一判据:权限请求归「授权」,不算进收件箱未读 + const { normal, permissions } = splitByPermission(inbox); + const unread = normal.filter(m => m.status === 'unread').length; + const pendingPerms = countPendingPermissions(permissions); const contacts = useContactStore(s => s.contacts); const user = useAuthStore(s => s.user); @@ -45,7 +60,7 @@ export default function NarrowNav() { return (