update:语音加载优化,修复大乱斗未重连比赛服问题
This commit is contained in:
@@ -108,6 +108,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
|
void audioManager.warmButton();
|
||||||
uni.$on("update-user", emitUpdateUser);
|
uni.$on("update-user", emitUpdateUser);
|
||||||
uni.$on("update-online", emitUpdateOnline);
|
uni.$on("update-online", emitUpdateOnline);
|
||||||
uni.$on("session-kicked-out", onSessionKickedOut);
|
uni.$on("session-kicked-out", onSessionKickedOut);
|
||||||
|
|||||||
+307
-247
@@ -95,6 +95,28 @@ export const audioFils = {
|
|||||||
"https://static.shelingxingqiu.com/shootaudio/%E5%91%BD%E4%B8%AD.mp3"
|
"https://static.shelingxingqiu.com/shootaudio/%E5%91%BD%E4%B8%AD.mp3"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AUDIO_LOAD_TIMEOUT_MS = 5000;
|
||||||
|
const AUDIO_WARM_CONCURRENCY = 3;
|
||||||
|
const AUDIO_WARM_RETRIES = 1;
|
||||||
|
const AUDIO_WARM_PRIORITY_KEYS = [
|
||||||
|
"点击按钮",
|
||||||
|
"比赛开始",
|
||||||
|
"练习开始",
|
||||||
|
"请开始射击",
|
||||||
|
"轮到你了",
|
||||||
|
"比赛结束",
|
||||||
|
"射击无效",
|
||||||
|
"中场休息",
|
||||||
|
"下半场开始",
|
||||||
|
"决金箭轮",
|
||||||
|
"请蓝方射箭",
|
||||||
|
"请红方射箭",
|
||||||
|
"距离合格",
|
||||||
|
"距离不足",
|
||||||
|
"未上靶",
|
||||||
|
"X环",
|
||||||
|
];
|
||||||
|
|
||||||
// 版本控制日志函数
|
// 版本控制日志函数
|
||||||
function debugLog(...args) {
|
function debugLog(...args) {
|
||||||
// 获取当前环境信息
|
// 获取当前环境信息
|
||||||
@@ -111,18 +133,15 @@ class AudioManager {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.audioMap = new Map();
|
this.audioMap = new Map();
|
||||||
this.currentPlayingKey = null;
|
this.currentPlayingKey = null;
|
||||||
this.maxRetries = 3;
|
|
||||||
// 多轮统一重试:最多重试的轮次与每轮间隔
|
|
||||||
this.maxRetryRounds = 10;
|
|
||||||
this.retryRoundIntervalMs = 1500;
|
|
||||||
// 显式授权播放标记,防止 iOS 在设置 src 后误播
|
// 显式授权播放标记,防止 iOS 在设置 src 后误播
|
||||||
this.allowPlayMap = new Map();
|
this.allowPlayMap = new Map();
|
||||||
|
|
||||||
// 串行加载相关属性
|
// 后台预热与单条加载状态
|
||||||
this.audioKeys = [];
|
|
||||||
this.currentLoadingIndex = 0;
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.loadingPromise = null;
|
this.loadingKeyPromises = new Map();
|
||||||
|
this.pendingPlayKeys = new Set();
|
||||||
|
this.cacheWritePromises = new Map();
|
||||||
|
this.warmAllPromise = null;
|
||||||
|
|
||||||
// 连续播放队列相关属性
|
// 连续播放队列相关属性
|
||||||
this.sequenceQueue = [];
|
this.sequenceQueue = [];
|
||||||
@@ -141,19 +160,14 @@ class AudioManager {
|
|||||||
// 静音开关
|
// 静音开关
|
||||||
this.isMuted = false;
|
this.isMuted = false;
|
||||||
this.pendingPlayKey = null;
|
this.pendingPlayKey = null;
|
||||||
// 新增:就绪状态映射
|
|
||||||
this.readyMap = new Map();
|
this.readyMap = new Map();
|
||||||
// 新增:首轮失败的音频集合与重试阶段标识
|
// 加载代数,用于 reloadAll 时作废旧的异步加载
|
||||||
this.failedLoadKeys = new Set();
|
|
||||||
// 加载代数,用于 reloadAll 时作废旧的加载循环
|
|
||||||
this.loadGeneration = 0;
|
this.loadGeneration = 0;
|
||||||
// 本地路径缓存 Map: { url: localPath }
|
// 本地路径缓存 Map: { url: localPath }
|
||||||
this.localFileCache = uni.getStorageSync("audio_local_files") || {};
|
this.localFileCache = uni.getStorageSync("audio_local_files") || {};
|
||||||
// 启动时自动清理过期的缓存文件(URL 已不在 audioFils 中的文件)
|
// 启动时自动清理过期的缓存文件(URL 已不在 audioFils 中的文件)
|
||||||
this.cleanObsoleteCache();
|
this.cleanObsoleteCache();
|
||||||
this.bindAudioInterruptionEvents();
|
this.bindAudioInterruptionEvents();
|
||||||
|
|
||||||
this.initAudios();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bindAudioInterruptionEvents() {
|
bindAudioInterruptionEvents() {
|
||||||
@@ -217,128 +231,122 @@ class AudioManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化音频(两阶段:首轮串行加载全部,次轮仅串行加载失败项一次)
|
getWarmupKeys() {
|
||||||
initAudios() {
|
return Array.from(
|
||||||
if (this.isLoading) {
|
new Set([...AUDIO_WARM_PRIORITY_KEYS, ...Object.keys(audioFils)])
|
||||||
debugLog("音频正在加载中,跳过重复初始化");
|
);
|
||||||
return this.loadingPromise;
|
|
||||||
}
|
|
||||||
debugLog("开始串行加载音频...");
|
|
||||||
this.isLoading = true;
|
|
||||||
this.audioKeys = Object.keys(audioFils);
|
|
||||||
this.currentLoadingIndex = 0;
|
|
||||||
this.failedLoadKeys.clear();
|
|
||||||
|
|
||||||
// 增加代数,使得旧的加载循环失效
|
|
||||||
this.loadGeneration = (this.loadGeneration || 0) + 1;
|
|
||||||
const currentGen = this.loadGeneration;
|
|
||||||
|
|
||||||
this.loadingPromise = new Promise((resolve) => {
|
|
||||||
const finalize = () => {
|
|
||||||
if (currentGen !== this.loadGeneration) return;
|
|
||||||
const runRounds = (round) => {
|
|
||||||
if (currentGen !== this.loadGeneration) return;
|
|
||||||
// 达到最大轮次或没有失败项,收尾
|
|
||||||
if (this.failedLoadKeys.size === 0 || round > this.maxRetryRounds) {
|
|
||||||
this.isLoading = false;
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const retryKeys = Array.from(this.failedLoadKeys);
|
|
||||||
this.failedLoadKeys.clear();
|
|
||||||
debugLog(`开始第 ${round} 轮串行加载,共 ${retryKeys.length} 个`);
|
|
||||||
|
|
||||||
this.loadKeysSequentially(
|
|
||||||
retryKeys,
|
|
||||||
() => {
|
|
||||||
if (currentGen !== this.loadGeneration) return;
|
|
||||||
// 如仍有失败项,继续下一轮;否则结束
|
|
||||||
if (this.failedLoadKeys.size > 0 && round < this.maxRetryRounds) {
|
|
||||||
setTimeout(
|
|
||||||
() => runRounds(round + 1),
|
|
||||||
this.retryRoundIntervalMs
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.isLoading = false;
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
currentGen
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 启动第 1 轮重试(如有失败项)
|
|
||||||
runRounds(1);
|
|
||||||
};
|
|
||||||
this.loadNextAudio(finalize, currentGen);
|
|
||||||
});
|
|
||||||
return this.loadingPromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按自定义列表串行加载音频(避免并发过多)
|
ensureAudio(key) {
|
||||||
loadKeysSequentially(keys, onComplete, gen) {
|
if (!audioFils[key]) return Promise.resolve(false);
|
||||||
if (gen !== undefined && gen !== this.loadGeneration) return;
|
if (this.readyMap.get(key) && this.audioMap.has(key)) {
|
||||||
let idx = 0;
|
return Promise.resolve(true);
|
||||||
const list = Array.from(keys);
|
}
|
||||||
const next = () => {
|
|
||||||
if (gen !== undefined && gen !== this.loadGeneration) return;
|
|
||||||
if (idx >= list.length) {
|
|
||||||
if (onComplete) onComplete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const k = list[idx++];
|
|
||||||
|
|
||||||
// 已就绪的音频不再重载,避免把 ready 状态重置为 false
|
const loadingPromise = this.loadingKeyPromises.get(key);
|
||||||
if (this.readyMap.get(k)) {
|
if (loadingPromise) return loadingPromise;
|
||||||
setTimeout(next, 50);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未就绪:已存在则重载;不存在则创建
|
let promise;
|
||||||
if (this.audioMap.has(k)) {
|
promise = new Promise((resolve) => {
|
||||||
this.retryLoadAudio(k);
|
try {
|
||||||
setTimeout(next, 100);
|
this.createAudio(key, () => {
|
||||||
} else {
|
resolve(this.readyMap.get(key) === true && this.audioMap.has(key));
|
||||||
this.createAudio(k, () => {
|
|
||||||
setTimeout(next, 100);
|
|
||||||
});
|
});
|
||||||
return; // createAudio 内部会触发 next
|
} catch (error) {
|
||||||
|
debugLog(`音频 ${key} 创建失败`, error);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
if (this.loadingKeyPromises.get(key) === promise) {
|
||||||
|
this.loadingKeyPromises.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.loadingKeyPromises.set(key, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async warmKeys(
|
||||||
|
keys,
|
||||||
|
{ concurrency = AUDIO_WARM_CONCURRENCY, retries = AUDIO_WARM_RETRIES } = {}
|
||||||
|
) {
|
||||||
|
const queue = Array.from(new Set(keys)).filter((key) => !!audioFils[key]);
|
||||||
|
if (!queue.length) return;
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
const worker = async () => {
|
||||||
|
while (index < queue.length) {
|
||||||
|
const key = queue[index++];
|
||||||
|
let ready = await this.ensureAudio(key);
|
||||||
|
for (let attempt = 0; !ready && attempt < retries; attempt += 1) {
|
||||||
|
ready = await this.retryLoadAudio(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
next();
|
|
||||||
|
const workerCount = Math.min(Math.max(1, concurrency), queue.length);
|
||||||
|
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 串行加载下一个音频(首轮)
|
warmButton() {
|
||||||
loadNextAudio(onComplete, gen) {
|
return this.warmKeys(["点击按钮"], { concurrency: 1, retries: 1 });
|
||||||
if (gen !== undefined && gen !== this.loadGeneration) return;
|
}
|
||||||
if (this.currentLoadingIndex >= this.audioKeys.length) {
|
|
||||||
debugLog("首轮加载遍历完成", this.currentLoadingIndex);
|
warmAll() {
|
||||||
if (onComplete) onComplete();
|
if (this.warmAllPromise) return this.warmAllPromise;
|
||||||
|
|
||||||
|
debugLog("开始后台分级预热全部语音");
|
||||||
|
this.isLoading = true;
|
||||||
|
this.warmAllPromise = this.warmKeys(this.getWarmupKeys())
|
||||||
|
.catch((error) => {
|
||||||
|
debugLog("后台语音预热异常", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
debugLog("后台语音预热结束", this.getLoadProgress());
|
||||||
|
});
|
||||||
|
return this.warmAllPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留旧入口兼容测试页或其他历史调用,实际行为改为非阻塞后台预热。
|
||||||
|
initAudios() {
|
||||||
|
return this.warmAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建单个音频实例:缓存命中优先使用,未命中直接使用 CDN。
|
||||||
|
createAudio(key, callback) {
|
||||||
|
const src = audioFils[key];
|
||||||
|
const generation = this.loadGeneration;
|
||||||
|
let completed = false;
|
||||||
|
|
||||||
|
const complete = () => {
|
||||||
|
if (completed) return;
|
||||||
|
completed = true;
|
||||||
|
if (callback) callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!src) {
|
||||||
|
complete();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = this.audioKeys[this.currentLoadingIndex];
|
|
||||||
debugLog(
|
|
||||||
`开始加载音频 ${this.currentLoadingIndex + 1}/${
|
|
||||||
this.audioKeys.length
|
|
||||||
}: ${key}`
|
|
||||||
);
|
|
||||||
this.createAudio(key, () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.loadNextAudio(onComplete, gen);
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建单个音频实例(支持本地缓存)
|
const existingAudio = this.audioMap.get(key);
|
||||||
createAudio(key, callback) {
|
if (existingAudio && !this.readyMap.get(key)) {
|
||||||
this.currentLoadingIndex++;
|
try {
|
||||||
const src = audioFils[key];
|
existingAudio.destroy();
|
||||||
|
} catch (_) {}
|
||||||
|
this.audioMap.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentGeneration = () => generation === this.loadGeneration;
|
||||||
|
|
||||||
|
const setupAudio = (realSrc, resumeAfterReady = false) => {
|
||||||
|
if (!isCurrentGeneration()) {
|
||||||
|
complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const setupAudio = (realSrc) => {
|
|
||||||
const audio = uni.createInnerAudioContext();
|
const audio = uni.createInnerAudioContext();
|
||||||
audio.autoplay = false;
|
audio.autoplay = false;
|
||||||
audio.src = realSrc;
|
|
||||||
try {
|
try {
|
||||||
if (typeof audio.volume === "number") {
|
if (typeof audio.volume === "number") {
|
||||||
audio.volume = this.isMuted ? 0 : 1;
|
audio.volume = this.isMuted ? 0 : 1;
|
||||||
@@ -355,63 +363,99 @@ class AudioManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadTimeout = setTimeout(() => {
|
let loadTimeout = null;
|
||||||
debugLog(`音频 ${key} 加载超时`);
|
let readyHandled = false;
|
||||||
this.recordLoadFailure(key);
|
const clearLoadTimeout = () => {
|
||||||
|
if (!loadTimeout) return;
|
||||||
|
clearTimeout(loadTimeout);
|
||||||
|
loadTimeout = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadFailure = (message) => {
|
||||||
|
if (this.audioMap.get(key) !== audio) return;
|
||||||
|
clearLoadTimeout();
|
||||||
|
debugLog(`音频 ${key} ${message}`);
|
||||||
|
|
||||||
|
const isLocalCache =
|
||||||
|
realSrc !== src && this.localFileCache[src] === realSrc;
|
||||||
|
const shouldResume =
|
||||||
|
this.allowPlayMap.get(key) === true || this.currentPlayingKey === key;
|
||||||
|
const shouldResumeDirectly = shouldResume && completed;
|
||||||
|
|
||||||
|
this.clearPlayWatchdog(key);
|
||||||
|
this.allowPlayMap.set(key, false);
|
||||||
|
if (this.currentPlayingKey === key) {
|
||||||
|
this.currentPlayingKey = null;
|
||||||
|
}
|
||||||
|
if (this.lastPlayKey === key) {
|
||||||
|
this.lastPlayKey = null;
|
||||||
|
this.lastPlayAt = 0;
|
||||||
|
}
|
||||||
|
this.readyMap.set(key, false);
|
||||||
this.audioMap.delete(key);
|
this.audioMap.delete(key);
|
||||||
try {
|
try {
|
||||||
audio.destroy();
|
audio.destroy();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
if (isLocalCache && isCurrentGeneration()) {
|
||||||
|
debugLog(`本地缓存失效,立即切换 CDN: ${key}`);
|
||||||
|
delete this.localFileCache[src];
|
||||||
|
uni.setStorageSync("audio_local_files", this.localFileCache);
|
||||||
|
uni.removeSavedFile({ filePath: realSrc });
|
||||||
|
setupAudio(src, shouldResumeDirectly);
|
||||||
|
void this.cacheRemoteFile(src, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.finishPlayback(key, {
|
this.finishPlayback(key, {
|
||||||
advanceSequence: true,
|
advanceSequence: true,
|
||||||
emitEnded: true,
|
emitEnded: true,
|
||||||
});
|
});
|
||||||
if (callback) callback();
|
complete();
|
||||||
}, 10000);
|
};
|
||||||
|
|
||||||
|
loadTimeout = setTimeout(() => {
|
||||||
|
handleLoadFailure("加载超时");
|
||||||
|
}, AUDIO_LOAD_TIMEOUT_MS);
|
||||||
|
|
||||||
audio.onCanplay(() => {
|
audio.onCanplay(() => {
|
||||||
|
if (this.audioMap.get(key) !== audio) return;
|
||||||
|
if (readyHandled) return;
|
||||||
|
readyHandled = true;
|
||||||
|
if (!isCurrentGeneration()) {
|
||||||
|
clearLoadTimeout();
|
||||||
|
this.audioMap.delete(key);
|
||||||
|
try {
|
||||||
|
audio.destroy();
|
||||||
|
} catch (_) {}
|
||||||
|
complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.allowPlayMap.get(key)) {
|
if (!this.allowPlayMap.get(key)) {
|
||||||
try {
|
try {
|
||||||
audio.pause();
|
audio.pause();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
clearTimeout(loadTimeout);
|
clearLoadTimeout();
|
||||||
this.readyMap.set(key, true);
|
this.readyMap.set(key, true);
|
||||||
this.failedLoadKeys.delete(key);
|
|
||||||
// debugLog(`音频 ${key} 已加载完成`, this.getLoadProgress());
|
|
||||||
uni.$emit("audioLoaded", key);
|
uni.$emit("audioLoaded", key);
|
||||||
const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {};
|
const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {};
|
||||||
loadedAudioKeys[key] = true;
|
loadedAudioKeys[key] = true;
|
||||||
uni.setStorageSync("loadedAudioKeys", loadedAudioKeys);
|
uni.setStorageSync("loadedAudioKeys", loadedAudioKeys);
|
||||||
if (callback) callback();
|
complete();
|
||||||
|
|
||||||
|
if (resumeAfterReady) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const isSequenceCurrent =
|
||||||
|
this.isSequenceRunning &&
|
||||||
|
this.sequenceQueue[this.sequenceIndex] === key;
|
||||||
|
if (isSequenceCurrent) this._playSingle(key, false);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.onError((res) => {
|
audio.onError((res) => {
|
||||||
clearTimeout(loadTimeout);
|
handleLoadFailure(`加载失败:${res.errMsg || "unknown"}`);
|
||||||
debugLog(`音频 ${key} 加载失败:`, res.errMsg);
|
|
||||||
// 如果是本地文件加载失败,可能是文件损坏,清除缓存以便下次重新下载
|
|
||||||
if (realSrc !== src && this.localFileCache[src] === realSrc) {
|
|
||||||
debugLog(`本地缓存失效,移除记录: ${key}`);
|
|
||||||
delete this.localFileCache[src];
|
|
||||||
uni.setStorageSync("audio_local_files", this.localFileCache);
|
|
||||||
// 移除文件
|
|
||||||
uni.removeSavedFile({ filePath: realSrc });
|
|
||||||
}
|
|
||||||
this.recordLoadFailure(key);
|
|
||||||
this.audioMap.delete(key);
|
|
||||||
try {
|
|
||||||
audio.destroy();
|
|
||||||
} catch (_) {}
|
|
||||||
this.finishPlayback(key, {
|
|
||||||
advanceSequence: true,
|
|
||||||
emitEnded: true,
|
|
||||||
});
|
|
||||||
if (this.readyMap.get(key)) {
|
|
||||||
// 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载
|
|
||||||
// this.readyMap.set(key, false);
|
|
||||||
} else {
|
|
||||||
if (callback) callback();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.onEnded(() => {
|
audio.onEnded(() => {
|
||||||
@@ -426,58 +470,78 @@ class AudioManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.audioMap.set(key, audio);
|
this.audioMap.set(key, audio);
|
||||||
|
audio.src = realSrc;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查是否有可用的本地缓存
|
// 检查是否有可用的本地缓存
|
||||||
this.checkLocalFile(src).then((localPath) => {
|
this.checkLocalFile(src).then((localPath) => {
|
||||||
|
if (!isCurrentGeneration()) {
|
||||||
|
complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (localPath) {
|
if (localPath) {
|
||||||
debugLog(`命中本地缓存: ${key}`);
|
debugLog(`命中本地缓存: ${key}`);
|
||||||
setupAudio(localPath);
|
setupAudio(localPath);
|
||||||
} else {
|
} else {
|
||||||
// 下载并尝试保存
|
setupAudio(src);
|
||||||
uni.downloadFile({
|
void this.cacheRemoteFile(src, key);
|
||||||
url: src,
|
}
|
||||||
timeout: 20000,
|
}).catch(() => {
|
||||||
success: (res) => {
|
setupAudio(src);
|
||||||
if (res.tempFilePath) {
|
void this.cacheRemoteFile(src, key);
|
||||||
// 尝试保存文件到本地存储(持久化)
|
});
|
||||||
uni.getFileSystemManager().saveFile({
|
}
|
||||||
tempFilePath: res.tempFilePath,
|
|
||||||
success: (saveRes) => {
|
cacheRemoteFile(url, key) {
|
||||||
const savedPath = saveRes.savedFilePath;
|
if (this.localFileCache[url]) return Promise.resolve(true);
|
||||||
this.localFileCache[src] = savedPath;
|
const currentPromise = this.cacheWritePromises.get(url);
|
||||||
uni.setStorageSync("audio_local_files", this.localFileCache);
|
if (currentPromise) return currentPromise;
|
||||||
debugLog(`音频已缓存到本地: ${key}`);
|
|
||||||
setupAudio(savedPath);
|
let promise;
|
||||||
},
|
promise = new Promise((resolve) => {
|
||||||
fail: (err) => {
|
if (typeof uni.downloadFile !== "function") {
|
||||||
debugLog(
|
resolve(false);
|
||||||
`保存音频失败(可能空间不足),使用临时文件: ${key}`,
|
return;
|
||||||
err
|
}
|
||||||
);
|
|
||||||
setupAudio(res.tempFilePath);
|
uni.downloadFile({
|
||||||
},
|
url,
|
||||||
});
|
timeout: 20000,
|
||||||
} else {
|
success: (res) => {
|
||||||
this.recordLoadFailure(key);
|
if (!res.tempFilePath) {
|
||||||
this.finishPlayback(key, {
|
resolve(false);
|
||||||
advanceSequence: true,
|
return;
|
||||||
emitEnded: true,
|
}
|
||||||
});
|
|
||||||
if (callback) callback();
|
let fileSystemManager;
|
||||||
}
|
try {
|
||||||
},
|
fileSystemManager = uni.getFileSystemManager();
|
||||||
fail: () => {
|
} catch (_) {
|
||||||
this.recordLoadFailure(key);
|
resolve(false);
|
||||||
this.finishPlayback(key, {
|
return;
|
||||||
advanceSequence: true,
|
}
|
||||||
emitEnded: true,
|
|
||||||
});
|
fileSystemManager.saveFile({
|
||||||
if (callback) callback();
|
tempFilePath: res.tempFilePath,
|
||||||
},
|
success: (saveRes) => {
|
||||||
});
|
this.localFileCache[url] = saveRes.savedFilePath;
|
||||||
|
uni.setStorageSync("audio_local_files", this.localFileCache);
|
||||||
|
debugLog(`音频已后台缓存到本地: ${key}`);
|
||||||
|
resolve(true);
|
||||||
|
},
|
||||||
|
fail: () => resolve(false),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: () => resolve(false),
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
if (this.cacheWritePromises.get(url) === promise) {
|
||||||
|
this.cacheWritePromises.delete(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.cacheWritePromises.set(url, promise);
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查本地文件是否有效
|
// 检查本地文件是否有效
|
||||||
@@ -504,11 +568,6 @@ class AudioManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:记录失败(首轮与次轮都会用到)
|
|
||||||
recordLoadFailure(key) {
|
|
||||||
this.failedLoadKeys.add(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearPlayWatchdog(key) {
|
clearPlayWatchdog(key) {
|
||||||
const timer = this.playWatchdogTimers.get(key);
|
const timer = this.playWatchdogTimers.get(key);
|
||||||
if (timer) {
|
if (timer) {
|
||||||
@@ -612,19 +671,14 @@ class AudioManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reloadAudioKey(key) {
|
reloadAudioKey(key) {
|
||||||
const audio = this.audioMap.get(key);
|
return this.retryLoadAudio(key);
|
||||||
if (audio) {
|
|
||||||
try {
|
|
||||||
audio.destroy();
|
|
||||||
} catch (_) {}
|
|
||||||
this.audioMap.delete(key);
|
|
||||||
}
|
|
||||||
this.readyMap.set(key, false);
|
|
||||||
this.retryLoadAudio(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新加载音频
|
// 重新加载音频
|
||||||
retryLoadAudio(key) {
|
retryLoadAudio(key) {
|
||||||
|
const loadingPromise = this.loadingKeyPromises.get(key);
|
||||||
|
if (loadingPromise) return loadingPromise;
|
||||||
|
|
||||||
this.clearPlayWatchdog(key);
|
this.clearPlayWatchdog(key);
|
||||||
const oldAudio = this.audioMap.get(key);
|
const oldAudio = this.audioMap.get(key);
|
||||||
if (oldAudio) {
|
if (oldAudio) {
|
||||||
@@ -632,7 +686,9 @@ class AudioManager {
|
|||||||
oldAudio.destroy();
|
oldAudio.destroy();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
this.createAudio(key);
|
this.audioMap.delete(key);
|
||||||
|
this.readyMap.set(key, false);
|
||||||
|
return this.ensureAudio(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放指定音频或音频数组(数组则按顺序连续播放)
|
// 播放指定音频或音频数组(数组则按顺序连续播放)
|
||||||
@@ -695,6 +751,32 @@ class AudioManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
waitForAudioAndPlay(key) {
|
||||||
|
if (this.pendingPlayKeys.has(key)) return;
|
||||||
|
this.pendingPlayKeys.add(key);
|
||||||
|
|
||||||
|
this.ensureAudio(key)
|
||||||
|
.then((ready) => {
|
||||||
|
const isSequenceCurrent =
|
||||||
|
this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key;
|
||||||
|
if (!isSequenceCurrent) return;
|
||||||
|
|
||||||
|
if (ready) {
|
||||||
|
this._playSingle(key, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.finishPlayback(key, {
|
||||||
|
advanceSequence: true,
|
||||||
|
emitEnded: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.pendingPlayKeys.delete(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 内部方法:播放单个 key
|
// 内部方法:播放单个 key
|
||||||
_playSingle(key, forceStopAll = false) {
|
_playSingle(key, forceStopAll = false) {
|
||||||
if (this.isInterrupted) {
|
if (this.isInterrupted) {
|
||||||
@@ -726,7 +808,7 @@ class AudioManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const audio = this.audioMap.get(key);
|
const audio = this.audioMap.get(key);
|
||||||
if (audio) {
|
if (audio && this.readyMap.get(key)) {
|
||||||
// 播放前确保遵循当前静音状态
|
// 播放前确保遵循当前静音状态
|
||||||
try {
|
try {
|
||||||
if (typeof audio.volume === "number") {
|
if (typeof audio.volume === "number") {
|
||||||
@@ -768,32 +850,8 @@ class AudioManager {
|
|||||||
this.lastPlayAt = Date.now();
|
this.lastPlayAt = Date.now();
|
||||||
this.startPlayWatchdog(key);
|
this.startPlayWatchdog(key);
|
||||||
} else {
|
} else {
|
||||||
debugLog(`音频 ${key} 不存在,尝试重新加载...`);
|
debugLog(`音频 ${key} 尚未就绪,按需加载后播放...`);
|
||||||
this.retryLoadAudio(key);
|
this.waitForAudioAndPlay(key);
|
||||||
let loadWaitTimer = null;
|
|
||||||
const cleanup = () => {
|
|
||||||
try {
|
|
||||||
uni.$off("audioLoaded", handler);
|
|
||||||
} catch (_) {}
|
|
||||||
if (loadWaitTimer) {
|
|
||||||
clearTimeout(loadWaitTimer);
|
|
||||||
loadWaitTimer = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handler = (loadedKey) => {
|
|
||||||
if (loadedKey === key) {
|
|
||||||
cleanup();
|
|
||||||
// 再次校验是否存在且就绪
|
|
||||||
const a = this.audioMap.get(key);
|
|
||||||
if (a && this.readyMap.get(key)) {
|
|
||||||
this._playSingle(key, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
uni.$on("audioLoaded", handler);
|
|
||||||
} catch (_) {}
|
|
||||||
loadWaitTimer = setTimeout(cleanup, 12000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,9 +944,11 @@ class AudioManager {
|
|||||||
debugLog("本地音频缓存清理完成");
|
debugLog("本地音频缓存清理完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手动重置并重新加载所有音频(用于卡住时恢复)
|
// 手动重置音频实例,并按之前的预热范围在后台恢复。
|
||||||
reloadAll() {
|
reloadAll() {
|
||||||
debugLog("执行 reloadAll: 重置所有状态并重新加载");
|
debugLog("执行 reloadAll: 重置音频实例并后台恢复");
|
||||||
|
const shouldWarmAll = this.warmAllPromise !== null;
|
||||||
|
this.loadGeneration += 1;
|
||||||
|
|
||||||
// 1. 停止所有播放
|
// 1. 停止所有播放
|
||||||
this.stopAll();
|
this.stopAll();
|
||||||
@@ -903,8 +963,9 @@ class AudioManager {
|
|||||||
|
|
||||||
// 3. 重置状态
|
// 3. 重置状态
|
||||||
this.readyMap.clear();
|
this.readyMap.clear();
|
||||||
this.failedLoadKeys.clear();
|
|
||||||
this.allowPlayMap.clear();
|
this.allowPlayMap.clear();
|
||||||
|
this.loadingKeyPromises.clear();
|
||||||
|
this.pendingPlayKeys.clear();
|
||||||
this.clearAllPlayWatchdogs();
|
this.clearAllPlayWatchdogs();
|
||||||
this.currentPlayingKey = null;
|
this.currentPlayingKey = null;
|
||||||
this.sequenceQueue = [];
|
this.sequenceQueue = [];
|
||||||
@@ -915,13 +976,12 @@ class AudioManager {
|
|||||||
// 这里选择不自动全清,而是依赖 onError 里的单点清除。如果需要彻底重置,可取消注释:
|
// 这里选择不自动全清,而是依赖 onError 里的单点清除。如果需要彻底重置,可取消注释:
|
||||||
// this.clearCache();
|
// this.clearCache();
|
||||||
|
|
||||||
// 4. 强制重置加载锁
|
// 4. 重置后台预热状态
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.loadingPromise = null;
|
this.warmAllPromise = null;
|
||||||
this.currentLoadingIndex = 0;
|
|
||||||
|
|
||||||
// 5. 重新初始化 (initAudios 会自增 loadGeneration,从而终止之前的任何异步循环)
|
// 5. 之前已启动过全量预热则继续全量恢复,否则只恢复按钮音效。
|
||||||
return this.initAudios();
|
return shouldWarmAll ? this.warmAll() : this.warmButton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ const onClick = debounce(async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const result = await getBattleAPI();
|
const result = await getBattleAPI();
|
||||||
if (result && result.matchId) {
|
if (result && result.matchId) {
|
||||||
await uni.$checkAudio();
|
|
||||||
await returnToBattle(result, navigateOnce);
|
await returnToBattle(result, navigateOnce);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
import { ref } from "vue";
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onShow } from "@dcloudio/uni-app";
|
||||||
import AppBackground from "@/components/AppBackground.vue";
|
import AppBackground from "@/components/AppBackground.vue";
|
||||||
import Header from "@/components/Header.vue";
|
import Header from "@/components/Header.vue";
|
||||||
@@ -8,7 +8,6 @@ import BackToGame from "@/components/BackToGame.vue";
|
|||||||
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
||||||
import { capsuleHeight, debounce } from "@/util";
|
import { capsuleHeight, debounce } from "@/util";
|
||||||
import { returnToBattle } from "@/utils/matchReturn";
|
import { returnToBattle } from "@/utils/matchReturn";
|
||||||
import AudioManager from "@/audioManager";
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -55,9 +54,6 @@ const isIOS = uni.getDeviceInfo().osName === "ios";
|
|||||||
const showHint = ref(false);
|
const showHint = ref(false);
|
||||||
const hintType = ref(0);
|
const hintType = ref(0);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const audioInitProgress = ref(1);
|
|
||||||
const audioProgress = ref(0);
|
|
||||||
const audioTimer = ref(null);
|
|
||||||
|
|
||||||
const showGlobalHint = (type) => {
|
const showGlobalHint = (type) => {
|
||||||
hintType.value = type;
|
hintType.value = type;
|
||||||
@@ -68,46 +64,9 @@ const hideGlobalHint = () => {
|
|||||||
showHint.value = false;
|
showHint.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const restart = () => {
|
|
||||||
uni.restartMiniProgram({
|
|
||||||
path: "/pages/index",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkAudioProgress = async () => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
audioInitProgress.value = AudioManager.getLoadProgress();
|
|
||||||
if (audioInitProgress.value === 1) return resolve();
|
|
||||||
audioTimer.value = setInterval(() => {
|
|
||||||
audioProgress.value = AudioManager.getLoadProgress();
|
|
||||||
if (audioProgress.value === 1) {
|
|
||||||
setTimeout(() => {
|
|
||||||
audioInitProgress.value = 1;
|
|
||||||
}, 200);
|
|
||||||
clearInterval(audioTimer.value);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const audioFinalProgress = computed(() => {
|
|
||||||
const left = 1 - audioInitProgress.value;
|
|
||||||
return Math.max(0, (audioProgress.value - audioInitProgress.value) / left);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (audioTimer.value) clearInterval(audioTimer.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
uni.$showHint = showGlobalHint;
|
uni.$showHint = showGlobalHint;
|
||||||
uni.$hideHint = hideGlobalHint;
|
uni.$hideHint = hideGlobalHint;
|
||||||
uni.$checkAudio = checkAudioProgress;
|
|
||||||
showHint.value = false;
|
showHint.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,7 +86,6 @@ const backToGame = debounce(async () => {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
const result = await getBattleAPI();
|
const result = await getBattleAPI();
|
||||||
if (result && result.matchId) {
|
if (result && result.matchId) {
|
||||||
await checkAudioProgress();
|
|
||||||
await returnToBattle(result, navigateTo);
|
await returnToBattle(result, navigateTo);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -226,24 +184,6 @@ const goCalibration = async () => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</ScreenHint>
|
</ScreenHint>
|
||||||
<view v-if="audioInitProgress < 1" class="audio-progress">
|
|
||||||
<image
|
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-11-26/deihtj15xjwcz3c1tx.png"
|
|
||||||
mode="widthFix"
|
|
||||||
/>
|
|
||||||
<view>
|
|
||||||
<view :style="{ width: `${audioFinalProgress * 100}%` }">
|
|
||||||
<!-- <image
|
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-11-24/degu91a7si77sg9jqv.png"
|
|
||||||
mode="widthFix"
|
|
||||||
/> -->
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view>
|
|
||||||
<text>若加载时间过长,请</text>
|
|
||||||
<button hover-class="none" @click="restart">点击这里重启</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -285,62 +225,4 @@ const goCalibration = async () => {
|
|||||||
color: #666;
|
color: #666;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
.audio-progress {
|
|
||||||
z-index: 999;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background: rgb(0 0 0 / 0.8);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.audio-progress > image:nth-child(1) {
|
|
||||||
width: 140rpx;
|
|
||||||
height: 150rpx;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
.audio-progress > view:nth-child(2) {
|
|
||||||
width: 380rpx;
|
|
||||||
height: 6rpx;
|
|
||||||
background: #595959;
|
|
||||||
border-radius: 4rpx;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
.audio-progress > view:nth-child(2) > view {
|
|
||||||
background: #ffe431;
|
|
||||||
min-height: 6rpx;
|
|
||||||
border-radius: 4rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
transition: width 0.5s ease;
|
|
||||||
}
|
|
||||||
.audio-progress > view:nth-child(2) > view > image {
|
|
||||||
width: 46rpx;
|
|
||||||
height: 26rpx;
|
|
||||||
}
|
|
||||||
.audio-progress > view:nth-child(3) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.audio-progress > view:nth-child(3) > text {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #a2a2a2;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 32rpx;
|
|
||||||
}
|
|
||||||
.audio-progress > view:nth-child(3) > button {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #ffe431;
|
|
||||||
line-height: 32rpx;
|
|
||||||
padding: 20rpx 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ const onClickShare = debounce(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
void audioManager.warmAll();
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -256,11 +256,6 @@ const toPage = async (path) => {
|
|||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// if (path === "/pages/first-try") {
|
|
||||||
// if (canEenter(user.value, device.value, online.value, path)) {
|
|
||||||
// await uni.$checkAudio();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
uni.navigateTo({url: path});
|
uni.navigateTo({url: path});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
|||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import Matching from "@/components/Matching.vue";
|
import Matching from "@/components/Matching.vue";
|
||||||
import ModalDialog from "@/components/ModalDialog.vue";
|
import ModalDialog from "@/components/ModalDialog.vue";
|
||||||
|
import audioManager from "@/audioManager";
|
||||||
import { matchGameAPI, getBattleAPI } from "@/apis";
|
import { matchGameAPI, getBattleAPI } from "@/apis";
|
||||||
import { MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPESV2 } from "@/constants";
|
||||||
import { isLimitError } from "@/util";
|
import { isLimitError } from "@/util";
|
||||||
@@ -146,6 +147,7 @@ onLoad(async (options) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
void audioManager.warmAll();
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ function clearHalfRestCountdown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function leaveHalfRest() {
|
||||||
|
clearHalfRestCountdown();
|
||||||
|
halfTimeTip.value = false;
|
||||||
|
halfRest.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
function getHalfRestSeconds(battleInfo) {
|
function getHalfRestSeconds(battleInfo) {
|
||||||
const remainCandidates = [
|
const remainCandidates = [
|
||||||
battleInfo?.halfRestRemain,
|
battleInfo?.halfRestRemain,
|
||||||
@@ -204,7 +210,7 @@ function takeReadySnapshot(matchId) {
|
|||||||
|
|
||||||
function reconnectMatchServer(battleInfo) {
|
function reconnectMatchServer(battleInfo) {
|
||||||
const status = Number(battleInfo?.status);
|
const status = Number(battleInfo?.status);
|
||||||
if ([2, 3, 4].includes(status)) return;
|
if ([2, 4].includes(status)) return;
|
||||||
if (!battleInfo?.serverAddr) return;
|
if (!battleInfo?.serverAddr) return;
|
||||||
|
|
||||||
connectMatchWebSocket({
|
connectMatchWebSocket({
|
||||||
@@ -311,6 +317,7 @@ function recoverData(battleInfo, { force = false } = {}) {
|
|||||||
}, 200);
|
}, 200);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
leaveHalfRest();
|
||||||
if (force) {
|
if (force) {
|
||||||
const remain = (Date.now() - (battleInfo.current?.startTime || Date.now())) / 1000;
|
const remain = (Date.now() - (battleInfo.current?.startTime || Date.now())) / 1000;
|
||||||
console.log(`当前轮已进行${remain}秒`);
|
console.log(`当前轮已进行${remain}秒`);
|
||||||
@@ -422,9 +429,7 @@ async function onReceiveMessage(msg) {
|
|||||||
if (msg.type === MESSAGETYPESV2.AboutToStart) {
|
if (msg.type === MESSAGETYPESV2.AboutToStart) {
|
||||||
recoverData(msg);
|
recoverData(msg);
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleStart) {
|
} else if (msg.type === MESSAGETYPESV2.BattleStart) {
|
||||||
clearHalfRestCountdown();
|
leaveHalfRest();
|
||||||
halfTimeTip.value = false;
|
|
||||||
halfRest.value = false;
|
|
||||||
recoverData(msg);
|
recoverData(msg);
|
||||||
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||||
// 更新前快照各玩家本轮已射箭数,用于事后识别本次射手
|
// 更新前快照各玩家本轮已射箭数,用于事后识别本次射手
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ const onClickShare = debounce(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
void audioManager.warmAll();
|
||||||
// audioManager.play("第一轮");
|
// audioManager.play("第一轮");
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ const onClickShare = debounce(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
void audioManager.warmAll();
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -126,7 +126,6 @@ const toMatchPage = async (gameType, teamSize) => {
|
|||||||
showLimitModal.value = true;
|
showLimitModal.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await uni.$checkAudio();
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/match-page?gameType=${gameType}&teamSize=${teamSize}`,
|
url: `/pages/match-page?gameType=${gameType}&teamSize=${teamSize}`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ const onClick = debounce(async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const result = await getBattleAPI();
|
const result = await getBattleAPI();
|
||||||
if (result && result.matchId) {
|
if (result && result.matchId) {
|
||||||
await uni.$checkAudio();
|
|
||||||
await returnToBattle(result, navigateOnce);
|
await returnToBattle(result, navigateOnce);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
import { ref } from "vue";
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onShow } from "@dcloudio/uni-app";
|
||||||
import AppBackground from "./AppBackground.vue";
|
import AppBackground from "./AppBackground.vue";
|
||||||
import Header from "./Header.vue";
|
import Header from "./Header.vue";
|
||||||
@@ -8,7 +8,6 @@ import BackToGame from "./BackToGame.vue";
|
|||||||
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
||||||
import { capsuleHeight, debounce } from "@/util";
|
import { capsuleHeight, debounce } from "@/util";
|
||||||
import { returnToBattle } from "@/utils/matchReturn";
|
import { returnToBattle } from "@/utils/matchReturn";
|
||||||
import AudioManager from "@/audioManager";
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -59,9 +58,6 @@ const isIOS = uni.getDeviceInfo().osName === "ios";
|
|||||||
const showHint = ref(false);
|
const showHint = ref(false);
|
||||||
const hintType = ref(0);
|
const hintType = ref(0);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const audioInitProgress = ref(1);
|
|
||||||
const audioProgress = ref(0);
|
|
||||||
const audioTimer = ref(null);
|
|
||||||
|
|
||||||
const showGlobalHint = (type) => {
|
const showGlobalHint = (type) => {
|
||||||
hintType.value = type;
|
hintType.value = type;
|
||||||
@@ -72,52 +68,9 @@ const hideGlobalHint = () => {
|
|||||||
showHint.value = false;
|
showHint.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const restart = () => {
|
|
||||||
uni.restartMiniProgram({
|
|
||||||
path: "/pages/index",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkAudioProgress = async () => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
audioInitProgress.value = AudioManager.getLoadProgress();
|
|
||||||
if (audioInitProgress.value === 1) return resolve();
|
|
||||||
audioTimer.value = setInterval(() => {
|
|
||||||
audioProgress.value = AudioManager.getLoadProgress();
|
|
||||||
if (audioProgress.value === 1) {
|
|
||||||
setTimeout(() => {
|
|
||||||
audioInitProgress.value = 1;
|
|
||||||
}, 200);
|
|
||||||
clearInterval(audioTimer.value);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const audioFinalProgress = computed(() => {
|
|
||||||
const left = 1 - audioInitProgress.value;
|
|
||||||
if (left <= 0) return 0;
|
|
||||||
return Math.max(0, (audioProgress.value - audioInitProgress.value) / left);
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadingProgress = computed(() => {
|
|
||||||
if (props.loading && audioInitProgress.value >= 1) return 1;
|
|
||||||
return audioFinalProgress.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (audioTimer.value) clearInterval(audioTimer.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
uni.$showHint = showGlobalHint;
|
uni.$showHint = showGlobalHint;
|
||||||
uni.$hideHint = hideGlobalHint;
|
uni.$hideHint = hideGlobalHint;
|
||||||
uni.$checkAudio = checkAudioProgress;
|
|
||||||
showHint.value = false;
|
showHint.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,7 +90,6 @@ const backToGame = debounce(async () => {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
const result = await getBattleAPI();
|
const result = await getBattleAPI();
|
||||||
if (result && result.matchId) {
|
if (result && result.matchId) {
|
||||||
await checkAudioProgress();
|
|
||||||
await returnToBattle(result, navigateTo);
|
await returnToBattle(result, navigateTo);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -235,13 +187,13 @@ const goCalibration = async () => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</ScreenHint>
|
</ScreenHint>
|
||||||
<view v-if="loading || audioInitProgress < 1" class="audio-progress">
|
<view v-if="loading" class="audio-progress">
|
||||||
<image
|
<image
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-11-26/deihtj15xjwcz3c1tx.png"
|
src="https://static.shelingxingqiu.com/attachment/2025-11-26/deihtj15xjwcz3c1tx.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<view>
|
<view>
|
||||||
<view :style="{ width: `${loadingProgress * 100}%` }">
|
<view :style="{ width: '100%' }">
|
||||||
<!-- <image
|
<!-- <image
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-11-24/degu91a7si77sg9jqv.png"
|
src="https://static.shelingxingqiu.com/attachment/2025-11-24/degu91a7si77sg9jqv.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
@@ -249,8 +201,7 @@ const goCalibration = async () => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<text>{{ loading ? loadingText || "加载中..." : "若加载时间过长,请" }}</text>
|
<text>{{ loadingText || "加载中..." }}</text>
|
||||||
<button v-if="!loading" hover-class="none" @click="restart">点击这里重启</button>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ let pendingRoundAudio = false;
|
|||||||
// 一旦收到 BattleEnd,后续普通消息就不再进入队列。
|
// 一旦收到 BattleEnd,后续普通消息就不再进入队列。
|
||||||
let battleEnded = false;
|
let battleEnded = false;
|
||||||
let skipNextRestoreOnShow = false;
|
let skipNextRestoreOnShow = false;
|
||||||
|
let pendingReturnSnapshot = null;
|
||||||
const handledMessageKeys = new Set();
|
const handledMessageKeys = new Set();
|
||||||
const handledMessageKeyOrder = [];
|
const handledMessageKeyOrder = [];
|
||||||
const queuedMessageKeys = new Set();
|
const queuedMessageKeys = new Set();
|
||||||
@@ -978,9 +979,9 @@ async function runToSomeoneShootTask(task, runId) {
|
|||||||
notifyMatchAudioAck(task);
|
notifyMatchAudioAck(task);
|
||||||
if (!isQueueAlive(runId)) return;
|
if (!isQueueAlive(runId)) return;
|
||||||
|
|
||||||
const remainingSeconds = getRemainingSeconds(battleInfo, task, {
|
// 实时换人消息的倒计时由本条语音 ACK 驱动:后端收到 ACK 后才开始计时,
|
||||||
fullDurationIfNoBackendRemain: isRoundFirstShoot,
|
// 因此前端也应从完整 shootTime 启动,不能扣除消息排队及 tententen/提示语音耗时。
|
||||||
});
|
const remainingSeconds = getShootTimeSeconds(battleInfo);
|
||||||
const countdown = getCountdownPayload(remainingSeconds, shootTimeTotal.value);
|
const countdown = getCountdownPayload(remainingSeconds, shootTimeTotal.value);
|
||||||
markProgressDeadline(countdown);
|
markProgressDeadline(countdown);
|
||||||
uni.$emit("update-remain", {
|
uni.$emit("update-remain", {
|
||||||
@@ -1249,6 +1250,7 @@ function onReceiveMessage(message) {
|
|||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
console.log('重新进入了')
|
console.log('重新进入了')
|
||||||
const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null;
|
const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null;
|
||||||
|
pendingReturnSnapshot = returnSnapshot;
|
||||||
skipNextRestoreOnShow = false;
|
skipNextRestoreOnShow = false;
|
||||||
// 新对局入口:把所有会串场的状态、队列、时间戳和缓存一次性清空。
|
// 新对局入口:把所有会串场的状态、队列、时间戳和缓存一次性清空。
|
||||||
start.value = null;
|
start.value = null;
|
||||||
@@ -1293,8 +1295,7 @@ onLoad((options) => {
|
|||||||
latestShotFlash.value = null;
|
latestShotFlash.value = null;
|
||||||
if (returnSnapshot) {
|
if (returnSnapshot) {
|
||||||
skipNextRestoreOnShow = true;
|
skipNextRestoreOnShow = true;
|
||||||
applyBattleSnapshot(returnSnapshot, { restore: true });
|
showRestoreLoading();
|
||||||
reconnectMatchServer(returnSnapshot);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const readySnapshot = takeReadySnapshot(battleId.value);
|
const readySnapshot = takeReadySnapshot(battleId.value);
|
||||||
@@ -1316,6 +1317,12 @@ onMounted(async () => {
|
|||||||
uni.$on(PROGRESS_ZERO_EVENT, onProgressZero);
|
uni.$on(PROGRESS_ZERO_EVENT, onProgressZero);
|
||||||
uni.$on(COUNTDOWN_READY_EVENT, hideRestoreLoading);
|
uni.$on(COUNTDOWN_READY_EVENT, hideRestoreLoading);
|
||||||
uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
|
uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
|
||||||
|
if (pendingReturnSnapshot) {
|
||||||
|
const returnSnapshot = pendingReturnSnapshot;
|
||||||
|
pendingReturnSnapshot = null;
|
||||||
|
reconnectMatchServer(returnSnapshot);
|
||||||
|
scheduleRestoreLatestBattle();
|
||||||
|
}
|
||||||
await laserCloseAPI();
|
await laserCloseAPI();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1333,6 +1340,7 @@ onBeforeUnmount(() => {
|
|||||||
clearTimeout(pendingRestoreTimer);
|
clearTimeout(pendingRestoreTimer);
|
||||||
pendingRestoreTimer = null;
|
pendingRestoreTimer = null;
|
||||||
}
|
}
|
||||||
|
pendingReturnSnapshot = null;
|
||||||
hideRestoreLoading();
|
hideRestoreLoading();
|
||||||
invalidateBattleQueue({ stopAudio: true, stopProgress: true });
|
invalidateBattleQueue({ stopAudio: true, stopProgress: true });
|
||||||
closeBattleServer("team-battle-unmount");
|
closeBattleServer("team-battle-unmount");
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const toOrderPage = () => {
|
|||||||
|
|
||||||
const toFristTryPage = async () => {
|
const toFristTryPage = async () => {
|
||||||
if (canEenter(user.value, device.value, online.value, "/pages/first-try")) {
|
if (canEenter(user.value, device.value, online.value, "/pages/first-try")) {
|
||||||
await uni.$checkAudio();
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/first-try",
|
url: "/pages/first-try",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { connectMatchWebSocket } from "@/matchWebsocket";
|
|
||||||
import useStore from "@/store";
|
|
||||||
|
|
||||||
export const MATCH_RETURN_SNAPSHOT_KEY = "match-return-snapshot";
|
export const MATCH_RETURN_SNAPSHOT_KEY = "match-return-snapshot";
|
||||||
|
|
||||||
function getMatchId(battleInfo) {
|
function getMatchId(battleInfo) {
|
||||||
@@ -29,14 +26,7 @@ export async function returnToBattle(battleInfo, navigate) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
uni.setStorageSync(MATCH_RETURN_SNAPSHOT_KEY, battleInfo);
|
uni.setStorageSync(MATCH_RETURN_SNAPSHOT_KEY, battleInfo);
|
||||||
connectMatchWebSocket({
|
|
||||||
serverAddr: battleInfo.serverAddr,
|
|
||||||
matchId,
|
|
||||||
userId: store.user?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await navigate(getBattlePageUrl(battleInfo));
|
await navigate(getBattlePageUrl(battleInfo));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user