feat(attendance): add anomaly alert banner showing consecutive absences and frequent lates

This commit is contained in:
2026-07-06 14:27:21 +08:00
parent da69545f74
commit beeb8cb905
2 changed files with 111 additions and 17 deletions

View File

@@ -1,20 +1,7 @@
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import {
Table,
Button,
Modal,
Form,
DatePicker,
Select,
Space,
message,
Tag,
Card,
Input,
Tooltip,
Row,
Col,
Tabs,
Table, Button, Modal, Form, DatePicker, Select, Space, message,
Tag, Card, Input, Tooltip, Row, Col, Tabs, Alert,
} from 'antd';
import {
PlusOutlined,
@@ -115,6 +102,7 @@ interface DingRecord {
matchStatus: string;
studentId?: number;
}
interface AlertItem { studentId: number; studentName: string; className: string; type: string; count: number; lastDate: string }
// ── Component ──
@@ -126,6 +114,7 @@ const AttendancePage: React.FC = () => {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [classOptions, setClassOptions] = useState<ClassOption[]>([]);
const [alerts, setAlerts] = useState<AlertItem[]>([]);
// Filters
const [filterClassId, setFilterClassId] = useState<number | undefined>(undefined);
@@ -192,8 +181,9 @@ const AttendancePage: React.FC = () => {
message.success('更新成功');
setEditModalOpen(false);
fetchRecords();
} catch (e: any) {
message.error(e?.message || '更新失败');
} catch (e: unknown) {
const err = e as { message?: string };
message.error(err?.message || '更新失败');
}
};
@@ -275,6 +265,7 @@ const AttendancePage: React.FC = () => {
useEffect(() => {
fetchClasses();
}, [fetchClasses]);
useEffect(() => { api.get<AlertItem[]>('/attendance-records/alerts').then(setAlerts).catch(() => {}) }, []);
useEffect(() => {
if (calendarView) {
@@ -577,6 +568,11 @@ const AttendancePage: React.FC = () => {
// ── Render ──
return (
<div>
{alerts.length > 0 && (
<Alert type="warning" showIcon closable
message={`考勤预警:${alerts.length} 名学生异常`}
description={alerts.map(a => `${a.studentName}(${a.className || '-'})${a.type} ${a.count}次,最近${a.lastDate}`).join('')}
style={{ marginBottom: 16 }} />)}
<Tabs
activeKey={activeTab}
onChange={setActiveTab}