# 设计:公开主页预览 + 完整列表子页(文章 / 时光机 / 阅读) **日期**:2026-04-18 **状态**:已定稿(与产品对话一致) ## 1. 背景与目标 公开主页 `/@:publicSlug` 在 **「展示」** 与 **「阅读」** 两种布局下,**文章、时光机、阅读(RSS 公开项)** 目前或拉全量、或在「阅读」模式主栏内分页,首页信息密度与首包体积都不理想。 **目标**: - 首页每类仅展示 **最新 5 条** 预览,并显示 **总数**。 - 当 **`total > 5`** 时提供 **「查看全部」** 入口,进入 **独立子页面**;列表 **每页 10 条**,分页体现在 **URL**(`?page=`),便于分享与刷新。 - **瘦身** 聚合接口:首页只拉预览 + 总数;完整列表仅由 **分页 API** 提供。 - 在现有 Nuxt UI 与绿色主色体系内 **统一预览与列表的视觉层级**(卡片、时间轴、阅读行),不做换肤级重构。 ## 2. 产品规则(已确认) | 维度 | 规则 | |------|------| | 布局范围 | **「展示」与「阅读」两种模式都做**,三块行为一致:预览 5 条 + 条件「查看全部」。 | | 预览条数 | 三类 **统一** 各 **5** 条(`PUBLIC_PREVIEW_LIMIT = 5`)。 | | 列表每页条数 | **10** 条(`PUBLIC_LIST_PAGE_SIZE = 10`),**仅服务端生效**;请求中的 `pageSize` **忽略或拒绝**,防止任意大页。 | | 「查看全部」 | 仅当 **`total > 5`** 时显示;**`total === 0`** 不展示该区块;**`1 ≤ total ≤ 5`** 只展示预览,不显示「查看全部」。 | | 子页路由 | `/@:publicSlug/posts`、`/@:publicSlug/timeline`、`/@:publicSlug/reading`(与界面「阅读」一致,路径不用 `rss`)。 | | 聚合接口 | **`GET /api/public/profile/:publicSlug`** 改为每类 `{ items, total }`,**破坏性变更**;当前仅 `app/pages/@[publicSlug]/index.vue` 消费该聚合响应,改版面时一并更新类型。 | | 分页接口 | 三类独立 **`GET`**:`.../posts`、`.../timeline`、`.../reading`,Query **`page`**(默认 1,非法或 `<1` **规范为 1**)。 | | 空页 | 页码超出末页时 **`items: []`**,**不**对空页返回 **404**(`total` 与规范化后的 `page` 仍返回)。 | | 可见性 | 仅 **`visibility === public`** 的内容;unlisted / share 链路不在本次范围。 | ## 3. 非目标 - 管理后台(`/me/*`)列表与分页改造。 - RSS 同步策略、抓取频率、私有订阅源管理。 - 全文搜索、标签筛选、按日期归档视图。 - 为列表页单独做 SSR HTML 的复杂 SEO(仅轻量 `title`/meta 即可)。 ## 4. API 契约 ### 4.1 `GET /api/public/profile/:publicSlug`(变更) 响应中 **`posts` / `timeline` / `rssItems`** 从数组改为对象(字段名保持与现有前端语义一致,减少无谓重命名): ```ts posts: { items: Array<{ title, excerpt, slug, publishedAt, coverUrl }> // 长度 ≤ 5 total: number } timeline: { items: Array<{ /* 与现 listPublicTimelineBySlug 公开字段一致 */ }> total: number } rssItems: { items: Array<{ /* 与现 listPublicRssItemsBySlug 公开字段一致 */ }> total: number } ``` `user`、`bio`、`links` 保持现状。`total` 必须通过 **数据库计数**(或与分页等价的稳定查询)得到,**禁止**拉全表后在内存计算。 ### 4.2 新增分页接口 | 方法 | 路径 | Query | |------|------|--------| | GET | `/api/public/profile/:publicSlug/posts` | `page`(可选,默认 1) | | GET | `/api/public/profile/:publicSlug/timeline` | 同上 | | GET | `/api/public/profile/:publicSlug/reading` | 同上 | 与已有 **`GET .../posts/:postSlug`** 不冲突:列表与单篇分文件注册即可。 **成功响应**(三端统一形状): ```ts { items: [...] // 当前页,最多 10 条 total: number page: number // 规范化后的页码 pageSize: 10 // 固定常量,与 PUBLIC_LIST_PAGE_SIZE 一致 } ``` **404**:`publicSlug` 对应用户不存在或非 `active`,与现 profile 行为一致。 ### 4.3 服务层职责 在 `#server/service/posts`、`timeline`、`rss` 中新增或重构为: - 每类:**预览**(`items` 上限 5 + `total`)、**分页**(`page` → `items` + `total` + 规范化 `page` + `pageSize`)。 - 现有无 limit 的 `listPublic*` 应改为复用上述逻辑或内部 helper,避免重复 SQL;RSS 现有 **`limit(200)`** 的公开列表实现应由 **正式分页 + count** 替代,不再依赖硬编码 200 作为「全量」。 ## 5. 前端:路由与数据流 ### 5.1 新增页面 | 文件 | 路由 | |------|------| | `app/pages/@[publicSlug]/posts/index.vue` | `/@:publicSlug/posts` | | `app/pages/@[publicSlug]/timeline/index.vue` | `/@:publicSlug/timeline` | | `app/pages/@[publicSlug]/reading/index.vue` | `/@:publicSlug/reading` | 均使用 **`layout: 'public'`**。列表数据来自对应分页 **`GET`**,`page` 与 **`route.query.page`** 同步;翻页优先使用 **`router.replace`** 更新 query,减少历史栈噪音。 ### 5.2 主页 `@[publicSlug]/index.vue` - 仅请求 **`GET .../profile/:slug`**,使用返回的 **`items` / `total`**。 - **删除**「阅读」模式下主栏内对全量数组的 **`slice` + `UPagination`**;主栏三块与「展示」模式一致:**预览 + 条件「查看全部」**。 - 侧栏数字改为 **`total`**。 ### 5.3 分页 UI 使用 **`UPagination`**:`total` 为接口 `total`,`items-per-page` 固定 **10**。首屏从 URL 读取 `page`;非法字符串是否在客户端先规范为 **1** 由实现计划写明,并与服务端规范化一致。 ## 6. 体验与美化 - **文章预览**:展示 **日期**(沿用 `formatPublishedDateOnly`、`occurredOnToIsoAttr` 等工具,与旧 detailed 列表信息层级一致)。 - **时光机预览**:保留 **圆点 + 竖线** 时间轴;与现 showcase 块级卡片协调。 - **阅读预览**:从纯链接改为 **块级行**(标题 + hostname 次要文案),hover **边框/背景** 微变化,对齐旧 detailed 「阅读」列表气质。 - **「查看全部」**:`UButton` **outline** 或 **ghost + 右箭头**;位置在区块标题行或区块底部统一,**两种布局模式同一套位置**。 - **列表页**:分隔线/密度对齐现 detailed 列表;**`UEmpty`**、**`UAlert`**(404)、加载态与现站一致。 ## 7. 测试与验收 - **手工**:主页预览条数、`total` 与侧栏、按钮显隐(`total > 5`);子页翻页、URL、刷新、超大 `page` 空列表;单篇文章 `/@/posts/:slug` 仍可访问。 - **自动化(可选)**:若项目已有 service 层测试模式,可为 **计数 + limit/offset** 增加用例;API 路由可做轻量集成测试(视仓库惯例)。 ## 8. 自检记录(定稿前) - **占位符**:无 TBD。 - **一致性**:预览 5、列表 10、按钮仅 `total > 5`、空页不 404,与对话一致。 - **范围**:仅公开 profile 与三列表页;不含后台。 - **歧义**:`rssItems` 键名保留在 profile 响应中;公开分页路径使用 **`/reading`**,与页面路由一致。