272 lines
6.1 KiB
Vue
272 lines
6.1 KiB
Vue
<template>
|
||
<view class="container">
|
||
<NavBar class="nav-bar" title="快捷入口" :bgColor="data.navBar.bgColor" isRightButton @right-click="onRightClick">
|
||
</NavBar>
|
||
<view class="content">
|
||
<view class="grid-card">
|
||
<view class="grid-list">
|
||
<!-- 遍历显示网格项,绑定触摸事件以支持拖拽 -->
|
||
<view class="grid-item" v-for="(item, index) in data.list" :key="item.id"
|
||
:class="{ 'dragging-placeholder': draggingItem && draggingItem.id === item.id }"
|
||
@touchstart="handleTouchStart($event, item, index)" @touchmove.stop.prevent="handleTouchMove"
|
||
@touchend="handleTouchEnd">
|
||
<view class="icon-box">
|
||
<image class="icon" :src="`/static/image/balance/menu-icon/${item.label}.png`"
|
||
mode="aspectFit"></image>
|
||
</view>
|
||
<text class="name">{{ item.name }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="footer-tip">*长按可以排序哟</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 拖拽时显示的浮动项(跟随手指) -->
|
||
<view v-if="draggingItem" class="grid-item floating-item" :style="dragStyle">
|
||
<view class="icon-box">
|
||
<image class="icon" :src="`/static/image/balance/menu-icon/${draggingItem.label}.png`" mode="aspectFit">
|
||
</image>
|
||
</view>
|
||
<text class="name">{{ draggingItem.name }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import NavBar from '@/components/nav-bar/nav-bar.vue'
|
||
import { fastEntranceList } from '@/static/json/initial.json'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import { storage } from '@/utils/storage'
|
||
import {
|
||
reactive,
|
||
toRefs,
|
||
ref,
|
||
nextTick
|
||
} from 'vue'
|
||
|
||
const data = reactive({
|
||
navBar: {
|
||
bgColor: "#F5F5F5"
|
||
},
|
||
list: []
|
||
})
|
||
|
||
let { list } = toRefs(data)
|
||
|
||
// 拖拽相关状态
|
||
const draggingItem = ref(null) // 当前正在拖拽的项
|
||
const dragStyle = ref({ top: '0px', left: '0px' }) // 拖拽项的样式(位置)
|
||
let startX = 0 // 触摸开始X坐标
|
||
let startY = 0 // 触摸开始Y坐标
|
||
let longPressTimer = null // 长按定时器
|
||
let itemRects = [] // 所有网格项的位置信息
|
||
let containerTop = 0 // 容器顶部偏移
|
||
|
||
onShow(() => {
|
||
// 优先从缓存读取
|
||
const cachedList = storage.get('fastEntranceList')
|
||
if (cachedList && cachedList.length > 0) {
|
||
data.list = cachedList
|
||
} else {
|
||
// 深拷贝数据,防止直接修改原引用的json数据,允许页面内重新排序
|
||
data.list = JSON.parse(JSON.stringify(fastEntranceList))
|
||
}
|
||
|
||
// 渲染完成后测量位置
|
||
nextTick(() => {
|
||
measureItems()
|
||
})
|
||
})
|
||
|
||
/**
|
||
* 测量所有网格项的位置,用于后续碰撞检测
|
||
*/
|
||
const measureItems = () => {
|
||
uni.createSelectorQuery().select('.grid-list').boundingClientRect(res => {
|
||
if (res) {
|
||
containerTop = res.top
|
||
}
|
||
}).exec()
|
||
|
||
uni.createSelectorQuery().selectAll('.grid-item').boundingClientRect(rects => {
|
||
itemRects = rects
|
||
}).exec()
|
||
}
|
||
|
||
/**
|
||
* 触摸开始:启动长按定时器
|
||
*/
|
||
const handleTouchStart = (e, item, index) => {
|
||
if (e.touches.length !== 1) return
|
||
|
||
startX = e.touches[0].clientX
|
||
startY = e.touches[0].clientY
|
||
|
||
longPressTimer = setTimeout(() => {
|
||
// 长按触发拖拽
|
||
draggingItem.value = item
|
||
// 更新拖拽项位置使其出现在手指中心
|
||
updateDragPosition(startX, startY)
|
||
|
||
// 震动反馈
|
||
uni.vibrateShort()
|
||
}, 500) // 500ms 长按触发
|
||
}
|
||
|
||
/**
|
||
* 触摸移动:更新拖拽位置并检测排序
|
||
*/
|
||
const handleTouchMove = (e) => {
|
||
if (!draggingItem.value) {
|
||
// 如果还没触发拖拽,检测手指移动距离,如果移动过多则取消长按定时器(由点击变为滑动)
|
||
const moveX = e.touches[0].clientX
|
||
const moveY = e.touches[0].clientY
|
||
if (Math.abs(moveX - startX) > 10 || Math.abs(moveY - startY) > 10) {
|
||
clearTimeout(longPressTimer)
|
||
}
|
||
return
|
||
}
|
||
|
||
const x = e.touches[0].clientX
|
||
const y = e.touches[0].clientY
|
||
|
||
updateDragPosition(x, y)
|
||
checkReorder(x, y)
|
||
}
|
||
|
||
/**
|
||
* 触摸结束:重置状态
|
||
*/
|
||
const handleTouchEnd = () => {
|
||
clearTimeout(longPressTimer)
|
||
draggingItem.value = null
|
||
}
|
||
|
||
/**
|
||
* 更新拖拽浮层的位置
|
||
*/
|
||
const updateDragPosition = (x, y) => {
|
||
// 将项目中心定位到手指位置
|
||
// 假设项目宽度大约占屏幕宽度的20% (例如75px) -> 中心偏移约37px
|
||
// 仅用于视觉更新
|
||
dragStyle.value = {
|
||
top: (y - 40) + 'px',
|
||
left: (x - 37) + 'px',
|
||
position: 'fixed',
|
||
zIndex: 999,
|
||
opacity: 0.8,
|
||
width: itemRects[0] ? itemRects[0].width + 'px' : '20%' // 匹配实际宽度
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检测是否需要重新排序
|
||
*/
|
||
const checkReorder = (x, y) => {
|
||
// 查找当前手指位置所在的网格项索引
|
||
const targetIndex = itemRects.findIndex(rect =>
|
||
x >= rect.left && x <= rect.right &&
|
||
y >= rect.top && y <= rect.bottom
|
||
)
|
||
|
||
if (targetIndex !== -1) {
|
||
const draggedIndex = data.list.findIndex(item => item.id === draggingItem.value.id)
|
||
if (draggedIndex !== -1 && draggedIndex !== targetIndex) {
|
||
// 交换逻辑:将原位置删掉,插入新位置
|
||
const item = data.list.splice(draggedIndex, 1)[0]
|
||
data.list.splice(targetIndex, 0, item)
|
||
}
|
||
}
|
||
}
|
||
|
||
const onRightClick = () => {
|
||
// 保存到缓存
|
||
storage.set('fastEntranceList', data.list)
|
||
|
||
uni.showToast({
|
||
title: '保存成功',
|
||
icon: 'success',
|
||
duration: 500
|
||
})
|
||
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 500)
|
||
}
|
||
|
||
</script>
|
||
|
||
<style>
|
||
page {
|
||
background-color: #F5F5F5;
|
||
}
|
||
</style>
|
||
|
||
<style lang="less" scoped>
|
||
.container {
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.content {
|
||
padding: 12px;
|
||
}
|
||
|
||
.grid-card {
|
||
background-color: #FFFFFF;
|
||
border-radius: 12px;
|
||
padding: 20px 0;
|
||
padding-bottom: 12px;
|
||
|
||
.footer-tip {
|
||
text-align: right;
|
||
font-size: 20rpx;
|
||
color: #767676;
|
||
padding: 0 16rpx;
|
||
}
|
||
}
|
||
|
||
.grid-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.grid-item {
|
||
width: 20%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.icon-box {
|
||
width: 36px;
|
||
height: 36px;
|
||
border: 1px dashed #cccccc;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.icon {
|
||
width: 20px;
|
||
/* Adjusted based on visual ratio in screenshot inside the box */
|
||
height: 20px;
|
||
}
|
||
|
||
.name {
|
||
font-size: 12px;
|
||
color: #333333;
|
||
}
|
||
|
||
.dragging-placeholder {
|
||
opacity: 0;
|
||
}
|
||
|
||
.floating-item {
|
||
pointer-events: none;
|
||
/* Let touches pass through to underlying elements for detection */
|
||
}
|
||
</style>
|