Files
VoCat/web/src/components/devices/OverviewNetworkCard.tsx
T
MengMengCode 22487dbb1f Add PC/SC backend support for USB SIM readers
- Implemented a new `unsupportedBackend` in `backend_stub.go` to handle unsupported platforms.
- Created a `Service` struct in `service.go` to manage interactions with smart cards, including session management and identity reading.
- Added methods for reading identity, checking readiness, and authenticating with USIM applications.
- Introduced a `scriptedCard` for testing purposes in `service_test.go` to simulate card responses.
- Defined necessary types and error handling in `types.go` for better clarity and usability.
- Developed a `PCSCAdapter` in `vowifi/pcsc_adapter.go` to integrate PC/SC service with Wi-Fi calling functionalities.
- Added a new SVG icon for USB SIM readers in `public/sim-reader.svg`.
- Implemented tests to ensure correct functionality and error handling in various scenarios.
2026-08-12 02:33:32 +08:00

142 lines
6.3 KiB
TypeScript

import { SettingsRegular } from "@fluentui/react-icons";
import { cx } from "../../lib/utils";
import { StatusDot } from "../ui";
import { FieldRow } from "./FieldRow";
import { isDeviceOnline, isRegistered, isRecoveringPhase, lifecycleLabel, signalLevel, signalTone } from "./shared";
import type { DeviceDetail } from "./types";
import { useI18n } from "../../lib/i18n";
import { CountryFlag } from "../CountryFlag";
const BAR_HEIGHTS = ["h-[28%]", "h-[46%]", "h-[64%]", "h-[82%]", "h-full"];
const TEXT_TONE = {
green: "text-emerald-500 dark:text-emerald-400",
amber: "text-amber-500 dark:text-amber-400",
red: "text-red-500 dark:text-red-400",
gray: "text-gray-400 dark:text-gray-500",
} as const;
const BAR_TONE = {
green: "bg-emerald-500",
amber: "bg-amber-500",
red: "bg-red-500",
gray: "bg-gray-300 dark:bg-gray-600",
} as const;
export function OverviewNetworkCard({ device, onOpenOperatorSelection }: { device: DeviceDetail; onOpenOperatorSelection: () => void }) {
const { t } = useI18n();
const modem = device.modem;
const online = isDeviceOnline(device);
const cellularRegistered = isRegistered(device);
const simMissing = !!modem?.imei && modem?.simInserted === false;
// A persisted runtime may briefly describe the old session while disable is
// being cleaned up. Desired policy is authoritative for the overview badge.
const vowifiRegistered = !!device.vowifiEnabled && !!(device.vowifiActive || device.vowifiRuntime?.smsReady);
const registered = !simMissing && (cellularRegistered || vowifiRegistered);
const radioOffForVowifi = vowifiRegistered && (modem?.operatingMode === 0 || modem?.operatingMode === 4 || device.flightMode);
const tone = isRecoveringPhase(device.lifecyclePhase) ? "warning" : online ? (registered ? "success" : "warning") : "danger";
let statusText: string;
const phaseLabel = lifecycleLabel(device.lifecyclePhase);
if (phaseLabel && device.lifecyclePhase !== "online" && device.lifecyclePhase !== "offline") statusText = phaseLabel;
else if (online) {
if (simMissing) statusText = t("SIM卡未插入");
else
statusText = registered
? ""
: device.registrationStateLabel === "searching"
? t("搜索网络中")
: device.registrationStateLabel === "denied"
? t("驻网被拒")
: t("未驻网");
} else statusText = device.running ? t("控制面恢复中") : t("离线");
const level = signalLevel(modem?.signalDbm);
const sigTone = signalTone(modem?.signalDbm);
const netMode = [modem?.networkDuplex, modem?.networkMode].filter(Boolean).join(" ");
const cellularRegistrationText = simMissing
? t("SIM卡未插入")
: modem?.regStatus === 5
? t("已驻网(漫游)")
: modem?.regStatus === 1
? t("已驻网")
: device.registrationStateLabel === "searching"
? t("正在搜索网络")
: device.registrationStateLabel === "denied"
? t("驻网被拒")
: t("未驻网");
return (
<>
<div
className={cx(
"mb-3 flex items-center gap-2.5 rounded-xl border px-3.5 py-2.5",
registered
? "border-emerald-200 bg-emerald-50 dark:border-emerald-500/25 dark:bg-emerald-500/10"
: online
? "border-amber-200 bg-amber-50 dark:border-amber-500/25 dark:bg-amber-500/10"
: "border-gray-200 bg-gray-100 dark:border-white/10 dark:bg-white/5",
)}
>
<StatusDot tone={tone} size="sm" animated={registered} />
<div className="min-w-0 flex-1">
<div
className={cx(
"text-sm font-bold leading-tight",
registered ? "text-emerald-700 dark:text-emerald-300" : online ? "text-amber-700 dark:text-amber-300" : "text-gray-500 dark:text-gray-400",
)}
>
{vowifiRegistered ? (
<>{t("WiFi Calling 已注册")}</>
) : registered ? (
<span className="inline-flex items-center gap-1.5">
<CountryFlag countryCode={modem?.operatorCountryCode} />
<span>{modem?.operator || "--"}</span>{" "}
{modem?.networkMode ? <span className="opacity-70">· {netMode}</span> : null}
</span>
) : (
statusText
)}
</div>
</div>
<button
type="button"
onClick={onOpenOperatorSelection}
className="rounded p-1 transition-colors hover:bg-black/5 dark:hover:bg-white/10"
title={t("网络选择设置")}
>
<SettingsRegular className="h-5 w-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
<div className="mb-3 rounded-xl border border-gray-200 px-3.5 py-3 dark:border-white/10">
<div className="mb-1.5 text-[10px] font-bold uppercase tracking-wider text-gray-400">{t("信号强度")}</div>
<div className="flex items-center gap-3">
<div>
<div className="flex items-baseline gap-1">
<span className={cx("text-2xl font-extrabold tabular-nums leading-none", TEXT_TONE[sigTone])}>
{radioOffForVowifi ? "--" : (modem?.signalDbm ?? "--")}
</span>
<span className="text-xs text-gray-400">dBm</span>
</div>
<div className="mt-1 text-[10px] text-gray-400">
{radioOffForVowifi
? t("蜂窝射频已关闭(VoWiFi 接管)")
: <>RSRP {modem?.signalRsrp ?? "--"} · RSRQ {modem?.signalRsrq ?? "--"} · SINR {modem?.signalSinr ?? "--"}
{modem?.nr5gSignalSinr !== undefined ? ` ·NR5G SINR ${modem.nr5gSignalSinr}` : null}</>}
</div>
</div>
<div className="ml-auto flex h-7 items-end gap-0.5">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className={cx("w-1.5 rounded-sm", BAR_HEIGHTS[i - 1], i <= level ? BAR_TONE[sigTone] : "bg-gray-200 dark:bg-white/10")} />
))}
</div>
</div>
</div>
<div className="space-y-1.5 text-sm text-gray-700 dark:text-gray-200">
<FieldRow label={t("网络模式")} value={netMode || "--"} monospace />
<FieldRow label={t("频段")} value={modem?.radioBand || "--"} monospace />
<FieldRow label={t("信道")} value={modem?.radioChannel ? String(modem.radioChannel) : "--"} monospace />
<FieldRow label={t("注册状态")} value={vowifiRegistered ? t("WiFi Calling 已注册") : cellularRegistrationText} monospace />
</div>
</>
);
}