--- title: "Telegram-style Resource Stream(资料分类查看全部 UI 重构)" type: brainstorm date: 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/`。 - `src/pages/Browse.tsx`(221 行):含排序 tabs(最新/推荐/热门/发布)+ 类型 chips + 语言 chips + tag 过滤 + 分页(每页 24)。 - `src/pages/CategoryPage.tsx`(156 行):含类型 chips + 语言 chips + 分页。 - `src/pages/ResourceDetail.tsx`(229 行):`/r/:id` 资源详情独立路由。 - `src/pages/FavoritesPage.tsx` + `postFavoriteDelta`:收藏功能。 - `src/components/ResourceCard.tsx`:统一卡片,不区分资源类型。 - `Resource` schema(`src/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.tsx` 和 `CategoryPage.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 风格。 ### 拒绝其他子选项 - **保留排序 tabs**:Telegram 流天然按时间倒序,多余 tabs 破坏隐喻。Home 页仍保留"官方推荐 / 最新更新" section 作为入口。 - **保留收藏功能在列表/详情页**:用户明确要求"不需要 reaction",且收藏与 Telegram 隐喻不符;整体下线最干净。 - **保留 `ResourceDetail` 作 fallback**:所有交互(下载 / 全屏 / 链接)都能就地完成,独立详情页冗余;老 `/r/:id` 改 301 重定向到 `/category/#post-` 锚点。 - **`kind` 枚举铺开**:后端枚举膨胀难维护;前端按 `mime` / 文件后缀做细分图标更灵活。 ## Design ### 1. 数据模型(前端使用 + 后端接口契约) ```ts type Post = { id: string; categoryId: number; categorySlug: string; language: string; // zh-TW | zh-CN | en text?: string; // 可选;纯文本/图说,前端自动识别 https 链接 attachments: Attachment[]; // 0~N;text-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 仅有 `text`,`attachments: []`。 ### 2. 后端 API 契约(移交给后端) | 方法 | 路径 | 用途 | |---|---|---| | GET | `/api/posts?category=&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[*].mime` 或 `text` 满足;具体由后端定义。 ### 3. 组件结构 ``` src/pages/ CategoryPage.tsx ← 重写: Browse.tsx ← 重写: 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 截图 7:2-4 格 grid,4+ 时第 4 格模糊 + `+N` overlays/ ImageLightbox.tsx 全屏画廊(左右滑、缩放、关闭、下载) VideoPlayer.tsx 全屏视频播放器 hooks/ usePostStream.ts cursor 分页 + IntersectionObserver;mock/real 切换 useGroupedByDay.ts 按 publishedAt 本地日期分组 utils/ autolink.tsx 文本中 https?://... → fileIcon.ts 按 mime/扩展名返回图标 + 颜色 formatBytes.ts 3,549,239 → "3.5 MB" src/mocks/ mockPosts.ts 覆盖 7 种 bubble 类型的样本数据(图片用 picsum 占位或本地) ``` ### 4. Bubble 分发逻辑 ```ts 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-3`;md+ `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,间距 2px;4+ 时第 4 格 `relative` 叠 `bg-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 内 `