diff --git a/.gitignore b/.gitignore index 0728c8a..a58678c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ node_modules logs *.log +# Brainstorming visual companion sessions (local only) +.superpowers/ + # Misc .DS_Store .fleet diff --git a/docs/superpowers/specs/2026-04-18-discover-page-design.md b/docs/superpowers/specs/2026-04-18-discover-page-design.md new file mode 100644 index 0000000..6f42641 --- /dev/null +++ b/docs/superpowers/specs/2026-04-18-discover-page-design.md @@ -0,0 +1,109 @@ +# 发现页与「出现在发现中」设计 + +## 背景与目标 + +在站内提供 **「发现」** 入口:登录用户可浏览 **自愿出现在发现列表中的其他用户**,卡片展示有限公开信息(含用户配置的地址展示文案),点击进入其公开主页 `/@publicSlug`。 + +用户是否在发现中曝光、以及地址是否在卡片上展示,均在 **控制台 → 个人资料** 中配置,不在发现页内做复杂编辑。 + +## 需求结论(已确认) + +- **发现页含义**:全站用户展示名录(仅包含满足条件的用户),不是「仅看自己」的预览页。 +- **设置位置**:「是否出现在发现中」及地址相关展示设置在 **个人资料** 中维护。 +- **访问控制**:**仅登录用户**可打开发现页及列表接口;未登录访问 `/discover` 由现有全局路由中间件重定向至 `/login?redirect=...`;列表 API 走非公开 `/api/**`,由服务端 `10.auth-guard` 保证未登录返回 `401`。 +- **路由**:发现页路径 **`/discover`**;主导航(`AppShell`)在已登录时增加 **「发现」** 按钮/链接。 + +## 产品默认(评审未单列回复时采用的明确约定) + +以下在讨论中作为 **推荐默认** 写入 spec,避免实现阶段歧义: + +1. **`publicSlug` 为空**:用户 **不出现在** 发现列表中(避免卡片无法链到合法公开主页)。若用户打开「出现在发现中」但尚未设置公开标识,在个人资料页 **内联提示** 并引导填写 `publicSlug`。 +2. **发现列表中的头像**:遵守现有 **`avatarVisibility`**。当头像对公开策略为不可展示时,发现卡片使用占位(或不展示头像 URL),避免「发现」成为绕过资料隐私的渠道。 + +## 数据模型 + +在 `packages/drizzle-pkg/database/sqlite/schema/auth.ts` 的 `users` 表新增字段(命名实现时可与下列语义对齐): + +| 字段(语义) | 类型建议 | 默认值 | 说明 | +| --- | --- | --- | --- | +| 出现在发现中 | `integer`(0/1)或等价 boolean | **0(false)** | 默认不曝光,需用户主动打开 | +| 发现卡片地址文案 | `text`,可空 | `NULL` | 用户自填展示用字符串(首版不做地图/结构化省市区) | +| 在发现卡片上显示地址 | `integer`(0/1) | **0(false)** | 仅当「出现在发现中」为真时有意义;避免无文案仍出现「地址」标签 | + +列表查询 **必要条件**(AND): + +- `status = 'active'` +- 「出现在发现中」为真 +- `publicSlug IS NOT NULL`(且非空字符串) + +可选:若未来需要排序(如按更新时间),可依赖 `updatedAt`,本 spec 不强制。 + +## 服务端 API + +- **列表**:`GET /api/discover/users`(具体路径以实现为准,保持 REST 风格即可)。 + - **鉴权**:必须登录;未登录 `401`。 + - **查询参数**:分页参数与项目内公开列表风格一致(如 `page` + 固定 `pageSize` 常量,可新建 `DISCOVER_LIST_PAGE_SIZE` 或复用现有列表常量)。 + - **响应字段**(仅列展示所需,**禁止**返回 `email`、`password` 等敏感列): + - `publicSlug` + - `nickname`(或展示名回退规则与公开页一致) + - `avatar`:仅当 `avatarVisibility` 允许对外展示时返回 URL,否则 `null` 或省略(与前端占位约定一致) + - `discoverLocation`:仅当「出现在发现中」且「在卡片上显示地址」为真且文案非空时返回;否则不返回或返回 `null` + +实现上可抽取「根据 `avatarVisibility` 判断是否返回头像 URL」的辅助函数,与公开资料 API 逻辑对齐,避免分叉规则。 + +## 个人资料(PATCH/PUT) + +在现有 `server/service/profile/updateProfile`(及 `server/api/me/profile.put.ts` 请求体)中扩展: + +- `discoverVisible`(布尔) +- `discoverLocation`(可选字符串,长度上限与项目内其他 text 字段一致,如合理 `max`) +- `discoverShowLocation`(布尔) + +校验: + +- 使用 zod 或现有校验风格与白名单更新字段。 +- 当 `discoverVisible === true` 且当前用户 `publicSlug` 为空:允许保存其他字段,但 **发现列表仍不会出现该用户**;前端应在个人资料页提示「需设置公开标识才会出现在发现中」。 + +## 前端 + +### 导航 + +- `app/components/AppShell.vue`:已登录主导航中增加「发现」,指向 `/discover`,高亮规则与「首页」等一致(`navActive('/discover')`)。 + +### 页面 + +- 新建 `app/pages/discover/index.vue`(或等价路径): + - 调用列表 API,网格展示卡片(头像占位、昵称、`@publicSlug`、可选地址行)。 + - 卡片点击进入 `/@publicSlug`。 + - 空状态:无任何符合条件的用户时的说明。 + - 分页或「加载更多」,与项目现有列表 UX 一致。 + +### 个人资料 + +- `app/pages/me/profile/index.vue`(或当前资料编辑所在文件):新增分组 **「发现与展示」**: + - 开关:出现在发现中 + - 输入:地址/地区(展示文案) + - 开关:在发现卡片上显示上述地址 + - 与 `publicSlug` 的联动提示(见上文) + +## 错误处理与边界 + +- 停用/非 `active` 用户不出现在列表。 +- 列表接口错误:与项目统一 API 错误展示(如 toast)一致。 +- 首版 **不强制** 单独限流;若公开后需要,可后续对 `/api/discover/users` 增加与公开只读接口类似的保护。 + +## 测试建议 + +- **中间件**:未登录访问 `/discover` → 重定向登录且带 `redirect`。 +- **API**:未登录 `GET /api/discover/users` → `401`;登录且库中无候选人 → 空列表;多名用户仅部分满足条件 → 仅返回满足者。 +- **资料**:切换 `discoverVisible`、地址开关与文案后列表字段变化符合 spec;`avatarVisibility` 为 private 时响应中头像为空/占位。 +- **`publicSlug` 清空**:该用户从发现列表消失。 + +## 与现有设计的关系 + +- 页面访问模型见 `docs/superpowers/specs/2026-04-16-auth-access-control-design.md`(默认受保护、`/discover` 不在公开白名单)。 +- 公开主页路径仍为 `/@publicSlug`,与 `docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md` 一致。 + +## 实现后文档 + +若实现与 spec 不一致(例如产品改为「无 `publicSlug` 也展示但 CTA 禁用」),应 **先改本 spec** 再改代码,或在本文件追加修订段落并标注日期。