feat: enhance dashboard with comprehensive stats and charts

This commit is contained in:
2026-07-05 21:52:50 +08:00
parent ab5898e2cf
commit 2be8b55c93
3 changed files with 167 additions and 26 deletions

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Row, Col, Card, Statistic, DatePicker, Spin, Grid } from 'antd';
import { TeamOutlined, HomeOutlined, DollarOutlined, CheckCircleOutlined, BankOutlined, PercentageOutlined, UserSwitchOutlined } from '@ant-design/icons';
import { TeamOutlined, HomeOutlined, DollarOutlined, CheckCircleOutlined, BankOutlined, PercentageOutlined, UserSwitchOutlined, SolutionOutlined, WalletOutlined, FileProtectOutlined } from '@ant-design/icons';
import ReactECharts from 'echarts-for-react';
import dayjs from 'dayjs';
import api from '../../api';
@@ -18,12 +18,40 @@ const COLORS = [
'#FFCC00',
];
interface BillStatRow { status: string; count: string; total: string }
interface AttendanceTrendRow { date: string; rate: string }
interface IncomeTrendRow { month: string; amount: number }
interface OccupancyByBuildingRow { building: string; count: string }
interface ExpenseByTypeRow { type: string; total: string }
interface DashboardStats {
totalRooms: number;
totalStudents: number;
occupiedBeds: number;
totalCapacity: number;
occupancyRate: string;
billStats: BillStatRow[];
classroomCount: number;
classroomOccupancyRate: string;
todayAttendanceRate: string;
monthlyIncome: number;
classCount: number;
teacherCount: number;
pendingDeposits: number;
activeRentals: number;
occupancyByBuilding: OccupancyByBuildingRow[];
attendanceByStatus: Record<string, number>;
expenseByType: ExpenseByTypeRow[];
attendanceTrend: AttendanceTrendRow[];
incomeTrend: IncomeTrendRow[];
}
interface ApiResponse<T> { data: T }
const DashboardPage: React.FC = () => {
const screens = Grid.useBreakpoint();
const isMobile = !screens.sm;
const [stats, setStats] = useState<any>(null);
const [expenseStats, setExpenseStats] = useState<any[]>([]);
const [roomRanking, setRoomRanking] = useState<any[]>([]);
const [stats, setStats] = useState<DashboardStats | null>(null);
const [roomRanking, setRoomRanking] = useState<Array<{ roomNumber: string; total: string }>>([]);
const [loading, setLoading] = useState(true);
const [period, setPeriod] = useState<[string, string]>([
dayjs().startOf('month').format('YYYY-MM-DD'),
@@ -33,18 +61,14 @@ const DashboardPage: React.FC = () => {
const fetchData = async () => {
setLoading(true);
try {
const [s, e, r] = await Promise.all([
const [s, r] = await Promise.all([
api.get('/dashboard/stats'),
api.get('/dashboard/expense-stats', {
params: { periodStart: period[0], periodEnd: period[1] },
}),
api.get('/dashboard/room-ranking', {
params: { periodStart: period[0], periodEnd: period[1] },
}),
]);
setStats(s);
setExpenseStats(e as any);
setRoomRanking(r as any);
setRoomRanking(r as unknown as Array<{ roomNumber: string; total: string }>);
} catch (e) {
console.error(e);
}
@@ -67,23 +91,34 @@ const DashboardPage: React.FC = () => {
other: '其他',
};
const attendanceLabelMap: Record<string, string> = {
present: '出勤',
absent: '缺勤',
late: '迟到',
early: '早退',
leave: '请假',
};
// 费用饼
const pieOption = {
// 今日出勤状态分布环
const attendanceRingOption = {
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
type: 'pie',
radius: ['40%', '70%'],
data: expenseStats.map((e) => ({
name: expenseTypeMap[e.type] || e.type,
value: Number(e.total),
center: ['50%', '45%'],
data: Object.entries(stats?.attendanceByStatus ?? {}).map(([status, count]) => ({
name: attendanceLabelMap[status] ?? status,
value: count,
})),
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
},
],
color: COLORS,
};
// 宿舍费用排行
const barOption = {
tooltip: {},
@@ -201,7 +236,7 @@ const DashboardPage: React.FC = () => {
title="入住率"
value={stats?.occupancyRate || 0}
suffix="%"
prefix={<DollarOutlined />}
prefix={<PercentageOutlined />}
/>
</Card>
</Col>
@@ -245,6 +280,34 @@ const DashboardPage: React.FC = () => {
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={12} sm={12} md={6}>
<Card>
<Statistic title="班级总数" value={stats?.classCount ?? 0} prefix={<TeamOutlined />} />
</Card>
</Col>
<Col xs={12} sm={12} md={6}>
<Card>
<Statistic title="教师总数" value={stats?.teacherCount ?? 0} prefix={<SolutionOutlined />} />
</Card>
</Col>
<Col xs={12} sm={12} md={6}>
<Card>
<Statistic
title="待退押金"
value={stats?.pendingDeposits ?? 0}
precision={2}
prefix="¥"
/>
</Card>
</Col>
<Col xs={12} sm={12} md={6}>
<Card>
<Statistic title="活跃租赁" value={stats?.activeRentals ?? 0} prefix={<FileProtectOutlined />} />
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={12}>
<Card title="考勤趋势近30天">
@@ -256,21 +319,26 @@ const DashboardPage: React.FC = () => {
</Card>
</Col>
<Col xs={24} sm={12}>
<Card title="收入趋势近6个月">
{(stats?.incomeTrend || []).length > 0 ? (
<ReactECharts option={incomeLineOption} style={{ width: '100%', height: isMobile ? 250 : 300 }} />
<Card title="今日出勤状态分布">
{Object.keys(stats?.attendanceByStatus ?? {}).length > 0 ? (
<ReactECharts option={attendanceRingOption} style={{ width: '100%', height: isMobile ? 250 : 300 }} />
) : (
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}></div>
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}></div>
)}
</Card>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={12}>
<Card title="费用类型分布">
{expenseStats.length > 0 ? (
<ReactECharts option={pieOption} style={{ width: '100%', height: isMobile ? 250 : 300 }} />
{((stats?.expenseByType) ?? []).length > 0 ? (
<ReactECharts option={{
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
color: COLORS,
series: [{ type: 'pie', radius: ['40%', '70%'], center: ['50%', '45%'], data: (stats?.expenseByType ?? []).map((e) => ({ name: expenseTypeMap[e.type] ?? e.type, value: Number(e.total) })) }],
}} style={{ width: '100%', height: isMobile ? 250 : 300 }} />
) : (
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}></div>
)}
@@ -286,6 +354,18 @@ const DashboardPage: React.FC = () => {
</Card>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col xs={24}>
<Card title="月度收入趋势">
{(stats?.incomeTrend || []).length > 0 ? (
<ReactECharts option={incomeLineOption} style={{ width: '100%', height: isMobile ? 250 : 300 }} />
) : (
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}></div>
)}
</Card>
</Col>
</Row>
</div>
);
};