Merge branch 'test' into feat-vip
@@ -10,6 +10,7 @@ lerna-debug.log*
|
|||||||
node_modules
|
node_modules
|
||||||
.history
|
.history
|
||||||
.github
|
.github
|
||||||
|
.claude
|
||||||
openspec
|
openspec
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
docs
|
docs
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
const {
|
const {
|
||||||
updateUser,
|
updateUser,
|
||||||
updateOnline,
|
updateOnline,
|
||||||
clearSessionState
|
clearSessionState,
|
||||||
|
clearDevice
|
||||||
} = store;
|
} = store;
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -63,6 +64,11 @@
|
|||||||
updateOnline(data.online);
|
updateOnline(data.online);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDeviceBindInvalid() {
|
||||||
|
clearDevice();
|
||||||
|
uni.setStorageSync("calibration", false);
|
||||||
|
}
|
||||||
|
|
||||||
function onDeviceShoot() {
|
function onDeviceShoot() {
|
||||||
// audioManager.play("射箭声音")
|
// audioManager.play("射箭声音")
|
||||||
}
|
}
|
||||||
@@ -78,6 +84,7 @@
|
|||||||
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);
|
||||||
|
uni.$on("device-bind-invalid", onDeviceBindInvalid);
|
||||||
const token = uni.getStorageSync(
|
const token = uni.getStorageSync(
|
||||||
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
);
|
);
|
||||||
@@ -91,6 +98,7 @@
|
|||||||
uni.$off("update-user", emitUpdateUser);
|
uni.$off("update-user", emitUpdateUser);
|
||||||
uni.$off("update-online", emitUpdateOnline);
|
uni.$off("update-online", emitUpdateOnline);
|
||||||
uni.$off("session-kicked-out", onSessionKickedOut);
|
uni.$off("session-kicked-out", onSessionKickedOut);
|
||||||
|
uni.$off("device-bind-invalid", onDeviceBindInvalid);
|
||||||
websocket.closeWebSocket();
|
websocket.closeWebSocket();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ try {
|
|||||||
|
|
||||||
const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot");
|
const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot");
|
||||||
|
|
||||||
|
// 统一处理业务接口请求,包含登录态、业务错误和 WiFi 连接空响应兼容。
|
||||||
function request(method, url, data = {}, baseUrl = BASE_URL) {
|
function request(method, url, data = {}, baseUrl = BASE_URL) {
|
||||||
const token = uni.getStorageSync(
|
const token = uni.getStorageSync(
|
||||||
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
@@ -39,6 +40,10 @@ function request(method, url, data = {}, baseUrl = BASE_URL) {
|
|||||||
data,
|
data,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
|
if (url === "/user/hardwareBox/connectWifi" && res.statusCode === 200 && res.data && Object.keys(res.data).length === 0) {
|
||||||
|
resolve({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
const {code, data, message} = res.data;
|
const {code, data, message} = res.data;
|
||||||
if (code === 0) resolve(data);
|
if (code === 0) resolve(data);
|
||||||
@@ -77,6 +82,15 @@ function request(method, url, data = {}, baseUrl = BASE_URL) {
|
|||||||
resolve({binded: true});
|
resolve({binded: true});
|
||||||
return;
|
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") {
|
if (message === "ERROR_ORDER_UNPAY") {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "当前有未支付订单",
|
title: "当前有未支付订单",
|
||||||
@@ -475,6 +489,26 @@ export const getDeviceBatteryAPI = async () => {
|
|||||||
return request("GET", "/user/device/battery");
|
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) => {
|
export const addNoteAPI = async (id, remark) => {
|
||||||
return request("POST", "/user/score/sheet/remark", {id, remark});
|
return request("POST", "/user/score/sheet/remark", {id, remark});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ class AudioManager {
|
|||||||
this.lastPlayKey = null;
|
this.lastPlayKey = null;
|
||||||
this.lastPlayAt = 0;
|
this.lastPlayAt = 0;
|
||||||
this.isInterrupted = false;
|
this.isInterrupted = false;
|
||||||
|
this.interruptedAt = 0;
|
||||||
|
this.interruptionFallbackMs = 5000;
|
||||||
|
this.playWatchdogMs = 8000;
|
||||||
|
this.playWatchdogTimers = new Map();
|
||||||
|
|
||||||
// 静音开关
|
// 静音开关
|
||||||
this.isMuted = false;
|
this.isMuted = false;
|
||||||
@@ -159,6 +163,7 @@ class AudioManager {
|
|||||||
const begin = () => {
|
const begin = () => {
|
||||||
if (this.isInterrupted) return;
|
if (this.isInterrupted) return;
|
||||||
this.isInterrupted = true;
|
this.isInterrupted = true;
|
||||||
|
this.interruptedAt = Date.now();
|
||||||
this.stopAll();
|
this.stopAll();
|
||||||
this.isSequenceRunning = false;
|
this.isSequenceRunning = false;
|
||||||
this.sequenceQueue = [];
|
this.sequenceQueue = [];
|
||||||
@@ -170,6 +175,7 @@ class AudioManager {
|
|||||||
const end = () => {
|
const end = () => {
|
||||||
if (!this.isInterrupted) return;
|
if (!this.isInterrupted) return;
|
||||||
this.isInterrupted = false;
|
this.isInterrupted = false;
|
||||||
|
this.interruptedAt = 0;
|
||||||
uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
|
uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
|
||||||
void this.reloadAll();
|
void this.reloadAll();
|
||||||
};
|
};
|
||||||
@@ -352,9 +358,14 @@ class AudioManager {
|
|||||||
const loadTimeout = setTimeout(() => {
|
const loadTimeout = setTimeout(() => {
|
||||||
debugLog(`音频 ${key} 加载超时`);
|
debugLog(`音频 ${key} 加载超时`);
|
||||||
this.recordLoadFailure(key);
|
this.recordLoadFailure(key);
|
||||||
|
this.audioMap.delete(key);
|
||||||
try {
|
try {
|
||||||
audio.destroy();
|
audio.destroy();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
this.finishPlayback(key, {
|
||||||
|
advanceSequence: true,
|
||||||
|
emitEnded: true,
|
||||||
|
});
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
@@ -388,7 +399,13 @@ class AudioManager {
|
|||||||
}
|
}
|
||||||
this.recordLoadFailure(key);
|
this.recordLoadFailure(key);
|
||||||
this.audioMap.delete(key);
|
this.audioMap.delete(key);
|
||||||
audio.destroy();
|
try {
|
||||||
|
audio.destroy();
|
||||||
|
} catch (_) {}
|
||||||
|
this.finishPlayback(key, {
|
||||||
|
advanceSequence: true,
|
||||||
|
emitEnded: true,
|
||||||
|
});
|
||||||
if (this.readyMap.get(key)) {
|
if (this.readyMap.get(key)) {
|
||||||
// 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载
|
// 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载
|
||||||
// this.readyMap.set(key, false);
|
// this.readyMap.set(key, false);
|
||||||
@@ -398,19 +415,14 @@ class AudioManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
audio.onEnded(() => {
|
audio.onEnded(() => {
|
||||||
if (this.currentPlayingKey === key) {
|
this.finishPlayback(key, {
|
||||||
this.currentPlayingKey = null;
|
advanceSequence: true,
|
||||||
}
|
emitEnded: true,
|
||||||
this.allowPlayMap.set(key, false);
|
});
|
||||||
this.onAudioEnded(key);
|
|
||||||
uni.$emit('audioEnded', key);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.onStop(() => {
|
audio.onStop(() => {
|
||||||
if (this.currentPlayingKey === key) {
|
this.finishPlayback(key);
|
||||||
this.currentPlayingKey = null;
|
|
||||||
}
|
|
||||||
this.allowPlayMap.set(key, false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.audioMap.set(key, audio);
|
this.audioMap.set(key, audio);
|
||||||
@@ -448,11 +460,19 @@ class AudioManager {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.recordLoadFailure(key);
|
this.recordLoadFailure(key);
|
||||||
|
this.finishPlayback(key, {
|
||||||
|
advanceSequence: true,
|
||||||
|
emitEnded: true,
|
||||||
|
});
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: () => {
|
fail: () => {
|
||||||
this.recordLoadFailure(key);
|
this.recordLoadFailure(key);
|
||||||
|
this.finishPlayback(key, {
|
||||||
|
advanceSequence: true,
|
||||||
|
emitEnded: true,
|
||||||
|
});
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -489,15 +509,137 @@ class AudioManager {
|
|||||||
this.failedLoadKeys.add(key);
|
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) {
|
retryLoadAudio(key) {
|
||||||
|
this.clearPlayWatchdog(key);
|
||||||
const oldAudio = this.audioMap.get(key);
|
const oldAudio = this.audioMap.get(key);
|
||||||
if (oldAudio) oldAudio.destroy();
|
if (oldAudio) {
|
||||||
|
try {
|
||||||
|
oldAudio.destroy();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
this.createAudio(key);
|
this.createAudio(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放指定音频或音频数组(数组则按顺序连续播放)
|
// 播放指定音频或音频数组(数组则按顺序连续播放)
|
||||||
play(input, interrupt = true) {
|
play(input, interrupt = true) {
|
||||||
|
if (this.isInterrupted) {
|
||||||
|
this.recoverFromInterruptionIfStale();
|
||||||
|
}
|
||||||
if (this.isInterrupted) {
|
if (this.isInterrupted) {
|
||||||
debugLog("音频处理中断状态,忽略播放请求");
|
debugLog("音频处理中断状态,忽略播放请求");
|
||||||
return;
|
return;
|
||||||
@@ -555,6 +697,9 @@ class AudioManager {
|
|||||||
|
|
||||||
// 内部方法:播放单个 key
|
// 内部方法:播放单个 key
|
||||||
_playSingle(key, forceStopAll = false) {
|
_playSingle(key, forceStopAll = false) {
|
||||||
|
if (this.isInterrupted) {
|
||||||
|
this.recoverFromInterruptionIfStale();
|
||||||
|
}
|
||||||
if (this.isInterrupted) {
|
if (this.isInterrupted) {
|
||||||
debugLog(`音频处理中断状态,跳过播放: ${key}`);
|
debugLog(`音频处理中断状态,跳过播放: ${key}`);
|
||||||
return;
|
return;
|
||||||
@@ -563,6 +708,11 @@ class AudioManager {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
|
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
|
||||||
debugLog(`忽略快速重复播放: ${key}`);
|
debugLog(`忽略快速重复播放: ${key}`);
|
||||||
|
this.finishPlayback(key, {
|
||||||
|
advanceSequence: true,
|
||||||
|
emitEnded: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,21 +755,34 @@ class AudioManager {
|
|||||||
try {
|
try {
|
||||||
audio.play();
|
audio.play();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.allowPlayMap.set(key, false);
|
this.finishPlayback(key, {
|
||||||
|
advanceSequence: true,
|
||||||
|
emitEnded: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err);
|
debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentPlayingKey = key;
|
this.currentPlayingKey = key;
|
||||||
this.lastPlayKey = key;
|
this.lastPlayKey = key;
|
||||||
this.lastPlayAt = Date.now();
|
this.lastPlayAt = Date.now();
|
||||||
|
this.startPlayWatchdog(key);
|
||||||
} else {
|
} else {
|
||||||
debugLog(`音频 ${key} 不存在,尝试重新加载...`);
|
debugLog(`音频 ${key} 不存在,尝试重新加载...`);
|
||||||
this.retryLoadAudio(key);
|
this.retryLoadAudio(key);
|
||||||
|
let loadWaitTimer = null;
|
||||||
|
const cleanup = () => {
|
||||||
|
try {
|
||||||
|
uni.$off("audioLoaded", handler);
|
||||||
|
} catch (_) {}
|
||||||
|
if (loadWaitTimer) {
|
||||||
|
clearTimeout(loadWaitTimer);
|
||||||
|
loadWaitTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
const handler = (loadedKey) => {
|
const handler = (loadedKey) => {
|
||||||
if (loadedKey === key) {
|
if (loadedKey === key) {
|
||||||
try {
|
cleanup();
|
||||||
uni.$off("audioLoaded", handler);
|
|
||||||
} catch (_) {}
|
|
||||||
// 再次校验是否存在且就绪
|
// 再次校验是否存在且就绪
|
||||||
const a = this.audioMap.get(key);
|
const a = this.audioMap.get(key);
|
||||||
if (a && this.readyMap.get(key)) {
|
if (a && this.readyMap.get(key)) {
|
||||||
@@ -630,6 +793,7 @@ class AudioManager {
|
|||||||
try {
|
try {
|
||||||
uni.$on("audioLoaded", handler);
|
uni.$on("audioLoaded", handler);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
loadWaitTimer = setTimeout(cleanup, 12000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,6 +819,7 @@ class AudioManager {
|
|||||||
// 停止指定音频
|
// 停止指定音频
|
||||||
stop(key) {
|
stop(key) {
|
||||||
const audio = this.audioMap.get(key);
|
const audio = this.audioMap.get(key);
|
||||||
|
this.clearPlayWatchdog(key);
|
||||||
if (audio) {
|
if (audio) {
|
||||||
audio.stop();
|
audio.stop();
|
||||||
this.allowPlayMap.set(key, false);
|
this.allowPlayMap.set(key, false);
|
||||||
@@ -666,6 +831,7 @@ class AudioManager {
|
|||||||
|
|
||||||
// 停止所有音频
|
// 停止所有音频
|
||||||
stopAll() {
|
stopAll() {
|
||||||
|
this.clearAllPlayWatchdogs();
|
||||||
for (const [k, audio] of this.audioMap.entries()) {
|
for (const [k, audio] of this.audioMap.entries()) {
|
||||||
try {
|
try {
|
||||||
audio.stop();
|
audio.stop();
|
||||||
@@ -739,6 +905,7 @@ class AudioManager {
|
|||||||
this.readyMap.clear();
|
this.readyMap.clear();
|
||||||
this.failedLoadKeys.clear();
|
this.failedLoadKeys.clear();
|
||||||
this.allowPlayMap.clear();
|
this.allowPlayMap.clear();
|
||||||
|
this.clearAllPlayWatchdogs();
|
||||||
this.currentPlayingKey = null;
|
this.currentPlayingKey = null;
|
||||||
this.sequenceQueue = [];
|
this.sequenceQueue = [];
|
||||||
this.sequenceIndex = 0;
|
this.sequenceIndex = 0;
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { getDeviceBatteryAPI } from "@/apis";
|
||||||
|
|
||||||
|
const OTA_MIN_BATTERY = 50;
|
||||||
|
const OTA_LOW_BATTERY_TEXT = "电量不足 50%,暂不支持 OTA 升级";
|
||||||
|
const OTA_OFFLINE_TEXT = "请先开启智能弓";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
type: String,
|
||||||
|
default: "new_version", // new_version | update_progress | update_success | update_failure
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
type: Number,
|
||||||
|
default: 40,
|
||||||
|
},
|
||||||
|
// 副标题:如“新版本将优化智能弓体验”
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
// 详细说明:如“升级前请确保:...”
|
||||||
|
changelog: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
forceUpdate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update", "skip", "close", "done", "retry"]);
|
||||||
|
|
||||||
|
const isNewVersion = computed(() => props.state === "new_version");
|
||||||
|
const isProgress = computed(() => props.state === "update_progress");
|
||||||
|
const isSuccess = computed(() => props.state === "update_success");
|
||||||
|
const isFailure = computed(() => props.state === "update_failure");
|
||||||
|
// Clamp progress to keep the progress bar width within its container.
|
||||||
|
const progressValue = computed(() => Math.min(100, Math.max(0, Number(props.progress) || 0)));
|
||||||
|
|
||||||
|
// 点击立即更新前先校验设备在线状态,再校验设备电量。
|
||||||
|
const handleUpdateClick = async () => {
|
||||||
|
try {
|
||||||
|
const deviceStatus = await getDeviceBatteryAPI();
|
||||||
|
if (deviceStatus?.online !== true) {
|
||||||
|
uni.showToast({
|
||||||
|
title: OTA_OFFLINE_TEXT,
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(deviceStatus?.battery) <= OTA_MIN_BATTERY) {
|
||||||
|
uni.showToast({
|
||||||
|
title: OTA_LOW_BATTERY_TEXT,
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
emit("update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("update");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view v-if="visible" class="ota-mask">
|
||||||
|
<!-- 图标 + 弹窗卡片 容器 -->
|
||||||
|
<view
|
||||||
|
class="ota-outer"
|
||||||
|
:class="isNewVersion ? 'outer-new' : 'outer-result'"
|
||||||
|
>
|
||||||
|
<!-- 悬浮图标(溢出卡片顶部) -->
|
||||||
|
<image
|
||||||
|
v-if="isNewVersion"
|
||||||
|
src="../static/ota/ota-mascot.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="float-icon float-mascot"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-else-if="isSuccess"
|
||||||
|
src="../static/ota/check-char.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="float-icon float-check"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-else-if="isFailure"
|
||||||
|
src="../static/ota/close-char.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="float-icon float-close"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-else-if="isProgress"
|
||||||
|
src="../static/ota/target-char.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="float-icon float-target"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 弹窗卡片:overflow:visible 允许按钮溢出底部,背景图通过 ota-bg-clip 独立裁剪保持圆角 -->
|
||||||
|
<view class="ota-dialog">
|
||||||
|
<view class="ota-bg-clip">
|
||||||
|
<image src="../static/ota/ota-bg.png" mode="aspectFill" class="ota-bg" />
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="ota-content"
|
||||||
|
:class="{ 'content-new': isNewVersion, 'content-result': isProgress || isSuccess || isFailure }"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 发现新版本:new-ver.png 已包含标题图,不再重复文字;版本号使用 ota-ver.png 胶囊背景 -->
|
||||||
|
<block v-if="isNewVersion">
|
||||||
|
<image src="../static/ota/new-ver.png" mode="aspectFit" class="new-ver-img" />
|
||||||
|
<view v-if="version" class="version-tag-wrap">
|
||||||
|
<image src="../static/ota/ota-ver.png" mode="aspectFit" class="version-tag-bg-img" />
|
||||||
|
<text class="version-tag">{{ version }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 副标题:如“新版本将优化智能弓体验”,离下方详情 12rpx -->
|
||||||
|
<text v-if="description" class="desc-text">{{ description }}</text>
|
||||||
|
<!-- 详细说明:如“升级前请确保:...” -->
|
||||||
|
<text v-if="changelog" class="changelog-text">{{ changelog }}</text>
|
||||||
|
<view class="btn-group">
|
||||||
|
<view class="primary-btn" @click="handleUpdateClick">
|
||||||
|
<text class="primary-btn-text">立即更新</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="!forceUpdate" class="skip-text" @click="emit('skip')">暂不更新</text>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<!-- 更新成功:图片左边距 34rpx,文案左边距 44rpx,按钮浮动底部居中 -->
|
||||||
|
<block v-else-if="isSuccess">
|
||||||
|
<image src="../static/ota/update-ok.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" />
|
||||||
|
<text class="dialog-desc">请关机并重启智能弓</text>
|
||||||
|
<view class="btn-group-result">
|
||||||
|
<view class="primary-btn" @click="emit('done')">
|
||||||
|
<text class="primary-btn-text">完成</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<!-- 更新中:复用成功标题图,正文区域展示进度条,无底部按钮 -->
|
||||||
|
<block v-else-if="isProgress">
|
||||||
|
<image src="../static/ota/update_progress.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" />
|
||||||
|
<view class="progress-wrap">
|
||||||
|
<view class="progress-track">
|
||||||
|
<view class="progress-fill" :style="{ width: `${progressValue}%` }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<!-- 更新失败:图片左边距 34rpx,文案左对齐 44rpx,按钮浮动底部居中 -->
|
||||||
|
<block v-else-if="isFailure">
|
||||||
|
<image src="../static/ota/update-fail.png" mode="aspectFit" class="result-title-img" style="width: 222rpx; height: 62rpx;" />
|
||||||
|
<text class="dialog-desc">请确保:</text>
|
||||||
|
<text class="dialog-desc">1、智能弓已开启</text>
|
||||||
|
<text class="dialog-desc">2、网路连接稳定</text>
|
||||||
|
<view class="btn-group-result">
|
||||||
|
<view class="primary-btn" @click="emit('retry')">
|
||||||
|
<text class="primary-btn-text">重试</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关闭按钮(仅新版本状态,非强制更新时,位于弹窗下方) -->
|
||||||
|
<view
|
||||||
|
v-if="(isNewVersion || isFailure) && !forceUpdate"
|
||||||
|
class="ota-close-below"
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
<image src="../static/sicon/close.png" mode="aspectFit" style="width: 56rpx; height: 56rpx;" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ota-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 外层容器:相对定位,为浮动图标创造溢出空间 */
|
||||||
|
.ota-outer {
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
/* 设计图:吉祥物向上突出弹窗顶部 40px(375px基准)× 2 = 80rpx */
|
||||||
|
.outer-new {
|
||||||
|
padding-top: 80rpx;
|
||||||
|
}
|
||||||
|
.outer-result {
|
||||||
|
padding-top: 80rpx;
|
||||||
|
padding-bottom: 66rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浮动图标(绝对定位,位于卡片顶部上方) */
|
||||||
|
.float-icon {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
/* 吉祥物尺寸:设计图 149×109px(375px基准)× 2 = 298×218rpx */
|
||||||
|
.float-mascot {
|
||||||
|
width: 298rpx;
|
||||||
|
height: 218rpx;
|
||||||
|
top: -5px;
|
||||||
|
right: -74rpx;
|
||||||
|
}
|
||||||
|
.float-check {
|
||||||
|
width: 194rpx;
|
||||||
|
height: 166rpx;
|
||||||
|
top: 20px;
|
||||||
|
right: 30rpx;
|
||||||
|
}
|
||||||
|
.float-close {
|
||||||
|
width: 194rpx;
|
||||||
|
height: 164rpx;
|
||||||
|
top: 20px;
|
||||||
|
right: 30rpx;
|
||||||
|
}
|
||||||
|
.float-target {
|
||||||
|
width: 194rpx;
|
||||||
|
height: 166rpx;
|
||||||
|
top: 20px;
|
||||||
|
right: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗卡片:overflow:visible 允许按钮溢出底部,背景通过 ota-bg-clip 独立裁剪 */
|
||||||
|
.ota-dialog {
|
||||||
|
position: relative;
|
||||||
|
width: 482rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 2rpx solid #F9D5A1;
|
||||||
|
overflow: visible;
|
||||||
|
background-color: #392F1D;
|
||||||
|
}
|
||||||
|
/* 背景图裁剪层:独立 overflow:hidden + border-radius 保持圆角效果 */
|
||||||
|
.ota-bg-clip {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.ota-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.ota-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* 按钮以外内容均左对齐 */
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.content-new {
|
||||||
|
padding: 30rpx 0 40rpx 0;
|
||||||
|
}
|
||||||
|
.content-result {
|
||||||
|
padding: 30rpx 0 66rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 发现新版本内容 */
|
||||||
|
.new-ver-img {
|
||||||
|
width: 274rpx;
|
||||||
|
height: 62rpx;
|
||||||
|
/* 左边距 34rpx,去掉 margin-bottom */
|
||||||
|
margin-left: 34rpx;
|
||||||
|
}
|
||||||
|
/* 版本号胶囊容器:相对定位,使 ota-ver.png 作为背景衬底 */
|
||||||
|
.version-tag-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 116rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
/* 离标题图 -10rpx,左边距 50rpx,离下方副标题 22rpx */
|
||||||
|
margin-top: -10rpx;
|
||||||
|
margin-left: 50rpx;
|
||||||
|
margin-bottom: 22rpx;
|
||||||
|
}
|
||||||
|
/* ota-ver.png 胶囊背景图 */
|
||||||
|
.version-tag-bg-img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
/* 版本号文字:浮于背景图之上 */
|
||||||
|
.version-tag {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
color: rgba(254, 222, 100, 1);
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 8rpx 22rpx 4rpx 24rpx;
|
||||||
|
}
|
||||||
|
/* 副标题(如"新版本将优化智能弓体验"):左边距 44rpx,离下方文案 12rpx */
|
||||||
|
.desc-text {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 36rpx;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 44rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
/* 详细说明文案(如“升级前请确保:...”):左边距 44rpx */
|
||||||
|
.changelog-text {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 44rpx;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮组(新版本状态):离上方文案 30rpx,内部按钮间距 24rpx */
|
||||||
|
.btn-group {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
/* 按钮组(结果状态):绝对定位,溢出卡片底边 -35rpx 悬浮在底边中间 */
|
||||||
|
.btn-group-result {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -35rpx;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
/* 主按钮:按照设计规范 width: 232rpx, height: 70rpx */
|
||||||
|
.primary-btn {
|
||||||
|
width: 232rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
background-color: #FED847;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.primary-btn-text {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #000000;
|
||||||
|
line-height: 36rpx;
|
||||||
|
}
|
||||||
|
/* 暂不更新:设计规范颜色 #5FADFF 蓝色 */
|
||||||
|
.skip-text {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #5FADFF;
|
||||||
|
line-height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 更新结果内容:图片左边距 34rpx,下边距 16rpx */
|
||||||
|
.result-title-img {
|
||||||
|
margin-left: 34rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
/* 结果页文案:左对齐,左边距 44rpx,与 new_version 保持一致 */
|
||||||
|
.dialog-desc {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 44rpx;
|
||||||
|
}
|
||||||
|
.progress-wrap {
|
||||||
|
width: 394rpx;
|
||||||
|
margin-top: 40rpx;
|
||||||
|
margin-left: 44rpx;
|
||||||
|
}
|
||||||
|
.progress-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 18rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.28);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #FED847;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关闭按钮(位于弹窗下方) */
|
||||||
|
.ota-close-below {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,6 +16,11 @@ const props = defineProps({
|
|||||||
|
|
||||||
const rowCount = new Array(6).fill(0);
|
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 isMember = (player = {}) => player.vip === true || player.sVip === true;
|
||||||
|
|
||||||
const getMemberNicknameClass = (player = {}) => [
|
const getMemberNicknameClass = (player = {}) => [
|
||||||
@@ -52,23 +57,19 @@ const getMemberNicknameClass = (player = {}) => [
|
|||||||
<view>
|
<view>
|
||||||
<view>
|
<view>
|
||||||
<view v-for="(_, index) in rowCount" :key="index">
|
<view v-for="(_, index) in rowCount" :key="index">
|
||||||
<text>{{
|
<text>{{ getRingText(scores[0]?.[index]) }}</text>
|
||||||
scores[0] && scores[0][index] ? `${scores[0][index].ring}环` : "-"
|
|
||||||
}}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<view v-for="(_, index) in rowCount" :key="index">
|
<view v-for="(_, index) in rowCount" :key="index">
|
||||||
<text>{{
|
<text>{{ getRingText(scores[1]?.[index]) }}</text>
|
||||||
scores[1] && scores[1][index] ? `${scores[1][index].ring}环` : "-"
|
|
||||||
}}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text
|
<text
|
||||||
>{{
|
>{{
|
||||||
scores
|
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)
|
.reduce((last, next) => last + next, 0)
|
||||||
}}环</text
|
}}环</text
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
|
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { updateUser, updateDevice, updateOnline } = store;
|
const { updateUser, updateDevice, updateOnline, clearDevice } = store;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
@@ -122,6 +122,8 @@ async function doLogin() {
|
|||||||
);
|
);
|
||||||
const data = await getDeviceBatteryAPI();
|
const data = await getDeviceBatteryAPI();
|
||||||
updateOnline(data.online);
|
updateOnline(data.online);
|
||||||
|
} else {
|
||||||
|
clearDevice();
|
||||||
}
|
}
|
||||||
props.onClose();
|
props.onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -64,7 +64,11 @@
|
|||||||
"usingComponents" : true,
|
"usingComponents" : true,
|
||||||
"darkmode" : true,
|
"darkmode" : true,
|
||||||
"themeLocation" : "theme.json",
|
"themeLocation" : "theme.json",
|
||||||
"permission" : {},
|
"permission" : {
|
||||||
|
"scope.userLocation": {
|
||||||
|
"desc": "用于扫描附近 WiFi,完成设备 OTA 升级网络连接"
|
||||||
|
}
|
||||||
|
},
|
||||||
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
|
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/mine-bow-data"
|
"path": "pages/mine-bow-data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/ota-wifi",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ function goBack() {
|
|||||||
<Container
|
<Container
|
||||||
:bgType="data.mode > 3 ? -1 : 0"
|
:bgType="data.mode > 3 ? -1 : 0"
|
||||||
bgColor="#000000"
|
bgColor="#000000"
|
||||||
:onBack="goBack"
|
:onBack="exit"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- ----- Banner 区:game 胜负展示图(仅 NvN 对抗模式)----- -->
|
<!-- ----- Banner 区:game 胜负展示图(仅 NvN 对抗模式)----- -->
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from "vue";
|
import {onMounted, onUnmounted, ref} from "vue";
|
||||||
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
|
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import AppFooter from "@/components/AppFooter.vue";
|
import AppFooter from "@/components/AppFooter.vue";
|
||||||
import UserHeader from "@/components/UserHeader.vue";
|
import UserHeader from "@/components/UserHeader.vue";
|
||||||
import Signin from "@/components/Signin.vue";
|
import Signin from "@/components/Signin.vue";
|
||||||
import BubbleTip from "@/components/BubbleTip.vue";
|
import BubbleTip from "@/components/BubbleTip.vue";
|
||||||
|
import OtaModal from "@/components/OtaModal.vue";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
checkUserBindAPI,
|
checkUserBindAPI,
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
getDeviceBatteryAPI,
|
getDeviceBatteryAPI,
|
||||||
|
getHardwareBoxTaskStatusAPI,
|
||||||
|
getHardwareBoxVersionAPI,
|
||||||
getHomeData,
|
getHomeData,
|
||||||
getMyDevicesAPI,
|
getMyDevicesAPI,
|
||||||
getScoreRankList,
|
getScoreRankList,
|
||||||
|
sendHardwareBoxUpdateAPI,
|
||||||
silentLoginAPI,
|
silentLoginAPI,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
import {topThreeColors} from "@/constants";
|
import {topThreeColors} from "@/constants";
|
||||||
@@ -26,6 +30,7 @@ const {
|
|||||||
updateConfig,
|
updateConfig,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateDevice,
|
updateDevice,
|
||||||
|
clearDevice,
|
||||||
getLvlName,
|
getLvlName,
|
||||||
getLvlNameByScore,
|
getLvlNameByScore,
|
||||||
updateOnline,
|
updateOnline,
|
||||||
@@ -36,6 +41,208 @@ const showModal = ref(false);
|
|||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const scoreRankList = ref([]);
|
const scoreRankList = ref([]);
|
||||||
|
|
||||||
|
// OTA 相关
|
||||||
|
const otaVisible = ref(false);
|
||||||
|
const otaState = ref("new_version");
|
||||||
|
const otaProgress = ref(0);
|
||||||
|
const otaInfo = ref({
|
||||||
|
versionNumber: "",
|
||||||
|
versionInfo: "",
|
||||||
|
resourceUrl: "",
|
||||||
|
forceUpdate: false,
|
||||||
|
});
|
||||||
|
const isStartingOta = ref(false);
|
||||||
|
let otaProgressTimer = null;
|
||||||
|
let otaStatusTimer = null;
|
||||||
|
let otaTimeoutTimer = null;
|
||||||
|
|
||||||
|
// 清理首页 OTA 更新定时器,避免弹窗关闭或页面卸载后继续轮询。
|
||||||
|
const clearOtaUpdateTimers = () => {
|
||||||
|
clearInterval(otaProgressTimer);
|
||||||
|
clearTimeout(otaStatusTimer);
|
||||||
|
clearTimeout(otaTimeoutTimer);
|
||||||
|
otaProgressTimer = null;
|
||||||
|
otaStatusTimer = null;
|
||||||
|
otaTimeoutTimer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 启动首页 OTA 本地进度动画,最终成功失败以后端任务状态为准。
|
||||||
|
const startOtaProgressAnimation = () => {
|
||||||
|
clearInterval(otaProgressTimer);
|
||||||
|
otaProgressTimer = setInterval(() => {
|
||||||
|
if (otaProgress.value >= 90) {
|
||||||
|
clearInterval(otaProgressTimer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const increment = Math.max(0.5, 2 - otaProgress.value / 60);
|
||||||
|
otaProgress.value = Math.min(90, otaProgress.value + increment);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取并保存后端返回的 OTA 版本信息,供弹窗展示和更新接口使用。
|
||||||
|
const applyOtaVersionInfo = (versionInfo) => {
|
||||||
|
otaInfo.value = {
|
||||||
|
versionNumber: versionInfo?.versionNumber || "",
|
||||||
|
versionInfo: versionInfo?.versionInfo || "",
|
||||||
|
resourceUrl: versionInfo?.resourceUrl || "",
|
||||||
|
forceUpdate: Number(versionInfo?.forceUpdate) === 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查当前设备盒子是否存在可升级版本。
|
||||||
|
const checkOtaUpdate = async () => {
|
||||||
|
let versionInfo;
|
||||||
|
try {
|
||||||
|
versionInfo = await getHardwareBoxVersionAPI();
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!versionInfo?.needUpdate) return;
|
||||||
|
applyOtaVersionInfo(versionInfo);
|
||||||
|
|
||||||
|
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
|
||||||
|
const now = Date.now();
|
||||||
|
if (!otaInfo.value.forceUpdate && dismissedAt && now - dismissedAt < 24 * 60 * 60 * 1000) return;
|
||||||
|
otaState.value = "new_version";
|
||||||
|
otaVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拼接 OTA WiFi 页参数,让未连 WiFi 的设备继续使用同一份版本信息。
|
||||||
|
const getOtaWifiUrl = () => {
|
||||||
|
const { versionNumber, resourceUrl } = otaInfo.value;
|
||||||
|
const query = [
|
||||||
|
`versionNumber=${encodeURIComponent(versionNumber)}`,
|
||||||
|
`resourceUrl=${encodeURIComponent(resourceUrl)}`,
|
||||||
|
].join("&");
|
||||||
|
return `/pages/ota-wifi?${query}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 OTA 弹窗暂不更新,强制更新时不允许关闭。
|
||||||
|
const handleOtaDismiss = () => {
|
||||||
|
if (otaInfo.value.forceUpdate) return;
|
||||||
|
uni.setStorageSync("ota_dismissed_at", Date.now());
|
||||||
|
otaVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将首页 OTA 直连更新流程标记为失败。
|
||||||
|
const failHomeOtaUpdate = () => {
|
||||||
|
clearOtaUpdateTimers();
|
||||||
|
isStartingOta.value = false;
|
||||||
|
otaState.value = "update_failure";
|
||||||
|
otaVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将首页 OTA 直连更新流程标记为成功。
|
||||||
|
const completeHomeOtaUpdate = () => {
|
||||||
|
clearOtaUpdateTimers();
|
||||||
|
isStartingOta.value = false;
|
||||||
|
otaProgress.value = 100;
|
||||||
|
setTimeout(() => {
|
||||||
|
otaState.value = "update_success";
|
||||||
|
otaVisible.value = true;
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 轮询首页直接发起的 OTA 更新任务状态。
|
||||||
|
const pollHomeOtaTaskStatus = (taskId) => {
|
||||||
|
clearTimeout(otaStatusTimer);
|
||||||
|
otaStatusTimer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const taskStatus = await getHardwareBoxTaskStatusAPI(taskId);
|
||||||
|
const status = Number(taskStatus?.status);
|
||||||
|
if (status === 2) {
|
||||||
|
completeHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === 3) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === 0 || status === 1) {
|
||||||
|
pollHomeOtaTaskStatus(taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
} catch (err) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设备盒子已连 WiFi 时,从首页直接传空 WiFi 信息发起 OTA 更新。
|
||||||
|
const startHomeOtaUpdate = async () => {
|
||||||
|
otaState.value = "update_progress";
|
||||||
|
otaVisible.value = true;
|
||||||
|
otaProgress.value = 0;
|
||||||
|
startOtaProgressAnimation();
|
||||||
|
otaTimeoutTimer = setTimeout(() => {
|
||||||
|
if (otaState.value === "update_progress") {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
}
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updateResult = await sendHardwareBoxUpdateAPI({
|
||||||
|
versionNumber: otaInfo.value.versionNumber,
|
||||||
|
wifiSsid: "",
|
||||||
|
wifiPassword: "",
|
||||||
|
resourceUrl: otaInfo.value.resourceUrl,
|
||||||
|
});
|
||||||
|
if (!updateResult?.taskId) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pollHomeOtaTaskStatus(updateResult.taskId);
|
||||||
|
} catch (err) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击立即更新时先判断设备是否在线并已通过 WiFi 联网,已联网则首页直接更新,否则跳转 WiFi 页面。
|
||||||
|
const handleOtaUpdate = async () => {
|
||||||
|
if (isStartingOta.value) return;
|
||||||
|
isStartingOta.value = true;
|
||||||
|
let deviceStatus;
|
||||||
|
try {
|
||||||
|
deviceStatus = await getDeviceBatteryAPI();
|
||||||
|
} catch (err) {
|
||||||
|
isStartingOta.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: "获取设备状态失败,请重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceStatus?.online !== true) {
|
||||||
|
isStartingOta.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: "请先开启智能弓",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(deviceStatus?.netType || "").toLowerCase() === "wifi") {
|
||||||
|
startHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isStartingOta.value = false;
|
||||||
|
otaVisible.value = false;
|
||||||
|
uni.navigateTo({ url: getOtaWifiUrl() });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 OTA 更新成功后的完成按钮,关闭结果弹窗。
|
||||||
|
const handleOtaDone = () => {
|
||||||
|
otaVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 OTA 更新失败后的重试按钮,重新走立即更新判断流程。
|
||||||
|
const handleOtaRetry = () => {
|
||||||
|
handleOtaUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
// 提取积分榜接口返回的榜单数组,兼容数组和对象两种返回格式。
|
// 提取积分榜接口返回的榜单数组,兼容数组和对象两种返回格式。
|
||||||
const getScoreRankData = (result) => {
|
const getScoreRankData = (result) => {
|
||||||
if (Array.isArray(result)) return result;
|
if (Array.isArray(result)) return result;
|
||||||
@@ -63,10 +270,18 @@ const toRankListPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(async (options) => {
|
||||||
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
||||||
const token = uni.getStorageSync(`${env}_token`);
|
const token = uni.getStorageSync(`${env}_token`);
|
||||||
|
|
||||||
|
// 检查是否从 OTA 更新页面返回
|
||||||
|
if (options && options.updateResult) {
|
||||||
|
otaState.value = options.updateResult;
|
||||||
|
otaVisible.value = true;
|
||||||
|
} else if (token || user.value.id) {
|
||||||
|
await checkOtaUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.value.id && !token) {
|
if (!user.value.id && !token) {
|
||||||
// showModal.value = true;
|
// showModal.value = true;
|
||||||
// try {
|
// try {
|
||||||
@@ -127,6 +342,8 @@ onShow(async () => {
|
|||||||
);
|
);
|
||||||
const data = await getDeviceBatteryAPI();
|
const data = await getDeviceBatteryAPI();
|
||||||
updateOnline(data.online);
|
updateOnline(data.online);
|
||||||
|
} else {
|
||||||
|
clearDevice();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,6 +355,10 @@ onMounted(async () => {
|
|||||||
console.log("全局配置:", config);
|
console.log("全局配置:", config);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearOtaUpdateTimers();
|
||||||
|
});
|
||||||
|
|
||||||
onShareAppMessage(() => {
|
onShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
|
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
|
||||||
@@ -158,6 +379,21 @@ onShareTimeline(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container :isHome="true" :showBackToGame="true">
|
<Container :isHome="true" :showBackToGame="true">
|
||||||
|
<!-- OTA 升级弹窗:使用 visible 控制显隐,description 为副标题,changelog 为详细说明 -->
|
||||||
|
<OtaModal
|
||||||
|
:visible="otaVisible"
|
||||||
|
:state="otaState"
|
||||||
|
:version="otaInfo.versionNumber"
|
||||||
|
:progress="otaProgress"
|
||||||
|
:description="''"
|
||||||
|
:changelog="otaInfo.versionInfo"
|
||||||
|
:forceUpdate="otaInfo.forceUpdate"
|
||||||
|
@update="handleOtaUpdate"
|
||||||
|
@skip="handleOtaDismiss"
|
||||||
|
@close="handleOtaDismiss"
|
||||||
|
@done="handleOtaDone"
|
||||||
|
@retry="handleOtaRetry"
|
||||||
|
/>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<view class="top-theme">
|
<view class="top-theme">
|
||||||
<!-- <image
|
<!-- <image
|
||||||
|
|||||||
@@ -30,11 +30,64 @@ const playersSorted = ref([]);
|
|||||||
const playersScores = ref([]);
|
const playersScores = ref([]);
|
||||||
const halfTimeTip = ref(false);
|
const halfTimeTip = ref(false);
|
||||||
const halfRest = ref(false);
|
const halfRest = ref(false);
|
||||||
|
const HALF_REST_SECONDS = 20;
|
||||||
|
const halfRestRemain = ref(HALF_REST_SECONDS);
|
||||||
|
let halfRestTimer = null;
|
||||||
/** 控制设备离线提示弹窗的显示状态 */
|
/** 控制设备离线提示弹窗的显示状态 */
|
||||||
const showOfflineModal = ref(false);
|
const showOfflineModal = ref(false);
|
||||||
/** 记录每位玩家当前半场连续 X 环数,key 为 playerId,用于触发 tententen 音效 */
|
/** 记录每位玩家当前半场连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
|
||||||
const xRingStreaks = ref({});
|
const xRingStreaks = ref({});
|
||||||
|
|
||||||
|
function clearHalfRestCountdown() {
|
||||||
|
if (halfRestTimer) {
|
||||||
|
clearInterval(halfRestTimer);
|
||||||
|
halfRestTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHalfRestSeconds(battleInfo) {
|
||||||
|
const remainCandidates = [
|
||||||
|
battleInfo?.halfRestRemain,
|
||||||
|
battleInfo?.halfRestRemainSeconds,
|
||||||
|
battleInfo?.restRemain,
|
||||||
|
battleInfo?.restRemainSeconds,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const item of remainCandidates) {
|
||||||
|
const remain = Number(item);
|
||||||
|
if (Number.isFinite(remain) && remain > 0 && remain <= HALF_REST_SECONDS) {
|
||||||
|
return Math.ceil(remain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = Number(battleInfo?.halfRestEndTime ?? battleInfo?.restEndTime);
|
||||||
|
if (!Number.isFinite(endTime) || endTime <= 0) return HALF_REST_SECONDS;
|
||||||
|
|
||||||
|
const timestamp = endTime < 1e12 ? endTime * 1000 : endTime;
|
||||||
|
const diffSeconds = (timestamp - Date.now()) / 1000;
|
||||||
|
if (diffSeconds > 0 && diffSeconds <= HALF_REST_SECONDS) {
|
||||||
|
return Math.ceil(diffSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HALF_REST_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startHalfRestCountdown(seconds = HALF_REST_SECONDS) {
|
||||||
|
clearHalfRestCountdown();
|
||||||
|
halfRestRemain.value = Math.max(0, Math.ceil(Number(seconds) || HALF_REST_SECONDS));
|
||||||
|
|
||||||
|
if (halfRestRemain.value <= 0) return;
|
||||||
|
|
||||||
|
halfRestTimer = setInterval(() => {
|
||||||
|
if (halfRestRemain.value <= 1) {
|
||||||
|
halfRestRemain.value = 0;
|
||||||
|
clearHalfRestCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
halfRestRemain.value -= 1;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
const currentPlayer = computed(() =>
|
const currentPlayer = computed(() =>
|
||||||
players.value.find((player) => String(player?.id) === String(user.value.id))
|
players.value.find((player) => String(player?.id) === String(user.value.id))
|
||||||
);
|
);
|
||||||
@@ -96,8 +149,7 @@ function recoverData(battleInfo, { force = false } = {}) {
|
|||||||
halfTimeTip.value = true;
|
halfTimeTip.value = true;
|
||||||
halfRest.value = true;
|
halfRest.value = true;
|
||||||
tips.value = "准备下半场";
|
tips.value = "准备下半场";
|
||||||
// 剩余休息时间
|
startHalfRestCountdown(getHalfRestSeconds(battleInfo));
|
||||||
// const remain = (Date.now() - battleInfo.timeoutTime) / 1000;
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.$emit("update-remain", 0);
|
uni.$emit("update-remain", 0);
|
||||||
}, 200);
|
}, 200);
|
||||||
@@ -128,23 +180,27 @@ onLoad(async (options) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测指定玩家连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
* 检测指定玩家连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
||||||
* @param {number|string} playerId - 本次射手的 ID(大乱斗中 ShootResult 保留 playerId)
|
* @param {number|string} playerId - 本次射手的 ID(大乱斗中 ShootResult 保留 playerId)
|
||||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||||
*/
|
*/
|
||||||
function checkAndPlayTententen(playerId, isXRing) {
|
function isTenPlusRing(shot) {
|
||||||
|
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndPlayTententen(playerId, isTenPlusRingShot) {
|
||||||
if (!playerId) return;
|
if (!playerId) return;
|
||||||
const id = parseInt(playerId);
|
const id = parseInt(playerId);
|
||||||
if (isXRing) {
|
if (isTenPlusRingShot) {
|
||||||
xRingStreaks.value[id] = (xRingStreaks.value[id] || 0) + 1;
|
xRingStreaks.value[id] = (xRingStreaks.value[id] || 0) + 1;
|
||||||
// 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放
|
// 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
|
||||||
if (xRingStreaks.value[id] >= 3) {
|
if (xRingStreaks.value[id] >= 3) {
|
||||||
xRingStreaks.value[id] = 0;
|
xRingStreaks.value[id] = 0;
|
||||||
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
|
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
|
||||||
nextTick(() => audioManager.play("tententen", false));
|
nextTick(() => audioManager.play("tententen", false));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非 X 环则重置该玩家的连续计数
|
// 低于 10 环或未上靶则重置该玩家的连续计数
|
||||||
xRingStreaks.value[id] = 0;
|
xRingStreaks.value[id] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,6 +208,7 @@ function checkAndPlayTententen(playerId, isXRing) {
|
|||||||
async function onReceiveMessage(msg) {
|
async function onReceiveMessage(msg) {
|
||||||
if (Array.isArray(msg)) return;
|
if (Array.isArray(msg)) return;
|
||||||
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
||||||
|
clearHalfRestCountdown();
|
||||||
halfTimeTip.value = false;
|
halfTimeTip.value = false;
|
||||||
halfRest.value = false;
|
halfRest.value = false;
|
||||||
recoverData(msg);
|
recoverData(msg);
|
||||||
@@ -166,22 +223,23 @@ async function onReceiveMessage(msg) {
|
|||||||
// 对比更新后数据找出箭数增加的玩家(即本次射手),并读取其最新箭的 ring 数据
|
// 对比更新后数据找出箭数增加的玩家(即本次射手),并读取其最新箭的 ring 数据
|
||||||
const newRound = playersScores.value[playersScores.value.length - 1] || {};
|
const newRound = playersScores.value[playersScores.value.length - 1] || {};
|
||||||
let shooterId = null;
|
let shooterId = null;
|
||||||
let isXRing = false;
|
let isTenPlusRingShot = false;
|
||||||
for (const pid of Object.keys(newRound)) {
|
for (const pid of Object.keys(newRound)) {
|
||||||
const newLen = (newRound[pid] || []).length;
|
const newLen = (newRound[pid] || []).length;
|
||||||
if (newLen > (prevCounts[pid] || 0)) {
|
if (newLen > (prevCounts[pid] || 0)) {
|
||||||
shooterId = parseInt(pid);
|
shooterId = parseInt(pid);
|
||||||
const shot = newRound[pid][newLen - 1];
|
const shot = newRound[pid][newLen - 1];
|
||||||
isXRing = !!(shot?.ringX && shot?.ring);
|
isTenPlusRingShot = isTenPlusRing(shot);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 检测同一玩家三箭全 X 环,触发 tententen 音效
|
// 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
|
||||||
checkAndPlayTententen(shooterId, isXRing);
|
checkAndPlayTententen(shooterId, isTenPlusRingShot);
|
||||||
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
|
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
|
||||||
halfTimeTip.value = true;
|
halfTimeTip.value = true;
|
||||||
halfRest.value = true;
|
halfRest.value = true;
|
||||||
tips.value = "准备下半场";
|
tips.value = "准备下半场";
|
||||||
|
startHalfRestCountdown();
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 全部跳转到新结算页
|
// 全部跳转到新结算页
|
||||||
@@ -202,6 +260,7 @@ onBeforeUnmount(() => {
|
|||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
|
clearHalfRestCountdown();
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
uni.$off("socket-inbox", onReceiveMessage);
|
||||||
audioManager.stopAll();
|
audioManager.stopAll();
|
||||||
});
|
});
|
||||||
@@ -268,7 +327,7 @@ onShow(async () => {
|
|||||||
>
|
>
|
||||||
<view class="half-time-tip">
|
<view class="half-time-tip">
|
||||||
<text>上半场结束,休息一下吧:)</text>
|
<text>上半场结束,休息一下吧:)</text>
|
||||||
<text>20秒后开始下半场</text>
|
<text>{{ halfRestRemain }}秒后开始下半场</text>
|
||||||
</view>
|
</view>
|
||||||
</ScreenHint>
|
</ScreenHint>
|
||||||
<!-- 设备离线提示弹窗 -->
|
<!-- 设备离线提示弹窗 -->
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const showTip = ref(false);
|
|||||||
const confirmBindTip = ref(false);
|
const confirmBindTip = ref(false);
|
||||||
const addDevice = ref();
|
const addDevice = ref();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { updateDevice } = store;
|
const { updateDevice, clearDevice } = store;
|
||||||
const { user, device } = storeToRefs(store);
|
const { user, device } = storeToRefs(store);
|
||||||
const justBind = ref(false);
|
const justBind = ref(false);
|
||||||
const calibration = ref(false);
|
const calibration = ref(false);
|
||||||
@@ -86,13 +86,26 @@ const toFristTryPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const unbindDevice = async () => {
|
const unbindDevice = async () => {
|
||||||
await unbindDeviceAPI(device.value.deviceId);
|
try {
|
||||||
|
await unbindDeviceAPI(device.value.deviceId);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.type === "DEVICE_BIND_INVALID") {
|
||||||
|
uni.setStorageSync("calibration", false);
|
||||||
|
clearDevice();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
uni.setStorageSync("calibration", false);
|
uni.setStorageSync("calibration", false);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "解绑成功",
|
title: "解绑成功",
|
||||||
icon: "success",
|
icon: "success",
|
||||||
});
|
});
|
||||||
device.value = {};
|
clearDevice();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 连接wifi跳转到wifi列表页面 */
|
||||||
|
const joinWifi = () => {
|
||||||
|
uni.navigateTo({ url: "/pages/ota-wifi" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const toDeviceIntroPage = () => {
|
const toDeviceIntroPage = () => {
|
||||||
@@ -124,8 +137,23 @@ const goCalibration = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onShow(() => {
|
const syncDeviceBinding = async () => {
|
||||||
|
if (!user.value.id) return;
|
||||||
|
try {
|
||||||
|
const devices = await getMyDevicesAPI();
|
||||||
|
if (devices.bindings && devices.bindings.length) {
|
||||||
|
updateDevice(devices.bindings[0].deviceId, devices.bindings[0].deviceName);
|
||||||
|
} else {
|
||||||
|
clearDevice();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("sync device binding error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onShow(async () => {
|
||||||
calibration.value = uni.getStorageSync("calibration");
|
calibration.value = uni.getStorageSync("calibration");
|
||||||
|
await syncDeviceBinding();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -299,6 +327,11 @@ onShow(() => {
|
|||||||
>解绑</SButton
|
>解绑</SButton
|
||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
|
<view :style="{ marginTop: '20rpx' }">
|
||||||
|
<SButton :onClick="() => $clickSound(joinWifi)" width="80vw" :rounded="40"
|
||||||
|
>设备连接WIFI</SButton
|
||||||
|
>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const start = ref(false);
|
|||||||
const scores = ref([]);
|
const scores = ref([]);
|
||||||
const isSvip = ref(false);
|
const isSvip = ref(false);
|
||||||
const total = 12;
|
const total = 12;
|
||||||
/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */
|
/** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */
|
||||||
const xRingStreak = ref(0);
|
const xRingStreak = ref(0);
|
||||||
const practiseResult = ref({});
|
const practiseResult = ref({});
|
||||||
const practiseId = ref("");
|
const practiseId = ref("");
|
||||||
@@ -62,19 +62,23 @@ const onOver = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效
|
* 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效
|
||||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||||
*/
|
*/
|
||||||
function checkAndPlayTententen(isXRing) {
|
function isTenPlusRing(shot) {
|
||||||
if (isXRing) {
|
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndPlayTententen(isTenPlusRingShot) {
|
||||||
|
if (isTenPlusRingShot) {
|
||||||
xRingStreak.value += 1;
|
xRingStreak.value += 1;
|
||||||
// 连续 3 箭均为 X 环,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
// 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
||||||
if (xRingStreak.value >= 3) {
|
if (xRingStreak.value >= 3) {
|
||||||
xRingStreak.value = 0;
|
xRingStreak.value = 0;
|
||||||
nextTick(() => audioManager.play("tententen", false));
|
nextTick(() => audioManager.play("tententen", false));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非 X 环则重置连续计数
|
// 低于 10 环或未上靶则重置连续计数
|
||||||
xRingStreak.value = 0;
|
xRingStreak.value = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,10 +88,10 @@ async function onReceiveMessage(msg) {
|
|||||||
const prevLen = scores.value.length;
|
const prevLen = scores.value.length;
|
||||||
isSvip.value = msg.sVip === true;
|
isSvip.value = msg.sVip === true;
|
||||||
scores.value = msg.details;
|
scores.value = msg.details;
|
||||||
// 有新箭时取最后一箭判断是否 X 环并检测连续计数
|
// 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数
|
||||||
if (scores.value.length > prevLen) {
|
if (scores.value.length > prevLen) {
|
||||||
const latestArrow = scores.value[scores.value.length - 1];
|
const latestArrow = scores.value[scores.value.length - 1];
|
||||||
checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring));
|
checkAndPlayTententen(isTenPlusRing(latestArrow));
|
||||||
}
|
}
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
// setTimeout(onOver, 1500);
|
// setTimeout(onOver, 1500);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const start = ref(false);
|
|||||||
const scores = ref([]);
|
const scores = ref([]);
|
||||||
const isSvip = ref(false);
|
const isSvip = ref(false);
|
||||||
const total = 36;
|
const total = 36;
|
||||||
/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */
|
/** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */
|
||||||
const xRingStreak = ref(0);
|
const xRingStreak = ref(0);
|
||||||
const practiseResult = ref({});
|
const practiseResult = ref({});
|
||||||
const practiseId = ref("");
|
const practiseId = ref("");
|
||||||
@@ -61,19 +61,23 @@ const onOver = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效
|
* 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效
|
||||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||||
*/
|
*/
|
||||||
function checkAndPlayTententen(isXRing) {
|
function isTenPlusRing(shot) {
|
||||||
if (isXRing) {
|
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndPlayTententen(isTenPlusRingShot) {
|
||||||
|
if (isTenPlusRingShot) {
|
||||||
xRingStreak.value += 1;
|
xRingStreak.value += 1;
|
||||||
// 连续 3 箭均为 X 环,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
// 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
||||||
if (xRingStreak.value >= 3) {
|
if (xRingStreak.value >= 3) {
|
||||||
xRingStreak.value = 0;
|
xRingStreak.value = 0;
|
||||||
nextTick(() => audioManager.play("tententen", false));
|
nextTick(() => audioManager.play("tententen", false));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非 X 环则重置连续计数
|
// 低于 10 环或未上靶则重置连续计数
|
||||||
xRingStreak.value = 0;
|
xRingStreak.value = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,10 +87,10 @@ async function onReceiveMessage(msg) {
|
|||||||
const prevLen = scores.value.length;
|
const prevLen = scores.value.length;
|
||||||
isSvip.value = msg.sVip === true;
|
isSvip.value = msg.sVip === true;
|
||||||
scores.value = msg.details;
|
scores.value = msg.details;
|
||||||
// 有新箭时取最后一箭判断是否 X 环并检测连续计数
|
// 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数
|
||||||
if (scores.value.length > prevLen) {
|
if (scores.value.length > prevLen) {
|
||||||
const latestArrow = scores.value[scores.value.length - 1];
|
const latestArrow = scores.value[scores.value.length - 1];
|
||||||
checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring));
|
checkAndPlayTententen(isTenPlusRing(latestArrow));
|
||||||
}
|
}
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
setTimeout(onOver, 1500);
|
setTimeout(onOver, 1500);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const battleWay = ref(0);
|
|||||||
const lastToSomeoneShootKey = ref("");
|
const lastToSomeoneShootKey = ref("");
|
||||||
/** 控制设备离线提示弹窗的显示状态 */
|
/** 控制设备离线提示弹窗的显示状态 */
|
||||||
const showOfflineModal = ref(false);
|
const showOfflineModal = ref(false);
|
||||||
/** 记录每位玩家当前轮连续 X 环数,key 为 playerId,用于触发 tententen 音效 */
|
/** 记录每位玩家当前轮连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
|
||||||
const xRingStreaks = ref({});
|
const xRingStreaks = ref({});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,22 +234,26 @@ function onNewRound(msg, prevRound) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测指定射手连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
* 检测指定射手连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
||||||
* @param {number} shooterId - 本次射手的 ID(取自 currentShooterId.value)
|
* @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 (!shooterId) return;
|
||||||
if (isXRing) {
|
if (isTenPlusRingShot) {
|
||||||
xRingStreaks.value[shooterId] = (xRingStreaks.value[shooterId] || 0) + 1;
|
xRingStreaks.value[shooterId] = (xRingStreaks.value[shooterId] || 0) + 1;
|
||||||
// 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放
|
// 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
|
||||||
if (xRingStreaks.value[shooterId] >= 3) {
|
if (xRingStreaks.value[shooterId] >= 3) {
|
||||||
xRingStreaks.value[shooterId] = 0;
|
xRingStreaks.value[shooterId] = 0;
|
||||||
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
|
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
|
||||||
nextTick(() => audioManager.play("tententen", false));
|
nextTick(() => audioManager.play("tententen", false));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非 X 环则重置该玩家的连续计数
|
// 低于 10 环或未上靶则重置该玩家的连续计数
|
||||||
xRingStreaks.value[shooterId] = 0;
|
xRingStreaks.value[shooterId] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,9 +272,9 @@ async function onReceiveMessage(msg) {
|
|||||||
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||||
showRoundTip.value = false;
|
showRoundTip.value = false;
|
||||||
recoverData(msg, {arrowOnly: true});
|
recoverData(msg, {arrowOnly: true});
|
||||||
// 检测同一玩家三箭全 X 环,触发 tententen 音效
|
// 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
|
||||||
// currentShooterId 在 ToSomeoneShoot 时写入,ShootResult 不会覆盖,可靠识别本次射手
|
// currentShooterId 在 ToSomeoneShoot 时写入,ShootResult 不会覆盖,可靠识别本次射手
|
||||||
checkAndPlayTententen(currentShooterId.value, !!(msg.shootData?.ringX && msg.shootData?.ring));
|
checkAndPlayTententen(currentShooterId.value, isTenPlusRing(msg.shootData));
|
||||||
} else if (msg.type === MESSAGETYPESV2.NewRound) {
|
} else if (msg.type === MESSAGETYPESV2.NewRound) {
|
||||||
// 在进入延迟前先捕获当前轮次,供 onNewRound 使用,防止 800ms 内 ToSomeoneShoot 提前更新 currentRound 造成 Tip 展示错轮
|
// 在进入延迟前先捕获当前轮次,供 onNewRound 使用,防止 800ms 内 ToSomeoneShoot 提前更新 currentRound 造成 Tip 展示错轮
|
||||||
const prevRound = currentRound.value;
|
const prevRound = currentRound.value;
|
||||||
|
|||||||
@@ -432,7 +432,10 @@ function playAudioKeys(keys, { interrupt = false, timeout } = {}) {
|
|||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const timer = setTimeout(waiter.done, waitTime);
|
const timer = setTimeout(() => {
|
||||||
|
audioManager.recoverIfStale(expectedKey);
|
||||||
|
waiter.done();
|
||||||
|
}, waitTime);
|
||||||
audioWaiters.add(waiter);
|
audioWaiters.add(waiter);
|
||||||
audioManager.play(audioKeys, interrupt);
|
audioManager.play(audioKeys, interrupt);
|
||||||
});
|
});
|
||||||
@@ -473,17 +476,14 @@ function updateTeams(battleInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateGoldenRound(battleInfo) {
|
function updateGoldenRound(battleInfo) {
|
||||||
const rounds = Array.isArray(battleInfo?.rounds) ? battleInfo.rounds : [];
|
if (!battleInfo?.current?.goldRound) {
|
||||||
const currentRoundNo = Number(battleInfo?.current?.round || 0);
|
goldenRound.value = 0;
|
||||||
const currentRoundInfo = rounds.find((round) => Number(round?.round) === currentRoundNo);
|
return;
|
||||||
const activeGoldRoundInfo = rounds.find(
|
}
|
||||||
(round) => Number(round?.goldRound || 0) > 0 && round?.status === 1
|
const rounds = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : [];
|
||||||
);
|
const finishedGoldCount = rounds.filter((round) => !!round?.ifGold).length;
|
||||||
const roundGoldRound = Number(currentRoundInfo?.goldRound || 0);
|
// goldenRound.value = Math.max(1, finishedGoldCount + (battleInfo.current?.playerId ? 1 : 0));
|
||||||
const activeGoldRound = Number(activeGoldRoundInfo?.goldRound || 0);
|
goldenRound.value = Math.max(1, finishedGoldCount);
|
||||||
const currentGoldRound = Number(battleInfo?.current?.goldRound || 0);
|
|
||||||
const nextGoldRound = roundGoldRound || activeGoldRound || currentGoldRound;
|
|
||||||
goldenRound.value = nextGoldRound > 0 ? nextGoldRound : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore an info snapshot whose eventType points at the NewRound phase.
|
// 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;
|
if (!shooterId) return false;
|
||||||
const id = String(shooterId);
|
const id = String(shooterId);
|
||||||
if (!isXRing) {
|
if (!isTenPlusRingShot) {
|
||||||
xRingStreaks.value[id] = 0;
|
xRingStreaks.value[id] = 0;
|
||||||
saveXRingStreaks();
|
saveXRingStreaks();
|
||||||
return false;
|
return false;
|
||||||
@@ -909,7 +913,7 @@ async function runShootResultTask(task) {
|
|||||||
|
|
||||||
const isTententen = updateXRingStreak(
|
const isTententen = updateXRingStreak(
|
||||||
currentShooterId.value,
|
currentShooterId.value,
|
||||||
!!(battleInfo.shootData?.ringX && battleInfo.shootData?.ring)
|
isTenPlusRing(battleInfo.shootData)
|
||||||
);
|
);
|
||||||
const audioKeys = buildShootResultAudioKeys(battleInfo.shootData);
|
const audioKeys = buildShootResultAudioKeys(battleInfo.shootData);
|
||||||
if (isTententen) audioKeys.push("tententen");
|
if (isTententen) audioKeys.push("tententen");
|
||||||
@@ -1255,7 +1259,7 @@ onShow(() => {
|
|||||||
<view class="offline-modal">
|
<view class="offline-modal">
|
||||||
<text class="offline-title">设备已离线</text>
|
<text class="offline-title">设备已离线</text>
|
||||||
<text class="offline-desc">检测到设备已断开连接,请检查设备后继续比赛</text>
|
<text class="offline-desc">检测到设备已断开连接,请检查设备后继续比赛</text>
|
||||||
<SButton @click="showOfflineModal = false">我知道了</SButton>
|
<SButton :onClick="() => (showOfflineModal = false)">我知道了</SButton>
|
||||||
</view>
|
</view>
|
||||||
</SModal>
|
</SModal>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 237 B |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 277 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 603 B |
|
After Width: | Height: | Size: 395 B |
|
After Width: | Height: | Size: 457 B |
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 494 B |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 548 B |
@@ -149,6 +149,10 @@ export default defineStore("store", {
|
|||||||
this.device.deviceId = deviceId;
|
this.device.deviceId = deviceId;
|
||||||
this.device.deviceName = deviceName;
|
this.device.deviceName = deviceName;
|
||||||
},
|
},
|
||||||
|
clearDevice() {
|
||||||
|
this.device = getDefaultDevice();
|
||||||
|
this.online = false;
|
||||||
|
},
|
||||||
async updateConfig(config) {
|
async updateConfig(config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
if (this.user.scores !== undefined) {
|
if (this.user.scores !== undefined) {
|
||||||
|
|||||||