# 顾客详情页恢复与表单统一设计 ## 背景 顾客模块上一轮实现为了严格收口 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 范围不扩展到完整历史、照片和提醒系统。