import { useEffect, useMemo, useState } from 'react'; import { Copy, Edit3, Plus, Search, Settings2, Trash2 } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { Breadcrumb, Button, Input, Modal, Table, Tabs, Tag, type TableColumn } from '@/components/ui'; import { adminApi, type ApplicationCmppParams, type CmppConnectionState, type EnterpriseApplication, type TenantOption } from '@/api/adminApi'; type SmsApp = { id: string; tenantId: string; name: string; enterprise: string; appId: string; enabled: boolean; sentToday: number; deliveryRate: number; unitPrice: number; cmppStatus: 'connected' | 'disconnected' | 'inactive'; cmppConnections: CmppConnection[]; cmppParams: CmppParams; }; type CmppParams = { host: string; port: number; enterpriseCode: string; account: string; password: string; accessNumber: string; maxConnections: number; heartbeatSeconds: number; windowSize: number; protocolVersion: string; }; type CmppConnection = { id: string; state: 'open' | 'closed' | 'reconnecting'; bindType: 'transceiver' | 'submitter' | 'receiver'; clientIp: string; sourceAddr: string; establishedAt: string; lastHeartbeatAt: string; lastSubmitAt: string; pendingWindow: number; }; function enabledTag(enabled: boolean) { return {enabled ? '启用' : '停用'}; } function ConfirmModal({ message, danger, onCancel, onConfirm }: { message: string; danger?: boolean; onCancel: () => void; onConfirm: () => void }) { return ( )} onClose={onCancel} open title="操作确认" >

{message}

); } function AddApplicationModal({ tenants, loading, selectedTenantId, onChange, onCancel, onConfirm, }: { tenants: TenantOption[]; loading: boolean; selectedTenantId: string; onChange: (tenantId: string) => void; onCancel: () => void; onConfirm: () => void; }) { return ( )} onClose={onCancel} open title={

新建企业应用

先选择真实企业,再配置短信应用和三网通道组。

} >
{selectedTenantId ? tenants.find((tenant) => tenant.id === selectedTenantId)?.name : '请选择要开通短信应用的企业'} 下一步会进入应用参数、客户单价、IP 白名单和运营商通道组配置。
); } const connectionStateMeta: Record = { open: { label: '已连接', tone: 'success' }, closed: { label: '已断开', tone: 'neutral' }, reconnecting: { label: '重连中', tone: 'warning' }, }; function formatCmppParams(app: SmsApp, params?: ApplicationCmppParams | null) { const cmppParams = params ?? app.cmppParams; return [ `应用名称: ${app.name}`, `企业名称: ${app.enterprise}`, `AppID: ${app.appId}`, `CMPP网关地址: ${'gatewayHost' in cmppParams ? cmppParams.gatewayHost : cmppParams.host}`, `CMPP网关端口: ${'gatewayPort' in cmppParams ? cmppParams.gatewayPort : cmppParams.port}`, `企业代码: ${cmppParams.enterpriseCode}`, `接口账号: ${cmppParams.account}`, `接口密码: ${'passwordCipher' in cmppParams ? cmppParams.passwordCipher : cmppParams.password}`, `接入号: ${'srcId' in cmppParams ? cmppParams.srcId : cmppParams.accessNumber}`, `最大连接数: ${cmppParams.maxConnections}`, `心跳间隔: ${cmppParams.heartbeatSeconds}秒`, `提交窗口: ${cmppParams.windowSize}`, `协议版本: ${cmppParams.protocolVersion}`, ].join('\n'); } function CmppParamsModal({ app, params, onClose }: { app: SmsApp; params?: ApplicationCmppParams | null; onClose: () => void }) { const [copied, setCopied] = useState(false); const paramsText = formatCmppParams(app, params); const host = params?.gatewayHost ?? app.cmppParams.host; const port = params?.gatewayPort ?? app.cmppParams.port; const password = params?.passwordCipher ?? app.cmppParams.password; const srcId = params?.srcId ?? app.cmppParams.accessNumber; async function copyParams() { await navigator.clipboard.writeText(paramsText); setCopied(true); window.setTimeout(() => setCopied(false), 1600); } return ( )} onClose={onClose} open size="xl" title={

CMPP连接参数

{app.enterprise} / {app.name}

} >
CMPP网关地址{host}
CMPP网关端口{port}
企业代码{params?.enterpriseCode ?? app.cmppParams.enterpriseCode}
接口账号{params?.account ?? app.cmppParams.account}
接口密码{password}
接入号{srcId}
最大连接数{params?.maxConnections ?? app.cmppParams.maxConnections}
心跳间隔{params?.heartbeatSeconds ?? app.cmppParams.heartbeatSeconds} 秒
提交窗口{params?.windowSize ?? app.cmppParams.windowSize}
协议版本{params?.protocolVersion ?? app.cmppParams.protocolVersion}
{paramsText}
); } function CmppConnectionModal({ app, onClose, onDeleteConnection, }: { app: SmsApp; onClose: () => void; onDeleteConnection: (connectionId: string) => void; }) { const activeConnections = app.cmppConnections.filter((item) => item.state === 'open').length; return ( 关闭} onClose={onClose} open size="xl" title={

CMPP连接详情

{app.enterprise} / {app.name}

} >
当前连接数{activeConnections}
配置连接数{Math.max(activeConnections, app.cmppConnections.length)}
AppID{app.appId}
连接状态{app.cmppStatus === 'connected' ? '在线' : app.cmppStatus === 'disconnected' ? '离线' : '未开通'}
{record.id} }, { key: 'state', title: '状态', width: '130px', render: (record: CmppConnection) => {connectionStateMeta[record.state].label} }, { key: 'bindType', title: '绑定类型', width: '120px', render: (record: CmppConnection) => record.bindType }, { key: 'clientIp', title: '客户端IP', width: '170px', render: (record: CmppConnection) => record.clientIp }, { key: 'sourceAddr', title: '企业代码', width: '120px', render: (record: CmppConnection) => record.sourceAddr }, { key: 'establishedAt', title: '连接建立时间', width: '180px', render: (record: CmppConnection) => record.establishedAt }, { key: 'lastHeartbeatAt', title: '上次心跳', width: '180px', render: (record: CmppConnection) => record.lastHeartbeatAt }, { key: 'lastSubmitAt', title: '上次提交', width: '180px', render: (record: CmppConnection) => record.lastSubmitAt }, { key: 'pendingWindow', title: '窗口占用', align: 'right', width: '120px', render: (record: CmppConnection) => record.pendingWindow }, { key: 'actions', title: '操作', align: 'right', width: '120px', render: (record: CmppConnection) => ( ), }, ]} data={app.cmppConnections} emptyText="暂无CMPP连接" rowKey="id" /> ); } export function AdminEnterpriseApplicationsPage() { const navigate = useNavigate(); const [smsApps, setSmsApps] = useState([]); const [enterpriseKeyword, setEnterpriseKeyword] = useState(''); const [connectionApp, setConnectionApp] = useState(null); const [paramsApp, setParamsApp] = useState(null); const [paramsDetail, setParamsDetail] = useState(null); const [error, setError] = useState(''); const [addModalOpen, setAddModalOpen] = useState(false); const [tenants, setTenants] = useState([]); const [tenantsLoading, setTenantsLoading] = useState(false); const [selectedTenantId, setSelectedTenantId] = useState(''); const [confirmAction, setConfirmAction] = useState< | { action: 'toggle'; id: string; name: string; enabled: boolean } | { action: 'delete'; id: string; name: string } | null >(null); async function loadSmsApps() { try { const applications = await adminApi.listEnterpriseApplications({ keyword: enterpriseKeyword }); setSmsApps(applications.map(mapApplication)); setError(''); } catch (err) { setSmsApps([]); setError(err instanceof Error ? err.message : '企业应用加载失败'); } } useEffect(() => { void loadSmsApps(); }, [enterpriseKeyword]); async function openAddModal() { setAddModalOpen(true); if (tenants.length === 0) { setTenantsLoading(true); try { setTenants((await adminApi.listTenants()).filter((tenant) => tenant.status !== 'deleted')); } catch (err) { setError(err instanceof Error ? err.message : '企业列表加载失败'); } finally { setTenantsLoading(false); } } } function confirmAddApplication() { if (selectedTenantId) { navigate(`/admin/customers/${selectedTenantId}/sms-apps/new`); } } async function confirmToggle(id: string) { const app = smsApps.find((item) => item.id === id); if (app) { await adminApi.changeApplicationStatus(id, app.enabled ? 'disabled' : 'active', '运营端企业应用管理'); await loadSmsApps(); } } async function confirmDelete(id: string) { await adminApi.changeApplicationStatus(id, 'deleted', '运营端删除应用'); await loadSmsApps(); } async function runConfirmedAction() { if (!confirmAction) { return; } if (confirmAction.action === 'toggle') { await confirmToggle(confirmAction.id); } else { await confirmDelete(confirmAction.id); } setConfirmAction(null); } async function deleteConnection(appId: string, connectionId: string) { await adminApi.disconnectApplicationConnection(appId, connectionId, '运营端断开企业应用 CMPP 连接'); const data = await adminApi.listApplicationConnections(appId); const nextApp = mapApplication({ ...data.application, cmppConnections: data.connections, cmppStatus: data.summary.status as EnterpriseApplication['cmppStatus'] }); setConnectionApp(nextApp); await loadSmsApps(); } async function openParams(app: SmsApp) { setParamsApp(app); setParamsDetail(await adminApi.getApplicationCmppParams(app.id)); } const filteredSmsApps = useMemo( () => smsApps.filter((item) => !enterpriseKeyword || item.enterprise.includes(enterpriseKeyword)), [enterpriseKeyword, smsApps], ); const smsColumns = useMemo>>(() => [ { key: 'name', title: '应用名称', width: '180px', render: (record) => {record.name} }, { key: 'enterprise', title: '企业名称', width: '220px', render: (record) => record.enterprise }, { key: 'appId', title: 'AppID', width: '220px', render: (record) => record.appId }, { key: 'sentToday', title: '今日发送', width: '120px', render: (record) => `${record.sentToday.toLocaleString('zh-CN')} 条` }, { key: 'deliveryRate', title: '到达率', width: '130px', render: (record) => `${record.deliveryRate}%` }, { key: 'unitPrice', title: '单价', width: '130px', render: (record) => `${record.unitPrice.toFixed(3)} 元` }, { key: 'cmppStatus', title: 'CMPP状态', width: '230px', render: (record) => (
{record.cmppStatus === 'connected' ? '已连接' : record.cmppStatus === 'disconnected' ? '已断开' : '未开通'}
), }, { key: 'enabled', title: '状态', width: '130px', render: (record) => enabledTag(record.enabled) }, { key: 'actions', title: '操作', align: 'right', width: '190px', render: (record) => (
), }, ], [navigate]); return (

企业应用管理

setEnterpriseKeyword(event.target.value)} placeholder="请输入企业名称" prefix={} value={enterpriseKeyword} />
{error ?
{error}
: null}
}, { label: '彩信应用', value: 'mms', pending: true, content:
彩信应用待后端能力确认,本页不展示演示数据。
}, ]} />
{confirmAction ? ( setConfirmAction(null)} onConfirm={() => { void runConfirmedAction(); }} /> ) : null} {addModalOpen ? ( setAddModalOpen(false)} onChange={setSelectedTenantId} onConfirm={confirmAddApplication} selectedTenantId={selectedTenantId} tenants={tenants} /> ) : null} {connectionApp ? ( setConnectionApp(null)} onDeleteConnection={(connectionId) => { void deleteConnection(connectionApp.id, connectionId); }} /> ) : null} {paramsApp ? { setParamsApp(null); setParamsDetail(null); }} /> : null}
); } function mapApplication(application: EnterpriseApplication): SmsApp { const connections = (application.cmppConnections ?? []).map(mapConnection); return { id: application.id, tenantId: application.tenantId, name: application.name, enterprise: application.tenant?.name ?? application.tenantId, appId: application.id, enabled: application.status === 'active', sentToday: application.sentToday ?? 0, deliveryRate: application.deliveryRate ?? 0, unitPrice: (application.customerUnitPrice ?? 0) / 100, cmppStatus: application.cmppStatus === 'connected' ? 'connected' : application.cmppStatus === 'inactive' ? 'inactive' : 'disconnected', cmppParams: { host: '', port: 0, enterpriseCode: application.tenant?.code ?? application.tenantId, account: application.tenant?.code ?? application.tenantId, password: '', accessNumber: '', maxConnections: 0, heartbeatSeconds: 30, windowSize: 16, protocolVersion: 'CMPP 3.0' }, cmppConnections: connections, }; } function mapConnection(connection: CmppConnectionState): CmppConnection { const isOpen = ['online', 'connected', 'open'].includes(connection.status) && connection.currentConnections > 0; return { id: connection.connectionId, state: isOpen ? 'open' : connection.status === 'reconnecting' ? 'reconnecting' : 'closed', bindType: 'transceiver', clientIp: String(connection.channel?.gatewayHost ?? ''), sourceAddr: String(connection.channel?.enterpriseCode ?? ''), establishedAt: connection.lastConnectedAt ? new Date(connection.lastConnectedAt).toLocaleString('zh-CN') : '', lastHeartbeatAt: connection.lastHeartbeatAt ? new Date(connection.lastHeartbeatAt).toLocaleString('zh-CN') : '', lastSubmitAt: connection.updatedAt ? new Date(connection.updatedAt).toLocaleString('zh-CN') : '', pendingWindow: connection.currentConnections, }; }