feat(admin): 管理端考勤明细批量纠错
- 原始考勤明细表支持行选择(跨页保留),选中后出现 「标记正常/标记请假/标记缺勤」批量操作条 - 批量提交前确认,成功后按 updated/failed 数量反馈, 失败记录(如已结算)提示原因;筛选条件变化自动清空选择 - 复用 PUT /attendance-records/batch-status 接口 aislop scan: 5 引擎 0 issues
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Card, Empty, Input, Spin, Table } from 'antd';
|
||||
import { Button, Card, Empty, Input, Spin, Table } from 'antd';
|
||||
import { ExportOutlined } from '@ant-design/icons';
|
||||
import PermissionButton from '../../components/PermissionButton';
|
||||
import {
|
||||
@@ -27,6 +27,11 @@ export const AttendanceAdminWorkspace: React.FC<{
|
||||
pageSize: number;
|
||||
total: number;
|
||||
onPageChange: (page: number, pageSize: number) => void;
|
||||
/** 明细表批量纠错 */
|
||||
selectedRecordKeys: number[];
|
||||
onSelectRecords: (keys: number[]) => void;
|
||||
onBatchCorrect: (status: string) => void;
|
||||
batchCorrecting: boolean;
|
||||
}> = ({
|
||||
metricFilter,
|
||||
studentSearch,
|
||||
@@ -44,6 +49,10 @@ export const AttendanceAdminWorkspace: React.FC<{
|
||||
pageSize,
|
||||
total,
|
||||
onPageChange,
|
||||
selectedRecordKeys,
|
||||
onSelectRecords,
|
||||
onBatchCorrect,
|
||||
batchCorrecting,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
@@ -135,12 +144,51 @@ export const AttendanceAdminWorkspace: React.FC<{
|
||||
</section>
|
||||
|
||||
<Card className="student-record-card" variant="borderless" title="原始考勤明细">
|
||||
{selectedRecordKeys.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<span>已选 {selectedRecordKeys.length} 条</span>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
loading={batchCorrecting}
|
||||
onClick={() => onBatchCorrect('present')}
|
||||
>
|
||||
标记正常
|
||||
</Button>
|
||||
<Button size="small" loading={batchCorrecting} onClick={() => onBatchCorrect('leave')}>
|
||||
标记请假
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
loading={batchCorrecting}
|
||||
onClick={() => onBatchCorrect('absent')}
|
||||
>
|
||||
标记缺勤
|
||||
</Button>
|
||||
<Button size="small" type="text" onClick={() => onSelectRecords([])}>
|
||||
清空选择
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<Table<AttendanceRecordItem>
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={records}
|
||||
loading={loading}
|
||||
scroll={{ x: 'max-content' }}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedRecordKeys,
|
||||
onChange: (keys) => onSelectRecords(keys as number[]),
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useApiMutation } from '../../hooks/useApiMutation';
|
||||
import { validateResponse } from '../../utils/validate';
|
||||
import { getErrorMessage } from '../../utils/error';
|
||||
import {
|
||||
attendanceAlertsSchema,
|
||||
attendanceClassOptionsSchema,
|
||||
@@ -57,6 +58,9 @@ export const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit
|
||||
const [studentSearch, setStudentSearch] = useState('');
|
||||
const [selectedStudent, setSelectedStudent] = useState<AdminStudentPanel | null>(null);
|
||||
const [correctingRecordId, setCorrectingRecordId] = useState<number | null>(null);
|
||||
// 明细表批量纠错:选中的记录 ID + 执行中状态
|
||||
const [selectedRecordIds, setSelectedRecordIds] = useState<number[]>([]);
|
||||
const [batchCorrecting, setBatchCorrecting] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
useVisibleRefetch(['attendance', 'records']);
|
||||
const recordQueryKey = [
|
||||
@@ -71,6 +75,11 @@ export const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit
|
||||
scheduleId,
|
||||
] as const;
|
||||
|
||||
// 筛选条件变化时清空批量选择,避免把上一批条件的记录带到新条件下提交
|
||||
useEffect(() => {
|
||||
setSelectedRecordIds([]);
|
||||
}, [classId, attendanceDate, status, session, scheduleId]);
|
||||
|
||||
const { data: classOptions = [] } = useQuery<ClassOption[]>({
|
||||
queryKey: ['attendance', 'meta', 'classes'],
|
||||
queryFn: async () =>
|
||||
@@ -400,6 +409,36 @@ export const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit
|
||||
}
|
||||
};
|
||||
|
||||
/** 批量纠错:对选中的明细记录统一标记状态 */
|
||||
const batchCorrectStatus = async (nextStatus: string) => {
|
||||
if (selectedRecordIds.length === 0) return;
|
||||
const statusLabel =
|
||||
nextStatus === 'present' ? '正常' : nextStatus === 'leave' ? '请假' : '缺勤';
|
||||
modal.confirm({
|
||||
title: `将选中的 ${selectedRecordIds.length} 条记录标记为「${statusLabel}」?`,
|
||||
content: '此操作会立即写入考勤记录;已结算的记录无法修改。',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
setBatchCorrecting(true);
|
||||
try {
|
||||
const res = await api.put<{ updated: number; failed: number; failedIds: number[] }>(
|
||||
'/attendance-records/batch-status',
|
||||
{ ids: selectedRecordIds, status: nextStatus },
|
||||
);
|
||||
setSelectedRecordIds([]);
|
||||
message.success(`已更新 ${res.updated} 条记录`);
|
||||
if (res.failed > 0) message.warning(`有 ${res.failed} 条更新失败(可能已结算)`);
|
||||
void refetchRecords();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e, '批量更新失败'));
|
||||
} finally {
|
||||
setBatchCorrecting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const saveAdminRecordCell = async (
|
||||
record: AttendanceRecordItem,
|
||||
field: 'status' | 'remark',
|
||||
@@ -544,6 +583,10 @@ export const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit
|
||||
setPage(nextPage);
|
||||
setPageSize(nextPageSize);
|
||||
}}
|
||||
selectedRecordKeys={selectedRecordIds}
|
||||
onSelectRecords={setSelectedRecordIds}
|
||||
onBatchCorrect={batchCorrectStatus}
|
||||
batchCorrecting={batchCorrecting}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user