feat: wire admin workflows to real APIs

This commit is contained in:
hectorzhao
2026-07-02 13:50:09 +08:00
parent f8c9b78c21
commit ab421cf8a7
42 changed files with 2596 additions and 250 deletions
@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react';
import { Edit3, Plus, Search, Trash2 } from 'lucide-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';
@@ -13,18 +13,74 @@ type SmsApp = {
deliveryRate: number;
unitPrice: number;
cmppStatus: 'connected' | 'disconnected' | 'inactive';
cmppConnections: CmppConnection[];
cmppParams: CmppParams;
};
type MmsApp = Omit<SmsApp, 'cmppStatus'> & {
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;
};
type MmsApp = Omit<SmsApp, 'cmppStatus' | 'cmppConnections' | 'cmppParams'> & {
pointPrice: number;
};
type AppKind = 'sms' | 'mms';
const initialSmsApps: SmsApp[] = [
{ id: 'app-1', name: '营销推广平台', enterprise: '上海XXXXX科技有限公司', appId: 'AK_2024010912345678', enabled: true, sentToday: 1500, deliveryRate: 95, unitPrice: 0.05, cmppStatus: 'connected' },
{ id: 'app-2', name: '客户服务系统', enterprise: '重庆进载数智', appId: 'AK_2024010987654321', enabled: true, sentToday: 800, deliveryRate: 90, unitPrice: 0.06, cmppStatus: 'disconnected' },
{ id: 'app-3', name: '验证码服务', enterprise: '超感世纪互三网', appId: 'AK_2024010811223344', enabled: false, sentToday: 0, deliveryRate: 0, unitPrice: 0.04, cmppStatus: 'inactive' },
{
id: 'app-1',
name: '营销推广平台',
enterprise: '上海XXXXX科技有限公司',
appId: 'AK_2024010912345678',
enabled: true,
sentToday: 1500,
deliveryRate: 95,
unitPrice: 0.05,
cmppStatus: 'connected',
cmppParams: { host: '127.0.0.1', port: 7890, enterpriseCode: '900123', account: 'AC900123', password: 'PW-9x8k2m', accessNumber: '106900123', maxConnections: 2, heartbeatSeconds: 30, windowSize: 32, protocolVersion: 'CMPP 2.0' },
cmppConnections: [
{ id: 'CMPP-001-A', state: 'open', bindType: 'transceiver', clientIp: '10.24.8.12:32516', sourceAddr: '900123', establishedAt: '2026-07-02 08:42:11', lastHeartbeatAt: '2026-07-02 10:18:32', lastSubmitAt: '2026-07-02 10:17:58', pendingWindow: 18 },
{ id: 'CMPP-001-B', state: 'open', bindType: 'submitter', clientIp: '10.24.8.13:32520', sourceAddr: '900123', establishedAt: '2026-07-02 08:43:02', lastHeartbeatAt: '2026-07-02 10:18:28', lastSubmitAt: '2026-07-02 10:18:06', pendingWindow: 11 },
],
},
{
id: 'app-2',
name: '客户服务系统',
enterprise: '重庆进载数智',
appId: 'AK_2024010987654321',
enabled: true,
sentToday: 800,
deliveryRate: 90,
unitPrice: 0.06,
cmppStatus: 'disconnected',
cmppParams: { host: '127.0.0.1', port: 7891, enterpriseCode: '901778', account: 'AC901778', password: 'PW-4n7q1a', accessNumber: '106901778', maxConnections: 1, heartbeatSeconds: 30, windowSize: 16, protocolVersion: 'CMPP 2.0' },
cmppConnections: [
{ id: 'CMPP-002-A', state: 'closed', bindType: 'transceiver', clientIp: '10.24.9.21:31888', sourceAddr: '901778', establishedAt: '2026-07-02 07:55:19', lastHeartbeatAt: '2026-07-02 09:21:44', lastSubmitAt: '2026-07-02 09:20:17', pendingWindow: 0 },
],
},
{ id: 'app-3', name: '验证码服务', enterprise: '超感世纪互三网', appId: 'AK_2024010811223344', enabled: false, sentToday: 0, deliveryRate: 0, unitPrice: 0.04, cmppStatus: 'inactive', cmppParams: { host: '127.0.0.1', port: 7892, enterpriseCode: '902456', account: 'AC902456', password: 'PW-2d6f8p', accessNumber: '106902456', maxConnections: 0, heartbeatSeconds: 30, windowSize: 16, protocolVersion: 'CMPP 2.0' }, cmppConnections: [] },
];
const initialMmsApps: MmsApp[] = [
@@ -55,11 +111,136 @@ function ConfirmModal({ message, danger, onCancel, onConfirm }: { message: strin
);
}
const connectionStateMeta: Record<CmppConnection['state'], { label: string; tone: 'success' | 'warning' | 'neutral' }> = {
open: { label: '已连接', tone: 'success' },
closed: { label: '已断开', tone: 'neutral' },
reconnecting: { label: '重连中', tone: 'warning' },
};
function formatCmppParams(app: SmsApp) {
const { cmppParams } = app;
return [
`应用名称: ${app.name}`,
`企业名称: ${app.enterprise}`,
`AppID: ${app.appId}`,
`CMPP网关地址: ${cmppParams.host}`,
`CMPP网关端口: ${cmppParams.port}`,
`企业代码: ${cmppParams.enterpriseCode}`,
`接口账号: ${cmppParams.account}`,
`接口密码: ${cmppParams.password}`,
`接入号: ${cmppParams.accessNumber}`,
`最大连接数: ${cmppParams.maxConnections}`,
`心跳间隔: ${cmppParams.heartbeatSeconds}`,
`提交窗口: ${cmppParams.windowSize}`,
`协议版本: ${cmppParams.protocolVersion}`,
].join('\n');
}
function CmppParamsModal({ app, onClose }: { app: SmsApp; onClose: () => void }) {
const [copied, setCopied] = useState(false);
const paramsText = formatCmppParams(app);
async function copyParams() {
await navigator.clipboard.writeText(paramsText);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
}
return (
<Modal
footer={(
<>
<Button onClick={onClose} variant="ghost"></Button>
<Button icon={<Copy size={15} />} onClick={copyParams}>{copied ? '已复制' : '一键复制'}</Button>
</>
)}
onClose={onClose}
open
size="xl"
title={<div className="template-modal-title"><h2>CMPP连接参数</h2><p>{app.enterprise} / {app.name}</p></div>}
>
<div className="cmpp-param-detail">
<div className="cmpp-param-grid">
<div><span>CMPP网关地址</span><strong>{app.cmppParams.host}</strong></div>
<div><span>CMPP网关端口</span><strong>{app.cmppParams.port}</strong></div>
<div><span></span><strong>{app.cmppParams.enterpriseCode}</strong></div>
<div><span></span><strong>{app.cmppParams.account}</strong></div>
<div><span></span><strong>{app.cmppParams.password}</strong></div>
<div><span></span><strong>{app.cmppParams.accessNumber}</strong></div>
<div><span></span><strong>{app.cmppParams.maxConnections}</strong></div>
<div><span></span><strong>{app.cmppParams.heartbeatSeconds} </strong></div>
<div><span></span><strong>{app.cmppParams.windowSize}</strong></div>
<div><span></span><strong>{app.cmppParams.protocolVersion}</strong></div>
</div>
<pre className="cmpp-param-copy">{paramsText}</pre>
</div>
</Modal>
);
}
function CmppConnectionModal({
app,
onClose,
onDeleteConnection,
}: {
app: SmsApp;
onClose: () => void;
onDeleteConnection: (connectionId: string) => void;
}) {
const activeConnections = app.cmppConnections.filter((item) => item.state === 'open').length;
return (
<Modal
footer={<Button onClick={onClose}></Button>}
onClose={onClose}
open
size="xl"
title={<div className="template-modal-title"><h2>CMPP连接详情</h2><p>{app.enterprise} / {app.name}</p></div>}
>
<div className="cmpp-connection-detail">
<div className="cmpp-connection-summary">
<div><span></span><strong>{activeConnections}</strong></div>
<div><span></span><strong>{Math.max(activeConnections, app.cmppConnections.length)}</strong></div>
<div><span>AppID</span><strong>{app.appId}</strong></div>
<div><span></span><Tag tone={app.cmppStatus === 'connected' ? 'success' : app.cmppStatus === 'disconnected' ? 'danger' : 'neutral'}>{app.cmppStatus === 'connected' ? '在线' : app.cmppStatus === 'disconnected' ? '离线' : '未开通'}</Tag></div>
</div>
<Table
columns={[
{ key: 'id', title: '连接ID', width: '150px', render: (record: CmppConnection) => <strong>{record.id}</strong> },
{ key: 'state', title: '状态', width: '100px', render: (record: CmppConnection) => <Tag tone={connectionStateMeta[record.state].tone}>{connectionStateMeta[record.state].label}</Tag> },
{ 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: '100px', render: (record: CmppConnection) => record.pendingWindow },
{
key: 'actions',
title: '操作',
align: 'right',
width: '100px',
render: (record: CmppConnection) => (
<Button icon={<Trash2 size={14} />} onClick={() => onDeleteConnection(record.id)} size="sm" variant="danger"></Button>
),
},
]}
data={app.cmppConnections}
emptyText="暂无CMPP连接"
rowKey="id"
/>
</div>
</Modal>
);
}
export function AdminEnterpriseApplicationsPage() {
const navigate = useNavigate();
const [smsApps, setSmsApps] = useState(initialSmsApps);
const [mmsApps, setMmsApps] = useState(initialMmsApps);
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
const [connectionApp, setConnectionApp] = useState<SmsApp | null>(null);
const [paramsApp, setParamsApp] = useState<SmsApp | null>(null);
const [confirmAction, setConfirmAction] = useState<
| { action: 'toggle'; kind: AppKind; id: string; name: string; enabled: boolean }
| { action: 'delete'; kind: AppKind; id: string; name: string }
@@ -95,6 +276,26 @@ export function AdminEnterpriseApplicationsPage() {
setConfirmAction(null);
}
function deleteConnection(appId: string, connectionId: string) {
let nextConnectionApp: SmsApp | null = null;
setSmsApps((current) => current.map((app) => {
if (app.id !== appId) {
return app;
}
const nextConnections = app.cmppConnections.filter((connection) => connection.id !== connectionId);
const nextOpenCount = nextConnections.filter((connection) => connection.state === 'open').length;
const nextApp: SmsApp = {
...app,
cmppConnections: nextConnections,
cmppStatus: nextOpenCount > 0 ? 'connected' : app.enabled ? 'disconnected' : 'inactive',
};
nextConnectionApp = nextApp;
return nextApp;
}));
setConnectionApp(nextConnectionApp);
}
const filteredSmsApps = useMemo(
() => smsApps.filter((item) => !enterpriseKeyword || item.enterprise.includes(enterpriseKeyword)),
[enterpriseKeyword, smsApps],
@@ -115,11 +316,20 @@ export function AdminEnterpriseApplicationsPage() {
{
key: 'cmppStatus',
title: 'CMPP状态',
width: '130px',
width: '230px',
render: (record) => (
<Tag tone={record.cmppStatus === 'connected' ? 'success' : record.cmppStatus === 'disconnected' ? 'danger' : 'neutral'}>
{record.cmppStatus === 'connected' ? '已连接' : record.cmppStatus === 'disconnected' ? '已断开' : '未开通'}
</Tag>
<div className="cmpp-status-cell">
<Tag tone={record.cmppStatus === 'connected' ? 'success' : record.cmppStatus === 'disconnected' ? 'danger' : 'neutral'}>
{record.cmppStatus === 'connected' ? '已连接' : record.cmppStatus === 'disconnected' ? '已断开' : '未开通'}
</Tag>
<button onClick={() => setConnectionApp(record)} type="button">
{record.cmppConnections.filter((item) => item.state === 'open').length}
</button>
<button className="cmpp-status-cell__params" onClick={() => setParamsApp(record)} type="button">
<Settings2 size={13} />
</button>
</div>
),
},
{ key: 'enabled', title: '状态', width: '100px', render: (record) => enabledTag(record.enabled) },
@@ -206,6 +416,14 @@ export function AdminEnterpriseApplicationsPage() {
onConfirm={runConfirmedAction}
/>
) : null}
{connectionApp ? (
<CmppConnectionModal
app={connectionApp}
onClose={() => setConnectionApp(null)}
onDeleteConnection={(connectionId) => deleteConnection(connectionApp.id, connectionId)}
/>
) : null}
{paramsApp ? <CmppParamsModal app={paramsApp} onClose={() => setParamsApp(null)} /> : null}
</section>
);
}