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

12 KiB
Raw Blame History

title, type, date, workbranch, specs
title type date workbranch specs
Telegram-style Resource Stream — Implementation Plan plan 2026-05-25 feat/telegram-stream
.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-streamgit 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 导出 PostAttachmentPostListResponsePostScope
      • 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 testnpx 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.exampleVITE_USE_MOCK_POSTS=true 注释
      4. npm testnpx 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:
      • FilterChipssticky 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触发 loadMoreloadingRef 守护避免重复)
        • 容器:手机 max-w-full px-3 mx-automd+ 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.tsxsrc/pages/ResourceDetail.tsxsrc/components/ResourceCard.tsxsrc/components/ResourceListFooter.tsx
      • App.tsx 移除 /favorites 路由;/r/:id 改为新组件 PostRedirectmock 模式下从 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 优化