8.0 KiB
阅间 Demo 设计与数据流
页面分工
统计并入阅读概览;导航保留阅读概览、我的书架、阅读目标、笔记与划线、发现好书。概览展示阅读统计和目标摘要,目标页管理多份具体阅读清单。
完整功能、业务规则、接口落点与状态见 功能目标清单。该清单先于本轮实现建立,后续代码开发继续追加和更新。
该设计探索已归档,当前正式看板由根目录 Flask 服务提供。
数据流
demo-data.js 提供样例书籍、书单及初始目标。app.js 根据当前页面和筛选条件渲染内容;用户输入写入目标草稿,验证通过后保存到浏览器,再更新目标列表。现有 Flask 服务和数据采集代码不参与本轮 Demo。
书籍卡片打开详情;详情页可选目录、个人笔记、热门划线、点评、相似书。个人笔记与公开点评使用不同数据集合。热门划线中的想法继续打开单条详情。
书籍、书单详情均可加入已有目标,也可带着已选内容创建新目标。
阅读目标口径与例子
目标字段:id、name、start、end、ids、sources。ids 存放去重后的书籍 ID;sources 记录曾加入的书单名称,作为来源说明,不表示持续关联。
例:单独选择已读完的《活着》,再加入包含《活着》《人生海海》《悉达多》的书单。保存后总数为 3 本、已读完为 1 本,进度为 33%。从目标中移除《人生海海》后,进度为 1 / 2,即 50%。
完成状态取书籍当前已读状态,不受目标起止日期约束。同一本书可计入多个目标。同一目标内不重复计数。
状态判断先检查是否全部读完,再检查日期。例如目标时间为未来一周,但其中两本书都已读完,直接显示已完成;尚未全部完成时,按日期显示未开始、进行中、已到期。
书单采用加入时的书目,不自动跟随外部增删。移除目标内一本书,只影响这个目标,不改变书架或其他目标。
实现位置
| 职责 | 位置 |
|---|---|
| 去重与完成率、状态 | app.js:uniqueIds、goalMetrics |
| 新建、编辑、书单混选 | app.js:openGoalEditor、renderPicker、saveGoal |
| 加入已有目标 | app.js:chooseTarget、appendGoal |
| 本地持久化与异常恢复 | app.js:初始加载、persistGoals |
| 页面与详情导航 | app.js:navigate、renderPage、handleAction |
| 笔记导出 | app.js:exportNotes |
用户输入的目标名称和来源在 HTML 渲染前转义。日期和内容在保存前校验;原生对话框提供焦点管理和 Escape 关闭。
部署流程
GitHub Actions 的 .github/workflows/deploy.yml 监听 dev 分支。分支 push 后,Actions 使用仓库 Secrets 中的服务器地址、用户和 SSH 私钥登录服务器,进入 /opt/weread-board/WeReadBoard 后调用 deploy.sh。
deploy.sh 以当前触发分支为准拉取代码,再检查项目 .venv。如果 .venv/bin/python 不存在,脚本使用服务器上的 python3 或 python 创建虚拟环境;随后用 .venv/bin/python -m pip install -r requirements.txt 安装依赖,执行 migrations/001_reading_goals.sql,最后重启 weread.service。
例:用户把代码推送到 dev 分支,服务器原来没有 .venv。部署脚本会在 /opt/weread-board/WeReadBoard/.venv 创建环境,安装 Flask 和 PyMySQL,确认阅读目标表存在,再让 systemd 重新用 .venv/bin/python app.py 启动服务。
示例与真实接口的边界
概览图表使用固定样例;目标日期状态使用浏览器当前本地日期。笔记导出包含当前选中书目的划线和个人想法全集,不受内容类型筛选影响,不包含书签内容及公开点评。
书架的有声内容完结不等于用户读完,所以未用其完结状态计算阅读目标。文章收藏是入口,不假装能直接列出内部文章。缺失资料显示暂无,不补造 ISBN 等字段。
正式接口接入单独推进,必须核对实时可用接口、鉴权、分页、错误、升级提示及字段口径;本轮 Demo 的完成状态不得用于宣称这些能力已真实接入。
正式看板的紧凑布局
正式看板在 static/css/styles.css 中统一调整页面密度。顶栏高度为桌面 58 px、手机 56 px;标题区采用更短的上下间距,隐藏页面标题前的英文装饰文字。标题、说明、筛选、内容依次展示,操作按钮在桌面与标题并排,手机上自然换行。
书架按可用宽度自动排列书籍,每列约 116 px 起步;手机固定两列。真实封面维持原比例,并受所在列宽度约束。例如,书架网格可用宽度为 900 px 时显示多列紧凑排布,手机宽度为 390 px 时显示 2 列,长书名仍使用已有省略规则。
目标介绍区在宽屏上把说明与统计并排,窄屏上改为上下排列;目标卡按可用宽度自动排列,每张卡约 280 px 起步,并缩小封面预览和上下间距。例如,一个包含 6 本书、已读完 2 本的月度目标仍展示原有完成数和进度,只减少卡片占位。页面渲染、筛选、目标计算和数据流保持由现有 JavaScript 实现,CSS 不改变业务口径。
正式看板的阅读目标持久化
正式看板使用 static/js/app.js、static/js/weapi.js 和 reading_api.py。本节覆盖正式页面;前文的 Demo 存储方式不适用于正式目标。
reading_goals 保存 id、name、start_date、end_date、来源书单名称 sources 和创建、修改时间。reading_goal_books 保存 goal_id、book_id,联合主键防止同一目标重复计书。目标 ID 沿用前端生成值,便于导入旧目标。来源名称保存为 JSON 数组,不表示持续关联书单。
目标成员只通过外键关联目标。删除目标时级联删除成员,书架不受影响;书籍移出书架时也不删除目标成员。迁移位于 migrations/001_reading_goals.sql,只需部署时执行,不在请求中自动建表。
页面加载时 GET 目标列表,新建时 POST,编辑及追加书籍时 PUT,删除时 DELETE。表单先校验名称、日期和书目,服务端再次校验。目标与成员在同一事务中提交,编辑时锁定目标父记录并整体替换成员,避免并发请求混入两份书目。多个浏览器同时编辑同一目标时,以最后成功保存的完整内容为准。
前端收到提交成功响应后更新内存并关闭表单。保存中阻止重复提交;目标列表未从数据库读取完成前,不打开新建或编辑窗口,避免初始读取失败状态覆盖刚保存的目标。新建重试保持原 ID。数据库错误转换为 503,回滚事务并让前端保留草稿。目标列表不使用进程缓存,刷新即读取数据库。
例:创建“秋季阅读”,日期为 2026-09-01 至 2026-09-30,输入书籍 ["A", "A", "B"] 和来源 ["文学书单"]。数据库生成 1 条目标、2 条成员。若 A 已读完而 B 未读完,页面显示 1 / 2,50%;B 暂时不在书架中时仍显示 1 / 2,50%,B 的资料占位显示“状态待同步”,可在编辑器移除。若用户明确移除 B,则显示 1 / 1,100%。
完成率和状态不写入目标表。当前已知已读状态用于计算,不要求在目标日期内读完;全部已读完优先显示“已完成”,否则按浏览器本地日期显示未开始、进行中或已到期,起止当天均属于进行中。未知状态不计为已完成,但仍计入分母。
旧数据仅在用户点击导入时发送给 /api/reading/goals/import。先校验整批,再在事务中逐项插入;相同 ID 已存在时保留数据库内容,批次内重复 ID 只处理第一条。例如库中已有 G1,导入 [G1, G2, G2] 返回 {"imported":1},再次导入返回 {"imported":0}。任何写入失败均回滚整批;成功后保留原浏览器备份并记录导入标记。