From c5618fb60b0b35d414de1dcdc7b699264c801ca0 Mon Sep 17 00:00:00 2001 From: zhangyibo95 <690096405@qq.com> Date: Fri, 24 Jul 2026 16:00:46 +0800 Subject: [PATCH] =?UTF-8?q?update:=E6=96=B0=E5=A2=9E=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E8=AE=AD=E7=BB=83=E9=87=8D=E8=BF=9E=E6=9C=BA=E5=88=B6=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=B7=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis.js | 6 +- src/canvas.js | 47 +++- src/components/BattleHeader.vue | 6 +- src/components/BowTarget.vue | 12 +- src/matchWebsocket.js | 18 +- src/pages/match-detail.vue | 59 +++- src/pages/practise-one.vue | 262 +++++++++++++++++- src/pages/practise-two.vue | 262 +++++++++++++++++- .../team-battle/components/BowTarget.vue | 12 +- src/pages/training/components/BowTarget.vue | 12 +- src/pages/training/practise-one.vue | 182 ++++++++++-- src/websocket.js | 2 +- 12 files changed, 811 insertions(+), 69 deletions(-) diff --git a/src/apis.js b/src/apis.js index 2873cb5..f00f561 100644 --- a/src/apis.js +++ b/src/apis.js @@ -8,7 +8,7 @@ try { switch (envVersion) { case "develop": // 开发版 - // BASE_URL = "http://192.168.1.5:8000/api/shoot"; + // BASE_URL = "http://192.168.1.2:8000/api/shoot"; BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot"; break; case "trial": // 体验版 @@ -269,6 +269,10 @@ export const createPractiseV2API = (trainingType, difficultyLevel) => { }); }; +export const getCurrentPractiseAPI = () => { + return request("GET", "/user/practice/current"); +}; + export const startPractiseAPI = (id) => { return request("POST", "/user/practice/begin", { id }); }; diff --git a/src/canvas.js b/src/canvas.js index 7c5b66b..853fae6 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -635,7 +635,7 @@ export function renderScores(ctx, arrows = [], bgImg) { ); } else { ctx.drawImage( - "../static/score-bg.png", + "/static/score-bg.png", 16 + (i % 9) * 30, 290 + Math.ceil((i + 1) / 9) * 30, 27, @@ -657,7 +657,7 @@ export function renderScores(ctx, arrows = [], bgImg) { ctx.drawImage(bgImg, 24 + rowIndex * 42, i > 5 ? 362 : 320, 38, 38); } else { ctx.drawImage( - "../static/score-bg.png", + "/static/score-bg.png", 24 + rowIndex * 42, i > 5 ? 362 : 320, 38, @@ -706,22 +706,39 @@ export async function sharePractiseData(canvasId, type, user, data) { ); const bgImg = await loadCanvasImage(canvas, bgImgSrc); - const avatarImgPromise = loadImage(user.avatar).then((path) => - loadCanvasImage(canvas, path) - ); - const lvlImgPromise = loadImage(user.lvlImage).then((path) => - loadCanvasImage(canvas, path) - ); + // 头像与段位框属于装饰图片,缺失时使用兜底或跳过,避免阻断分享。 + const loadProfileImage = async (src, fallbackSrc = "", label = "") => { + const normalizedSrc = + typeof src === "string" && src.startsWith("../static/") + ? src.slice(2) + : src; + if (normalizedSrc) { + try { + const path = await loadImage(normalizedSrc); + return await loadCanvasImage(canvas, path); + } catch (error) { + console.warn(`share ${label} image load failed`, error); + } + } + return fallbackSrc ? loadCanvasImage(canvas, fallbackSrc) : null; + }; - let titleImageSrc = "../static/first-try-title.png"; + const avatarImgPromise = loadProfileImage( + user?.avatar, + "/static/user-icon.png", + "avatar" + ); + const lvlImgPromise = loadProfileImage(user?.lvlImage, "", "level"); + + let titleImageSrc = "/static/first-try-title.png"; if (type == 2) { - titleImageSrc = "../static/practise-one-title.png"; + titleImageSrc = "/static/practise-one-title.png"; } else if (type == 3) { - titleImageSrc = "../static/practise-two-title.png"; + titleImageSrc = "/static/practise-two-title.png"; } const titleImgPromise = loadCanvasImage(canvas, titleImageSrc); - const scoreBgImgPromise = loadCanvasImage(canvas, "../static/score-bg.png"); - const qrCodeImgPromise = loadCanvasImage(canvas, "../static/qr-code.png"); + const scoreBgImgPromise = loadCanvasImage(canvas, "/static/score-bg.png"); + const qrCodeImgPromise = loadCanvasImage(canvas, "/static/qr-code.png"); const [avatarImg, lvlImg, titleImg, scoreBgImg, qrCodeImg] = await Promise.all([ @@ -738,8 +755,8 @@ export async function sharePractiseData(canvasId, type, user, data) { ctx.drawImage(bgImg, 0, 0, width, height); - drawRoundImage(ctx, avatarImg, 17, 20, 32, 32, 20); - ctx.drawImage(lvlImg, 12, 15, 42, 42); + if (avatarImg) drawRoundImage(ctx, avatarImg, 17, 20, 32, 32, 20); + if (lvlImg) ctx.drawImage(lvlImg, 12, 15, 42, 42); renderText(ctx, user.nickName, 13, "#fff", 58, 34); renderRankTitle(ctx, user.lvlName); diff --git a/src/components/BattleHeader.vue b/src/components/BattleHeader.vue index 8bf6458..9cdc95e 100644 --- a/src/components/BattleHeader.vue +++ b/src/components/BattleHeader.vue @@ -38,7 +38,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true; diff --git a/src/matchWebsocket.js b/src/matchWebsocket.js index 28bc04d..4e4ad12 100644 --- a/src/matchWebsocket.js +++ b/src/matchWebsocket.js @@ -849,6 +849,7 @@ export function connectMatchWebSocket(options = {}) { token, mode, requestPracticeInfoOnOpen = false, + appHideResumable = false, force = false, reconnecting = false, reconnectReason = "", @@ -920,6 +921,7 @@ export function connectMatchWebSocket(options = {}) { isMelee: Number.isFinite(normalizedMode) ? normalizedMode > 3 : undefined, requestPracticeInfoOnOpen: requestPracticeInfoOnOpen === true, + appHideResumable: appHideResumable === true, meleeHalfRest: isSameContext ? currentContext?.meleeHalfRest === true : false, @@ -1016,9 +1018,22 @@ export function connectMatchWebSocketFromNotice(notice, fallbackUserId) { }); } +export function setMatchAppHideResumable(enabled) { + if (!currentContext) return; + currentContext.appHideResumable = enabled === true; +} + export function closeMatchWebSocket(options = {}) { // 默认关闭时会发送 LEAVE;内部切换连接可通过 sendLeave:false 跳过。 - const { sendLeave: shouldSendLeave = true, reason = "manual" } = options; + const { reason = "manual" } = options; + // 可恢复训练切后台只断开传输层,不能把它上报成主动离场。 + const shouldSendLeave = + options.sendLeave === undefined + ? !( + reason === "app-hide" && + currentContext?.appHideResumable === true + ) + : options.sendLeave === true; manualClose = true; clearReconnectTimer(); @@ -1051,6 +1066,7 @@ export default { ServerMessageType, connectMatchWebSocket, connectMatchWebSocketFromNotice, + setMatchAppHideResumable, closeMatchWebSocket, forceReconnectMatchWebSocket, handleMatchNetworkStatusChange, diff --git a/src/pages/match-detail.vue b/src/pages/match-detail.vue index b604584..e2bc460 100644 --- a/src/pages/match-detail.vue +++ b/src/pages/match-detail.vue @@ -13,10 +13,10 @@ const data = ref({ rounds: [], }); const players = ref([]); +const isLoading = ref(true); +const loadError = ref(""); -onLoad(async (options) => { - if (!options.battleId) return; - battleId.value = options.battleId || "60510101693403136"; +const loadBattle = async () => { const result = await getBattleAPI(battleId.value); data.value = result; if (result.mode > 3) { @@ -62,6 +62,32 @@ onLoad(async (options) => { players.value = [...rankedPlayers, ...unrankedPlayers]; } + + // 数据和派生列表均已就绪后再关闭占位,避免空壳闪烁。 + isLoading.value = false; +}; + +const requestBattle = () => { + if (!battleId.value) return; + + isLoading.value = true; + loadError.value = ""; + loadBattle().catch((error) => { + console.error("加载比赛详情失败:", error); + loadError.value = "赛况加载失败"; + isLoading.value = false; + }); +}; + +onLoad((options) => { + if (!options.battleId) { + loadError.value = "缺少比赛信息"; + isLoading.value = false; + return; + } + + battleId.value = options.battleId; + requestBattle(); }); const checkBowData = (selected) => { @@ -80,6 +106,17 @@ const checkBowData = (selected) => {