diff --git a/src/components/Header.vue b/src/components/Header.vue index a1692c5..828b0a8 100644 --- a/src/components/Header.vue +++ b/src/components/Header.vue @@ -57,7 +57,6 @@ const signin = () => { const loading = ref(false); const pointBook = ref(null); -const showProgress = ref(false); const heat = ref(0); /** 房间号按钮动态定位样式(position: fixed,根据胶囊真实位置计算,脱离 flex 流避免挤压标题) */ const battleRoomBtnStyle = ref({}); @@ -82,9 +81,6 @@ onMounted(() => { pointBook.value = uni.getStorageSync("last-point-book"); } } - if (currentPage.route === "pages/team-battle") { - showProgress.value = true; - } // 仅在对战房间页获取胶囊位置,按钮用 fixed 定位精确贴靠胶囊左侧(脱离 flex 流,不挤压标题) if (currentPage.route === "pages/battle-room") { try { @@ -189,7 +185,7 @@ onBeforeUnmount(() => { }} - + diff --git a/src/components/HeaderProgress.vue b/src/components/HeaderProgress.vue index 887afc9..522f75c 100644 --- a/src/components/HeaderProgress.vue +++ b/src/components/HeaderProgress.vue @@ -33,8 +33,8 @@ watch( newVal.includes("你") ? "轮到你了" : newVal.includes("红队") - ? "请红方射箭" - : "请蓝方射箭" + ? "请红方射箭" + : "请蓝方射箭" ); audioManager.play(key, false); currentRoundEnded.value = false; @@ -60,7 +60,8 @@ async function onReceiveMessage(message) { audioManager.play("比赛结束", false); } else if (type === MESSAGETYPESV2.ShootResult) { if (melee.value && current.playerId !== user.value.id) return; - if (current.playerId === user.value.id) currentShot.value++; + // 仅计用户自己的命中箭,并加上限防护,防止超过 totalShot(如重入时 currentShot 初始值偏高) + if (current.playerId === user.value.id && currentShot.value < totalShot.value) currentShot.value++; if (message.shootData) { let key = []; key.push( @@ -93,22 +94,34 @@ const onUpdateTips = (newVal) => { tips.value = newVal; }; -const onUpdateTotalShot = (newVal) => { - currentShot.value = newVal.currentShot; - totalShot.value = newVal.totalShot; -}; +// 监听 Pinia store 中 totalShot 变化,用于比赛恢复时同步箭数(替代 uni.$emit 避免时序问题) +// 使用 immediate: true 确保组件创建时立即读取 store 当前值(解决重入时 totalShot 值不变 watch 不触发的问题) +watch(() => store.game.totalShot, (newVal) => { + if (newVal > 0) { + totalShot.value = newVal; + currentShot.value = store.game.currentShot; + } +}, { immediate: true }); + +// 监听 Pinia store 中 tips 变化,用于比赛恢复时同步提示文案(替代 uni.$emit 避免时序问题) +// 使用 immediate: true 确保组件创建时立即读取 store 当前值(解决 onShow 早于 onMounted 导致 uni.$emit 事件丢失的问题) +watch(() => store.game.tips, (newVal) => { + if (newVal) { + tips.value = newVal; + } +}, { immediate: true }); onMounted(() => { - uni.$on("update-shot", onUpdateTotalShot); uni.$on("update-tips", onUpdateTips); uni.$on("socket-inbox", onReceiveMessage); uni.$on("play-sound", playSound); }); onBeforeUnmount(() => { - uni.$off("update-shot", onUpdateTotalShot); uni.$off("socket-inbox", onReceiveMessage); uni.$off("play-sound", playSound); + // 补充取消 update-tips 监听,防止页面重建时监听器叠加 + uni.$off("update-tips", onUpdateTips); if (timer.value) clearInterval(timer.value); }); @@ -118,10 +131,7 @@ onBeforeUnmount(() => { {{ (tips || "").replace(/你/g, "").replace(/重回/g, "") }} ({{ currentShot }}/{{ totalShot }}) @@ -135,11 +145,13 @@ onBeforeUnmount(() => { justify-content: center; font-weight: 500; } -.container > button:last-child { + +.container>button:last-child { width: 36px; height: 36px; } -.container > button:last-child > image { + +.container>button:last-child>image { width: 36px; min-height: 36px; } diff --git a/src/pages/team-battle.vue b/src/pages/team-battle.vue index 21bb3ad..56ae0b6 100644 --- a/src/pages/team-battle.vue +++ b/src/pages/team-battle.vue @@ -113,17 +113,39 @@ const recoverData = (battleInfo, {force = false, arrowOnly = false} = {}) => { audioManager.play(audioKey, false); } tips.value = nextTips; - uni.$emit("update-tips", nextTips); + // 同步写入 Pinia store,供 HeaderProgress 通过 immediate watch 在 onMounted 时立即读取 + // 规避 WeChat 小程序 onShow 在组件 onMounted 之前触发导致 uni.$emit 事件丢失的时序问题 + store.updateTips(nextTips); + // force 模式下改为在 nextTick 内发送 update-tips,规避 WeChat 小程序 + // onShow 与 onMounted 时序问题导致 HeaderProgress 监听器尚未注册的边界情况 + if (!force) { + uni.$emit("update-tips", nextTips); + } // redPlayer 已在上方 find() 确认:不为 null 则当前射手在红队 - uni.$emit("update-remain", {reset: true, value: 15, team: redPlayer?'red':'blue'}); + const targetTeam = redPlayer ? 'red' : 'blue'; if (force) { - const remain = (Date.now() - battleInfo.current.startTime) / 1000; - console.log(`当前轮已进行${remain}秒`); - if (remain > 0 && remain < 15) { - updateRemainSecond.value = 15 - remain - 0.2 + // force 模式下 start.value 刚由 null 变 true,Vue 异步调度渲染, + // ShootProgress2(v-if="start")尚未挂载,update-remain 事件会被丢弃。 + // 同时 HeaderProgress 对含"重回"的 tips 拦截音频,倒计时无法依赖 + // onAudioEnded 驱动,需在 nextTick 后直接启动。 + const elapsed = (Date.now() - battleInfo.current.startTime) / 1000; + console.log(`当前轮已进行${elapsed}秒`); + if (elapsed > 0 && elapsed < 15) { + updateRemainSecond.value = 15 - elapsed - 0.2; } + const actualRemain = updateRemainSecond.value; + nextTick(() => { + // 在 nextTick 内统一发送,确保 ShootProgress2 和 HeaderProgress 均已挂载 + uni.$emit("update-tips", nextTips); + uni.$emit("update-remain", {reset: true, value: 15, team: targetTeam}); + // 重置动画完成后,直接启动倒计时(无需依赖音频结束回调) + setTimeout(() => { + uni.$emit("update-remain", {stop: false, value: actualRemain, team: targetTeam}); + }, 100); + }); } else { - updateRemainSecond.value = battleInfo.readyTime + uni.$emit("update-remain", {reset: true, value: 15, team: targetTeam}); + updateRemainSecond.value = battleInfo.readyTime; } } else { currentRound.value = battleInfo.current.round || 1; @@ -176,7 +198,7 @@ function onNewRound(msg, prevRound) { isFinalShoot.value = msg.current.goldRound; // 决金箭轮每人只射一箭,重置箭数显示为 (0/1) if (msg.current.goldRound) { - uni.$emit("update-shot", { currentShot: 0, totalShot: 1 }); + store.updateShotInfo(0, 1); } // 用传入的 prevRound(捕获时刻的旧轮次)展示结算 Tip,与进度条 currentRound 解耦 roundTipRound.value = prevRound; @@ -228,6 +250,9 @@ async function onReceiveMessage(msg) { onLoad(async (options) => { if (options.battleId) battleId.value = options.battleId; + // 重置箭数和提示文案,防止因 Pinia 保留上一场比赛的旧值而错误展示 + store.updateShotInfo(0, 0); + store.updateTips(""); // uni.enableAlertBeforeUnload({ // message: "离开比赛可能导致比赛失败,是否继续?", // success: (res) => { @@ -263,9 +288,16 @@ onShow(async () => { url: `/pages/friend-battle-result?battleId=${result.matchId}`, }); } else { - // shootNumber 来自后端,恢复状态时同步 totalShot,防止 BattleStart 不重新推送导致显示错误 - if (result.shootNumber) { - uni.$emit("update-shot", { currentShot: 0, totalShot: result.shootNumber }); + if (result.status !== 0) { + // 比赛进行中,从后端恢复箭数(测距阶段不展示) + if (result.shootNumber) { + // current.index 为 0 基的已射箭数(0=尚未射出),与 ShootResult 累加语义一致 + // 注意:不再 +1,避免重进时与后续 ShootResult 自增形成双重计数导致超出 totalShot + store.updateShotInfo(result.current?.index ?? 0, result.shootNumber); + } + } else { + // 测距阶段重置箭数,防止 ImmediateWatcher 读取 Pinia 保留的旧值 + store.updateShotInfo(0, 0); } recoverData(result, {force: true}); } diff --git a/src/store.js b/src/store.js index b53daef..7549772 100644 --- a/src/store.js +++ b/src/store.js @@ -80,6 +80,9 @@ export default defineStore("store", { roomID: "", inBattle: false, roomNumber: "", // 当前房间号,供 Header 展示房号胶囊 + currentShot: 0, // 当前已射箭数(用于 HeaderProgress 恢复状态) + totalShot: 0, // 轮次总箭数(用于 HeaderProgress 恢复状态) + tips: "", // 当前提示文案(用于 HeaderProgress 恢复状态,替代 uni.$emit 避免时序问题) }, }), @@ -136,6 +139,15 @@ export default defineStore("store", { this.game.roomID = roomID; this.game.inBattle = inBattle; }, + /** 更新当前射箭进度(用于 HeaderProgress 恢复状态,替代 uni.$emit 避免时序问题) */ + updateShotInfo(currentShot = 0, totalShot = 0) { + this.game.currentShot = currentShot; + this.game.totalShot = totalShot; + }, + /** 更新当前提示文案(用于 HeaderProgress 恢复状态,替代 uni.$emit 避免时序问题) */ + updateTips(tips = "") { + this.game.tips = tips; + }, /** 更新当前房间号,供 Header 组件展示房号胶囊 */ updateRoomNumber(number) { this.game.roomNumber = number;