feat: DingTalk attendance import + integration config + expense types + UI polish
Server: - Add DingTalk attendance import service with SSE progress streaming - Add IntegrationConfig entity & module for multi-tenant DingTalk setup - Add ExpenseType entity & ExpenseTypesModule - Add SeedModule for DB initialization - Add UserDingMapping entity for DingTalk user linkage - Attendance service: import flow with dedup & student auto-mapping - Rooms service: time-range overlap queries - Sync controller/service: DingTalk integration wiring - Permission guard: refactor to pure re-export - Campus scope middleware: tenant-aware filtering Admin UI: - Attendance page: import UI with progress & result summary - All pages: tableStyle/tablePagination standardization - Login page: responsive styling - Sensitive data: useViewSensitive hook for masked viewing - Vite config: path aliases, build optimization - Test infra: vitest config, test utilities Docs: PRD DingTalk batch 1 & 2 design docs
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Row, Col, Card, Statistic, DatePicker, Spin, Grid } from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Row, Col, Card, Statistic, DatePicker, Spin, Grid, message } from 'antd';
|
||||
import { TeamOutlined, HomeOutlined, CheckCircleOutlined, BankOutlined, PercentageOutlined, UserSwitchOutlined, SolutionOutlined, FileProtectOutlined, ReadOutlined, CalendarOutlined } from '@ant-design/icons';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -62,6 +62,17 @@ interface DashboardStats {
|
||||
incomeTrend: IncomeTrendRow[];
|
||||
}
|
||||
|
||||
const attendanceLabelMap: Record<string, string> = {
|
||||
present: '出勤',
|
||||
absent: '缺勤',
|
||||
late: '迟到',
|
||||
early: '早退',
|
||||
leave: '请假',
|
||||
};
|
||||
|
||||
const SECTION_ROW_STYLE: React.CSSProperties = { marginBottom: 24 };
|
||||
const MARGIN_BOTTOM_16_STYLE: React.CSSProperties = { marginBottom: 16 };
|
||||
|
||||
const DashboardPage: React.FC = () => {
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = !screens.sm;
|
||||
@@ -77,7 +88,7 @@ const DashboardPage: React.FC = () => {
|
||||
dayjs().endOf('month').format('YYYY-MM-DD'),
|
||||
]);
|
||||
|
||||
const fetchData = async () => {
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [s, rr, cr, g] = await Promise.all([
|
||||
@@ -100,13 +111,14 @@ const DashboardPage: React.FC = () => {
|
||||
setClassroomUtil(cu);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error('数据加载失败,请稍后重试');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
}, [period]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [period]);
|
||||
}, [fetchData, period]);
|
||||
|
||||
const [expenseTypeMap, setExpenseTypeMap] = useState<Record<string, string>>({});
|
||||
|
||||
@@ -118,16 +130,8 @@ const DashboardPage: React.FC = () => {
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const attendanceLabelMap: Record<string, string> = {
|
||||
present: '出勤',
|
||||
absent: '缺勤',
|
||||
late: '迟到',
|
||||
early: '早退',
|
||||
leave: '请假',
|
||||
};
|
||||
|
||||
// 今日出勤状态分布环图
|
||||
const attendanceRingOption = {
|
||||
const attendanceRingOption = useMemo(() => ({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: 0 },
|
||||
series: [
|
||||
@@ -143,11 +147,11 @@ const DashboardPage: React.FC = () => {
|
||||
},
|
||||
],
|
||||
color: COLORS,
|
||||
};
|
||||
}), [stats?.attendanceByStatus]);
|
||||
|
||||
|
||||
// 宿舍费用排行
|
||||
const barOption = {
|
||||
const barOption = useMemo(() => ({
|
||||
tooltip: {},
|
||||
grid: { left: 80, right: 20, bottom: 30, top: 10 },
|
||||
xAxis: { type: 'value' },
|
||||
@@ -163,7 +167,7 @@ const DashboardPage: React.FC = () => {
|
||||
itemStyle: { color: '#007AFF', borderRadius: [0, 4, 4, 0] },
|
||||
},
|
||||
],
|
||||
};
|
||||
}), [roomRanking]);
|
||||
|
||||
// 班级考勤排行 - 前5
|
||||
const classRankingTopOption = {
|
||||
@@ -249,7 +253,7 @@ const DashboardPage: React.FC = () => {
|
||||
};
|
||||
|
||||
// 入住时间线(甘特图)
|
||||
const ganttOption = {
|
||||
const ganttOption = useMemo(() => ({
|
||||
tooltip: {
|
||||
formatter: (p: { data: { name: string; value: [string, string, string, boolean] } }) =>
|
||||
`${p.data.name}<br/>入住: ${p.data.value[1]}<br/>退宿: ${p.data.value[2]}`,
|
||||
@@ -300,7 +304,7 @@ const DashboardPage: React.FC = () => {
|
||||
}))
|
||||
),
|
||||
}],
|
||||
};
|
||||
}), [ganttData]);
|
||||
|
||||
if (loading && !stats)
|
||||
return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
|
||||
@@ -319,6 +323,7 @@ const DashboardPage: React.FC = () => {
|
||||
>
|
||||
<h2 style={{ margin: 0 }}>数据面板</h2>
|
||||
<RangePicker
|
||||
aria-label="选择日期范围"
|
||||
value={[dayjs(period[0]), dayjs(period[1])]}
|
||||
onChange={(dates) => {
|
||||
if (dates) setPeriod([dates[0]!.format('YYYY-MM-DD'), dates[1]!.format('YYYY-MM-DD')]);
|
||||
@@ -326,7 +331,7 @@ const DashboardPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]} style={SECTION_ROW_STYLE}>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="宿舍总数" value={stats?.totalRooms || 0} prefix={<HomeOutlined />} />
|
||||
@@ -363,7 +368,7 @@ const DashboardPage: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]} style={SECTION_ROW_STYLE}>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="教室总数" value={stats?.classroomCount || 0} prefix={<BankOutlined />} />
|
||||
@@ -401,7 +406,7 @@ const DashboardPage: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]} style={SECTION_ROW_STYLE}>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="班级总数" value={stats?.classCount ?? 0} prefix={<TeamOutlined />} />
|
||||
@@ -439,7 +444,7 @@ const DashboardPage: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="教室利用率" style={{ marginBottom: 16 }}>
|
||||
<Card title="教室利用率" style={MARGIN_BOTTOM_16_STYLE}>
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col xs={12} sm={6}>
|
||||
<Statistic title="教室总数" value={classroomUtil?.totalClassrooms ?? '-'} prefix={<ReadOutlined />} />
|
||||
@@ -453,7 +458,7 @@ const DashboardPage: React.FC = () => {
|
||||
value={classroomUtil?.utilizationRate ?? '-'}
|
||||
suffix="%"
|
||||
prefix={<PercentageOutlined />}
|
||||
valueStyle={{ color: Number(classroomUtil?.utilizationRate ?? 0) > 70 ? '#34C759' : '#FF9500' }}
|
||||
styles={{ value: { color: Number(classroomUtil?.utilizationRate ?? 0) > 70 ? '#34C759' : '#FF9500' } }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
@@ -462,7 +467,7 @@ const DashboardPage: React.FC = () => {
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]} style={SECTION_ROW_STYLE}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Card title="考勤趋势(近30天)">
|
||||
{(stats?.attendanceTrend || []).length > 0 ? (
|
||||
@@ -483,7 +488,7 @@ const DashboardPage: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]} style={SECTION_ROW_STYLE}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Card title="班级出勤率 TOP 5">
|
||||
{classRanking.top.length > 0 ? (
|
||||
@@ -504,7 +509,7 @@ const DashboardPage: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]} style={SECTION_ROW_STYLE}>
|
||||
<Col xs={24}>
|
||||
<Card title="教室占用热力图">
|
||||
{classroomOccupancy.length > 0 ? (
|
||||
@@ -533,7 +538,7 @@ const DashboardPage: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]} style={SECTION_ROW_STYLE}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Card title="费用类型分布">
|
||||
{((stats?.expenseByType) ?? []).length > 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user