fix: harden real backend admin workflows and ui
This commit is contained in:
@@ -1,105 +1,109 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { ClipboardCopy, FileText } from 'lucide-react';
|
||||
import { clientApi, type ApplicationCmppParams, type ClientSmsApplication } from '@/api/adminApi';
|
||||
import { Button, Modal, Tag } from '@/components/ui';
|
||||
|
||||
type LinkStatus = 'connected' | 'disconnected' | 'inactive';
|
||||
type LinkStatus = 'connected' | 'degraded' | 'disconnected' | 'inactive';
|
||||
|
||||
type SmsApplication = {
|
||||
id: string;
|
||||
name: string;
|
||||
appid: string;
|
||||
todaySuccess: number;
|
||||
deliveryRate?: number;
|
||||
price: string;
|
||||
score?: string;
|
||||
status: LinkStatus;
|
||||
params: Array<{ label: string; value: string; highlight?: boolean }>;
|
||||
type ParamRow = {
|
||||
label: string;
|
||||
value: string;
|
||||
highlight?: boolean;
|
||||
};
|
||||
|
||||
const statusLabelMap: Record<LinkStatus, string> = {
|
||||
connected: '已连接',
|
||||
degraded: '部分连接',
|
||||
disconnected: '已断',
|
||||
inactive: '未开通',
|
||||
};
|
||||
|
||||
const statusToneMap: Record<LinkStatus, 'success' | 'danger' | 'info'> = {
|
||||
const statusToneMap: Record<LinkStatus, 'success' | 'warning' | 'danger' | 'info'> = {
|
||||
connected: 'success',
|
||||
degraded: 'warning',
|
||||
disconnected: 'danger',
|
||||
inactive: 'info',
|
||||
};
|
||||
|
||||
const applications: SmsApplication[] = [
|
||||
{
|
||||
id: 'app-1',
|
||||
name: '营销推广平台',
|
||||
appid: 'AK_2024010912345678',
|
||||
todaySuccess: 1500,
|
||||
deliveryRate: 95,
|
||||
price: '0.050 元',
|
||||
score: '100分',
|
||||
status: 'connected',
|
||||
params: [
|
||||
{ label: 'ID', value: '113009756' },
|
||||
{ label: '企业名', value: '启瑞物业三网' },
|
||||
{ label: '开通时间', value: '2023-12-13' },
|
||||
{ label: '企业代码', value: 'qrhyyd' },
|
||||
{ label: '账号', value: 'qrhyyd' },
|
||||
{ label: '密码', value: 'm6yZvZKn', highlight: true },
|
||||
{ label: '网关IP', value: '121.40.172.212' },
|
||||
{ label: '网关端口', value: '17890' },
|
||||
{ label: '接入号', value: '106999999' },
|
||||
{ label: '绑定IP', value: '61.129.57.48' },
|
||||
{ label: '连接数', value: '1' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'app-2',
|
||||
name: '客服系统',
|
||||
appid: 'AK_2024010987654321',
|
||||
todaySuccess: 800,
|
||||
deliveryRate: 90,
|
||||
price: '0.060 元',
|
||||
score: '80分',
|
||||
status: 'disconnected',
|
||||
params: [
|
||||
{ label: 'ID', value: '113009812' },
|
||||
{ label: '企业名', value: '客服系统三网' },
|
||||
{ label: '开通时间', value: '2024-01-09' },
|
||||
{ label: '企业代码', value: 'kfxt' },
|
||||
{ label: '账号', value: 'kfxt' },
|
||||
{ label: '密码', value: 'r8xKvP2m', highlight: true },
|
||||
{ label: '网关IP', value: '121.40.172.213' },
|
||||
{ label: '网关端口', value: '17890' },
|
||||
{ label: '接入号', value: '106988888' },
|
||||
{ label: '绑定IP', value: '61.129.57.49' },
|
||||
{ label: '连接数', value: '1' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'app-3',
|
||||
name: '验证码服务',
|
||||
appid: 'AK_2024010811223344',
|
||||
todaySuccess: 0,
|
||||
price: '0.040 元',
|
||||
status: 'inactive',
|
||||
params: [
|
||||
{ label: 'ID', value: '-' },
|
||||
{ label: '企业名', value: '验证码服务' },
|
||||
{ label: '开通时间', value: '-' },
|
||||
{ label: '企业代码', value: '-' },
|
||||
{ label: '账号', value: '-' },
|
||||
{ label: '密码', value: '-' },
|
||||
{ label: '网关IP', value: '-' },
|
||||
{ label: '网关端口', value: '-' },
|
||||
{ label: '接入号', value: '-' },
|
||||
{ label: '绑定IP', value: '-' },
|
||||
{ label: '连接数', value: '-' },
|
||||
],
|
||||
},
|
||||
];
|
||||
function normalizeStatus(application: ClientSmsApplication): LinkStatus {
|
||||
if (application.status !== 'active') {
|
||||
return 'inactive';
|
||||
}
|
||||
return application.cmppStatus ?? 'inactive';
|
||||
}
|
||||
|
||||
function formatPrice(cents?: number | null) {
|
||||
return `${((cents ?? 0) / 100).toFixed(4)} 元`;
|
||||
}
|
||||
|
||||
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: String(params.windowSize) },
|
||||
{ label: '协议版本', value: params.protocolVersion },
|
||||
];
|
||||
}
|
||||
|
||||
export function ClientApplicationsPage() {
|
||||
const [selectedApp, setSelectedApp] = useState<SmsApplication | null>(null);
|
||||
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 [error, setError] = useState('');
|
||||
const [paramsError, setParamsError] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
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) {
|
||||
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]);
|
||||
|
||||
function copyParams() {
|
||||
if (selectedRows.length === 0) {
|
||||
return;
|
||||
}
|
||||
const text = selectedRows.map((item) => `${item.label}: ${item.value}`).join('\n');
|
||||
void navigator.clipboard.writeText(text).then(() => setCopied(true));
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
@@ -111,47 +115,59 @@ export function ClientApplicationsPage() {
|
||||
<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">
|
||||
{applications.map((application) => (
|
||||
<article className="sms-app-card" key={application.id}>
|
||||
<h2>{application.name}</h2>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>appid</dt>
|
||||
<dd>{application.appid}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>今日发送成功</dt>
|
||||
<dd>{application.todaySuccess.toLocaleString('zh-CN')} 条</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>到达率</dt>
|
||||
<dd>{application.deliveryRate ? `${application.deliveryRate}%` : '-'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>单价</dt>
|
||||
<dd>{application.price}{application.score ? <span>({application.score})</span> : null}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>CMPP链接状态</dt>
|
||||
<dd><Tag tone={statusToneMap[application.status]}>{statusLabelMap[application.status]}</Tag></dd>
|
||||
</div>
|
||||
</dl>
|
||||
<Button onClick={() => setSelectedApp(application)} variant="ghost">查看对接参数</Button>
|
||||
</article>
|
||||
))}
|
||||
{applications.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>
|
||||
</dl>
|
||||
<Button onClick={() => openParams(application)} variant="ghost">查看对接参数</Button>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
footer={<Button icon={<ClipboardCopy size={16} />}>复制参数</Button>}
|
||||
footer={<Button disabled={!params} icon={<ClipboardCopy size={16} />} onClick={copyParams}>{copied ? '已复制' : '复制参数'}</Button>}
|
||||
onClose={() => setSelectedApp(null)}
|
||||
open={Boolean(selectedApp)}
|
||||
size="xl"
|
||||
title="接口参数"
|
||||
>
|
||||
{selectedApp ? (
|
||||
{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">
|
||||
{selectedApp.params.map((item) => (
|
||||
{selectedRows.map((item) => (
|
||||
<div key={item.label}>
|
||||
<span>{item.label}</span>
|
||||
<strong className={item.highlight ? 'text-blue' : undefined}>{item.value}</strong>
|
||||
|
||||
Reference in New Issue
Block a user