Commit 0abe465b by Hantao

feat: 重构游戏进度管理并添加持久化存储

- 引入 Pinia 状态管理库及持久化插件,统一管理游戏进度
- 新增游戏进度存储模块,支持关卡完成状态的保存与恢复
- 重构各关卡页面,移除本地存储逻辑,统一使用状态管理
- 更新第二关进度展示,根据实际完成情况动态显示进度条
- 在开始页面添加进度重置功能,支持重新开始游戏
- 调整依赖版本,确保 Vue 2/3 兼容性
parent 7e51f76f
......@@ -50,9 +50,11 @@
"@dcloudio/uni-quickapp-webview": "3.0.0-3090620231104001",
"clipboard": "^2.0.11",
"dayjs": "^1.11.19",
"pinia": "^3.0.4",
"pinia": "^2.0.36",
"pinia-plugin-persistedstate": "^3.2.1",
"uview-plus": "^3.7.0",
"vue": "^3.2.45",
"vue-demi": "^0.14.10",
"vue-i18n": "^9.1.9"
},
"devDependencies": {
......
......@@ -13,11 +13,18 @@ app.$mount()
// #ifdef VUE3
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(uviewPlus)
return {
app
app,
pinia
}
}
// #endif
\ No newline at end of file
......@@ -59,6 +59,12 @@
}
},
{
"path": "pages/second/part3",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/second/part4/start",
"style": {
"navigationStyle": "custom"
......
......@@ -6,6 +6,9 @@ import SelectReagents from './components/selectReagents.vue';
import CompleteComponent from './components/complete.vue';
import navBar from '@/components/navBar.vue';
import success from '@/components/success.vue';
import { useGameStore } from '@/stores/game';
const gameStore = useGameStore();
const ASSETS = Object.freeze({
bg1: '/static/first/oneBackground.png',
......@@ -22,13 +25,24 @@ const ASSETS = Object.freeze({
});
// 当前游戏阶段
const currentStage = ref('garbageCleanup');
const currentStage = ref(gameStore.levels.first.lastStage || 'garbageCleanup');
// 各阶段完成状态
const isGarbageCleaned = ref(false);
const isOilSeparationComplete = ref(false);
const isAcidBaseNeutralizationComplete = ref(false);
onMounted(() => {
// 如果第一关已经完成,直接显示完成状态
if (gameStore.levels.first.completed) {
showCompleteComponent.value = true;
currentStage.value = 'acidBaseNeutralization';
isGarbageCleaned.value = true;
isOilSeparationComplete.value = true;
isAcidBaseNeutralizationComplete.value = true;
}
});
// 其他状态
const showPurificationValue = ref(false);
const showCompletionModal = ref(false);
......@@ -217,6 +231,7 @@ const handleNextStep = () => {
const transitionToOilSeparation = () => {
setTimeout(() => {
currentStage.value = 'oilFilmSeparation';
gameStore.setFirstLevelStatus(false, 'oilFilmSeparation');
uni.hideLoading();
}, 1000);
};
......@@ -225,6 +240,7 @@ const transitionToOilSeparation = () => {
const transitionToAcidBaseNeutralization = () => {
setTimeout(() => {
currentStage.value = 'acidBaseNeutralization';
gameStore.setFirstLevelStatus(false, 'acidBaseNeutralization');
uni.hideLoading();
}, 1000);
};
......@@ -232,8 +248,9 @@ const transitionToAcidBaseNeutralization = () => {
// 成功弹窗确认按钮点击
const handleSuccessConfirm = () => {
showSuccess.value = false;
gameStore.setFirstLevelStatus(true, 'acidBaseNeutralization');
uni.reLaunch({
url: '/pages/start/index?progress=1'
url: '/pages/start/index'
});
};
</script>
......
<script setup>
import navBar from '@/components/navBar.vue';
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { useGameStore } from '@/stores/game';
const gameStore = useGameStore();
const ASSETS = Object.freeze({
bg: '/static/second/bg.webp',
......@@ -9,24 +12,53 @@ const ASSETS = Object.freeze({
info2: '/static/second/info2.webp',
info3: '/static/second/info3.webp',
info4: '/static/second/info4.webp',
info: '/static/second/info.webp',
title: '/static/second/title.webp',
jdt: '/static/second/jdt.webp',
jdt1: '/static/second/jdt1.webp',
jdt2: '/static/second/jdt2.webp',
jdt3: '/static/second/jdt3.webp',
jdt4: '/static/second/jdt4.webp',
next: '/static/second/next.webp',
});
const INFO_MAP = [ASSETS.info1, ASSETS.info2, ASSETS.info3, ASSETS.info4];
const JDT_MAP = [ASSETS.jdt1, ASSETS.jdt2, ASSETS.jdt3, ASSETS.jdt4];
const currentStep = computed(() => {
const parts = gameStore.levels.second.parts;
if (parts.part4) return 4; // 全部完成
if (parts.part3) return 3;
if (parts.part2) return 2;
if (parts.part1) return 1;
return 0;
});
const currentInfo = ref(ASSETS.info1);
const currentInfo = computed(() => {
const step = currentStep.value >= 4 ? 3 : currentStep.value;
return INFO_MAP[step] || ASSETS.info1;
});
onShow(() => {
const step = uni.getStorageSync('second_part1_completed') || 0;
currentInfo.value = INFO_MAP[step] || ASSETS.info1;
const currentJdt = computed(() => {
const step = currentStep.value >= 4 ? 3 : currentStep.value;
return JDT_MAP[step] || ASSETS.jdt1;
});
const handleNext = () => {
uni.navigateTo({
url: '/pages/second/part1'
});
const step = currentStep.value;
if (step >= 4) {
// 如果已经全部完成,跳转到第三关
uni.navigateTo({ url: '/pages/third/index' });
return;
}
const urlMap = {
0: '/pages/second/part1',
1: '/pages/second/part2',
2: '/pages/second/part3',
3: '/pages/second/part4/index',
};
const url = urlMap[step];
uni.navigateTo({ url });
};
</script>
......@@ -50,7 +82,7 @@ const handleNext = () => {
<!-- 底部区域 -->
<view class="bottom-area">
<image class="progress-bar" :src="ASSETS.jdt" mode="scaleToFill" />
<image class="progress-bar" :src="currentJdt" mode="scaleToFill" />
<view class="next-btn-wrapper" @click="handleNext">
<image class="next-btn" :src="ASSETS.next" mode="scaleToFill" />
......
<script setup>
import { ref, onMounted, getCurrentInstance, computed } from 'vue';
import { ref, computed, onMounted, getCurrentInstance } from 'vue';
import { useDrag } from './hooks/useDrag';
import navBar from '@/components/navBar.vue';
import { useGameStore } from '@/stores/game';
const gameStore = useGameStore();
const ASSETS = Object.freeze({
title: '/static/second/part1/title.webp',
......@@ -77,13 +80,7 @@ const PUZZLE_DIMENSIONS = {
}
};
const STORAGE_KEY = 'second_part1_puzzle_status';
const getInitialItems = () => {
const savedStatus = uni.getStorageSync(STORAGE_KEY);
if (savedStatus) {
return savedStatus;
}
return [
{ id: 1, type: 'puzzle1', status: 'default' },
{ id: 2, type: 'puzzle2', status: 'default' },
......@@ -134,31 +131,23 @@ const currentProgressImgStyle = computed(() => {
});
const showCompletion = ref(false);
const hasCompletedBefore = ref(!!uni.getStorageSync('second_part1_completed'));
const handleReset = () => {
trayItems.value.forEach(item => {
item.status = 'default';
});
uni.removeStorageSync(STORAGE_KEY);
};
const handleComplete = () => {
if (isComplete.value) {
if (hasCompletedBefore.value) {
uni.redirectTo({
url: '/pages/second/index'
});
} else {
hasCompletedBefore.value = true;
uni.setStorageSync('second_part1_completed', 1);
showCompletion.value = true;
}
showCompletion.value = true;
}
};
const closeCompletion = () => {
showCompletion.value = false;
gameStore.setSecondLevelPartStatus('part1', true);
uni.navigateBack();
};
const targetRects = ref({});
......@@ -195,7 +184,6 @@ const {
// 简单的碰撞检测
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
item.status = 'placed';
uni.setStorageSync(STORAGE_KEY, trayItems.value);
}
}
}
......
<script setup>
import { ref, onMounted, getCurrentInstance, computed } from 'vue';
import { ref, computed, onMounted, getCurrentInstance } from 'vue';
import { useDrag } from './hooks/useDrag';
import navBar from '@/components/navBar.vue';
import { useGameStore } from '@/stores/game';
const gameStore = useGameStore();
const ASSETS = Object.freeze({
title: '/static/second/part2/title.webp',
......@@ -94,6 +97,8 @@ const handleComplete = () => {
const closeCompletion = () => {
showCompletion.value = false;
gameStore.setSecondLevelPartStatus('part2', true);
uni.navigateBack();
};
const centerRect = ref(null);
......
<script setup>
import { useGameStore } from '@/stores/game';
const gameStore = useGameStore();
const handleNext = () => {
gameStore.setSecondLevelPartStatus('part3', true);
uni.navigateBack();
};
</script>
<template>
<view class="next" @click="handleNext">下一关</view>
</template>
<style scoped lang="scss">
.next {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 40rpx;
color: #333;
}
</style>
\ No newline at end of file
......@@ -2,6 +2,9 @@
import { ref, computed, onMounted, getCurrentInstance } from 'vue';
import { useDrag } from '../hooks/useDrag';
import navBar from '@/components/navBar.vue';
import { useGameStore } from '@/stores/game';
const gameStore = useGameStore();
const ASSETS = Object.freeze({
box: '/static/second/part4/index/box.webp',
......@@ -116,7 +119,13 @@ const handleReset = () => {
};
const handleComplete = () => isComplete.value && (showCompletion.value = true);
const closeCompletion = () => showCompletion.value = false;
const closeCompletion = () => {
showCompletion.value = false;
gameStore.setSecondLevelPartStatus('part4', true);
uni.reLaunch({
url: '/pages/second/index'
});
};
// 获取电池图标
const getBatteryIcon = (item) => draggedItem.value?.id === item.id ? item.dragIcon : item.icon;
......
<script setup>
import { ref } from 'vue';
import { onShareAppMessage } from '@dcloudio/uni-app';
import { onLoad } from '@dcloudio/uni-app';
import { ref, computed } from 'vue';
import { onShareAppMessage, onLoad } from '@dcloudio/uni-app';
import { useGameStore } from '@/stores/game';
const gameStore = useGameStore();
// 图片路径
const ASSETS = Object.freeze ({
......@@ -27,59 +29,46 @@ const ASSETS = Object.freeze ({
finishBtn: '/static/start/finishBtn.webp',
});
const progressImgs = ref(0);
// 接收页面参数
onLoad((options) => {
if (options.progress) {
progressImgs.value = Number(options.progress);
// 根据 store 计算进度
const progressLevel = computed(() => {
if (gameStore.levels.third.completed) return 5;
if (gameStore.levels.second.completed) return 4;
// 第二关的部分进度
const parts = gameStore.levels.second.parts;
const completedParts = Object.values(parts).filter(p => p).length;
if (completedParts > 0) {
// 映射到 1-4 之间的进度
return 1 + completedParts;
}
if (gameStore.levels.first.completed) return 1;
return 0;
});
const getProgressImage = () => {
switch (progressImgs.value) {
case 0:
return ASSETS.progress;
case 1:
return ASSETS.progress1;
case 2:
return ASSETS.progress2;
case 3:
return ASSETS.progress3;
case 4:
return ASSETS.progress4;
case 5:
return ASSETS.finish;
default:
return ASSETS.progress;
const level = Math.floor(progressLevel.value);
switch (level) {
case 0: return ASSETS.progress;
case 1: return ASSETS.progress1;
case 2: return ASSETS.progress2;
case 3: return ASSETS.progress3;
case 4: return ASSETS.progress4;
case 5: return ASSETS.finish;
default: return ASSETS.progress;
}
};
const badgeImgs = ref(0);
// 接收页面参数
onLoad((options) => {
if (options.progress) {
badgeImgs.value = Number(options.progress);
}
});
const getBadgeImage = () => {
switch (badgeImgs.value) {
case 0:
return;
case 1:
return ASSETS.badge1;
case 2:
return ASSETS.badge2;
case 3:
return ASSETS.badge3;
case 4:
return ASSETS.badge4;
case 5:
return ASSETS.badge5;
default:
return ASSETS.badge1;
const level = Math.floor(progressLevel.value);
switch (level) {
case 0: return;
case 1: return ASSETS.badge1;
case 2: return ASSETS.badge2;
case 3: return ASSETS.badge3;
case 4: return ASSETS.badge4;
case 5: return ASSETS.badge5;
default: return ASSETS.badge1;
}
};
......@@ -87,24 +76,30 @@ const getBadgeImage = () => {
const isSidebarExpanded = ref(false);
const handleStart = () => {
if (progressImgs.value === 0 && badgeImgs.value === 0) {
uni.navigateTo({
url: '/pages/first/index'
});
}
if (progressImgs.value === 1 && badgeImgs.value === 1) {
uni.navigateTo({
url: '/pages/second/index'
});
const level = Math.floor(progressLevel.value);
if (level === 0) {
uni.navigateTo({ url: '/pages/first/index' });
} else if (level < 4) {
uni.navigateTo({ url: '/pages/second/index' });
} else {
uni.navigateTo({ url: '/pages/third/index' });
}
};
const handleRestart = () => {
uni.showModal({
title: '提示',
content: '确定要重置所有进度吗?',
success: (res) => {
if (res.confirm) {
gameStore.resetAllProgress();
}
}
});
};
const navToBadgePage = () => {
const badge = progressImgs.value > 0 ? progressImgs.value : 0;
const badge = Math.floor(progressLevel.value);
uni.navigateTo({
url: `/pages/badge/index?badge=${badge}`
});
......
......@@ -5,6 +5,9 @@ import navBar from '@/components/navBar.vue';
import Beaker from './components/beaker.vue';
import TabsInstructionsPanel from './components/TabsInstructionsPanel.vue';
import BottomActionBar from './components/BottomActionBar.vue';
import { useGameStore } from '@/stores/game';
const gameStore = useGameStore();
const assets = {
bg: 'https://userone-oss-cdn.angelgroup.com.cn/static/2026-01-23/963079c98b914f349fe9ff5e600d0f65%E6%B0%B4%E8%B4%A8%E6%A3%80%E6%B5%8B%E7%AB%99_%E7%BA%AF%E8%83%8C%E6%99%AF%201.webp',
......@@ -57,6 +60,10 @@ const handleShowCompletion = () => {
const closeCompletion = () => {
showCompletion.value = false;
gameStore.setThirdLevelStatus(true);
uni.reLaunch({
url: '/pages/start/index'
});
};
</script>
......
import { defineStore } from 'pinia';
export const useGameStore = defineStore('game', {
state: () => ({
// 关卡完成状态
levels: {
first: {
completed: false,
lastStage: 'garbageCleanup', // 当前完成到的阶段
},
second: {
completed: false,
parts: {
part1: false,
part2: false,
part3: false,
part4: false
}
},
third: {
completed: false
}
}
}),
actions: {
// 设置第一关状态
setFirstLevelStatus(status, stage) {
this.levels.first.completed = status;
if (stage) {
this.levels.first.lastStage = stage;
}
},
// 设置第二关各部分状态
setSecondLevelPartStatus(part, status) {
if (this.levels.second.parts.hasOwnProperty(part)) {
this.levels.second.parts[part] = status;
// 检查是否所有部分都完成了
const allPartsCompleted = Object.values(this.levels.second.parts).every(p => p === true);
if (allPartsCompleted) {
this.levels.second.completed = true;
}
}
},
// 设置第三关状态
setThirdLevelStatus(status) {
this.levels.third.completed = status;
},
// 重置所有进度
resetAllProgress() {
this.levels = {
first: {
completed: false,
lastStage: 'garbageCleanup',
},
second: {
completed: false,
parts: {
part1: false,
part2: false,
part3: false,
part4: false
}
},
third: {
completed: false
}
};
}
},
persist: {
key: 'game_progress',
storage: {
getItem: (key) => uni.getStorageSync(key),
setItem: (key, value) => uni.setStorageSync(key, value),
},
},
});
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