qs_xinchun2026_h5/components/DongzhimenScene.vue

663 lines
16 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.

<script setup>
import { ref, onMounted, onUnmounted, computed, nextTick, getCurrentInstance as vueGetCurrentInstance } from 'vue'
import FuClickArea from './FuClickArea.vue'
// 获取 Vue 实例
const instance = vueGetCurrentInstance()
// 组件属性
const props = defineProps({
active: {
type: Boolean,
default: false
},
scrollPosition: {
type: Number,
default: 0
}
})
const emit = defineEmits(['collect-seal'])
// 是否收集福印
const sealCollected = ref(false)
// 福字点击区域状态
const fuClickAreaVisible = ref(true)
const sq3ImageVisible = ref(false)
// 计算视差效果的偏移量
const parallaxOffset = computed(() => {
return props.scrollPosition * 0.1
})
// 点击福字区域
const handleFuClick = () => {
fuClickAreaVisible.value = false
sq3ImageVisible.value = true
emit('collect-seal')
}
// 拖拽状态
const isDragging = ref(false)
const showDuck = ref(true) // 调试时默认显示鸭子
const deskImage = ref('/static/dzm/img_desk1.png')
const showGuideElements = ref(true)
// Canvas 触摸是否禁用
const canvasDisabled = ref(false)
// 调试模式
const debugMode = ref(false)
// Canvas 上下文
const ctx = ref(null)
// 鸭子图片路径
const duckImagePath = ref(null)
// rpx 转换比例
const rpxRatio = ref(0.5) // 默认 iPhone 6/7/8 的 375px / 750rpx = 0.5
// 鸭子位置和尺寸(使用 rpx 单位,在 750rpx 设计稿下)
const duckRpx = ref({
x: 550, // 275px * 2 = 550rpx
y: 220, // 110px * 2 = 220rpx
width: 72, // 36px * 2 = 72rpx
height: 140 // 70px * 2 = 140rpx
})
// 鸭子当前位置px 单位,用于绘制)
const duckX = ref(0)
const duckY = ref(0)
const duckWidth = ref(0)
const duckHeight = ref(0)
// 拖拽偏移量
const dragOffsetX = ref(0)
const dragOffsetY = ref(0)
// Canvas 位置和尺寸(使用 rpx 单位)
const canvasRpx = ref({
x: 0,
y: 1550, // 800px * 2 = 1600rpx
width: 750, // 375px * 2 = 750rpx (全屏宽度)
height: 600 // 300px * 2 = 600rpx
})
// Canvas 当前位置和尺寸px 单位)
const canvasRect = ref({
left: 0,
top: 0,
width: 0,
height: 0
})
// 目标区域 (餐桌区域) - 使用 rpx 单位
const targetAreaRpx = ref({
x: 0,
y: 160, // 80px * 2 = 160rpx
width: 400, // 200px * 2 = 400rpx
height: 300 // 150px * 2 = 300rpx
})
// 目标区域当前位置和尺寸px 单位)
const targetArea = ref({
x: 0,
y: 0,
width: 0,
height: 0
})
// 动画帧 ID
const animationId = ref(null)
// 初始化 Canvas
const initCanvas = () => {
console.log('正在初始化 Canvas...')
// 在 uni-app 中使用 uni.createCanvasContext
ctx.value = uni.createCanvasContext('dragDuckCanvas', instance)
if (ctx.value) {
console.log('Canvas 上下文创建成功')
// 加载鸭子图片
loadDuckImage()
// 获取 Canvas 位置
getCanvasPosition()
// 开始动画循环
startAnimation()
} else {
console.error('Canvas 上下文创建失败')
}
}
// 加载鸭子图片
const loadDuckImage = () => {
uni.getImageInfo({
src: '/static/dzm/img_duck.png',
success: (res) => {
console.log('鸭子图片加载成功:', res)
duckImagePath.value = res.path
},
fail: (err) => {
console.error('鸭子图片加载失败:', err)
}
})
}
// rpx 转 px
const rpxToPx = (rpx) => {
return rpx * rpxRatio.value
}
// 更新鸭子位置(根据 rpx 计算 px
const updateDuckPosition = () => {
duckX.value = rpxToPx(duckRpx.value.x)
duckY.value = rpxToPx(duckRpx.value.y)
duckWidth.value = rpxToPx(duckRpx.value.width)
duckHeight.value = rpxToPx(duckRpx.value.height)
}
// 更新目标区域(根据 rpx 计算 px
const updateTargetArea = () => {
targetArea.value = {
x: rpxToPx(targetAreaRpx.value.x),
y: rpxToPx(targetAreaRpx.value.y),
width: rpxToPx(targetAreaRpx.value.width),
height: rpxToPx(targetAreaRpx.value.height)
}
}
// 更新 Canvas 位置和尺寸(根据 rpx 计算 px
const updateCanvasRect = () => {
canvasRect.value = {
left: rpxToPx(canvasRpx.value.x),
top: rpxToPx(canvasRpx.value.y),
width: rpxToPx(canvasRpx.value.width),
height: rpxToPx(canvasRpx.value.height)
}
}
// Canvas 在视口中的实际位置(用于触摸计算)
const canvasBoundingRect = ref({
left: 0,
top: 0
})
// 获取 Canvas 元素位置
const getCanvasPosition = () => {
// 计算 rpx 比例:屏幕宽度 / 750
const systemInfo = uni.getSystemInfoSync()
rpxRatio.value = systemInfo.windowWidth / 750
console.log('屏幕宽度:', systemInfo.windowWidth, 'rpxRatio:', rpxRatio.value)
// 更新 Canvas、鸭子和目标区域的 px 值
updateCanvasRect()
updateDuckPosition()
updateTargetArea()
console.log('Canvas 位置信息:', canvasRect.value)
}
// 获取 Canvas 在视口中的实际位置(用于触摸坐标计算)
const getCanvasBoundingRect = () => {
const query = uni.createSelectorQuery().in(instance)
query.select('#dragDuckCanvas').boundingClientRect(res => {
if (res) {
canvasBoundingRect.value = {
left: res.left,
top: res.top
}
console.log('Canvas 视口位置:', canvasBoundingRect.value)
}
}).exec()
}
// 开始动画循环
const startAnimation = () => {
const animate = () => {
drawCanvas()
animationId.value = requestAnimationFrame(animate)
}
animationId.value = requestAnimationFrame(animate)
}
// 绘制 Canvas
const drawCanvas = () => {
if (!ctx.value) return
// 清除 Canvas
ctx.value.clearRect(0, 0, canvasRect.value.width || 400, canvasRect.value.height || 600)
// 绘制目标区域
drawTarget()
// 绘制鸭子
drawDuck()
// 执行绘制
ctx.value.draw(true)
}
// 绘制目标区域
const drawTarget = () => {
const target = targetArea.value
if (debugMode.value) {
// 调试模式:显示目标区域边框和文字
// 绘制目标区域背景
ctx.value.setFillStyle('rgba(52, 152, 219, 0.3)')
ctx.value.fillRect(target.x, target.y, target.width, target.height)
// 绘制目标区域边框
ctx.value.setStrokeStyle('#3498db')
ctx.value.setLineWidth(2)
ctx.value.setLineDash([5, 5])
ctx.value.strokeRect(target.x, target.y, target.width, target.height)
ctx.value.setLineDash([])
// 绘制目标区域文字
ctx.value.setFontSize(14)
ctx.value.setFillStyle('#3498db')
ctx.value.setTextAlign('center')
ctx.value.fillText(
'目标区域',
target.x + target.width / 2,
target.y + target.height / 2
)
}
// 非调试模式:完全透明,不绘制任何内容
}
// 绘制鸭子
const drawDuck = () => {
if (!showDuck.value) return
// 非调试模式下,只有拖拽时才显示鸭子
if (!debugMode.value && !isDragging.value) {
return
}
// 如果正在拖拽,设置半透明
if (isDragging.value) {
ctx.value.globalAlpha = 0.6
} else {
ctx.value.globalAlpha = 1.0
}
// 如果图片已加载,绘制图片
if (duckImagePath.value) {
ctx.value.drawImage(duckImagePath.value, duckX.value, duckY.value, duckWidth.value, duckHeight.value)
} else {
// 图片未加载时,绘制占位方块
ctx.value.setFillStyle('#ffcc00')
ctx.value.fillRect(duckX.value, duckY.value, duckWidth.value, duckHeight.value)
// 绘制边框
ctx.value.setStrokeStyle('#333')
ctx.value.setLineWidth(2)
ctx.value.strokeRect(duckX.value, duckY.value, duckWidth.value, duckHeight.value)
// 绘制文字
ctx.value.setFontSize(14)
ctx.value.setFillStyle('#FFFFFF')
ctx.value.setTextAlign('center')
ctx.value.setTextBaseline('middle')
ctx.value.fillText('鸭', duckX.value + duckWidth.value / 2, duckY.value + duckHeight.value / 2)
}
// 恢复透明度
ctx.value.globalAlpha = 1.0
// 调试模式下显示拖拽效果红框
if (debugMode.value && isDragging.value) {
// 绘制拖拽阴影
ctx.value.setFillStyle('rgba(231, 76, 60, 0.3)')
ctx.value.fillRect(duckX.value, duckY.value, duckWidth.value, duckHeight.value)
// 绘制拖拽边框
ctx.value.setStrokeStyle('#e74c3c')
ctx.value.setLineWidth(2)
ctx.value.setLineDash([5, 5])
ctx.value.strokeRect(duckX.value - 3, duckY.value - 3, duckWidth.value + 6, duckHeight.value + 6)
ctx.value.setLineDash([])
}
}
// 检查触摸点是否在鸭子内
const checkTouchInDuck = (x, y) => {
return x >= duckX.value &&
x <= duckX.value + duckWidth.value &&
y >= duckY.value &&
y <= duckY.value + duckHeight.value
}
// 检查鸭子是否在目标区域内
const checkDuckInTarget = () => {
const target = targetArea.value
// 计算鸭子中心点
const duckCenterX = duckX.value + duckWidth.value / 2
const duckCenterY = duckY.value + duckHeight.value / 2
// 检查中心点是否在目标区域内
return duckCenterX >= target.x &&
duckCenterX <= target.x + target.width &&
duckCenterY >= target.y &&
duckCenterY <= target.y + target.height
}
// 触摸开始事件
const handleTouchStart = (e) => {
// 如果 Canvas 已禁用,不处理触摸事件
if (canvasDisabled.value) {
console.log('Canvas 已禁用,忽略触摸事件')
return
}
console.log('触摸开始事件触发', e)
// 获取触摸点
const touch = e.touches[0]
if (!touch) {
console.error('没有触摸点信息')
return
}
// 获取 Canvas 在视口中的实际位置
getCanvasBoundingRect()
// 异步获取 Canvas 位置后执行检查
setTimeout(() => {
// 计算相对于 Canvas 的坐标(使用视口位置)
const touchX = touch.clientX - canvasBoundingRect.value.left
const touchY = touch.clientY - canvasBoundingRect.value.top
console.log(`触摸点 client: (${touch.clientX}, ${touch.clientY})`)
console.log(`Canvas 视口位置: (${canvasBoundingRect.value.left}, ${canvasBoundingRect.value.top})`)
console.log(`触摸点相对 Canvas: (${touchX}, ${touchY})`)
console.log(`鸭子位置: (${duckX.value}, ${duckY.value})`)
console.log(`鸭子尺寸: (${duckWidth.value}, ${duckHeight.value})`)
// 检查是否点击在鸭子上
if (checkTouchInDuck(touchX, touchY)) {
console.log('触摸点在鸭子内,开始拖拽')
// 设置拖拽状态和偏移量
isDragging.value = true
dragOffsetX.value = touchX - duckX.value
dragOffsetY.value = touchY - duckY.value
showDuck.value = true
} else {
console.log('触摸点不在鸭子内')
}
}, 50)
}
// 触摸移动事件
const handleTouchMove = (e) => {
if (!isDragging.value) return
e.preventDefault()
const touch = e.touches[0]
if (!touch) return
// 计算相对于 Canvas 的坐标(使用视口位置)
const touchX = touch.clientX - canvasBoundingRect.value.left
const touchY = touch.clientY - canvasBoundingRect.value.top
// 更新鸭子位置
duckX.value = touchX - dragOffsetX.value
duckY.value = touchY - dragOffsetY.value
// 限制边界
const canvasW = canvasRect.value.width || 400
const canvasH = canvasRect.value.height || 600
duckX.value = Math.max(0, Math.min(canvasW - duckWidth.value, duckX.value))
duckY.value = Math.max(0, Math.min(canvasH - duckHeight.value, duckY.value))
console.log(`鸭子新位置: (${duckX.value}, ${duckY.value})`)
}
// 触摸结束事件
const handleTouchEnd = () => {
console.log('触摸结束事件触发')
if (isDragging.value) {
isDragging.value = false
// 检查是否成功放入目标区域
if (checkDuckInTarget()) {
console.log('成功放入目标区域!')
// 更换餐桌图片
deskImage.value = '/static/dzm/img_desk2.png'
// 隐藏引导元素
showGuideElements.value = false
// 隐藏鸭子
showDuck.value = false
// 禁用 Canvas 触摸事件
canvasDisabled.value = true
console.log('Canvas 触摸事件已禁用')
} else {
// 未放入目标区域,回归到原始位置
console.log('未放入目标区域,回归原始位置')
resetDuckPosition()
}
}
}
// 重置鸭子位置到初始值
const resetDuckPosition = () => {
// 恢复原始 rpx 值
duckRpx.value.x = 550
duckRpx.value.y = 220
// 更新 px 值
updateDuckPosition()
console.log('鸭子已回归原始位置:', duckX.value, duckY.value)
}
// 开始拖拽(从触发区域开始)
const startDrag = (e) => {
console.log('开始拖拽', e)
// 初始化 Canvas
if (!ctx.value) {
initCanvas()
}
// 设置鸭子初始位置(使用 rpx 值,会自动转换为 px
duckRpx.value.x = 100 // 100rpx 对应约 50px (iPhone 6/7/8)
duckRpx.value.y = 100
updateDuckPosition()
showDuck.value = true
// 触发触摸开始事件处理
handleTouchStart(e)
}
// 页面挂载时的初始化
onMounted(() => {
// 添加动画类,触发入场动画
const container = document.querySelector('.dongzhimen-scene-container')
if (container) {
container.classList.add('animate-in')
}
// 调试时自动初始化 Canvas
setTimeout(() => {
initCanvas()
}, 500)
})
// 组件卸载时清理
onUnmounted(() => {
// 停止动画
if (animationId.value) {
cancelAnimationFrame(animationId.value)
}
})
</script>
<template>
<view class="dongzhimen-scene-container" :class="{ 'active': active }">
<!-- 背景图片层 -->
<view class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
<image src="/static/bg/bg5.jpg" alt="东直门商圈" class="background-image" mode="widthFix" />
</view>
<!-- 福字点击区域 -->
<FuClickArea
:visible="fuClickAreaVisible"
:x-range="630"
:y-range="400"
:y-start="150"
:fu-width="100"
:fu-height="100"
@click="handleFuClick"
/>
<!-- sq3图片 -->
<image
v-if="sq3ImageVisible"
src="/static/images/sq5.png"
alt="新春祝福"
class="sq-image"
mode="widthFix"
/>
<!-- 装饰图片 -->
<image :src="deskImage" alt="餐桌" class="deco-img desk-img" mode="widthFix" />
<image src="/static/dzm/img_stove1.png" alt="灶台" class="deco-img stove-img" mode="widthFix" />
<image v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" mode="widthFix" />
<image v-if="showGuideElements" src="/static/images/icon_hand.png" alt="手势" class="deco-img hand-img" mode="widthFix" />
<!-- Canvas 拖拽区域 -->
<canvas
canvas-id="dragDuckCanvas"
id="dragDuckCanvas"
class="drag-canvas"
:style="{
left: canvasRect.left + 'px',
top: canvasRect.top + 'px',
width: canvasRect.width + 'px',
height: canvasRect.height + 'px'
}"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
<!-- 拖拽触发区域 -->
<view
v-if="showGuideElements"
class="drag-trigger-area"
@touchstart="startDrag"
></view>
</view>
</template>
<style scoped>
.dongzhimen-scene-container {
position: relative;
width: 100%;
height: auto;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
background-color: #ff6b35;
}
.background-layer {
position: relative;
width: 100%;
transition: transform 0.1s ease;
height: auto;
}
.background-image {
width: 100%;
height: auto;
display: block;
}
.sq-image {
position: absolute;
top: 220rpx;
right: -6rpx;
width: 300rpx;
height: auto;
z-index: 20;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.deco-img {
position: absolute;
z-index: 25;
}
.desk-img {
left: 13rpx;
top: 1665rpx;
width: 441rpx;
height: auto;
}
.stove-img {
left: 492rpx;
top: 1711rpx;
width: 241rpx;
height: auto;
}
.line-img {
left: 250rpx;
top: 1842rpx;
width: 360rpx;
height: auto;
}
.hand-img {
left: 440rpx;
top: 1900rpx;
width: 38rpx;
height: auto;
animation: arcSlideLeft 1.2s ease-in-out infinite;
}
@keyframes arcSlideLeft {
0% {
transform: translateX(0) translateY(0);
opacity: 1;
}
100% {
transform: translateX(-80rpx) translateY(3rpx);
opacity: 1;
}
}
.drag-canvas {
position: absolute;
z-index: 100;
pointer-events: auto;
}
.drag-trigger-area {
position: absolute;
left: 540rpx;
top: 1781rpx;
width: 100rpx;
height: 100rpx;
z-index: 30;
}
</style>