From 3c5754b3fdec8112fe233d7edcf130d25d15b45a Mon Sep 17 00:00:00 2001 From: zhangyibo95 <690096405@qq.com> Date: Thu, 23 Jul 2026 09:44:58 +0800 Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A=E5=AF=B9=E6=8E=A5=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E8=AE=AD=E7=BB=83=E6=94=B9=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis.js | 16 +- src/components/TargetCanvas.vue | 290 ++++--- src/manifest.json | 3 + src/matchWebsocket.js | 99 ++- src/mock/index.js | 22 +- src/pages.json | 23 +- src/pages/training/components/BowTarget.vue | 327 ++++++-- src/pages/training/components/ScorePanel.vue | 4 +- src/pages/training/components/ScorePanel2.vue | 7 +- src/pages/training/components/ScoreResult.vue | 313 +++++-- src/pages/training/components/ScreenHint.vue | 10 +- .../training/components/ShootProgress.vue | 84 +- .../training/components/TestDistance.vue | 8 +- .../components/TrainingDifficultyBadge.vue | 4 +- .../TrainingDifficultyPreviewCard.vue | 9 +- .../TrainingDifficultyStartButton.vue | 2 +- src/pages/training/difficulty.vue | 93 ++- src/pages/training/index.vue | 123 ++- src/pages/training/practise-one.vue | 771 ++++++++++++++++-- .../training-difficulty-design/block-gold.png | Bin .../training-difficulty-design/block-gray.png | Bin .../static/training-difficulty-design/btn.png | Bin .../static/training-difficulty-design/cha.png | Bin .../training-difficulty-design/dao-icon.png | Bin .../static/training-difficulty-design/gou.png | Bin .../training-difficulty-design/jiantou.png | Bin .../training-difficulty-design/lock.svg | 0 .../training-difficulty-design/par-bg.png | Bin .../training-difficulty-design/par-star.png | Bin .../training-difficulty-design/result-bg.png | Bin .../result-c-bg.png | Bin .../result-icon-1.png | Bin .../result-icon-2.png | Bin .../result-icon-3.png | Bin .../result-rice.png | Bin .../result-t-bg.png | Bin .../training-difficulty-design/result-up.png | Bin .../text-icon-jcxl.png} | Bin .../text-icon-jingzxl.png | Bin 0 -> 22972 bytes .../text-icon-jzxl.png | Bin 0 -> 20684 bytes .../text-icon-nlxl.png | Bin 0 -> 20229 bytes .../training-difficulty-design/text.png | Bin .../training-difficulty-design/unlock.svg | 0 .../training}/static/training-home/done.png | Bin .../training}/static/training-home/img_16.png | Bin .../training}/static/training-home/img_17.png | Bin .../training}/static/training-home/img_19.png | Bin .../training}/static/training-home/img_20.png | Bin .../training}/static/training-home/img_21.png | Bin .../training}/static/training-home/img_22.png | Bin .../training}/static/training-home/img_28.png | Bin .../training}/static/training-home/img_3.png | Bin .../training}/static/training-home/img_4.png | Bin .../training}/static/training-home/img_5.png | Bin .../training}/static/training-home/img_6.png | Bin .../training}/static/training-home/img_7.png | Bin .../static/training-home/jiezouxunlian.png | Bin .../static/training-home/jingzhunxunlian.png | Bin .../static/training-home/liliangxulian.png | Bin .../training}/static/training-home/missed.png | Bin .../static/training-home/nailixunlian.png | Bin src/utils/match.min.js | 2 +- src/utils/matchProtocol.js | 45 + src/websocket.js | 2 +- 64 files changed, 1769 insertions(+), 488 deletions(-) rename src/{ => pages/training}/static/training-difficulty-design/block-gold.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/block-gray.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/btn.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/cha.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/dao-icon.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/gou.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/jiantou.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/lock.svg (100%) rename src/{ => pages/training}/static/training-difficulty-design/par-bg.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/par-star.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/result-bg.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/result-c-bg.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/result-icon-1.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/result-icon-2.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/result-icon-3.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/result-rice.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/result-t-bg.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/result-up.png (100%) rename src/{static/training-difficulty-design/text-icon-cgxl.png => pages/training/static/training-difficulty-design/text-icon-jcxl.png} (100%) create mode 100644 src/pages/training/static/training-difficulty-design/text-icon-jingzxl.png create mode 100644 src/pages/training/static/training-difficulty-design/text-icon-jzxl.png create mode 100644 src/pages/training/static/training-difficulty-design/text-icon-nlxl.png rename src/{ => pages/training}/static/training-difficulty-design/text.png (100%) rename src/{ => pages/training}/static/training-difficulty-design/unlock.svg (100%) rename src/{ => pages/training}/static/training-home/done.png (100%) rename src/{ => pages/training}/static/training-home/img_16.png (100%) rename src/{ => pages/training}/static/training-home/img_17.png (100%) rename src/{ => pages/training}/static/training-home/img_19.png (100%) rename src/{ => pages/training}/static/training-home/img_20.png (100%) rename src/{ => pages/training}/static/training-home/img_21.png (100%) rename src/{ => pages/training}/static/training-home/img_22.png (100%) rename src/{ => pages/training}/static/training-home/img_28.png (100%) rename src/{ => pages/training}/static/training-home/img_3.png (100%) rename src/{ => pages/training}/static/training-home/img_4.png (100%) rename src/{ => pages/training}/static/training-home/img_5.png (100%) rename src/{ => pages/training}/static/training-home/img_6.png (100%) rename src/{ => pages/training}/static/training-home/img_7.png (100%) rename src/{ => pages/training}/static/training-home/jiezouxunlian.png (100%) rename src/{ => pages/training}/static/training-home/jingzhunxunlian.png (100%) rename src/{ => pages/training}/static/training-home/liliangxulian.png (100%) rename src/{ => pages/training}/static/training-home/missed.png (100%) rename src/{ => pages/training}/static/training-home/nailixunlian.png (100%) diff --git a/src/apis.js b/src/apis.js index 35fe672..2873cb5 100644 --- a/src/apis.js +++ b/src/apis.js @@ -8,7 +8,7 @@ try { switch (envVersion) { case "develop": // 开发版 - // BASE_URL = "http://192.168.1.2:8000/api/shoot"; + // BASE_URL = "http://192.168.1.5:8000/api/shoot"; BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot"; break; case "trial": // 体验版 @@ -26,8 +26,6 @@ try { } const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot"); -const PRACTICE_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api"); - // 统一处理业务接口请求,包含登录态、业务错误和 WiFi 连接空响应兼容。 function request(method, url, data = {}, baseUrl = BASE_URL) { const token = uni.getStorageSync( @@ -264,13 +262,11 @@ export const createPractiseAPI = (arrows, time, target) => { }); }; -export const createPractiseV2API = (arrows, time, target, deviceId) => { - return request("POST", "/practice/create-v2", { - shootNumber: arrows, - shootTime: time, - targetType: Number(target || 1) * 20, - deviceId, - }, PRACTICE_BASE_URL); +export const createPractiseV2API = (trainingType, difficultyLevel) => { + return request("POST", "/user/practice/create/v2", { + trainingType, + difficultyLevel, + }); }; export const startPractiseAPI = (id) => { diff --git a/src/components/TargetCanvas.vue b/src/components/TargetCanvas.vue index 05a9a56..f699158 100644 --- a/src/components/TargetCanvas.vue +++ b/src/components/TargetCanvas.vue @@ -21,34 +21,29 @@ const props = defineProps({ type: Boolean, default: false, }, - // 是否显示象限文字。 - showQuadrantLabels: { - type: Boolean, - default: false, - }, // 是否显示环数文字。 showRingLabels: { type: Boolean, default: true, }, - // 象限文字配置,key 为 1/2/3/4。 - quadrantLabels: { - type: Object, - default: () => ({ - 1: "1", - 2: "2", - 3: "3", - 4: "4", - }), + // 从正上方开始顺时针等分的区域数量。 + sectorCount: { + type: Number, + default: 0, }, - // 高亮区域数组。 - // quadrant: 1/2/3/4,表示第几个象限。 - // rings: "all" 或环数数组,例如 [7, 8, 9, 10]。 - // scope: "box" 表示整象限矩形,"sector" 表示环形扇区。 - // style: 可覆盖高亮填充色、描边色、线宽比例。 - highlightAreas: { - type: Array, - default: () => [], + // 当前高亮区域,范围为 1 到 sectorCount。 + activeSector: { + type: Number, + default: 0, + }, + // 指定环数,1 到 10;无效值表示高亮整个区域。 + activeRing: { + type: Number, + default: 0, + }, + showSectorLabels: { + type: Boolean, + default: false, }, // 只绘制透明高亮层,不绘制完整靶纸;用于叠加在靶纸图片上。 highlightOnly: { @@ -74,8 +69,18 @@ const props = defineProps({ type: Object, default: () => ({}), }, - // 象限文字样式覆盖配置。 - quadrantLabelStyle: { + // 区域分割线样式覆盖配置。 + sectorStyle: { + type: Object, + default: () => ({}), + }, + // 区域数字样式覆盖配置。 + sectorLabelStyle: { + type: Object, + default: () => ({}), + }, + // 高亮样式覆盖配置。 + highlightStyle: { type: Object, default: () => ({}), }, @@ -120,16 +125,24 @@ const defaultCrosshairStyle = { lineWidthRatio: 0.0025, }; -// 象限文字默认样式。 -const defaultQuadrantLabelStyle = { +// 顺时针等分线默认样式。 +const defaultSectorStyle = { + color: "rgba(255, 255, 255, 0.82)", + lineWidthRatio: 0.004, +}; + +// 区域数字默认样式。 +const defaultSectorLabelStyle = { color: "#ffffff", - fontSizeRatio: 0.045, - offsetRatio: 0.78, + backgroundColor: "rgba(0, 0, 0, 0.62)", + fontSizeRatio: 0.075, + radiusRatio: 0.76, + badgeRadiusRatio: 0.07, }; // 高亮区域默认样式。 const defaultHighlightStyle = { - color: "rgba(254, 216, 71, 0.34)", + color: "rgba(255, 228, 0, 0.6)", strokeColor: "rgba(254, 216, 71, 0.82)", lineWidthRatio: 0.003, }; @@ -155,40 +168,24 @@ const getRingColor = (ring, config) => { return config.ringColors?.[ring] || config.ringColors?.[String(ring)] || "#ffffff"; }; -// 规范化高亮环数配置;"all" 表示全部环,数组或单值会过滤非法环数。 -const normalizeRings = (rings, ringCount) => { - if (rings === "all") { - return "all"; - } - - const rawRings = Array.isArray(rings) ? rings : [rings]; - return rawRings - .map((ring) => Number(ring)) - .filter((ring) => Number.isInteger(ring) && ring >= 1 && ring <= ringCount); +const getPositiveInteger = (value) => { + const numberValue = Number(value); + return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0; }; -// 获取象限对应的扇形弧度范围。 -const getQuadrantAngles = (quadrant) => { - const angleMap = { - 1: [Math.PI, Math.PI * 1.5], - 2: [Math.PI * 1.5, Math.PI * 2], - 3: [Math.PI * 0.5, Math.PI], - 4: [0, Math.PI * 0.5], +// 正上方作为第一区起始边界,Canvas 角度递增方向即为顺时针。 +const getSectorAngles = (sector, sectorCount) => { + const count = getPositiveInteger(sectorCount); + const index = getPositiveInteger(sector); + if (!count || !index || index > count) return null; + + const step = (Math.PI * 2) / count; + const startAngle = -Math.PI / 2 + (index - 1) * step; + return { + startAngle, + endAngle: startAngle + step, + middleAngle: startAngle + step / 2, }; - - return angleMap[Number(quadrant)] || null; -}; - -// 获取象限对应的矩形区域,用于整象限高亮。 -const getQuadrantBox = (quadrant, centerX, centerY, radius) => { - const boxMap = { - 1: [centerX - radius, centerY - radius, radius, radius], - 2: [centerX, centerY - radius, radius, radius], - 3: [centerX - radius, centerY, radius, radius], - 4: [centerX, centerY, radius, radius], - }; - - return boxMap[Number(quadrant)] || null; }; // 绘制实心圆,靶纸环区和中心点都会用到。 @@ -240,58 +237,61 @@ const drawTargetRings = (ctx, centerX, centerY, targetRadius, config) => { } }; -// 绘制所有高亮区域,支持整象限矩形高亮和指定环数扇区高亮。 -const drawHighlights = (ctx, centerX, centerY, targetRadius, config) => { - props.highlightAreas.forEach((area = {}) => { - const angles = getQuadrantAngles(area.quadrant); +// 高亮后端指定区域;activeRing 有效时只高亮该区域内的单个环。 +const drawSectorHighlight = (ctx, centerX, centerY, targetRadius, config) => { + const angles = getSectorAngles(props.activeSector, props.sectorCount); + if (!angles) return; - if (!angles) { - return; - } + const ring = getPositiveInteger(props.activeRing); + const hasActiveRing = ring >= 1 && ring <= config.ringCount; + const innerRadius = hasActiveRing + ? targetRadius * ((config.ringCount - ring) / config.ringCount) + : 0; + const outerRadius = hasActiveRing + ? targetRadius * ((config.ringCount + 1 - ring) / config.ringCount) + : targetRadius; + const style = { + ...defaultHighlightStyle, + ...props.highlightStyle, + }; - const highlightStyle = { - ...defaultHighlightStyle, - ...(area.style || {}), - }; - const highlightLineWidth = Math.max(1, targetRadius * highlightStyle.lineWidthRatio); - const rings = normalizeRings(area.rings || "all", config.ringCount); - const scope = area.scope || (rings === "all" ? "box" : "sector"); + drawAnnularSector( + ctx, + centerX, + centerY, + innerRadius, + outerRadius, + angles.startAngle, + angles.endAngle, + style.color, + style.strokeColor, + Math.max(1, targetRadius * style.lineWidthRatio) + ); +}; - // 整象限默认画成矩形高亮,便于对应 1/2/3/4 号框训练提示。 - if (rings === "all" && scope === "box") { - const box = getQuadrantBox(area.quadrant, centerX, centerY, targetRadius); - if (!box) return; - ctx.beginPath(); - ctx.rect(...box); - ctx.setFillStyle(highlightStyle.color); - ctx.fill(); - ctx.setStrokeStyle(highlightStyle.strokeColor); - ctx.setLineWidth(highlightLineWidth); - ctx.stroke(); - return; - } +// 从正上方开始顺时针绘制所有区域边界。 +const drawSectorLines = (ctx, centerX, centerY, targetRadius) => { + const count = getPositiveInteger(props.sectorCount); + if (!count) return; - const targetRings = rings === "all" - ? Array.from({ length: config.ringCount }, (_, index) => index + 1) - : rings; + const style = { + ...defaultSectorStyle, + ...props.sectorStyle, + }; + const step = (Math.PI * 2) / count; - targetRings.forEach((ring) => { - const innerRadius = targetRadius * ((config.ringCount - ring) / config.ringCount); - const outerRadius = targetRadius * ((config.ringCount + 1 - ring) / config.ringCount); - drawAnnularSector( - ctx, - centerX, - centerY, - innerRadius, - outerRadius, - angles[0], - angles[1], - highlightStyle.color, - highlightStyle.strokeColor, - highlightLineWidth - ); - }); - }); + ctx.beginPath(); + for (let index = 0; index < count; index += 1) { + const angle = -Math.PI / 2 + index * step; + ctx.moveTo(centerX, centerY); + ctx.lineTo( + centerX + Math.cos(angle) * targetRadius, + centerY + Math.sin(angle) * targetRadius + ); + } + ctx.setStrokeStyle(style.color); + ctx.setLineWidth(Math.max(1, targetRadius * style.lineWidthRatio)); + ctx.stroke(); }; // 绘制各环之间的分割线。 @@ -351,44 +351,30 @@ const drawRingLabels = (ctx, centerX, centerY, targetRadius, config) => { } }; -// 绘制象限文字。 -const drawQuadrantLabels = (ctx, centerX, centerY, targetRadius) => { - if (!props.showQuadrantLabels) { - return; - } +// 在每个区域中线位置绘制编号,编号层始终位于高亮和分割线之上。 +const drawSectorLabels = (ctx, centerX, centerY, targetRadius) => { + const count = getPositiveInteger(props.sectorCount); + if (!props.showSectorLabels || !count) return; const style = { - ...defaultQuadrantLabelStyle, - ...props.quadrantLabelStyle, - }; - const offset = targetRadius * style.offsetRatio; - const positions = { - 1: [centerX - offset, centerY - offset], - 2: [centerX + offset, centerY - offset], - 3: [centerX - offset, centerY + offset], - 4: [centerX + offset, centerY + offset], + ...defaultSectorLabelStyle, + ...props.sectorLabelStyle, }; + const labelRadius = targetRadius * style.radiusRatio; + const badgeRadius = Math.max(10, targetRadius * style.badgeRadiusRatio); - ctx.setFontSize(Math.max(12, targetRadius * style.fontSizeRatio)); + ctx.setFontSize(Math.max(11, targetRadius * style.fontSizeRatio)); ctx.setTextAlign("center"); ctx.setTextBaseline("middle"); - ctx.setFillStyle(style.color); - Object.entries(positions).forEach(([key, position]) => { - const label = props.quadrantLabels?.[key] || props.quadrantLabels?.[Number(key)]; - if (label === undefined || label === null || label === "") return; - ctx.fillText(String(label), position[0], position[1]); - }); -}; - -// 生成高亮区域的绘制 key,只保留真正影响画面的字段。 -const getHighlightDrawKeyAreas = () => { - return props.highlightAreas.map((area = {}) => ({ - quadrant: area.quadrant, - rings: area.rings, - scope: area.scope, - style: area.style, - })); + for (let sector = 1; sector <= count; sector += 1) { + const angles = getSectorAngles(sector, count); + const x = centerX + Math.cos(angles.middleAngle) * labelRadius; + const y = centerY + Math.sin(angles.middleAngle) * labelRadius; + drawCircle(ctx, x, y, badgeRadius, style.backgroundColor); + ctx.setFillStyle(style.color); + ctx.fillText(String(sector), x, y); + } }; // 生成本次绘制状态的唯一 key,用于避免相同内容重复 draw。 @@ -398,12 +384,16 @@ const getDrawKey = (width, height) => { height, coordinateRadius: props.coordinateRadius, showCrosshair: props.showCrosshair, - showQuadrantLabels: props.showQuadrantLabels, showRingLabels: props.showRingLabels, - highlightAreas: getHighlightDrawKeyAreas(), + sectorCount: props.sectorCount, + activeSector: props.activeSector, + activeRing: props.activeRing, + showSectorLabels: props.showSectorLabels, targetStyleConfig: props.targetStyleConfig, crosshairStyle: props.crosshairStyle, - quadrantLabelStyle: props.quadrantLabelStyle, + sectorStyle: props.sectorStyle, + sectorLabelStyle: props.sectorLabelStyle, + highlightStyle: props.highlightStyle, highlightOnly: props.highlightOnly, }); }; @@ -431,7 +421,7 @@ const drawTarget = () => { drawTargetRings(ctx, centerX, centerY, targetRadius, config); } - drawHighlights(ctx, centerX, centerY, targetRadius, config); + drawSectorHighlight(ctx, centerX, centerY, targetRadius, config); if (!props.highlightOnly) { drawRingLines(ctx, centerX, centerY, targetRadius, config); @@ -444,9 +434,12 @@ const drawTarget = () => { ); drawCrosshair(ctx, centerX, centerY, targetRadius); drawRingLabels(ctx, centerX, centerY, targetRadius, config); - drawQuadrantLabels(ctx, centerX, centerY, targetRadius); } + // 高亮先画,等分线和编号后画,避免高亮覆盖区域边界。 + drawSectorLines(ctx, centerX, centerY, targetRadius); + drawSectorLabels(ctx, centerX, centerY, targetRadius); + ctx.draw(); lastDrawKey.value = drawKey; }; @@ -494,16 +487,19 @@ watch( () => [ props.coordinateRadius, props.showCrosshair, - props.showQuadrantLabels, props.showRingLabels, - props.highlightAreas, + props.sectorCount, + props.activeSector, + props.activeRing, + props.showSectorLabels, props.highlightOnly, props.canvasWidth, props.canvasHeight, - props.quadrantLabels, props.targetStyleConfig, props.crosshairStyle, - props.quadrantLabelStyle, + props.sectorStyle, + props.sectorLabelStyle, + props.highlightStyle, ], scheduleDraw, { diff --git a/src/manifest.json b/src/manifest.json index a074640..ff67d53 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -289,6 +289,9 @@ } ] }, + "optimization" : { + "subPackages" : true + }, "setting" : { "urlCheck" : false, "minified" : true, diff --git a/src/matchWebsocket.js b/src/matchWebsocket.js index bcf5e13..28bc04d 100644 --- a/src/matchWebsocket.js +++ b/src/matchWebsocket.js @@ -4,6 +4,7 @@ import { createAckMessage, createHeartbeatAckMessage, createLeaveMessage, + createSyncPracticeInfoMessage, decodeServerMessage, getServerMessageTypeName, } from "@/utils/matchProtocol"; @@ -57,6 +58,7 @@ const BUSINESS_TYPE_BY_SERVER_TYPE = { const MATCH_READY_SNAPSHOT_PREFIX = "match-ready-snapshot:"; export const MATCH_WS_AUDIO_ACK_EVENT = "match-ws-audio-ack"; export const MATCH_WS_STATE_EVENT = "match-ws-state"; +export const MATCH_WS_PRACTICE_SYNC_EVENT = "match-ws-practice-sync"; function normalizeShootData(shootData) { if (!shootData || typeof shootData !== "object") return shootData; @@ -184,6 +186,22 @@ function buildBusinessMessage(message) { }; } +function buildPracticeSyncMessage(message) { + const practiceInfo = normalizePracticeInfo(message.practice_info); + const matchId = normalizeId( + pickField(message, "matchId", "match_id") || + practiceInfo.id || + currentContext?.matchId + ); + + return { + matchId, + timestamp: message.timestamp, + sequence: message.sequence, + practiceInfo, + }; +} + function getReadySnapshotKey(matchId) { return `${MATCH_READY_SNAPSHOT_PREFIX}${normalizeId(matchId)}`; } @@ -611,6 +629,24 @@ function sendHeartbeatAck() { ); } +function sendPracticeInfoSync() { + if (!socket || !currentContext?.matchId || !currentContext?.userId) return; + + const clientMessage = { + type: ClientMessageType.CLIENT_MSG_SYNC_PRACTICE_INFO, + match_id: currentContext.matchId, + user_id: currentContext.userId, + }; + sendBuffer( + createSyncPracticeInfoMessage({ + matchId: clientMessage.match_id, + userId: clientMessage.user_id, + }), + "CLIENT_MSG_SYNC_PRACTICE_INFO", + clientMessage + ); +} + function sendAck({ matchId, sequence }) { // sequence 由后端处理,前端只原样带回,不做重排和断线补发。 if (sequence === undefined || sequence === null || sequence === "") return; @@ -682,26 +718,27 @@ function removeAudioAckListener() { } function queueAckAfterAudio(message, businessMessage) { - // 除心跳外,只要服务端带了 sequence,都需要走 ACK;没有语音的消息立即 ACK。 - if ( - message.sequence === undefined || - message.sequence === null || - message.sequence === "" - ) { - return; - } + // 终止消息即使没有 sequence,也要等结束语音完成后主动关闭连接。 + const leaveAfterAck = shouldCloseAfterAck(message, businessMessage); + const hasSequence = + message.sequence !== undefined && + message.sequence !== null && + message.sequence !== ""; + if (!hasSequence && !leaveAfterAck) return; + const task = { matchId: normalizeId( pickField(message, "matchId", "match_id") || currentContext?.matchId ), sequence: message.sequence, - leaveAfterAck: shouldCloseAfterAck(message, businessMessage), + leaveAfterAck, }; + const actionLabel = hasSequence ? "ack" : "terminal close"; const audioKeys = getAckAudioKeys(message, businessMessage).filter(Boolean); if (!audioKeys.length) { console.log( - "[match-ws] ack immediately without audio", + `[match-ws] ${actionLabel} immediately without audio`, getServerMessageTypeName(message.type), message.sequence ); @@ -715,7 +752,7 @@ function queueAckAfterAudio(message, businessMessage) { if (index === -1) return; pendingAcks.splice(index, 1); console.log( - "[match-ws] ack audio wait timeout", + `[match-ws] ${actionLabel} audio wait timeout`, getServerMessageTypeName(message.type), message.sequence, task.expectedAudioKey @@ -724,7 +761,7 @@ function queueAckAfterAudio(message, businessMessage) { }, ACK_AUDIO_TIMEOUT_MS); pendingAcks.push(task); console.log( - "[match-ws] ack queued until audioEnded", + `[match-ws] ${actionLabel} queued until audioEnded`, getServerMessageTypeName(message.type), message.sequence, task.expectedAudioKey @@ -741,11 +778,6 @@ function handleMessage(data) { return; } - const decodedMatchId = normalizeId(pickField(message, "matchId", "match_id")); - if (decodedMatchId && currentContext) { - currentContext.matchId = decodedMatchId; - } - if (message.type === ServerMessageType.SERVER_MSG_HEARTBEAT) { sendHeartbeatAck(); return; @@ -754,6 +786,34 @@ function handleMessage(data) { const typeName = getServerMessageTypeName(message.type); console.log("收到比赛服 WebSocket 消息", typeName, message); + const decodedMatchId = normalizeId(pickField(message, "matchId", "match_id")); + if (message.type === ServerMessageType.SERVER_MSG_SYNC_PRACTICE_INFO) { + // 同步响应是完整快照,不映射成开始/报靶等实时事件,避免重放页面副作用。 + queueAckAfterAudio(message, null); + if ( + decodedMatchId && + currentContext?.matchId && + decodedMatchId !== currentContext.matchId + ) { + console.log("[match-ws] ignore mismatched practice sync", { + expectedMatchId: currentContext.matchId, + receivedMatchId: decodedMatchId, + }); + return; + } + + const syncMessage = buildPracticeSyncMessage(message); + if (syncMessage.matchId && currentContext) { + currentContext.matchId = syncMessage.matchId; + } + uni.$emit(MATCH_WS_PRACTICE_SYNC_EVENT, syncMessage); + return; + } + + if (decodedMatchId && currentContext) { + currentContext.matchId = decodedMatchId; + } + const businessMessage = buildBusinessMessage(message); if (businessMessage?.matchId && currentContext) { currentContext.matchId = businessMessage.matchId; @@ -788,6 +848,7 @@ export function connectMatchWebSocket(options = {}) { userId, token, mode, + requestPracticeInfoOnOpen = false, force = false, reconnecting = false, reconnectReason = "", @@ -858,6 +919,7 @@ export function connectMatchWebSocket(options = {}) { mode: Number.isFinite(normalizedMode) ? normalizedMode : undefined, isMelee: Number.isFinite(normalizedMode) ? normalizedMode > 3 : undefined, + requestPracticeInfoOnOpen: requestPracticeInfoOnOpen === true, meleeHalfRest: isSameContext ? currentContext?.meleeHalfRest === true : false, @@ -911,6 +973,9 @@ export function connectMatchWebSocket(options = {}) { reason: reconnectReason, reconnected: wasReconnected, }); + if (currentContext?.requestPracticeInfoOnOpen) { + sendPracticeInfoSync(); + } }); socketTask.onMessage((res) => { diff --git a/src/mock/index.js b/src/mock/index.js index 9d627a8..78da2c2 100644 --- a/src/mock/index.js +++ b/src/mock/index.js @@ -4,43 +4,43 @@ export const trainingHomeWeekSchedule = [ key: "mon", label: "周一", status: "done", - icon: "../../static/training-home/done.png", + icon: "/pages/training/static/training-home/done.png", }, { key: "tue", label: "周二", status: "done", - icon: "../../static/training-home/done.png", + icon: "/pages/training/static/training-home/done.png", }, { key: "wed", label: "周三", status: "missed", - icon: "../../static/training-home/missed.png", + icon: "/pages/training/static/training-home/missed.png", }, { key: "thu", label: "周四", status: "missed", - icon: "../../static/training-home/missed.png", + icon: "/pages/training/static/training-home/missed.png", }, { key: "fri", label: "周五", status: "done", - icon: "../../static/training-home/done.png", + icon: "/pages/training/static/training-home/done.png", }, { key: "sat", label: "周六", status: "done", - icon: "../../static/training-home/done.png", + icon: "/pages/training/static/training-home/done.png", }, { key: "sun", label: "周日", status: "missed", - icon: "../../static/training-home/missed.png", + icon: "/pages/training/static/training-home/missed.png", }, ]; @@ -73,7 +73,7 @@ export const trainingHomeModes = [ key: "endurance", title: "耐力训练", progressText: "当前进度 LV5 >", - icon: "../../static/training-home/img_3.png", + icon: "/pages/training/static/training-home/img_3.png", recommended: true, disabled: false, }, @@ -81,7 +81,7 @@ export const trainingHomeModes = [ key: "precision", title: "精准训练", progressText: "当前进度 LV3 >", - icon: "../../static/training-home/img_4.png", + icon: "/pages/training/static/training-home/img_4.png", recommended: false, disabled: false, }, @@ -89,7 +89,7 @@ export const trainingHomeModes = [ key: "rhythm", title: "节奏训练", progressText: "当前进度 LV6 >", - icon: "../../static/training-home/img_5.png", + icon: "/pages/training/static/training-home/img_5.png", recommended: false, disabled: false, }, @@ -97,7 +97,7 @@ export const trainingHomeModes = [ key: "power", title: "力量训练", progressText: "Coming! LV10", - icon: "../../static/training-home/img_6.png", + icon: "/pages/training/static/training-home/img_6.png", recommended: false, disabled: true, }, diff --git a/src/pages.json b/src/pages.json index e47dbd0..1a12552 100644 --- a/src/pages.json +++ b/src/pages.json @@ -105,15 +105,6 @@ { "path": "pages/mine-bow-data" }, - { - "path": "pages/training/difficulty" - }, - { - "path": "pages/training/index" - }, - { - "path": "pages/training/practise-one" - }, { "path": "pages/ota-wifi", "style": { @@ -169,6 +160,20 @@ "path": "team-bow-data" } ] + }, + { + "root": "pages/training", + "pages": [ + { + "path": "index" + }, + { + "path": "difficulty" + }, + { + "path": "practise-one" + } + ] } ] } diff --git a/src/pages/training/components/BowTarget.vue b/src/pages/training/components/BowTarget.vue index fb44324..e2cf77e 100644 --- a/src/pages/training/components/BowTarget.vue +++ b/src/pages/training/components/BowTarget.vue @@ -1,11 +1,14 @@