feat:新增管理标签独立页面,支持左滑与 × 按钮删除

This commit is contained in:
2026-06-24 21:56:46 +08:00
parent 53e895797e
commit 843ddd4c69
@@ -0,0 +1,356 @@
<template>
<view class="manage-page">
<view class="space-bg"></view>
<view class="status-space" :style="{ height: capsuleTopReservePx + 'px' }"></view>
<view class="topbar">
<text class="back" @click="goBack"></text>
<text class="title">{{ pageTitle }}</text>
<text class="placeholder-slot"></text>
</view>
<scroll-view class="scroll" scroll-y :show-scrollbar="false">
<view class="panel glass-card">
<view class="section-head">
<text class="section-title">全部标签</text>
<text class="section-hint"> {{ library.length }} </text>
</view>
<view v-if="library.length === 0" class="empty-tip">
<text>暂无标签</text>
</view>
<view
v-for="(tag, index) in library"
:key="tag"
class="tag-row"
@touchstart="onTouchStart($event, index)"
@touchmove="onTouchMove($event, index)"
@touchend="onTouchEnd($event, index)"
>
<view class="tag-row-inner" :style="{ transform: `translateX(${swipeOffsets[index] || 0}rpx)` }">
<view class="tag-row-main">
<text class="tag-text">{{ tag }}</text>
</view>
<view class="tag-row-delete" @click.stop="confirmDelete(tag)">
<text class="delete-text">删除</text>
</view>
</view>
<view class="tag-row-close" @click.stop="confirmDelete(tag)">
<text class="close-icon">×</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
const store = useAppStore()
const { capsuleTopReservePx } = useMenuButtonSafeArea({ extraTopPx: 2 })
const type = ref('personality')
const library = ref([])
const selected = ref([])
const swipeOffsets = reactive({})
// 预设数组(与编辑资料页保持一致)
const personalityTags = ['理性', '感性', '乐观', '独立', '有创造力', '坚韧', '细腻', '好奇', '内敛', '冒险', '自由']
const hobbyOptions = ['阅读', '旅行', '音乐', '写作', '摄影', '电影', '运动', '绘画', '咖啡', '游戏']
const pageTitle = computed(() => (type.value === 'personality' ? '管理性格标签' : '管理兴趣爱好'))
/**
* 从 store 初始化库数据(与编辑页 syncFromStore 同逻辑)
*/
const buildInitialLibrary = (preset, selectedArr) => {
const lib = [...preset]
if (Array.isArray(selectedArr)) {
selectedArr.forEach((tag) => {
if (!lib.includes(tag)) lib.push(tag)
})
}
return lib
}
const initFromStore = () => {
const source = store.userProfile || store.registrationData || {}
const isPersonality = type.value === 'personality'
const preset = isPersonality ? personalityTags : hobbyOptions
const selectedArr = isPersonality ? source.personalityTags : source.hobbies
const libraryArr = isPersonality ? source.personalityTagLibrary : source.hobbyLibrary
const finalLibrary = Array.isArray(libraryArr) && libraryArr.length > 0
? [...libraryArr]
: buildInitialLibrary(preset, selectedArr)
library.value = finalLibrary
selected.value = Array.isArray(selectedArr) ? [...selectedArr] : []
}
/**
* 左滑手势处理
*/
const touchState = ref({ startX: 0, startY: 0, currentIndex: -1, moved: false })
const DELETE_THRESHOLD = -60
const onTouchStart = (event, index) => {
const touch = event.touches[0]
touchState.value = { startX: touch.clientX, startY: touch.clientY, currentIndex: index, moved: false }
}
const onTouchMove = (event, index) => {
const touch = event.touches[0]
const deltaX = touch.clientX - touchState.value.startX
const deltaY = touch.clientY - touchState.value.startY
// 只处理水平滑动
if (Math.abs(deltaX) < Math.abs(deltaY)) return
touchState.value.moved = true
const offset = Math.min(0, Math.max(deltaX, -100))
swipeOffsets[index] = offset
}
const onTouchEnd = (event, index) => {
const currentOffset = swipeOffsets[index] || 0
if (currentOffset < DELETE_THRESHOLD) {
swipeOffsets[index] = -80
} else {
swipeOffsets[index] = 0
}
}
/**
* 删除确认
*/
const confirmDelete = (tag) => {
uni.showModal({
title: '确认删除',
content: `确定删除「${tag}」吗?删除后不可恢复`,
success: (res) => {
if (!res.confirm) return
deleteTag(tag)
}
})
}
/**
* 执行删除:从库数组移除,若同时在已选数组也一并移除
*/
const deleteTag = (tag) => {
const idx = library.value.indexOf(tag)
if (idx >= 0) library.value.splice(idx, 1)
const selectedIdx = selected.value.indexOf(tag)
if (selectedIdx >= 0) selected.value.splice(selectedIdx, 1)
// 同步到 store.registrationData
const isPersonality = type.value === 'personality'
const key = isPersonality ? 'personalityTagLibrary' : 'hobbyLibrary'
const selectedKey = isPersonality ? 'personalityTags' : 'hobbies'
store.updateRegistration({
[key]: [...library.value],
[selectedKey]: [...selected.value]
})
// 通知编辑资料页同步
uni.$emit('tag-library-updated', {
type: type.value,
library: [...library.value],
selected: [...selected.value]
})
}
const goBack = () => {
uni.navigateBack()
}
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
type.value = currentPage?.options?.type === 'hobby' ? 'hobby' : 'personality'
initFromStore()
})
</script>
<style scoped>
.manage-page {
position: relative;
height: 100vh;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
color: #fff;
background: #050615;
}
.space-bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 84% 3%, rgba(87, 122, 255, 0.12), transparent 28%),
radial-gradient(circle at 14% 26%, rgba(130, 71, 255, 0.16), transparent 30%),
linear-gradient(180deg, #05081b 0%, #07031a 52%, #03020d 100%);
}
.status-space,
.topbar,
.scroll {
position: relative;
z-index: 1;
}
.status-space,
.topbar {
flex-shrink: 0;
}
.topbar {
height: 72rpx;
display: grid;
grid-template-columns: 76rpx 1fr 76rpx;
align-items: center;
padding: 0 28rpx;
}
.back {
color: #fff;
font-size: 56rpx;
line-height: 1;
}
.title {
text-align: center;
color: #fff;
font-size: 32rpx;
font-weight: 900;
}
.placeholder-slot {
width: 1px;
}
.scroll {
flex: 1;
height: 0;
min-height: 0;
box-sizing: border-box;
padding: 0 24rpx 28rpx;
margin-top: -6rpx;
}
.glass-card {
position: relative;
overflow: hidden;
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);
}
.panel {
border-radius: 22rpx;
margin-bottom: 18rpx;
padding: 22rpx 24rpx;
}
.section-head {
display: flex;
align-items: center;
min-height: 44rpx;
gap: 10rpx;
}
.section-title {
color: rgba(239, 232, 255, 0.9);
font-size: 24rpx;
font-weight: 800;
}
.section-hint {
color: rgba(222, 211, 240, 0.54);
font-size: 22rpx;
}
.empty-tip {
padding: 40rpx 0;
text-align: center;
color: rgba(222, 211, 240, 0.5);
font-size: 24rpx;
}
.tag-row {
position: relative;
overflow: hidden;
border-top: 1rpx solid rgba(180, 139, 255, 0.12);
}
.tag-row-inner {
position: relative;
z-index: 2;
display: flex;
align-items: center;
min-height: 72rpx;
background: transparent;
transition: transform 0.2s ease;
}
.tag-row-main {
flex: 1;
min-width: 0;
padding: 16rpx 8rpx;
}
.tag-text {
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
}
.tag-row-delete {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 140rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #ff4d6d, #c9184a);
z-index: 1;
}
.delete-text {
color: #fff;
font-size: 24rpx;
font-weight: 700;
}
.tag-row-close {
position: absolute;
right: 16rpx;
top: 50%;
transform: translateY(-50%);
width: 44rpx;
height: 44rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 77, 109, 0.16);
z-index: 3;
}
.close-icon {
color: #ff4d6d;
font-size: 32rpx;
line-height: 1;
font-weight: 700;
}
</style>