From 8b5fa98028dd38fbbe2367526a9b331aa401beb2 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Fri, 7 Aug 2026 18:03:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E7=AE=A1=E7=90=86=E7=AB=AF?= =?UTF-8?q?=E8=80=83=E5=8B=A4=E6=98=8E=E7=BB=86=E6=89=B9=E9=87=8F=E7=BA=A0?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原始考勤明细表支持行选择(跨页保留),选中后出现 「标记正常/标记请假/标记缺勤」批量操作条 - 批量提交前确认,成功后按 updated/failed 数量反馈, 失败记录(如已结算)提示原因;筛选条件变化自动清空选择 - 复用 PUT /attendance-records/batch-status 接口 aislop scan: 5 引擎 0 issues --- .../Attendance/AttendanceAdminWorkspace.tsx | 50 ++++++++++++++++++- apps/admin/src/pages/Attendance/admin.tsx | 45 ++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/apps/admin/src/pages/Attendance/AttendanceAdminWorkspace.tsx b/apps/admin/src/pages/Attendance/AttendanceAdminWorkspace.tsx index ae8c78f..4c68761 100644 --- a/apps/admin/src/pages/Attendance/AttendanceAdminWorkspace.tsx +++ b/apps/admin/src/pages/Attendance/AttendanceAdminWorkspace.tsx @@ -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<{ + {selectedRecordKeys.length > 0 ? ( +
+ 已选 {selectedRecordKeys.length} 条 + + + + +
+ ) : null} 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, diff --git a/apps/admin/src/pages/Attendance/admin.tsx b/apps/admin/src/pages/Attendance/admin.tsx index 1306fcd..610062a 100644 --- a/apps/admin/src/pages/Attendance/admin.tsx +++ b/apps/admin/src/pages/Attendance/admin.tsx @@ -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(null); const [correctingRecordId, setCorrectingRecordId] = useState(null); + // 明细表批量纠错:选中的记录 ID + 执行中状态 + const [selectedRecordIds, setSelectedRecordIds] = useState([]); + 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({ 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} /> )}