709 lines
21 KiB
Vue
709 lines
21 KiB
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;"
|
||
:class="i===target&&!opensecondmenu?`pulse`: ``"
|
||
:src="`/static/index/watch/Wheel/${i+1}${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>
|
||
|
||
<!-- 二级转盘 -->
|
||
<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 !== -1">
|
||
<view v-for="(item, i) in items2" :key="i" class="compass-item" :style="itemStyle2(item.baseAngle)">
|
||
<text class="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;"
|
||
:class="i===target2&&opensecondmenu?`pulse`: ``" :src="`/static/index/watch/Wheel/${target === -1 ? 0 : target+1}${i}${i===secondMapTarget[target]?1:0}.png`" />
|
||
<view v-show="target!==-1" :style="i===secondMapTarget[target]?{color:'#0E86EA'}:{}">
|
||
{{ item.label }}
|
||
</view>
|
||
</view>
|
||
</text>
|
||
</view>
|
||
</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'
|
||
|
||
const props = defineProps({
|
||
opensecondmenu: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
})
|
||
const emit = defineEmits(["firstIndex", "secondIndex"])
|
||
/* ========== 基础数据 ========== */
|
||
const labels = ['静音', '对讲', '截屏', '录制', '方位', '清晰度', '分屏', '翻转', '告警']
|
||
const secondMapTarget = ref([0, 1, 1, 1, 1, 0, 0, 3, 1])
|
||
const count = labels.length
|
||
|
||
const items = reactive(labels.map((label, idx) => ({
|
||
label,
|
||
baseAngle: (360 / count) * idx
|
||
})))
|
||
|
||
/* ========== 定时器 & 常量 ========== */
|
||
let restoreTimer : number | null = null
|
||
let restoreTimer2 : number | null = null
|
||
const ANIM_MS = 300
|
||
const RESTORE_MS = 300
|
||
|
||
/* ========== 角度状态 ========== */
|
||
// 逻辑角(连续累积,可超过多圈)—— 由手势增量累加得到
|
||
const currentAngle = ref(0)
|
||
// 用于渲染到 DOM 的“显示角”,我们会对它做最短角度动画
|
||
const displayAngle = ref(0)
|
||
|
||
const startchange = () => {
|
||
secondMapTarget.value[target.value] = target2.value;
|
||
}
|
||
|
||
|
||
// 拖拽状态
|
||
const dragging = ref<null | 'first' | 'second'>(null)
|
||
let prevTouchAngle : number | null = null
|
||
let lastMoveDelta = 0
|
||
|
||
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()
|
||
|
||
// 初始对齐:用 currentAngle (0) 找到左侧项并对齐 displayAngle
|
||
await nextTick()
|
||
const init = getLeftmostIndexFromAngle(currentAngle.value)
|
||
target.value = init
|
||
// 直接把 display 对齐到 current(无动画)
|
||
displayAngle.value = currentAngle.value
|
||
focusIndex(init, false)
|
||
})
|
||
|
||
/* ========== 显示样式依赖 displayAngle ========== */
|
||
const transitioning = ref(false)
|
||
const wrapperStyle = computed(() => ({
|
||
transform: `rotate(${displayAngle.value}deg)`,
|
||
transition: transitioning.value ? `transform ${ANIM_MS}ms ease-out` : 'none'
|
||
}))
|
||
const labelStyle = computed(() => ({ transform: `rotate(${-displayAngle.value}deg)` }))
|
||
|
||
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)` }
|
||
}
|
||
|
||
/* ========== 角度工具函数 ========== */
|
||
function normalize360(a : number) {
|
||
let v = a % 360
|
||
if (v < 0) v += 360
|
||
return v
|
||
}
|
||
// 返回 toAngle 相对于 fromAngle 的最短有向角差(-180,180]
|
||
function signedShortestAngleDiff(toAngle : number, fromAngle : number) {
|
||
// 直接利用归一化到 [0,360) 再差值并修正
|
||
const a = normalize360(toAngle)
|
||
const b = normalize360(fromAngle)
|
||
let diff = a - b
|
||
if (diff > 180) diff -= 360
|
||
if (diff <= -180) diff += 360
|
||
return diff
|
||
}
|
||
// 给出 targetAngle(0..360),返回与 reference 最接近的等价角(可能是 target±360k)
|
||
function chooseClosestEquivalent(targetAngle : number, reference : number) {
|
||
const base = normalize360(targetAngle)
|
||
let best = base
|
||
let bestDiff = Math.abs(best - reference)
|
||
for (let k = -3; k <= 3; k++) {
|
||
const cand = base + k * 360
|
||
const d = Math.abs(cand - reference)
|
||
if (d < bestDiff) {
|
||
bestDiff = d
|
||
best = cand
|
||
}
|
||
}
|
||
return best
|
||
}
|
||
|
||
/* ====== touch 角度 (标准 atan2 返回 -180..180) ====== */
|
||
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
|
||
}
|
||
|
||
/* ========== 选中 & 恢复计时 ========== */
|
||
const target = ref(5)
|
||
const saveindex = ref(-1)
|
||
function clearRestoreTimer() {
|
||
if (restoreTimer !== null) { clearTimeout(restoreTimer); restoreTimer = null }
|
||
}
|
||
function startRestoreTimer() {
|
||
clearRestoreTimer()
|
||
restoreTimer = setTimeout(() => {
|
||
if (target.value === -1) target.value = saveindex.value
|
||
}, RESTORE_MS) as unknown as number
|
||
}
|
||
function startRestoreTimer2() {
|
||
clearTimeout(restoreTimer2)
|
||
restoreTimer2 = setTimeout(() => {
|
||
if (target2.value === -1) target2.value = saveindex2.value ;emit("secondIndex", target2.value)
|
||
}, RESTORE_MS) as unknown as number
|
||
}
|
||
|
||
/* ========== touch handlers:一级(使用 currentAngle 做逻辑,displayAngle 做渲染) ========== */
|
||
function onTouchStart(e : TouchEvent) {
|
||
saveindex.value = target.value
|
||
target.value = -1
|
||
transitioning.value = false
|
||
prevTouchAngle = getTouchAngle(e)
|
||
lastMoveDelta = 0
|
||
dragging.value = 'first'
|
||
startRestoreTimer()
|
||
}
|
||
function onTouchMove(e : TouchEvent) {
|
||
if (prevTouchAngle === null) {
|
||
prevTouchAngle = getTouchAngle(e); return
|
||
}
|
||
const angle = getTouchAngle(e)
|
||
// 计算相对增量(短差),累加到 currentAngle(逻辑角)
|
||
const delta = signedShortestAngleDiff(angle, prevTouchAngle as number)
|
||
lastMoveDelta = delta
|
||
currentAngle.value = currentAngle.value + delta
|
||
// 直接把显示角同步到当前逻辑角(无动画)
|
||
transitioning.value = false
|
||
displayAngle.value = currentAngle.value
|
||
prevTouchAngle = angle
|
||
armIdleFinish(onTouchEnd)
|
||
startRestoreTimer()
|
||
}
|
||
function onTouchEnd() {
|
||
prevTouchAngle = null
|
||
clearRestoreTimer()
|
||
|
||
// 吸附到网格(snap)
|
||
const step = 360 / count
|
||
const snapUnit = (count % 2 === 0) ? step : step / 2
|
||
|
||
const raw = currentAngle.value
|
||
const snappedBase = Math.round(raw / snapUnit) * snapUnit
|
||
const targetNorm = normalize360(snappedBase) // 0..360 的目标表示
|
||
|
||
// 逻辑角:选择一个跟 raw 相近的等价角(保持数值连续性)
|
||
const finalAngle = chooseClosestEquivalent(targetNorm, raw)
|
||
// 立即把逻辑角更新为 finalAngle(这样后续逻辑以 finalAngle 为准)
|
||
currentAngle.value = finalAngle
|
||
|
||
// 计算显示端要动画到哪个“等价角”——选择对 displayAngle 最接近的等价角
|
||
const displayTarget = chooseClosestEquivalent(targetNorm, displayAngle.value)
|
||
|
||
// 计算最短差并让 displayAngle 增量变化(CSS 会走最短路径)
|
||
let diff = displayTarget - displayAngle.value
|
||
if (diff > 180) diff -= 360
|
||
if (diff <= -180) diff += 360
|
||
|
||
// 启动过渡动画
|
||
transitioning.value = true
|
||
displayAngle.value = displayAngle.value + diff
|
||
|
||
// 先立刻更新 target(逻辑上的选中项)
|
||
const leftIndex = getLeftmostIndexFromAngle(finalAngle)
|
||
if (target.value !== leftIndex) {
|
||
emit("firstIndex", leftIndex)
|
||
target.value = leftIndex
|
||
}
|
||
|
||
|
||
// chuangti()
|
||
// 动画结束后:关闭过渡、并用无动画方式把 displayAngle 数值对齐到逻辑角 currentAngle
|
||
setTimeout(() => {
|
||
transitioning.value = false
|
||
// 把 displayAngle 对齐到 currentAngle 的数值(可能相差整圈),但无动画
|
||
displayAngle.value = currentAngle.value
|
||
}, ANIM_MS)
|
||
|
||
dragging.value = null
|
||
lastMoveDelta = 0
|
||
}
|
||
|
||
/* ========== focusIndex(外部调用或按钮触发的精确对齐) ========== */
|
||
function angleForIndex(index : number) {
|
||
if (!items[index]) return 0
|
||
return normalize360(180 - items[index].baseAngle)
|
||
}
|
||
function focusIndex(index : number, animate = true) {
|
||
if (index == null || index < 0 || index >= items.length) return
|
||
const angle = angleForIndex(index) // 0..360 target
|
||
// 逻辑上选择最接近 currentAngle 的等价角
|
||
const finalAngle = chooseClosestEquivalent(angle, currentAngle.value)
|
||
currentAngle.value = finalAngle
|
||
|
||
// 显示端选择对 displayAngle 最接近的等价角去动画
|
||
const displayTarget = chooseClosestEquivalent(angle, displayAngle.value)
|
||
if (!animate) {
|
||
transitioning.value = false
|
||
displayAngle.value = finalAngle
|
||
// target.value = index
|
||
if (target.value !== index) {
|
||
emit("firstIndex", index)
|
||
target.value = index
|
||
}
|
||
return
|
||
}
|
||
|
||
let diff = displayTarget - displayAngle.value
|
||
if (diff > 180) diff -= 360
|
||
if (diff <= -180) diff += 360
|
||
|
||
transitioning.value = true
|
||
displayAngle.value = displayAngle.value + diff
|
||
if (target.value !== index) {
|
||
emit("firstIndex", index)
|
||
target.value = index
|
||
}
|
||
|
||
setTimeout(() => {
|
||
transitioning.value = false
|
||
displayAngle.value = currentAngle.value
|
||
}, ANIM_MS)
|
||
|
||
dragging.value = null
|
||
}
|
||
|
||
/* ========== 更稳健的 getLeftmost(基于任意角度) ========== */
|
||
function angleDiff(a : number, b : number) {
|
||
const diff = Math.abs(normalize360(a) - normalize360(b))
|
||
return Math.min(diff, 360 - diff)
|
||
}
|
||
function getLeftmostIndexFromAngle(angle : number) {
|
||
if (!items.length) return -1
|
||
const cur = normalize360(angle)
|
||
let minDiff = Infinity
|
||
let idx = 0
|
||
items.forEach((item, i) => {
|
||
const targetAngleForItem = normalize360(180 - item.baseAngle)
|
||
const diff = angleDiff(cur, targetAngleForItem)
|
||
if (diff < minDiff - 1e-9) {
|
||
minDiff = diff
|
||
idx = i
|
||
}
|
||
})
|
||
return idx
|
||
}
|
||
function getLeftmostIndex() {
|
||
return getLeftmostIndexFromAngle(currentAngle.value)
|
||
}
|
||
|
||
/* ================= 二级(竖向左半弧) ================ */
|
||
const secondMapByLabel : Record<string, string[]> = {
|
||
'静音': ['开启静音', '关闭静音',], '对讲': ['开启对讲', '关闭对讲',], '截屏': ["截屏"], '录制': ['开启录制', '关闭录制',], '方位': ['开启方位', '关闭方位',],
|
||
'清晰度': ['高清', '流畅'],
|
||
'分屏': ['原图', '四分屏', '180°全景', '360°全景', '环状全景'],
|
||
'翻转': ['左右翻转', '上下翻转', '中心翻转', '关闭'],
|
||
'告警': ['开启告警', '关闭告警',]
|
||
}
|
||
|
||
const items2 = reactive<{ label : string; baseAngle : number }[]>([])
|
||
const target2 = ref(0)
|
||
|
||
const currentOffset2 = ref(0)
|
||
const step2 = ref(0)
|
||
const transitioning2 = ref(false)
|
||
const DEG_PER_PX = 0.5
|
||
|
||
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)` }))
|
||
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)
|
||
function rebuildSecondByFirstIndex(firstIdx : number) {
|
||
const firstLabel = items[firstIdx]?.label
|
||
const list = (firstLabel && secondMapByLabel[firstLabel]) ? secondMapByLabel[firstLabel].slice(0, 5) : []
|
||
const angles = getBalancedAngles(list.length)
|
||
minOffset2.value = 180 - Math.max(...(angles.length ? angles : [180]))
|
||
maxOffset2.value = 180 - Math.min(...(angles.length ? angles : [180]))
|
||
items2.splice(0, items2.length)
|
||
if (!list.length) {
|
||
currentOffset2.value = 0
|
||
step2.value = 0
|
||
target2.value = -1
|
||
emit("secondIndex", target2.value)
|
||
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();emit("secondIndex", target2.value) })
|
||
}
|
||
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
|
||
}
|
||
|
||
watch(() => target.value, (idx) => {
|
||
if (idx >= 0) {
|
||
rebuildSecondByFirstIndex(idx)
|
||
focusIndex(idx, true)
|
||
} else {
|
||
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
|
||
emit("secondIndex", target2.value)
|
||
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
|
||
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)
|
||
|
||
setTimeout(() => (transitioning2.value = false), 250)
|
||
dragging.value = null
|
||
if (target.value !== getLeftmostIndex2()) {
|
||
emit("secondIndex", target2.value)
|
||
target2.value = getLeftmostIndex2()
|
||
}
|
||
|
||
}
|
||
|
||
/* ========== 外部控制与清理 ========== */
|
||
function moveFirstUp() {
|
||
if (dragging.value === 'first') return
|
||
const cur = getLeftmostIndex(); if (cur < 0) return
|
||
const next = (cur - 1 + items.length) % items.length
|
||
focusIndex(next, true);
|
||
if (target.value !== next) {
|
||
emit("firstIndex", next)
|
||
target.value = next
|
||
}
|
||
|
||
// chuangti()
|
||
}
|
||
function moveFirstDown() {
|
||
if (dragging.value === 'first') return
|
||
const cur = getLeftmostIndex(); if (cur < 0) return
|
||
const next = (cur + 1) % items.length
|
||
focusIndex(next, true);
|
||
if (target.value !== next) {
|
||
emit("firstIndex", next)
|
||
target.value = next
|
||
}
|
||
// chuangti()
|
||
}
|
||
/* ======= 为二级盘新增外部控制(按偏移量,而不是按索引) ======= */
|
||
function moveSecondUp() {
|
||
if (dragging.value === 'second') return
|
||
if (!items2.length) return
|
||
const Δ = step2.value || 1
|
||
// 手指向上 -> currentOffset2 增大 (onTouchMove 中也是这样:向上使 currentOffset2 增加)
|
||
let target = currentOffset2.value + Δ
|
||
// 把目标对齐到 step 网格并限制到 min/max
|
||
target = Math.round(target / Δ) * Δ
|
||
target = clamp2(target)
|
||
transitioning2.value = true
|
||
currentOffset2.value = target
|
||
// 更新选中项(根据新的 offset 计算最靠近 180° 的项)
|
||
target2.value = getLeftmostIndex2()
|
||
emit("secondIndex", target2.value)
|
||
|
||
setTimeout(() => (transitioning2.value = false), 250)
|
||
}
|
||
|
||
function moveSecondDown() {
|
||
if (dragging.value === 'second') return
|
||
if (!items2.length) return
|
||
const Δ = step2.value || 1
|
||
// 手指向下 -> currentOffset2 减小
|
||
let target = currentOffset2.value - Δ
|
||
target = Math.round(target / Δ) * Δ
|
||
target = clamp2(target)
|
||
transitioning2.value = true
|
||
currentOffset2.value = target
|
||
target2.value = getLeftmostIndex2()
|
||
emit("secondIndex", target2.value)
|
||
setTimeout(() => (transitioning2.value = false), 250)
|
||
}
|
||
|
||
defineExpose({ moveFirstUp, moveFirstDown, moveSecondUp, moveSecondDown, startchange })
|
||
|
||
onBeforeUnmount(() => {
|
||
if (restoreTimer !== null) clearTimeout(restoreTimer)
|
||
if (restoreTimer2 !== null) clearTimeout(restoreTimer2)
|
||
clearIdle()
|
||
})
|
||
</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: -65%;
|
||
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;
|
||
}
|
||
|
||
.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: -75%;
|
||
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;
|
||
}
|
||
|
||
.pulse {
|
||
/* 可调参数 */
|
||
--scale: 1.5;
|
||
--dur: 1.1s;
|
||
|
||
animation: pulse var(--dur) ease-in-out infinite;
|
||
transform-origin: center center;
|
||
will-change: transform;
|
||
}
|
||
|
||
/* 放大到一定值再回到原始(平滑) */
|
||
@keyframes pulse {
|
||
0% {
|
||
transform: scale(1);
|
||
}
|
||
|
||
50% {
|
||
transform: scale(var(--scale));
|
||
}
|
||
|
||
100% {
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.targetbutton {
|
||
--color: #99C9FD;
|
||
--thick: 2px;
|
||
--radius: 60rpx;
|
||
--outline-offset: -10rpx;
|
||
/* 外扩多少 */
|
||
|
||
/* 内层虚线(你现在用的) */
|
||
border-radius: var(--radius);
|
||
// background-color: #ddf0ff;
|
||
/* 内部背景 */
|
||
animation: scalePulse 360ms cubic-bezier(.2, .8, .2, 1);
|
||
/* 外层虚线:放在 outline(不会影响元素尺寸) */
|
||
outline: var(--thick) dashed var(--color);
|
||
outline-offset: var(--outline-offset);
|
||
|
||
/* 保证文本 / 子元素在最上层 */
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
</style> |