import { AddRegular, DeleteRegular } from "@fluentui/react-icons"; import { useEffect, useMemo, useState } from "react"; import { api } from "../../api"; import type { DeviceListItem, DeviceProxyBinding, EsimOverview, ProfileProxyCandidate, UpstreamProxy } from "../../types"; import { Button, EmptyState, Modal, Tag } from "../ui"; import { useI18n } from "../../lib/i18n"; export interface DeviceBindingsDialogProps { open: boolean; proxy: UpstreamProxy | null; proxies: UpstreamProxy[]; devices: DeviceListItem[]; bindings: DeviceProxyBinding[]; busy: boolean; onAdd: (profiles: ProfileProxyCandidate[]) => void; onDelete: (iccids: string[]) => void; onClose: () => void; } function profileLabel(profile: { name?: string; serviceProviderName?: string; iccid: string }) { return String(profile.name || profile.serviceProviderName || profile.iccid).trim(); } function currentDeviceICCID(device: DeviceListItem) { return String(device.modem?.iccid || device.vowifiRuntime?.iccid || "").trim(); } export function DeviceBindingsDialog(props: DeviceBindingsDialogProps) { const { t } = useI18n(); const { open, proxy, proxies, devices, bindings, busy, onAdd, onDelete, onClose } = props; const [adding, setAdding] = useState(false); const [loadingProfiles, setLoadingProfiles] = useState(false); const [candidates, setCandidates] = useState([]); const [selected, setSelected] = useState([]); const proxyName = proxy?.name || proxy?.id || ""; const deviceKey = devices .map((device) => `${device.id}:${currentDeviceICCID(device)}`) .sort() .join("|"); const current = useMemo( () => bindings.filter((item) => item.upstreamProxyId === proxy?.id), [bindings, proxy?.id], ); const bindingByICCID = useMemo(() => new Map(bindings.map((item) => [item.iccid, item])), [bindings]); const proxyNameById = useMemo(() => new Map(proxies.map((item) => [item.id, item.name || item.id])), [proxies]); useEffect(() => { if (!open) { setAdding(false); setSelected([]); setCandidates([]); } }, [open]); useEffect(() => { if (!adding || !open) return; let active = true; setLoadingProfiles(true); Promise.allSettled(devices.map(async (device) => { const currentICCID = currentDeviceICCID(device); let installed: ProfileProxyCandidate[] = []; try { const data = await api(`/devices/${encodeURIComponent(device.id)}/esim`); installed = (data.profiles || []).flatMap((group) => (group.profiles || []).map((profile) => ({ deviceId: device.id, iccid: String(profile.iccid || "").trim(), profileName: profileLabel(profile), stateText: profile.stateText, }))).filter((profile) => profile.iccid); } catch { // A traditional SIM and some readers do not expose an eSIM profile // inventory. Their live ICCID is still a valid VoWiFi route key. } if (currentICCID && !installed.some((profile) => profile.iccid === currentICCID)) { installed.push({ deviceId: device.id, iccid: currentICCID, profileName: t("当前 SIM 卡"), stateText: t("当前使用中"), }); } return installed; })).then((results) => { if (!active) return; const unique = new Map(); for (const result of results) { if (result.status !== "fulfilled") continue; for (const profile of result.value) if (!unique.has(profile.iccid)) unique.set(profile.iccid, profile); } setCandidates(Array.from(unique.values()).sort((a, b) => a.deviceId.localeCompare(b.deviceId) || a.profileName.localeCompare(b.profileName))); }).finally(() => { if (active) setLoadingProfiles(false); }); return () => { active = false; }; }, [adding, open, deviceKey, t]); useEffect(() => { if (!adding || selected.length === 0) return; if (selected.every((iccid) => bindingByICCID.get(iccid)?.upstreamProxyId === proxy?.id)) { setAdding(false); setSelected([]); } }, [adding, selected, bindingByICCID, proxy?.id]); useEffect(() => { if (adding) return; const available = new Set(current.map((item) => item.iccid)); setSelected((items) => items.filter((iccid) => available.has(iccid))); }, [adding, current]); const rows = adding ? candidates : current; const selectable = rows.filter((row) => adding ? !bindingByICCID.has(row.iccid) : true).map((row) => row.iccid); const allSelected = selectable.length > 0 && selectable.every((iccid) => selected.includes(iccid)); const toggle = (iccid: string) => setSelected((values) => values.includes(iccid) ? values.filter((item) => item !== iccid) : [...values, iccid]); const toggleAll = () => setSelected(allSelected ? [] : selectable); return (
{t("VoWiFi 会按当前 ICCID 选择代理。实体 SIM 和 eSIM Profile 都可以绑定;同一 ICCID 只能绑定一个代理。")}
{adding ? t("从当前 SIM 卡和已安装的 eSIM Profile 中选择") : `${current.length} ${t("个 SIM / Profile")}`}
{adding ? ( ) : null} {adding ? ( ) : ( <> )}
{adding ? : null} {rows.map((row) => { const existing = bindingByICCID.get(row.iccid); const unavailable = adding && !!existing; return ( {adding ? ( ) : null} ); })}
{t("设备 ID")} ICCID {t("SIM / Profile")}{t("状态")}
toggle(row.iccid)} disabled={unavailable || busy} aria-label={row.iccid} /> {row.deviceId} {row.iccid} {row.profileName || row.iccid} {existing ? {existing.upstreamProxyId === proxy?.id ? t("已绑定此代理") : `${t("已绑定")}: ${proxyNameById.get(existing.upstreamProxyId) || existing.upstreamProxyId}`} : {("stateText" in row && row.stateText) || t("可绑定")}}
{loadingProfiles ?
{t("读取 Profile 中...")}
: null} {!loadingProfiles && rows.length === 0 ? : null}
); }