feat: add phone frequency controls and modularize codebase

This commit is contained in:
hectorzhao
2026-07-31 22:25:23 +08:00
parent 0af671b4ed
commit ca4f591a13
216 changed files with 41579 additions and 23694 deletions
+101
View File
@@ -0,0 +1,101 @@
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>
);
}