feat: OTA部分对接代码提交
This commit is contained in:
+29
@@ -23,6 +23,7 @@ try {
|
|||||||
console.error("获取环境信息失败,使用默认正式环境", e);
|
console.error("获取环境信息失败,使用默认正式环境", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统一处理业务接口请求,包含登录态、业务错误和 WiFi 连接空响应兼容。
|
||||||
function request(method, url, data = {}) {
|
function request(method, url, data = {}) {
|
||||||
const token = uni.getStorageSync(
|
const token = uni.getStorageSync(
|
||||||
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
@@ -37,6 +38,10 @@ function request(method, url, data = {}) {
|
|||||||
data,
|
data,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
|
if (url === "/user/hardwareBox/connectWifi" && res.statusCode === 200 && res.data && Object.keys(res.data).length === 0) {
|
||||||
|
resolve({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
const {code, data, message} = res.data;
|
const {code, data, message} = res.data;
|
||||||
if (code === 0) resolve(data);
|
if (code === 0) resolve(data);
|
||||||
@@ -84,6 +89,10 @@ function request(method, url, data = {}) {
|
|||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (url === "/user/hardwareBox/connectWifi") {
|
||||||
|
reject({code, message});
|
||||||
|
return;
|
||||||
|
}
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: message,
|
title: message,
|
||||||
icon: "none",
|
icon: "none",
|
||||||
@@ -448,6 +457,26 @@ export const getDeviceBatteryAPI = async () => {
|
|||||||
return request("GET", "/user/device/battery");
|
return request("GET", "/user/device/battery");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 设备连接指定 WiFi,只下发 WiFi 凭证,不触发 OTA 升级。
|
||||||
|
export const connectDeviceWifiAPI = async (ssid, password) => {
|
||||||
|
return request("POST", "/user/hardwareBox/connectWifi", {ssid, password});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取硬件盒子版本信息,用于判断当前设备是否需要 OTA 升级。
|
||||||
|
export const getHardwareBoxVersionAPI = async () => {
|
||||||
|
return request("GET", "/user/hardwareBox/version");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送硬件盒子 OTA 更新指令,服务端会返回后续轮询使用的任务 ID。
|
||||||
|
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) => {
|
export const addNoteAPI = async (id, remark) => {
|
||||||
return request("POST", "/user/score/sheet/remark", {id, remark});
|
return request("POST", "/user/score/sheet/remark", {id, remark});
|
||||||
};
|
};
|
||||||
|
|||||||
+185
-22
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from "vue";
|
import {onMounted, onUnmounted, ref} from "vue";
|
||||||
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
|
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import AppFooter from "@/components/AppFooter.vue";
|
import AppFooter from "@/components/AppFooter.vue";
|
||||||
@@ -12,9 +12,12 @@ import {
|
|||||||
checkUserBindAPI,
|
checkUserBindAPI,
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
getDeviceBatteryAPI,
|
getDeviceBatteryAPI,
|
||||||
|
getHardwareBoxTaskStatusAPI,
|
||||||
|
getHardwareBoxVersionAPI,
|
||||||
getHomeData,
|
getHomeData,
|
||||||
getMyDevicesAPI,
|
getMyDevicesAPI,
|
||||||
getScoreRankList,
|
getScoreRankList,
|
||||||
|
sendHardwareBoxUpdateAPI,
|
||||||
silentLoginAPI,
|
silentLoginAPI,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
import {topThreeColors} from "@/constants";
|
import {topThreeColors} from "@/constants";
|
||||||
@@ -40,39 +43,194 @@ const scoreRankList = ref([]);
|
|||||||
// OTA 相关
|
// OTA 相关
|
||||||
const otaVisible = ref(false);
|
const otaVisible = ref(false);
|
||||||
const otaState = ref("new_version");
|
const otaState = ref("new_version");
|
||||||
const OTA_MOCK = {
|
const otaProgress = ref(0);
|
||||||
hasUpdate: true,
|
const otaInfo = ref({
|
||||||
version: "V8.7.0",
|
versionNumber: "",
|
||||||
description: "新版本将优化智能弓体验",
|
versionInfo: "",
|
||||||
details: "升级前请确保:\n1、智能弓已开启,且电量充足\n2、所处稳定的 Wi-Fi 环境中。",
|
resourceUrl: "",
|
||||||
forceUpdate: false,
|
forceUpdate: false,
|
||||||
|
});
|
||||||
|
const isStartingOta = ref(false);
|
||||||
|
let otaProgressTimer = null;
|
||||||
|
let otaStatusTimer = null;
|
||||||
|
let otaTimeoutTimer = null;
|
||||||
|
|
||||||
|
// 清理首页 OTA 更新定时器,避免弹窗关闭或页面卸载后继续轮询。
|
||||||
|
const clearOtaUpdateTimers = () => {
|
||||||
|
clearInterval(otaProgressTimer);
|
||||||
|
clearTimeout(otaStatusTimer);
|
||||||
|
clearTimeout(otaTimeoutTimer);
|
||||||
|
otaProgressTimer = null;
|
||||||
|
otaStatusTimer = null;
|
||||||
|
otaTimeoutTimer = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkOtaUpdate = () => {
|
// 启动首页 OTA 本地进度动画,最终成功失败以后端任务状态为准。
|
||||||
if (!OTA_MOCK.hasUpdate) return;
|
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 = {
|
||||||
|
versionNumber: versionInfo?.versionNumber || "",
|
||||||
|
versionInfo: versionInfo?.versionInfo || "",
|
||||||
|
resourceUrl: versionInfo?.resourceUrl || "",
|
||||||
|
forceUpdate: Number(versionInfo?.forceUpdate) === 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查当前设备盒子是否存在可升级版本。
|
||||||
|
const checkOtaUpdate = async () => {
|
||||||
|
let versionInfo;
|
||||||
|
try {
|
||||||
|
versionInfo = await getHardwareBoxVersionAPI();
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!versionInfo?.needUpdate) return;
|
||||||
|
applyOtaVersionInfo(versionInfo);
|
||||||
|
|
||||||
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
|
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (dismissedAt && now - dismissedAt < 24 * 60 * 60 * 1000) return;
|
if (!otaInfo.value.forceUpdate && dismissedAt && now - dismissedAt < 24 * 60 * 60 * 1000) return;
|
||||||
otaState.value = "new_version";
|
otaState.value = "new_version";
|
||||||
otaVisible.value = true;
|
otaVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 拼接 OTA WiFi 页参数,让未连 WiFi 的设备继续使用同一份版本信息。
|
||||||
|
const getOtaWifiUrl = () => {
|
||||||
|
const { versionNumber, resourceUrl } = otaInfo.value;
|
||||||
|
const query = [
|
||||||
|
`versionNumber=${encodeURIComponent(versionNumber)}`,
|
||||||
|
`resourceUrl=${encodeURIComponent(resourceUrl)}`,
|
||||||
|
].join("&");
|
||||||
|
return `/pages/ota-wifi?${query}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 OTA 弹窗暂不更新,强制更新时不允许关闭。
|
||||||
const handleOtaDismiss = () => {
|
const handleOtaDismiss = () => {
|
||||||
|
if (otaInfo.value.forceUpdate) return;
|
||||||
uni.setStorageSync("ota_dismissed_at", Date.now());
|
uni.setStorageSync("ota_dismissed_at", Date.now());
|
||||||
otaVisible.value = false;
|
otaVisible.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOtaUpdate = () => {
|
// 将首页 OTA 直连更新流程标记为失败。
|
||||||
otaVisible.value = false;
|
const failHomeOtaUpdate = () => {
|
||||||
uni.navigateTo({ url: "/pages/ota-wifi" });
|
clearOtaUpdateTimers();
|
||||||
|
isStartingOta.value = false;
|
||||||
|
otaState.value = "update_failure";
|
||||||
|
otaVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 将首页 OTA 直连更新流程标记为成功。
|
||||||
|
const completeHomeOtaUpdate = () => {
|
||||||
|
clearOtaUpdateTimers();
|
||||||
|
isStartingOta.value = false;
|
||||||
|
otaProgress.value = 100;
|
||||||
|
setTimeout(() => {
|
||||||
|
otaState.value = "update_success";
|
||||||
|
otaVisible.value = true;
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 轮询首页直接发起的 OTA 更新任务状态。
|
||||||
|
const pollHomeOtaTaskStatus = (taskId) => {
|
||||||
|
clearTimeout(otaStatusTimer);
|
||||||
|
otaStatusTimer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const taskStatus = await getHardwareBoxTaskStatusAPI(taskId);
|
||||||
|
const status = Number(taskStatus?.status);
|
||||||
|
if (status === 2) {
|
||||||
|
completeHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === 3) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === 0 || status === 1) {
|
||||||
|
pollHomeOtaTaskStatus(taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
} catch (err) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设备盒子已连 WiFi 时,从首页直接传空 WiFi 信息发起 OTA 更新。
|
||||||
|
const startHomeOtaUpdate = async () => {
|
||||||
|
otaState.value = "update_progress";
|
||||||
|
otaVisible.value = true;
|
||||||
|
otaProgress.value = 0;
|
||||||
|
startOtaProgressAnimation();
|
||||||
|
otaTimeoutTimer = setTimeout(() => {
|
||||||
|
if (otaState.value === "update_progress") {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
}
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updateResult = await sendHardwareBoxUpdateAPI({
|
||||||
|
versionNumber: otaInfo.value.versionNumber,
|
||||||
|
wifiSsid: "",
|
||||||
|
wifiPassword: "",
|
||||||
|
resourceUrl: otaInfo.value.resourceUrl,
|
||||||
|
});
|
||||||
|
if (!updateResult?.taskId) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pollHomeOtaTaskStatus(updateResult.taskId);
|
||||||
|
} catch (err) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击立即更新时先判断设备是否已通过 WiFi 联网,已联网则首页直接更新,否则跳转 WiFi 页面。
|
||||||
|
const handleOtaUpdate = async () => {
|
||||||
|
if (isStartingOta.value) return;
|
||||||
|
isStartingOta.value = true;
|
||||||
|
let deviceStatus;
|
||||||
|
try {
|
||||||
|
deviceStatus = await getDeviceBatteryAPI();
|
||||||
|
} catch (err) {
|
||||||
|
isStartingOta.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: "获取设备状态失败,请重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(deviceStatus?.netType || "").toLowerCase() === "wifi") {
|
||||||
|
startHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isStartingOta.value = false;
|
||||||
|
otaVisible.value = false;
|
||||||
|
uni.navigateTo({ url: getOtaWifiUrl() });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 OTA 更新成功后的完成按钮,关闭结果弹窗。
|
||||||
const handleOtaDone = () => {
|
const handleOtaDone = () => {
|
||||||
otaVisible.value = false;
|
otaVisible.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理 OTA 更新失败后的重试按钮,重新走立即更新判断流程。
|
||||||
const handleOtaRetry = () => {
|
const handleOtaRetry = () => {
|
||||||
otaVisible.value = false;
|
handleOtaUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提取积分榜接口返回的榜单数组,兼容数组和对象两种返回格式。
|
// 提取积分榜接口返回的榜单数组,兼容数组和对象两种返回格式。
|
||||||
@@ -103,17 +261,17 @@ const toRankListPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onShow(async (options) => {
|
onShow(async (options) => {
|
||||||
|
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
||||||
|
const token = uni.getStorageSync(`${env}_token`);
|
||||||
|
|
||||||
// 检查是否从 OTA 更新页面返回
|
// 检查是否从 OTA 更新页面返回
|
||||||
if (options && options.updateResult) {
|
if (options && options.updateResult) {
|
||||||
otaState.value = options.updateResult;
|
otaState.value = options.updateResult;
|
||||||
otaVisible.value = true;
|
otaVisible.value = true;
|
||||||
} else {
|
} else if (token || user.value.id) {
|
||||||
checkOtaUpdate();
|
await checkOtaUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
|
||||||
const token = uni.getStorageSync(`${env}_token`);
|
|
||||||
|
|
||||||
if (!user.value.id && !token) {
|
if (!user.value.id && !token) {
|
||||||
// showModal.value = true;
|
// showModal.value = true;
|
||||||
// try {
|
// try {
|
||||||
@@ -185,6 +343,10 @@ onMounted(async () => {
|
|||||||
console.log("全局配置:", config);
|
console.log("全局配置:", config);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearOtaUpdateTimers();
|
||||||
|
});
|
||||||
|
|
||||||
onShareAppMessage(() => {
|
onShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
|
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
|
||||||
@@ -209,10 +371,11 @@ onShareTimeline(() => {
|
|||||||
<OtaModal
|
<OtaModal
|
||||||
:visible="otaVisible"
|
:visible="otaVisible"
|
||||||
:state="otaState"
|
:state="otaState"
|
||||||
:version="OTA_MOCK.version"
|
:version="otaInfo.versionNumber"
|
||||||
:description="OTA_MOCK.description"
|
:progress="otaProgress"
|
||||||
:changelog="OTA_MOCK.details"
|
:description="''"
|
||||||
:forceUpdate="OTA_MOCK.forceUpdate"
|
:changelog="otaInfo.versionInfo"
|
||||||
|
:forceUpdate="otaInfo.forceUpdate"
|
||||||
@update="handleOtaUpdate"
|
@update="handleOtaUpdate"
|
||||||
@skip="handleOtaDismiss"
|
@skip="handleOtaDismiss"
|
||||||
@close="handleOtaDismiss"
|
@close="handleOtaDismiss"
|
||||||
|
|||||||
+292
-29
@@ -1,8 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import ScreenHint from "@/components/ScreenHint.vue";
|
import ScreenHint from "@/components/ScreenHint.vue";
|
||||||
|
import {
|
||||||
|
connectDeviceWifiAPI,
|
||||||
|
getDeviceBatteryAPI,
|
||||||
|
getHardwareBoxTaskStatusAPI,
|
||||||
|
getHardwareBoxVersionAPI,
|
||||||
|
sendHardwareBoxUpdateAPI,
|
||||||
|
} from "@/apis";
|
||||||
|
|
||||||
const STATES = {
|
const STATES = {
|
||||||
SCANNING: "SCANNING",
|
SCANNING: "SCANNING",
|
||||||
@@ -24,15 +31,29 @@ const connectingWifi = ref(null);
|
|||||||
const connectInput = ref({ ssid: "", password: "" });
|
const connectInput = ref({ ssid: "", password: "" });
|
||||||
const connectMode = ref("secure"); // secure | open | manual
|
const connectMode = ref("secure"); // secure | open | manual
|
||||||
const connectError = ref("");
|
const connectError = ref("");
|
||||||
|
const connectStatusText = ref("");
|
||||||
|
const isSubmittingWifi = ref(false);
|
||||||
const keyboardHeight = ref(0);
|
const keyboardHeight = ref(0);
|
||||||
// 控制密码输入框是否显示明文
|
// 控制密码输入框是否显示明文
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
// 刷新防抖标志:扫描进行中为 true,禁止重复点击;扫描结束(成功/失败)后重置为 false。
|
// 刷新防抖标志:扫描进行中为 true,禁止重复点击;扫描结束(成功/失败)后重置为 false。
|
||||||
const isRefreshing = ref(false);
|
const isRefreshing = ref(false);
|
||||||
|
const isStartingUpdate = ref(false);
|
||||||
|
const routeOtaInfo = ref({
|
||||||
|
versionNumber: "",
|
||||||
|
resourceUrl: "",
|
||||||
|
});
|
||||||
|
|
||||||
const progress = ref(0);
|
const progress = ref(0);
|
||||||
let progressTimer = null;
|
let progressTimer = null;
|
||||||
let timeoutTimer = null;
|
let timeoutTimer = null;
|
||||||
|
let statusTimer = null;
|
||||||
|
let wifiConnectTimer = null;
|
||||||
|
let wifiConnectRequestId = 0;
|
||||||
|
let wifiConnectPollCount = 0;
|
||||||
|
const WIFI_CONNECT_POLL_INTERVAL = 2000;
|
||||||
|
const WIFI_CONNECT_MAX_POLL_COUNT = 15;
|
||||||
|
const WIFI_CONNECT_FAILED_TEXT = "连接失败,请检查WiFi密码或WiFi状态";
|
||||||
// 控制授权拒绝弹窗显示/隐藏
|
// 控制授权拒绝弹窗显示/隐藏
|
||||||
const wifiAuthDeniedVisible = ref(false);
|
const wifiAuthDeniedVisible = ref(false);
|
||||||
|
|
||||||
@@ -182,7 +203,9 @@ const startScanning = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 选择列表中的 WiFi,并打开密码输入弹窗。
|
||||||
const selectWifi = (wifi) => {
|
const selectWifi = (wifi) => {
|
||||||
|
cancelWifiConnectPolling();
|
||||||
connectingWifi.value = wifi;
|
connectingWifi.value = wifi;
|
||||||
connectInput.value = { ssid: wifi.SSID, password: "" };
|
connectInput.value = { ssid: wifi.SSID, password: "" };
|
||||||
connectMode.value = wifi.secure ? "secure" : "open";
|
connectMode.value = wifi.secure ? "secure" : "open";
|
||||||
@@ -190,7 +213,9 @@ const selectWifi = (wifi) => {
|
|||||||
currentState.value = STATES.CONNECTING;
|
currentState.value = STATES.CONNECTING;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 选择手动输入 WiFi,并打开手动输入弹窗。
|
||||||
const selectOther = () => {
|
const selectOther = () => {
|
||||||
|
cancelWifiConnectPolling();
|
||||||
connectingWifi.value = null;
|
connectingWifi.value = null;
|
||||||
connectInput.value = { ssid: "", password: "" };
|
connectInput.value = { ssid: "", password: "" };
|
||||||
connectMode.value = "manual";
|
connectMode.value = "manual";
|
||||||
@@ -198,7 +223,9 @@ const selectOther = () => {
|
|||||||
currentState.value = STATES.CONNECTING;
|
currentState.value = STATES.CONNECTING;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 关闭连接弹窗,并停止当前 WiFi 连接轮询。
|
||||||
const closeConnectSheet = () => {
|
const closeConnectSheet = () => {
|
||||||
|
cancelWifiConnectPolling();
|
||||||
connectError.value = "";
|
connectError.value = "";
|
||||||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
||||||
};
|
};
|
||||||
@@ -212,7 +239,9 @@ const ssidWarning = computed(() => {
|
|||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 判断当前是否禁止点击加入按钮,提交中也禁止重复点击。
|
||||||
const joinDisabled = computed(() => {
|
const joinDisabled = computed(() => {
|
||||||
|
if (isSubmittingWifi.value) return true;
|
||||||
if (connectMode.value === "secure") return !connectInput.value.password;
|
if (connectMode.value === "secure") return !connectInput.value.password;
|
||||||
if (connectMode.value === "manual") return !connectInput.value.ssid;
|
if (connectMode.value === "manual") return !connectInput.value.ssid;
|
||||||
return false;
|
return false;
|
||||||
@@ -226,17 +255,107 @@ const wifiListScrollHeight = computed(() => {
|
|||||||
return `${Math.min(itemCount * 92, maxHeight)}rpx`;
|
return `${Math.min(itemCount * 92, maxHeight)}rpx`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 提交 WiFi 配置给游戏设备;后端接口未接入前只提示占位信息并停留在弹窗。
|
// 清理 WiFi 连接轮询定时器。
|
||||||
const submitDeviceWifiConfig = ({ ssid, password }) => {
|
const clearWifiConnectTimer = () => {
|
||||||
console.log("[OTA WiFi] submit device wifi config pending:", {
|
clearTimeout(wifiConnectTimer);
|
||||||
ssid,
|
wifiConnectTimer = null;
|
||||||
hasPassword: !!password,
|
wifiConnectPollCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消当前 WiFi 连接轮询,并恢复弹窗提交状态。
|
||||||
|
const cancelWifiConnectPolling = () => {
|
||||||
|
wifiConnectRequestId += 1;
|
||||||
|
clearWifiConnectTimer();
|
||||||
|
connectStatusText.value = "";
|
||||||
|
isSubmittingWifi.value = false;
|
||||||
|
uni.hideLoading();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断设备是否已经在线且连接到 WiFi。
|
||||||
|
const isDeviceConnectedByWifi = (deviceStatus) => {
|
||||||
|
return deviceStatus?.online === true && String(deviceStatus?.netType || "").toLowerCase() === "wifi";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 轮询设备电量接口,确认设备已经切换到 WiFi 在线状态。
|
||||||
|
const waitForDeviceWifiConnected = (requestId) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const poll = async () => {
|
||||||
|
if (requestId !== wifiConnectRequestId) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wifiConnectPollCount += 1;
|
||||||
|
try {
|
||||||
|
const deviceStatus = await getDeviceBatteryAPI();
|
||||||
|
if (requestId !== wifiConnectRequestId) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isDeviceConnectedByWifi(deviceStatus)) {
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (requestId !== wifiConnectRequestId) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wifiConnectPollCount >= WIFI_CONNECT_MAX_POLL_COUNT) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wifiConnectTimer = setTimeout(poll, WIFI_CONNECT_POLL_INTERVAL);
|
||||||
|
};
|
||||||
|
|
||||||
|
poll();
|
||||||
});
|
});
|
||||||
connectError.value = "设备连接接口待接入";
|
};
|
||||||
uni.showToast({
|
|
||||||
title: "设备连接接口待接入",
|
// 提交 WiFi 配置给游戏设备,并轮询确认设备真实连上 WiFi 后再展示成功。
|
||||||
icon: "none",
|
const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
||||||
|
if (isSubmittingWifi.value) return;
|
||||||
|
wifiConnectRequestId += 1;
|
||||||
|
const requestId = wifiConnectRequestId;
|
||||||
|
clearWifiConnectTimer();
|
||||||
|
isSubmittingWifi.value = true;
|
||||||
|
connectStatusText.value = "WiFi连接中...";
|
||||||
|
uni.showLoading({
|
||||||
|
title: "WiFi连接中",
|
||||||
|
mask: true,
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
await connectDeviceWifiAPI(ssid, password);
|
||||||
|
const isConnected = await waitForDeviceWifiConnected(requestId);
|
||||||
|
if (requestId !== wifiConnectRequestId) return;
|
||||||
|
if (!isConnected) {
|
||||||
|
connectError.value = WIFI_CONNECT_FAILED_TEXT;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connectedWifi.value = {
|
||||||
|
...(connectingWifi.value || {}),
|
||||||
|
SSID: ssid,
|
||||||
|
password,
|
||||||
|
secure: connectMode.value === "secure" || !!password,
|
||||||
|
};
|
||||||
|
connectError.value = "";
|
||||||
|
currentState.value = STATES.CONNECTED;
|
||||||
|
} catch (err) {
|
||||||
|
if (requestId === wifiConnectRequestId) {
|
||||||
|
connectError.value =
|
||||||
|
err?.code === -1 && err?.message ? err.message : WIFI_CONNECT_FAILED_TEXT;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === wifiConnectRequestId) {
|
||||||
|
clearWifiConnectTimer();
|
||||||
|
connectStatusText.value = "";
|
||||||
|
isSubmittingWifi.value = false;
|
||||||
|
uni.hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 校验用户输入并提交 WiFi 配置,不再把手机连接结果当作设备连接成功。
|
// 校验用户输入并提交 WiFi 配置,不再把手机连接结果当作设备连接成功。
|
||||||
@@ -249,9 +368,18 @@ const joinNetwork = () => {
|
|||||||
submitDeviceWifiConfig({ ssid, password });
|
submitDeviceWifiConfig({ ssid, password });
|
||||||
};
|
};
|
||||||
|
|
||||||
const startUpdate = () => {
|
// 清理 OTA 更新相关定时器,避免页面退出或状态结束后继续执行。
|
||||||
currentState.value = STATES.UPDATING;
|
const clearUpdateTimers = () => {
|
||||||
progress.value = 0;
|
clearInterval(progressTimer);
|
||||||
|
clearTimeout(timeoutTimer);
|
||||||
|
clearTimeout(statusTimer);
|
||||||
|
progressTimer = null;
|
||||||
|
timeoutTimer = null;
|
||||||
|
statusTimer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 启动本地进度条动画,真实完成状态以后端任务轮询结果为准。
|
||||||
|
const startProgressAnimation = () => {
|
||||||
clearInterval(progressTimer);
|
clearInterval(progressTimer);
|
||||||
progressTimer = setInterval(() => {
|
progressTimer = setInterval(() => {
|
||||||
if (progress.value >= 90) {
|
if (progress.value >= 90) {
|
||||||
@@ -261,28 +389,144 @@ const startUpdate = () => {
|
|||||||
const increment = Math.max(0.5, 2 - progress.value / 60);
|
const increment = Math.max(0.5, 2 - progress.value / 60);
|
||||||
progress.value = Math.min(90, progress.value + increment);
|
progress.value = Math.min(90, progress.value + increment);
|
||||||
}, 500);
|
}, 500);
|
||||||
clearTimeout(timeoutTimer);
|
|
||||||
timeoutTimer = setTimeout(() => {
|
|
||||||
if (currentState.value === STATES.UPDATING) {
|
|
||||||
clearInterval(progressTimer);
|
|
||||||
currentState.value = STATES.FAILED;
|
|
||||||
}
|
|
||||||
}, 5 * 60 * 1000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWsDone = () => {
|
// 将 OTA 更新流程标记为失败并切换到失败页面。
|
||||||
clearInterval(progressTimer);
|
const failUpdate = () => {
|
||||||
clearTimeout(timeoutTimer);
|
clearUpdateTimers();
|
||||||
|
isStartingUpdate.value = false;
|
||||||
|
currentState.value = STATES.FAILED;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将 OTA 更新流程标记为成功,进度补到 100% 后进入完成页面。
|
||||||
|
const completeUpdate = () => {
|
||||||
|
clearUpdateTimers();
|
||||||
|
isStartingUpdate.value = false;
|
||||||
progress.value = 100;
|
progress.value = 100;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
currentState.value = STATES.DONE;
|
currentState.value = STATES.DONE;
|
||||||
}, 300);
|
}, 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 (Number(deviceStatus?.battery) <= 20) return "设备电量不足,请充电至 20% 以上后再更新";
|
||||||
|
if (String(deviceStatus?.netType || "").toLowerCase() !== "wifi") return "设备当前未连接 WiFi,请先连接 WiFi 后再更新";
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取 OTA 更新版本信息,优先使用首页跳转传入的数据,没有传参时再请求后端版本接口。
|
||||||
|
const getOtaVersionInfo = async () => {
|
||||||
|
if (routeOtaInfo.value.versionNumber && routeOtaInfo.value.resourceUrl) {
|
||||||
|
return {
|
||||||
|
needUpdate: true,
|
||||||
|
versionNumber: routeOtaInfo.value.versionNumber,
|
||||||
|
resourceUrl: routeOtaInfo.value.resourceUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return getHardwareBoxVersionAPI();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击开始更新时先判断设备状态和版本信息,满足条件才发送 OTA 指令并开始轮询任务状态。
|
||||||
|
const startUpdate = async () => {
|
||||||
|
if (isStartingUpdate.value) return;
|
||||||
|
if (!connectedWifi.value) return;
|
||||||
|
isStartingUpdate.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deviceStatus = await getDeviceBatteryAPI();
|
||||||
|
const disabledReason = getUpdateDisabledReason(deviceStatus);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!versionInfo?.needUpdate) {
|
||||||
|
isStartingUpdate.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: "当前已是最新版本",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState.value = STATES.UPDATING;
|
||||||
|
progress.value = 0;
|
||||||
|
startProgressAnimation();
|
||||||
|
timeoutTimer = setTimeout(() => {
|
||||||
|
if (currentState.value === STATES.UPDATING) {
|
||||||
|
failUpdate();
|
||||||
|
}
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
const updateResult = await sendHardwareBoxUpdateAPI({
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// WebSocket 成功回调保留兜底能力,触发后直接按更新完成处理。
|
||||||
|
const handleWsDone = () => {
|
||||||
|
completeUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
// WebSocket 失败回调保留兜底能力,触发后直接按更新失败处理。
|
||||||
const handleWsFail = () => {
|
const handleWsFail = () => {
|
||||||
clearInterval(progressTimer);
|
failUpdate();
|
||||||
clearTimeout(timeoutTimer);
|
|
||||||
currentState.value = STATES.FAILED;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDone = () => {
|
const handleDone = () => {
|
||||||
@@ -322,6 +566,14 @@ const togglePasswordVisibility = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 页面加载时接收首页传入的 OTA 版本号和固件地址。
|
||||||
|
onLoad((options = {}) => {
|
||||||
|
routeOtaInfo.value = {
|
||||||
|
versionNumber: decodeURIComponent(options.versionNumber || ""),
|
||||||
|
resourceUrl: decodeURIComponent(options.resourceUrl || ""),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// 页面挂载后启动 WiFi 扫描流程。
|
// 页面挂载后启动 WiFi 扫描流程。
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (typeof uni.onKeyboardHeightChange === "function") {
|
if (typeof uni.onKeyboardHeightChange === "function") {
|
||||||
@@ -341,8 +593,8 @@ onUnmounted(() => {
|
|||||||
if (typeof uni.offKeyboardHeightChange === "function") {
|
if (typeof uni.offKeyboardHeightChange === "function") {
|
||||||
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
|
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
|
||||||
}
|
}
|
||||||
clearInterval(progressTimer);
|
cancelWifiConnectPolling();
|
||||||
clearTimeout(timeoutTimer);
|
clearUpdateTimers();
|
||||||
wx.offGetWifiList && wx.offGetWifiList();
|
wx.offGetWifiList && wx.offGetWifiList();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -524,6 +776,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<text v-if="connectStatusText" class="connect-status">{{ connectStatusText }}</text>
|
||||||
<text v-if="connectError" class="connect-error">{{ connectError }}</text>
|
<text v-if="connectError" class="connect-error">{{ connectError }}</text>
|
||||||
</block>
|
</block>
|
||||||
|
|
||||||
@@ -538,7 +791,8 @@ onUnmounted(() => {
|
|||||||
<image src="../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
<image src="../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="sheet-hint">该网络为开放网络,点击 ✓ 加入</text>
|
<text v-if="connectStatusText" class="connect-status">{{ connectStatusText }}</text>
|
||||||
|
<text v-else class="sheet-hint">该网络为开放网络,点击 ✓ 加入</text>
|
||||||
<text v-if="connectError" class="connect-error">{{ connectError }}</text>
|
<text v-if="connectError" class="connect-error">{{ connectError }}</text>
|
||||||
</block>
|
</block>
|
||||||
|
|
||||||
@@ -588,6 +842,7 @@ onUnmounted(() => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text v-if="ssidWarning" class="connect-error">{{ ssidWarning }}</text>
|
<text v-if="ssidWarning" class="connect-error">{{ ssidWarning }}</text>
|
||||||
|
<text v-else-if="connectStatusText" class="connect-status">{{ connectStatusText }}</text>
|
||||||
<text v-else-if="connectError" class="connect-error">{{ connectError }}</text>
|
<text v-else-if="connectError" class="connect-error">{{ connectError }}</text>
|
||||||
</block>
|
</block>
|
||||||
|
|
||||||
@@ -927,6 +1182,14 @@ onUnmounted(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 24rpx;
|
margin-bottom: 24rpx;
|
||||||
}
|
}
|
||||||
|
.connect-status {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
.connect-error {
|
.connect-error {
|
||||||
color: rgba(254, 216, 71, 1);
|
color: rgba(254, 216, 71, 1);
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
|
|||||||
Reference in New Issue
Block a user