44 Commits
Author SHA1 Message Date
chenlimao ad8bc1f152 fix: 把 OTA 图片本地引用改成了同名 CDN 地址 2026-07-09 16:25:28 +08:00
zhangyi 348d490de8 update:优化获取手机号 2026-07-06 11:15:38 +08:00
zhangyi d3e0307854 update:优化vip套餐 2026-07-06 09:37:08 +08:00
zhangyi 0a678cb387 update:修复大乱斗特效飘移 2026-07-03 17:46:00 +08:00
zhangyi 3c7467fbdf Merge branch 'test' into feat-vip 2026-07-03 17:04:09 +08:00
zhangyi 8b11e41957 Merge branch 'feat-vip' into test 2026-07-03 13:56:06 +08:00
chenlimao 78a495de60 fix: OTA升级电量限制改为50 2026-07-02 09:22:35 +08:00
chenlimao ef13ff6a2b fix: 修复wifi页面连接超时问题 2026-07-01 15:56:21 +08:00
chenlimao 8b7a94b8c4 fix: 修复wifi页面连接超时问题 2026-07-01 15:54:58 +08:00
chenlimao 40b94dbad8 fix: OTAwifi页面跳转逻辑优化 2026-06-30 17:06:52 +08:00
chenlimao 422a93dbe2 fix: 首页ota更新失败弹窗增加关闭按钮 2026-06-29 15:44:52 +08:00
chenlimao 5c74edf001 fix: 修复版本号背景图展示不完全问题 2026-06-29 15:11:06 +08:00
chenlimao 937308e51d feat: OTA部分接口对接后端 2026-06-29 13:41:28 +08:00
chenlimao 165f1a40bd Merge branch 'test' into ota-mode
# Conflicts:
#	src/apis.js
2026-06-26 14:45:51 +08:00
chenlimao 35c49bbb10 feat: OTA部分对接代码提交 2026-06-26 14:43:44 +08:00
zhangyi ad0fb0b337 update:教练点评判断优化 2026-06-26 11:04:51 +08:00
zhangyi df628b48c6 update:优化套餐说明 2026-06-26 10:32:25 +08:00
zhangyi c435372d33 Merge branch 'feat-vip' into test 2026-06-25 18:16:54 +08:00
zhangyi b6b517be9e Merge branch 'feat-vip' into test 2026-06-24 17:10:24 +08:00
zhangyi bd352c04a8 update:vip补充协议 2026-06-22 15:37:01 +08:00
zhangyi 4abb0de1e7 update:修改会员页默认值 2026-06-18 18:10:21 +08:00
zhangyi 7591058786 Merge branch 'feat-vip' into test 2026-06-18 16:24:47 +08:00
zhangyi 045827cb33 update:优化语音播报流程 2026-06-15 15:35:04 +08:00
zhangyi 69ed5bda1b Merge branch 'fix-bug' into test 2026-06-15 09:30:24 +08:00
zhangyi 5bab30d2e4 update:修复大乱斗显示问题,修复连续3次10环播报 2026-06-15 09:30:02 +08:00
zhangyi 8b8366f30f update:大乱斗优化 2026-06-12 16:44:34 +08:00
zhangyi 8b2ea24f38 update:大乱斗优化 2026-06-12 16:44:17 +08:00
zhangyi c30aa45e5b update:更新靶纸 2026-06-11 18:13:12 +08:00
zhangyi 5cf243d187 update:优化设备缓存逻辑 2026-06-11 18:10:59 +08:00
zhangyi bfdd40ec93 update:修复对战后退异常问题,修复对战中设备离线弹窗点击问题 2026-06-10 17:36:37 +08:00
chenlimao 54e5427a77 fix:新增更新中图片&设备页面新增跳转wifi页面入口 2026-06-09 09:59:13 +08:00
chenlimao 9b47b313a7 fix:wifi页面底部弹窗错误文案样式修改 2026-06-03 16:07:47 +08:00
chenlimao 350c6373ab fix:修复wifi弹窗不同输入法导致的结构错层 2026-06-03 13:42:48 +08:00
chenlimao 048b0e9fa9 fix:OTA弹窗新增更新中状态弹窗 2026-06-03 10:09:27 +08:00
chenlimao 39d34d5bff fix:wifi页面各种更新状态UI样式优化 2026-06-02 17:55:18 +08:00
chenlimao 37f4f0c019 fix:wifi页面密码查看图标引入 2026-06-02 16:50:10 +08:00
chenlimao e9cd9c8c9f fix:wifi页面新增授权失败的逻辑 2026-06-02 15:52:31 +08:00
chenlimao 73a41ab897 fix:wifi页面wifi列表新增刷新功能 2026-06-02 15:30:31 +08:00
chenlimao 32c37e283f fix:wifi页面弹窗隐藏逻辑修改&密码输入框新增查看密码功能 2026-06-02 15:03:10 +08:00
chenlimao 209103742d fix:wifi页面解决输入框遮住问题 2026-06-02 14:48:24 +08:00
chenlimao 27d11a8129 fix:小程序获取wifi兼容iso设备 2026-06-01 18:10:17 +08:00
chenlimao c9ea368a19 fix:wifi页面列表样式优化 2026-06-01 17:46:18 +08:00
chenlimao b5e7e72104 fix:已连接网络模块结构样式完成 2026-06-01 17:09:14 +08:00
chenlimao 7a00c1bb1f fix:OTA弹窗+wifi页面结构样式修改 2026-06-01 16:44:08 +08:00
33 changed files with 2473 additions and 182 deletions
+1
View File
@@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
.history
.github
.claude
openspec
CLAUDE.md
docs
+9 -1
View File
@@ -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();
});
</script>
+38
View File
@@ -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});
};
@@ -487,6 +521,10 @@ export const getPhoneNumberAPI = (data) => {
return request("POST", "/index/getPhone", data);
};
export const getPhoneNumberAPIv2 = (data) => {
return request("POST", "/index/getPhone/v2", data);
};
export const getPointBookRankListAPI = (page = 1) => {
return request(
"GET",
+183 -16
View File
@@ -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;
+16 -3
View File
@@ -34,6 +34,7 @@ const phase = ref("idle");
const activePlayKey = ref("");
const animationKey = ref("");
const impactEmitted = ref(false);
const activeShot = ref(null);
let timers = [];
const isActive = computed(() => phase.value !== "idle");
@@ -54,9 +55,17 @@ const safeTargetSize = computed(() => {
};
});
function hasShotPoint(shot) {
const x = Number(shot?.x);
const y = Number(shot?.y);
return Number.isFinite(x) && Number.isFinite(y);
}
const effectiveShot = computed(() => activeShot.value || props.shot);
const shotPoint = computed(() => {
const x = Number(props.shot?.x);
const y = Number(props.shot?.y);
const x = Number(effectiveShot.value?.x);
const y = Number(effectiveShot.value?.y);
return {
x: Number.isFinite(x) ? x : 0,
y: Number.isFinite(y) ? y : 0,
@@ -179,16 +188,20 @@ function finish(playKey) {
clearTimers();
phase.value = "idle";
activePlayKey.value = "";
activeShot.value = null;
emit("complete", playKey);
}
function play() {
if (!props.playKey || !props.shot || !props.shot.ring) return;
if (!props.playKey || !props.shot || !props.shot.ring || !hasShotPoint(props.shot)) {
return;
}
clearTimers();
activePlayKey.value = props.playKey;
animationKey.value = `${props.playKey}`;
impactEmitted.value = false;
activeShot.value = { ...props.shot };
phase.value = "playing";
queueTimer(() => {
+65 -24
View File
@@ -1,5 +1,13 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount, computed, nextTick } from "vue";
import {
ref,
watch,
onMounted,
onBeforeUnmount,
computed,
nextTick,
getCurrentInstance,
} from "vue";
import PointSwitcher from "@/components/PointSwitcher.vue";
import BowShotEffect from "@/components/BowShotEffect.vue";
@@ -56,8 +64,6 @@ const props = defineProps({
const pMode = ref(true);
const latestOne = ref(null);
const bluelatestOne = ref(null);
const prevScores = ref([]);
const prevBlueScores = ref([]);
const timer = ref(null);
const dirTimer = ref(null);
const angle = ref(null);
@@ -66,7 +72,9 @@ const shotEffect = ref(null);
const hiddenRedLatestKey = ref("");
const hiddenBlueLatestKey = ref("");
const targetShaking = ref(false);
const targetSize = ref({ width: 0, height: 0 });
const shakeTimer = ref(null);
const instance = getCurrentInstance();
const ROUND_TIP_OFFSET_Y = -32;
const EXPERIENCE_TIP_OFFSET_Y = -68;
@@ -82,8 +90,14 @@ function buildShotEffectKey(team, shot, index) {
].join("-");
}
function hasShotPoint(shot) {
const x = Number(shot?.x);
const y = Number(shot?.y);
return Number.isFinite(x) && Number.isFinite(y);
}
function shouldPlayShotEffect(shot) {
return props.isSvip && !!shot && Number(shot.ring) > 0;
return props.isSvip && !!shot && Number(shot.ring) > 0 && hasShotPoint(shot);
}
function clearTipTimer() {
@@ -154,6 +168,29 @@ function shakeTarget() {
});
}
function updateTargetSize() {
nextTick(() => {
const query = instance?.proxy
? uni.createSelectorQuery().in(instance.proxy)
: uni.createSelectorQuery();
query
.select(".target")
.boundingClientRect((rect) => {
const width = Number(rect?.width);
const height = Number(rect?.height);
if (!Number.isFinite(width) || !Number.isFinite(height)) return;
if (width <= 0 || height <= 0) return;
targetSize.value = { width, height };
})
.exec();
});
}
function handleWindowResize() {
updateTargetSize();
}
function shouldHideRedHit(index) {
return !!hiddenRedLatestKey.value && index === props.scores.length - 1;
}
@@ -163,46 +200,44 @@ function shouldHideBlueHit(index) {
}
watch(
() => props.scores,
(newVal) => {
if (newVal.length - prevScores.value.length === 1) {
const latestShot = newVal[newVal.length - 1];
() => props.scores.length,
(newLen, oldLen) => {
if (newLen === oldLen + 1) {
const latestShot = props.scores[newLen - 1];
if (shouldPlayShotEffect(latestShot)) {
triggerShotEffect("red", latestShot, newVal.length - 1);
triggerShotEffect("red", latestShot, newLen - 1);
} else {
showShotTip("red", latestShot);
}
} else if (newVal.length <= prevScores.value.length) {
return;
}
if (newLen < oldLen) {
latestOne.value = null;
hiddenRedLatestKey.value = "";
if (shotEffect.value?.team === "red") shotEffect.value = null;
}
prevScores.value = [...newVal];
},
{
deep: true,
}
);
watch(
() => props.blueScores,
(newVal) => {
if (newVal.length - prevBlueScores.value.length === 1) {
const latestShot = newVal[newVal.length - 1];
() => props.blueScores.length,
(newLen, oldLen) => {
if (newLen === oldLen + 1) {
const latestShot = props.blueScores[newLen - 1];
if (shouldPlayShotEffect(latestShot)) {
triggerShotEffect("blue", latestShot, newVal.length - 1);
triggerShotEffect("blue", latestShot, newLen - 1);
} else {
showShotTip("blue", latestShot);
}
} else if (newVal.length <= prevBlueScores.value.length) {
return;
}
if (newLen < oldLen) {
bluelatestOne.value = null;
hiddenBlueLatestKey.value = "";
if (shotEffect.value?.team === "blue") shotEffect.value = null;
}
prevBlueScores.value = [...newVal];
},
{
deep: true,
}
);
@@ -349,6 +384,8 @@ async function onReceiveMessage(message) {
onMounted(() => {
uni.$on("socket-inbox", onReceiveMessage);
updateTargetSize();
if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
});
onBeforeUnmount(() => {
@@ -365,6 +402,7 @@ onBeforeUnmount(() => {
shakeTimer.value = null;
}
uni.$off("socket-inbox", onReceiveMessage);
if (uni.offWindowResize) uni.offWindowResize(handleWindowResize);
});
</script>
@@ -461,6 +499,9 @@ onBeforeUnmount(() => {
:shot="shotEffect && shotEffect.shot"
:playKey="shotEffect ? shotEffect.key : ''"
:targetRadius="safeTargetRadius"
:targetWidth="targetSize.width"
:targetHeight="targetSize.height"
:hitOffsetPx="currentHitRadiusPx"
@impact="shakeTarget"
@complete="completeShotEffect"
/>
+429
View File
@@ -0,0 +1,429 @@
<script setup>
import { computed } from "vue";
import { getDeviceBatteryAPI } from "@/apis";
const OTA_MIN_BATTERY = 50;
const OTA_LOW_BATTERY_TEXT = "电量不足 50%,暂不支持 OTA 升级";
const OTA_OFFLINE_TEXT = "请先开启智能弓";
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
state: {
type: String,
default: "new_version", // new_version | update_progress | update_success | update_failure
},
version: {
type: String,
default: "",
},
progress: {
type: Number,
default: 40,
},
// 副标题:如“新版本将优化智能弓体验”
description: {
type: String,
default: "",
},
// 详细说明:如“升级前请确保:...”
changelog: {
type: String,
default: "",
},
forceUpdate: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update", "skip", "close", "done", "retry"]);
const isNewVersion = computed(() => props.state === "new_version");
const isProgress = computed(() => props.state === "update_progress");
const isSuccess = computed(() => props.state === "update_success");
const isFailure = computed(() => props.state === "update_failure");
// Clamp progress to keep the progress bar width within its container.
const progressValue = computed(() => Math.min(100, Math.max(0, Number(props.progress) || 0)));
// 点击立即更新前先校验设备在线状态,再校验设备电量。
const handleUpdateClick = async () => {
try {
const deviceStatus = await getDeviceBatteryAPI();
if (deviceStatus?.online !== true) {
uni.showToast({
title: OTA_OFFLINE_TEXT,
icon: "none",
});
return;
}
if (Number(deviceStatus?.battery) <= OTA_MIN_BATTERY) {
uni.showToast({
title: OTA_LOW_BATTERY_TEXT,
icon: "none",
});
return;
}
} catch (err) {
emit("update");
return;
}
emit("update");
};
</script>
<template>
<view v-if="visible" class="ota-mask">
<!-- 图标 + 弹窗卡片 容器 -->
<view
class="ota-outer"
:class="isNewVersion ? 'outer-new' : 'outer-result'"
>
<!-- 悬浮图标溢出卡片顶部 -->
<image
v-if="isNewVersion"
src="https://static.shelingxingqiu.com/shootmini/static/ota/ota-mascot.png"
mode="aspectFit"
class="float-icon float-mascot"
/>
<image
v-else-if="isSuccess"
src="https://static.shelingxingqiu.com/shootmini/static/ota/check-char.png"
mode="aspectFit"
class="float-icon float-check"
/>
<image
v-else-if="isFailure"
src="https://static.shelingxingqiu.com/shootmini/static/ota/close-char.png"
mode="aspectFit"
class="float-icon float-close"
/>
<image
v-else-if="isProgress"
src="https://static.shelingxingqiu.com/shootmini/static/ota/target-char.png"
mode="aspectFit"
class="float-icon float-target"
/>
<!-- 弹窗卡片overflow:visible 允许按钮溢出底部背景图通过 ota-bg-clip 独立裁剪保持圆角 -->
<view class="ota-dialog">
<view class="ota-bg-clip">
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/ota-bg.png" mode="aspectFill" class="ota-bg" />
</view>
<view
class="ota-content"
:class="{ 'content-new': isNewVersion, 'content-result': isProgress || isSuccess || isFailure }"
>
<!-- 发现新版本new-ver.png 已包含标题图不再重复文字版本号使用 ota-ver.png 胶囊背景 -->
<block v-if="isNewVersion">
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/new-ver.png" mode="aspectFit" class="new-ver-img" />
<view v-if="version" class="version-tag-wrap">
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/ota-ver.png" mode="aspectFit" class="version-tag-bg-img" />
<text class="version-tag">{{ version }}</text>
</view>
<!-- 副标题新版本将优化智能弓体验离下方详情 12rpx -->
<text v-if="description" class="desc-text">{{ description }}</text>
<!-- 详细说明升级前请确保... -->
<text v-if="changelog" class="changelog-text">{{ changelog }}</text>
<view class="btn-group">
<view class="primary-btn" @click="handleUpdateClick">
<text class="primary-btn-text">立即更新</text>
</view>
<text v-if="!forceUpdate" class="skip-text" @click="emit('skip')">暂不更新</text>
</view>
</block>
<!-- 更新成功图片左边距 34rpx文案左边距 44rpx按钮浮动底部居中 -->
<block v-else-if="isSuccess">
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/update-ok.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" />
<text class="dialog-desc">请关机并重启智能弓</text>
<view class="btn-group-result">
<view class="primary-btn" @click="emit('done')">
<text class="primary-btn-text">完成</text>
</view>
</view>
</block>
<!-- 更新中复用成功标题图正文区域展示进度条无底部按钮 -->
<block v-else-if="isProgress">
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/update_progress.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" />
<view class="progress-wrap">
<view class="progress-track">
<view class="progress-fill" :style="{ width: `${progressValue}%` }"></view>
</view>
</view>
</block>
<!-- 更新失败图片左边距 34rpx文案左对齐 44rpx按钮浮动底部居中 -->
<block v-else-if="isFailure">
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/update-fail.png" mode="aspectFit" class="result-title-img" style="width: 222rpx; height: 62rpx;" />
<text class="dialog-desc">请确保</text>
<text class="dialog-desc">1智能弓已开启</text>
<text class="dialog-desc">2网路连接稳定</text>
<view class="btn-group-result">
<view class="primary-btn" @click="emit('retry')">
<text class="primary-btn-text">重试</text>
</view>
</view>
</block>
</view>
</view>
</view>
<!-- 关闭按钮仅新版本状态非强制更新时位于弹窗下方 -->
<view
v-if="(isNewVersion || isFailure) && !forceUpdate"
class="ota-close-below"
@click="emit('close')"
>
<image src="../static/sicon/close.png" mode="aspectFit" style="width: 56rpx; height: 56rpx;" />
</view>
</view>
</template>
<style scoped>
.ota-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* 外层容器:相对定位,为浮动图标创造溢出空间 */
.ota-outer {
position: relative;
overflow: visible;
}
/* 设计图:吉祥物向上突出弹窗顶部 40px(375px基准)× 2 = 80rpx */
.outer-new {
padding-top: 80rpx;
}
.outer-result {
padding-top: 80rpx;
padding-bottom: 66rpx;
}
/* 浮动图标(绝对定位,位于卡片顶部上方) */
.float-icon {
position: absolute;
z-index: 2;
}
/* 吉祥物尺寸:设计图 149×109px375px基准)× 2 = 298×218rpx */
.float-mascot {
width: 298rpx;
height: 218rpx;
top: -5px;
right: -74rpx;
}
.float-check {
width: 194rpx;
height: 166rpx;
top: 20px;
right: 30rpx;
}
.float-close {
width: 194rpx;
height: 164rpx;
top: 20px;
right: 30rpx;
}
.float-target {
width: 194rpx;
height: 166rpx;
top: 20px;
right: 30rpx;
}
/* 弹窗卡片:overflow:visible 允许按钮溢出底部,背景通过 ota-bg-clip 独立裁剪 */
.ota-dialog {
position: relative;
width: 482rpx;
border-radius: 24rpx;
border: 2rpx solid #F9D5A1;
overflow: visible;
background-color: #392F1D;
}
/* 背景图裁剪层:独立 overflow:hidden + border-radius 保持圆角效果 */
.ota-bg-clip {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 24rpx;
overflow: hidden;
z-index: 0;
}
.ota-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ota-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
/* 按钮以外内容均左对齐 */
align-items: flex-start;
}
.content-new {
padding: 30rpx 0 40rpx 0;
}
.content-result {
padding: 30rpx 0 66rpx 0;
}
/* 发现新版本内容 */
.new-ver-img {
width: 274rpx;
height: 62rpx;
/* 左边距 34rpx,去掉 margin-bottom */
margin-left: 34rpx;
}
/* 版本号胶囊容器:相对定位,使 ota-ver.png 作为背景衬底 */
.version-tag-wrap {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 116rpx;
height: 44rpx;
/* 离标题图 -10rpx,左边距 50rpx,离下方副标题 22rpx */
margin-top: -10rpx;
margin-left: 50rpx;
margin-bottom: 22rpx;
}
/* ota-ver.png 胶囊背景图 */
.version-tag-bg-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* 版本号文字:浮于背景图之上 */
.version-tag {
position: relative;
z-index: 1;
color: rgba(254, 222, 100, 1);
font-size: 24rpx;
padding: 8rpx 22rpx 4rpx 24rpx;
}
/* 副标题(如"新版本将优化智能弓体验"):左边距 44rpx,离下方文案 12rpx */
.desc-text {
font-weight: 500;
font-size: 26rpx;
color: #FFFFFF;
line-height: 36rpx;
text-align: left;
margin-left: 44rpx;
margin-bottom: 12rpx;
}
/* 详细说明文案(如“升级前请确保:...”):左边距 44rpx */
.changelog-text {
font-weight: 400;
font-size: 26rpx;
color: #FFFFFF;
line-height: 40rpx;
text-align: left;
margin-left: 44rpx;
margin-bottom: 0;
}
/* 按钮组(新版本状态):离上方文案 30rpx,内部按钮间距 24rpx */
.btn-group {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 30rpx;
gap: 24rpx;
}
/* 按钮组(结果状态):绝对定位,溢出卡片底边 -35rpx 悬浮在底边中间 */
.btn-group-result {
position: absolute;
bottom: -35rpx;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
}
/* 主按钮:按照设计规范 width: 232rpx, height: 70rpx */
.primary-btn {
width: 232rpx;
height: 70rpx;
background-color: #FED847;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.primary-btn-text {
font-weight: 500;
font-size: 26rpx;
color: #000000;
line-height: 36rpx;
}
/* 暂不更新:设计规范颜色 #5FADFF 蓝色 */
.skip-text {
font-weight: 400;
font-size: 26rpx;
color: #5FADFF;
line-height: 36rpx;
}
/* 更新结果内容:图片左边距 34rpx,下边距 16rpx */
.result-title-img {
margin-left: 34rpx;
margin-bottom: 16rpx;
}
/* 结果页文案:左对齐,左边距 44rpx,与 new_version 保持一致 */
.dialog-desc {
font-weight: 400;
font-size: 26rpx;
color: #FFFFFF;
line-height: 40rpx;
text-align: left;
margin-left: 44rpx;
}
.progress-wrap {
width: 394rpx;
margin-top: 40rpx;
margin-left: 44rpx;
}
.progress-track {
width: 100%;
height: 18rpx;
background-color: rgba(255, 255, 255, 0.28);
border-radius: 999rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #FED847;
border-radius: 999rpx;
}
/* 关闭按钮(位于弹窗下方) */
.ota-close-below {
margin-top: 40rpx;
display: flex;
justify-content: center;
}
</style>
+8 -7
View File
@@ -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 = {}) => [
<view>
<view>
<view v-for="(_, index) in rowCount" :key="index">
<text>{{
scores[0] && scores[0][index] ? `${scores[0][index].ring}` : "-"
}}</text>
<text>{{ getRingText(scores[0]?.[index]) }}</text>
</view>
</view>
<view>
<view v-for="(_, index) in rowCount" :key="index">
<text>{{
scores[1] && scores[1][index] ? `${scores[1][index].ring}` : "-"
}}</text>
<text>{{ getRingText(scores[1]?.[index]) }}</text>
</view>
</view>
</view>
<text
>{{
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)
}}</text
>
+9 -6
View File
@@ -11,12 +11,13 @@ import {
loginAPI,
getHomeData,
getPhoneNumberAPI,
getPhoneNumberAPIv2,
getDeviceBatteryAPI,
} from "@/apis";
import useStore from "@/store";
const store = useStore();
const { updateUser, updateDevice, updateOnline } = store;
const { updateUser, updateDevice, updateOnline, clearDevice } = store;
const props = defineProps({
show: {
@@ -43,12 +44,12 @@ const handleAgree = () => {
async function getphonenumber(e) {
if (e.detail.code) {
const wxResult = await wxLogin();
const result = await getPhoneNumberAPI({
...e.detail,
code: wxResult.code,
// const wxResult = await wxLogin();
const result = await getPhoneNumberAPIv2({
// ...e.detail,
code: e.detail.code,
});
if (result.phone) phone.value = result.phone;
if (result.purePhoneNumber) phone.value = result.purePhoneNumber;
}
}
@@ -122,6 +123,8 @@ async function doLogin() {
);
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
} else {
clearDevice();
}
props.onClose();
} catch (error) {
+5 -1
View File
@@ -64,7 +64,11 @@
"usingComponents" : true,
"darkmode" : true,
"themeLocation" : "theme.json",
"permission" : {},
"permission" : {
"scope.userLocation": {
"desc": "用于扫描附近 WiFi,完成设备 OTA 升级网络连接"
}
},
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
}
}
+6
View File
@@ -125,6 +125,12 @@
},
{
"path": "pages/mine-bow-data"
},
{
"path": "pages/ota-wifi",
"style": {
"navigationStyle": "custom"
}
}
],
"globalStyle": {
+1 -1
View File
@@ -315,7 +315,7 @@ function goBack() {
<Container
:bgType="data.mode > 3 ? -1 : 0"
bgColor="#000000"
:onBack="goBack"
:onBack="exit"
>
<!-- ----- Banner game 胜负展示图 NvN 对抗模式----- -->
+238 -2
View File
@@ -1,19 +1,23 @@
<script setup>
import {onMounted, ref} from "vue";
import {onMounted, onUnmounted, ref} from "vue";
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import AppFooter from "@/components/AppFooter.vue";
import UserHeader from "@/components/UserHeader.vue";
import Signin from "@/components/Signin.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import OtaModal from "@/components/OtaModal.vue";
import {
checkUserBindAPI,
getAppConfig,
getDeviceBatteryAPI,
getHardwareBoxTaskStatusAPI,
getHardwareBoxVersionAPI,
getHomeData,
getMyDevicesAPI,
getScoreRankList,
sendHardwareBoxUpdateAPI,
silentLoginAPI,
} from "@/apis";
import {topThreeColors} from "@/constants";
@@ -26,6 +30,7 @@ const {
updateConfig,
updateUser,
updateDevice,
clearDevice,
getLvlName,
getLvlNameByScore,
updateOnline,
@@ -36,6 +41,208 @@ const showModal = ref(false);
const showGuide = ref(false);
const scoreRankList = ref([]);
// OTA 相关
const otaVisible = ref(false);
const otaState = ref("new_version");
const otaProgress = ref(0);
const otaInfo = ref({
versionNumber: "",
versionInfo: "",
resourceUrl: "",
forceUpdate: false,
});
const isStartingOta = ref(false);
let otaProgressTimer = null;
let otaStatusTimer = null;
let otaTimeoutTimer = null;
// 清理首页 OTA 更新定时器,避免弹窗关闭或页面卸载后继续轮询。
const clearOtaUpdateTimers = () => {
clearInterval(otaProgressTimer);
clearTimeout(otaStatusTimer);
clearTimeout(otaTimeoutTimer);
otaProgressTimer = null;
otaStatusTimer = null;
otaTimeoutTimer = null;
};
// 启动首页 OTA 本地进度动画,最终成功失败以后端任务状态为准。
const startOtaProgressAnimation = () => {
clearInterval(otaProgressTimer);
otaProgressTimer = setInterval(() => {
if (otaProgress.value >= 90) {
clearInterval(otaProgressTimer);
return;
}
const increment = Math.max(0.5, 2 - otaProgress.value / 60);
otaProgress.value = Math.min(90, otaProgress.value + increment);
}, 500);
};
// 获取并保存后端返回的 OTA 版本信息,供弹窗展示和更新接口使用。
const applyOtaVersionInfo = (versionInfo) => {
otaInfo.value = {
versionNumber: versionInfo?.versionNumber || "",
versionInfo: versionInfo?.versionInfo || "",
resourceUrl: versionInfo?.resourceUrl || "",
forceUpdate: Number(versionInfo?.forceUpdate) === 1,
};
};
// 检查当前设备盒子是否存在可升级版本。
const checkOtaUpdate = async () => {
let versionInfo;
try {
versionInfo = await getHardwareBoxVersionAPI();
} catch (err) {
return;
}
if (!versionInfo?.needUpdate) return;
applyOtaVersionInfo(versionInfo);
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
const now = Date.now();
if (!otaInfo.value.forceUpdate && dismissedAt && now - dismissedAt < 24 * 60 * 60 * 1000) return;
otaState.value = "new_version";
otaVisible.value = true;
};
// 拼接 OTA WiFi 页参数,让未连 WiFi 的设备继续使用同一份版本信息。
const getOtaWifiUrl = () => {
const { versionNumber, resourceUrl } = otaInfo.value;
const query = [
`versionNumber=${encodeURIComponent(versionNumber)}`,
`resourceUrl=${encodeURIComponent(resourceUrl)}`,
].join("&");
return `/pages/ota-wifi?${query}`;
};
// 处理 OTA 弹窗暂不更新,强制更新时不允许关闭。
const handleOtaDismiss = () => {
if (otaInfo.value.forceUpdate) return;
uni.setStorageSync("ota_dismissed_at", Date.now());
otaVisible.value = false;
};
// 将首页 OTA 直连更新流程标记为失败。
const failHomeOtaUpdate = () => {
clearOtaUpdateTimers();
isStartingOta.value = false;
otaState.value = "update_failure";
otaVisible.value = true;
};
// 将首页 OTA 直连更新流程标记为成功。
const completeHomeOtaUpdate = () => {
clearOtaUpdateTimers();
isStartingOta.value = false;
otaProgress.value = 100;
setTimeout(() => {
otaState.value = "update_success";
otaVisible.value = true;
}, 300);
};
// 轮询首页直接发起的 OTA 更新任务状态。
const pollHomeOtaTaskStatus = (taskId) => {
clearTimeout(otaStatusTimer);
otaStatusTimer = setTimeout(async () => {
try {
const taskStatus = await getHardwareBoxTaskStatusAPI(taskId);
const status = Number(taskStatus?.status);
if (status === 2) {
completeHomeOtaUpdate();
return;
}
if (status === 3) {
failHomeOtaUpdate();
return;
}
if (status === 0 || status === 1) {
pollHomeOtaTaskStatus(taskId);
return;
}
failHomeOtaUpdate();
} catch (err) {
failHomeOtaUpdate();
}
}, 3000);
};
// 设备盒子已连 WiFi 时,从首页直接传空 WiFi 信息发起 OTA 更新。
const startHomeOtaUpdate = async () => {
otaState.value = "update_progress";
otaVisible.value = true;
otaProgress.value = 0;
startOtaProgressAnimation();
otaTimeoutTimer = setTimeout(() => {
if (otaState.value === "update_progress") {
failHomeOtaUpdate();
}
}, 5 * 60 * 1000);
try {
const updateResult = await sendHardwareBoxUpdateAPI({
versionNumber: otaInfo.value.versionNumber,
wifiSsid: "",
wifiPassword: "",
resourceUrl: otaInfo.value.resourceUrl,
});
if (!updateResult?.taskId) {
failHomeOtaUpdate();
return;
}
pollHomeOtaTaskStatus(updateResult.taskId);
} catch (err) {
failHomeOtaUpdate();
}
};
// 点击立即更新时先判断设备是否在线并已通过 WiFi 联网,已联网则首页直接更新,否则跳转 WiFi 页面。
const handleOtaUpdate = async () => {
if (isStartingOta.value) return;
isStartingOta.value = true;
let deviceStatus;
try {
deviceStatus = await getDeviceBatteryAPI();
} catch (err) {
isStartingOta.value = false;
uni.showToast({
title: "获取设备状态失败,请重试",
icon: "none",
});
return;
}
if (deviceStatus?.online !== true) {
isStartingOta.value = false;
uni.showToast({
title: "请先开启智能弓",
icon: "none",
});
return;
}
if (String(deviceStatus?.netType || "").toLowerCase() === "wifi") {
startHomeOtaUpdate();
return;
}
isStartingOta.value = false;
otaVisible.value = false;
uni.navigateTo({ url: getOtaWifiUrl() });
};
// 处理 OTA 更新成功后的完成按钮,关闭结果弹窗。
const handleOtaDone = () => {
otaVisible.value = false;
};
// 处理 OTA 更新失败后的重试按钮,重新走立即更新判断流程。
const handleOtaRetry = () => {
handleOtaUpdate();
};
// 提取积分榜接口返回的榜单数组,兼容数组和对象两种返回格式。
const getScoreRankData = (result) => {
if (Array.isArray(result)) return result;
@@ -63,10 +270,18 @@ const toRankListPage = () => {
});
};
onShow(async () => {
onShow(async (options) => {
const env = uni.getAccountInfoSync().miniProgram.envVersion;
const token = uni.getStorageSync(`${env}_token`);
// 检查是否从 OTA 更新页面返回
if (options && options.updateResult) {
otaState.value = options.updateResult;
otaVisible.value = true;
} else if (token || user.value.id) {
await checkOtaUpdate();
}
if (!user.value.id && !token) {
// showModal.value = true;
// try {
@@ -127,6 +342,8 @@ onShow(async () => {
);
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
} else {
clearDevice();
}
}
}
@@ -138,6 +355,10 @@ onMounted(async () => {
console.log("全局配置:", config);
});
onUnmounted(() => {
clearOtaUpdateTimers();
});
onShareAppMessage(() => {
return {
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
@@ -158,6 +379,21 @@ onShareTimeline(() => {
<template>
<Container :isHome="true" :showBackToGame="true">
<!-- OTA 升级弹窗使用 visible 控制显隐description 为副标题changelog 为详细说明 -->
<OtaModal
:visible="otaVisible"
:state="otaState"
:version="otaInfo.versionNumber"
:progress="otaProgress"
:description="''"
:changelog="otaInfo.versionInfo"
:forceUpdate="otaInfo.forceUpdate"
@update="handleOtaUpdate"
@skip="handleOtaDismiss"
@close="handleOtaDismiss"
@done="handleOtaDone"
@retry="handleOtaRetry"
/>
<view class="container">
<view class="top-theme">
<!-- <image
+73 -14
View File
@@ -30,11 +30,64 @@ const playersSorted = ref([]);
const playersScores = ref([]);
const halfTimeTip = ref(false);
const halfRest = ref(false);
const HALF_REST_SECONDS = 20;
const halfRestRemain = ref(HALF_REST_SECONDS);
let halfRestTimer = null;
/** 控制设备离线提示弹窗的显示状态 */
const showOfflineModal = ref(false);
/** 记录每位玩家当前半场连续 X 环数,key 为 playerId,用于触发 tententen 音效 */
/** 记录每位玩家当前半场连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
const xRingStreaks = ref({});
function clearHalfRestCountdown() {
if (halfRestTimer) {
clearInterval(halfRestTimer);
halfRestTimer = null;
}
}
function getHalfRestSeconds(battleInfo) {
const remainCandidates = [
battleInfo?.halfRestRemain,
battleInfo?.halfRestRemainSeconds,
battleInfo?.restRemain,
battleInfo?.restRemainSeconds,
];
for (const item of remainCandidates) {
const remain = Number(item);
if (Number.isFinite(remain) && remain > 0 && remain <= HALF_REST_SECONDS) {
return Math.ceil(remain);
}
}
const endTime = Number(battleInfo?.halfRestEndTime ?? battleInfo?.restEndTime);
if (!Number.isFinite(endTime) || endTime <= 0) return HALF_REST_SECONDS;
const timestamp = endTime < 1e12 ? endTime * 1000 : endTime;
const diffSeconds = (timestamp - Date.now()) / 1000;
if (diffSeconds > 0 && diffSeconds <= HALF_REST_SECONDS) {
return Math.ceil(diffSeconds);
}
return HALF_REST_SECONDS;
}
function startHalfRestCountdown(seconds = HALF_REST_SECONDS) {
clearHalfRestCountdown();
halfRestRemain.value = Math.max(0, Math.ceil(Number(seconds) || HALF_REST_SECONDS));
if (halfRestRemain.value <= 0) return;
halfRestTimer = setInterval(() => {
if (halfRestRemain.value <= 1) {
halfRestRemain.value = 0;
clearHalfRestCountdown();
return;
}
halfRestRemain.value -= 1;
}, 1000);
}
const currentPlayer = computed(() =>
players.value.find((player) => String(player?.id) === String(user.value.id))
);
@@ -96,8 +149,7 @@ function recoverData(battleInfo, { force = false } = {}) {
halfTimeTip.value = true;
halfRest.value = true;
tips.value = "准备下半场";
// 剩余休息时间
// const remain = (Date.now() - battleInfo.timeoutTime) / 1000;
startHalfRestCountdown(getHalfRestSeconds(battleInfo));
setTimeout(() => {
uni.$emit("update-remain", 0);
}, 200);
@@ -128,23 +180,27 @@ onLoad(async (options) => {
});
/**
* 检测指定玩家连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
* 检测指定玩家连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
* @param {number|string} playerId - 本次射手的 ID(大乱斗中 ShootResult 保留 playerId
* @param {boolean} isXRing - 本次射击是否为 X 环
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
*/
function checkAndPlayTententen(playerId, isXRing) {
function isTenPlusRing(shot) {
return !!(shot?.ringX || Number(shot?.ring) >= 10);
}
function checkAndPlayTententen(playerId, isTenPlusRingShot) {
if (!playerId) return;
const id = parseInt(playerId);
if (isXRing) {
if (isTenPlusRingShot) {
xRingStreaks.value[id] = (xRingStreaks.value[id] || 0) + 1;
// 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放
// 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
if (xRingStreaks.value[id] >= 3) {
xRingStreaks.value[id] = 0;
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
nextTick(() => audioManager.play("tententen", false));
}
} else {
// 非 X 环则重置该玩家的连续计数
// 低于 10 环或未上靶则重置该玩家的连续计数
xRingStreaks.value[id] = 0;
}
}
@@ -152,6 +208,7 @@ function checkAndPlayTententen(playerId, isXRing) {
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.BattleStart) {
clearHalfRestCountdown();
halfTimeTip.value = false;
halfRest.value = false;
recoverData(msg);
@@ -166,22 +223,23 @@ async function onReceiveMessage(msg) {
// 对比更新后数据找出箭数增加的玩家(即本次射手),并读取其最新箭的 ring 数据
const newRound = playersScores.value[playersScores.value.length - 1] || {};
let shooterId = null;
let isXRing = false;
let isTenPlusRingShot = false;
for (const pid of Object.keys(newRound)) {
const newLen = (newRound[pid] || []).length;
if (newLen > (prevCounts[pid] || 0)) {
shooterId = parseInt(pid);
const shot = newRound[pid][newLen - 1];
isXRing = !!(shot?.ringX && shot?.ring);
isTenPlusRingShot = isTenPlusRing(shot);
break;
}
}
// 检测同一玩家三箭全 X 环,触发 tententen 音效
checkAndPlayTententen(shooterId, isXRing);
// 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
checkAndPlayTententen(shooterId, isTenPlusRingShot);
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
halfTimeTip.value = true;
halfRest.value = true;
tips.value = "准备下半场";
startHalfRestCountdown();
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(() => {
// 全部跳转到新结算页
@@ -202,6 +260,7 @@ onBeforeUnmount(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
clearHalfRestCountdown();
uni.$off("socket-inbox", onReceiveMessage);
audioManager.stopAll();
});
@@ -268,7 +327,7 @@ onShow(async () => {
>
<view class="half-time-tip">
<text>上半场结束休息一下吧:</text>
<text>20秒后开始下半场</text>
<text>{{ halfRestRemain }}秒后开始下半场</text>
</view>
</ScreenHint>
<!-- 设备离线提示弹窗 -->
+31 -60
View File
@@ -19,7 +19,7 @@ const loadingConfig = ref(false);
const paying = ref(false);
const refreshing = ref(false);
// 会员页核心展示数据:视觉、权益、套餐均按蓝湖当前两张设计稿拆分。
// 会员页核心展示数据:视觉、权益按蓝湖当前两张设计稿拆分,套餐完全使用接口数据
const memberTypes = [
{
key: "normal",
@@ -41,11 +41,6 @@ const memberTypes = [
{ label: "排位赛\n每日+20次", icon: "../../static/vip/vip-rank.png" },
{ label: "约战\n每日+20次", icon: "../../static/vip/vip-battle.png" },
],
packages: [
{ name: "连续包月", price: "20", original: "35" },
{ name: "12个月", price: "300", original: "420" },
{ name: "3个月", price: "84", original: "105" },
],
},
{
key: "super",
@@ -70,11 +65,6 @@ const memberTypes = [
{ label: "排位赛无限制", icon: "../../static/vip/svip-rank.png" },
{ label: "专享SVIP客服", icon: "../../static/vip/svip-service.png" },
],
packages: [
{ name: "连续包月", price: "20", original: "35" },
{ name: "12个月", price: "300", original: "420" },
{ name: "3个月", price: "84", original: "105" },
],
},
];
@@ -140,9 +130,17 @@ const getMenuName = (item) => {
return item.name || item.vipName || item.title || "";
};
const formatPrice = (value) => {
if (value === undefined || value === null || value === "") return "";
return String(value).replace("¥", "").replace("¥", "");
};
const getMenuPrice = (item) => {
const value = item.price || item.total || item.amount || item.money;
return value ? String(value).replace("¥", "").replace("¥", "") : "";
return formatPrice(item && item.price);
};
const getMenuOriginalPrice = (item) => {
return formatPrice(item && item.originalPrice);
};
const getMenuSortValue = (item) => {
@@ -177,48 +175,19 @@ const getMenuType = (item) => {
return "";
};
const getPackageMonths = (name) => {
if (/连续包月/.test(name)) return 1;
const match = String(name || "").match(/(\d+)\s*个?月/);
return match ? Number(match[1]) : 0;
};
const matchPackageSource = (type, pack, index) => {
const menus = sortMenusBySort(configMenus.value);
const typedMenus = menus.filter((item) => {
const menuType = getMenuType(item);
if (type.key === "super") return menuType === "super";
return menuType === "normal";
});
const pool = typedMenus.length ? typedMenus : menus;
const packMonths = getPackageMonths(pack.name);
return (
pool.find((item) => getMenuName(item).indexOf(pack.name) !== -1) ||
pool.find((item) => packMonths && getPackageMonths(getMenuName(item)) === packMonths) ||
pool.find((item) => getMenuPrice(item) === pack.price) ||
pool[index]
);
};
const getPackages = (type) => {
return type.packages
.map((item, index) => {
const source = matchPackageSource(type, item, index);
return sortMenusBySort(configMenus.value)
.filter((item) => getMenuType(item) === type.key)
.map((item) => {
return {
...item,
source,
name: source ? getMenuName(source) || item.name : item.name,
price: source ? getMenuPrice(source) || item.price : item.price,
icon: source && source.icon,
id: source && source.id,
source: item,
id: item.id,
name: getMenuName(item),
price: getMenuPrice(item),
originalPrice: getMenuOriginalPrice(item),
icon: item.icon,
};
})
.map((item, index) => ({ item, index }))
.sort((a, b) => {
const sortDiff = getMenuSortValue(a.item.source) - getMenuSortValue(b.item.source);
return sortDiff || a.index - b.index;
})
.map(({ item }) => item);
});
};
const currentPackages = computed(() => {
@@ -487,7 +456,7 @@ onShow(loadVipConfig);
<view class="package-list">
<view
v-for="(pack, index) in getPackages(type)"
:key="`${type.key}-${pack.id || pack.name}`"
:key="`${type.key}-${pack.id || pack.name || index}`"
class="package-card"
:class="{ 'package-card--active': selectedPackageIndex === index }"
@click="selectPackage(index)"
@@ -498,8 +467,8 @@ onShow(loadVipConfig);
<text class="package-price__symbol">¥</text>
<text class="package-price__value">{{ pack.price }}</text>
</view>
<view class="package-origin">
<text>¥{{ pack.original }}</text>
<view v-if="pack.originalPrice" class="package-origin">
<text>¥{{ pack.originalPrice }}</text>
<view class="package-origin__line" />
</view>
</view>
@@ -511,13 +480,14 @@ onShow(loadVipConfig);
hover-class="none"
class="activate-btn"
:class="type.buttonClass"
:disabled="loadingConfig || paying || refreshing"
:disabled="loadingConfig || paying || refreshing || !selectedPackage"
@click="onPay"
>
<text v-if="loadingConfig">加载套餐中</text>
<text v-else-if="paying">创建订单中</text>
<text v-else-if="!refreshing">¥ {{ selectedPackage.price }} 一键激活</text>
<text v-else>刷新会员状态中</text>
<text v-else-if="refreshing">刷新会员状态中</text>
<text v-else-if="selectedPackage">¥ {{ selectedPackage.price }} 一键激活</text>
<text v-else>套餐暂不可购买</text>
</button>
<view class="agreement">
@@ -784,8 +754,8 @@ onShow(loadVipConfig);
}
.package-list {
display: flex;
width: 984rpx;
display: inline-flex;
min-width: 100%;
padding: 6rpx 84rpx 6rpx 6rpx;
box-sizing: border-box;
}
@@ -793,6 +763,7 @@ onShow(loadVipConfig);
.package-card {
position: relative;
width: 264rpx;
flex: 0 0 264rpx;
height: 224rpx;
border-radius: 16rpx;
border: 2rpx solid #999999;
+37 -4
View File
@@ -16,7 +16,7 @@ const showTip = ref(false);
const confirmBindTip = ref(false);
const addDevice = ref();
const store = useStore();
const { updateDevice } = store;
const { updateDevice, clearDevice } = store;
const { user, device } = storeToRefs(store);
const justBind = ref(false);
const calibration = ref(false);
@@ -86,13 +86,26 @@ const toFristTryPage = () => {
};
const unbindDevice = async () => {
await unbindDeviceAPI(device.value.deviceId);
try {
await unbindDeviceAPI(device.value.deviceId);
} catch (error) {
if (error?.type === "DEVICE_BIND_INVALID") {
uni.setStorageSync("calibration", false);
clearDevice();
}
return;
}
uni.setStorageSync("calibration", false);
uni.showToast({
title: "解绑成功",
icon: "success",
});
device.value = {};
clearDevice();
};
/** 连接wifi跳转到wifi列表页面 */
const joinWifi = () => {
uni.navigateTo({ url: "/pages/ota-wifi" });
};
const toDeviceIntroPage = () => {
@@ -124,8 +137,23 @@ const goCalibration = async () => {
});
};
onShow(() => {
const syncDeviceBinding = async () => {
if (!user.value.id) return;
try {
const devices = await getMyDevicesAPI();
if (devices.bindings && devices.bindings.length) {
updateDevice(devices.bindings[0].deviceId, devices.bindings[0].deviceName);
} else {
clearDevice();
}
} catch (error) {
console.log("sync device binding error", error);
}
};
onShow(async () => {
calibration.value = uni.getStorageSync("calibration");
await syncDeviceBinding();
});
</script>
@@ -299,6 +327,11 @@ onShow(() => {
>解绑</SButton
>
</view>
<view :style="{ marginTop: '20rpx' }">
<SButton :onClick="() => $clickSound(joinWifi)" width="80vw" :rounded="40"
>设备连接WIFI</SButton
>
</view>
</view>
</Container>
</template>
File diff suppressed because it is too large Load Diff
+13 -9
View File
@@ -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);
+13 -9
View File
@@ -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);
+13 -9
View File
@@ -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;
+20 -16
View File
@@ -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(() => {
<view class="offline-modal">
<text class="offline-title">设备已离线</text>
<text class="offline-desc">检测到设备已断开连接请检查设备后继续比赛</text>
<SButton @click="showOfflineModal = false">我知道了</SButton>
<SButton :onClick="() => (showOfflineModal = false)">我知道了</SButton>
</view>
</SModal>
</view>
Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

+4
View File
@@ -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) {