Compare commits
6
Commits
e7f073ed7c
...
fix-ota
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75cfcd66ee | ||
|
|
7955b37ec5 | ||
|
|
9e6ad4cf50 | ||
|
|
6c55d32f90 | ||
|
|
63fb4ae85a | ||
|
|
2628a17b3f |
@@ -28,6 +28,7 @@
|
|||||||
const {
|
const {
|
||||||
updateUser,
|
updateUser,
|
||||||
updateOnline,
|
updateOnline,
|
||||||
|
showDeviceChargingDialog,
|
||||||
clearSessionState,
|
clearSessionState,
|
||||||
clearDevice
|
clearDevice
|
||||||
} = store;
|
} = store;
|
||||||
@@ -82,6 +83,10 @@
|
|||||||
uni.setStorageSync("calibration", false);
|
uni.setStorageSync("calibration", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDeviceCharging() {
|
||||||
|
showDeviceChargingDialog();
|
||||||
|
}
|
||||||
|
|
||||||
function onDeviceShoot() {
|
function onDeviceShoot() {
|
||||||
// audioManager.play("射箭声音")
|
// audioManager.play("射箭声音")
|
||||||
}
|
}
|
||||||
@@ -119,6 +124,7 @@
|
|||||||
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);
|
uni.$on("device-bind-invalid", onDeviceBindInvalid);
|
||||||
|
uni.$on("device-charging", onDeviceCharging);
|
||||||
if (typeof uni.offNetworkStatusChange === "function") {
|
if (typeof uni.offNetworkStatusChange === "function") {
|
||||||
uni.offNetworkStatusChange(onNetworkStatusChange);
|
uni.offNetworkStatusChange(onNetworkStatusChange);
|
||||||
}
|
}
|
||||||
@@ -144,6 +150,7 @@
|
|||||||
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);
|
uni.$off("device-bind-invalid", onDeviceBindInvalid);
|
||||||
|
uni.$off("device-charging", onDeviceCharging);
|
||||||
if (typeof uni.offNetworkStatusChange === "function") {
|
if (typeof uni.offNetworkStatusChange === "function") {
|
||||||
uni.offNetworkStatusChange(onNetworkStatusChange);
|
uni.offNetworkStatusChange(onNetworkStatusChange);
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-22
@@ -112,6 +112,8 @@ export const audioFils = {
|
|||||||
const AUDIO_LOAD_TIMEOUT_MS = 5000;
|
const AUDIO_LOAD_TIMEOUT_MS = 5000;
|
||||||
const AUDIO_WARM_CONCURRENCY = 3;
|
const AUDIO_WARM_CONCURRENCY = 3;
|
||||||
const AUDIO_WARM_RETRIES = 1;
|
const AUDIO_WARM_RETRIES = 1;
|
||||||
|
const AUDIO_INSTANCE_LIMIT = 24;
|
||||||
|
const AUDIO_QUEUE_COMPACT_THRESHOLD = 12;
|
||||||
const AUDIO_WARM_PRIORITY_KEYS = [
|
const AUDIO_WARM_PRIORITY_KEYS = [
|
||||||
"点击按钮",
|
"点击按钮",
|
||||||
"设备连接已断开",
|
"设备连接已断开",
|
||||||
@@ -162,6 +164,8 @@ class AudioManager {
|
|||||||
this.loadingKeyPromises = new Map();
|
this.loadingKeyPromises = new Map();
|
||||||
this.pendingPlayKeys = new Set();
|
this.pendingPlayKeys = new Set();
|
||||||
this.cacheWritePromises = new Map();
|
this.cacheWritePromises = new Map();
|
||||||
|
this.audioLastUsedAt = new Map();
|
||||||
|
this.warmPriorityPromise = null;
|
||||||
this.warmAllPromise = null;
|
this.warmAllPromise = null;
|
||||||
|
|
||||||
// 连续播放队列相关属性
|
// 连续播放队列相关属性
|
||||||
@@ -253,14 +257,74 @@ class AudioManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getWarmupKeys() {
|
getWarmupKeys() {
|
||||||
return Array.from(
|
return Array.from(new Set(AUDIO_WARM_PRIORITY_KEYS));
|
||||||
new Set([...AUDIO_WARM_PRIORITY_KEYS, ...Object.keys(audioFils)])
|
}
|
||||||
);
|
|
||||||
|
getAllAudioKeys() {
|
||||||
|
return Object.keys(audioFils);
|
||||||
|
}
|
||||||
|
|
||||||
|
touchAudioKey(key) {
|
||||||
|
this.audioLastUsedAt.set(key, Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyAudioInstance(key) {
|
||||||
|
const audio = this.audioMap.get(key);
|
||||||
|
if (!audio) return false;
|
||||||
|
|
||||||
|
this.clearPlayWatchdog(key);
|
||||||
|
this.audioMap.delete(key);
|
||||||
|
this.readyMap.delete(key);
|
||||||
|
this.allowPlayMap.delete(key);
|
||||||
|
this.audioLastUsedAt.delete(key);
|
||||||
|
try {
|
||||||
|
audio.destroy();
|
||||||
|
} catch (_) {}
|
||||||
|
// destroy 可能同步触发 onStop,最后再清一次对应状态。
|
||||||
|
this.allowPlayMap.delete(key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneAudioInstances(preserveKey) {
|
||||||
|
while (this.audioMap.size >= AUDIO_INSTANCE_LIMIT) {
|
||||||
|
const candidate = Array.from(this.audioMap.keys())
|
||||||
|
.filter(
|
||||||
|
(key) =>
|
||||||
|
key !== preserveKey &&
|
||||||
|
key !== this.currentPlayingKey &&
|
||||||
|
!this.loadingKeyPromises.has(key)
|
||||||
|
)
|
||||||
|
.sort(
|
||||||
|
(left, right) =>
|
||||||
|
(this.audioLastUsedAt.get(left) || 0) -
|
||||||
|
(this.audioLastUsedAt.get(right) || 0)
|
||||||
|
)[0];
|
||||||
|
if (!candidate || !this.destroyAudioInstance(candidate)) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compactSequenceQueue(force = false) {
|
||||||
|
if (!this.isSequenceRunning || this.sequenceIndex <= 0) return;
|
||||||
|
if (!force && this.sequenceIndex < AUDIO_QUEUE_COMPACT_THRESHOLD) return;
|
||||||
|
this.sequenceQueue = this.sequenceQueue.slice(this.sequenceIndex);
|
||||||
|
this.sequenceIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRuntimeStats() {
|
||||||
|
return {
|
||||||
|
audioInstanceCount: this.audioMap.size,
|
||||||
|
loadingAudioCount: this.loadingKeyPromises.size,
|
||||||
|
pendingPlayCount: this.pendingPlayKeys.size,
|
||||||
|
sequenceQueueLength: this.isSequenceRunning
|
||||||
|
? Math.max(this.sequenceQueue.length - this.sequenceIndex, 0)
|
||||||
|
: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureAudio(key) {
|
ensureAudio(key) {
|
||||||
if (!audioFils[key]) return Promise.resolve(false);
|
if (!audioFils[key]) return Promise.resolve(false);
|
||||||
if (this.readyMap.get(key) && this.audioMap.has(key)) {
|
if (this.readyMap.get(key) && this.audioMap.has(key)) {
|
||||||
|
this.touchAudioKey(key);
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,23 +376,39 @@ class AudioManager {
|
|||||||
return this.warmKeys(["点击按钮"], { concurrency: 1, retries: 1 });
|
return this.warmKeys(["点击按钮"], { concurrency: 1, retries: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
warmAll() {
|
warmCommon() {
|
||||||
if (this.warmAllPromise) return this.warmAllPromise;
|
if (this.warmPriorityPromise) return this.warmPriorityPromise;
|
||||||
|
|
||||||
debugLog("开始后台分级预热全部语音");
|
debugLog("开始后台预热常用语音");
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.warmAllPromise = this.warmKeys(this.getWarmupKeys())
|
this.warmPriorityPromise = this.warmKeys(this.getWarmupKeys())
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
debugLog("后台语音预热异常", error);
|
debugLog("常用语音预热异常", error);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
debugLog("后台语音预热结束", this.getLoadProgress());
|
debugLog("常用语音预热结束", this.getRuntimeStats());
|
||||||
|
});
|
||||||
|
return this.warmPriorityPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
warmAll() {
|
||||||
|
if (this.warmAllPromise) return this.warmAllPromise;
|
||||||
|
|
||||||
|
debugLog("音频测试页开始遍历全部语音");
|
||||||
|
this.isLoading = true;
|
||||||
|
this.warmAllPromise = this.warmKeys(this.getAllAudioKeys())
|
||||||
|
.catch((error) => {
|
||||||
|
debugLog("全部语音遍历异常", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
debugLog("全部语音遍历结束", this.getRuntimeStats());
|
||||||
});
|
});
|
||||||
return this.warmAllPromise;
|
return this.warmAllPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保留旧入口兼容测试页或其他历史调用,实际行为改为非阻塞后台预热。
|
// 音频测试页保留全量遍历;实例池会及时淘汰旧实例,避免同时常驻全部语音。
|
||||||
initAudios() {
|
initAudios() {
|
||||||
return this.warmAll();
|
return this.warmAll();
|
||||||
}
|
}
|
||||||
@@ -366,6 +446,7 @@ class AudioManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pruneAudioInstances(key);
|
||||||
const audio = uni.createInnerAudioContext();
|
const audio = uni.createInnerAudioContext();
|
||||||
audio.autoplay = false;
|
audio.autoplay = false;
|
||||||
try {
|
try {
|
||||||
@@ -377,6 +458,7 @@ class AudioManager {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
this.allowPlayMap.set(key, false);
|
this.allowPlayMap.set(key, false);
|
||||||
audio.onPlay(() => {
|
audio.onPlay(() => {
|
||||||
|
if (this.audioMap.get(key) !== audio) return;
|
||||||
if (!this.allowPlayMap.get(key)) {
|
if (!this.allowPlayMap.get(key)) {
|
||||||
try {
|
try {
|
||||||
audio.stop();
|
audio.stop();
|
||||||
@@ -414,6 +496,7 @@ class AudioManager {
|
|||||||
}
|
}
|
||||||
this.readyMap.set(key, false);
|
this.readyMap.set(key, false);
|
||||||
this.audioMap.delete(key);
|
this.audioMap.delete(key);
|
||||||
|
this.audioLastUsedAt.delete(key);
|
||||||
try {
|
try {
|
||||||
audio.destroy();
|
audio.destroy();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -446,6 +529,7 @@ class AudioManager {
|
|||||||
if (!isCurrentGeneration()) {
|
if (!isCurrentGeneration()) {
|
||||||
clearLoadTimeout();
|
clearLoadTimeout();
|
||||||
this.audioMap.delete(key);
|
this.audioMap.delete(key);
|
||||||
|
this.audioLastUsedAt.delete(key);
|
||||||
try {
|
try {
|
||||||
audio.destroy();
|
audio.destroy();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -480,6 +564,7 @@ class AudioManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
audio.onEnded(() => {
|
audio.onEnded(() => {
|
||||||
|
if (this.audioMap.get(key) !== audio) return;
|
||||||
this.finishPlayback(key, {
|
this.finishPlayback(key, {
|
||||||
advanceSequence: true,
|
advanceSequence: true,
|
||||||
emitEnded: true,
|
emitEnded: true,
|
||||||
@@ -487,10 +572,12 @@ class AudioManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
audio.onStop(() => {
|
audio.onStop(() => {
|
||||||
|
if (this.audioMap.get(key) !== audio) return;
|
||||||
this.finishPlayback(key);
|
this.finishPlayback(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.audioMap.set(key, audio);
|
this.audioMap.set(key, audio);
|
||||||
|
this.touchAudioKey(key);
|
||||||
audio.src = realSrc;
|
audio.src = realSrc;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -700,14 +787,7 @@ class AudioManager {
|
|||||||
const loadingPromise = this.loadingKeyPromises.get(key);
|
const loadingPromise = this.loadingKeyPromises.get(key);
|
||||||
if (loadingPromise) return loadingPromise;
|
if (loadingPromise) return loadingPromise;
|
||||||
|
|
||||||
this.clearPlayWatchdog(key);
|
this.destroyAudioInstance(key);
|
||||||
const oldAudio = this.audioMap.get(key);
|
|
||||||
if (oldAudio) {
|
|
||||||
try {
|
|
||||||
oldAudio.destroy();
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
this.audioMap.delete(key);
|
|
||||||
this.readyMap.set(key, false);
|
this.readyMap.set(key, false);
|
||||||
return this.ensureAudio(key);
|
return this.ensureAudio(key);
|
||||||
}
|
}
|
||||||
@@ -754,7 +834,8 @@ class AudioManager {
|
|||||||
// 不打断当前播放:把新的队列加入到序列中,等待当前播放结束后衔接
|
// 不打断当前播放:把新的队列加入到序列中,等待当前播放结束后衔接
|
||||||
if (this.currentPlayingKey) {
|
if (this.currentPlayingKey) {
|
||||||
if (this.isSequenceRunning) {
|
if (this.isSequenceRunning) {
|
||||||
// 已有序列在跑:直接追加
|
// 已有序列在跑:先移除已消费前缀,再追加待播语音。
|
||||||
|
this.compactSequenceQueue(true);
|
||||||
this.sequenceQueue = this.sequenceQueue.concat(queue);
|
this.sequenceQueue = this.sequenceQueue.concat(queue);
|
||||||
} else {
|
} else {
|
||||||
// 没有序列但当前有正在播放的:以当前为序列的起点
|
// 没有序列但当前有正在播放的:以当前为序列的起点
|
||||||
@@ -763,6 +844,17 @@ class AudioManager {
|
|||||||
this.sequenceIndex = 0;
|
this.sequenceIndex = 0;
|
||||||
// 不触发 _playSingle,等待当前音频自然结束后由 onAudioEnded 接管
|
// 不触发 _playSingle,等待当前音频自然结束后由 onAudioEnded 接管
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
this.isSequenceRunning &&
|
||||||
|
this.sequenceQueue[this.sequenceIndex]
|
||||||
|
) {
|
||||||
|
// 当前队首正在按需加载时只追加,不能覆盖已经排队的语音。
|
||||||
|
this.compactSequenceQueue(true);
|
||||||
|
this.sequenceQueue = this.sequenceQueue.concat(queue);
|
||||||
|
const pendingKey = this.sequenceQueue[this.sequenceIndex];
|
||||||
|
if (!this.pendingPlayKeys.has(pendingKey)) {
|
||||||
|
this._playSingle(pendingKey, false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 当前没有播放:直接启动新的序列
|
// 当前没有播放:直接启动新的序列
|
||||||
this.sequenceQueue = queue;
|
this.sequenceQueue = queue;
|
||||||
@@ -869,6 +961,7 @@ class AudioManager {
|
|||||||
this.currentPlayingKey = key;
|
this.currentPlayingKey = key;
|
||||||
this.lastPlayKey = key;
|
this.lastPlayKey = key;
|
||||||
this.lastPlayAt = Date.now();
|
this.lastPlayAt = Date.now();
|
||||||
|
this.touchAudioKey(key);
|
||||||
this.startPlayWatchdog(key);
|
this.startPlayWatchdog(key);
|
||||||
} else {
|
} else {
|
||||||
debugLog(`音频 ${key} 尚未就绪,按需加载后播放...`);
|
debugLog(`音频 ${key} 尚未就绪,按需加载后播放...`);
|
||||||
@@ -885,7 +978,8 @@ class AudioManager {
|
|||||||
const nextIndex = this.sequenceIndex + 1;
|
const nextIndex = this.sequenceIndex + 1;
|
||||||
if (nextIndex < this.sequenceQueue.length) {
|
if (nextIndex < this.sequenceQueue.length) {
|
||||||
this.sequenceIndex = nextIndex;
|
this.sequenceIndex = nextIndex;
|
||||||
const nextKey = this.sequenceQueue[nextIndex];
|
this.compactSequenceQueue();
|
||||||
|
const nextKey = this.sequenceQueue[this.sequenceIndex];
|
||||||
this._playSingle(nextKey, false);
|
this._playSingle(nextKey, false);
|
||||||
} else {
|
} else {
|
||||||
// 队列播放完成
|
// 队列播放完成
|
||||||
@@ -969,6 +1063,7 @@ class AudioManager {
|
|||||||
reloadAll() {
|
reloadAll() {
|
||||||
debugLog("执行 reloadAll: 重置音频实例并后台恢复");
|
debugLog("执行 reloadAll: 重置音频实例并后台恢复");
|
||||||
const shouldWarmAll = this.warmAllPromise !== null;
|
const shouldWarmAll = this.warmAllPromise !== null;
|
||||||
|
const shouldWarmPriority = this.warmPriorityPromise !== null;
|
||||||
this.loadGeneration += 1;
|
this.loadGeneration += 1;
|
||||||
|
|
||||||
// 1. 停止所有播放
|
// 1. 停止所有播放
|
||||||
@@ -981,6 +1076,7 @@ class AudioManager {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
this.audioMap.clear();
|
this.audioMap.clear();
|
||||||
|
this.audioLastUsedAt.clear();
|
||||||
|
|
||||||
// 3. 重置状态
|
// 3. 重置状态
|
||||||
this.readyMap.clear();
|
this.readyMap.clear();
|
||||||
@@ -999,10 +1095,13 @@ class AudioManager {
|
|||||||
|
|
||||||
// 4. 重置后台预热状态
|
// 4. 重置后台预热状态
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
this.warmPriorityPromise = null;
|
||||||
this.warmAllPromise = null;
|
this.warmAllPromise = null;
|
||||||
|
|
||||||
// 5. 之前已启动过全量预热则继续全量恢复,否则只恢复按钮音效。
|
// 5. 按中断前使用范围恢复,产品页不再全量创建语音实例。
|
||||||
return shouldWarmAll ? this.warmAll() : this.warmButton();
|
if (shouldWarmAll) return this.warmAll();
|
||||||
|
if (shouldWarmPriority) return this.warmCommon();
|
||||||
|
return this.warmButton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,34 @@ import { getDeviceBatteryAPI } from "@/apis";
|
|||||||
|
|
||||||
const power = ref(0);
|
const power = ref(0);
|
||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
|
let disposed = false;
|
||||||
|
let requestInFlight = false;
|
||||||
|
|
||||||
|
const refreshPower = async () => {
|
||||||
|
if (disposed || requestInFlight) return;
|
||||||
|
requestInFlight = true;
|
||||||
|
try {
|
||||||
|
const data = await getDeviceBatteryAPI();
|
||||||
|
if (!disposed) power.value = data.battery;
|
||||||
|
} catch (_) {
|
||||||
|
// 电量轮询失败时等待下一轮,避免产生未处理的 Promise 拒绝。
|
||||||
|
} finally {
|
||||||
|
requestInFlight = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const data = await getDeviceBatteryAPI();
|
await refreshPower();
|
||||||
power.value = data.battery;
|
if (disposed) return;
|
||||||
timer.value = setInterval(async () => {
|
timer.value = setInterval(() => {
|
||||||
const data = await getDeviceBatteryAPI();
|
void refreshPower();
|
||||||
power.value = data.battery;
|
|
||||||
}, 1000 * 10);
|
}, 1000 * 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
disposed = true;
|
||||||
clearInterval(timer.value);
|
clearInterval(timer.value);
|
||||||
|
timer.value = null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import AppBackground from "@/components/AppBackground.vue";
|
|||||||
import Header from "@/components/Header.vue";
|
import Header from "@/components/Header.vue";
|
||||||
import ScreenHint from "@/components/ScreenHint.vue";
|
import ScreenHint from "@/components/ScreenHint.vue";
|
||||||
import BackToGame from "@/components/BackToGame.vue";
|
import BackToGame from "@/components/BackToGame.vue";
|
||||||
|
import DeviceChargingDialog from "@/components/DeviceChargingDialog.vue";
|
||||||
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
||||||
import { capsuleHeight, debounce } from "@/util";
|
import { capsuleHeight, debounce } from "@/util";
|
||||||
import { returnToBattle } from "@/utils/matchReturn";
|
import { returnToBattle } from "@/utils/matchReturn";
|
||||||
@@ -226,6 +227,7 @@ const goCalibration = async () => {
|
|||||||
<text>{{ loadingText || "加载中..." }}</text>
|
<text>{{ loadingText || "加载中..." }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<DeviceChargingDialog />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import ModalDialog from "@/components/ModalDialog.vue";
|
||||||
|
import useStore from "@/store";
|
||||||
|
|
||||||
|
const chargingNotice =
|
||||||
|
"检测到智能弓箭已开始充电,为保护电池,设备即将自动关机。\n(注意:此时拔出充电线将会重启设备。)";
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { deviceChargingDialogVisible } = storeToRefs(store);
|
||||||
|
const { hideDeviceChargingDialog } = store;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalDialog
|
||||||
|
:show="deviceChargingDialogVisible"
|
||||||
|
:content="chargingNotice"
|
||||||
|
:show-cancel="false"
|
||||||
|
confirm-text="我知道了"
|
||||||
|
:on-confirm="hideDeviceChargingDialog"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
import { ref, watch } from "vue";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rowCount: {
|
rowCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -29,31 +29,24 @@ const bgImages = [
|
|||||||
"https://static.shelingxingqiu.com/shootmini/static/complete-light1.png",
|
"https://static.shelingxingqiu.com/shootmini/static/complete-light1.png",
|
||||||
"https://static.shelingxingqiu.com/shootmini/static/complete-light2.png",
|
"https://static.shelingxingqiu.com/shootmini/static/complete-light2.png",
|
||||||
];
|
];
|
||||||
const bgIndex = ref(0);
|
|
||||||
watch(
|
watch(
|
||||||
() => props.total,
|
() => props.total,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
items.value = new Array(newValue).fill(9);
|
items.value = new Array(newValue).fill(9);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const timer = ref(null);
|
|
||||||
onMounted(() => {
|
|
||||||
timer.value = setInterval(() => {
|
|
||||||
bgIndex.value = bgIndex.value === 0 ? 1 : 0;
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (timer.value) {
|
|
||||||
clearInterval(timer.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
|
<template v-if="total > 0 && arrows.length === total && completeEffect">
|
||||||
<image
|
<image
|
||||||
v-if="total > 0 && arrows.length === total && completeEffect"
|
v-for="(image, index) in bgImages"
|
||||||
:src="bgImages[bgIndex]"
|
:key="image"
|
||||||
class="complete-light"
|
:src="image"
|
||||||
|
:class="[
|
||||||
|
'complete-light',
|
||||||
|
index === 0 ? 'complete-light--first' : 'complete-light--second',
|
||||||
|
]"
|
||||||
:style="{
|
:style="{
|
||||||
width: `calc(${(100 / (rowCount + 2)) * rowCount}vw + ${
|
width: `calc(${(100 / (rowCount + 2)) * rowCount}vw + ${
|
||||||
(100 / (total * 2)) * (rowCount * 2 + (total === 12 ? 8 : 24))
|
(100 / (total * 2)) * (rowCount * 2 + (total === 12 ? 8 : 24))
|
||||||
@@ -65,6 +58,7 @@ onBeforeUnmount(() => {
|
|||||||
top: `${total === 12 ? -2 : -3}vw`,
|
top: `${total === 12 ? -2 : -3}vw`,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
<view
|
<view
|
||||||
v-for="(_, index) in items"
|
v-for="(_, index) in items"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -121,4 +115,30 @@ onBeforeUnmount(() => {
|
|||||||
.complete-light {
|
.complete-light {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
.complete-light--first {
|
||||||
|
animation: complete-light-first 400ms steps(1, end) infinite;
|
||||||
|
}
|
||||||
|
.complete-light--second {
|
||||||
|
animation: complete-light-second 400ms steps(1, end) infinite;
|
||||||
|
}
|
||||||
|
@keyframes complete-light-first {
|
||||||
|
0%,
|
||||||
|
49.9% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes complete-light-second {
|
||||||
|
0%,
|
||||||
|
49.9% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ const props = defineProps({
|
|||||||
const show = ref(false);
|
const show = ref(false);
|
||||||
const count = ref(props.countdown);
|
const count = ref(props.countdown);
|
||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
|
const showTimer = ref(null);
|
||||||
const updateTimer = (value) => {
|
const updateTimer = (value) => {
|
||||||
count.value = Math.round(value);
|
count.value = Math.round(value);
|
||||||
};
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => {
|
showTimer.value = setTimeout(() => {
|
||||||
|
showTimer.value = null;
|
||||||
show.value = true;
|
show.value = true;
|
||||||
timer.value = setInterval(() => {
|
timer.value = setInterval(() => {
|
||||||
if (count.value === 0) {
|
if (count.value === 0) {
|
||||||
@@ -27,7 +29,10 @@ onMounted(() => {
|
|||||||
uni.$on("update-timer", updateTimer);
|
uni.$on("update-timer", updateTimer);
|
||||||
});
|
});
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
if (showTimer.value) clearTimeout(showTimer.value);
|
||||||
|
showTimer.value = null;
|
||||||
if (timer.value) clearInterval(timer.value);
|
if (timer.value) clearInterval(timer.value);
|
||||||
|
timer.value = null;
|
||||||
uni.$off("update-timer", updateTimer);
|
uni.$off("update-timer", updateTimer);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const MAX_VISIBLE_SCORE_CARDS = 60;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
arrows: {
|
arrows: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -32,8 +34,16 @@ const isFailed = (arrow = {}) => {
|
|||||||
return arrow.ok !== true;
|
return arrow.ok !== true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hiddenScoreCount = computed(() =>
|
||||||
|
props.recordMode
|
||||||
|
? 0
|
||||||
|
: Math.max(props.arrows.length - MAX_VISIBLE_SCORE_CARDS, 0)
|
||||||
|
);
|
||||||
|
|
||||||
const displayArrows = computed(() => {
|
const displayArrows = computed(() => {
|
||||||
const list = [...props.arrows];
|
const list = props.recordMode
|
||||||
|
? [...props.arrows]
|
||||||
|
: props.arrows.slice(-MAX_VISIBLE_SCORE_CARDS);
|
||||||
// total 是达标箭数,不是实际射箭上限;训练中始终预留下一箭空框。
|
// total 是达标箭数,不是实际射箭上限;训练中始终预留下一箭空框。
|
||||||
if (!props.recordMode) list.push(null);
|
if (!props.recordMode) list.push(null);
|
||||||
return list;
|
return list;
|
||||||
@@ -42,6 +52,10 @@ const displayArrows = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view v-if="displayArrows.length" class="score-panel">
|
<view v-if="displayArrows.length" class="score-panel">
|
||||||
|
<text v-if="hiddenScoreCount" class="score-window-tip">
|
||||||
|
已省略前 {{ hiddenScoreCount }} 支,仅显示最近
|
||||||
|
{{ MAX_VISIBLE_SCORE_CARDS }} 支
|
||||||
|
</text>
|
||||||
<view class="score-grid">
|
<view class="score-grid">
|
||||||
<view
|
<view
|
||||||
v-for="(arrow, index) in displayArrows"
|
v-for="(arrow, index) in displayArrows"
|
||||||
@@ -90,6 +104,14 @@ const displayArrows = computed(() => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.score-window-tip {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.score-card {
|
.score-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100rpx;
|
width: 100rpx;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const MESSAGETYPES = {
|
|||||||
DeviceOnline: 4168086626,
|
DeviceOnline: 4168086626,
|
||||||
DeviceOffline: 4168086627,
|
DeviceOffline: 4168086627,
|
||||||
SomeoneIsReady: 4168086628,
|
SomeoneIsReady: 4168086628,
|
||||||
|
DeviceCharging: 4168086631, // 设备充电中
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MESSAGETYPESV2 = {
|
export const MESSAGETYPESV2 = {
|
||||||
|
|||||||
+28
-1
@@ -41,6 +41,27 @@ const LIVENESS_TIMEOUT_MS = 30000;
|
|||||||
const RECONNECT_DELAYS_MS = [1000, 2000, 4000, 8000, 15000];
|
const RECONNECT_DELAYS_MS = [1000, 2000, 4000, 8000, 15000];
|
||||||
const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"];
|
const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"];
|
||||||
const MATCH_STATUS_HALF_REST = 3;
|
const MATCH_STATUS_HALF_REST = 3;
|
||||||
|
const ENABLE_REALTIME_MESSAGE_LOG = (() => {
|
||||||
|
try {
|
||||||
|
return uni.getAccountInfoSync().miniProgram.envVersion !== "release";
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
function getServerMessageLogSummary(message = {}) {
|
||||||
|
const practiceInfo = message.practice_info || message.practiceInfo || {};
|
||||||
|
const matchInfo = message.match_info || message.matchInfo || {};
|
||||||
|
return {
|
||||||
|
type: message.type,
|
||||||
|
matchId: normalizeId(pickField(message, "matchId", "match_id")),
|
||||||
|
sequence: message.sequence,
|
||||||
|
practiceDetailCount: Array.isArray(practiceInfo.details)
|
||||||
|
? practiceInfo.details.length
|
||||||
|
: 0,
|
||||||
|
roundCount: Array.isArray(matchInfo.rounds) ? matchInfo.rounds.length : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 比赛服消息类型先映射成项目里已有的 V2 业务消息,页面仍然复用原来的 socket-inbox 流程。
|
// 比赛服消息类型先映射成项目里已有的 V2 业务消息,页面仍然复用原来的 socket-inbox 流程。
|
||||||
const BUSINESS_TYPE_BY_SERVER_TYPE = {
|
const BUSINESS_TYPE_BY_SERVER_TYPE = {
|
||||||
@@ -785,7 +806,13 @@ function handleMessage(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const typeName = getServerMessageTypeName(message.type);
|
const typeName = getServerMessageTypeName(message.type);
|
||||||
console.log("收到比赛服 WebSocket 消息", typeName, message);
|
if (ENABLE_REALTIME_MESSAGE_LOG) {
|
||||||
|
console.log(
|
||||||
|
"收到比赛服 WebSocket 消息",
|
||||||
|
typeName,
|
||||||
|
getServerMessageLogSummary(message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const decodedMatchId = normalizeId(pickField(message, "matchId", "match_id"));
|
const decodedMatchId = normalizeId(pickField(message, "matchId", "match_id"));
|
||||||
if (message.type === ServerMessageType.SERVER_MSG_SYNC_PRACTICE_INFO) {
|
if (message.type === ServerMessageType.SERVER_MSG_SYNC_PRACTICE_INFO) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from "vue";
|
|||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
import UserUpgrade from "@/components/UserUpgrade.vue";
|
import UserUpgrade from "@/components/UserUpgrade.vue";
|
||||||
|
import DeviceChargingDialog from "@/components/DeviceChargingDialog.vue";
|
||||||
import { getBattleAPI } from "@/apis";
|
import { getBattleAPI } from "@/apis";
|
||||||
import { topThreeColors, getBattleResultTips } from "@/constants";
|
import { topThreeColors, getBattleResultTips } from "@/constants";
|
||||||
import audioManager from "@/audioManager";
|
import audioManager from "@/audioManager";
|
||||||
@@ -287,6 +288,7 @@ const checkBowData = () => {
|
|||||||
<view @click="exit">返回</view>
|
<view @click="exit">返回</view>
|
||||||
</view>
|
</view>
|
||||||
<UserUpgrade />
|
<UserUpgrade />
|
||||||
|
<DeviceChargingDialog />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
|
import DeviceChargingDialog from "@/components/DeviceChargingDialog.vue";
|
||||||
|
|
||||||
import { capsuleHeight } from "@/util";
|
import { capsuleHeight } from "@/util";
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ const onScrollView = (e) => {
|
|||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
<DeviceChargingDialog />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ const onClickShare = debounce(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void audioManager.warmAll();
|
void audioManager.warmCommon();
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
});
|
});
|
||||||
|
|||||||
+95
-24
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, onUnmounted, ref} from "vue";
|
import {onMounted, onUnmounted, ref, watch} 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";
|
||||||
@@ -52,20 +52,41 @@ const otaInfo = ref({
|
|||||||
forceUpdate: false,
|
forceUpdate: false,
|
||||||
});
|
});
|
||||||
const isStartingOta = ref(false);
|
const isStartingOta = ref(false);
|
||||||
|
let isCheckingOta = false;
|
||||||
|
let otaCheckQueuedForOnline = false;
|
||||||
let otaProgressTimer = null;
|
let otaProgressTimer = null;
|
||||||
let otaStatusTimer = null;
|
let otaStatusTimer = null;
|
||||||
let otaTimeoutTimer = null;
|
let otaTimeoutTimer = null;
|
||||||
|
let otaResultTimer = null;
|
||||||
|
let otaStatusPollCount = 0;
|
||||||
|
let otaUpdateRunId = 0;
|
||||||
|
// 首页 OTA 轮询采用请求次数和总时长双重兜底,避免更新中状态长期卡住。
|
||||||
|
const OTA_TASK_STATUS_POLL_INTERVAL = 2000;
|
||||||
|
const OTA_TASK_STATUS_MAX_POLL_COUNT = 15;
|
||||||
|
const OTA_TASK_STATUS_TIMEOUT = 30000;
|
||||||
|
|
||||||
// 清理首页 OTA 更新定时器,避免弹窗关闭或页面卸载后继续轮询。
|
// 清理首页 OTA 更新定时器,避免弹窗关闭或页面卸载后继续轮询。
|
||||||
const clearOtaUpdateTimers = () => {
|
const clearOtaUpdateTimers = () => {
|
||||||
clearInterval(otaProgressTimer);
|
clearInterval(otaProgressTimer);
|
||||||
clearTimeout(otaStatusTimer);
|
clearTimeout(otaStatusTimer);
|
||||||
clearTimeout(otaTimeoutTimer);
|
clearTimeout(otaTimeoutTimer);
|
||||||
|
clearTimeout(otaResultTimer);
|
||||||
otaProgressTimer = null;
|
otaProgressTimer = null;
|
||||||
otaStatusTimer = null;
|
otaStatusTimer = null;
|
||||||
otaTimeoutTimer = null;
|
otaTimeoutTimer = null;
|
||||||
|
otaResultTimer = null;
|
||||||
|
otaStatusPollCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 使当前 OTA 运行失效,确保旧请求返回后不会继续轮询或覆盖新状态。
|
||||||
|
const invalidateOtaUpdateRun = () => {
|
||||||
|
otaUpdateRunId += 1;
|
||||||
|
clearOtaUpdateTimers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOtaUpdateRunActive = (runId) =>
|
||||||
|
runId === otaUpdateRunId && otaState.value === "update_progress";
|
||||||
|
|
||||||
// 启动首页 OTA 本地进度动画,最终成功失败以后端任务状态为准。
|
// 启动首页 OTA 本地进度动画,最终成功失败以后端任务状态为准。
|
||||||
const startOtaProgressAnimation = () => {
|
const startOtaProgressAnimation = () => {
|
||||||
clearInterval(otaProgressTimer);
|
clearInterval(otaProgressTimer);
|
||||||
@@ -91,13 +112,25 @@ const applyOtaVersionInfo = (versionInfo) => {
|
|||||||
|
|
||||||
// 检查当前设备盒子是否存在可升级版本。
|
// 检查当前设备盒子是否存在可升级版本。
|
||||||
const checkOtaUpdate = async () => {
|
const checkOtaUpdate = async () => {
|
||||||
|
if (isCheckingOta || otaVisible.value) return;
|
||||||
|
isCheckingOta = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let deviceStatus;
|
||||||
|
try {
|
||||||
|
deviceStatus = await getDeviceBatteryAPI();
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deviceStatus?.online !== true || otaVisible.value) return;
|
||||||
|
|
||||||
let versionInfo;
|
let versionInfo;
|
||||||
try {
|
try {
|
||||||
versionInfo = await getHardwareBoxVersionAPI();
|
versionInfo = await getHardwareBoxVersionAPI();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!versionInfo?.needUpdate) return;
|
if (!versionInfo?.needUpdate || otaVisible.value) return;
|
||||||
applyOtaVersionInfo(versionInfo);
|
applyOtaVersionInfo(versionInfo);
|
||||||
|
|
||||||
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
|
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
|
||||||
@@ -105,8 +138,26 @@ const checkOtaUpdate = async () => {
|
|||||||
if (!otaInfo.value.forceUpdate && dismissedAt && now - dismissedAt < 24 * 60 * 60 * 1000) return;
|
if (!otaInfo.value.forceUpdate && dismissedAt && now - dismissedAt < 24 * 60 * 60 * 1000) return;
|
||||||
otaState.value = "new_version";
|
otaState.value = "new_version";
|
||||||
otaVisible.value = true;
|
otaVisible.value = true;
|
||||||
|
} finally {
|
||||||
|
isCheckingOta = false;
|
||||||
|
const shouldRecheckForOnline = otaCheckQueuedForOnline;
|
||||||
|
otaCheckQueuedForOnline = false;
|
||||||
|
if (shouldRecheckForOnline && !otaVisible.value) {
|
||||||
|
void checkOtaUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 设备由 WS 通知上线时补查 OTA;已显示任何 OTA 弹窗时保留当前流程和结果。
|
||||||
|
watch(online, (nextOnline, previousOnline) => {
|
||||||
|
if (previousOnline !== false || nextOnline !== true || otaVisible.value) return;
|
||||||
|
if (isCheckingOta) {
|
||||||
|
otaCheckQueuedForOnline = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void checkOtaUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
// 拼接 OTA WiFi 页参数,让未连 WiFi 的设备继续使用同一份版本信息。
|
// 拼接 OTA WiFi 页参数,让未连 WiFi 的设备继续使用同一份版本信息。
|
||||||
const getOtaWifiUrl = () => {
|
const getOtaWifiUrl = () => {
|
||||||
const { versionNumber, resourceUrl } = otaInfo.value;
|
const { versionNumber, resourceUrl } = otaInfo.value;
|
||||||
@@ -125,61 +176,78 @@ const handleOtaDismiss = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 将首页 OTA 直连更新流程标记为失败。
|
// 将首页 OTA 直连更新流程标记为失败。
|
||||||
const failHomeOtaUpdate = () => {
|
const failHomeOtaUpdate = (runId) => {
|
||||||
clearOtaUpdateTimers();
|
if (!isOtaUpdateRunActive(runId)) return;
|
||||||
|
invalidateOtaUpdateRun();
|
||||||
isStartingOta.value = false;
|
isStartingOta.value = false;
|
||||||
otaState.value = "update_failure";
|
otaState.value = "update_failure";
|
||||||
otaVisible.value = true;
|
otaVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 将首页 OTA 直连更新流程标记为成功。
|
// 将首页 OTA 直连更新流程标记为成功。
|
||||||
const completeHomeOtaUpdate = () => {
|
const completeHomeOtaUpdate = (runId) => {
|
||||||
clearOtaUpdateTimers();
|
if (!isOtaUpdateRunActive(runId)) return;
|
||||||
|
invalidateOtaUpdateRun();
|
||||||
isStartingOta.value = false;
|
isStartingOta.value = false;
|
||||||
otaProgress.value = 100;
|
otaProgress.value = 100;
|
||||||
setTimeout(() => {
|
const completedRunId = otaUpdateRunId;
|
||||||
|
otaResultTimer = setTimeout(() => {
|
||||||
|
otaResultTimer = null;
|
||||||
|
if (completedRunId !== otaUpdateRunId) return;
|
||||||
otaState.value = "update_success";
|
otaState.value = "update_success";
|
||||||
otaVisible.value = true;
|
otaVisible.value = true;
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 轮询首页直接发起的 OTA 更新任务状态。
|
// 轮询首页直接发起的 OTA 更新任务状态。
|
||||||
const pollHomeOtaTaskStatus = (taskId) => {
|
const pollHomeOtaTaskStatus = async (taskId, runId) => {
|
||||||
clearTimeout(otaStatusTimer);
|
if (!isOtaUpdateRunActive(runId)) return;
|
||||||
otaStatusTimer = setTimeout(async () => {
|
|
||||||
|
otaStatusPollCount += 1;
|
||||||
try {
|
try {
|
||||||
const taskStatus = await getHardwareBoxTaskStatusAPI(taskId);
|
const taskStatus = await getHardwareBoxTaskStatusAPI(taskId);
|
||||||
|
if (!isOtaUpdateRunActive(runId)) return;
|
||||||
|
|
||||||
const status = Number(taskStatus?.status);
|
const status = Number(taskStatus?.status);
|
||||||
if (status === 2) {
|
if (status === 2) {
|
||||||
completeHomeOtaUpdate();
|
completeHomeOtaUpdate(runId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status === 3) {
|
if (status === 3) {
|
||||||
failHomeOtaUpdate();
|
failHomeOtaUpdate(runId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status === 0 || status === 1) {
|
if (status === 0 || status === 1) {
|
||||||
pollHomeOtaTaskStatus(taskId);
|
if (otaStatusPollCount >= OTA_TASK_STATUS_MAX_POLL_COUNT) {
|
||||||
|
failHomeOtaUpdate(runId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
failHomeOtaUpdate();
|
otaStatusTimer = setTimeout(() => {
|
||||||
|
otaStatusTimer = null;
|
||||||
|
if (!isOtaUpdateRunActive(runId)) return;
|
||||||
|
void pollHomeOtaTaskStatus(taskId, runId);
|
||||||
|
}, OTA_TASK_STATUS_POLL_INTERVAL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
failHomeOtaUpdate(runId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failHomeOtaUpdate();
|
if (!isOtaUpdateRunActive(runId)) return;
|
||||||
|
failHomeOtaUpdate(runId);
|
||||||
}
|
}
|
||||||
}, 3000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设备盒子已连 WiFi 时,从首页直接传空 WiFi 信息发起 OTA 更新。
|
// 设备盒子已连 WiFi 时,从首页直接传空 WiFi 信息发起 OTA 更新。
|
||||||
const startHomeOtaUpdate = async () => {
|
const startHomeOtaUpdate = async () => {
|
||||||
|
invalidateOtaUpdateRun();
|
||||||
|
const runId = otaUpdateRunId;
|
||||||
otaState.value = "update_progress";
|
otaState.value = "update_progress";
|
||||||
otaVisible.value = true;
|
otaVisible.value = true;
|
||||||
otaProgress.value = 0;
|
otaProgress.value = 0;
|
||||||
startOtaProgressAnimation();
|
startOtaProgressAnimation();
|
||||||
otaTimeoutTimer = setTimeout(() => {
|
otaTimeoutTimer = setTimeout(() => {
|
||||||
if (otaState.value === "update_progress") {
|
if (!isOtaUpdateRunActive(runId)) return;
|
||||||
failHomeOtaUpdate();
|
failHomeOtaUpdate(runId);
|
||||||
}
|
}, OTA_TASK_STATUS_TIMEOUT);
|
||||||
}, 5 * 60 * 1000);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updateResult = await sendHardwareBoxUpdateAPI({
|
const updateResult = await sendHardwareBoxUpdateAPI({
|
||||||
@@ -188,13 +256,15 @@ const startHomeOtaUpdate = async () => {
|
|||||||
wifiPassword: "",
|
wifiPassword: "",
|
||||||
resourceUrl: otaInfo.value.resourceUrl,
|
resourceUrl: otaInfo.value.resourceUrl,
|
||||||
});
|
});
|
||||||
|
if (!isOtaUpdateRunActive(runId)) return;
|
||||||
if (!updateResult?.taskId) {
|
if (!updateResult?.taskId) {
|
||||||
failHomeOtaUpdate();
|
failHomeOtaUpdate(runId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pollHomeOtaTaskStatus(updateResult.taskId);
|
void pollHomeOtaTaskStatus(updateResult.taskId, runId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failHomeOtaUpdate();
|
if (!isOtaUpdateRunActive(runId)) return;
|
||||||
|
failHomeOtaUpdate(runId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,6 +310,7 @@ const handleOtaDone = () => {
|
|||||||
|
|
||||||
// 处理 OTA 更新失败后的重试按钮,重新走立即更新判断流程。
|
// 处理 OTA 更新失败后的重试按钮,重新走立即更新判断流程。
|
||||||
const handleOtaRetry = () => {
|
const handleOtaRetry = () => {
|
||||||
|
invalidateOtaUpdateRun();
|
||||||
handleOtaUpdate();
|
handleOtaUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -351,7 +422,7 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearOtaUpdateTimers();
|
invalidateOtaUpdateRun();
|
||||||
});
|
});
|
||||||
|
|
||||||
onShareAppMessage(() => {
|
onShareAppMessage(() => {
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ onLoad(async (options) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void audioManager.warmAll();
|
void audioManager.warmCommon();
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref } from "vue";
|
|||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
import Signin from "@/components/Signin.vue";
|
import Signin from "@/components/Signin.vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
|
import DeviceChargingDialog from "@/components/DeviceChargingDialog.vue";
|
||||||
import { tempBindOrgAPI } from "@/apis";
|
import { tempBindOrgAPI } from "@/apis";
|
||||||
|
|
||||||
const scene = ref("");
|
const scene = ref("");
|
||||||
@@ -129,6 +130,7 @@ onLoad((options) => {
|
|||||||
:onClose="closeSignin"
|
:onClose="closeSignin"
|
||||||
:onSuccess="handleLoginSuccess"
|
:onSuccess="handleLoginSuccess"
|
||||||
/>
|
/>
|
||||||
|
<DeviceChargingDialog />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import SButton from "@/components/SButton.vue";
|
|||||||
import BowTargetEdit from "@/components/BowTargetEdit.vue";
|
import BowTargetEdit from "@/components/BowTargetEdit.vue";
|
||||||
import ScreenHint2 from "@/components/ScreenHint2.vue";
|
import ScreenHint2 from "@/components/ScreenHint2.vue";
|
||||||
import RingBarChart from "@/components/RingBarChart.vue";
|
import RingBarChart from "@/components/RingBarChart.vue";
|
||||||
|
import DeviceChargingDialog from "@/components/DeviceChargingDialog.vue";
|
||||||
import { getPointBookDetailAPI, getPointBookConfigAPI } from "@/apis";
|
import { getPointBookDetailAPI, getPointBookConfigAPI } from "@/apis";
|
||||||
|
|
||||||
const selectedIndex = ref(0);
|
const selectedIndex = ref(0);
|
||||||
@@ -238,6 +239,7 @@ const targetTypeName = computed(() => {
|
|||||||
</block>
|
</block>
|
||||||
</view>
|
</view>
|
||||||
</ScreenHint2>
|
</ScreenHint2>
|
||||||
|
<DeviceChargingDialog />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -403,7 +403,7 @@ onShow(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
void audioManager.warmAll();
|
void audioManager.warmCommon();
|
||||||
// audioManager.play("第一轮");
|
// audioManager.play("第一轮");
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ onShow(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
void audioManager.warmAll();
|
void audioManager.warmCommon();
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed, nextTick, onMounted, ref } from "vue";
|
import { computed, nextTick, onMounted, ref } from "vue";
|
||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
|
import DeviceChargingDialog from "@/components/DeviceChargingDialog.vue";
|
||||||
import {
|
import {
|
||||||
getMvpRankList,
|
getMvpRankList,
|
||||||
getMyMvpRank,
|
getMyMvpRank,
|
||||||
@@ -524,6 +525,7 @@ const measureTabsMetrics = () => {
|
|||||||
{{ getRankUnit() }}
|
{{ getRankUnit() }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
<DeviceChargingDialog />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,34 @@ import { getDeviceBatteryAPI } from "@/apis";
|
|||||||
|
|
||||||
const power = ref(0);
|
const power = ref(0);
|
||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
|
let disposed = false;
|
||||||
|
let requestInFlight = false;
|
||||||
|
|
||||||
|
const refreshPower = async () => {
|
||||||
|
if (disposed || requestInFlight) return;
|
||||||
|
requestInFlight = true;
|
||||||
|
try {
|
||||||
|
const data = await getDeviceBatteryAPI();
|
||||||
|
if (!disposed) power.value = data.battery;
|
||||||
|
} catch (_) {
|
||||||
|
// 电量轮询失败时等待下一轮,避免产生未处理的 Promise 拒绝。
|
||||||
|
} finally {
|
||||||
|
requestInFlight = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const data = await getDeviceBatteryAPI();
|
await refreshPower();
|
||||||
power.value = data.battery;
|
if (disposed) return;
|
||||||
timer.value = setInterval(async () => {
|
timer.value = setInterval(() => {
|
||||||
const data = await getDeviceBatteryAPI();
|
void refreshPower();
|
||||||
power.value = data.battery;
|
|
||||||
}, 1000 * 10);
|
}, 1000 * 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
disposed = true;
|
||||||
clearInterval(timer.value);
|
clearInterval(timer.value);
|
||||||
|
timer.value = null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import AppBackground from "./AppBackground.vue";
|
|||||||
import Header from "./Header.vue";
|
import Header from "./Header.vue";
|
||||||
import ScreenHint from "./ScreenHint.vue";
|
import ScreenHint from "./ScreenHint.vue";
|
||||||
import BackToGame from "./BackToGame.vue";
|
import BackToGame from "./BackToGame.vue";
|
||||||
|
import DeviceChargingDialog from "@/components/DeviceChargingDialog.vue";
|
||||||
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
|
||||||
import { capsuleHeight, debounce } from "@/util";
|
import { capsuleHeight, debounce } from "@/util";
|
||||||
import { returnToBattle } from "@/utils/matchReturn";
|
import { returnToBattle } from "@/utils/matchReturn";
|
||||||
@@ -204,6 +205,7 @@ const goCalibration = async () => {
|
|||||||
<text>{{ loadingText || "加载中..." }}</text>
|
<text>{{ loadingText || "加载中..." }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<DeviceChargingDialog />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -99,8 +99,6 @@ const emit = defineEmits(["shot-effect-complete"]);
|
|||||||
const pMode = ref(true);
|
const pMode = ref(true);
|
||||||
const latestOne = ref(null);
|
const latestOne = ref(null);
|
||||||
const bluelatestOne = ref(null);
|
const bluelatestOne = ref(null);
|
||||||
const prevScores = ref([]);
|
|
||||||
const prevBlueScores = ref([]);
|
|
||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
const dirTimer = ref(null);
|
const dirTimer = ref(null);
|
||||||
const angle = ref(null);
|
const angle = ref(null);
|
||||||
@@ -113,9 +111,23 @@ const targetRect = ref({ left: 0, top: 0, width: 0, height: 0 });
|
|||||||
const shakeTimer = ref(null);
|
const shakeTimer = ref(null);
|
||||||
const instance = getCurrentInstance();
|
const instance = getCurrentInstance();
|
||||||
let shotEffectRequestGeneration = 0;
|
let shotEffectRequestGeneration = 0;
|
||||||
|
const MAX_VISIBLE_TARGET_SHOTS = 12;
|
||||||
const ROUND_TIP_OFFSET_Y = -32;
|
const ROUND_TIP_OFFSET_Y = -32;
|
||||||
const EXPERIENCE_TIP_OFFSET_Y = -68;
|
const EXPERIENCE_TIP_OFFSET_Y = -68;
|
||||||
|
|
||||||
|
// 长时训练只保留最近一组命中点在靶面上,完整成绩仍由父页面保存并用于结算。
|
||||||
|
const getVisibleScoreEntries = (scores = []) => {
|
||||||
|
const startIndex = Math.max(scores.length - MAX_VISIBLE_TARGET_SHOTS, 0);
|
||||||
|
return scores.slice(startIndex).map((shot, offset) => ({
|
||||||
|
shot,
|
||||||
|
index: startIndex + offset,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const visibleScoreEntries = computed(() => getVisibleScoreEntries(props.scores));
|
||||||
|
const visibleBlueScoreEntries = computed(() =>
|
||||||
|
getVisibleScoreEntries(props.blueScores)
|
||||||
|
);
|
||||||
|
|
||||||
const getNumber = (value, fallback = 0) => {
|
const getNumber = (value, fallback = 0) => {
|
||||||
const numberValue = Number(value);
|
const numberValue = Number(value);
|
||||||
return Number.isFinite(numberValue) ? numberValue : fallback;
|
return Number.isFinite(numberValue) ? numberValue : fallback;
|
||||||
@@ -376,11 +388,11 @@ function handleWindowResize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.scores,
|
() => props.scores.length,
|
||||||
(newVal) => {
|
(newLength, oldLength = 0) => {
|
||||||
if (newVal.length - prevScores.value.length === 1) {
|
if (newLength - oldLength === 1) {
|
||||||
showShotTip(newVal[newVal.length - 1]);
|
showShotTip(props.scores[newLength - 1]);
|
||||||
} else if (newVal.length < prevScores.value.length) {
|
} else if (newLength < oldLength) {
|
||||||
shotEffectRequestGeneration += 1;
|
shotEffectRequestGeneration += 1;
|
||||||
pendingShotEffect.value = null;
|
pendingShotEffect.value = null;
|
||||||
clearTipTimer();
|
clearTipTimer();
|
||||||
@@ -388,10 +400,6 @@ watch(
|
|||||||
hiddenLatestKey.value = "";
|
hiddenLatestKey.value = "";
|
||||||
shotEffect.value = null;
|
shotEffect.value = null;
|
||||||
}
|
}
|
||||||
prevScores.value = [...newVal];
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deep: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -412,19 +420,15 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.blueScores,
|
() => props.blueScores.length,
|
||||||
(newVal) => {
|
(newLength, oldLength = 0) => {
|
||||||
if (newVal.length - prevBlueScores.value.length === 1) {
|
if (newLength - oldLength === 1) {
|
||||||
bluelatestOne.value = newVal[newVal.length - 1];
|
bluelatestOne.value = props.blueScores[newLength - 1];
|
||||||
if (timer.value) clearTimeout(timer.value);
|
if (timer.value) clearTimeout(timer.value);
|
||||||
timer.value = setTimeout(() => {
|
timer.value = setTimeout(() => {
|
||||||
bluelatestOne.value = null;
|
bluelatestOne.value = null;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
prevBlueScores.value = [...newVal];
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deep: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -580,38 +584,45 @@ onBeforeUnmount(() => {
|
|||||||
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
|
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
|
||||||
}}<text v-if="bluelatestOne.ring">环</text></view
|
}}<text v-if="bluelatestOne.ring">环</text></view
|
||||||
>
|
>
|
||||||
<block v-for="(bow, index) in scores" :key="index">
|
<block v-for="entry in visibleScoreEntries" :key="entry.index">
|
||||||
<image
|
<image
|
||||||
v-if="pMode && isSvip && bow.ring > 0 && !shouldHideLatestHit(index)"
|
v-if="
|
||||||
|
pMode &&
|
||||||
|
isSvip &&
|
||||||
|
entry.shot.ring > 0 &&
|
||||||
|
!shouldHideLatestHit(entry.index)
|
||||||
|
"
|
||||||
class="svip-hit-bg"
|
class="svip-hit-bg"
|
||||||
src="../../../static/vip/svip-xuan.png"
|
src="../../../static/vip/svip-xuan.png"
|
||||||
:style="getSvipHitBgStyle(bow)"
|
:style="getSvipHitBgStyle(entry.shot)"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
/>
|
/>
|
||||||
<view
|
<view
|
||||||
v-if="bow.ring > 0 && !shouldHideLatestHit(index)"
|
v-if="entry.shot.ring > 0 && !shouldHideLatestHit(entry.index)"
|
||||||
:class="`hit ${pMode ? 'b' : 's'}-point ${
|
:class="`hit ${pMode ? 'b' : 's'}-point ${
|
||||||
index === scores.length - 1 && latestOne ? 'pump-in' : ''
|
entry.index === scores.length - 1 && latestOne ? 'pump-in' : ''
|
||||||
}`"
|
}`"
|
||||||
:style="{
|
:style="{
|
||||||
...getHitStyle(bow),
|
...getHitStyle(entry.shot),
|
||||||
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
|
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
|
||||||
}"
|
}"
|
||||||
><text v-if="pMode">{{ index + 1 }}</text></view
|
><text v-if="pMode">{{ entry.index + 1 }}</text></view
|
||||||
>
|
>
|
||||||
</block>
|
</block>
|
||||||
<block v-for="(bow, index) in blueScores" :key="index">
|
<block v-for="entry in visibleBlueScoreEntries" :key="entry.index">
|
||||||
<view
|
<view
|
||||||
v-if="bow.ring > 0"
|
v-if="entry.shot.ring > 0"
|
||||||
:class="`hit ${pMode ? 'b' : 's'}-point ${
|
:class="`hit ${pMode ? 'b' : 's'}-point ${
|
||||||
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
|
entry.index === blueScores.length - 1 && bluelatestOne
|
||||||
|
? 'pump-in'
|
||||||
|
: ''
|
||||||
}`"
|
}`"
|
||||||
:style="{
|
:style="{
|
||||||
...getHitStyle(bow),
|
...getHitStyle(entry.shot),
|
||||||
backgroundColor: '#1840FF',
|
backgroundColor: '#1840FF',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<text v-if="pMode">{{ index + 1 }}</text>
|
<text v-if="pMode">{{ entry.index + 1 }}</text>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</block>
|
||||||
<BowShotEffect
|
<BowShotEffect
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
import { ref, watch } from "vue";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rowCount: {
|
rowCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -27,8 +27,6 @@ const bgImages = [
|
|||||||
"../static/complete-light1.png",
|
"../static/complete-light1.png",
|
||||||
"../static/complete-light2.png",
|
"../static/complete-light2.png",
|
||||||
];
|
];
|
||||||
const bgIndex = ref(0);
|
|
||||||
|
|
||||||
const getDisplayText = (arrow) => {
|
const getDisplayText = (arrow) => {
|
||||||
if (!arrow) return "-";
|
if (!arrow) return "-";
|
||||||
if (arrow.ringX) return "X";
|
if (arrow.ringX) return "X";
|
||||||
@@ -47,24 +45,18 @@ watch(
|
|||||||
items.value = new Array(newValue).fill(9);
|
items.value = new Array(newValue).fill(9);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const timer = ref(null);
|
|
||||||
onMounted(() => {
|
|
||||||
timer.value = setInterval(() => {
|
|
||||||
bgIndex.value = bgIndex.value === 0 ? 1 : 0;
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (timer.value) {
|
|
||||||
clearInterval(timer.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
|
<template v-if="total > 0 && arrows.length === total && completeEffect">
|
||||||
<image
|
<image
|
||||||
v-if="total > 0 && arrows.length === total && completeEffect"
|
v-for="(image, index) in bgImages"
|
||||||
:src="bgImages[bgIndex]"
|
:key="image"
|
||||||
class="complete-light"
|
:src="image"
|
||||||
|
:class="[
|
||||||
|
'complete-light',
|
||||||
|
index === 0 ? 'complete-light--first' : 'complete-light--second',
|
||||||
|
]"
|
||||||
:style="{
|
:style="{
|
||||||
width: `calc(${(100 / (rowCount + 2)) * rowCount}vw + ${
|
width: `calc(${(100 / (rowCount + 2)) * rowCount}vw + ${
|
||||||
(100 / (total * 2)) * (rowCount * 2 + (total === 12 ? 8 : 24))
|
(100 / (total * 2)) * (rowCount * 2 + (total === 12 ? 8 : 24))
|
||||||
@@ -76,6 +68,7 @@ onBeforeUnmount(() => {
|
|||||||
top: `${total === 12 ? -2 : -3}vw`,
|
top: `${total === 12 ? -2 : -3}vw`,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
<view
|
<view
|
||||||
v-for="(_, index) in items"
|
v-for="(_, index) in items"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -154,4 +147,30 @@ onBeforeUnmount(() => {
|
|||||||
.complete-light {
|
.complete-light {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
.complete-light--first {
|
||||||
|
animation: complete-light-first 400ms steps(1, end) infinite;
|
||||||
|
}
|
||||||
|
.complete-light--second {
|
||||||
|
animation: complete-light-second 400ms steps(1, end) infinite;
|
||||||
|
}
|
||||||
|
@keyframes complete-light-first {
|
||||||
|
0%,
|
||||||
|
49.9% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes complete-light-second {
|
||||||
|
0%,
|
||||||
|
49.9% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ const SHOT_EFFECT_WAIT_TIMEOUT_MS = 1200;
|
|||||||
const AUDIO_TIMEOUT_BASE = 3500;
|
const AUDIO_TIMEOUT_BASE = 3500;
|
||||||
const AUDIO_TIMEOUT_PER_KEY = 2600;
|
const AUDIO_TIMEOUT_PER_KEY = 2600;
|
||||||
const AUDIO_TIMEOUT_MAX = 12000;
|
const AUDIO_TIMEOUT_MAX = 12000;
|
||||||
|
const RUNTIME_DIAGNOSTIC_INTERVAL_MS = 60000;
|
||||||
|
let lastRuntimeDiagnosticAt = 0;
|
||||||
|
|
||||||
const env = computed(() => {
|
const env = computed(() => {
|
||||||
try {
|
try {
|
||||||
@@ -108,6 +110,19 @@ const env = computed(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const maybeLogRuntimeStats = () => {
|
||||||
|
if (env.value !== "develop" && env.value !== "trial") return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastRuntimeDiagnosticAt < RUNTIME_DIAGNOSTIC_INTERVAL_MS) return;
|
||||||
|
lastRuntimeDiagnosticAt = now;
|
||||||
|
console.log("[training-runtime]", {
|
||||||
|
scoreCount: scores.value.length,
|
||||||
|
renderedTargetShotCount: Math.min(scores.value.length, 12),
|
||||||
|
renderedScoreCardCount: Math.min(scores.value.length, 60) + 1,
|
||||||
|
...audioManager.getRuntimeStats(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const isDistanceStage = computed(() => pageStage.value === pageStages.DISTANCE);
|
const isDistanceStage = computed(() => pageStage.value === pageStages.DISTANCE);
|
||||||
const isShootingStage = computed(() => pageStage.value === pageStages.SHOOTING);
|
const isShootingStage = computed(() => pageStage.value === pageStages.SHOOTING);
|
||||||
const hasPractiseResult = computed(() => !!practiseResult.value?.details);
|
const hasPractiseResult = computed(() => !!practiseResult.value?.details);
|
||||||
@@ -1111,6 +1126,7 @@ async function onReceiveMessage(msg) {
|
|||||||
scores.value = msg.details;
|
scores.value = msg.details;
|
||||||
hasNewShot = msg.details.length === previousScoreLength + 1;
|
hasNewShot = msg.details.length === previousScoreLength + 1;
|
||||||
latestShot = hasNewShot ? msg.details[msg.details.length - 1] : null;
|
latestShot = hasNewShot ? msg.details[msg.details.length - 1] : null;
|
||||||
|
maybeLogRuntimeStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trainingType.value === "precision") {
|
if (trainingType.value === "precision") {
|
||||||
@@ -1339,7 +1355,6 @@ onBeforeUnmount(() => {
|
|||||||
:activeSector="precisionRandomBlock"
|
:activeSector="precisionRandomBlock"
|
||||||
:activeRing="precisionRandomRingArea"
|
:activeRing="precisionRandomRingArea"
|
||||||
:highlightRefreshToken="precisionTargetRefreshToken"
|
:highlightRefreshToken="precisionTargetRefreshToken"
|
||||||
:showSectorLabels="precisionBlocks > 0"
|
|
||||||
stable-shot-effect
|
stable-shot-effect
|
||||||
@shot-effect-complete="onShotEffectComplete"
|
@shot-effect-complete="onShotEffectComplete"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export default defineStore("store", {
|
|||||||
tips: "", // 当前提示文案(用于 HeaderProgress 恢复状态,替代 uni.$emit 避免时序问题)
|
tips: "", // 当前提示文案(用于 HeaderProgress 恢复状态,替代 uni.$emit 避免时序问题)
|
||||||
},
|
},
|
||||||
dailyCount: getDefaultDailyCount(),
|
dailyCount: getDefaultDailyCount(),
|
||||||
|
deviceChargingDialogVisible: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
@@ -187,6 +188,12 @@ export default defineStore("store", {
|
|||||||
...(data || {}),
|
...(data || {}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
showDeviceChargingDialog() {
|
||||||
|
this.deviceChargingDialogVisible = true;
|
||||||
|
},
|
||||||
|
hideDeviceChargingDialog() {
|
||||||
|
this.deviceChargingDialogVisible = false;
|
||||||
|
},
|
||||||
clearSessionState() {
|
clearSessionState() {
|
||||||
this.$patch({
|
this.$patch({
|
||||||
user: getDefaultUser(),
|
user: getDefaultUser(),
|
||||||
@@ -194,6 +201,7 @@ export default defineStore("store", {
|
|||||||
online: false,
|
online: false,
|
||||||
game: getDefaultGame(),
|
game: getDefaultGame(),
|
||||||
dailyCount: getDefaultDailyCount(),
|
dailyCount: getDefaultDailyCount(),
|
||||||
|
deviceChargingDialogVisible: false,
|
||||||
});
|
});
|
||||||
uni.removeStorageSync(PERSISTED_STORE_KEY);
|
uni.removeStorageSync(PERSISTED_STORE_KEY);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
+15
-6
@@ -8,6 +8,13 @@ let manualClose = false;
|
|||||||
let checkingSession = false;
|
let checkingSession = false;
|
||||||
let kickedOut = false;
|
let kickedOut = false;
|
||||||
let isConnecting = false;
|
let isConnecting = false;
|
||||||
|
const ENABLE_REALTIME_MESSAGE_LOG = (() => {
|
||||||
|
try {
|
||||||
|
return uni.getAccountInfoSync().miniProgram.envVersion !== "release";
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
function createWebSocket(token, onMessage) {
|
function createWebSocket(token, onMessage) {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
@@ -73,22 +80,22 @@ function createWebSocket(token, onMessage) {
|
|||||||
const { data, event } = JSON.parse(res.data);
|
const { data, event } = JSON.parse(res.data);
|
||||||
if (event === "pong") return;
|
if (event === "pong") return;
|
||||||
if (data.type) {
|
if (data.type) {
|
||||||
console.log(
|
if (ENABLE_REALTIME_MESSAGE_LOG) {
|
||||||
"收到 WebSocket 消息",
|
console.log("收到 WebSocket 消息", getMessageTypeName(data.type));
|
||||||
getMessageTypeName(data.type),
|
}
|
||||||
data.data
|
|
||||||
);
|
|
||||||
if (onMessage) onMessage({ ...(data.data || {}), type: data.type });
|
if (onMessage) onMessage({ ...(data.data || {}), type: data.type });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (onMessage && data.updates) onMessage(data.updates);
|
if (onMessage && data.updates) onMessage(data.updates);
|
||||||
const msg = data.updates[0];
|
const msg = data.updates[0];
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
if (ENABLE_REALTIME_MESSAGE_LOG) {
|
||||||
console.log(
|
console.log(
|
||||||
"收到 WebSocket 更新",
|
"收到 WebSocket 更新",
|
||||||
getMessageTypeName(msg.constructor),
|
getMessageTypeName(msg.constructor),
|
||||||
msg
|
{ updateCount: data.updates.length }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
if (msg.constructor === MESSAGETYPES.RankUpdate) {
|
if (msg.constructor === MESSAGETYPES.RankUpdate) {
|
||||||
uni.setStorageSync("latestRank", msg.lvl);
|
uni.setStorageSync("latestRank", msg.lvl);
|
||||||
} else if (msg.constructor === MESSAGETYPES.LvlUpdate) {
|
} else if (msg.constructor === MESSAGETYPES.LvlUpdate) {
|
||||||
@@ -97,6 +104,8 @@ function createWebSocket(token, onMessage) {
|
|||||||
uni.$emit("update-online");
|
uni.$emit("update-online");
|
||||||
} else if (msg.constructor === MESSAGETYPES.DeviceOffline) {
|
} else if (msg.constructor === MESSAGETYPES.DeviceOffline) {
|
||||||
uni.$emit("update-online");
|
uni.$emit("update-online");
|
||||||
|
} else if (msg.constructor === MESSAGETYPES.DeviceCharging) {
|
||||||
|
uni.$emit("device-charging");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user