diff --git a/apps/admin/src/pages/Rooms/index.tsx b/apps/admin/src/pages/Rooms/index.tsx index 1be1186..19a833f 100644 --- a/apps/admin/src/pages/Rooms/index.tsx +++ b/apps/admin/src/pages/Rooms/index.tsx @@ -13,6 +13,8 @@ import { Popconfirm, Badge, Upload, + Drawer, + Tabs, } from 'antd'; import type { UploadRequestError, UploadRequestOption } from '@rc-component/upload/lib/interface'; import { @@ -61,12 +63,9 @@ function parseRoomNumber(input: string) { 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(''); @@ -75,6 +74,18 @@ const RoomsPage: React.FC = () => { 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 handleBatchDelete = async () => { try { @@ -146,13 +157,86 @@ const RoomsPage: React.FC = () => { } }; - const showDetail = async (id: number) => { + const fetchBeds = async (roomId: number) => { try { - const res = await api.get(`/rooms/${id}`); - setDetailModal(res); - } catch (e) { - console.error(e); - } + const res = await api.get(`/rooms/${roomId}/beds`); + setBeds(res); + } catch (e) { console.error(e); } + }; + + const fetchLockers = async (roomId: number) => { + try { + const res = await api.get(`/rooms/${roomId}/lockers`); + setLockers(res); + } catch (e) { console.error(e); } + }; + + const handleSaveBed = async () => { + const values = await bedForm.validateFields(); + setSavingBed(true); + try { + if (bedEditing) { + await api.put(`/rooms/${drawerRoom.id}/beds/${bedEditing.id}`, values); + } else { + await api.post(`/rooms/${drawerRoom.id}/beds`, values); + } + message.success(bedEditing ? '更新成功' : '添加成功'); + setBedModalOpen(false); + bedForm.resetFields(); + setBedEditing(null); + fetchBeds(drawerRoom.id); + } catch (e: any) { message.error(e?.message || '操作失败'); } + finally { setSavingBed(false); } + }; + + const handleDeleteBed = async (id: number) => { + try { + await api.delete(`/rooms/${drawerRoom.id}/beds/${id}`); + message.success('已删除'); + fetchBeds(drawerRoom.id); + } catch (e: any) { message.error(e?.message || '删除失败'); } + }; + + const handleBatchBeds = async (count: number) => { + try { + await api.post(`/rooms/${drawerRoom.id}/beds/batch`, { count }); + message.success(`已生成 ${count} 张床位`); + fetchBeds(drawerRoom.id); + } catch (e: any) { message.error(e?.message || '批量生成失败'); } + }; + + const handleSaveLocker = async () => { + const values = await lockerForm.validateFields(); + setSavingLocker(true); + try { + if (lockerEditing) { + await api.put(`/rooms/${drawerRoom.id}/lockers/${lockerEditing.id}`, values); + } else { + await api.post(`/rooms/${drawerRoom.id}/lockers`, values); + } + message.success(lockerEditing ? '更新成功' : '添加成功'); + setLockerModalOpen(false); + lockerForm.resetFields(); + setLockerEditing(null); + fetchLockers(drawerRoom.id); + } catch (e: any) { message.error(e?.message || '操作失败'); } + finally { setSavingLocker(false); } + }; + + const handleDeleteLocker = async (id: number) => { + try { + await api.delete(`/rooms/${drawerRoom.id}/lockers/${id}`); + message.success('已删除'); + fetchLockers(drawerRoom.id); + } catch (e: any) { message.error(e?.message || '删除失败'); } + }; + + const handleBatchLockers = async (count: number) => { + try { + await api.post(`/rooms/${drawerRoom.id}/lockers/batch`, { count }); + message.success(`已生成 ${count} 个柜子`); + fetchLockers(drawerRoom.id); + } catch (e: any) { message.error(e?.message || '批量生成失败'); } }; const handleArchive = async (id: number) => { @@ -253,16 +337,17 @@ const RoomsPage: React.FC = () => { } type="link"> 恢复 - - ) : ( - <> showDetail(record.id)} + onClick={async () => { + setDrawerRoom(record); + setDrawerOpen(true); + await Promise.all([fetchBeds(record.id), fetchLockers(record.id)]); + }} > - 查看住户 + 查看 { )} - ), - }, - ], [showArchived, buildings, handleBatchDelete, handleRestore, handleArchive, showDetail]); + ], [showArchived, buildings, handleBatchDelete, handleRestore, handleArchive, fetchBeds, fetchLockers]); return (
@@ -478,27 +561,254 @@ const RoomsPage: React.FC = () => { - setDetailModal(null)} - footer={null} - width={600} + { setDrawerOpen(false); setDrawerRoom(null); }} + width={640} + destroyOnClose > - {detailModal?.currentOccupants?.length > 0 ? ( - r.student?.name }, - { title: '入住日期', dataIndex: 'checkInDate' }, - { title: '计费起始', dataIndex: 'billingStartDate' }, - ]} - /> - ) : ( -
暂无住户
- )} + +
房间号:{drawerRoom.roomNumber}
+
楼栋:{drawerRoom.building || '-'}
+
楼层:{drawerRoom.floor ?? '-'}
+
类型:{drawerRoom.roomType || '-'}
+
额定人数:{drawerRoom.capacity}
+
租赁类别:{drawerRoom.rentalCategory === 'long' ? '长租' : '短租'}
+
月租金:{drawerRoom.monthlyRate ? `¥${drawerRoom.monthlyRate}` : '-'}
+
状态:{statusMap[drawerRoom.status]?.text}
+ + ), + }, + { + key: 'beds', + label: `床位管理 (${beds.length})`, + children: ( +
+
+ + + } + onConfirm={() => { + const input = document.getElementById('batch-bed-count') as HTMLInputElement; + handleBatchBeds(input ? parseInt(input.value) || 4 : 4); + }} + okText="生成" + disabled={drawerRoom?.status === 'archived'} + > + + +
+
{ + const map: Record = { + available: { text: '空闲', color: 'green' }, + occupied: { text: '占用', color: 'blue' }, + maintenance: { text: '维修', color: 'orange' }, + }; + return {map[s]?.text || s}; + }, + }, + { title: '备注', dataIndex: 'notes', render: (v: string) => v || '-' }, + { + title: '操作', width: 120, + render: (_: any, r: any) => ( + + { setBedEditing(r); bedForm.setFieldsValue(r); setBedModalOpen(true); }} + > + 编辑 + + {r.status !== 'occupied' && ( + handleDeleteBed(r.id)}> + + 删除 + + + )} + + ), + }, + ]} + /> + + ), + }, + { + key: 'lockers', + label: `柜子管理 (${lockers.length})`, + children: ( +
+
+ + + } + onConfirm={() => { + const input = document.getElementById('batch-locker-count') as HTMLInputElement; + handleBatchLockers(input ? parseInt(input.value) || 4 : 4); + }} + okText="生成" + disabled={drawerRoom?.status === 'archived'} + > + + +
+
{ + const map: Record = { + available: { text: '空闲', color: 'green' }, + occupied: { text: '占用', color: 'blue' }, + maintenance: { text: '维修', color: 'orange' }, + }; + return {map[s]?.text || s}; + }, + }, + { title: '备注', dataIndex: 'notes', render: (v: string) => v || '-' }, + { + title: '操作', width: 120, + render: (_: any, r: any) => ( + + { setLockerEditing(r); lockerForm.setFieldsValue(r); setLockerModalOpen(true); }} + > + 编辑 + + {r.status !== 'occupied' && ( + handleDeleteLocker(r.id)}> + + 删除 + + + )} + + ), + }, + ]} + /> + + ), + }, + ]} + /> + + + { setBedModalOpen(false); setBedEditing(null); }} + confirmLoading={savingBed} + okText="保存" + > +
+ + + + + + + +