feat: 优化通道运营商与金额展示
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
type UnreportedSignatureItem,
|
||||
type PagedResult,
|
||||
} from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Pagination, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, CarrierTag, Input, Pagination, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { successRateClassName, successRateTone } from '@/utils/successRate';
|
||||
|
||||
const carrierOrder = ['mobile', 'unicom', 'telecom', 'unknown'];
|
||||
@@ -348,7 +348,7 @@ function RetirementHeatmap({ date, dimensionType, dimensions, items, title }: {
|
||||
<strong title={`企业:${row.tenantName}\n企业应用:${row.applicationName || '未绑定企业应用'}`}>{row.signatureName}</strong>
|
||||
{row.channelName ? <small>{row.channelName}</small> : null}
|
||||
</span>
|
||||
<Tag tone="neutral">{carrierLabels[row.carrier] ?? row.carrier}</Tag>
|
||||
<CarrierTag carrier={row.carrier} />
|
||||
</th>
|
||||
<td className="signature-retirement-heatmap__total">{row.total.toLocaleString('zh-CN')}</td>
|
||||
{dates.map((dateKey) => {
|
||||
@@ -508,7 +508,7 @@ function SignatureQualityDrawer({
|
||||
{carriers.map((carrier) => (
|
||||
<article className={`signature-carrier-card signature-carrier-card--${normalizeCarrier(carrier.carrier)}`} key={carrier.carrier}>
|
||||
<div>
|
||||
<Tag tone={carrierTagTone(carrier.carrier)}>{carrierLabel(carrier.carrier)}</Tag>
|
||||
<CarrierTag carrier={carrier.carrier} />
|
||||
<strong>{carrier.businessMessageCount.toLocaleString('zh-CN')} 条业务短信</strong>
|
||||
</div>
|
||||
<dl>
|
||||
@@ -639,14 +639,6 @@ function carrierLabel(value: string) {
|
||||
return carrierLabels[normalizeCarrier(value)] ?? '未知';
|
||||
}
|
||||
|
||||
function carrierTagTone(value: string): 'info' | 'accent' | 'warning' | 'neutral' {
|
||||
const carrier = normalizeCarrier(value);
|
||||
if (carrier === 'mobile') return 'info';
|
||||
if (carrier === 'unicom') return 'accent';
|
||||
if (carrier === 'telecom') return 'warning';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
function formatDuration(value?: number | null) {
|
||||
if (value == null) return '—';
|
||||
if (value < 1000) return `${Math.round(value)} 毫秒`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||
import { Database, ListFilter, Plus, RotateCcw, Search, Trash2 } from 'lucide-react';
|
||||
import { Breadcrumb, Button, Input, Modal, Pagination, Select, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, CarrierTag, Input, Modal, Pagination, Select, Table, Tabs, type TableColumn } from '@/components/ui';
|
||||
import { adminApi, type DictionaryItem } from '@/api/adminApi';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
@@ -18,18 +18,6 @@ type CarrierRule = DictionaryItem & {
|
||||
remark?: string | null;
|
||||
};
|
||||
|
||||
const carrierTone: Record<string, 'success' | 'info' | 'warning' | 'neutral'> = {
|
||||
中国移动: 'success',
|
||||
中国联通: 'info',
|
||||
中国电信: 'warning',
|
||||
};
|
||||
|
||||
const ruleCarrierMeta: Record<string, { label: string; tone: 'success' | 'info' | 'warning' | 'neutral' }> = {
|
||||
mobile: { label: '中国移动', tone: 'success' },
|
||||
unicom: { label: '中国联通', tone: 'info' },
|
||||
telecom: { label: '中国电信', tone: 'warning' },
|
||||
};
|
||||
|
||||
type PhoneSegmentSummaryProps = {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
@@ -165,7 +153,7 @@ export function AdminPhoneSegmentsPage() {
|
||||
|
||||
const columns = useMemo<Array<TableColumn<PhoneSegment>>>(() => [
|
||||
{ key: 'segment', title: '手机号段(前7位)', width: '190px', render: (record) => <strong className="phone-segment-prefix">{record.prefix}</strong> },
|
||||
{ key: 'carrier', title: '运营商', width: '130px', render: (record) => record.carrier ? <Tag tone={carrierTone[record.carrier] ?? 'neutral'}>{record.carrier}</Tag> : '-' },
|
||||
{ key: 'carrier', title: '运营商', width: '130px', render: (record) => record.carrier ? <CarrierTag carrier={record.carrier} /> : '-' },
|
||||
{ key: 'province', title: '省份', width: '110px', render: (record) => record.province ?? '-' },
|
||||
{ key: 'city', title: '城市', width: '110px', render: (record) => record.city ?? '-' },
|
||||
{ key: 'createdAt', title: '创建时间', width: '170px', render: (record) => formatDateTime(record.createdAt) },
|
||||
@@ -173,7 +161,7 @@ export function AdminPhoneSegmentsPage() {
|
||||
], []);
|
||||
|
||||
const ruleColumns = useMemo<Array<TableColumn<CarrierRule>>>(() => [
|
||||
{ key: 'carrier', title: '运营商', width: '140px', render: (record) => { const meta = ruleCarrierMeta[record.carrier ?? '']; return meta ? <Tag tone={meta.tone}>{meta.label}</Tag> : record.carrier ?? '-'; } },
|
||||
{ key: 'carrier', title: '运营商', width: '140px', render: (record) => record.carrier ? <CarrierTag carrier={record.carrier} /> : '-' },
|
||||
{ key: 'pattern', title: '号码前缀正则', render: (record) => <strong>{record.pattern}</strong> },
|
||||
{ key: 'priority', title: '优先级', width: '120px', render: (record) => record.priority ?? 100 },
|
||||
{ key: 'remark', title: '备注', render: (record) => record.remark ?? '-' },
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type SignatureRetirementWebhook,
|
||||
type TenantOption,
|
||||
} from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Table, Tabs, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, CarrierTag, DateRangeInput, Input, Modal, Pagination, Select, Table, Tabs, Tag, Textarea, type DateRangeValue, type TableColumn } from '@/components/ui';
|
||||
|
||||
const carriers = ['mobile', 'unicom', 'telecom'] as const;
|
||||
const carrierLabels = { mobile: '移动', unicom: '联通', telecom: '电信' };
|
||||
@@ -112,7 +112,7 @@ export function AdminSignatureRetirementPage() {
|
||||
const messageColumns: Array<TableColumn<SignatureRetirementMessage>> = [
|
||||
{ key: 'title', title: '预警', width: '340px', render: (item) => <div className="ui-table__long-text"><strong>{item.title}</strong><br /><span>{item.content}</span></div> },
|
||||
{ key: 'dimension', title: '维度', width: '240px', render: (item) => <>{item.tenantName ?? '-'}<br /><small>{item.applicationName ?? '未关联企业应用'} / {item.signatureName ?? '-'}</small><br /><small>{item.channelName ?? '企业维度'}</small></> },
|
||||
{ key: 'carrier', title: '运营商', width: '90px', render: (item) => <Tag tone="warning">{carrierLabels[item.detection?.carrier ?? 'mobile']}</Tag> },
|
||||
{ key: 'carrier', title: '运营商', width: '90px', render: (item) => <CarrierTag carrier={item.detection?.carrier ?? 'mobile'} /> },
|
||||
{ key: 'count', title: '活动量', width: '130px', render: (item) => item.detection ? `${item.detection.acceptedBusinessCount} / 阈值${item.detection.threshold}` : '-' },
|
||||
{ key: 'time', title: '消息时间', width: '170px', render: (item) => formatDateTime(item.createdAt) },
|
||||
{ key: 'state', title: '状态', width: '90px', render: (item) => item.suppressed ? <Tag>已抑制</Tag> : item.isRead ? <Tag tone="info">已读</Tag> : <Tag tone="warning">未读</Tag> },
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
.sms-channel-table__row {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: minmax(150px, 1.2fr) 88px 86px 96px minmax(280px, 1.6fr) 190px;
|
||||
grid-template-columns: minmax(190px, 1.25fr) 110px 86px 96px minmax(280px, 1.6fr) 190px;
|
||||
min-width: 940px;
|
||||
}
|
||||
|
||||
@@ -60,11 +60,18 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sms-channel-identity span {
|
||||
.sms-channel-identity > span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.sms-channel-identity__carriers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.sms-channel-carrier-price {
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
@@ -72,6 +79,12 @@
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.sms-channel-carrier-price strong {
|
||||
color: var(--color-text-strong);
|
||||
font-size: var(--font-size-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.sms-channel-status-cell {
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
@@ -136,6 +149,10 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sms-channel-rate .sms-channel-rate__empty {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.sms-channel-actions {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Copy, Eye, FileText, Pencil, Power, Send } from 'lucide-react';
|
||||
import { DeleteRiskAction, MoneyText, Pagination, Tag } from '@/components/ui';
|
||||
import { formatCents } from '@/utils/currency';
|
||||
import { CarrierTag, DeleteRiskAction, Pagination, Tag } from '@/components/ui';
|
||||
import { formatRateAmount } from '@/utils/currency';
|
||||
import { successRateClassName } from '@/utils/successRate';
|
||||
import { carrierLabelMap, carrierToneMap, statusLabelMap, statusToneMap } from './channelModel';
|
||||
import { statusLabelMap, statusToneMap } from './channelModel';
|
||||
import type { ChannelConfirmAction, ChannelModalState, SmsChannel } from './channelTypes';
|
||||
|
||||
function RateBlock({ label, rate, count, isSuccess = false }: { label: string; rate: number; count: number; isSuccess?: boolean }) {
|
||||
function RateBlock({ label, rate, count, empty, isSuccess = false }: { label: string; rate: number; count: number; empty: boolean; isSuccess?: boolean }) {
|
||||
return (
|
||||
<div className="sms-channel-rate">
|
||||
<small>{label}</small>
|
||||
<strong className={isSuccess ? successRateClassName(rate) : undefined}>{rate}%</strong>
|
||||
<strong className={empty ? 'sms-channel-rate__empty' : isSuccess ? successRateClassName(rate) : undefined}>{empty ? '-' : `${rate}%`}</strong>
|
||||
<span>{count.toLocaleString('zh-CN')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -44,7 +44,7 @@ export function ChannelTable({
|
||||
<div className="surface sms-channel-table">
|
||||
<div className="sms-channel-table__head">
|
||||
<span>通道信息</span>
|
||||
<span>运营商 / 成本</span>
|
||||
<span>成本费率</span>
|
||||
<span>状态</span>
|
||||
<span>今日提交</span>
|
||||
<span>今日提交 / 送达质量</span>
|
||||
@@ -55,10 +55,10 @@ export function ChannelTable({
|
||||
<div className="sms-channel-identity">
|
||||
<strong>{channel.name}</strong>
|
||||
<span>通道 ID:{channel.id}</span>
|
||||
<div className="sms-channel-identity__carriers">{channel.carriers.map((carrier) => <CarrierTag carrier={carrier} key={carrier} />)}</div>
|
||||
</div>
|
||||
<div className="sms-channel-carrier-price">
|
||||
<div>{channel.carriers.map((carrier) => <Tag key={carrier} tone={carrierToneMap[carrier]}>{carrierLabelMap[carrier]}</Tag>)}</div>
|
||||
<strong><MoneyText>{formatCents(channel.unitPrice)} 元</MoneyText></strong>
|
||||
<strong>{formatRateAmount(channel.unitPrice / 10_000)} 元/条</strong>
|
||||
</div>
|
||||
<div className="sms-channel-status-cell">
|
||||
<Tag tone={statusToneMap[channel.status]}>{statusLabelMap[channel.status]}</Tag>
|
||||
@@ -68,10 +68,10 @@ export function ChannelTable({
|
||||
</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} />
|
||||
<RateBlock count={channel.successCount} isSuccess label="送达成功" rate={channel.successRate} />
|
||||
<RateBlock count={channel.unknownCount} label="回执未知" rate={channel.unknownRate} />
|
||||
<RateBlock count={channel.failureCount} label="送达失败" rate={channel.failureRate} />
|
||||
<RateBlock count={channel.submitFailureCount} empty={channel.total === 0} label="提交失败" rate={channel.submitFailureRate} />
|
||||
<RateBlock count={channel.successCount} empty={channel.total === 0} isSuccess label="送达成功" rate={channel.successRate} />
|
||||
<RateBlock count={channel.unknownCount} empty={channel.total === 0} label="回执未知" rate={channel.unknownRate} />
|
||||
<RateBlock count={channel.failureCount} empty={channel.total === 0} label="送达失败" rate={channel.failureRate} />
|
||||
</div>
|
||||
<div className="sms-channel-actions">
|
||||
<button className="sms-channel-report-entry" onClick={() => onOpenReports(channel)} type="button">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Edit3, Settings2, Trash2 } from 'lucide-react';
|
||||
import { Button, MoneyText, Pagination, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Button, Pagination, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
import { formatAmount } from '@/utils/currency';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
import type { ConfirmAction, SmsApp } from './applicationTypes';
|
||||
@@ -56,7 +56,7 @@ export function EnterpriseApplicationTable({
|
||||
{ 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) => <MoneyText>{formatAmount(record.unitPrice)} 元</MoneyText> },
|
||||
{ key: 'unitPrice', title: '单价', width: '130px', render: (record) => <strong className="application-unit-price">{formatAmount(record.unitPrice)} 元</strong> },
|
||||
{
|
||||
key: 'cmppStatus',
|
||||
title: '客户连接状态',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Info } from 'lucide-react';
|
||||
import { adminApi, type ClientSmsSignature } from '@/api/adminApi';
|
||||
import { Button, Modal, Select, Textarea } from '@/components/ui';
|
||||
import { Button, CarrierTag, Modal, Select, Textarea } from '@/components/ui';
|
||||
import { AuditStatusTag, carrierLabel, CarrierReportTag, formatDate } from './signature.helpers';
|
||||
import type { DrainageInfo } from './signature.types';
|
||||
|
||||
@@ -35,6 +35,7 @@ export function SignatureReportModal({ item, onClose }: { item: ClientSmsSignatu
|
||||
|
||||
export function ChannelReportStatusModal({ item, onClose, onSaved }: { item: ClientSmsSignature; onClose: () => void; onSaved: () => void }) {
|
||||
const targets = item.reportTargets ?? [];
|
||||
const carriers = ['mobile', 'unicom', 'telecom'] as const;
|
||||
const [statuses, setStatuses] = useState<Record<string, string>>(() => Object.fromEntries(targets.map((target) => [`${target.channelId}:${target.carrier}`, target.status])));
|
||||
const [reason, setReason] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -46,10 +47,10 @@ export function ChannelReportStatusModal({ item, onClose, onSaved }: { item: Cli
|
||||
onSaved();
|
||||
} catch (failure) { setError(failure instanceof Error ? failure.message : '报备状态保存失败'); } finally { setSaving(false); }
|
||||
}
|
||||
return <Modal footer={<><Button onClick={onClose} variant="ghost">取消</Button><Button disabled={!targets.length || saving} onClick={() => void save()}>{saving ? '保存中...' : '保存状态'}</Button></>} onClose={onClose} open size="xl" title="按通道修改签名报备状态">
|
||||
<div className="page-stack"><div className="signature-alert"><Info size={18} /><span>企业签名只展示汇总结果;这里修改的是每个具体通道的报备任务,保存后会同步通道详情、报备任务和企业签名三网状态。</span></div>
|
||||
return <Modal footer={<><Button onClick={onClose} variant="ghost">取消</Button><Button disabled={!targets.length || saving} onClick={() => void save()}>{saving ? '保存中...' : '保存状态'}</Button></>} onClose={onClose} open size="xl" title="修改签名报备状态">
|
||||
<div className="signature-report-status"><div className="signature-report-status__context"><strong>{item.name}</strong><span>{item.tenant?.name ?? item.tenantId} · {item.application?.name ?? '-'}</span></div><div className="signature-alert"><Info size={18} /><span>修改具体通道的报备状态;保存后同步通道详情、报备任务和企业签名三网状态。</span></div>
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
{targets.length ? targets.map((target) => { const key = `${target.channelId}:${target.carrier}`; return <div className="surface admin-report-target-row" key={key}><div><strong>{target.channel.name}</strong><div className="muted">{carrierLabel(target.carrier)}</div></div><Select onChange={(event) => setStatuses((current) => ({ ...current, [key]: event.target.value }))} options={reportStatusOptions} value={statuses[key] ?? target.status} /></div>; }) : <div className="empty-state">该企业应用当前没有配置目标通道。</div>}
|
||||
{targets.length ? <div className="signature-report-status__carriers">{carriers.map((carrier) => { const carrierTargets = targets.filter((target) => target.carrier === carrier); return <section className="signature-report-status__carrier" key={carrier}><header><CarrierTag carrier={carrier} /><span>{carrierTargets.length} 个通道</span></header><div className="signature-report-status__list">{carrierTargets.length ? carrierTargets.map((target) => { const key = `${target.channelId}:${target.carrier}`; return <div className="signature-report-status__row" key={key}><strong title={target.channel.name}>{target.channel.name}</strong><Select aria-label={`${target.channel.name}${carrierLabel(target.carrier)}报备状态`} onChange={(event) => setStatuses((current) => ({ ...current, [key]: event.target.value }))} options={reportStatusOptions} value={statuses[key] ?? target.status} /></div>; }) : <div className="signature-report-status__empty">暂无{carrierLabel(carrier)}目标通道</div>}</div></section>; })}</div> : <div className="empty-state">该企业应用当前没有配置目标通道。</div>}
|
||||
<Textarea label="修改原因" onChange={(event) => setReason(event.target.value)} placeholder="请输入运营商工单、确认依据或人工处理说明" rows={3} value={reason} />
|
||||
</div>
|
||||
</Modal>;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
const carrierMeta = {
|
||||
mobile: { label: '移动', className: 'ui-carrier-tag--mobile' },
|
||||
unicom: { label: '联通', className: 'ui-carrier-tag--unicom' },
|
||||
telecom: { label: '电信', className: 'ui-carrier-tag--telecom' },
|
||||
} as const;
|
||||
|
||||
export type CarrierTagValue = keyof typeof carrierMeta;
|
||||
|
||||
export function normalizeCarrierTag(value?: string | null): CarrierTagValue | null {
|
||||
const normalized = String(value ?? '').trim().toLowerCase();
|
||||
if (['mobile', 'cmcc', '移动', '中国移动'].includes(normalized)) return 'mobile';
|
||||
if (['unicom', 'cucc', '联通', '中国联通'].includes(normalized)) return 'unicom';
|
||||
if (['telecom', 'ctcc', '电信', '中国电信'].includes(normalized)) return 'telecom';
|
||||
return null;
|
||||
}
|
||||
|
||||
export function CarrierTag({ carrier, className = '', ...props }: HTMLAttributes<HTMLSpanElement> & { carrier: string }) {
|
||||
const normalized = normalizeCarrierTag(carrier);
|
||||
if (!normalized) return <span className={['ui-carrier-tag', 'ui-carrier-tag--neutral', className].filter(Boolean).join(' ')} {...props}>{carrier || '未知'}</span>;
|
||||
const meta = carrierMeta[normalized];
|
||||
return <span className={['ui-carrier-tag', meta.className, className].filter(Boolean).join(' ')} {...props}>{meta.label}</span>;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ type ModalProps = {
|
||||
initialFocusRef?: RefObject<HTMLElement | null>;
|
||||
closeGuardTitle?: string;
|
||||
closeGuardDescription?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const focusableSelector = [
|
||||
@@ -96,6 +97,7 @@ export function Modal({
|
||||
initialFocusRef,
|
||||
closeGuardTitle = '放弃未保存的修改?',
|
||||
closeGuardDescription = '当前内容尚未保存。放弃后无法恢复,请确认是否关闭。',
|
||||
className,
|
||||
}: ModalProps) {
|
||||
const titleId = useId();
|
||||
const guardTitleId = useId();
|
||||
@@ -213,7 +215,7 @@ export function Modal({
|
||||
<section
|
||||
aria-labelledby={titleId}
|
||||
aria-modal="true"
|
||||
className={['ui-modal__panel', `ui-modal__panel--${size}`].join(' ')}
|
||||
className={['ui-modal__panel', `ui-modal__panel--${size}`, className].filter(Boolean).join(' ')}
|
||||
ref={panelRef}
|
||||
role="dialog"
|
||||
tabIndex={-1}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Children, type ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export function MoneyText({ children, className }: { children: ReactNode; className?: string }) {
|
||||
const text = Children.toArray(children).map((value) => typeof value === 'string' || typeof value === 'number' ? String(value) : '').join('');
|
||||
const match = text.match(/^(.*?)(\.\d+)(\s*[^\d]*)$/);
|
||||
if (!match) return <span className={className}>{text}</span>;
|
||||
return <span className={className}>{match[1]}<span className="money-text__fraction">{match[2]}</span>{match[3]}</span>;
|
||||
return <span className={className}>{children}</span>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { formatCents } from '@/utils/currency';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
import { Button } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
import { MoneyText } from './MoneyText';
|
||||
|
||||
type RechargeReceiptDialogProps = {
|
||||
open: boolean;
|
||||
@@ -14,7 +13,7 @@ type RechargeReceiptDialogProps = {
|
||||
};
|
||||
|
||||
export function formatReceiptAmount(moneyUnits: number) {
|
||||
return formatCents(Math.abs(moneyUnits)).replace(/\.?0+$/, '');
|
||||
return formatCents(Math.abs(moneyUnits));
|
||||
}
|
||||
|
||||
export function RechargeReceiptDialog({
|
||||
@@ -35,12 +34,13 @@ export function RechargeReceiptDialog({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="recharge-receipt-modal"
|
||||
footer={<Button onClick={onClose}>完成</Button>}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
title="账户充值回执"
|
||||
>
|
||||
<article className="recharge-receipt">
|
||||
<article className={['recharge-receipt', isCorrection ? 'recharge-receipt--correction' : ''].filter(Boolean).join(' ')}>
|
||||
<header className="recharge-receipt__header">
|
||||
<img
|
||||
alt="聆界短信服务平台"
|
||||
@@ -58,7 +58,7 @@ export function RechargeReceiptDialog({
|
||||
<strong>
|
||||
{isCorrection ? '−' : '+'}
|
||||
<small>¥</small>
|
||||
<MoneyText>{formatReceiptAmount(record.amountCents)}</MoneyText>
|
||||
{formatReceiptAmount(record.amountCents)}
|
||||
</strong>
|
||||
</section>
|
||||
|
||||
@@ -79,11 +79,11 @@ export function RechargeReceiptDialog({
|
||||
</div>
|
||||
<div>
|
||||
<dt>入账前余额</dt>
|
||||
<dd>{balanceBefore === null ? '-' : <MoneyText>¥{formatCents(balanceBefore)}</MoneyText>}</dd>
|
||||
<dd>{balanceBefore === null ? '-' : <>¥{formatCents(balanceBefore)}</>}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>入账后余额</dt>
|
||||
<dd>{balanceAfter === null || balanceAfter === undefined ? '-' : <MoneyText>¥{formatCents(balanceAfter)}</MoneyText>}</dd>
|
||||
<dd>{balanceAfter === null || balanceAfter === undefined ? '-' : <>¥{formatCents(balanceAfter)}</>}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>入账方式</dt>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export { Button } from './Button';
|
||||
export { Breadcrumb } from './Breadcrumb';
|
||||
export { Chart } from './Chart';
|
||||
export { CarrierTag, normalizeCarrierTag } from './CarrierTag';
|
||||
export type { CarrierTagValue } from './CarrierTag';
|
||||
export { DateRangeInput } from './DateRangeInput';
|
||||
export { DateTimeInput } from './DateTimeInput';
|
||||
export { DetailInfoGrid, DetailProgressStats, DetailSection, DetailTitle, getRateTone, ProgressBar, RateCard, RateOverview } from './Detail';
|
||||
|
||||
@@ -1075,6 +1075,24 @@
|
||||
color: var(--color-accent-strong);
|
||||
}
|
||||
|
||||
.ui-carrier-tag {
|
||||
align-items: center;
|
||||
border-radius: var(--radius-full);
|
||||
display: inline-flex;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
min-height: 26px;
|
||||
padding: 0 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-carrier-tag--mobile { background: #e7ebf0; color: #536170; }
|
||||
.ui-carrier-tag--unicom { background: #eeeae7; color: #6a6058; }
|
||||
.ui-carrier-tag--telecom { background: #e7ece9; color: #58685e; }
|
||||
.ui-carrier-tag--neutral { background: #eceff2; color: #596474; }
|
||||
|
||||
.ui-tabs {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
|
||||
+97
-15
@@ -2015,11 +2015,70 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.money-text__fraction {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.92em;
|
||||
.signature-report-status {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.signature-report-status__context {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2) var(--space-3);
|
||||
}
|
||||
|
||||
.signature-report-status__context span { color: var(--color-text-muted); font-size: var(--font-size-sm); }
|
||||
|
||||
.signature-report-status__carriers {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.signature-report-status__carrier {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.signature-report-status__carrier > header {
|
||||
align-items: center;
|
||||
background: var(--color-surface-subtle);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-height: 54px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
|
||||
.signature-report-status__carrier > header > span:last-child { color: var(--color-text-muted); font-size: var(--font-size-xs); }
|
||||
|
||||
.signature-report-status__list {
|
||||
max-height: 280px;
|
||||
min-height: 190px;
|
||||
overflow: auto;
|
||||
padding: 0 var(--space-4);
|
||||
}
|
||||
|
||||
.signature-report-status__row {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: minmax(0, 1fr) 168px;
|
||||
min-height: 64px;
|
||||
}
|
||||
|
||||
.signature-report-status__row:last-child { border-bottom: 0; }
|
||||
.signature-report-status__row strong { font-size: var(--font-size-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.signature-report-status__empty { color: var(--color-text-muted); font-size: var(--font-size-sm); padding: var(--space-7) 0; text-align: center; }
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.signature-report-status__carriers { grid-template-columns: 1fr; }
|
||||
.signature-report-status__list { min-height: 0; }
|
||||
}
|
||||
|
||||
|
||||
.signature-retirement-heatmap__total {
|
||||
color: var(--color-text-strong);
|
||||
font-weight: 700;
|
||||
@@ -7941,6 +8000,15 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.recharge-receipt-modal .ui-modal__header,
|
||||
.recharge-receipt-modal .ui-modal__footer {
|
||||
padding: var(--space-3) var(--space-5);
|
||||
}
|
||||
|
||||
.recharge-receipt-modal .ui-modal__body {
|
||||
padding: var(--space-3) var(--space-5);
|
||||
}
|
||||
|
||||
.recharge-receipt {
|
||||
background:
|
||||
linear-gradient(135deg, rgba(217, 195, 160, 0.18), transparent 42%),
|
||||
@@ -7964,13 +8032,13 @@
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-height: 74px;
|
||||
padding: var(--space-6) var(--space-7) var(--space-4);
|
||||
min-height: 52px;
|
||||
padding: var(--space-3) var(--space-5) var(--space-2);
|
||||
}
|
||||
|
||||
.recharge-receipt__logo {
|
||||
display: block;
|
||||
height: 38px;
|
||||
height: 32px;
|
||||
max-width: 190px;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
@@ -8001,7 +8069,7 @@
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
padding: var(--space-5) var(--space-7) var(--space-8);
|
||||
padding: var(--space-2) var(--space-5) var(--space-3);
|
||||
}
|
||||
|
||||
.recharge-receipt__amount > span,
|
||||
@@ -8013,11 +8081,11 @@
|
||||
|
||||
.recharge-receipt__amount strong {
|
||||
color: var(--color-text-strong);
|
||||
font-size: clamp(36px, 7vw, 52px);
|
||||
font-size: clamp(28px, 4vw, 34px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.035em;
|
||||
line-height: 1.15;
|
||||
margin-top: var(--space-2);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.recharge-receipt__amount small {
|
||||
@@ -8029,12 +8097,12 @@
|
||||
.recharge-receipt__enterprise {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-6) var(--space-7);
|
||||
padding: var(--space-3) var(--space-5);
|
||||
}
|
||||
|
||||
.recharge-receipt__enterprise strong {
|
||||
color: var(--color-text-strong);
|
||||
font-size: var(--font-size-xl);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.recharge-receipt__enterprise small {
|
||||
@@ -8048,14 +8116,14 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin: 0;
|
||||
padding: var(--space-2) var(--space-7);
|
||||
padding: var(--space-1) var(--space-5);
|
||||
}
|
||||
|
||||
.recharge-receipt__details > div {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
min-width: 0;
|
||||
padding: var(--space-4) 0;
|
||||
padding: var(--space-2) 0;
|
||||
}
|
||||
|
||||
.recharge-receipt__details > div:nth-child(even) {
|
||||
@@ -8078,7 +8146,7 @@
|
||||
.recharge-receipt__remark {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-5) var(--space-7);
|
||||
padding: var(--space-2) var(--space-5);
|
||||
}
|
||||
|
||||
.recharge-receipt__remark p {
|
||||
@@ -8093,10 +8161,19 @@
|
||||
border-top: 1px dashed var(--color-border-strong);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
padding: var(--space-4) var(--space-7);
|
||||
padding: var(--space-2) var(--space-5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recharge-receipt--correction .recharge-receipt__amount {
|
||||
padding-bottom: var(--space-2);
|
||||
padding-top: var(--space-1);
|
||||
}
|
||||
|
||||
.recharge-receipt--correction .recharge-receipt__amount strong {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.recharge-receipt__header,
|
||||
.recharge-receipt__enterprise,
|
||||
@@ -9344,3 +9421,8 @@
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
.application-unit-price {
|
||||
color: var(--color-text-strong);
|
||||
font-size: var(--font-size-base);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@ export const MONEY_UNITS_PER_YUAN = 10_000;
|
||||
|
||||
export function formatAmount(value: number) {
|
||||
return value.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 4,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 4,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatRateAmount(value: number) {
|
||||
return value.toLocaleString('zh-CN', { minimumFractionDigits: 4, maximumFractionDigits: 4 });
|
||||
}
|
||||
|
||||
export function formatCents(cents?: number | null) {
|
||||
return formatAmount((cents ?? 0) / MONEY_UNITS_PER_YUAN);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user