update:新增ota
This commit is contained in:
+110
-293
@@ -1,28 +1,21 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
import ScreenHint from "@/components/ScreenHint.vue";
|
||||
import ModalDialog from "@/components/ModalDialog.vue";
|
||||
import OtaModal from "@/components/OtaModal.vue";
|
||||
import {
|
||||
connectDeviceWifiAPI,
|
||||
getHardwareBoxTaskStatusAPI,
|
||||
getHardwareBoxVersionAPI,
|
||||
sendHardwareBoxUpdateAPI,
|
||||
} from "@/apis";
|
||||
import useStore from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const store = useStore();
|
||||
const { deviceStatus } = storeToRefs(store);
|
||||
import { useOtaUpdate } from "./composables/useOtaUpdate";
|
||||
|
||||
const STATES = {
|
||||
SCANNING: "SCANNING",
|
||||
LIST: "LIST",
|
||||
CONNECTING: "CONNECTING",
|
||||
CONNECTED: "CONNECTED",
|
||||
UPDATING: "UPDATING",
|
||||
DONE: "DONE",
|
||||
FAILED: "FAILED",
|
||||
};
|
||||
|
||||
const isIOS = uni.getDeviceInfo().osName === "ios";
|
||||
@@ -42,26 +35,37 @@ const keyboardHeight = ref(0);
|
||||
const showPassword = ref(false);
|
||||
// 刷新防抖标志:扫描进行中为 true,禁止重复点击;扫描结束(成功/失败)后重置为 false。
|
||||
const isRefreshing = ref(false);
|
||||
const isStartingUpdate = ref(false);
|
||||
const fromFirmwareUpdate = ref(false);
|
||||
const routeOtaInfo = ref({
|
||||
versionNumber: "",
|
||||
resourceUrl: "",
|
||||
});
|
||||
|
||||
const progress = ref(0);
|
||||
let progressTimer = null;
|
||||
let timeoutTimer = null;
|
||||
let statusTimer = null;
|
||||
let wifiConnectTimeoutTimer = null;
|
||||
let stopWifiStatusWatcher = null;
|
||||
let settleWifiConnectWaiting = null;
|
||||
const countdownVisible = ref(false);
|
||||
const countdownSeconds = ref(3);
|
||||
const firmwareMessageVisible = ref(false);
|
||||
const firmwareMessage = ref("");
|
||||
let countdownTimer = null;
|
||||
let wifiConnectRequestId = 0;
|
||||
const WIFI_CONNECT_TIMEOUT = 60000;
|
||||
const DEVICE_STATUS_STALE_TIME = 6000;
|
||||
const WIFI_CONNECT_FAILED_TEXT = "连接失败,请检查WiFi密码或WiFi状态";
|
||||
// 控制授权拒绝弹窗显示/隐藏
|
||||
const wifiAuthDeniedVisible = ref(false);
|
||||
|
||||
const {
|
||||
updating: otaUpdating,
|
||||
progress: otaProgress,
|
||||
phase: otaPhase,
|
||||
resultVisible: otaResultVisible,
|
||||
resultStatus: otaResultStatus,
|
||||
resultTitle: otaResultTitle,
|
||||
resultContent: otaResultContent,
|
||||
startUpdate: startOtaUpdate,
|
||||
closeResult: closeOtaResult,
|
||||
} = useOtaUpdate();
|
||||
|
||||
const countdownButtonText = computed(
|
||||
() => `${Math.max(1, countdownSeconds.value)}秒后开始`
|
||||
);
|
||||
|
||||
// 判断 WiFi 列表失败是否由用户拒绝授权引起(兼容 errno:103 及各平台 errMsg 变体)。
|
||||
const isWifiPermissionDenied = (err) => {
|
||||
if (err?.errno === 103) return true;
|
||||
@@ -260,84 +264,30 @@ const wifiListScrollHeight = computed(() => {
|
||||
return `${Math.min(itemCount * 92, maxHeight)}rpx`;
|
||||
});
|
||||
|
||||
// 清理本次 WiFi 连接状态监听和超时计时器。
|
||||
const clearWifiConnectWatcher = () => {
|
||||
clearTimeout(wifiConnectTimeoutTimer);
|
||||
wifiConnectTimeoutTimer = null;
|
||||
if (stopWifiStatusWatcher) {
|
||||
stopWifiStatusWatcher();
|
||||
stopWifiStatusWatcher = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消当前 WiFi 连接确认,并恢复弹窗提交状态。
|
||||
// 取消当前 WiFi 连接请求的页面等待状态,并忽略可能迟到的响应。
|
||||
const cancelWifiConnectWaiting = () => {
|
||||
wifiConnectRequestId += 1;
|
||||
if (settleWifiConnectWaiting) {
|
||||
settleWifiConnectWaiting(false);
|
||||
}
|
||||
clearWifiConnectWatcher();
|
||||
connectStatusText.value = "";
|
||||
isSubmittingWifi.value = false;
|
||||
uni.hideLoading();
|
||||
};
|
||||
|
||||
// 根据实时推送的 online/netType 判断设备是否已通过 WiFi 在线。
|
||||
// 返回值含义:true → WiFi 在线成功;"net_fail" → 设备走 4g 失败;false → 未就绪。
|
||||
const isDeviceConnectedByWifi = (deviceStatus) => {
|
||||
// online 不为 true → 设备尚未在线,继续等待推送
|
||||
if (deviceStatus?.online !== true) return false;
|
||||
const netType = String(deviceStatus?.netType || "").toLowerCase();
|
||||
// online:true + netType:4g → 设备已切 4g,WiFi 连接失败
|
||||
if (netType === "4g") return "net_fail";
|
||||
// online:true + netType:wifi → WiFi 连接成功
|
||||
// online:true + netType:"" → 设备在线但 netType 暂未上报,继续等待推送
|
||||
return netType === "wifi";
|
||||
// 把接口失败和传输异常转换为连接弹窗内的用户提示。
|
||||
const getWifiConnectErrorText = (error) => {
|
||||
const message = error?.message || "";
|
||||
if (message.includes("请先开启智能弓")) return "请先开启智能弓";
|
||||
if (message.includes("超时") || error?.errMsg?.includes("timeout")) {
|
||||
return "等待设备响应超时,请重试";
|
||||
}
|
||||
if (error?.errMsg) return "网络异常,请检查网络后重试";
|
||||
return message || WIFI_CONNECT_FAILED_TEXT;
|
||||
};
|
||||
|
||||
// 等待提交配置后的新 WS 状态;忽略提交前的旧状态,超时后按连接失败处理。
|
||||
const waitForDeviceWifiConnected = (requestId, receivedAfter) => {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const finish = (result) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearWifiConnectWatcher();
|
||||
settleWifiConnectWaiting = null;
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
settleWifiConnectWaiting = finish;
|
||||
stopWifiStatusWatcher = watch(
|
||||
deviceStatus,
|
||||
(status) => {
|
||||
if (requestId !== wifiConnectRequestId) {
|
||||
finish(false);
|
||||
return;
|
||||
}
|
||||
if (Number(status?.receivedAt) <= receivedAfter) return;
|
||||
|
||||
const connResult = isDeviceConnectedByWifi(status);
|
||||
if (connResult === true) {
|
||||
finish(true);
|
||||
} else if (connResult === "net_fail") {
|
||||
finish(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
wifiConnectTimeoutTimer = setTimeout(
|
||||
() => finish(false),
|
||||
WIFI_CONNECT_TIMEOUT
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 提交 WiFi 配置给游戏设备,并通过 WS 确认设备真实连上 WiFi 后再展示成功。
|
||||
// 提交 WiFi 配置,并以接口返回的设备最终连接结果更新页面。
|
||||
const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
||||
if (isSubmittingWifi.value) return;
|
||||
wifiConnectRequestId += 1;
|
||||
const requestId = wifiConnectRequestId;
|
||||
clearWifiConnectWatcher();
|
||||
isSubmittingWifi.value = true;
|
||||
connectStatusText.value = "WiFi连接中...";
|
||||
uni.showLoading({
|
||||
@@ -345,11 +295,9 @@ const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
||||
mask: true,
|
||||
});
|
||||
try {
|
||||
await connectDeviceWifiAPI(ssid, password);
|
||||
const connectResult = await connectDeviceWifiAPI(ssid, password);
|
||||
if (requestId !== wifiConnectRequestId) return;
|
||||
const isConnected = await waitForDeviceWifiConnected(requestId, Date.now());
|
||||
if (requestId !== wifiConnectRequestId) return;
|
||||
if (!isConnected) {
|
||||
if (connectResult?.success !== true) {
|
||||
connectError.value = WIFI_CONNECT_FAILED_TEXT;
|
||||
return;
|
||||
}
|
||||
@@ -361,14 +309,15 @@ const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
||||
};
|
||||
connectError.value = "";
|
||||
currentState.value = STATES.CONNECTED;
|
||||
if (fromFirmwareUpdate.value) {
|
||||
openFirmwareCountdown();
|
||||
}
|
||||
} catch (err) {
|
||||
if (requestId === wifiConnectRequestId) {
|
||||
connectError.value =
|
||||
err?.code === -1 && err?.message ? err.message : WIFI_CONNECT_FAILED_TEXT;
|
||||
connectError.value = getWifiConnectErrorText(err);
|
||||
}
|
||||
} finally {
|
||||
if (requestId === wifiConnectRequestId) {
|
||||
clearWifiConnectWatcher();
|
||||
connectStatusText.value = "";
|
||||
isSubmittingWifi.value = false;
|
||||
uni.hideLoading();
|
||||
@@ -386,83 +335,7 @@ const joinNetwork = () => {
|
||||
submitDeviceWifiConfig({ ssid, password });
|
||||
};
|
||||
|
||||
// 清理 OTA 更新相关定时器,避免页面退出或状态结束后继续执行。
|
||||
const clearUpdateTimers = () => {
|
||||
clearInterval(progressTimer);
|
||||
clearTimeout(timeoutTimer);
|
||||
clearTimeout(statusTimer);
|
||||
progressTimer = null;
|
||||
timeoutTimer = null;
|
||||
statusTimer = null;
|
||||
};
|
||||
|
||||
// 启动本地进度条动画,真实完成状态以后端任务轮询结果为准。
|
||||
const startProgressAnimation = () => {
|
||||
clearInterval(progressTimer);
|
||||
progressTimer = setInterval(() => {
|
||||
if (progress.value >= 90) {
|
||||
clearInterval(progressTimer);
|
||||
return;
|
||||
}
|
||||
const increment = Math.max(0.5, 2 - progress.value / 60);
|
||||
progress.value = Math.min(90, progress.value + increment);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 将 OTA 更新流程标记为失败并切换到失败页面。
|
||||
const failUpdate = () => {
|
||||
clearUpdateTimers();
|
||||
isStartingUpdate.value = false;
|
||||
currentState.value = STATES.FAILED;
|
||||
};
|
||||
|
||||
// 将 OTA 更新流程标记为成功,进度补到 100% 后进入完成页面。
|
||||
const completeUpdate = () => {
|
||||
clearUpdateTimers();
|
||||
isStartingUpdate.value = false;
|
||||
progress.value = 100;
|
||||
setTimeout(() => {
|
||||
currentState.value = STATES.DONE;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// 轮询 OTA 更新任务状态,状态为处理中则继续轮询,成功或失败则结束流程。
|
||||
const pollUpdateTaskStatus = (taskId) => {
|
||||
clearTimeout(statusTimer);
|
||||
statusTimer = setTimeout(async () => {
|
||||
try {
|
||||
const taskStatus = await getHardwareBoxTaskStatusAPI(taskId);
|
||||
const status = Number(taskStatus?.status);
|
||||
if (status === 2) {
|
||||
completeUpdate();
|
||||
return;
|
||||
}
|
||||
if (status === 3) {
|
||||
failUpdate();
|
||||
return;
|
||||
}
|
||||
if (status === 0 || status === 1) {
|
||||
pollUpdateTaskStatus(taskId);
|
||||
return;
|
||||
}
|
||||
failUpdate();
|
||||
} catch (err) {
|
||||
failUpdate();
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 判断设备是否满足 OTA 更新条件,不满足时返回精确提示文案。
|
||||
const getUpdateDisabledReason = (deviceStatus) => {
|
||||
if (deviceStatus?.online !== true) return "请先开启智能弓";
|
||||
if (Date.now() - Number(deviceStatus?.receivedAt || 0) > DEVICE_STATUS_STALE_TIME) {
|
||||
return "设备状态同步中,请稍后重试";
|
||||
}
|
||||
if (String(deviceStatus?.netType || "").toLowerCase() !== "wifi") return "设备当前未连接 WiFi,请先连接 WiFi 后再更新";
|
||||
return "";
|
||||
};
|
||||
|
||||
// 获取 OTA 更新版本信息,优先使用首页跳转传入的数据,没有传参时再请求后端版本接口。
|
||||
// 获取 OTA 更新版本信息,正常使用入口传参,缺失时再请求后端兜底。
|
||||
const getOtaVersionInfo = async () => {
|
||||
if (routeOtaInfo.value.versionNumber && routeOtaInfo.value.resourceUrl) {
|
||||
return {
|
||||
@@ -474,100 +347,43 @@ const getOtaVersionInfo = async () => {
|
||||
return getHardwareBoxVersionAPI();
|
||||
};
|
||||
|
||||
// 点击开始更新时先判断设备状态和版本信息,满足条件才发送 OTA 指令并开始轮询任务状态。
|
||||
const startUpdate = async () => {
|
||||
if (isStartingUpdate.value) return;
|
||||
if (!connectedWifi.value) return;
|
||||
isStartingUpdate.value = true;
|
||||
|
||||
const startFirmwareUpdate = async () => {
|
||||
if (!fromFirmwareUpdate.value || !connectedWifi.value || otaUpdating.value) return;
|
||||
try {
|
||||
const disabledReason = getUpdateDisabledReason(deviceStatus.value);
|
||||
if (disabledReason) {
|
||||
isStartingUpdate.value = false;
|
||||
uni.showToast({
|
||||
title: disabledReason,
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let versionInfo;
|
||||
try {
|
||||
versionInfo = await getOtaVersionInfo();
|
||||
} catch (err) {
|
||||
isStartingUpdate.value = false;
|
||||
uni.showToast({
|
||||
title: "获取更新版本失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const versionInfo = await getOtaVersionInfo();
|
||||
if (!versionInfo?.needUpdate) {
|
||||
isStartingUpdate.value = false;
|
||||
uni.showToast({
|
||||
title: "当前已是最新版本",
|
||||
icon: "none",
|
||||
});
|
||||
firmwareMessage.value = "当前已是最新版本";
|
||||
firmwareMessageVisible.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
currentState.value = STATES.UPDATING;
|
||||
progress.value = 0;
|
||||
startProgressAnimation();
|
||||
timeoutTimer = setTimeout(() => {
|
||||
if (currentState.value === STATES.UPDATING) {
|
||||
failUpdate();
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
const updateResult = await sendHardwareBoxUpdateAPI({
|
||||
await startOtaUpdate({
|
||||
versionNumber: versionInfo.versionNumber,
|
||||
wifiSsid: connectedWifi.value.SSID,
|
||||
wifiPassword: connectedWifi.value.password || "",
|
||||
resourceUrl: versionInfo.resourceUrl,
|
||||
});
|
||||
if (!updateResult?.taskId) {
|
||||
failUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
pollUpdateTaskStatus(updateResult.taskId);
|
||||
} catch (err) {
|
||||
failUpdate();
|
||||
} catch (error) {
|
||||
firmwareMessage.value = "获取更新版本失败,请重试";
|
||||
firmwareMessageVisible.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// WebSocket 成功回调保留兜底能力,触发后直接按更新完成处理。
|
||||
const handleWsDone = () => {
|
||||
completeUpdate();
|
||||
const clearFirmwareCountdown = () => {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
};
|
||||
|
||||
// WebSocket 失败回调保留兜底能力,触发后直接按更新失败处理。
|
||||
const handleWsFail = () => {
|
||||
failUpdate();
|
||||
};
|
||||
|
||||
// 处理更新完成返回,兼容首页 OTA 弹窗入口和设备页普通入口。
|
||||
const handleDone = () => {
|
||||
const pages = getCurrentPages();
|
||||
const prevPage = pages[pages.length - 2];
|
||||
const prevVm = prevPage?.$vm;
|
||||
|
||||
if (prevVm && "otaState" in prevVm && "otaVisible" in prevVm) {
|
||||
prevVm.otaState = "update_success";
|
||||
prevVm.otaVisible = true;
|
||||
}
|
||||
|
||||
uni.navigateBack({ delta: 1 });
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
if (connectedWifi.value) {
|
||||
currentState.value = STATES.CONNECTED;
|
||||
} else {
|
||||
startScanning();
|
||||
}
|
||||
const openFirmwareCountdown = () => {
|
||||
clearFirmwareCountdown();
|
||||
countdownSeconds.value = 3;
|
||||
countdownVisible.value = true;
|
||||
countdownTimer = setInterval(() => {
|
||||
countdownSeconds.value -= 1;
|
||||
if (countdownSeconds.value > 0) return;
|
||||
clearFirmwareCountdown();
|
||||
countdownVisible.value = false;
|
||||
void startFirmwareUpdate();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 监听系统输入法高度,用于让底部弹窗避开键盘遮挡。
|
||||
@@ -585,8 +401,9 @@ const togglePasswordVisibility = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 页面加载时接收首页传入的 OTA 版本号和固件地址。
|
||||
// 页面加载时识别普通 WiFi 入口和固件更新入口。
|
||||
onLoad((options = {}) => {
|
||||
fromFirmwareUpdate.value = options.source === "firmware-update";
|
||||
routeOtaInfo.value = {
|
||||
versionNumber: decodeURIComponent(options.versionNumber || ""),
|
||||
resourceUrl: decodeURIComponent(options.resourceUrl || ""),
|
||||
@@ -613,7 +430,7 @@ onUnmounted(() => {
|
||||
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
|
||||
}
|
||||
cancelWifiConnectWaiting();
|
||||
clearUpdateTimers();
|
||||
clearFirmwareCountdown();
|
||||
wx.offGetWifiList && wx.offGetWifiList();
|
||||
});
|
||||
</script>
|
||||
@@ -711,46 +528,6 @@ onUnmounted(() => {
|
||||
</block>
|
||||
</scroll-view>
|
||||
|
||||
<!-- CONNECTED:开始更新按钮 -->
|
||||
<view v-if="currentState === 'CONNECTED'" class="bottom-btn-area connected-bottom-btn-area">
|
||||
<view class="primary-btn update-btn" @click="startUpdate">
|
||||
<text class="primary-btn-text">开始更新</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- UPDATING -->
|
||||
<view v-else-if="currentState === 'UPDATING'" class="center-page">
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/target-char.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" />
|
||||
<text class="page-title" style="margin-top: 24rpx;">更新中,请稍等片刻...</text>
|
||||
<view class="progress-wrap">
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
|
||||
</view>
|
||||
<text class="progress-pct">{{ Math.floor(progress) }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- DONE -->
|
||||
<view v-else-if="currentState === 'DONE'" class="center-page">
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/check-char.png" mode="aspectFit" style="width: 194rpx; height: 166rpx;" />
|
||||
<text class="page-title" style="margin-top: 24rpx;">更新完成</text>
|
||||
<text class="page-desc-white">请关机并重启智能弓</text>
|
||||
<view class="primary-btn done-btn" style="margin-top:20px" @click="handleDone">
|
||||
<text class="primary-btn-text">完成</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- FAILED -->
|
||||
<view v-else-if="currentState === 'FAILED'" class="center-page">
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/close-char.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" />
|
||||
<text class="page-title fail-title" style="margin-top: 24rpx;">更新失败</text>
|
||||
<text class="page-desc-white">请确保:</text>
|
||||
<text class="page-desc-white">1、智能弓已开启</text>
|
||||
<text class="page-desc-white">2、网路连接稳定</text>
|
||||
<view class="primary-btn done-btn" style="margin-top: 40rpx;" @click="handleRetry">
|
||||
<text class="primary-btn-text">重试</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- CONNECTING 底部弹窗 -->
|
||||
@@ -883,6 +660,46 @@ onUnmounted(() => {
|
||||
</view>
|
||||
</view>
|
||||
</ScreenHint>
|
||||
|
||||
<ModalDialog
|
||||
:show="countdownVisible"
|
||||
title="WiFi连接成功"
|
||||
content="3秒后将自动开始更新"
|
||||
:confirmText="countdownButtonText"
|
||||
:showCancel="false"
|
||||
:confirmDisabled="true"
|
||||
></ModalDialog>
|
||||
|
||||
<OtaModal
|
||||
:visible="otaUpdating"
|
||||
state="update_progress"
|
||||
:progress="otaProgress"
|
||||
:phase="otaPhase"
|
||||
/>
|
||||
|
||||
<OtaModal
|
||||
:visible="otaResultVisible && otaResultStatus === 'success'"
|
||||
state="update_success"
|
||||
@done="closeOtaResult"
|
||||
/>
|
||||
|
||||
<ModalDialog
|
||||
:show="otaResultVisible && otaResultStatus === 'failed'"
|
||||
:title="otaResultTitle"
|
||||
:content="otaResultContent"
|
||||
confirmText="关闭"
|
||||
:showCancel="false"
|
||||
:onConfirm="closeOtaResult"
|
||||
></ModalDialog>
|
||||
|
||||
<ModalDialog
|
||||
:show="firmwareMessageVisible"
|
||||
title="固件更新"
|
||||
:content="firmwareMessage"
|
||||
confirmText="关闭"
|
||||
:showCancel="false"
|
||||
:onConfirm="() => (firmwareMessageVisible = false)"
|
||||
></ModalDialog>
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user