feat: refine operations UI and transport limits

This commit is contained in:
hectorzhao
2026-08-13 10:42:59 +08:00
parent fb02cbcf39
commit 67fee21616
34 changed files with 314 additions and 188 deletions
+2 -2
View File
@@ -537,13 +537,13 @@ function SignatureQualityDrawer({
{matrixMode === 'overall' ? (
<tr>
<th></th>
{visibleCarriers.map((carrier) => <th key={carrier}>{carrierLabel(carrier)}</th>)}
{visibleCarriers.map((carrier) => <th key={carrier}><CarrierTag carrier={carrier} /></th>)}
</tr>
) : (
<tr>
<th></th>
<th></th>
{majorCarrierOrder.map((carrier) => <th key={carrier}>{carrierLabel(carrier)}</th>)}
{majorCarrierOrder.map((carrier) => <th key={carrier}><CarrierTag carrier={carrier} /></th>)}
</tr>
)}
</thead>
+2 -10
View File
@@ -1,17 +1,9 @@
import { useEffect, useMemo, useState } from 'react';
import { Clock3, Layers3, Pencil, Plus, RadioTower, Search, Trash2 } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Breadcrumb, Button, Input, Modal, Pagination, Select, Tag } from '@/components/ui';
import { Breadcrumb, Button, CarrierTag, Input, Modal, Pagination, Select, Tag } from '@/components/ui';
import { adminApi, type AdminChannel, type ChannelGroup, type ChannelGroupDeletionImpact } from '@/api/adminApi';
type GroupCarrier = 'mobile' | 'unicom' | 'telecom';
const carrierLabels: Record<GroupCarrier, string> = {
mobile: '移动',
unicom: '联通',
telecom: '电信',
};
function formatRetryLimit(group: ChannelGroup) {
if (group.retryEnabled === false) return '已关闭';
const totalMinutes = group.retryTimeLimitMinutes ?? (group.retryTimeLimitHours ?? 12) * 60;
@@ -154,7 +146,7 @@ export function AdminChannelGroupsPage() {
<span className="channel-group-config-item__icon"><Layers3 size={18} /></span>
<div>
<strong>{group.name}</strong>
<span>{carrierLabels[group.carrier] ?? group.carrier}</span>
<span><CarrierTag carrier={group.carrier} /> </span>
</div>
</div>
+2 -2
View File
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { ArrowLeft, Eye, FileSliders, Search } from 'lucide-react';
import { useNavigate, useParams } from 'react-router-dom';
import { adminApi, type AdminChannel, type ChannelReportField, type ClientSmsSignature, type DictionaryItem, type ReportRecord, type ReportTask } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
import { Breadcrumb, Button, CarrierTag, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
import { successRateClassName } from '@/utils/successRate';
import { ReportFieldMappingModal } from './ReportFieldMappingModal';
@@ -185,7 +185,7 @@ export function AdminChannelReportPage() {
const reportedAt = task.approvedAt ?? approvedRecord(task.id)?.createdAt;
return <div className={`channel-report-row ${drainage ? 'channel-report-row--drainage' : 'channel-report-row--signature'}`} key={task.id}>
<span />
<div className={`channel-report-name ${drainage ? 'channel-report-name--flow' : ''}`}>{drainage ? <i /> : null}<span><strong>{drainage ? String(drainage.siteName || drainage.url || '引流信息') : formatSignatureName(signature?.name ?? task.signature?.name)}</strong><small>{drainage ? `${String(drainage.url ?? '')} · ${formatSignatureName(signature?.name ?? task.signature?.name)}` : `${signature?.tenant?.name ?? task.tenantId} · ${task.carrier ? ({ mobile: '移动', unicom: '联通', telecom: '电信' } as const)[task.carrier] : '历史通道级(未拆分)'}`}</small></span></div>
<div className={`channel-report-name ${drainage ? 'channel-report-name--flow' : ''}`}>{drainage ? <i /> : null}<span><strong>{drainage ? String(drainage.siteName || drainage.url || '引流信息') : formatSignatureName(signature?.name ?? task.signature?.name)}</strong><small>{drainage ? `${String(drainage.url ?? '')} · ${formatSignatureName(signature?.name ?? task.signature?.name)}` : <>{signature?.tenant?.name ?? task.tenantId} · {task.carrier ? <CarrierTag carrier={task.carrier} /> : '历史通道级(未拆分)'}</>}</small></span></div>
<ReportStatus value={task.status} />
<DateTime value={drainage?.submittedAt ?? task.createdAt} />
<DateTime value={reportedAt} />
+3 -85
View File
@@ -12,12 +12,11 @@ import {
Chart,
Modal,
MoneyText,
Pagination,
Table,
Tag,
type TableColumn,
} from '@/components/ui';
import { adminApi, type DashboardResponse, type SendQualityResponse, type SignatureQualityStat } from '@/api/adminApi';
import { adminApi, type DashboardResponse, type SendQualityResponse } from '@/api/adminApi';
import { createDualAxisBarLineOption, createLineOption } from '@/theme/chartOptions';
import { formatAmount, moneyUnitsToYuan } from '@/utils/currency';
@@ -29,12 +28,6 @@ type EnterpriseSpendRank = {
availableBalance: number;
};
type RankedSignatureQualityStat = SignatureQualityStat & {
rank: number;
};
const SIGNATURE_PAGE_SIZE = 10;
const balanceTone = {
: 'success',
: 'warning',
@@ -55,8 +48,6 @@ export function AdminHome() {
const [quality, setQuality] = useState<SendQualityResponse | null>(null);
const [error, setError] = useState('');
const [selectedEnterprise, setSelectedEnterprise] = useState<EnterpriseSpendRank | null>(null);
const [plainSignaturePage, setPlainSignaturePage] = useState(1);
const [drainageSignaturePage, setDrainageSignaturePage] = useState(1);
useEffect(() => {
Promise.all([adminApi.getDashboard(), adminApi.getSendQuality()])
@@ -146,36 +137,6 @@ export function AdminHome() {
},
];
const signatureColumns: Array<TableColumn<RankedSignatureQualityStat>> = [
{ key: 'rank', title: '排名', width: '72px', render: (record) => record.rank },
{ key: 'signatureName', title: '签名', render: (record) => <div><strong>{record.signatureName}</strong><p className="text-caption">{record.tenantName}</p></div> },
{ key: 'total', title: '发送总数', align: 'right', render: (record) => formatCount(record.total) },
{ key: 'submitFailureCount', title: '提交失败', align: 'right', render: (record) => formatCount(record.submitFailureCount) },
{ key: 'successCount', title: '成功', align: 'right', render: (record) => formatCount(record.successCount) },
{ key: 'unknownCount', title: '未知', align: 'right', render: (record) => formatCount(record.unknownCount) },
{ key: 'failureCount', title: '失败', align: 'right', render: (record) => formatCount(record.failureCount) },
{ key: 'successRate', title: '成功率', align: 'right', render: (record) => `${record.successRate.toFixed(1)}%` },
{ key: 'averageArrivalMs', title: '平均到达', align: 'right', render: (record) => record.averageArrivalMs === null || record.averageArrivalMs === undefined ? '-' : `${(record.averageArrivalMs / 1000).toFixed(1)}` },
];
const plainSignatureQuality = quality?.signatures ?? [];
const drainageSignatureQuality = quality?.drainageSignatures ?? [];
const plainSignatureTotalPages = Math.max(1, Math.ceil(plainSignatureQuality.length / SIGNATURE_PAGE_SIZE));
const drainageSignatureTotalPages = Math.max(1, Math.ceil(drainageSignatureQuality.length / SIGNATURE_PAGE_SIZE));
const pagedPlainSignatureQuality = plainSignatureQuality
.slice((plainSignaturePage - 1) * SIGNATURE_PAGE_SIZE, plainSignaturePage * SIGNATURE_PAGE_SIZE)
.map((item, index) => ({ ...item, rank: (plainSignaturePage - 1) * SIGNATURE_PAGE_SIZE + index + 1 }));
const pagedDrainageSignatureQuality = drainageSignatureQuality
.slice((drainageSignaturePage - 1) * SIGNATURE_PAGE_SIZE, drainageSignaturePage * SIGNATURE_PAGE_SIZE)
.map((item, index) => ({ ...item, rank: (drainageSignaturePage - 1) * SIGNATURE_PAGE_SIZE + index + 1 }));
useEffect(() => {
setPlainSignaturePage((page) => Math.min(page, plainSignatureTotalPages));
}, [plainSignatureTotalPages]);
useEffect(() => {
setDrainageSignaturePage((page) => Math.min(page, drainageSignatureTotalPages));
}, [drainageSignatureTotalPages]);
return (
<section className="page-stack admin-dashboard">
<div className="overview-hero admin-dashboard-hero">
@@ -205,8 +166,8 @@ export function AdminHome() {
<small>delivered / </small>
</div>
<div className="surface metric-card">
<span></span>
<strong><MoneyText>¥{formatCurrency(todaySpend)}</MoneyText></strong>
<span></span>
<strong>¥{formatCurrency(todaySpend)}</strong>
<small></small>
</div>
<div className="surface metric-card">
@@ -230,49 +191,6 @@ export function AdminHome() {
</div>
</div>
<div className="overview-grid admin-signature-rank-grid">
<div className="surface section-stack">
<div className="section-heading">
<div>
<h2></h2>
<p className="muted"></p>
</div>
<Tag tone="info">{plainSignatureQuality.length} </Tag>
</div>
<Table columns={signatureColumns} data={pagedPlainSignatureQuality} emptyText="今日暂无签名发送记录" rowKey="id" />
<Pagination
total={plainSignatureQuality.length}
page={plainSignaturePage}
totalPages={plainSignatureTotalPages}
previousDisabled={plainSignaturePage <= 1}
nextDisabled={plainSignaturePage >= plainSignatureTotalPages}
onPrevious={() => setPlainSignaturePage((page) => Math.max(1, page - 1))}
onNext={() => setPlainSignaturePage((page) => Math.min(plainSignatureTotalPages, page + 1))}
onPageChange={setPlainSignaturePage}
/>
</div>
<div className="surface section-stack">
<div className="section-heading">
<div>
<h2> - </h2>
<p className="muted"></p>
</div>
<Tag tone="accent">{drainageSignatureQuality.length} </Tag>
</div>
<Table columns={signatureColumns} data={pagedDrainageSignatureQuality} emptyText="今日暂无含引流的签名发送记录" rowKey="id" />
<Pagination
total={drainageSignatureQuality.length}
page={drainageSignaturePage}
totalPages={drainageSignatureTotalPages}
previousDisabled={drainageSignaturePage <= 1}
nextDisabled={drainageSignaturePage >= drainageSignatureTotalPages}
onPrevious={() => setDrainageSignaturePage((page) => Math.max(1, page - 1))}
onNext={() => setDrainageSignaturePage((page) => Math.min(drainageSignatureTotalPages, page + 1))}
onPageChange={setDrainageSignaturePage}
/>
</div>
</div>
<div className="surface section-stack">
<div className="section-heading">
<div>
+5 -18
View File
@@ -1,12 +1,15 @@
import { useEffect, useMemo, useState } from 'react';
import { Activity } from 'lucide-react';
import { Breadcrumb, Button, Table, Tag, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, CarrierTag, Table, Tag, type TableColumn } from '@/components/ui';
import { adminApi, type AdminChannel } from '@/api/adminApi';
const columns: Array<TableColumn<AdminChannel>> = [
{ key: 'id', title: '通道编号', render: (record) => record.id },
{ key: 'name', title: '通道名称', render: (record) => record.name },
{ key: 'carrier', title: '运营商', render: (record) => carrierLabel(record.carrier) },
{ key: 'carrier', title: '运营商', render: (record) => {
const carriers = record.carriers?.length ? record.carriers : record.carrier === 'all' ? ['mobile', 'unicom', 'telecom'] : record.carrier ? [record.carrier] : [];
return carriers.length ? <span className="ui-carrier-tags">{carriers.map((carrier) => <CarrierTag carrier={carrier} key={carrier} />)}</span> : '-';
} },
{ key: 'gatewayHost', title: '网关地址', render: (record) => `${record.gatewayHost}:${record.gatewayPort}` },
{ key: 'rateLimitPerSecond', title: '限速', render: (record) => `${record.rateLimitPerSecond} 条/秒` },
{
@@ -16,22 +19,6 @@ const columns: Array<TableColumn<AdminChannel>> = [
},
];
const carrierLabels: Record<string, string> = {
mobile: '移动',
cmcc: '移动',
unicom: '联通',
cucc: '联通',
telecom: '电信',
ctcc: '电信',
all: '三网',
unknown: '未识别',
};
function carrierLabel(value?: string | null) {
if (!value) return '-';
return carrierLabels[value.trim().toLowerCase()] ?? value;
}
export function AdminMonitorPage() {
const [channels, setChannels] = useState<AdminChannel[]>([]);
const [monitor, setMonitor] = useState<Record<string, unknown>>({});
+2 -1
View File
@@ -11,6 +11,7 @@ import {
import {
Breadcrumb,
Button,
CarrierTag,
DateRangeInput,
Input,
Modal,
@@ -263,7 +264,7 @@ export function AdminReportMaterialsPage() {
<Modal footer={batchResult ? <Button onClick={() => setConfirmOpen(false)}></Button> : <><Button disabled={busy} onClick={() => setConfirmOpen(false)} variant="ghost"></Button><Button disabled={preflightBusy || busy || !preflight?.eligible} icon={<ShieldCheck size={16} />} onClick={() => void createBatch()}>{busy ? '生成处理中…' : '确认生成'}</Button></>} onClose={() => { if (!busy) setConfirmOpen(false); }} open={confirmOpen} size="xl" title="报备生成资格预检">
<div className="report-batch-preflight">
{preflightBusy ? <p role="status"></p> : null}
{preflight ? <><div className="report-batch-summary"><span><CheckCircle2 size={17} /> {preflight.eligibleTargetCount} </span><span><AlertTriangle size={17} /> {preflight.skippedTargetCount} </span></div>{preflight.items.map((item) => <article key={item.id}><div><strong>{item.name}</strong><small>{item.tenantName} · {item.applicationName} · V{item.materialVersion}</small></div>{item.targets.length ? <ul>{item.targets.map((target) => <li className={target.eligible ? 'is-eligible' : 'is-blocked'} key={target.businessKey}><span>{target.name} · {target.carrier}</span><small>{target.eligible ? '资格通过' : target.blockedReasons.join('')}</small></li>)}</ul> : <p className="form-error">{item.blockedReasons.join('')}</p>}</article>)}</> : null}
{preflight ? <><div className="report-batch-summary"><span><CheckCircle2 size={17} /> {preflight.eligibleTargetCount} </span><span><AlertTriangle size={17} /> {preflight.skippedTargetCount} </span></div>{preflight.items.map((item) => <article key={item.id}><div><strong>{item.name}</strong><small>{item.tenantName} · {item.applicationName} · V{item.materialVersion}</small></div>{item.targets.length ? <ul>{item.targets.map((target) => <li className={target.eligible ? 'is-eligible' : 'is-blocked'} key={target.businessKey}><span>{target.name} · <CarrierTag carrier={target.carrier} /></span><small>{target.eligible ? '资格通过' : target.blockedReasons.join('')}</small></li>)}</ul> : <p className="form-error">{item.blockedReasons.join('')}</p>}</article>)}</> : null}
{batchResult ? <div className="risk-action-result" role="status"><ShieldCheck size={20} /><div><strong> {batchResult.batchNo} </strong><span> {batchResult.result.successCount} · {batchResult.result.skippedCount} · {batchResult.result.failedCount}</span><span>{batchResult.operationId}{batchResult.replayed ? '(幂等重放)' : ''}</span></div></div> : null}
</div>
</Modal>
+3 -3
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Eye, Search } from 'lucide-react';
import { adminApi, fileDownloadUrl, type ReportTask } from '@/api/adminApi';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, CarrierTag, DateRangeInput, Input, Modal, Pagination, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
const statusMeta: Record<string, { label: string; tone: 'neutral' | 'info' | 'success' | 'warning' | 'danger' }> = {
@@ -39,7 +39,7 @@ function TaskDetailModal({ task, onClose }: { task: ReportTask; onClose: () => v
<div><span></span><strong>{task.signature?.tenant?.name ?? task.tenantId}</strong></div>
<div><span></span><strong>{task.signature?.application?.name ?? '未指定应用'}</strong></div>
<div><span></span><strong>{task.channel?.name ?? task.channelId}</strong></div>
{task.reportType !== 'drainage' ? <div><span></span><strong>{task.carrier ? ({ mobile: '移动', unicom: '联通', telecom: '电信' } as const)[task.carrier] : '历史通道级(未拆分)'}</strong></div> : null}
{task.reportType !== 'drainage' ? <div><span></span>{task.carrier ? <CarrierTag carrier={task.carrier} /> : <strong></strong>}</div> : null}
{task.reportType !== 'drainage' ? <div><span></span><strong>{task.approvedAt ? formatDateTime(task.approvedAt) : '-'}</strong></div> : null}
<div><span></span><Tag tone={status.tone}>{status.label}</Tag></div>
<div><span></span><strong>{formatDateTime(task.createdAt)}</strong></div>
@@ -130,7 +130,7 @@ export function AdminReportTasksPage() {
const columns: Array<TableColumn<ReportTask>> = [
{ key: 'target', title: '报备对象', render: (record) => <div><strong>{taskTargetLabel(record)}</strong><div className="muted">{record.reportType === 'drainage' ? '引流信息' : '签名'} · {record.signature?.tenant?.name ?? record.tenantId}</div></div> },
{ key: 'application', title: '企业应用', render: (record) => record.signature?.application?.name ?? '未指定应用' },
{ key: 'channel', title: '通道/运营商', render: (record) => <div><strong>{record.channel?.name ?? record.channelId}</strong>{record.reportType !== 'drainage' ? <div className="muted">{record.carrier ? ({ mobile: '移动', unicom: '联通', telecom: '电信' } as const)[record.carrier] : '历史通道级(未拆分)'}</div> : null}</div> },
{ key: 'channel', title: '通道/运营商', render: (record) => <div><strong>{record.channel?.name ?? record.channelId}</strong>{record.reportType !== 'drainage' ? <div className="muted">{record.carrier ? <CarrierTag carrier={record.carrier} /> : '历史通道级(未拆分)'}</div> : null}</div> },
{ key: 'batch', title: '批次/版本', render: (record) => {
const source = record.exportItems?.[0];
return source ? <div><strong>{source.batchItem.batch.batchNo}</strong><div className="muted">V{source.batchItem.materialVersion} · {source.rowNumber}</div></div> : '-';
@@ -119,7 +119,7 @@ export function AdminSignatureRetirementPage() {
{ key: 'actions', title: '操作', align: 'right', width: '190px', render: (item) => <div className="page-actions">{!item.isRead ? <Button onClick={() => void readMessage(item.id)} size="sm" variant="ghost"></Button> : null}{!item.suppressed ? <Button onClick={() => openSuppression(item.id)} size="sm" variant="ghost"></Button> : null}</div> },
];
const suppressionColumns: Array<TableColumn<SignatureRetirementSuppression>> = [
{ key: 'dimension', title: '维度', render: (item) => `${item.dimensionType === 'enterprise' ? '企业' : '通道'} / ${carrierLabels[item.carrier]}` },
{ key: 'dimension', title: '维度', render: (item) => <span>{item.dimensionType === 'enterprise' ? '企业' : '通道'} / <CarrierTag carrier={item.carrier} /></span> },
{ key: 'mode', title: '方式', render: (item) => item.mode === 'permanent' ? '永久抑制' : `临时至 ${formatDate(item.muteUntil)}` },
{ key: 'reason', title: '原因', render: (item) => item.reason || '-' },
{ key: 'actions', title: '操作', align: 'right', render: (item) => <Button icon={<ShieldOff size={15} />} onClick={() => { setActionError(''); setCancelSuppressionDraft({ id: item.id, reason: '' }); }} size="sm" variant="ghost"></Button> },
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ArrowLeft, Globe2, Info, RadioTower, RefreshCw } from 'lucide-react';
import { adminApi, type ChannelGroup, type DictionaryItem, type EnterpriseApplication, type HttpApiConfig } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Select, Tag } from '@/components/ui';
import { Breadcrumb, Button, CarrierTag, Input, Select, Tag } from '@/components/ui';
import { isValidMoneyInput, moneyUnitsToYuan, yuanToMoneyUnits } from '@/utils/currency';
import { createRandomHex } from '@/utils/randomId';
@@ -450,7 +450,7 @@ export function AdminSmsApplicationFormPage() {
<header>
<span><RadioTower size={18} /></span>
<div>
<strong>{meta.label}</strong>
<strong><CarrierTag carrier={card.carrier} /> </strong>
<small>{meta.description}</small>
</div>
<Tag tone={card.groupId ? 'success' : available.length ? 'neutral' : 'warning'}>{card.groupId ? '已选择' : `${available.length} 个可选`}</Tag>
+2 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Check, Eye, Search, X } from 'lucide-react';
import { adminApi, type RiskReviewTask, type RiskTaskMessagePage } from '@/api/adminApi';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, CarrierTag, DateRangeInput, Input, Modal, Pagination, Select, Table, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
const statusLabel: Record<string, string> = {
@@ -263,7 +263,7 @@ export function AdminSmsAuditPage() {
columns={[
{ key: 'phoneNumber', title: '手机号码', render: (item) => <strong>{item.phoneNumber}</strong> },
{ key: 'province', title: '号码归属地', render: (item) => item.province || '-' },
{ key: 'carrier', title: '运营商', render: (item) => item.carrier || '-' },
{ key: 'carrier', title: '运营商', render: (item) => item.carrier ? <CarrierTag carrier={item.carrier} /> : '-' },
{ key: 'status', title: '短信记录状态', render: (item) => <Tag tone={item.status === 'pending_review' ? 'warning' : item.status === 'failed' || item.status === 'submit_failed' ? 'danger' : 'info'}>{messageStatusLabel(item.status)}</Tag> },
]}
data={phoneData.items}
@@ -55,8 +55,8 @@ export function EnterpriseApplicationTable({
{ key: 'name', title: '应用名称', width: '180px', render: (record) => <strong>{record.name}</strong> },
{ key: 'enterprise', title: '企业名称', width: '220px', render: (record) => record.enterprise },
{ key: 'sentToday', title: '今日发送', width: '120px', render: (record) => `${record.sentToday.toLocaleString('zh-CN')}` },
{ key: 'deliveryRate', title: '到达率', width: '130px', render: (record) => `${record.deliveryRate}%` },
{ key: 'unitPrice', title: '单价', width: '130px', render: (record) => <strong className="application-unit-price">{formatAmount(record.unitPrice)} </strong> },
{ key: 'deliveryRate', title: '到达率', width: '104px', render: (record) => `${record.deliveryRate}%` },
{ key: 'unitPrice', title: '单价', width: '104px', render: (record) => <strong className="application-unit-price">{formatAmount(record.unitPrice)} </strong> },
{
key: 'cmppStatus',
title: '客户连接状态',
@@ -89,7 +89,7 @@ export function EnterpriseApplicationTable({
</div>
),
},
{ key: 'enabled', title: '状态', width: '130px', render: applicationStatusTag },
{ key: 'enabled', title: '状态', width: '104px', render: applicationStatusTag },
{
key: 'actions',
title: '操作',
@@ -1,7 +1,7 @@
import type { Dispatch, SetStateAction } from 'react';
import { ChevronDown, ChevronRight, Edit3, FileText, Plus } from 'lucide-react';
import type { ClientSmsSignature } from '@/api/adminApi';
import { Button, DeleteRiskAction, Pagination } from '@/components/ui';
import { Button, CarrierTag, DeleteRiskAction, Pagination } from '@/components/ui';
import { AuditStatusTag, CarrierReportTag, readDrainagePayload, signatureCardVisual } from './signature.helpers';
import type { DrainageInfo } from './signature.types';
@@ -63,9 +63,9 @@ export function EnterpriseSignaturesTable({
<div><span></span><span className="signature-summary__regular-value">{signature.tenant?.name ?? signature.tenantId}</span></div>
<div><span></span><span className="signature-summary__regular-value">{signature.application?.name ?? '-'}</span></div>
<div><span></span><AuditStatusTag status={signature.auditStatus} /></div>
<div><span></span><CarrierReportTag summary={signature.carrierReportSummary?.mobile} /></div>
<div><span></span><CarrierReportTag summary={signature.carrierReportSummary?.unicom} /></div>
<div><span></span><CarrierReportTag summary={signature.carrierReportSummary?.telecom} /></div>
<div><CarrierTag carrier="mobile" /><CarrierReportTag summary={signature.carrierReportSummary?.mobile} /></div>
<div><CarrierTag carrier="unicom" /><CarrierReportTag summary={signature.carrierReportSummary?.unicom} /></div>
<div><CarrierTag carrier="telecom" /><CarrierReportTag summary={signature.carrierReportSummary?.telecom} /></div>
<div><span></span><strong>{payload.links.length} </strong></div>
<div className="signature-actions">
<Button icon={<FileText size={16} />} onClick={() => setSignatureReport(signature)} size="sm" variant="ghost"></Button>
@@ -82,9 +82,9 @@ export function EnterpriseSignaturesTable({
<div className="drainage-table__head">
<span>url或号码</span>
<span></span>
<span></span>
<span></span>
<span></span>
<CarrierTag carrier="mobile" />
<CarrierTag carrier="unicom" />
<CarrierTag carrier="telecom" />
<span></span>
</div>
{visibleDrainageLinks.map((item) => {
@@ -23,11 +23,11 @@ export function SignatureReportModal({ item, onClose }: { item: ClientSmsSignatu
<div><span></span><strong>{formatDate(item.updatedAt)}</strong></div>
</div>
<div className="admin-report-tabs">
<button className="admin-report-carrier--mobile active" type="button"><strong></strong><span><CarrierReportTag summary={item.carrierReportSummary?.mobile} /></span></button>
<button className="admin-report-carrier--unicom active" type="button"><strong></strong><span><CarrierReportTag summary={item.carrierReportSummary?.unicom} /></span></button>
<button className="admin-report-carrier--telecom active" type="button"><strong></strong><span><CarrierReportTag summary={item.carrierReportSummary?.telecom} /></span></button>
<button className="admin-report-carrier--mobile active" type="button"><CarrierTag carrier="mobile" /><span><CarrierReportTag summary={item.carrierReportSummary?.mobile} /></span></button>
<button className="admin-report-carrier--unicom active" type="button"><CarrierTag carrier="unicom" /><span><CarrierReportTag summary={item.carrierReportSummary?.unicom} /></span></button>
<button className="admin-report-carrier--telecom active" type="button"><CarrierTag carrier="telecom" /><span><CarrierReportTag summary={item.carrierReportSummary?.telecom} /></span></button>
</div>
<div className="page-stack">{(item.reportTargets ?? []).map((target) => <div className="surface" key={`${target.channelId}:${target.carrier}`} style={{ display: 'flex', justifyContent: 'space-between', padding: 12 }}><span>{target.channel.name}{carrierLabel(target.carrier)}</span><CarrierReportTag summary={{ status: target.status, approved: target.status === 'approved' ? 1 : 0, total: 1 }} /></div>)}</div>
<div className="page-stack">{(item.reportTargets ?? []).map((target) => <div className="surface" key={`${target.channelId}:${target.carrier}`} style={{ display: 'flex', justifyContent: 'space-between', padding: 12 }}><span>{target.channel.name} <CarrierTag carrier={target.carrier} /></span><CarrierReportTag summary={{ status: target.status, approved: target.status === 'approved' ? 1 : 0, total: 1 }} /></div>)}</div>
</div>
</Modal>
);
@@ -64,9 +64,9 @@ export function DrainageReportModal({ item, onClose, signature }: { item: Draina
<div className="detail-grid">
<div><span>url或号码</span><strong>{item.url}</strong></div>
<div><span>url或号码</span><strong>{item.url}</strong></div>
<div><span></span><CarrierReportTag summary={summary?.mobile} /></div>
<div><span></span><CarrierReportTag summary={summary?.unicom} /></div>
<div><span></span><CarrierReportTag summary={summary?.telecom} /></div>
<div><CarrierTag carrier="mobile" /><CarrierReportTag summary={summary?.mobile} /></div>
<div><CarrierTag carrier="unicom" /><CarrierReportTag summary={summary?.unicom} /></div>
<div><CarrierTag carrier="telecom" /><CarrierReportTag summary={summary?.telecom} /></div>
<div className="detail-grid__wide"><span></span><strong>{item.remark || '-'}</strong></div>
</div>
<div className="page-stack" style={{ marginTop: 16 }}>{targets.map((target) => <div className="surface" key={target.channelId} style={{ display: 'flex', justifyContent: 'space-between', padding: 12 }}><span>{target.channel.name}{carrierLabel(target.channel.carrier)}</span><CarrierReportTag summary={{ status: target.status, approved: target.status === 'approved' ? 1 : 0, total: 1 }} /></div>)}</div>
@@ -1,9 +1,8 @@
import { AlertTriangle, Info, MessageSquare } from 'lucide-react';
import type { SmsMessageRecord, SmsMessageSegmentAudit } from '@/api/adminApi';
import { Button, Modal, Tag } from '@/components/ui';
import { Button, CarrierTag, Modal, Tag } from '@/components/ui';
import {
buildRouteRows,
getCarrierLabel,
getReceiptNotice,
getRecordStatus,
getRecordStatusLabel,
@@ -52,7 +51,7 @@ export function SendDetailModal({
<div><span></span><strong>{record.receiptStatus ?? '-'}</strong></div>
<div><span></span><strong>{getTime(record.queuedAt)}</strong></div>
<div><span></span><strong>{record.phoneNumber || '-'}</strong></div>
<div><span></span><strong>{record.province ?? '-'} / {getCarrierLabel(record.carrier)}</strong></div>
<div><span></span><strong>{record.province ?? '-'} / {record.carrier ? <CarrierTag carrier={record.carrier} /> : '-'}</strong></div>
<div><span></span><strong>{channelGroupNames.join(' / ') || '-'}</strong></div>
<div><span></span><strong>{record.clientSrcId || '-'}</strong></div>
<div><span></span><strong>{sentAccessNumber || '-'}</strong></div>
+2 -3
View File
@@ -1,10 +1,9 @@
import { Download } from 'lucide-react';
import type { ReactNode } from 'react';
import type { SmsMessageRecord } from '@/api/adminApi';
import { Button, MoneyText, Pagination } from '@/components/ui';
import { Button, CarrierTag, MoneyText, Pagination } from '@/components/ui';
import { formatCents } from '@/utils/currency';
import {
getCarrierLabel,
getClock,
getDate,
getRecordStatus,
@@ -84,7 +83,7 @@ export function SmsRecordList({
</span>
</p>
<div className="admin-sms-record-card__meta">
<div><span></span><strong>{record.phoneNumber}</strong><small>{record.province ?? '-'} · {getCarrierLabel(record.carrier)}</small></div>
<div><span></span><strong>{record.phoneNumber}</strong><small>{record.province ?? '-'} {record.carrier ? <CarrierTag carrier={record.carrier} /> : '-'}</small></div>
<div><span></span><strong>{record.billingUnits} / <MoneyText>¥{formatCents(record.amountCents)}</MoneyText></strong><small>{record.content.length} </small></div>
<div><span></span><strong>{record.channel?.name ?? record.channelId ?? '-'}</strong><small> {getTime(record.deliveredAt)}</small></div>
</div>
@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import { Search } from 'lucide-react';
import { adminApi, type BatchTaskMessagePage } from '@/api/adminApi';
import { Button, Input, Modal, Pagination, Select, Table, Tag } from '@/components/ui';
import { carrierLabels, messageStatusLabel } from './taskModel';
import { Button, CarrierTag, Input, Modal, Pagination, Select, Table, Tag } from '@/components/ui';
import { messageStatusLabel } from './taskModel';
import type { SmsTask } from './taskTypes';
type TaskPhoneListModalProps = {
@@ -55,7 +55,7 @@ export function TaskPhoneListModal({ task, onClose, onError }: TaskPhoneListModa
columns={[
{ key: 'phoneNumber', title: '手机号码', render: (item) => <strong>{item.phoneNumber}</strong> },
{ key: 'province', title: '号码归属地', render: (item) => item.province || '-' },
{ key: 'carrier', title: '运营商', render: (item) => carrierLabels[item.carrier ?? '']?.label ?? item.carrier ?? '-' },
{ key: 'carrier', title: '运营商', render: (item) => item.carrier ? <CarrierTag carrier={item.carrier} /> : '-' },
{ key: 'status', title: '短信记录状态', render: (item) => <Tag tone={item.status === 'delivered' ? 'success' : ['failed', 'submit_failed', 'rejected', 'timeout'].includes(item.status) ? 'danger' : 'info'}>{messageStatusLabel(item.status)}</Tag> },
]}
data={data.items}
+2 -9
View File
@@ -3,6 +3,7 @@ import { FileText, Search, Smartphone } from 'lucide-react';
import { clientApi, type SmsMessageRecord } from '@/api/adminApi';
import {
DateRangeInput,
CarrierTag,
Input,
Pagination,
QueryPanel,
@@ -33,13 +34,6 @@ const statusToneMap: Record<string, 'success' | 'info' | 'danger' | 'neutral'> =
timeout: 'danger',
};
const carrierLabelMap: Record<string, string> = {
mobile: '中国移动',
unicom: '中国联通',
telecom: '中国电信',
all: '三网',
};
function getDate(value?: string | null) {
return value ? value.slice(0, 10) : '';
}
@@ -180,7 +174,6 @@ export function ClientSendDetailPage() {
<tr><td className="ui-table__empty" colSpan={9}></td></tr>
) : visibleRows.map((record) => {
const receipt = getReceipt(record);
const carrier = record.carrier ? carrierLabelMap[record.carrier] ?? record.carrier : '-';
const region = record.province ?? '-';
return (
<Fragment key={record.id}>
@@ -199,7 +192,7 @@ export function ClientSendDetailPage() {
</span>
</td>
<td><strong>{record.phoneNumber}</strong></td>
<td><strong className="send-detail-carrier">{carrier}</strong></td>
<td>{record.carrier ? <CarrierTag carrier={record.carrier} /> : '-'}</td>
<td><span className="send-detail-region">{region}</span></td>
<td style={{ textAlign: 'center' }}><Tag tone={statusToneMap[record.status] ?? 'info'}>{statusLabelMap[record.status] ?? record.status}</Tag></td>
<td style={{ textAlign: 'center' }}><strong className="send-detail-receipt-code">{receipt.status}</strong></td>