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 @@
@@ -89,6 +108,7 @@ const handleConfirm = () => {
{{ confirmText }}
@@ -96,6 +116,13 @@ const handleConfirm = () => {
+
+
+
@@ -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"
/>
+