feat: 优化签名热力图与金额展示
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user