Commit 0abe465b by Hantao

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

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