docs:添加按钮简化与自定义弹窗实现计划

This commit is contained in:
2026-06-24 22:37:48 +08:00
parent 1900ba6efc
commit 637334adb8
@@ -0,0 +1,679 @@
# 小程序添加标签按钮简化与自定义弹窗 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 简化编辑资料页"添加"按钮文字、收紧字数限制到 4 字、标签不换行省略号截断、用自定义深色太空主题弹窗组件替换 `uni.showModal`
**Architecture:** 新建 `TagDialog.vue` 组件支持 input / confirm 双模式,样式与小程序深色太空主题一致;编辑资料页和管理页分别引入该组件,重构 `addCustomTag``confirmDelete``uni.showModal` 切换到自定义弹窗;标签样式增加 `white-space: nowrap` 三件套防止换行。
**Tech Stack:** UniApp 3 + Vue 3 Composition API (`<script setup>`),样式用 rpx,深色太空主题(glass-card 毛玻璃 + 紫色渐变 + 金色强调)。
**Spec 文档:** `docs/superpowers/specs/2026-06-24-tag-add-button-simplify-design.md`
---
## 文件结构总览
### 新增文件
| 文件 | 职责 |
|---|---|
| `mini-program/src/components/TagDialog.vue` | 自定义弹窗组件,input / confirm 双模式,深色太空主题样式 |
### 修改文件
| 文件 | 修改内容 |
|---|---|
| `mini-program/src/pages/onboarding/index.vue` | 按钮文字简化为" 添加"、引入 TagDialog、重构 addCustomTag、字数限制 8→4、toast 文案、`.tag-choice` 不换行 |
| `mini-program/src/pages/onboarding/tag-manage.vue` | 引入 TagDialog、重构 confirmDelete、`.tag-text` 不换行 |
---
## Task 1: 新建 TagDialog.vue 组件
**目标:** 创建自定义弹窗组件,支持 input(输入)和 confirm(确认)两种模式,样式与小程序深色太空主题一致。
**Files:**
- Create: `mini-program/src/components/TagDialog.vue`
### Step 1: 创建组件文件
创建 `mini-program/src/components/TagDialog.vue`,完整内容:
```vue
<template>
<view v-if="visible" class="tag-dialog-mask" @click="onMaskClick">
<view class="tag-dialog-card glass-card" @click.stop>
<view class="tag-dialog-title-row">
<text class="tag-dialog-gold"></text>
<text class="tag-dialog-title">{{ title }}</text>
</view>
<input
v-if="mode === 'input'"
class="tag-dialog-input"
:value="modelValue"
:placeholder="placeholder"
placeholder-class="tag-dialog-placeholder"
:maxlength="4"
@input="onInput"
@confirm="onInputConfirm"
/>
<text v-if="mode === 'confirm'" class="tag-dialog-content">{{ content }}</text>
<view class="tag-dialog-actions">
<view class="tag-dialog-btn tag-dialog-btn-cancel" @click="onCancel">
<text>取消</text>
</view>
<view class="tag-dialog-btn tag-dialog-btn-confirm" @click="onConfirm">
<text>确定</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
visible: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'input'
},
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['confirm', 'cancel', 'update:modelValue', 'update:visible'])
const onInput = (event) => {
const value = event.detail.value
emit('update:modelValue', value)
}
const onInputConfirm = () => {
emit('confirm', props.modelValue)
}
const onConfirm = () => {
if (props.mode === 'input') {
emit('confirm', props.modelValue)
} else {
emit('confirm')
}
}
const onCancel = () => {
emit('cancel')
emit('update:visible', false)
}
const onMaskClick = () => {
emit('cancel')
emit('update:visible', false)
}
</script>
<style scoped>
.tag-dialog-mask {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(3, 2, 13, 0.72);
}
.tag-dialog-card {
width: 560rpx;
box-sizing: border-box;
padding: 36rpx 32rpx 28rpx;
border-radius: 22rpx;
border: 1rpx solid rgba(155, 110, 255, 0.14);
background:
radial-gradient(circle at 92% 12%, rgba(104, 66, 255, 0.1), transparent 34%),
rgba(10, 13, 43, 0.54);
box-shadow: inset 0 0 30rpx rgba(123, 60, 255, 0.05), 0 10rpx 36rpx rgba(0, 0, 0, 0.16);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
}
.tag-dialog-title-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
}
.tag-dialog-gold {
color: #ffd58c;
font-size: 26rpx;
text-shadow: 0 0 20rpx rgba(255, 202, 125, 0.45);
}
.tag-dialog-title {
color: #fff;
font-size: 30rpx;
font-weight: 900;
}
.tag-dialog-input {
width: 100%;
box-sizing: border-box;
height: 80rpx;
margin-bottom: 28rpx;
padding: 0 20rpx;
border-radius: 14rpx;
border: 1rpx solid rgba(151, 111, 255, 0.22);
background: rgba(10, 12, 40, 0.66);
color: #fff;
font-size: 26rpx;
}
.tag-dialog-placeholder {
color: rgba(214, 204, 235, 0.42);
}
.tag-dialog-content {
display: block;
margin-bottom: 32rpx;
color: rgba(239, 232, 255, 0.86);
font-size: 26rpx;
line-height: 1.6;
}
.tag-dialog-actions {
display: flex;
gap: 20rpx;
}
.tag-dialog-btn {
flex: 1;
height: 72rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 700;
}
.tag-dialog-btn-cancel {
color: rgba(239, 232, 255, 0.82);
border: 1rpx solid rgba(151, 111, 255, 0.42);
background: rgba(255, 255, 255, 0.02);
}
.tag-dialog-btn-confirm {
color: #fff;
border: 1rpx solid rgba(206, 82, 255, 0.95);
background: linear-gradient(180deg, rgba(169, 61, 255, 0.62), rgba(107, 41, 206, 0.5));
box-shadow: 0 0 18rpx rgba(168, 67, 255, 0.52), inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
}
</style>
```
### Step 2: 提交
```bash
git add mini-program/src/components/TagDialog.vue
git commit -m "feat:新增 TagDialog 自定义弹窗组件,深色太空主题样式"
```
---
## Task 2: 编辑资料页改动(index.vue
**目标:** 简化添加按钮文字、引入 TagDialog、重构 addCustomTag 用自定义弹窗、字数限制收紧到 4、标签不换行。
**Files:**
- Modify: `mini-program/src/pages/onboarding/index.vue`
### Step 1: 模板 — 简化按钮文字
修改 `mini-program/src/pages/onboarding/index.vue`
找到性格标签 panel 末尾占位(约第 146 行):
```html
<text class="tag-choice dashed" @click="addCustomTag('personality')"> 添加标签</text>
```
改为:
```html
<text class="tag-choice dashed" @click="addCustomTag('personality')"> 添加</text>
```
找到兴趣爱好 panel 末尾占位(约第 167 行):
```html
<text class="tag-choice dashed" @click="addCustomTag('hobby')"> 添加兴趣</text>
```
改为:
```html
<text class="tag-choice dashed" @click="addCustomTag('hobby')"> 添加</text>
```
### Step 2: 模板 — 挂载 TagDialog 组件
找到 `</scroll-view>``</view>` 包裹的页面结构末尾(约第 185-186 行):
```html
</scroll-view>
</view>
</template>
```
`</scroll-view>` 之后、`</view>` 之前插入 TagDialog 挂载:
```html
</scroll-view>
<TagDialog
v-model="dialogInput"
v-model:visible="dialogVisible"
:mode="dialogMode"
:title="dialogTitle"
:placeholder="dialogPlaceholder"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
</view>
</template>
```
### Step 3: script — 引入组件
找到 import 区(约第 189-193 行):
```javascript
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
```
在最后一行 import 之后追加:
```javascript
import TagDialog from '../../components/TagDialog.vue'
```
### Step 4: script — 新增弹窗响应式状态
找到 `const saving = ref(false)` 这一行(约第 198 行附近,在 `const isEdit = ref(false)` 之后),在其下方追加:
```javascript
const dialogVisible = ref(false)
const dialogMode = ref('input')
const dialogTitle = ref('')
const dialogPlaceholder = ref('')
const dialogContent = ref('')
const dialogInput = ref('')
const dialogAction = ref(null)
```
### Step 5: script — 重构 addCustomTag 并新增 handleAddTag / onDialogConfirm / onDialogCancel
找到现有的 `addCustomTag` 函数(约第 410-449 行,从 `/**` 注释到 `}` 结束):
```javascript
/**
* 新增自定义标签(性格标签 / 兴趣爱好通用)
* @param {string} type 'personality' 或 'hobby'
*/
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
const title = isPersonality ? '添加性格标签' : '添加兴趣爱好'
uni.showModal({
title,
editable: true,
placeholderText: '请输入标签(最多8个字符)',
success: (res) => {
const value = String(res.content || '').trim()
if (!res.confirm) return
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 8) {
uni.showToast({ title: '标签最多8个字符', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
// 新标签插入到库数组最前面
library.unshift(value)
// 若已选未满 5 个,自动加入已选;否则仅提示
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
})
}
```
完整替换为以下四个函数:
```javascript
/**
* 打开添加标签弹窗
* @param {string} type 'personality' 或 'hobby'
*/
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
}
/**
* 处理添加标签的实际逻辑(字数限制 4 字)
*/
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)
// 若已选未满 5 个,自动加入已选;否则仅提示
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
/**
* 弹窗确认回调
*/
const onDialogConfirm = (value) => {
const action = dialogAction.value
if (!action) return
if (action.kind === 'add') {
handleAddTag(action.type, value)
}
dialogAction.value = null
dialogVisible.value = false
}
/**
* 弹窗取消回调
*/
const onDialogCancel = () => {
dialogAction.value = null
dialogVisible.value = false
}
```
### Step 6: 样式 — .tag-choice 不换行
找到 `.tag-choice` 样式(约第 1008-1011 行):
```css
.tag-choice {
height: 40rpx;
font-size: 21rpx;
}
```
改为:
```css
.tag-choice {
height: 40rpx;
font-size: 21rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
```
### Step 7: 提交
```bash
git add mini-program/src/pages/onboarding/index.vue
git commit -m "feat:编辑页添加按钮简化为添加,字数限制4字,标签不换行,接入自定义弹窗"
```
---
## Task 3: 管理页改动(tag-manage.vue
**目标:** 引入 TagDialog、重构 confirmDelete 用自定义弹窗、标签文字不换行。
**Files:**
- Modify: `mini-program/src/pages/onboarding/tag-manage.vue`
### Step 1: 模板 — 挂载 TagDialog 组件
找到 `</scroll-view>``</view>` 包裹的页面结构末尾(约第 44-46 行):
```html
</scroll-view>
</view>
</template>
```
`</scroll-view>` 之后、`</view>` 之前插入 TagDialog 挂载:
```html
</scroll-view>
<TagDialog
v-model:visible="dialogVisible"
mode="confirm"
:title="dialogTitle"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
</view>
</template>
```
### Step 2: script — 引入组件
找到 import 区(约第 48-51 行):
```javascript
import { computed, onMounted, reactive, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
```
在最后一行 import 之后追加:
```javascript
import TagDialog from '../../components/TagDialog.vue'
```
### Step 3: script — 新增弹窗响应式状态
找到 `const type = ref('personality')` 这一行(约第 56 行),在其下方追加:
```javascript
const dialogVisible = ref(false)
const dialogTitle = ref('确认删除')
const dialogContent = ref('')
const pendingTag = ref(null)
```
### Step 4: script — 重构 confirmDelete 并新增 onDialogConfirm / onDialogCancel
找到现有的 `confirmDelete` 函数(约第 127-139 行):
```javascript
/**
* 删除确认
*/
const confirmDelete = (tag) => {
uni.showModal({
title: '确认删除',
content: `确定删除「${tag}」吗?删除后不可恢复`,
success: (res) => {
if (!res.confirm) return
deleteTag(tag)
}
})
}
```
完整替换为以下三个函数:
```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
}
```
### Step 5: 样式 — .tag-text 不换行
找到 `.tag-text` 样式(约第 311-314 行):
```css
.tag-text {
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
}
```
改为:
```css
.tag-text {
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
```
### Step 6: 提交
```bash
git add mini-program/src/pages/onboarding/tag-manage.vue
git commit -m "feat:管理页删除确认接入自定义弹窗,标签文字不换行"
```
---
## Task 4: H5 浏览器验证
**目标:** 在 H5 模式下验证 spec 的 8 条验收标准。
### Step 1: 确认 H5 dev server 运行
```bash
python dev-services.py status
```
确认 `Emotion-museum-uniapp 前端` 状态为 `运行中`(端口 5175)。若未运行:
```bash
cd mini-program
npm run dev:h5
```
### Step 2: 逐条验证验收标准
在浏览器中打开 `http://localhost:5175`,登录后进入编辑资料页(`?edit=1`):
| # | 验收标准 | 验证方法 |
|---|---|---|
| 1 | 添加按钮只显示"+ 添加" | 观察性格标签、兴趣爱好 panel 末尾占位文字 |
| 2 | 点击"+ 添加"弹出深色太空主题弹窗 | 点击按钮,验证弹窗为毛玻璃卡片 + 紫色渐变按钮 + 金色 ✦,非白底蓝按钮 |
| 3 | 输入 5 字 toast 提示"内容不能超过4个字" | 输入 5 个字点确定,验证 toast 文案 |
| 4 | 输入 4 字以内新标签,确认后出现在最前 | 输入"测试"点确定,验证标签出现在列表第一位 |
| 5 | 标签过长时省略号截断不换行 | 输入 4 字标签,观察窄屏下是否单行省略号 |
| 6 | 管理页删除弹窗为深色主题 | 点"管理"进入管理页,点 × 或左滑,验证弹窗样式与文案 |
| 7 | 取消按钮和遮罩层点击都能关闭弹窗 | 分别测试取消按钮、点击遮罩层,验证弹窗关闭且不触发动作 |
| 8 | 弹窗 H5 下样式与深色主题一致 | 整体视觉检查 |
### Step 3: 验收通过后报告
所有 8 条通过后,确认工作区干净:
```bash
git status
```
---
## 执行顺序建议
1. Task 1(新建组件)→ Task 2(编辑页接入)→ Task 3(管理页接入)→ Task 4(验证)
2. Task 1 必须先完成,Task 2/3 依赖组件存在。
3. Task 2/3 完成后,H5 dev server 热加载自动生效,可直接进入 Task 4 验证。