2026-06-28-customer-detail-and-form-unification-design.md 16 KB

顾客详情页恢复与表单统一设计

背景

顾客模块上一轮实现为了严格收口 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 对齐。

当前缺失:

  • 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

返回字段以当前顾客实体为主,并额外附加:

type CustomerDetailResponse = CustomerEntity & {
  visitFrequencyLabel: string
}

规则:

  • 仅允许访问当前登录发型师名下顾客
  • 若顾客不存在或不属于当前发型师,返回 404
  • 未开发区块所需内容不从该接口额外扩展复杂嵌套结构

2. 更新顾客

接口:

PATCH /api/v1/customers/:id

请求体与创建顾客使用同一字段模型:

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 范围不扩展到完整历史、照片和提醒系统。