383 lines
8.9 KiB
Vue
383 lines
8.9 KiB
Vue
<template>
|
||
<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">
|
||
|
||
</view>
|
||
|
||
<!-- 四个方向按钮 -->
|
||
<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)" />
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
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
|
||
}
|
||
}
|
||
|
||
function updateShadowPosition(pageX : number, pageY : number) {
|
||
let x = pageX
|
||
let y = pageY - TOP - 50 + props.movebottom/2
|
||
|
||
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>
|
||
|
||
<style lang="less" scoped>
|
||
.move-circle {
|
||
position: absolute;
|
||
bottom: 0rpx;
|
||
left: 0rpx;
|
||
width: 350rpx;
|
||
height: 350rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 99;
|
||
touch-action: none;
|
||
|
||
.click-box-top {
|
||
position: absolute;
|
||
top: 20rpx;
|
||
left: 70rpx;
|
||
width: 220rpx;
|
||
height: 80rpx;
|
||
}
|
||
|
||
.click-box-bottom {
|
||
position: absolute;
|
||
bottom: 20rpx;
|
||
left: 70rpx;
|
||
width: 220rpx;
|
||
height: 80rpx;
|
||
}
|
||
|
||
.click-box-left {
|
||
position: absolute;
|
||
bottom: 100rpx;
|
||
left: 0;
|
||
width: 90rpx;
|
||
height: 150rpx;
|
||
}
|
||
|
||
.click-box-right {
|
||
position: absolute;
|
||
bottom: 100rpx;
|
||
right: 0;
|
||
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;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
</style> |