Commit 076aeaaf by Hantao

feat(拼图游戏): 实现零件拖拽与进度显示功能

- 实现拖拽交互逻辑,支持零件从托盘拖拽到中心区域
- 添加拼图进度显示,根据已放置零件数量更新进度条
- 完成拼图后显示完成弹窗
- 优化界面布局和样式,适配不同尺寸的拼图零件
parent e485bbf1
<script setup> <script setup>
import { ref, onMounted, getCurrentInstance, computed } from 'vue';
import { useDrag } from './hooks/useDrag';
import navBar from '@/components/navBar.vue'; import navBar from '@/components/navBar.vue';
const ASSETS = Object.freeze({ const ASSETS = Object.freeze({
title: '/static/second/part1/title.webp', title: '/static/second/part1/title.webp',
center: '/static/second/part1/center.webp', center: '/static/second/part1/center.webp',
zzjd: '/static/second/zzjd1.webp', zzjd1: '/static/second/zzjd1.webp',
zzjd2: '/static/second/zzjd2.webp',
zzjd3: '/static/second/zzjd3.webp',
zzjd4: '/static/second/zzjd4.webp',
zzjd5: '/static/second/zzjd5.webp',
zzjd6: '/static/second/zzjd6.webp',
zzjd: '/static/second/zzjd.webp',
panzi: '/static/second/panzi.webp', panzi: '/static/second/panzi.webp',
resetIcon: '/static/second/reset.webp', resetIcon: '/static/second/reset.webp',
blockIcon: '/static/second/block.webp', blockIcon: '/static/second/block.webp',
arrow: '/static/second/jt.webp', arrow: '/static/second/jt.webp',
puzzle1: '/static/second/part1/1.webp',
puzzle2: '/static/second/part1/2.webp',
puzzle3: '/static/second/part1/3.webp',
puzzle4: '/static/second/part1/4.webp',
puzzle5: '/static/second/part1/5.webp',
puzzle6: '/static/second/part1/6.webp',
puzzle1Grey: '/static/second/part1/1-grey.webp',
puzzle2Grey: '/static/second/part1/2-grey.webp',
puzzle3Grey: '/static/second/part1/3-grey.webp',
puzzle4Grey: '/static/second/part1/4-grey.webp',
puzzle5Grey: '/static/second/part1/5-grey.webp',
puzzle6Grey: '/static/second/part1/6-grey.webp',
puzzle1Finish: '/static/second/part1/1-finish.webp',
puzzle2Finish: '/static/second/part1/2-finish.webp',
puzzle3Finish: '/static/second/part1/3-finish.webp',
puzzle4Finish: '/static/second/part1/4-finish.webp',
puzzle5Finish: '/static/second/part1/5-finish.webp',
puzzle6Finish: '/static/second/part1/6-finish.webp',
puzzle1Move: '/static/second/part1/1-move.webp',
puzzle2Move: '/static/second/part1/2-move.webp',
puzzle3Move: '/static/second/part1/3-move.webp',
puzzle4Move: '/static/second/part1/4-move.webp',
puzzle5Move: '/static/second/part1/5-move.webp',
puzzle6Move: '/static/second/part1/6-move.webp',
puzzle1Wait: '/static/second/part1/1-wait.webp',
puzzle2Wait: '/static/second/part1/2-wait.webp',
puzzle3Wait: '/static/second/part1/3-wait.webp',
puzzle4Wait: '/static/second/part1/4-wait.webp',
puzzle5Wait: '/static/second/part1/5-wait.webp',
puzzle6Wait: '/static/second/part1/6-wait.webp',
completionPopup: 'https://userone-oss-cdn.angelgroup.com.cn/static/2026-01-23/d204d71cf2a54b18a6404e6132618cd6%E5%AE%8C%E6%88%90%E5%BC%B9%E7%AA%97.webp',
}); });
const PUZZLE_DIMENSIONS = {
center: {
1: { width: '138rpx', height: '126rpx' },
2: { width: '164rpx', height: '140rpx' },
3: { width: '130rpx', height: '140rpx' },
4: { width: '132rpx', height: '130rpx' },
5: { width: '162rpx', height: '130rpx' },
6: { width: '126rpx', height: '130rpx' },
},
tray: {
1: { width: '154rpx', height: '132rpx' },
2: { width: '172rpx', height: '148rpx' },
3: { width: '136rpx', height: '148rpx' },
4: { width: '144rpx', height: '146rpx' },
5: { width: '170rpx', height: '146rpx' },
6: { width: '140rpx', height: '146rpx' },
},
move: {
1: { width: '204rpx', height: '186rpx' },
2: { width: '260rpx', height: '214rpx' },
3: { width: '190rpx', height: '214rpx' },
4: { width: '208rpx', height: '212rpx' },
5: { width: '252rpx', height: '212rpx' },
6: { width: '198rpx', height: '212rpx' },
}
};
const trayItems = ref([
{ id: 1, type: 'puzzle1', status: 'default' },
{ id: 2, type: 'puzzle2', status: 'default' },
{ id: 3, type: 'puzzle3', status: 'default' },
{ id: 4, type: 'puzzle4', status: 'default' },
{ id: 5, type: 'puzzle5', status: 'default' },
{ id: 6, type: 'puzzle6', status: 'default' },
]);
const placedCount = computed(() => {
return trayItems.value.filter(item => item.status === 'placed').length;
});
const isComplete = computed(() => {
return placedCount.value === 6;
});
const PROGRESS_MAP = [
ASSETS.zzjd1, // 0
ASSETS.zzjd2, // 1
ASSETS.zzjd3, // 2
ASSETS.zzjd4, // 3
ASSETS.zzjd5, // 4
ASSETS.zzjd6, // 5
ASSETS.zzjd, // 6
];
const currentProgressImg = computed(() => {
return PROGRESS_MAP[placedCount.value] || ASSETS.zzjd1;
});
const currentProgressImgStyle = computed(() => {
const currentImg = PROGRESS_MAP[placedCount.value] || ASSETS.zzjd1;
if (currentImg === ASSETS.zzjd6) {
return { width: '94rpx', height: '580rpx' };
}
if (currentImg === ASSETS.zzjd) {
return { width: '70rpx', height: '554rpx' };
}
// 默认尺寸
return { width: '94rpx', height: '548rpx' };
});
const showCompletion = ref(false);
const handleReset = () => {
trayItems.value.forEach(item => {
item.status = 'default';
});
};
const handleComplete = () => {
if (isComplete.value) {
showCompletion.value = true;
}
};
const closeCompletion = () => {
showCompletion.value = false;
};
const targetRects = ref({});
const instance = getCurrentInstance();
onMounted(() => {
const query = uni.createSelectorQuery().in(instance);
// 获取每个拼图放置区域的坐标
for (let i = 1; i <= 6; i++) {
query.select(`.puzzle-zone-${i}`).boundingClientRect(data => {
if (data) {
targetRects.value[i] = data;
}
});
}
query.exec();
});
const {
isDragging,
draggedItem,
dragPosition,
handleTouchStart,
handleTouchMove,
handleTouchEnd,
handleMouseDown,
handleMouseMove,
handleMouseUp,
} = useDrag({
onDrop: (item, position) => {
if (item && targetRects.value[item.id]) {
const rect = targetRects.value[item.id];
const { x, y } = position;
// 简单的碰撞检测
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
item.status = 'placed';
}
}
}
});
// 获取托盘中拼图的图标
const getTrayItemIcon = (item) => {
if (item.status === 'placed') {
return ASSETS[`${item.type}Grey`];
}
if (draggedItem.value && draggedItem.value.id === item.id) {
return ASSETS[`${item.type}Grey`]; // 拖拽时托盘显示灰色
}
return ASSETS[`${item.type}`];
};
// 获取拖拽时的镜像图标
const getGhostIcon = () => {
if (!draggedItem.value) return '';
return ASSETS[`${draggedItem.value.type}Move`];
};
// 获取中间区域拼图的图标
const getCenterItemIcon = (index) => {
const item = trayItems.value.find(i => i.id === index);
if (item && item.status === 'placed') {
return ASSETS[`puzzle${index}Finish`];
}
return ASSETS[`puzzle${index}Wait`];
};
const getTrayItemStyle = (item) => {
const id = item.id;
return PUZZLE_DIMENSIONS.tray[id];
};
const getGhostStyle = () => {
if (!draggedItem.value) return {};
const id = draggedItem.value.id;
return PUZZLE_DIMENSIONS.move[id];
};
const getCenterItemStyle = (index) => {
return PUZZLE_DIMENSIONS.center[index];
};
</script> </script>
<template> <template>
<view class="part1-container"> <view class="part1-container">
<navBar /> <navBar />
<!-- 拖拽时的镜像 -->
<view
v-if="isDragging"
class="drag-ghost"
:style="{ left: dragPosition.x + 'px', top: dragPosition.y + 'px' }"
>
<image :src="getGhostIcon()" :style="getGhostStyle()" mode="scaleToFill" class="ghost-icon" />
</view>
<!-- 顶部标题 --> <!-- 顶部标题 -->
<view class="header-section"> <view class="header-section">
<image class="title-img" :src="ASSETS.title" mode="widthFix" /> <image class="title-img" :src="ASSETS.title" mode="widthFix" />
...@@ -24,19 +243,42 @@ const ASSETS = Object.freeze({ ...@@ -24,19 +243,42 @@ const ASSETS = Object.freeze({
<view class="main-area"> <view class="main-area">
<!-- 中间拼图区域 --> <!-- 中间拼图区域 -->
<view class="center-wrapper"> <view class="center-wrapper">
<image class="center-img" :src="ASSETS.center" mode="widthFix" /> <image class="center-img" :src="ASSETS.center" mode="scaleToFill" />
<!-- 拼图放置区域 -->
<view class="puzzle-zones">
<view v-for="i in 6" :key="i" :class="['puzzle-zone', `puzzle-zone-${i}`]">
<image :src="getCenterItemIcon(i)" :style="getCenterItemStyle(i)" mode="scaleToFill" class="zone-img" />
</view>
</view>
</view> </view>
<!-- 右侧进度条 --> <!-- 右侧进度条 -->
<view class="progress-wrapper"> <view class="progress-wrapper">
<text class="progress-title">组装进度</text> <text class="progress-title">组装进度</text>
<image class="progress-img" :src="ASSETS.zzjd" mode="scaleToFill" /> <image class="progress-img" :src="currentProgressImg" :style="currentProgressImgStyle" mode="scaleToFill" />
</view> </view>
</view> </view>
<!-- 下方托盘区域 --> <!-- 下方托盘区域 -->
<view class="tray-area"> <view class="tray-area">
<image class="tray-img" :src="ASSETS.panzi" mode="scaleToFill" /> <image class="tray-img" :src="ASSETS.panzi" mode="scaleToFill" />
<view class="tray-items-grid">
<view
v-for="item in trayItems"
:key="item.id"
class="tray-item"
@touchstart="handleTouchStart($event, item)"
@touchmove.stop.prevent="handleTouchMove"
@touchend="handleTouchEnd(item)"
@mousedown="handleMouseDown($event, item)"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
>
<image class="item-icon" :src="getTrayItemIcon(item)" :style="getTrayItemStyle(item)" mode="scaleToFill" />
</view>
</view>
</view> </view>
<!-- 底部按钮 --> <!-- 底部按钮 -->
...@@ -45,16 +287,33 @@ const ASSETS = Object.freeze({ ...@@ -45,16 +287,33 @@ const ASSETS = Object.freeze({
<image class="btn-icon" :src="ASSETS.resetIcon" mode="widthFix" /> <image class="btn-icon" :src="ASSETS.resetIcon" mode="widthFix" />
<text>重置</text> <text>重置</text>
</view> </view>
<view class="action-btn complete-btn" @click="handleComplete"> <view
<image class="btn-icon block-icon" :src="ASSETS.blockIcon" mode="widthFix" /> :class="['action-btn', 'complete-btn', { 'complete-btn-active': placedCount === 6 }]"
<view class="btn-text-col"> @click="handleComplete"
<text class="main-text">完成</text> >
<text class="sub-text">完成0/6吸附</text> <template v-if="placedCount < 6">
</view> <image class="btn-icon block-icon" :src="ASSETS.blockIcon" mode="widthFix" />
<view class="btn-text-col">
<text class="main-text">完成</text>
<text class="sub-text">完成{{ placedCount }}/6吸附</text>
</view>
</template>
<template v-else>
<text class="complete-text">完成</text>
</template>
</view> </view>
</view> </view>
<image class="arrow-icon" :src="ASSETS.arrow" mode="scaleToFill" /> <image class="arrow-icon" :src="ASSETS.arrow" mode="scaleToFill" />
<view class="completion-overlay" v-if="showCompletion" @click="closeCompletion">
<image
class="completion-img"
:src="ASSETS.completionPopup"
mode="scaleToFill"
@click.stop="closeCompletion"
/>
</view>
</view> </view>
</template> </template>
...@@ -96,8 +355,8 @@ const ASSETS = Object.freeze({ ...@@ -96,8 +355,8 @@ const ASSETS = Object.freeze({
margin-left: -130rpx; margin-left: -130rpx;
.center-img { .center-img {
width: 580rpx; width: 600rpx;
height: auto; height: 600rpx;
} }
} }
...@@ -122,6 +381,40 @@ const ASSETS = Object.freeze({ ...@@ -122,6 +381,40 @@ const ASSETS = Object.freeze({
} }
} }
.drag-ghost {
position: fixed;
z-index: 9999;
transform: translate(-50%, -50%);
pointer-events: none;
.ghost-icon {
/* width: 200rpx; removed fixed size */
/* height: auto; */
}
}
.puzzle-zones {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.puzzle-zone {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
.puzzle-zone-1 { top: 22rpx; left: 50%; transform: translateX(-50%); }
.puzzle-zone-2 { top: 130rpx; left: 42rpx; }
.puzzle-zone-6 { top: 130rpx; right: 70rpx; }
.puzzle-zone-3 { bottom: 120rpx; left: 70rpx; }
.puzzle-zone-5 { bottom: 130rpx; right: 42rpx; }
.puzzle-zone-4 { bottom: 40rpx; left: 50%; transform: translateX(-50%); }
.tray-area { .tray-area {
width: 88%; width: 88%;
display: flex; display: flex;
...@@ -136,6 +429,23 @@ const ASSETS = Object.freeze({ ...@@ -136,6 +429,23 @@ const ASSETS = Object.freeze({
width: 100%; width: 100%;
height: 466rpx; height: 466rpx;
} }
.tray-items-grid {
position: absolute;
top: 50rpx;
left: 46%;
transform: translateX(-50%);
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30rpx 60rpx;
width: 540rpx;
.tray-item {
display: flex;
flex-direction: column;
align-items: center;
}
}
} }
.btn-group { .btn-group {
...@@ -203,15 +513,43 @@ const ASSETS = Object.freeze({ ...@@ -203,15 +513,43 @@ const ASSETS = Object.freeze({
line-height: 1; line-height: 1;
} }
} }
.complete-text {
font-size: 34rpx;
font-weight: bold;
}
&.complete-btn-active {
border-radius: 40rpx;
background: #FAD43E;
box-shadow: 0 4rpx 2rpx 0 #ffffff80 inset, 0 6rpx 2rpx 0 #3d330ba6;
}
} }
} }
.arrow-icon { .arrow-icon {
position: absolute; position: absolute;
top: 48%; top: 48%;
left: 62%; left: 66%;
width: 116rpx; width: 116rpx;
height: 144rpx; height: 144rpx;
z-index: 100; z-index: 100;
} }
.completion-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
.completion-img {
width: 100%;
height: 100%;
}
}
</style> </style>
...@@ -63,6 +63,21 @@ const currentProgressImg = computed(() => { ...@@ -63,6 +63,21 @@ const currentProgressImg = computed(() => {
return PROGRESS_MAP[placedCount.value] || ASSETS.zzjd1; return PROGRESS_MAP[placedCount.value] || ASSETS.zzjd1;
}); });
const currentProgressImgStyle = computed(() => {
const currentImg = PROGRESS_MAP[placedCount.value] || ASSETS.zzjd1;
if (currentImg === ASSETS.zzjd6) {
return { width: '94rpx', height: '580rpx' };
}
if (currentImg === ASSETS.zzjd) {
return { width: '70rpx', height: '554rpx' };
}
// 默认尺寸
return { width: '94rpx', height: '548rpx' };
});
const showCompletion = ref(false); const showCompletion = ref(false);
const handleReset = () => { const handleReset = () => {
...@@ -182,7 +197,7 @@ const getGhostIcon = () => { ...@@ -182,7 +197,7 @@ const getGhostIcon = () => {
<!-- 右侧进度条 --> <!-- 右侧进度条 -->
<view class="progress-wrapper"> <view class="progress-wrapper">
<text class="progress-title">组装进度</text> <text class="progress-title">组装进度</text>
<image class="progress-img" :src="currentProgressImg" mode="scaleToFill" /> <image class="progress-img" :src="currentProgressImg" :style="currentProgressImgStyle" mode="scaleToFill" />
</view> </view>
<!-- 下方托盘区域 --> <!-- 下方托盘区域 -->
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment