210 lines
8.0 KiB
TypeScript
210 lines
8.0 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { ClipboardCopy, FileText } from 'lucide-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { clientApi, type ApplicationCmppParams, type ClientSmsApplication } from '@/api/adminApi';
|
|
import { formatCents } from '@/utils/currency';
|
|
import { Button, Modal, Pagination, Tag } from '@/components/ui';
|
|
import { copyText } from '@/utils/clipboard';
|
|
|
|
type LinkStatus = 'connected' | 'degraded' | 'disconnected' | 'inactive';
|
|
|
|
type ParamRow = {
|
|
label: string;
|
|
value: string;
|
|
highlight?: boolean;
|
|
};
|
|
|
|
const statusLabelMap: Record<LinkStatus, string> = {
|
|
connected: '已连接',
|
|
degraded: '部分连接',
|
|
disconnected: '已断',
|
|
inactive: '未开通',
|
|
};
|
|
|
|
const statusToneMap: Record<LinkStatus, 'success' | 'warning' | 'danger' | 'info'> = {
|
|
connected: 'success',
|
|
degraded: 'warning',
|
|
disconnected: 'danger',
|
|
inactive: 'info',
|
|
};
|
|
|
|
function normalizeStatus(application: ClientSmsApplication): LinkStatus {
|
|
if (application.status !== 'active') {
|
|
return 'inactive';
|
|
}
|
|
return application.cmppStatus ?? 'inactive';
|
|
}
|
|
|
|
function formatPrice(cents?: number | null) {
|
|
return `${formatCents(cents)} 元`;
|
|
}
|
|
|
|
function mapParams(params: ApplicationCmppParams): ParamRow[] {
|
|
return [
|
|
{ label: 'ID', value: params.applicationId },
|
|
{ label: '企业名', value: params.tenantName },
|
|
{ label: '应用名称', value: params.applicationName },
|
|
{ label: 'AppID', value: params.appCode },
|
|
{ label: '企业代码', value: params.enterpriseCode },
|
|
{ label: '账号', value: params.account },
|
|
{ label: '密码', value: params.passwordCipher, highlight: true },
|
|
{ label: '网关IP', value: params.gatewayHost || '-' },
|
|
{ label: '网关端口', value: String(params.gatewayPort || '-') },
|
|
{ label: '接入号', value: params.srcId || '-' },
|
|
{ label: '连接数', value: String(params.maxConnections || '-') },
|
|
{ label: '心跳间隔', value: `${params.heartbeatSeconds} 秒` },
|
|
{ label: '协议版本', value: params.protocolVersion },
|
|
];
|
|
}
|
|
|
|
export function ClientApplicationsPage() {
|
|
const navigate = useNavigate();
|
|
const [applications, setApplications] = useState<ClientSmsApplication[]>([]);
|
|
const [selectedApp, setSelectedApp] = useState<ClientSmsApplication | null>(null);
|
|
const [params, setParams] = useState<ApplicationCmppParams | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [paramsLoading, setParamsLoading] = useState(false);
|
|
const [page, setPage] = useState(1);
|
|
const [error, setError] = useState('');
|
|
const [paramsError, setParamsError] = useState('');
|
|
const [copied, setCopied] = useState(false);
|
|
const [copyError, setCopyError] = useState('');
|
|
|
|
function loadApplications() {
|
|
setLoading(true);
|
|
clientApi.listApplications()
|
|
.then((items) => {
|
|
setApplications(items.filter((item) => item.status !== 'deleted'));
|
|
setError('');
|
|
})
|
|
.catch((reason: Error) => setError(reason.message || '短信应用加载失败'))
|
|
.finally(() => setLoading(false));
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadApplications();
|
|
}, []);
|
|
|
|
function openParams(application: ClientSmsApplication) {
|
|
if (application.interfaceEnabled === false) return;
|
|
setSelectedApp(application);
|
|
setParams(null);
|
|
setParamsError('');
|
|
setCopied(false);
|
|
setParamsLoading(true);
|
|
clientApi.getApplicationCmppParams(application.id)
|
|
.then((data) => {
|
|
setParams(data);
|
|
setParamsError('');
|
|
})
|
|
.catch((reason: Error) => setParamsError(reason.message || '接口参数加载失败'))
|
|
.finally(() => setParamsLoading(false));
|
|
}
|
|
|
|
const selectedRows = useMemo(() => params ? mapParams(params) : [], [params]);
|
|
const pageSize = 10;
|
|
const totalPages = Math.max(1, Math.ceil(applications.length / pageSize));
|
|
const currentPage = Math.min(page, totalPages);
|
|
const visibleApplications = applications.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
|
|
|
useEffect(() => {
|
|
setPage(1);
|
|
}, [applications.length]);
|
|
|
|
function copyParams() {
|
|
if (selectedRows.length === 0) {
|
|
return;
|
|
}
|
|
const text = selectedRows.map((item) => `${item.label}: ${item.value}`).join('\n');
|
|
void copyText(text)
|
|
.then(() => { setCopied(true); setCopyError(''); })
|
|
.catch((failure: Error) => setCopyError(failure.message || '复制失败'));
|
|
}
|
|
|
|
return (
|
|
<section className="page-stack">
|
|
<div className="sms-send-title">
|
|
<span className="sms-send-title__icon">
|
|
<FileText size={22} />
|
|
</span>
|
|
<h1>短信应用列表</h1>
|
|
<span className="muted">共 {applications.length} 个应用</span>
|
|
</div>
|
|
|
|
{loading ? <p className="muted">正在加载短信应用...</p> : null}
|
|
{error ? <p className="form-error">{error}</p> : null}
|
|
|
|
{!loading && !error && applications.length === 0 ? (
|
|
<div className="surface ui-table__empty">暂无短信应用</div>
|
|
) : null}
|
|
|
|
<div className="sms-app-grid">
|
|
{visibleApplications.map((application) => {
|
|
const linkStatus = normalizeStatus(application);
|
|
return (
|
|
<article className="sms-app-card" key={application.id}>
|
|
<h2>{application.name}</h2>
|
|
<dl>
|
|
<div>
|
|
<dt>appid</dt>
|
|
<dd>{application.id}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>今日发送成功</dt>
|
|
<dd>{(application.sentToday ?? 0).toLocaleString('zh-CN')} 条</dd>
|
|
</div>
|
|
<div>
|
|
<dt>到达率</dt>
|
|
<dd>{application.deliveryRate !== undefined ? `${application.deliveryRate}%` : '-'}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>单价</dt>
|
|
<dd>{formatPrice(application.customerUnitPrice)}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>CMPP连接状态</dt>
|
|
<dd><Tag tone={statusToneMap[linkStatus]}>{statusLabelMap[linkStatus]}</Tag></dd>
|
|
</div>
|
|
<div><dt>HTTP接口</dt><dd><Tag tone={application.httpConfig?.enabled ? 'success' : 'info'}>{application.httpConfig?.enabled ? '已开通' : '未开通'}</Tag></dd></div>
|
|
</dl>
|
|
<div className="table-actions"><Button disabled={application.interfaceEnabled === false} onClick={() => openParams(application)} variant="ghost">{application.interfaceEnabled === false ? 'CMPP未开通' : 'CMPP参数'}</Button><Button disabled={!application.httpConfig?.enabled} onClick={() => navigate('/client/http-api')} variant="ghost">HTTP接口对接</Button></div>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
<Pagination
|
|
nextDisabled={currentPage >= totalPages}
|
|
onNext={() => setPage((value) => Math.min(totalPages, value + 1))}
|
|
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
|
|
page={currentPage}
|
|
totalPages={totalPages}
|
|
onPageChange={setPage}
|
|
previousDisabled={currentPage <= 1}
|
|
total={applications.length}
|
|
/>
|
|
|
|
<Modal
|
|
footer={<Button disabled={!params} icon={<ClipboardCopy size={16} />} onClick={copyParams}>{copied ? '已复制' : '复制参数'}</Button>}
|
|
onClose={() => setSelectedApp(null)}
|
|
open={Boolean(selectedApp)}
|
|
size="xl"
|
|
title="接口参数"
|
|
>
|
|
{paramsLoading ? <p className="muted">正在加载接口参数...</p> : null}
|
|
{paramsError ? <p className="form-error">{paramsError}</p> : null}
|
|
{!paramsLoading && !paramsError && selectedRows.length > 0 ? (
|
|
<div className="sms-app-param-table">
|
|
{selectedRows.map((item) => (
|
|
<div key={item.label}>
|
|
<span>{item.label}</span>
|
|
<strong className={item.highlight ? 'text-blue' : undefined}>{item.value}</strong>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
{copyError ? <p className="form-error">{copyError}</p> : null}
|
|
</Modal>
|
|
</section>
|
|
);
|
|
}
|