import React, { useEffect, useState, useMemo } from 'react'; import { Table, Button, Modal, Form, Input, InputNumber, Select, Space, message, Tag, Popconfirm, Badge, Upload, } from 'antd'; import type { UploadRequestError, UploadRequestOption } from '@rc-component/upload/lib/interface'; import { PlusOutlined, UploadOutlined, DownloadOutlined, UndoOutlined, InboxOutlined, SearchOutlined, ExportOutlined, DeleteOutlined, } from '@ant-design/icons'; import api from '../../api'; import { downloadBlob } from '../../utils/download'; import PermissionButton from '../../components/PermissionButton'; const statusMap: Record = { available: { text: '可入住', color: 'green' }, full: { text: '已满', color: 'red' }, maintenance: { text: '维修中', color: 'orange' }, archived: { text: '已归档', color: '#999' }, }; function parseRoomNumber(input: string) { const cleaned = input.replace(/[((].*?[))]/g, '').trim(); const familyMatch = cleaned.match(/^(\d+)-(\d+)-(\d+)$/); if (familyMatch) { return { building: `${familyMatch[1]}-${familyMatch[2]}栋`, floor: parseInt(familyMatch[3].charAt(0), 10) || undefined, roomType: '家庭房', capacity: 4, }; } const stdMatch = cleaned.match(/^(\d+)-(\d+)$/); if (stdMatch) { const bldgNum = stdMatch[1]; const roomPart = stdMatch[2]; const floor = parseInt(roomPart.charAt(0), 10) || undefined; let roomType = '四人间'; let capacity = 4; if (bldgNum === '2') { roomType = '单人间'; capacity = 1; } else if (bldgNum === '8') { roomType = '爆改房'; capacity = 2; } return { building: `${bldgNum}号楼`, floor, roomType, capacity }; } return null; } const RoomsPage: React.FC = () => { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [detailModal, setDetailModal] = useState(null); const [showArchived, setShowArchived] = useState(false); const [archivedCount, setArchivedCount] = useState(0); const [searchText, setSearchText] = useState(''); const [filterBuilding, setFilterBuilding] = useState(undefined); const [filterStatus, setFilterStatus] = useState(undefined); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [saving, setSaving] = useState(false); const [form] = Form.useForm(); const handleBatchDelete = async () => { try { const res: any = await api.post('/rooms/batch-delete', { ids: selectedRowKeys }); message.success(res?.message || `已批量归档 ${selectedRowKeys.length} 间`); setSelectedRowKeys([]); fetchData(); } catch (e: any) { message.error(e?.message || '批量归档失败'); } }; const fetchData = async () => { setLoading(true); try { const params: any = { includeArchived: 'true' }; const res: any = await api.get('/rooms/overview', { params }); const archived = res.filter((r: any) => r.status === 'archived'); setArchivedCount(archived.length); const filtered = showArchived ? res : res.filter((r: any) => r.status !== 'archived'); setData(filtered); } catch (e) { console.error(e); } setLoading(false); }; useEffect(() => { fetchData(); }, [showArchived]); // 获取楼栋列表用于筛选 const buildings = useMemo(() => { const set = new Set(data.map((r: any) => r.building).filter(Boolean)); 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); return result; }, [data, searchText, filterBuilding, filterStatus]); const handleSave = async () => { const values = await form.validateFields(); setSaving(true); try { if (editing) { await api.put(`/rooms/${editing.id}`, values); message.success('更新成功'); } else { await api.post('/rooms', values); message.success('创建成功'); } setModalOpen(false); form.resetFields(); setEditing(null); fetchData(); } catch (e: any) { message.error(e?.message || '操作失败'); } finally { setSaving(false); } }; const showDetail = async (id: number) => { try { const res = await api.get(`/rooms/${id}`); setDetailModal(res); } catch (e) { console.error(e); } }; const handleArchive = async (id: number) => { try { await api.delete(`/rooms/${id}`); message.success('已归档'); fetchData(); } catch (e: any) { message.error(e?.message || '归档失败'); } }; const handleRestore = async (id: number) => { try { await api.put(`/rooms/${id}/restore`); message.success('已恢复'); fetchData(); } catch (e: any) { message.error(e?.message || '恢复失败'); } }; 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 = useMemo(() => [ { title: '房间号', dataIndex: 'roomNumber', width: 100, sorter: (a: any, b: any) => a.roomNumber.localeCompare(b.roomNumber), }, { title: '楼栋', dataIndex: 'building', width: 80 }, { title: '楼层', dataIndex: 'floor', width: 80 }, { title: '类型', dataIndex: 'roomType', width: 90, render: (v: any) => v || '-' }, { title: '租赁类型', dataIndex: 'rentalCategory', width: 100, render: (v: string) => { if (v === 'long') return 长租; if (v === 'short') return 短租; return '-'; }, }, { title: '月租金', dataIndex: 'monthlyRate', width: 100, render: (v: number) => (v ? `¥${v}` : '-'), }, { title: '额定人数', dataIndex: 'capacity', width: 80 }, { title: '当前入住', width: 80, render: (_: any, r: any) => r.status === 'archived' ? ( - ) : ( = r.capacity ? '#ff4d4f' : '#52c41a' }} /> ), }, { title: '性别', dataIndex: 'gender', width: 80, render: (v: any) => (v ? {v} : '-'), }, { title: '状态', dataIndex: 'status', width: 80, render: (s: string) => {statusMap[s]?.text || s}, }, { title: '操作', width: 220, render: (_: any, record: any) => ( {record.status === 'archived' ? ( handleRestore(record.id)} okText="恢复" cancelText="取消" > } type="link"> 恢复 ) : ( <> showDetail(record.id)} > 查看住户 { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }} > 编辑 handleArchive(record.id)} okText="归档" cancelText="取消" > }> 归档 )} ), }, ], [showArchived, buildings, handleBatchDelete, handleRestore, handleArchive, showDetail]); return (

宿舍管理

} />
} disabled={selectedRowKeys.length === 0}> 批量归档 } onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }} > 添加宿舍 ) => { const { file, onSuccess, onError } = options; if (typeof file === 'string') { message.error('不支持字符串文件'); return; } try { const formData = new FormData(); formData.append('file', file); const res = await api.post<{ message?: string }>('/rooms/import', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); message.success(res.message || '导入成功'); onSuccess?.(res); fetchData(); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '导入失败'); onError?.(e as UploadRequestError); } }} > } onClick={handleDownloadTemplate} > 下载模板 } onClick={handleExport}> 导出列表
`共 ${total} 间` }} rowClassName={(record) => (record.status === 'archived' ? 'archived-row' : '')} rowSelection={{ selectedRowKeys, onChange: (keys) => setSelectedRowKeys(keys as number[]), getCheckboxProps: (record: any) => ({ disabled: record.status === 'archived' }), }} /> { setModalOpen(false); setEditing(null); }} okText="保存" confirmLoading={saving} >
{ const parsed = parseRoomNumber(e.target.value); if (parsed) form.setFieldsValue(parsed); }} /> {editing && (
r.student?.name }, { title: '入住日期', dataIndex: 'checkInDate' }, { title: '计费起始', dataIndex: 'billingStartDate' }, ]} /> ) : (
暂无住户
)} ); }; export default RoomsPage;