update:优化ota状态共享

This commit is contained in:
2026-09-21 16:22:05 +08:00
parent 4bd7599cc0
commit 01e8f6c6da
6 changed files with 328 additions and 328 deletions
+9
View File
@@ -158,6 +158,7 @@ const handleUpdateClick = () => {
<view class="progress-track">
<view class="progress-fill" :style="{ width: `${progressValue}%` }"></view>
</view>
<text class="progress-warning">请勿离开当前页面</text>
</view>
</block>
@@ -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 {
+227
View File
@@ -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,
};
};
@@ -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,
};
};
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -9,7 +9,7 @@ import {
connectDeviceWifiAPI,
getHardwareBoxVersionAPI,
} from "@/apis";
import { useOtaUpdate } from "./composables/useOtaUpdate";
import { useOtaUpdate } from "@/composables/useOtaUpdate";
const STATES = {
SCANNING: "SCANNING",
+87 -135
View File
@@ -1,5 +1,5 @@
<script setup>
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
import {computed, onMounted, ref, watch} from "vue";
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import AppFooter from "@/components/AppFooter.vue";
@@ -16,10 +16,10 @@ import {
getHomeData,
getMyDevicesAPI,
getScoreRankList,
sendHardwareBoxUpdateAPI,
silentLoginAPI,
} from "@/apis";
import {topThreeColors} from "@/constants";
import {useOtaUpdate} from "@/composables/useOtaUpdate";
import useStore from "@/store";
import {storeToRefs} from "pinia";
@@ -70,9 +70,6 @@ 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: "",
@@ -83,26 +80,16 @@ const otaInfo = ref({
const isStartingOta = ref(false);
let isCheckingOta = false;
let otaCheckQueuedForOnline = false;
let otaTimeoutTimer = null;
let otaUpdateRunId = 0;
const OTA_UPDATE_TIMEOUT = 10 * 60 * 1000;
const OTA_PROGRESS_EVENT = "/addons/shoot/otaProgress";
const OTA_RESULT_EVENT = "/addons/shoot/otaResult";
// 清理首页 OTA 更新超时计时器。
const clearOtaUpdateTimers = () => {
clearTimeout(otaTimeoutTimer);
otaTimeoutTimer = null;
};
// 使当前 OTA 运行失效,确保旧请求返回后不会覆盖新状态。
const invalidateOtaUpdateRun = () => {
otaUpdateRunId += 1;
clearOtaUpdateTimers();
};
const isOtaUpdateRunActive = (runId) =>
runId === otaUpdateRunId && otaState.value === "update_progress";
const {
updating: otaUpdating,
progress: otaProgress,
phase: otaPhase,
resultVisible: otaResultVisible,
resultStatus: otaResultStatus,
startUpdate: startOtaUpdate,
closeResult: closeOtaResult,
} = useOtaUpdate();
// 获取并保存后端返回的 OTA 版本信息,供弹窗展示和更新接口使用。
const applyOtaVersionInfo = (versionInfo) => {
@@ -118,11 +105,21 @@ const applyOtaVersionInfo = (versionInfo) => {
// 检查当前设备盒子是否存在可升级版本。
const checkOtaUpdate = async () => {
if (isCheckingOta || otaVisible.value) return;
if (
isCheckingOta ||
otaVisible.value ||
otaUpdating.value ||
otaResultVisible.value
) return;
isCheckingOta = true;
try {
if (online.value !== true || otaVisible.value) return;
if (
online.value !== true ||
otaVisible.value ||
otaUpdating.value ||
otaResultVisible.value
) return;
let versionInfo;
try {
@@ -130,20 +127,24 @@ const checkOtaUpdate = async () => {
} catch (err) {
return;
}
if (otaVisible.value) return;
if (otaVisible.value || otaUpdating.value || otaResultVisible.value) return;
applyOtaVersionInfo(versionInfo);
if (!otaInfo.value.needUpdate) return;
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
const now = Date.now();
if (!otaInfo.value.forceUpdate && dismissedAt && now - dismissedAt < 24 * 60 * 60 * 1000) return;
otaState.value = "new_version";
otaVisible.value = true;
} finally {
isCheckingOta = false;
const shouldRecheckForOnline = otaCheckQueuedForOnline;
otaCheckQueuedForOnline = false;
if (shouldRecheckForOnline && !otaVisible.value) {
if (
shouldRecheckForOnline &&
!otaVisible.value &&
!otaUpdating.value &&
!otaResultVisible.value
) {
void checkOtaUpdate();
}
}
@@ -151,7 +152,13 @@ const checkOtaUpdate = async () => {
// 设备由 WS 通知上线时补查 OTA;已显示任何 OTA 弹窗时保留当前流程和结果。
watch(online, (nextOnline, previousOnline) => {
if (previousOnline !== false || nextOnline !== true || otaVisible.value) return;
if (
previousOnline !== false ||
nextOnline !== true ||
otaVisible.value ||
otaUpdating.value ||
otaResultVisible.value
) return;
if (isCheckingOta) {
otaCheckQueuedForOnline = true;
return;
@@ -159,6 +166,13 @@ watch(online, (nextOnline, previousOnline) => {
void checkOtaUpdate();
});
// 任一页面发起或恢复 OTA 后,关闭首页的版本发现弹窗,避免两层弹窗重叠。
watch([otaUpdating, otaResultVisible], ([updating, resultVisible]) => {
if (!updating && !resultVisible) return;
otaVisible.value = false;
isStartingOta.value = false;
});
// 拼接 OTA WiFi 页参数,让未连 WiFi 的设备继续使用同一份版本信息。
const getOtaWifiUrl = () => {
const { versionNumber, resourceUrl } = otaInfo.value;
@@ -177,89 +191,17 @@ const handleOtaDismiss = () => {
otaVisible.value = false;
};
// 将首页 OTA 直连更新流程标记为失败
const failHomeOtaUpdate = (runId) => {
if (!isOtaUpdateRunActive(runId)) return;
invalidateOtaUpdateRun();
isStartingOta.value = false;
otaState.value = "update_failure";
otaVisible.value = true;
};
// 将首页 OTA 直连更新流程标记为成功。
const completeHomeOtaUpdate = (runId) => {
if (!isOtaUpdateRunActive(runId)) return;
invalidateOtaUpdateRun();
isStartingOta.value = false;
otaInfo.value = {...otaInfo.value, needUpdate: false};
otaProgress.value = 100;
otaPhase.value = "installing";
otaState.value = "update_success";
otaVisible.value = true;
};
// 首页只处理当前设备、当前目标版本的 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;
const messageDeviceId = String(message.data?.deviceId || "");
const currentDeviceId = String(device.value?.deviceId || "");
if (messageDeviceId && currentDeviceId && messageDeviceId !== currentDeviceId) 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;
}
return;
}
if (message.data?.status === "success") {
completeHomeOtaUpdate(runId);
} else if (message.data?.status === "failed") {
failHomeOtaUpdate(runId);
}
};
// 设备盒子已连 WiFi 时,从首页直接传空 WiFi 信息发起 OTA 更新。
// 设备盒子已连 WiFi 时,通过共享 OTA 状态发起更新
const startHomeOtaUpdate = async () => {
invalidateOtaUpdateRun();
const runId = otaUpdateRunId;
otaState.value = "update_progress";
otaVisible.value = true;
otaProgress.value = 0;
otaPhase.value = "started";
otaTimeoutTimer = setTimeout(() => {
if (!isOtaUpdateRunActive(runId)) return;
failHomeOtaUpdate(runId);
}, OTA_UPDATE_TIMEOUT);
try {
const updateResult = await sendHardwareBoxUpdateAPI({
otaVisible.value = false;
await startOtaUpdate({
versionNumber: otaInfo.value.versionNumber,
wifiSsid: "",
wifiPassword: "",
resourceUrl: otaInfo.value.resourceUrl,
onSuccess: () => {
otaInfo.value = {...otaInfo.value, needUpdate: false};
},
});
if (!isOtaUpdateRunActive(runId)) return;
if (!updateResult?.taskId) {
failHomeOtaUpdate(runId);
return;
}
} catch (err) {
if (!isOtaUpdateRunActive(runId)) return;
failHomeOtaUpdate(runId);
}
isStartingOta.value = false;
};
// 点击立即更新时先判断设备是否在线并已通过 WiFi 联网,未联网时先展示连接引导。
@@ -305,15 +247,25 @@ const goWifiForOtaUpdate = () => {
uni.navigateTo({ url: getOtaWifiUrl() });
};
// 处理 OTA 更新成功后的完成按钮,关闭结果弹窗。
// 处理 OTA 更新成功后的完成按钮,关闭共享结果弹窗。
const handleOtaDone = () => {
otaVisible.value = false;
closeOtaResult();
};
// 处理 OTA 更新失败后的重试按钮,重新走立即更新判断流程
const handleOtaRetry = () => {
invalidateOtaUpdateRun();
handleOtaUpdate();
// 处理共享 OTA 更新失败后的重试,先刷新版本参数再重新判断网络状态
const handleOtaRetry = async () => {
closeOtaResult();
try {
const versionInfo = await getHardwareBoxVersionAPI();
applyOtaVersionInfo(versionInfo);
if (!otaInfo.value.needUpdate) return;
await handleOtaUpdate();
} catch (error) {
uni.showToast({
title: "获取更新版本失败,请重试",
icon: "none",
});
}
};
// 提取积分榜接口返回的榜单数组,兼容数组和对象两种返回格式。
@@ -383,18 +335,11 @@ const syncHomeDevice = async () => {
};
onShow(async (options) => {
onShow(async () => {
const env = uni.getAccountInfoSync().miniProgram.envVersion;
const token = uni.getStorageSync(`${env}_token`);
// 检查是否从 OTA 更新页面返回
if (options && options.updateResult) {
otaState.value = options.updateResult;
if (options.updateResult === "update_success") {
otaInfo.value = {...otaInfo.value, needUpdate: false};
}
otaVisible.value = true;
} else if (token || user.value.id) {
if (token || user.value.id) {
await checkOtaUpdate();
}
@@ -455,17 +400,11 @@ 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();
});
onShareAppMessage(() => {
return {
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
@@ -486,21 +425,34 @@ onShareTimeline(() => {
<template>
<Container :isHome="true" :showBackToGame="true">
<!-- OTA 升级弹窗使用 visible 控制显隐description 为副标题changelog 为详细说明 -->
<!-- 首页版本发现弹窗仅负责触发更新执行状态由三个页面共享 -->
<OtaModal
:visible="otaVisible"
:state="otaState"
:visible="otaVisible && !otaUpdating && !otaResultVisible"
state="new_version"
:version="otaInfo.versionNumber"
:progress="otaProgress"
:phase="otaPhase"
:description="''"
:changelog="otaInfo.versionInfo"
:forceUpdate="otaInfo.forceUpdate"
@update="handleOtaUpdate"
@skip="handleOtaDismiss"
@close="handleOtaDismiss"
/>
<OtaModal
:visible="otaUpdating"
state="update_progress"
:progress="otaProgress"
:phase="otaPhase"
/>
<OtaModal
:visible="otaResultVisible && otaResultStatus === 'success'"
state="update_success"
@done="handleOtaDone"
/>
<OtaModal
:visible="otaResultVisible && otaResultStatus === 'failed'"
state="update_failure"
@retry="handleOtaRetry"
@close="closeOtaResult"
/>
<ModalDialog
:show="wifiRequiredVisible"