Commit 7e3a4f67 by luoqi

fix(persona-ui): 说明卡恢复「取值 + 短说明」两列结构

上一版为了"精简"把自明标签的 rules 清成空数组,结果卡片退化成三段平铺文字 ——
没有层次,也没有取值之间的对照,取值多的标签(权益身份 5 类、治疗史 4 类)尤其难读。

精简该精简的是 body 字数,不是砍结构。16 个标签统一回「加粗取值 + 一句短说明」:
客服先扫左边那列找到自己要的取值,再读右边一句。措辞仍是上一版改过的客服版,
口径订正(权益身份 5 类、转介绍达人门槛、急迫档位)都保留。

`rules[].label` 由可选改必填;CI 断言相应收紧:至少一条、每条都得有 label。

454 tests green · pac-web build 通过 · 本地逐个 hover 核对过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent 5b72d147
......@@ -41,10 +41,16 @@ describe('PERSONA_FEATURE_SPECS ↔ FeatureRegistry', () => {
});
describe('spec.display —— 详情页说明', () => {
test('⭐ 每个标签都要有一句副标题(rules 可为空 —— 取值自明就别复述)', () => {
test('⭐ 副标题 + 至少一条取值条目,且每条都带加粗取值', () => {
// 结构即可读性:卡片靠「取值 → 说明」两列对照来扫。曾试过把自明的标签 rules 清空,
// 卡片退化成三段平铺文字,没有层次也无从对照 —— 要精简就精简 body,不是砍结构。
for (const [key, spec] of Object.entries(PERSONA_FEATURE_SPECS)) {
expect(spec.display?.subtitle?.length ?? 0).toBeGreaterThan(0);
for (const r of spec.display.rules ?? []) expect(r.body.length).toBeGreaterThan(0);
expect(spec.display?.rules?.length ?? 0).toBeGreaterThan(0);
for (const r of spec.display.rules) {
expect(`${key} 缺 label`).toBe(r.label ? `${key} 缺 label` : '');
expect(r.body.length).toBeGreaterThan(0);
}
expect(`${key}:${spec.display.subtitle}`).not.toContain('undefined');
}
});
......
......@@ -53,8 +53,9 @@ export function PersonaFeatureHover({
{display.rules.map((r, i) => (
<li key={i} className="flex gap-2">
<span className="flex-none text-slate-400">·</span>
{/* 两列对照:加粗取值 + 短说明。客服先扫左边找到自己要的那个取值,再读右边 */}
<span>
{r.label && <strong className="mr-1 text-slate-800">{r.label}</strong>}
<strong className="mr-1 text-slate-800">{r.label}</strong>
{r.body}
</span>
</li>
......
......@@ -49,11 +49,14 @@ export interface PersonaFeatureDisplay {
/** 一句话副标题:这个标签在看什么 */
subtitle: string;
/**
* 取值条目:label = 标签值,body = 什么情况下会是它。
* **取值本身已经说清楚了就留空**(如性别、治疗史、权益身份)—— 把 chip 上已有的词
* 在下面再列一遍是噪音,客服要的是"我没看懂的那部分"。
* 取值条目 —— **每条都是「加粗取值 + 一句短说明」**,这个结构本身就是可读性:
* 客服扫的是左边那列取值,看到自己要找的那个再读右边一句。
*
* ⚠️ 不要为了"精简"把 label 去掉或整块留空 —— 试过一版把自明的标签清成空数组,
* 结果卡片只剩三段平铺文字,没有层次也没有对照,反而更难读(取值多的标签尤其明显)。
* 要精简就精简 body 的字数,不是砍掉结构。
*/
rules: Array<{ label?: string; body: string }>;
rules: Array<{ label: string; body: string }>;
/** 使用提醒 —— 只写会改变客服**做法**的(边界、已知局限);实现细节不写 */
note?: string;
/** 详情页分组 */
......@@ -119,13 +122,15 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
' R≥4 F≥3 M<4→一般价值 / R=3 F≥3 M<4→一般保持 / R≥4 F=2 M<4→一般发展 / 其余(含 R≤2 任意)→低活跃',
].join('\n'),
display: {
subtitle: '按最近来没来、来得勤不勤、花了多少分层',
subtitle: '按最近就诊、来的频次、累计消费分层',
rules: [
{ body: '「重要」= 消费排在前 40%;「一般」= 消费偏低' },
{ body: '价值 → 保持 → 发展 → 挽留:最近还在来 → 越来越久没来' },
{ label: '低活跃', body: '很久没来,或三项都低' },
{ label: '重要价值 / 一般价值', body: '最近还在来,而且来得勤' },
{ label: '重要保持 / 一般保持', body: '来得勤,但有段时间没来了' },
{ label: '重要发展 / 一般发展', body: '最近来过,但来的次数少' },
{ label: '重要挽留', body: '消费高,但很久没来了' },
{ label: '低活跃', body: '很久没来,或三项都偏低' },
],
note: '消费高低是跟同院其他客户比出来的,不是绝对金额',
note: '「重要」= 消费排在同院前 40%,「一般」= 消费偏低',
group: 'value',
order: 10,
},
......@@ -163,8 +168,9 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
display: {
subtitle: '按建档生日算的当下年龄',
rules: [
{ body: '婴幼儿 0-2 · 学龄前 3-6 · 替牙期 7-11 · 青少年 12-17' },
{ body: '青年 18-25 · 中青年 26-30 · 中年 31-45 · 中老年 46-54 · 老年 55+' },
{ label: '婴幼儿 / 学龄前 / 替牙期', body: '0-2 / 3-6 / 7-11 岁' },
{ label: '青少年 / 青年 / 中青年', body: '12-17 / 18-25 / 26-30 岁' },
{ label: '中年 / 中老年 / 老年', body: '31-45 / 46-54 / 55 岁以上' },
],
group: 'basic',
order: 10,
......@@ -185,8 +191,11 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
meaning: '客户生理性别,影响沟通话术与项目推荐',
algorithm: 'client_gender 直接映射:"M"/"男"→男性;"F"/"女"→女性;其他→未知',
display: {
subtitle: '按建档性别',
rules: [], // 取值本身已自明,不再复述一遍
subtitle: '影响称呼与项目推荐',
rules: [
{ label: '男性 / 女性', body: '按建档性别' },
{ label: '未知', body: '建档没填,或填的值无法识别' },
],
group: 'basic',
order: 20,
},
......@@ -207,7 +216,12 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
algorithm: 'host primary_category 经 assembler enum_mapping 归一到 PAC 立柱标准(走入→walk_in 等);二级 sub_category 原值透传。冲突解决(数仓侧):推荐人 > 活动码/渠道码 > 客户类型',
display: {
subtitle: '第一次到诊是怎么来的',
rules: [], // 取值本身已自明,不再复述一遍
rules: [
{ label: '走入 / 口碑客户', body: '直接上门 / 老客户推荐来的' },
{ label: '集团、地区渠道', body: '销售或营销活动带来' },
{ label: '电商平台 / 自媒体网络', body: '线上平台来的' },
{ label: '内部员工 / 其他', body: '其余来源' },
],
note: '一经判定不再改,后来谁推荐过也不会覆盖',
group: 'basic',
order: 40,
......@@ -234,7 +248,10 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
display: {
subtitle: '按院内已建档的亲属关系推断',
rules: [
{ body: '有长辈 → 多代之家;有子女 → 多口之家;有配偶 → 两口之家;其余 → 单身家庭' },
{ label: '多代之家', body: '有长辈(父母 / 祖辈)' },
{ label: '多口之家', body: '有子女' },
{ label: '两口之家', body: '有配偶' },
{ label: '单身家庭', body: '只有旁系或朋友' },
],
note: '家人没在本院看过牙就推不出来',
group: 'basic',
......@@ -290,7 +307,13 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
].join('\n'),
display: {
subtitle: '历史结算里用过的卡券或保险',
rules: [], // 取值本身已自明,不再复述一遍
rules: [
{ label: '高端保险直付', body: 'Bupa、万欣和等商保直付' },
{ label: '银行私行权益', body: '私人银行 / 白金卡' },
{ label: '储值会员', body: '有储值或预存' },
{ label: '儿牙会员', body: '儿牙卡 / 涂氟年卡' },
{ label: '医保客户', body: '用过医保结算' },
],
note: '只说明曾经这样结算过,不代表现在还有效',
group: 'value',
order: 40,
......@@ -320,7 +343,7 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
rules: [
{ label: '潜客 / 新客', body: '还没来过 / 首诊半年内且不超过 3 次' },
{ label: '成长客 / 成熟客', body: '还在来,消费在涨 / 消费平稳' },
{ label: '待激活 / 沉睡 / 流失', body: '半年~1年半 / 1年半~2年 / 2年以上没来' },
{ label: '待激活 / 沉睡客 / 流失客', body: '半年~1年半 / 1年半~2年 / 2年以上没来' },
],
group: 'value',
order: 20,
......@@ -345,7 +368,11 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
].join('\n'),
display: {
subtitle: '做过哪些主要治疗',
rules: [], // 取值本身已自明,不再复述一遍
rules: [
{ label: '种植史 / 正畸史', body: '种过牙 / 做过矫正' },
{ label: '修复史', body: '冠桥 / 贴面 / 嵌体' },
{ label: '牙周治疗史', body: '洁牙 / 刮治 / 牙周序列' },
],
note: '拔牙、补牙、根管等基础项目不计入',
group: 'value',
order: 30,
......@@ -371,8 +398,11 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
].join('\n'),
display: {
subtitle: '习惯什么时候来',
rules: [], // 取值本身已自明,不再复述一遍
note: '看的是近两年的预约时间,不是实际到诊时间',
rules: [
{ label: '工作日 / 周末', body: '近两年预约多在这几天' },
{ label: '上午 / 下午 / 晚间', body: '8-12 / 12-18 / 18-21 点' },
],
note: '看的是预约时间,不是实际到诊时间',
group: 'action',
order: 60,
},
......@@ -396,7 +426,10 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
].join('\n'),
display: {
subtitle: '历史上给过的最深一次折扣',
rules: [], // 取值本身已自明,不再复述一遍
rules: [
{ label: '最低折扣', body: '原价 500 元以上的治疗里谈成过的最深折扣' },
{ label: '日期 / 项目', body: '那一次是什么时候、做的什么' },
],
note: '免费洁牙等促销不计入。锚点可能很久远,报价前先看日期',
group: 'action',
order: 70,
......@@ -453,7 +486,11 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
].join('\n'),
display: {
subtitle: '沟通时要提前安抚的点',
rules: [], // 取值本身已自明,不再复述一遍
rules: [
{ label: '看牙恐惧', body: '病历提过害怕看牙' },
{ label: '晕针 / 晕血', body: '提过怕打针 / 见血不适' },
{ label: '密闭恐惧', body: '提过幽闭 / 长时间张口不适' },
],
note: '从病历文本里识别,没标不等于患者不敏感',
group: 'action',
order: 50,
......@@ -479,7 +516,11 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
].join('\n'),
display: {
subtitle: '诊断或建议过、但还没做的项目',
rules: [], // 取值本身已自明,不再复述一遍
rules: [
{ label: '种植 / 修复 / 拔牙', body: '缺牙待种、需要冠桥、残根残冠需拔' },
{ label: '正畸 / 早矫', body: '成人正畸 13-40 岁 / 儿童早矫 3-12 岁' },
{ label: '根管 / 牙周 / 补牙', body: '牙髓、牙周、龋齿' },
],
note: '已经在别处做掉的,这里看不到',
group: 'action',
order: 10,
......@@ -504,11 +545,13 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
'无潜在治疗→不打标签。Step2 取最大(多路径 MAX,现仅 1 路径)。',
].join('\n'),
display: {
subtitle: '有待做的项目,而且多久没来了',
subtitle: '有待做的项目,多久没来了',
rules: [
{ body: '紧急 = 90 天以上 · 高 = 30~90 天 · 中 = 30 天内' },
{ label: '紧急', body: '有待治项且 90 天以上没来' },
{ label: '高', body: '30-90 天没来' },
{ label: '中', body: '近期来过或新发现' },
],
note: '没有待做项目就不显示这个标签',
note: '没有待做项目就不显示这个标签',
group: 'action',
order: 20,
},
......@@ -536,7 +579,7 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
rules: [
{ label: '种植禁忌', body: '未满 19 岁,颌骨尚未发育完全' },
],
note: '目前只做了年龄这一项 —— 没有标记**不等于**没有其他禁忌',
note: '目前只做了年龄这一项 —— 没有标记不等于没有其他禁忌',
group: 'action',
order: 40,
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment