来源:notes/documents/网页版多模块设计:外壳契约、页边档号与日程模块.md

网页版多模块设计:外壳契约、页边档号与日程模块

背景与范围

2026-09-22,用 frontend-design 给网页版做了一次「多模块化」的设计:外壳怎么承载未来会不断增加的 模块,笔记与日程两种阅读模式怎么长在同一副骨架上。这一轮只出设计,不写代码。

触发这次设计的两个前提:一是未来要把 daily log 以日历形式搬上网页,网页版必须按多模块来设计, 不能做成「笔记查看器 + 以后再加一个页面」;二是上一轮的体检与实施计划(网页版前端首轮体检与优化实施计划) 只解决了坏掉的部分,没有回答「它该长成什么样」。

2026-09-22 当天把三处待拍板定了:正文用宋体(限定在阅读页)、默认素纸、第三第四模块是 任务与材料/来源。

设计依据的是真实数据,不是假设:事件表 285 条、覆盖 87 天(2025-11-18 至 2026-09-20), 最忙的一天 18 条;类型分布 life 206、reflection 30、note 25、investment 16、task 6、 meeting 2;只有 16 条有结束时间,duration_minuteslocation 全空。笔记侧构建产物 156 篇: 文档 109 / 来源 25 / 书籍 11 / 专题 6 / 项目 4 / 知识库 1(产物里没有 notes 这一类)。

2026-09-22 订正:本文原写的「documents 71 / notes 34」「已完成 67 条」「14 本书与 3 个项目」 是设计时的旧数,已按构建产物与 SQLite 实测值改掉;原型与后续实现以订正后的数为准。

题材与读者

  • 题材:一个人的私人档案室。产品叫 Folio(对开本、卷册、页码),数据全是「有编号的东西」—— 笔记有 Note Path,日记有日期。这不是 SaaS 控制台,是给自己每天翻几眼的册子。
  • 读者:一个人,长期重复访问,不是新用户。因此不要欢迎语、不要「空状态邀请你开始」、不要营销首屏。
  • 主任务有两个且时间尺度不同:笔记是无时间的长文(要能久读),日程是有时间的流水(要能扫读)。 多模块设计的难点就在这里,两种阅读模式必须长在同一副骨架上。

色彩

五个角色,两套主题同源:

角色 亮色 暗色 用途
#F1F3F4 素纸 #14171A 夜纸 底色,冷灰白,不是米白
#22303C 靛墨 #E6E8EA 正文与标题
#556470 淡墨 #9AA5AE 时间、计数、次要信息
#2F5D8C 靛青 #7FA8D4 可交互与「当前位置」,唯一功能色
#B4442F 朱砂 #E0705C 只用于标记今天与当前项,面积不超过千分之一

暗色不是反色滤镜,是同一组角色的另一套值——这样「亮色下白字白底」那类问题从 token 层面不可能出现。

淡墨原写 #5C6B78,在素纸上实测只有 4.93:1。2026-09-22 决定压深一档到 #556470 (5.48:1),保住下面那条 5:1,而不是把标准降到 4.5。

默认取素纸:这套视觉的论点就是「册子的纸」,默认压成暗色等于自己拆掉论点;同时系统级 prefers-color-scheme: dark 直接切到夜纸,夜纸同样是完整一套,不是附属品。排版度量(34 字一行、 各档对比度阈值)一律在素纸上校准,夜纸照同一套阈值验收。

字体

  • 正文与标题:思源宋体(Source Han Serif SCSongti SCserif)。中文长文用宋体是这套设计里 最刻意的一个选择:绝大多数中文界面默认黑体,而读 150 篇笔记的场景该有书页的质感。
  • 界面与元数据:思源黑体(Source Han Sans SCPingFang SCsans-serif)。导航、日期、列表、 档号都用它,保证小字号清楚。
  • 数字不用等宽字体,改用 font-variant-numeric: tabular-nums,日历与计数列同样对齐。
  • 字号:正文 17px / 1.9,行宽 34 个汉字(约 640px);h1 32 / h2 24 / h3 19; 元数据 13;档号 12

宋体的适用范围:只用在阅读页的正文与标题。索引、月历、任务行、档号、按钮一律黑体——小字号 在宋体下发虚,那里不需要书页感,需要清楚。先不引入中文 web font(思源宋体动辄数 MB,为一个人 的站点不划算),靠系统栈回落;因此规定正文最小 17px。若实测在 Windows 回落到 SimSun 后仍不可接受, 退路是把正文也换黑体——外壳契约与页边节律不受影响,这也是把这套设计建立在结构而非字体上的原因。

布局:外壳契约

一个外壳,四件事固定:脊(模块)→ 索引(选什么)→ 页(看什么)→ 页边档号(这是什么)。 任何新模块只允许替换「索引」和「页」两处,外壳、路由形态、导航位置一律不动。

┌──────┬──────────────────┬────────────────────────────────────────────┐
│      │                  │                                            │
│ 笔记 │  documents       │  档号   documents/06_dg板卡X5_…            │
│ 日程 │  ────────────    │  ────────────────────────────────          │
│ 任务 │  documents 109   │                                            │
│ 材料 │  来源       25   │  正文,一行三十四个汉字,宋体,行高 1.9     │
│      │  书籍       11   │                                            │
│      │  专题        6   │  次级标题                                   │
│      │  项目        4   │                                            │
│      │  知识库      1   │  再往下是段落、清单、表格、代码             │
│  64  │       300        │              余下留白                      │
└──────┴──────────────────┴────────────────────────────────────────────┘
   ↑ 脊       ↑ 索引                ↑ 页(左侧一条 72px 的页边 gutter)

整套设计里会被记住的那个元素是页边 gutter 的垂直节律:模块名、索引行、正文头上的「身份戳」 全部对齐在同一条竖线上。笔记模块这条线上是档号(路径),日程模块是日期,任务模块是状态。 它不是装饰,是每样东西的身份证;同时它让两个模块看起来是同一本书的不同分册,而不是两套界面。

模块一:笔记

  • 索引按分类分组(文档 / 来源 / 书籍 / 专题 / 项目 / 知识库),行内只有标题与 页边戳;没有卡片、没有 hover 动画,当前项左侧一个朱砂墨点。
  • 页内正文用宋体,34 字一行。现在详情头那行 path · 大小 · 更新时间中缀元数据取消, 拆成 gutter 里的两行戳(上:档号;下:更新日期)——顺带解决体检里那条「中缀加小灰字」的模板感。
  • 搜索是外壳级能力,不是模块:它替换索引的内容,不新开页面。本期它只搜笔记,所以搜索框只长在笔记索引头;要不要让任何模块都能搜笔记,留着当一条决定。

模块二:日程(2026-09-22 已实现,见文末「实现后记」)

数据怎么进 web 已经定了:本地 SQLite 是权威源,Neon 是只读投影,web 读投影(ADR-0011); 第一期不做「在网页里写日记」。

数据决定了形态:一天最多 18 条、71% 是 life、几乎没有时长信息,所以周视图时间网格是错的 (大片空白),正确形态是月历定位加当日流水。

┌──────┬──────────────────┬────────────────────────────────────────────┐
┌──────┬──────────────────┬────────────────────────────────────────────┐
│ 笔记 │ 有记录的天       │ 2026 年 6 月      78 条 / 19 天            │
│ 日程 │ ────────────     │ 日   一   二   三   四   五   六           │
│ 任务 │ 3 日      7 条   │       1    2    3    4    5    6           │
│ 材料 │ 4 日     16 条   │               19:10 开始返程通勤           │
│      │ 5 日      1 条   │               20:00 通勤中问点             │
│      │ 6 日      6 条   │               20:29 通勤节点               │
│      │ 7 日      1 条   │               还有 4 条                    │
│      │ 8 日     10 条   │   7    8    9   10   11   12   13          │
│      │ 9 日      3 条   │  11:15 生活事件                            │
│      │  ...             │   ─────────────────────────────────        │
│      │                  │  档号  2026-06-04                          │
│      │                  │                                            │
│      │                  │  6-4 周四                    16 条         │
│      │                  │  08:21  早晨通勤开始                       │
│      │                  │  09:40  早晨通勤结束                       │
└──────┴──────────────────┴────────────────────────────────────────────┘

三条由数据推出的规则:

  • 默认不给 life 上色,只给少数类型(投资、任务、会议、回顾、笔记)一个不显眼的记号。 71% 是默认值,给它上色等于给噪声上色。
  • 月历格子里放「时间 + 标题」,每格最多 3 条,第 4 行是「还有 N 条」;格子约 120px,所以 日历必须占满「页」的宽度,塞不进索引列。索引列因此改成「有记录的天」——一天一行(日期、 当天的第一条、条数),点某天,页里下半段才是当天流水(时间在 gutter,文本在页)。
  • 跨模块的桥:note 类事件指向当天写下的笔记,点过去就是笔记模块里那一篇。这是两个模块之间 唯一需要的通道,也是这套外壳值得存在的理由。第一期不做note_reference 现有 285 条 全空,没有数据支撑,等有了写入口再单独定义。

模块三:任务

tasks/ 三个文件定义:todo.mdin_progress.mddone.md。真实体量很小——活跃的只有 5 条(待办 4、进行中 1),已完成 13 条,字段是固定的四个:目标、下一步动作、截止时间、状态时间线。

  • 索引的分组换成状态(待办 / 进行中 / 已完成),与笔记模块「按分类分组」是同一手法; 已完成默认折叠,13 条历史不该占据首屏。
  • 页里一条任务就是那四个字段加一条状态时间线(开始时间、本次启动时间、完成时间), 页边 gutter 放截止或完成日期。
  • 因为体量小,这个模块不需要任何新控件:不加看板、不加拖拽、不做进度环。5 条任务配一个看板 是给自己演杂技。
┌──────┬──────────────────┬────────────────────────────────────────────┐
│ 笔记 │  待办         4  │  截止   2026-06-08                          │
│ 日程 │  ────────────    │  ────────────────────────────────           │
│ 任务 │  写技术方案    ● │  目标     完成今天需要提交的技术方案         │
│ 材料 │  安装 kimiclaw   │  下一步   对照 PRD 逐段 review 时间配置口径   │
│      │  ────────────    │  时间线   06-04 11:22 开始                  │
│      │  完成        13  │           06-08 16:43 重新启动              │
│      │  (默认折叠)     │           关联笔记 → 笔记模块               │
└──────┴──────────────────┴────────────────────────────────────────────┘

模块四:材料与来源

notes/sources/ 定义:共 19 个来源(14 本书 / 研报、2 个项目、3 个记录),每个来源下有 原始文件(PDF / EPUB / MOBI,共 120 MB)与转换出的全文(pdf_full_text.md / epub_full_text.md)、 有时还有转写(*.funasr.transcript.txt)。

这个模块要回答的不是「存在哪」,而是「谁写的」:材料里是别人写的原文,笔记里是我写的解读。 现在这两类混在同一个平铺列表里分不出来——而 25 个 sources/**/*.md、5.53 MB 全文其实已经进了 构建产物,只是没有自己的门。所以这个模块的第一期不是「把材料搬上网」,是「把它标出来并隔开」。

  • 索引按来源分组(书名 / 研报 / 项目),每个来源下列出它有的产物类型:原文、转换全文、转写。
  • 页里是转换全文(可能很长,需要页内目录),页边 gutter 放来源与产物类型。
  • 硬约束:120 MB 的原始 PDF / EPUB / MOBI 不进构建产物,函数包和仓库都不该扛这个。 页里只给原始文件的本地路径与说明,不在网页里提供原文二进制。
  • 同时把已上线的全文全文的体量当成需要交代的事:5.5 MB 正文压在一个 JSON 里,随每次部署重发。 真要优化,是把材料正文改成按需加载,而不是全量塞进首屏载荷。

窄屏

小于 900px 时:脊变成底部固定条(模块名横排、四五个字直接可见,不用汉堡菜单);索引收成页顶的 可折叠抽屉,默认只显示当前项;页边 gutter 降级为正文上方的一行档号。

自评:砍掉的默认款

对着计划又走了一遍,砍掉这些,每条都写了改成什么:

  • 「近黑底加一个亮强调色」——当下最典型的 AI 味配色,改成素纸默认、靛墨为墨。
  • 卡片套件(所有内容切成同样的圆角卡片加同一个阴影)——全部去掉,分隔只靠留白与细线, 圆角只留给按钮(2px)。
  • path · 大小 · 日期 这种中缀元数据行——拆进 gutter 的戳。
  • 「小字号加字距的灰色标签」(中文里全大写 eyebrow 的等价物)——模块名与标签改用正常 字号字重,靠位置表意。
  • 「大数字加小标签」的统计头——不要,计数并进索引头当普通文字。
  • 小数据用等宽字体——改为 tabular-nums
  • 每张卡片的 hover 过渡与入场淡入——删,只留「当前项墨点」,并尊重 prefers-reduced-motion
  • 渐变——零。

质量底线

正文对比度至少 12:1,淡墨至少 5:1,靛青至少 4.5:1,朱砂只做非文字记号(至少 3:1);键盘焦点 在两种主题下都要可见(体检里实测通过的那条焦点环要保住);亮暗两套主题各自成体系。

已定的三处

  1. 正文用宋体,但只限阅读页的正文与标题(索引、月历、任务行、档号仍用黑体;先不引入中文 web font,靠系统栈回落;正文最小 17px)。
  2. 默认素纸,系统偏好为暗色时切夜纸,两套都是完整主题;度量一律在素纸上校准。
  3. 第三、第四模块是任务与材料/来源,外壳按四个模块定版,脊宽从 56px 调到 64px。

与实施计划的关系

  • 这份设计回答「该长成什么样」,网页版前端首轮体检与优化实施计划 回答「现在哪里坏了、怎么修」。
  • 设计里的色彩与字体 token 正好覆盖实施计划 P0(亮色主题下不可见文字)与 P2(品牌面): P0 的修法就是把这五个角色落成变量,P2 的方向由这里的配色与「档号」语言给出。
  • 后续的落地文档(2026-09-22 接上):网页版多模块改版 spec.scratch/web-multimodule-shell/spec.md) 把这份设计翻成了 39 条用户故事与 11 张 ticket;外壳契约与路由记在 ADR 0009 / 0010, 日程的数据链路(SQLite 权威源 → Neon 只读投影 → web)记在 ADR 0011。设计里有歧义的地方以 spec 为准。
  • 已经拍板但本文没写全的两处:日程的月历采用原型里的 C 变体(日历在页里、索引列是「有记录的天」); 淡墨压到 #556470。理由都在 spec 与上文的订正里。
  • 新发现的一条待办(不属于原体检范围):材料模块那 5.53 MB 已进构建产物的正文,以及它和解读类 笔记在同一个列表里分不出来的问题,需要在做这个模块时一并处理。

实现后记(2026-09-22)

这份设计已经落地:外壳四段、笔记模块、日程模块都装订完了(提交 c0e3e30ccfd8b9), 设计里的四个模块有三个从「形态」变成了「能打开的页面」(任务与材料仍是诚实的占位,等各自的 spec)。 落地后与本文有出入的地方,以这里为准:

  • 日程模块不再是「未来做」:月历在页、索引列是「有记录的天」一天一行(日期、类型记号、 当天的第一条、条数),点某天在月历下方出流水,时间在页边、文本在页。数据走 Neon 投影(ADR-0011), 不做构建期产物。本文那段 ASCII 里的「3 日 7 条」那类索引行,落地后多了类型记号。
  • 今天与当前项是两个记号:今天 = 月历格子的日期数字用朱砂;当前项 = 索引行左侧朱砂墨点 + 格子底色。同时出现时互不顶掉(素纸下朱砂落在选中底色上是 4.45:1,仍在「朱砂是非文字记号 ≥3:1」 这条之内,记在 ticket 09 的 Answer 里)。
  • 窄屏只降级三处,月历格子照旧:本文只写了脊 / 索引 / 页边三处降级,实现时确认月历格子里 仍然显示事件(窄屏竖排、字号收到 10/9px),因为「格子里就是当天的事件」是这个模块的论点。
  • 本文没写、实现时必须补的一条:笔记正文里的图片。notes/documents/assets/ 下的图原来 既不在产物里也不在页面上(全是 404),现在构建期拷进 /note-assets/(5 篇笔记、33 张、5.1 MiB), 渲染侧只放行同源这些路径与 https:
  • 数据链路已通到真机:2026-09-22 跑完真机迁移与首次投影(cd67238),285 条事件落地 Neon, 2026-06 是 78 条 / 19 天、06-04 是 16 条,幂等重跑零变更;本地日志里一条历史 bug 写坏的 end 时间戳被丢弃(事件照常落地)。月历与当日流水在本地夹具里读的就是这份真数据。
  • 还没定的一件:「搜索是不是每个模块都能用」——本期搜索只搜笔记,搜索框只在笔记索引头。

日程模块的第二轮:网页与飞书日历同源(2026-09-22)

触发是一条具体的错位:网页日程里 09-19 之后什么都看不到,飞书日历上却有一片。查下来不是投影坏了, 是两个目的地各写了一套「什么算一条日程」的规则——网页那份还多一条 sensitivity 过滤,且根本没有笔记线。

  • 取合规则只有一份了:本地视图 calendar_entriesscripts/calendar_entries.py)把事件线与笔记线 统一成同一形状,kind 区分 daily / note;飞书日历与网页都是它的兄弟投影,见工作区 ADR 0010。 条目范围 = 现在合规 ∪ 已经发出去过(台账 target_id 非空)。后半句是这次的要点: 日历上的内容是「写出去的历史」,不是「现在推导出来的集合」——本地 2026-06 那 78 条事件 calendar_enabled 现在全是 0,只按「现在合规」取,网页永远对不上。
  • 网页现在两条线都显示:月历格子上笔记条目有自己的记号(mk-note,与投资/任务/会议/反思并列), 日流里笔记条目带来源标记「笔记」、点它跳到笔记模块(?n=<Note Path>)。图文形态见 .scratch/web-calendar-parity/assets/07/
  • 历史条目回来了:老账 config/feishu_calendar_sync.json 里的 event_id 一次性回填进台账 (224 条命中),带 target_id 的行从 39 涨到 263,2026-06 那一批因此重新出现在网页上。
  • 同源是可以证明的python3 scripts/calendar_parity_report.py 只读地拉一次日历对差。 2026-09-22 实测:本地 372 / 日历 386 / 对上 371(台账 306 + 内容 64 + 描述键 1), 「只在日历」15 条全部有解释(11 条白名单 + 4 条重复),「只在本地」1 条是本地重复行。
  • 两笔已知账:65 条老账的 event_id 已经过期(记录还在日历上,只是事件被重建、id 换了), 报告单列成 id_stale;19 条「本地 00:00 / 日历 12:00」是旧脚本对「没有时刻」的条目的兜底, 这一轮不统一(改了就是改历史)。两者都不影响网页上看到的内容。
  • 一条口径决定:笔记线只投 active,不做 sensitivity 过滤——109 篇里 106 篇是 private, 而笔记正文本来就整篇进构建产物(OAuth 之后),网页与博客两侧口径一致;日线仍然保留 active + 非 private
  • 不撤回:条目一旦发出去就留着(用户 2026-09-22 定)。这一轮不实现任何删除日历的规则, 除了「withdrawn 的笔记不算在集合里」。