186 lines
7.4 KiB
TypeScript
186 lines
7.4 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { Clock3, Layers3, Pencil, Plus, RadioTower, Search, Trash2 } from 'lucide-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Breadcrumb, Button, Input, Modal, Pagination, Tag } from '@/components/ui';
|
|
import { adminApi, type ChannelGroup } from '@/api/adminApi';
|
|
|
|
type GroupCarrier = 'mobile' | 'unicom' | 'telecom';
|
|
|
|
const carrierLabels: Record<GroupCarrier, string> = {
|
|
mobile: '移动',
|
|
unicom: '联通',
|
|
telecom: '电信',
|
|
};
|
|
|
|
function formatRetryLimit(group: ChannelGroup) {
|
|
if (group.retryEnabled === false) return '已关闭';
|
|
const totalMinutes = group.retryTimeLimitMinutes ?? (group.retryTimeLimitHours ?? 12) * 60;
|
|
return `${Math.floor(totalMinutes / 60)}小时${totalMinutes % 60}分钟`;
|
|
}
|
|
|
|
function getGroupSummary(group: ChannelGroup) {
|
|
const items = group.items ?? [];
|
|
const provinceCount = items.filter((item) => item.province).length;
|
|
const nationalCount = items.length - provinceCount;
|
|
const connectedCount = items.filter((item) => (item.channel?.connectionStates ?? []).some((connection) =>
|
|
connection.status === 'connected'
|
|
&& connection.desiredConnections > 0
|
|
&& connection.currentConnections > 0
|
|
)).length;
|
|
return { connectedCount, nationalCount, provinceCount, totalCount: items.length };
|
|
}
|
|
|
|
export function AdminChannelGroupsPage() {
|
|
const navigate = useNavigate();
|
|
const [groupName, setGroupName] = useState('');
|
|
const [groups, setGroups] = useState<ChannelGroup[]>([]);
|
|
const [deleteTarget, setDeleteTarget] = useState<ChannelGroup | null>(null);
|
|
const [page, setPage] = useState(1);
|
|
const [error, setError] = useState('');
|
|
const pageSize = 10;
|
|
|
|
function loadData() {
|
|
adminApi.listChannelGroups()
|
|
.then((items) => {
|
|
setGroups(items);
|
|
setError('');
|
|
})
|
|
.catch((failure: Error) => setError(failure.message || '通道组加载失败'));
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const filteredGroups = useMemo(() => groups.filter((group) => !groupName.trim() || group.name.includes(groupName.trim())), [groupName, groups]);
|
|
const totalPages = Math.max(1, Math.ceil(filteredGroups.length / pageSize));
|
|
const currentPage = Math.min(page, totalPages);
|
|
const visibleGroups = filteredGroups.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
|
|
|
useEffect(() => {
|
|
setPage(1);
|
|
}, [groupName, groups.length]);
|
|
|
|
function deleteGroup() {
|
|
if (!deleteTarget) return;
|
|
adminApi.deleteChannelGroup(deleteTarget.id)
|
|
.then(() => {
|
|
setDeleteTarget(null);
|
|
loadData();
|
|
})
|
|
.catch((failure: Error) => setError(failure.message || '通道组删除失败'));
|
|
}
|
|
|
|
return (
|
|
<div className="page-stack sms-channel-group-page">
|
|
<div className="page-heading">
|
|
<div>
|
|
<Breadcrumb items={['短信通道组管理']} />
|
|
</div>
|
|
<Button icon={<Plus size={16} />} onClick={() => navigate('/admin/channel-groups/new')}>
|
|
添加通道组
|
|
</Button>
|
|
</div>
|
|
{error ? <p className="form-error">{error}</p> : null}
|
|
|
|
<section className="surface channel-group-filter">
|
|
<Input label="通道组名称" onChange={(event) => setGroupName(event.target.value)} placeholder="请输入通道组名称" value={groupName} />
|
|
<div className="channel-group-filter__actions">
|
|
<Button icon={<Search size={16} />} onClick={loadData}>查询</Button>
|
|
<Button onClick={() => setGroupName('')} variant="ghost">重置</Button>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="surface channel-group-list">
|
|
<div className="channel-group-config-list">
|
|
{visibleGroups.map((group) => {
|
|
const summary = getGroupSummary(group);
|
|
const previewItems = (group.items ?? []).slice(0, 4);
|
|
return (
|
|
<article className="channel-group-config-item" key={group.id}>
|
|
<div className="channel-group-config-item__identity">
|
|
<span className="channel-group-config-item__icon"><Layers3 size={18} /></span>
|
|
<div>
|
|
<strong>{group.name}</strong>
|
|
<span>{carrierLabels[group.carrier] ?? group.carrier}通道组</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="channel-group-config-item__metrics" aria-label="通道组配置摘要">
|
|
<div>
|
|
<span>省网</span>
|
|
<strong>{summary.provinceCount}</strong>
|
|
</div>
|
|
<div>
|
|
<span>全国</span>
|
|
<strong>{summary.nationalCount}</strong>
|
|
</div>
|
|
<div>
|
|
<span>链接正常</span>
|
|
<strong>{summary.connectedCount}/{summary.totalCount}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="channel-group-config-item__policy">
|
|
<Tag tone={group.retryEnabled === false ? 'neutral' : 'success'}>
|
|
{group.retryEnabled === false ? '补发关闭' : '补发开启'}
|
|
</Tag>
|
|
<span><Clock3 size={15} />{formatRetryLimit(group)}</span>
|
|
<span><RadioTower size={15} />{summary.totalCount ? `${summary.totalCount} 个通道` : '暂无通道'}</span>
|
|
</div>
|
|
|
|
<div className="channel-group-config-item__channels">
|
|
{previewItems.map((item) => (
|
|
<span key={item.id}>
|
|
{item.province ? `${item.province} / ` : `P${item.priority} / `}
|
|
{item.channel?.name ?? '未命名通道'}
|
|
</span>
|
|
))}
|
|
{summary.totalCount > previewItems.length ? <span>+{summary.totalCount - previewItems.length}</span> : null}
|
|
{summary.totalCount === 0 ? <span>暂无绑定通道</span> : null}
|
|
</div>
|
|
|
|
<div className="channel-group-config-item__actions">
|
|
<button onClick={() => navigate(`/admin/channel-groups/${group.id}/edit`)} type="button">
|
|
<Pencil size={15} />编辑
|
|
</button>
|
|
<button className="is-danger" onClick={() => setDeleteTarget(group)} type="button">
|
|
<Trash2 size={15} />删除
|
|
</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={filteredGroups.length}
|
|
/>
|
|
</section>
|
|
|
|
<Modal
|
|
footer={(
|
|
<>
|
|
<Button onClick={() => setDeleteTarget(null)} variant="ghost">取消</Button>
|
|
<Button onClick={deleteGroup} variant="danger">确认删除</Button>
|
|
</>
|
|
)}
|
|
onClose={() => setDeleteTarget(null)}
|
|
open={Boolean(deleteTarget)}
|
|
title="删除通道组"
|
|
>
|
|
<div className="channel-confirm">
|
|
<strong>{deleteTarget?.name}</strong>
|
|
<p>删除前会校验真实路由绑定;已被企业应用使用的通道组不会被删除。</p>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|