From 0fa7b79232f02fa1a34f61736cbe49d3319787f2 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 19 Apr 2026 10:46:56 +0800 Subject: [PATCH] docs: add media library and /me/media hub design spec Made-with: Cursor --- .../specs/2026-04-19-media-library-design.md | 104 +++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-19-media-library-design.md diff --git a/docs/superpowers/specs/2026-04-19-media-library-design.md b/docs/superpowers/specs/2026-04-19-media-library-design.md new file mode 100644 index 0000000..b363956 --- /dev/null +++ b/docs/superpowers/specs/2026-04-19-media-library-design.md @@ -0,0 +1,104 @@ +# 媒体库与「媒体」入口设计 + +## 背景与目标 + +控制台已有 **`/me/media/orphans`**(图片孤儿审查与删除)与 **`POST /api/file/upload`**(图片入库到 `media_assets`),但缺少面向作者的 **资源库**:集中上传、浏览自己名下的媒体,并 **复制公开 URL / Markdown 图片语法**,以便在文章等编辑器中手动粘贴使用。 + +本设计首版强调 **与编辑器解耦**:不修改 Markdown 编辑器行为;后续可在编辑器内增加「从媒体库选用」等深度集成。 + +## 需求结论(已确认) + +- **核心价值**:以 **上传 + 选用(复制)** 为主,列表浏览为辅。 +- **首版范围**:**媒体库页面 + 复制公开 URL + 复制 `![](url)`**,不在文章/资料编辑器内做集成。 +- **复制形态**:同时提供 **「复制图片 URL」** 与 **「复制 Markdown 图片」** 两种操作(两个按钮或等价 UI)。 +- **URL 策略**:在浏览器端使用 **`window.location.origin + '/public/assets/'`** 生成 **绝对 URL**,两种复制均使用该地址,避免相对路径在外部编辑器中失效。 +- **导航**:**单一「媒体」入口**,下挂 **资源库** 与 **孤儿清理** 两个子能力。 +- **首版不做**:媒体库内对仍被引用的资源的一键删除(避免与 `media_refs` / 正文同步规则冲突);孤儿删除与宽限逻辑 **继续仅在「孤儿清理」子页** 完成。 + +## 信息架构与路由 + +采用 **Nuxt 父布局 + 子页面**(推荐方案,已采纳): + +| 路径 | 说明 | +|------|------| +| `/me/media` | 默认落地:**资源库**(`media/index.vue`) | +| `/me/media/orphans` | **图片孤儿审查**(沿用现有逻辑与 API,页面作为子路由) | + +新增父级壳页面(例如 `app/pages/me/media.vue`): + +- 顶区标题:**媒体** +- 子导航:**资源库** ↔ **孤儿清理**(与当前子路由联动高亮) +- 内容区:`` + +文件布局约定: + +- `app/pages/me/media.vue` — 壳(导航 + 插槽) +- `app/pages/me/media/index.vue` — 资源库 +- `app/pages/me/media/orphans.vue` — 孤儿页(由现有页面迁入嵌套结构;业务与接口尽量不变) + +## 媒体库页(功能) + +### 列表 + +- 数据源:当前登录用户的 **`media_assets`**(`userId` 严格等于会话用户)。 +- 排序:默认 **`createdAt` 降序**。 +- 分页:与孤儿页类似,提供 **10 / 20 / 50** 每页可选。 +- 展示:**缩略图网格**;每张卡片展示 **文件大小**、**上传时间**。 +- **引用数**:对每条资产聚合 **`media_refs` 行数** 作为 `refCount` 展示,便于区分「已出现在文章/封面/资料等」与「仅上传未写入内容」。不展示可编辑的「引用详情」全文(首版保持简单)。 + +### 上传 + +- 调用现有 **`POST /api/file/upload`**(multipart,与资料页一致:格式、大小上限、多文件上限等沿用服务端实现)。 +- 上传成功后 **刷新列表**,并用 toast 提示成功;失败展示服务端错误信息。 + +### 复制 + +- **复制图片 URL**:将绝对 URL 写入剪贴板。 +- **复制 Markdown 图片**:写入 `![](<绝对URL>)`。 +- 复制成功 / 失败:**toast** 反馈。若剪贴板 API 不可用,首版以 toast 说明即可;**可选增强**:弹窗展示可手动复制的文本(实现阶段再决定是否纳入首版计划)。 + +### 明确不包含(首版) + +- 媒体库内 **删除** 仍被引用或非孤儿宽限内的资源。 +- 文章/资料编辑器内嵌媒体选择器、自动插入光标等。 + +## 服务端 API + +### `GET /api/me/media/assets` + +- **鉴权**:必须登录;仅返回 **当前用户** 的 `media_assets`。 +- **Query**:`page`(从 1 起)、`pageSize`(允许值与前端分页选项对齐,服务端需校验上限)。 +- **响应**(示意):`{ items: [...], total: number }` + 每项至少包含:`id`、`storageKey`(或等价字段)、`sizeBytes`、`createdAt`(ISO 或统一项目惯例)、`mime`、`refCount`;**公开路径**可与前端约定为固定前缀 `/public/assets/` + `storageKey`,或由服务端返回 `publicPath` 以避免重复逻辑。 +- **安全**:禁止按参数指定其他 `userId`;不得泄露其他用户资源。 + +列表查询实现可放在 `server/service/media` 或并列模块,保持 handler 轻薄。 + +## 全局导航与控制台首页 + +### `AppShell` 控制台子导航 + +- 在现有「资料 / 文章 / 时光机 / RSS」基础上增加 **「媒体」→ `/me/media`**。 +- 更新相关注释(原「桌面端下拉内仅四个子链接」等)以反映 **五个** 子项。 + +### `/me` 控制台首页 + +- 将原 **「文章媒体清理」** 卡片 **合并** 为单一 **「媒体」** 入口:文案说明内含 **资源库** 与 **孤儿清理**,主按钮指向 **`/me/media`**。 +- 不在首页重复放置两个独立卡片指向同一能力域(除非产品明确要求「孤儿」快捷直达,本 spec 不采用)。 + +## 错误处理与空状态 + +- 列表加载失败:toast + 可重试(按钮或自动提示)。 +- 空列表:说明可上传图片,并提供上传入口。 +- 上传失败:toast,错误信息来自服务端。 + +## 测试建议 + +- **`GET /api/me/media/assets`**:未登录返回 **401**;分页边界;**用户隔离**(仅本人数据);`pageSize` 非法值处理。 +- 若有可单测的纯函数(例如 Markdown 拼接),可补充 **单元测试**;UI 以手测为主。 + +## 后续扩展(非本 spec 交付范围) + +- 文章 Markdown 编辑器内:**打开媒体库**、插入链接或 Markdown。 +- 资料页头像/头图:**从媒体库选用**(仍可与裁剪流程组合)。 +- 媒体库内 **删除**:需与 `syncPostMediaRefs` / `syncProfileMediaRefs` 及孤儿规则统一产品设计后再做。