import { useState } from 'react'; import { Button, Checkbox, Field, Input, Select } from '../components/ui.jsx'; import { Icon, PageTitle, Toolbar, Panel, ApiNotice, Modal, ConfirmDialog, SimpleTable } from '../components/layout.jsx'; import { vendors, gateways } from '../fixtures/devFixtures.js'; import { explainApiError } from '../api.js'; export function VendorGatewaysPage({ gatewayRows: apiGatewayRows, setGatewayRows: setApiGatewayRows, vendorRows: apiVendorRows, apiLoading, apiError, refreshApi, can = () => true, onUpdateGateway, onToggleGatewayStatus, onDeleteGateway }) { const [localGatewayRows, setLocalGatewayRows] = useState(gateways); const gatewayRows = Array.isArray(apiGatewayRows) ? apiGatewayRows : localGatewayRows; const setGatewayRows = setApiGatewayRows || setLocalGatewayRows; const vendorOptions = apiVendorRows?.length ? apiVendorRows : vendors; const [actionError, setActionError] = useState(''); const [editingGateway, setEditingGateway] = useState(null); const [gatewayConfirm, setGatewayConfirm] = useState(null); const provinceOptions = ['北京', '上海', '广东', '浙江', '江苏', '新疆', '西藏', '港澳台', '海外']; const codecOptions = ['PCMA', 'PCMU', 'G729', 'G722', 'OPUS']; const canManage = can('vendor_gateways.manage'); const splitList = (value) => (value && !['无', '-'].includes(value) ? value.split(/[,,、]/).map((item) => item.trim()).filter(Boolean) : []); const splitTimeRanges = (value) => { const ranges = String(value || '') .split(/[,,、\n]/) .map((item) => { const [start = '', end = ''] = item.split('-').map((part) => part.trim()); return { start, end }; }) .filter((item) => item.start || item.end); return ranges.length ? ranges : [{ start: '', end: '' }]; }; const splitRate = (value) => String(value || '').replace(/[^\d]/g, ''); const formatMinuteRate = (cycle, rate) => { const billingCycle = Number(cycle); const cycleRate = Number(rate); if (!billingCycle || !Number.isFinite(billingCycle) || !Number.isFinite(cycleRate)) return '-'; return `¥${((cycleRate * 60) / billingCycle).toFixed(4)}/分钟`; }; const rewritePoolRows = (value) => { const rows = Array.isArray(value) ? value.map((item) => ({ caller: item.caller || '', weight: String(item.weight || 1) })) : []; return rows.length ? rows : [{ caller: '', weight: '1' }]; }; const emptyGatewayForm = { vendor: vendorOptions[0]?.name || '', name: '', authMode: 'IP', ipAddress: '', sipAccount: '', sipPassword: '', concurrencyLimit: '', billingCycle: '60', cycleRate: '', requestRate: '', blockedProvinces: [], forbiddenPeriods: [{ start: '', end: '' }], codecs: [], landingCalleePrefix: '', callerRewritePool: [{ caller: '', weight: '1' }], }; const [gatewayForm, setGatewayForm] = useState(emptyGatewayForm); const openEditGateway = (gateway) => { setEditingGateway(gateway); const rateValue = splitRate(gateway.requestRate); setGatewayForm({ vendor: gateway.vendor, name: gateway.name, authMode: gateway.authMode, ipAddress: gateway.ipAddress || '', sipAccount: gateway.sipAccount || '', sipPassword: gateway.sipPassword || '', concurrencyLimit: String(gateway.concurrencyLimit), billingCycle: String(gateway.billingCycle || 60), cycleRate: String(gateway.cycleRate ?? ''), requestRate: rateValue, blockedProvinces: splitList(gateway.blockedProvinces), forbiddenPeriods: splitTimeRanges(gateway.callTimeLimit), codecs: splitList(gateway.codecs), landingCalleePrefix: gateway.landingCalleePrefix || '', callerRewritePool: rewritePoolRows(gateway.callerRewritePool), }); }; const closeEditGateway = () => { setEditingGateway(null); setGatewayForm(emptyGatewayForm); }; const submitGateway = async (event) => { event.preventDefault(); if (!editingGateway || !gatewayForm.name.trim() || !gatewayForm.concurrencyLimit || !gatewayForm.billingCycle || !gatewayForm.cycleRate || !gatewayForm.requestRate) return; const billingCycle = Number(gatewayForm.billingCycle); const cycleRate = Number(gatewayForm.cycleRate); const cpsLimit = Number(gatewayForm.requestRate); if (!Number.isFinite(billingCycle) || billingCycle <= 0 || billingCycle > 60 || !Number.isFinite(cycleRate) || cycleRate < 0) return; if (!Number.isInteger(cpsLimit) || cpsLimit < 1 || cpsLimit > 10000) return; const ipAddress = gatewayForm.ipAddress.trim(); const sipAccount = gatewayForm.sipAccount.trim(); const sipPassword = gatewayForm.sipPassword.trim(); const authValid = gatewayForm.authMode === 'IP' ? ipAddress : ipAddress && sipAccount; if (!authValid) return; const vendorId = vendorOptions.find((vendor) => vendor.name === gatewayForm.vendor)?.id || editingGateway.vendorId; const callerRewritePool = gatewayForm.callerRewritePool .map((item) => ({ caller: item.caller.trim(), weight: Number(item.weight || 1), status: 'ENABLED' })) .filter((item) => item.caller); const body = { vendorId, name: gatewayForm.name.trim(), authMode: gatewayForm.authMode === 'SIP注册' ? 'SIP_DIGEST' : gatewayForm.authMode, host: ipAddress, port: editingGateway.port || 5060, transport: editingGateway.transport || 'udp', sipUsername: sipAccount || undefined, cpsLimit, concurrencyLimit: Number(gatewayForm.concurrencyLimit), billingCycleSec: billingCycle, cycleRate: String(cycleRate), landingCalleePrefix: gatewayForm.landingCalleePrefix.trim() || null, callerRewritePool, forbiddenPeriods: gatewayForm.forbiddenPeriods .filter((period) => period.start || period.end) .map((period) => ({ weekdayMask: 127, startTime: `${period.start || '00:00'}:00`, endTime: `${period.end || '23:59'}:00` })), codecs: gatewayForm.codecs.map((codec, index) => ({ codec, priority: index + 1 })), prefixRules: [], }; if (sipPassword) { body.sipPassword = sipPassword; } setActionError(''); try { if (onUpdateGateway) { await onUpdateGateway(editingGateway.id, body); } else { setGatewayRows((rows) => rows.map((gateway) => ( gateway.id === editingGateway.id ? { ...gateway, vendor: gatewayForm.vendor, vendorId, name: body.name, authMode: gatewayForm.authMode, ipAddress, sipAccount, sipPassword: sipPassword ? '******' : gateway.sipPassword, concurrencyLimit: body.concurrencyLimit, billingCycle, cycleRate, requestRate: cpsLimit, blockedProvinces: gatewayForm.blockedProvinces.length ? gatewayForm.blockedProvinces.join('、') : '无', callTimeLimit: gatewayForm.forbiddenPeriods .filter((period) => period.start || period.end) .map((period) => `${period.start || '00:00'}-${period.end || '23:59'}`) .join('、') || '无', codecs: gatewayForm.codecs.length ? gatewayForm.codecs.join(', ') : '-', landingCalleePrefix: body.landingCalleePrefix || '', callerRewritePool, calleePrefixTransform: '-', callerPrefixTransform: '-', } : gateway ))); } closeEditGateway(); } catch (error) { setActionError(explainApiError(error)); } }; const toggleArrayValue = (field, value) => { setGatewayForm((form) => ({ ...form, [field]: form[field].includes(value) ? form[field].filter((item) => item !== value) : [...form[field], value], })); }; const updateCallerRewrite = (index, key, value) => { setGatewayForm((form) => ({ ...form, callerRewritePool: form.callerRewritePool.map((rule, ruleIndex) => (ruleIndex === index ? { ...rule, [key]: value } : rule)), })); }; const addCallerRewrite = () => { setGatewayForm((form) => ({ ...form, callerRewritePool: [...form.callerRewritePool, { caller: '', weight: '1' }] })); }; const removeCallerRewrite = (index) => { setGatewayForm((form) => ({ ...form, callerRewritePool: form.callerRewritePool.length > 1 ? form.callerRewritePool.filter((_, ruleIndex) => ruleIndex !== index) : [{ caller: '', weight: '1' }], })); }; const updateForbiddenPeriod = (index, key, value) => { setGatewayForm((form) => ({ ...form, forbiddenPeriods: form.forbiddenPeriods.map((period, periodIndex) => ( periodIndex === index ? { ...period, [key]: value } : period )), })); }; const addForbiddenPeriod = () => { setGatewayForm((form) => ({ ...form, forbiddenPeriods: [...form.forbiddenPeriods, { start: '', end: '' }] })); }; const removeForbiddenPeriod = (index) => { setGatewayForm((form) => ({ ...form, forbiddenPeriods: form.forbiddenPeriods.length > 1 ? form.forbiddenPeriods.filter((_, periodIndex) => periodIndex !== index) : [{ start: '', end: '' }], })); }; const toggleVendorGatewayStatus = async (gateway) => { setActionError(''); try { if (onToggleGatewayStatus) { await onToggleGatewayStatus(gateway); return; } setGatewayRows((rows) => rows.map((item) => ( item.id === gateway.id ? { ...item, status: item.status === '启用' ? '禁用' : '启用' } : item ))); } catch (error) { setActionError(explainApiError(error)); } }; const deleteVendorGateway = async (gateway) => { setActionError(''); try { if (onDeleteGateway) { await onDeleteGateway(gateway.id); return; } setGatewayRows((rows) => rows.filter((item) => item.id !== gateway.id)); } catch (error) { setActionError(explainApiError(error)); } }; return ( <> }>新增落地网关 : null} />
(row.authMode === 'IP' ? row.ipAddress : row.sipAccount) }, { key: 'requestRate', label: 'CPS' }, { key: 'concurrencyLimit', label: '并发上限' }, { key: 'minuteRate', label: '价格', render: (row) => formatMinuteRate(row.billingCycle, row.cycleRate) }, { key: 'landingCalleePrefix', label: '落地被叫前缀', render: (row) => row.landingCalleePrefix || '-' }, { key: 'callerRewriteCount', label: '指定主叫数', render: (row) => (row.callerRewritePool || []).length }, { key: 'status', label: '状态', status: true }, { key: 'actions', label: '操作', render: (row) => (
{canManage ? : null} {canManage ? ( ) : null} {canManage ? : '-'}
), }, ]} />
{editingGateway ? (
供应商名称 *}> 落地网关名称 *}> setGatewayForm({ ...gatewayForm, name: event.target.value })} placeholder="请输入落地网关名称" required /> 认证方式 *}> 落地主机/IP *}> setGatewayForm({ ...gatewayForm, ipAddress: event.target.value })} placeholder="例如 203.0.113.18 或 sip.carrier.local" required /> {gatewayForm.authMode !== 'IP' ? ( <> SIP账号 *}> setGatewayForm({ ...gatewayForm, sipAccount: event.target.value })} placeholder="请输入 SIP 账号" required /> setGatewayForm({ ...gatewayForm, sipPassword: event.target.value })} placeholder="至少 12 位" /> ) : null} 并发上限 *}> setGatewayForm({ ...gatewayForm, concurrencyLimit: event.target.value })} placeholder="例如 800" required />
费率配置 {formatMinuteRate(gatewayForm.billingCycle, gatewayForm.cycleRate)}
计费周期 *}> setGatewayForm({ ...gatewayForm, billingCycle: event.target.value })} placeholder="最大 60 秒" required /> 周期费率 *}> setGatewayForm({ ...gatewayForm, cycleRate: event.target.value })} placeholder="例如 0.02" required />
CPS *}> setGatewayForm({ ...gatewayForm, requestRate: event.target.value })} placeholder="1-10000" required />
屏蔽省份
{provinceOptions.map((province) => ( toggleArrayValue('blockedProvinces', province)} /> ))}
禁呼时段
{gatewayForm.forbiddenPeriods.map((period, index) => (
updateForbiddenPeriod(index, 'start', event.target.value)} /> updateForbiddenPeriod(index, 'end', event.target.value)} />
))}
语音编码限制
{codecOptions.map((codec) => ( toggleArrayValue('codecs', codec)} /> ))}
落地要求被叫前缀 setGatewayForm({ ...gatewayForm, landingCalleePrefix: event.target.value })} placeholder="可为空,例如 86 或 ABC" />
落地要求指定主叫
{gatewayForm.callerRewritePool.map((rule, index) => (
updateCallerRewrite(index, 'caller', event.target.value)} placeholder="指定主叫,如 02160010001" /> 权重 updateCallerRewrite(index, 'weight', event.target.value)} />
))}
) : null} {gatewayConfirm ? ( setGatewayConfirm(null)} onConfirm={() => { const { type, row } = gatewayConfirm; setGatewayConfirm(null); return type === 'delete' ? void deleteVendorGateway(row) : void toggleVendorGatewayStatus(row); }} > {gatewayConfirm.type === 'delete' ? (

确认删除落地网关「{gatewayConfirm.row.name}」吗?删除后该网关将不再参与落地。

) : (

确认{gatewayConfirm.row.status === '启用' ? '禁用' : '启用'}落地网关「{gatewayConfirm.row.name}」吗?

)}
) : null} ); }