feat:新增管理标签独立页面,支持左滑与 × 按钮删除
This commit is contained in:
@@ -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>
|
||||||
Reference in New Issue
Block a user