fix: bind physical SIMs and validate multi-arch images

This commit is contained in:
MengMengCode
2026-08-13 11:01:36 +08:00
parent c436e12532
commit b45f7825ca
10 changed files with 135 additions and 38 deletions
@@ -1,8 +1,8 @@
import { AddRegular, DeleteRegular } from "@fluentui/react-icons";
import { useEffect, useMemo, useState } from "react";
import { api, apiMessage } from "../../api";
import { api } from "../../api";
import type { DeviceListItem, DeviceProxyBinding, EsimOverview, ProfileProxyCandidate, UpstreamProxy } from "../../types";
import { Button, EmptyState, Modal, Tag, message } from "../ui";
import { Button, EmptyState, Modal, Tag } from "../ui";
import { useI18n } from "../../lib/i18n";
export interface DeviceBindingsDialogProps {
@@ -29,7 +29,10 @@ export function DeviceBindingsDialog(props: DeviceBindingsDialogProps) {
const [candidates, setCandidates] = useState<ProfileProxyCandidate[]>([]);
const [selected, setSelected] = useState<string[]>([]);
const proxyName = proxy?.name || proxy?.id || "";
const deviceKey = devices.map((device) => device.id).sort().join("|");
const deviceKey = devices
.map((device) => `${device.id}:${String(device.modem?.iccid || "").trim()}`)
.sort()
.join("|");
const current = useMemo(
() => bindings.filter((item) => item.upstreamProxyId === proxy?.id),
[bindings, proxy?.id],
@@ -50,13 +53,29 @@ export function DeviceBindingsDialog(props: DeviceBindingsDialogProps) {
let active = true;
setLoadingProfiles(true);
Promise.allSettled(devices.map(async (device) => {
const data = await api<EsimOverview>(`/devices/${encodeURIComponent(device.id)}/esim`);
return (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);
const currentICCID = String(device.modem?.iccid || "").trim();
let installed: ProfileProxyCandidate[] = [];
try {
const data = await api<EsimOverview>(`/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<string, ProfileProxyCandidate>();
@@ -65,8 +84,6 @@ export function DeviceBindingsDialog(props: DeviceBindingsDialogProps) {
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)));
}).catch((error) => {
if (active) message.error(apiMessage(error) || t("读取 eSIM Profile 失败"));
}).finally(() => {
if (active) setLoadingProfiles(false);
});
@@ -94,13 +111,13 @@ export function DeviceBindingsDialog(props: DeviceBindingsDialogProps) {
const toggleAll = () => setSelected(allSelected ? [] : selectable);
return (
<Modal open={open} onClose={onClose} title={`${adding ? t("添加 Profile 绑定") : t("Profile 绑定")}${proxyName}`} width="max-w-5xl">
<Modal open={open} onClose={onClose} title={`${adding ? t("添加 SIM / Profile 绑定") : t("SIM / Profile 绑定")}${proxyName}`} width="max-w-5xl">
<div className="space-y-4 pb-2">
<div className="rounded-lg border border-sky-200/70 bg-sky-50 px-3 py-2 text-xs text-sky-800 dark:border-sky-800/50 dark:bg-sky-900/20 dark:text-sky-200">
{t("VoWiFi 会按当前 ICCID 选择代理。同一 ICCID 只能绑定一个代理,一个代理可以绑定多台设备上的多个 Profile。")}
{t("VoWiFi 会按当前 ICCID 选择代理。实体 SIM 和 eSIM Profile 都可以绑定;同一 ICCID 只能绑定一个代理。")}
</div>
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="text-xs text-gray-500">{adding ? t("从设备已安装的 eSIM Profile 中选择") : `${current.length} ${t("个 Profile")}`}</div>
<div className="text-xs text-gray-500">{adding ? t("从当前 SIM 卡和已安装的 eSIM Profile 中选择") : `${current.length} ${t("个 SIM / Profile")}`}</div>
<div className="flex gap-2">
{adding ? (
<Button size="small" onClick={() => { setAdding(false); setSelected([]); }}>{t("返回绑定列表")}</Button>
@@ -130,7 +147,7 @@ export function DeviceBindingsDialog(props: DeviceBindingsDialogProps) {
<th className="w-12 px-4 py-3"><input type="checkbox" checked={allSelected} onChange={toggleAll} disabled={selectable.length === 0 || busy} aria-label={t("全选")} /></th>
<th className="px-4 py-3">{t("设备 ID")}</th>
<th className="px-4 py-3">ICCID</th>
<th className="px-4 py-3">{t("Profile 名称")}</th>
<th className="px-4 py-3">{t("SIM / Profile")}</th>
{adding ? <th className="px-4 py-3">{t("状态")}</th> : null}
</tr>
</thead>
@@ -155,7 +172,7 @@ export function DeviceBindingsDialog(props: DeviceBindingsDialogProps) {
</tbody>
</table>
{loadingProfiles ? <div className="px-6 py-12 text-center text-sm text-gray-400">{t("读取 Profile 中...")}</div> : null}
{!loadingProfiles && rows.length === 0 ? <EmptyState title={adding ? t("没有可显示的 eSIM Profile") : t("尚未绑定 Profile")} subtitle={adding ? t("请确认设备在线且支持 eSIM Profile 列表读取。") : t("点击添加,从设备 Profile 列表中选择。")}/>: null}
{!loadingProfiles && rows.length === 0 ? <EmptyState title={adding ? t("没有可显示的 SIM / Profile") : t("尚未绑定 SIM / Profile")} subtitle={adding ? t("请确认设备在线并已读取到 SIM 卡 ICCID。") : t("点击添加,从 SIM / Profile 列表中选择。")}/>: null}
</div>
</div>
</Modal>
+4 -4
View File
@@ -38,7 +38,7 @@ export function UpstreamSection({ rows, loading, error, onRetry, onEdit, onDelet
<th className="px-4 py-3">{t("地址")}</th>
<th className="px-4 py-3">{t("鉴权")}</th>
<th className="px-4 py-3">{t("状态")}</th>
<th className="px-4 py-3">{t("Profile 绑定")}</th>
<th className="px-4 py-3">{t("SIM / Profile 绑定")}</th>
<th className="px-4 py-3 text-right">{t("操作")}</th>
</tr>
</thead>
@@ -53,12 +53,12 @@ export function UpstreamSection({ rows, loading, error, onRetry, onEdit, onDelet
<td className="px-4 py-3">
<div className="inline-flex items-center gap-1 rounded border border-indigo-200/60 bg-indigo-50 px-2 py-0.5 text-[11px] font-medium text-indigo-600 dark:border-indigo-800/40 dark:bg-indigo-900/20 dark:text-indigo-400">
<DesktopRegular className="text-[14px]" />
<span>{row.bindingCount} {t("个 Profile")}</span>
<span>{row.bindingCount} {t("个 SIM / Profile")}</span>
</div>
</td>
<td className="px-4 py-3">
<div className="flex justify-end gap-2">
<Button size="small" icon={<DesktopRegular />} onClick={() => onOpenBindings(row)}>{t("Profile 绑定")}</Button>
<Button size="small" icon={<DesktopRegular />} onClick={() => onOpenBindings(row)}>{t("SIM / Profile 绑定")}</Button>
<Button size="small" icon={<EditRegular />} onClick={() => onEdit(row)}>{t("编辑")}</Button>
<Button size="small" variant="danger" plain icon={<DeleteRegular />} onClick={() => onDelete(row)}>{t("删除")}</Button>
</div>
@@ -72,7 +72,7 @@ export function UpstreamSection({ rows, loading, error, onRetry, onEdit, onDelet
<div className="flex flex-col items-center justify-center px-6 py-16 text-center text-gray-400">
<GlobeRegular className="mb-3 text-4xl" />
<div className="text-sm">{t("暂无上游代理")}</div>
<div className="mt-1 text-xs">{t("点击“新增代理”创建 SOCKS5 上游代理,再按 ICCID 绑定需要使用它的 eSIM Profile;未绑定 Profile 默认直连。")}</div>
<div className="mt-1 text-xs">{t("点击“新增代理”创建 SOCKS5 上游代理,再按 ICCID 绑定实体 SIM 或 eSIM Profile;未绑定的卡默认直连。")}</div>
</div>
) : null}
{loading ? <div className="px-6 py-16 text-center text-sm text-gray-400">{t("加载中...")}</div> : null}