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