소스 검색

docs: add customer detail and form unification design

ChenYu 4 달 전
부모
커밋
da4a98f500
1개의 변경된 파일과 564개의 추가작업 그리고 0개의 파일을 삭제
  1. 564 0
      docs/superpowers/specs/2026-06-28-customer-detail-and-form-unification-design.md

+ 564 - 0
docs/superpowers/specs/2026-06-28-customer-detail-and-form-unification-design.md

@@ -0,0 +1,564 @@
+# 顾客详情页恢复与表单统一设计
+
+## 背景
+
+顾客模块上一轮实现为了严格收口 case 范围,已经把顾客详情页和编辑页从当前主线代码中移除,只保留了:
+
+- 顾客列表页
+- 新建顾客页
+- 顾客汇总接口
+
+但本次新 case 的目标已经变化,用户明确要求:
+
+- 顾客列表中的“详情”按钮恢复可用,并优化其 UI 表现
+- 顾客详情页恢复出来,优先复用之前那版详情页的页面结构
+- 新增页与编辑页不再分裂维护,而是统一为一个共享表单页面
+- 详情页中的“施术历史”“过往照片”“AI 发型模拟”“发送提醒”等能力,本次明确标记为未开发,不做伪功能
+- “到店频率”需要依赖 `customer_treatment_record` 做推断,而不是继续沿用旧字段模型中的静态字段
+
+当前仓库中虽然已经删除了 `CustomerDetailPage`,但通过 git 历史可以确认:
+
+- `frontend/src/pages/dashboard/customers/CustomerDetailPage.tsx` 曾经存在
+- 旧版详情页提供了可复用的页面骨架
+- 旧版字段模型与当前已对齐数据库设计的顾客模型不一致,因此不能原样恢复
+
+因此,本次正确方向不是“直接把旧文件拷回来”,而是:
+
+- 恢复旧版详情页的结构和信息层级
+- 用当前数据库对齐后的字段语义重新映射页面内容
+- 同时把新增和编辑统一到同一套表单页中
+
+## 目标
+
+- 恢复顾客详情链路:列表 -> 详情 -> 编辑。
+- 保持本 case 的边界清晰,只补齐真实需要的顾客详情能力。
+- 详情页复用旧版页面结构,但完全基于当前 `customer` 表字段重建展示语义。
+- 新增和编辑共用 `CustomerFormPage`,以当前较完整的表单布局为统一基线。
+- 新增页和编辑页都必须包含“提醒时间”和“下次到店”两个字段输入。
+- 详情页中尚未进入本 case 的业务能力,统一呈现为明确的未开发占位。
+- 到店频率由后端按施术历史记录推导,避免引入新的冗余数据库字段。
+
+## 非目标
+
+- 本次不实现施术历史真实列表接口。
+- 本次不实现过往照片真实聚合接口。
+- 本次不实现 AI 发型模拟历史聚合接口。
+- 本次不实现提醒发送接口。
+- 本次不实现新的工作台聚合能力。
+- 本次不扩展顾客主表字段,不引入新的顾客冗余展示字段。
+
+## 设计原则
+
+### 1. 优先复用旧页面结构,不复活旧错误字段
+
+旧版详情页中可复用的是布局骨架和区块组织方式,不可复用的是字段语义。
+
+可复用的结构包括:
+
+- 顶部 Hero 区
+- 基础信息区
+- 历史区块
+- 照片区块
+- 页面底部 CTA 区
+
+不可继续沿用的旧字段包括但不限于:
+
+- `image`
+- `summary`
+- `nextVisit`
+- `visitCycle`
+- `hairCondition`
+- `formulaNote`
+- `followMessage`
+- `lineStatus`
+
+这些字段已经不符合当前以 `customer` 表为唯一事实来源的顾客模型,不应在本次恢复时被重新带回主线代码。
+
+### 2. 新增与编辑只维护一套表单布局
+
+当前 `CustomerFormPage` 已经是完整度较高的表单骨架,因此本次不重新做独立编辑页布局,而是:
+
+- 继续以 `CustomerFormPage` 作为唯一顾客表单页
+- 通过路由参数和加载逻辑区分 `new` / `edit` 模式
+- 仅通过 copy、按钮文案、返回路径和提交逻辑体现模式差异
+
+这样可以避免新建和编辑页面继续发生结构漂移。
+
+### 3. 未开发模块必须显式可感知
+
+用户已经明确指出:
+
+- 施术历史先标记为未开发
+- 过往照片和 AI 发型模拟先标记为未开发
+- 发送提醒按钮也先标记为未开发
+
+因此本次不做假数据、不做空跳转、不做点击后 toast 提示,而是:
+
+- 保留区块标题和结构
+- 明确显示“当前版本暂未开发”
+- 对未开发按钮使用禁用态或不可点击态
+
+### 4. 计算类展示信息由后端推导,不回写数据库
+
+“到店频率”本质上是基于顾客施术记录的派生信息。
+
+因此本次采用:
+
+- 后端详情接口临时计算 `visitFrequencyLabel`
+- 前端只展示该派生值
+- 不在 `customer` 表中新增存储列
+
+## 现状分析
+
+### 1. 当前前端现状
+
+当前前端主线中已经具备:
+
+- 顾客列表页 `CustomerListPage`
+- 顾客新建页 `CustomerFormPage`
+- 顾客 API:列表、创建、汇总
+- 顾客表单映射工具 `shared/customerForm.ts`
+
+当前缺失:
+
+- 顾客详情路由
+- 顾客详情页组件
+- 顾客编辑路由
+- 顾客详情接口
+- 顾客更新接口
+
+同时,当前 `CustomerFormPage` 已经包含以下字段:
+
+- `nextFollowDate`
+- `recommendVisitDate`
+
+这意味着“新增页缺少提醒/下次到店输入”的问题,在当前主线代码里已经部分补齐,但还没有与编辑模式统一收口。
+
+### 2. 当前后端现状
+
+当前后端已经具备:
+
+- `POST /api/v1/customers`
+- `GET /api/v1/customers`
+- `GET /api/v1/customers/summary`
+
+且数据模型已经与 [数据库设计.md](file:///Users/yuchen/Documents/hairstylist/backend/docs/数据库设计.md#L16-L54) 对齐。
+
+当前缺失:
+
+- `GET /api/v1/customers/:id`
+- `PATCH /api/v1/customers/:id`
+- 基于 `customer_treatment_record` 的到店频率推导
+
+### 3. 旧详情页的可复用价值
+
+通过 git 历史可以确认旧版 `CustomerDetailPage` 具备以下价值:
+
+- 页面层次完整
+- 顶部 Hero 区适合承接头像、姓名、下次到店信息
+- 卡片式信息区适合继续复用到当前顾客详情
+- 未开发区块可直接复用旧页面中的“历史/照片/CTA”视觉结构
+
+但旧详情页曾依赖旧字段模型,因此恢复时必须做一次结构保留、字段重映射。
+
+## 方案选择
+
+本次采用以下方案:
+
+- 恢复旧版详情页的页面结构
+- 按当前数据库字段和真实 API 重建详情页数据模型
+- 将 `CustomerFormPage` 改造为 `new` / `edit` 共用页面
+- 只补充顾客详情与顾客更新接口,不扩展未开发模块的真实后端能力
+
+不采用以下方案:
+
+- **直接恢复旧文件**:会重新引入旧错误字段模型
+- **重新完全改写详情页结构**:偏离“优先用之前的详情页面”的用户要求
+- **本次连施术历史、照片、提醒接口一起做完**:会把当前 case 做胖,超出确认范围
+
+## 路由设计
+
+本次顾客模块路由收口为:
+
+- `/:locale/customers`:顾客列表页
+- `/:locale/customers/new`:顾客新建页
+- `/:locale/customers/:customerId`:顾客详情页
+- `/:locale/customers/:customerId/edit`:顾客编辑页
+
+其中:
+
+- `CustomerListPage` 负责进入详情页
+- `CustomerDetailPage` 负责进入编辑页
+- `CustomerFormPage` 同时处理 `new` 与 `edit`
+
+## 页面设计
+
+## 1. 顾客列表页
+
+### 1.1 详情按钮 UI
+
+当前列表页没有独立操作列,详情入口的视觉层级也不够明确。
+
+本次调整为:
+
+- 在列表每行增加独立操作列
+- 详情入口使用次级按钮样式
+- 按钮文案继续使用当前双语 copy 中的“详情 / 詳細”
+- 按钮点击进入 `/customers/:customerId`
+
+设计意图:
+
+- 保持操作入口清晰
+- 避免“详情”按钮和顾客信息、状态标签混在一起
+- 不在本 case 中额外加入更多行内动作,保持单一目标
+
+### 1.2 列表项字段
+
+列表页继续只展示当前必要字段:
+
+- 头像
+- 姓名
+- 电话
+- 发长
+- 到店计划 `planDate`
+- 烦恼 `trouble`
+- 跟进状态 `followState`
+- 操作按钮
+
+## 2. 顾客详情页
+
+详情页基于旧版页面结构恢复,但展示内容按当前字段语义重构。
+
+### 2.1 顶部 Hero 区
+
+Hero 区展示:
+
+- 顾客头像
+- 顾客姓名
+- 姓名读音
+- 电话
+- 期望联系渠道
+- 下次到店日期 `recommendVisitDate`
+- 提醒时间 `nextFollowDate`
+
+右上角操作区保留:
+
+- 返回列表
+- 进入编辑
+
+旧版中的“开始模拟”入口本次不作为主操作恢复到详情页 Hero 中,原因如下:
+
+- 用户已经要求 AI 发型模拟在详情页中标记为未开发
+- 若在 Hero 中保留可点击模拟入口,会造成和未开发状态冲突
+
+### 2.2 基础信息区
+
+基础信息区展示以下字段:
+
+- 手机号 `phone`
+- 邮箱 `email`
+- 生日 `birthday`
+- 期望联系渠道 `expectContact`
+- 到店频率 `visitFrequencyLabel`
+
+当 `visitFrequencyLabel` 无法计算时,显示:
+
+- `未记录`
+
+### 2.3 咨询信息区
+
+展示以下字段:
+
+- 到店计划 `planDate`
+- 来店目的 `visitPurpose`
+- 烦恼 `trouble`
+- 咨询内容 `consultContent`
+- 期待造型 `expectLook`
+
+### 2.4 头发信息区
+
+展示以下字段:
+
+- 发长 `hairLength`
+- 发质 `hairTexture`
+- 注意事项 `notice`
+- 过敏备注 `allergy`
+- 内部备注 `innerRemark`
+
+### 2.5 下次到店区
+
+单独展示:
+
+- 下次到店 `recommendVisitDate`
+- 提醒时间 `nextFollowDate`
+
+并保留一个“发送提醒”按钮,但本次处理为:
+
+- 禁用态按钮
+- 明确显示“未开发”
+
+### 2.6 施术历史区
+
+施术历史区保留标题与卡片结构,但内容统一为未开发态:
+
+- 标题存在
+- 内容显示“当前版本暂未开发”
+- 不渲染伪造历史记录列表
+
+### 2.7 过往照片区
+
+过往照片区保留旧版图片区块视觉结构,但内容统一为未开发态:
+
+- 标题存在
+- 网格区显示空占位
+- 辅助说明“当前版本暂未开发”
+
+### 2.8 AI 发型模拟区
+
+AI 发型模拟区单独保留为一个未开发卡片或 CTA 区:
+
+- 标题存在
+- 主按钮存在但禁用
+- 文案明确为未开发
+
+这样可以延续旧版详情页的页面节奏,但不会误导为可用能力。
+
+## 3. 顾客表单页
+
+### 3.1 页面定位
+
+`CustomerFormPage` 是本次唯一的顾客表单页面。
+
+它承载两种模式:
+
+- `new`
+- `edit`
+
+### 3.2 模式判定
+
+模式判定规则:
+
+- 无 `customerId` 时为 `new`
+- 有 `customerId` 时为 `edit`
+
+### 3.3 `new` 模式
+
+行为:
+
+- 使用 `emptyCustomerForm()`
+- 提交到 `createCustomer()`
+- 成功后返回顾客列表页
+
+### 3.4 `edit` 模式
+
+行为:
+
+- 页面初始化请求顾客详情
+- 通过 `customerToFormValues()` 回填表单
+- 提交到 `updateCustomer()`
+- 成功后返回对应顾客详情页
+
+### 3.5 统一字段集合
+
+新增与编辑必须共享同一套字段:
+
+- 基础信息
+- 头像
+- 咨询信息
+- 头发信息
+- 跟进信息
+
+尤其要确保以下字段在 `new` 和 `edit` 都存在:
+
+- `nextFollowDate`
+- `recommendVisitDate`
+
+### 3.6 copy 策略
+
+本次不新建第二套布局 copy,而是对现有 copy 做模式化扩展:
+
+- 标题
+- 页面描述
+- badge
+- 返回按钮
+- 提交按钮
+- 底部说明文案
+
+这些文案按 `new` / `edit` 分别定义。
+
+## API 设计
+
+## 1. 获取顾客详情
+
+接口:
+
+`GET /api/v1/customers/:id`
+
+返回字段以当前顾客实体为主,并额外附加:
+
+```ts
+type CustomerDetailResponse = CustomerEntity & {
+  visitFrequencyLabel: string
+}
+```
+
+规则:
+
+- 仅允许访问当前登录发型师名下顾客
+- 若顾客不存在或不属于当前发型师,返回 `404`
+- 未开发区块所需内容不从该接口额外扩展复杂嵌套结构
+
+## 2. 更新顾客
+
+接口:
+
+`PATCH /api/v1/customers/:id`
+
+请求体与创建顾客使用同一字段模型:
+
+```ts
+type UpdateCustomerRequest = CustomerUpsertInput
+```
+
+规则:
+
+- 仅允许更新当前登录发型师名下顾客
+- 不允许修改 `hairstylist_id`
+- 不允许前端传 `isFirstRegister` 作为编辑开关
+- 依旧沿用当前数据库字段模型,不引入旧字段兼容
+
+## 到店频率计算规则
+
+“到店频率”基于 `customer_treatment_record` 计算。
+
+### 1. 数据来源
+
+查询条件:
+
+- `customer_id = 当前顾客`
+- `hairstylist_id = 当前登录发型师`
+- `is_deleted = 0`
+
+按 `treatment_time` 升序排序。
+
+### 2. 空态
+
+若有效施术记录少于 2 条,则返回:
+
+- `未记录`
+
+### 3. 计算方法
+
+若有效施术记录不少于 2 条:
+
+- 计算相邻施术时间的天数差
+- 求平均值
+- 按区间映射为展示文案
+
+### 4. 文案映射
+
+- `<= 45 天`:`约 1.5 个月`
+- `46 - 75 天`:`约 2 个月`
+- `76 - 105 天`:`约 3 个月`
+- `> 105 天`:`约 3 个月以上`
+
+该值只用于详情页展示,不回写顾客表。
+
+## 前端数据模型调整
+
+本次前端 API 层需要新增:
+
+- `getCustomerById(customerId)`
+- `updateCustomer(customerId, input)`
+
+`contracts.ts` 中需要扩展顾客详情返回结构,以承接:
+
+- `visitFrequencyLabel`
+
+`shared/customerForm.ts` 继续作为创建与编辑的统一映射层,不新增第二套编辑专用映射逻辑。
+
+## 未开发占位策略
+
+本次统一采用“结构保留 + 能力禁用”的未开发策略:
+
+- 施术历史:区块保留,内容未开发
+- 过往照片:区块保留,内容未开发
+- AI 发型模拟:区块保留,按钮禁用
+- 发送提醒:按钮禁用
+
+原因:
+
+- 便于后续 case 继续在既有结构上填充真实能力
+- 避免出现“没有页面结构,后续又要重搭布局”的返工
+- 同时满足本 case 不扩展功能边界的要求
+
+## 测试策略
+
+### 1. 后端单测
+
+至少覆盖:
+
+1. `getById()` 仅能读取当前发型师的顾客
+2. `update()` 仅能更新当前发型师的顾客
+3. 施术记录不足 2 条时返回 `未记录`
+4. 施术记录达到 2 条以上时返回正确的频率区间文案
+
+### 2. 后端 e2e
+
+至少覆盖:
+
+1. 未登录访问详情返回 `401`
+2. 获取详情成功
+3. 编辑顾客成功
+4. 非所属发型师访问顾客详情返回 `404`
+
+### 3. 前端测试
+
+至少覆盖:
+
+1. 列表页详情按钮跳转到详情页
+2. 详情页正确渲染顾客字段
+3. 详情页未开发模块占位可见
+4. `CustomerFormPage` 在 `new` / `edit` 模式下显示不同文案
+5. `CustomerFormPage` 在 `edit` 模式下会回填顾客数据
+6. `CustomerFormPage` 在 `edit` 模式下提交更新接口
+
+## 风险与约束
+
+### 1. 旧版详情页字段模型已经失效
+
+实施时要警惕直接复制旧代码,避免把旧字段名重新带回当前主线。
+
+### 2. 当前 copy 结构可能不足以承载 `edit` 模式
+
+本次需要扩充文案配置,但仍应以当前 copy 结构延展为主,避免另起一套页面文案模型。
+
+### 3. `customer_treatment_record` 当前未接入前端
+
+这正是本次将施术历史区块标记为未开发的原因。后端只需要为“到店频率”做最小计算,不需要同步开放完整历史接口。
+
+### 4. 详情页恢复后会重新打开此前被删除的路由
+
+因此需要同步更新现有 `App` 路由测试,移除“已删除详情页会重定向”的断言,改为新的可访问行为验证。
+
+## 验收标准
+
+- 顾客列表页中的“详情”按钮具有清晰可点击的按钮样式,并可进入详情页。
+- 顾客详情页成功恢复,且页面结构沿用旧版详情页的大框架。
+- 顾客详情页展示字段完全基于当前数据库对齐后的顾客模型。
+- “到店频率”由后端基于施术记录推断,无记录时显示 `未记录`。
+- 施术历史、过往照片、AI 发型模拟、发送提醒均明确显示为未开发。
+- `CustomerFormPage` 同时支持新建和编辑两种模式。
+- 新建和编辑页面共享同一套布局和字段集合。
+- 新建和编辑均包含提醒时间与下次到店字段。
+- 详情页、编辑页只允许访问当前登录发型师名下顾客。
+
+## 最终结论
+
+本次 case 的正确完成定义为:
+
+1. 顾客详情页重新回到主线代码中,但以当前数据库字段语义为准,而不是复活旧模型。
+2. 新增与编辑统一为一个共享表单页面,不再维护两套布局。
+3. 到店频率由后端按 `customer_treatment_record` 推导,不新增冗余存储字段。
+4. 施术历史、过往照片、AI 发型模拟与发送提醒全部明确标记为未开发。
+5. 列表 -> 详情 -> 编辑链路可用,但 case 范围不扩展到完整历史、照片和提醒系统。