diff --git a/.gitignore b/.gitignore index c91affc..23a9575 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* node_modules .history .github +.claude openspec CLAUDE.md docs diff --git a/src/App.vue b/src/App.vue index 6b3b1f0..b62dd14 100644 --- a/src/App.vue +++ b/src/App.vue @@ -22,7 +22,8 @@ const { updateUser, updateOnline, - clearSessionState + clearSessionState, + clearDevice } = store; watch( @@ -63,6 +64,11 @@ updateOnline(data.online); } + function onDeviceBindInvalid() { + clearDevice(); + uni.setStorageSync("calibration", false); + } + function onDeviceShoot() { // audioManager.play("射箭声音") } @@ -78,6 +84,7 @@ uni.$on("update-user", emitUpdateUser); uni.$on("update-online", emitUpdateOnline); uni.$on("session-kicked-out", onSessionKickedOut); + uni.$on("device-bind-invalid", onDeviceBindInvalid); const token = uni.getStorageSync( `${uni.getAccountInfoSync().miniProgram.envVersion}_token` ); @@ -91,6 +98,7 @@ uni.$off("update-user", emitUpdateUser); uni.$off("update-online", emitUpdateOnline); uni.$off("session-kicked-out", onSessionKickedOut); + uni.$off("device-bind-invalid", onDeviceBindInvalid); websocket.closeWebSocket(); }); diff --git a/src/apis.js b/src/apis.js index 0ce28f2..de645e1 100644 --- a/src/apis.js +++ b/src/apis.js @@ -25,6 +25,7 @@ try { const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot"); +// 统一处理业务接口请求,包含登录态、业务错误和 WiFi 连接空响应兼容。 function request(method, url, data = {}, baseUrl = BASE_URL) { const token = uni.getStorageSync( `${uni.getAccountInfoSync().miniProgram.envVersion}_token` @@ -39,6 +40,10 @@ function request(method, url, data = {}, baseUrl = BASE_URL) { data, timeout: 10000, success: (res) => { + if (url === "/user/hardwareBox/connectWifi" && res.statusCode === 200 && res.data && Object.keys(res.data).length === 0) { + resolve({}); + return; + } if (res.data) { const {code, data, message} = res.data; if (code === 0) resolve(data); @@ -77,6 +82,15 @@ function request(method, url, data = {}, baseUrl = BASE_URL) { resolve({binded: true}); return; } + if (message === "BIND_FAILD") { + uni.$emit("device-bind-invalid"); + uni.showToast({ + title: "设备绑定状态已失效,请重新绑定", + icon: "none", + }); + reject({type: "DEVICE_BIND_INVALID", message}); + return; + } if (message === "ERROR_ORDER_UNPAY") { uni.showToast({ title: "当前有未支付订单", @@ -475,6 +489,26 @@ export const getDeviceBatteryAPI = async () => { return request("GET", "/user/device/battery"); }; +// 设备连接指定 WiFi,只下发 WiFi 凭证,不触发 OTA 升级。 +export const connectDeviceWifiAPI = async (ssid, password) => { + return request("POST", "/user/hardwareBox/connectWifi", {ssid, password}); +}; + +// 获取硬件盒子版本信息,用于判断当前设备是否需要 OTA 升级。 +export const getHardwareBoxVersionAPI = async () => { + return request("GET", "/user/hardwareBox/version"); +}; + +// 发送硬件盒子 OTA 更新指令,服务端会返回后续轮询使用的任务 ID。 +export const sendHardwareBoxUpdateAPI = async (data) => { + return request("POST", "/user/hardwareBox/sendUpdate", data); +}; + +// 根据任务 ID 获取硬件盒子 OTA 更新状态。 +export const getHardwareBoxTaskStatusAPI = async (taskId) => { + return request("GET", `/user/hardwareBox/taskStatus?taskId=${taskId}`); +}; + export const addNoteAPI = async (id, remark) => { return request("POST", "/user/score/sheet/remark", {id, remark}); }; diff --git a/src/audioManager.js b/src/audioManager.js index 3ef733e..156553c 100644 --- a/src/audioManager.js +++ b/src/audioManager.js @@ -133,6 +133,10 @@ class AudioManager { this.lastPlayKey = null; this.lastPlayAt = 0; this.isInterrupted = false; + this.interruptedAt = 0; + this.interruptionFallbackMs = 5000; + this.playWatchdogMs = 8000; + this.playWatchdogTimers = new Map(); // 静音开关 this.isMuted = false; @@ -159,6 +163,7 @@ class AudioManager { const begin = () => { if (this.isInterrupted) return; this.isInterrupted = true; + this.interruptedAt = Date.now(); this.stopAll(); this.isSequenceRunning = false; this.sequenceQueue = []; @@ -170,6 +175,7 @@ class AudioManager { const end = () => { if (!this.isInterrupted) return; this.isInterrupted = false; + this.interruptedAt = 0; uni.$emit(AUDIO_INTERRUPTION_END_EVENT); void this.reloadAll(); }; @@ -352,9 +358,14 @@ class AudioManager { const loadTimeout = setTimeout(() => { debugLog(`音频 ${key} 加载超时`); this.recordLoadFailure(key); + this.audioMap.delete(key); try { audio.destroy(); } catch (_) {} + this.finishPlayback(key, { + advanceSequence: true, + emitEnded: true, + }); if (callback) callback(); }, 10000); @@ -388,7 +399,13 @@ class AudioManager { } this.recordLoadFailure(key); this.audioMap.delete(key); - audio.destroy(); + try { + audio.destroy(); + } catch (_) {} + this.finishPlayback(key, { + advanceSequence: true, + emitEnded: true, + }); if (this.readyMap.get(key)) { // 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载 // this.readyMap.set(key, false); @@ -398,19 +415,14 @@ class AudioManager { }); audio.onEnded(() => { - if (this.currentPlayingKey === key) { - this.currentPlayingKey = null; - } - this.allowPlayMap.set(key, false); - this.onAudioEnded(key); - uni.$emit('audioEnded', key); + this.finishPlayback(key, { + advanceSequence: true, + emitEnded: true, + }); }); audio.onStop(() => { - if (this.currentPlayingKey === key) { - this.currentPlayingKey = null; - } - this.allowPlayMap.set(key, false); + this.finishPlayback(key); }); this.audioMap.set(key, audio); @@ -448,11 +460,19 @@ class AudioManager { }); } else { this.recordLoadFailure(key); + this.finishPlayback(key, { + advanceSequence: true, + emitEnded: true, + }); if (callback) callback(); } }, fail: () => { this.recordLoadFailure(key); + this.finishPlayback(key, { + advanceSequence: true, + emitEnded: true, + }); if (callback) callback(); }, }); @@ -489,15 +509,137 @@ class AudioManager { this.failedLoadKeys.add(key); } + clearPlayWatchdog(key) { + const timer = this.playWatchdogTimers.get(key); + if (timer) { + clearTimeout(timer); + this.playWatchdogTimers.delete(key); + } + } + + clearAllPlayWatchdogs() { + for (const timer of this.playWatchdogTimers.values()) { + clearTimeout(timer); + } + this.playWatchdogTimers.clear(); + } + + startPlayWatchdog(key) { + this.clearPlayWatchdog(key); + const timer = setTimeout(() => { + if (this.currentPlayingKey !== key) return; + debugLog(`音频 ${key} 播放超时,跳过当前音频并继续队列`); + this.finishPlayback(key, { + advanceSequence: true, + emitEnded: true, + force: true, + }); + this.reloadAudioKey(key); + }, this.playWatchdogMs); + this.playWatchdogTimers.set(key, timer); + } + + finishPlayback(key, { advanceSequence = false, emitEnded = false, force = false } = {}) { + const wasCurrent = this.currentPlayingKey === key; + const isSequenceCurrent = + this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key; + + this.clearPlayWatchdog(key); + this.allowPlayMap.set(key, false); + + if (!force && !wasCurrent && !isSequenceCurrent) return false; + + if (wasCurrent) { + this.currentPlayingKey = null; + } + + if (advanceSequence && isSequenceCurrent) { + this.onAudioEnded(key); + } + + if (emitEnded) { + uni.$emit("audioEnded", key); + } + + return true; + } + + recoverFromInterruptionIfStale(force = false) { + if (!this.isInterrupted) return false; + const interruptedFor = Date.now() - (this.interruptedAt || Date.now()); + if (!force && interruptedFor < this.interruptionFallbackMs) return false; + + debugLog("音频中断状态超时,执行兜底恢复"); + this.isInterrupted = false; + this.interruptedAt = 0; + uni.$emit(AUDIO_INTERRUPTION_END_EVENT); + void this.reloadAll(); + return true; + } + + recoverIfStale(expectedKey) { + if (this.recoverFromInterruptionIfStale(true)) return; + + const key = + expectedKey || this.currentPlayingKey || this.sequenceQueue[this.sequenceIndex]; + if (!key) { + if (this.isSequenceRunning) { + this.sequenceQueue = []; + this.sequenceIndex = 0; + this.isSequenceRunning = false; + } + return; + } + + const isStaleCurrent = + this.currentPlayingKey === key || + (this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key); + if (!isStaleCurrent) return; + + debugLog(`音频 ${key} 等待超时,执行轻量恢复`); + const audio = this.audioMap.get(key); + if (audio) { + try { + audio.stop(); + } catch (_) {} + } + this.finishPlayback(key, { + advanceSequence: true, + emitEnded: true, + force: true, + }); + this.reloadAudioKey(key); + } + + reloadAudioKey(key) { + const audio = this.audioMap.get(key); + if (audio) { + try { + audio.destroy(); + } catch (_) {} + this.audioMap.delete(key); + } + this.readyMap.set(key, false); + this.retryLoadAudio(key); + } + // 重新加载音频 retryLoadAudio(key) { + this.clearPlayWatchdog(key); const oldAudio = this.audioMap.get(key); - if (oldAudio) oldAudio.destroy(); + if (oldAudio) { + try { + oldAudio.destroy(); + } catch (_) {} + } this.createAudio(key); } // 播放指定音频或音频数组(数组则按顺序连续播放) play(input, interrupt = true) { + if (this.isInterrupted) { + this.recoverFromInterruptionIfStale(); + } if (this.isInterrupted) { debugLog("音频处理中断状态,忽略播放请求"); return; @@ -555,6 +697,9 @@ class AudioManager { // 内部方法:播放单个 key _playSingle(key, forceStopAll = false) { + if (this.isInterrupted) { + this.recoverFromInterruptionIfStale(); + } if (this.isInterrupted) { debugLog(`音频处理中断状态,跳过播放: ${key}`); return; @@ -563,6 +708,11 @@ class AudioManager { const now = Date.now(); if (this.lastPlayKey === key && now - this.lastPlayAt < 250) { debugLog(`忽略快速重复播放: ${key}`); + this.finishPlayback(key, { + advanceSequence: true, + emitEnded: true, + force: true, + }); return; } @@ -605,21 +755,34 @@ class AudioManager { try { audio.play(); } catch (err) { - this.allowPlayMap.set(key, false); + this.finishPlayback(key, { + advanceSequence: true, + emitEnded: true, + force: true, + }); debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err); return; } this.currentPlayingKey = key; this.lastPlayKey = key; this.lastPlayAt = Date.now(); + this.startPlayWatchdog(key); } else { debugLog(`音频 ${key} 不存在,尝试重新加载...`); this.retryLoadAudio(key); + let loadWaitTimer = null; + const cleanup = () => { + try { + uni.$off("audioLoaded", handler); + } catch (_) {} + if (loadWaitTimer) { + clearTimeout(loadWaitTimer); + loadWaitTimer = null; + } + }; const handler = (loadedKey) => { if (loadedKey === key) { - try { - uni.$off("audioLoaded", handler); - } catch (_) {} + cleanup(); // 再次校验是否存在且就绪 const a = this.audioMap.get(key); if (a && this.readyMap.get(key)) { @@ -630,6 +793,7 @@ class AudioManager { try { uni.$on("audioLoaded", handler); } catch (_) {} + loadWaitTimer = setTimeout(cleanup, 12000); } } @@ -655,6 +819,7 @@ class AudioManager { // 停止指定音频 stop(key) { const audio = this.audioMap.get(key); + this.clearPlayWatchdog(key); if (audio) { audio.stop(); this.allowPlayMap.set(key, false); @@ -666,6 +831,7 @@ class AudioManager { // 停止所有音频 stopAll() { + this.clearAllPlayWatchdogs(); for (const [k, audio] of this.audioMap.entries()) { try { audio.stop(); @@ -739,6 +905,7 @@ class AudioManager { this.readyMap.clear(); this.failedLoadKeys.clear(); this.allowPlayMap.clear(); + this.clearAllPlayWatchdogs(); this.currentPlayingKey = null; this.sequenceQueue = []; this.sequenceIndex = 0; diff --git a/src/components/OtaModal.vue b/src/components/OtaModal.vue new file mode 100644 index 0000000..c08ce2c --- /dev/null +++ b/src/components/OtaModal.vue @@ -0,0 +1,429 @@ + + + + + diff --git a/src/components/PlayerScore.vue b/src/components/PlayerScore.vue index 6a91367..f32c431 100644 --- a/src/components/PlayerScore.vue +++ b/src/components/PlayerScore.vue @@ -16,6 +16,11 @@ const props = defineProps({ const rowCount = new Array(6).fill(0); +const getRingText = (arrow) => { + if (!arrow) return "-"; + if (arrow.ringX && arrow.ring) return "X环"; + return arrow.ring ? `${arrow.ring}环` : "-"; +}; const isMember = (player = {}) => player.vip === true || player.sVip === true; const getMemberNicknameClass = (player = {}) => [ @@ -52,23 +57,19 @@ const getMemberNicknameClass = (player = {}) => [ - {{ - scores[0] && scores[0][index] ? `${scores[0][index].ring}环` : "-" - }} + {{ getRingText(scores[0]?.[index]) }} - {{ - scores[1] && scores[1][index] ? `${scores[1][index].ring}环` : "-" - }} + {{ getRingText(scores[1]?.[index]) }} {{ scores - .map((s) => s.reduce((last, next) => last + next.ring, 0)) + .map((s) => (s || []).reduce((last, next) => last + next.ring, 0)) .reduce((last, next) => last + next, 0) }}环 diff --git a/src/components/Signin.vue b/src/components/Signin.vue index 53ee292..c1d9ea7 100644 --- a/src/components/Signin.vue +++ b/src/components/Signin.vue @@ -16,7 +16,7 @@ import { import useStore from "@/store"; const store = useStore(); -const { updateUser, updateDevice, updateOnline } = store; +const { updateUser, updateDevice, updateOnline, clearDevice } = store; const props = defineProps({ show: { @@ -122,6 +122,8 @@ async function doLogin() { ); const data = await getDeviceBatteryAPI(); updateOnline(data.online); + } else { + clearDevice(); } props.onClose(); } catch (error) { diff --git a/src/manifest.json b/src/manifest.json index c0fc1eb..425ffa4 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -64,7 +64,11 @@ "usingComponents" : true, "darkmode" : true, "themeLocation" : "theme.json", - "permission" : {}, + "permission" : { + "scope.userLocation": { + "desc": "用于扫描附近 WiFi,完成设备 OTA 升级网络连接" + } + }, "requiredPrivateInfos" : [ "getLocation", "chooseLocation" ] } } diff --git a/src/pages.json b/src/pages.json index 5547f22..400bd40 100644 --- a/src/pages.json +++ b/src/pages.json @@ -125,6 +125,12 @@ }, { "path": "pages/mine-bow-data" + }, + { + "path": "pages/ota-wifi", + "style": { + "navigationStyle": "custom" + } } ], "globalStyle": { diff --git a/src/pages/friend-battle-result.vue b/src/pages/friend-battle-result.vue index d290a7f..4139afd 100644 --- a/src/pages/friend-battle-result.vue +++ b/src/pages/friend-battle-result.vue @@ -315,7 +315,7 @@ function goBack() { diff --git a/src/pages/index.vue b/src/pages/index.vue index bedd679..b3035ee 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -1,19 +1,23 @@ @@ -299,6 +327,11 @@ onShow(() => { >解绑 + + 设备连接WIFI + diff --git a/src/pages/ota-wifi.vue b/src/pages/ota-wifi.vue new file mode 100644 index 0000000..c787110 --- /dev/null +++ b/src/pages/ota-wifi.vue @@ -0,0 +1,1261 @@ + + + + + diff --git a/src/pages/practise-one.vue b/src/pages/practise-one.vue index 8e7376d..a779994 100644 --- a/src/pages/practise-one.vue +++ b/src/pages/practise-one.vue @@ -32,7 +32,7 @@ const start = ref(false); const scores = ref([]); const isSvip = ref(false); const total = 12; -/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */ +/** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */ const xRingStreak = ref(0); const practiseResult = ref({}); const practiseId = ref(""); @@ -62,19 +62,23 @@ const onOver = async () => { }; /** - * 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效 - * @param {boolean} isXRing - 本次射击是否为 X 环 + * 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效 + * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上 */ -function checkAndPlayTententen(isXRing) { - if (isXRing) { +function isTenPlusRing(shot) { + return !!(shot?.ringX || Number(shot?.ring) >= 10); +} + +function checkAndPlayTententen(isTenPlusRingShot) { + if (isTenPlusRingShot) { xRingStreak.value += 1; - // 连续 3 箭均为 X 环,在环数播报入队后追加 tententen,避免播放顺序颠倒 + // 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen,避免播放顺序颠倒 if (xRingStreak.value >= 3) { xRingStreak.value = 0; nextTick(() => audioManager.play("tententen", false)); } } else { - // 非 X 环则重置连续计数 + // 低于 10 环或未上靶则重置连续计数 xRingStreak.value = 0; } } @@ -84,10 +88,10 @@ async function onReceiveMessage(msg) { const prevLen = scores.value.length; isSvip.value = msg.sVip === true; scores.value = msg.details; - // 有新箭时取最后一箭判断是否 X 环并检测连续计数 + // 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数 if (scores.value.length > prevLen) { const latestArrow = scores.value[scores.value.length - 1]; - checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring)); + checkAndPlayTententen(isTenPlusRing(latestArrow)); } } else if (msg.type === MESSAGETYPESV2.BattleEnd) { // setTimeout(onOver, 1500); diff --git a/src/pages/practise-two.vue b/src/pages/practise-two.vue index 794e794..7f6cac4 100644 --- a/src/pages/practise-two.vue +++ b/src/pages/practise-two.vue @@ -32,7 +32,7 @@ const start = ref(false); const scores = ref([]); const isSvip = ref(false); const total = 36; -/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */ +/** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */ const xRingStreak = ref(0); const practiseResult = ref({}); const practiseId = ref(""); @@ -61,19 +61,23 @@ const onOver = async () => { }; /** - * 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效 - * @param {boolean} isXRing - 本次射击是否为 X 环 + * 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效 + * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上 */ -function checkAndPlayTententen(isXRing) { - if (isXRing) { +function isTenPlusRing(shot) { + return !!(shot?.ringX || Number(shot?.ring) >= 10); +} + +function checkAndPlayTententen(isTenPlusRingShot) { + if (isTenPlusRingShot) { xRingStreak.value += 1; - // 连续 3 箭均为 X 环,在环数播报入队后追加 tententen,避免播放顺序颠倒 + // 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen,避免播放顺序颠倒 if (xRingStreak.value >= 3) { xRingStreak.value = 0; nextTick(() => audioManager.play("tententen", false)); } } else { - // 非 X 环则重置连续计数 + // 低于 10 环或未上靶则重置连续计数 xRingStreak.value = 0; } } @@ -83,10 +87,10 @@ async function onReceiveMessage(msg) { const prevLen = scores.value.length; isSvip.value = msg.sVip === true; scores.value = msg.details; - // 有新箭时取最后一箭判断是否 X 环并检测连续计数 + // 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数 if (scores.value.length > prevLen) { const latestArrow = scores.value[scores.value.length - 1]; - checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring)); + checkAndPlayTententen(isTenPlusRing(latestArrow)); } } else if (msg.type === MESSAGETYPESV2.BattleEnd) { setTimeout(onOver, 1500); diff --git a/src/pages/team-battle.vue b/src/pages/team-battle.vue index 466319d..1a0d802 100644 --- a/src/pages/team-battle.vue +++ b/src/pages/team-battle.vue @@ -49,7 +49,7 @@ const battleWay = ref(0); const lastToSomeoneShootKey = ref(""); /** 控制设备离线提示弹窗的显示状态 */ const showOfflineModal = ref(false); -/** 记录每位玩家当前轮连续 X 环数,key 为 playerId,用于触发 tententen 音效 */ +/** 记录每位玩家当前轮连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */ const xRingStreaks = ref({}); /** @@ -234,22 +234,26 @@ function onNewRound(msg, prevRound) { } /** - * 检测指定射手连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效 + * 检测指定射手连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效 * @param {number} shooterId - 本次射手的 ID(取自 currentShooterId.value) - * @param {boolean} isXRing - 本次射击是否为 X 环 + * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上 */ -function checkAndPlayTententen(shooterId, isXRing) { +function isTenPlusRing(shot) { + return !!(shot?.ringX || Number(shot?.ring) >= 10); +} + +function checkAndPlayTententen(shooterId, isTenPlusRingShot) { if (!shooterId) return; - if (isXRing) { + if (isTenPlusRingShot) { xRingStreaks.value[shooterId] = (xRingStreaks.value[shooterId] || 0) + 1; - // 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放 + // 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放 if (xRingStreaks.value[shooterId] >= 3) { xRingStreaks.value[shooterId] = 0; // nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒 nextTick(() => audioManager.play("tententen", false)); } } else { - // 非 X 环则重置该玩家的连续计数 + // 低于 10 环或未上靶则重置该玩家的连续计数 xRingStreaks.value[shooterId] = 0; } } @@ -268,9 +272,9 @@ async function onReceiveMessage(msg) { } else if (msg.type === MESSAGETYPESV2.ShootResult) { showRoundTip.value = false; recoverData(msg, {arrowOnly: true}); - // 检测同一玩家三箭全 X 环,触发 tententen 音效 + // 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效 // currentShooterId 在 ToSomeoneShoot 时写入,ShootResult 不会覆盖,可靠识别本次射手 - checkAndPlayTententen(currentShooterId.value, !!(msg.shootData?.ringX && msg.shootData?.ring)); + checkAndPlayTententen(currentShooterId.value, isTenPlusRing(msg.shootData)); } else if (msg.type === MESSAGETYPESV2.NewRound) { // 在进入延迟前先捕获当前轮次,供 onNewRound 使用,防止 800ms 内 ToSomeoneShoot 提前更新 currentRound 造成 Tip 展示错轮 const prevRound = currentRound.value; diff --git a/src/pages/team-battle/index.vue b/src/pages/team-battle/index.vue index 08d4099..79b7dd4 100644 --- a/src/pages/team-battle/index.vue +++ b/src/pages/team-battle/index.vue @@ -432,7 +432,10 @@ function playAudioKeys(keys, { interrupt = false, timeout } = {}) { resolve(); }, }; - const timer = setTimeout(waiter.done, waitTime); + const timer = setTimeout(() => { + audioManager.recoverIfStale(expectedKey); + waiter.done(); + }, waitTime); audioWaiters.add(waiter); audioManager.play(audioKeys, interrupt); }); @@ -473,17 +476,14 @@ function updateTeams(battleInfo) { } function updateGoldenRound(battleInfo) { - const rounds = Array.isArray(battleInfo?.rounds) ? battleInfo.rounds : []; - const currentRoundNo = Number(battleInfo?.current?.round || 0); - const currentRoundInfo = rounds.find((round) => Number(round?.round) === currentRoundNo); - const activeGoldRoundInfo = rounds.find( - (round) => Number(round?.goldRound || 0) > 0 && round?.status === 1 - ); - const roundGoldRound = Number(currentRoundInfo?.goldRound || 0); - const activeGoldRound = Number(activeGoldRoundInfo?.goldRound || 0); - const currentGoldRound = Number(battleInfo?.current?.goldRound || 0); - const nextGoldRound = roundGoldRound || activeGoldRound || currentGoldRound; - goldenRound.value = nextGoldRound > 0 ? nextGoldRound : 0; + if (!battleInfo?.current?.goldRound) { + goldenRound.value = 0; + return; + } + const rounds = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : []; + const finishedGoldCount = rounds.filter((round) => !!round?.ifGold).length; + // goldenRound.value = Math.max(1, finishedGoldCount + (battleInfo.current?.playerId ? 1 : 0)); + goldenRound.value = Math.max(1, finishedGoldCount); } // Restore an info snapshot whose eventType points at the NewRound phase. @@ -861,10 +861,14 @@ async function runToSomeoneShootTask(task, runId) { }); } -function updateXRingStreak(shooterId, isXRing) { +function isTenPlusRing(shot) { + return !!(shot?.ringX || Number(shot?.ring) >= 10); +} + +function updateXRingStreak(shooterId, isTenPlusRingShot) { if (!shooterId) return false; const id = String(shooterId); - if (!isXRing) { + if (!isTenPlusRingShot) { xRingStreaks.value[id] = 0; saveXRingStreaks(); return false; @@ -909,7 +913,7 @@ async function runShootResultTask(task) { const isTententen = updateXRingStreak( currentShooterId.value, - !!(battleInfo.shootData?.ringX && battleInfo.shootData?.ring) + isTenPlusRing(battleInfo.shootData) ); const audioKeys = buildShootResultAudioKeys(battleInfo.shootData); if (isTententen) audioKeys.push("tententen"); @@ -1255,7 +1259,7 @@ onShow(() => { 设备已离线 检测到设备已断开连接,请检查设备后继续比赛 - 我知道了 + 我知道了 diff --git a/src/static/ota/check-char.png b/src/static/ota/check-char.png new file mode 100644 index 0000000..d262047 Binary files /dev/null and b/src/static/ota/check-char.png differ diff --git a/src/static/ota/close-char.png b/src/static/ota/close-char.png new file mode 100644 index 0000000..a563bb1 Binary files /dev/null and b/src/static/ota/close-char.png differ diff --git a/src/static/ota/new-ver.png b/src/static/ota/new-ver.png new file mode 100644 index 0000000..c6c5e5f Binary files /dev/null and b/src/static/ota/new-ver.png differ diff --git a/src/static/ota/ota-bg.png b/src/static/ota/ota-bg.png new file mode 100644 index 0000000..56635f6 Binary files /dev/null and b/src/static/ota/ota-bg.png differ diff --git a/src/static/ota/ota-mascot.png b/src/static/ota/ota-mascot.png new file mode 100644 index 0000000..2dac0a6 Binary files /dev/null and b/src/static/ota/ota-mascot.png differ diff --git a/src/static/ota/ota-ver.png b/src/static/ota/ota-ver.png new file mode 100644 index 0000000..a34a678 Binary files /dev/null and b/src/static/ota/ota-ver.png differ diff --git a/src/static/ota/target-char.png b/src/static/ota/target-char.png new file mode 100644 index 0000000..073cafb Binary files /dev/null and b/src/static/ota/target-char.png differ diff --git a/src/static/ota/update-fail.png b/src/static/ota/update-fail.png new file mode 100644 index 0000000..754b226 Binary files /dev/null and b/src/static/ota/update-fail.png differ diff --git a/src/static/ota/update-ok.png b/src/static/ota/update-ok.png new file mode 100644 index 0000000..edaae5f Binary files /dev/null and b/src/static/ota/update-ok.png differ diff --git a/src/static/ota/update_progress.png b/src/static/ota/update_progress.png new file mode 100644 index 0000000..510d8d2 Binary files /dev/null and b/src/static/ota/update_progress.png differ diff --git a/src/static/ota/wifi1.png b/src/static/ota/wifi1.png new file mode 100644 index 0000000..6931b7b Binary files /dev/null and b/src/static/ota/wifi1.png differ diff --git a/src/static/ota/wifi2.png b/src/static/ota/wifi2.png new file mode 100644 index 0000000..e93d538 Binary files /dev/null and b/src/static/ota/wifi2.png differ diff --git a/src/static/sicon/arrow-left.png b/src/static/sicon/arrow-left.png new file mode 100644 index 0000000..5b8dd15 Binary files /dev/null and b/src/static/sicon/arrow-left.png differ diff --git a/src/static/sicon/cancel.png b/src/static/sicon/cancel.png new file mode 100644 index 0000000..4149c11 Binary files /dev/null and b/src/static/sicon/cancel.png differ diff --git a/src/static/sicon/check.png b/src/static/sicon/check.png new file mode 100644 index 0000000..abb70ce Binary files /dev/null and b/src/static/sicon/check.png differ diff --git a/src/static/sicon/close.png b/src/static/sicon/close.png new file mode 100644 index 0000000..ea537f8 Binary files /dev/null and b/src/static/sicon/close.png differ diff --git a/src/static/sicon/eye-off.png b/src/static/sicon/eye-off.png new file mode 100644 index 0000000..d69b3fc Binary files /dev/null and b/src/static/sicon/eye-off.png differ diff --git a/src/static/sicon/eye-on.png b/src/static/sicon/eye-on.png new file mode 100644 index 0000000..f5e1027 Binary files /dev/null and b/src/static/sicon/eye-on.png differ diff --git a/src/static/sicon/pwd.png b/src/static/sicon/pwd.png new file mode 100644 index 0000000..6d12fa9 Binary files /dev/null and b/src/static/sicon/pwd.png differ diff --git a/src/static/sicon/refresh.png b/src/static/sicon/refresh.png new file mode 100644 index 0000000..84403c3 Binary files /dev/null and b/src/static/sicon/refresh.png differ diff --git a/src/static/sicon/target_icon.png b/src/static/sicon/target_icon.png new file mode 100644 index 0000000..9ef9893 Binary files /dev/null and b/src/static/sicon/target_icon.png differ diff --git a/src/static/sicon/tick.png b/src/static/sicon/tick.png new file mode 100644 index 0000000..9de986d Binary files /dev/null and b/src/static/sicon/tick.png differ diff --git a/src/static/sicon/wifi.png b/src/static/sicon/wifi.png new file mode 100644 index 0000000..eeea5e2 Binary files /dev/null and b/src/static/sicon/wifi.png differ diff --git a/src/store.js b/src/store.js index 14f2205..45a4510 100644 --- a/src/store.js +++ b/src/store.js @@ -149,6 +149,10 @@ export default defineStore("store", { this.device.deviceId = deviceId; this.device.deviceName = deviceName; }, + clearDevice() { + this.device = getDefaultDevice(); + this.online = false; + }, async updateConfig(config) { this.config = config; if (this.user.scores !== undefined) {