feat: 优化签名热力图与金额展示

This commit is contained in:
hectorzhao
2026-08-12 11:41:47 +08:00
parent e64b5e23fe
commit 0cd353450a
33 changed files with 344 additions and 121 deletions
+2 -1
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { adminApi, clientApi, type CaptchaResponse } from '@/api/adminApi';
import { consumeSessionRecovery, markUserActivity, readSessionRecovery, writeSession, type Portal } from '@/api/session';
import { Button, Input, Modal } from '@/components/ui';
import { Button, ClientLoginCanvas, Input, Modal } from '@/components/ui';
type LoginPageProps = {
portal: Portal;
@@ -75,6 +75,7 @@ export function LoginPage({ portal }: LoginPageProps) {
return (
<main className="login-page">
{!isAdmin ? <ClientLoginCanvas /> : null}
<section className="login-panel">
<div className="login-brand">
<img alt={isAdmin ? 'CMPP 运营端 logo' : 'CMPP 客户端 logo'} src="/logo/logo1.png" />
+7 -4
View File
@@ -292,6 +292,7 @@ function RetirementHeatmap({ date, dimensionType, dimensions, items, title }: {
const deferredKeyword = useDeferredValue(keyword.trim().toLocaleLowerCase('zh-CN'));
const visible = items.filter((item) => item.dimensionType === dimensionType);
const dates = previousDateKeys(date, 30);
const cellMap = new Map(visible.map((item) => [`${item.signatureId}:${item.channelId ?? ''}:${item.carrier}:${item.activityDate.slice(0, 10)}`, item]));
const rows = dimensions
.filter((item) => item.dimensionType === dimensionType)
.filter((item) => !deferredKeyword || [item.tenantName, item.applicationName, item.signatureName]
@@ -304,11 +305,12 @@ function RetirementHeatmap({ date, dimensionType, dimensions, items, title }: {
applicationName: item.applicationName,
carrier: item.carrier,
approvedAt: item.approvedAt.slice(0, 10),
}));
total: dates.reduce((sum, dateKey) => sum + (cellMap.get(`${item.signatureId}:${item.channelId ?? ''}:${item.carrier}:${dateKey}`)?.acceptedBusinessCount ?? 0), 0),
}))
.sort((left, right) => right.total - left.total || left.signatureName.localeCompare(right.signatureName, 'zh-CN'));
const totalPages = Math.max(1, Math.ceil(rows.length / pageSize));
const currentPage = Math.min(page, totalPages);
const pagedRows = rows.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const cellMap = new Map(visible.map((item) => [`${item.signatureId}:${item.channelId ?? ''}:${item.carrier}:${item.detectionDate.slice(0, 10)}`, item]));
useEffect(() => {
setPage(1);
@@ -336,7 +338,7 @@ function RetirementHeatmap({ date, dimensionType, dimensions, items, title }: {
<div className="signature-retirement-heatmap__scroll">
<table>
<thead>
<tr><th></th>{dates.map((dateKey) => <th key={dateKey}>{dateKey.slice(5)}</th>)}</tr>
<tr><th></th><th>30</th>{dates.map((dateKey) => <th key={dateKey}>{dateKey.slice(5)}</th>)}</tr>
</thead>
<tbody>
{pagedRows.map((row) => (
@@ -348,12 +350,13 @@ function RetirementHeatmap({ date, dimensionType, dimensions, items, title }: {
</span>
<Tag tone="neutral">{carrierLabels[row.carrier] ?? row.carrier}</Tag>
</th>
<td className="signature-retirement-heatmap__total">{row.total.toLocaleString('zh-CN')}</td>
{dates.map((dateKey) => {
const item = cellMap.get(`${row.key}:${dateKey}`);
const beforeApproval = dateKey < row.approvedAt;
const successRate = item?.acceptedBusinessCount ? item.deliveredBusinessCount / item.acceptedBusinessCount * 100 : 0;
const className = beforeApproval ? 'is-inapplicable' : !item ? '' : item.acceptedBusinessCount === 0 ? 'is-zero' : `is-rate-${successRateTone(successRate)}`;
const titleText = beforeApproval ? '报备前,不适用' : item ? `提交条数:${item.submittedAttempts}\n上游接受条数:${item.acceptedBusinessCount}\n发送成功条数:${item.deliveredBusinessCount}\n发送成功率:${successRate.toFixed(1)}%\n预警阈值:${item.threshold}` : '当日无检测快照';
const titleText = beforeApproval ? '报备前,不适用' : item ? `提交条数:${item.submittedAttempts}\n上游接受条数:${item.acceptedBusinessCount}\n发送成功条数:${item.deliveredBusinessCount}\n发送成功率:${successRate.toFixed(1)}%\n检测状态:${item.status === 'observing' ? '观察中(不预警)' : item.status === 'alert' ? '预警' : '正常'}\n预警阈值:${item.threshold}` : '当日无检测快照';
return <td className={className} key={dateKey} title={titleText}>{beforeApproval ? 'N/A' : item ? item.acceptedBusinessCount : '—'}</td>;
})}
</tr>
+3 -3
View File
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ArrowLeft, FileText, MessageSquare, Server } from 'lucide-react';
import { adminApi, type ClientSmsSignature, type ClientSmsTemplate, type EnterpriseApplication, type TenantAccount, type TenantOption } from '@/api/adminApi';
import { Breadcrumb, Button, Table, Tag, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, MoneyText, Table, Tag, type TableColumn } from '@/components/ui';
import { formatCents } from '@/utils/currency';
export function AdminCustomerDetailPage() {
@@ -68,8 +68,8 @@ export function AdminCustomerDetailPage() {
<div className="dashboard-grid enterprise-summary-grid">
<div className="surface mini-status-card"><Server size={22} /><div><span></span><strong>{tenant?.code ?? '-'}</strong><small>{tenant?.status ?? '-'}</small></div></div>
<div className="surface mini-status-card"><MessageSquare size={22} /><div><span></span><strong></strong><small></small></div></div>
<div className="surface mini-status-card"><FileText size={22} /><div><span></span><strong>¥{formatCents(account?.balanceCents)}</strong><small></small></div></div>
<div className="surface mini-status-card"><FileText size={22} /><div><span></span><strong>¥{formatCents(account?.creditCents)}</strong><small> 0</small></div></div>
<div className="surface mini-status-card"><FileText size={22} /><div><span></span><strong><MoneyText>¥{formatCents(account?.balanceCents)}</MoneyText></strong><small></small></div></div>
<div className="surface mini-status-card"><FileText size={22} /><div><span></span><strong><MoneyText>¥{formatCents(account?.creditCents)}</MoneyText></strong><small> 0</small></div></div>
</div>
<div className="surface section-stack">
+6 -6
View File
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Building2, DollarSign, Plus, Trash2, TrendingDown, TrendingUp } from 'lucide-react';
import { adminApi, type TenantManagementRow } from '@/api/adminApi';
import { Breadcrumb, Button, Input, ManualRechargeDialog, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, Input, ManualRechargeDialog, Modal, MoneyText, Select, Table, Tag, type TableColumn } from '@/components/ui';
import { formatCents } from '@/utils/currency';
type AdminCustomersPageProps = {
@@ -85,15 +85,15 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
const balance = record.account?.balanceCents ?? 0;
return (
<span className={balance < 0 ? 'status-danger' : ''}>
¥{formatCents(balance)}
<MoneyText>¥{formatCents(balance)}</MoneyText>
{balance < 0 ? <Tag tone="danger" className="enterprise-inline-tag"></Tag> : null}
</span>
);
},
},
{ key: 'creditLimit', title: '授信额度', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.account?.creditCents ?? 0)}` },
{ key: 'todaySpend', title: '今日消费', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.todaySpendCents)}` },
{ key: 'todayRefund', title: '今日返还', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.todayRefundCents)}` },
{ key: 'creditLimit', title: '授信额度', width: '150px', align: 'right', render: (record) => <MoneyText>¥{formatCents(record.account?.creditCents ?? 0)}</MoneyText> },
{ key: 'todaySpend', title: '今日消费', width: '150px', align: 'right', render: (record) => <MoneyText>¥{formatCents(record.todaySpendCents)}</MoneyText> },
{ key: 'todayRefund', title: '今日返还', width: '150px', align: 'right', render: (record) => <MoneyText>¥{formatCents(record.todayRefundCents)}</MoneyText> },
{ key: 'status', title: '企业状态', width: '130px', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'warning'}>{record.status === 'active' ? '正常' : '已禁用'}</Tag> },
{
key: 'actions',
@@ -160,7 +160,7 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
<div className="surface mini-status-card"><Building2 size={22} /><div><span></span><strong>{records.length}</strong><small></small></div></div>
<div className="surface mini-status-card"><TrendingUp size={22} /><div><span></span><strong>{activeCount}</strong><small></small></div></div>
<div className="surface mini-status-card"><TrendingDown size={22} /><div><span></span><strong>{disabledCount}</strong><small></small></div></div>
<div className="surface mini-status-card"><DollarSign size={22} /><div><span></span><strong>¥{formatCents(totalBalance)}</strong><small></small></div></div>
<div className="surface mini-status-card"><DollarSign size={22} /><div><span></span><strong><MoneyText>¥{formatCents(totalBalance)}</MoneyText></strong><small></small></div></div>
</div>
<div className="surface ui-query-panel">
+2 -2
View File
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { Check, FileSearch, Search, X } from 'lucide-react';
import { adminApi, type EnterpriseCertification } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Select, Table, Tag, type DateRangeValue, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, MoneyText, Select, Table, Tag, type DateRangeValue, type TableColumn } from '@/components/ui';
type EnterpriseAuditStatus = 'pending' | 'approved' | 'rejected';
@@ -194,7 +194,7 @@ export function AdminEnterpriseAuditPage() {
<div><span></span><strong>{detailRecord.bankAccountName}</strong></div>
<div><span></span><strong>{detailRecord.bankName}</strong></div>
<div><span></span><strong>{detailRecord.bankAccountNo}</strong></div>
<div><span></span><strong>{detailRecord.verificationAmount}</strong></div>
<div><span></span><strong><MoneyText>{detailRecord.verificationAmount}</MoneyText></strong></div>
</section>
<section>
<h3></h3>
+4 -3
View File
@@ -11,6 +11,7 @@ import {
Button,
Chart,
Modal,
MoneyText,
Pagination,
Table,
Tag,
@@ -130,7 +131,7 @@ export function AdminHome() {
</div>
),
},
{ key: 'todaySpend', title: '今日消费(元)', align: 'right', render: (record) => `¥${formatCurrency(record.todaySpend)}` },
{ key: 'todaySpend', title: '今日消费(元)', align: 'right', render: (record) => <MoneyText>¥{formatCurrency(record.todaySpend)}</MoneyText> },
{ key: 'availableBalance', title: '可用余额', align: 'right', render: (record) => formatCount(record.availableBalance) },
{ key: 'balanceStatus', title: '余额状态', render: (record) => <Tag tone={balanceTone[record.balanceStatus]}>{record.balanceStatus}</Tag> },
{
@@ -205,7 +206,7 @@ export function AdminHome() {
</div>
<div className="surface metric-card">
<span></span>
<strong>¥{formatCurrency(todaySpend)}</strong>
<strong><MoneyText>¥{formatCurrency(todaySpend)}</MoneyText></strong>
<small></small>
</div>
<div className="surface metric-card">
@@ -372,7 +373,7 @@ export function AdminHome() {
</div>
<div className="ui-detail-info-grid__item">
<span></span>
<strong>¥{formatCurrency(selectedEnterprise.todaySpend)}</strong>
<strong><MoneyText>¥{formatCurrency(selectedEnterprise.todaySpend)}</MoneyText></strong>
</div>
<div className="ui-detail-info-grid__item">
<span></span>
+9 -9
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Download, Search } from 'lucide-react';
import { adminApi, type AdminChannel, type DailyProfitReport, type EnterpriseApplication, type ProfitReportSummary, type TenantOption } from '@/api/adminApi';
import { Breadcrumb, Button, DateRangeInput, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui';
import { Breadcrumb, Button, DateRangeInput, MoneyText, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui';
import { formatCents } from '@/utils/currency';
import { formatDateTime } from '@/utils/dateTime';
@@ -78,19 +78,19 @@ export function AdminProfitReportsPage() {
<div className="admin-report-summary__heading"><strong></strong><span></span></div>
<div className="admin-report-summary__grid">{[
['提交合计', summary.submittedUnits.toLocaleString('zh-CN')], ['发送合计', summary.sentUnits.toLocaleString('zh-CN')], ['未知合计', summary.unknownUnits.toLocaleString('zh-CN')], ['成功合计', summary.successUnits.toLocaleString('zh-CN')], ['失败合计', summary.failedUnits.toLocaleString('zh-CN')],
['净消费合计', `¥${formatCents(summary.revenueCents)}`], ['返还合计', `¥${formatCents(summary.refundCents)}`], ['成本合计', `¥${formatCents(summary.costCents)}`], ['利润合计', `¥${formatCents(summary.profitCents)}`], ['综合利润率', `${(summary.profitRateBps / 100).toFixed(2)}%`],
].map(([label, value]) => <div key={label}><span>{label}</span><strong>{value}</strong></div>)}</div>
['收入合计', `¥${formatCents(summary.revenueCents)}`], ['成本合计', `¥${formatCents(summary.costCents)}`], ['利润合计', `¥${formatCents(summary.profitCents)}`], ['综合利润率', `${(summary.profitRateBps / 100).toFixed(2)}%`],
].map(([label, value]) => <div key={label}><span>{label}</span><strong>{label.includes('合计') && ['收入合计', '成本合计', '利润合计'].includes(label) ? <MoneyText>{value}</MoneyText> : value}</strong></div>)}</div>
</div>
<div className="surface">
<div className="ui-table-wrap">
<table className="ui-table">
<thead><tr><th></th><th>{dimensionType === 'application' ? '企业 / 企业应用' : '通道'}</th><th></th><th></th><th></th><th></th><th></th><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead>
<thead><tr><th></th><th>{dimensionType === 'application' ? '企业 / 企业应用' : '通道'}</th><th></th><th></th><th></th><th></th><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>
{error ? <tr><td className="ui-table__empty" colSpan={13}>{error}</td></tr>
: loading ? <tr><td className="ui-table__empty" colSpan={13}>...</td></tr>
: rows.length === 0 ? <tr><td className="ui-table__empty" colSpan={13}></td></tr>
: rows.map((row) => <tr key={row.id}><td>{row.reportDate.slice(0, 10)}</td><td><strong>{row.dimensionName}</strong>{row.tenantName ? <div className="muted">{row.tenantName}</div> : null}</td><td>{row.submittedUnits.toLocaleString('zh-CN')}</td><td>{row.sentUnits.toLocaleString('zh-CN')}</td><td>{row.unknownUnits.toLocaleString('zh-CN')}</td><td>{row.successUnits.toLocaleString('zh-CN')}</td><td>{row.failedUnits.toLocaleString('zh-CN')}</td><td>¥{formatCents(row.revenueCents)}</td><td>¥{formatCents(row.refundCents)}</td><td>¥{formatCents(row.costCents)}</td><td style={{ color: row.profitCents < 0 ? 'var(--danger)' : undefined }}>¥{formatCents(row.profitCents)}</td><td>{(row.profitRateBps / 100).toFixed(2)}%</td><td>{formatDateTime(row.generatedAt)}</td></tr>)}
{error ? <tr><td className="ui-table__empty" colSpan={12}>{error}</td></tr>
: loading ? <tr><td className="ui-table__empty" colSpan={12}>...</td></tr>
: rows.length === 0 ? <tr><td className="ui-table__empty" colSpan={12}></td></tr>
: rows.map((row) => <tr key={row.id}><td>{row.reportDate.slice(0, 10)}</td><td><strong>{row.dimensionName}</strong>{row.tenantName ? <div className="muted">{row.tenantName}</div> : null}</td><td>{row.submittedUnits.toLocaleString('zh-CN')}</td><td>{row.sentUnits.toLocaleString('zh-CN')}</td><td>{row.unknownUnits.toLocaleString('zh-CN')}</td><td>{row.successUnits.toLocaleString('zh-CN')}</td><td>{row.failedUnits.toLocaleString('zh-CN')}</td><td><MoneyText>¥{formatCents(row.revenueCents)}</MoneyText></td><td><MoneyText>¥{formatCents(row.costCents)}</MoneyText></td><td style={{ color: row.profitCents < 0 ? 'var(--danger)' : undefined }}><MoneyText>¥{formatCents(row.profitCents)}</MoneyText></td><td>{(row.profitRateBps / 100).toFixed(2)}%</td><td>{formatDateTime(row.generatedAt)}</td></tr>)}
</tbody>
</table>
</div>
@@ -100,7 +100,7 @@ export function AdminProfitReportsPage() {
);
}
const emptyProfitSummary: ProfitReportSummary = { submittedUnits: 0, sentUnits: 0, unknownUnits: 0, successUnits: 0, failedUnits: 0, revenueCents: 0, refundCents: 0, costCents: 0, profitCents: 0, profitRateBps: 0 };
const emptyProfitSummary: ProfitReportSummary = { submittedUnits: 0, sentUnits: 0, unknownUnits: 0, successUnits: 0, failedUnits: 0, revenueCents: 0, costCents: 0, profitCents: 0, profitRateBps: 0 };
function defaultDateRange(): DateRangeValue {
const end = new Date();
+3 -3
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { Plus, ReceiptText, Search } from 'lucide-react';
import { Breadcrumb, Button, DateRangeInput, Input, ManualRechargeDialog, Pagination, RechargeReceiptDialog, Tag, type DateRangeValue } from '@/components/ui';
import { Breadcrumb, Button, DateRangeInput, Input, ManualRechargeDialog, MoneyText, Pagination, RechargeReceiptDialog, Tag, type DateRangeValue } from '@/components/ui';
import { adminApi, type RechargeOrder, type TenantAccount, type TenantOption } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
import { formatCents } from '@/utils/currency';
@@ -123,8 +123,8 @@ export function AdminRechargeRecordsPage() {
<tr key={record.id}>
<td><strong>{tenantName}</strong></td>
<td>{formatDateTime(record.paidAt ?? record.createdAt)}</td>
<td>¥{formatCents(record.amountCents)}</td>
<td>{record.balanceAfterCents === null || record.balanceAfterCents === undefined ? '-' : `¥${formatCents(record.balanceAfterCents)}`}</td>
<td><MoneyText>¥{formatCents(record.amountCents)}</MoneyText></td>
<td>{record.balanceAfterCents === null || record.balanceAfterCents === undefined ? '-' : <MoneyText>¥{formatCents(record.balanceAfterCents)}</MoneyText>}</td>
<td><Tag tone="warning"></Tag></td>
<td><RemarkCell value={record.remark ?? undefined} /></td>
<td>
+2 -2
View File
@@ -1,5 +1,5 @@
import { Copy, Eye, FileText, Pencil, Power, Send } from 'lucide-react';
import { DeleteRiskAction, Pagination, Tag } from '@/components/ui';
import { DeleteRiskAction, MoneyText, Pagination, Tag } from '@/components/ui';
import { formatCents } from '@/utils/currency';
import { successRateClassName } from '@/utils/successRate';
import { carrierLabelMap, carrierToneMap, statusLabelMap, statusToneMap } from './channelModel';
@@ -58,7 +58,7 @@ export function ChannelTable({
</div>
<div className="sms-channel-carrier-price">
<div>{channel.carriers.map((carrier) => <Tag key={carrier} tone={carrierToneMap[carrier]}>{carrierLabelMap[carrier]}</Tag>)}</div>
<strong>{formatCents(channel.unitPrice)} </strong>
<strong><MoneyText>{formatCents(channel.unitPrice)} </MoneyText></strong>
</div>
<div className="sms-channel-status-cell">
<Tag tone={statusToneMap[channel.status]}>{statusLabelMap[channel.status]}</Tag>
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { Edit3, Settings2, Trash2 } from 'lucide-react';
import { Button, Pagination, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
import { Button, MoneyText, 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) => `${formatAmount(record.unitPrice)}` },
{ key: 'unitPrice', title: '单价', width: '130px', render: (record) => <MoneyText>{formatAmount(record.unitPrice)} </MoneyText> },
{
key: 'cmppStatus',
title: '客户连接状态',
@@ -60,8 +60,8 @@ export function EnterpriseSignaturesTable({
{expanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
</button>
<div><span></span><strong>{signature.name}</strong></div>
<div><span></span><strong>{signature.tenant?.name ?? signature.tenantId}</strong></div>
<div><span></span><strong>{signature.application?.name ?? '-'}</strong></div>
<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>
+2 -2
View File
@@ -1,7 +1,7 @@
import { Download } from 'lucide-react';
import type { ReactNode } from 'react';
import type { SmsMessageRecord } from '@/api/adminApi';
import { Button, Pagination } from '@/components/ui';
import { Button, MoneyText, Pagination } from '@/components/ui';
import { formatCents } from '@/utils/currency';
import {
getCarrierLabel,
@@ -85,7 +85,7 @@ export function SmsRecordList({
</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.billingUnits} / ¥{formatCents(record.amountCents)}</strong><small>{record.content.length} </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>
<footer><button className="admin-sms-record-detail-link" onClick={() => onOpenDetail(record)} type="button"></button></footer>
+2 -6
View File
@@ -3,7 +3,7 @@ import { ClipboardCopy, FileText } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { clientApi, type ApplicationCmppParams, type ClientSmsApplication } from '@/api/adminApi';
import { formatCents } from '@/utils/currency';
import { Button, Modal, Pagination, Tag } from '@/components/ui';
import { Button, Modal, MoneyText, Pagination, Tag } from '@/components/ui';
import { copyText } from '@/utils/clipboard';
type LinkStatus = 'connected' | 'degraded' | 'disconnected' | 'inactive';
@@ -35,10 +35,6 @@ function normalizeStatus(application: ClientSmsApplication): LinkStatus {
return application.cmppStatus ?? 'inactive';
}
function formatPrice(cents?: number | null) {
return `${formatCents(cents)}`;
}
function mapParams(params: ApplicationCmppParams): ParamRow[] {
return [
{ label: 'ID', value: params.applicationId },
@@ -157,7 +153,7 @@ export function ClientApplicationsPage() {
</div>
<div>
<dt></dt>
<dd>{formatPrice(application.customerUnitPrice)}</dd>
<dd><MoneyText>{formatCents(application.customerUnitPrice)} </MoneyText></dd>
</div>
<div>
<dt>CMPP连接状态</dt>
+4 -4
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { WalletCards } from 'lucide-react';
import { Pagination, Tag } from '@/components/ui';
import { MoneyText, Pagination, Tag } from '@/components/ui';
import { clientApi, type RechargeOrder } from '@/api/adminApi';
import { formatCents } from '@/utils/currency';
import { formatDateTime } from '@/utils/dateTime';
@@ -40,11 +40,11 @@ export function ClientBillingPage() {
<div className="dashboard-grid enterprise-summary-grid">
<div className="surface mini-status-card">
<WalletCards size={22} />
<div><span></span><strong>¥{formatCents(balanceCents)}</strong><small></small></div>
<div><span></span><strong><MoneyText>¥{formatCents(balanceCents)}</MoneyText></strong><small></small></div>
</div>
<div className="surface mini-status-card">
<WalletCards size={22} />
<div><span></span><strong>¥{formatCents(balanceCents + creditCents)}</strong><small> 0 </small></div>
<div><span></span><strong><MoneyText>¥{formatCents(balanceCents + creditCents)}</MoneyText></strong><small> 0 </small></div>
</div>
</div>
<div className="surface section-stack">
@@ -60,7 +60,7 @@ export function ClientBillingPage() {
<tr key={order.id}>
<td><span className="table-mono-id">{order.orderNo}</span></td>
<td>{formatDateTime(order.paidAt ?? order.createdAt)}</td>
<td>¥{formatCents(order.amountCents)}</td>
<td><MoneyText>¥{formatCents(order.amountCents)}</MoneyText></td>
<td><Tag tone={order.status === 'paid' ? 'success' : 'warning'}>{order.status === 'paid' ? '已入账' : order.status}</Tag></td>
<td>{order.remark || '-'}</td>
</tr>
+6 -6
View File
@@ -10,7 +10,7 @@ import {
WalletCards,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Button, Chart, Table, Tag, type TableColumn } from '@/components/ui';
import { Button, Chart, MoneyText, Table, Tag, type TableColumn } from '@/components/ui';
import { clientApi, type DashboardResponse } from '@/api/adminApi';
import { createLineOption, createPieOption } from '@/theme/chartOptions';
import { formatDateTime } from '@/utils/dateTime';
@@ -101,8 +101,8 @@ export function ClientHome() {
<div className="dashboard-grid dashboard-grid--four">
<div className="surface metric-card metric-card--featured">
<span></span>
<strong>¥{formatAmount(availableBalance)}</strong>
<small> ¥{formatCents(account?.balanceCents)}</small>
<strong><MoneyText>¥{formatAmount(availableBalance)}</MoneyText></strong>
<small> <MoneyText>¥{formatCents(account?.balanceCents)}</MoneyText></small>
</div>
<div className="surface metric-card">
<span></span>
@@ -111,12 +111,12 @@ export function ClientHome() {
</div>
<div className="surface metric-card">
<span></span>
<strong>¥{formatCents(dashboard?.today.spendCents)}</strong>
<strong><MoneyText>¥{formatCents(dashboard?.today.spendCents)}</MoneyText></strong>
<small></small>
</div>
<div className="surface metric-card">
<span></span>
<strong>¥{formatCents(todayRefundCents)}</strong>
<strong><MoneyText>¥{formatCents(todayRefundCents)}</MoneyText></strong>
<small>退</small>
</div>
</div>
@@ -175,7 +175,7 @@ export function ClientHome() {
</div>
<div>
<span></span>
<strong>{latestRecharge ? `¥${formatCents(latestRecharge.amountCents)}` : '暂无充值'}</strong>
<strong>{latestRecharge ? <MoneyText>¥{formatCents(latestRecharge.amountCents)}</MoneyText> : '暂无充值'}</strong>
</div>
</div>
<div>
+2 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Check, Download, FileText, Plus, Search, Send, Trash2 } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Button, DateTimeInput, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
import { Button, DateTimeInput, Input, Modal, MoneyText, Select, Tag, Textarea } from '@/components/ui';
import { clientApi, type ClientSmsApplication, type ClientSmsSignatureView, type ClientSmsTemplate, type ImportPreviewResponse, type SmsBatchTask } from '@/api/adminApi';
import { formatCents } from '@/utils/currency';
import { replaceLeadingSmsSignature } from '@/utils/smsSignature';
@@ -419,7 +419,7 @@ export function ClientSendPage() {
</div>
<div>
<span></span>
<strong>¥{formatCents(selectedApplication?.customerUnitPrice)} / </strong>
<strong><MoneyText>¥{formatCents(selectedApplication?.customerUnitPrice)} / </MoneyText></strong>
</div>
</div>
<div className="preview-note"> 70 / 67 /</div>