All checks were successful
Build and Deploy Web / build-and-deploy (push) Successful in 1m38s
251 lines
8.7 KiB
TypeScript
251 lines
8.7 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Row, Col, Card, Typography, Spin } from 'antd';
|
|
import {
|
|
UserOutlined,
|
|
MobileOutlined,
|
|
AppstoreOutlined,
|
|
InboxOutlined,
|
|
CheckCircleOutlined,
|
|
DatabaseOutlined,
|
|
} from '@ant-design/icons';
|
|
import { Pie, Column } from '@ant-design/charts';
|
|
import request from '../../api/request';
|
|
import { statColors } from '../../theme/tokens';
|
|
|
|
const { Title } = Typography;
|
|
|
|
interface DashboardStats {
|
|
user_count: number;
|
|
device_count: number;
|
|
device_type_count: number;
|
|
batch_count: number;
|
|
bound_device_count: number;
|
|
in_stock_device_count: number;
|
|
}
|
|
|
|
const getDashboardStats = async (): Promise<DashboardStats> => {
|
|
const res = await request.get<unknown, { data: DashboardStats }>('/api/admin/dashboard/stats/');
|
|
return res.data;
|
|
};
|
|
|
|
const Dashboard: React.FC = () => {
|
|
const [loading, setLoading] = useState(true);
|
|
const [stats, setStats] = useState<DashboardStats>({
|
|
user_count: 0,
|
|
device_count: 0,
|
|
device_type_count: 0,
|
|
batch_count: 0,
|
|
bound_device_count: 0,
|
|
in_stock_device_count: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
loadStats();
|
|
}, []);
|
|
|
|
const loadStats = async () => {
|
|
try {
|
|
const data = await getDashboardStats();
|
|
setStats(data);
|
|
} catch (error) {
|
|
// 静默失败,使用默认值
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const statCards = [
|
|
{
|
|
title: '用户总数',
|
|
value: stats.user_count,
|
|
icon: <UserOutlined />,
|
|
color: statColors.primary,
|
|
bgColor: 'rgba(99, 102, 241, 0.08)',
|
|
},
|
|
{
|
|
title: '设备总数',
|
|
value: stats.device_count,
|
|
icon: <MobileOutlined />,
|
|
color: statColors.success,
|
|
bgColor: 'rgba(16, 185, 129, 0.08)',
|
|
},
|
|
{
|
|
title: '设备类型',
|
|
value: stats.device_type_count,
|
|
icon: <AppstoreOutlined />,
|
|
color: statColors.purple,
|
|
bgColor: 'rgba(139, 92, 246, 0.08)',
|
|
},
|
|
{
|
|
title: '入库批次',
|
|
value: stats.batch_count,
|
|
icon: <InboxOutlined />,
|
|
color: statColors.warning,
|
|
bgColor: 'rgba(245, 158, 11, 0.08)',
|
|
},
|
|
{
|
|
title: '已绑定设备',
|
|
value: stats.bound_device_count,
|
|
icon: <CheckCircleOutlined />,
|
|
color: statColors.cyan,
|
|
bgColor: 'rgba(6, 182, 212, 0.08)',
|
|
},
|
|
{
|
|
title: '库存设备',
|
|
value: stats.in_stock_device_count,
|
|
icon: <DatabaseOutlined />,
|
|
color: statColors.pink,
|
|
bgColor: 'rgba(236, 72, 153, 0.08)',
|
|
},
|
|
];
|
|
|
|
// 设备状态分布数据
|
|
const otherCount = Math.max(0, stats.device_count - stats.in_stock_device_count - stats.bound_device_count);
|
|
const pieData = [
|
|
{ type: '库存中', value: stats.in_stock_device_count },
|
|
{ type: '已绑定', value: stats.bound_device_count },
|
|
...(otherCount > 0 ? [{ type: '其他', value: otherCount }] : []),
|
|
];
|
|
|
|
const pieConfig = {
|
|
data: pieData,
|
|
angleField: 'value',
|
|
colorField: 'type',
|
|
radius: 0.85,
|
|
innerRadius: 0.6,
|
|
color: [statColors.primary, statColors.success, statColors.warning],
|
|
label: {
|
|
text: (d: { type: string; value: number }) => `${d.type}\n${d.value}`,
|
|
style: { fontSize: 12, fontWeight: 500 },
|
|
},
|
|
legend: {
|
|
color: {
|
|
position: 'bottom' as const,
|
|
layout: { justifyContent: 'center' as const },
|
|
},
|
|
},
|
|
interaction: {
|
|
elementHighlight: true,
|
|
},
|
|
};
|
|
|
|
// 数据概览柱状图
|
|
const columnData = [
|
|
{ name: '用户', value: stats.user_count, type: '数量' },
|
|
{ name: '设备', value: stats.device_count, type: '数量' },
|
|
{ name: '已绑定', value: stats.bound_device_count, type: '数量' },
|
|
{ name: '库存', value: stats.in_stock_device_count, type: '数量' },
|
|
{ name: '批次', value: stats.batch_count, type: '数量' },
|
|
{ name: '类型', value: stats.device_type_count, type: '数量' },
|
|
];
|
|
|
|
const columnConfig = {
|
|
data: columnData,
|
|
xField: 'name',
|
|
yField: 'value',
|
|
color: statColors.primary,
|
|
style: {
|
|
radiusTopLeft: 6,
|
|
radiusTopRight: 6,
|
|
fill: statColors.primary,
|
|
fillOpacity: 0.85,
|
|
},
|
|
label: {
|
|
text: (d: { value: number }) => `${d.value}`,
|
|
textBaseline: 'bottom' as const,
|
|
style: { dy: -4, fontSize: 12 },
|
|
},
|
|
axis: {
|
|
y: { title: false },
|
|
x: { title: false },
|
|
},
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Title level={4} style={{ marginBottom: 24, color: '#1f2937' }}>
|
|
仪表盘
|
|
</Title>
|
|
|
|
<Spin spinning={loading}>
|
|
{/* Stat Cards */}
|
|
<Row gutter={[16, 16]}>
|
|
{statCards.map((stat, index) => (
|
|
<Col xs={24} sm={12} lg={8} xl={4} key={index}>
|
|
<Card
|
|
hoverable
|
|
styles={{ body: { padding: '20px' } }}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
|
<div
|
|
style={{
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 12,
|
|
background: stat.bgColor,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: 20,
|
|
color: stat.color,
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{stat.icon}
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 2 }}>
|
|
{stat.title}
|
|
</div>
|
|
<div style={{ fontSize: 24, fontWeight: 700, color: '#1f2937', lineHeight: 1.2 }}>
|
|
{stat.value.toLocaleString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
|
|
{/* Charts */}
|
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
<Col xs={24} lg={12}>
|
|
<Card
|
|
title="设备状态分布"
|
|
styles={{ body: { padding: '16px 24px' } }}
|
|
>
|
|
<div style={{ height: 320 }}>
|
|
{stats.device_count > 0 ? (
|
|
<Pie {...pieConfig} />
|
|
) : (
|
|
<div style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
height: '100%',
|
|
color: '#9ca3af',
|
|
}}>
|
|
暂无设备数据
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} lg={12}>
|
|
<Card
|
|
title="数据概览"
|
|
styles={{ body: { padding: '16px 24px' } }}
|
|
>
|
|
<div style={{ height: 320 }}>
|
|
<Column {...columnConfig} />
|
|
</div>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
</Spin>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Dashboard;
|