import { useCallback, useEffect, useMemo, useState } from "react"; import { apiMessage } from "../../api"; import type { CardPolicy } from "../../types"; import { Button, Input, Modal, Select, Spinner, Switch, Tag, confirmDialog, message } from "../ui"; import { useI18n } from "../../lib/i18n"; import { createCardAPN, deleteCardAPN, getCardAPNs, getDeviceAPNs, updateCardAPN, updateCardPolicy, type CardAPNProfile, type ModemAPNProfile, } from "./deviceActions"; interface CardPolicyAPNProps { deviceId: string; iccid: string; policy: CardPolicy | null; deviceOnline: boolean; onSaved: (policy: CardPolicy) => void; } interface APNRow { key: string; apn: string; ipVersion: "IP" | "IPV6" | "IPV4V6"; source: "automatic" | "modem" | "custom"; cid?: number; customID?: number; username?: string; hasPassword?: boolean; proxy?: string; mcc?: string; mnc?: string; roamingIPVersion?: "IP" | "IPV6" | "IPV4V6"; authType?: "NONE" | "PAP" | "CHAP" | "PAP_OR_CHAP"; } const APN_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/; export function CardPolicyAPN({ deviceId, iccid, policy, deviceOnline, onSaved }: CardPolicyAPNProps) { const { t } = useI18n(); const [modemProfiles, setModemProfiles] = useState([]); const [customProfiles, setCustomProfiles] = useState([]); const [loading, setLoading] = useState(true); const [editorOpen, setEditorOpen] = useState(false); const [editingProfile, setEditingProfile] = useState(null); const [newAPN, setNewAPN] = useState(""); const [newUsername, setNewUsername] = useState(""); const [newPassword, setNewPassword] = useState(""); const [newProxy, setNewProxy] = useState(""); const [newMCC, setNewMCC] = useState(""); const [newMNC, setNewMNC] = useState(""); const [newIPVersion, setNewIPVersion] = useState<"IP" | "IPV6" | "IPV4V6">("IPV4V6"); const [newRoamingIPVersion, setNewRoamingIPVersion] = useState<"IP" | "IPV6" | "IPV4V6">("IP"); const [newAuthType, setNewAuthType] = useState<"NONE" | "PAP" | "CHAP" | "PAP_OR_CHAP">("NONE"); const [clearPassword, setClearPassword] = useState(false); const [adding, setAdding] = useState(false); const [pendingKey, setPendingKey] = useState(""); const load = useCallback(async () => { setLoading(true); const [customResult, modemResult] = await Promise.allSettled([ getCardAPNs(iccid), deviceOnline ? getDeviceAPNs(deviceId) : Promise.resolve({ items: [] as ModemAPNProfile[] }), ]); setCustomProfiles(customResult.status === "fulfilled" ? customResult.value.items || [] : []); setModemProfiles(modemResult.status === "fulfilled" ? modemResult.value.items || [] : []); setLoading(false); }, [deviceId, deviceOnline, iccid]); useEffect(() => { load(); }, [load]); const rows = useMemo(() => { const result: APNRow[] = [ { key: "automatic", apn: "", ipVersion: "IPV4V6", source: "automatic" }, ]; for (const item of modemProfiles) { result.push({ key: `modem:${item.cid}:${item.apn}:${item.ipVersion}`, apn: item.apn, ipVersion: item.ipVersion, source: "modem", cid: item.cid, }); } for (const item of customProfiles) { result.push({ key: `custom:${item.id}`, apn: item.apn, ipVersion: item.ipVersion, source: "custom", customID: item.id, username: item.username, hasPassword: item.hasPassword, proxy: item.proxy, mcc: item.mcc, mnc: item.mnc, roamingIPVersion: item.roamingIpVersion, authType: item.authType, }); } return result; }, [customProfiles, modemProfiles]); function isActive(row: APNRow) { const activeAPN = policy?.apn || ""; const activeIP = policy?.ipVersion || "IPV4V6"; if (row.source === "automatic") return activeAPN === ""; if (row.apn !== activeAPN || row.ipVersion !== activeIP) return false; const activeCustom = customProfiles.some((item) => item.apn === activeAPN && item.ipVersion === activeIP); return row.source === "custom" || !activeCustom; } async function enable(row: APNRow) { setPendingKey(row.key); try { const saved = await updateCardPolicy(iccid, { apn: row.apn, ipVersion: row.ipVersion }); onSaved(saved); message.success(row.source === "automatic" ? t("已使用运营商自动 APN 配置") : t("APN 已启用")); } catch (error) { message.error(apiMessage(error) || t("启用 APN 失败")); } finally { setPendingKey(""); } } function openEditor(profile?: CardAPNProfile) { setEditingProfile(profile || null); setNewAPN(profile?.apn || ""); setNewUsername(profile?.username || ""); setNewPassword(""); setNewProxy(profile?.proxy || ""); setNewMCC(profile?.mcc || ""); setNewMNC(profile?.mnc || ""); setNewIPVersion(profile?.ipVersion || "IPV4V6"); setNewRoamingIPVersion(profile?.roamingIpVersion || "IP"); setNewAuthType(profile?.authType || "NONE"); setClearPassword(false); setEditorOpen(true); } async function saveEditor() { const cleanAPN = newAPN.trim(); if (!cleanAPN || !APN_PATTERN.test(cleanAPN)) { message.warning(t("APN 只能包含字母、数字、点、下划线或连字符,且最长 100 个字符")); return; } if (newMCC && !/^\d{3}$/.test(newMCC)) { message.warning(t("MCC 必须是 3 位数字")); return; } if (newMNC && !/^\d{2,3}$/.test(newMNC)) { message.warning(t("MNC 必须是 2 或 3 位数字")); return; } setAdding(true); try { const payload = { apn: cleanAPN, username: newUsername.trim(), proxy: newProxy.trim(), mcc: newMCC.trim(), mnc: newMNC.trim(), ipVersion: newIPVersion, roamingIpVersion: newRoamingIPVersion, authType: newAuthType, }; if (editingProfile) { await updateCardAPN(iccid, editingProfile.id, { ...payload, ...(newPassword ? { password: newPassword } : {}), clearPassword, }); } else { await createCardAPN(iccid, { ...payload, password: newPassword }); } setEditorOpen(false); await load(); message.success(editingProfile ? t("自定义 APN 已修改") : t("自定义 APN 已添加,请点击启用后使用")); } catch (error) { message.error(apiMessage(error) || (editingProfile ? t("修改 APN 失败") : t("添加 APN 失败"))); } finally { setAdding(false); } } async function remove(row: APNRow) { if (!row.customID) return; const confirmed = await confirmDialog( t("确定删除这个自定义 APN 配置吗?"), t("删除 APN"), { type: "danger", confirmVariant: "danger", confirmText: t("删除") }, ); if (!confirmed) return; setPendingKey(row.key); try { const wasActive = isActive(row); await deleteCardAPN(iccid, row.customID); if (wasActive && policy) { onSaved({ ...policy, apn: "", ipVersion: "IPV4V6" }); } await load(); message.success(wasActive ? t("APN 已删除,并恢复运营商自动配置") : t("自定义 APN 已删除")); } catch (error) { message.error(apiMessage(error) || t("删除 APN 失败")); } finally { setPendingKey(""); } } function edit(row: APNRow) { const profile = customProfiles.find((item) => item.id === row.customID); if (profile) openEditor(profile); } const sourceLabel = (row: APNRow) => { if (row.source === "automatic") return t("默认"); if (row.source === "modem") return t("模组已有"); return t("自定义"); }; const protocolLabel = (value?: "IP" | "IPV6" | "IPV4V6") => value === "IP" ? "IPv4" : value === "IPV6" ? "IPv6" : value === "IPV4V6" ? "IPv4 / IPv6" : "—"; return (
{t("蜂窝 APN")}
{t("APN 列表和启用状态跟随当前 ICCID/Profile 保存")}
{loading ? (
{t("正在读取 APN 列表...")}
) : (
{rows.map((row) => { const active = isActive(row); return ( ); })}
APN {t("账号 / 认证")} MCC / MNC {t("协议 / 漫游")} Proxy {t("来源")} {t("状态")} {t("操作")}
{row.source === "automatic" ? t("运营商自动配置") : row.apn} {row.cid ? CID {row.cid} : null}
{row.username || "—"}{row.hasPassword ? {t("已设密码")} : null}
{row.authType && row.authType !== "NONE" ?
{row.authType.replace("PAP_OR_CHAP", "PAP / CHAP")}
: null}
{row.mcc || "—"} / {row.mnc || "—"}
{protocolLabel(row.ipVersion)}
{t("漫游")}:{protocolLabel(row.roamingIPVersion)}
{row.proxy || "—"} {sourceLabel(row)} {active ? {t("使用中")} : } {row.source === "custom" ? ( <> ) : null}
)} {!deviceOnline ?
{t("设备离线:自定义列表仍可管理,模组已有 APN 将在上线后读取")}
: null} setEditorOpen(false)} title={editingProfile ? t("修改 APN 配置") : t("新增 APN 配置")} width="max-w-4xl" closeOnOverlay={!adding} footer={ <> } >