Files
shoot-miniprograms/src/pages/device/ota-wifi.vue
T
2026-09-22 10:00:26 +08:00

1079 lines
31 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, onMounted, onUnmounted } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import ModalDialog from "@/components/ModalDialog.vue";
import OtaModal from "@/components/OtaModal.vue";
import {
connectDeviceWifiAPI,
getHardwareBoxVersionAPI,
} from "@/apis";
import { useOtaUpdate } from "@/composables/useOtaUpdate";
const STATES = {
SCANNING: "SCANNING",
LIST: "LIST",
CONNECTING: "CONNECTING",
CONNECTED: "CONNECTED",
};
const isIOS = uni.getDeviceInfo().osName === "ios";
const currentState = ref(STATES.SCANNING);
const connectedWifi = ref(null);
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 routeOtaInfo = ref({
versionNumber: "",
resourceUrl: "",
});
const countdownVisible = ref(false);
const countdownSeconds = ref(3);
const firmwareMessageVisible = ref(false);
const firmwareMessage = ref("");
let countdownTimer = null;
let wifiConnectRequestId = 0;
const WIFI_CONNECT_FAILED_TEXT = "连接失败,请检查WiFi密码或WiFi状态";
// 控制授权拒绝弹窗显示/隐藏
const wifiAuthDeniedVisible = ref(false);
const {
updating: otaUpdating,
progress: otaProgress,
phase: otaPhase,
resultVisible: otaResultVisible,
resultStatus: otaResultStatus,
resultTitle: otaResultTitle,
resultContent: otaResultContent,
startUpdate: startOtaUpdate,
closeResult: closeOtaResult,
} = useOtaUpdate();
const countdownButtonText = computed(
() => `${Math.max(1, countdownSeconds.value)}秒后开始`
);
// 判断 WiFi 列表失败是否由用户拒绝授权引起(兼容 errno:103 及各平台 errMsg 变体)。
const isWifiPermissionDenied = (err) => {
if (err?.errno === 103) return true;
const errMsg = err?.errMsg || "";
return /auth den|authorize|permission denied|user denied|scope\.userLocation/i.test(errMsg);
};
// 显示授权拒绝弹窗,通过 ScreenHint 组件呈现。
const showWifiPermissionDeniedModal = () => {
wifiAuthDeniedVisible.value = true;
};
// 用户点击「重新授权」时,打开小程序权限设置页,引导用户开启位置权限后重新扫描。
const handleReauthorize = () => {
wifiAuthDeniedVisible.value = false;
uni.openSetting({
success(res) {
// 若用户在设置页开启了位置权限,则重新启动 WiFi 扫描
if (res.authSetting["scope.userLocation"]) {
startScanning();
}
},
});
};
// 将 iOS 当前手机连接的 WiFi 写入候选列表,供用户继续填写设备连接密码;扫描结束后重置刷新防抖标志。
const applyIOSConnectedWifi = (wifi) => {
const ssid = wifi?.SSID;
if (!ssid) {
wifiList.value = [];
currentState.value = STATES.LIST;
isRefreshing.value = false;
return;
}
wifiList.value = [
{
...wifi,
SSID: ssid,
secure: wifi.secure !== false,
fromCurrentWifi: true,
},
];
currentState.value = 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 = 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 = 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;
}
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)) showWifiPermissionDeniedModal();
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)) showWifiPermissionDeniedModal();
if (err.errCode === 12005) showWifiBanner.value = true;
isRefreshing.value = false;
},
});
wx.onGetWifiList((res) => {
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 = 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;
});
// 按当前页面状态限制 WiFi 列表高度,避免列表撑开整页。
const wifiListScrollHeight = computed(() => {
if (currentState.value === STATES.SCANNING) return "auto";
const itemCount = wifiList.value.length + 1;
const maxHeight = currentState.value === STATES.CONNECTED ? 276 : 420;
return `${Math.min(itemCount * 92, maxHeight)}rpx`;
});
// 取消当前 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;
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 });
};
// 获取 OTA 更新版本信息,正常使用入口传参,缺失时再请求后端兜底。
const getOtaVersionInfo = async () => {
if (routeOtaInfo.value.versionNumber && routeOtaInfo.value.resourceUrl) {
return {
needUpdate: true,
versionNumber: routeOtaInfo.value.versionNumber,
resourceUrl: routeOtaInfo.value.resourceUrl,
};
}
return getHardwareBoxVersionAPI();
};
const startFirmwareUpdate = async () => {
if (!fromFirmwareUpdate.value || !connectedWifi.value || otaUpdating.value) return;
try {
const versionInfo = await getOtaVersionInfo();
if (!versionInfo?.needUpdate) {
firmwareMessage.value = "当前已是最新版本";
firmwareMessageVisible.value = true;
return;
}
await startOtaUpdate({
versionNumber: versionInfo.versionNumber,
wifiSsid: connectedWifi.value.SSID,
wifiPassword: connectedWifi.value.password || "",
resourceUrl: versionInfo.resourceUrl,
});
} catch (error) {
firmwareMessage.value = "获取更新版本失败,请重试";
firmwareMessageVisible.value = true;
}
};
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 = res?.height || 0;
};
// 切换密码显示状态前先收起键盘,避免安全键盘和普通输入法切换时复用旧高度产生黑条。
const togglePasswordVisibility = () => {
uni.hideKeyboard({
complete() {
keyboardHeight.value = 0;
showPassword.value = !showPassword.value;
},
});
};
// 页面加载时识别普通 WiFi 入口和固件更新入口。
onLoad((options = {}) => {
fromFirmwareUpdate.value = options.source === "firmware-update";
routeOtaInfo.value = {
versionNumber: decodeURIComponent(options.versionNumber || ""),
resourceUrl: decodeURIComponent(options.resourceUrl || ""),
};
});
// 页面挂载后启动 WiFi 扫描流程。
onMounted(() => {
if (typeof uni.onKeyboardHeightChange === "function") {
uni.onKeyboardHeightChange(handleKeyboardHeightChange);
}
startScanning();
});
// iOS 从系统 WiFi 设置返回小程序后,同步当前手机连接的 WiFi。
onShow(() => {
if (isIOS && currentState.value === STATES.LIST) {
syncIOSConnectedWifi();
}
});
onUnmounted(() => {
if (typeof uni.offKeyboardHeightChange === "function") {
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
}
cancelWifiConnectWaiting();
clearFirmwareCountdown();
wx.offGetWifiList && wx.offGetWifiList();
});
</script>
<template>
<Container title="连接无线网络">
<!-- WiFi 不可用 Banner -->
<view v-if="showWifiBanner" class="wifi-banner">
<text class="wifi-banner-text">请先开启 WiFi 后刷新重试</text>
</view>
<!-- SCANNING / LIST / CONNECTED -->
<view
v-if="currentState === 'SCANNING' || currentState === 'LIST' || currentState === 'CONNECTED'"
class="wifi-page"
>
<!-- Hero: wifi1.png 单图(已内含吉祥物) -->
<view class="hero-area">
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/wifi1.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" />
</view>
<text class="page-title" :class="{ 'connected-page-title': currentState === 'CONNECTED' }">
{{ currentState === 'CONNECTED' ? '连接成功' : '连接无线网络' }}
</text>
<text v-if="currentState !== 'CONNECTED'" class="page-subtitle">网络名称仅支持英文字符及数字</text>
<view v-if="isIOS && currentState !== 'CONNECTED'" class="ios-guide">
<text class="ios-guide-text">请先在系统 WiFi 设置中连接目标网络,返回后选择当前 WiFi 并填写密码</text>
</view>
<!-- 已连接网络卡片 -->
<view v-if="currentState === 'CONNECTED'" class="section-label-row connected-section-label-row">
<text class="section-label">连接网络</text>
</view>
<view v-if="currentState === 'CONNECTED'" class="wifi-list-card connected-wifi-card">
<view class="wifi-item connected-wifi-item">
<image class="check-icon" src="https://static.shelingxingqiu.com/shootmini/static/sicon/check.png" mode="aspectFit" />
<text class="wifi-ssid connected-wifi-ssid">{{ connectedWifi?.SSID }}</text>
<view class="wifi-icons connected-wifi-icons">
<image
v-if="connectedWifi?.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>
<!-- 网络列表标题 -->
<view class="section-label-row">
<text class="section-label">网络</text>
<image
src="https://static.shelingxingqiu.com/shootmini/static/sicon/refresh.png"
mode="aspectFit"
:style="{ width: '34rpx', height: '34rpx', marginLeft: '8rpx', opacity: isRefreshing ? 0.3 : 0.7 }"
@click="startScanning"
/>
</view>
<!-- 网络列表卡片 -->
<scroll-view
scroll-y
class="wifi-list-card"
:class="{ 'wifi-list-scroll': currentState !== 'SCANNING' }"
:style="{ height: wifiListScrollHeight }"
>
<block v-if="currentState === 'SCANNING'">
<view class="wifi-item" @click="selectOther">
<text class="wifi-ssid other-text">其他...</text>
</view>
</block>
<block v-else>
<view
v-for="(wifi, idx) in wifiList"
:key="wifi.SSID"
class="wifi-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="wifi-item" @click="selectOther">
<text class="wifi-ssid other-text">其他...</text>
</view>
</block>
</scroll-view>
</view>
<!-- CONNECTING 底部弹窗 -->
<view v-if="currentState === 'CONNECTING'" class="sheet-mask">
<view class="sheet" :style="{ marginBottom: keyboardHeight + 'px' }" @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>
<!-- 授权拒绝弹窗 -->
<ScreenHint :show="wifiAuthDeniedVisible" :onClose="() => (wifiAuthDeniedVisible = false)">
<view class="wifi-auth-denied">
<text class="wifi-auth-denied-text">拒绝授权获取wifi列表失败,请重新授权或手动输入</text>
<view class="wifi-auth-denied-btns">
<view class="wifi-auth-denied-btn cancel-btn" @click="wifiAuthDeniedVisible = false">
<text>取消授权</text>
</view>
<view class="wifi-auth-denied-btn confirm-btn" @click="handleReauthorize">
<text>重新授权</text>
</view>
</view>
</view>
</ScreenHint>
<ModalDialog
:show="countdownVisible"
title="WiFi连接成功"
content="3秒后将自动开始更新"
:confirmText="countdownButtonText"
:showCancel="false"
:confirmDisabled="true"
></ModalDialog>
<OtaModal
:visible="otaUpdating"
state="update_progress"
:progress="otaProgress"
:phase="otaPhase"
/>
<OtaModal
:visible="otaResultVisible && otaResultStatus === 'success'"
state="update_success"
@done="closeOtaResult"
/>
<ModalDialog
:show="otaResultVisible && otaResultStatus === 'failed'"
:title="otaResultTitle"
:content="otaResultContent"
confirmText="关闭"
:showCancel="false"
:onConfirm="closeOtaResult"
></ModalDialog>
<ModalDialog
:show="firmwareMessageVisible"
title="固件更新"
:content="firmwareMessage"
confirmText="关闭"
:showCancel="false"
:onConfirm="() => (firmwareMessageVisible = false)"
></ModalDialog>
</Container>
</template>
<style scoped>
.wifi-auth-denied {
display: flex;
flex-direction: column;
align-items: center;
padding: 32rpx 40rpx 24rpx;
gap: 32rpx;
}
.wifi-auth-denied-text {
color: #fff;
font-size: 28rpx;
text-align: center;
line-height: 1.6;
}
.wifi-auth-denied-btns {
display: flex;
flex-direction: row;
gap: 24rpx;
width: 100%;
justify-content: center;
}
.wifi-auth-denied-btn {
flex: 1;
height: 72rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.wifi-auth-denied-btn.cancel-btn {
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
.wifi-auth-denied-btn.confirm-btn {
background: rgba(254, 216, 71, 1);
color: #1a1a2e;
font-weight: 600;
}
.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 {
padding: 0 67rpx 40rpx;
display: flex;
flex-direction: column;
}
.hero-area {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0 24rpx;
}
.page-title {
color: rgba(91, 196, 255, 1);
font-size: 36rpx;
font-weight: 600;
text-align: center;
margin-bottom: 8rpx;
}
.connected-page-title {
color: #4DD9C8;
}
.page-subtitle {
color: rgba(255, 255, 255, 0.5);
font-size: 24rpx;
text-align: center;
margin-bottom: 32rpx;
}
.fail-title {
color: rgba(255, 100, 100, 1);
}
.ios-guide {
background-color: rgba(95, 173, 255, 0.12);
border-radius: 8rpx;
padding: 16rpx 24rpx;
margin-bottom: 20rpx;
}
.ios-guide-text {
color: rgba(95, 173, 255, 1);
font-size: 26rpx;
}
.section-label-row {
display: flex;
align-items: center;
margin-bottom: 24rpx;
padding: 0 22rpx;
}
.connected-section-label-row {
margin-top: 32rpx;
}
.section-label {
color: rgba(255, 255, 255, 0.56);
font-size: 28rpx;
}
.wifi-list-card {
background-color: rgba(30, 35, 50, 0.96);
border-radius: 24rpx;
overflow: hidden;
margin-bottom: 20rpx;
}
.wifi-list-scroll {
flex-shrink: 0;
}
.connected-wifi-card {
background-color: rgba(35, 40, 56, 0.96);
margin-bottom: 44rpx;
}
.wifi-item {
position: relative;
display: flex;
align-items: center;
min-height: 92rpx;
padding: 0 26rpx 0 66rpx;
}
.wifi-item:not(:last-child)::after {
content: "";
position: absolute;
left: 66rpx;
right: 26rpx;
bottom: 0;
height: 1rpx;
background-color: rgba(255, 255, 255, 0.08);
}
.connected-wifi-item {
min-height: 88rpx;
padding-left: 28rpx;
}
.wifi-ssid {
color: rgba(255, 255, 255, 1);
font-size: 28rpx;
line-height: 36rpx;
flex: 1;
}
.connected-wifi-ssid {
font-weight: 500;
}
.other-text {
color: rgba(255, 255, 255, 0.86);
}
.wifi-icons {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
}
.connected-wifi-icons {
gap: 14rpx;
}
.check-icon {
width: 28rpx;
height: 24rpx;
margin-right: 16rpx;
flex-shrink: 0;
}
.security-icon {
width: 24rpx;
height: 30rpx;
flex-shrink: 0;
}
.signal-icon {
width: 36rpx;
height: 32rpx;
flex-shrink: 0;
}
.bottom-btn-area {
margin-top: 24rpx;
}
.connected-bottom-btn-area {
display: flex;
justify-content: center;
margin-top: 20rpx;
}
.primary-btn {
background-color: rgba(254, 216, 71, 1);
border-radius: 50rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
}
.update-btn {
width: 412rpx;
}
.primary-btn-text {
color: rgba(0, 0, 0, 1);
font-size: 30rpx;
font-weight: 500;
}
.center-page {
display: flex;
flex-direction: column;
align-items: center;
padding: 80rpx 32rpx 40rpx;
}
.page-desc-white {
color: rgba(255, 255, 255, 1);
font-size: 28rpx;
line-height: 52rpx;
text-align: center;
}
.done-btn {
width: 80%;
}
.progress-wrap {
width: 80%;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
margin-top: 40rpx;
}
.progress-track {
width: 100%;
height: 10rpx;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 5rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: rgba(91, 196, 255, 1);
border-radius: 5rpx;
transition: width 0.4s ease;
}
.progress-pct {
color: rgba(91, 196, 255, 1);
font-size: 28rpx;
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>