fix: polish channel groups and add production deployment

This commit is contained in:
hectorzhao
2026-07-07 11:16:08 +08:00
parent b5132d7f4e
commit 72f2c010ce
44 changed files with 1265 additions and 489 deletions
+95 -30
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Layers3, Pencil, Plus, Search, Trash2, UsersRound } from 'lucide-react';
import { Clock3, Layers3, Pencil, Plus, RadioTower, Search, Trash2 } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Breadcrumb, Button, Input, Modal, Pagination } from '@/components/ui';
import { Breadcrumb, Button, Input, Modal, Pagination, Tag } from '@/components/ui';
import { adminApi, type ChannelGroup } from '@/api/adminApi';
type GroupCarrier = 'mobile' | 'unicom' | 'telecom';
@@ -12,12 +12,32 @@ const carrierLabels: Record<GroupCarrier, string> = {
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()
@@ -33,6 +53,13 @@ export function AdminChannelGroupsPage() {
}, []);
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;
@@ -65,36 +92,74 @@ export function AdminChannelGroupsPage() {
</section>
<section className="surface channel-group-list">
<div className="channel-group-grid">
{filteredGroups.map((group) => (
<article className="channel-group-card" key={group.id}>
<header>
<div>
<Layers3 size={18} />
<strong>{group.name}</strong>
<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>
<span title="运营商">{carrierLabels[group.carrier] ?? group.carrier}</span>
<span title="包含通道数"><UsersRound size={16} />{group.items?.length ?? 0}</span>
</header>
<div className="channel-group-card__body">
{(group.items ?? []).slice(0, 5).map((item, index) => {
const channel = item.channel as { name?: string } | undefined;
return <p key={`${group.id}-${index}`}>{channel?.name ?? '未命名通道'}</p>;
})}
{(group.items?.length ?? 0) === 0 ? <p className="muted"></p> : null}
</div>
<footer>
<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>
</footer>
</article>
))}
<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={false} page={1} total={filteredGroups.length} />
<Pagination
nextDisabled={currentPage >= totalPages}
onNext={() => setPage((value) => Math.min(totalPages, value + 1))}
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
page={currentPage}
previousDisabled={currentPage <= 1}
total={filteredGroups.length}
/>
</section>
<Modal