---
author: Peanut
created_at: 2026-06-24
purpose: 明确小程序编辑资料页"性格标签"与"兴趣爱好"两个模块的扩展设计：支持用户新增自定义标签、统一管理标签库、删除标签，并保证编辑页标签容器自适应撑开展示所有标签。
---

# 小程序编辑资料页：性格标签 / 兴趣爱好 标签库扩展设计

## 背景

当前小程序的编辑资料页面（`mini-program/src/pages/onboarding/index.vue`）已具备性格标签和兴趣爱好的基础选择功能：

- 性格标签预设 11 个（理性、感性、乐观、独立、有创造力、坚韧、细腻、好奇、内敛、冒险、自由）。
- 兴趣爱好预设 10 个（阅读、旅行、音乐、写作、摄影、电影、运动、绘画、咖啡、游戏）。
- 兴趣爱好模块支持通过弹窗输入自定义标签。
- 性格标签模块 UI 上已有"＋ 添加标签"占位，但未绑定事件。
- 两个模块均不支持删除标签，也没有统一的管理入口。
- 后端 `UserProfile` 实体只保存"用户已选中的标签"两个 JSON 字段（`personality_tags` / `hobbies`），不区分预设与自定义，也没有完整标签库的概念。

用户新增需求：性格标签和兴趣爱好两个模块均支持**用户自由新增自定义标签、保存、回显、删除**；编辑资料页的标签容器要**自适应撑开**，保证所有标签都能展示；**新增的标签展示在最前面**。

## 目标

1. 性格标签和兴趣爱好两个模块支持用户新增自定义标签、保存、回显、删除。
2. 新增的自定义标签展示在列表最前面。
3. 编辑资料页标签容器自适应撑开，不限行数，展示所有标签。
4. 提供独立的"管理标签"页面，支持统一删除（含预设与自定义）。
5. 老用户数据平滑迁移，不破坏现有已选标签。
6. 输入校验完整，避免重复、空值、超长。

## 非目标

- 不引入标签的分类、排序、分组等高级管理能力。
- 不做标签的全局共享、推荐、热门度等运营功能。
- 不修改后端存储架构（继续用 JSON 字段，不拆子表）。
- 不在编辑资料页做长按手势删除，统一收敛到管理页。

## 关键设计决策

| 决策点 | 选择 | 理由 |
|---|---|---|
| 新增标签输入方式 | `uni.showModal` 弹窗输入 | 与现有"自定义兴趣"交互保持一致，改动成本最低 |
| 删除入口 | 独立的"管理标签"页面 | 统一删除操作入口，避免编辑页交互过载；预设与自定义标签都能被删除 |
| 删除后的可见性 | 删除后从当前用户账号彻底移除 | 用户希望不再看到的标签就不会再出现 |
| 数据存储 | `UserProfile` 新增 `personality_tag_library` / `hobby_library` 两个 JSON 字段 | 后端需区分"用户已选中"与"用户可见的完整标签库"，原有字段语义保留不变 |
| 预设 vs 自定义标签 | 前端维护一份内置预设数组；库里存的是用户专属合并后的字符串数组 | 预设标签库存在代码中便于维护，用户库字段只关心"当前账号可见的全部标签" |
| 编辑页标签顺序 | 新增的自定义标签 → 已选中的其他标签 → 其余未选中标签 | 严格保证"新增的标签展示在最前面"，其他已选中标签紧随其后 |

## 后端改动

### 实体与数据库

`server` 模块：

- `UserProfile` 实体新增两个字段：

| 字段名 | Java 类型 | 数据库列 | 类型 | 说明 |
|---|---|---|---|---|
| `personalityTagLibrary` | `String` | `personality_tag_library` | `TEXT` | 用户的完整性格标签库，JSON 字符串（字符串数组） |
| `hobbyLibrary` | `String` | `hobby_library` | `TEXT` | 用户的完整兴趣爱好库，JSON 字符串（字符串数组） |

- 原 `personalityTags` / `hobbies` 字段保持不变，继续表示"用户已选中的标签"。
- 新增数据库迁移 SQL：

```sql
ALTER TABLE user_profile
  ADD COLUMN personality_tag_library TEXT DEFAULT NULL COMMENT '用户完整性格标签库(JSON数组)',
  ADD COLUMN hobby_library TEXT DEFAULT NULL COMMENT '用户完整兴趣爱好库(JSON数组)';
```

### 接口变更

`UserProfileController` / `UserProfileService` 无需新增接口，复用现有：

- `PUT /user-profile/update`：入参 DTO 增加 `personalityTagLibrary` / `hobbyLibrary`（均为 `String`），直接持久化。
- `GET /user-profile/me`：返回体增加上述两个字段。
- `POST /user-profile/create`：同上。

### 数据兼容性

- 老用户（`personalityTagLibrary` 为空或 `null`）查询接口返回 `null`，由前端做兜底初始化。
- 后端不做兜底填充，严格遵守"没有就是没有"的禁止 mock 规则。

## 前端改动

### 数据层（`stores/app.js` 与 `services/userProfile.js`）

- 请求返回的 `userProfile` 新增 `personalityTagLibrary` / `hobbyLibrary` 字段解析（JSON.parse）。
- `saveUserProfile` 提交时把两个库字段 `JSON.stringify` 后一并提交。

### 编辑资料页（`mini-program/src/pages/onboarding/index.vue`）

#### 标签区结构（性格标签与兴趣爱好同构）

```
┌──────────────────────────────────────┐
│ 😊 性格标签               管理 >    │  ← 标题行，右侧"管理"按钮跳转管理页
│ （最多选择5个）                       │
│ ┌────┐┌────┐┌────┐┌────┐            │
│ │ 理性 ││ 感性 ││ 乐观 ││ 独立 │...  │  ← 标签网格，flex-wrap 自适应
│ └────┘└────┘└────┘└────┘            │
│ ┌───────┐                           │
│ │ ＋ 添加 │                         │  ← 末尾占位，点击打开弹窗
│ └───────┘                           │
└──────────────────────────────────────┘
```

- 容器使用 `display: flex; flex-wrap: wrap;`，**高度随内容自适应撑开**。
- 标签顺序规则（严格保证"新增的标签展示在最前面"）：
  1. **新增的自定义标签**排在最前面（按创建时间倒序）。
  2. **已选中的其他标签**（含预设与早期自定义）紧随其后。
  3. **其余未选中的预设标签**排在最后。
- 样式沿用现有 `.tag-choice` / `.tag-choice.active`。
- 点击标签：沿用 `toggleList(list, tag, 5)` 逻辑，超限 toast 提示。

#### 新增标签弹窗

- 点击"＋ 添加标签"触发 `uni.showModal`，`editable: true`，占位符"请输入标签"。
- 校验规则：
  - 非空、去除首尾空格。
  - 长度 ≤ 8 个字符。
  - 不能和当前库中任何标签完全相等（字符串完全一致比对）。
- 校验失败通过 `uni.showToast` 提示原因，不静默忽略。
- 校验通过后：
  1. 新标签**插入到对应库数组的最前面**。
  2. 若已选未满 5 个，自动加入已选数组；满则 toast 提示"已达上限，请先进入管理页删除其他标签"，仍把新标签加入库，但不加入已选。

#### 保存流程

- 点击"保存"按钮时，把以下数据一起提交：
  - `personalityTags`（已选中数组）
  - `personalityTagLibrary`（完整库数组）
  - `hobbies`（已选中数组）
  - `hobbyLibrary`（完整库数组）
- 保存失败：toast 提示后端返回的真实错误信息，页面状态保留不变。
- 保存成功：toast 提示"已保存"，350ms 后 `uni.navigateBack()`。

### 管理页（新增独立页面）

路径：`mini-program/src/pages/onboarding/tag-manage.vue`。
通过 `pages.json` 注册路由：`/pages/onboarding/tag-manage`。

#### 入口与参数

- 编辑资料页的"管理"按钮跳转：`/pages/onboarding/tag-manage?type=personality` 或 `?type=hobby`。
- 页面标题：根据 type 显示"管理性格标签"或"管理兴趣爱好"。

#### 页面结构

- 列表渲染当前库中全部标签（含剩余预设 + 自定义）。
- 每行结构：

```
┌────────────────────────────────────┐
│  理性                              × │
├────────────────────────────────────┤
│  感性                              × │
├────────────────────────────────────┤
│  ...                                 │
└────────────────────────────────────┘
```

- **左滑删除**：使用 UniApp 的 `uni-swipe-action` 组件或自定义实现，向右滑动显示"删除"按钮。
- **点击 × 按钮**：同样触发删除。
- 删除前 `uni.showModal` 二次确认，提示"确定删除该标签吗？删除后不可恢复"。
- 删除后：
  - 从库数组中移除该标签。
  - 若该标签同时在已选数组中，一并移除。
  - 页面列表立即刷新。
- 删除接口调用：删除动作本身不单独调用后端，仅在用户返回编辑资料页点击"保存"时统一提交。

#### 返回与数据同步

- 管理页的库数据存储在页面局部响应式变量中。
- 返回编辑资料页时，通过 `onUnload` 生命周期或全局 store 的 `updateRegistration` 把最新库数据回传给编辑资料页。
- 推荐方案：使用 `uni.$emit('tag-library-updated', { type, library })` 事件，编辑资料页在 `onShow` 中监听并合并。

## 数据初始化兼容方案

编辑资料页在 `onLoad` / `onShow` 时执行：

```
if (后端返回的 personalityTagLibrary 为空) {
  库 = [...内置预设数组]
  if (后端返回的 personalityTags 非空) {
    // 把已选中的标签也合入库（老用户首次编辑时保留）
    库 = 去重合并(库, 已选数组)
  }
}
```

兴趣爱好同理。此逻辑保证老用户数据不丢失，新用户直接拿预设库。

## 错误处理

- 接口请求失败：按现有 `request.js` 约定，`try/catch` 返回 `{ success: false, error: 真实错误 }`，页面 toast 提示真实错误信息。**禁止本地 mock 成功响应、禁止兜底默认值**。
- 管理页删除：若保存时接口失败，本地已删除的标签状态**回滚**（恢复删除前的库内容），toast 提示失败原因。
- 网络超时：按 `request.js` 的现有超时策略处理。

## 文件清单

### 新增文件

- `mini-program/src/pages/onboarding/tag-manage.vue`：管理标签独立页面。

### 修改文件

**后端（`server/src/main/java/com/emotion/`）：**

- `entity/UserProfile.java`：新增 `personalityTagLibrary`、`hobbyLibrary` 两个 `String` 字段。
- `controller/UserProfileController.java`：无需新增接口，入参 DTO 通过 `UserProfileUpdateRequest` / `UserProfileCreateRequest` 已支持新字段透传。
- `dto/request/userprofile/UserProfileUpdateRequest.java`：新增 `personalityTagLibrary`、`hobbyLibrary` 两个 `String` 字段。
- `dto/request/userprofile/UserProfileCreateRequest.java`：同上。
- `dto/response/userprofile/UserProfileResponse.java`：新增 `personalityTagLibrary`、`hobbyLibrary` 两个 `String` 字段。
- `service/UserProfileService.java` 与 `service/impl/UserProfileServiceImpl.java`：无业务逻辑改动，字段通过 MyBatis-Plus 自动映射持久化。
- 数据库迁移脚本：`server/src/main/resources/db/migration/2026-06-24-user-profile-tag-library.sql`，遵循现有 `YYYY-MM-DD-描述.sql` 命名约定。

**前端（`mini-program/`）：**

- `src/pages/onboarding/index.vue`：重构标签区，新增管理入口、弹窗新增逻辑、自适应样式。
- `src/services/userProfile.js`：请求体与响应解析增加两个库字段。
- `src/stores/app.js`：`saveUserProfile` 提交时携带库字段。
- `src/pages.json`：注册 `tag-manage` 路由。

**不修改的文件：**

- `src/pages/main/MineView.vue`：展示区只读"已选中的标签"（沿用 `hobbies` 字段），不需要感知完整库，**无需修改**。

## 验收标准

1. 进入编辑资料页，性格标签、兴趣爱好两个区域的标签容器高度随内容自适应撑开，无截断。
2. 点击"＋ 添加标签"或"＋ 自定义兴趣"，弹窗输入后，新标签出现在列表最前面。
3. 重复输入同名标签，toast 提示"标签已存在"。
4. 空输入、超长输入（>8 字符）分别 toast 提示对应原因。
5. 点击"管理"按钮，跳转到管理页，列表展示全部标签（预设 + 自定义）。
6. 管理页左滑或点 × 删除标签，二次确认后标签从列表消失。
7. 删除已在"已选"中的标签，返回编辑资料页后已选列表同步移除。
8. 保存后重新进入编辑资料页，新增的标签仍展示在最前面。
9. 老用户（无新字段）首次进入编辑资料页，已选标签保留，预设标签可见。
10. 接口失败时，页面保留原状态并 toast 真实错误，不静默成功。
