Files
shoot-miniprograms/src/pages/device/ota-wifi.vue
T
2026-09-24 15:16:14 +08:00

1173 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>