chore: directory migration - gateway→server, web→client/electron
This commit is contained in:
135
client/electron/src/components/NarrowNav.tsx
Normal file
135
client/electron/src/components/NarrowNav.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
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,
|
||||
ShieldIcon,
|
||||
CalendarIcon
|
||||
} from './icons';
|
||||
import { ConnectionIndicator } from './ConnectionIndicator';
|
||||
import { countPendingPermissions, splitByPermission } from '../lib/mailGroups';
|
||||
|
||||
/**
|
||||
* 窄屏底部导航。
|
||||
*
|
||||
* 移动端把主导航放底部而不是顶部:拇指够得到。
|
||||
* 宽屏用的是左侧竖条(Sidebar),两者共用 uiStore 的 viewMode,
|
||||
* 所以从窄拖到宽不会丢失当前位置。
|
||||
*
|
||||
* 这里只放最常用的几项 + 一个「更多」入口(打开抽屉式 Sidebar)——
|
||||
* 底部塞满图标会挤成一排看不懂的小方块。
|
||||
*/
|
||||
const items: {
|
||||
short: string;
|
||||
mode: ViewMode;
|
||||
Icon: (p: { className?: string }) => JSX.Element;
|
||||
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 }
|
||||
];
|
||||
|
||||
export default function NarrowNav() {
|
||||
const viewMode = useUIStore(s => s.viewMode);
|
||||
const setViewMode = useUIStore(s => s.setViewMode);
|
||||
const composing = useUIStore(s => s.composing);
|
||||
const startCompose = useUIStore(s => s.startCompose);
|
||||
const narrowPane = useUIStore(s => s.narrowPane);
|
||||
|
||||
const inbox = useMailStore(s => s.inbox);
|
||||
// 与 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);
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
const visible = items.filter(n => !n.adminOnly || isAdmin);
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="narrow-nav shrink-0 border-t border-chrome-700 bg-chrome-900 flex items-stretch"
|
||||
// 底部安全区:iPhone 的手势条会盖住最后一排
|
||||
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
|
||||
>
|
||||
{visible.map(({ short, mode, Icon }) => {
|
||||
// 详情栏打开时不高亮任何导航项:此刻用户看的是某封邮件,
|
||||
// 高亮「收件」会让人以为点它能回到列表(其实是同一项)
|
||||
const active = viewMode === mode && !composing && narrowPane === 'list';
|
||||
const badge =
|
||||
mode === 'inbox'
|
||||
? unread
|
||||
: mode === 'permissions'
|
||||
? pendingPerms
|
||||
: mode === 'contacts'
|
||||
? contacts.length
|
||||
: 0;
|
||||
return (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => setViewMode(mode)}
|
||||
className={`relative flex-1 py-2 flex flex-col items-center justify-center gap-0.5 transition-colors ${
|
||||
active ? 'text-white' : 'text-chrome-400 active:bg-chrome-800'
|
||||
}`}
|
||||
>
|
||||
<Icon />
|
||||
<span className="text-[10px] leading-none">{short}</span>
|
||||
{badge > 0 && (
|
||||
<span
|
||||
className={`absolute top-1 right-[22%] min-w-[15px] h-[15px] px-1 rounded-full text-[9px] font-bold flex items-center justify-center ${
|
||||
mode === 'inbox'
|
||||
? 'bg-red-600 text-white'
|
||||
: mode === 'permissions'
|
||||
? 'bg-orange-700 text-white'
|
||||
: 'bg-chrome-600 text-chrome-100'
|
||||
}`}
|
||||
>
|
||||
{badge > 99 ? '99+' : badge}
|
||||
</span>
|
||||
)}
|
||||
{active && <span className="absolute top-0 left-1/4 right-1/4 h-0.5 bg-blue-400" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<button
|
||||
onClick={() => startCompose()}
|
||||
className={`flex-1 py-2 flex flex-col items-center justify-center gap-0.5 ${
|
||||
composing ? 'text-blue-300' : 'text-blue-400 active:bg-chrome-800'
|
||||
}`}
|
||||
>
|
||||
<ComposeIcon />
|
||||
<span className="text-[10px] leading-none">新建</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setViewMode('account')}
|
||||
className={`flex-1 py-2 flex flex-col items-center justify-center gap-0.5 ${
|
||||
viewMode === 'account' && !composing
|
||||
? 'text-white'
|
||||
: 'text-chrome-400 active:bg-chrome-800'
|
||||
}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<PersonIcon />
|
||||
<span className="absolute -top-0.5 -right-1.5">
|
||||
<ConnectionIndicator />
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] leading-none">我的</span>
|
||||
</button>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user