1173 lines
34 KiB
Vue
1173 lines
34 KiB
Vue
<script setup>
|
||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||
import { storeToRefs } from "pinia";
|
||
import Container from "@/components/Container.vue";
|
||
import ModalDialog from "@/components/ModalDialog.vue";
|
||
import OtaModal from "@/components/OtaModal.vue";
|
||
import useStore from "@/store";
|
||
import { capsuleHeight } from "@/util";
|
||
import {
|
||
connectDeviceWifiAPI,
|
||
getDeviceDetailAPI,
|
||
getDeviceWifiStatusAPI,
|
||
} from "@/apis";
|
||
import { getOtaInfoFromDetail, useOtaUpdate } from "@/composables/useOtaUpdate";
|
||
|
||
const STATES = {
|
||
SCANNING: "SCANNING",
|
||
LIST: "LIST",
|
||
CONNECTING: "CONNECTING",
|
||
CONNECTED: "CONNECTED",
|
||
LOCATION_REQUIRED: "LOCATION_REQUIRED",
|
||
};
|
||
|
||
const isIOS = uni.getDeviceInfo().osName === "ios";
|
||
const { device, deviceStatus } = storeToRefs(useStore());
|
||
const currentState = ref(STATES.SCANNING);
|
||
const connectedWifi = ref(null);
|
||
const connectedWifiName = ref("");
|
||
const isDeviceOnWifi = computed(
|
||
() => deviceStatus.value.online === true && deviceStatus.value.netType === "wifi"
|
||
);
|
||
const wifiList = ref([]);
|
||
const showWifiBanner = ref(false);
|
||
|
||
const connectingWifi = ref(null);
|
||
const connectInput = ref({ ssid: "", password: "" });
|
||
const connectMode = ref("secure"); // secure | open | manual
|
||
const connectError = ref("");
|
||
const connectStatusText = ref("");
|
||
const isSubmittingWifi = ref(false);
|
||
const keyboardHeight = ref(0);
|
||
// 控制密码输入框是否显示明文
|
||
const showPassword = ref(false);
|
||
// 刷新防抖标志:扫描进行中为 true,禁止重复点击;扫描结束(成功/失败)后重置为 false。
|
||
const isRefreshing = ref(false);
|
||
const fromFirmwareUpdate = ref(false);
|
||
const routeOtaVersionNumber = ref("");
|
||
const countdownVisible = ref(false);
|
||
const countdownSeconds = ref(3);
|
||
const firmwareMessageVisible = ref(false);
|
||
const firmwareMessage = ref("");
|
||
let countdownTimer = null;
|
||
let wifiConnectRequestId = 0;
|
||
let wifiStatusRequestId = 0;
|
||
const WIFI_CONNECT_FAILED_TEXT = "连接失败,请检查WiFi密码或WiFi状态";
|
||
// 与 Container 的可滚动区域保持一致,底部说明在长短屏上都能贴近安全区。
|
||
const windowInfo = uni.getWindowInfo();
|
||
const contentMinHeight = Math.max(
|
||
0,
|
||
Math.round(
|
||
(windowInfo.windowHeight - capsuleHeight - 50) *
|
||
(750 / windowInfo.windowWidth)
|
||
)
|
||
);
|
||
|
||
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;
|
||
const errMsg = err?.errMsg || "";
|
||
return /auth den|authorize|permission denied|user denied|scope\.userLocation/i.test(errMsg);
|
||
};
|
||
|
||
// Android 定位不可用时展示蓝湖中的独立引导状态。
|
||
const showLocationPermission = () => {
|
||
currentState.value = STATES.LOCATION_REQUIRED;
|
||
isRefreshing.value = false;
|
||
};
|
||
|
||
// 已拒绝授权时进入小程序设置;首次授权则重新扫描以触发系统权限流程。
|
||
const handleReauthorize = () => {
|
||
if (uni.getSystemSetting().locationEnabled === false) {
|
||
uni.showToast({ title: "请先开启手机定位服务", icon: "none" });
|
||
return;
|
||
}
|
||
uni.getSetting({
|
||
success(res) {
|
||
if (res.authSetting["scope.userLocation"] !== false) {
|
||
startScanning();
|
||
return;
|
||
}
|
||
uni.openSetting({
|
||
success(setting) {
|
||
if (setting.authSetting["scope.userLocation"]) startScanning();
|
||
},
|
||
});
|
||
},
|
||
});
|
||
};
|
||
|
||
// 将 iOS 当前手机连接的 WiFi 写入候选列表,供用户继续填写设备连接密码;扫描结束后重置刷新防抖标志。
|
||
const applyIOSConnectedWifi = (wifi) => {
|
||
const ssid = wifi?.SSID;
|
||
if (!ssid) {
|
||
wifiList.value = [];
|
||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
||
isRefreshing.value = false;
|
||
return;
|
||
}
|
||
|
||
wifiList.value = [
|
||
{
|
||
...wifi,
|
||
SSID: ssid,
|
||
secure: wifi.secure !== false,
|
||
fromCurrentWifi: true,
|
||
},
|
||
];
|
||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
||
isRefreshing.value = false;
|
||
};
|
||
|
||
// 获取 iOS 当前手机连接的 WiFi,并把它作为可选网络展示。
|
||
const syncIOSConnectedWifi = () => {
|
||
wx.getConnectedWifi({
|
||
success(res) {
|
||
console.log("[OTA WiFi] wx.getConnectedWifi success:", res);
|
||
applyIOSConnectedWifi(res.wifi || res);
|
||
},
|
||
fail(err) {
|
||
console.warn("[OTA WiFi] wx.getConnectedWifi fail:", {
|
||
errCode: err?.errCode,
|
||
errMsg: err?.errMsg,
|
||
err,
|
||
});
|
||
wifiList.value = [];
|
||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
||
isRefreshing.value = false;
|
||
},
|
||
});
|
||
};
|
||
|
||
// 启动 iOS WiFi 能力;iOS 只用当前手机连接的 WiFi 作为候选网络。
|
||
const startIOSScanning = () => {
|
||
wx.startWifi({
|
||
success(res) {
|
||
console.log("[OTA WiFi] wx.startWifi success:", res);
|
||
syncIOSConnectedWifi();
|
||
},
|
||
fail(err) {
|
||
console.error("[OTA WiFi] wx.startWifi fail:", {
|
||
errCode: err?.errCode,
|
||
errMsg: err?.errMsg,
|
||
err,
|
||
});
|
||
if (err.errCode === 12005) showWifiBanner.value = true;
|
||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
||
isRefreshing.value = false;
|
||
},
|
||
});
|
||
};
|
||
|
||
// 启动小程序 WiFi 能力并按平台获取可用网络;也作为刷新按钮的点击处理器,重新获取最新 WiFi 列表。
|
||
// 扫描进行中时防抖:若上次扫描尚未完成则直接 return,避免重复触发导致弹窗异常。
|
||
const startScanning = () => {
|
||
if (isRefreshing.value) return;
|
||
isRefreshing.value = true;
|
||
currentState.value = STATES.SCANNING;
|
||
wifiList.value = [];
|
||
showWifiBanner.value = false;
|
||
connectError.value = "";
|
||
|
||
if (isIOS) {
|
||
startIOSScanning();
|
||
return;
|
||
}
|
||
|
||
if (uni.getSystemSetting().locationEnabled === false) {
|
||
showLocationPermission();
|
||
return;
|
||
}
|
||
|
||
wx.startWifi({
|
||
success(res) {
|
||
console.log("[OTA WiFi] wx.startWifi success:", res);
|
||
wx.getWifiList({
|
||
success(res) {
|
||
console.log("[OTA WiFi] wx.getWifiList request success:", res);
|
||
},
|
||
fail(err) {
|
||
console.error("[OTA WiFi] wx.getWifiList fail:", {
|
||
errCode: err?.errCode,
|
||
errMsg: err?.errMsg,
|
||
err,
|
||
});
|
||
if (isWifiPermissionDenied(err)) {
|
||
showLocationPermission();
|
||
} else {
|
||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
||
}
|
||
if (err.errCode === 12005) showWifiBanner.value = true;
|
||
isRefreshing.value = false;
|
||
},
|
||
});
|
||
},
|
||
fail(err) {
|
||
console.error("[OTA WiFi] wx.startWifi fail:", {
|
||
errCode: err?.errCode,
|
||
errMsg: err?.errMsg,
|
||
err,
|
||
});
|
||
if (isWifiPermissionDenied(err)) {
|
||
showLocationPermission();
|
||
} else {
|
||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
||
}
|
||
if (err.errCode === 12005) showWifiBanner.value = true;
|
||
isRefreshing.value = false;
|
||
},
|
||
});
|
||
|
||
wx.onGetWifiList((res) => {
|
||
if (currentState.value === STATES.LOCATION_REQUIRED) return;
|
||
const realWifiList = res.wifiList || [];
|
||
if (realWifiList.length) {
|
||
console.log("[OTA WiFi] wx.onGetWifiList received wifiList:", realWifiList);
|
||
} else {
|
||
console.warn("[OTA WiFi] wx.onGetWifiList received empty wifiList:", res);
|
||
}
|
||
wifiList.value = realWifiList;
|
||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
||
isRefreshing.value = false;
|
||
});
|
||
};
|
||
|
||
// 选择列表中的 WiFi,并打开密码输入弹窗。
|
||
const selectWifi = (wifi) => {
|
||
cancelWifiConnectWaiting();
|
||
connectingWifi.value = wifi;
|
||
connectInput.value = { ssid: wifi.SSID, password: "" };
|
||
connectMode.value = wifi.secure ? "secure" : "open";
|
||
connectError.value = "";
|
||
currentState.value = STATES.CONNECTING;
|
||
};
|
||
|
||
// 选择手动输入 WiFi,并打开手动输入弹窗。
|
||
const selectOther = () => {
|
||
cancelWifiConnectWaiting();
|
||
connectingWifi.value = null;
|
||
connectInput.value = { ssid: "", password: "" };
|
||
connectMode.value = "manual";
|
||
connectError.value = "";
|
||
currentState.value = STATES.CONNECTING;
|
||
};
|
||
|
||
// 关闭连接弹窗,并停止等待当前 WiFi 连接结果。
|
||
const closeConnectSheet = () => {
|
||
cancelWifiConnectWaiting();
|
||
connectError.value = "";
|
||
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
|
||
};
|
||
|
||
const hasChinese = (str) => /[\u4e00-\u9fa5]/.test(str);
|
||
|
||
const ssidWarning = computed(() => {
|
||
if (connectMode.value === "manual" && hasChinese(connectInput.value.ssid)) {
|
||
return "网络名称仅支持英文字符及数字,请连接英文名网络或把网络改为英文";
|
||
}
|
||
return "";
|
||
});
|
||
|
||
// 判断当前是否禁止点击加入按钮,提交中也禁止重复点击。
|
||
const joinDisabled = computed(() => {
|
||
if (isSubmittingWifi.value) return true;
|
||
if (connectMode.value === "secure") return !connectInput.value.password;
|
||
if (connectMode.value === "manual") return !connectInput.value.ssid;
|
||
return false;
|
||
});
|
||
|
||
// 列表按实际条目收起;空间不足时由页面布局限制高度并在卡片内滚动。
|
||
const wifiListScrollHeight = computed(() => {
|
||
const itemCount = wifiList.value.length + 1;
|
||
return `${itemCount * 108}rpx`;
|
||
});
|
||
|
||
// 展示条件只取 WebSocket 设备状态;接口仅提供名称,过期请求不得覆盖新状态。
|
||
const refreshConnectedWifiName = async () => {
|
||
const requestId = ++wifiStatusRequestId;
|
||
if (!isDeviceOnWifi.value) {
|
||
connectedWifiName.value = "";
|
||
return;
|
||
}
|
||
connectedWifiName.value = "";
|
||
try {
|
||
const status = await getDeviceWifiStatusAPI();
|
||
if (requestId !== wifiStatusRequestId || !isDeviceOnWifi.value) return;
|
||
connectedWifiName.value = String(status?.ssid ?? "").trim();
|
||
} catch (error) {
|
||
if (requestId === wifiStatusRequestId) connectedWifiName.value = "";
|
||
}
|
||
};
|
||
|
||
watch(isDeviceOnWifi, () => {
|
||
void refreshConnectedWifiName();
|
||
});
|
||
|
||
// 取消当前 WiFi 连接请求的页面等待状态,并忽略可能迟到的响应。
|
||
const cancelWifiConnectWaiting = () => {
|
||
wifiConnectRequestId += 1;
|
||
connectStatusText.value = "";
|
||
isSubmittingWifi.value = false;
|
||
uni.hideLoading();
|
||
};
|
||
|
||
// 把接口失败和传输异常转换为连接弹窗内的用户提示。
|
||
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;
|
||
};
|
||
|
||
// 提交 WiFi 配置,并以接口返回的设备最终连接结果更新页面。
|
||
const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
||
if (isSubmittingWifi.value) return;
|
||
wifiConnectRequestId += 1;
|
||
const requestId = wifiConnectRequestId;
|
||
isSubmittingWifi.value = true;
|
||
connectStatusText.value = "WiFi连接中...";
|
||
uni.showLoading({
|
||
title: "WiFi连接中",
|
||
mask: true,
|
||
});
|
||
try {
|
||
const connectResult = await connectDeviceWifiAPI(ssid, password);
|
||
if (requestId !== wifiConnectRequestId) return;
|
||
if (connectResult?.success !== true) {
|
||
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;
|
||
void refreshConnectedWifiName();
|
||
if (fromFirmwareUpdate.value) {
|
||
openFirmwareCountdown();
|
||
}
|
||
} catch (err) {
|
||
if (requestId === wifiConnectRequestId) {
|
||
connectError.value = getWifiConnectErrorText(err);
|
||
}
|
||
} finally {
|
||
if (requestId === wifiConnectRequestId) {
|
||
connectStatusText.value = "";
|
||
isSubmittingWifi.value = false;
|
||
uni.hideLoading();
|
||
}
|
||
}
|
||
};
|
||
|
||
// 校验用户输入并提交 WiFi 配置,不再把手机连接结果当作设备连接成功。
|
||
const joinNetwork = () => {
|
||
if (joinDisabled.value) return;
|
||
connectError.value = "";
|
||
const ssid = connectInput.value.ssid;
|
||
const password = connectInput.value.password;
|
||
|
||
submitDeviceWifiConfig({ ssid, password });
|
||
};
|
||
|
||
const startFirmwareUpdate = async () => {
|
||
if (!fromFirmwareUpdate.value || !connectedWifi.value || otaUpdating.value) return;
|
||
const deviceId = device.value.deviceId;
|
||
const wifi = connectedWifi.value;
|
||
if (!deviceId) return;
|
||
try {
|
||
const response = await getDeviceDetailAPI(deviceId);
|
||
const detail = response?.detail || response?.data?.detail;
|
||
if (!detail || device.value.deviceId !== deviceId) throw new Error("设备详情已失效");
|
||
const otaInfo = getOtaInfoFromDetail(detail);
|
||
if (!otaInfo.needUpdate) {
|
||
firmwareMessage.value = "当前已是最新版本";
|
||
firmwareMessageVisible.value = true;
|
||
return;
|
||
}
|
||
if (!otaInfo.versionNumber) {
|
||
firmwareMessage.value = "缺少固件目标版本,请返回后重试";
|
||
firmwareMessageVisible.value = true;
|
||
return;
|
||
}
|
||
// WiFi 连接可能耗时较长,以更新前的最新设备详情为准。
|
||
routeOtaVersionNumber.value = otaInfo.versionNumber;
|
||
} catch (error) {
|
||
firmwareMessage.value = "获取设备详情失败,请返回后重试";
|
||
firmwareMessageVisible.value = true;
|
||
return;
|
||
}
|
||
await startOtaUpdate({
|
||
versionNumber: routeOtaVersionNumber.value,
|
||
wifiSsid: wifi.SSID,
|
||
wifiPassword: wifi.password || "",
|
||
});
|
||
};
|
||
|
||
const clearFirmwareCountdown = () => {
|
||
clearInterval(countdownTimer);
|
||
countdownTimer = null;
|
||
};
|
||
|
||
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);
|
||
};
|
||
|
||
// 监听系统输入法高度,用于让底部弹窗避开键盘遮挡。
|
||
const handleKeyboardHeightChange = (res) => {
|
||
keyboardHeight.value = Math.round(
|
||
(res?.height || 0) * (750 / windowInfo.windowWidth)
|
||
);
|
||
};
|
||
|
||
// 切换密码显示状态前先收起键盘,避免安全键盘和普通输入法切换时复用旧高度产生黑条。
|
||
const togglePasswordVisibility = () => {
|
||
uni.hideKeyboard({
|
||
complete() {
|
||
keyboardHeight.value = 0;
|
||
showPassword.value = !showPassword.value;
|
||
},
|
||
});
|
||
};
|
||
|
||
// 页面加载时识别普通 WiFi 入口和固件更新入口。
|
||
onLoad((options = {}) => {
|
||
fromFirmwareUpdate.value = options.source === "firmware-update";
|
||
routeOtaVersionNumber.value = decodeURIComponent(options.versionNumber || "");
|
||
});
|
||
|
||
// 页面挂载后启动 WiFi 扫描流程。
|
||
onMounted(() => {
|
||
if (typeof uni.onKeyboardHeightChange === "function") {
|
||
uni.onKeyboardHeightChange(handleKeyboardHeightChange);
|
||
}
|
||
startScanning();
|
||
});
|
||
|
||
// iOS 从系统 WiFi 设置返回小程序后,同步当前手机连接的 WiFi。
|
||
onShow(() => {
|
||
void refreshConnectedWifiName();
|
||
if (isIOS && currentState.value === STATES.LIST) {
|
||
syncIOSConnectedWifi();
|
||
} else if (
|
||
!isIOS &&
|
||
currentState.value === STATES.LOCATION_REQUIRED &&
|
||
uni.getSystemSetting().locationEnabled !== false
|
||
) {
|
||
uni.getSetting({
|
||
success(res) {
|
||
if (res.authSetting["scope.userLocation"] !== false) startScanning();
|
||
},
|
||
});
|
||
}
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
wifiStatusRequestId += 1;
|
||
if (typeof uni.offKeyboardHeightChange === "function") {
|
||
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
|
||
}
|
||
cancelWifiConnectWaiting();
|
||
clearFirmwareCountdown();
|
||
wx.offGetWifiList && wx.offGetWifiList();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<Container :scroll="false">
|
||
<!-- WiFi 不可用 Banner -->
|
||
<view v-if="showWifiBanner" class="wifi-banner">
|
||
<text class="wifi-banner-text">请先开启 WiFi 后刷新重试</text>
|
||
</view>
|
||
|
||
<!-- 扫描、候选网络与设备已连接网络共用页面主体 -->
|
||
<view
|
||
v-if="currentState === 'SCANNING' || currentState === 'LIST' || currentState === 'CONNECTED'"
|
||
class="wifi-page"
|
||
:style="{ height: contentMinHeight + 'rpx' }"
|
||
>
|
||
<view class="hero-area">
|
||
<image
|
||
class="hero-image"
|
||
src="https://static.shelingxingqiu.com/shootmini/static/ota/wifi1.png"
|
||
mode="aspectFit"
|
||
/>
|
||
</view>
|
||
<text class="page-title">智能弓箭连接无线网络</text>
|
||
<text class="page-subtitle">提醒:本操作将为智能弓箭连接WIFI网络,不会影响手机自身连接的WIFI网络。</text>
|
||
|
||
<view class="network-sections">
|
||
<!-- 仅设备 WebSocket 状态为 WiFi 时显示;此卡片不触发密码输入。 -->
|
||
<view v-if="isDeviceOnWifi && connectedWifiName" class="network-section connected-section">
|
||
<text class="section-label">已连接</text>
|
||
<view class="connected-wifi-card">
|
||
<text class="wifi-ssid">{{ connectedWifiName }}</text>
|
||
<image
|
||
class="check-icon"
|
||
src="https://static.shelingxingqiu.com/shootmini/static/sicon/check.png"
|
||
mode="aspectFit"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="network-section">
|
||
<view class="section-label-row">
|
||
<text class="section-label">选择网络</text>
|
||
<image
|
||
class="refresh-icon"
|
||
src="https://static.shelingxingqiu.com/shootmini/static/sicon/refresh.png"
|
||
mode="aspectFit"
|
||
:style="{ opacity: isRefreshing ? 0.3 : 0.6 }"
|
||
@click="startScanning"
|
||
/>
|
||
</view>
|
||
|
||
<block v-if="isIOS">
|
||
<view
|
||
v-if="wifiList.length"
|
||
class="ios-network-card current-network-card"
|
||
@click="selectWifi(wifiList[0])"
|
||
>
|
||
<view class="network-copy">
|
||
<text class="wifi-ssid">{{ wifiList[0].SSID }}</text>
|
||
<text class="network-desc">手机当前连接的网络</text>
|
||
</view>
|
||
<image
|
||
class="signal-icon"
|
||
src="https://static.shelingxingqiu.com/shootmini/static/sicon/wifi.png"
|
||
mode="aspectFit"
|
||
/>
|
||
</view>
|
||
<view class="ios-network-card manual-network-card" @click="selectOther">
|
||
<view class="network-copy">
|
||
<text class="wifi-ssid">手动连接其他网络</text>
|
||
<text class="network-desc">由于权限限制,无法获取附近网络列表,如需连接其他网络请手动输入网络名称与密码进行连接。</text>
|
||
</view>
|
||
<image
|
||
class="signal-icon"
|
||
src="https://static.shelingxingqiu.com/shootmini/static/sicon/wifi.png"
|
||
mode="aspectFit"
|
||
/>
|
||
</view>
|
||
</block>
|
||
|
||
<scroll-view
|
||
v-else
|
||
scroll-y
|
||
class="android-list-card"
|
||
:style="{ maxHeight: wifiListScrollHeight }"
|
||
>
|
||
<view
|
||
v-for="wifi in wifiList"
|
||
:key="wifi.SSID"
|
||
class="android-network-item"
|
||
@click="selectWifi(wifi)"
|
||
>
|
||
<text class="wifi-ssid">{{ wifi.SSID }}</text>
|
||
<view class="wifi-icons">
|
||
<image
|
||
v-if="wifi.secure"
|
||
class="security-icon"
|
||
src="https://static.shelingxingqiu.com/shootmini/static/sicon/pwd.png"
|
||
mode="aspectFit"
|
||
/>
|
||
<image
|
||
class="signal-icon"
|
||
src="https://static.shelingxingqiu.com/shootmini/static/sicon/wifi.png"
|
||
mode="aspectFit"
|
||
/>
|
||
</view>
|
||
</view>
|
||
<view class="android-network-item manual-network-item" @click="selectOther">
|
||
<text class="wifi-ssid">手动连接其他网络</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
|
||
<text class="wifi-footer">智能弓箭目前仅支持保存一条网络信息,若连接新的网络将删除上一条网络信息。</text>
|
||
</view>
|
||
|
||
<!-- Android 定位权限不足时的整页引导状态 -->
|
||
<view
|
||
v-if="currentState === 'LOCATION_REQUIRED'"
|
||
class="location-page"
|
||
:style="{ minHeight: contentMinHeight + 'rpx' }"
|
||
>
|
||
<view class="location-image-wrap">
|
||
<image
|
||
class="location-image"
|
||
src="https://static.shelingxingqiu.com/shootmini/static/device-assets/wifi-location.png"
|
||
mode="aspectFit"
|
||
/>
|
||
<view class="location-shadow"></view>
|
||
</view>
|
||
<text class="location-desc">请授权允许获取定位信息,并开启定位功能,以查找附近可用无线网络。</text>
|
||
<view class="location-button" @click="handleReauthorize">
|
||
<text>授权获取定位信息</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- CONNECTING 底部弹窗 -->
|
||
<view v-if="currentState === 'CONNECTING'" class="sheet-mask">
|
||
<view class="sheet" :style="{ marginBottom: keyboardHeight + 'rpx' }" @click.stop="">
|
||
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/ota-bg.png" mode="aspectFill" class="sheet-bg" />
|
||
<view class="sheet-inner">
|
||
|
||
<!-- 有密码网络 -->
|
||
<block v-if="connectMode === 'secure'">
|
||
<view class="sheet-header">
|
||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
||
<image src="https://static.shelingxingqiu.com/shootmini/static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||
</view>
|
||
<text class="sheet-title">加入"{{ connectInput.ssid }}"</text>
|
||
<view
|
||
class="sheet-nav-btn"
|
||
:class="{ 'nav-disabled': joinDisabled }"
|
||
@click="joinNetwork"
|
||
>
|
||
<image src="https://static.shelingxingqiu.com/shootmini/static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
||
</view>
|
||
</view>
|
||
<view class="input-row-card">
|
||
<text class="input-label">密码</text>
|
||
<input
|
||
class="input-field"
|
||
:password="!showPassword"
|
||
:adjust-position="false"
|
||
v-model="connectInput.password"
|
||
placeholder="输入网络密码"
|
||
placeholder-class="input-placeholder"
|
||
confirm-type="done"
|
||
@confirm="joinNetwork"
|
||
/>
|
||
<!-- 密码显示/隐藏切换按钮 -->
|
||
<view class="pwd-eye-btn" @click="togglePasswordVisibility">
|
||
<image
|
||
:src="showPassword ? 'https://static.shelingxingqiu.com/shootmini/static/sicon/eye-on.png' : 'https://static.shelingxingqiu.com/shootmini/static/sicon/eye-off.png'"
|
||
mode="aspectFit"
|
||
style="width: 40rpx; height: 40rpx;"
|
||
/>
|
||
</view>
|
||
</view>
|
||
<text v-if="connectStatusText" class="connect-status">{{ connectStatusText }}</text>
|
||
<text v-if="connectError" class="connect-error">{{ connectError }}</text>
|
||
</block>
|
||
|
||
<!-- 无密码网络 -->
|
||
<block v-else-if="connectMode === 'open'">
|
||
<view class="sheet-header">
|
||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
||
<image src="https://static.shelingxingqiu.com/shootmini/static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||
</view>
|
||
<text class="sheet-title">加入"{{ connectInput.ssid }}"</text>
|
||
<view class="sheet-nav-btn" @click="joinNetwork">
|
||
<image src="https://static.shelingxingqiu.com/shootmini/static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
||
</view>
|
||
</view>
|
||
<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>
|
||
</block>
|
||
|
||
<!-- 手动输入 -->
|
||
<block v-else-if="connectMode === 'manual'">
|
||
<view class="sheet-header">
|
||
<view class="sheet-nav-btn" @click="closeConnectSheet">
|
||
<image src="https://static.shelingxingqiu.com/shootmini/static/sicon/arrow-left.png" mode="aspectFit" style="width: 40rpx; height: 40rpx;" />
|
||
</view>
|
||
<text class="sheet-title">加入无线网络</text>
|
||
<view
|
||
class="sheet-nav-btn"
|
||
:class="{ 'nav-disabled': joinDisabled }"
|
||
@click="joinNetwork"
|
||
>
|
||
<image src="https://static.shelingxingqiu.com/shootmini/static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
|
||
</view>
|
||
</view>
|
||
<view class="input-row-card">
|
||
<text class="input-label">名称</text>
|
||
<input
|
||
class="input-field"
|
||
v-model="connectInput.ssid"
|
||
placeholder="输入网络名称"
|
||
placeholder-class="input-placeholder"
|
||
/>
|
||
</view>
|
||
<view class="input-row-card">
|
||
<text class="input-label">密码</text>
|
||
<input
|
||
class="input-field"
|
||
:password="!showPassword"
|
||
:adjust-position="false"
|
||
v-model="connectInput.password"
|
||
placeholder="输入网络密码"
|
||
placeholder-class="input-placeholder"
|
||
confirm-type="done"
|
||
@confirm="joinNetwork"
|
||
/>
|
||
<!-- 密码显示/隐藏切换按钮 -->
|
||
<view class="pwd-eye-btn" @click="togglePasswordVisibility">
|
||
<image
|
||
:src="showPassword ? 'https://static.shelingxingqiu.com/shootmini/static/sicon/eye-on.png' : 'https://static.shelingxingqiu.com/shootmini/static/sicon/eye-off.png'"
|
||
mode="aspectFit"
|
||
style="width: 40rpx; height: 40rpx;"
|
||
/>
|
||
</view>
|
||
</view>
|
||
<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>
|
||
</block>
|
||
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<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>
|
||
|
||
<style scoped>
|
||
.wifi-banner {
|
||
background-color: rgba(255, 200, 0, 0.12);
|
||
border: 1rpx solid rgba(254, 216, 71, 0.5);
|
||
border-radius: 8rpx;
|
||
padding: 16rpx 24rpx;
|
||
margin: 16rpx 24rpx 0;
|
||
}
|
||
.wifi-banner-text {
|
||
color: rgba(254, 216, 71, 1);
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.wifi-page {
|
||
box-sizing: border-box;
|
||
padding: 0 40rpx 54rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hero-area {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-top: 12rpx;
|
||
}
|
||
.hero-image {
|
||
width: 164rpx;
|
||
height: 152rpx;
|
||
}
|
||
|
||
.page-title {
|
||
display: block;
|
||
color: #5fadff;
|
||
font-size: 36rpx;
|
||
line-height: 50rpx;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
margin-top: 22rpx;
|
||
}
|
||
.page-subtitle {
|
||
display: block;
|
||
align-self: center;
|
||
width: 552rpx;
|
||
max-width: 100%;
|
||
color: #fff;
|
||
font-size: 24rpx;
|
||
line-height: 34rpx;
|
||
text-align: center;
|
||
margin-top: 4rpx;
|
||
}
|
||
.network-sections {
|
||
margin-top: 60rpx;
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
}
|
||
.network-section:last-child {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
}
|
||
.connected-section {
|
||
margin-bottom: 60rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.section-label {
|
||
display: block;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
font-size: 26rpx;
|
||
line-height: 36rpx;
|
||
}
|
||
.section-label-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 36rpx;
|
||
margin: 0 40rpx 20rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.connected-section > .section-label {
|
||
margin: 0 40rpx 20rpx;
|
||
}
|
||
.refresh-icon {
|
||
width: 30rpx;
|
||
height: 30rpx;
|
||
}
|
||
.connected-wifi-card,
|
||
.ios-network-card,
|
||
.android-list-card {
|
||
box-sizing: border-box;
|
||
background-color: rgba(255, 255, 255, 0.08);
|
||
border-radius: 24rpx;
|
||
overflow: hidden;
|
||
}
|
||
.connected-wifi-card {
|
||
height: 104rpx;
|
||
padding: 0 40rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.wifi-ssid {
|
||
display: block;
|
||
min-width: 0;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: #fff;
|
||
font-size: 32rpx;
|
||
line-height: 44rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
.ios-network-card {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 40rpx;
|
||
}
|
||
.current-network-card {
|
||
height: 124rpx;
|
||
}
|
||
.manual-network-card {
|
||
height: 156rpx;
|
||
margin-top: 20rpx;
|
||
}
|
||
.network-copy {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
.network-desc {
|
||
display: block;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
font-size: 22rpx;
|
||
line-height: 32rpx;
|
||
}
|
||
.manual-network-card .network-desc {
|
||
max-width: 484rpx;
|
||
}
|
||
.android-list-card {
|
||
padding: 0 40rpx;
|
||
flex: 1;
|
||
height: 0;
|
||
min-height: 0;
|
||
}
|
||
.android-network-item {
|
||
box-sizing: border-box;
|
||
height: 108rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 2rpx solid rgba(255, 255, 255, 0.15);
|
||
}
|
||
.android-network-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.wifi-icons {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.check-icon {
|
||
width: 40rpx;
|
||
height: 32rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.security-icon {
|
||
width: 24rpx;
|
||
height: 28rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.signal-icon {
|
||
width: 40rpx;
|
||
height: 32rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.wifi-footer {
|
||
display: block;
|
||
flex-shrink: 0;
|
||
align-self: center;
|
||
width: 550rpx;
|
||
max-width: 100%;
|
||
margin-top: auto;
|
||
padding-top: 60rpx;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
font-size: 22rpx;
|
||
line-height: 32rpx;
|
||
text-align: center;
|
||
}
|
||
.location-page {
|
||
box-sizing: border-box;
|
||
padding: 0 40rpx 54rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
.location-image-wrap {
|
||
position: relative;
|
||
width: 200rpx;
|
||
height: 266rpx;
|
||
margin-top: 212rpx;
|
||
}
|
||
.location-image {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 200rpx;
|
||
height: 266rpx;
|
||
}
|
||
.location-shadow {
|
||
position: absolute;
|
||
left: 34rpx;
|
||
bottom: -22rpx;
|
||
width: 132rpx;
|
||
height: 50rpx;
|
||
border-radius: 50%;
|
||
background-color: rgba(255, 255, 255, 0.08);
|
||
}
|
||
.location-desc {
|
||
display: block;
|
||
width: 560rpx;
|
||
max-width: 100%;
|
||
margin-top: 48rpx;
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
line-height: 40rpx;
|
||
text-align: center;
|
||
}
|
||
.location-button {
|
||
box-sizing: border-box;
|
||
width: 364rpx;
|
||
height: 74rpx;
|
||
margin-top: 38rpx;
|
||
border: 2rpx solid #ffd800;
|
||
border-radius: 40rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fed847;
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sheet-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
}
|
||
.sheet {
|
||
position: relative;
|
||
background-color: rgba(57, 47, 29, 1);
|
||
border-top-left-radius: 28rpx;
|
||
border-top-right-radius: 28rpx;
|
||
overflow: hidden;
|
||
min-height: 320rpx;
|
||
border: 1rpx solid rgba(249, 213, 161, 0.4);
|
||
border-bottom: none;
|
||
}
|
||
.sheet-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 0;
|
||
}
|
||
.sheet-inner {
|
||
position: relative;
|
||
z-index: 1;
|
||
padding: 32rpx 32rpx 112rpx;
|
||
}
|
||
|
||
.sheet-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 32rpx;
|
||
}
|
||
.sheet-nav-btn {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.nav-disabled {
|
||
opacity: 0.3;
|
||
}
|
||
.sheet-title {
|
||
color: rgba(255, 255, 255, 1);
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
flex: 1;
|
||
text-align: center;
|
||
}
|
||
.sheet-hint {
|
||
color: rgba(255, 255, 255, 0.7);
|
||
font-size: 26rpx;
|
||
text-align: center;
|
||
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 {
|
||
color: rgba(254, 216, 71, 1);
|
||
font-size: 26rpx;
|
||
line-height: 40rpx;
|
||
margin-top: 16rpx;
|
||
display: block;
|
||
/* 错误提示文案居中展示 */
|
||
text-align: center;
|
||
}
|
||
|
||
.input-row-card {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
border-radius: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 24rpx 24rpx;
|
||
margin-left: 48rpx;
|
||
margin-right: 48rpx;
|
||
}
|
||
.input-row-card + .input-row-card {
|
||
margin-top: 16rpx;
|
||
}
|
||
.input-label {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
font-size: 28rpx;
|
||
width: 80rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.input-field {
|
||
flex: 1;
|
||
color: rgba(255, 255, 255, 1);
|
||
font-size: 28rpx;
|
||
background: transparent;
|
||
border: none;
|
||
padding: 0 8rpx;
|
||
}
|
||
.input-placeholder {
|
||
color: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
/* 密码查看切换按钮 */
|
||
.pwd-eye-btn {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
</style>
|