165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
import React from 'react';
|
||
import { Card, Empty, Input, Spin, Table } from 'antd';
|
||
import { ExportOutlined } from '@ant-design/icons';
|
||
import PermissionButton from '../../components/PermissionButton';
|
||
import {
|
||
ADMIN_METRIC_META,
|
||
STATUS_META,
|
||
displayAttendanceStatus,
|
||
type AdminStudentPanel,
|
||
type AttendanceRecordItem,
|
||
} from './AttendanceAdmin.helpers';
|
||
|
||
export const AttendanceAdminWorkspace: React.FC<{
|
||
metricFilter: string;
|
||
studentSearch: string;
|
||
onSearchChange: (value: string) => void;
|
||
onExport: () => void;
|
||
visibleStudents: AdminStudentPanel[];
|
||
loading: boolean;
|
||
selectedStudentId?: number;
|
||
onSelectStudent: (student: AdminStudentPanel) => void;
|
||
sortAttendanceRecords: (items: readonly AttendanceRecordItem[]) => AttendanceRecordItem[];
|
||
sessionMap: Record<string, string>;
|
||
records: AttendanceRecordItem[];
|
||
columns: any[];
|
||
page: number;
|
||
pageSize: number;
|
||
total: number;
|
||
onPageChange: (page: number, pageSize: number) => void;
|
||
}> = ({
|
||
metricFilter,
|
||
studentSearch,
|
||
onSearchChange,
|
||
onExport,
|
||
visibleStudents,
|
||
loading,
|
||
selectedStudentId,
|
||
onSelectStudent,
|
||
sortAttendanceRecords,
|
||
sessionMap,
|
||
records,
|
||
columns,
|
||
page,
|
||
pageSize,
|
||
total,
|
||
onPageChange,
|
||
}) => {
|
||
return (
|
||
<>
|
||
<section className="student-workspace">
|
||
<header className="student-workspace-header">
|
||
<div>
|
||
<h3>班级学生考勤</h3>
|
||
<span>
|
||
{metricFilter === 'all'
|
||
? `显示全部 ${visibleStudents.length} 名学生`
|
||
: `筛出 ${
|
||
ADMIN_METRIC_META.find((item) => item.key === metricFilter)?.label || ''
|
||
}相关 ${visibleStudents.length} 名学生`}
|
||
</span>
|
||
</div>
|
||
<div className="student-workspace-tools">
|
||
<Input.Search
|
||
allowClear
|
||
placeholder="搜索姓名或学号"
|
||
value={studentSearch}
|
||
onChange={(event) => onSearchChange(event.target.value)}
|
||
/>
|
||
<PermissionButton
|
||
permission="attendance:export"
|
||
icon={<ExportOutlined />}
|
||
onClick={onExport}
|
||
>
|
||
导出
|
||
</PermissionButton>
|
||
</div>
|
||
</header>
|
||
<div className="student-legend">
|
||
<span>
|
||
<i className="is-present" />
|
||
正常
|
||
</span>
|
||
<span>
|
||
<i className="is-leave" />
|
||
请假
|
||
</span>
|
||
<span>
|
||
<i className="is-absent" />
|
||
缺勤
|
||
</span>
|
||
<em>仅显示当天已生成考勤的课程/时段,不再为无课时段补默认缺勤</em>
|
||
</div>
|
||
<Spin spinning={loading}>
|
||
{visibleStudents.length === 0 ? (
|
||
<Empty
|
||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||
description="未找到匹配学生,请调整筛选条件或搜索关键词"
|
||
/>
|
||
) : (
|
||
<div className="student-card-grid">
|
||
{visibleStudents.map((student) => (
|
||
<button
|
||
type="button"
|
||
key={student.key}
|
||
className={`student-attendance-card ${
|
||
selectedStudentId === student.studentId ? 'selected' : ''
|
||
}`}
|
||
onClick={() => onSelectStudent(student)}
|
||
>
|
||
<span className="student-attendance-head">
|
||
<strong>{student.studentName}</strong>
|
||
{student.studentNo && <small>学号 {student.studentNo}</small>}
|
||
</span>
|
||
<span className="student-status-blocks">
|
||
{sortAttendanceRecords(student.records).map((record) => {
|
||
const currentStatus = displayAttendanceStatus(record.status);
|
||
const meta = STATUS_META[currentStatus] ?? STATUS_META.absent;
|
||
const sessionLabel = sessionMap[record.session] || record.session;
|
||
return (
|
||
<span
|
||
key={record.id}
|
||
className={`student-status-block ${meta.className}`}
|
||
title={`${sessionLabel}:${meta.label}`}
|
||
>
|
||
{meta.label === '出勤' ? '正常' : meta.label}
|
||
</span>
|
||
);
|
||
})}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</Spin>
|
||
</section>
|
||
|
||
<Card className="student-record-card" bordered={false} title="原始考勤明细">
|
||
<Table<AttendanceRecordItem>
|
||
rowKey="id"
|
||
columns={columns}
|
||
dataSource={records}
|
||
loading={loading}
|
||
scroll={{ x: 'max-content' }}
|
||
pagination={{
|
||
current: page,
|
||
pageSize,
|
||
total,
|
||
showSizeChanger: true,
|
||
showTotal: (value) => `共 ${value} 条历史记录`,
|
||
onChange: onPageChange,
|
||
}}
|
||
locale={{
|
||
emptyText: (
|
||
<Empty
|
||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||
description="当前条件下没有历史考勤记录"
|
||
/>
|
||
),
|
||
}}
|
||
/>
|
||
</Card>
|
||
</>
|
||
);
|
||
};
|