实现多商户履约平台基础

This commit is contained in:
yml2213
2026-07-30 12:02:32 +08:00
parent dde6f2c269
commit 83429456a6
41 changed files with 5487 additions and 504 deletions
+731
View File
@@ -0,0 +1,731 @@
import { useCallback, useEffect, useState } from 'react'
import {
Button,
Descriptions,
Form,
Input,
InputNumber,
Modal,
Select,
Space,
Table,
Tabs,
Tag,
Typography,
message,
} from 'antd'
import {
ApiOutlined,
CopyOutlined,
PlusOutlined,
ReloadOutlined,
WalletOutlined,
} from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
import dayjs from 'dayjs'
import { merchantApi } from '../api'
import type {
ApiClient,
ApiCredential,
CallbackCredential,
CallbackSubscription,
FulfillmentOrder,
Merchant,
MerchantMember,
MerchantProduct,
PageResult,
WalletAccount,
WalletLedgerEntry,
} from '../types'
const fulfillmentStatusMap: Record<string, { color: string; text: string }> = {
pending: { color: 'orange', text: '待履约' },
processing: { color: 'cyan', text: '履约中' },
succeeded: { color: 'green', text: '已成功' },
failed: { color: 'red', text: '已失败' },
cancelled: { color: 'default', text: '已取消' },
}
const paymentStatusMap: Record<string, { color: string; text: string }> = {
pending: { color: 'orange', text: '待支付' },
paid: { color: 'blue', text: '已支付' },
refunded: { color: 'purple', text: '已退款' },
cancelled: { color: 'default', text: '已取消' },
}
const memberRoleOptions = [
{ value: 'owner', label: '负责人' },
{ value: 'operator', label: '运营' },
{ value: 'finance', label: '财务' },
{ value: 'viewer', label: '只读' },
]
const scopeOptions = [
{ value: 'products:read', label: '商品读取' },
{ value: 'orders:read', label: '订单读取' },
{ value: 'orders:write', label: '订单写入' },
{ value: 'fulfillment:read', label: '履约读取' },
{ value: 'fulfillment:write', label: '履约写入' },
{ value: 'wallet:read', label: '钱包读取' },
]
const eventOptions = [
{ value: 'order.created', label: '订单创建' },
{ value: 'order.fulfillment.updated', label: '履约更新' },
{ value: 'order.cancelled', label: '订单取消' },
]
export default function MerchantCenter() {
const [merchant, setMerchant] = useState<Merchant | null>(null)
const [merchantRole, setMerchantRole] = useState<MerchantMember['role']>()
const [activeTab, setActiveTab] = useState('products')
const [products, setProducts] = useState<PageResult<MerchantProduct>>({ list: [], total: 0, page: 1, size: 10 })
const [orders, setOrders] = useState<PageResult<FulfillmentOrder>>({ list: [], total: 0, page: 1, size: 10 })
const [ledger, setLedger] = useState<PageResult<WalletLedgerEntry>>({ list: [], total: 0, page: 1, size: 10 })
const [wallet, setWallet] = useState<WalletAccount | null>(null)
const [apiClients, setApiClients] = useState<ApiClient[]>([])
const [callbacks, setCallbacks] = useState<CallbackSubscription[]>([])
const [members, setMembers] = useState<MerchantMember[]>([])
const [loading, setLoading] = useState(false)
const [productOpen, setProductOpen] = useState(false)
const [editingProduct, setEditingProduct] = useState<MerchantProduct | null>(null)
const [walletOpen, setWalletOpen] = useState(false)
const [apiClientOpen, setApiClientOpen] = useState(false)
const [apiCredential, setApiCredential] = useState<ApiCredential | null>(null)
const [callbackOpen, setCallbackOpen] = useState(false)
const [callbackCredential, setCallbackCredential] = useState<CallbackCredential | null>(null)
const [memberOpen, setMemberOpen] = useState(false)
const [productForm] = Form.useForm()
const [walletForm] = Form.useForm()
const [apiClientForm] = Form.useForm()
const [callbackForm] = Form.useForm()
const [memberForm] = Form.useForm()
const canManage = merchantRole === 'owner' || merchantRole === 'operator'
const canFinance = merchantRole === 'owner' || merchantRole === 'finance'
const loadCurrent = useCallback(async () => {
const data = await merchantApi.current()
setMerchant(data.merchant)
setMerchantRole(data.role)
}, [])
const loadProducts = useCallback(async (page = products.page, size = products.size) => {
const data = await merchantApi.products({ page, size })
setProducts(data)
}, [products.page, products.size])
const loadOrders = useCallback(async (page = orders.page, size = orders.size) => {
const data = await merchantApi.orders({ page, size })
setOrders(data)
}, [orders.page, orders.size])
const loadWallet = useCallback(async (page = ledger.page, size = ledger.size) => {
const [walletData, ledgerData] = await Promise.all([
merchantApi.wallet(),
merchantApi.ledger({ page, size }),
])
setWallet(walletData)
setLedger(ledgerData)
}, [ledger.page, ledger.size])
const loadIntegrations = useCallback(async () => {
const [clientData, callbackData, memberData] = await Promise.all([
merchantApi.apiClients(),
merchantApi.callbacks(),
merchantApi.members(),
])
setApiClients(clientData || [])
setCallbacks(callbackData || [])
setMembers(memberData || [])
}, [])
const loadAll = useCallback(async () => {
setLoading(true)
try {
await loadCurrent()
await Promise.all([loadProducts(), loadOrders(), loadWallet(), loadIntegrations()])
} catch (e) {
message.error(e instanceof Error ? e.message : '加载失败')
} finally {
setLoading(false)
}
}, [loadCurrent, loadProducts, loadOrders, loadWallet, loadIntegrations])
useEffect(() => {
loadAll()
}, [loadAll])
const openProductCreate = () => {
setEditingProduct(null)
productForm.resetFields()
productForm.setFieldsValue({
currency: 'CNY',
stock: -1,
status: 'active',
price_yuan: 0,
cost_yuan: 0,
})
setProductOpen(true)
}
const openProductEdit = (record: MerchantProduct) => {
setEditingProduct(record)
productForm.setFieldsValue({
...record,
price_yuan: centsToYuan(record.price_amount),
cost_yuan: centsToYuan(record.cost_amount),
})
setProductOpen(true)
}
const submitProduct = async () => {
const values = await productForm.validateFields()
try {
const payload = {
...values,
price_amount: yuanToCents(values.price_yuan),
cost_amount: yuanToCents(values.cost_yuan),
}
delete payload.price_yuan
delete payload.cost_yuan
if (editingProduct) {
await merchantApi.updateProduct(editingProduct.id, payload)
message.success('商品已更新')
} else {
await merchantApi.createProduct(payload)
message.success('商品已创建')
}
setProductOpen(false)
loadProducts()
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败')
}
}
const submitWalletAdjust = async () => {
const values = await walletForm.validateFields()
try {
const walletData = await merchantApi.adjustWallet({
amount: yuanToCents(values.amount_yuan),
idempotency_key: values.idempotency_key,
note: values.note,
})
setWallet(walletData)
setWalletOpen(false)
message.success('钱包已调整')
loadWallet()
} catch (e) {
message.error(e instanceof Error ? e.message : '调整失败')
}
}
const submitAPIClient = async () => {
const values = await apiClientForm.validateFields()
try {
const credential = await merchantApi.createApiClient({
name: values.name,
scopes: (values.scopes || []).join(','),
signature_version: values.signature_version,
expires_at: values.expires_at,
})
setApiCredential(credential)
setApiClientOpen(false)
message.success('API 客户端已创建')
loadIntegrations()
} catch (e) {
message.error(e instanceof Error ? e.message : '创建失败')
}
}
const submitCallback = async () => {
const values = await callbackForm.validateFields()
try {
const credential = await merchantApi.createCallback({
name: values.name,
url: values.url,
events: (values.events || []).join(','),
})
setCallbackCredential(credential)
setCallbackOpen(false)
message.success('回调已创建')
loadIntegrations()
} catch (e) {
message.error(e instanceof Error ? e.message : '创建失败')
}
}
const submitMember = async () => {
const values = await memberForm.validateFields()
try {
await merchantApi.addMember({
user_id: values.user_id,
role: values.role,
is_default: values.is_default,
})
setMemberOpen(false)
message.success('成员已添加')
loadIntegrations()
} catch (e) {
message.error(e instanceof Error ? e.message : '添加失败')
}
}
const toggleAPIClient = async (record: ApiClient) => {
try {
await merchantApi.updateApiClientStatus(record.id, record.status === 'active' ? 'disabled' : 'active')
message.success('状态已更新')
loadIntegrations()
} catch (e) {
message.error(e instanceof Error ? e.message : '更新失败')
}
}
const toggleCallback = async (record: CallbackSubscription) => {
try {
await merchantApi.updateCallbackStatus(record.id, record.status === 'active' ? 'disabled' : 'active')
message.success('状态已更新')
loadIntegrations()
} catch (e) {
message.error(e instanceof Error ? e.message : '更新失败')
}
}
const productColumns: ColumnsType<MerchantProduct> = [
{ title: 'SKU', dataIndex: 'sku', width: 180, render: (v) => <Typography.Text code copyable>{v}</Typography.Text> },
{ title: '名称', dataIndex: 'display_name', ellipsis: true, render: (_, r) => r.display_name || r.product?.name || '-' },
{ title: '目录编码', dataIndex: ['product', 'code'], width: 160, ellipsis: true, render: (_, r) => r.product?.code || '-' },
{ title: '售价', dataIndex: 'price_amount', width: 110, render: money },
{ title: '成本', dataIndex: 'cost_amount', width: 110, render: money },
{ title: '库存', dataIndex: 'stock', width: 90, render: (v) => (v < 0 ? '无限' : v) },
{ title: '状态', dataIndex: 'status', width: 90, render: productStatusTag },
{
title: '操作',
key: 'action',
width: 90,
render: (_, record) => canManage ? (
<Button type="link" size="small" onClick={() => openProductEdit(record)}></Button>
) : '-',
},
]
const orderColumns: ColumnsType<FulfillmentOrder> = [
{ title: '平台订单号', dataIndex: 'order_no', width: 210, render: (v) => <Typography.Text copyable>{v}</Typography.Text> },
{ title: '商户单号', dataIndex: 'client_order_no', width: 160, ellipsis: true },
{ title: 'SKU', dataIndex: 'product_sku', width: 150, render: (v) => <Typography.Text code>{v}</Typography.Text> },
{ title: '商品', dataIndex: 'product_name', ellipsis: true },
{ title: '金额', dataIndex: 'amount', width: 100, render: money },
{ title: '支付', dataIndex: 'payment_status', width: 90, render: paymentStatusTag },
{ title: '履约', dataIndex: 'fulfillment_status', width: 100, render: fulfillmentStatusTag },
{ title: '上游单号', dataIndex: 'provider_order_no', width: 140, ellipsis: true, render: (v) => v || '-' },
{ title: '时间', dataIndex: 'created_at', width: 160, render: formatTime },
]
const ledgerColumns: ColumnsType<WalletLedgerEntry> = [
{ title: '流水号', dataIndex: 'entry_no', width: 210, ellipsis: true },
{ title: '类型', dataIndex: 'type', width: 90, render: ledgerTypeTag },
{ title: '金额', dataIndex: 'amount', width: 110, render: moneyWithSign },
{ title: '余额', dataIndex: 'balance_after', width: 110, render: money },
{ title: '关联单号', dataIndex: 'reference_no', ellipsis: true },
{ title: '备注', dataIndex: 'note', ellipsis: true, render: (v) => v || '-' },
{ title: '时间', dataIndex: 'created_at', width: 160, render: formatTime },
]
const apiClientColumns: ColumnsType<ApiClient> = [
{ title: '名称', dataIndex: 'name', width: 180, ellipsis: true },
{ title: 'App Key', dataIndex: 'app_key', width: 260, render: (v) => <Typography.Text code copyable>{v}</Typography.Text> },
{ title: '签名', dataIndex: 'signature_version', width: 110, render: (v) => <Tag>{v}</Tag> },
{ title: '权限', dataIndex: 'scopes', ellipsis: true },
{ title: '状态', dataIndex: 'status', width: 90, render: activeStatusTag },
{ title: '最后使用', dataIndex: 'last_used_at', width: 160, render: formatTime },
{
title: '操作',
key: 'action',
width: 90,
render: (_, record) => canManage ? (
<Button type="link" size="small" onClick={() => toggleAPIClient(record)}>
{record.status === 'active' ? '禁用' : '启用'}
</Button>
) : '-',
},
]
const callbackColumns: ColumnsType<CallbackSubscription> = [
{ title: '名称', dataIndex: 'name', width: 180, ellipsis: true },
{ title: 'URL', dataIndex: 'url', ellipsis: true },
{ title: '事件', dataIndex: 'events', width: 260, ellipsis: true },
{ title: '状态', dataIndex: 'status', width: 90, render: activeStatusTag },
{ title: '创建时间', dataIndex: 'created_at', width: 160, render: formatTime },
{
title: '操作',
key: 'action',
width: 90,
render: (_, record) => canManage ? (
<Button type="link" size="small" onClick={() => toggleCallback(record)}>
{record.status === 'active' ? '禁用' : '启用'}
</Button>
) : '-',
},
]
const memberColumns: ColumnsType<MerchantMember> = [
{ title: '用户', dataIndex: ['user', 'username'], render: (_, r) => r.user?.username || `#${r.user_id}` },
{ title: '昵称', dataIndex: ['user', 'nickname'], render: (_, r) => r.user?.nickname || '-' },
{ title: '角色', dataIndex: 'role', width: 120, render: memberRoleTag },
{ title: '默认', dataIndex: 'is_default', width: 80, render: (v) => (v ? <Tag color="blue"></Tag> : '-') },
{ title: '状态', dataIndex: 'status', width: 90, render: (v) => (v === 1 ? <Tag color="green"></Tag> : <Tag></Tag>) },
]
return (
<div>
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<div>
<Typography.Title level={4} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
{merchant ? `${merchant.name} / ${merchant.code}` : '加载中'} · {merchantRole ? roleText(merchantRole) : '-'}
</Typography.Text>
</div>
<Button icon={<ReloadOutlined />} loading={loading} onClick={loadAll}>
</Button>
</Space>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={[
{
key: 'products',
label: '商品',
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> SKU </Typography.Text>
{canManage && <Button type="primary" icon={<PlusOutlined />} onClick={openProductCreate}></Button>}
</Space>
<Table
rowKey="id"
loading={loading}
columns={productColumns}
dataSource={products.list}
tableLayout="fixed"
pagination={pageConfig(products, loadProducts)}
/>
</Space>
),
},
{
key: 'orders',
label: '履约订单',
children: (
<Table
rowKey="id"
loading={loading}
columns={orderColumns}
dataSource={orders.list}
tableLayout="fixed"
scroll={{ x: 1300 }}
pagination={pageConfig(orders, loadOrders)}
/>
),
},
{
key: 'wallet',
label: '钱包',
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Descriptions size="small" bordered column={3} style={{ flex: 1 }}>
<Descriptions.Item label="可用余额">{money(wallet?.available_balance ?? 0)}</Descriptions.Item>
<Descriptions.Item label="冻结余额">{money(wallet?.frozen_balance ?? 0)}</Descriptions.Item>
<Descriptions.Item label="币种">{wallet?.currency || 'CNY'}</Descriptions.Item>
</Descriptions>
{canFinance && (
<Button
icon={<WalletOutlined />}
onClick={() => {
walletForm.resetFields()
walletForm.setFieldsValue({ idempotency_key: `manual-${Date.now()}` })
setWalletOpen(true)
}}
>
</Button>
)}
</Space>
<Table
rowKey="id"
loading={loading}
columns={ledgerColumns}
dataSource={ledger.list}
tableLayout="fixed"
pagination={pageConfig(ledger, loadWallet)}
/>
</Space>
),
},
{
key: 'api',
label: 'API 客户端',
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"></Typography.Text>
{canManage && <Button type="primary" icon={<ApiOutlined />} onClick={() => {
apiClientForm.resetFields()
apiClientForm.setFieldsValue({ signature_version: 'v1', scopes: ['products:read', 'orders:read', 'orders:write'] })
setApiClientOpen(true)
}}></Button>}
</Space>
<Table rowKey="id" loading={loading} columns={apiClientColumns} dataSource={apiClients} tableLayout="fixed" />
</Space>
),
},
{
key: 'callbacks',
label: '回调',
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> outbox </Typography.Text>
{canManage && <Button type="primary" icon={<PlusOutlined />} onClick={() => {
callbackForm.resetFields()
callbackForm.setFieldsValue({ events: ['order.fulfillment.updated'] })
setCallbackOpen(true)
}}></Button>}
</Space>
<Table rowKey="id" loading={loading} columns={callbackColumns} dataSource={callbacks} tableLayout="fixed" />
</Space>
),
},
{
key: 'members',
label: '成员',
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"></Typography.Text>
{merchantRole === 'owner' && <Button type="primary" icon={<PlusOutlined />} onClick={() => {
memberForm.resetFields()
memberForm.setFieldsValue({ role: 'operator' })
setMemberOpen(true)
}}></Button>}
</Space>
<Table rowKey="id" loading={loading} columns={memberColumns} dataSource={members} tableLayout="fixed" />
</Space>
),
},
]}
/>
<Modal title={editingProduct ? '编辑商品' : '新增商品'} open={productOpen} onOk={submitProduct} onCancel={() => setProductOpen(false)} destroyOnClose width={620}>
<Form form={productForm} layout="vertical" style={{ marginTop: 16 }}>
{!editingProduct && (
<>
<Form.Item name="product_code" label="目录编码">
<Input placeholder="已有目录编码可选填" />
</Form.Item>
<Form.Item name="product_name" label="商品名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
</>
)}
<Space size="middle" style={{ width: '100%' }}>
<Form.Item name="sku" label="商户 SKU" rules={[{ required: true }]} style={{ width: 280 }}>
<Input disabled={!!editingProduct} />
</Form.Item>
<Form.Item name="display_name" label="展示名" style={{ width: 280 }}>
<Input />
</Form.Item>
</Space>
<Space size="middle" style={{ width: '100%' }}>
<Form.Item name="price_yuan" label="售价" rules={[{ required: true }]} style={{ width: 180 }}>
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="cost_yuan" label="成本" style={{ width: 180 }}>
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="currency" label="币种" style={{ width: 180 }}>
<Select options={[{ value: 'CNY', label: 'CNY' }]} />
</Form.Item>
</Space>
<Space size="middle" style={{ width: '100%' }}>
<Form.Item name="stock" label="库存(-1 无限)" style={{ width: 180 }}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="status" label="状态" style={{ width: 180 }}>
<Select options={[{ value: 'active', label: '上架' }, { value: 'inactive', label: '下架' }]} />
</Form.Item>
{!editingProduct && (
<Form.Item name="category" label="品类" style={{ width: 180 }}>
<Input />
</Form.Item>
)}
</Space>
<Form.Item name="fulfillment_config" label="履约配置">
<Input.TextArea rows={3} placeholder='{"provider":"manual"}' />
</Form.Item>
</Form>
</Modal>
<Modal title="钱包调整" open={walletOpen} onOk={submitWalletAdjust} onCancel={() => setWalletOpen(false)} destroyOnClose>
<Form form={walletForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="amount_yuan" label="调整金额" rules={[{ required: true }]}>
<InputNumber precision={2} style={{ width: '100%' }} placeholder="正数充值,负数扣减" />
</Form.Item>
<Form.Item name="idempotency_key" label="幂等键" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="note" label="备注">
<Input />
</Form.Item>
</Form>
</Modal>
<Modal title="新增 API 客户端" open={apiClientOpen} onOk={submitAPIClient} onCancel={() => setApiClientOpen(false)} destroyOnClose>
<Form form={apiClientForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="scopes" label="权限" rules={[{ required: true }]}>
<Select mode="multiple" options={scopeOptions} />
</Form.Item>
<Form.Item name="signature_version" label="签名版本">
<Select options={[{ value: 'v1', label: 'v1' }]} />
</Form.Item>
</Form>
</Modal>
<Modal title="API 密钥" open={!!apiCredential} onCancel={() => setApiCredential(null)} footer={<Button type="primary" onClick={() => setApiCredential(null)}></Button>}>
{apiCredential && <SecretBlock appKey={apiCredential.client.app_key} secret={apiCredential.secret} />}
</Modal>
<Modal title="新增回调" open={callbackOpen} onOk={submitCallback} onCancel={() => setCallbackOpen(false)} destroyOnClose>
<Form form={callbackForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="url" label="回调 URL" rules={[{ required: true }]}>
<Input placeholder="https://example.com/callback" />
</Form.Item>
<Form.Item name="events" label="事件" rules={[{ required: true }]}>
<Select mode="multiple" options={eventOptions} />
</Form.Item>
</Form>
</Modal>
<Modal title="回调密钥" open={!!callbackCredential} onCancel={() => setCallbackCredential(null)} footer={<Button type="primary" onClick={() => setCallbackCredential(null)}></Button>}>
{callbackCredential && <SecretBlock secret={callbackCredential.secret} />}
</Modal>
<Modal title="添加成员" open={memberOpen} onOk={submitMember} onCancel={() => setMemberOpen(false)} destroyOnClose>
<Form form={memberForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="user_id" label="用户 ID" rules={[{ required: true }]}>
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="role" label="角色" rules={[{ required: true }]}>
<Select options={memberRoleOptions} />
</Form.Item>
<Form.Item name="is_default" label="默认商户">
<Select options={[{ value: true, label: '是' }, { value: false, label: '否' }]} />
</Form.Item>
</Form>
</Modal>
</div>
)
}
function pageConfig<T extends { page: number; size: number; total: number }>(
data: T,
load: (page: number, size: number) => void,
) {
return {
current: data.page,
pageSize: data.size,
total: data.total,
showSizeChanger: true,
showTotal: (total: number) => `${total}`,
onChange: load,
}
}
function SecretBlock({ appKey, secret }: { appKey?: string; secret: string }) {
return (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
{appKey && (
<div>
<Typography.Text type="secondary">App Key</Typography.Text>
<Typography.Paragraph copyable={{ text: appKey }} code style={{ marginTop: 8 }}>{appKey}</Typography.Paragraph>
</div>
)}
<div>
<Typography.Text type="secondary">Secret</Typography.Text>
<Typography.Paragraph copyable={{ text: secret }} code style={{ marginTop: 8 }}>{secret}</Typography.Paragraph>
</div>
<Button icon={<CopyOutlined />} onClick={() => navigator.clipboard.writeText(secret).then(() => message.success('已复制'))}>
Secret
</Button>
</Space>
)
}
function yuanToCents(value?: number | null) {
return Math.round(Number(value || 0) * 100)
}
function centsToYuan(value?: number | null) {
return Number(((value || 0) / 100).toFixed(2))
}
function money(value?: number | null) {
return `¥${centsToYuan(value).toFixed(2)}`
}
function moneyWithSign(value?: number | null) {
const amount = Number(value || 0)
const prefix = amount > 0 ? '+' : ''
return `${prefix}${money(amount)}`
}
function formatTime(value?: string | null) {
return value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-'
}
function productStatusTag(value: string) {
return value === 'active' ? <Tag color="green"></Tag> : <Tag></Tag>
}
function activeStatusTag(value: string) {
return value === 'active' ? <Tag color="green"></Tag> : <Tag></Tag>
}
function paymentStatusTag(value: string) {
const item = paymentStatusMap[value] || { color: 'default', text: value }
return <Tag color={item.color}>{item.text}</Tag>
}
function fulfillmentStatusTag(value: string) {
const item = fulfillmentStatusMap[value] || { color: 'default', text: value }
return <Tag color={item.color}>{item.text}</Tag>
}
function ledgerTypeTag(value: string) {
const map: Record<string, { color: string; text: string }> = {
credit: { color: 'green', text: '入账' },
debit: { color: 'red', text: '扣款' },
refund: { color: 'purple', text: '退款' },
adjust: { color: 'blue', text: '调整' },
}
const item = map[value] || { color: 'default', text: value }
return <Tag color={item.color}>{item.text}</Tag>
}
function memberRoleTag(value: MerchantMember['role']) {
return <Tag color={value === 'owner' ? 'gold' : 'blue'}>{roleText(value)}</Tag>
}
function roleText(value: MerchantMember['role']) {
return memberRoleOptions.find((item) => item.value === value)?.label || value
}