# Admin 用户列表统计与公开页链接 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:** 在管理员用户列表接口与页面中,为每位用户展示文章 / 时间线 / RSS 源数量,并在存在 `publicSlug` 时提供站内打开与复制完整公开页 URL。 **Architecture:** 在单一 `GET /api/admin/users` 响应中通过 Drizzle + SQLite 为每行附加三个计数(相关子查询 `COUNT(*)`,不按 `visibility` 过滤);前端在用户管理表格增加列与操作,复制 URL 使用 `URL` 构造函数与可单测的小工具函数生成,失败时用 Nuxt UI `useToast` 提示。 **Tech Stack:** Nuxt 4 / Nitro、Vue 3、Drizzle ORM 0.45、SQLite(`drizzle-pkg`)、Bun test、`@nuxt/ui` 4.6。 **Spec:** `docs/superpowers/specs/2026-04-18-admin-user-stats-design.md` --- ## 文件结构(将创建 / 修改) | 路径 | 职责 | |------|------| | `server/api/admin/users.get.ts` | 在现有分页查询的 `select` 中增加三个计数字段并保持 `requireAdmin` | | `app/utils/public-profile-url.ts` | 根据 `origin` 与 `publicSlug` 生成与站内 `/@slug` 一致的绝对 URL(供复制与单测) | | `app/utils/public-profile-url.test.ts` | 对 URL 构造函数的回归测试 | | `app/pages/me/admin/users/index.vue` | 表格列、打开链接、`useToast` + 剪贴板复制 | --- ### Task 1: 公开页绝对 URL 小工具(TDD) **Files:** - Create: `app/utils/public-profile-url.ts` - Create: `app/utils/public-profile-url.test.ts` - [ ] **Step 1: 编写失败测试** `app/utils/public-profile-url.test.ts`: ```typescript import { describe, expect, test } from "bun:test"; import { buildPublicProfileAbsoluteUrl } from "./public-profile-url"; describe("buildPublicProfileAbsoluteUrl", () => { test("joins origin with /@slug path", () => { expect(buildPublicProfileAbsoluteUrl("https://example.com", "my-slug")).toBe( "https://example.com/@my-slug", ); }); test("normalizes origin without trailing slash", () => { expect(buildPublicProfileAbsoluteUrl("https://example.com/", "ab")).toBe("https://example.com/@ab"); }); }); ``` - [ ] **Step 2: 运行测试确认失败** Run: ```bash cd /home/dash/projects/person-panel && bun test app/utils/public-profile-url.test.ts ``` Expected: FAIL(模块或函数不存在)。 - [ ] **Step 3: 最小实现** `app/utils/public-profile-url.ts`: ```typescript /** 与站内路由 `/@${publicSlug}` 对应的绝对 URL(用于复制到剪贴板)。 */ export function buildPublicProfileAbsoluteUrl(origin: string, publicSlug: string): string { const base = origin.replace(/\/+$/, ""); const path = `/@${publicSlug}`; return new URL(path, `${base}/`).href; } ``` - [ ] **Step 4: 运行测试确认通过** Run: ```bash cd /home/dash/projects/person-panel && bun test app/utils/public-profile-url.test.ts ``` Expected: PASS。 - [ ] **Step 5: Commit** ```bash cd /home/dash/projects/person-panel git add app/utils/public-profile-url.ts app/utils/public-profile-url.test.ts git commit -m "feat(admin-ui): add public profile absolute URL helper" ``` --- ### Task 2: 扩展 `GET /api/admin/users` 返回三个计数 **Files:** - Modify: `server/api/admin/users.get.ts` - [ ] **Step 1: 在 `select` 中加入相关子查询计数** 在文件顶部增加导入: ```typescript import { sql } from "drizzle-orm"; import { posts, timelineEvents } from "drizzle-pkg/lib/schema/content"; import { rssFeeds } from "drizzle-pkg/lib/schema/rss"; ``` 将 `dbGlobal.select({ ... })` 的对象扩展为在现有字段之外增加(字段名与 spec 一致): ```typescript postCount: sql`(select count(*) from ${posts} where ${posts.userId} = ${users.id})`.mapWith( Number, ), timelineEventCount: sql`(select count(*) from ${timelineEvents} where ${timelineEvents.userId} = ${users.id})`.mapWith( Number, ), rssFeedCount: sql`(select count(*) from ${rssFeeds} where ${rssFeeds.userId} = ${users.id})`.mapWith( Number, ), ``` 保持 `from(users)`、`orderBy(desc(users.id))`、`limit`/`offset` 不变。若本地运行时报错(与 Drizzle 生成 SQL 有关),可改为「三个 `groupBy(userId)` 子查询 `leftJoin`」的等价写法,但须保持响应字段名不变。 - [ ] **Step 2: 手工验证 API** 1. 以管理员账号登录(`role` 为 `admin`)。 2. 在浏览器开发者工具中请求 `GET /api/admin/users`(或 `curl` 带上有效会话 Cookie)。 3. 确认 JSON 中每个 `users[]` 元素含 `postCount`、`timelineEventCount`、`rssFeedCount`,且为非负整数;与库内实际行数一致(可任选一名用户用 DB 客户端 `select count(*)` 对照)。 - [ ] **Step 3: 回归测试** Run: ```bash cd /home/dash/projects/person-panel && bun test ``` Expected: 全部 PASS(含 Task 1 新增测试)。 - [ ] **Step 4: Commit** ```bash cd /home/dash/projects/person-panel git add server/api/admin/users.get.ts git commit -m "feat(admin): include per-user content counts in users list API" ``` --- ### Task 3: 用户管理页表格与复制链接 **Files:** - Modify: `app/pages/me/admin/users/index.vue` - [ ] **Step 1: 扩展行类型与导入** 在 `