diff --git a/src/components/OtaModal.vue b/src/components/OtaModal.vue
index c94047c..4a61c0a 100644
--- a/src/components/OtaModal.vue
+++ b/src/components/OtaModal.vue
@@ -158,6 +158,7 @@ const handleUpdateClick = () => {
+ 请勿离开当前页面!
@@ -439,6 +440,14 @@ const handleUpdateClick = () => {
background-color: #FED847;
border-radius: 999rpx;
}
+.progress-warning {
+ display: block;
+ margin-top: 16rpx;
+ color: rgba(255, 255, 255, 0.88);
+ font-size: 22rpx;
+ line-height: 32rpx;
+ text-align: center;
+}
/* 关闭按钮(位于弹窗下方) */
.ota-close-below {
diff --git a/src/composables/useOtaUpdate.js b/src/composables/useOtaUpdate.js
new file mode 100644
index 0000000..2fcc788
--- /dev/null
+++ b/src/composables/useOtaUpdate.js
@@ -0,0 +1,227 @@
+import { computed, 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: "正在安装固件",
+};
+
+// OTA 状态由首页、WiFi 设置页和我的设备页共享,页面切换后仍可继续接收进度。
+const updating = ref(false);
+const progress = ref(0);
+const phase = ref("started");
+const resultVisible = ref(false);
+const resultStatus = ref("");
+const resultReason = ref("");
+
+let deviceRef = null;
+let updateRunId = 0;
+let targetVersion = "";
+let lastFinishedVersion = "";
+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 resetActiveUpdate = () => {
+ clearTimers();
+ 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;
+ lastFinishedVersion = targetVersion;
+ if (status === "success") {
+ progress.value = 100;
+ phase.value = "installing";
+ onSuccess?.();
+ }
+ resultVisible.value = true;
+};
+
+const isCurrentDeviceMessage = (data) => {
+ const messageDeviceId = String(data?.deviceId || "");
+ const currentDeviceId = String(deviceRef?.value?.deviceId || "");
+ return !messageDeviceId || !currentDeviceId || messageDeviceId === currentDeviceId;
+};
+
+const isCurrentVersionMessage = (data) => {
+ const messageVersion = String(data?.versionNumber || "");
+ return !messageVersion || !targetVersion || messageVersion === targetVersion;
+};
+
+// 页面切换导致弹窗关闭后,使用下一条进度消息恢复当前 OTA 会话。
+const recoverUpdateFromMessage = (data) => {
+ updateRunId += 1;
+ const runId = updateRunId;
+ targetVersion = String(data?.versionNumber || "");
+ lastFinishedVersion = "";
+ successCallback = null;
+ resultVisible.value = false;
+ resultStatus.value = "";
+ resultReason.value = "";
+ progress.value = 0;
+ phase.value = "started";
+ updating.value = true;
+ clearTimers();
+ timeoutTimer = setTimeout(() => {
+ finishUpdate("failed", "固件更新超时,请稍后重试", runId);
+ }, UPDATE_TIMEOUT);
+};
+
+function handleSocketMessage(message) {
+ if (Number(message?.code ?? 0) !== 0) return;
+ if (![OTA_PROGRESS_EVENT, OTA_RESULT_EVENT].includes(message?.event)) return;
+ if (!isCurrentDeviceMessage(message.data)) return;
+
+ const messageVersion = String(message.data?.versionNumber || "");
+
+ if (message.event === OTA_PROGRESS_EVENT) {
+ if (!updating.value) {
+ if (
+ lastFinishedVersion &&
+ (!messageVersion || messageVersion === lastFinishedVersion)
+ ) {
+ return;
+ }
+ recoverUpdateFromMessage(message.data);
+ } else if (!isCurrentVersionMessage(message.data)) {
+ return;
+ }
+
+ 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 (!updating.value) {
+ if (
+ lastFinishedVersion &&
+ (!messageVersion || messageVersion === lastFinishedVersion)
+ ) {
+ return;
+ }
+ recoverUpdateFromMessage(message.data);
+ } else if (!isCurrentVersionMessage(message.data)) {
+ return;
+ }
+
+ if (message.data?.status === "success") {
+ finishUpdate("success");
+ } else if (message.data?.status === "failed") {
+ finishUpdate("failed", message.data?.reason || "");
+ }
+}
+
+const startSocketListening = () => {
+ if (socketListening) return;
+ uni.$on("socket-inbox", handleSocketMessage);
+ socketListening = true;
+};
+
+export const useOtaUpdate = () => {
+ const store = useStore();
+ const { device } = storeToRefs(store);
+ deviceRef = device;
+ startSocketListening();
+
+ const startUpdate = async ({
+ versionNumber,
+ resourceUrl,
+ wifiSsid = "",
+ wifiPassword = "",
+ onSuccess,
+ }) => {
+ if (updating.value) return false;
+
+ updateRunId += 1;
+ const runId = updateRunId;
+ targetVersion = String(versionNumber || "");
+ lastFinishedVersion = "";
+ successCallback = onSuccess || null;
+ resultVisible.value = false;
+ resultStatus.value = "";
+ resultReason.value = "";
+ progress.value = 0;
+ phase.value = "started";
+ updating.value = true;
+ clearTimers();
+ 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;
+ };
+
+ return {
+ updating,
+ progress,
+ phase,
+ phaseText,
+ resultVisible,
+ resultStatus,
+ resultTitle,
+ resultContent,
+ startUpdate,
+ closeResult,
+ };
+};
diff --git a/src/pages/device/composables/useOtaUpdate.js b/src/pages/device/composables/useOtaUpdate.js
deleted file mode 100644
index 6c6b39f..0000000
--- a/src/pages/device/composables/useOtaUpdate.js
+++ /dev/null
@@ -1,188 +0,0 @@
-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 ff7b693..2a4f61f 100644
--- a/src/pages/device/my-device.vue
+++ b/src/pages/device/my-device.vue
@@ -18,7 +18,7 @@ import {
DEVICE_NAME_STORAGE_KEY,
useDeviceStatus,
} from "./composables/useDeviceStatus";
-import { useOtaUpdate } from "./composables/useOtaUpdate";
+import { useOtaUpdate } from "@/composables/useOtaUpdate";
const store = useStore();
const { updateDevice, clearDevice } = store;
diff --git a/src/pages/device/ota-wifi.vue b/src/pages/device/ota-wifi.vue
index 805a8d5..d287558 100644
--- a/src/pages/device/ota-wifi.vue
+++ b/src/pages/device/ota-wifi.vue
@@ -9,7 +9,7 @@ import {
connectDeviceWifiAPI,
getHardwareBoxVersionAPI,
} from "@/apis";
-import { useOtaUpdate } from "./composables/useOtaUpdate";
+import { useOtaUpdate } from "@/composables/useOtaUpdate";
const STATES = {
SCANNING: "SCANNING",
diff --git a/src/pages/index.vue b/src/pages/index.vue
index 8597b64..9a3a965 100644
--- a/src/pages/index.vue
+++ b/src/pages/index.vue
@@ -1,5 +1,5 @@