# Commilitia Drop — 前端设计 补充 PROJECT_BRIEF.md,专注 web 前端实现。后端、协议、鉴权见主 brief。 > 本文记录 MVP 初版前端设计(Mantine + 浅色)。其后前端经「Theme B」重塑:自定义设计 token、品牌 web font、Dracula 深色、聚珍(Juzhen)CJK 综合排版——视觉部分以 [`CHANGELOG`](CHANGELOG.html) 与实际代码为准。 --- ## 1. 技术栈 ``` Vite 构建 React 18 + TypeScript 框架 TanStack Router 路由(类型安全 > React Router) Zustand state 管理 Mantine v7 UI 组件库(CSS modules,零运行时开销) @mantine/dropzone 文件拖拽 @mantine/notifications toast Tailwind CSS utility 微调(不主导样式) lucide-react 图标 原生 RTCPeerConnection P2P ``` **不引入** starter 模板。从 `npm create vite@latest -- --template react-ts` 起步,按需装包。 **不引入**:MUI / Ant Design(bundle 过大)、shadcn/ui(手搓成本与 MVP 速度冲突)、TanStack Query(MVP 阶段 SSE + Zustand 已够,需要时再加)、Storybook / i18next / Husky 等企业级配套。 --- ## 2. 资源占用 前端是纯静态资源,部署在 Caddy 静态目录。服务器开销可忽略: - gzipped bundle 估计 400-600 KB - 服务器内存 / CPU 接近零(无 SSR) - 真正吃资源的是后端 Go 进程的 ring buffer 和 SSE 长连接 构建在本地完成,部署只 push 静态文件。 --- ## 3. 设计语言 中性、克制、工具感。参考点: - **Linear**:留白、灰阶、克制的强调色 - **Cloudflare Dashboard**:信息密度合理的卡片式布局 - **Tailscale Admin**:设备列表、状态指示 避开: - AirDrop 圆圈布局(不打算抄 Apple 的视觉语言,且圆圈在设备数 > 8 时退化严重) - Snapdrop / PairDrop 极简主义(看着是玩具,不是工具) - Material You(不适合自托管极简风) **配色**:浅色优先,且 MVP 阶段**仅实现浅色**。深色模式属阶段三 UX 完善的范围,不在 MVP 内。Mantine 配置 `defaultColorScheme="light"`,不读取 `prefers-color-scheme`,不提供切换控件。 强调色 cdrop 蓝(`#3b82f6` 量级,待最终选定)。文本主色深灰(`#1f2937`),次级灰阶按 Mantine `gray.6 / gray.7` 走。背景纯白或近白(`#fafafa`)以减少屏幕反光强度。 ### 字体与 CJK 排版 > 原 MVP 计划用系统字体栈、不引入 web font;前端重塑(Theme B)后改为品牌 web font + CJK 综合排版。 **字体栈**(`web/src/styles/fonts.css`,逐 region 由 `:lang()` 切 SC/TC/JP/KR,`:root` 默认 SC,亦兼英文 / 无 lang 回退): - **Sans**(正文):`Orbit Gothic CJK ` + 系统回退 - **Serif**:`Noto Serif CJK `(本地合一、全字覆盖)→ `Noto Serif `(Google Fonts Web 子集,跨区并集补字)→ 系统 - **Mono**:`Maple Mono` + `Orbit Gothic CJK ` + 系统 web font 由三个 CDN 的 `@font-face` 提供(`index.html` 三条 ``),均 `font-display: swap`。因这些 `@font-face` 不含 `local()`、family 名又与本机安装同名 → 浏览器被 `@font-face` 捕获、永远走网络(同名本地副本不被用)。故 `web/src/styles/fontface.css` 加**本地优先覆盖层**:在 CDN `` 之后、对项目用到的字重重声明 `@font-face`,`src: local(...), url()` —— 本机命中即零网络瞬时渲染,未装才回退 CDN(swap)。 **CJK 综合排版(聚珍 / Juzhen)**:submodule `web/src/lib/cjk-autospace`(上游 `git.commilitia.net/admin/cjk-autospace`;引擎机制见其 `README.md` / `ARCHITECTURE.md`)。`web/src/utils/cjkAutospace.ts` 为 cdrop 侧薄壳: - `createJuzhen({ lang:{style:"quanjiao"}, scope:{include:".juzhen"}, level:{text:".juzhen"}, pillSelector:"code, kbd, .cjk-pill" })`,`render` / `revert` + MutationObserver 增量(render 幂等)。 - **全角式**(`quanjiao`):句内点号不挤、占全角,仅承载 justify 单份间距与行端收缩。 - **作用域** `.juzhen`(`#root` 加该类):处理与 normalize 收敛于此子树,`body` 上 Mantine portal 不受扰。 - **功能分级**:文本级(中西间隙 / 标点挤压,任意文本片段适用)全树默认跑;段落级(禁则避头尾 / 垂悬字避免 / 长英文断词,需行段布局)仅带 `data-jz-level="paragraph"` 的流动正文(消息 / 剪贴板 / Callout / Toast / 登录说明等)跑。cdrop 绝大多数是 UI chrome(短标签),故默认文本级、正文 opt-in,失败模式良性。 - **copy-clean**:注入的间隙 / 结构元素不入剪贴板,复制得紧凑原文。 --- ## 4. 核心交互:选择目标设备 放弃圆圈布局,采用**单行可横向滚动的设备条**(接近 AirDrop iOS 分享面板)。要求同时支持两种顺序: - **先选文件,再选设备**(拖入 → 高亮可用设备 → 点击设备发送) - **先选设备,再选文件**(点击设备打钩 → 拖入或点击"选择文件" → 自动发送) ### 布局(Home 页) ``` ┌──────────────────────────────────────────────────────────────┐ │ Commilitia Drop [Settings] [User] │ ├──────────────────────────────────────────────────────────────┤ │ │ │ Devices (3 online) │ │ ┌──────────┬──────────┬──────────┬──────────┬───── │ │ │ ● │ ● │ ○ │ ● │ … │ │ │ MBA M4 │ Win-Desk │ iPhone │ Mac mini │ │ │ │ desktop │ desktop │ mobile │ desktop │ │ │ │ this dev │ ✓ │ │ │ │ │ └──────────┴──────────┴──────────┴──────────┴───── │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ Drop files here, or click to browse │ │ │ │ │ │ │ │ Selected: report.pdf (4.2 MB) [×] │ │ │ │ │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ [ Send to Win-Desk ] ← primary CTA, 状态联动 │ │ │ ├──────────────────────────────────────────────────────────────┤ │ Recent transfers │ │ • report.pdf → Win-Desk ✓ 2 min ago │ │ • photo.zip ← iPhone ✓ 5 min ago │ │ • video.mp4 → Mac mini × failed (timeout) │ └──────────────────────────────────────────────────────────────┘ ``` ### 设备卡片状态 每张卡片宽度约 120-140 px,固定不随屏幕拉伸。横向 `overflow-x: auto`,触控板 / 鼠标滚轮可推。 | 状态 | 视觉 | |---|---| | this device | 灰色边框 + "this device" 标签,不可选 | | online idle | 白底 + 浅灰边框 + 绿点,可选 | | online selected | 蓝色描边 + ✓ 角标 + 浅蓝底 | | online incompatible(文件已选但目标设备 capability 不允许) | 灰底 + 50% 透明 + 不可选 | | online incompatible(被选后用户拖入了不兼容文件) | 同上,自动取消选中 + Notification 提示 | | online busy(已在收发) | 黄色环 + 进度数字 | | offline | 灰底 + 50% 透明 + 不可选(默认隐藏,"显示离线"开关展开) | 每张卡片显示:状态点 + 设备名 + 类型小字。不显示头像 / 图标(避免维护设备图标资源)。设备名超长截断为省略号,hover 显示 tooltip 全名。 ### 双向选择顺序的状态机 文件和目标设备分别独立可选,CTA 状态由两者组合: ``` state = (file: File | null, target: Device | null) (null, null) → CTA: disabled "Select file and device" (file, null) → CTA: disabled "Select a device" (null, device) → CTA: disabled "Select a file" (file, device, ok) → CTA: enabled "Send to {device.name}" (file, device, !compat) → CTA: disabled "Cannot send {file.type} to {device.type}" ``` CTA 按钮永远可见在拖拽区下方,文案随状态变化。点击 CTA 即触发 `POST /api/transfer/initiate`。 ### 拖拽优化 - **Tab 键支持**:设备条可用方向键导航,Space 切换选中。无障碍是免费的,Mantine 默认就有 - **拖入页面任意位置**触发全屏 dropzone overlay,提示松手位置(默认行为继承 dropzone) - 文件拖到具体设备卡片上 → 该设备自动选中并立即发送("快捷路径",跳过 CTA)。这是 AirDrop 没有的额外便捷 - 选了不止一个文件 → MVP 拒绝(提示"目前每次只支持一个文件")。多文件留到阶段三 ### 为什么不用圆圈 - 圆圈布局在设备数 > 8 时退化为环形排列,视觉混乱 - 圆圈区分度低,必须靠头像区分设备,但头像维护成本高 - 单行卡片在桌面浏览器(cdrop 主要场景)上信息密度更合适 - 一行卡片可以承载更多信息(设备名 + 类型 + 状态 + 选中标),圆圈只能放头像和小字 - AirDrop 单行布局是 Apple 在分享面板里给出的样式,本身就是为"多目标快速选择"优化过的 --- ## 5. 路由 ``` / Home(设备 + 拖拽 + 历史) /login Casdoor PKCE 跳转中转 /oauth/callback Casdoor 回调 /setup 首次填设备名(设备名未设时强制跳转此页) /settings 设备名修改、退出登录 后续:shortcut token 管理、外观(深色切换) ``` TanStack Router file-based 路由模式,类型安全 + 自动代码分割。 --- ## 6. 状态管理 Zustand 一个 store,分 slice: ``` useStore = { auth: { tokens, user, login, logout, refresh } device: { selfName, selfType, set, hydrate } hub: { connected, devices, signal, sse-handlers } transfer: { current, history, initiate, accept, cancel } ui: { selectedDevice, selectedFile, notifications } } ``` 不引入 Redux 或 Jotai。Zustand 的 selector + `useShallow` 处理性能问题足够。 --- ## 7. 组件清单 业务组件(自己写): ``` DeviceCard 单设备卡片(状态联动) DeviceStrip 横向滚动设备条 FileDropzone 拖拽区(@mantine/dropzone 包一层) SendCTA 主按钮(状态机文案) TransferRow 历史记录单行 TransferProgress 进度条 / RingProgress DeviceTypeBadge desktop / mobile / browser 标签 ``` 页面级: ``` HomePage LoginPage SetupPage SettingsPage OAuthCallbackPage ``` 通用 lib(与组件无关): ``` lib/auth.ts PKCE flow, refresh, fetch wrapper lib/api.ts 带 Authorization + X-Device-Name 的 fetch 封装 lib/hub.ts 自实现 SSE 客户端(fetch + ReadableStream) lib/p2p.ts P2PSession(裸 RTCPeerConnection) lib/relay.ts RelaySession(流式 GET + 分块 POST) lib/transfer.ts Orchestrator(决定 P2P / Relay / fallback) ``` --- ## 8. SSE 客户端要点 浏览器原生 `EventSource` 不支持自定义 header,所以不能直接带 `Authorization`。自实现: ```ts const res = await fetch('/api/hub/events', { headers: { Authorization: `Bearer ${token}`, 'X-Device-Name': name }, signal: abortController.signal }); const reader = res.body!.getReader(); const decoder = new TextDecoder(); let buf = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); // 按 '\n\n' 切分 SSE 事件,按 'event:' / 'data:' 字段解析 } ``` 重连:指数退避(1s → 2s → 4s → 8s → 16s 上限),断开时 UI 显示 "Reconnecting..."。 token 接近过期时(< 5min)先 refresh,再重建 SSE 连接,避免连接中途 401。 --- ## 9. 性能 / Bundle 优化 - TanStack Router 自动代码分割:每个路由独立 chunk - Mantine 按需 import,不要 `import { Button } from '@mantine/core/dist/...'` - 使用 `vite-plugin-imp` 或依赖 Mantine 自身的 ESM tree-shake(v7 默认就好) - recharts / d3 不需要(cdrop 不画图) - `react-helmet` 不需要(应用很简单,document.title 直接改) 构建产物期望: ``` index.html ~ 2 KB assets/index.[hash].js ~ 250-350 KB (gzipped) assets/index.[hash].css ~ 30-50 KB (gzipped) 其他 chunk 按路由懒加载 ``` --- ## 10. 浏览器兼容 目标: - Chrome / Edge ≥ 110(涵盖 99% Chromium 用户) - Firefox ≥ 110 - Safari ≥ 16.4(macOS 13+ / iOS 16.4+,WebRTC 完整支持) 不支持 IE / 旧 Edge / iOS 15 及以下。原因:WebRTC DataChannel 行为差异和 fetch streaming 兼容问题不值得投入精力。 --- ## 11. 与主 brief 的对齐 §3 技术栈一行更新为: ``` **前端**:Vite + React 18 + TypeScript / Mantine v7 + Tailwind / TanStack Router / Zustand / 原生 RTCPeerConnection / lucide-react ``` §7 MVP 验收标准的 "Home 页看到对方在线" 默认指本文件 §4 描述的设备条。 §8 阶段三 UX 完善中"暗色模式"对应本文件 §3 提到的 MVP 不实现深色——阶段三再做。 --- ## 12. 你(Claude Code)需要决策的事 1. 设备卡片的具体尺寸、间距、状态色值,等 §3 强调色锁定后再敲定 2. CTA 按钮的具体文案(中文 vs 英文 vs 双语)。MVP 用英文,i18n 走阶段三 3. 历史记录显示几条 / 是否分页 / 是否支持过滤。MVP 默认显示最近 20 条 4. 有更好的设计参考时,可以反驳 §3 的方向选择