diff --git a/changelogs/2026-04/2026-04-20-dict-color-product-tag.md b/changelogs/2026-04/2026-04-20-dict-color-product-tag.md new file mode 100644 index 0000000..190a485 --- /dev/null +++ b/changelogs/2026-04/2026-04-20-dict-color-product-tag.md @@ -0,0 +1,198 @@ +# 字典加颜色字段 + 产品线主题标签字典化(前端要改) + +**日期**:2026-04-20 +**PR**:#1017(Closes Issue #1016) +**合并到 dev commit**:0dc2cb8c +**后端服务**:hl-user-service(实质改动)+ hl-product-service-v2(仅 VO 注释完善) +**影响**:管理端「字典管理」表单 + 「产品线/主题」编辑弹窗 + +--- + +## 为什么变 + +两件事一起做: + +1. **字典数据项加 `color` 字段**:`sys_dict_data` 全局加可选 hex 颜色值,方便所有字典(不只是 product_tag)都能配色 +2. **产品线(主题)的「标签」从自由输入改为字典选择**:使用 `product_tag` 字典,标签存 dict_value(英文 code),展示用 dict_label(中文)+ color(颜色渲染) + +> 后端「字典管理 CRUD + 产品线 VO 注释」全部就绪,**`product_tag` 字典本身的数据由 wx 在字典管理页录入,本次不预置业务数据**。 + +--- + +## 接口契约变化 + +### 1. 字典 CRUD —— 新增 `color` 字段 + +**类型**:`string | null`,hex 格式 `#RGB` / `#RRGGBB` / `#RRGGBBAA`,可为空字符串 +**校验**:后端正则 `^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$|^$`,传 `red` / `#XYZ` 等非 hex 会被 400 拒绝 + +#### POST /admin/dict/data + +```jsonc +{ + "dictType": "product_tag", + "dictLabel": "亲子游", + "dictValue": "PARENT_CHILD", + "icon": "🏖️", + "color": "#FF5722", // ⭐ 新增可选 + "sortOrder": 10, + "remark": "亲子主题标签" +} +``` + +#### PUT /admin/dict/data/{id} + +```jsonc +{ + "dictLabel": "亲子游", + "color": "#FF5722" // ⭐ 新增可选;不传则不覆盖 +} +``` + +> PUT 语义:字段缺失 = 不改;传 `null` = 不改;传空字符串 `""` = 清空 + +#### GET 类接口 —— 响应新增 `color` + +| Method | Path | 变化 | +|---|---|---| +| GET | `/admin/dict/data/{id}` | 响应新增 `color`(NULL 时省略) | +| GET | `/admin/dict/data/{dictType}` | 响应每项新增 `color`(NULL 时省略) | +| GET | `/admin/dict/all` | 响应每项新增 `color`(NULL 时省略) | +| GET | `/dict/all`(公开,无需 token) | `dataList[].color`(NULL 时省略) | + +**重要**:`color` 用了 `@JsonInclude(NON_NULL)`,**老字典 / 没配色的字典项响应里不会有 `color` 这个 key**,前端解构时记得用可选链 / 默认值,不要 `data.color.toUpperCase()` 这种硬解构。 + +#### `/dict/all` 公开接口响应样例 + +```jsonc +{ + "code": 200, + "data": [ + { + "dictType": "product_tag", + "dictName": "产品标签", + "dataList": [ + { + "dictValue": "PARENT_CHILD", + "dictLabel": "亲子游", + "icon": "🏖️", + "color": "#FF5722", // ⭐ 配了色就有 + "sort": 1, + "remark": null + }, + { + "dictValue": "SELF_DRIVE", + "dictLabel": "自驾", + "icon": null, + // ⭐ 没配色就没有 color 字段(NON_NULL) + "sort": 2, + "remark": null + } + ] + } + ] +} +``` + +--- + +### 2. 产品线 VO(无字段类型变化,仅注释完善) + +| 文件 | 字段 | 改动 | +|---|---|---| +| `ProductLineSaveReqVO.tags` | `List` | 注释从「标签」改为「标签(product_tag字典的dict_value英文code列表,无数量上限,可为空)」+ example=`PARENT_CHILD,SELF_DRIVE` | +| `ProductLineRespVO.tags` | `List` | 注释从「产品线标签」改为「产品线标签(product_tag字典的dict_value英文code列表,可为空)」+ example=`PARENT_CHILD,SELF_DRIVE` | + +**接口签名 0 变化**:`POST /admin/product/line` / `PUT /admin/product/line/{lineId}` / `GET /admin/product/line/{lineId}` 的 `tags` 字段类型仍是 `string[]`,前端字段类型不需要改,**只是语义换了**: + +- 旧语义:自由文本数组(用户随便输入) +- 新语义:`product_tag` 字典的 **dict_value(英文 code)** 数组 + +后端**不强制校验** tags 必须在 product_tag 字典中(兼容老数据 + 留前端逐步迁移空间)。 + +--- + +## 前端要做的事 + +### 1. 字典管理页 —— CRUD 表单加「颜色值」输入 + +- 在 `dictLabel` / `icon` 旁边加一个「颜色值」输入框 +- 推荐控件:**ColorPicker**(最佳);或简单 hex 文本框(placeholder:`#FF5722`,可为空) +- **校验**:前端可不做(后端会校验),但建议加 hex 正则提示提升体验 +- 列表展示:在 dictLabel 旁渲染一个小色块(``),无 color 时不渲染或灰色占位 + +### 2. dict store —— 自动透传,无需改代码 + +- `useDictStore` 内部按 `/dict/all` 整个对象塞入,每个 dataItem 多一个 `color` key 自动透传给业务页面 +- `getDictOptions(dictType)` 返回的每项都会带 `color`(如果配了) +- 业务侧使用:`option.color || '#999'`(默认色逻辑由前端决定) + +### 3. 产品线(主题)编辑弹窗 —— 标签控件改造 + +**这是本次最大的前端改造点。** 文件位置参考:`hl-ui/src/views/product-v2/line/index.vue:312` 附近。 + +**控件替换**: + +- 旧:`n-dynamic-tags`(用户自由输入字符串) +- 新:`n-select` 多选模式,options 来自 `useDictStore().getDictOptions('product_tag')` + +```js +// 伪代码示意 +const tagOptions = computed(() => + dictStore.getDictOptions('product_tag').map(item => ({ + label: item.dictLabel, // 中文展示 + value: item.dictValue, // 英文 code 入库 + color: item.color, // 用于自定义渲染 + })) +) +``` + +**多选无上限**(沿用现有自由度)。 + +**自定义渲染**(重点): + +- `n-select` 的 `render-tag` / `render-label` slot 用 `option.color` 给 chip 上色 +- 卡片展示标签也按 color 渲染(`hl-ui/src/views/product-v2/line/index.vue:125-136` 那块卡片标签 chip) + +```vue + + {{ option.label }} + +``` + +### 4. 老数据兼容(关键) + +现有 `product_line.tags` 里很可能存的是老的**自由文本**(中文随手输入),不是字典 code: + +- **不要把 tags 里不在 product_tag 字典里的值过滤掉** —— 这会悄悄丢数据 +- **建议展示**:老 tag 值在 n-select 里按字符串原样回填(`v-model:value` 数组里塞进去),找不到对应 option 时按文本展示,灰色提示「已下架/未识别标签」 +- 用户保存时引导用户从 product_tag 字典里重新选,保存后老值就被覆盖 + +后端**保存时不校验** tags 必须在字典中,所以前端就算把老中文文本一起传上来也不会被拒。 + +--- + +## 兼容性 / 不影响的地方 + +- **老字典 color 为 null 时**:`/dict/all` 响应里就没有 `color` 这个 key(NON_NULL),前端按"无 color = 默认色(建议 #999 灰)"处理即可 +- **老 product_line.tags 自由文本仍可显示**:n-select 多选模式可以容纳"不在 options 里的值"(按 NaiveUI 文档 `tag` 模式或回退到字符串展示) +- **mp 端产品线展示**:当前 mp 端 `MpProductLineVO` **没有 tags 字段**,本次也不加。如果以后小程序要展示标签 + 颜色,等明确需求再做 + +--- + +## 不在本次范围 + +- ❌ **不预置 product_tag 字典数据**:交付能力,wx 在字典管理页手动录入业务标签项 +- ❌ **不做老 product_line.tags 数据迁移**:现存自由文本由用户后续手工编辑替换 +- ❌ **不动 ProductDO 的 tags 字段**(产品本身的 tags 走另一个 product_tag 字典使用,已是单选,见 [2026-04-20_product-tag-single-select.md](./2026-04-20_product-tag-single-select.md))—— 本次只动「产品线/主题」的 tags +- ❌ **不动 mp 端**:`/mp/product/line/*` 接口 tags 不返回,零前端改造影响 + +--- + +## 验证 + +- ✅ 测试环境 hl-user-service 已部署(commit 0dc2cb8c) +- ✅ POST /admin/dict/data 带 `color` 字段保存成功 +- ✅ GET /dict/all 配过 color 的项响应带 color,未配的项无 color key +- ✅ POST /admin/dict/data 传 `color: "red"` 返回 400 校验错误 +- ⏳ 前端 dict 管理页加 ColorPicker + 产品线弹窗改 n-select 后,wx 在字典管理页录入 product_tag 字典项