import React, { useEffect, useState, useMemo } from 'react'; import { Table, Button, Modal, Form, Select, DatePicker, Input, InputNumber, Space, message, Tag, Popconfirm, Upload, Switch, Alert, Tooltip, } from 'antd'; import { PlusOutlined, SwapOutlined, LogoutOutlined, DeleteOutlined, UploadOutlined, DownloadOutlined, ExportOutlined, } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; const OccupanciesPage: React.FC = () => { const [data, setData] = useState([]); const [students, setStudents] = useState([]); const [rooms, setRooms] = useState([]); const [loading, setLoading] = useState(false); const [checkInModal, setCheckInModal] = useState(false); const [checkOutModal, setCheckOutModal] = useState(null); const [transferModal, setTransferModal] = useState(null); const [showActive, setShowActive] = useState(true); const [autoDeposit, setAutoDeposit] = useState(true); const [depositAmount, setDepositAmount] = useState(500); const [searchText, setSearchText] = useState(''); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchCheckOutModal, setBatchCheckOutModal] = useState(false); const [checkInForm] = Form.useForm(); const [checkOutForm] = Form.useForm(); const [transferForm] = Form.useForm(); const [batchCheckOutForm] = Form.useForm(); const fetchData = async () => { setLoading(true); try { const [occ, stu, rm]: any[] = await Promise.all([ api.get('/occupancies', { params: { active: showActive ? 'true' : undefined } }), api.get('/students'), api.get('/rooms/overview'), ]); setData(occ); setStudents(stu); setRooms(rm); } catch (e) { console.error(e); } setLoading(false); }; useEffect(() => { fetchData(); setSelectedRowKeys([]); }, [showActive]); const filteredData = useMemo(() => { if (!searchText) return data; const keyword = searchText.toLowerCase(); return data.filter( (r: any) => r.student?.name?.toLowerCase().includes(keyword) || r.room?.roomNumber?.toLowerCase().includes(keyword), ); }, [data, searchText]); const handleCheckIn = async () => { const values = await checkInForm.validateFields(); try { await api.post('/occupancies/check-in', { studentId: values.studentId, roomId: values.roomId, checkInDate: values.checkInDate.format('YYYY-MM-DD'), billingStartDate: values.billingStartDate?.format('YYYY-MM-DD'), notes: values.notes, }); message.success('入住登记成功'); setCheckInModal(false); checkInForm.resetFields(); fetchData(); } catch (e: any) { message.error(e?.message || '操作失败'); } }; const handleCheckOut = async () => { const values = await checkOutForm.validateFields(); try { await api.put(`/occupancies/${checkOutModal.id}/check-out`, { checkOutDate: values.checkOutDate.format('YYYY-MM-DD'), billingEndDate: values.billingEndDate?.format('YYYY-MM-DD'), checkOutReason: values.checkOutReason, }); message.success('退宿成功'); setCheckOutModal(null); checkOutForm.resetFields(); fetchData(); } catch (e: any) { message.error(e?.message || '操作失败'); } }; const handleTransfer = async () => { const values = await transferForm.validateFields(); try { await api.put(`/occupancies/${transferModal.id}/transfer`, { newRoomId: values.newRoomId, transferDate: values.transferDate.format('YYYY-MM-DD'), oldBillingEndDate: values.oldBillingEndDate?.format('YYYY-MM-DD'), newBillingStartDate: values.newBillingStartDate?.format('YYYY-MM-DD'), reason: values.reason, }); message.success('换房成功'); setTransferModal(null); transferForm.resetFields(); fetchData(); } catch (e: any) { message.error(e?.message || '操作失败'); } }; const handleBatchCheckOut = async () => { const values = await batchCheckOutForm.validateFields(); try { const res: any = await api.post('/occupancies/batch-check-out', { ids: selectedRowKeys, checkOutDate: values.checkOutDate.format('YYYY-MM-DD'), billingEndDate: values.billingEndDate?.format('YYYY-MM-DD'), checkOutReason: values.checkOutReason, }); message.success(res.message || `已成功退宿 ${res.success} 人`); setBatchCheckOutModal(false); batchCheckOutForm.resetFields(); setSelectedRowKeys([]); fetchData(); } catch (e: any) { message.error(e?.message || '批量退宿失败'); } }; const handleBatchDelete = async () => { try { const res: any = await api.post('/occupancies/batch-delete', { ids: selectedRowKeys }); message.success(res?.message || `已删除 ${selectedRowKeys.length} 条`); setSelectedRowKeys([]); fetchData(); } catch (e: any) { message.error(e?.message || '批量删除失败'); } }; const columns = [ { title: '学生', render: (_: any, r: any) => r.student?.name || '-' }, { title: '宿舍', render: (_: any, r: any) => r.room?.roomNumber || '-' }, { title: '入住日期', dataIndex: 'checkInDate' }, { title: '计费起始', dataIndex: 'billingStartDate' }, { title: '退宿日期', dataIndex: 'checkOutDate', render: (v: any) => v || 在住, }, { title: '计费截止', dataIndex: 'billingEndDate', render: (v: any) => v || '-' }, { title: '退宿原因', dataIndex: 'checkOutReason', render: (v: any) => v || '-' }, { title: '操作', width: 200, render: (_: any, record: any) => !record.checkOutDate ? ( } onClick={() => { setCheckOutModal(record); checkOutForm.setFieldsValue({ checkOutDate: dayjs() }); }} > 退宿 } onClick={() => { setTransferModal(record); transferForm.setFieldsValue({ transferDate: dayjs() }); }} > 换房 ) : ( 已退宿 { try { await api.delete(`/occupancies/${record.id}`); message.success('删除成功'); fetchData(); } catch (e: any) { message.error(e?.message || '删除失败'); } }} > } onClick={() => { checkInForm.resetFields(); checkInForm.setFieldsValue({ checkInDate: dayjs() }); setCheckInModal(true); }} > 入住登记 { const formData = new FormData(); formData.append('file', file); const params = new URLSearchParams(); if (autoDeposit) { params.set('autoDeposit', 'true'); params.set('depositAmount', String(depositAmount)); } try { const res: any = await api.post( `/occupancies/import?${params.toString()}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }, ); if (res.errors?.length > 0) { Modal.warning({ title: res.message, content: res.errors.join('\n'), width: 500, }); } else { message.success(res.message); } onSuccess?.(res); fetchData(); } catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); } }} > } onClick={() => { const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); fetch(`${baseURL}/occupancies/template`, { headers: { Authorization: `Bearer ${token}` }, }) .then((res) => res.blob()) .then((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = '入住名单导入模板.xlsx'; a.click(); URL.revokeObjectURL(url); }) .catch(() => message.error('下载失败')); }} > 下载模板 } onClick={() => { const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); const params = showActive ? '?active=true' : ''; fetch(`${baseURL}/occupancies/export${params}`, { headers: { Authorization: `Bearer ${token}` }, }) .then((res) => res.blob()) .then((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = showActive ? '在住记录.xlsx' : '全部入住记录.xlsx'; a.click(); URL.revokeObjectURL(url); }) .catch(() => message.error('导出失败')); }} > 导出记录 导入时自动收押金 {autoDeposit && ( setDepositAmount(v || 500)} style={{ width: 80 }} addonAfter="元" /> )} {selectedRowKeys.length > 0 && ( 已选 {selectedRowKeys.length} 条记录 {showActive ? ( } onClick={() => { batchCheckOutForm.resetFields(); batchCheckOutForm.setFieldsValue({ checkOutDate: dayjs() }); setBatchCheckOutModal(true); }} style={{ marginLeft: 12 }} > 批量退宿 ) : ( )} } type="info" style={{ marginBottom: 12 }} /> )} `共 ${total} 条` }} rowSelection={rowSelection} /> {/* 入住登记弹窗 */} setCheckInModal(false)} okText="确认入住" width={500} >
({ value: r.id, label: `${r.roomNumber} (${r.building || ''}) [${r.currentCount}/${r.capacity}]`, disabled: r.currentCount >= r.capacity, }))} />
{/* 退宿弹窗 */} setCheckOutModal(null)} okText="确认退宿" >
即将退宿的学生:
{data .filter((r: any) => selectedRowKeys.includes(r.id)) .map((r: any) => ( {r.student?.name} ({r.room?.roomNumber}) ))}
{/* 换房弹窗 */} setTransferModal(null)} okText="确认换房" width={500} >
); }; export default OccupanciesPage;