diff --git a/src/App.vue b/src/App.vue index 4e6d0d8..5ccb912 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,9 +8,6 @@ } from "@dcloudio/uni-app"; import websocket from "@/websocket"; import matchWebsocket from "@/matchWebsocket"; - import { - getDeviceBatteryAPI - } from "@/apis"; import { MESSAGETYPES } from "@/constants"; @@ -27,7 +24,9 @@ } = storeToRefs(store); const { updateUser, - updateOnline, + updateDeviceStatus, + setDeviceOnline, + clearDeviceStatus, showDeviceChargingDialog, clearSessionState, clearDevice @@ -69,13 +68,19 @@ }); } - async function emitUpdateOnline() { - const data = await getDeviceBatteryAPI(); + function emitUpdateOnline(nextOnline) { const wasOnline = Boolean(online.value); - const nextOnline = Boolean(data.online); - updateOnline(nextOnline); - if (!device.value.deviceId || wasOnline === nextOnline) return; - audioManager.play(nextOnline ? "设备已连接" : "设备连接已断开"); + setDeviceOnline(nextOnline === true); + if (!device.value.deviceId || wasOnline === (nextOnline === true)) return; + audioManager.play(nextOnline === true ? "设备已连接" : "设备连接已断开"); + } + + function onDeviceStatusPush(status) { + updateDeviceStatus(status); + } + + function onShootSocketDisconnected() { + clearDeviceStatus(); } function onDeviceBindInvalid() { @@ -111,6 +116,10 @@ } function onShootWsMsg(content) { + if (content?.event === "/addons/shoot/battery") { + onDeviceStatusPush(content.data); + return; + } if(content.type === 'shoot-trigger'){ onDeviceShoot() } @@ -122,6 +131,7 @@ void audioManager.warmButton(); uni.$on("update-user", emitUpdateUser); uni.$on("update-online", emitUpdateOnline); + uni.$on("shoot-socket-disconnected", onShootSocketDisconnected); uni.$on("session-kicked-out", onSessionKickedOut); uni.$on("device-bind-invalid", onDeviceBindInvalid); uni.$on("device-charging", onDeviceCharging); @@ -148,6 +158,7 @@ onHide(() => { uni.$off("update-user", emitUpdateUser); uni.$off("update-online", emitUpdateOnline); + uni.$off("shoot-socket-disconnected", onShootSocketDisconnected); uni.$off("session-kicked-out", onSessionKickedOut); uni.$off("device-bind-invalid", onDeviceBindInvalid); uni.$off("device-charging", onDeviceCharging); @@ -157,6 +168,7 @@ matchWebsocket.closeMatchWebSocket({ reason: "app-hide" }); + clearDeviceStatus(); websocket.closeWebSocket(); }); diff --git a/src/apis.js b/src/apis.js index 6bf5715..eb20f8f 100644 --- a/src/apis.js +++ b/src/apis.js @@ -28,7 +28,8 @@ try { const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot"); const API_ROOT_URL = BASE_URL.replace(/\/api\/shoot$/, ""); // 统一处理业务接口请求,包含登录态、业务错误和特定接口空响应兼容。 -function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0]) { +function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0], options = {}) { + const {timeout = 10000, showErrorToast = true} = options; const token = uni.getStorageSync( `${uni.getAccountInfoSync().miniProgram.envVersion}_token` ); @@ -40,8 +41,16 @@ function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0]) method, header, data, - timeout: 10000, + timeout, success: (res) => { + if ( + url === "/user/hardwareBox/connectWifi" && + res.statusCode === 200 && + typeof res.data?.success === "boolean" + ) { + resolve(res.data); + return; + } const acceptsEmptyResponse = [ "/user/hardwareBox/connectWifi", "/user/device/unbindByQrcodeId", @@ -111,10 +120,12 @@ function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0]) icon: "none", }); } - uni.showToast({ - title: message, - icon: "none", - }); + if (showErrorToast) { + uni.showToast({ + title: message, + icon: "none", + }); + } reject(error); return; } @@ -122,7 +133,7 @@ function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0]) } }, fail: (err) => { - handleRequestError(err, url); + if (showErrorToast) handleRequestError(err, url); reject(err); }, }); @@ -281,6 +292,14 @@ export const getMyDevicesAPI = () => { return request("GET", "/user/device/getBindings"); }; +export const getDeviceDetailAPI = (deviceId) => { + return request("GET", `/user/device/getDetail?deviceId=${encodeURIComponent(deviceId)}`); +}; + +export const updateDeviceAliasAPI = (deviceId, alias) => { + return request("POST", "/user/device/updateAlias", {deviceId, alias}); +}; + export const createPractiseAPI = (arrows, time, target) => { return request("POST", "/user/practice/create", { shootNumber: arrows, @@ -561,28 +580,32 @@ export const laserCloseAPI = async () => { return request("POST", "/user/device/closeAim"); }; -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}); + return request( + "POST", + "/user/hardwareBox/connectWifi", + {ssid, password}, + BASE_URL, + [0], + {timeout: 20000, showErrorToast: false} + ); }; -// 获取硬件盒子版本信息,用于判断当前设备是否需要 OTA 升级。 -export const getHardwareBoxVersionAPI = async () => { - return request("GET", "/user/hardwareBox/version"); +// 查询设备当前联网类型及已连接的 WiFi 名称。 +export const getDeviceWifiStatusAPI = async () => { + return request("GET", "/user/device/wifiStatus", {}, BASE_URL, [0], { + showErrorToast: false, + }); }; // 发送硬件盒子 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 sendHardwareBoxUpdateAPI = async ({versionNumber, wifiSsid, wifiPassword}) => { + return request("POST", "/user/hardwareBox/sendUpdate", { + versionNumber, + wifiSsid, + wifiPassword, + }); }; export const addNoteAPI = async (id, remark) => { diff --git a/src/audioManager.js b/src/audioManager.js index 595e25f..fec9d59 100644 --- a/src/audioManager.js +++ b/src/audioManager.js @@ -2,7 +2,7 @@ export const AUDIO_INTERRUPTION_BEGIN_EVENT = "audio-interruption-begin"; export const AUDIO_INTERRUPTION_END_EVENT = "audio-interruption-end"; export const RHYTHM_SHOOT_WINDOW_AUDIO_KEY = "请射箭"; export const STABILITY_START_AUDIO_KEY = - "请在计时结束前能量条达到百分百"; + "请在计时结束时能量条达到标记区域"; export const STABILITY_ENERGY_50_AUDIO_KEY = "加油!到达50%啦"; export const STABILITY_ENERGY_70_AUDIO_KEY = "70%啦!还差一点,胜利就在眼前"; @@ -12,7 +12,7 @@ const TRAINING_START_AUDIO_KEY_MAP = Object.freeze({ endurance: "请于计时前完成累计环数和箭量", precision: "请射箭命中高亮区域", rhythm: "请在读条推进到标记区间内射箭", - stability: "请在计时结束前能量条达到百分百", + stability: STABILITY_START_AUDIO_KEY, }); export const getTrainingStartAudioKey = (trainingType, fallback = "") => @@ -176,7 +176,7 @@ export const audioFils = { 练习开始: "https://static.shelingxingqiu.com/attachment/2025-11-14/de88w0lmmt43nnfmoi.mp3", [STABILITY_START_AUDIO_KEY]: - "https://static.shelingxingqiu.com/shootaudio/%E8%AF%B7%E5%9C%A8%E8%AE%A1%E6%97%B6%E7%BB%93%E6%9D%9F%E5%89%8D%E8%83%BD%E9%87%8F%E6%9D%A1%E8%BE%BE%E5%88%B0%E7%99%BE%E5%88%86%E7%99%BE.MP3", + "https://static.shelingxingqiu.com/shootmini/static/audio0820/%E8%AF%B7%E5%9C%A8%E8%AE%A1%E6%97%B6%E7%BB%93%E6%9D%9F%E6%97%B6%E8%83%BD%E9%87%8F%E6%9D%A1%E8%BE%BE%E5%88%B0%E6%A0%87%E8%AE%B0%E5%8C%BA%E5%9F%9F.MP3", [STABILITY_ENERGY_50_AUDIO_KEY]: "https://static.shelingxingqiu.com/shootmini/static/audio0820/%E5%8A%A0%E6%B2%B9%EF%BC%81%E5%88%B0%E8%BE%BE50%25%E5%95%A6.MP3", [STABILITY_ENERGY_70_AUDIO_KEY]: diff --git a/src/canvas.js b/src/canvas.js index 853fae6..30be04a 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -451,7 +451,7 @@ export const generateShareImage = async (canvasId, data) => { hasPoint ? 756 : 402 ); renderText(ctx, "扫码打卡", 13, "#FFA118", 142, hasPoint ? 777 : 422); - const pointImg = await loadCanvasImage(canvas, "../static/point.png"); + const pointImg = await loadCanvasImage(canvas, "https://static.shelingxingqiu.com/shootmini/static/point.png"); ctx.drawImage(pointImg, 120, hasPoint ? 765 : 410, 18, 14); // 2D 即时绘制,无需 ctx.draw() } catch (e) { @@ -635,7 +635,7 @@ export function renderScores(ctx, arrows = [], bgImg) { ); } else { ctx.drawImage( - "/static/score-bg.png", + "https://static.shelingxingqiu.com/shootmini/static/score-bg.png", 16 + (i % 9) * 30, 290 + Math.ceil((i + 1) / 9) * 30, 27, @@ -657,7 +657,7 @@ export function renderScores(ctx, arrows = [], bgImg) { ctx.drawImage(bgImg, 24 + rowIndex * 42, i > 5 ? 362 : 320, 38, 38); } else { ctx.drawImage( - "/static/score-bg.png", + "https://static.shelingxingqiu.com/shootmini/static/score-bg.png", 24 + rowIndex * 42, i > 5 ? 362 : 320, 38, @@ -708,10 +708,7 @@ export async function sharePractiseData(canvasId, type, user, data) { // 头像与段位框属于装饰图片,缺失时使用兜底或跳过,避免阻断分享。 const loadProfileImage = async (src, fallbackSrc = "", label = "") => { - const normalizedSrc = - typeof src === "string" && src.startsWith("../static/") - ? src.slice(2) - : src; + const normalizedSrc = typeof src === "string" ? src : ""; if (normalizedSrc) { try { const path = await loadImage(normalizedSrc); @@ -725,20 +722,20 @@ export async function sharePractiseData(canvasId, type, user, data) { const avatarImgPromise = loadProfileImage( user?.avatar, - "/static/user-icon.png", + "https://static.shelingxingqiu.com/shootmini/static/user-icon.png", "avatar" ); const lvlImgPromise = loadProfileImage(user?.lvlImage, "", "level"); - let titleImageSrc = "/static/first-try-title.png"; + let titleImageSrc = "https://static.shelingxingqiu.com/shootmini/static/first-try-title.png"; if (type == 2) { - titleImageSrc = "/static/practise-one-title.png"; + titleImageSrc = "https://static.shelingxingqiu.com/shootmini/static/practise-one-title.png"; } else if (type == 3) { - titleImageSrc = "/static/practise-two-title.png"; + titleImageSrc = "https://static.shelingxingqiu.com/shootmini/static/practise-two-title.png"; } const titleImgPromise = loadCanvasImage(canvas, titleImageSrc); - const scoreBgImgPromise = loadCanvasImage(canvas, "/static/score-bg.png"); - const qrCodeImgPromise = loadCanvasImage(canvas, "/static/qr-code.png"); + const scoreBgImgPromise = loadCanvasImage(canvas, "https://static.shelingxingqiu.com/shootmini/static/score-bg.png"); + const qrCodeImgPromise = loadCanvasImage(canvas, "https://static.shelingxingqiu.com/shootmini/static/qr-code.png"); const [avatarImg, lvlImg, titleImg, scoreBgImg, qrCodeImg] = await Promise.all([ diff --git a/src/components/AppBackground.vue b/src/components/AppBackground.vue index 7feadea..9495dad 100644 --- a/src/components/AppBackground.vue +++ b/src/components/AppBackground.vue @@ -81,6 +81,13 @@ const props = defineProps({ src="https://static.shelingxingqiu.com/shootmini/static/app-bg9.png" mode="widthFix" /> + + const tabs = [ - { image: "../static/tab-vip.png" }, + { image: "https://static.shelingxingqiu.com/shootmini/static/tab-vip.png" }, { image: "https://static.shelingxingqiu.com/shootmini/static/tab-point-book.png" }, { image: "https://static.shelingxingqiu.com/shootmini/static/tab-mall.png" }, ]; function handleTabClick(index) { if (index === 0) { - uni.navigateTo({ - url: "/pages/member/be-vip", + uni.showToast({ + title: "功能尚未开启", + icon: "none", }); + // uni.navigateTo({ + // url: "/pages/member/be-vip", + // }); } if (index === 1) { uni.navigateTo({ @@ -18,7 +22,7 @@ function handleTabClick(index) { } if (index === 2) { uni.navigateTo({ - url: "/pages/device-intro", + url: "/pages/device/device-intro", }); } } diff --git a/src/components/Avatar.vue b/src/components/Avatar.vue index 470db77..7c0fce6 100644 --- a/src/components/Avatar.vue +++ b/src/components/Avatar.vue @@ -77,25 +77,25 @@ watch( /> {{ rank }} { 返回房间 - + diff --git a/src/components/BattleHeader.vue b/src/components/BattleHeader.vue index 9cdc95e..9bee7a4 100644 --- a/src/components/BattleHeader.vue +++ b/src/components/BattleHeader.vue @@ -72,7 +72,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true; @@ -100,7 +100,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true; diff --git a/src/components/BowData.vue b/src/components/BowData.vue index 409f98f..371cadf 100644 --- a/src/components/BowData.vue +++ b/src/components/BowData.vue @@ -58,7 +58,7 @@ const props = defineProps({ - + diff --git a/src/components/BowPower.vue b/src/components/BowPower.vue index 528c1e7..d72d6c9 100644 --- a/src/components/BowPower.vue +++ b/src/components/BowPower.vue @@ -1,44 +1,15 @@ diff --git a/src/components/BowTarget.vue b/src/components/BowTarget.vue index 77127b9..feb5063 100644 --- a/src/components/BowTarget.vue +++ b/src/components/BowTarget.vue @@ -527,7 +527,7 @@ onBeforeUnmount(() => { - + 中场休息 @@ -565,7 +565,7 @@ onBeforeUnmount(() => { @@ -585,7 +585,7 @@ onBeforeUnmount(() => { diff --git a/src/components/BowTargetEdit.vue b/src/components/BowTargetEdit.vue index ff7242c..16f880d 100644 --- a/src/components/BowTargetEdit.vue +++ b/src/components/BowTargetEdit.vue @@ -266,16 +266,16 @@ onBeforeUnmount(() => { @touchstart.stop="confirmAdd" :style="{ ...getNewPos() }" > - + - + - + diff --git a/src/components/BubbleTip.vue b/src/components/BubbleTip.vue index 4fdfdb6..7a61ccd 100644 --- a/src/components/BubbleTip.vue +++ b/src/components/BubbleTip.vue @@ -29,14 +29,14 @@ const props = defineProps({ font-size: 24rpx; } .normal { - background-image: url("../static/bubble-tip.png"); + background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip.png"); width: 157rpx; height: 105rpx; padding-top: 10px; padding-left: 30rpx; } .normal2 { - background-image: url("../static/bubble-tip4.png"); + background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip4.png"); width: 190rpx; height: 105rpx; padding-top: 10px; @@ -46,14 +46,14 @@ const props = defineProps({ z-index: 1; } .long { - background-image: url("../static/bubble-tip2.png"); + background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip2.png"); width: 370rpx; height: 70rpx; top: -50%; left: 49%; } .short { - background-image: url("../static/bubble-tip3.png"); + background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip3.png"); width: 300rpx; height: 70rpx; top: -50%; diff --git a/src/components/Container.vue b/src/components/Container.vue index cf3c3dc..c06a5f2 100644 --- a/src/components/Container.vue +++ b/src/components/Container.vue @@ -124,7 +124,7 @@ const cancelMatching = async () => { const goCalibration = async () => { await laserAimAPI(); uni.navigateTo({ - url: "/pages/calibration", + url: "/pages/device/calibration", }); }; diff --git a/src/components/DeviceOnlineEffects.vue b/src/components/DeviceOnlineEffects.vue new file mode 100644 index 0000000..a039ae5 --- /dev/null +++ b/src/components/DeviceOnlineEffects.vue @@ -0,0 +1,399 @@ + + + + + diff --git a/src/components/EditOption.vue b/src/components/EditOption.vue index 555d4b2..fe6d52c 100644 --- a/src/components/EditOption.vue +++ b/src/components/EditOption.vue @@ -203,7 +203,7 @@ onMounted(async () => { diff --git a/src/components/ModalDialog.vue b/src/components/ModalDialog.vue index 5ab17a6..f0157e1 100644 --- a/src/components/ModalDialog.vue +++ b/src/components/ModalDialog.vue @@ -1,4 +1,6 @@ @@ -108,6 +135,7 @@ const handleConfirm = () => { top: 0; left: 0; background-color: rgba(0, 0, 0, 0.62); + flex-direction: column; justify-content: center; align-items: center; z-index: 999; @@ -116,6 +144,7 @@ const handleConfirm = () => { .modal-wrap { position: relative; display: flex; + flex-direction: column; width: 549rpx; min-height: 318rpx;; padding-top: 168rpx; @@ -222,6 +251,14 @@ const handleConfirm = () => { background-color: #ffda3f; } +.dialog-button.confirm.disabled { + opacity: 0.62; +} + +.dialog-close { + margin-top: 28rpx; +} + @keyframes rotateLight { from { transform: translateX(-50%) rotate(0deg); diff --git a/src/components/OtaModal.vue b/src/components/OtaModal.vue index 303c1a0..30af95b 100644 --- a/src/components/OtaModal.vue +++ b/src/components/OtaModal.vue @@ -1,10 +1,11 @@ + + + + diff --git a/src/pages/device/device-bind-failure.vue b/src/pages/device/device-bind-failure.vue new file mode 100644 index 0000000..a434278 --- /dev/null +++ b/src/pages/device/device-bind-failure.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/src/pages/device/device-bind-success.vue b/src/pages/device/device-bind-success.vue new file mode 100644 index 0000000..4df34be --- /dev/null +++ b/src/pages/device/device-bind-success.vue @@ -0,0 +1,248 @@ + + + + + diff --git a/src/pages/device-intro.vue b/src/pages/device/device-intro.vue similarity index 95% rename from src/pages/device-intro.vue rename to src/pages/device/device-intro.vue index f780e14..855b5ec 100644 --- a/src/pages/device-intro.vue +++ b/src/pages/device/device-intro.vue @@ -38,7 +38,7 @@ const onScrollView = (e) => { mode="widthFix" /> - + +import { ref } from "vue"; +import { onLoad } from "@dcloudio/uni-app"; +import Container from "@/components/Container.vue"; +import { getDeviceDetailAPI } from "@/apis"; +import useStore from "@/store"; +import { storeToRefs } from "pinia"; + +const store = useStore(); +const { device } = storeToRefs(store); +const deviceId = ref(""); +const qrImageUrl = ref(""); +const qrSaved = ref(false); +const loading = ref(true); + +const loadQrCode = async () => { + if (!deviceId.value) { + loading.value = false; + return; + } + + try { + const response = await getDeviceDetailAPI(deviceId.value); + const detail = response?.detail || response?.data?.detail; + qrImageUrl.value = String(detail?.qrCodeUrl ?? "").trim(); + } catch (error) { + console.error("获取设备二维码失败", error); + qrImageUrl.value = ""; + } finally { + loading.value = false; + } +}; + +const saveQrCode = async () => { + if (!qrImageUrl.value) return; + + let filePath = qrImageUrl.value; + try { + if (/^https?:\/\//.test(filePath)) { + filePath = await new Promise((resolve, reject) => { + uni.downloadFile({ + url: filePath, + success: (result) => + result.statusCode === 200 + ? resolve(result.tempFilePath) + : reject(new Error("二维码下载失败")), + fail: reject, + }); + }); + } + + await new Promise((resolve, reject) => { + uni.saveImageToPhotosAlbum({ success: resolve, fail: reject, filePath }); + }); + qrSaved.value = true; + uni.showToast({ title: "已保存至相册", icon: "success" }); + } catch (error) { + uni.showToast({ title: "请长按二维码保存", icon: "none" }); + } +}; + +onLoad((options = {}) => { + try { + deviceId.value = decodeURIComponent(options.deviceId || "") || device.value.deviceId || ""; + } catch (error) { + deviceId.value = device.value.deviceId || ""; + } + void loadQrCode(); +}); + + + + + diff --git a/src/pages/device/my-device.vue b/src/pages/device/my-device.vue new file mode 100644 index 0000000..911d1e9 --- /dev/null +++ b/src/pages/device/my-device.vue @@ -0,0 +1,1422 @@ + + + + + diff --git a/src/pages/device/ota-wifi.vue b/src/pages/device/ota-wifi.vue new file mode 100644 index 0000000..213a003 --- /dev/null +++ b/src/pages/device/ota-wifi.vue @@ -0,0 +1,1172 @@ + + + + + diff --git a/src/pages/friend-battle-result.vue b/src/pages/friend-battle-result.vue index b84e46f..db618c5 100644 --- a/src/pages/friend-battle-result.vue +++ b/src/pages/friend-battle-result.vue @@ -168,7 +168,7 @@ const meleeRankList = computed(() => { */ function getMeleeRankBgSrc(rank) { const names = ["one", "two", "three"]; - return `../static/friend-battle-result/rank-${names[rank - 1]}.svg`; + return `https://static.shelingxingqiu.com/shootmini/static/friend-battle-result/rank-${names[rank - 1]}.svg`; } /** @@ -485,7 +485,7 @@ function goBack() { diff --git a/src/pages/friend-battle.vue b/src/pages/friend-battle.vue index 98719b2..e632653 100644 --- a/src/pages/friend-battle.vue +++ b/src/pages/friend-battle.vue @@ -190,10 +190,10 @@ onLoad(async (options) => { - - + - + 挑战难度 @@ -201,7 +201,7 @@ onLoad(async (options) => { - + 进入房间 @@ -214,7 +214,7 @@ onLoad(async (options) => { - + diff --git a/src/pages/index.vue b/src/pages/index.vue index b95216b..6e34ff2 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -1,26 +1,26 @@ - - - - diff --git a/src/pages/my-growth.vue b/src/pages/my-growth.vue index 4353e28..ce29bf2 100644 --- a/src/pages/my-growth.vue +++ b/src/pages/my-growth.vue @@ -116,7 +116,7 @@ onLoad((options) => { {{ getName(item) }} {{ item.createTime }} - + { {{ getName(item) }} {{ item.createTime }} - + { >{{ item.trainingTypeText }} {{ formatPractiseTime(item.createTime) }} - + diff --git a/src/pages/org-bind.vue b/src/pages/org-bind.vue index 3f5932d..f3fe10d 100644 --- a/src/pages/org-bind.vue +++ b/src/pages/org-bind.vue @@ -130,7 +130,7 @@ onLoad((options) => { {{ @@ -151,7 +151,7 @@ onLoad((options) => { /> diff --git a/src/pages/ota-wifi.vue b/src/pages/ota-wifi.vue deleted file mode 100644 index 9a8e9b7..0000000 --- a/src/pages/ota-wifi.vue +++ /dev/null @@ -1,1261 +0,0 @@ - - - - - diff --git a/src/pages/point-book-detail-share.vue b/src/pages/point-book-detail-share.vue index 1f51176..9a10b27 100644 --- a/src/pages/point-book-detail-share.vue +++ b/src/pages/point-book-detail-share.vue @@ -115,7 +115,7 @@ const targetTypeName = computed(() => { /> @@ -136,7 +136,7 @@ const targetTypeName = computed(() => { > 落点稳定性 @@ -165,7 +165,7 @@ const targetTypeName = computed(() => { 落点分布 diff --git a/src/pages/training/components/TestDistance.vue b/src/pages/training/components/TestDistance.vue index e38f095..cbafb3a 100644 --- a/src/pages/training/components/TestDistance.vue +++ b/src/pages/training/components/TestDistance.vue @@ -162,7 +162,7 @@ onBeforeUnmount(() => { - + 距离正式比赛还有 {{ count }} diff --git a/src/pages/training/practise-one.vue b/src/pages/training/practise-one.vue index 3dbb376..afb5175 100644 --- a/src/pages/training/practise-one.vue +++ b/src/pages/training/practise-one.vue @@ -1918,7 +1918,7 @@ onBeforeUnmount(() => { diff --git a/src/pages/user.vue b/src/pages/user.vue index 1e7cdba..1aaef37 100644 --- a/src/pages/user.vue +++ b/src/pages/user.vue @@ -130,7 +130,7 @@ const buildVersion = typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : '' title="我的金币" :onClick="toCoinPage" /> - + Lv{{ user.lvl }} ({ deviceName: "", }); +const getDefaultDeviceStatus = () => ({ + battery: null, + charging: false, + version: "", + online: false, + netType: "", + onlineDuration: null, + receivedAt: 0, +}); + +const getDefaultDeviceUsageDuration = () => ({ + deviceId: "", + seconds: null, +}); + const getDefaultGame = () => ({ roomID: "", inBattle: false, @@ -100,6 +115,12 @@ export default defineStore("store", { ringRank: [], }, online: false, + // 设备电量属于运行时状态,null 表示当前没有有效数据。 + deviceBattery: null, + // WebSocket 实时推送的设备状态,不参与持久化。 + deviceStatus: getDefaultDeviceStatus(), + // 按设备记录本次运行期间已知的最大累计使用时长,离线时保持数值不回退。 + deviceUsageDuration: getDefaultDeviceUsageDuration(), game: { roomID: "", inBattle: false, @@ -134,7 +155,77 @@ export default defineStore("store", { this.rankData = { ...(data || {}) }; }, updateOnline(online) { + this.setDeviceOnline(online); + }, + updateDeviceBattery(value) { + if (value === null || value === undefined || value === "") { + this.deviceBattery = null; + return; + } + + const battery = Number(value); + this.deviceBattery = Number.isFinite(battery) + ? Math.min(100, Math.max(0, battery)) + : null; + }, + updateDeviceStatus(status = {}) { + const battery = Number(status.battery ?? status.power); + const onlineDuration = Number(status.onlineDuration); + const online = status.online === true; + const nextStatus = { + battery: Number.isFinite(battery) + ? Math.min(100, Math.max(0, battery)) + : null, + charging: status.charging === true, + version: String(status.version ?? "").trim(), + online, + netType: String(status.netType ?? "").trim().toLowerCase(), + onlineDuration: + Number.isFinite(onlineDuration) && onlineDuration >= 0 + ? onlineDuration + : null, + receivedAt: Date.now(), + }; + + this.deviceStatus = nextStatus; this.online = online; + this.deviceBattery = online ? nextStatus.battery : null; + this.updateDeviceUsageDuration(nextStatus.onlineDuration); + }, + updateDeviceUsageDuration(value, deviceId = this.device.deviceId) { + if (value === null || value === undefined || String(value).trim() === "") return; + + const seconds = Number(value); + const normalizedDeviceId = String(deviceId || ""); + if (!normalizedDeviceId || !Number.isFinite(seconds) || seconds < 0) return; + + const cachedDeviceId = String(this.deviceUsageDuration.deviceId || ""); + const cachedSeconds = Number(this.deviceUsageDuration.seconds); + this.deviceUsageDuration = { + deviceId: normalizedDeviceId, + seconds: + cachedDeviceId === normalizedDeviceId && Number.isFinite(cachedSeconds) + ? Math.max(cachedSeconds, seconds) + : seconds, + }; + }, + clearDeviceUsageDuration() { + this.deviceUsageDuration = getDefaultDeviceUsageDuration(); + }, + setDeviceOnline(online) { + const nextOnline = online === true; + this.online = nextOnline; + if (nextOnline) { + this.deviceStatus = { ...this.deviceStatus, online: true }; + return; + } + this.deviceBattery = null; + this.deviceStatus = getDefaultDeviceStatus(); + }, + clearDeviceStatus() { + this.online = false; + this.deviceBattery = null; + this.deviceStatus = getDefaultDeviceStatus(); }, async updateUser(user = {}) { this.user = { ...getDefaultUser(), ...user }; @@ -145,12 +236,17 @@ export default defineStore("store", { ); }, updateDevice(deviceId, deviceName) { + if (String(this.device.deviceId || "") !== String(deviceId || "")) { + this.clearDeviceStatus(); + this.clearDeviceUsageDuration(); + } this.device.deviceId = deviceId; this.device.deviceName = deviceName; }, clearDevice() { this.device = getDefaultDevice(); - this.online = false; + this.clearDeviceStatus(); + this.clearDeviceUsageDuration(); }, async updateConfig(config) { this.config = config; @@ -199,6 +295,9 @@ export default defineStore("store", { user: getDefaultUser(), device: getDefaultDevice(), online: false, + deviceBattery: null, + deviceStatus: getDefaultDeviceStatus(), + deviceUsageDuration: getDefaultDeviceUsageDuration(), game: getDefaultGame(), dailyCount: getDefaultDailyCount(), deviceChargingDialogVisible: false, diff --git a/src/websocket.js b/src/websocket.js index 77bd29a..9ac894b 100644 --- a/src/websocket.js +++ b/src/websocket.js @@ -77,19 +77,38 @@ function createWebSocket(token, onMessage) { socketTask.onMessage((res) => { if (socket !== socketTask) return; - const { data, event } = JSON.parse(res.data); + let response; + try { + response = JSON.parse(res.data); + } catch (err) { + console.error("WebSocket 消息解析失败", err); + return; + } + + const { data, event, code, timestamp } = response || {}; if (event === "pong") return; - if (!data || typeof data !== "object") return; - if (data.type) { + const passthroughEvents = [ + "/addons/shoot/battery", + "/addons/shoot/otaProgress", + "/addons/shoot/otaResult", + ]; + if (passthroughEvents.includes(event)) { + if ((code == null || Number(code) === 0) && onMessage && data && typeof data === "object") { + onMessage({ event, data, code, timestamp }); + } + return; + } + if (data?.type) { if (ENABLE_REALTIME_MESSAGE_LOG) { console.log("收到 WebSocket 消息", getMessageTypeName(data.type)); } if (onMessage) onMessage({ ...(data.data || {}), type: data.type }); return; } - if (!Array.isArray(data.updates)) return; - if (onMessage) onMessage(data.updates); - const msg = data.updates[0]; + const updates = Array.isArray(data?.updates) ? data.updates : []; + if (!updates.length) return; + if (onMessage) onMessage(updates); + const msg = updates[0]; if (msg) { if (ENABLE_REALTIME_MESSAGE_LOG) { console.log( @@ -103,9 +122,9 @@ function createWebSocket(token, onMessage) { } else if (msg.constructor === MESSAGETYPES.LvlUpdate) { uni.setStorageSync("latestLvl", msg.lvl); } else if (msg.constructor === MESSAGETYPES.DeviceOnline) { - uni.$emit("update-online"); + uni.$emit("update-online", true); } else if (msg.constructor === MESSAGETYPES.DeviceOffline) { - uni.$emit("update-online"); + uni.$emit("update-online", false); } else if (msg.constructor === MESSAGETYPES.DeviceCharging) { uni.$emit("device-charging"); } @@ -123,6 +142,7 @@ function createWebSocket(token, onMessage) { stopHeartbeat(); socket = null; isConnecting = false; + uni.$emit("shoot-socket-disconnected"); if (manualClose || kickedOut) return; await handleUnexpectedClose(onMessage);