// aislop-ignore-file: duplicate-block -- 表格/表单声明结构相似且参数不同,渲染逻辑已共享组件化 import React, { useState, useMemo, useCallback } from 'react'; import { useQuery } from '@tanstack/react-query'; import { validateResponse } from '../../utils/validate'; import { roomsOverviewSchema } from '../../api/schemas'; import { App, Form, } from 'antd'; import type { UploadRequestOption } from '@rc-component/upload/lib/interface'; import api from '../../api'; import { downloadBlob } from '../../utils/download'; import { message } from '../../ui/app-message'; import { usePermission } from '../../hooks/usePermission'; import { useVisibleRefetch } from '../../hooks/usePageVisible'; import { selectArchiveRecords } from '../archive-view'; import { getErrorMessage } from '../../utils/error'; import { QueryErrorState } from '../../components/QueryState'; import { useRoomColumns, type BedItem, type LockerItem } from './RoomColumns'; import { RoomDetailArea } from './RoomModals'; import { RoomsToolbar } from './RoomsToolbar'; import { RoomsTable } from './RoomsTable'; import { useRoomItemMutations, useRoomMutations } from './useRoomMutations'; const RoomsPage: React.FC = () => { const { modal } = App.useApp(); const { hasPermission, permissionsReady } = usePermission(); const canEditRooms = permissionsReady && hasPermission('room:edit'); const canCreateRooms = permissionsReady && hasPermission('room:create'); const canDeleteRooms = permissionsReady && hasPermission('room:delete'); const canPurgeRooms = permissionsReady && hasPermission('room:purge'); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const canSaveRoom = editing ? canEditRooms : canCreateRooms; const [showArchived, setShowArchived] = useState(false); const [searchText, setSearchText] = useState(''); const [filterBuilding, setFilterBuilding] = useState(undefined); const [filterStatus, setFilterStatus] = useState(undefined); const [filterRentalCategory, setFilterRentalCategory] = useState(undefined); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [saving, setSaving] = useState(false); const [form] = Form.useForm(); const [drawerOpen, setDrawerOpen] = useState(false); const [drawerRoom, setDrawerRoom] = useState(null); const [beds, setBeds] = useState([]); const [lockers, setLockers] = useState([]); const [bedModalOpen, setBedModalOpen] = useState(false); const [bedEditing, setBedEditing] = useState(null); const [lockerModalOpen, setLockerModalOpen] = useState(false); const [lockerEditing, setLockerEditing] = useState(null); const [bedForm] = Form.useForm(); const [lockerForm] = Form.useForm(); const [savingBed, setSavingBed] = useState(false); const [savingLocker, setSavingLocker] = useState(false); const [batchLoading, setBatchLoading] = useState(false); const [bedsError, setBedsError] = useState(false); const [lockersError, setLockersError] = useState(false); const { data = [], isLoading, isFetching, isError, refetch, } = useQuery({ queryKey: ['rooms', 'overview', showArchived], queryFn: async () => { const params: any = { includeArchived: showArchived ? 'true' : undefined }; const res: any = await api.get('/rooms/overview', { params }); return selectArchiveRecords( validateResponse(roomsOverviewSchema, res), showArchived ? 'archived' : 'active', ); }, }); const loading = isLoading || isFetching; // RouteKeeper 保活页面切回时刷新列表,避免看到陈旧数据 useVisibleRefetch(['rooms']); const { saveMutation, saveRoomCellMutation, archiveMutation, restoreMutation, purgeMutation, batchDeleteMutation, batchRestoreMutation, batchPurgeMutation, importMutation, } = useRoomMutations(editing); const { saveBedCellMutation, saveLockerCellMutation, saveBedMutation, deleteBedMutation, batchBedsMutation, saveLockerMutation, deleteLockerMutation, batchLockersMutation, } = useRoomItemMutations(); const handleBatchDelete = useCallback(async () => { if (batchLoading) return; setBatchLoading(true); try { const res: any = await batchDeleteMutation.mutateAsync(selectedRowKeys); message.success(res?.message || `已批量归档 ${selectedRowKeys.length} 间`); setSelectedRowKeys([]); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setBatchLoading(false); } }, [batchLoading, batchDeleteMutation, selectedRowKeys]); const handleBatchRestore = useCallback(async () => { if (batchLoading) return; setBatchLoading(true); try { const res = await batchRestoreMutation.mutateAsync(selectedRowKeys); message.success( `已批量恢复 ${res.restored} 间${res.skipped ? `,跳过 ${res.skipped} 间` : ''}`, ); setSelectedRowKeys([]); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setBatchLoading(false); } }, [batchLoading, batchRestoreMutation, selectedRowKeys]); const buildings = useMemo(() => { const set = new Set(data.flatMap((r: any) => (r.building ? [r.building] : []))); return [...set].sort(); }, [data]); const filteredData = useMemo(() => { let result = data; if (searchText) { const keyword = searchText.toLowerCase(); result = result.filter( (r: Record) => typeof r.roomNumber === 'string' && r.roomNumber.toLowerCase().includes(keyword), ); } if (filterBuilding) result = result.filter((r: Record) => r.building === filterBuilding); if (filterStatus) result = result.filter((r: Record) => r.status === filterStatus); if (filterRentalCategory) { result = result.filter( (r: Record) => r.rentalCategory === filterRentalCategory, ); } return result; }, [data, searchText, filterBuilding, filterStatus, filterRentalCategory]); const remainingBedSlots = useMemo(() => { const capacity = Number(drawerRoom?.capacity) || 0; return Math.max(capacity - beds.length, 0); }, [drawerRoom?.capacity, beds.length]); const defaultBatchBedCount = Math.min(4, Math.max(remainingBedSlots, 1)); const handleSave = async () => { const values = await form.validateFields(); setSaving(true); try { await saveMutation.mutateAsync(values); message.success( editing ? '更新成功' : `创建成功,已自动生成 ${values.capacity} 张床位`, ); setModalOpen(false); form.resetFields(); setEditing(null); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setSaving(false); } }; const saveRoomCell = useCallback( async (record: any, field: string, value: unknown) => { try { await saveRoomCellMutation.mutateAsync({ record, field, value }); message.success('已保存'); } catch { // 错误提示由 useApiMutation 统一处理 } }, [saveRoomCellMutation], ); const saveBedCell = async (record: BedItem, field: string, value: unknown) => { if (!drawerRoom) return; try { await saveBedCellMutation.mutateAsync({ roomId: drawerRoom.id, bedId: record.id, field, value, }); message.success('已保存'); await fetchBeds(drawerRoom.id); } catch { // 错误提示由 useApiMutation 统一处理 } }; const saveLockerCell = async (record: LockerItem, field: string, value: unknown) => { if (!drawerRoom) return; try { await saveLockerCellMutation.mutateAsync({ roomId: drawerRoom.id, lockerId: record.id, field, value, }); message.success('已保存'); await fetchLockers(drawerRoom.id); } catch { // 错误提示由 useApiMutation 统一处理 } }; const fetchBeds = useCallback(async (roomId: number) => { try { const res = await api.get(`/rooms/${roomId}/beds`); setBeds(res); setBedsError(false); } catch (e: unknown) { setBedsError(true); message.error(getErrorMessage(e, '加载床位失败')); } }, []); const fetchLockers = useCallback(async (roomId: number) => { try { const res = await api.get(`/rooms/${roomId}/lockers`); setLockers(res); setLockersError(false); } catch (e: unknown) { setLockersError(true); message.error(getErrorMessage(e, '加载柜子失败')); } }, []); const handleSaveBed = async () => { if (!drawerRoom) return; const values = await bedForm.validateFields(); setSavingBed(true); try { await saveBedMutation.mutateAsync({ roomId: drawerRoom.id, bedId: bedEditing?.id, values, }); message.success(bedEditing ? '更新成功' : '添加成功'); setBedModalOpen(false); bedForm.resetFields(); setBedEditing(null); await fetchBeds(drawerRoom.id); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setSavingBed(false); } }; const handleDeleteBed = async (id: number) => { if (!drawerRoom) return; try { await deleteBedMutation.mutateAsync({ roomId: drawerRoom.id, bedId: id }); message.success('已归档'); await fetchBeds(drawerRoom.id); } catch { // 错误提示由 useApiMutation 统一处理 } }; const handleBatchBeds = async (count: number) => { if (!drawerRoom) return; try { await batchBedsMutation.mutateAsync({ roomId: drawerRoom.id, count }); message.success(`已生成 ${count} 张床位`); await fetchBeds(drawerRoom.id); } catch { // 错误提示由 useApiMutation 统一处理 } }; const handleSaveLocker = async () => { if (!drawerRoom) return; const values = await lockerForm.validateFields(); setSavingLocker(true); try { await saveLockerMutation.mutateAsync({ roomId: drawerRoom.id, lockerId: lockerEditing?.id, values, }); message.success(lockerEditing ? '更新成功' : '添加成功'); setLockerModalOpen(false); lockerForm.resetFields(); setLockerEditing(null); await fetchLockers(drawerRoom.id); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setSavingLocker(false); } }; const handleDeleteLocker = async (id: number) => { if (!drawerRoom) return; try { await deleteLockerMutation.mutateAsync({ roomId: drawerRoom.id, lockerId: id }); message.success('已归档'); await fetchLockers(drawerRoom.id); } catch { // 错误提示由 useApiMutation 统一处理 } }; const handleBatchLockers = async (count: number) => { if (!drawerRoom) return; try { await batchLockersMutation.mutateAsync({ roomId: drawerRoom.id, count }); message.success(`已生成 ${count} 个柜子`); await fetchLockers(drawerRoom.id); } catch { // 错误提示由 useApiMutation 统一处理 } }; const handleArchive = useCallback( async (id: number) => { try { await archiveMutation.mutateAsync(id); message.success('已归档'); } catch { // 错误提示由 useApiMutation 统一处理 } }, [archiveMutation], ); const handleRestore = useCallback( async (id: number) => { try { await restoreMutation.mutateAsync(id); message.success('已恢复'); } catch { // 错误提示由 useApiMutation 统一处理 } }, [restoreMutation], ); const handlePurge = useCallback( (id: number, roomNumber: string) => { modal.confirm({ title: `永久删除宿舍「${roomNumber}」?`, content: '删除后不可恢复,该宿舍及其床位、柜子、查寝记录将无法找回。确定继续?', okText: '永久删除', okButtonProps: { danger: true }, cancelText: '取消', onOk: async () => { try { await purgeMutation.mutateAsync(id); message.success('已永久删除(不可恢复)'); } catch { // 错误提示由 useApiMutation 统一处理 } }, }); }, [modal, purgeMutation], ); const handleBatchPurge = async () => { if (batchLoading) return; setBatchLoading(true); try { const res: any = await batchPurgeMutation.mutateAsync(selectedRowKeys); message.success(res?.message || `已永久删除 ${selectedRowKeys.length} 间宿舍`); setSelectedRowKeys([]); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setBatchLoading(false); } }; const handleDownloadTemplate = () => { downloadBlob('/rooms/template', '房间导入模板.xlsx').catch(() => message.error('下载失败')); }; const handleExport = () => { const params = showArchived ? '?includeArchived=true' : ''; downloadBlob('/rooms/export' + params, '房间列表.xlsx').catch(() => message.error('导出失败'), ); }; const columns = useRoomColumns({ canEditRooms, canDeleteRooms, canPurgeRooms, onSaveRoomCell: saveRoomCell, onRestore: handleRestore, onArchive: handleArchive, onPurge: handlePurge, onView: (record) => { setDrawerRoom(record); setDrawerOpen(true); void Promise.all([fetchBeds(record.id), fetchLockers(record.id)]); }, onEdit: (record) => { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }, }); return (
{ setShowArchived(!showArchived); setFilterStatus(undefined); setSelectedRowKeys([]); }} selectedRowKeys={selectedRowKeys} batchLoading={batchLoading} canEditRooms={canEditRooms} canPurgeRooms={canPurgeRooms} canDeleteRooms={canDeleteRooms} hasCreatePermission={hasPermission('room:create')} onBatchRestore={handleBatchRestore} onBatchPurge={handleBatchPurge} onBatchDelete={handleBatchDelete} onAddRoom={() => { setEditing(null); form.resetFields(); setModalOpen(true); }} onImport={async (options: UploadRequestOption<{ message?: string }>) => { const { file, onSuccess, onError } = options; if (typeof file === 'string') { message.error('不支持字符串文件'); return; } try { const formData = new FormData(); formData.append('file', file); const res = await importMutation.mutateAsync(formData); message.success(res.message || '导入成功'); onSuccess?.(res); } catch (e) { onError?.(e as Error); } }} onDownloadTemplate={handleDownloadTemplate} onExport={handleExport} /> {isError ? ( void refetch()} /> ) : ( )} { setModalOpen(false); setEditing(null); }} drawerOpen={drawerOpen} drawerRoom={drawerRoom} beds={beds} lockers={lockers} bedsError={bedsError} lockersError={lockersError} onRetryBeds={() => { if (drawerRoom) void fetchBeds(drawerRoom.id); }} onRetryLockers={() => { if (drawerRoom) void fetchLockers(drawerRoom.id); }} canEditRooms={canEditRooms} remainingBedSlots={remainingBedSlots} defaultBatchBedCount={defaultBatchBedCount} onCloseDrawer={() => { setDrawerOpen(false); setDrawerRoom(null); }} onAddBed={() => { setBedEditing(null); bedForm.resetFields(); setBedModalOpen(true); }} onBatchBeds={handleBatchBeds} onEditBed={(r) => { setBedEditing(r); bedForm.setFieldsValue(r); setBedModalOpen(true); }} onDeleteBed={handleDeleteBed} onSaveBedCell={saveBedCell} onAddLocker={() => { setLockerEditing(null); lockerForm.resetFields(); setLockerModalOpen(true); }} onBatchLockers={handleBatchLockers} onEditLocker={(r) => { setLockerEditing(r); lockerForm.setFieldsValue(r); setLockerModalOpen(true); }} onDeleteLocker={handleDeleteLocker} onSaveLockerCell={saveLockerCell} bedModalOpen={bedModalOpen} bedEditing={!!bedEditing} savingBed={savingBed} bedForm={bedForm} onSaveBed={handleSaveBed} onCloseBedModal={() => { setBedModalOpen(false); setBedEditing(null); }} lockerModalOpen={lockerModalOpen} lockerEditing={!!lockerEditing} savingLocker={savingLocker} lockerForm={lockerForm} onSaveLocker={handleSaveLocker} onCloseLockerModal={() => { setLockerModalOpen(false); setLockerEditing(null); }} />
); }; export default RoomsPage;