zyc b4108961fa
All checks were successful
Build and Deploy Web / build-and-deploy (push) Successful in 1m38s
fix ui
2026-02-10 16:02:02 +08:00

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;