From f8efe4c4a4249247eb61874d7611dc0f43774e22 Mon Sep 17 00:00:00 2001 From: zhangyibo95 <690096405@qq.com> Date: Thu, 9 Jul 2026 11:43:06 +0800 Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A=E4=BC=98=E5=8C=96=E6=AF=94?= =?UTF-8?q?=E8=B5=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BackToGame.vue | 7 +-- src/components/Container.vue | 20 +++---- src/matchWebsocket.js | 3 +- src/pages/melee-battle.vue | 35 +++++++++++- .../team-battle/components/BackToGame.vue | 7 +-- .../team-battle/components/Container.vue | 20 +++---- src/pages/team-battle/index.vue | 29 +++++++++- src/utils/match.min.js | 53 ------------------- src/utils/matchReturn.js | 42 +++++++++++++++ 9 files changed, 130 insertions(+), 86 deletions(-) create mode 100644 src/utils/matchReturn.js diff --git a/src/components/BackToGame.vue b/src/components/BackToGame.vue index 5297d74..b979277 100644 --- a/src/components/BackToGame.vue +++ b/src/components/BackToGame.vue @@ -4,6 +4,7 @@ import { onShow } from "@dcloudio/uni-app"; import { getBattleAPI, getUserGameState } from "@/apis"; import { debounce } from "@/util"; +import { returnToBattle } from "@/utils/matchReturn"; import useStore from "@/store"; import { storeToRefs } from "pinia"; @@ -67,11 +68,7 @@ const onClick = debounce(async () => { const result = await getBattleAPI(); if (result && result.matchId) { await uni.$checkAudio(); - if (result.mode <= 3) { - await navigateOnce(`/pages/team-battle/index?battleId=${result.matchId}`); - } else { - await navigateOnce(`/pages/melee-battle?battleId=${result.matchId}`); - } + await returnToBattle(result, navigateOnce); return; } if (game.value.roomID) { diff --git a/src/components/Container.vue b/src/components/Container.vue index d29a221..46106aa 100644 --- a/src/components/Container.vue +++ b/src/components/Container.vue @@ -7,6 +7,7 @@ import ScreenHint from "@/components/ScreenHint.vue"; import BackToGame from "@/components/BackToGame.vue"; import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis"; import { capsuleHeight, debounce } from "@/util"; +import { returnToBattle } from "@/utils/matchReturn"; import AudioManager from "@/audioManager"; const props = defineProps({ title: { @@ -110,6 +111,15 @@ onShow(() => { showHint.value = false; }); +const navigateTo = (url) => + new Promise((resolve, reject) => { + uni.navigateTo({ + url, + success: resolve, + fail: reject, + }); + }); + const backToGame = debounce(async () => { if (isLoading.value) return; // 防止重复点击 @@ -118,15 +128,7 @@ const backToGame = debounce(async () => { const result = await getBattleAPI(); if (result && result.matchId) { await checkAudioProgress(); - if (result.mode <= 3) { - uni.navigateTo({ - url: `/pages/team-battle/index?battleId=${result.matchId}`, - }); - } else { - uni.navigateTo({ - url: `/pages/melee-battle?battleId=${result.matchId}`, - }); - } + await returnToBattle(result, navigateTo); } } catch (error) { console.error("获取当前游戏失败:", error); diff --git a/src/matchWebsocket.js b/src/matchWebsocket.js index 8ef3b39..b4452f7 100644 --- a/src/matchWebsocket.js +++ b/src/matchWebsocket.js @@ -258,7 +258,8 @@ function sendBuffer(buffer, label) { currentSocket.send({ data: buffer, success: () => { - console.log(`[match-ws] ${label} sent`); + if (label === "heartbeat ack") return; + console.log(`[match-ws] ${label} sent`, new Date()); }, fail: (err) => { if (socket !== currentSocket) return; diff --git a/src/pages/melee-battle.vue b/src/pages/melee-battle.vue index cb7ab04..de8f9e1 100644 --- a/src/pages/melee-battle.vue +++ b/src/pages/melee-battle.vue @@ -13,8 +13,9 @@ import TestDistance from "@/components/TestDistance.vue"; import SModal from "@/components/SModal.vue"; import audioManager from "@/audioManager"; import { getBattleAPI, laserCloseAPI } from "@/apis"; -import { connectMatchWebSocket } from "@/matchWebsocket"; +import { closeMatchWebSocket, connectMatchWebSocket } from "@/matchWebsocket"; import { MESSAGETYPESV2 } from "@/constants"; +import { takeMatchReturnSnapshot } from "@/utils/matchReturn"; import useStore from "@/store"; import { storeToRefs } from "pinia"; const store = useStore(); @@ -46,6 +47,8 @@ let halfRestTimer = null; const showOfflineModal = ref(false); /** 记录每位玩家当前半场连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */ const xRingStreaks = ref({}); +let battleEnded = false; +let skipNextRestoreOnShow = false; function clearHalfRestCountdown() { if (halfRestTimer) { @@ -164,6 +167,11 @@ function reconnectMatchServer(battleInfo) { }); } +function closeBattleServer(reason) { + if (battleEnded) return; + closeMatchWebSocket({ reason }); +} + /** * 监听设备在线状态,大乱斗比赛进行中设备离线时弹窗提示用户 */ @@ -176,6 +184,9 @@ watch(online, (newVal, oldVal) => { function recoverData(battleInfo, { force = false } = {}) { battleInfo = normalizeBattleInfo(battleInfo); if (!battleInfo) return; + if (battleInfo.status !== undefined) { + battleEnded = [2, 4].includes(Number(battleInfo.status)); + } try { if (battleInfo.way === 1) title.value = "好友约战 - 大乱斗"; if (battleInfo.way === 2) title.value = "排位赛 - 大乱斗"; @@ -250,7 +261,16 @@ function recoverData(battleInfo, { force = false } = {}) { } onLoad(async (options) => { - if (options.battleId) battleId.value = options.battleId; + const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null; + skipNextRestoreOnShow = false; + if (returnSnapshot?.matchId) battleId.value = returnSnapshot.matchId; + else if (options.battleId) battleId.value = options.battleId; + if (returnSnapshot) { + skipNextRestoreOnShow = true; + reconnectMatchServer(returnSnapshot); + recoverData(returnSnapshot, { force: true }); + return; + } const readySnapshot = takeReadySnapshot(battleId.value); if (readySnapshot?.status === 0) recoverData(readySnapshot); // uni.enableAlertBeforeUnload({ @@ -325,6 +345,7 @@ async function onReceiveMessage(msg) { tips.value = "准备下半场"; startHalfRestCountdown(); } else if (msg.type === MESSAGETYPESV2.BattleEnd) { + battleEnded = true; setTimeout(() => { // 全部跳转到新结算页 uni.redirectTo({ @@ -346,14 +367,24 @@ onBeforeUnmount(() => { }); clearHalfRestCountdown(); uni.$off("socket-inbox", onReceiveMessage); + closeBattleServer("melee-battle-unmount"); audioManager.stopAll(); }); +onHide(() => { + closeBattleServer("melee-battle-hide"); +}); + onShow(async () => { + if (skipNextRestoreOnShow) { + skipNextRestoreOnShow = false; + return; + } if (battleId.value) { const result = normalizeBattleInfo(await getBattleAPI(battleId.value)); if (!result) return; if (result.status === 2) { + battleEnded = true; uni.showToast({ title: "比赛已结束", icon: "none", diff --git a/src/pages/team-battle/components/BackToGame.vue b/src/pages/team-battle/components/BackToGame.vue index 2ca9a7c..8485fef 100644 --- a/src/pages/team-battle/components/BackToGame.vue +++ b/src/pages/team-battle/components/BackToGame.vue @@ -4,6 +4,7 @@ import { onShow } from "@dcloudio/uni-app"; import { getBattleAPI, getUserGameState } from "@/apis"; import { debounce } from "@/util"; +import { returnToBattle } from "@/utils/matchReturn"; import useStore from "@/store"; import { storeToRefs } from "pinia"; @@ -67,11 +68,7 @@ const onClick = debounce(async () => { const result = await getBattleAPI(); if (result && result.matchId) { await uni.$checkAudio(); - if (result.mode <= 3) { - await navigateOnce(`/pages/team-battle/index?battleId=${result.matchId}`); - } else { - await navigateOnce(`/pages/melee-battle?battleId=${result.matchId}`); - } + await returnToBattle(result, navigateOnce); return; } if (game.value.roomID) { diff --git a/src/pages/team-battle/components/Container.vue b/src/pages/team-battle/components/Container.vue index 5e3ce8e..7253252 100644 --- a/src/pages/team-battle/components/Container.vue +++ b/src/pages/team-battle/components/Container.vue @@ -7,6 +7,7 @@ import ScreenHint from "./ScreenHint.vue"; import BackToGame from "./BackToGame.vue"; import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis"; import { capsuleHeight, debounce } from "@/util"; +import { returnToBattle } from "@/utils/matchReturn"; import AudioManager from "@/audioManager"; const props = defineProps({ title: { @@ -120,6 +121,15 @@ onShow(() => { showHint.value = false; }); +const navigateTo = (url) => + new Promise((resolve, reject) => { + uni.navigateTo({ + url, + success: resolve, + fail: reject, + }); + }); + const backToGame = debounce(async () => { if (isLoading.value) return; // 防止重复点击 @@ -128,15 +138,7 @@ const backToGame = debounce(async () => { const result = await getBattleAPI(); if (result && result.matchId) { await checkAudioProgress(); - if (result.mode <= 3) { - uni.navigateTo({ - url: `/pages/team-battle/index?battleId=${result.matchId}`, - }); - } else { - uni.navigateTo({ - url: `/pages/melee-battle?battleId=${result.matchId}`, - }); - } + await returnToBattle(result, navigateTo); } } catch (error) { console.error("获取当前游戏失败:", error); diff --git a/src/pages/team-battle/index.vue b/src/pages/team-battle/index.vue index e7734e6..e9a37e5 100644 --- a/src/pages/team-battle/index.vue +++ b/src/pages/team-battle/index.vue @@ -13,9 +13,14 @@ import TeamAvatars from "./components/TeamAvatars.vue"; import ShootProgress2 from "./components/ShootProgress2.vue"; import SModal from "./components/SModal.vue"; import { laserCloseAPI, getBattleAPI } from "@/apis"; -import { connectMatchWebSocket, MATCH_WS_AUDIO_ACK_EVENT } from "@/matchWebsocket"; +import { + closeMatchWebSocket, + connectMatchWebSocket, + MATCH_WS_AUDIO_ACK_EVENT, +} from "@/matchWebsocket"; import { MESSAGETYPESV2 } from "@/constants"; import { getDirectionText } from "@/util"; +import { takeMatchReturnSnapshot } from "@/utils/matchReturn"; import audioManager, { AUDIO_INTERRUPTION_BEGIN_EVENT, AUDIO_INTERRUPTION_END_EVENT, @@ -98,6 +103,7 @@ let restoreLoadingTimer = null; let pendingRoundAudio = false; // 一旦收到 BattleEnd,后续普通消息就不再进入队列。 let battleEnded = false; +let skipNextRestoreOnShow = false; const handledMessageKeys = new Set(); const handledMessageKeyOrder = []; const queuedMessageKeys = new Set(); @@ -557,6 +563,11 @@ function handleBattleRecovered() { scheduleRestoreLatestBattle(); } +function closeBattleServer(reason) { + if (battleEnded) return; + closeMatchWebSocket({ reason }); +} + // 队伍信息优先用接口返回值;接口缺失时使用本地缓存,避免重进页面时头像为空。 function loadTeamPlayers(teamInfo, storageKey) { if (Array.isArray(teamInfo?.players)) return [...teamInfo.players]; @@ -1205,10 +1216,12 @@ function onReceiveMessage(message) { // 新对局入口:彻底清空上一局残留的状态、队列和缓存。 onLoad((options) => { console.log('重新进入了') + const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null; + skipNextRestoreOnShow = false; // 新对局入口:把所有会串场的状态、队列、时间戳和缓存一次性清空。 start.value = null; tips.value = ""; - battleId.value = options.battleId || ""; + battleId.value = returnSnapshot?.matchId || options.battleId || ""; currentRound.value = 0; roundTipRound.value = 0; goldenRound.value = 0; @@ -1246,6 +1259,12 @@ onLoad((options) => { store.updateShotInfo(0, 0); store.updateTips(""); latestShotFlash.value = null; + if (returnSnapshot) { + skipNextRestoreOnShow = true; + applyBattleSnapshot(returnSnapshot, { restore: true }); + reconnectMatchServer(returnSnapshot); + return; + } const readySnapshot = takeReadySnapshot(battleId.value); if (readySnapshot?.status === 0) { applyBattleSnapshot(readySnapshot, { restore: true }); @@ -1282,6 +1301,7 @@ onBeforeUnmount(() => { } hideRestoreLoading(); invalidateBattleQueue({ stopAudio: true, stopProgress: true }); + closeBattleServer("team-battle-unmount"); console.log('onBeforeUnmount', '页面卸载前') audioManager.stopAll(); uni.$off(AUDIO_INTERRUPTION_BEGIN_EVENT, handleBattleCovered); @@ -1291,11 +1311,16 @@ onBeforeUnmount(() => { onHide(()=>{ console.log('onHide', '页面大退') handleBattleCovered(); + closeBattleServer("team-battle-hide"); }) // 每次回到前台都重新拉最新比赛快照,确保画面与后端一致。 onShow(() => { console.log('onshow') + if (skipNextRestoreOnShow) { + skipNextRestoreOnShow = false; + return; + } scheduleRestoreLatestBattle(); }); diff --git a/src/utils/match.min.js b/src/utils/match.min.js index 0570ddd..6673378 100644 --- a/src/utils/match.min.js +++ b/src/utils/match.min.js @@ -1,57 +1,4 @@ /* eslint-disable */ import * as $protobuf from "protobufjs"; - -if (typeof $protobuf.Root.create !== "function") { - const isObject = (value) => value && typeof value === "object" && !Array.isArray(value); - - const normalizeField = (field) => { - if (!isObject(field)) return field; - const next = { ...field }; - if (next.keytype && !next.keyType) { - next.keyType = next.keytype; - delete next.keytype; - } - delete next.oneof; - return next; - }; - - const normalizeType = (json) => { - const next = { ...json }; - if (next.fields) { - const oneofs = { ...(next.oneofs || {}) }; - next.fields = Object.keys(next.fields).reduce((fields, name) => { - const field = next.fields[name]; - fields[name] = normalizeField(field); - if (field && field.oneof) { - if (!oneofs[field.oneof]) oneofs[field.oneof] = { oneof: [] }; - oneofs[field.oneof].oneof.push(name); - } - return fields; - }, {}); - if (Object.keys(oneofs).length) next.oneofs = oneofs; - } - if (next.nested) next.nested = normalizeNested(next.nested); - return next; - }; - - const normalizeDescriptor = (json) => { - if (!isObject(json)) return json; - if (json.fields) return normalizeType(json); - if (json.values || json.methods || json.id !== undefined) return json; - if (Object.keys(json).every((key) => typeof json[key] === "number")) { - return { values: json }; - } - if (json.nested) return { ...json, nested: normalizeNested(json.nested) }; - return { nested: normalizeNested(json) }; - }; - - const normalizeNested = (nested) => - Object.keys(nested || {}).reduce((result, name) => { - result[name] = normalizeDescriptor(nested[name]); - return result; - }, {}); - - $protobuf.Root.create = (json) => $protobuf.Root.fromJSON(normalizeDescriptor(json)); -} const $root=$protobuf.Root.create({nested:{rpc:{ServerMessageType:{SERVER_MSG_UNKNOWN:0,SERVER_MSG_MATCH_READY:1,SERVER_MSG_MATCH_START:2,SERVER_MSG_NOW_YOU:3,SERVER_MSG_SHOT:4,SERVER_MSG_NEW_ROUND:5,SERVER_MSG_MATCH_END:6,SERVER_MSG_TIMEOUT:7,SERVER_MSG_CHECK:8,SERVER_MSG_RAND:9,SERVER_MSG_NOT_ENOUGH_DISTANCE:10,SERVER_MSG_PLAYER_LEFT:11,SERVER_MSG_HEARTBEAT:12,SERVER_MSG_PRACTICE_END:13},ClientMessageType:{CLIENT_MSG_UNKNOWN:0,CLIENT_MSG_HEARTBEAT_ACK:1,CLIENT_MSG_SHOOT_DATA:2,CLIENT_MSG_ACK:3,CLIENT_MSG_LEAVE:4},MatchStatus:{MATCH_STATUS_READY:0,MATCH_STATUS_STARTED:1,MATCH_STATUS_END:2,MATCH_STATUS_TIMEOUT:3,MATCH_STATUS_UNEXPECTEDLY:4},MatchPlayerStatus:{MATCH_PLAYER_STATUS_READY:0,MATCH_PLAYER_STATUS_STARTED:1,MATCH_PLAYER_STATUS_END:2},MatchShoot:{fields:{player_id:{type:"int64",id:1},status:{type:"int32",id:2},x:{type:"float",id:3},y:{type:"float",id:4},ring:{type:"int32",id:5},ring_x:{type:"bool",id:6},angle:{type:"float",id:7},distance:{type:"float",id:8}}},MatchShootList:{fields:{items:{rule:"repeated",type:"MatchShoot",id:1}}},RoundScore:{fields:{total_ring:{type:"int32",id:1},score:{type:"int32",id:2},if_win:{type:"bool",id:3}}},MatchRound:{fields:{shoots:{rule:"map",type:"MatchShootList",id:1,keytype:"int64"},scores:{rule:"map",type:"RoundScore",id:2,keytype:"int32"},round:{type:"int32",id:3},if_gold:{type:"bool",id:4},status:{type:"int32",id:5},gold_round:{type:"int32",id:6}}},PlayerMatchResult:{fields:{total_ring:{type:"int32",id:1},user_id:{type:"int64",id:2},ten_ring_count:{type:"int32",id:3},average_ring:{type:"float",id:4}}},PlayerFull:{fields:{id:{type:"int64",id:1},name:{type:"string",id:2},avatar:{type:"string",id:3},exp:{type:"int32",id:4},score:{type:"int32",id:5},level:{type:"int32",id:6},before_level:{type:"int32",id:7},before_exp:{type:"int32",id:8},current_exp:{type:"int32",id:9},upgrade_exp:{type:"int32",id:10},active:{type:"int32",id:11},device_id:{type:"string",id:12},s_vip:{type:"bool",id:13},vip:{type:"bool",id:14},player_match_result:{type:"PlayerMatchResult",id:15}}},TeamInfo:{fields:{players:{rule:"repeated",type:"PlayerFull",id:1},id:{type:"int32",id:2},name:{type:"string",id:3},score:{type:"int32",id:4}}},CurrentShoot:{fields:{round:{type:"int32",id:1},round_id:{type:"int64",id:2},index:{type:"int32",id:3},start_time:{type:"int64",id:4},player_id:{type:"int64",id:5},gold_round:{type:"bool",id:6},start_time_text:{type:"string",id:7},my_index:{type:"int32",id:8},index_map:{rule:"map",type:"int32",id:9,keytype:"int64"}}},ShootData:{fields:{x:{type:"float",id:1},y:{type:"float",id:2},r:{type:"float",id:3},dst:{type:"float",id:4},m:{type:"string",id:5},adc:{type:"float",id:6},device_id:{type:"string",id:7},shoot_id:{type:"string",id:8}}},PracticeInfo:{fields:{id:{type:"int64",id:1},user_id:{type:"int64",id:2},status:{type:"int32",id:3},status_text:{type:"string",id:4},start_time:{type:"int64",id:5},target_type:{type:"int32",id:6},vip:{type:"bool",id:7},s_vip:{type:"bool",id:8},device_id:{type:"string",id:9},shoot_data:{type:"MatchShoot",id:10},details:{rule:"repeated",type:"MatchShoot",id:11}}},MatchInfo:{fields:{match_id:{type:"int64",id:1},create_time:{type:"int64",id:2},start_time:{type:"int64",id:3},server_time:{type:"int64",id:4},shoot_time:{type:"int32",id:5},shoot_number:{type:"int32",id:6},ready_time:{type:"int32",id:7},way:{type:"int32",id:8},mode:{type:"int32",id:9},status:{type:"MatchStatus",id:10},status_text:{type:"string",id:11},rounds:{rule:"repeated",type:"MatchRound",id:12},teams:{rule:"map",type:"TeamInfo",id:13,keytype:"int32"},current:{type:"CurrentShoot",id:14},next:{type:"CurrentShoot",id:15},shoot_data:{type:"MatchShoot",id:16},win_team:{type:"int32",id:17},mvp:{type:"PlayerFull",id:18},room_id:{type:"string",id:19},result_list:{rule:"repeated",type:"PlayerMatchResult",id:20},timeout_time:{type:"int64",id:21},target_type:{type:"int32",id:22},event_type:{type:"int32",id:23},timeout:{type:"int32",id:24},server_addr:{type:"string",id:25}}},ServerMessage:{fields:{type:{type:"ServerMessageType",id:1},match_id:{type:"int64",id:2},timestamp:{type:"int64",id:3},match_info:{type:"MatchInfo",id:4,oneof:"payload"},shoot_data:{type:"ShootData",id:5,oneof:"payload"},practice_info:{type:"PracticeInfo",id:6,oneof:"payload"},sequence:{type:"int64",id:7}}},ClientMessage:{fields:{type:{type:"ClientMessageType",id:1},match_id:{type:"int64",id:2},user_id:{type:"int64",id:3},sequence:{type:"int64",id:4},data:{type:"bytes",id:5}}}}}}); export default $root; diff --git a/src/utils/matchReturn.js b/src/utils/matchReturn.js new file mode 100644 index 0000000..a9d89d4 --- /dev/null +++ b/src/utils/matchReturn.js @@ -0,0 +1,42 @@ +import { connectMatchWebSocket } from "@/matchWebsocket"; +import useStore from "@/store"; + +export const MATCH_RETURN_SNAPSHOT_KEY = "match-return-snapshot"; + +function getMatchId(battleInfo) { + return battleInfo?.matchId || battleInfo?.id || ""; +} + +function getBattlePageUrl(battleInfo) { + return Number(battleInfo?.mode) <= 3 + ? "/pages/team-battle/index?fromReturn=1" + : "/pages/melee-battle?fromReturn=1"; +} + +export function takeMatchReturnSnapshot() { + const snapshot = uni.getStorageSync(MATCH_RETURN_SNAPSHOT_KEY); + if (snapshot) uni.removeStorageSync(MATCH_RETURN_SNAPSHOT_KEY); + return snapshot && typeof snapshot === "object" ? snapshot : null; +} + +export async function returnToBattle(battleInfo, navigate) { + const matchId = getMatchId(battleInfo); + if (!battleInfo || !matchId || !battleInfo.serverAddr) { + uni.showToast({ + title: "比赛连接信息异常", + icon: "none", + }); + return false; + } + + const store = useStore(); + uni.setStorageSync(MATCH_RETURN_SNAPSHOT_KEY, battleInfo); + connectMatchWebSocket({ + serverAddr: battleInfo.serverAddr, + matchId, + userId: store.user?.id, + }); + + await navigate(getBattlePageUrl(battleInfo)); + return true; +}