mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-18 22:03:44 +08:00
feat: add custom phone number support to card policies
- Introduced a new field `custom_phone_number` in the CardPolicy model and database schema. - Updated the API to handle custom phone number input, including validation and normalization. - Modified the CardPolicyPanel component to allow users to set and save a custom phone number. - Enhanced the settings API to include the custom phone number in responses and updates. - Added tests to ensure the correct functionality of custom phone number handling. - Removed hardcoded environment variable for VOCAT_ADDR in service files.
This commit is contained in:
@@ -1,25 +1,30 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { CardUiRegular } from "@fluentui/react-icons";
|
||||
import { Tag } from "../ui";
|
||||
import { Button, Input, Tag, message } from "../ui";
|
||||
import { PolicySwitchCard } from "./PolicySwitchCard";
|
||||
import { CardPolicyAPN } from "./CardPolicyAPN";
|
||||
import { useCardPolicyToggles } from "./useCardPolicyToggles";
|
||||
import { enableVoWiFi, disableVoWiFi, setFlightMode } from "./deviceActions";
|
||||
import { enableVoWiFi, disableVoWiFi, setFlightMode, updateCardPolicy } from "./deviceActions";
|
||||
import type { CardPolicy } from "../../types";
|
||||
import { useI18n } from "../../lib/i18n";
|
||||
import { apiMessage } from "../../api";
|
||||
|
||||
export interface CardPolicyPanelProps {
|
||||
deviceId: string;
|
||||
iccid?: string;
|
||||
policy: CardPolicy | null;
|
||||
deviceOnline: boolean;
|
||||
onPolicyChanged: () => void;
|
||||
onPolicyChanged: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function CardPolicyPanel({ deviceId, iccid, policy, deviceOnline, onPolicyChanged }: CardPolicyPanelProps) {
|
||||
const { t } = useI18n();
|
||||
const operable = deviceOnline && !!iccid;
|
||||
const flags = policy
|
||||
? { vowifiEnabled: policy.vowifiEnabled, airplaneEnabled: policy.airplaneEnabled }
|
||||
const currentPolicy = policy?.iccid === iccid ? policy : null;
|
||||
const [customPhoneNumber, setCustomPhoneNumber] = useState(currentPolicy?.customPhoneNumber || "");
|
||||
const [phoneSaving, setPhoneSaving] = useState(false);
|
||||
const flags = currentPolicy
|
||||
? { vowifiEnabled: currentPolicy.vowifiEnabled, airplaneEnabled: currentPolicy.airplaneEnabled }
|
||||
: null;
|
||||
|
||||
const toggles = useCardPolicyToggles(flags, {
|
||||
@@ -28,9 +33,30 @@ export function CardPolicyPanel({ deviceId, iccid, policy, deviceOnline, onPolic
|
||||
onChanged: onPolicyChanged,
|
||||
});
|
||||
|
||||
const isManual = policy?.source === "user" || policy?.source === "manual";
|
||||
const sourceLabel = policy ? (isManual ? t("手动设置") : t("自动默认")) : "";
|
||||
const isManual = currentPolicy?.source === "user" || currentPolicy?.source === "manual";
|
||||
const sourceLabel = currentPolicy ? (isManual ? t("手动设置") : t("自动默认")) : "";
|
||||
const { local } = toggles;
|
||||
const savedPhoneNumber = currentPolicy?.customPhoneNumber || "";
|
||||
const phoneChanged = customPhoneNumber.trim() !== savedPhoneNumber;
|
||||
|
||||
useEffect(() => {
|
||||
setCustomPhoneNumber(currentPolicy?.customPhoneNumber || "");
|
||||
}, [iccid, currentPolicy?.customPhoneNumber]);
|
||||
|
||||
const saveCustomPhoneNumber = async () => {
|
||||
if (!iccid || phoneSaving || !phoneChanged) return;
|
||||
setPhoneSaving(true);
|
||||
try {
|
||||
const saved = await updateCardPolicy(iccid, { customPhoneNumber: customPhoneNumber.trim() });
|
||||
setCustomPhoneNumber(saved.customPhoneNumber || "");
|
||||
message.success(saved.customPhoneNumber ? t("自定义手机号已保存") : t("已恢复显示系统读取的号码"));
|
||||
await onPolicyChanged();
|
||||
} catch (error) {
|
||||
message.error(apiMessage(error) || t("保存自定义手机号失败"));
|
||||
} finally {
|
||||
setPhoneSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -53,12 +79,44 @@ export function CardPolicyPanel({ deviceId, iccid, policy, deviceOnline, onPolic
|
||||
) : null}
|
||||
{iccid ? (
|
||||
<div className="space-y-3">
|
||||
<div className="ui-panel-muted flex items-center justify-between p-3">
|
||||
<div>
|
||||
<div className="mb-0.5 text-xs font-bold uppercase tracking-wider text-gray-500">{t("当前卡 ICCID")}</div>
|
||||
<div className="font-mono text-sm text-gray-800 dark:text-gray-100">{iccid}</div>
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
<div className="ui-panel-muted flex min-w-0 items-center justify-between gap-3 p-3">
|
||||
<div className="min-w-0">
|
||||
<div className="mb-0.5 text-xs font-bold uppercase tracking-wider text-gray-500">{t("当前卡 ICCID")}</div>
|
||||
<div className="truncate font-mono text-sm text-gray-800 dark:text-gray-100" title={iccid}>{iccid}</div>
|
||||
</div>
|
||||
{sourceLabel ? <Tag type={isManual ? "primary" : "info"}>{sourceLabel}</Tag> : null}
|
||||
</div>
|
||||
<div className="ui-panel-muted p-3">
|
||||
<div className="mb-1.5 text-xs font-bold uppercase tracking-wider text-gray-500">{t("自定义手机号")}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={customPhoneNumber}
|
||||
onChange={(event) => setCustomPhoneNumber(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") void saveCustomPhoneNumber();
|
||||
}}
|
||||
placeholder={t("请输入手机号(可留空)")}
|
||||
inputMode="tel"
|
||||
maxLength={32}
|
||||
disabled={phoneSaving}
|
||||
aria-label={t("自定义手机号")}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
className="shrink-0 !border-0"
|
||||
loading={phoneSaving}
|
||||
disabled={!phoneChanged}
|
||||
onClick={() => void saveCustomPhoneNumber()}
|
||||
>
|
||||
{t("保存")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-1.5 text-[11px] leading-4 text-gray-500 dark:text-gray-400">
|
||||
{t("支持开头的 + 和 3-20 位数字;留空时显示系统从 SIM/网络读取的号码")}
|
||||
</div>
|
||||
</div>
|
||||
{sourceLabel ? <Tag type={isManual ? "primary" : "info"}>{sourceLabel}</Tag> : null}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
<PolicySwitchCard
|
||||
@@ -85,7 +143,7 @@ export function CardPolicyPanel({ deviceId, iccid, policy, deviceOnline, onPolic
|
||||
<CardPolicyAPN
|
||||
deviceId={deviceId}
|
||||
iccid={iccid}
|
||||
policy={policy}
|
||||
policy={currentPolicy}
|
||||
deviceOnline={deviceOnline}
|
||||
onSaved={onPolicyChanged}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { isVoWiFiInUse } from "./shared";
|
||||
export interface DeviceOverviewTabProps {
|
||||
device: DeviceDetail;
|
||||
simOperatorDisplay: string;
|
||||
customPhoneNumber?: string;
|
||||
trafficSpeedRx: string;
|
||||
trafficSpeedTx: string;
|
||||
trafficMinuteRx: string;
|
||||
@@ -39,6 +40,7 @@ export function DeviceOverviewTab(props: DeviceOverviewTabProps) {
|
||||
<OverviewSimPanel
|
||||
device={device}
|
||||
simOperatorDisplay={props.simOperatorDisplay}
|
||||
customPhoneNumber={props.customPhoneNumber}
|
||||
e911Starting={props.e911Starting}
|
||||
onSetupE911={props.onSetupE911}
|
||||
/>
|
||||
|
||||
@@ -10,11 +10,12 @@ import { CountryFlag } from "../CountryFlag";
|
||||
export interface OverviewSimPanelProps {
|
||||
device: DeviceDetail;
|
||||
simOperatorDisplay: string;
|
||||
customPhoneNumber?: string;
|
||||
e911Starting: boolean;
|
||||
onSetupE911: () => void;
|
||||
}
|
||||
|
||||
export function OverviewSimPanel({ device, simOperatorDisplay, e911Starting, onSetupE911 }: OverviewSimPanelProps) {
|
||||
export function OverviewSimPanel({ device, simOperatorDisplay, customPhoneNumber, e911Starting, onSetupE911 }: OverviewSimPanelProps) {
|
||||
const { t } = useI18n();
|
||||
const [showSensitive, toggleSensitive] = useShowSensitive();
|
||||
const modem = device.modem;
|
||||
@@ -22,6 +23,7 @@ export function OverviewSimPanel({ device, simOperatorDisplay, e911Starting, onS
|
||||
const activeEsim = (device.activeEsimProfileName || "").trim();
|
||||
const flightOn = device.vowifiActive || modem?.operatingMode === 0 || modem?.operatingMode === 4;
|
||||
const carrierCountryCode = carrierBrandIso(modem?.nativeSpn, modem?.imsi);
|
||||
const displayedPhoneNumber = customPhoneNumber?.trim() || device.localPhone || "--";
|
||||
const backendLabel =
|
||||
device.backendMode === "qmi" ? "QMI" : device.backendMode === "mbim" ? "MBIM" : device.backendMode === "at" ? "AT" : "Auto";
|
||||
|
||||
@@ -40,7 +42,7 @@ export function OverviewSimPanel({ device, simOperatorDisplay, e911Starting, onS
|
||||
<FieldRow label="IMEI" value={modem?.imei} sensitive={sensitive} monospace copyable />
|
||||
<FieldRow label="ICCID" value={modem?.iccid} sensitive={sensitive} monospace copyable />
|
||||
<FieldRow label="IMSI" value={modem?.imsi} sensitive={sensitive} monospace copyable />
|
||||
<FieldRow label={t("本机号码")} value={device.localPhone || "--"} sensitive={sensitive} monospace copyable />
|
||||
<FieldRow label={t("本机号码")} value={displayedPhoneNumber} sensitive={sensitive} monospace copyable />
|
||||
{device?.e911SetupAvailable ? (
|
||||
<div className="flex justify-between gap-3">
|
||||
<span className="text-gray-500">{t("E911地址")}</span>
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface CardPolicyUpdate {
|
||||
airplaneEnabled?: boolean;
|
||||
apn?: string;
|
||||
ipVersion?: "IP" | "IPV6" | "IPV4V6";
|
||||
customPhoneNumber?: string;
|
||||
}
|
||||
export function updateCardPolicy(iccid: string, body: CardPolicyUpdate) {
|
||||
return api<CardPolicy>(`/cards/${iccid}/policy`, { method: "PUT", body });
|
||||
|
||||
@@ -5,6 +5,52 @@
|
||||
* 富文本片段(嵌套链接/代码块的说明框)不走字典,在组件里按语言分支渲染。
|
||||
*/
|
||||
export const EN_DICT: Record<string, string> = {
|
||||
// Cellular APN profiles.
|
||||
"蜂窝 APN": "Cellular APN",
|
||||
"APN 列表和启用状态跟随当前 ICCID/Profile 保存": "APN profiles and the active selection are saved per ICCID/Profile",
|
||||
"新增 APN": "Add APN",
|
||||
"正在读取 APN 列表...": "Loading APN profiles...",
|
||||
"账号 / 认证": "Account / Authentication",
|
||||
"协议 / 漫游": "Protocol / Roaming",
|
||||
来源: "Source",
|
||||
"运营商自动配置": "Carrier Automatic",
|
||||
默认: "Default",
|
||||
"模组已有": "Modem",
|
||||
自定义: "Custom",
|
||||
漫游: "Roaming",
|
||||
"已设密码": "Password set",
|
||||
使用中: "Active",
|
||||
"APN 已启用": "APN enabled",
|
||||
"已使用运营商自动 APN 配置": "Carrier automatic APN configuration enabled",
|
||||
"启用 APN 失败": "Failed to enable APN",
|
||||
"设备离线:自定义列表仍可管理,模组已有 APN 将在上线后读取":
|
||||
"Device offline: custom profiles can still be managed; modem APNs will load when the device comes online",
|
||||
"新增 APN 配置": "Add APN Profile",
|
||||
"修改 APN 配置": "Edit APN Profile",
|
||||
"保存修改": "Save Changes",
|
||||
"添加到列表": "Add to List",
|
||||
留空: "Leave blank",
|
||||
"留空表示保持原密码": "Leave blank to keep the current password",
|
||||
"APN 协议": "APN Protocol",
|
||||
"APN 漫游协议": "APN Roaming Protocol",
|
||||
"认证类型": "Authentication Type",
|
||||
"清除已保存密码": "Clear Saved Password",
|
||||
"关闭时,密码输入框留空会保持原密码": "When disabled, leaving the password blank keeps the current password",
|
||||
"APN 只能包含字母、数字、点、下划线或连字符,且最长 100 个字符":
|
||||
"APN may contain only letters, numbers, dots, underscores, or hyphens, with a maximum of 100 characters",
|
||||
"MCC 必须是 3 位数字": "MCC must be exactly 3 digits",
|
||||
"MNC 必须是 2 或 3 位数字": "MNC must be 2 or 3 digits",
|
||||
"自定义 APN 已添加,请点击启用后使用": "Custom APN added. Click Enable to use it",
|
||||
"自定义 APN 已修改": "Custom APN updated",
|
||||
"添加 APN 失败": "Failed to add APN",
|
||||
"修改 APN 失败": "Failed to update APN",
|
||||
"确定删除这个自定义 APN 配置吗?": "Delete this custom APN profile?",
|
||||
"删除 APN": "Delete APN",
|
||||
"APN 已删除,并恢复运营商自动配置": "APN deleted; carrier automatic configuration restored",
|
||||
"自定义 APN 已删除": "Custom APN deleted",
|
||||
"删除 APN 失败": "Failed to delete APN",
|
||||
修改: "Edit",
|
||||
|
||||
// External extensions.
|
||||
"插件": "Plugins",
|
||||
"通过 URL 或本地插件包扩展 VoCat 功能": "Extend VoCat with a URL or a local plugin package",
|
||||
@@ -939,6 +985,15 @@ export const EN_DICT: Record<string, string> = {
|
||||
"大疆 4G 模块(移远芯片)": "DJI 4G Module (Quectel)",
|
||||
"PCIe EC20/EC25(移远芯片)": "PCIe EC20/EC25 (Quectel)",
|
||||
|
||||
// ---- Per-Profile phone display override ----
|
||||
"自定义手机号": "Custom Phone Number",
|
||||
"请输入手机号(可留空)": "Enter a phone number (optional)",
|
||||
"自定义手机号已保存": "Custom phone number saved",
|
||||
"已恢复显示系统读取的号码": "Restored the system-detected number",
|
||||
"保存自定义手机号失败": "Failed to save custom phone number",
|
||||
"支持开头的 + 和 3-20 位数字;留空时显示系统从 SIM/网络读取的号码":
|
||||
"Supports a leading + and 3–20 digits. Leave blank to show the number read from the SIM/network.",
|
||||
|
||||
// ---- AT 快捷指令(按 group · item 分组翻译) ----
|
||||
基础: "Basics",
|
||||
网络控制: "Network Control",
|
||||
|
||||
@@ -589,6 +589,7 @@ export default function DevicesPage() {
|
||||
<DeviceOverviewTab
|
||||
device={detail}
|
||||
simOperatorDisplay={simOperator}
|
||||
customPhoneNumber={cardPolicy?.iccid === detail.modem?.iccid ? cardPolicy.customPhoneNumber : ""}
|
||||
trafficSpeedRx={''}
|
||||
trafficSpeedTx={''}
|
||||
trafficMinuteRx={''}
|
||||
|
||||
@@ -230,6 +230,7 @@ export interface CardPolicy {
|
||||
airplaneEnabled: boolean;
|
||||
apn?: string;
|
||||
ipVersion?: string;
|
||||
customPhoneNumber?: string;
|
||||
source?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
|
||||
Reference in New Issue
Block a user