Commit 92a4a283 by Hantao

feat(电池排序): 重构拖拽放置逻辑并增强用户反馈

- 重构电池放置逻辑,使用双向映射和状态管理替代硬编码配置
- 新增放置错误提示(红色高亮)和进度项动态高亮
- 增加蓝色电池第三个热区,并添加前置黑色电池放置条件
- 统一电池样式配置,支持不同排列位置的差异化显示
- 优化代码结构,提取常量数组和工具函数,减少重复代码
parent d0ad412c
......@@ -28,18 +28,14 @@ const ASSETS = Object.freeze({
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',
});
// 电池类型与 arrange 区域的映射关系
const BATTERY_ARRANGE_MAP = Object.freeze({
white: 'arrange1',
grey: 'arrange2',
blue: 'arrange3',
black: 'arrange4',
});
const BATTERY_TYPES = ['white', 'grey', 'blue', 'black'];
const ARRANGE_KEYS = ['arrange1', 'arrange2', 'arrange3', 'arrange4'];
// 放置顺序配置
const ARRANGE_ORDER = ['arrange1', 'arrange2', 'arrange3', 'arrange4'];
// 电池类型与arrange区域的双向映射
const BATTERY_ARRANGE_MAP = Object.freeze({ white: 'arrange1', grey: 'arrange2', blue: 'arrange3', black: 'arrange4' });
const ARRANGE_BATTERY_MAP = Object.freeze(Object.fromEntries(Object.entries(BATTERY_ARRANGE_MAP).map(([k, v]) => [v, k])));
// 热区配置:每个热区对应的电池索引
// 热区配置
const HOTAREA_CONFIG = Object.freeze([
{ class: 'btn-white1', batteryIndex: 0 },
{ class: 'btn-white2', batteryIndex: 0 },
......@@ -48,6 +44,7 @@ const HOTAREA_CONFIG = Object.freeze([
{ class: 'btn-black', batteryIndex: 2 },
{ class: 'btn-blue1', batteryIndex: 3 },
{ class: 'btn-blue2', batteryIndex: 3 },
{ class: 'btn-blue3', batteryIndex: 3, requireBlackPlaced: true },
]);
// 进度列表配置
......@@ -58,13 +55,36 @@ const PROGRESS_LIST = Object.freeze([
{ key: 'arrange4', label: '后置炭棒(4/4)' },
]);
// 排序区域配置
const ARRANGE_CONFIG = Object.freeze([
{ key: 'arrange1', imgKey: 'white2', imgClass: 'white2' },
{ key: 'arrange2', imgKey: 'grey2', imgClass: 'grey2' },
{ key: 'arrange3', imgKey: 'blue2', imgClass: 'blue2' },
{ key: 'arrange4', imgKey: 'black2', imgClass: 'black2' },
]);
// 电池在各个arrange位置的样式配置
const BATTERY_STYLE_MAP = Object.freeze({
white: {
arrange1: { left: '-16rpx', top: '-96rpx', width: '330rpx', height: '330rpx' },
arrange2: { left: '-20rpx', top: '-100rpx', width: '342rpx', height: '342rpx' },
arrange3: { left: '-20rpx', top: '-100rpx', width: '342rpx', height: '342rpx' },
arrange4: { left: '-20rpx', top: '-100rpx', width: '342rpx', height: '342rpx' },
},
grey: {
arrange1: { left: '8rpx', top: '20rpx', width: '270rpx', height: '100rpx' },
arrange2: { left: '20rpx', top: '20rpx', width: '260rpx', height: '98rpx' },
arrange3: { left: '8rpx', top: '20rpx', width: '270rpx', height: '100rpx' },
arrange4: { left: '8rpx', top: '16rpx', width: '270rpx', height: '100rpx' },
},
blue: {
arrange1: { left: '0', top: '-44rpx', width: '298rpx', height: '224rpx' },
arrange2: { left: '0', top: '-44rpx', width: '298rpx', height: '224rpx' },
arrange3: { left: '0', top: '-44rpx', width: '298rpx', height: '224rpx' },
arrange4: { left: '0', top: '-44rpx', width: '298rpx', height: '224rpx' },
},
black: {
arrange1: { left: '-12rpx', top: '-92rpx', width: '324rpx', height: '324rpx' },
arrange2: { left: '-12rpx', top: '-92rpx', width: '324rpx', height: '324rpx' },
arrange3: { left: '-12rpx', top: '-92rpx', width: '324rpx', height: '324rpx' },
arrange4: { left: '-12rpx', top: '-92rpx', width: '324rpx', height: '324rpx' },
},
});
const PROGRESS_IMAGES = [ASSETS.empty, ASSETS.progress1, ASSETS.progress2, ASSETS.progress3, ASSETS.progress4];
const createState = (keys, defaultVal) => Object.fromEntries(keys.map(k => [k, defaultVal]));
const batteryItems = ref([
{ id: 1, type: 'white', icon: ASSETS.white, dragIcon: ASSETS.white1, status: 'default' },
......@@ -73,146 +93,95 @@ const batteryItems = ref([
{ id: 4, type: 'blue', icon: ASSETS.blue, dragIcon: ASSETS.blue1, status: 'default' },
]);
const arrangeStatus = ref({
arrange1: false,
arrange2: false,
arrange3: false,
arrange4: false,
});
const arrangeStatus = ref(createState(ARRANGE_KEYS, false));
const arrangeError = ref(createState(ARRANGE_KEYS, false));
const arrangePlaced = ref(createState(ARRANGE_KEYS, null));
const batteryCorrectlyPlaced = ref(createState(BATTERY_TYPES, false));
const arrangeRects = ref({});
// 计算已完成的排序数量
const completedCount = computed(() => {
return Object.values(arrangeStatus.value).filter(Boolean).length;
});
// 根据完成数量返回对应的进度条图片
const tasteBarImage = computed(() => {
const images = [ASSETS.empty, ASSETS.progress1, ASSETS.progress2, ASSETS.progress3, ASSETS.progress4];
return images[completedCount.value];
});
// 是否全部完成
const isComplete = computed(() => {
return completedCount.value === 4;
});
const showCompletion = ref(false);
// 重置
const completedCount = computed(() => Object.values(batteryCorrectlyPlaced.value).filter(Boolean).length);
const tasteBarImage = computed(() => PROGRESS_IMAGES[completedCount.value]);
const isComplete = computed(() => completedCount.value === 4);
const handleReset = () => {
batteryItems.value.forEach(item => {
item.status = 'default';
});
Object.keys(arrangeStatus.value).forEach(key => {
batteryItems.value.forEach(item => item.status = 'default');
ARRANGE_KEYS.forEach(key => {
arrangeStatus.value[key] = false;
arrangeError.value[key] = false;
arrangePlaced.value[key] = null;
});
BATTERY_TYPES.forEach(type => batteryCorrectlyPlaced.value[type] = false);
};
// 完成
const handleComplete = () => {
if (isComplete.value) {
showCompletion.value = true;
}
};
// 关闭弹窗
const closeCompletion = () => {
showCompletion.value = false;
};
const instance = getCurrentInstance();
onMounted(() => {
const query = uni.createSelectorQuery().in(instance);
Object.values(BATTERY_ARRANGE_MAP).forEach(name => {
query.select(`.${name}`).boundingClientRect(data => {
if (data) arrangeRects.value[name] = data;
});
});
query.exec();
});
const handleComplete = () => isComplete.value && (showCompletion.value = true);
const closeCompletion = () => showCompletion.value = false;
// 获取电池图标
const getBatteryIcon = (item) => {
return draggedItem.value?.id === item.id ? item.dragIcon : item.icon;
};
// 获取拖拽镜像图标
const getBatteryIcon = (item) => draggedItem.value?.id === item.id ? item.dragIcon : item.icon;
const getGhostIcon = () => draggedItem.value?.dragIcon || '';
// 获取拖拽电池类型
const getDraggedType = () => draggedItem.value?.type || '';
// 判断电池是否正在被拖拽
const isDraggingItem = (item) => {
return isDragging.value && draggedItem.value?.id === item.id;
// 获取放置图片样式
const getPlacedImgStyle = (arrangeKey) => {
const batteryType = arrangePlaced.value[arrangeKey];
const style = BATTERY_STYLE_MAP[batteryType]?.[arrangeKey];
return style ? { position: 'absolute', ...style } : {};
};
// 判断电池是否应该显示
const shouldShowBattery = (item) => {
return !isDraggingItem(item) && item.status !== 'placed';
// 判断进度项是否高亮
const isProgressItemActive = (arrangeKey) => {
const batteryType = ARRANGE_BATTERY_MAP[arrangeKey];
if (isDragging.value && draggedItem.value?.type === batteryType) return true;
const battery = batteryItems.value.find(item => item.type === batteryType);
return battery?.status === 'placed' && !batteryCorrectlyPlaced.value[batteryType];
};
// 判断电池状态
const isDraggingItem = (item) => isDragging.value && draggedItem.value?.id === item.id;
const shouldShowBattery = (item) => !isDraggingItem(item) && item.status !== 'placed';
const isHotareaEnabled = (hotarea) => !hotarea.requireBlackPlaced || batteryItems.value[2].status === 'placed';
// 检测坐标是否在矩形区域内
const isInRect = (position, rect) => {
if (!rect) return false;
const { x, y } = position;
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
};
const isInRect = ({ x, y }, rect) => rect && x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
// 检测并处理放置结果
const checkAndPlace = (item, position) => {
const targetArrange = BATTERY_ARRANGE_MAP[item.type];
const targetRect = arrangeRects.value[targetArrange];
// 检查前置是否已放置
const currentIndex = ARRANGE_ORDER.indexOf(targetArrange);
if (currentIndex > 0) {
const prevArrange = ARRANGE_ORDER[currentIndex - 1];
if (!arrangeStatus.value[prevArrange]) {
return;
const correctArrange = BATTERY_ARRANGE_MAP[item.type];
for (const arrangeName of ARRANGE_KEYS) {
if (isInRect(position, arrangeRects.value[arrangeName]) && !arrangeStatus.value[arrangeName]) {
arrangeStatus.value[arrangeName] = true;
arrangePlaced.value[arrangeName] = item.type;
item.status = 'placed';
arrangeName === correctArrange
? (batteryCorrectlyPlaced.value[item.type] = true)
: (arrangeError.value[arrangeName] = true);
break;
}
}
if (isInRect(position, targetRect)) {
arrangeStatus.value[targetArrange] = true;
item.status = 'placed';
}
};
// 拖拽逻辑
const {
isDragging,
draggedItem,
dragPosition,
handleTouchStart,
handleTouchMove,
handleTouchEnd,
handleMouseDown,
handleMouseMove,
handleMouseUp,
} = useDrag({
onDrop: (item, position) => {
if (item) checkAndPlace(item, position);
}
isDragging, draggedItem, dragPosition,
handleTouchStart, handleTouchMove, handleTouchEnd,
handleMouseDown, handleMouseMove, handleMouseUp,
} = useDrag({ onDrop: (item, position) => item && checkAndPlace(item, position) });
const instance = getCurrentInstance();
onMounted(() => {
const query = uni.createSelectorQuery().in(instance);
ARRANGE_KEYS.forEach(name => {
query.select(`.${name}`).boundingClientRect(data => data && (arrangeRects.value[name] = data));
});
query.exec();
});
</script>
<template>
<view class="part4-container">
<!-- 拖拽镜像 -->
<view
v-if="isDragging"
class="drag-ghost"
:style="{ left: `${dragPosition.x}px`, top: `${dragPosition.y}px` }"
>
<image
:src="getGhostIcon()"
mode="scaleToFill"
:class="['ghost-icon', `ghost-${getDraggedType()}`]"
/>
<view v-if="isDragging" class="drag-ghost" :style="{ left: `${dragPosition.x}px`, top: `${dragPosition.y}px` }">
<image :src="getGhostIcon()" mode="scaleToFill" :class="['ghost-icon', `ghost-${getDraggedType()}`]" />
</view>
<!-- 标题 -->
......@@ -221,16 +190,10 @@ const {
<!-- 排序区域 -->
<image :src="ASSETS.arrange" mode="scaleToFill" class="arrange" />
<view
v-for="item in ARRANGE_CONFIG"
:key="item.key"
:class="[item.key, { 'arrange-active': arrangeStatus[item.key] }]"
v-for="key in ARRANGE_KEYS" :key="key"
:class="[key, { 'arrange-active': arrangeStatus[key] && !arrangeError[key], 'arrange-error': arrangeError[key] }]"
>
<image
v-show="arrangeStatus[item.key]"
:src="ASSETS[item.imgKey]"
mode="scaleToFill"
:class="item.imgClass"
/>
<image v-show="arrangeStatus[key]" :src="ASSETS[`${arrangePlaced[key]}2`]" mode="scaleToFill" :style="getPlacedImgStyle(key)" />
</view>
<!-- 提示图 -->
......@@ -240,9 +203,9 @@ const {
<view class="progress">
<view class="progress-title">排序进度</view>
<view class="progress-list">
<view v-for="item in PROGRESS_LIST" :key="item.key" class="progress-item">
<image v-if="arrangeStatus[item.key]" :src="ASSETS.right" mode="scaleToFill" class="progress-icon-done" />
<view v-else class="progress-icon"><view class="progress-icon-inner"></view></view>
<view v-for="item in PROGRESS_LIST" :key="item.key" :class="['progress-item', { 'progress-item-active': isProgressItemActive(item.key) }]">
<image v-if="batteryCorrectlyPlaced[ARRANGE_BATTERY_MAP[item.key]]" :src="ASSETS.right" mode="scaleToFill" class="progress-icon-done" />
<view v-else class="progress-icon"><view class="progress-icon-inner" /></view>
<text class="progress-text">{{ item.label }}</text>
</view>
</view>
......@@ -258,20 +221,15 @@ const {
<!-- 电池区域 -->
<view class="battery">
<!-- 电池图片 -->
<image
v-for="(item, index) in batteryItems"
:key="item.id"
v-for="item in batteryItems" :key="item.id"
v-show="shouldShowBattery(item)"
:src="getBatteryIcon(item)"
mode="scaleToFill"
:class="item.type"
mode="scaleToFill" :class="item.type"
/>
<!-- 拖拽热区 -->
<view
v-for="hotarea in HOTAREA_CONFIG"
:key="hotarea.class"
v-for="hotarea in HOTAREA_CONFIG" :key="hotarea.class"
v-show="isHotareaEnabled(hotarea)"
:class="['btn', hotarea.class]"
@touchstart="handleTouchStart($event, batteryItems[hotarea.batteryIndex])"
@touchmove.stop.prevent="handleTouchMove"
......@@ -288,23 +246,15 @@ const {
<image class="btn-icon" :src="ASSETS.resetIcon" mode="widthFix" />
<text>重置</text>
</view>
<view
:class="['action-btn', 'complete-btn', { 'complete-btn-active': isComplete }]"
@click="handleComplete"
>
<view :class="['action-btn', 'complete-btn', { 'complete-btn-active': isComplete }]" @click="handleComplete">
<image v-if="!isComplete" class="btn-icon block-icon" :src="ASSETS.blockIcon" mode="widthFix" />
<text class="complete-text">完成</text>
</view>
</view>
<!-- 完成弹窗 -->
<view class="completion-overlay" v-if="showCompletion" @click="closeCompletion">
<image
class="completion-img"
:src="ASSETS.completionPopup"
mode="scaleToFill"
@click.stop="closeCompletion"
/>
<view v-if="showCompletion" class="completion-overlay" @click="closeCompletion">
<image class="completion-img" :src="ASSETS.completionPopup" mode="scaleToFill" @click.stop="closeCompletion" />
</view>
</view>
</template>
......@@ -343,6 +293,10 @@ const {
background-color: #00CC99;
}
.arrange-error {
background-color: #FF6938;
}
.arrange1,
.arrange2,
.arrange3,
......@@ -354,53 +308,10 @@ const {
border: 12rpx;
}
.arrange1 {
top: 320rpx;
.white2 {
position: absolute;
left: -16rpx;
top: -96rpx;
width: 330rpx;
height: 330rpx;
}
}
.arrange2 {
top: 499rpx;
.grey2 {
position: absolute;
left: 20rpx;
top: 20rpx;
width: 260rpx;
height: 98rpx;
}
}
.arrange3 {
top: 677rpx;
.blue2 {
position: absolute;
left: 0;
top: -44rpx;
width: 298rpx;
height: 224rpx;
}
}
.arrange4 {
top: 855rpx;
.black2 {
position: absolute;
left: -12rpx;
top: -92rpx;
width: 324rpx;
height: 324rpx;
}
}
.arrange1 { top: 320rpx; }
.arrange2 { top: 499rpx; }
.arrange3 { top: 677rpx; }
.arrange4 { top: 855rpx; }
.sequence {
position: absolute;
......@@ -448,6 +359,13 @@ const {
gap: 12rpx;
}
.progress-item-active {
background: #FFAA00;
border-radius: 40rpx;
padding: 0 16rpx 0 8rpx;
margin-left: -8rpx;
}
.progress-icon {
width: 24rpx;
height: 24rpx;
......@@ -611,6 +529,14 @@ const {
transform: rotate(-38deg);
}
.btn-blue3 {
bottom: 312rpx;
right: 167rpx;
width: 108rpx;
height: 98rpx;
transform: rotate(22deg);
}
.drag-ghost {
position: fixed;
z-index: 9999;
......
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