主题
Lampio 桌面端 · 前端(React / TypeScript)侧指南
会话触达
src/下任何文件时,本文件叠加到根CLAUDE.md之上。 从根 CLAUDE.md 拆出:2026-08-28(docs/plans/archive/rvh-merge-plan.md阶段 1,裁定表见其 §3 T1-1)。⚠️ 这份是叠加,不是替代。 设计 token / 组件变体 / 间距字号 scale 的真相源仍是
docs/ui-standards.md;组件调用点与废弃候选归docs/ui-component-inventory.md(那份定期重扫全仓)。 本文件不抄它们任何一句 —— 抄了就是第二本账。⚠️ Tauri 边界那侧(webview 生命周期 / IPC / Tab 系统 / 状态归属三原则)在
../src-tauri/CLAUDE.md§1「架构要点」。新功能要不要造 Tab、 状态放 local 还是 store、跨边界怎么通信,判据在那边。
目录
1. 前端技术选型
框架层(Tauri / React / Vite / Tailwind / Zustand / Supabase)见根
CLAUDE.md§1 技术栈。 这里只列 UI 层——headless + utility,无组件库。
| 层 | 技术 |
|---|---|
| UI 库(headless + utility,无组件库) | lucide-react(图标) · class-variance-authority + tailwind-merge + clsx(cva 变体 + class 合并) · sonner(toast) · @radix-ui/react-{dialog,popover,tooltip}(focus-trap / a11y primitives) |
业务 molecule(自建于 src/components/ui/) | ListItem / SectionHeader / EmptyState / Divider / Icon / SiteFavicon / TabButton / Button / IconButton / ChromeButton / Modal / ConfirmDialog / Popover / Tooltip / Input 等(详见 docs/ui-component-inventory.md) |
2. 顶层导航模型
顶层导航 = Workspace 模块(
useWorkspaceStore.activeModule+ 左 railActivityBar):Discover / Read / Review / Library / Stats + Settings / Account,启动默认落 Discover。Discover = 独立模块渲染HomePage(推荐进水口);Home 自 2026-08-01 App-shell 重构起不再是 Read 内的 tab(app-shell-redesign-plan.md)。左右辅助面板互斥(usePanelStore,单辅助面板占阅读宽度)。详见docs/architecture.md §5。🔒 模块切换只换视图,不销毁状态;销毁只由用户显式动作触发(2026-08-12 定,
module-state-preservation-plan.md)。落点三条:① 切模块走lib/moduleNav.switchModule,离开 Review 是挂起(leaveReviewModule)不是endSession——后者的合法调用方只剩「用户显式结束」与 🔒登出(红线 I3);② 卡侧栏的挂载条件是isInFocusSession、可见条件才是activeModule==='review'(ReviewSession的 cards/currentIndex/revealed 是 local state,一卸载就从第一张重来)——代价是它在别的模块里仍挂载着,凡「只在复习界面才该发生」的副作用(键盘 ←/→ 等)都要再判activeModule;③ Library 的「我正在看哪个词/笔记」进useLibraryStore四槽位(各 tab id 空间不同,不共用),page/entries/多选仍 local(重拉才是对的)。
3. 技术红线(前端)🔒
🔒 编号是全局唯一标识符,不因分表而重排。桌面端另外 15 条在
../src-tauri/CLAUDE.md§2;双端契约四条(#5i / #6d / #9 / #10) 在根CLAUDE.md§4,那里还有一张 20 行全量索引,标明每条正文在哪。
| # | 规则 | 原因 |
|---|---|---|
| 8 | 组件内禁止 hex 字面量 | 用 lib/design-tokens.ts |
另外三条不在红线表、但同样由 /arch-check 机械扫描的前端禁止项(正文在根 CLAUDE.md §12): 组件直接 invoke()(走 src/lib/commands.ts)· 新命令用 Result<_, String> · JSX 中直接写中文字面量(走 src/lib/strings/,守门 pnpm run check:no-inline-zh)。
4. 非显然约定索引(前端侧)
这里只放"读代码得不到"的东西。 加新行前先自问:打开那个文件的前 20 行能不能得到这句话?能,就别加。 文件有什么,
ls和 Grep 比这张表新鲜;踩过什么坑,只有这里有。 (本节曾是一张 44 行的文件清单,34 行只是把文件名换成中文再说一遍——那种行是会过期的副本, 2026-08 抓到过两行指着已删除的组件,故砍到只剩坑。) Rust / content-script 侧的同款表在../src-tauri/CLAUDE.md§5。
| 文件 | 非显然之处 |
|---|---|
src/pages/ | 只有 HomePage + BrowserPage 两个页面。其余「页面」全是 Workspace 模块下的面板组件(在 src/components/)——按 pages/ 去找会找不到(2026-08-30 自根 CLAUDE.md §2 下沉) |
src/lib/commands.ts | 所有 Tauri invoke 的唯一出口。组件/页面直连 invoke() 是禁止的(/arch-check 扫,见 docs/coding-standards.md §3.6) |
src/components/TextDraftEditor.tsx | 粘贴文本编辑态。裁定:编辑归 React,阅读归 webview(2026-08-07 从 content webview 的 Rust 合成页迁来) |
src/lib/epubStart.ts | 「这本书从哪一章开始读」的唯一决策处。回落链:调用方给的锚点 → 库里最近读过的锚点 → EpubInfo.start_index 跳过封面 → chapters[0]。位置信息取自 reading_pages.source_ref 的 fragment,故零 migration |
src/lib/url.ts | stripFragment/fragmentOf:EPUB 的 source_ref 恒带 #锚点,凡把它当文件路径用的地方都必须先剥(Rust 侧 notes.rs 早有同款)。忘了剥 = 一个根因三处断路 |
src/lib/moduleNav.ts | 「切模块」的唯一入口,三个调用方(左 rail /「← 返回生词本」/「首次遇见于」)都走它,禁止各自手拼。endReviewAndLeave 的显式结束必须连带切模块,否则 review 模块没 body、用户盯着空白。🔒 页面定位走 __RB_SCROLL_TO_ANNOTATION,绝不用复习那条 __RB_REVIEW_MODE——后者是「答案揭晓」通道,且会盖过 ipc.js::effectiveSourceUrl 的归属回落 → 在该页查词/划线的来源被写歪。两条通道分开正是 recall 与 revisit 不合流的地方 |
src/lib/revisit.ts | 「这一页还能不能在本地打开重温」的唯一判据。判据是「有没有可读本地 URL」不是「有没有 cached_file_path」——后者会把粘贴文档全判成"无来源"(其正文本身就是那个 rb-cache 文件,从不另存快照)。⚠️ 不许按路径前缀分派:EPUB 章节的 cached_file_path 也是 web/<hash>.html |
UI 业务组件库(src/components/ui/):每个组件的实际调用点、业务实体映射、0 调用的废弃候选、 未抽象但重复的 pattern,统一由 docs/ui-component-inventory.md 维护 (那份会定期重扫全仓)。这里不再抄一份 —— 抄了就是第二本账,必然与它漂移。 "应该用哪个 variant" 归 docs/ui-standards.md §13。