- 发货提交记录阶段推进与失败分类,失败不再清空 result_data - 超时巡检区分已提交上游与提交中断两类卡单,避免误判 - 已提交上游的失败订单禁止自动重发,防止重复发货 - ship_attempts 仅在 claim 时计数,失败阶段只记录分类信息 - CanFulfill 对已提交上游的失败单返回不可发货 - 删除预留的 pending 订单状态,统一订单状态模型 - 契约改名:order.fulfillment.updated -> order.shipping.updated,fulfillment:read -> shipping:read - 文档修正 scope 为或关系
229 lines
7.1 KiB
TypeScript
229 lines
7.1 KiB
TypeScript
import { useEffect, useState, type ReactNode } from 'react'
|
|
import { Card, Col, Progress, Row, Space, Statistic, Typography, Spin, message } from 'antd'
|
|
import {
|
|
ApiOutlined,
|
|
CheckCircleOutlined,
|
|
ClockCircleOutlined,
|
|
CloseCircleOutlined,
|
|
DatabaseOutlined,
|
|
DollarOutlined,
|
|
PercentageOutlined,
|
|
ShopOutlined,
|
|
ShoppingOutlined,
|
|
TeamOutlined,
|
|
WalletOutlined,
|
|
} from '@ant-design/icons'
|
|
import { dashboardApi } from '../api'
|
|
import type { DashboardStats } from '../types'
|
|
|
|
const numberText = (value?: number) => (value ?? 0).toLocaleString('zh-CN')
|
|
|
|
function ratio(part: number, total: number) {
|
|
if (total <= 0) return 0
|
|
return Math.round((part / total) * 100)
|
|
}
|
|
|
|
function MetricCard({
|
|
title,
|
|
value,
|
|
icon,
|
|
suffix,
|
|
color,
|
|
}: {
|
|
title: string
|
|
value: number
|
|
icon: ReactNode
|
|
suffix?: string
|
|
color?: string
|
|
}) {
|
|
return (
|
|
<Card>
|
|
<Statistic
|
|
title={title}
|
|
value={value}
|
|
formatter={() => numberText(value)}
|
|
prefix={icon}
|
|
suffix={suffix}
|
|
valueStyle={color ? { color } : undefined}
|
|
/>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
function StatusLine({
|
|
label,
|
|
value,
|
|
total,
|
|
color,
|
|
}: {
|
|
label: string
|
|
value: number
|
|
total: number
|
|
color: string
|
|
}) {
|
|
return (
|
|
<div>
|
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
|
<Typography.Text>{label}</Typography.Text>
|
|
<Typography.Text strong>{numberText(value)}</Typography.Text>
|
|
</Space>
|
|
<Progress percent={ratio(value, total)} strokeColor={color} showInfo={false} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function Dashboard() {
|
|
const [stats, setStats] = useState<DashboardStats | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
dashboardApi
|
|
.stats()
|
|
.then(setStats)
|
|
.catch((e) => message.error(e.message))
|
|
.finally(() => setLoading(false))
|
|
}, [])
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ textAlign: 'center', padding: 80 }}>
|
|
<Spin size="large" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 16 }} align="start">
|
|
<div>
|
|
<Typography.Title level={4} style={{ marginTop: 0, marginBottom: 4 }}>
|
|
数据概览
|
|
</Typography.Title>
|
|
<Typography.Text type="secondary">
|
|
{stats?.scope === 'platform' ? '平台全局运营数据' : '当前商户运营数据'}
|
|
</Typography.Text>
|
|
</div>
|
|
</Space>
|
|
|
|
<Row gutter={[16, 16]}>
|
|
<Col xs={24} sm={12} lg={8}>
|
|
<MetricCard
|
|
title={stats?.scope === 'platform' ? '可售商品' : '商户商品'}
|
|
value={stats?.active_product_count ?? 0}
|
|
icon={<ShoppingOutlined />}
|
|
/>
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={8}>
|
|
<MetricCard
|
|
title={stats?.scope === 'platform' ? '启用商户' : '当前商户'}
|
|
value={stats?.active_merchant_count ?? 0}
|
|
icon={<ShopOutlined />}
|
|
/>
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={8}>
|
|
<MetricCard title="成员账号" value={stats?.user_count ?? 0} icon={<TeamOutlined />} />
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={8}>
|
|
<MetricCard title="订单总数" value={stats?.order_count ?? 0} icon={<DatabaseOutlined />} />
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={8}>
|
|
<MetricCard title="今日订单" value={stats?.today_order_count ?? 0} icon={<ClockCircleOutlined />} />
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={8}>
|
|
<MetricCard
|
|
title="待发货订单"
|
|
value={stats?.paid_order_count ?? 0}
|
|
icon={<ClockCircleOutlined />}
|
|
color={(stats?.paid_order_count ?? 0) > 0 ? '#cf1322' : undefined}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Typography.Title level={5} style={{ marginTop: 24 }}>
|
|
积分与费用
|
|
</Typography.Title>
|
|
<Row gutter={[16, 16]}>
|
|
<Col xs={24} sm={12} lg={6}>
|
|
<MetricCard title="成交积分" value={stats?.total_sales ?? 0} icon={<DollarOutlined />} suffix="积分" />
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={6}>
|
|
<MetricCard title="今日成交" value={stats?.today_sales ?? 0} icon={<DollarOutlined />} suffix="积分" />
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={6}>
|
|
<MetricCard title="平台手续费" value={stats?.total_fees ?? 0} icon={<PercentageOutlined />} suffix="积分" />
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={6}>
|
|
<MetricCard title="可用余额" value={stats?.wallet_available_balance ?? 0} icon={<WalletOutlined />} suffix="积分" />
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
<Col xs={24} lg={14}>
|
|
<Card title="订单状态">
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
<StatusLine
|
|
label="待发货"
|
|
value={stats?.paid_order_count ?? 0}
|
|
total={stats?.order_count ?? 0}
|
|
color="#1677ff"
|
|
/>
|
|
<StatusLine
|
|
label="发货中"
|
|
value={stats?.delivering_order_count ?? 0}
|
|
total={stats?.order_count ?? 0}
|
|
color="#13c2c2"
|
|
/>
|
|
<StatusLine
|
|
label="已交付"
|
|
value={stats?.delivered_order_count ?? 0}
|
|
total={stats?.order_count ?? 0}
|
|
color="#52c41a"
|
|
/>
|
|
<StatusLine
|
|
label="发货失败"
|
|
value={stats?.ship_failed_order_count ?? 0}
|
|
total={stats?.order_count ?? 0}
|
|
color="#ff4d4f"
|
|
/>
|
|
<StatusLine
|
|
label="已取消"
|
|
value={stats?.cancelled_order_count ?? 0}
|
|
total={stats?.order_count ?? 0}
|
|
color="#8c8c8c"
|
|
/>
|
|
</Space>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} lg={10}>
|
|
<Card title="接口与回调">
|
|
<Row gutter={[16, 16]}>
|
|
<Col span={12}>
|
|
<Statistic
|
|
title="API 密钥"
|
|
value={stats?.active_api_client_count ?? 0}
|
|
prefix={<ApiOutlined />}
|
|
suffix={`/ ${numberText(stats?.api_client_count)}`}
|
|
/>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Statistic title="回调订阅" value={stats?.callback_subscription_count ?? 0} prefix={<CheckCircleOutlined />} />
|
|
</Col>
|
|
<Col span={12}>
|
|
<Statistic title="待投递回调" value={stats?.pending_callback_count ?? 0} prefix={<ClockCircleOutlined />} />
|
|
</Col>
|
|
<Col span={12}>
|
|
<Statistic
|
|
title="失败回调"
|
|
value={stats?.failed_callback_count ?? 0}
|
|
prefix={<CloseCircleOutlined />}
|
|
valueStyle={(stats?.failed_callback_count ?? 0) > 0 ? { color: '#cf1322' } : undefined}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
)
|
|
}
|