Files
affiliate_dash/frontend/src/pages/Dashboard.tsx
T

360 lines
12 KiB
TypeScript

import { useEffect, useState, type ReactNode } from 'react'
import { Card, Col, Progress, Row, Space, Statistic, Typography, Spin, message, Tag } from 'antd'
import {
ApiOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
CodeOutlined,
DatabaseOutlined,
DollarOutlined,
FileTextOutlined,
PlusOutlined,
ShopOutlined,
ShoppingOutlined,
TeamOutlined,
WalletOutlined,
} from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import { dashboardApi } from '../api'
import type { DashboardStats } from '../types'
import { PageHeader } from '../components/PageHeader'
import { useAuth } from '../store/auth'
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 ModernMetricCard({
title,
value,
icon,
unit = '',
iconTheme = '',
footerText,
footerValue,
}: {
title: string
value: number
icon: ReactNode
unit?: string
iconTheme?: string
footerText?: string
footerValue?: string | number
}) {
return (
<div className="metric-card-box">
<div className="metric-card-header">
<span className="metric-card-title">{title}</span>
<div className={`metric-card-icon ${iconTheme}`}>{icon}</div>
</div>
<div className="metric-card-value">
{numberText(value)}
{unit && <span style={{ fontSize: 13, fontWeight: 500, marginLeft: 4, color: '#64748b' }}>{unit}</span>}
</div>
{footerText && (
<div className="metric-card-footer">
<span>{footerText}</span>
<span style={{ fontWeight: 600, color: '#0f172a' }}>{footerValue}</span>
</div>
)}
</div>
)
}
function CustomStatusLine({
label,
value,
total,
color,
tagClass,
}: {
label: string
value: number
total: number
color: string
tagClass: string
}) {
const percent = ratio(value, total)
return (
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
<div className={`status-tag ${tagClass}`}>
<span className="status-dot"></span>
{label}
</div>
<Space size="small">
<Typography.Text strong style={{ fontSize: 15, color: '#0f172a' }}>
{numberText(value)}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
({percent}%)
</Typography.Text>
</Space>
</div>
<Progress percent={percent} strokeColor={color} showInfo={false} size="small" />
</div>
)
}
export default function Dashboard() {
const [stats, setStats] = useState<DashboardStats | null>(null)
const [loading, setLoading] = useState(true)
const { user, isAdmin } = useAuth()
const navigate = useNavigate()
useEffect(() => {
dashboardApi
.stats()
.then(setStats)
.catch((e) => message.error(e.message))
.finally(() => setLoading(false))
}, [])
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '100px 0' }}>
<Spin size="large" tip="正在加载仪表盘数据..." />
</div>
)
}
return (
<div>
<PageHeader
title="运营工作台"
subtitle={stats?.scope === 'platform' ? '平台全局运营数据与发货概览' : '当前商户运营数据与极速发货概览'}
breadcrumbs={[{ title: '工作台' }]}
extra={
<Tag color="blue" style={{ padding: '4px 12px', borderRadius: 20, fontSize: 12, fontWeight: 600 }}>
{stats?.scope === 'platform' ? '全局视图' : '商户视图'}
</Tag>
}
/>
{/* 顶部欢迎卡片 & 快捷 LaunchPad */}
<div className="dash-welcome-card">
<h2 className="dash-welcome-title">
你好,{user?.nickname || user?.username} 👋
</h2>
<p className="dash-welcome-sub">
欢迎回到 Skin Hub 游戏皮肤分销管理平台。以下是系统的最新业务状态与发货订单追踪数据。
</p>
<div className="dash-quick-launch">
<button className="dash-quick-btn" type="button" onClick={() => navigate('/merchant-orders')}>
<PlusOutlined /> 发货订单管理
</button>
<button className="dash-quick-btn" type="button" onClick={() => navigate('/merchant-wallet')}>
<WalletOutlined /> 积分流水查看
</button>
{isAdmin && (
<>
<button className="dash-quick-btn" type="button" onClick={() => navigate('/platform-merchants')}>
<ShopOutlined /> 平台商户管理
</button>
<button className="dash-quick-btn" type="button" onClick={() => navigate('/open-api')}>
<FileTextOutlined /> 开放接口文档
</button>
<button className="dash-quick-btn" type="button" onClick={() => navigate('/api-debug')}>
<CodeOutlined /> 在线 API 调试
</button>
</>
)}
</div>
</div>
{/* 第一组:核心业务指标 */}
<Typography.Title level={5} style={{ marginBottom: 14, fontWeight: 700, color: '#1e293b' }}>
基础数据指标
</Typography.Title>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={12} lg={8}>
<ModernMetricCard
title={stats?.scope === 'platform' ? '平台可售商品' : '商户上架商品'}
value={stats?.active_product_count ?? 0}
icon={<ShoppingOutlined />}
iconTheme=""
footerText="活跃商品库"
footerValue="已上线"
/>
</Col>
<Col xs={24} sm={12} lg={8}>
<ModernMetricCard
title={stats?.scope === 'platform' ? '平台启用商户' : '当前合作商户'}
value={stats?.active_merchant_count ?? 0}
icon={<ShopOutlined />}
iconTheme="metric-card-icon--purple"
footerText="商户状态"
footerValue="正常运行"
/>
</Col>
<Col xs={24} sm={12} lg={8}>
<ModernMetricCard
title="平台成员账号"
value={stats?.user_count ?? 0}
icon={<TeamOutlined />}
iconTheme="metric-card-icon--emerald"
footerText="团队成员"
footerValue="活跃"
/>
</Col>
<Col xs={24} sm={12} lg={8}>
<ModernMetricCard
title="累计订单总数"
value={stats?.order_count ?? 0}
icon={<DatabaseOutlined />}
iconTheme="metric-card-icon--amber"
footerText="今日新增订单"
footerValue={`${stats?.today_order_count ?? 0} 单`}
/>
</Col>
<Col xs={24} sm={12} lg={8}>
<ModernMetricCard
title="今日订单总量"
value={stats?.today_order_count ?? 0}
icon={<ClockCircleOutlined />}
iconTheme=""
footerText="占比全量"
footerValue={`${ratio(stats?.today_order_count ?? 0, stats?.order_count ?? 0)}%`}
/>
</Col>
<Col xs={24} sm={12} lg={8}>
<ModernMetricCard
title="待处理/待发货订单"
value={stats?.paid_order_count ?? 0}
icon={<ClockCircleOutlined />}
iconTheme={(stats?.paid_order_count ?? 0) > 0 ? 'metric-card-icon--rose' : ''}
footerText="需及时发货"
footerValue={(stats?.paid_order_count ?? 0) > 0 ? '需处理' : '无积压'}
/>
</Col>
</Row>
{/* 第二组:积分与资金监控 */}
<Typography.Title level={5} style={{ marginBottom: 14, fontWeight: 700, color: '#1e293b' }}>
积分变动与账户额度
</Typography.Title>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={12} lg={6}>
<ModernMetricCard
title="累计成交积分"
value={stats?.total_sales ?? 0}
unit="积分"
icon={<DollarOutlined />}
iconTheme="metric-card-icon--emerald"
/>
</Col>
<Col xs={24} sm={12} lg={6}>
<ModernMetricCard
title="今日成交积分"
value={stats?.today_sales ?? 0}
unit="积分"
icon={<DollarOutlined />}
iconTheme="metric-card-icon--amber"
/>
</Col>
<Col xs={24} sm={12} lg={6}>
<ModernMetricCard
title="平台扣费/手续费"
value={stats?.total_fees ?? 0}
unit="积分"
icon={<WalletOutlined />}
iconTheme="metric-card-icon--purple"
/>
</Col>
<Col xs={24} sm={12} lg={6}>
<ModernMetricCard
title="账户可用余额"
value={stats?.wallet_available_balance ?? 0}
unit="积分"
icon={<WalletOutlined />}
iconTheme=""
/>
</Col>
</Row>
{/* 第三组:订单发货分布与 API 监控 */}
<Row gutter={[20, 20]}>
<Col xs={24} lg={14}>
<Card title="发货订单状态占比分布" style={{ height: '100%' }}>
<CustomStatusLine
label="待发货 (paid)"
value={stats?.paid_order_count ?? 0}
total={stats?.order_count ?? 0}
color="#2563eb"
tagClass="status-tag--blue"
/>
<CustomStatusLine
label="发货中 (delivering)"
value={stats?.delivering_order_count ?? 0}
total={stats?.order_count ?? 0}
color="#0891b2"
tagClass="status-tag--cyan"
/>
<CustomStatusLine
label="已交付 (delivered)"
value={stats?.delivered_order_count ?? 0}
total={stats?.order_count ?? 0}
color="#16a34a"
tagClass="status-tag--green"
/>
<CustomStatusLine
label="发货失败 (ship_failed)"
value={stats?.ship_failed_order_count ?? 0}
total={stats?.order_count ?? 0}
color="#dc2626"
tagClass="status-tag--red"
/>
<CustomStatusLine
label="已取消 (cancelled)"
value={stats?.cancelled_order_count ?? 0}
total={stats?.order_count ?? 0}
color="#64748b"
tagClass="status-tag--gray"
/>
</Card>
</Col>
<Col xs={24} lg={10}>
<Card title="接口密钥与 Webhook 回调监控" style={{ height: '100%' }}>
<Row gutter={[16, 20]}>
<Col span={12}>
<Statistic
title="API 密钥 (活跃/总量)"
value={stats?.active_api_client_count ?? 0}
prefix={<ApiOutlined style={{ color: '#2563eb' }} />}
suffix={`/ ${numberText(stats?.api_client_count)}`}
/>
</Col>
<Col span={12}>
<Statistic
title="Webhook 回调订阅"
value={stats?.callback_subscription_count ?? 0}
prefix={<CheckCircleOutlined style={{ color: '#16a34a' }} />}
/>
</Col>
<Col span={12}>
<Statistic
title="待投递回调队列"
value={stats?.pending_callback_count ?? 0}
prefix={<ClockCircleOutlined style={{ color: '#d97706' }} />}
/>
</Col>
<Col span={12}>
<Statistic
title="投递失败回调"
value={stats?.failed_callback_count ?? 0}
prefix={<CloseCircleOutlined style={{ color: '#dc2626' }} />}
valueStyle={(stats?.failed_callback_count ?? 0) > 0 ? { color: '#dc2626' } : undefined}
/>
</Col>
</Row>
</Card>
</Col>
</Row>
</div>
)
}