--:--:--
左侧选择笔记,或新建 / 上传

THE PERSONAL JOURNAL OF JIANGANG KONG

Notes on an
unfolding life.

一些思考,一些尝试。
还有尚未定稿的生活。

关于技术、语言与日常的个人记录。

A landscape, always becoming.

Collected here.

记录、实践,以及时间留下的东西。
Jiangang.A personal collection. An open ending.
关于本站与设计规约 +

关于本站

一处持续生长的个人空间,连接知识、工具与生活。

视觉 v3 · 暖纸白 / 炭黑 · 陶土色与矿物蓝 · 衬线排版与动态地景

视觉 v3 · 当前规范

共享视觉令牌采用暖纸白、墨色文字与陶土色强调;首页以衬线大字、流动地景与目录式布局组织内容,工作区以读数与阅读效率为先。导航提供主页、笔记、资产总览,以及登录后可见的雅思口语教练。默认采用纸白主题,保留深色偏好。动态支持暂停与系统减少动态设置。下方 v1 实现记录供维护参考;视觉数值以当前共享样式为准。

展开 v1 设计与实现记录

一、这份规约是什么

  • 给谁看:给未来改这个站的人(很可能是几个月后的自己)。所以写的是「为什么这么定」,不只是「是什么」。
  • 效力:新增 UI 先查这里有没有现成模式;没有就先把模式定下来、写进这一页,再写代码。
  • 维护:规约与代码不一致时必须改这一页(或改代码)。第十四节专门记录已知的不一致项,别让它们烂在那儿。
  • 怎么用:改样式看第四节(令牌)与第五节(布局),写组件看第六节(组件)与第七节(状态),收尾看第十三节(自检清单)。

二、设计原则

  • 读数优先:这是给自己看的资产仪表盘。数字要一眼能读、能横向比较,装饰永远让位于可读性。
  • 一套语言,不留特例:颜色、字号、圆角、间距、图标都取自固定几档。遇到「就这一处特殊」时,先怀疑是不是该改整体。
  • 深浅两套都要成立:任何颜色都走变量,改完两边都看一眼。默认浅色。
  • 状态要看得见:悬停、选中、聚焦、拖拽、空、加载、失败 —— 七种状态都要有明确视觉,不能靠猜。
  • 不拿可访问性换观感:焦点环、对比度、触控目标尺寸是硬约束,风格让步。
  • 改动可验证:涉及数据读写的改动,先在隔离环境验证,不拿真实数据试(见第十三节)。

三、信息架构

Tab回答什么问题核心内容
我和本站这个站怎么做的、怎么改本页:设计规约
笔记我要记什么左侧目录(文件树 / 大纲)→ 右侧正文(预览 / 编辑)
资产总览我有多少钱、怎么配的、往哪走总资产 → 资产配置(环形图 + 图例)→ 持仓明细 → 年被动收入 / 总资产走势
  • 页面骨架统一:站点头部(标签页 + 时钟 + 站点级控件)→ 内容区。站点级控件在任何 tab 都可见可用。
  • 纵向顺序=重要度:每页最重要的数字放最上面(资产页是总资产,笔记页是当前笔记标题)。

3.1 可见性(谁能看到什么)

内容未登录已登录
资产总览(数据)登录卡片完整仪表盘
笔记(目录与正文)可读可读
笔记(新建 / 编辑 / 删除 / 移动)拦截并提示去登录可用
我和本站(本页)可读可读
  • 登录入口只有一个:在「资产总览」tab 里。未登录时该 tab 显示登录卡片,登录后整站所有编辑操作一起解锁。
  • 登录方式:config/users.json 里的密码 + Google Authenticator 动态码(验证器账号在 config/totp.json)。动态码每 30 秒更新,同一码不能重复使用;验证器设备丢失时可用一次性恢复码。
  • 会话规则:单台设备登录后 7 天内免登录(HttpOnly Cookie,服务重启不失效);同一站点最多同时登录 3 台设备,超出时自动挤掉最久未使用的那台。
  • 未登录的编辑入口不隐藏、而是给出指引:点新建/编辑会弹出「需要先登录 + 去哪登录」,而不是静默失败或点了没反应。

四、设计令牌

色卡的色值实时读取当前主题的 CSS 变量,切换深浅色会自动更新;其余数值为当前代码实测值。本页示例数字均为虚构,不代表真实资产。

4.1 色彩

  • 语义色只有三个:--up 红(涨 / 支出 / 风险)、--down 绿(跌 / 收益)、--flat 灰(持平)。本站在涨跌上用的是红涨绿跌(A 股习惯),不要按欧美习惯改成绿涨红跌。
  • --s1…--s4 是四类资产(加密货币 / 股票 / 现金 / 房产)的辨识色,只用于图表、圆点和进度条;--g1…--g4 是配套的辉光色,成对使用。
  • 文字三层就够:--ink 主文字、--ink-2 次文字、--muted 弱化说明。不要再加中间灰。
  • 描边一律用半透明的 --border,不要用实色灰 —— 半透明在深浅两套底色上都能自然融合。
  • 强调色固定为 macOS 系统蓝(深色 #0a84ff / 浅色 #007aff),不提供切换开关,视觉语言统一优先。需要强调色的地方一律 var(--accent),不要写死。
  • 需要带透明度的强调色辉光时用 rgba(var(--glow-rgb), α) —— --glow-rgb 是 --accent 的 RGB 分量形式(rgba() 不认十六进制变量)。

4.2 字体

¥128,064.50L1 · 42px · 焦点数字(总资产),JetBrains Mono 600
页面与文档标题L2 · 26px · 页面 / 文档大标题,Inter 700
正文一级标题L3 · 22px · 正文 h1 / 文档 h1 / 弹窗标题
正文二级标题L4 · 18px · 正文 h2 / 文档 h2(带下边框)
面板标题与正文L5 · 15px · 面板标题 / 正文 / 标签页 / 文档 h3
控件文字与文件名L6 · 14px · 控件、表格、正文 h4
次要信息(用得最多的一档)L7 · 13px · 次要信息 / 代码块 / 正文 h5
弱化说明与图表轴L8 · 12px · 弱化说明 / 图表轴标签 / 正文 h6
徽章与极小标注L9 · 11px · 徽章 / 极小标注
  • 字体栈顺序有讲究:--font-sans 把自托管的 Inter 放最前,后面才是 system-ui / -apple-system / PingFang SC。Inter 只含拉丁字母和数字,汉字会自动落到系统字体(Mac 苹方 / Win 雅黑 / Android Roboto)—— 中文在各平台都是原生观感,也不必打包几 MB 的中文字体。
  • --font-mono 是自托管的 JetBrains Mono,只给金额、涨跌幅、时钟和代码块用。
  • 数字必须等宽 + tabular-nums,上下几行的金额才能对齐比较。这是本站最不能破的一条。
  • 行高:正文 1.5、文章预览 1.75、编辑器 1.7。不要用 line-height: normal。
  • 字重只用 400 / 600 / 700 三档,不用细体(可读性差)。
  • 字号只有这 9 档。新增文字时挂到最接近的一档,不要新造 16px、17px 这类值。浏览器给 button 的默认值(13.33px)也算离群值 —— 自己设过的控件要显式写 font-size。
  • 编辑器与预览的字号层级必须一致:Vditor 默认 16px 基准、标题按 em 派生(h1 会到 28px),与预览完全是两套,所以 styles.css 里显式覆盖了编辑器正文与 h1–h6 的字号。

4.3 间距与栅格

  • macOS 的布局栅格是 4pt,本站取它的一半:2pt 栅格(信息密度优先)。所有 padding / margin / gap 只用 2 的倍数。
  • 唯一例外是 1px —— 只留给细描边和代码块内边距,不当作间距用。
  • 常用档:2 / 4 / 6 / 8 / 10 / 12 / 16 / 20 / 24。出现 5px、7px、11px 这类值视为样式缺陷。

4.4 圆角 · 描边 · 阴影

用途值例子
装饰条 / 微徽章2px标题级别徽章、引用条
小控件 · 内联元素4px行内代码、图例行与圆点、仪表框、拖拽手柄
按钮 · 行 · 中间层6px按钮、目录树行、菜单项、站点控件、滚动条滑块
容器8px面板、卡片、正文区、编辑器、笔记侧栏、代码块
浮层10px弹窗、下拉菜单、浮动工具栏
  • 描边:全站 1px var(--border);选中态用 2px var(--accent)(标签页下划线、文档块选中)。不要用更粗的边框做强调。
  • 阴影只用于浮层,且是「细描边 + 柔投影」两段式(--shadow-pop)。平面内容不用阴影,靠底色与描边分层。
  • 全站唯一的辉光是总资产数字的 text-shadow —— 只给这一处,因为它全站只有一个。

4.5 动效

  • 过渡统一 0.2s ease-out(快起缓停,接近 macOS 手感)。只做颜色、描边、阴影、透明度、位移,不做缩放弹跳。
  • 拖拽 150ms;prefers-reduced-motion 下全部关掉(含扫描线与脉冲)。
  • 循环动画只允许两处装饰(雷达扫描 7s、刷新按钮 0.6s),且不能干扰读数。

4.6 层级(z-index)

层值内容
背景0点阵与辉光背景层
内容1页面主体
悬浮信息10图表 tooltip
页内浮层40新建菜单
模态100弹窗遮罩与弹窗
编辑器浮层200斜杠菜单、类型菜单、选中工具栏

新增浮层请套用上表已有的层,不要随手写 999。

五、布局与响应式

  • 版心铺满视口:.console 不设 max-width,内边距 24px。三个 tab 用同一套宽度 —— 所以三个 .tab-panel 必须都放在 .console 里面(2026-10-11 修过一处:「我和本站」的面板曾被落在 .console 外面,于是它既通栏贴边、左边界为 0,又比另两个 tab 多出 40px 顶距 —— 就是 .console 自己的 padding-bottom)。窄屏(≤600px)内边距整体收到 12px(上下左右都是)—— 桌面那 24px 是版心留白,手机上等于白扔掉近 1/5 的宽度(实测:390px 的屏上,词条英文原先要从屏幕左缘 36px 才开始、按钮离右缘 40px);桌面仍是 24px。标签行到内容的距离也由 18px 收到 8px(三个 tab 一致;量出来 tab 行底边到内容是 9px,多出的 1px 是 .topbar 的分隔线本身)。笔记正文的留白收在一个变量里:--notes-pad(桌面 12px 16px、窄屏 12px 12px)。阅读区 .notes-article、飞书文档编辑器 #doc-editor、Vditor 的正文容器三处必须共用它 —— 只改一处,只读↔编辑切换时正文就会横向跳动、宽度缩水(原来靠三处各写一遍 14px 18px 加注释维系,2026-10-11 收成一个变量,顺手把越档的 14/18 换成档内的 12/16)。窄屏为什么要再收到 12px:同一目录里 html 笔记的正文离屏幕左缘 24px、fdoc 却是 31px,来回看就显出来了,收到 12px 后两者对齐。
  • 两栏页只给侧栏封顶:笔记页侧栏 14.45%、最小 145px、宽屏上限 272px;正文吃掉剩下的全部。
  • 高度用视口算:笔记区 calc(100vh - 128px),移动端 calc(100dvh - 118px)(dvh 才能躲开手机地址栏)。改头部高度必须同步改这两个数。
  • 移动端不用抽屉:侧栏变成顶部一整块,可整块收起,把高度让给正文。
断点改什么为什么
≤ 1199px年被动收入 / 总资产走势 由并排改上下走势图等比缩放,再窄坐标轴文字就不可读
≤ 1049px持仓卡片由三列一行改单列三列时每张放不下四列表格,会横向溢出
≤ 780px隐藏头部时钟给标签页与控件让位
≤ 600px笔记页改上下堆叠;目录树 / 大纲高度上限 24vh;目录可整块收起;文档编辑器隐藏块手柄触屏没有 hover,且高度是最稀缺资源
≥ 601px笔记侧栏宽度上限 272px避免超宽屏下侧栏吃掉正文

六、组件

主按钮 .btn-primary
次按钮 .edit-btn
危险 .edit-btn.danger
图标按钮 .ctl
  • 按钮只有三种:主按钮(强调色实底 + 深色字,一屏最多一个,用于确认)、次按钮(无底色 + 1px 描边 + 强调色文字)、图标按钮(32×32 方格,只放图标)。不要自创第四种。
  • 面板 .panel:--panel 底 + 1px 描边 + 8px 圆角 + 20px 22px 内边距,块间距 14px。仪表类内容都装在面板里。
  • 侧栏是一张圆角卡片:8px 圆角 + 1px 描边 + 毛玻璃底 + 10px 8px 8px 内边距,与右侧正文卡片同规格,靠 16px 间距分隔(不画分隔线)。
  • 侧栏自上而下:工具栏(新建 / 新建文件夹 / 上传,右侧是视图切换与收起)→ 分组标题(文件 · N 项,切大纲时变 大纲 · N 项,带分隔线)→ 列表。三者共用同一套左右内缩,所以选中色块左右对称。
  • 选中态用实心药丸:强调色填充 + 白字白图标(macOS 侧栏的选中样式),与 --panel-2 的悬停态一眼分开。
  • 层级要有引导线:嵌套列表左侧一条 1px 竖线(位置由 JS 写入 --guide-x 对齐父级箭头)—— 只靠缩进很难看出从属关系。
  • 图标只有一套:线性、stroke-width: 1.3、stroke="currentColor"、16 视框,定义在 app.js 的 ICON_*。不要用 emoji 当图标 —— emoji 的字形与粗细由系统决定,跨平台会走样。区分格式靠颜色(飞书文档用品牌蓝 #3370ff),不靠换形状。
  • 浮层用毛玻璃材质:侧栏、下拉菜单、浮动工具栏、弹窗用 --material + backdrop-filter;不支持毛玻璃的浏览器自动退回不透明底色(已写 @supports 兜底)。
  • 标签页 .tab:文字按钮,激活时底边 2px 强调色。新增 tab 只要加 data-tab="名字" 与对应 #tab-名字 面板,切换逻辑自动接管。
  • 滚动条全站统一:用 scrollbar-width: thin + scrollbar-color(轨道透明、滑块半透明),宽度交给各引擎自己的 thin 档,不要再写死像素宽或在局部另写一套。

七、交互状态(七种都要有)

状态表现本站实例
默认——
悬停底色 --panel-2,或描边转强调色目录行、图例行、次按钮
选中 / 当前列表用强调色实心药丸 + 白字;按钮用 2px 强调色描边当前笔记、正在编辑的按钮
键盘聚焦3px 强调色光晕(--focus-ring),全站统一所有可交互控件
拖拽中被拖项半透明跟随光标;悬停到折叠文件夹时自动展开目录树拖拽、文档块排序
空居中说明 + 下一步怎么做,不是一行灰字「还没有笔记 / 点左上角 + 新建,或用 ⬆ 上传 .md」
加载 / 失败加载给文字提示不白屏;失败弹窗说清原因(含 HTTP 状态)「编辑器加载中…」;保存失败弹窗

八、无障碍

  • 焦点环是硬要求:任何可聚焦元素在键盘聚焦时都必须有 --focus-ring 光晕,新组件要按 Tab 键确认一遍。
  • 对比度:正文与背景 ≥ 4.5:1;--muted 只用于非关键信息。
  • 不用颜色单独承载信息:涨跌除了红绿还有 +/− 号;文件格式除了颜色还有图标与文字。
  • 触控目标:可点区域不小于 32×32(站点控件正好 32,目录行 33 高)。
  • 图标按钮必须有 title + aria-label,纯装饰图标加 aria-hidden。
  • 目录树支持键盘操作:Tab 进树、↑↓ 移动、←→ 收起/展开(→ 在已展开的文件夹上进入首个子项)、Enter 打开、Home / End 跳首尾。树的 DOM 重建后要恢复焦点(见 renderTree() 里的 hadFocus)。
  • 尊重 prefers-reduced-motion:动画在系统「减少动态」时全部关闭。

九、内容与文案

  • 中文优先;中英混排时数字与英文用半角、两侧留一个空格(如「更新于 2026-09-25 14:46:58」)。
  • 金额格式:人民币 ¥128,064.50(千分位 + 两位小数,走 Intl.NumberFormat("zh-CN"));美元同理;汇率类加 ≈ 前缀。
  • 按钮用动词:编辑 / 完成 / 删除 / 上传 / 导出。不要用「确定 / 提交」这类没有信息量的词。
  • 空态写「下一步」:说清为什么空、怎么才能不空。
  • 错误说清三件事:发生了什么、为什么、怎么办。例如「保存失败:文档结构错误(HTTP 400)」。
  • 术语一致:站内统一用「笔记」(markdown)、「飞书文档」(块文档)、「目录」、「大纲」,不要混用「文件夹 / 目录 / 分类」。
  • 公开页面一律脱敏:笔记与「我和本站」对所有人可见,因此不得出现真实金额、持仓、账号。示例数字必须虚构(例如用 ¥128,064.50),截图同理。

十、与 macOS 的对齐(哪些跟、哪些不跟)

视觉语言以 macOS 人机界面指南为参照,但不是照搬:凡是会伤害信息密度或可访问性的,一律不跟。

项macOS 做法本站
布局栅格4pt / 8pt 栅格跟,取一半:2pt 栅格(信息密度优先)
字体SF Pro / SF Mono,正文 13pt跟观感:Inter + JetBrains Mono 自托管;正文 15px(比 macOS 大一档,读数字更舒服)
中文字体苹方跟随系统:打包字体不含汉字,Mac 上就是苹方
主题默认跟随系统外观不跟:本站固定默认浅色(用户切过之后记住选择)
键盘焦点环所有控件都有跟:统一 --focus-ring 光晕
浮层阴影细描边 + 柔投影跟:--shadow-pop 两段式
材质 / 毛玻璃侧栏与菜单半透明模糊跟:--material + backdrop-filter
选中样式侧栏选中是强调色实心圆角块跟:列表选中用实心药丸 + 白字
滚动条浮层细滚动条跟:全站统一透明轨道 + 内缩滑块
圆角窗口约 10px,连续曲率(squircle)跟尺寸(4–10px 分档);曲率不跟 —— CSS border-radius 只能画圆弧
强调色跟随系统 controlAccentColor跟:固定系统蓝,不提供切换(统一优先)
图标SF Symbols跟规范(等粗笔画 + currentColor + 16 视框),但不是 SF Symbols 的图形
鼠标指针按钮上仍是箭头不跟:网页里按钮用手型,用户有预期
涨跌色语义色不分涨跌不跟:红涨绿跌是 A 股习惯
信息密度桌面控件偏小巧不跟:行高按手指点选设计,触屏也能用

十一、交互实现约定(都是踩过坑的)

  • 主题必须在 <head> 内联脚本里先定好(见 index.html / login.html),否则会先渲染默认色再跳成用户选的色、闪一下。
  • 目录树里的点击一律用 pointer 事件(见 onTap()),不要用 click —— SortableJS 拖拽结束后会用捕获阶段的监听吞掉紧接着的第一次 click,表现为「拖完点不动」。
  • 拖拽统一用 SortableJS,且必须 forceFallback: true(原生 DnD 在嵌套列表里不工作)+ fallbackOnBody,触屏再加 delay: 150 防误触。放置目标用 document.elementFromPoint() 判定,不要只信 evt.to。
  • 主题切换要一次性同步三样:vditor.setTheme(编辑器主题, 内容主题, 高亮主题, 内容主题路径)。Vditor 没有 setContentTheme 这个方法 —— 只传第一个参数会让正文颜色留在旧主题上(深色模式变成深字配深底,正文直接看不见)。
  • 自动保存:编辑停止 2 秒后静默保存;点「完成」立即保存并回到预览。只进编辑态、没输入不触发保存。
  • 中文输入法:所有 keydown 处理前先判 isComposing,合成期间不切块、不重渲染。
  • 飞书文档的代码块不显示预览视图,常显可编辑源码 —— 否则展开时源码与预览会同时渲染,代码显示两遍。

十二、数据与接口

格式文件内容
一篇笔记<名字>/<名字>.mdMarkdown 正文,用 Vditor 编辑、marked.js 预览
一篇飞书文档<名字>/<名字>.fdocJSON:{ version, title, blocks[] },块编辑器自研(www/doc-editor.js)
一篇网页笔记<名字>/<名字>.html整份自包含 html(自带 CSS/JS),在沙箱 iframe 里只读渲染,可做交互;站内不提供源码编辑
该笔记的资源<名字>/image.png与正文同一个目录,正文只存 /media/<笔记路径>/<文件名>。目录里只有这篇笔记的东西
(网页笔记)读音文件<名字>/audio/<单词>.mp3子目录不会出现在目录树里(笔记目录不再往下展开),由生成脚本下载。为什么不用浏览器自带的语音合成:它读的是各设备自己装的语音包(iPhone 是 Apple 语音、Windows 是微软语音、安卓是 Google 语音、Linux 常常一个都没有),同一个词四端四个声音。放成固定文件,四端听到的才一样、离线也能用;万一取不到文件,页面会退回语音合成兜底

块类型:text 正文 · h1/h2/h3 标题 · bulleted 无序 · numbered 有序 · todo 待办(带 checked)· quote 引用 · callout 高亮块(带 icon)· code 代码块(纯文本存 text)· divider 分割线 · image 图片(存 url + alt)· table 表格(存 rows 二维数组 + header)。行内格式(加粗 / 斜体 / 删除线 / 行内代码 / 链接)存为白名单 HTML。

  • 一篇笔记一个目录:正文与它的资源同在一个目录里,目录里只有这篇笔记的东西。data/notes/周报/W4-Sep-2026/ 下是 W4-Sep-2026.md + image.png。同名文件自动加序号(截图.png → 截图-1.png)。
  • 目录树怎么区分笔记与分组:目录里有与目录同名的笔记文件(<目录名>.md / .fdoc / .html)→ 显示为一篇笔记;否则是分组文件夹。所以不需要额外的「资源目录隐藏」规则。
  • 移动笔记搬整个目录,正文与资源一起走,图片不会断链;同时会重写正文里的 /media/<旧路径>/ 前缀。
  • 删笔记连同目录一起删(正文 + 资源)。只删其中一种格式的文件时不要动目录 —— 否则目录会以空文件夹的样子冒回目录树里。
  • 每篇笔记有自己的 URL:/note/<笔记路径>(如 /note/经历领悟/判断)。站内点链接不刷新页面、浏览器前进后退可用、链接可直接发给别人。后端把 /note/* 交给前端按路径打开。注意:页面里引用静态资源必须写绝对路径(/app.js 而不是 app.js),否则深层 URL 下相对路径会解析错。
  • 内部链接语法 [[笔记路径]]:markdown 与飞书文档都支持(飞书文档在渲染时统一转换,所以已有内容里的 [[…]] 也会自动变成链接);带别名写 [[路径|显示文字]]。渲染成 <a href="/note/路径">,点击在站内打开目标笔记。编辑态里点链接不跳转(那是在改链接文字)。
  • 跳到某篇笔记的某个小节:[[路径#小节]]「#」后面是那篇笔记里的标题文字(不是编号),打开后自动滚到那一节。URL 形如 /note/<路径>#<小节>,可直接分享/收藏;找不到同名标题就停在文档顶部,不报错。别省显示文字——不带 |别名 时,带小节的链接显示的就是小节名(如 [[个人知识库/哲学#延异]] 显示「延异」),这正是概念表里想要的效果。阅读态标题会被写上 id(slug = sec- + 百分号编码的标题文字),fdoc 与 markdown 两条渲染路径共用同一套规则;同一篇里标题文字重复时只有第一个能被锚点命中。
  • 句子朗读按钮:markdown 笔记里写 <button class="say"></button>,预览态会自动补上喇叭图标(图标由 app.js 的 initSpeech() 注入,源文件保持干净)。点一下用浏览器自带的语音合成朗读,再点一下停。读什么:优先 data-say="…";没有就取所在块的文字,并自动跳过 .zh(中文译文)和按钮本身 —— 所以一句英文只写一遍,译文包在 <span class="zh">…</span> 里即可。音色优先挑英音(en-GB),语速固定 1 倍速(不提供调速,听的就是考试语速)。实例见 IELTS/listening。只在预览态生效,编辑态看到的是源码(Vditor 保存往返不会弄丢这段 HTML)。
  • 网页笔记(html):正文是一整份自包含 html(自带 CSS/JS,只用浏览器原生能力、无外部依赖),在阅读区用一个 sandbox="allow-scripts" 的 iframe 渲染 —— 不给 allow-same-origin,所以它碰不到站点页面。站内只读(不提供源码编辑,改内容就改文件),因此也没有站内大纲。iframe 与站点只通过 postMessage 来往:{type:"wordlist-mark", word, value} 上行(只报「哪一个词改成了什么」,不报整张表),由 /api/notes/marks 把正文里的状态块 <script type="application/json" id="marks">…</script> 就地改掉;{type:"note-theme", value} 与 {type:"note-auth", authed} 下行(换主题只通知、不重建 iframe,免得丢掉滚动位置和正在放的声音;登录状态另见下条)。为什么是「只改那一块」而不是整篇保存:浏览器手里的正文副本可能是旧的(笔记更新后那个标签页一直没关),整篇写回会把笔记回退到旧版本 —— 点一下「熟悉」就把整篇笔记还原,实测踩过这个坑。独立打开这个文件也能用(此时标记存在 localStorage)。未登录的访客只给朗读、不给标记按钮(登录状态由父页面写进 <html data-canmark>,首屏就正确,不会先闪出按钮;中途登录/注销用 {type:"note-auth"} 补正)—— 访客仍能按分类浏览笔记里已有的标记。实例:IELTS/奶酪单词(2232 词的词表,由 tools/词表生成/ 的脚本从 PDF 生成,读音由 fetch_audio.py 预下到同目录的 audio/)。
  • 概念表的标准形态:一张长表——列固定为「根域 | 一级范畴 | 类别 | 概念 | 概念数」,一个类别一行,概念用 [[个人知识库/<一级范畴>#<概念名>]] 平铺罗列。类别按研究领域分(以哲学为例:形而上学与本体论、认识论与逻辑、语言与心智、伦理学、政治哲学、美学、科学哲学与宗教),再按「学派与立场 / 人物 / 著作」组织——不要用「概念簇 A / B / C」这种无意义的编号。同一类别下若还有子维度(如人物的时期),不另起一行,在「概念」格里用 <b>标签(条数)</b> 加分号换行 <br /> 分段,例:人物 一行里分「古希腊与希腊化 / 中世纪至近代早期 / 近代 / 现代与当代」四段。总表只是索引:概念的解释(释义)写在「个人知识库/<一级范畴>」那篇 fdoc 里,一条概念一节,小节标题就是概念名(所以总表的链接能靠锚点落到那一节),每节固定两块:先说一句大白话,再放原释义。实例:个人知识库/PKO 是总表,个人知识库/哲学 是哲学这一范畴的释义。根域、一级范畴这类逐行重复的列只在第一个数据行写值、其余留空,表格块加 "merge": true 后阅读态会自动把空格并进上一格(rowspan);编辑态不合并,模型里始终是完整矩形 —— 否则 syncBlock 靠 data-r/data-c 逐格读回时会串行。
  • 两种笔记都支持图片:markdown 里粘贴/拖入即成 ![名字](/media/…);飞书文档里粘贴会插入一个「图片块」(斜杠菜单或块类型菜单里选「图片」也可以),存的是 {type:"image", url, alt}。图片地址只允许 /media/… 与 http(s),data: 内嵌会被丢弃。绝不要用 base64 内嵌图片 —— 一张 800KB 的图内嵌后正文变 1.1MB,实测让「进入编辑」从 150ms 涨到 3.5 秒。
  • 两种格式混在同一个目录树里,同一目录禁止同名(周报.md 与 周报.fdoc 会撞路径)。
  • 路径安全:后端 _safe() 把 / \\ . : 等字符替换成下划线,目录分隔只认 /,写文件一律限制在 data/notes/ 内。
  • 飞书文档写入时校验 JSON 合法性(必须有 blocks 数组),不合法返回 400,不会写坏文件。
  • 笔记区的高度不要写死像素:正文要占满「视口 − 顶部实际占用」。做法是把 body[data-tab="notes"] → .console → #tab-notes → .notes-layout 串成一条纵向 flex 链(styles.css 里 .notes-layout 下方那段),高度由 flex 分配。早期这里写的是 calc(100vh - 128px)(窄屏 - 118px):顶部实际只占约 80px,于是底部白白空出 48px(手机 38px),看着铺不满,换机型/转屏就更不准。三个别踩的坑:① .console 必须显式 width: 100% —— 它有 margin: 0 auto,成为 flex 项目后自动外边距会吃掉两侧空间、把宽度收缩成内容宽度;② body 要 min-height: 0 —— 基础样式里的 min-height: 100vh 在 iOS 上是「地址栏收起时」的高度,会把 100dvh 顶大、正文反探出屏幕;③ 只挂 body[data-tab="notes"] 下面,资产总览 / 我和本站两个 tab 的整页滚动不能受影响。
  • 删除任何一篇笔记(md / fdoc / html)都要在弹窗里再输一次密码 + 验证器动态码(#del-modal,走 /api/notes/delete):删除不可撤销,正文与该笔记自己的图片会一起删掉,所以除登录外再输一次密码和当前动态码(与改资产、改预期同一套路)。密码或动态码错就留在框里提示,不会关掉让你重来。登录与操作分别记录已用过的步数,同一个码不能在操作里重复使用;删除接口的试错与登录共用同一套节流。
方法路径可见性说明
GET/api/session公开当前是否已登录、会话绑定的用户名 + 已登录设备列表
POST/api/login · /api/logout公开密码 + 验证器动态码登录(设 7 天 HttpOnly Cookie)与退出
GET/api/notes/tree公开目录树,每个笔记带 kind(md / doc / html)
GET/api/notes/content?path=&kind=公开读单篇笔记内容
POST/api/notes/save / mkdir / move需登录写、建目录、移动,请求体都带 kind
POST/api/notes/delete需登录 + 密码 + 验证器动态码删除不可撤销(正文连同该笔记的图片一起删),所以除登录外还要再输一次密码和当前动态码(password、code 字段),试错与登录共用同一套节流
POST/api/notes/marks需登录网页笔记的「难点 / 熟悉」标记:{path, word, value}(value 0 取消 / 1 难点 / 2 熟悉),只改正文里的状态块,其余字节不动
POST/api/notes/asset需登录上传图片(base64 JSON),存进该笔记的资源目录,返回 /media/… 链接
GET/media/<笔记路径>/<文件名>公开读笔记资源(图片 + 音频),文件名不可变可长期缓存。MIME 表是 notes.SERVE_EXT:比 IMAGE_EXT 多 .mp3/.wav —— 后者管的是「上传只允许图片」,两者别混用
GET/note/<笔记路径>公开笔记的独立 URL:返回前端,由前端按路径打开对应笔记(可分享)
GET/api/portfolio需登录汇总数据(总资产、配置、年被动收入);带 ?force 跳过缓存
GET/api/assets · /api/history · /api/projection需登录手工维护的资产、月度快照、未来预期
POST/api/assets / update / delete · /api/projection需登录 + 密码 + 验证器动态码增删改资产与未来预期

页面与静态资源全部公开;笔记只读公开、写需登录;资产类接口全部需登录。登录只用 config/users.json 的密码 + Google Authenticator 动态码;资产编辑与删除笔记在登录之外还要再输一次密码和当前动态码(防止设备被临时借用时误改)。会话存 data/auth/sessions.json,服务重启不失效。

十三、目录结构与交付前自检

MY_PROFILE/
├─ server.py            # 路由 + 鉴权 + 静态文件(systemd 跑在 80 端口)
├─ backend/             # auth / assets / history / projection / portfolio / notes
├─ www/                 # 前端(无构建,直接跑)
│  ├─ index.html        # 三个 tab:我和本站 / 笔记 / 资产总览
│  ├─ login.html        # 登录页(独立页面,自带主题预置脚本)
│  ├─ styles.css        # 全部样式与设计变量
│  ├─ fonts.css         # 自托管字体 @font-face
│  ├─ app.js            # 全部前端逻辑
│  ├─ doc-editor.js     # 飞书文档块编辑器
│  └─ vendor/           # vditor / marked / sortable / fonts(均自托管)
├─ data/                # 真实数据:assets.json、history.json、projection.json、notes/
└─ tests/               # test_notes.py(后端单测)、serve_test.py(:8081 隔离测试服务)

交付前自检 —— 改完样式至少过一遍:

  • 深浅两套主题各看一眼;颜色全部走变量,没有写死色值。
  • 间距是否都是 2 的倍数;圆角是否取自 4.4 的档位。
  • 新交互元素按 Tab 有没有焦点环;图标按钮有没有 title / aria-label。
  • 1440 与 390 两个宽度各看一眼,断点行为符合第五节。
  • 触控目标 ≥ 32×32,手机上能否单手点中。
  • 数字是否等宽对齐。