# 字典加颜色字段 + 产品线主题标签字典化（前端要改）

**日期**：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<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` 这个 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 字典项
