From 771c3df976e19775a3a22b0e2ca1c3c2da5e40f1 Mon Sep 17 00:00:00 2001 From: QiuSW Date: Tue, 15 Sep 2026 22:14:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AB=A0=E8=8A=82=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E5=B9=B6=E5=85=A5=E7=AB=A0=E8=8A=82=E7=BC=96=E8=BE=91=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A1=86=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 章节列表每行只保留「编辑」;移除「附件」按钮与独立的附件对话框 - 章节编辑对话框同时编辑标题、正文、本章插图与本章音频,并明确写出生效时机: 标题与正文点「保存章节」后生效,插图与音频在选中文件后立即上传、取消不撤销 - 无插图/音频的章节仍显示「未设置」,后端口与 schema 无变化(仍 v9) - 测试:157 单测与 26 项 E2E 通过(新断言:章节行只有一个按钮、对话框含四类控件与时机说明、 对话框内上传后仍可用保存按钮提交文本);真实链路确认按钮数为 1、上传即时生效、取消后阅读页正常 - 文档:Architecture / Business-Rules / Local-Development / Requirements / Home 同步 --- docs/02-architecture-and-code-map.md | 19 ++- docs/03-business-rules-and-glossary.md | 22 +++- docs/04-local-development-and-verification.md | 46 +++++++- docs/09-product-requirements-overview.md | 10 +- docs/README.md | 6 +- learner/e2e/attachments.spec.ts | 18 ++- learner/src/__tests__/attachments.spec.ts | 30 ++++- learner/src/style.css | 1 + learner/src/views/BookView.vue | 109 ++++++++---------- 9 files changed, 181 insertions(+), 80 deletions(-) diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md index 10501e3..c39538a 100644 --- a/docs/02-architecture-and-code-map.md +++ b/docs/02-architecture-and-code-map.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Architecture-and-Code-Map wiki_url: https://git.ilapage.cn/OPC/lexgo/wiki/Architecture-and-Code-Map.- -wiki_revision: b9666699e8f01a117412b0b65880b0402b1bef0c -synchronized_at: 2026-09-15T13:35:21Z +wiki_revision: c75593d72432e5195ea84c766a81449ccb91f648 +synchronized_at: 2026-09-15T14:13:31Z # 架构与代码地图 @@ -446,3 +446,18 @@ schema v8 新增两张表,都只用可重放的 `CREATE TABLE IF NOT EXISTS` 复用不变:`sniffAttachment`(magic bytes 判定)、`validateAttachment`(音频 ≤20 MiB;图片 ≤2 MiB 且 ≤4096×4096)、`coverDimensions`(JPEG/PNG 用标准库、WebP 读容器头)、`binaryResponse` + `http.ServeContent`(Range/206、416、ETag/304)、上传「先校验后写入、失败保留旧文件」的流程,以及 `chapterAttachmentViews`/`chapterPlaybackSeconds` 的批量读取(一次查询喂整个章节列表)。 学习端:`stores/library.ts` 用 `uploadChapterFile`/`deleteChapterFile`/`saveChapterPlayback`/`reportChapterPlayback` 替换了书级音频动作,并新增 `illustrationUrls`(按章节)与 `audioChapterId`(记录当前加载的音频属于哪一章,切章不会复用上一章的文件);`views/BookView.vue` 的封面区块收窄为封面并新增「章节附件」对话框(`data-testid="attachment-dialog"`:插图与音频各自的预览、状态、上传/替换/移除与规格提示),章节列表行新增缩略图列与「附件」按钮;`views/ReaderView.vue` 在正文上方渲染本章插图的**缩略图**(高 120px 的按钮,`data-testid="chapter-illustration"`),点击后在对话框(`data-testid="illustration-dialog"`)里按原图显示(最大 `min(88vw,1200px)` × `78vh`,保持比例不裁切),播放器只在**本章**有音频时出现,离开或切换章节时上报一次位置;**书籍页的章节列表不显示也不预取插图**(2026-09-15 按用户要求修订)。 + +## #37 章节级音频与章节插图(2026-09-15) + +**schema v9**:新增 `lexgo_chapter_attachments`(主键 `(chapter_id, kind)`,`kind ∈ {audio, illustration}`,`bytes MEDIUMBLOB`,外键级联到章节与账号)与 `lexgo_chapter_playback_positions`(主键 `(owner_id, chapter_id)`),并执行一条幂等语句 `DELETE FROM lexgo_book_attachments WHERE kind='audio'`。三张表都只用可重放的 DDL;`lexgo_book_attachments` 与 `lexgo_playback_positions` 保留结构(不删表、不改列),书级音频停止写入。 + +| 层 | 变化 | +|---|---| +| 书级 | 只剩**封面**:`kind='cover'`、`GET/POST/DELETE /api/v1/books/:id/cover`、`coverVersion` 缓存失效机制全部保持原样 | +| 章节级 | `POST/DELETE/GET /api/v1/chapters/:id/audio` 与 `.../illustration`、`PUT /api/v1/chapters/:id/playback` | +| 退役 | `POST/DELETE/GET /api/v1/books/:id/audio`、`PUT /api/v1/books/:id/playback`(路由不再注册,返回 404) | +| 响应 | `ChapterSummary` 增加 `illustrationVersion`、`audioVersion`、`playbackSeconds`,字段始终存在(无文件时为空串),书籍详情的章节列表与阅读器响应都带上它们;书的 `attachments` 只剩 `cover` | + +复用不变:`sniffAttachment`(magic bytes 判定)、`validateAttachment`(音频 ≤20 MiB;图片 ≤2 MiB 且 ≤4096×4096)、`coverDimensions`(JPEG/PNG 用标准库、WebP 读容器头)、`binaryResponse` + `http.ServeContent`(Range/206、416、ETag/304)、上传「先校验后写入、失败保留旧文件」的流程,以及 `chapterAttachmentViews`/`chapterPlaybackSeconds` 的批量读取(一次查询喂整个章节列表)。 + +学习端:`stores/library.ts` 用 `uploadChapterFile`/`deleteChapterFile`/`saveChapterPlayback`/`reportChapterPlayback` 替换了书级音频动作,并新增 `illustrationUrls`(按章节)与 `audioChapterId`(记录当前加载的音频属于哪一章,切章不会复用上一章的文件);`views/BookView.vue` 的封面区块收窄为封面,**章节列表每行只有一个「编辑」按钮**,章节编辑对话框(`data-testid="chapter-dialog"`)同时编辑标题、正文、插图与音频(插图/音频区块带预览、状态、上传/替换/移除与规格提示,并明确写出「标题与正文点保存后生效,文件选中后立即上传」),`views/ReaderView.vue` 在正文上方渲染本章插图的**缩略图**(高 120px 的按钮,`data-testid="chapter-illustration"`),点击后在对话框(`data-testid="illustration-dialog"`)里按原图显示(最大 `min(88vw,1200px)` × `78vh`,保持比例不裁切),播放器只在**本章**有音频时出现,离开或切换章节时上报一次位置;**书籍页的章节列表不显示也不预取插图**(2026-09-15 按用户要求修订两次:先改为缩略图+弹窗,再把附件并入章节编辑对话框)。 diff --git a/docs/03-business-rules-and-glossary.md b/docs/03-business-rules-and-glossary.md index 175322c..ee60c5c 100644 --- a/docs/03-business-rules-and-glossary.md +++ b/docs/03-business-rules-and-glossary.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Business-Rules-and-Glossary wiki_url: https://git.ilapage.cn/OPC/lexgo/wiki/Business-Rules-and-Glossary.- -wiki_revision: a226a6026e8415d6f2981fc87b52396f30ab3708 -synchronized_at: 2026-09-15T13:35:21Z +wiki_revision: 081daac73fa8d172a477d196e964382735122466 +synchronized_at: 2026-09-15T14:13:31Z # 业务规则与术语 @@ -356,3 +356,21 @@ exact优先;未命中再按WordNet异常表/词尾规则查候选,词性顺 **删除与备份**:删除章节(#10 的事务级联)连带删除该章附件与该章所有账号的位置记录;删除书籍通过章节级联清空。附件存于数据库,因此 #15 的备份与恢复自动覆盖,恢复后按字节相同并可继续拖动。 **范围外**:多张插图、正文中间图文混排、缩略图墙式独立浏览界面、多音轨与分章音轨、字幕与时间戳同步、逐句跟读、转写、TTS、转码、自动搜图、在线音频地址、公开分享与跨用户共享。 + +## #37 章级附件规则(2026-09-15) + +**挂载单位**:封面属于**书**(一本书一张,维持 #21 已验收的规则);音频与插图属于**章**(一章各一张)。多篇文章合并成一本书时,每章可以有自己的音频与插图,互不影响。没有附件的章节保持原样:不显示占位框、不显示播放器。 + +**格式与上限**:音频只接受 **MP3**(ID3 或帧同步),≤ **20 MiB**;插图与封面同规格:**JPG/PNG/WebP**,≤ **2 MiB**,像素 ≤ **4096×4096**;一律**按文件内容判定类型**,不看扩展名与客户端声明。超限 `413`、类型或内容非法 `400` 并给出可读原因;**被拒绝的上传保留旧文件**。 + +**替换与位置**:替换即覆盖同一章同一类型的那一行。**替换或移除音频会清空该章的播放位置**(旧位置对新文件没有意义);插图与音频互不影响,移除其一不会动另一个。 + +**播放位置**:粒度是**账号+章节**,不是账号+书——同一本书的不同章节各记各的。播放中每 5 秒、暂停时、离开当前章节或切换章节时各上报一次;服务端只做 0~24 小时的上界校验与 upsert,并发以最后一次写入为准,不解析音频时长。 + +**读取与缓存**:插图与音频都需要会话,跨账号一律 `404`、匿名 `401`;支持 **HTTP Range(206)**、`416` 与 `ETag`/`304`。前端用带凭据的 fetch 取字节再交给 ``/`