1 changed files with 514 additions and 0 deletions
@ -0,0 +1,514 @@ |
|||
# 公开主页预览 + 列表子页 Implementation Plan |
|||
|
|||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
|||
|
|||
**Goal:** 公开主页「展示 / 阅读」双模式下文章、时光机、阅读仅预览各 5 条并显示总数;`total > 5` 时进入独立子页完整列表(10 条/页、URL `?page=`);聚合 profile 接口瘦身,新增三类分页 API。 |
|||
|
|||
**Architecture:** 在 `server/constants` 固定 `PUBLIC_PREVIEW_LIMIT` / `PUBLIC_LIST_PAGE_SIZE`;`server/utils/public-pagination.ts` 统一解析 `page`;`#server/service/posts|timeline|rss` 提供 `*PreviewBySlug` 与 `*PageBySlug`(count + `limit/offset`);Nitro 在 `server/api/public/profile/[publicSlug]/` 下增加 `posts/index.get.ts`、`timeline/index.get.ts`、`reading/index.get.ts`;Vue 侧重构 `@[publicSlug]/index.vue` 并新增三列表页,分页与 `route.query` 用 `router.replace` 同步。 |
|||
|
|||
**Tech Stack:** Nuxt 4、Nitro、h3、`drizzle-orm` + SQLite(`drizzle-pkg`)、Bun test、`@nuxt/ui`(`UPagination`、`UButton` 等)。 |
|||
|
|||
**Spec:** `docs/superpowers/specs/2026-04-18-public-profile-preview-and-list-design.md` |
|||
|
|||
--- |
|||
|
|||
## File map(创建 / 修改) |
|||
|
|||
| 路径 | 职责 | |
|||
|------|------| |
|||
| `server/constants/public-profile-lists.ts` | `PUBLIC_PREVIEW_LIMIT`、`PUBLIC_LIST_PAGE_SIZE` | |
|||
| `server/utils/public-pagination.ts` | `normalizePublicListPage(raw)` | |
|||
| `server/utils/public-pagination.test.ts` | 分页 query 解析单测 | |
|||
| `server/service/posts/index.ts` | 公开预览 + 分页;删除或内联替代原 `listPublicPostsBySlug` | |
|||
| `server/service/timeline/index.ts` | 同上 | |
|||
| `server/service/rss/index.ts` | 同上;去掉公开列表 `limit(200)` | |
|||
| `server/api/public/profile/[publicSlug].get.ts` | 返回 `posts/timeline/rssItems` 为 `{ items, total }` | |
|||
| `server/api/public/profile/[publicSlug]/posts/index.get.ts` | `GET .../posts?page=` | |
|||
| `server/api/public/profile/[publicSlug]/timeline/index.get.ts` | `GET .../timeline?page=` | |
|||
| `server/api/public/profile/[publicSlug]/reading/index.get.ts` | `GET .../reading?page=` | |
|||
| `app/pages/@[publicSlug]/index.vue` | 双模式预览 + 「查看全部」;侧栏 `total`;移除主栏内联分页 | |
|||
| `app/pages/@[publicSlug]/posts/index.vue` | 文章列表 + `UPagination` | |
|||
| `app/pages/@[publicSlug]/timeline/index.vue` | 时光机列表 | |
|||
| `app/pages/@[publicSlug]/reading/index.vue` | 阅读列表 | |
|||
|
|||
--- |
|||
|
|||
### Task 1: `normalizePublicListPage` + 常量文件 |
|||
|
|||
**Files:** |
|||
- Create: `server/constants/public-profile-lists.ts` |
|||
- Create: `server/utils/public-pagination.ts` |
|||
- Create: `server/utils/public-pagination.test.ts` |
|||
|
|||
- [ ] **Step 1: 写失败单测** |
|||
|
|||
创建 `server/utils/public-pagination.test.ts`: |
|||
|
|||
```typescript |
|||
import { describe, expect, test } from "bun:test"; |
|||
import { normalizePublicListPage } from "./public-pagination"; |
|||
|
|||
describe("normalizePublicListPage", () => { |
|||
test("invalid or <1 becomes 1", () => { |
|||
expect(normalizePublicListPage(undefined)).toBe(1); |
|||
expect(normalizePublicListPage(null)).toBe(1); |
|||
expect(normalizePublicListPage("")).toBe(1); |
|||
expect(normalizePublicListPage("0")).toBe(1); |
|||
expect(normalizePublicListPage("-3")).toBe(1); |
|||
expect(normalizePublicListPage("abc")).toBe(1); |
|||
expect(normalizePublicListPage(0)).toBe(1); |
|||
}); |
|||
|
|||
test("parses positive integers", () => { |
|||
expect(normalizePublicListPage("1")).toBe(1); |
|||
expect(normalizePublicListPage(2)).toBe(2); |
|||
expect(normalizePublicListPage("999")).toBe(999); |
|||
}); |
|||
|
|||
test("floors floats", () => { |
|||
expect(normalizePublicListPage(2.7)).toBe(2); |
|||
expect(normalizePublicListPage("3.9")).toBe(3); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
- [ ] **Step 2: 运行单测确认失败** |
|||
|
|||
运行:`cd /home/dash/projects/person-panel && bun test server/utils/public-pagination.test.ts` |
|||
|
|||
预期:FAIL(模块不存在或函数未导出)。 |
|||
|
|||
- [ ] **Step 3: 实现常量与工具函数** |
|||
|
|||
创建 `server/constants/public-profile-lists.ts`: |
|||
|
|||
```typescript |
|||
/** 公开主页 profile 聚合接口中每类预览条数 */ |
|||
export const PUBLIC_PREVIEW_LIMIT = 5; |
|||
|
|||
/** 公开列表子页每页条数(仅服务端使用) */ |
|||
export const PUBLIC_LIST_PAGE_SIZE = 10; |
|||
``` |
|||
|
|||
创建 `server/utils/public-pagination.ts`: |
|||
|
|||
```typescript |
|||
/** |
|||
* 解析公开列表的 ?page= query:非有限数或 <1 时返回 1;否则返回正整数(float 向下取整)。 |
|||
*/ |
|||
export function normalizePublicListPage(raw: unknown): number { |
|||
const n = |
|||
typeof raw === "string" |
|||
? Number.parseInt(raw, 10) |
|||
: typeof raw === "number" |
|||
? raw |
|||
: Number.NaN; |
|||
if (!Number.isFinite(n) || n < 1) { |
|||
return 1; |
|||
} |
|||
return Math.floor(n); |
|||
} |
|||
``` |
|||
|
|||
- [ ] **Step 4: 运行单测确认通过** |
|||
|
|||
运行:`bun test server/utils/public-pagination.test.ts` |
|||
|
|||
预期:全部 PASS。 |
|||
|
|||
- [ ] **Step 5: Commit** |
|||
|
|||
```bash |
|||
git add server/constants/public-profile-lists.ts server/utils/public-pagination.ts server/utils/public-pagination.test.ts |
|||
git commit -m "feat(server): add public list pagination constants and page normalizer" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 2: Posts 服务 — 公开预览与分页 |
|||
|
|||
**Files:** |
|||
- Modify: `server/service/posts/index.ts` |
|||
|
|||
**约定:** 与现 `listPublicPostsBySlug` 相同的 `where`(`users.publicSlug`、`users.status === active`、`posts.visibility === public`)、排序 `desc(publishedAt), desc(id)`。删除对外导出的 `listPublicPostsBySlug`,改为 `getPublicPostsPreviewBySlug`、`getPublicPostsPageBySlug`(仅 profile 与列表 API 使用)。 |
|||
|
|||
- [ ] **Step 1: 增加 `count` 与 `sql` 导入** |
|||
|
|||
在 `drizzle-orm` 导入中加入 `count`(若尚未使用)。 |
|||
|
|||
- [ ] **Step 2: 实现 `getPublicPostsPreviewBySlug`** |
|||
|
|||
逻辑:`Promise.all` 并行执行 |
|||
|
|||
1. `select({ value: count() }).from(posts).innerJoin(users, ...).where(同上)` |
|||
2. `select({ title, excerpt, slug, coverUrl, publishedAt }).from(...).where(...).orderBy(...).limit(PUBLIC_PREVIEW_LIMIT)` |
|||
|
|||
返回 `{ items: 查询2结果, total: 查询1的 value }`。 |
|||
|
|||
- [ ] **Step 3: 实现 `getPublicPostsPageBySlug`** |
|||
|
|||
签名:`getPublicPostsPageBySlug(publicSlug: string, pageRaw: unknown)`。 |
|||
|
|||
- `page = normalizePublicListPage(pageRaw)` |
|||
- `pageSize = PUBLIC_LIST_PAGE_SIZE` |
|||
- `total` 同上 count |
|||
- `offset = (page - 1) * pageSize`,`select` 同上字段 `limit(pageSize).offset(offset)` |
|||
- 返回 `{ items, total, page, pageSize }` |
|||
|
|||
- [ ] **Step 4: 删除 `listPublicPostsBySlug`** |
|||
|
|||
并确保仓库内无残留引用(`rg listPublicPostsBySlug`)。 |
|||
|
|||
- [ ] **Step 5: Commit** |
|||
|
|||
```bash |
|||
git add server/service/posts/index.ts |
|||
git commit -m "feat(server/posts): public preview and paginated list by slug" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 3: Timeline 服务 — 公开预览与分页 |
|||
|
|||
**Files:** |
|||
- Modify: `server/service/timeline/index.ts` |
|||
|
|||
**约定:** 与现 `listPublicTimelineBySlug` 相同过滤与排序 `desc(occurredOn), desc(id)`。`items` 中每条为 `timelineEvents` 行对象(与现 `rows.map((r) => r.ev)` 一致)。 |
|||
|
|||
- [ ] **Step 1: 实现 `getPublicTimelinePreviewBySlug`** |
|||
|
|||
`count` + `select ev limit PUBLIC_PREVIEW_LIMIT`,返回 `{ items, total }`。 |
|||
|
|||
- [ ] **Step 2: 实现 `getPublicTimelinePageBySlug(publicSlug, pageRaw)`** |
|||
|
|||
`limit/offset` 使用 `PUBLIC_LIST_PAGE_SIZE`,返回 `{ items, total, page, pageSize }`。 |
|||
|
|||
- [ ] **Step 3: 删除 `listPublicTimelineBySlug`** |
|||
|
|||
`rg listPublicTimelineBySlug` 应为 0。 |
|||
|
|||
- [ ] **Step 4: Commit** |
|||
|
|||
```bash |
|||
git add server/service/timeline/index.ts |
|||
git commit -m "feat(server/timeline): public preview and paginated list by slug" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 4: RSS 服务 — 公开预览与分页 |
|||
|
|||
**Files:** |
|||
- Modify: `server/service/rss/index.ts` |
|||
|
|||
**约定:** 与现 `listPublicRssItemsBySlug` 相同 `where`、`orderBy(desc(publishedAt), desc(id))`,**移除** `.limit(200)`。预览 `limit(PUBLIC_PREVIEW_LIMIT)`;分页使用 `PUBLIC_LIST_PAGE_SIZE` 与 `offset`。 |
|||
|
|||
- [ ] **Step 1: 实现 `getPublicRssPreviewBySlug`** |
|||
|
|||
返回 `{ items: rssItems 行数组(与现 map 一致), total }`。 |
|||
|
|||
- [ ] **Step 2: 实现 `getPublicRssPageBySlug(publicSlug, pageRaw)`** |
|||
|
|||
返回 `{ items, total, page, pageSize }`。 |
|||
|
|||
- [ ] **Step 3: 删除 `listPublicRssItemsBySlug`** |
|||
|
|||
- [ ] **Step 4: Commit** |
|||
|
|||
```bash |
|||
git add server/service/rss/index.ts |
|||
git commit -m "feat(server/rss): public preview and paginated list by slug" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 5: 聚合 profile API |
|||
|
|||
**Files:** |
|||
- Modify: `server/api/public/profile/[publicSlug].get.ts` |
|||
|
|||
- [ ] **Step 1: 替换 import** |
|||
|
|||
改为 `getPublicPostsPreviewBySlug`、`getPublicTimelinePreviewBySlug`、`getPublicRssPreviewBySlug`。 |
|||
|
|||
- [ ] **Step 2: 构造 payload** |
|||
|
|||
```typescript |
|||
const [posts, timeline, rssItems] = await Promise.all([ |
|||
getPublicPostsPreviewBySlug(publicSlug), |
|||
getPublicTimelinePreviewBySlug(publicSlug), |
|||
getPublicRssPreviewBySlug(publicSlug), |
|||
]); |
|||
// payload.posts = posts; payload.timeline = timeline; payload.rssItems = rssItems; |
|||
``` |
|||
|
|||
删除原 `Awaited<ReturnType<typeof listPublic...>>` 类型,改为显式 `{ items; total }` 类型或 `typeof posts`。 |
|||
|
|||
- [ ] **Step 3: 手工验证** |
|||
|
|||
启动 `bun run dev`,`curl -s` 你的站点 `/api/public/profile/<有效slug>`,确认 `posts`/`timeline`/`rssItems` 均为 `{ items: [], total: number }` 形状。 |
|||
|
|||
- [ ] **Step 4: Commit** |
|||
|
|||
```bash |
|||
git add server/api/public/profile/[publicSlug].get.ts |
|||
git commit -m "feat(api): slim public profile to preview slices with totals" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 6: 三个分页 API 路由 |
|||
|
|||
**Files:** |
|||
- Create: `server/api/public/profile/[publicSlug]/posts/index.get.ts` |
|||
- Create: `server/api/public/profile/[publicSlug]/timeline/index.get.ts` |
|||
- Create: `server/api/public/profile/[publicSlug]/reading/index.get.ts` |
|||
|
|||
**模式(以 posts 为例,三者仅 service 与 404 文案不同):** |
|||
|
|||
```typescript |
|||
import { getPublicPostsPageBySlug } from "#server/service/posts"; |
|||
import { dbGlobal } from "drizzle-pkg/lib/db"; |
|||
import { users } from "drizzle-pkg/lib/schema/auth"; |
|||
import { and, eq } from "drizzle-orm"; |
|||
import { getQuery } from "h3"; |
|||
import { normalizePublicListPage } from "#server/utils/public-pagination"; |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
const publicSlug = event.context.params?.publicSlug; |
|||
if (!publicSlug || typeof publicSlug !== "string") { |
|||
throw createError({ statusCode: 400, statusMessage: "无效主页" }); |
|||
} |
|||
|
|||
const [owner] = await dbGlobal |
|||
.select({ id: users.id }) |
|||
.from(users) |
|||
.where(and(eq(users.publicSlug, publicSlug), eq(users.status, "active"))) |
|||
.limit(1); |
|||
|
|||
if (!owner) { |
|||
throw createError({ statusCode: 404, statusMessage: "未找到" }); |
|||
} |
|||
|
|||
const q = getQuery(event); |
|||
const page = normalizePublicListPage(q.page); |
|||
const data = await getPublicPostsPageBySlug(publicSlug, page); |
|||
return R.success(data); |
|||
}); |
|||
``` |
|||
|
|||
- timeline:`getPublicTimelinePageBySlug` |
|||
- reading:`getPublicRssPageBySlug` |
|||
|
|||
- [ ] **Step 1: 创建三文件并按上式接线** |
|||
|
|||
- [ ] **Step 2: 确认与 `posts/[postSlug].get.ts` 无路由冲突** |
|||
|
|||
本地访问 `GET /api/public/profile/foo/posts` 与 `GET /api/public/profile/foo/posts/bar` 均 200(slug 有效时)。 |
|||
|
|||
- [ ] **Step 3: Commit** |
|||
|
|||
```bash |
|||
git add server/api/public/profile/[publicSlug]/posts/index.get.ts server/api/public/profile/[publicSlug]/timeline/index.get.ts server/api/public/profile/[publicSlug]/reading/index.get.ts |
|||
git commit -m "feat(api): public paginated posts, timeline, and reading lists" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 7: 重构公开主页 `@[publicSlug]/index.vue` |
|||
|
|||
**Files:** |
|||
- Modify: `app/pages/@[publicSlug]/index.vue` |
|||
|
|||
- [ ] **Step 1: 更新 `Payload` 类型** |
|||
|
|||
`posts` / `timeline` / `rssItems` 为 `{ items: ...; total: number }`。 |
|||
|
|||
- [ ] **Step 2: 删除** `PAGE_SIZE`、`postsPage`、`timelinePage`、`rssPage`、`slicePage`、`postsChunk`、`timelineChunk`、`rssChunk`,以及 **detailed** 主栏内三个 `section` 中的 `UPagination` 与 `v-for` 对 chunk 的引用。 |
|||
|
|||
- [ ] **Step 3: 抽取共用区块渲染** |
|||
|
|||
**展示模式**与**阅读模式主栏**均使用同一套: |
|||
|
|||
- **文章:** `data.posts.items` 渲染;若 `data.posts.total > 5` 显示 `UButton` `to="\`/@${slug}/posts\`"`,文案 `查看全部(共 ${data.posts.total} 条)`;预览项展示 **日期**(`formatPublishedDateOnly`、`occurredOnToIsoAttr` 用于 `publishedAt`)。 |
|||
- **时光机:** `data.timeline.items`;`total > 5` → `/@slug/timeline`。 |
|||
- **阅读:** `data.rssItems.items`;块级链接样式(标题 + hostname);`total > 5` → `/@slug/reading`。 |
|||
|
|||
- [ ] **Step 4: 侧栏导航** |
|||
|
|||
所有 `data.posts.length` 改为 `data.posts.total`,`timeline`/`rssItems` 同理;`readingSectionValid` 用 `total > 0`。 |
|||
|
|||
- [ ] **Step 5: `firstReadingSection` / `watch`** |
|||
|
|||
依据 `total` 判断默认区块。 |
|||
|
|||
- [ ] **Step 6: Commit** |
|||
|
|||
```bash |
|||
git add app/pages/@[publicSlug]/index.vue |
|||
git commit -m "feat(public): profile preview slices and links to full list pages" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 8: 文章列表页 `posts/index.vue` |
|||
|
|||
**Files:** |
|||
- Create: `app/pages/@[publicSlug]/posts/index.vue` |
|||
|
|||
- [ ] **Step 1: 页面骨架** |
|||
|
|||
```vue |
|||
<script setup lang="ts"> |
|||
import { unwrapApiBody, type ApiResponse } from '../../../utils/http/factory' |
|||
import { formatPublishedDateOnly, occurredOnToIsoAttr } from '../../../utils/timeline-datetime' |
|||
|
|||
definePageMeta({ layout: 'public', title: '文章' }) |
|||
|
|||
const route = useRoute() |
|||
const router = useRouter() |
|||
const slug = computed(() => route.params.publicSlug as string) |
|||
|
|||
function pageFromRoute(): number { |
|||
const raw = route.query.page |
|||
const n = typeof raw === 'string' ? Number.parseInt(raw, 10) : Number.NaN |
|||
if (!Number.isFinite(n) || n < 1) return 1 |
|||
return Math.floor(n) |
|||
} |
|||
|
|||
const page = ref(pageFromRoute()) |
|||
|
|||
watch( |
|||
() => route.query.page, |
|||
() => { page.value = pageFromRoute() }, |
|||
) |
|||
|
|||
type Row = { title: string; excerpt: string; slug: string; publishedAt: Date | null; coverUrl?: string | null } |
|||
type Payload = { items: Row[]; total: number; page: number; pageSize: number } |
|||
|
|||
const { data, pending, error } = await useAsyncData( |
|||
() => `public-posts-${slug.value}-${page.value}`, |
|||
async () => { |
|||
const q = page.value > 1 ? `?page=${page.value}` : '' |
|||
const res = await $fetch<ApiResponse<Payload>>( |
|||
`/api/public/profile/${encodeURIComponent(slug.value)}/posts${q}`, |
|||
) |
|||
return unwrapApiBody(res) |
|||
}, |
|||
{ watch: [slug, page] }, |
|||
) |
|||
|
|||
function onPageChange(p: number) { |
|||
page.value = p |
|||
router.replace({ query: p <= 1 ? {} : { ...route.query, page: String(p) } }) |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
模板:顶栏下 `UContainer`,`UAlert`(error)、加载态、`ul` 列表结构 **复制** 现 `index.vue` detailed 文章 section 的 `NuxtLink` + 封面 + 日期 + 标题 + excerpt;底部 `UPagination`:`v-model:page="page"` 改为监听 `@update:page` 或在 `watch(page)` 里 `replace`(避免与 `useAsyncData` 死循环:以 **`page` ref 为唯一真源**,`watch(page, syncQuery)`)。 |
|||
|
|||
实现时推荐:`const listPage = computed({ get: () => pageFromRoute(), set: (p) => router.replace(...) })` 与 `useAsyncData` 的 `watch: [slug, () => route.query.page]` 二选一,**务必**在 spec 验收下能 **刷新保持页码**。 |
|||
|
|||
- [ ] **Step 2: 空列表 `UEmpty`** |
|||
|
|||
`!pending && !error && data?.total === 0`。 |
|||
|
|||
- [ ] **Step 3: Commit** |
|||
|
|||
```bash |
|||
git add app/pages/@[publicSlug]/posts/index.vue |
|||
git commit -m "feat(public): paginated public post list page" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 9: 时光机列表页 `timeline/index.vue` |
|||
|
|||
**Files:** |
|||
- Create: `app/pages/@[publicSlug]/timeline/index.vue` |
|||
|
|||
- [ ] **Step 1:** 复制 Task 8 的分页与 `useAsyncData` 模式,请求 `/api/public/profile/${slug}/timeline?page=`。 |
|||
|
|||
- [ ] **Step 2:** 列表项 UI 对齐现 `index.vue` detailed 时光机 `article` 卡片(时间、标题、`bodyMarkdown`、`linkUrl`)。 |
|||
|
|||
- [ ] **Step 3:** `definePageMeta({ layout: 'public', title: '时光机' })` |
|||
|
|||
- [ ] **Step 4: Commit** |
|||
|
|||
```bash |
|||
git add app/pages/@[publicSlug]/timeline/index.vue |
|||
git commit -m "feat(public): paginated public timeline list page" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 10: 阅读列表页 `reading/index.vue` |
|||
|
|||
**Files:** |
|||
- Create: `app/pages/@[publicSlug]/reading/index.vue` |
|||
|
|||
- [ ] **Step 1:** 同上,请求 `/api/public/profile/${slug}/reading?page=`。 |
|||
|
|||
- [ ] **Step 2:** 列表 UI 对齐 detailed **阅读** section(外链、`rssPublicTitle` / hostname 逻辑可抽成 `utils` 或内联复制 `index.vue` 中的函数)。 |
|||
|
|||
- [ ] **Step 3:** `definePageMeta({ layout: 'public', title: '阅读' })` |
|||
|
|||
- [ ] **Step 4: Commit** |
|||
|
|||
```bash |
|||
git add app/pages/@[publicSlug]/reading/index.vue |
|||
git commit -m "feat(public): paginated public reading list page" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 11: 全量验证与收尾 |
|||
|
|||
- [ ] **Step 1: 运行单测** |
|||
|
|||
`bun test server/utils/public-pagination.test.ts` |
|||
|
|||
(若其它测试存在:`bun test`。) |
|||
|
|||
- [ ] **Step 2: 构建** |
|||
|
|||
`bun run build` |
|||
|
|||
预期:成功完成 `nuxt build`。 |
|||
|
|||
- [ ] **Step 3: 手工验收(对照 spec §7)** |
|||
|
|||
- 主页预览 ≤5,`total` 与侧栏一致;`total > 5` 出现「查看全部」;`1≤total≤5` 无按钮。 |
|||
- 三子页 `?page=` 与 `UPagination` 同步;`page` 超大时 **空列表**、**非 404**。 |
|||
- `/@slug/posts/old-slug` 单篇仍可打开。 |
|||
|
|||
- [ ] **Step 4: 若有未提交改动,合并为一次 `feat` 或 `fix` commit** |
|||
|
|||
--- |
|||
|
|||
## Plan self-review |
|||
|
|||
| Spec 条款 | 对应 Task | |
|||
|-----------|-----------| |
|||
| 预览 5 条 + total | Task 2–5、7 | |
|||
| 列表 10 条/页、仅服务端 pageSize | 常量 + Task 2–4、6 | |
|||
| `total > 5` 才「查看全部」 | Task 7 | |
|||
| 子路由 posts/timeline/reading | Task 8–10 | |
|||
| profile 破坏性 `{ items, total }` | Task 5、7 | |
|||
| 空页不 404 | Task 2–4 offset 行为 + 6 | |
|||
| 双布局一致 | Task 7 | |
|||
| 美化(日期、块级阅读、按钮) | Task 7–10 | |
|||
|
|||
**Placeholder 扫描:** 无 TBD。 |
|||
**命名一致:** `rssItems` 仅在 profile;分页路径 `/reading` 与页面一致。 |
|||
|
|||
--- |
|||
|
|||
**Plan 已保存至 `docs/superpowers/plans/2026-04-18-public-profile-preview-and-list-implementation-plan.md`。** |
|||
|
|||
**执行方式可选:** |
|||
|
|||
1. **Subagent-Driven(推荐)** — 每 Task 派生子代理并在任务间复核。需配合 **superpowers:subagent-driven-development**。 |
|||
2. **Inline Execution** — 本会话按 Task 顺序执行,配合 **superpowers:executing-plans** 与检查点。 |
|||
|
|||
你更倾向哪一种? |
|||
Loading…
Reference in new issue