feat: 字典加颜色字段 + 产品线主题标签字典化 (PR #1017)

这个提交包含在:
API Changelog Bot 2026-04-20 18:49:47 +08:00
父节点 8dafe1b9cb
当前提交 42ba23bba6

查看文件

@ -0,0 +1,198 @@
# 字典加颜色字段 + 产品线主题标签字典化(前端要改)
**日期**2026-04-20
**PR**#1017Closes 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<String>` | 注释从「标签」改为「标签(product_tag字典的dict_value英文code列表,无数量上限,可为空)」+ example=`PARENT_CHILD,SELF_DRIVE` |
| `ProductLineRespVO.tags` | `List<String>` | 注释从「产品线标签」改为「产品线标签(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 旁渲染一个小色块(`<span style="background:{color}">`),无 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
<n-tag :color="{ color: option.color || '#999', textColor: '#fff' }">
{{ option.label }}
</n-tag>
```
### 4. 老数据兼容(关键)
现有 `product_line.tags` 里很可能存的是老的**自由文本**(中文随手输入),不是字典 code
- **不要把 tags 里不在 product_tag 字典里的值过滤掉** —— 这会悄悄丢数据
- **建议展示**:老 tag 值在 n-select 里按字符串原样回填(`v-model:value` 数组里塞进去),找不到对应 option 时按文本展示,灰色提示「已下架/未识别标签」
- 用户保存时引导用户从 product_tag 字典里重新选,保存后老值就被覆盖
后端**保存时不校验** tags 必须在字典中,所以前端就算把老中文文本一起传上来也不会被拒。
---
## 兼容性 / 不影响的地方
- **老字典 color 为 null 时**`/dict/all` 响应里就没有 `color` 这个 keyNON_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 字典项