From 63fb4ae85ad5702685c4dcf743cd739eada35d4d Mon Sep 17 00:00:00 2001 From: zhangyibo95 <690096405@qq.com> Date: Mon, 17 Aug 2026 10:11:28 +0800 Subject: [PATCH] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/audioManager.js | 143 +++++++++++++++--- src/components/BowPower.vue | 26 +++- src/components/ScorePanel.vue | 76 ++++++---- src/components/Timer.vue | 7 +- src/components/TrainingScorePanel.vue | 24 ++- src/matchWebsocket.js | 29 +++- src/pages/first-try.vue | 2 +- src/pages/match-page.vue | 2 +- src/pages/practise-one.vue | 2 +- src/pages/practise-two.vue | 2 +- src/pages/team-battle/components/BowPower.vue | 26 +++- src/pages/training/components/BowTarget.vue | 73 +++++---- src/pages/training/components/ScorePanel.vue | 77 ++++++---- src/pages/training/practise-one.vue | 16 ++ src/websocket.js | 27 ++-- 15 files changed, 395 insertions(+), 137 deletions(-) diff --git a/src/audioManager.js b/src/audioManager.js index 35d9de5..6fb4882 100644 --- a/src/audioManager.js +++ b/src/audioManager.js @@ -112,6 +112,8 @@ export const audioFils = { const AUDIO_LOAD_TIMEOUT_MS = 5000; const AUDIO_WARM_CONCURRENCY = 3; const AUDIO_WARM_RETRIES = 1; +const AUDIO_INSTANCE_LIMIT = 24; +const AUDIO_QUEUE_COMPACT_THRESHOLD = 12; const AUDIO_WARM_PRIORITY_KEYS = [ "点击按钮", "设备连接已断开", @@ -162,6 +164,8 @@ class AudioManager { this.loadingKeyPromises = new Map(); this.pendingPlayKeys = new Set(); this.cacheWritePromises = new Map(); + this.audioLastUsedAt = new Map(); + this.warmPriorityPromise = null; this.warmAllPromise = null; // 连续播放队列相关属性 @@ -253,14 +257,74 @@ class AudioManager { } getWarmupKeys() { - return Array.from( - new Set([...AUDIO_WARM_PRIORITY_KEYS, ...Object.keys(audioFils)]) - ); + return Array.from(new Set(AUDIO_WARM_PRIORITY_KEYS)); + } + + getAllAudioKeys() { + return Object.keys(audioFils); + } + + touchAudioKey(key) { + this.audioLastUsedAt.set(key, Date.now()); + } + + destroyAudioInstance(key) { + const audio = this.audioMap.get(key); + if (!audio) return false; + + this.clearPlayWatchdog(key); + this.audioMap.delete(key); + this.readyMap.delete(key); + this.allowPlayMap.delete(key); + this.audioLastUsedAt.delete(key); + try { + audio.destroy(); + } catch (_) {} + // destroy 可能同步触发 onStop,最后再清一次对应状态。 + this.allowPlayMap.delete(key); + return true; + } + + pruneAudioInstances(preserveKey) { + while (this.audioMap.size >= AUDIO_INSTANCE_LIMIT) { + const candidate = Array.from(this.audioMap.keys()) + .filter( + (key) => + key !== preserveKey && + key !== this.currentPlayingKey && + !this.loadingKeyPromises.has(key) + ) + .sort( + (left, right) => + (this.audioLastUsedAt.get(left) || 0) - + (this.audioLastUsedAt.get(right) || 0) + )[0]; + if (!candidate || !this.destroyAudioInstance(candidate)) return; + } + } + + compactSequenceQueue(force = false) { + if (!this.isSequenceRunning || this.sequenceIndex <= 0) return; + if (!force && this.sequenceIndex < AUDIO_QUEUE_COMPACT_THRESHOLD) return; + this.sequenceQueue = this.sequenceQueue.slice(this.sequenceIndex); + this.sequenceIndex = 0; + } + + getRuntimeStats() { + return { + audioInstanceCount: this.audioMap.size, + loadingAudioCount: this.loadingKeyPromises.size, + pendingPlayCount: this.pendingPlayKeys.size, + sequenceQueueLength: this.isSequenceRunning + ? Math.max(this.sequenceQueue.length - this.sequenceIndex, 0) + : 0, + }; } ensureAudio(key) { if (!audioFils[key]) return Promise.resolve(false); if (this.readyMap.get(key) && this.audioMap.has(key)) { + this.touchAudioKey(key); return Promise.resolve(true); } @@ -312,23 +376,39 @@ class AudioManager { return this.warmKeys(["点击按钮"], { concurrency: 1, retries: 1 }); } - warmAll() { - if (this.warmAllPromise) return this.warmAllPromise; + warmCommon() { + if (this.warmPriorityPromise) return this.warmPriorityPromise; - debugLog("开始后台分级预热全部语音"); + debugLog("开始后台预热常用语音"); this.isLoading = true; - this.warmAllPromise = this.warmKeys(this.getWarmupKeys()) + this.warmPriorityPromise = this.warmKeys(this.getWarmupKeys()) .catch((error) => { - debugLog("后台语音预热异常", error); + debugLog("常用语音预热异常", error); }) .finally(() => { this.isLoading = false; - debugLog("后台语音预热结束", this.getLoadProgress()); + debugLog("常用语音预热结束", this.getRuntimeStats()); + }); + return this.warmPriorityPromise; + } + + warmAll() { + if (this.warmAllPromise) return this.warmAllPromise; + + debugLog("音频测试页开始遍历全部语音"); + this.isLoading = true; + this.warmAllPromise = this.warmKeys(this.getAllAudioKeys()) + .catch((error) => { + debugLog("全部语音遍历异常", error); + }) + .finally(() => { + this.isLoading = false; + debugLog("全部语音遍历结束", this.getRuntimeStats()); }); return this.warmAllPromise; } - // 保留旧入口兼容测试页或其他历史调用,实际行为改为非阻塞后台预热。 + // 音频测试页保留全量遍历;实例池会及时淘汰旧实例,避免同时常驻全部语音。 initAudios() { return this.warmAll(); } @@ -366,6 +446,7 @@ class AudioManager { return; } + this.pruneAudioInstances(key); const audio = uni.createInnerAudioContext(); audio.autoplay = false; try { @@ -377,6 +458,7 @@ class AudioManager { } catch (_) {} this.allowPlayMap.set(key, false); audio.onPlay(() => { + if (this.audioMap.get(key) !== audio) return; if (!this.allowPlayMap.get(key)) { try { audio.stop(); @@ -414,6 +496,7 @@ class AudioManager { } this.readyMap.set(key, false); this.audioMap.delete(key); + this.audioLastUsedAt.delete(key); try { audio.destroy(); } catch (_) {} @@ -446,6 +529,7 @@ class AudioManager { if (!isCurrentGeneration()) { clearLoadTimeout(); this.audioMap.delete(key); + this.audioLastUsedAt.delete(key); try { audio.destroy(); } catch (_) {} @@ -480,6 +564,7 @@ class AudioManager { }); audio.onEnded(() => { + if (this.audioMap.get(key) !== audio) return; this.finishPlayback(key, { advanceSequence: true, emitEnded: true, @@ -487,10 +572,12 @@ class AudioManager { }); audio.onStop(() => { + if (this.audioMap.get(key) !== audio) return; this.finishPlayback(key); }); this.audioMap.set(key, audio); + this.touchAudioKey(key); audio.src = realSrc; }; @@ -700,14 +787,7 @@ class AudioManager { const loadingPromise = this.loadingKeyPromises.get(key); if (loadingPromise) return loadingPromise; - this.clearPlayWatchdog(key); - const oldAudio = this.audioMap.get(key); - if (oldAudio) { - try { - oldAudio.destroy(); - } catch (_) {} - } - this.audioMap.delete(key); + this.destroyAudioInstance(key); this.readyMap.set(key, false); return this.ensureAudio(key); } @@ -754,7 +834,8 @@ class AudioManager { // 不打断当前播放:把新的队列加入到序列中,等待当前播放结束后衔接 if (this.currentPlayingKey) { if (this.isSequenceRunning) { - // 已有序列在跑:直接追加 + // 已有序列在跑:先移除已消费前缀,再追加待播语音。 + this.compactSequenceQueue(true); this.sequenceQueue = this.sequenceQueue.concat(queue); } else { // 没有序列但当前有正在播放的:以当前为序列的起点 @@ -763,6 +844,17 @@ class AudioManager { this.sequenceIndex = 0; // 不触发 _playSingle,等待当前音频自然结束后由 onAudioEnded 接管 } + } else if ( + this.isSequenceRunning && + this.sequenceQueue[this.sequenceIndex] + ) { + // 当前队首正在按需加载时只追加,不能覆盖已经排队的语音。 + this.compactSequenceQueue(true); + this.sequenceQueue = this.sequenceQueue.concat(queue); + const pendingKey = this.sequenceQueue[this.sequenceIndex]; + if (!this.pendingPlayKeys.has(pendingKey)) { + this._playSingle(pendingKey, false); + } } else { // 当前没有播放:直接启动新的序列 this.sequenceQueue = queue; @@ -869,6 +961,7 @@ class AudioManager { this.currentPlayingKey = key; this.lastPlayKey = key; this.lastPlayAt = Date.now(); + this.touchAudioKey(key); this.startPlayWatchdog(key); } else { debugLog(`音频 ${key} 尚未就绪,按需加载后播放...`); @@ -885,7 +978,8 @@ class AudioManager { const nextIndex = this.sequenceIndex + 1; if (nextIndex < this.sequenceQueue.length) { this.sequenceIndex = nextIndex; - const nextKey = this.sequenceQueue[nextIndex]; + this.compactSequenceQueue(); + const nextKey = this.sequenceQueue[this.sequenceIndex]; this._playSingle(nextKey, false); } else { // 队列播放完成 @@ -969,6 +1063,7 @@ class AudioManager { reloadAll() { debugLog("执行 reloadAll: 重置音频实例并后台恢复"); const shouldWarmAll = this.warmAllPromise !== null; + const shouldWarmPriority = this.warmPriorityPromise !== null; this.loadGeneration += 1; // 1. 停止所有播放 @@ -981,6 +1076,7 @@ class AudioManager { } catch (_) {} } this.audioMap.clear(); + this.audioLastUsedAt.clear(); // 3. 重置状态 this.readyMap.clear(); @@ -999,10 +1095,13 @@ class AudioManager { // 4. 重置后台预热状态 this.isLoading = false; + this.warmPriorityPromise = null; this.warmAllPromise = null; - // 5. 之前已启动过全量预热则继续全量恢复,否则只恢复按钮音效。 - return shouldWarmAll ? this.warmAll() : this.warmButton(); + // 5. 按中断前使用范围恢复,产品页不再全量创建语音实例。 + if (shouldWarmAll) return this.warmAll(); + if (shouldWarmPriority) return this.warmCommon(); + return this.warmButton(); } } diff --git a/src/components/BowPower.vue b/src/components/BowPower.vue index bfcfecf..528c1e7 100644 --- a/src/components/BowPower.vue +++ b/src/components/BowPower.vue @@ -4,18 +4,34 @@ import { getDeviceBatteryAPI } from "@/apis"; const power = ref(0); const timer = ref(null); +let disposed = false; +let requestInFlight = false; + +const refreshPower = async () => { + if (disposed || requestInFlight) return; + requestInFlight = true; + try { + const data = await getDeviceBatteryAPI(); + if (!disposed) power.value = data.battery; + } catch (_) { + // 电量轮询失败时等待下一轮,避免产生未处理的 Promise 拒绝。 + } finally { + requestInFlight = false; + } +}; onMounted(async () => { - const data = await getDeviceBatteryAPI(); - power.value = data.battery; - timer.value = setInterval(async () => { - const data = await getDeviceBatteryAPI(); - power.value = data.battery; + await refreshPower(); + if (disposed) return; + timer.value = setInterval(() => { + void refreshPower(); }, 1000 * 10); }); onBeforeUnmount(() => { + disposed = true; clearInterval(timer.value); + timer.value = null; }); diff --git a/src/components/ScorePanel.vue b/src/components/ScorePanel.vue index 12c57e0..d446669 100644 --- a/src/components/ScorePanel.vue +++ b/src/components/ScorePanel.vue @@ -1,5 +1,5 @@