---
author: Peanut
created_at: 2026-06-24
purpose: 明确小程序编辑资料页"添加"按钮文字简化、字数限制收紧到4字、标签不换行、弹窗替换为深色太空主题自定义组件的改动设计。
---

# 小程序添加标签按钮简化与自定义弹窗设计

## 背景

当前小程序编辑资料页（`mini-program/src/pages/onboarding/index.vue`）和管理标签页（`mini-program/src/pages/onboarding/tag-manage.vue`）的"添加"按钮和弹窗存在以下问题：

- 编辑页性格标签的添加按钮文字是"＋ 添加标签"，兴趣爱好是"＋ 添加兴趣"，文字偏长，与紧凑的标签网格不协调。
- 添加标签的字数上限是 8 个字符，偏宽松，容易生成过长的标签破坏网格布局。
- 标签 `.tag-choice` / `.tag-text` 没有设置 `white-space: nowrap`，长标签会自动换行撑高单元格。
- 添加和删除确认都使用 `uni.showModal`，在 H5 模式下是 uni-app 默认的白底蓝按钮样式，与小程序整体的深色太空主题（深紫底、毛玻璃、紫色渐变、金色强调）严重不一致。

## 目标

1. 编辑页两个添加按钮文字统一简化为"＋ 添加"。
2. 添加标签字数限制收紧到 4 个字。
3. 标签文字过长时不换行，用省略号截断。
4. 添加和删除确认弹窗替换为自定义弹窗组件，样式与深色太空主题保持一致。

## 非目标

- 不改标签的选择/取消选中逻辑、最多 5 个的限制。
- 不改后端接口或数据结构。
- 不改管理页的左滑删除手势。
- 不引入第三方 UI 组件库。

## 关键设计决策

| 决策点 | 选择 | 理由 |
|---|---|---|
| 添加按钮文字 | "＋ 添加"（去掉"标签"/"兴趣"后缀） | 用户明确要求只保留"添加"两字 |
| 弹窗 title | 保持 `添加性格标签` / `添加兴趣爱好` | 按钮简、弹窗清，分开处理 |
| 字数限制 | 4 个字 | 用户明确要求 |
| toast 文案 | "内容不能超过4个字" | 用户指定 |
| 不换行实现 | `white-space: nowrap` + `overflow: hidden` + `text-overflow: ellipsis` | 标准三件套，标签固定高度不撑高 |
| 弹窗实现 | 新建自定义组件 `TagDialog.vue`，两种模式（input / confirm） | 替换 `uni.showModal`，样式可控、深色主题统一 |

## 新增组件

### `mini-program/src/components/TagDialog.vue`

**职责**：统一的弹窗组件，支持输入模式和确认模式，样式与深色太空主题一致。

**Props**：

| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `visible` | `Boolean` | `false` | 是否显示弹窗 |
| `mode` | `String` | `'input'` | `'input'` 或 `'confirm'` |
| `title` | `String` | `''` | 弹窗标题 |
| `content` | `String` | `''` | confirm 模式的提示文案 |
| `placeholder` | `String` | `''` | input 模式的输入框占位符 |
| `modelValue` | `String` | `''` | input 模式的输入值（支持 v-model） |

**Emits**：

| 事件 | 参数 | 说明 |
|---|---|---|
| `confirm` | input 模式：当前输入值；confirm 模式：无 | 点击确定按钮 |
| `cancel` | 无 | 点击取消按钮或遮罩层 |
| `update:modelValue` | 新值 | 输入框内容变化 |
| `update:visible` | `false` | 关闭弹窗 |

**模板结构**：

```
┌────────────────────────────────┐
│  遮罩层（半透明深色）            │
│  ┌──────────────────────────┐  │
│  │  ✦ 添加性格标签           │  │  ← 标题 + 金色 ✦
│  │  ┌────────────────────┐  │  │
│  │  │  输入框（深色底紫边）│  │  │  ← input 模式才有
│  │  └────────────────────┘  │  │
│  │  确定删除「xxx」吗？...   │  │  ← confirm 模式才有
│  │                          │  │
│  │  [ 取消 ]   [ 确定 ]     │  │  ← 按钮
│  └──────────────────────────┘  │
└────────────────────────────────┘
```

**样式**（与小程序深色太空主题一致）：
- 遮罩层：`rgba(3, 2, 13, 0.72)` 全屏覆盖
- 弹窗卡片：`glass-card` 毛玻璃 + `rgba(155, 110, 255, 0.14)` 紫色边框 + `22rpx` 圆角
- 标题：白色 `#fff` 加粗，配金色 `✦`（`#ffd58c`）
- 输入框：深色底 `rgba(10, 12, 40, 0.66)` + 紫色边框 `rgba(151, 111, 255, 0.22)`
- 取消按钮：透明底 + 紫色边框
- 确定按钮：紫色渐变 `linear-gradient(180deg, #a855ff, #6b29ce)`
- 所有尺寸用 `rpx`

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

### 模板

- 性格标签 panel 末尾占位：`＋ 添加标签` → `＋ 添加`
- 兴趣爱好 panel 末尾占位：`＋ 添加兴趣` → `＋ 添加`
- 在 `<scroll-view>` 之外、`</view>` 之前挂载 `<TagDialog>` 组件：

```html
<TagDialog
  v-model="dialogInput"
  v-model:visible="dialogVisible"
  :mode="dialogMode"
  :title="dialogTitle"
  :placeholder="dialogPlaceholder"
  :content="dialogContent"
  @confirm="onDialogConfirm"
  @cancel="onDialogCancel"
/>
```

### script setup

- 引入组件：`import TagDialog from '../../components/TagDialog.vue'`
- 新增响应式状态：

```javascript
const dialogVisible = ref(false)
const dialogMode = ref('input')              // 'input' 或 'confirm'
const dialogTitle = ref('')
const dialogPlaceholder = ref('')
const dialogContent = ref('')
const dialogInput = ref('')
const dialogAction = ref(null)               // 记录当前弹窗对应的动作：{ kind: 'add'|'delete', type: 'personality'|'hobby', tag?: string }
```

- 重构 `addCustomTag`：改为打开弹窗（input 模式），不再调 `uni.showModal`：

```javascript
const addCustomTag = (type) => {
  const isPersonality = type === 'personality'
  dialogMode.value = 'input'
  dialogTitle.value = isPersonality ? '添加性格标签' : '添加兴趣爱好'
  dialogPlaceholder.value = '请输入标签（最多4个字）'
  dialogInput.value = ''
  dialogAction.value = { kind: 'add', type }
  dialogVisible.value = true
}
```

- 新增 `onDialogConfirm`：处理弹窗确认（add / delete 两种动作）：

```javascript
const onDialogConfirm = (value) => {
  const action = dialogAction.value
  if (!action) return
  if (action.kind === 'add') {
    handleAddTag(action.type, value)
  } else if (action.kind === 'delete') {
    handleDeleteTag(action.type, action.tag)
  }
  dialogVisible.value = false
}

const onDialogCancel = () => {
  dialogAction.value = null
  dialogVisible.value = false
}
```

- 抽出 `handleAddTag`（原 `addCustomTag` 的 success 回调逻辑，字数限制改为 4）：

```javascript
const handleAddTag = (type, rawValue) => {
  const value = String(rawValue || '').trim()
  const isPersonality = type === 'personality'
  const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
  const selected = isPersonality ? form.personalityTags : form.hobbies

  if (!value) {
    uni.showToast({ title: '请输入标签内容', icon: 'none' })
    return
  }
  if (value.length > 4) {
    uni.showToast({ title: '内容不能超过4个字', icon: 'none' })
    return
  }
  if (library.includes(value)) {
    uni.showToast({ title: '标签已存在', icon: 'none' })
    return
  }
  library.unshift(value)
  if (selected.length >= 5) {
    uni.showToast({ title: '已达上限，仅加入标签库', icon: 'none' })
    return
  }
  selected.push(value)
}
```

### 样式

`.tag-choice` 增加：

```css
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
```

## 管理页改动（`mini-program/src/pages/onboarding/tag-manage.vue`）

### 模板

挂载 `<TagDialog>` 组件（confirm 模式）：

```html
<TagDialog
  v-model:visible="dialogVisible"
  :mode="'confirm'"
  :title="dialogTitle"
  :content="dialogContent"
  @confirm="onDialogConfirm"
  @cancel="onDialogCancel"
/>
```

### script setup

- 引入组件：`import TagDialog from '../../components/TagDialog.vue'`
- 新增响应式状态：

```javascript
const dialogVisible = ref(false)
const dialogTitle = ref('确认删除')
const dialogContent = ref('')
const pendingTag = ref(null)
```

- 重构 `confirmDelete`：改为打开弹窗（confirm 模式）：

```javascript
const confirmDelete = (tag) => {
  pendingTag.value = tag
  dialogContent.value = `确定删除「${tag}」吗？删除后不可恢复`
  dialogVisible.value = true
}

const onDialogConfirm = () => {
  if (pendingTag.value) {
    deleteTag(pendingTag.value)
    pendingTag.value = null
  }
  dialogVisible.value = false
}

const onDialogCancel = () => {
  pendingTag.value = null
  dialogVisible.value = false
}
```

- `deleteTag` 函数保持不变（已经在 Task 6 实现）。

### 样式

`.tag-text` 增加：

```css
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
```

## 文件清单

### 新增文件

- `mini-program/src/components/TagDialog.vue`：自定义弹窗组件。

### 修改文件

- `mini-program/src/pages/onboarding/index.vue`：按钮文字简化、字数限制 4、不换行样式、引入 TagDialog、重构 addCustomTag。
- `mini-program/src/pages/onboarding/tag-manage.vue`：引入 TagDialog、重构 confirmDelete、不换行样式。

## 验收标准

1. 编辑页性格标签和兴趣爱好的添加按钮只显示"＋ 添加"。
2. 点击"＋ 添加"弹出深色太空主题弹窗（毛玻璃卡片、紫色渐变、金色 ✦），不再是白底蓝按钮。
3. 输入 5 个字时 toast 提示"内容不能超过4个字"。
4. 输入 4 个字以内的新标签，确认后标签出现在列表最前面。
5. 标签文字过长时（如 4 字标签在窄屏），单元格内省略号截断，不换行撑高。
6. 管理页点 × 或左滑删除时，弹出深色主题确认弹窗，文案"确定删除「xxx」吗？删除后不可恢复"。
7. 弹窗的取消按钮和遮罩层点击都能关闭弹窗，不触发任何动作。
8. 弹窗在 H5 模式下样式与小程序深色太空主题一致。
