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

17 KiB
Raw Blame History

title, type, date
title type date
Telegram-style Resource Stream资料分类查看全部 UI 重构) brainstorm 2026-05-25

Telegram-style Resource Stream

Problem Statement

当前"查看全部"打开的资料列表(/browse/category/:slug)使用统一的卡片网格,无法表达"admin 上传的内容本质是不同类型的消息"(一张图、一段文字+链接、一个视频、一个 PDF、4+ 张图相册等)。手机端用户体验偏弱,缺少 Telegram 那种按类型差异化呈现的直观感受。

本次重构目标:把 /browse/category/:slug 改成单列、按时间倒序、按日期分组、按上传类型差异化渲染的 Telegram-style 消息流,手机优先,保留 ARK 既有色系(深底 + 金色高亮)。

后端 endpoint 尚未实现。本次前端先用 mock data 完成视觉与交互,验收后再交接 API 契约给后端。

Context

当前实现

  • src/pages/Home.tsx:分类 section 头部"查看全部" → /browse;分类卡片 → /category/<slug>
  • src/pages/Browse.tsx221 行):含排序 tabs最新/推荐/热门/发布)+ 类型 chips + 语言 chips + tag 过滤 + 分页(每页 24
  • src/pages/CategoryPage.tsx156 行):含类型 chips + 语言 chips + 分页。
  • src/pages/ResourceDetail.tsx229 行):/r/:id 资源详情独立路由。
  • src/pages/FavoritesPage.tsx + postFavoriteDelta:收藏功能。
  • src/components/ResourceCard.tsx:统一卡片,不区分资源类型。
  • Resource schemasrc/api.ts扁平1 个 resource = 1 个文件(coverImage / fileUrl),无 attachments 数组。

设计参考(用户提供 7 张 Telegram 截图)

  1. 图片当文档上传:缩略图 + ↓ + filename.ext + size + 右下时间,无头像、无 reaction。
  2. PDF / AI / PPT 等文档:蓝圆 ↓ + filename + size。
  3. 纯文本 + 链接:https://... 自动识别为可点链接。
  4. 视频:海报 + ▶️,第一次点 inline 播放预览,第二次点全屏;下方可有 admin 写的说明文字。
  5. 单张图片:直接显示,点全屏。
  6. 图片 + 文字:图片上方/下方文字,文字内链接 autolink。
  7. 4+ 张图相册1-4 格 grid第 4 张模糊 + +N;点开后全屏画廊。

Chosen Approach

方案 A自建 MessageStream + 多态 MessageBubble 家族 + Mock-data layer

  • 新建一个 MessageStream 容器组件,被 Browse.tsxCategoryPage.tsx 共用,差异通过 scope props 注入。
  • MessageBubble 内根据 Post.attachments 的形状分发到 6 个子组件FileDoc / Text / Video / Image / ImageWithText / Album
  • 全屏交互(图片画廊 / 视频全屏)走 portal overlay。
  • 数据层用 usePostStream hook 抽象,默认走 src/mocks/mockPosts.ts(受 VITE_USE_MOCK_POSTS 控制),后端 ready 后切换为真 API。
  • 同时收尾收藏功能与详情页:删除 /favorites/r/:id、heart 按钮、postFavoriteDelta

Why This Approach

拒绝方案 B聊天 UI 库 @chatscope/chat-ui-kit-react 等)

  • 默认主题与 ARK 深底+金色严重冲突,改主题成本 ≈ 自己写。
  • 不支持"4+ 图相册 +N 模糊"自定义。
  • 增加包体积与灰盒 bug 风险。

拒绝方案 C重样 ResourceCard

  • 当前卡片统一渲染,无法满足"按类型差异化"(视频内嵌播放器 vs 多图相册 vs 文本+链接)。
  • 改动表面但不达 Telegram 风格。

拒绝其他子选项

  • 保留排序 tabsTelegram 流天然按时间倒序,多余 tabs 破坏隐喻。Home 页仍保留"官方推荐 / 最新更新" section 作为入口。
  • 保留收藏功能在列表/详情页:用户明确要求"不需要 reaction",且收藏与 Telegram 隐喻不符;整体下线最干净。
  • 保留 ResourceDetail 作 fallback:所有交互(下载 / 全屏 / 链接)都能就地完成,独立详情页冗余;老 /r/:id 改 301 重定向到 /category/<slug>#post-<id> 锚点。
  • kind 枚举铺开:后端枚举膨胀难维护;前端按 mime / 文件后缀做细分图标更灵活。

Design

1. 数据模型(前端使用 + 后端接口契约)

type Post = {
  id: string;
  categoryId: number;
  categorySlug: string;
  language: string;             // zh-TW | zh-CN | en
  text?: string;                // 可选;纯文本/图说,前端自动识别 https 链接
  attachments: Attachment[];    // 0~Ntext-only post 时为 []
  isRecommended: boolean;
  publishedAt: string;          // ISO用于排序 + 日期分组
  updatedAt: string;
};

type Attachment = {
  id: string;
  kind: "image" | "video" | "document";
  url: string;
  mime: string;                 // image/jpeg | application/pdf | video/mp4 | ...
  filename: string;             // "ARK项目一图读懂-01.jpg"
  sizeBytes: number;
  width?: number;
  height?: number;
  durationSec?: number;         // video 专用
  posterUrl?: string;           // video 海报
  thumbnailUrl?: string;        // image 缩略
};

关键约定:

  • kind: "document" + mime.startsWith("image/") = 图片当文档上传(截图 1
  • kind: "image" = 图片当图片呈现(截图 5、6、7。该开关在 admin 上传 UI 决定,传到后端落库。
  • 多图相册 = 一个 Post 带多个 kind: "image" 的 attachments。
  • 图片+文字 = Post 同时有 text 与 attachments。
  • 纯文本+链接 = Post 仅有 textattachments: []

2. 后端 API 契约(移交给后端)

方法 路径 用途
GET /api/posts?category=<slug>&lang=&type=&language=&cursor=&limit=20 分类内消息流
GET /api/posts?lang=&type=&language=&cursor=&limit=20 全部消息流(/browse
GET /api/posts/recommended?lang=&limit= Home 推荐 section
GET /api/posts/latest?lang=&limit= Home 最新 section
GET /api/posts/:id 单条(用于老 /r/:id 301 重定向落地,前端拿到 categorySlug 后跳锚点)
GET /api/categories 不变
POST/PUT/DELETE /api/admin/posts/... Admin CRUD支持多附件 + 文本 + "图片是否以文档呈现"开关

废弃:

  • /api/resources/:id/favorite
  • /api/resources* 系列保留过渡期,由后端写迁移脚本:每个老 Resource → 一个 Post。

返回结构:{ items: Post[], nextCursor?: string }cursor 由后端不透明字符串提供。

type 参数语义:all / image / video / pdf / ppt / text / link / archive。一个 Post 命中条件 = attachments[*].mimetext 满足;具体由后端定义。

3. 组件结构

src/pages/
  CategoryPage.tsx           ← 重写:<MessageStream scope={{ kind:'category', slug }} />
  Browse.tsx                 ← 重写:<MessageStream scope={{ kind:'all' }} />

src/components/messageStream/
  MessageStream.tsx          顶层fetch + 无限滚动 + 日期分组 + sticky filter chips
  FilterChips.tsx            类型 + 语言 chips横向滚动sticky top
  DaySeparator.tsx           "2 月 27 日" 胶囊
  MessageBubble.tsx          单条 Post 容器:决定子组件 + 右下角时间戳
  bubbles/
    FileDocBubble.tsx        截图 1 + 2文档图片当文档 / pdf / ai / ppt / docx
    TextBubble.tsx           截图 3纯文本 + autolink
    VideoBubble.tsx          截图 4海报 + ▶️,先 inline 后全屏
    ImageBubble.tsx          截图 5单张图片
    ImageWithTextBubble.tsx  截图 6图片 + 文本 + autolink
    AlbumBubble.tsx          截图 72-4 格 grid4+ 时第 4 格模糊 + `+N`
  overlays/
    ImageLightbox.tsx        全屏画廊(左右滑、缩放、关闭、下载)
    VideoPlayer.tsx          全屏视频播放器
  hooks/
    usePostStream.ts         cursor 分页 + IntersectionObservermock/real 切换
    useGroupedByDay.ts       按 publishedAt 本地日期分组
  utils/
    autolink.tsx             文本中 https?://... → <a target="_blank" rel="noopener">
    fileIcon.ts              按 mime/扩展名返回图标 + 颜色
    formatBytes.ts           3,549,239 → "3.5 MB"

src/mocks/
  mockPosts.ts               覆盖 7 种 bubble 类型的样本数据(图片用 picsum 占位或本地)

4. Bubble 分发逻辑

function pickBubble(post: Post) {
  const a = post.attachments;
  if (a.length === 0) return TextBubble;
  if (a.length >= 2 && a.every(x => x.kind === "image")) return AlbumBubble;
  const only = a[0];
  if (only.kind === "video") return VideoBubble;
  if (only.kind === "image") {
    return post.text ? ImageWithTextBubble : ImageBubble;
  }
  return FileDocBubble;   // document含图片当文档内部用 thumbnail 替代蓝圆图标)
}

5. 移动端布局规范

  • 容器宽度:手机 max-w-full px-3md+ max-w-[640px] mx-auto。桌面端不做多列,保持单列聊天流(左右大留白)。
  • 气泡:rounded-2xl bg-ark-panel,左对齐,无头像,内边距 p-3(文本 px-4 py-2.5)。
  • 时间戳:右下角 text-[11px] text-neutral-500,绝对定位。
  • 文档下载按钮:圆形 36×36金色 bg-ark-gold + 黑色 ↓。
  • Day separator胶囊 rounded-full bg-ark-panel/70 backdrop-blur px-3 py-1 text-xs text-neutral-400居中、sticky 在 FilterChips 下。
  • 多图 grid宽度 100%2×2间距 2px4+ 时第 4 格 relativebg-black/45 backdrop-blur-sm + +N 居中文字。
  • FilterChips 容器:sticky top-0 z-10 bg-ark-bg/90 backdrop-blur + 横向滚动 overflow-x-auto whitespace-nowrap

6. 交互

交互 行为
点击文档下载按钮 window.open(attachment.url, "_blank") 触发浏览器下载
点击单张图片 打开 ImageLightbox(单图)
点击相册任一图 / +N 打开 ImageLightbox,可左右切换
点击视频海报 第一次bubble 内 <video controls autoPlay> inline 播放
点击播放中的视频 打开 VideoPlayer 全屏 overlay
文本中的链接 target="_blank" rel="noopener noreferrer" 新标签打开
滚动到底部 IntersectionObserver 触发下一页 cursor 拉取
筛选 chips 变化 重置 cursor重新拉取同步 URL ?type=&language=
长按气泡 暂不做,列入 Open Questions

7. Mock data 层

src/mocks/mockPosts.ts 导出 MOCK_POSTS: Post[],至少包含:

  • 2 条"图片当文档"(不同 mimejpg、png
  • 2 条文档pdf、ai
  • 2 条纯文本+链接(含中文 + 多链接 + emoji
  • 1 条视频(带 posterUrl + duration
  • 2 条单图(不同宽高比)
  • 1 条图+文字
  • 1 条 3 图相册
  • 1 条 7 图相册(验证 +N 行为)
  • 跨多天的 publishedAt,验证 DaySeparator

usePostStream 行为:

const useMock = import.meta.env.VITE_USE_MOCK_POSTS !== "false";
if (useMock) {
  // 1. 按 scope.slug / type / language 过滤 MOCK_POSTS
  // 2. 按 publishedAt 倒序
  // 3. 按 cursor数字 offset 字符串)切 20 条
  // 4. setTimeout 200ms 模拟延迟
  // 5. 返回 nextCursor = offset+20 或 undefined
} else {
  // fetch /api/posts?... 真接口
}

切真接口时只需在部署环境设 VITE_USE_MOCK_POSTS=false(或干脆删 mock 分支)。

8. 锚点 + 分享

  • 每个 bubble 渲染为 <article id="post-${post.id}">
  • 老路由 /r/:id 改为一个轻量重定向组件fetch /api/posts/:id 拿到 categorySlugnavigate(/category/${slug}#post-${id}, { replace: true })scrollIntoView
  • Mock 模式下从 MOCK_POSTS 找。

9. 移除清单

文件:

  • src/pages/ResourceDetail.tsx
  • src/pages/FavoritesPage.tsx
  • src/components/ResourceCard.tsx
  • src/components/ResourceListFooter.tsx

代码:

  • postFavoriteDelta 及所有调用点
  • i18n keysfavorites, addFavorite, removeFavorite 等收藏相关
  • Home 中的 /favorites 入口

路由:

  • /favorites:删除
  • /r/:id:保留为轻量重定向

10. 测试 / 验证策略

  • 视觉验证:本地 npm run dev手机模拟器Chrome DevTools iPhone 14 Pro 视口)逐一对照 7 张截图。
  • 单元测试:pickBubble 分发逻辑、autolink 正则、formatBytesuseGroupedByDay
  • 类型检查:npx tsc --noEmit(项目 strict
  • 格式化:npm run format
  • 删除后回归:确认 /favorites/r/:id 老链接不报 404 而是合理跳转或 410。

11. 风险与缓解

  • 真接口 schema 与 mock 不一致spec 是合同;后端实现时若需偏离,必须先回来改 spec。前端 hook 内 Post 类型从 src/types/post.ts 单一来源导入。
  • +N 相册和单图 lightbox 的状态管理混乱:用 React Context<ImageLightboxProvider>)暴露 openLightbox(images, startIndex) 单一入口,所有 bubble 调它。
  • 视频 inline → 全屏切换的状态丢失:全屏 overlay 接 currentTime 参数,避免重新加载。
  • scroll restorationcursor 分页页内来回滑动时 IntersectionObserver 容易重复触发;用 loadingRef 守护。

Implementation Checklist

全部项已被 .unipi/docs/plans/2026-05-25-telegram-style-resource-stream-plan.md 覆盖。

  • 定义 src/types/post.tsPostAttachmentPostListResponse
  • 创建 src/mocks/mockPosts.ts:覆盖 7 种 bubble 类型 + 跨日期样本
  • 创建 src/components/messageStream/hooks/usePostStream.tsmock + real 双模式 + cursor 分页 + IntersectionObserver
  • 创建 src/components/messageStream/hooks/useGroupedByDay.ts
  • 创建 src/components/messageStream/utils/autolink.tsx
  • 创建 src/components/messageStream/utils/fileIcon.ts
  • 创建 src/components/messageStream/utils/formatBytes.ts
  • 创建 FilterChips.tsxsticky + 横向滚动)
  • 创建 DaySeparator.tsx
  • 创建 MessageBubble.tsx(含 pickBubble 分发)
  • 创建 bubbles/FileDocBubble.tsx(图片当文档 + pdf/ai/ppt
  • 创建 bubbles/TextBubble.tsxautolink
  • 创建 bubbles/VideoBubble.tsxinline 播放 + 全屏触发)
  • 创建 bubbles/ImageBubble.tsx
  • 创建 bubbles/ImageWithTextBubble.tsx
  • 创建 bubbles/AlbumBubble.tsx2-4 grid + +N
  • 创建 overlays/ImageLightbox.tsx + ImageLightboxProvider context
  • 创建 overlays/VideoPlayer.tsx
  • 创建 MessageStream.tsx 顶层组件
  • 重写 src/pages/CategoryPage.tsx<MessageStream scope={{ kind:'category', slug }} />
  • 重写 src/pages/Browse.tsx<MessageStream scope={{ kind:'all' }} />
  • 删除 src/pages/ResourceDetail.tsx,将 /r/:id 改为重定向组件mock 模式下从 MOCK_POSTS 查)
  • 删除 src/pages/FavoritesPage.tsxsrc/components/ResourceCard.tsxsrc/components/ResourceListFooter.tsx
  • 移除 postFavoriteDelta 及全部调用点
  • 移除 App.tsx/favorites 路由 + Home 入口
  • 清理 i18n favorites 相关 keys
  • 单元测试:pickBubbleautolinkformatBytesuseGroupedByDay
  • 视觉对照 7 张参考截图iPhone 14 Pro 视口)
  • 运行 npx tsc --noEmit && npm run format:check && npm test
  • 文档:在 README 注明 VITE_USE_MOCK_POSTS 用法
  • 交付后端 API 契约文档(本 spec 的 §2 部分单独抽出 markdown 给后端)

Open Questions

  • 长按 / 右键菜单:是否需要"复制链接"、"举报"、"分享"v2 决定。
  • type 筛选语义边界:一个 Post 含多种 attachment 时(图+视频混合,目前 mock 不出现),type=video 命中规则由后端定,前端按返回展示即可。
  • 空状态文案:消息流为空时显示什么?目前沿用 t("noResults")
  • 错误重试:网络失败时是否提供"重试"按钮?建议下方加一个 inline 重试条。
  • 视频自动暂停:滚出视口时是否自动暂停?建议做,体验更顺。
  • i18n 时间戳格式:是否需要适配繁体/简体/英文不同的日期分组格式?沿用 Intl.DateTimeFormatlang 切换。
  • SEO:删除 /r/:id 详情页后,搜索引擎抓取深度受影响吗?目前站点未做强 SEO可忽略如需保留可让 /r/:id 渲染服务端可解析的 <noscript> 摘要后再 JS 重定向。
  • Admin 上传 UI 改造本次只覆盖前台浏览端admin 端 Post 编辑器(多附件 + 文本 + 图片呈现方式开关)需要单独的 spec / 任务。

Out of Scope

  • Home 页面布局调整(分类卡片网格、推荐/最新 section 保持不变)
  • Admin 后台 UI 改造(单独 spec
  • 真实 API 实现(后端工作)
  • 后端数据迁移脚本
  • 长按菜单、举报、分享等社交功能
  • 评论 / Reaction
  • 离线缓存 / Service Worker
  • 桌面端多列布局