102 lines
4.6 KiB
TypeScript
102 lines
4.6 KiB
TypeScript
import { Copy, Eye, FileText, Pencil, Power, Send } from 'lucide-react';
|
||
import { DeleteRiskAction, Pagination, Tag } from '@/components/ui';
|
||
import { formatCents } from '@/utils/currency';
|
||
import { carrierLabelMap, carrierToneMap, statusLabelMap, statusToneMap } from './channelModel';
|
||
import type { ChannelConfirmAction, ChannelModalState, SmsChannel } from './channelTypes';
|
||
|
||
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}`}>
|
||
<small>{label}</small>
|
||
<strong>{rate}%</strong>
|
||
<span>{count.toLocaleString('zh-CN')}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ChannelTable({
|
||
channels,
|
||
currentPage,
|
||
total,
|
||
totalPages,
|
||
onPageChange,
|
||
onOpenLogs,
|
||
onOpenReports,
|
||
onEdit,
|
||
onConfirm,
|
||
onTest,
|
||
onDeleted,
|
||
}: {
|
||
channels: SmsChannel[];
|
||
currentPage: number;
|
||
total: number;
|
||
totalPages: number;
|
||
onPageChange: (page: number | ((value: number) => number)) => void;
|
||
onOpenLogs: (channel: SmsChannel) => void;
|
||
onOpenReports: (channel: SmsChannel) => void;
|
||
onEdit: (modal: ChannelModalState) => void;
|
||
onConfirm: (action: ChannelConfirmAction) => void;
|
||
onTest: (channel: SmsChannel) => void;
|
||
onDeleted: () => void;
|
||
}) {
|
||
return (
|
||
<div className="surface sms-channel-table">
|
||
<div className="sms-channel-table__head">
|
||
<span>通道信息</span>
|
||
<span>运营商 / 成本</span>
|
||
<span>状态</span>
|
||
<span>今日提交</span>
|
||
<span>今日提交 / 送达质量</span>
|
||
<span>操作</span>
|
||
</div>
|
||
{channels.map((channel) => (
|
||
<article className="sms-channel-table__row" key={channel.id}>
|
||
<div className="sms-channel-identity">
|
||
<strong>{channel.name}</strong>
|
||
<span>通道 ID:{channel.id}</span>
|
||
</div>
|
||
<div className="sms-channel-carrier-price">
|
||
<Tag tone={carrierToneMap[channel.carrier]}>{carrierLabelMap[channel.carrier]}</Tag>
|
||
<strong>{formatCents(channel.unitPrice)} 元</strong>
|
||
</div>
|
||
<div className="sms-channel-status-cell">
|
||
<Tag tone={statusToneMap[channel.status]}>{statusLabelMap[channel.status]}</Tag>
|
||
<button onClick={() => onOpenLogs(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.submitFailureCount} label="提交失败" rate={channel.submitFailureRate} tone={channel.submitFailureCount > 0 ? 'danger' : 'neutral'} />
|
||
<RateBlock count={channel.successCount} label="送达成功" rate={channel.successRate} tone={channel.successRate >= 80 ? 'success' : 'warning'} />
|
||
<RateBlock count={channel.unknownCount} label="回执未知" rate={channel.unknownRate} />
|
||
<RateBlock count={channel.failureCount} label="送达失败" rate={channel.failureRate} tone={channel.failureRate >= 50 ? 'danger' : 'neutral'} />
|
||
</div>
|
||
<div className="sms-channel-actions">
|
||
<button className="sms-channel-report-entry" onClick={() => onOpenReports(channel)} type="button">
|
||
<Eye size={15} />报备详情
|
||
</button>
|
||
<button onClick={() => onEdit({ mode: 'edit', channel })} type="button"><Pencil size={15} />编辑</button>
|
||
<button onClick={() => onConfirm({ type: 'copy', channel })} type="button"><Copy size={15} />复制通道</button>
|
||
<button onClick={() => onTest(channel)} type="button"><Send size={15} />发送测试</button>
|
||
<button className={channel.status === 'stopped' ? 'is-success' : 'is-warning'} onClick={() => onConfirm({ type: 'toggle', channel })} type="button">
|
||
<Power size={15} />{channel.status === 'stopped' ? '启用' : '停用'}
|
||
</button>
|
||
<DeleteRiskAction onCompleted={onDeleted} portal="admin" targetId={channel.id} targetType="channel" />
|
||
</div>
|
||
</article>
|
||
))}
|
||
<Pagination
|
||
nextDisabled={currentPage >= totalPages}
|
||
onNext={() => onPageChange((value) => Math.min(totalPages, value + 1))}
|
||
onPrevious={() => onPageChange((value) => Math.max(1, value - 1))}
|
||
page={currentPage}
|
||
totalPages={totalPages}
|
||
onPageChange={onPageChange}
|
||
previousDisabled={currentPage <= 1}
|
||
total={total}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|