Files
lislgosms/src/apps/admin/AdminChannelsPage.tsx
T

198 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { Plus, Search } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { adminApi, type CmppConnectionState } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Modal, Select } from '@/components/ui';
import { ChannelFormModal } from './channels/ChannelFormModal';
import { ChannelLogModal } from './channels/ChannelLogModal';
import { ChannelTable } from './channels/ChannelTable';
import { SmsTestModal } from './channels/SmsTestModal';
import { buildChannelPayload, carrierOptions, mapApiChannel, mapUiStatusToApi, statusOptions } from './channels/channelModel';
import type { ChannelConfirmAction, ChannelLogState, ChannelModalState, SmsChannel } from './channels/channelTypes';
import './channels/AdminChannelsPage.css';
export function AdminChannelsPage() {
const navigate = useNavigate();
const [channels, setChannels] = useState<SmsChannel[]>([]);
const [error, setError] = useState('');
const [keyword, setKeyword] = useState('');
const [carrier, setCarrier] = useState('all');
const [status, setStatus] = useState('all');
const [modal, setModal] = useState<ChannelModalState | null>(null);
const [testChannel, setTestChannel] = useState<SmsChannel | null>(null);
const [confirmAction, setConfirmAction] = useState<ChannelConfirmAction | null>(null);
const [logState, setLogState] = useState<ChannelLogState | null>(null);
const [logKeyword, setLogKeyword] = useState('');
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 10;
function loadChannels(targetPage = page, filters = { keyword, carrier, status }) {
Promise.all([
adminApi.listChannelsPage({ keyword: filters.keyword.trim() || undefined, carrier: filters.carrier, status: filters.status, page: targetPage, pageSize }),
adminApi.getSendQuality(),
])
.then(async ([result, quality]) => {
const visibleChannels = result.items;
const connections = await Promise.all(visibleChannels.map((channel) =>
adminApi.listChannelConnections(channel.id).catch(() => [] as CmppConnectionState[]),
));
const qualityByChannel = new Map(quality.channels.map((item) => [item.channelId, item]));
setChannels(visibleChannels.map((item, index) => mapApiChannel(item, connections[index], qualityByChannel.get(item.id))));
setTotal(result.total);
setError('');
})
.catch((failure: Error) => setError(failure.message || '通道列表加载失败'));
}
useEffect(() => {
loadChannels(page);
}, [page]);
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const currentPage = Math.min(page, totalPages);
async function upsertChannel(nextChannel: SmsChannel) {
try {
if (modal?.mode === 'edit' && modal.channel) {
await adminApi.updateChannel(modal.channel.id, buildChannelPayload(nextChannel, nextChannel.passwordCipher));
} else {
await adminApi.createChannel({
code: `CH-${Date.now()}`,
...buildChannelPayload(nextChannel, nextChannel.passwordCipher || 'secret'),
status: 'active',
});
}
loadChannels();
setModal(null);
setError('');
} catch (failure) {
setError(failure instanceof Error ? failure.message : '通道保存失败');
}
}
async function toggleChannel(channel: SmsChannel) {
await adminApi.changeChannelStatus(channel.id, mapUiStatusToApi(channel));
loadChannels();
}
async function copyChannel(channel: SmsChannel) {
await adminApi.copyChannel(channel.id);
loadChannels();
}
async function openLinkLogs(channel: SmsChannel) {
setLogState({ channel });
setLogKeyword('');
try {
const data = await adminApi.listChannelConnectionLogs(channel.id);
setLogState({ channel, data });
} catch (failure) {
setLogState(null);
setError(failure instanceof Error ? failure.message : '连接日志加载失败');
}
}
function submitConfirmAction() {
if (!confirmAction) {
return;
}
if (confirmAction.type === 'toggle') {
void toggleChannel(confirmAction.channel);
}
if (confirmAction.type === 'copy') {
void copyChannel(confirmAction.channel);
}
setConfirmAction(null);
}
const confirmTitle = confirmAction?.type === 'copy'
? '确认复制通道'
: confirmAction?.channel.status === 'stopped'
? '确认启用通道'
: '确认停用通道';
const confirmDescription = confirmAction?.type === 'copy'
? '系统将复制当前通道配置和报备详情,并新建一条名称带“副本”的通道。'
: confirmAction?.channel.status === 'stopped'
? '启用后通道会进入连接中状态,后续可继续观察网关连接。'
: '停用后该通道将不再承接新的发送任务。';
return (
<section className="page-stack sms-channel-page">
<div className="page-heading">
<Breadcrumb items={['短信通道管理']} />
<Button icon={<Plus size={16} />} onClick={() => setModal({ mode: 'create' })}>添加通道</Button>
</div>
{error ? <p className="form-error">{error}</p> : null}
<div className="surface sms-channel-filter">
<div className="sms-channel-filter-grid">
<Input label="通道名称" onChange={(event) => setKeyword(event.target.value)} placeholder="请输入通道名称" value={keyword} />
<Select label="运营商" onChange={(event) => setCarrier(event.target.value)} options={carrierOptions} value={carrier} />
<Select label="当前状态" onChange={(event) => setStatus(event.target.value)} options={statusOptions} value={status} />
<div className="audit-filter-actions">
<Button icon={<Search size={16} />} onClick={() => { setPage(1); void loadChannels(1); }}>查询</Button>
<Button onClick={() => { setKeyword(''); setCarrier('all'); setStatus('all'); setPage(1); void loadChannels(1, { keyword: '', carrier: 'all', status: 'all' }); }} variant="ghost">重置</Button>
</div>
</div>
</div>
<ChannelTable
channels={channels}
currentPage={currentPage}
onConfirm={setConfirmAction}
onDeleted={() => void loadChannels()}
onEdit={setModal}
onOpenLogs={(channel) => void openLinkLogs(channel)}
onOpenReports={(channel) => navigate(`/admin/channels/${channel.id}/reports`)}
onPageChange={setPage}
onTest={setTestChannel}
total={total}
totalPages={totalPages}
/>
{modal ? <ChannelFormModal modal={modal} onClose={() => setModal(null)} onSubmit={upsertChannel} /> : null}
{testChannel ? (
<SmsTestModal
channel={testChannel}
onClose={() => setTestChannel(null)}
onOpenRecords={() => {
setTestChannel(null);
navigate('/admin/sms-records');
}}
/>
) : null}
{confirmAction ? (
<Modal
footer={(
<>
<Button onClick={() => setConfirmAction(null)} variant="ghost">取消</Button>
<Button onClick={submitConfirmAction}>确认</Button>
</>
)}
onClose={() => setConfirmAction(null)}
open
title={confirmTitle}
>
<div className="channel-confirm">
<strong>{confirmAction.channel.name}</strong>
<span>通道 ID{confirmAction.channel.id}</span>
<p>{confirmDescription}</p>
</div>
</Modal>
) : null}
{logState ? (
<ChannelLogModal
keyword={logKeyword}
logState={logState}
onClose={() => setLogState(null)}
onKeywordChange={setLogKeyword}
/>
) : null}
</section>
);
}