feat: wire admin workflows to real APIs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Eye, Info, Pencil, Plus, Power, Search, Send, Trash2 } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Copy, Eye, FileText, Info, Pencil, Plus, Power, Search, Send, Trash2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { adminApi, type AdminChannel, type ChannelLinkLogResponse } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
|
||||
|
||||
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||
@@ -31,6 +32,16 @@ type ChannelModalState = {
|
||||
channel?: SmsChannel;
|
||||
};
|
||||
|
||||
type ChannelConfirmAction = {
|
||||
type: 'toggle' | 'delete' | 'copy';
|
||||
channel: SmsChannel;
|
||||
};
|
||||
|
||||
type ChannelLogState = {
|
||||
channel: SmsChannel;
|
||||
data?: ChannelLinkLogResponse;
|
||||
};
|
||||
|
||||
const carrierOptions = [
|
||||
{ label: '全部运营商', value: 'all' },
|
||||
{ label: '移动', value: 'mobile' },
|
||||
@@ -171,6 +182,39 @@ const initialChannels: SmsChannel[] = [
|
||||
},
|
||||
];
|
||||
|
||||
function mapApiChannel(channel: AdminChannel): SmsChannel {
|
||||
const statusMap: Record<string, ChannelStatus> = {
|
||||
active: 'normal',
|
||||
disabled: 'stopped',
|
||||
deleted: 'stopped',
|
||||
connecting: 'connecting',
|
||||
failed: 'failed',
|
||||
};
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
carrier: channel.carrier === 'unicom' || channel.carrier === 'telecom' ? channel.carrier : 'mobile',
|
||||
unitPrice: channel.unitPrice,
|
||||
status: statusMap[channel.status] ?? 'normal',
|
||||
total: 0,
|
||||
successRate: 0,
|
||||
successCount: 0,
|
||||
unknownRate: 0,
|
||||
unknownCount: 0,
|
||||
failureRate: 0,
|
||||
failureCount: 0,
|
||||
gatewayHost: channel.gatewayHost,
|
||||
gatewayPort: String(channel.gatewayPort),
|
||||
corpCode: channel.enterpriseCode ?? channel.code,
|
||||
account: channel.account,
|
||||
accessNo: channel.srcId,
|
||||
};
|
||||
}
|
||||
|
||||
function mapUiStatusToApi(channel: SmsChannel) {
|
||||
return channel.status === 'stopped' ? 'active' : 'disabled';
|
||||
}
|
||||
|
||||
function RateBlock({ label, rate, count, tone = 'neutral' }: { label: string; rate: number; count: number; tone?: 'success' | 'warning' | 'danger' | 'neutral' }) {
|
||||
return (
|
||||
<div className={`sms-channel-rate sms-channel-rate--${tone}`}>
|
||||
@@ -362,6 +406,14 @@ export function AdminChannelsPage() {
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
adminApi.listChannels()
|
||||
.then((items) => setChannels(items.filter((item) => item.status !== 'deleted').map(mapApiChannel)))
|
||||
.catch(() => undefined);
|
||||
}, []);
|
||||
|
||||
const filteredChannels = useMemo(
|
||||
() => channels.filter((channel) => {
|
||||
@@ -381,16 +433,63 @@ export function AdminChannelsPage() {
|
||||
setModal(null);
|
||||
}
|
||||
|
||||
function toggleChannel(id: string) {
|
||||
setChannels((items) => items.map((item) => (
|
||||
item.id === id ? { ...item, status: item.status === 'stopped' ? 'connecting' : 'stopped' } : item
|
||||
)));
|
||||
async function toggleChannel(channel: SmsChannel) {
|
||||
const updated = await adminApi.changeChannelStatus(channel.id, mapUiStatusToApi(channel));
|
||||
setChannels((items) => items.map((item) => (item.id === channel.id ? mapApiChannel(updated) : item)));
|
||||
}
|
||||
|
||||
function deleteChannel(id: string) {
|
||||
async function deleteChannel(id: string) {
|
||||
await adminApi.deleteChannel(id, '运营端删除通道');
|
||||
setChannels((items) => items.filter((item) => item.id !== id));
|
||||
}
|
||||
|
||||
async function copyChannel(channel: SmsChannel) {
|
||||
const copied = await adminApi.copyChannel(channel.id);
|
||||
setChannels((items) => [mapApiChannel(copied), ...items]);
|
||||
}
|
||||
|
||||
async function openLinkLogs(channel: SmsChannel) {
|
||||
setLogState({ channel });
|
||||
const data = await adminApi.listChannelLinkLogs(channel.id);
|
||||
setLogState({ channel, data });
|
||||
}
|
||||
|
||||
function submitConfirmAction() {
|
||||
if (!confirmAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmAction.type === 'toggle') {
|
||||
void toggleChannel(confirmAction.channel);
|
||||
}
|
||||
|
||||
if (confirmAction.type === 'delete') {
|
||||
void deleteChannel(confirmAction.channel.id);
|
||||
}
|
||||
|
||||
if (confirmAction.type === 'copy') {
|
||||
void copyChannel(confirmAction.channel);
|
||||
}
|
||||
|
||||
setConfirmAction(null);
|
||||
}
|
||||
|
||||
const confirmTitle = confirmAction?.type === 'delete'
|
||||
? '确认删除通道'
|
||||
: confirmAction?.type === 'copy'
|
||||
? '确认复制通道'
|
||||
: confirmAction?.channel.status === 'stopped'
|
||||
? '确认启用通道'
|
||||
: '确认停用通道';
|
||||
|
||||
const confirmDescription = confirmAction?.type === 'delete'
|
||||
? '删除后该通道将从列表移除,副本通道的本地记录也会同步清理。'
|
||||
: confirmAction?.type === 'copy'
|
||||
? '系统将复制当前通道配置和报备详情,并新建一条名称带“副本”的通道。'
|
||||
: confirmAction?.channel.status === 'stopped'
|
||||
? '启用后通道会进入链接中状态,后续可继续观察网关连接。'
|
||||
: '停用后该通道将不再承接新的发送任务。';
|
||||
|
||||
return (
|
||||
<section className="page-stack sms-channel-page">
|
||||
<div className="page-heading">
|
||||
@@ -429,7 +528,12 @@ export function AdminChannelsPage() {
|
||||
<Tag tone={carrierToneMap[channel.carrier]}>{carrierLabelMap[channel.carrier]}</Tag>
|
||||
<strong>{channel.unitPrice.toFixed(1)} 分</strong>
|
||||
</div>
|
||||
<Tag tone={statusToneMap[channel.status]}>{statusLabelMap[channel.status]}</Tag>
|
||||
<div className="sms-channel-status-cell">
|
||||
<Tag tone={statusToneMap[channel.status]}>{statusLabelMap[channel.status]}</Tag>
|
||||
<button onClick={() => void openLinkLogs(channel)} type="button">
|
||||
<FileText size={14} />链接日志
|
||||
</button>
|
||||
</div>
|
||||
<strong className="sms-channel-total">{channel.total.toLocaleString('zh-CN')}</strong>
|
||||
<div className="sms-channel-quality">
|
||||
<RateBlock count={channel.successCount} label="成功" rate={channel.successRate} tone={channel.successRate >= 80 ? 'success' : 'warning'} />
|
||||
@@ -438,14 +542,15 @@ export function AdminChannelsPage() {
|
||||
</div>
|
||||
<div className="sms-channel-actions">
|
||||
<button className="sms-channel-report-entry" onClick={() => navigate(`/admin/channels/${channel.id}/reports`)} type="button">
|
||||
<Eye size={15} />通道报备详情
|
||||
<Eye size={15} />报备详情
|
||||
</button>
|
||||
<button onClick={() => setModal({ mode: 'edit', channel })} type="button"><Pencil size={15} />编辑</button>
|
||||
<button onClick={() => setConfirmAction({ type: 'copy', channel })} type="button"><Copy size={15} />复制通道</button>
|
||||
<button onClick={() => setTestChannel(channel)} type="button"><Send size={15} />发送测试</button>
|
||||
<button className={channel.status === 'stopped' ? 'is-success' : 'is-warning'} onClick={() => toggleChannel(channel.id)} type="button">
|
||||
<button className={channel.status === 'stopped' ? 'is-success' : 'is-warning'} onClick={() => setConfirmAction({ type: 'toggle', channel })} type="button">
|
||||
<Power size={15} />{channel.status === 'stopped' ? '启用' : '停用'}
|
||||
</button>
|
||||
<button className="is-danger" onClick={() => deleteChannel(channel.id)} type="button"><Trash2 size={15} />删除</button>
|
||||
<button className="is-danger" onClick={() => setConfirmAction({ type: 'delete', channel })} type="button"><Trash2 size={15} />删除</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
@@ -472,6 +577,53 @@ export function AdminChannelsPage() {
|
||||
onClose={() => setTestChannel(null)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{confirmAction ? (
|
||||
<Modal
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={() => setConfirmAction(null)} variant="ghost">取消</Button>
|
||||
<Button onClick={submitConfirmAction} variant={confirmAction.type === 'delete' ? 'danger' : 'primary'}>确认</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 ? (
|
||||
<Modal
|
||||
footer={<Button onClick={() => setLogState(null)} variant="ghost">关闭</Button>}
|
||||
onClose={() => setLogState(null)}
|
||||
open
|
||||
size="xl"
|
||||
title={<div className="template-modal-title"><h2>链接日志</h2><p>{logState.channel.name}</p></div>}
|
||||
>
|
||||
<div className="channel-log-list">
|
||||
{(logState.data?.logs ?? []).map((log) => (
|
||||
<article className="channel-log-item" key={log.id}>
|
||||
<div>
|
||||
<strong>{log.event}</strong>
|
||||
<span>{new Date(log.time).toLocaleString('zh-CN', { hour12: false })}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{log.resourceId}</span>
|
||||
<p>{typeof log.detail === 'string' ? log.detail : JSON.stringify(log.detail ?? {})}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{logState.data && logState.data.logs.length === 0 ? <p className="muted">暂无链接日志</p> : null}
|
||||
{!logState.data ? <p className="muted">正在加载链接日志...</p> : null}
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user