Files
affiliate_dash/frontend/src/pages/Dashboard.tsx
T
yml2213 eac025c92f 加强发货提交可靠性并统一契约口径
- 发货提交记录阶段推进与失败分类,失败不再清空 result_data
- 超时巡检区分已提交上游与提交中断两类卡单,避免误判
- 已提交上游的失败订单禁止自动重发,防止重复发货
- ship_attempts 仅在 claim 时计数,失败阶段只记录分类信息
- CanFulfill 对已提交上游的失败单返回不可发货
- 删除预留的 pending 订单状态,统一订单状态模型
- 契约改名:order.fulfillment.updated -> order.shipping.updated,fulfillment:read -> shipping:read
- 文档修正 scope 为或关系
2026-07-31 18:42:52 +08:00

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>
)
}