feat: 考勤模块重构与钉钉考勤同步

This commit is contained in:
2026-08-05 17:11:23 +08:00
parent c622e40a12
commit e9c8a1085d
36 changed files with 5122 additions and 3458 deletions

View File

@@ -0,0 +1,194 @@
import React from 'react';
import {
Alert,
Avatar,
Button,
Drawer,
Form,
Input,
Modal,
Segmented,
Select,
} from 'antd';
import { UndoOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import {
ADMIN_CORRECTION_OPTIONS,
AttendanceStatusTag,
displayAttendanceStatus,
type AdminStudentPanel,
type AttendancePeriodConfigItem,
type AttendanceRecordItem,
} from './AttendanceAdmin.helpers';
export const PeriodConfigModal: React.FC<{
open: boolean;
form: ReturnType<typeof Form.useForm<{ periods: AttendancePeriodConfigItem[] }>>[0];
onOk: () => void;
onCancel: () => void;
onReset: () => void;
}> = ({ open, form, onOk, onCancel, onReset }) => {
return (
<Modal
open={open}
title="考勤时段配置"
okText="保存配置"
width={860}
className="attendance-period-modal"
onOk={onOk}
onCancel={onCancel}
footer={(_, { OkBtn, CancelBtn }) => (
<>
<Button icon={<UndoOutlined />} onClick={onReset}>
</Button>
<CancelBtn />
<OkBtn />
</>
)}
>
<Alert
type="info"
showIcon
title="按课程开始时间自动匹配时段"
description="默认07:30-08:30 早自习09:00-12:00 早课14:00-17:00 晚课18:30-21:00 晚自习。"
style={{ marginBottom: 16 }}
/>
<Form form={form} layout="vertical">
<Form.List name="periods">
{(fields, { add, remove }) => (
<div className="attendance-period-editor">
{fields.map((field) => (
<div className="attendance-period-row" key={field.key}>
<Form.Item name={[field.name, 'label']} label="显示名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="早课" />
</Form.Item>
<Form.Item name={[field.name, 'periodKey']} label="类型标识" rules={[{ required: true, message: '请输入标识' }]}>
<Input placeholder="morning" />
</Form.Item>
<Form.Item name={[field.name, 'startTime']} label="开始时间" rules={[{ required: true, message: '请选择开始时间' }]}>
<Input type="time" />
</Form.Item>
<Form.Item name={[field.name, 'endTime']} label="结束时间" rules={[{ required: true, message: '请选择结束时间' }]}>
<Input type="time" />
</Form.Item>
<Form.Item name={[field.name, 'enabled']} label="状态">
<Select
options={[
{ value: true, label: '启用' },
{ value: false, label: '停用' },
]}
/>
</Form.Item>
<Button danger className="attendance-period-row__delete" onClick={() => remove(field.name)}>
</Button>
</div>
))}
<Button
block
type="dashed"
className="attendance-period-add"
onClick={() =>
add({
periodKey: '',
label: '',
startTime: '00:00',
endTime: '00:30',
enabled: true,
})
}
>
</Button>
</div>
)}
</Form.List>
</Form>
</Modal>
);
};
export const StudentDetailDrawer: React.FC<{
student: AdminStudentPanel | null;
isMobile: boolean;
canEdit: boolean;
sessionMap: Record<string, string>;
correctingRecordId: number | null;
sortAttendanceRecords: (items: readonly AttendanceRecordItem[]) => AttendanceRecordItem[];
onClose: () => void;
onUpdateAdminRecordStatus: (record: AttendanceRecordItem, nextStatus: string) => void;
}> = ({
student,
isMobile,
canEdit,
sessionMap,
correctingRecordId,
sortAttendanceRecords,
onClose,
onUpdateAdminRecordStatus,
}) => {
return (
<Drawer
open={Boolean(student)}
onClose={onClose}
size={isMobile ? '100%' : 520}
title="学生考勤明细"
className="student-detail-drawer"
>
{student && (
<div className="student-detail-panel">
<section className="student-detail-profile">
<Avatar size={54}>{student.studentName.slice(0, 1)}</Avatar>
<div>
<h4>{student.studentName}</h4>
<p>
{student.className}
{student.studentNo ? ` · 学号 ${student.studentNo}` : ''}
</p>
</div>
<div className="student-detail-rate">
<strong>{student.rate}%</strong>
<span></span>
</div>
</section>
<section className="student-detail-rates">
{sortAttendanceRecords(student.records).map((record) => {
const normal = record.status === 'present';
return (
<div key={record.id}>
<strong>{normal ? '100%' : '0%'}</strong>
<span>{sessionMap[record.session] || record.session}</span>
</div>
);
})}
</section>
<section className="student-detail-section">
<h5></h5>
<div className="student-detail-timeline">
{sortAttendanceRecords(student.records).map((record) => (
<div key={record.id}>
<span>{sessionMap[record.session] || record.session}</span>
<AttendanceStatusTag status={displayAttendanceStatus(record.status)} />
<strong>
{record.punchTime ? dayjs(record.punchTime).format('HH:mm:ss') : '未记录'}
</strong>
{canEdit && (
<Segmented
size="small"
className="attendance-correction-segment"
options={ADMIN_CORRECTION_OPTIONS}
value={displayAttendanceStatus(record.status)}
disabled={correctingRecordId === record.id}
onChange={(value) => void onUpdateAdminRecordStatus(record, String(value))}
/>
)}
</div>
))}
</div>
</section>
</div>
)}
</Drawer>
);
};