hldy_app/pages/watch/drawer/index.vue

522 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

```vue
<!-- 轮盘(一级圆盘 + 二级左半弧滚动盘)-->
<template>
<view class="draw-all">
<!-- 摄像头 -->
<view class="carmera">
<image class="all-size" src="/static/index/watch/camera.png" />
</view>
<!-- 轮子背景 -->
<view class="roll">
<image class="all-size" src="/static/index/watch/panzi.png" />
</view>
<!-- 一级转盘:完整圆盘,可旋转、自动校正、选中最左侧 -->
<view ref="compass" class="compass-container" @touchstart.stop.prevent="onTouchStart"
@touchmove.prevent.stop="onTouchMove" @touchend.stop.prevent="onTouchEnd"
@touchcancel.stop.prevent="onTouchEnd" :style="wrapperStyle">
<view v-for="(item, i) in items" :key="i" class="compass-item" :style="itemStyle(item.baseAngle)">
<text :class="i===target?`item-label-target`: `item-label`" :style="labelStyle">
<view
style="z-index: 2;display: flex;flex-direction: column;justify-content: center;align-items: center;">
<image style="width: 50rpx;height: 50rpx;margin-bottom: 0rpx;"
:src="`/static/index/watch/Wheel/${i}${i===target?1:0}.png`" />
<view :style="i===target?{color:'#fff'}:{}">
{{ item.label }}
</view>
</view>
<image class="targetimge" src="/static/index/watch/bluetarget.png"
:style="{opacity:i===target?1:0}" />
</text>
</view>
</view>
<!-- 二级转盘只占左半球90°~270°竖向滚动、有边界、吸附、最多 5 个 -->
<view v-if="items2.length" ref="compass2" class="compass-container second"
@touchstart.stop.prevent="onTouchStart2" @touchmove.prevent.stop="onTouchMove2"
@touchend.stop.prevent="onTouchEnd2" @touchcancel.stop.prevent="onTouchEnd2" :style="wrapperStyle2"
v-show="target">
<view v-for="(item, i) in items2" :key="i" class="compass-item" :style="itemStyle2(item.baseAngle)">
<text :class="i===target2?`item-label-second-target`: `item-label-second`" :style="labelStyle2">
<view
style="z-index: 2;display: flex;flex-direction: column;justify-content: center;align-items: center;">
<image style="width: 70rpx;height: 70rpx;margin-bottom: 0rpx;"
:src="`/static/index/watch/Wheel/${target}${i}${i===target2?1:0}.png`" />
<view v-show="target!==-1" :style="i===target2?{color:'#0E86EA'}:{}">
{{ item.label }}
</view>
</view>
</text>
</view>
</view>
<view class="" v-show="target===5">
<joystick @movecard="" :movebottom="44" :moveleft="-5" :pao="false" :notext="true" />
</view>
<!-- 一级手势蒙层 -->
<view v-if="dragging==='first'" class="gesture-mask" @touchmove.stop.prevent="onTouchMove"
@touchend.stop.prevent="onTouchEnd" @touchcancel.stop.prevent="onTouchEnd" />
<!-- 二级手势蒙层 -->
<view v-if="dragging==='second'" class="gesture-mask" @touchmove.stop.prevent="onTouchMove2"
@touchend.stop.prevent="onTouchEnd2" @touchcancel.stop.prevent="onTouchEnd2" />
</view>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, nextTick, watch, onBeforeUnmount } from 'vue'
import joystick from '@/component/public/newgame/joysticknew.vue';
const emit = defineEmits([])
const props = defineProps({})
/* ===================== 一级:完整圆盘 ===================== */
// 10 个预设方向文字
const labels = ['开机', '静音', '对讲', '截屏', '录制', '方位', '清晰度', '分屏', '翻转', '告警']
const count = labels.length
// 一级每个文字对应的初始角度360 等分)
const items = reactive(
labels.map((label, idx) => ({
label,
baseAngle: (360 / count) * idx
}))
)
// 恢复定时器(最简单的实现)
let restoreTimer : number | null = null
let restoreTimer2 : number | null = null
const RESTORE_MS = 300 // 0.3s
// 当前累积旋转角度(一级)
const currentAngle = ref(0)
let startAngle = 0
const dragging = ref<null | 'first' | 'second'>(null)
let idleTimer : number | null = null
function armIdleFinish(handler : () => void, ms = 160) {
if (idleTimer) clearTimeout(idleTimer)
idleTimer = setTimeout(() => {
if (dragging.value) handler()
}, ms) as unknown as number
}
function clearIdle() {
if (idleTimer) {
clearTimeout(idleTimer)
idleTimer = null
}
}
// 圆心坐标(用于一级 atan2
const center = reactive({ x: 0, y: 0 })
// 获取圆心(以一级容器为基准)
onMounted(async () => {
await nextTick()
uni
.createSelectorQuery()
.select('.compass-container')
.boundingClientRect(rect => {
if (rect) {
center.x = rect.left + rect.width / 2
center.y = rect.top + rect.height / 2
}
})
.exec()
})
// 容器样式(一级)
const transitioning = ref(false)
const wrapperStyle = computed(() => ({
transform: `rotate(${currentAngle.value}deg)`,
transition: transitioning.value ? 'transform 0.3s ease-out' : 'none'
}))
// 单项样式(一级)
function itemStyle(baseAngle : number) {
const radius = 190
const rad = (baseAngle * Math.PI) / 180
const x = radius * Math.cos(rad)
const y = radius * Math.sin(rad)
return { transform: `translate(${x}px, ${y}px)` }
}
// 标签正立(一级)
const labelStyle = computed(() => ({
transform: `rotate(${-currentAngle.value}deg)`
}))
// atan2 角度(一级)
function getTouchAngle(e : TouchEvent) {
const t = e.touches[0]
const dx = t.clientX - center.x
const dy = t.clientY - center.y
return ((Math.atan2(dy, dx) * 180) / Math.PI) * 2 // 灵敏度×2
}
function clearRestoreTimer() {
if (restoreTimer !== null) {
clearTimeout(restoreTimer)
restoreTimer = null
}
}
// 选中最左侧索引(一级)
const target = ref(5)
const saveindex = ref(-1)
function startRestoreTimer() {
clearTimeout(restoreTimer)
restoreTimer = setTimeout(() => {
if (target.value === -1) {
target.value = saveindex.value
}
}, 300)
}
function startRestoreTimer2() {
clearTimeout(restoreTimer2)
restoreTimer2 = setTimeout(() => {
if (target2.value === -1) {
target2.value = saveindex2.value
}
}, 300)
}
function onTouchStart(e : TouchEvent) {
saveindex.value = target.value
target.value = -1
transitioning.value = false
startAngle = getTouchAngle(e) - currentAngle.value
dragging.value = 'first'
startRestoreTimer()
}
function onTouchMove(e : TouchEvent) {
const angle = getTouchAngle(e)
currentAngle.value = angle - startAngle
armIdleFinish(onTouchEnd)
// 重启恢复定时器0.3s 内没有新的 move 就恢复 saveindex
startRestoreTimer()
}
function getLeftmostIndex() {
let minDiff = Infinity
let idx = 0
items.forEach((item, i) => {
let real = (item.baseAngle + currentAngle.value) % 360
if (real < 0) real += 360
const diff = Math.abs(real - 180)
if (diff < minDiff) {
minDiff = diff
idx = i
}
})
return idx
}
function onTouchEnd() {
// 结束时先清定时器,避免同时恢复和吸附冲突
clearRestoreTimer()
const step = 360 / count
const raw = currentAngle.value
const nearest = Math.round(raw / step) * step
transitioning.value = true
currentAngle.value = nearest
const leftIndex = getLeftmostIndex()
target.value = leftIndex
// console.log("???", target.value)
setTimeout(() => (transitioning.value = false), 300)
dragging.value = null
}
onBeforeUnmount(() => {
if (restoreTimer !== null) clearTimeout(restoreTimer)
if (restoreTimer2 !== null) clearTimeout(restoreTimer2)
clearIdle()
})
/* ===================== 二级:左半弧竖向滚动盘 ===================== */
// 二级映射(每组最多 5 个)
const secondMap : Record<number, string[]> = {
0: [],
1: [],
2: [],
3: [],
4: [],
5: [],
6: ['超清', '流畅', '自动'],
7: ['180°全景', '四分屏', '360°全景', '全景拉伸', '原图'],
8: ['上下翻转', '关闭', '左右翻转'],
9: []
}
// 二级数据与状态
const items2 = reactive<{ label : string; baseAngle : number }[]>([])
const target2 = ref(0)
// 二级:半弧滚动偏移角(只允许在 [-Δ, +Δ]
const currentOffset2 = ref(0)
const step2 = ref(0) // Δ = 180 / (n + 1)
const transitioning2 = ref(false)
// 竖向滚动灵敏度(度/像素),可按设备调节
const DEG_PER_PX = 0.5
// 容器样式(二级:整体绕圆心微旋转 currentOffset2
const wrapperStyle2 = computed(() => ({
transform: `rotate(${currentOffset2.value}deg)`,
transition: transitioning2.value ? 'transform 0.25s ease-out' : 'none'
}))
// 标签正立(二级)
const labelStyle2 = computed(() => ({
transform: `rotate(${-currentOffset2.value}deg)`
}))
// 单项样式(二级:布局在左半弧 90°~270°
function itemStyle2(baseAngle : number) {
const radius = 240
const rad = (baseAngle * Math.PI) / 180
const x = radius * Math.cos(rad)
const y = radius * Math.sin(rad)
return { transform: `translate(${x}px, ${y}px)` }
}
// 固定角度位置(左半球从上到下)
const presetAngles = [135, 157.5, 180, 202.5, 225]
// 优先顺序:中间 → 上 → 下 → 上上 → 下下
function getBalancedAngles(n : number) : number[] {
const order = [2, 1, 3, 0, 4]
return order.slice(0, n).map(i => presetAngles[i])
}
const minOffset2 = ref(0);
const maxOffset2 = ref(0);
// 替换原来的 rebuildSecondByFirstIndex
function rebuildSecondByFirstIndex(firstIdx : number) {
const list = (secondMap[firstIdx] || []).slice(0, 5)
const angles = getBalancedAngles(list.length)
minOffset2.value = 180 - Math.max(...angles)
maxOffset2.value = 180 - Math.min(...angles)
items2.splice(0, items2.length)
if (!list.length) {
currentOffset2.value = 0
step2.value = 0
target2.value = -1
return
}
for (let i = 0; i < list.length; i++) {
items2.push({
label: list[i],
baseAngle: angles[i]
})
}
currentOffset2.value = 0
step2.value = 22.5 // 每个角度偏移
nextTick(() => {
target2.value = getLeftmostIndex2()
})
}
// 获取二级“最左侧”(最靠近 180°的索引
function getLeftmostIndex2() {
if (!items2.length) return -1
let minDiff = Infinity
let idx = 0
items2.forEach((item, i) => {
let real = (item.baseAngle + currentOffset2.value) % 360
if (real < 0) real += 360
const diff = Math.abs(real - 180)
if (diff < minDiff) {
minDiff = diff
idx = i
}
})
return idx
}
// 监听一级 target联动二级
watch(
() => target.value,
(idx) => {
if (idx >= 0) rebuildSecondByFirstIndex(idx)
},
{ immediate: true }
)
/* ---- 二级手势:竖向滚动到极限距离([-Δ, +Δ]),松手吸附到 Δ 的整数倍 ---- */
let startY2 = 0
let startOffset2 = 0
function clamp2(val : number) {
return Math.max(minOffset2.value, Math.min(maxOffset2.value, val))
}
const saveindex2 =ref(-1)
function onTouchStart2(e : TouchEvent) {
saveindex2.value = target2.value
const t = e.touches[0]
startY2 = t.clientY
startOffset2 = currentOffset2.value
transitioning2.value = false
target2.value = -1
dragging.value = 'second'
startRestoreTimer2()
}
function onTouchMove2(e : TouchEvent) {
if (!items2.length) return
const t = e.touches[0]
const dy = t.clientY - startY2
const Δ = step2.value || 1
// 上滑为正角度:使用 -dy
const raw = startOffset2 + (-dy) * DEG_PER_PX
currentOffset2.value = clamp2(raw, -Δ, +Δ)
armIdleFinish(onTouchEnd2)
startRestoreTimer2()
}
function onTouchEnd2() {
if (!items2.length) return
const Δ = step2.value || 1
// 吸附到 Δ 的整数倍,并限制在 [-Δ, +Δ]
const snapped = Math.round(currentOffset2.value / Δ) * Δ
transitioning2.value = true
currentOffset2.value = clamp2(snapped, -Δ, +Δ)
// 选中靠近 180° 的项
target2.value = getLeftmostIndex2()
setTimeout(() => (transitioning2.value = false), 250)
dragging.value = null
}
</script>
<style lang="less" scoped>
.draw-all {
width: 100%;
height: 100%;
background-color: #eff0f4;
overflow: hidden;
position: relative;
.carmera {
position: absolute;
right: 0;
bottom: 250rpx;
height: 600rpx;
width: 200rpx;
z-index: 1;
}
.roll {
position: absolute;
right: 0;
bottom: 0rpx;
height: 1300rpx;
width: 650rpx;
}
}
.compass-container {
width: 380px;
height: 380px;
border-radius: 50%;
position: absolute;
right: -60%;
transform: translateY(-60%);
bottom: 270rpx;
margin: auto;
touch-action: none;
z-index: 9999;
}
.compass-item {
position: absolute;
top: 50%;
left: 50%;
width: 100px;
height: 100px;
margin: -50px 0 0 -50px;
display: flex;
align-items: center;
justify-content: center;
}
.item-label {
font-size: 25rpx;
width: 130rpx;
height: 130rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(to bottom, #e6e7ed, #f4f5f7);
border: 2rpx solid #fff;
position: relative;
}
.item-label-target {
font-size: 25rpx;
width: 130rpx;
height: 130rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(to bottom, #e6e7ed, #f4f5f7);
position: relative;
}
.item-label-second {
font-size: 25rpx;
width: 130rpx;
height: 130rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.item-label-second-target {
font-size: 25rpx;
width: 130rpx;
height: 130rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.all-size {
width: 100%;
height: 100%;
}
.targetimge {
width: 150rpx;
height: 130rpx;
position: absolute;
right: 0;
top: 0;
z-index: 1;
transition: opacity 0.8s ease;
}
/* 二级容器放在一级之下,尺寸更大一圈(视觉在后面) */
.compass-container.second {
width: 480px;
height: 480px;
border-radius: 50%;
position: absolute;
right: -70%;
transform: translateY(-70%);
bottom: 172rpx;
margin: auto;
touch-action: none;
z-index: 1; // 在一级之下
}
.gesture-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 9999;
background: transparent;
/* 或 rgba(0,0,0,0.001) */
}
</style>
```