feat: 为批量归档补充批量恢复

This commit is contained in:
2026-07-25 09:34:03 +08:00
parent 1da763109b
commit 0ef86e65ce
19 changed files with 1365 additions and 317 deletions

View File

@@ -39,6 +39,7 @@ import JinshujuMatchModal from '../../components/JinshujuMatchModal';
import { maskIdNumber, maskPhone } from '../../utils/sensitive';
import { message } from '../../ui/app-message';
import { usePermission } from '../../hooks/usePermission';
import { selectArchiveRecords } from '../archive-view';
const statusMap: Record<string, { text: string; color: string }> = {
active: { text: '在读', color: 'green' },
@@ -111,7 +112,6 @@ const StudentsPage: React.FC = () => {
const [classOptions, setClassOptions] = useState<StudentFilterLookups['classes']>([]);
const [teacherOptions, setTeacherOptions] = useState<StudentFilterLookups['teachers']>([]);
const [showArchived, setShowArchived] = useState(false);
const [archivedCount, setArchivedCount] = useState(0);
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
const [batchLoading, setBatchLoading] = useState(false);
const [enrollmentData, setEnrollmentData] = useState<Record<number, EnrollmentInfo[]>>({});
@@ -185,6 +185,7 @@ const StudentsPage: React.FC = () => {
};
const handleBatchDelete = async () => {
if (batchLoading) return;
setBatchLoading(true);
try {
const res: any = await api.post('/students/batch-delete', { ids: selectedRowKeys });
@@ -198,22 +199,41 @@ const StudentsPage: React.FC = () => {
}
};
const handleBatchRestore = async () => {
if (batchLoading) return;
setBatchLoading(true);
try {
const res = await api.put<{ message?: string; restored: number; skipped: number }>(
'/students/batch-restore',
{ ids: selectedRowKeys },
);
message.success(
`已批量恢复 ${res.restored}${res.skipped ? `,跳过 ${res.skipped}` : ''}`,
);
setSelectedRowKeys([]);
fetchData();
} catch (e: any) {
message.error(e?.message || '批量恢复失败');
} finally {
setBatchLoading(false);
}
};
const fetchData = useCallback(async () => {
setLoading(true);
try {
const params: Record<string, unknown> = {
name: searchName || undefined,
includeArchived: 'true',
includeArchived: showArchived ? 'true' : undefined,
};
if (filterStatus) params.status = filterStatus;
if (showArchived) params.status = 'archived';
else if (filterStatus) params.status = filterStatus;
if (filterOrganizationId) params.organizationId = filterOrganizationId;
if (filterClassId) params.classId = filterClassId;
if (filterTeacherId) params.teacherId = filterTeacherId;
const res = (await api.get('/students', { params })) as Array<Record<string, unknown>>;
const list = res as Array<Record<string, unknown>>;
const archived = list.filter((r) => r.status === 'archived');
setArchivedCount(archived.length);
setData(showArchived ? list : list.filter((r) => r.status !== 'archived'));
setData(selectArchiveRecords(list, showArchived ? 'archived' : 'active'));
} catch (e: unknown) {
const err = e as { message?: string };
message.error(err?.message || '加载失败,请稍后重试');
@@ -702,14 +722,11 @@ const StudentsPage: React.FC = () => {
okText="归档"
cancelText="取消"
>
<Button
size="small"
icon={<InboxOutlined />}
>
</Button>
</Popconfirm>
) : null}
<Button size="small" icon={<InboxOutlined />}>
</Button>
</Popconfirm>
) : null}
</>
)}
</Space>
@@ -803,15 +820,34 @@ const StudentsPage: React.FC = () => {
/>
<Button
type={showArchived ? 'primary' : 'default'}
onClick={() => setShowArchived(!showArchived)}
onClick={() => {
setShowArchived(!showArchived);
setFilterStatus(undefined);
setSelectedRowKeys([]);
}}
>
{showArchived
? '隐藏已归档'
: `显示已归档${archivedCount > 0 ? ` (${archivedCount})` : ''}`}
{showArchived ? '返回正常数据' : '查看已归档'}
</Button>
</Space>
<Space wrap className="responsive-toolbar__group">
{canDeleteStudent ? (
{showArchived && canEditStudent ? (
<Popconfirm
title={`确定批量恢复选中的 ${selectedRowKeys.length} 名学生?`}
onConfirm={handleBatchRestore}
okText="恢复"
cancelText="取消"
disabled={selectedRowKeys.length === 0}
>
<Button
type="primary"
icon={<UndoOutlined />}
disabled={selectedRowKeys.length === 0}
loading={batchLoading}
>
</Button>
</Popconfirm>
) : !showArchived && canDeleteStudent ? (
<Popconfirm
title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`}
onConfirm={handleBatchDelete}
@@ -829,21 +865,23 @@ const StudentsPage: React.FC = () => {
</Button>
</Popconfirm>
) : null}
<PermissionButton
permission="student:create"
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditing(null);
form.resetFields();
const host = organizations.find((organization) => organization.isHost);
if (host) form.setFieldValue('organizationId', host.id);
setModalOpen(true);
}}
>
</PermissionButton>
{hasPermission('student:import') ? (
{!showArchived ? (
<PermissionButton
permission="student:create"
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditing(null);
form.resetFields();
const host = organizations.find((organization) => organization.isHost);
if (host) form.setFieldValue('organizationId', host.id);
setModalOpen(true);
}}
>
</PermissionButton>
) : null}
{!showArchived && hasPermission('student:import') ? (
<>
<Upload
accept=".xlsx,.xls"
@@ -861,7 +899,7 @@ const StudentsPage: React.FC = () => {
</Upload>
</>
) : null}
{canSyncJinshuju ? (
{!showArchived && canSyncJinshuju ? (
<Button icon={<CloudUploadOutlined />} onClick={() => setJinshujuOpen(true)}>
</Button>
@@ -910,7 +948,6 @@ const StudentsPage: React.FC = () => {
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys as number[]),
getCheckboxProps: (record: any) => ({ disabled: record.status === 'archived' }),
}}
expandable={{
rowExpandable: () => true,