Files
shoot-miniprograms/src/pages/device/ota-wifi.vue
T

1194 lines
35 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 { t } from "@/locales";
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 wifiSuccessVisible = 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 = t('connection_failed_check_the_wi_fi_password_and_network_status');
// 与 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(
() => t('starts_in_values', { p0: 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: t('please_enable_location_services_on_your_phone_first'), 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 t('network_names_support_only_english_characters_and_numbers_use_or');
}
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 t('please_turn_on_your_smart_bow_first');
if (message.includes("超时") || error?.errMsg?.includes("timeout")) {
return t('device_response_timed_out_please_retry');
}
if (error?.errMsg) return t('network_error_check_your_connection_and_retry');
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 = t('connecting_to_wi_fi');
uni.showLoading({
title: t('connecting_to_wi_fi_518'),
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 = "";
void refreshConnectedWifiName();
if (fromFirmwareUpdate.value) {
currentState.value = STATES.CONNECTED;
openFirmwareCountdown();
} else {
// 保留配网输入界面,避免成功提示出现前先露出 WiFi 列表。
wifiSuccessVisible.value = true;
}
} catch (err) {
if (requestId === wifiConnectRequestId) {
connectError.value = getWifiConnectErrorText(err);
}
} finally {
if (requestId === wifiConnectRequestId) {
connectStatusText.value = "";
isSubmittingWifi.value = false;
uni.hideLoading();
}
}
};
// 普通配网成功后,确认并收起输入界面,停留在当前 WiFi 页面。
const confirmWifiSuccess = () => {
if (!wifiSuccessVisible.value) return;
wifiSuccessVisible.value = false;
currentState.value = STATES.CONNECTED;
};
// 校验用户输入并提交 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 = t('already_up_to_date_520');
firmwareMessageVisible.value = true;
return;
}
if (!otaInfo.versionNumber) {
firmwareMessage.value = t('target_firmware_version_is_missing_go_back_and_retry');
firmwareMessageVisible.value = true;
return;
}
// WiFi 连接可能耗时较长,以更新前的最新设备详情为准。
routeOtaVersionNumber.value = otaInfo.versionNumber;
} catch (error) {
firmwareMessage.value = t('could_not_load_device_details_go_back_and_retry');
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">{{ t('please_enable_wi_fi_then_refresh_and_retry') }}</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">{{ t('connect_your_smart_bow_to_wi_fi') }}</text>
<text class="page-subtitle">{{ t('this_connects_your_smart_bow_to_wi_fi_it_will_not_change_the_wi_') }}</text>
<view class="network-sections">
<!-- 仅设备 WebSocket 状态为 WiFi 时显示;此卡片不触发密码输入。 -->
<view v-if="isDeviceOnWifi && connectedWifiName" class="network-section connected-section">
<text class="section-label">{{ t('connected') }}</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">{{ t('select_network') }}</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">{{ t('your_phone_s_current_network') }}</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">{{ t('connect_to_another_network_manually') }}</text>
<text class="network-desc">{{ t('permissions_prevent_nearby_networks_from_being_listed_to_connect') }}</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">{{ t('connect_to_another_network_manually') }}</text>
</view>
</scroll-view>
</view>
</view>
<text class="wifi-footer">{{ t('your_smart_bow_can_save_only_one_network_connecting_to_a_new_net') }}</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">{{ t('allow_location_access_and_enable_location_services_to_find_nearb') }}</text>
<view class="location-button" @click="handleReauthorize">
<text>{{ t('allow_location_access') }}</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">{{ t('join_533') }}{{ 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">{{ t('password') }}</text>
<input
class="input-field"
:password="!showPassword"
:adjust-position="false"
v-model="connectInput.password"
:placeholder="t('enter_network_password')"
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">{{ t('join_533') }}{{ 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">{{ t('this_is_an_open_network_tap_to_join') }}</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">{{ t('join_wi_fi_network') }}</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">{{ t('name') }}</text>
<input
class="input-field"
v-model="connectInput.ssid"
:placeholder="t('enter_network_name')"
placeholder-class="input-placeholder"
/>
</view>
<view class="input-row-card">
<text class="input-label">{{ t('password') }}</text>
<input
class="input-field"
:password="!showPassword"
:adjust-position="false"
v-model="connectInput.password"
:placeholder="t('enter_network_password')"
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="wifiSuccessVisible"
:content="t('wi_fi_connected')"
:confirmText="t('confirm_134')"
:showCancel="false"
:onConfirm="confirmWifiSuccess"
></ModalDialog>
<ModalDialog
:show="countdownVisible"
:title="t('wi_fi_connected_541')"
:content="t('the_update_will_start_automatically_in_3_seconds')"
: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="t('close')"
:showCancel="false"
:onConfirm="closeOtaResult"
></ModalDialog>
<ModalDialog
:show="firmwareMessageVisible"
:title="t('firmware_update')"
:content="firmwareMessage"
:confirmText="t('close')"
: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>