227 lines
5.8 KiB
Vue
227 lines
5.8 KiB
Vue
|
<template>
|
|||
|
<view class="move-circle" :style="{ bottom: `${movebottom}rpx`, left: `${moveleft}rpx` }" @touchend="onLongPressEnd"
|
|||
|
@touchcancel="onLongPressEnd">
|
|||
|
|
|||
|
<!-- 返回 -->
|
|||
|
<view :class="beblue === 5 ? 'click-box-target' : 'click-box'" @tap="onTap(5)"
|
|||
|
@longpress="(e) => onLongPressStart(5, e)">
|
|||
|
<span :class="beblue === 5 ? 'grad-text' : ''">返回</span>
|
|||
|
</view>
|
|||
|
|
|||
|
<!-- 上 -->
|
|||
|
<view :class="beblue === 0 ? 'click-box-target' : 'click-box'" @tap="onTap(0)"
|
|||
|
@longpress="(e) => onLongPressStart(0, e)">
|
|||
|
<image :src="`/static/index/newruler/arrow_1${beblue===0 ? '_1' : ''}.png`" class="image-photo" />
|
|||
|
</view>
|
|||
|
|
|||
|
<!-- 确定 -->
|
|||
|
<view :class="beblue === 4 ? 'click-box-target' : 'click-box'" @tap="onTap(4)"
|
|||
|
@longpress="(e) => onLongPressStart(4, e)">
|
|||
|
<span :class="beblue === 4 ? 'grad-text' : ''">确定</span>
|
|||
|
</view>
|
|||
|
|
|||
|
<!-- 右 -->
|
|||
|
<view :class="beblue === 3 ? 'click-box-target' : 'click-box'" @tap="onTap(3)"
|
|||
|
@longpress="(e) => onLongPressStart(3, e)">
|
|||
|
<image :src="`/static/index/newruler/arrow_3${beblue===3 ? '_3' : ''}.png`" class="image-photo" />
|
|||
|
</view>
|
|||
|
|
|||
|
<!-- 下 -->
|
|||
|
<view :class="beblue === 2 ? 'click-box-target' : 'click-box'" @tap="onTap(2)"
|
|||
|
@longpress="(e) => onLongPressStart(2, e)">
|
|||
|
<image :src="`/static/index/newruler/arrow_2${beblue===2 ? '_2' : ''}.png`" class="image-photo" />
|
|||
|
</view>
|
|||
|
|
|||
|
<!-- 左 -->
|
|||
|
<view :class="beblue === 1 ? 'click-box-target' : 'click-box'" @tap="onTap(1)"
|
|||
|
@longpress="(e) => onLongPressStart(1, e)">
|
|||
|
<image :src="`/static/index/newruler/arrow_4${beblue===1 ? '_4' : ''}.png`" class="image-photo" />
|
|||
|
</view>
|
|||
|
</view>
|
|||
|
</template>
|
|||
|
|
|||
|
<script setup lang="ts">
|
|||
|
import { ref, onBeforeUnmount } from 'vue'
|
|||
|
|
|||
|
const emit = defineEmits<{ (e : 'movecard', dir : number) : void }>()
|
|||
|
|
|||
|
let clickResetTimer : ReturnType<typeof setTimeout> | null = null
|
|||
|
let longPressInterval : ReturnType<typeof setInterval> | null = null
|
|||
|
let isLongPress = false
|
|||
|
const beblue = ref<number>(-1)
|
|||
|
let activeLongPressDir : number | null = null
|
|||
|
|
|||
|
const props = defineProps({
|
|||
|
movebottom: {
|
|||
|
type: Number,
|
|||
|
default: 0,
|
|||
|
},
|
|||
|
moveleft: {
|
|||
|
type: Number,
|
|||
|
default: 0,
|
|||
|
},
|
|||
|
})
|
|||
|
|
|||
|
function clearClickResetTimer() {
|
|||
|
if (clickResetTimer) {
|
|||
|
clearTimeout(clickResetTimer)
|
|||
|
clickResetTimer = null
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function clearLongPressInterval() {
|
|||
|
if (longPressInterval) {
|
|||
|
clearInterval(longPressInterval)
|
|||
|
longPressInterval = null
|
|||
|
}
|
|||
|
isLongPress = false
|
|||
|
activeLongPressDir = null
|
|||
|
}
|
|||
|
|
|||
|
// 单击(或短按)
|
|||
|
function onTap(dir : number) {
|
|||
|
// 立刻触发一次
|
|||
|
clearLongPressInterval()
|
|||
|
clearClickResetTimer()
|
|||
|
|
|||
|
beblue.value = dir
|
|||
|
emit('movecard', dir)
|
|||
|
|
|||
|
// 800ms 后恢复(如果期间有新点击会清除并重置)
|
|||
|
clickResetTimer = setTimeout(() => {
|
|||
|
beblue.value = -1
|
|||
|
clickResetTimer = null
|
|||
|
}, 500)
|
|||
|
}
|
|||
|
|
|||
|
// 长按开始(由 longpress 事件触发)
|
|||
|
function onLongPressStart(dir : number, e ?: any) {
|
|||
|
// 先清理点击计时器,确保长按状态保持
|
|||
|
clearClickResetTimer()
|
|||
|
clearLongPressInterval()
|
|||
|
|
|||
|
beblue.value = dir
|
|||
|
emit('movecard', dir)
|
|||
|
|
|||
|
// 开始每 500ms 发一次
|
|||
|
activeLongPressDir = dir
|
|||
|
isLongPress = true
|
|||
|
longPressInterval = setInterval(() => {
|
|||
|
if (activeLongPressDir !== null) emit('movecard', activeLongPressDir)
|
|||
|
}, 500)
|
|||
|
}
|
|||
|
|
|||
|
// 长按结束(touchend / touchcancel)
|
|||
|
function onLongPressEnd() {
|
|||
|
// 如果没有长按在进行,直接返回(防止意外触发)
|
|||
|
if (!isLongPress) return
|
|||
|
|
|||
|
// 停止 interval
|
|||
|
clearLongPressInterval()
|
|||
|
|
|||
|
// 0.8s 后恢复选中态
|
|||
|
clearClickResetTimer()
|
|||
|
clickResetTimer = setTimeout(() => {
|
|||
|
beblue.value = -1
|
|||
|
clickResetTimer = null
|
|||
|
}, 500)
|
|||
|
}
|
|||
|
|
|||
|
onBeforeUnmount(() => {
|
|||
|
clearClickResetTimer()
|
|||
|
clearLongPressInterval()
|
|||
|
})
|
|||
|
</script>
|
|||
|
|
|||
|
<style lang="less" scoped>
|
|||
|
.move-circle {
|
|||
|
position: absolute;
|
|||
|
bottom: 0rpx;
|
|||
|
left: 0rpx;
|
|||
|
width: 400rpx;
|
|||
|
display: flex;
|
|||
|
flex-wrap: wrap;
|
|||
|
z-index: 99;
|
|||
|
touch-action: none;
|
|||
|
}
|
|||
|
|
|||
|
.click-box,
|
|||
|
.click-box-target {
|
|||
|
width: 100rpx;
|
|||
|
height: 100rpx;
|
|||
|
margin-left: 15rpx;
|
|||
|
margin-bottom: 15rpx;
|
|||
|
display: flex;
|
|||
|
justify-content: center;
|
|||
|
align-items: center;
|
|||
|
border-radius: 30rpx;
|
|||
|
font-size: 28rpx;
|
|||
|
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.25s ease;
|
|||
|
-webkit-tap-highlight-color: transparent;
|
|||
|
}
|
|||
|
|
|||
|
.click-box {
|
|||
|
background-color: #f6f6f9;
|
|||
|
box-shadow: 0 4px 12px #e5e7ea;
|
|||
|
border: 2rpx solid #e6e7eb;
|
|||
|
color: #888d99;
|
|||
|
}
|
|||
|
|
|||
|
/* 选中态:背景径向渐变 + 中心缩放动画(从中间放大再回到原样) */
|
|||
|
.click-box-target {
|
|||
|
background: radial-gradient(circle at 30% 30%, #f2f7fd 0%, #deeaf9 100%);
|
|||
|
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
|
|||
|
border: 2rpx solid #c3ccd9;
|
|||
|
color: transparent;
|
|||
|
/* 文字使用渐变填充 */
|
|||
|
animation: scalePulse 360ms cubic-bezier(.2, .8, .2, 1);
|
|||
|
transform-origin: center center;
|
|||
|
}
|
|||
|
|
|||
|
@keyframes scalePulse {
|
|||
|
0% {
|
|||
|
transform: scale(1);
|
|||
|
}
|
|||
|
25% {
|
|||
|
/* 先收缩一点点 */
|
|||
|
transform: scale(0.94);
|
|||
|
}
|
|||
|
65% {
|
|||
|
/* 再放大到略超出的感觉 */
|
|||
|
transform: scale(1.08);
|
|||
|
}
|
|||
|
100% {
|
|||
|
transform: scale(1);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/* 文本渐变(用于返回/确定文字) */
|
|||
|
.grad-text {
|
|||
|
background-image: linear-gradient(90deg, #5b8bb3, #87a1bd);
|
|||
|
background-size: 200% 100%;
|
|||
|
background-position: 0% 50%;
|
|||
|
-webkit-background-clip: text;
|
|||
|
background-clip: text;
|
|||
|
-webkit-text-fill-color: transparent;
|
|||
|
transition: background-position 0.8s linear;
|
|||
|
}
|
|||
|
|
|||
|
/* 选中时文字渐变滚动效果 */
|
|||
|
.click-box-target .grad-text {
|
|||
|
background-position: 100% 50%;
|
|||
|
}
|
|||
|
|
|||
|
.image-photo {
|
|||
|
width: 30%;
|
|||
|
height: 30%;
|
|||
|
transition: transform 0.18s ease, filter 0.18s ease;
|
|||
|
}
|
|||
|
|
|||
|
/* 选中时图片略微放大 */
|
|||
|
.click-box-target .image-photo {
|
|||
|
/* 让图片跟随父元素缩放,不额外放大,保留平滑过渡 */
|
|||
|
transform: none;
|
|||
|
transition: transform 0.18s ease, filter 0.18s ease;
|
|||
|
filter: none;
|
|||
|
}
|
|||
|
</style>
|