fix: 触控组件审查修复(道具图标逐项渲染/连线配对去重/拖拽排序坐标判定/命中区取真实 rect/done 守卫)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="drag-place">
|
||||
<view class="target" :style="{ background: over ? '#ffe0c2' : '#f8f2e8' }" :class="{ win: winDrop }">
|
||||
<view class="target" :class="{ win: winDrop }" :style="{ background: over ? '#ffe0c2' : '#f8f2e8' }">
|
||||
<text class="target-text">{{ over ? '放这里!' : '把道具拖到这里' }}</text>
|
||||
</view>
|
||||
<view class="items">
|
||||
@@ -27,7 +27,7 @@ import { playSound } from '../../utils/sound.js'
|
||||
export default {
|
||||
name: 'DragPlace',
|
||||
props: { config: { type: String, default: '' } },
|
||||
data() { return { dragIdx: -1, over: false, retries: 0, done: false, winDrop: false } },
|
||||
data() { return { dragIdx: -1, over: false, retries: 0, done: false, winDrop: false, target: null } },
|
||||
computed: {
|
||||
cfg() {
|
||||
try { return JSON.parse(this.config) } catch (e) { return { kind: 'drag_place', items: [], answer: 0 } }
|
||||
@@ -36,22 +36,20 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
propVisual,
|
||||
targetRect() {
|
||||
// 目标框位置(H5:用元素坐标近似,touch 事件 y < 200rpx 视为命中)
|
||||
const sys = uni.getSystemInfoSync()
|
||||
return sys.windowHeight * 0.28
|
||||
},
|
||||
onStart(e, i) {
|
||||
if (this.done) return
|
||||
this.dragIdx = i
|
||||
// 拿起时量一次目标框实际位置;touchmove 里纯坐标比较,避免逐帧调系统 API
|
||||
uni.createSelectorQuery().in(this).select('.target').boundingClientRect(rect => {
|
||||
this.target = rect || null
|
||||
}).exec()
|
||||
},
|
||||
onMove(e) {
|
||||
if (this.dragIdx === -1) return
|
||||
const t = e.touches && e.touches[0]
|
||||
if (!t) return
|
||||
// 手指 y 在屏幕上方 28% 内视为拖到目标框
|
||||
const sys = uni.getSystemInfoSync()
|
||||
this.over = t.clientY < sys.windowHeight * 0.28
|
||||
const r = this.target
|
||||
if (!t || !r) return
|
||||
this.over = t.clientX >= r.left && t.clientX <= r.right && t.clientY >= r.top && t.clientY <= r.bottom
|
||||
},
|
||||
onEnd(it, i) {
|
||||
if (this.done || this.dragIdx === -1) return
|
||||
@@ -77,20 +75,14 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.target { height: 220rpx; border-radius: 28rpx; border: 4rpx dashed #d5c4ae; display: flex; align-items: center; justify-content: center; margin-bottom: 48rpx; }
|
||||
.target { height: 220rpx; border-radius: 28rpx; border: 4rpx dashed #d5c4ae; display: flex; align-items: center; justify-content: center; margin-bottom: 48rpx; transition: transform .12s ease, box-shadow .12s ease; }
|
||||
.target.win { transform: scale(1.05); border: 4rpx solid #ffb347; animation: win-pulse .5s ease; }
|
||||
.target-text { color: #a08c74; font-size: 30rpx; }
|
||||
.items { display: flex; gap: 24rpx; justify-content: center; }
|
||||
.item { display: flex; flex-direction: column; align-items: center; background: #fff; border-radius: 20rpx; padding: 20rpx 24rpx; }
|
||||
.item { display: flex; flex-direction: column; align-items: center; background: #fff; border-radius: 20rpx; padding: 20rpx 24rpx; transition: transform .12s ease; }
|
||||
.item:active { transform: scale(.92); }
|
||||
.item.dragging { opacity: .5; transform: scale(1.05); }
|
||||
.item-icon { font-size: 52rpx; margin-bottom: 8rpx; }
|
||||
.item-name { font-size: 26rpx; color: #5b4636; }
|
||||
.item, .target { transition: transform .12s ease, box-shadow .12s ease; }
|
||||
.item:active { transform: scale(.92); }
|
||||
.target.win {
|
||||
transform: scale(1.08); border: 4rpx solid #ffb347;
|
||||
animation: win-pulse .5s ease;
|
||||
}
|
||||
.item.wrong { animation: shake .4s ease; border: 4rpx solid #ff6b6b; }
|
||||
@keyframes win-pulse { 0% { transform: scale(.9); } 50% { transform: scale(1.15); } 100% { transform: scale(1.08); } }
|
||||
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-10rpx); } 75% { transform: translateX(10rpx); } }
|
||||
@keyframes win-pulse { 0% { transform: scale(.9); } 50% { transform: scale(1.15); } 100% { transform: scale(1.05); } }
|
||||
</style>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { playSound } from '../../utils/sound.js'
|
||||
export default {
|
||||
name: 'FindSpot',
|
||||
props: { config: { type: String, default: '' } },
|
||||
data() { return { retries: 0, found: false, foundIdx: -1 } },
|
||||
data() { return { retries: 0, found: false, done: false, foundIdx: -1 } },
|
||||
computed: {
|
||||
cfg() {
|
||||
try { return JSON.parse(this.config) } catch (e) { return { kind: 'find_spot', items: [], answer: 0 } }
|
||||
@@ -43,16 +43,20 @@ export default {
|
||||
return spots[i % spots.length]
|
||||
},
|
||||
tap(it, i) {
|
||||
if (this.found) return
|
||||
if (this.found || this.done) return
|
||||
if (it.id === this.cfg.answer) {
|
||||
this.found = true
|
||||
this.done = true
|
||||
this.foundIdx = i
|
||||
playSound('good')
|
||||
this.$emit('done', { success: true })
|
||||
} else {
|
||||
this.retries++
|
||||
playSound('bad')
|
||||
if (this.retries >= 2) this.$emit('done', { success: false })
|
||||
if (this.retries >= 2) {
|
||||
this.done = true
|
||||
this.$emit('done', { success: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,16 +66,10 @@ export default {
|
||||
<style scoped>
|
||||
.scene { position: relative; height: 420rpx; border-radius: 28rpx; margin-bottom: 24rpx; }
|
||||
.scene-emoji { position: absolute; right: 20rpx; top: 10rpx; font-size: 120rpx; opacity: .35; }
|
||||
.spot { position: absolute; width: 100rpx; height: 100rpx; border-radius: 24rpx; background: #fff; box-shadow: 0 4rpx 14rpx rgba(91,70,54,.18); display: flex; align-items: center; justify-content: center; }
|
||||
.spot { position: absolute; width: 100rpx; height: 100rpx; border-radius: 24rpx; background: #fff; box-shadow: 0 4rpx 14rpx rgba(91,70,54,.18); display: flex; align-items: center; justify-content: center; transition: transform .12s ease; }
|
||||
.spot:active { transform: scale(.92); }
|
||||
.spot.win { transform: scale(1.08); border: 4rpx solid #ffb347; animation: win-pulse .5s ease; }
|
||||
.spot-icon { font-size: 52rpx; }
|
||||
.hint { text-align: center; color: #a08c74; }
|
||||
.spot { transition: transform .12s ease, box-shadow .12s ease; }
|
||||
.spot:active { transform: scale(.92); }
|
||||
.spot.win {
|
||||
transform: scale(1.08); border: 4rpx solid #ffb347;
|
||||
animation: win-pulse .5s ease;
|
||||
}
|
||||
.spot.wrong { animation: shake .4s ease; border: 4rpx solid #ff6b6b; }
|
||||
@keyframes win-pulse { 0% { transform: scale(.9); } 50% { transform: scale(1.15); } 100% { transform: scale(1.08); } }
|
||||
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-10rpx); } 75% { transform: translateX(10rpx); } }
|
||||
</style>
|
||||
|
||||
@@ -59,8 +59,9 @@ export default {
|
||||
propVisual,
|
||||
personVisual,
|
||||
lineY(i) {
|
||||
// 每条线固定插在人物与道具列之间,按匹配顺序错开
|
||||
return 60 + i * 110 + '%'
|
||||
// 在 12%-88% 区间按索引线性插值,避免固定步长在 i≥1 时越出视口
|
||||
const n = Math.max(this.persons.length - 1, 1)
|
||||
return 12 + (i / n) * 76 + '%'
|
||||
},
|
||||
tapPerson(p) {
|
||||
if (this.done) return
|
||||
@@ -76,15 +77,21 @@ export default {
|
||||
if (!this.selPerson || !this.selItem) return
|
||||
const pair = [this.selPerson, this.selItem]
|
||||
const ans = this.cfg.pairs || []
|
||||
// 已配对的组合重复点选不重复计数(否则同对可反复刷到 done)
|
||||
if (this.matches.some(([p, i]) => p === pair[0] && i === pair[1])) {
|
||||
this.selPerson = 0
|
||||
this.selItem = 0
|
||||
return
|
||||
}
|
||||
const correct = ans.some(([p, i]) => p === pair[0] && i === pair[1])
|
||||
if (correct) {
|
||||
this.matches.push(pair)
|
||||
this.winPair = true
|
||||
this.selPerson = 0
|
||||
this.selItem = 0
|
||||
playSound('good')
|
||||
if (this.matches.length === this.persons.length && this.matches.length === ans.length) {
|
||||
if (this.matches.length === ans.length) {
|
||||
this.done = true
|
||||
this.winPair = true
|
||||
playSound('good')
|
||||
this.$emit('done', { success: true })
|
||||
}
|
||||
} else {
|
||||
@@ -107,18 +114,12 @@ export default {
|
||||
.board { display: flex; justify-content: space-between; align-items: stretch; }
|
||||
.col { display: flex; flex-direction: column; gap: 24rpx; width: 36%; }
|
||||
.lines { width: 24%; position: relative; min-height: 400rpx; }
|
||||
.node { display: flex; align-items: center; background: #fff; border-radius: 20rpx; padding: 16rpx 20rpx; }
|
||||
.node { display: flex; align-items: center; background: #fff; border-radius: 20rpx; padding: 16rpx 20rpx; transition: transform .12s ease; }
|
||||
.node:active { transform: scale(.95); }
|
||||
.node.selected { border: 4rpx solid #ff9a3d; }
|
||||
.node.win { border: 4rpx solid #ffb347; animation: win-pulse .5s ease; }
|
||||
.node-avatar { width: 64rpx; height: 64rpx; border-radius: 50%; color: #fff; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-right: 16rpx; }
|
||||
.node-icon { font-size: 44rpx; margin-right: 16rpx; }
|
||||
.node-name { font-size: 28rpx; color: #5b4636; }
|
||||
.node { transition: transform .12s ease, box-shadow .12s ease; }
|
||||
.node:active { transform: scale(.92); }
|
||||
.node.win {
|
||||
transform: scale(1.08); border: 4rpx solid #ffb347;
|
||||
animation: win-pulse .5s ease;
|
||||
}
|
||||
.node.wrong { animation: shake .4s ease; border: 4rpx solid #ff6b6b; }
|
||||
@keyframes win-pulse { 0% { transform: scale(.9); } 50% { transform: scale(1.15); } 100% { transform: scale(1.08); } }
|
||||
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-10rpx); } 75% { transform: translateX(10rpx); } }
|
||||
@keyframes win-pulse { 0% { transform: scale(.95); } 50% { transform: scale(1.08); } 100% { transform: scale(1); } }
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
:class="{ picked: picked === i, win: winIdx === i, wrong: wrongIdx === i }"
|
||||
@click="pick(it, i)"
|
||||
>
|
||||
<view class="item-icon" :style="{ background: visual.color }">{{ visual.emoji }}</view>
|
||||
<view class="item-icon" :style="{ background: '#fff0e5' }">{{ propVisual(it.name).emoji }}</view>
|
||||
<view class="item-name">{{ it.name }}</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -25,21 +25,23 @@ export default {
|
||||
name: 'PropPick',
|
||||
props: { config: { type: String, default: '' } },
|
||||
data() {
|
||||
return { picked: -1, wrongIdx: -1, winIdx: -1, retries: 0 }
|
||||
return { picked: -1, wrongIdx: -1, winIdx: -1, retries: 0, done: false }
|
||||
},
|
||||
computed: {
|
||||
cfg() {
|
||||
try { return JSON.parse(this.config) } catch (e) { return { kind: 'prop_pick', items: [], answer: 0 } }
|
||||
},
|
||||
items() { return this.cfg.items || [] },
|
||||
visual() { return propVisual(this.items[this.picked] ? this.items[this.picked].name : '') }
|
||||
items() { return this.cfg.items || [] }
|
||||
},
|
||||
methods: {
|
||||
propVisual,
|
||||
pick(it, i) {
|
||||
if (this.done) return
|
||||
if (it.id === this.cfg.answer) {
|
||||
this.picked = i
|
||||
this.winIdx = i
|
||||
this.wrongIdx = -1
|
||||
this.done = true
|
||||
playSound('good')
|
||||
this.$emit('done', { success: true })
|
||||
} else {
|
||||
@@ -47,7 +49,10 @@ export default {
|
||||
this.wrongIdx = i
|
||||
this.picked = -1
|
||||
playSound('bad')
|
||||
if (this.retries >= 2) this.$emit('done', { success: false })
|
||||
if (this.retries >= 2) {
|
||||
this.done = true
|
||||
this.$emit('done', { success: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,19 +61,15 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.items { display: flex; gap: 24rpx; flex-wrap: wrap; justify-content: center; }
|
||||
.item { display: flex; flex-direction: column; align-items: center; background: #fff; border-radius: 20rpx; padding: 24rpx 28rpx; box-shadow: 0 4rpx 14rpx rgba(91,70,54,.08); }
|
||||
.item { display: flex; flex-direction: column; align-items: center; background: #fff; border-radius: 20rpx; padding: 24rpx 28rpx; box-shadow: 0 4rpx 14rpx rgba(91,70,54,.08); transition: transform .12s ease, box-shadow .12s ease; }
|
||||
.item:active { transform: scale(.92); }
|
||||
.item.picked { border: 4rpx solid #ff9a3d; }
|
||||
.item.win { transform: scale(1.08); border: 4rpx solid #ffb347; animation: win-pulse .5s ease; }
|
||||
.item.wrong { animation: shake .4s ease; border: 4rpx solid #ff6b6b; }
|
||||
.item-icon { width: 88rpx; height: 88rpx; border-radius: 24rpx; display: flex; align-items: center; justify-content: center; font-size: 48rpx; margin-bottom: 12rpx; }
|
||||
.item-name { font-size: 28rpx; color: #5b4636; font-weight: 600; }
|
||||
.hint { text-align: center; color: #a08c74; margin-bottom: 28rpx; }
|
||||
.wrong-tip { text-align: center; color: #ff6b6b; margin-top: 24rpx; }
|
||||
.item { transition: transform .12s ease, box-shadow .12s ease; }
|
||||
.item:active { transform: scale(.92); }
|
||||
.item.win {
|
||||
transform: scale(1.08); border: 4rpx solid #ffb347;
|
||||
animation: win-pulse .5s ease;
|
||||
}
|
||||
.item.wrong { animation: shake .4s ease; border: 4rpx solid #ff6b6b; }
|
||||
@keyframes win-pulse { 0% { transform: scale(.9); } 50% { transform: scale(1.15); } 100% { transform: scale(1.08); } }
|
||||
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-10rpx); } 75% { transform: translateX(10rpx); } }
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<view class="step-sort">
|
||||
<!-- touchmove 挂容器:touch 事件的 target 恒为 touchstart 元素,须按手指坐标判定目标槽位 -->
|
||||
<view class="step-sort" @touchmove.prevent="onMove($event)">
|
||||
<view class="hint">按住道具拖一拖,按顺序排好</view>
|
||||
<view
|
||||
v-for="(it, i) in items"
|
||||
:key="it.id"
|
||||
class="slot"
|
||||
:class="{ over: overIdx === i, dragging: dragIdx === i, win: allWin, wrong: wrong }"
|
||||
:class="{ over: overIdx === i, dragging: dragIdx === i, win: allWin }"
|
||||
@touchstart="onStart($event, i)"
|
||||
@touchmove.prevent="onMove($event, i)"
|
||||
@touchend="onEnd"
|
||||
>
|
||||
<view class="order-no">{{ i + 1 }}</view>
|
||||
@@ -25,7 +25,7 @@ import { playSound } from '../../utils/sound.js'
|
||||
export default {
|
||||
name: 'StepSort',
|
||||
props: { config: { type: String, default: '' } },
|
||||
data() { return { order: [], dragIdx: -1, overIdx: -1, retries: 0, wrong: false, done: false, allWin: false } },
|
||||
data() { return { order: [], dragIdx: -1, overIdx: -1, retries: 0, wrong: false, done: false, allWin: false, rects: [] } },
|
||||
computed: {
|
||||
cfg() {
|
||||
try { return JSON.parse(this.config) } catch (e) { return { kind: 'step_sort', items: [], answer_order: [] } }
|
||||
@@ -48,12 +48,27 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
propVisual,
|
||||
onStart(e, i) { this.dragIdx = i },
|
||||
onMove(e, i) {
|
||||
if (this.dragIdx === -1 || this.dragIdx === i) return
|
||||
this.overIdx = i
|
||||
onStart(e, i) {
|
||||
if (this.done) return
|
||||
this.dragIdx = i
|
||||
// 拿起时量一次所有槽位位置,touchmove 按坐标判定
|
||||
uni.createSelectorQuery().in(this).selectAll('.slot').boundingClientRect(rects => {
|
||||
this.rects = rects || []
|
||||
}).exec()
|
||||
},
|
||||
onMove(e) {
|
||||
if (this.dragIdx === -1) return
|
||||
const t = e.touches && e.touches[0]
|
||||
if (!t || !this.rects.length) return
|
||||
let idx = -1
|
||||
for (let i = 0; i < this.rects.length; i++) {
|
||||
const r = this.rects[i]
|
||||
if (t.clientY >= r.top && t.clientY <= r.bottom) { idx = i; break }
|
||||
}
|
||||
this.overIdx = idx
|
||||
},
|
||||
onEnd() {
|
||||
if (this.done) return
|
||||
if (this.dragIdx === -1 || this.overIdx === -1) { this.dragIdx = -1; this.overIdx = -1; return }
|
||||
const arr = this.order.slice()
|
||||
;[arr[this.dragIdx], arr[this.overIdx]] = [arr[this.overIdx], arr[this.dragIdx]]
|
||||
@@ -88,20 +103,14 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.hint { text-align: center; color: #a08c74; margin-bottom: 28rpx; }
|
||||
.slot { display: flex; align-items: center; background: #fff; border-radius: 20rpx; padding: 20rpx 24rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 14rpx rgba(91,70,54,.08); }
|
||||
.slot { display: flex; align-items: center; background: #fff; border-radius: 20rpx; padding: 20rpx 24rpx; margin-bottom: 20rpx; box-shadow: 0 4rpx 14rpx rgba(91,70,54,.08); transition: transform .12s ease; }
|
||||
.slot:active { transform: scale(.98); }
|
||||
.slot.over { border: 4rpx solid #ff9a3d; }
|
||||
.slot.dragging { opacity: .6; }
|
||||
.slot.win { border: 4rpx solid #ffb347; animation: win-pulse .5s ease; }
|
||||
.order-no { width: 56rpx; height: 56rpx; border-radius: 50%; background: #ff9a3d; color: #fff; font-weight: 800; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; }
|
||||
.slot-icon { font-size: 44rpx; margin-right: 20rpx; }
|
||||
.slot-name { font-size: 30rpx; color: #5b4636; font-weight: 600; }
|
||||
.wrong-tip { text-align: center; color: #ff6b6b; margin-top: 20rpx; }
|
||||
.slot { transition: transform .12s ease, box-shadow .12s ease; }
|
||||
.slot:active { transform: scale(.92); }
|
||||
.slot.win {
|
||||
transform: scale(1.08); border: 4rpx solid #ffb347;
|
||||
animation: win-pulse .5s ease;
|
||||
}
|
||||
.slot.wrong { animation: shake .4s ease; border: 4rpx solid #ff6b6b; }
|
||||
@keyframes win-pulse { 0% { transform: scale(.9); } 50% { transform: scale(1.15); } 100% { transform: scale(1.08); } }
|
||||
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-10rpx); } 75% { transform: translateX(10rpx); } }
|
||||
@keyframes win-pulse { 0% { transform: scale(.96); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user