From 12729a1cef4c5911b8d5d28ba9612e4b190644ae Mon Sep 17 00:00:00 2001 From: zhangyibo95 <690096405@qq.com> Date: Thu, 9 Jul 2026 14:35:48 +0800 Subject: [PATCH] =?UTF-8?q?update:=E4=BF=AE=E5=A4=8D=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E5=92=8C=E9=98=B2=E6=8A=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CreateRoom.vue | 9 ++++-- src/matchWebsocket.js | 55 ++++++++++++++++++++++++++--------- src/pages/battle-room.vue | 17 ++++++++--- src/pages/friend-battle.vue | 32 +++++++++++++------- src/pages/melee-battle.vue | 24 ++++++++++++++- 5 files changed, 106 insertions(+), 31 deletions(-) diff --git a/src/components/CreateRoom.vue b/src/components/CreateRoom.vue index e66dd22..c263440 100644 --- a/src/components/CreateRoom.vue +++ b/src/components/CreateRoom.vue @@ -36,6 +36,7 @@ const createRoom = debounce(async () => { } if (loading.value === true) return; loading.value = true; + let keepLoading = false; let size = 2; if (battleMode.value === 2) size = 10; if (battleMode.value === 3) size = 4; @@ -49,14 +50,18 @@ const createRoom = debounce(async () => { if (result.number) { props.onConfirm(); await joinRoomAPI(result.number); + keepLoading = true; uni.navigateTo({ url: "/pages/battle-room?roomNumber=" + result.number + "&target=" + targetMode.value, + fail: () => { + loading.value = false; + }, }); } } catch (error) { console.log(error); } finally { - loading.value = false; + if (!keepLoading) loading.value = false; } }); @@ -112,7 +117,7 @@ const createRoom = debounce(async () => { 40厘米全环靶 - 创建房间 + 创建房间 diff --git a/src/matchWebsocket.js b/src/matchWebsocket.js index b4452f7..cf6d9c3 100644 --- a/src/matchWebsocket.js +++ b/src/matchWebsocket.js @@ -159,7 +159,6 @@ function emitBusinessMessage(message) { scheduleReadyRouteFallback(message); } - console.log("[match-ws] emit socket-inbox", message.matchWsTypeName, message); uni.$emit("socket-inbox", message); } @@ -223,6 +222,13 @@ function getAckAudioKeys(message, businessMessage) { } } +function shouldCloseAfterAck(message) { + return ( + message.type === ServerMessageType.SERVER_MSG_MATCH_END || + message.type === ServerMessageType.SERVER_MSG_TIMEOUT + ); +} + // 追加 token 查询参数;如果后端地址已经带 token,则保持原样。 function appendQuery(url, key, value) { if (!value || new RegExp(`[?&]${key}=`).test(url)) return url; @@ -251,32 +257,47 @@ function normalizeServerUrl(serverAddr, token) { } // 统一发送二进制 protobuf 消息,并在日志里保留发送类型。 -function sendBuffer(buffer, label) { +function sendBuffer(buffer, label, clientMessage) { if (!socket || !buffer) return; const currentSocket = socket; currentSocket.send({ data: buffer, success: () => { - if (label === "heartbeat ack") return; - console.log(`[match-ws] ${label} sent`, new Date()); + console.log("发送比赛服 WebSocket 消息", label, clientMessage); }, fail: (err) => { if (socket !== currentSocket) return; - console.log(`[match-ws] ${label} send failed`, err); + console.log("比赛服 WebSocket 消息发送失败", label, clientMessage, err); }, }); } function sendHeartbeatAck() { // 心跳 ACK 不进入 pendingAcks,收到后立即回复。 - sendBuffer(createHeartbeatAckMessage(), "heartbeat ack"); + const clientMessage = { + type: ClientMessageType.CLIENT_MSG_HEARTBEAT_ACK, + }; + sendBuffer( + createHeartbeatAckMessage(), + "CLIENT_MSG_HEARTBEAT_ACK", + clientMessage + ); } function sendAck({ matchId, sequence }) { // sequence 由后端处理,前端只原样带回,不做重排和断线补发。 if (sequence === undefined || sequence === null || sequence === "") return; - sendBuffer(createAckMessage({ matchId, sequence }), `ack ${sequence}`); + const clientMessage = { + type: ClientMessageType.CLIENT_MSG_ACK, + match_id: matchId, + sequence, + }; + sendBuffer( + createAckMessage({ matchId, sequence }), + "CLIENT_MSG_ACK", + clientMessage + ); } function completeAckTask(task) { @@ -348,7 +369,7 @@ function queueAckAfterAudio(message, businessMessage) { pickField(message, "matchId", "match_id") || currentContext?.matchId ), sequence: message.sequence, - leaveAfterAck: message.type === ServerMessageType.SERVER_MSG_MATCH_END, + leaveAfterAck: shouldCloseAfterAck(message), }; const audioKeys = getAckAudioKeys(message, businessMessage).filter(Boolean); @@ -394,9 +415,6 @@ function handleMessage(data) { return; } - const typeName = getServerMessageTypeName(message.type); - console.log("[match-ws] message decoded", typeName, message); - const decodedMatchId = normalizeId(pickField(message, "matchId", "match_id")); if (decodedMatchId && currentContext) { currentContext.matchId = decodedMatchId; @@ -407,6 +425,9 @@ function handleMessage(data) { return; } + const typeName = getServerMessageTypeName(message.type); + console.log("收到比赛服 WebSocket 消息", typeName, message); + const businessMessage = buildBusinessMessage(message); if (businessMessage?.matchId && currentContext) { currentContext.matchId = businessMessage.matchId; @@ -418,12 +439,18 @@ function handleMessage(data) { function sendLeave() { // 主动关闭、比赛结束、页面离开时发 CLIENT_MSG_LEAVE。 if (!socket || !currentContext?.matchId) return; + const clientMessage = { + type: ClientMessageType.CLIENT_MSG_LEAVE, + match_id: currentContext.matchId, + user_id: currentContext.userId, + }; sendBuffer( createLeaveMessage({ - matchId: currentContext.matchId, - userId: currentContext.userId, + matchId: clientMessage.match_id, + userId: clientMessage.user_id, }), - "leave" + "CLIENT_MSG_LEAVE", + clientMessage ); } diff --git a/src/pages/battle-room.vue b/src/pages/battle-room.vue index 2ae2dc3..afd351a 100644 --- a/src/pages/battle-room.vue +++ b/src/pages/battle-room.vue @@ -15,7 +15,7 @@ import { getReadyAPI, kickPlayerAPI, } from "@/apis"; -import { isLimitError } from "@/util"; +import { debounce, isLimitError } from "@/util"; import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants"; import useStore from "@/store"; import { storeToRefs } from "pinia"; @@ -59,6 +59,7 @@ const allReady = ref(false); const timer = ref(null); const goBattle = ref(false); const showLimitModal = ref(false); +const readySubmitting = ref(false); /** 从结算页返回时为 true,跳过进场靶纸语音 */ const skipTargetAudio = ref(false); @@ -139,17 +140,22 @@ async function refreshRoomData() { // timer.value = setTimeout(refreshRoomData, 2000); } -const getReady = async () => { +const getReady = debounce(async () => { + if (readySubmitting.value) return; + readySubmitting.value = true; try { await getReadyAPI(roomNumber.value); + ready.value = true; } catch (error) { if (isLimitError(error)) { showLimitModal.value = true; return; } console.log("room ready error", error); + } finally { + readySubmitting.value = false; } -}; +}); const closeLimitModal = () => { showLimitModal.value = false; @@ -465,7 +471,10 @@ onBeforeUnmount(() => { - + {{ allReady.value ? "即将进入对局..." diff --git a/src/pages/friend-battle.vue b/src/pages/friend-battle.vue index fd53ad6..f7a17eb 100644 --- a/src/pages/friend-battle.vue +++ b/src/pages/friend-battle.vue @@ -27,6 +27,7 @@ const data = ref({}); const roomID = ref(""); const loading = ref(false); const showLimitModal = ref(false); +const createRoomLoading = ref(false); const isSVip = computed(() => user.value.sVip === true); const isVip = computed(() => user.value.vip === true && !isSVip.value); const challengeLimitText = computed(() => @@ -45,6 +46,7 @@ const enterRoom = debounce(async (number) => { showModal.value = true; return; } + loading.value = true; try { const room = await getRoomAPI(number); if (!room.number) { @@ -63,7 +65,6 @@ const enterRoom = debounce(async (number) => { return; } } - loading.value = true; uni.navigateTo({ url: "/pages/battle-room?roomNumber=" + number, }); @@ -71,16 +72,22 @@ const enterRoom = debounce(async (number) => { loading.value = false; } }); -const onCreateRoom = async () => { +const onCreateRoom = debounce(async () => { + if (createRoomLoading.value) return; if (!canEenter(user.value, device.value, online.value)) return; - const countData = await loadDailyCount(); - if (isLimitReached(countData.challenge)) { - showLimitModal.value = true; - return; + createRoomLoading.value = true; + try { + const countData = await loadDailyCount(); + if (isLimitReached(countData.challenge)) { + showLimitModal.value = true; + return; + } + warnning.value = ""; + showModal.value = true; + } finally { + createRoomLoading.value = false; } - warnning.value = ""; - showModal.value = true; -}; +}); const closeLimitModal = () => { showLimitModal.value = false; @@ -208,7 +215,12 @@ onLoad(async (options) => { {{ challengeLimitText }} - + 创建约战房 diff --git a/src/pages/melee-battle.vue b/src/pages/melee-battle.vue index de8f9e1..bb0cebc 100644 --- a/src/pages/melee-battle.vue +++ b/src/pages/melee-battle.vue @@ -13,7 +13,11 @@ import TestDistance from "@/components/TestDistance.vue"; import SModal from "@/components/SModal.vue"; import audioManager from "@/audioManager"; import { getBattleAPI, laserCloseAPI } from "@/apis"; -import { closeMatchWebSocket, connectMatchWebSocket } from "@/matchWebsocket"; +import { + closeMatchWebSocket, + connectMatchWebSocket, + MATCH_WS_AUDIO_ACK_EVENT, +} from "@/matchWebsocket"; import { MESSAGETYPESV2 } from "@/constants"; import { takeMatchReturnSnapshot } from "@/utils/matchReturn"; import useStore from "@/store"; @@ -172,6 +176,23 @@ function closeBattleServer(reason) { closeMatchWebSocket({ reason }); } +function notifyMatchAudioAck(message) { + if ( + message?.sequence === undefined || + message?.sequence === null || + message?.sequence === "" + ) { + return; + } + + uni.$emit(MATCH_WS_AUDIO_ACK_EVENT, { + matchId: message.matchId || battleId.value, + sequence: message.sequence, + matchWsType: message.matchWsType, + matchWsTypeName: message.matchWsTypeName, + }); +} + /** * 监听设备在线状态,大乱斗比赛进行中设备离线时弹窗提示用户 */ @@ -346,6 +367,7 @@ async function onReceiveMessage(msg) { startHalfRestCountdown(); } else if (msg.type === MESSAGETYPESV2.BattleEnd) { battleEnded = true; + notifyMatchAudioAck(msg); setTimeout(() => { // 全部跳转到新结算页 uni.redirectTo({