feat(reading-goals): 持久化阅读目标并收紧看板布局

This commit is contained in:
admin committed 2026-09-12 17:16:40 +08:00
1 parent 61d3951905
commit 094e1aca3d
13 files changed
+834 -39

No files matched your search

+26
View File
@@ -48,3 +48,29 @@
书架的有声内容完结不等于用户读完,所以未用其完结状态计算阅读目标。文章收藏是入口,不假装能直接列出内部文章。缺失资料显示暂无,不补造 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` 和 `api_v2.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/v2/goals/import`。先校验整批,再在事务中逐项插入;相同 ID 已存在时保留数据库内容,批次内重复 ID 只处理第一条。例如库中已有 G1,导入 `[G1, G2, G2]` 返回 `{"imported":1}`,再次导入返回 `{"imported":0}`。任何写入失败均回滚整批;成功后保留原浏览器备份并记录导入标记。