feat: 考勤模块重构与钉钉考勤同步
This commit is contained in:
194
apps/admin/src/pages/Attendance/AttendanceAdminModals.tsx
Normal file
194
apps/admin/src/pages/Attendance/AttendanceAdminModals.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user