2025-04-28 17:33:10 +08:00
|
|
|
|
<template>
|
2025-08-13 17:19:40 +08:00
|
|
|
|
<view class="move-circle" :style="{ bottom: `${movebottom}rpx`, left: `${moveleft}rpx` }" @touchmove="onTouchMove"
|
|
|
|
|
@touchend="onPressEnd" @touchcancel="onPressEnd">
|
|
|
|
|
<!-- 长按持续波纹动画 -->
|
|
|
|
|
<view v-if="showShadow && pao" class="light-shadow ripple-loop" :style="{
|
|
|
|
|
left: shadow.x + 'px',
|
|
|
|
|
top: shadow.y + 'px',
|
|
|
|
|
transform: 'translate(-50%, -50%)'
|
|
|
|
|
}"></view>
|
|
|
|
|
|
|
|
|
|
<!-- 点击单次波纹动画,用 key 刷新 -->
|
|
|
|
|
<view v-if="showRippleOnce && pao" :key="rippleKey" class="light-shadow ripple-once" :style="{
|
|
|
|
|
left: shadow.x + 'px',
|
|
|
|
|
top: shadow.y + 'px',
|
|
|
|
|
transform: 'translate(-50%, -50%)'
|
|
|
|
|
}" @animationend="onRippleAnimationEnd"></view>
|
|
|
|
|
|
|
|
|
|
<image :src="!notext?`/static/index/newruler/direction_1.png`: `/static/index/newruler/suere.png`" v-show="type==-1 || type==4" class="move-circle-all" />
|
|
|
|
|
<!-- <image src="/static/index/newruler/direction_2.png" v-show="type==-2" class="move-circle-all" />
|
|
|
|
|
<image src="/static/index/newruler/direction_3.png" v-show="type==-3" class="move-circle-all" /> -->
|
|
|
|
|
<image :src="!notext?`/static/index/newruler/direction_3.png`: `/static/index/newruler/sure_2.png`" v-show="type==3" class="move-circle-all" />
|
|
|
|
|
<image :src="!notext?`/static/index/newruler/direction_5.png`: `/static/index/newruler/sure_4.png`" v-show="type==2" class="move-circle-all" />
|
|
|
|
|
<image :src="!notext?`/static/index/newruler/direction_4.png`: `/static/index/newruler/sure_3.png`" v-show="type==0" class="move-circle-all" />
|
|
|
|
|
<image :src="!notext?`/static/index/newruler/direction_2.png`: `/static/index/newruler/sure_1.png`" v-show="type==1" class="move-circle-all" />
|
|
|
|
|
<view class="pulse-circle" v-if="getblue" :key="pulseKey">
|
|
|
|
|
|
2025-04-28 17:33:10 +08:00
|
|
|
|
</view>
|
2025-08-13 17:19:40 +08:00
|
|
|
|
|
|
|
|
|
<!-- 四个方向按钮 -->
|
|
|
|
|
<view class="click-box-top" @tap="onTap(0, $event)" @longpress="(e) => onLongPressStart(0, e)" />
|
|
|
|
|
<view class="click-box-left" @tap="onTap(3, $event)" @longpress="(e) => onLongPressStart(3, e)" />
|
|
|
|
|
<view class="click-box-bottom" @tap="onTap(2, $event)" @longpress="(e) => onLongPressStart(2, e)" />
|
|
|
|
|
<view class="click-box-right" @tap="onTap(1, $event)" @longpress="(e) => onLongPressStart(1, e)" />
|
|
|
|
|
<view class="click-box-center" @tap="onTap(4, $event)" @longpress="(e) => onLongPressStart(4, e)" />
|
2025-04-28 17:33:10 +08:00
|
|
|
|
</view>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-08-13 17:19:40 +08:00
|
|
|
|
import { ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue'
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
(e : 'movecard', dir : number) : void
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const key = ref(-1)
|
|
|
|
|
let clickResetTimer : ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
let longPressInterval : ReturnType<typeof setInterval> | null = null
|
|
|
|
|
let isLongPress = false
|
|
|
|
|
|
|
|
|
|
const type = ref(-1)
|
|
|
|
|
const shadow = reactive({ x: 0, y: 0 })
|
|
|
|
|
const showShadow = ref(false) // 长按持续波纹
|
|
|
|
|
const showRippleOnce = ref(false) // 点击单次波纹
|
|
|
|
|
const rippleKey = ref(0) // 动画 key,用来强制刷新
|
|
|
|
|
|
|
|
|
|
const RADIUS = uni.upx2px(175)
|
|
|
|
|
const DIAMETER = uni.upx2px(350)
|
|
|
|
|
const LEFT = uni.upx2px(-50)
|
|
|
|
|
const BOTTOM = uni.upx2px(100)
|
|
|
|
|
const windowHeight = uni.getSystemInfoSync().windowHeight
|
|
|
|
|
const TOP = windowHeight - BOTTOM - DIAMETER
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
getblue: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false
|
|
|
|
|
},
|
|
|
|
|
movebottom: {
|
|
|
|
|
type: Number,
|
|
|
|
|
default: 0
|
|
|
|
|
},
|
|
|
|
|
moveleft: {
|
|
|
|
|
type: Number,
|
|
|
|
|
default: 0
|
|
|
|
|
},
|
|
|
|
|
pao:{
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: true
|
|
|
|
|
},
|
|
|
|
|
notext:{
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const getblue = ref(false)
|
|
|
|
|
const pulseKey = ref(0)
|
|
|
|
|
|
|
|
|
|
let timeout1 : ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
let timeout2 : ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
let timeout3 : ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
|
|
|
|
|
const types = [-1, -2, -3]
|
|
|
|
|
const index = ref(0)
|
|
|
|
|
let timer = null;
|
|
|
|
|
const switchType = () => {
|
|
|
|
|
index.value = (index.value + 1) % types.length
|
|
|
|
|
type.value = types[index.value]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(() => props.getblue, (val) => {
|
|
|
|
|
// if (timer) clearInterval(timer)
|
|
|
|
|
// timer = setInterval(() => {
|
|
|
|
|
// switchType()
|
|
|
|
|
// }, 100)
|
|
|
|
|
// 清除上次的定时器,防止重复播放
|
|
|
|
|
// if (timeout1) clearTimeout(timeout1)
|
|
|
|
|
// if (timeout2) clearTimeout(timeout2)
|
|
|
|
|
// if (timeout3) clearTimeout(timeout3)
|
|
|
|
|
|
|
|
|
|
// // 重置动画状态
|
|
|
|
|
// getblue.value = false
|
|
|
|
|
|
|
|
|
|
// nextTick(() => {
|
|
|
|
|
// pulseKey.value++
|
|
|
|
|
// getblue.value = true
|
|
|
|
|
|
|
|
|
|
// // 第一阶段结束
|
|
|
|
|
// timeout1 = setTimeout(() => {
|
|
|
|
|
// getblue.value = false
|
|
|
|
|
// }, 3000)
|
|
|
|
|
|
|
|
|
|
// // 第二阶段开始
|
|
|
|
|
// timeout2 = setTimeout(() => {
|
|
|
|
|
// getblue.value = true
|
|
|
|
|
// }, 3010)
|
|
|
|
|
|
|
|
|
|
// // 第二阶段结束
|
|
|
|
|
// timeout3 = setTimeout(() => {
|
|
|
|
|
// getblue.value = false
|
|
|
|
|
// }, 6500)
|
|
|
|
|
// })
|
|
|
|
|
// }
|
|
|
|
|
})
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
if (timer) clearInterval(timer)
|
|
|
|
|
})
|
|
|
|
|
function onTap(dir : number, e : any) {
|
|
|
|
|
if (timer) clearInterval(timer)
|
|
|
|
|
if (isLongPress) return
|
|
|
|
|
clearClickTimer()
|
|
|
|
|
key.value = dir
|
|
|
|
|
emit('movecard', dir)
|
|
|
|
|
// console.log("?????",e.touches)
|
|
|
|
|
if (e?.touches && e.touches.length) {
|
|
|
|
|
const touch = e.touches[0]
|
|
|
|
|
updateShadowPosition(touch.pageX, touch.pageY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showRippleOnce.value = false
|
|
|
|
|
rippleKey.value++
|
|
|
|
|
type.value = dir
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
type.value = -1
|
|
|
|
|
}, 300)
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
showRippleOnce.value = true
|
|
|
|
|
}, 16)
|
|
|
|
|
|
|
|
|
|
clickResetTimer = setTimeout(() => {
|
|
|
|
|
key.value = -1
|
|
|
|
|
clickResetTimer = null
|
|
|
|
|
}, 300)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onRippleAnimationEnd() {
|
|
|
|
|
showRippleOnce.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onLongPressStart(dir : number, e : TouchEvent) {
|
|
|
|
|
if (timer) clearInterval(timer)
|
|
|
|
|
clearLongPressInterval()
|
|
|
|
|
isLongPress = true
|
|
|
|
|
showShadow.value = true
|
|
|
|
|
type.value = dir
|
|
|
|
|
|
|
|
|
|
const touch = (e?.touches || [])[0]
|
|
|
|
|
if (touch) updateShadowPosition(touch.pageX, touch.pageY)
|
|
|
|
|
|
|
|
|
|
key.value = dir
|
|
|
|
|
emit('movecard', dir)
|
|
|
|
|
|
|
|
|
|
longPressInterval = setInterval(() => {
|
|
|
|
|
key.value = dir
|
|
|
|
|
emit('movecard', dir)
|
|
|
|
|
}, 500)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onPressEnd() {
|
|
|
|
|
clearClickTimer()
|
|
|
|
|
clearLongPressInterval()
|
|
|
|
|
isLongPress = false
|
|
|
|
|
showShadow.value = false
|
|
|
|
|
key.value = -1;
|
|
|
|
|
type.value = -1
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearClickTimer() {
|
|
|
|
|
if (clickResetTimer) {
|
|
|
|
|
clearTimeout(clickResetTimer)
|
|
|
|
|
clickResetTimer = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearLongPressInterval() {
|
|
|
|
|
if (longPressInterval) {
|
|
|
|
|
clearInterval(longPressInterval)
|
|
|
|
|
longPressInterval = null
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-28 17:33:10 +08:00
|
|
|
|
|
2025-08-13 17:19:40 +08:00
|
|
|
|
function updateShadowPosition(pageX : number, pageY : number) {
|
|
|
|
|
let x = pageX
|
|
|
|
|
let y = pageY - TOP - 50 + props.movebottom/2
|
2025-04-28 17:33:10 +08:00
|
|
|
|
|
2025-08-13 17:19:40 +08:00
|
|
|
|
const dx = x - RADIUS
|
|
|
|
|
const dy = y - RADIUS
|
|
|
|
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
|
|
|
|
|
|
|
|
|
if (dist > RADIUS) {
|
|
|
|
|
const angle = Math.atan2(dy, dx)
|
|
|
|
|
x = RADIUS + RADIUS * Math.cos(angle)
|
|
|
|
|
y = RADIUS + RADIUS * Math.sin(angle)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shadow.x = x
|
|
|
|
|
shadow.y = y
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onTouchMove(e : any) {
|
|
|
|
|
if (!isLongPress) return
|
|
|
|
|
|
|
|
|
|
const touch = (e.detail?.touches || e.touches || [])[0]
|
|
|
|
|
if (!touch) return
|
|
|
|
|
|
|
|
|
|
updateShadowPosition(touch.pageX, touch.pageY)
|
|
|
|
|
}
|
|
|
|
|
</script>
|
2025-04-28 17:33:10 +08:00
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
.move-circle {
|
|
|
|
|
position: absolute;
|
2025-08-13 17:19:40 +08:00
|
|
|
|
bottom: 0rpx;
|
|
|
|
|
left: 0rpx;
|
|
|
|
|
width: 350rpx;
|
|
|
|
|
height: 350rpx;
|
2025-04-28 17:33:10 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
2025-08-13 17:19:40 +08:00
|
|
|
|
z-index: 99;
|
|
|
|
|
touch-action: none;
|
|
|
|
|
|
|
|
|
|
.click-box-top {
|
2025-04-28 17:33:10 +08:00
|
|
|
|
position: absolute;
|
2025-08-13 17:19:40 +08:00
|
|
|
|
top: 20rpx;
|
2025-04-28 17:33:10 +08:00
|
|
|
|
left: 70rpx;
|
2025-08-13 17:19:40 +08:00
|
|
|
|
width: 220rpx;
|
|
|
|
|
height: 80rpx;
|
2025-04-28 17:33:10 +08:00
|
|
|
|
}
|
2025-08-13 17:19:40 +08:00
|
|
|
|
|
|
|
|
|
.click-box-bottom {
|
2025-04-28 17:33:10 +08:00
|
|
|
|
position: absolute;
|
2025-08-13 17:19:40 +08:00
|
|
|
|
bottom: 20rpx;
|
2025-04-28 17:33:10 +08:00
|
|
|
|
left: 70rpx;
|
2025-08-13 17:19:40 +08:00
|
|
|
|
width: 220rpx;
|
|
|
|
|
height: 80rpx;
|
2025-04-28 17:33:10 +08:00
|
|
|
|
}
|
2025-08-13 17:19:40 +08:00
|
|
|
|
|
|
|
|
|
.click-box-left {
|
2025-04-28 17:33:10 +08:00
|
|
|
|
position: absolute;
|
2025-08-13 17:19:40 +08:00
|
|
|
|
bottom: 100rpx;
|
2025-04-28 17:33:10 +08:00
|
|
|
|
left: 0;
|
2025-08-13 17:19:40 +08:00
|
|
|
|
width: 90rpx;
|
|
|
|
|
height: 150rpx;
|
2025-04-28 17:33:10 +08:00
|
|
|
|
}
|
2025-08-13 17:19:40 +08:00
|
|
|
|
|
|
|
|
|
.click-box-right {
|
2025-04-28 17:33:10 +08:00
|
|
|
|
position: absolute;
|
2025-08-13 17:19:40 +08:00
|
|
|
|
bottom: 100rpx;
|
2025-04-28 17:33:10 +08:00
|
|
|
|
right: 0;
|
2025-08-13 17:19:40 +08:00
|
|
|
|
width: 90rpx;
|
|
|
|
|
height: 150rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.click-box-center {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 130rpx;
|
|
|
|
|
right: 130rpx;
|
|
|
|
|
width: 90rpx;
|
|
|
|
|
height: 90rpx;
|
|
|
|
|
// background-color: red;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.move-circle-all {
|
|
|
|
|
width: 350rpx;
|
|
|
|
|
height: 350rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.light-shadow {
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
|
|
|
|
width: 40rpx;
|
|
|
|
|
height: 40rpx;
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
border: 60rpx solid #3da6ff;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 无限循环波纹动画,长按时用 */
|
|
|
|
|
.ripple-loop {
|
|
|
|
|
animation: rippleLoop 1.2s ease-out infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 点击一次的波纹动画 */
|
|
|
|
|
.ripple-once {
|
|
|
|
|
animation: rippleLoop 1.2s ease-out forwards;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes rippleLoop {
|
|
|
|
|
0% {
|
|
|
|
|
transform: translate(-50%, -50%) scale(0.5);
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
100% {
|
|
|
|
|
transform: translate(-50%, -50%) scale(2.5);
|
|
|
|
|
opacity: 0;
|
2025-04-28 17:33:10 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-13 17:19:40 +08:00
|
|
|
|
|
|
|
|
|
.light-circle {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 150px;
|
|
|
|
|
height: 150px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: #111;
|
|
|
|
|
/* 你背景色自己改 */
|
|
|
|
|
overflow: visible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.circle {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 150px;
|
|
|
|
|
height: 150px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: #222;
|
|
|
|
|
margin: 50px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pulse-circle {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: radial-gradient(circle, #03a4ff 0%, transparent 70%);
|
|
|
|
|
animation: pulse 3s forwards;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
0% {
|
|
|
|
|
width: 0;
|
|
|
|
|
height: 0;
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
50% {
|
|
|
|
|
width: 350rpx;
|
|
|
|
|
height: 350rpx;
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
100% {
|
|
|
|
|
width: 0;
|
|
|
|
|
height: 0;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
2025-04-28 17:33:10 +08:00
|
|
|
|
}
|
|
|
|
|
</style>
|