195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
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>
|
||
);
|
||
};
|