diff --git a/src/apis.js b/src/apis.js index eb5690a..fb4ac1c 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); }, }); @@ -571,7 +582,14 @@ export const laserCloseAPI = async () => { // 设备连接指定 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 升级。 @@ -584,11 +602,6 @@ export const sendHardwareBoxUpdateAPI = async (data) => { return request("POST", "/user/hardwareBox/sendUpdate", data); }; -// 根据任务 ID 获取硬件盒子 OTA 更新状态。 -export const getHardwareBoxTaskStatusAPI = async (taskId) => { - return request("GET", `/user/hardwareBox/taskStatus?taskId=${taskId}`); -}; - export const addNoteAPI = async (id, remark) => { return request("POST", "/user/score/sheet/remark", {id, remark}); }; diff --git a/src/components/ModalDialog.vue b/src/components/ModalDialog.vue index 5ab17a6..710bba5 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 4861667..c94047c 100644 --- a/src/components/OtaModal.vue +++ b/src/components/OtaModal.vue @@ -24,6 +24,10 @@ const props = defineProps({ type: Number, default: 40, }, + phase: { + type: String, + default: "", + }, // 副标题:如“新版本将优化智能弓体验” description: { type: String, @@ -48,6 +52,14 @@ 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 progressPhaseText = computed(() => { + const phaseText = { + started: "正在准备固件更新", + downloading: "正在下载固件", + installing: "正在安装固件", + }; + return phaseText[props.phase] || "正在进行固件更新"; +}); // 点击立即更新前先校验设备在线状态。 const handleUpdateClick = () => { @@ -127,10 +139,10 @@ const handleUpdateClick = () => { - 请关机并重启智能弓 + 固件更新已完成 - 完成 + 关闭 @@ -139,6 +151,10 @@ const handleUpdateClick = () => { + + {{ progressPhaseText }} + {{ Math.floor(progressValue) }}% + @@ -391,9 +407,26 @@ const handleUpdateClick = () => { } .progress-wrap { width: 394rpx; - margin-top: 40rpx; + margin-top: 28rpx; margin-left: 44rpx; } +.progress-meta { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16rpx; + color: #ffffff; + font-size: 24rpx; + line-height: 34rpx; +} +.progress-phase { + flex: 1; +} +.progress-value { + flex-shrink: 0; + margin-left: 16rpx; + color: #fed847; +} .progress-track { width: 100%; height: 18rpx; diff --git a/src/pages/device/composables/useDeviceStatus.js b/src/pages/device/composables/useDeviceStatus.js index 98da87f..d7b8dd6 100644 --- a/src/pages/device/composables/useDeviceStatus.js +++ b/src/pages/device/composables/useDeviceStatus.js @@ -22,7 +22,13 @@ export function useDeviceStatus({ const isDeviceOnline = computed( () => deviceStatus.value.online === true || online.value === true ); - const statusText = computed(() => (isDeviceOnline.value ? "已连接" : "未连接")); + const isDeviceCharging = computed( + () => isDeviceOnline.value && deviceStatus.value.charging === true + ); + const statusText = computed(() => { + if (!isDeviceOnline.value) return "未连接"; + return isDeviceCharging.value ? "已连接(充电中)" : "已连接"; + }); const statusClass = computed(() => isDeviceOnline.value ? "device-status--online" : "device-status--offline" ); @@ -186,6 +192,7 @@ export function useDeviceStatus({ deviceDetails, deviceRows, getDeviceNameOverrides, + isDeviceCharging, isDeviceOnline, maskedDeviceId, networkText, diff --git a/src/pages/device/composables/useOtaUpdate.js b/src/pages/device/composables/useOtaUpdate.js new file mode 100644 index 0000000..6c6b39f --- /dev/null +++ b/src/pages/device/composables/useOtaUpdate.js @@ -0,0 +1,188 @@ +import { computed, onUnmounted, ref } from "vue"; +import { sendHardwareBoxUpdateAPI } from "@/apis"; +import useStore from "@/store"; +import { storeToRefs } from "pinia"; + +const OTA_PROGRESS_EVENT = "/addons/shoot/otaProgress"; +const OTA_RESULT_EVENT = "/addons/shoot/otaResult"; +const UPDATE_TIMEOUT = 10 * 60 * 1000; + +const phaseTextMap = { + started: "正在准备固件更新", + downloading: "正在下载固件", + installing: "正在安装固件", +}; + +export const useOtaUpdate = () => { + const store = useStore(); + const { device } = storeToRefs(store); + const updating = ref(false); + const progress = ref(0); + const phase = ref("started"); + const resultVisible = ref(false); + const resultStatus = ref(""); + const resultReason = ref(""); + + let updateRunId = 0; + let targetVersion = ""; + let timeoutTimer = null; + let successCallback = null; + let socketListening = false; + + const phaseText = computed( + () => phaseTextMap[phase.value] || "正在进行固件更新" + ); + const resultTitle = computed(() => + resultStatus.value === "success" ? "固件更新完成" : "固件更新失败" + ); + const resultContent = computed(() => { + if (resultStatus.value === "success") return "固件更新已完成"; + return resultReason.value || "更新失败,请检查设备及网络后重试"; + }); + + const clearTimers = () => { + clearTimeout(timeoutTimer); + timeoutTimer = null; + }; + + const stopSocketListening = () => { + if (!socketListening) return; + uni.$off("socket-inbox", handleSocketMessage); + socketListening = false; + }; + + const resetActiveUpdate = () => { + clearTimers(); + stopSocketListening(); + successCallback = null; + }; + + const finishUpdate = (status, reason = "", runId = updateRunId) => { + if (runId !== updateRunId || !updating.value) return; + const onSuccess = successCallback; + resetActiveUpdate(); + updating.value = false; + resultStatus.value = status; + resultReason.value = reason; + if (status === "success") { + progress.value = 100; + phase.value = "installing"; + onSuccess?.(); + } + resultVisible.value = true; + }; + + const isCurrentUpdateMessage = (data) => { + const messageDeviceId = String(data?.deviceId || ""); + const currentDeviceId = String(device.value?.deviceId || ""); + if (messageDeviceId && currentDeviceId && messageDeviceId !== currentDeviceId) { + return false; + } + + const messageVersion = String(data?.versionNumber || ""); + return !messageVersion || !targetVersion || messageVersion === targetVersion; + }; + + function handleSocketMessage(message) { + if (!updating.value || Number(message?.code ?? 0) !== 0) return; + if (![OTA_PROGRESS_EVENT, OTA_RESULT_EVENT].includes(message?.event)) return; + if (!isCurrentUpdateMessage(message.data)) return; + + if (message.event === OTA_PROGRESS_EVENT) { + const nextProgress = Math.min( + 100, + Math.max(0, Number(message.data?.progress) || 0) + ); + progress.value = Math.max(progress.value, nextProgress); + if (phaseTextMap[message.data?.phase]) { + phase.value = message.data.phase; + } + return; + } + + if (message.data?.status === "success") { + finishUpdate("success"); + return; + } + if (message.data?.status === "failed") { + finishUpdate("failed", message.data?.reason || ""); + } + } + + const startSocketListening = () => { + if (socketListening) return; + uni.$on("socket-inbox", handleSocketMessage); + socketListening = true; + }; + + const startUpdate = async ({ + versionNumber, + resourceUrl, + wifiSsid = "", + wifiPassword = "", + onSuccess, + }) => { + if (updating.value) return false; + + updateRunId += 1; + const runId = updateRunId; + targetVersion = String(versionNumber || ""); + successCallback = onSuccess || null; + resultVisible.value = false; + resultStatus.value = ""; + resultReason.value = ""; + progress.value = 0; + phase.value = "started"; + updating.value = true; + startSocketListening(); + timeoutTimer = setTimeout(() => { + finishUpdate("failed", "固件更新超时,请稍后重试", runId); + }, UPDATE_TIMEOUT); + + try { + const updateResult = await sendHardwareBoxUpdateAPI({ + versionNumber: targetVersion, + wifiSsid, + wifiPassword, + resourceUrl, + }); + if (runId !== updateRunId || !updating.value) return false; + if (!updateResult?.taskId) { + finishUpdate("failed", "固件更新任务创建失败,请稍后重试", runId); + return false; + } + return true; + } catch (error) { + if (runId === updateRunId) { + finishUpdate( + "failed", + error?.message || "固件更新请求失败,请稍后重试", + runId + ); + } + return false; + } + }; + + const closeResult = () => { + resultVisible.value = false; + }; + + onUnmounted(() => { + updateRunId += 1; + resetActiveUpdate(); + }); + + return { + updating, + progress, + phase, + phaseText, + resultVisible, + resultStatus, + resultTitle, + resultContent, + startUpdate, + closeResult, + }; +}; diff --git a/src/pages/device/my-device.vue b/src/pages/device/my-device.vue index 8cae4fd..ff7b693 100644 --- a/src/pages/device/my-device.vue +++ b/src/pages/device/my-device.vue @@ -5,6 +5,7 @@ import Container from "@/components/Container.vue"; import Header from "@/components/Header.vue"; import ScreenHint from "@/components/ScreenHint.vue"; import ModalDialog from "@/components/ModalDialog.vue"; +import OtaModal from "@/components/OtaModal.vue"; import { getHardwareBoxVersionAPI, laserAimAPI, @@ -17,10 +18,12 @@ import { DEVICE_NAME_STORAGE_KEY, useDeviceStatus, } from "./composables/useDeviceStatus"; +import { useOtaUpdate } from "./composables/useOtaUpdate"; const store = useStore(); const { updateDevice, clearDevice } = store; const { user, device, deviceStatus, online } = storeToRefs(store); +const DEVICE_NAME_MAX_LENGTH = 10; const formatBindingDate = (value) => { if (!value) return "--"; @@ -56,11 +59,26 @@ const retryScanOnShow = ref(false); const calibration = ref(false); const showDeviceId = ref(false); const latestVersionDialogVisible = ref(false); +const firmwareConfirmVisible = ref(false); +const wifiRequiredVisible = ref(false); const otaNeedUpdate = ref(false); const firmwareActionPending = ref(false); +const pendingOtaInfo = ref(null); let otaCheckPromise = null; let otaCheckRequestVersion = 0; +const { + updating: otaUpdating, + progress: otaProgress, + phase: otaPhase, + resultVisible: otaResultVisible, + resultStatus: otaResultStatus, + resultTitle: otaResultTitle, + resultContent: otaResultContent, + startUpdate: startOtaUpdate, + closeResult: closeOtaResult, +} = useOtaUpdate(); + const { batteryText, deviceDetails, @@ -94,8 +112,8 @@ const bindingDateText = computed(() => ); const deviceIdText = computed(() => - showDeviceId.value && device.value.deviceId - ? device.value.deviceId + showDeviceId.value && device.value.deviceName + ? device.value.deviceName : maskedDeviceId.value ); @@ -107,9 +125,11 @@ const designDeviceStats = computed(() => [ }, { label: "剩余电量", - value: batteryText.value === "暂无数据" || !isDeviceOnline.value - ? "--" - : batteryText.value, + value: !isDeviceOnline.value + ? "设备离线" + : batteryText.value === "暂无数据" + ? "--" + : batteryText.value, }, { label: "累计使用", @@ -150,6 +170,14 @@ const closeNameEditor = () => { nameEditorVisible.value = false; }; +const editingNameLength = computed(() => + Array.from(String(editingName.value || "")).length +); + +const isEditingNameTooLong = computed( + () => editingNameLength.value > DEVICE_NAME_MAX_LENGTH +); + const toggleDeviceId = () => { if (device.value.deviceId) showDeviceId.value = !showDeviceId.value; }; @@ -162,8 +190,15 @@ const confirmName = async () => { uni.showToast({ title: "请输入设备名", icon: "none" }); return; } - if (name.length > 10 || !/^[\u4e00-\u9fa5A-Za-z0-9_-]+$/.test(name)) { - uni.showToast({ title: "设备名格式不正确", icon: "none" }); + if (Array.from(name).length > DEVICE_NAME_MAX_LENGTH) { + uni.showToast({ title: "设备名最多支持10个字符", icon: "none" }); + return; + } + if (!/^[\u4e00-\u9fa5A-Za-z0-9_-]+$/.test(name)) { + uni.showToast({ + title: "仅支持中文、英文、数字、下划线和减号", + icon: "none", + }); return; } @@ -206,7 +241,7 @@ const toDeviceIntroPage = () => { const joinWifi = () => { if (!isDeviceOnline.value) { - uni.showToast({ title: "请先开启智能弓箭", icon: "none" }); + uni.showToast({ title: "请先开启智能弓", icon: "none" }); return; } uni.navigateTo({ url: "/pages/device/ota-wifi" }); @@ -277,6 +312,52 @@ const closeLatestVersionDialog = () => { latestVersionDialogVisible.value = false; }; +const closeFirmwareConfirm = () => { + firmwareConfirmVisible.value = false; +}; + +const runFirmwareUpdate = () => { + const versionInfo = pendingOtaInfo.value; + if (!versionInfo) return; + void startOtaUpdate({ + versionNumber: versionInfo.versionNumber, + resourceUrl: versionInfo.resourceUrl, + onSuccess: () => { + otaNeedUpdate.value = false; + }, + }); +}; + +const confirmFirmwareUpdate = () => { + firmwareConfirmVisible.value = false; + if (!isDeviceOnline.value) { + uni.showToast({ title: "请先开启智能弓", icon: "none" }); + return; + } + if (String(networkType.value || "").toLowerCase() === "wifi") { + runFirmwareUpdate(); + return; + } + wifiRequiredVisible.value = true; +}; + +const goWifiForFirmwareUpdate = () => { + const versionInfo = pendingOtaInfo.value; + if (!versionInfo) return; + wifiRequiredVisible.value = false; + const query = [ + "source=firmware-update", + `versionNumber=${encodeURIComponent(versionInfo.versionNumber)}`, + `resourceUrl=${encodeURIComponent(versionInfo.resourceUrl)}`, + ].join("&"); + uni.navigateTo({ url: `/pages/device/ota-wifi?${query}` }); +}; + +const handleOtaResultClose = () => { + closeOtaResult(); + void refreshOtaUpdateState(); +}; + const goFirmwareUpdate = async () => { if (firmwareActionPending.value) return; if (!isDeviceOnline.value) { @@ -293,11 +374,8 @@ const goFirmwareUpdate = async () => { return; } - const query = [ - `versionNumber=${encodeURIComponent(versionInfo.versionNumber)}`, - `resourceUrl=${encodeURIComponent(versionInfo.resourceUrl)}`, - ].join("&"); - uni.navigateTo({ url: `/pages/device/ota-wifi?${query}` }); + pendingOtaInfo.value = versionInfo; + firmwareConfirmVisible.value = true; } catch (error) { uni.showToast({ title: "获取更新版本失败,请重试", icon: "none" }); } finally { @@ -307,7 +385,7 @@ const goFirmwareUpdate = async () => { const goCalibration = async () => { if (!isDeviceOnline.value) { - uni.showToast({ title: "请先开启智能弓箭", icon: "none" }); + uni.showToast({ title: "请先开启智能弓", icon: "none" }); return; } try { @@ -546,10 +624,16 @@ onShow(async () => { - + 扫码绑定射灵弓箭 - 设备底部二维码样例 + 配套令牌样例 { - + @@ -598,6 +682,47 @@ onShow(async () => { :onConfirm="closeLatestVersionDialog" > + + + + + + + + + + { focus :cursor-spacing="24" :disabled="renaming" - maxlength="10" + :maxlength="-1" confirm-type="done" - placeholder="请输入设备名(最长10个汉字)" + placeholder="请输入设备名" placeholder-class="name-placeholder" @confirm="confirmName" /> @@ -631,7 +756,13 @@ onShow(async () => { /> - 仅支持中文、英文、数字、下划线、减号 + + 仅支持中文、英文、数字、下划线、减号 + {{ editingNameLength }}/{{ DEVICE_NAME_MAX_LENGTH }} + @@ -661,7 +792,7 @@ onShow(async () => { .device-nav .device-status { position: absolute; - top: calc(100% + 28rpx); + top: calc(100% + 0); left: 50%; white-space: nowrap; transform: translateX(-50%); @@ -851,7 +982,7 @@ onShow(async () => { display: flex; align-items: center; color: rgba(255, 255, 255, 0.9); - font-size: 24rpx; + font-size: 18rpx; line-height: 30rpx; } @@ -1120,12 +1251,13 @@ onShow(async () => { .scan-tips { display: flex; - width: 90%; + width: 100%; box-sizing: border-box; flex-direction: column; - margin-top: 20%; + align-items: flex-start; color: #ffffff; font-size: 24rpx; + text-align: left; } .scan-tips-title { @@ -1276,12 +1408,34 @@ onShow(async () => { opacity: 0.6; } -.name-helper { - display: block; +.name-meta { + display: flex; + width: 100%; + max-width: 620rpx; + align-items: flex-start; + justify-content: space-between; margin-top: 22rpx; + margin-right: auto; + margin-left: auto; + gap: 20rpx; +} + +.name-helper, +.name-count { color: rgba(255, 255, 255, 0.62); font-size: 24rpx; line-height: 34rpx; - text-align: center; +} + +.name-helper { + flex: 1; +} + +.name-count { + flex-shrink: 0; +} + +.name-count--exceeded { + color: #fed847; } diff --git a/src/pages/device/ota-wifi.vue b/src/pages/device/ota-wifi.vue index 31795d9..805a8d5 100644 --- a/src/pages/device/ota-wifi.vue +++ b/src/pages/device/ota-wifi.vue @@ -1,28 +1,21 @@ @@ -711,46 +528,6 @@ onUnmounted(() => { - - - - 开始更新 - - - - - - - - 更新中,请稍等片刻... - - - - - {{ Math.floor(progress) }}% - - - - - - - 更新完成 - 请关机并重启智能弓 - - 完成 - - - - - - - 更新失败 - 请确保: - 1、智能弓已开启 - 2、网路连接稳定 - - 重试 - @@ -883,6 +660,46 @@ onUnmounted(() => { + + + + + + + + + + diff --git a/src/pages/index.vue b/src/pages/index.vue index f195d59..8597b64 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -6,12 +6,12 @@ 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 ModalDialog from "@/components/ModalDialog.vue"; import OtaModal from "@/components/OtaModal.vue"; import { checkUserBindAPI, getAppConfig, - getHardwareBoxTaskStatusAPI, getHardwareBoxVersionAPI, getHomeData, getMyDevicesAPI, @@ -69,8 +69,10 @@ const deviceCardAsset = computed(() => deviceCardAssets[deviceCardState.value]); // OTA 相关 const otaVisible = ref(false); +const wifiRequiredVisible = ref(false); const otaState = ref("new_version"); const otaProgress = ref(0); +const otaPhase = ref("started"); const otaInfo = ref({ versionNumber: "", versionInfo: "", @@ -81,31 +83,19 @@ const otaInfo = ref({ const isStartingOta = ref(false); let isCheckingOta = false; let otaCheckQueuedForOnline = false; -let otaProgressTimer = null; -let otaStatusTimer = 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; +const OTA_UPDATE_TIMEOUT = 10 * 60 * 1000; +const OTA_PROGRESS_EVENT = "/addons/shoot/otaProgress"; +const OTA_RESULT_EVENT = "/addons/shoot/otaResult"; -// 清理首页 OTA 更新定时器,避免弹窗关闭或页面卸载后继续轮询。 +// 清理首页 OTA 更新超时计时器。 const clearOtaUpdateTimers = () => { - clearInterval(otaProgressTimer); - clearTimeout(otaStatusTimer); clearTimeout(otaTimeoutTimer); - clearTimeout(otaResultTimer); - otaProgressTimer = null; - otaStatusTimer = null; otaTimeoutTimer = null; - otaResultTimer = null; - otaStatusPollCount = 0; }; -// 使当前 OTA 运行失效,确保旧请求返回后不会继续轮询或覆盖新状态。 +// 使当前 OTA 运行失效,确保旧请求返回后不会覆盖新状态。 const invalidateOtaUpdateRun = () => { otaUpdateRunId += 1; clearOtaUpdateTimers(); @@ -114,19 +104,6 @@ const invalidateOtaUpdateRun = () => { const isOtaUpdateRunActive = (runId) => runId === otaUpdateRunId && otaState.value === "update_progress"; -// 启动首页 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 = { @@ -186,6 +163,7 @@ watch(online, (nextOnline, previousOnline) => { const getOtaWifiUrl = () => { const { versionNumber, resourceUrl } = otaInfo.value; const query = [ + "source=firmware-update", `versionNumber=${encodeURIComponent(versionNumber)}`, `resourceUrl=${encodeURIComponent(resourceUrl)}`, ].join("&"); @@ -215,48 +193,40 @@ const completeHomeOtaUpdate = (runId) => { isStartingOta.value = false; otaInfo.value = {...otaInfo.value, needUpdate: false}; otaProgress.value = 100; - const completedRunId = otaUpdateRunId; - otaResultTimer = setTimeout(() => { - otaResultTimer = null; - if (completedRunId !== otaUpdateRunId) return; - otaState.value = "update_success"; - otaVisible.value = true; - }, 300); + otaPhase.value = "installing"; + otaState.value = "update_success"; + otaVisible.value = true; }; -// 轮询首页直接发起的 OTA 更新任务状态。 -const pollHomeOtaTaskStatus = async (taskId, runId) => { - if (!isOtaUpdateRunActive(runId)) return; +// 首页只处理当前设备、当前目标版本的 OTA WebSocket 消息。 +const handleHomeOtaSocketMessage = (message) => { + const runId = otaUpdateRunId; + if (!isOtaUpdateRunActive(runId) || Number(message?.code ?? 0) !== 0) return; + if (![OTA_PROGRESS_EVENT, OTA_RESULT_EVENT].includes(message?.event)) return; - otaStatusPollCount += 1; - try { - const taskStatus = await getHardwareBoxTaskStatusAPI(taskId); - if (!isOtaUpdateRunActive(runId)) return; + const messageDeviceId = String(message.data?.deviceId || ""); + const currentDeviceId = String(device.value?.deviceId || ""); + if (messageDeviceId && currentDeviceId && messageDeviceId !== currentDeviceId) return; - const status = Number(taskStatus?.status); - if (status === 2) { - completeHomeOtaUpdate(runId); - return; + const messageVersion = String(message.data?.versionNumber || ""); + const currentVersion = String(otaInfo.value.versionNumber || ""); + if (messageVersion && currentVersion && messageVersion !== currentVersion) return; + + if (message.event === OTA_PROGRESS_EVENT) { + const nextProgress = Math.min( + 100, + Math.max(0, Number(message.data?.progress) || 0) + ); + otaProgress.value = Math.max(otaProgress.value, nextProgress); + if (["started", "downloading", "installing"].includes(message.data?.phase)) { + otaPhase.value = message.data.phase; } - if (status === 3) { - failHomeOtaUpdate(runId); - return; - } - if (status === 0 || status === 1) { - if (otaStatusPollCount >= OTA_TASK_STATUS_MAX_POLL_COUNT) { - failHomeOtaUpdate(runId); - return; - } - otaStatusTimer = setTimeout(() => { - otaStatusTimer = null; - if (!isOtaUpdateRunActive(runId)) return; - void pollHomeOtaTaskStatus(taskId, runId); - }, OTA_TASK_STATUS_POLL_INTERVAL); - return; - } - failHomeOtaUpdate(runId); - } catch (err) { - if (!isOtaUpdateRunActive(runId)) return; + return; + } + + if (message.data?.status === "success") { + completeHomeOtaUpdate(runId); + } else if (message.data?.status === "failed") { failHomeOtaUpdate(runId); } }; @@ -268,11 +238,11 @@ const startHomeOtaUpdate = async () => { otaState.value = "update_progress"; otaVisible.value = true; otaProgress.value = 0; - startOtaProgressAnimation(); + otaPhase.value = "started"; otaTimeoutTimer = setTimeout(() => { if (!isOtaUpdateRunActive(runId)) return; failHomeOtaUpdate(runId); - }, OTA_TASK_STATUS_TIMEOUT); + }, OTA_UPDATE_TIMEOUT); try { const updateResult = await sendHardwareBoxUpdateAPI({ @@ -286,14 +256,13 @@ const startHomeOtaUpdate = async () => { failHomeOtaUpdate(runId); return; } - void pollHomeOtaTaskStatus(updateResult.taskId, runId); } catch (err) { if (!isOtaUpdateRunActive(runId)) return; failHomeOtaUpdate(runId); } }; -// 点击立即更新时先判断设备是否在线并已通过 WiFi 联网,已联网则首页直接更新,否则跳转 WiFi 页面。 +// 点击立即更新时先判断设备是否在线并已通过 WiFi 联网,未联网时先展示连接引导。 const handleOtaUpdate = async () => { if (isStartingOta.value) return; isStartingOta.value = true; @@ -327,6 +296,12 @@ const handleOtaUpdate = async () => { isStartingOta.value = false; otaVisible.value = false; + wifiRequiredVisible.value = true; +}; + +// 从首页固件更新提示进入 WiFi 配置页,并继续沿用当前版本信息。 +const goWifiForOtaUpdate = () => { + wifiRequiredVisible.value = false; uni.navigateTo({ url: getOtaWifiUrl() }); }; @@ -480,12 +455,14 @@ onShow(async (options) => { }); onMounted(async () => { + uni.$on("socket-inbox", handleHomeOtaSocketMessage); const config = await getAppConfig(); updateConfig(config); console.log("全局配置:", config); }); onUnmounted(() => { + uni.$off("socket-inbox", handleHomeOtaSocketMessage); invalidateOtaUpdateRun(); }); @@ -515,6 +492,7 @@ onShareTimeline(() => { :state="otaState" :version="otaInfo.versionNumber" :progress="otaProgress" + :phase="otaPhase" :description="''" :changelog="otaInfo.versionInfo" :forceUpdate="otaInfo.forceUpdate" @@ -524,6 +502,14 @@ onShareTimeline(() => { @done="handleOtaDone" @retry="handleOtaRetry" /> +