fix: standardize currency and input selection styles
This commit is contained in:
@@ -318,6 +318,7 @@
|
|||||||
6. 最终失败、超时失败需要退费。
|
6. 最终失败、超时失败需要退费。
|
||||||
7. 三网通道成本只用于平台内部成本核算,不影响客户扣费金额。
|
7. 三网通道成本只用于平台内部成本核算,不影响客户扣费金额。
|
||||||
8. 当前版本计费口径固定为提交 accepted 扣费、最终 failed receipt/timeout 退款。
|
8. 当前版本计费口径固定为提交 accepted 扣费、最终 failed receipt/timeout 退款。
|
||||||
|
9. 所有面向用户展示的金额、余额、充值金额、套餐价格和单价统一以人民币元展示并固定保留三位小数;内部仍使用分或最小计费单位持久化,不以展示精度改变账务计算。
|
||||||
|
|
||||||
## 5. 功能需求
|
## 5. 功能需求
|
||||||
|
|
||||||
|
|||||||
@@ -2275,6 +2275,7 @@
|
|||||||
- 生成 account_transaction,类型为 recharge,关联 recharge_order。
|
- 生成 account_transaction,类型为 recharge,关联 recharge_order。
|
||||||
- 客户端余额、账单流水即时可见。
|
- 客户端余额、账单流水即时可见。
|
||||||
- 运营端充值记录、账单流水、系统日志三处可追溯。
|
- 运营端充值记录、账单流水、系统日志三处可追溯。
|
||||||
|
- 所有金额、余额、套餐价格和单价固定展示三位小数,例如 `¥1.000`。
|
||||||
|
|
||||||
### TC-BILLING-007 人工充值金额和短信条数只填其一
|
### TC-BILLING-007 人工充值金额和短信条数只填其一
|
||||||
|
|
||||||
|
|||||||
@@ -1540,5 +1540,6 @@ git diff --check
|
|||||||
|
|
||||||
- 逐图复查下载的《短信平台第一版瑕疵》后,确认中文图片文件名乱码仍真实存在:生产 `FileObject.fileName` 中可见 UTF-8 被按 Latin-1 解释后的值。上传链路现先恢复 multipart 文件名编码;历史记录由前端展示层兼容解码,避免签名材料和企业认证页继续显示乱码。
|
- 逐图复查下载的《短信平台第一版瑕疵》后,确认中文图片文件名乱码仍真实存在:生产 `FileObject.fileName` 中可见 UTF-8 被按 Latin-1 解释后的值。上传链路现先恢复 multipart 文件名编码;历史记录由前端展示层兼容解码,避免签名材料和企业认证页继续显示乱码。
|
||||||
- 通用输入框、文本框去除内部填充色;同时覆盖 Chromium 自动填充产生的蓝色内层背景。企业认证页此前额外写死的灰色输入背景已移除。
|
- 通用输入框、文本框去除内部填充色;同时覆盖 Chromium 自动填充产生的蓝色内层背景。企业认证页此前额外写死的灰色输入背景已移除。
|
||||||
|
- 2026-07-11 回归发现此前金额展示验收不充分:充值记录和多处金额页面仍混用整数、两位或四位小数。现统一金额展示为人民币元三位小数,并新增输入框聚焦底色与文本选中高亮;需求和 `TC-BILLING-006` 已同步。
|
||||||
- 已执行 FilesService 定向单测(3 项通过)、API build、前端 build 和 `git diff --check`;生产部署后四个服务均为 active,API/Gateway health 正常。通过真实 `POST /api/admin/files/upload` 上传 `营业执照-编码回归.png`,响应和 `FileObject` 持久化文件名均为正常中文。
|
- 已执行 FilesService 定向单测(3 项通过)、API build、前端 build 和 `git diff --check`;生产部署后四个服务均为 active,API/Gateway health 正常。通过真实 `POST /api/admin/files/upload` 上传 `营业执照-编码回归.png`,响应和 `FileObject` 持久化文件名均为正常中文。
|
||||||
- Batch 6 的 6.1、6.2、7.1、8.1 仍为待修,不得因之前的前端构建通过而标记完成;其余 8.x 与客户端菜单顺序将继续按原始文档逐项复核。
|
- Batch 6 的 6.1、6.2、7.1、8.1 仍为待修,不得因之前的前端构建通过而标记完成;其余 8.x 与客户端菜单顺序将继续按原始文档逐项复核。
|
||||||
|
|||||||
@@ -627,7 +627,7 @@ export function AdminChannelsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="sms-channel-carrier-price">
|
<div className="sms-channel-carrier-price">
|
||||||
<Tag tone={carrierToneMap[channel.carrier]}>{carrierLabelMap[channel.carrier]}</Tag>
|
<Tag tone={carrierToneMap[channel.carrier]}>{carrierLabelMap[channel.carrier]}</Tag>
|
||||||
<strong>{channel.unitPrice.toFixed(1)} 分</strong>
|
<strong>{channel.unitPrice.toFixed(3)} 分</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="sms-channel-status-cell">
|
<div className="sms-channel-status-cell">
|
||||||
<Tag tone={statusToneMap[channel.status]}>{statusLabelMap[channel.status]}</Tag>
|
<Tag tone={statusToneMap[channel.status]}>{statusLabelMap[channel.status]}</Tag>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
import { ArrowLeft, FileText, MessageSquare, Server } from 'lucide-react';
|
import { ArrowLeft, FileText, MessageSquare, Server } from 'lucide-react';
|
||||||
import { adminApi, type ClientSmsSignature, type ClientSmsTemplate, type EnterpriseApplication, type TenantAccount, type TenantOption } from '@/api/adminApi';
|
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, Table, Tag, type TableColumn } from '@/components/ui';
|
||||||
|
import { formatCents } from '@/utils/currency';
|
||||||
|
|
||||||
export function AdminCustomerDetailPage() {
|
export function AdminCustomerDetailPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -67,7 +68,7 @@ export function AdminCustomerDetailPage() {
|
|||||||
<div className="dashboard-grid enterprise-summary-grid">
|
<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"><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>{(account?.smsUnits ?? 0).toLocaleString('zh-CN')}</strong><small>真实账户余量</small></div></div>
|
<div className="surface mini-status-card"><MessageSquare size={22} /><div><span>短信余量</span><strong>{(account?.smsUnits ?? 0).toLocaleString('zh-CN')}</strong><small>真实账户余量</small></div></div>
|
||||||
<div className="surface mini-status-card"><FileText size={22} /><div><span>现金余额</span><strong>¥{((account?.balanceCents ?? 0) / 100).toLocaleString('zh-CN')}</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>
|
</div>
|
||||||
|
|
||||||
<div className="surface section-stack">
|
<div className="surface section-stack">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { Building2, DollarSign, Plus, Trash2, TrendingDown, TrendingUp } from 'lucide-react';
|
import { Building2, DollarSign, Plus, Trash2, TrendingDown, TrendingUp } from 'lucide-react';
|
||||||
import { adminApi, type TenantManagementRow } from '@/api/adminApi';
|
import { adminApi, type TenantManagementRow } from '@/api/adminApi';
|
||||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, Textarea, type TableColumn } from '@/components/ui';
|
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, Textarea, type TableColumn } from '@/components/ui';
|
||||||
|
import { formatCents } from '@/utils/currency';
|
||||||
|
|
||||||
type AdminCustomersPageProps = {
|
type AdminCustomersPageProps = {
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
@@ -16,10 +17,6 @@ type RechargeForm = {
|
|||||||
remark: string;
|
remark: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatCurrency(cents: number) {
|
|
||||||
return (cents / 100).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConfirmModal({ message, onCancel, onConfirm }: { message: string; onCancel: () => void; onConfirm: () => void }) {
|
function ConfirmModal({ message, onCancel, onConfirm }: { message: string; onCancel: () => void; onConfirm: () => void }) {
|
||||||
return (
|
return (
|
||||||
<Modal footer={<><Button onClick={onCancel} variant="ghost">取消</Button><Button onClick={onConfirm}>确认</Button></>} onClose={onCancel} open title="操作确认">
|
<Modal footer={<><Button onClick={onCancel} variant="ghost">取消</Button><Button onClick={onConfirm}>确认</Button></>} onClose={onCancel} open title="操作确认">
|
||||||
@@ -86,14 +83,14 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
|||||||
const balance = record.account?.balanceCents ?? 0;
|
const balance = record.account?.balanceCents ?? 0;
|
||||||
return (
|
return (
|
||||||
<span className={balance < 0 ? 'status-danger' : ''}>
|
<span className={balance < 0 ? 'status-danger' : ''}>
|
||||||
¥{formatCurrency(balance)}
|
¥{formatCents(balance)}
|
||||||
{balance < 0 ? <Tag tone="danger" className="enterprise-inline-tag">欠费</Tag> : null}
|
{balance < 0 ? <Tag tone="danger" className="enterprise-inline-tag">欠费</Tag> : null}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ key: 'overdraftLimit', title: '透支限额', width: '150px', align: 'right', render: (record) => `¥${formatCurrency(record.account?.creditCents ?? 0)}` },
|
{ key: 'overdraftLimit', title: '透支限额', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.account?.creditCents ?? 0)}` },
|
||||||
{ key: 'todaySpend', title: '今日消费', width: '150px', align: 'right', render: (record) => `¥${formatCurrency(record.todaySpendCents)}` },
|
{ key: 'todaySpend', title: '今日消费', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.todaySpendCents)}` },
|
||||||
{ key: 'status', title: '企业状态', width: '130px', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'warning'}>{record.status === 'active' ? '正常' : '已禁用'}</Tag> },
|
{ key: 'status', title: '企业状态', width: '130px', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'warning'}>{record.status === 'active' ? '正常' : '已禁用'}</Tag> },
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
@@ -172,7 +169,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"><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"><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"><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>¥{formatCurrency(totalBalance)}</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>
|
</div>
|
||||||
|
|
||||||
<div className="surface ui-query-panel">
|
<div className="surface ui-query-panel">
|
||||||
@@ -215,7 +212,7 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
|||||||
>
|
>
|
||||||
<div className="admin-system-modal-form">
|
<div className="admin-system-modal-form">
|
||||||
<Input disabled label="企业名称" value={rechargeTarget.name} />
|
<Input disabled label="企业名称" value={rechargeTarget.name} />
|
||||||
<Input disabled label="当前余额" prefix="¥" value={formatCurrency(rechargeTarget.account?.balanceCents ?? 0)} />
|
<Input disabled label="当前余额" prefix="¥" value={formatCents(rechargeTarget.account?.balanceCents ?? 0)} />
|
||||||
<Input label="充值金额" onChange={(event) => updateRechargeForm('amount', event.target.value)} prefix="¥" required type="number" value={rechargeForm.amount} />
|
<Input label="充值金额" onChange={(event) => updateRechargeForm('amount', event.target.value)} prefix="¥" required type="number" value={rechargeForm.amount} />
|
||||||
<Input label="操作人" onChange={(event) => updateRechargeForm('operator', event.target.value)} value={rechargeForm.operator} />
|
<Input label="操作人" onChange={(event) => updateRechargeForm('operator', event.target.value)} value={rechargeForm.operator} />
|
||||||
<Textarea className="admin-system-modal-form__wide" label="充值备注" onChange={(event) => updateRechargeForm('remark', event.target.value)} rows={4} value={rechargeForm.remark} />
|
<Textarea className="admin-system-modal-form__wide" label="充值备注" onChange={(event) => updateRechargeForm('remark', event.target.value)} rows={4} value={rechargeForm.remark} />
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ const balanceTone = {
|
|||||||
|
|
||||||
function formatCurrency(value: number) {
|
function formatCurrency(value: number) {
|
||||||
return value.toLocaleString('zh-CN', {
|
return value.toLocaleString('zh-CN', {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 3,
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 3,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Plus, Search } from 'lucide-react';
|
|||||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Textarea, Tag, type DateRangeValue } from '@/components/ui';
|
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Textarea, Tag, type DateRangeValue } from '@/components/ui';
|
||||||
import { adminApi, type RechargeOrder, type TenantOption } from '@/api/adminApi';
|
import { adminApi, type RechargeOrder, type TenantOption } from '@/api/adminApi';
|
||||||
import { formatDateTime } from '@/utils/dateTime';
|
import { formatDateTime } from '@/utils/dateTime';
|
||||||
|
import { formatAmount } from '@/utils/currency';
|
||||||
|
|
||||||
type ManualRechargeForm = {
|
type ManualRechargeForm = {
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
@@ -16,17 +17,6 @@ function getDate(value: string) {
|
|||||||
return value.slice(0, 10);
|
return value.slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAmount(value?: number) {
|
|
||||||
if (value === undefined) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.toLocaleString('zh-CN', {
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
minimumFractionDigits: Number.isInteger(value) ? 0 : 2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function RemarkCell({ value }: { value?: string }) {
|
function RemarkCell({ value }: { value?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className={['admin-remark-cell', value ? '' : 'admin-remark-cell--empty'].filter(Boolean).join(' ')}>
|
<div className={['admin-remark-cell', value ? '' : 'admin-remark-cell--empty'].filter(Boolean).join(' ')}>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function AdminSmsApplicationFormPage() {
|
|||||||
setAppName(application.name);
|
setAppName(application.name);
|
||||||
setScene(application.scene ?? '');
|
setScene(application.scene ?? '');
|
||||||
setDailyLimit(application.dailyLimit ? String(application.dailyLimit) : '');
|
setDailyLimit(application.dailyLimit ? String(application.dailyLimit) : '');
|
||||||
setCustomerUnitPrice(((application.customerUnitPrice ?? 0) / 100).toFixed(4));
|
setCustomerUnitPrice(((application.customerUnitPrice ?? 0) / 100).toFixed(3));
|
||||||
setQueuePriority(application.queuePriority === 'priority' ? 'priority' : 'normal');
|
setQueuePriority(application.queuePriority === 'priority' ? 'priority' : 'normal');
|
||||||
setCmppAccount(application.cmppAccount ?? '');
|
setCmppAccount(application.cmppAccount ?? '');
|
||||||
setCmppEnterpriseCode(application.cmppEnterpriseCode ?? application.tenant?.code ?? '');
|
setCmppEnterpriseCode(application.cmppEnterpriseCode ?? application.tenant?.code ?? '');
|
||||||
@@ -185,7 +185,7 @@ export function AdminSmsApplicationFormPage() {
|
|||||||
<Input label="应用名称" onChange={(event) => setAppName(event.target.value)} placeholder="请输入应用名称" required value={appName} />
|
<Input label="应用名称" onChange={(event) => setAppName(event.target.value)} placeholder="请输入应用名称" required value={appName} />
|
||||||
<Input label="应用场景" onChange={(event) => setScene(event.target.value)} placeholder="行业通知/营销推广/验证码" value={scene} />
|
<Input label="应用场景" onChange={(event) => setScene(event.target.value)} placeholder="行业通知/营销推广/验证码" value={scene} />
|
||||||
<Input label="日发送数量限制" onChange={(event) => setDailyLimit(event.target.value)} placeholder="100000" required value={dailyLimit} />
|
<Input label="日发送数量限制" onChange={(event) => setDailyLimit(event.target.value)} placeholder="100000" required value={dailyLimit} />
|
||||||
<Input label="客户单价(元/条)" onChange={(event) => setCustomerUnitPrice(event.target.value)} placeholder="0.0300" required value={customerUnitPrice} />
|
<Input label="客户单价(元/条)" onChange={(event) => setCustomerUnitPrice(event.target.value)} placeholder="0.030" required value={customerUnitPrice} />
|
||||||
<div className="admin-app-form-row admin-app-form-row--wide">
|
<div className="admin-app-form-row admin-app-form-row--wide">
|
||||||
<span>发送队列</span>
|
<span>发送队列</span>
|
||||||
<div className="radio-row">
|
<div className="radio-row">
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ function downloadCsv(records: SmsMessageRecord[]) {
|
|||||||
record.channel?.sendRegion ?? '',
|
record.channel?.sendRegion ?? '',
|
||||||
getCarrierLabel(record.channel?.carrier),
|
getCarrierLabel(record.channel?.carrier),
|
||||||
record.billingUnits,
|
record.billingUnits,
|
||||||
(record.amountCents / 100).toFixed(2),
|
(record.amountCents / 100).toFixed(3),
|
||||||
record.channel?.name ?? record.channelId ?? '',
|
record.channel?.name ?? record.channelId ?? '',
|
||||||
getStatusLabel(record.status),
|
getStatusLabel(record.status),
|
||||||
getTime(record.deliveredAt),
|
getTime(record.deliveredAt),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { ClipboardCopy, FileText } from 'lucide-react';
|
import { ClipboardCopy, FileText } from 'lucide-react';
|
||||||
import { clientApi, type ApplicationCmppParams, type ClientSmsApplication } from '@/api/adminApi';
|
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, Pagination, Tag } from '@/components/ui';
|
||||||
|
|
||||||
type LinkStatus = 'connected' | 'degraded' | 'disconnected' | 'inactive';
|
type LinkStatus = 'connected' | 'degraded' | 'disconnected' | 'inactive';
|
||||||
@@ -33,7 +34,7 @@ function normalizeStatus(application: ClientSmsApplication): LinkStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatPrice(cents?: number | null) {
|
function formatPrice(cents?: number | null) {
|
||||||
return `${((cents ?? 0) / 100).toFixed(4)} 元`;
|
return `${formatCents(cents)} 元`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapParams(params: ApplicationCmppParams): ParamRow[] {
|
function mapParams(params: ApplicationCmppParams): ParamRow[] {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { CreditCard } from 'lucide-react';
|
import { CreditCard } from 'lucide-react';
|
||||||
import { Button, Pagination, Tag } from '@/components/ui';
|
import { Button, Pagination, Tag } from '@/components/ui';
|
||||||
import { clientApi, type BillingPlan } from '@/api/adminApi';
|
import { clientApi, type BillingPlan } from '@/api/adminApi';
|
||||||
|
import { formatCents } from '@/utils/currency';
|
||||||
|
|
||||||
export function ClientBillingPage() {
|
export function ClientBillingPage() {
|
||||||
const [plans, setPlans] = useState<BillingPlan[]>([]);
|
const [plans, setPlans] = useState<BillingPlan[]>([]);
|
||||||
@@ -54,7 +55,7 @@ export function ClientBillingPage() {
|
|||||||
<strong>{plan.smsUnits.toLocaleString('zh-CN')} 条</strong>
|
<strong>{plan.smsUnits.toLocaleString('zh-CN')} 条</strong>
|
||||||
<p className="muted">{plan.description ?? '适合阶段性短信发送和活动通知。'}</p>
|
<p className="muted">{plan.description ?? '适合阶段性短信发送和活动通知。'}</p>
|
||||||
<Button icon={<CreditCard size={16} />} onClick={() => createOrder(plan)} variant={plan.smsUnits >= 100000 ? 'primary' : 'ghost'}>
|
<Button icon={<CreditCard size={16} />} onClick={() => createOrder(plan)} variant={plan.smsUnits >= 100000 ? 'primary' : 'ghost'}>
|
||||||
¥{(plan.amountCents / 100).toLocaleString('zh-CN')} 立即充值
|
¥{formatCents(plan.amountCents)} 立即充值
|
||||||
</Button>
|
</Button>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Button, Chart, Table, Tag, type TableColumn } from '@/components/ui';
|
|||||||
import { clientApi, type DashboardResponse } from '@/api/adminApi';
|
import { clientApi, type DashboardResponse } from '@/api/adminApi';
|
||||||
import { createLineOption, createPieOption } from '@/theme/chartOptions';
|
import { createLineOption, createPieOption } from '@/theme/chartOptions';
|
||||||
import { formatDateTime } from '@/utils/dateTime';
|
import { formatDateTime } from '@/utils/dateTime';
|
||||||
|
import { formatAmount, formatCents } from '@/utils/currency';
|
||||||
|
|
||||||
type RecentTaskRow = {
|
type RecentTaskRow = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -102,8 +103,8 @@ export function ClientHome() {
|
|||||||
<div className="dashboard-grid">
|
<div className="dashboard-grid">
|
||||||
<div className="surface metric-card metric-card--featured">
|
<div className="surface metric-card metric-card--featured">
|
||||||
<span>账户剩余余额</span>
|
<span>账户剩余余额</span>
|
||||||
<strong>¥{availableBalance.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</strong>
|
<strong>¥{formatAmount(availableBalance)}</strong>
|
||||||
<small>今日消费 ¥{todaySpend.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</small>
|
<small>今日消费 ¥{formatAmount(todaySpend)}</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="surface metric-card">
|
<div className="surface metric-card">
|
||||||
<span>今日发送</span>
|
<span>今日发送</span>
|
||||||
@@ -112,7 +113,7 @@ export function ClientHome() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="surface metric-card">
|
<div className="surface metric-card">
|
||||||
<span>今日返还金额</span>
|
<span>今日返还金额</span>
|
||||||
<strong>¥{todayRefund.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}</strong>
|
<strong>¥{formatAmount(todayRefund)}</strong>
|
||||||
<small>异常回执与退费返还</small>
|
<small>异常回执与退费返还</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +170,7 @@ export function ClientHome() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>最近充值</span>
|
<span>最近充值</span>
|
||||||
<strong>{latestRecharge ? `¥${(latestRecharge.amountCents / 100).toFixed(2)}` : '暂无充值'}</strong>
|
<strong>{latestRecharge ? `¥${formatCents(latestRecharge.amountCents)}` : '暂无充值'}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -202,9 +202,20 @@
|
|||||||
.ui-select:focus-within,
|
.ui-select:focus-within,
|
||||||
.ui-select--open {
|
.ui-select--open {
|
||||||
border-color: var(--color-selected);
|
border-color: var(--color-selected);
|
||||||
|
background: color-mix(in srgb, var(--color-selected) 4%, transparent);
|
||||||
box-shadow: var(--focus-ring);
|
box-shadow: var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-input input::selection,
|
||||||
|
.ui-textarea::selection {
|
||||||
|
background: var(--color-selected);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-textarea:focus {
|
||||||
|
background: color-mix(in srgb, var(--color-selected) 4%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.ui-input--error,
|
.ui-input--error,
|
||||||
.ui-select--error {
|
.ui-select--error {
|
||||||
border-color: var(--color-danger);
|
border-color: var(--color-danger);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export function formatAmount(value: number) {
|
||||||
|
return value.toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 3,
|
||||||
|
maximumFractionDigits: 3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCents(cents?: number | null) {
|
||||||
|
return formatAmount((cents ?? 0) / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatYuan(cents?: number | null) {
|
||||||
|
return `¥${formatCents(cents)}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user