fix: connect operations pages to real backend
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
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 } from '@/api/adminApi';
|
||||
|
||||
type SmsApp = {
|
||||
id: string;
|
||||
@@ -48,41 +49,6 @@ type MmsApp = Omit<SmsApp, 'cmppStatus' | 'cmppConnections' | 'cmppParams'> & {
|
||||
|
||||
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',
|
||||
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[] = [
|
||||
{ id: 'mms-app-1', name: '营销活动彩信', enterprise: '上海XXXXX科技有限公司', appId: 'MMS_2024020112345678', enabled: true, sentToday: 320, deliveryRate: 92, unitPrice: 0.15, pointPrice: 50 },
|
||||
{ id: 'mms-app-2', name: '节日祝福彩信', enterprise: '重庆进载数智', appId: 'MMS_2024020187654321', enabled: true, sentToday: 180, deliveryRate: 88, unitPrice: 0.12, pointPrice: 30 },
|
||||
@@ -117,18 +83,18 @@ const connectionStateMeta: Record<CmppConnection['state'], { label: string; tone
|
||||
reconnecting: { label: '重连中', tone: 'warning' },
|
||||
};
|
||||
|
||||
function formatCmppParams(app: SmsApp) {
|
||||
const { cmppParams } = app;
|
||||
function formatCmppParams(app: SmsApp, params?: ApplicationCmppParams | null) {
|
||||
const cmppParams = params ?? app.cmppParams;
|
||||
return [
|
||||
`应用名称: ${app.name}`,
|
||||
`企业名称: ${app.enterprise}`,
|
||||
`AppID: ${app.appId}`,
|
||||
`CMPP网关地址: ${cmppParams.host}`,
|
||||
`CMPP网关端口: ${cmppParams.port}`,
|
||||
`CMPP网关地址: ${'gatewayHost' in cmppParams ? cmppParams.gatewayHost : cmppParams.host}`,
|
||||
`CMPP网关端口: ${'gatewayPort' in cmppParams ? cmppParams.gatewayPort : cmppParams.port}`,
|
||||
`企业代码: ${cmppParams.enterpriseCode}`,
|
||||
`接口账号: ${cmppParams.account}`,
|
||||
`接口密码: ${cmppParams.password}`,
|
||||
`接入号: ${cmppParams.accessNumber}`,
|
||||
`接口密码: ${'passwordCipher' in cmppParams ? cmppParams.passwordCipher : cmppParams.password}`,
|
||||
`接入号: ${'srcId' in cmppParams ? cmppParams.srcId : cmppParams.accessNumber}`,
|
||||
`最大连接数: ${cmppParams.maxConnections}`,
|
||||
`心跳间隔: ${cmppParams.heartbeatSeconds}秒`,
|
||||
`提交窗口: ${cmppParams.windowSize}`,
|
||||
@@ -136,9 +102,13 @@ function formatCmppParams(app: SmsApp) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function CmppParamsModal({ app, onClose }: { app: SmsApp; onClose: () => void }) {
|
||||
function CmppParamsModal({ app, params, onClose }: { app: SmsApp; params?: ApplicationCmppParams | null; onClose: () => void }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const paramsText = formatCmppParams(app);
|
||||
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);
|
||||
@@ -161,16 +131,16 @@ function CmppParamsModal({ app, onClose }: { app: SmsApp; onClose: () => void })
|
||||
>
|
||||
<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><span>CMPP网关地址</span><strong>{host}</strong></div>
|
||||
<div><span>CMPP网关端口</span><strong>{port}</strong></div>
|
||||
<div><span>企业代码</span><strong>{params?.enterpriseCode ?? app.cmppParams.enterpriseCode}</strong></div>
|
||||
<div><span>接口账号</span><strong>{params?.account ?? app.cmppParams.account}</strong></div>
|
||||
<div><span>接口密码</span><strong>{password}</strong></div>
|
||||
<div><span>接入号</span><strong>{srcId}</strong></div>
|
||||
<div><span>最大连接数</span><strong>{params?.maxConnections ?? app.cmppParams.maxConnections}</strong></div>
|
||||
<div><span>心跳间隔</span><strong>{params?.heartbeatSeconds ?? app.cmppParams.heartbeatSeconds} 秒</strong></div>
|
||||
<div><span>提交窗口</span><strong>{params?.windowSize ?? app.cmppParams.windowSize}</strong></div>
|
||||
<div><span>协议版本</span><strong>{params?.protocolVersion ?? app.cmppParams.protocolVersion}</strong></div>
|
||||
</div>
|
||||
<pre className="cmpp-param-copy">{paramsText}</pre>
|
||||
</div>
|
||||
@@ -236,64 +206,79 @@ function CmppConnectionModal({
|
||||
|
||||
export function AdminEnterpriseApplicationsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [smsApps, setSmsApps] = useState(initialSmsApps);
|
||||
const [smsApps, setSmsApps] = useState<SmsApp[]>([]);
|
||||
const [mmsApps, setMmsApps] = useState(initialMmsApps);
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [connectionApp, setConnectionApp] = useState<SmsApp | null>(null);
|
||||
const [paramsApp, setParamsApp] = useState<SmsApp | null>(null);
|
||||
const [paramsDetail, setParamsDetail] = useState<ApplicationCmppParams | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [confirmAction, setConfirmAction] = useState<
|
||||
| { action: 'toggle'; kind: AppKind; id: string; name: string; enabled: boolean }
|
||||
| { action: 'delete'; kind: AppKind; id: string; name: string }
|
||||
| null
|
||||
>(null);
|
||||
|
||||
function confirmToggle(kind: AppKind, id: string) {
|
||||
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 confirmToggle(kind: AppKind, id: string) {
|
||||
if (kind === 'sms') {
|
||||
setSmsApps((current) => current.map((item) => item.id === id ? { ...item, enabled: !item.enabled } : item));
|
||||
const app = smsApps.find((item) => item.id === id);
|
||||
if (app) {
|
||||
await adminApi.changeApplicationStatus(id, app.enabled ? 'disabled' : 'active', '运营端企业应用管理');
|
||||
await loadSmsApps();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setMmsApps((current) => current.map((item) => item.id === id ? { ...item, enabled: !item.enabled } : item));
|
||||
}
|
||||
|
||||
function confirmDelete(kind: AppKind, id: string) {
|
||||
async function confirmDelete(kind: AppKind, id: string) {
|
||||
if (kind === 'sms') {
|
||||
setSmsApps((current) => current.filter((item) => item.id !== id));
|
||||
await adminApi.changeApplicationStatus(id, 'deleted', '运营端删除应用');
|
||||
await loadSmsApps();
|
||||
} else {
|
||||
setMmsApps((current) => current.filter((item) => item.id !== id));
|
||||
}
|
||||
}
|
||||
|
||||
function runConfirmedAction() {
|
||||
async function runConfirmedAction() {
|
||||
if (!confirmAction) {
|
||||
return;
|
||||
}
|
||||
if (confirmAction.action === 'toggle') {
|
||||
confirmToggle(confirmAction.kind, confirmAction.id);
|
||||
await confirmToggle(confirmAction.kind, confirmAction.id);
|
||||
} else {
|
||||
confirmDelete(confirmAction.kind, confirmAction.id);
|
||||
await confirmDelete(confirmAction.kind, confirmAction.id);
|
||||
}
|
||||
setConfirmAction(null);
|
||||
}
|
||||
|
||||
function deleteConnection(appId: string, connectionId: string) {
|
||||
let nextConnectionApp: SmsApp | null = null;
|
||||
setSmsApps((current) => current.map((app) => {
|
||||
if (app.id !== appId) {
|
||||
return app;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
async function openParams(app: SmsApp) {
|
||||
setParamsApp(app);
|
||||
setParamsDetail(await adminApi.getApplicationCmppParams(app.id));
|
||||
}
|
||||
|
||||
const filteredSmsApps = useMemo(
|
||||
@@ -325,7 +310,7 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
<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">
|
||||
<button className="cmpp-status-cell__params" onClick={() => { void openParams(record); }} type="button">
|
||||
<Settings2 size={13} />
|
||||
参数
|
||||
</button>
|
||||
@@ -397,6 +382,8 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
<Button onClick={() => setEnterpriseKeyword('')} variant="ghost">重置</Button>
|
||||
</div>
|
||||
|
||||
{error ? <div className="surface ui-table__empty">{error}</div> : null}
|
||||
|
||||
<div className="surface section-stack">
|
||||
<Tabs
|
||||
items={[
|
||||
@@ -413,17 +400,49 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
? `确认删除应用“${confirmAction.name}”吗?`
|
||||
: `确认${confirmAction.enabled ? '停用' : '启用'}应用“${confirmAction.name}”吗?`}
|
||||
onCancel={() => setConfirmAction(null)}
|
||||
onConfirm={runConfirmedAction}
|
||||
onConfirm={() => { void runConfirmedAction(); }}
|
||||
/>
|
||||
) : null}
|
||||
{connectionApp ? (
|
||||
<CmppConnectionModal
|
||||
app={connectionApp}
|
||||
onClose={() => setConnectionApp(null)}
|
||||
onDeleteConnection={(connectionId) => deleteConnection(connectionApp.id, connectionId)}
|
||||
onDeleteConnection={(connectionId) => { void deleteConnection(connectionApp.id, connectionId); }}
|
||||
/>
|
||||
) : null}
|
||||
{paramsApp ? <CmppParamsModal app={paramsApp} onClose={() => setParamsApp(null)} /> : null}
|
||||
{paramsApp ? <CmppParamsModal app={paramsApp} params={paramsDetail} onClose={() => { setParamsApp(null); setParamsDetail(null); }} /> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function mapApplication(application: EnterpriseApplication): SmsApp {
|
||||
const connections = (application.cmppConnections ?? []).map(mapConnection);
|
||||
return {
|
||||
id: application.id,
|
||||
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: 0,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user