diff --git a/web/src/components/devices/DeviceOverviewTab.tsx b/web/src/components/devices/DeviceOverviewTab.tsx index 664963a..a7c7c83 100644 --- a/web/src/components/devices/DeviceOverviewTab.tsx +++ b/web/src/components/devices/DeviceOverviewTab.tsx @@ -19,7 +19,7 @@ export interface DeviceOverviewTabProps { trafficMinuteTx: string; e911Starting: boolean; onSetupE911: () => void; - onRefresh: () => void; + onRefresh: () => void | Promise; } export function DeviceOverviewTab(props: DeviceOverviewTabProps) { @@ -45,6 +45,7 @@ export function DeviceOverviewTab(props: DeviceOverviewTabProps) { customPhoneNumber={props.customPhoneNumber} e911Starting={props.e911Starting} onSetupE911={props.onSetupE911} + onRefreshOverview={props.onRefresh} /> {showNetworkDetails ? void; + onRefreshOverview: () => void | Promise; } -export function OverviewSimPanel({ device, simOperatorDisplay, customPhoneNumber, e911Starting, onSetupE911 }: OverviewSimPanelProps) { +export function OverviewSimPanel({ device, simOperatorDisplay, customPhoneNumber, e911Starting, onSetupE911, onRefreshOverview }: OverviewSimPanelProps) { const { t } = useI18n(); const [showSensitive, toggleSensitive] = useShowSensitive(); const modem = device.modem; @@ -43,6 +45,7 @@ export function OverviewSimPanel({ device, simOperatorDisplay, customPhoneNumber + {device?.e911SetupAvailable ? (
{t("E911地址")} diff --git a/web/src/components/devices/SMSChannelStatusRow.tsx b/web/src/components/devices/SMSChannelStatusRow.tsx new file mode 100644 index 0000000..92a5a8f --- /dev/null +++ b/web/src/components/devices/SMSChannelStatusRow.tsx @@ -0,0 +1,115 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { ArrowSyncRegular } from "@fluentui/react-icons"; +import { apiMessage } from "../../api"; +import { Tag, type TagType } from "../ui/Tag"; +import { message } from "../ui"; +import { cx } from "../../lib/utils"; +import { useI18n } from "../../lib/i18n"; +import { getCellularIMS, type CellularIMSStatus } from "./deviceActions"; +import { isDeviceOnline, isVoWiFiInUse } from "./shared"; +import type { DeviceDetail } from "./types"; + +interface SMSChannelStatusRowProps { + device: DeviceDetail; + onRefreshOverview: () => void | Promise; +} + +interface DisplayStatus { + label: string; + tone: TagType; +} + +export function SMSChannelStatusRow({ device, onRefreshOverview }: SMSChannelStatusRowProps) { + const { t } = useI18n(); + const [cellular, setCellular] = useState(null); + const [loading, setLoading] = useState(false); + const requestSequence = useRef(0); + + const iccid = (device.modem?.iccid || "").trim(); + const usesVoWiFi = isVoWiFiInUse(device) && !(device.modem?.imei && device.modem?.simInserted === false); + const canProbeCellular = !usesVoWiFi && isDeviceOnline(device) && !!iccid && device.modem?.simInserted !== false; + + const probeCellular = useCallback(async () => { + if (!canProbeCellular) return; + const sequence = ++requestSequence.current; + setLoading(true); + try { + const next = await getCellularIMS(device.id); + if (sequence === requestSequence.current) setCellular(next); + } catch { + if (sequence === requestSequence.current) setCellular(null); + } finally { + if (sequence === requestSequence.current) setLoading(false); + } + }, [canProbeCellular, device.id]); + + // The overview tab is conditionally mounted, so this runs once when users + // return to it and again when the active device/SIM/profile changes. + useEffect(() => { + requestSequence.current += 1; + setCellular(null); + setLoading(false); + if (canProbeCellular) void probeCellular(); + return () => { + requestSequence.current += 1; + }; + }, [canProbeCellular, device.id, iccid, device.activeEsimProfileName, usesVoWiFi, probeCellular]); + + const refresh = async () => { + if (loading) return; + if (usesVoWiFi) { + const sequence = ++requestSequence.current; + setLoading(true); + try { + await onRefreshOverview(); + } catch (error) { + if (sequence === requestSequence.current) { + message.error(apiMessage(error) || t("刷新短信通道状态失败")); + } + } finally { + if (sequence === requestSequence.current) setLoading(false); + } + return; + } + await probeCellular(); + }; + + let display: DisplayStatus; + if (usesVoWiFi) { + if (device.vowifiRuntime?.smsReady) display = { label: t("已就绪(VoWiFi)"), tone: "success" }; + else if (device.vowifiRuntime?.imsReady) display = { label: t("已注册(IMS)"), tone: "success" }; + else if (device.vowifiRuntime?.enabled) display = { label: t("未注册短信域"), tone: "warning" }; + else display = { label: t("状态未知"), tone: "info" }; + } else if (!canProbeCellular || !cellular) { + display = { label: t("状态未知"), tone: "info" }; + } else if (cellular.registered && cellular.csRegistered) { + display = { label: t("已注册(CS, IMS)"), tone: "success" }; + } else if (cellular.registered) { + display = { label: t("已注册(IMS)"), tone: "success" }; + } else if (cellular.csRegistered) { + display = { label: t("已注册(CS)"), tone: "success" }; + } else if (cellular.csKnown && cellular.supported) { + display = { label: t("未注册短信域"), tone: "warning" }; + } else { + display = { label: t("状态未知"), tone: "info" }; + } + + return ( +
+ {t("短信通道")} +
+ {display.label} + +
+
+ ); +} diff --git a/web/src/lib/i18n-en.ts b/web/src/lib/i18n-en.ts index a89c806..982b9c1 100644 --- a/web/src/lib/i18n-en.ts +++ b/web/src/lib/i18n-en.ts @@ -1208,6 +1208,15 @@ export const EN_DICT: Record = { // Additional missing system strings "端口": "Port", "错误详情": "Error Details", + "短信通道": "SMS channel", + "刷新短信通道状态": "Refresh SMS channel status", + "刷新短信通道状态失败": "Failed to refresh SMS channel status", + "已注册(CS)": "Registered (CS)", + "已注册(IMS)": "Registered (IMS)", + "已注册(CS, IMS)": "Registered (CS, IMS)", + "已就绪(VoWiFi)": "Ready (VoWiFi)", + "未注册短信域": "No SMS domain registered", + "状态未知": "Unknown", "正在搜索网络": "Searching network", "SM-DP+ 的公开 Profile 库存已耗尽,请稍后重试或更换服务。": "The public profile inventory on the SM-DP+ is exhausted. Please try again later or use a different service.", @@ -1218,4 +1227,3 @@ export const EN_DICT: Record = { "已发现该模组,但未找到 AT 串口:通常是 option 驱动未认该 PID 或模组处于 MBIM/RNDIS 组态。可 ": "Modem detected, but no AT serial port found: option driver may not recognize this PID or modem is in MBIM/RNDIS mode. You can ", }; -