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 ReactECharts from 'echarts-for-react'; import dayjs from 'dayjs'; import api from '../../api'; const { RangePicker } = DatePicker; const COLORS = [ '#007AFF', '#34C759', '#FF9500', '#FF3B30', '#5AC8FA', '#AF52DE', '#FF2D55', '#FFCC00', ]; const DashboardPage: React.FC = () => { const screens = Grid.useBreakpoint(); const isMobile = !screens.sm; const [stats, setStats] = useState(null); const [expenseStats, setExpenseStats] = useState([]); const [roomRanking, setRoomRanking] = useState([]); const [loading, setLoading] = useState(true); const [period, setPeriod] = useState<[string, string]>([ dayjs().startOf('month').format('YYYY-MM-DD'), dayjs().endOf('month').format('YYYY-MM-DD'), ]); const fetchData = async () => { setLoading(true); try { const [s, e, 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); } catch (e) { console.error(e); } setLoading(false); }; useEffect(() => { fetchData(); }, [period]); const expenseTypeMap: Record = { water: '水费', electricity: '电费', cleaning: '保洁费', damage: '损坏赔偿', penalty: '罚款', key: '钥匙费', remote: '空调遥控器', deposit_deduction: '押金扣除', other: '其他', }; // 费用饼图 const pieOption = { 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), })), }, ], }; // 宿舍费用排行 const barOption = { tooltip: {}, grid: { left: 80, right: 20, bottom: 30, top: 10 }, xAxis: { type: 'value' }, yAxis: { type: 'category', data: roomRanking.map((r) => r.roomNumber).reverse(), inverse: false, }, series: [ { type: 'bar', data: roomRanking.map((r) => Number(r.total)).reverse(), itemStyle: { color: '#007AFF', borderRadius: [0, 4, 4, 0] }, }, ], }; // 考勤趋势折线图 const attendanceLineOption = { tooltip: { trigger: 'axis' }, grid: { left: 50, right: 20, bottom: 30, top: 10 }, xAxis: { type: 'category', data: (stats?.attendanceTrend || []).map((d: { date: string }) => d.date), axisLabel: { rotate: 45, fontSize: 10 }, }, yAxis: { type: 'value', min: 0, max: 100, axisLabel: { formatter: '{value}%' } }, series: [ { type: 'line', data: (stats?.attendanceTrend || []).map((d: { rate: string }) => parseFloat(d.rate) || 0), smooth: true, lineStyle: { color: '#007AFF', width: 2 }, itemStyle: { color: '#007AFF' }, areaStyle: { color: 'rgba(0,122,255,0.1)' }, }, ], }; // 收入趋势折线图 const incomeLineOption = { tooltip: { trigger: 'axis', valueFormatter: (v: number) => `¥${v.toLocaleString()}` }, grid: { left: 70, right: 20, bottom: 30, top: 10 }, xAxis: { type: 'category', data: (stats?.incomeTrend || []).map((d: { month: string }) => d.month), }, yAxis: { type: 'value', axisLabel: { formatter: (v: number) => `¥${(v / 10000).toFixed(0)}万` } }, series: [ { type: 'line', data: (stats?.incomeTrend || []).map((d: { amount: number }) => d.amount), smooth: true, lineStyle: { color: '#34C759', width: 2 }, itemStyle: { color: '#34C759' }, areaStyle: { color: 'rgba(52,199,89,0.1)' }, }, ], }; if (loading && !stats) return ; return (

数据面板

{ if (dates) setPeriod([dates[0]!.format('YYYY-MM-DD'), dates[1]!.format('YYYY-MM-DD')]); }} />
} /> } /> } /> } /> } /> } /> } /> {(stats?.attendanceTrend || []).length > 0 ? ( ) : (
暂无考勤数据
)}
{(stats?.incomeTrend || []).length > 0 ? ( ) : (
暂无收入数据
)}
{expenseStats.length > 0 ? ( ) : (
暂无费用数据
)}
{roomRanking.length > 0 ? ( ) : (
暂无费用数据
)}
); }; export default DashboardPage;