qs_xinchun2026_h5/components/DongzhimenScene.vue

695 lines
17 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, getCurrentInstance as vueGetCurrentInstance, watch } from 'vue'
import ImagePreloader from '@/utils/preload'
// 获取 Vue 实例
const instance = vueGetCurrentInstance()
// 组件属性
const props = defineProps({
active: {
type: Boolean,
default: false
},
scrollPosition: {
type: Number,
default: 0
}
})
const emit = defineEmits(['collect-seal'])
// sq3图片显示状态
const sq3ImageVisible = ref(false)
// 是否已经收集福印
const sealCollected = ref(false)
// 计算视差效果的偏移量
const parallaxOffset = computed(() => {
return props.scrollPosition * 0.1
})
// 动态加载东直门场景图片
const getDzmImageUrl = (name) => {
return new URL(`/static/dzm/${name}`, import.meta.url).href
}
// 拖拽状态
const isDragging = ref(false)
const showDuck = ref(true) // 调试时默认显示鸭子
const deskImage = ref(getDzmImageUrl('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)
// 图片加载状态
const imagesLoaded = ref(false)
const isPreloading = ref(false)
// 初始化 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: getDzmImageUrl('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 = getDzmImageUrl('img_desk2.png')
// 隐藏引导元素
showGuideElements.value = false
// 隐藏鸭子
showDuck.value = false
// 禁用 Canvas 触摸事件
canvasDisabled.value = true
console.log('Canvas 触摸事件已禁用')
// 第一次成功放入目标区域时显示sq3图片并收集福印
if (!sealCollected.value) {
sq3ImageVisible.value = true
sealCollected.value = true
emit('collect-seal')
}
} else {
// 未放入目标区域,回归到原始位置
console.log('未放入目标区域,回归原始位置')
resetDuckPosition()
}
}
}
// 重置鸭子位置到初始值
const resetDuckPosition = () => {
// 恢复原始 rpx 值
duckRpx.value.x = 550
duckRpx.value.y = 220
// 更新 px 值
updateDuckPosition()
console.log('鸭子已回归原始位置:', duckX.value, duckY.value)
}
// 预加载图片
const preloadImages = async () => {
if (imagesLoaded.value || isPreloading.value) return
isPreloading.value = true
try {
// 需要预加载的图片列表
const imageUrls = [
getDzmImageUrl('img_duck.png'),
getDzmImageUrl('img_desk1.png'),
getDzmImageUrl('img_desk2.png'),
getDzmImageUrl('img_stove1.png'),
getDzmImageUrl('img_line.png'),
'/static/images/icon_hand.png'
]
// 使用 ImagePreloader 批量预加载图片
await ImagePreloader.preloadAll(imageUrls)
imagesLoaded.value = true
console.log('东直门场景图片预加载完成')
} catch (error) {
console.error('图片预加载失败:', error)
} finally {
isPreloading.value = false
}
}
// 开始拖拽(从触发区域开始)
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)
}
// 监听激活状态变化,当组件激活时预加载图片
watch(() => props.active, async (newActive) => {
if (newActive) {
await preloadImages()
}
})
// 页面挂载时的初始化
onMounted(() => {
// 添加动画类,触发入场动画
const container = document.querySelector('.dongzhimen-scene-container')
if (container) {
container.classList.add('animate-in')
}
// 预加载图片
preloadImages()
// 调试时自动初始化 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>
<!-- 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="getDzmImageUrl('img_stove1.png')" alt="灶台" class="deco-img stove-img" mode="widthFix" />
<image v-if="showGuideElements" :src="getDzmImageUrl('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>