Files
Arkie-Library-Frontend/.unipi/docs/plans/2026-05-25-telegram-style-resource-stream-plan.md
2026-05-25 05:25:57 +08:00

211 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Telegram-style Resource Stream — Implementation Plan"
type: plan
date: 2026-05-25
workbranch: feat/telegram-stream
specs:
- .unipi/docs/specs/2026-05-25-telegram-style-resource-stream-design.md
---
# Telegram-style Resource Stream — Implementation Plan
## Overview
实现 `/browse``/category/:slug` 的 Telegram-style 消息流重构。前端先用 mock data 完成全部视觉与交互,等后端 `/api/posts` 系列接口 ready 后切换。同时收尾删除收藏功能与 `ResourceDetail` 详情页。
分支:`feat/telegram-stream`(在当前目录新建,不走 worktree。完成后由 Terry 显式确认才 merge。
## Sequencing
```
Task 1 (基础类型 + mock + utils)
Task 2 (hooks) ─────────────────┐
Task 3 (overlays) ─────┐ │
↓ │
Task 4 (bubbles)
Task 5 (stream 容器)
Task 6 (页面改写)
Task 7 (清理收藏 / 详情页) ── 可与 1-5 并行,但合并到 Task 6 之前完成
Task 8 (验证 + 文档 + API 契约) ── 最后
```
依赖关键路径1 → 2/3 → 4 → 5 → 6 → 8。Task 7 独立,建议早做以减少 imports 残留。
## Tasks
- unstarted: Task 0 — 创建分支
- Description: 在当前目录创建并切到 `feat/telegram-stream` 分支
- Dependencies: 无
- Acceptance Criteria: `git branch --show-current` 输出 `feat/telegram-stream``git status` 干净
- Steps:
1. `git status --short --branch` 确认无未提交改动
2. `git checkout -b feat/telegram-stream`
3. 确认 `.unipi/docs/specs/2026-05-25-telegram-style-resource-stream-design.md``.unipi/docs/plans/2026-05-25-telegram-style-resource-stream-plan.md` 已在该分支
- unstarted: Task 1 — 类型定义 + Mock data + 纯函数 utils
- Description: 建立 Post / Attachment 类型,写覆盖 7 种 bubble 的 mock 样本,写 3 个无依赖纯函数 util
- Dependencies: Task 0
- Acceptance Criteria:
- `src/types/post.ts` 导出 `Post``Attachment``PostListResponse``PostScope`
- `src/mocks/mockPosts.ts` 至少 12 条 Post 覆盖2 图片当文档 + 2 PDF/AI + 2 纯文本+链接 + 1 视频 + 2 单图 + 1 图+文 + 1 三图相册 + 1 七图相册publishedAt 跨 ≥3 个不同日期
- `formatBytes(3549239)``"3.4 MB"`unit tests pass
- `autolink("点 https://x.com 看")` 返回 React 节点数组,链接处为 `<a target="_blank" rel="noopener noreferrer">`unit tests pass
- `fileIcon({ mime, filename })` 返回 `{ Icon, color }`PDF 红、AI 橙、PPT 红、图片走 thumbnail 不返回 Icon
- `npx tsc --noEmit` 通过
- Steps:
1.`src/types/post.ts`
2.`src/components/messageStream/utils/formatBytes.ts` + `.test.ts`
3.`src/components/messageStream/utils/autolink.tsx` + `.test.tsx`
4.`src/components/messageStream/utils/fileIcon.ts`
5.`src/mocks/mockPosts.ts`(图片用 picsum.photos 占位,视频用公开 sample mp4 + 一张占位 poster
6.`npm test``npx tsc --noEmit`
- unstarted: Task 2 — Stream hooksusePostStream + useGroupedByDay
- Description: 数据层抽象mock/real 双模式 + 按日期分组
- Dependencies: Task 1
- Acceptance Criteria:
- `usePostStream({ scope, type, language })``VITE_USE_MOCK_POSTS !== "false"` 时从 `MOCK_POSTS` 过滤 + 倒序 + cursor 切片(每页 20+ 200ms 假延迟
- 真接口分支调用 `getJSON</api/posts?...>`(占位即可,后端未 ready 时不会走到)
- 返回 `{ items, isLoading, error, hasMore, loadMore, reset }`
- `useGroupedByDay(posts, lang)` 返回 `Array<{ dayLabel: string; items: Post[] }>`按本地时区日期分组dayLabel 通过 `Intl.DateTimeFormat` 按 lang 切换zh-TW/zh-CN/en
- 单元测试useGroupedByDay 在跨天数据上分出正确组数
- Steps:
1.`src/components/messageStream/hooks/usePostStream.ts`
2.`src/components/messageStream/hooks/useGroupedByDay.ts` + `.test.ts`
3.`.env.example``VITE_USE_MOCK_POSTS=true` 注释
4.`npm test``npx tsc --noEmit`
- unstarted: Task 3 — Overlay 基础设施ImageLightbox + VideoPlayer
- Description: 全屏画廊与视频播放器portal 渲染,单一入口 context
- Dependencies: Task 1
- Acceptance Criteria:
- `<ImageLightboxProvider>` 包在 App 根,暴露 `useLightbox()``openLightbox(images, startIndex)`
- `ImageLightbox` 支持:左右切换(按钮 + 键盘 ← → + 触屏左右滑、ESC/点遮罩关闭、右上角下载按钮
- `VideoPlayer` 支持:全屏遮罩 + `<video controls autoPlay>`、接 `currentTime` 参数避免重新加载、ESC/点遮罩关闭
- 两个 overlay 在手机端不溢出、不被底部 nav 遮挡
- `npx tsc --noEmit` 通过
- Steps:
1.`src/components/messageStream/overlays/ImageLightbox.tsx` + Provider/Context
2.`src/components/messageStream/overlays/VideoPlayer.tsx`
3.`App.tsx` / 根布局挂 Provider
4. 手动验证:在 dev console 临时调用 `openLightbox` 看是否正确呈现
- unstarted: Task 4 — Bubble 子组件6 个 + 分发器)
- Description: 按截图实现 6 种气泡 + `MessageBubble` 分发
- Dependencies: Task 1, Task 3
- Acceptance Criteria:
- `FileDocBubble` 处理 `kind: "document"`
-`mime.startsWith("image/")` → 左侧用 `thumbnailUrl` 缩略 + ↓ 覆盖;右侧 filename.ext + size截图 1
- 否则 → 左侧蓝圆 ↓ 图标(按 mime 取色)+ 右侧 filename + size截图 2
- `TextBubble` 渲染 `text`,调 `autolink`(截图 3
- `VideoBubble` 初始显示 posterUrl + ▶️ + 时长,第一次点 inline `<video controls autoPlay>`,第二次点(已播放)调 `openVideoPlayer`(截图 4
- `ImageBubble` 单张图,点击调 `openLightbox([att], 0)`(截图 5
- `ImageWithTextBubble` 单图 + 下方文本autolink截图 6
- `AlbumBubble` 2-4 格 grid间距 2pxattachments.length > 4 时第 4 格 `bg-black/45 backdrop-blur-sm` 覆盖 `+N`;点任一格调 `openLightbox(images, index)`(截图 7
- `MessageBubble` 实现 spec §4 的 `pickBubble` 分发,右下角绝对定位时间戳 `text-[11px] text-neutral-500`
- 所有 bubble 容器:`rounded-2xl bg-ark-panel p-3`(文本气泡 `px-4 py-2.5`),左对齐,无头像
- `npx tsc --noEmit` 通过
- Steps:
1.`MessageBubble.tsx`(含 pickBubble
2.`bubbles/TextBubble.tsx`
3.`bubbles/FileDocBubble.tsx`
4.`bubbles/ImageBubble.tsx`
5.`bubbles/ImageWithTextBubble.tsx`
6.`bubbles/AlbumBubble.tsx`
7.`bubbles/VideoBubble.tsx`
8. 在 dev 中临时挂一个 demo route 跑 MOCK_POSTS 全量渲染,目视检查 7 张截图对照
- unstarted: Task 5 — Stream 容器 + FilterChips + DaySeparator
- Description: 顶层组件接管 fetch、分组、无限滚动、sticky 筛选
- Dependencies: Task 2, Task 4
- Acceptance Criteria:
- `FilterChips`sticky top横向滚动 `overflow-x-auto whitespace-nowrap`,类型 chipsall/image/video/ppt/pdf/text/link/archive沿用 `typeFilterLabel`+ 语言 chipsall/zh-TW/zh-CN/en改变时 reset cursor 并同步 URL `?type=&language=`
- `DaySeparator`:胶囊样式,居中
- `MessageStream`
-`scope: { kind: "all" } | { kind: "category", slug: string }`
- 调用 `usePostStream` + `useGroupedByDay`
-`IntersectionObserver` 监听底部 sentinel触发 `loadMore`loadingRef 守护避免重复)
- 容器:手机 `max-w-full px-3 mx-auto`md+ `max-w-[640px] mx-auto`
- 空状态用 `t("noResults")`,错误用红色 inline 横幅 + 重试按钮
- `npx tsc --noEmit` 通过
- Steps:
1.`FilterChips.tsx`
2.`DaySeparator.tsx`
3.`MessageStream.tsx`
4. 在 dev demo route 挂 `<MessageStream scope={{ kind: "all" }} />` 验证滚动 + 筛选 + 分组
- unstarted: Task 6 — 重写 CategoryPage 与 Browse
- Description: 两个页面瘦身为单一组件调用
- Dependencies: Task 5, Task 7
- Acceptance Criteria:
- `src/pages/CategoryPage.tsx`:仅渲染分类标题 + `<MessageStream scope={{ kind: "category", slug }} />`,行数 < 30
- `src/pages/Browse.tsx`:仅渲染页面标题 + `<MessageStream scope={{ kind: "all" }} />`,行数 < 30不再读 `sort` / `tag` / `page` 参数
- 排序 tabs 全部去掉
- 移除对 `ResourceCard` / `ResourceListFooter` 的 import
- `App.tsx` 路由保持 `/browse``/category/:slug` 不变
- `npx tsc --noEmit` 通过
- Steps:
1. 改写 `CategoryPage.tsx`
2. 改写 `Browse.tsx`
3. 跑 dev server 在 `/browse``/category/<slug>` 验证
- unstarted: Task 7 — 移除收藏功能 + ResourceDetail
- Description: 整套清理
- Dependencies: Task 0可与 Task 1-5 并行)
- Acceptance Criteria:
- 删除文件:`src/pages/FavoritesPage.tsx``src/pages/ResourceDetail.tsx``src/components/ResourceCard.tsx``src/components/ResourceListFooter.tsx`
- `App.tsx` 移除 `/favorites` 路由;`/r/:id` 改为新组件 `PostRedirect`mock 模式下从 `MOCK_POSTS` 找 slug找不到 → navigate `/browse`
- `src/api.ts` 移除 `postFavoriteDelta`
- 全代码无 `postFavoriteDelta` / `FavoritesPage` / `ResourceDetail` / `ResourceCard` / `ResourceListFooter` 引用
- Home 中的 `/favorites` 入口(如有)移除
- `src/i18n.tsx` 移除 `favorites` / `addFavorite` / `removeFavorite` 等收藏 key三语言同步
- `npx tsc --noEmit` 通过(无 unused / 未引用错误)
- Steps:
1. `grep -rn "postFavoriteDelta\|FavoritesPage\|ResourceDetail\|ResourceCard\|ResourceListFooter\|/favorites\|/r/:id" src/` 列出所有引用点
2.`src/pages/PostRedirect.tsx`,挂到 `/r/:id` 路由
3. 删除 4 个文件 + 修改 `App.tsx` + `api.ts` + `i18n.tsx`
4. 再 grep 一次确认清零
5. `npx tsc --noEmit`
- unstarted: Task 8 — 验证 + 文档 + API 契约抽出
- Description: 全量验证 + 给后端的接口文档
- Dependencies: Task 6, Task 7
- Acceptance Criteria:
- `npx tsc --noEmit` 通过
- `npm run format:check` 通过(若不通过先 `npm run format`
- `npm test` 全绿
- `npm run build` 成功
- 手动视觉验证Chrome DevTools iPhone 14 Pro 视口逐一对照 7 张参考截图
- 新增 `.unipi/docs/specs/2026-05-25-posts-api-contract.md`:从主 spec §1-§2 抽出后端需要的所有内容Post/Attachment schema、6 个 endpoint、删除清单、迁移要求
- 更新 `README.md` 增加 `VITE_USE_MOCK_POSTS` 段落
- 不 commit、不 push等 Terry 显式确认)
- Steps:
1. 跑全套检查命令
2. dev server 手机视口检查
3. 写 API 契约文档
4. 改 README
5. 报告完成,等 Terry 审阅与 commit 指令
## Risks
- **mock 数据视觉与真实数据偏差**mock 用 picsum 占位图可能掩盖真实图片不同宽高比的边界情况。缓解mockPosts 中包含横图 / 竖图 / 接近正方形三种比例样本。
- **video poster 在 mock 模式不易获取**:用一张本地 SVG 占位即可,避免依赖外部链接。
- **i18n 删除收藏 key 后未使用的引用**tsconfig 的 `noUnusedLocals` 不覆盖 i18n key 的字符串引用,需手动 grep。
- **`PostRedirect` 在真接口模式下的实现**:当前先写 mock 分支,真接口分支 TODO 注释标明等 `/api/posts/:id` ready 后补。
- **infinite scroll + URL 同步**:用户改 filter chip 时既要 reset cursor 又要更新 URL注意避免 `setSearchParams` 触发额外 effect 循环。
- **后端最终 schema 与本 spec 偏差**:如有偏差,必须先回 spec 改契约,再调前端类型,避免散点修改。
## Out of Scope本 plan 不涵盖,遵循 spec
- Home 页布局调整
- Admin 后台 Post 编辑器
- 真实后端 API 实现 + 数据迁移
- 长按菜单 / 评论 / Reaction
- 桌面端多列布局
- SEO 优化