feat: upgrade Room detail to Drawer with Bed/Locker tabs

This commit is contained in:
2026-07-09 12:08:06 +08:00
parent 454a0d24c6
commit e572f92017

View File

@@ -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<any[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<any>(null);
const [detailModal, setDetailModal] = useState<any>(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<number[]>([]);
const [saving, setSaving] = useState(false);
const [form] = Form.useForm();
const [drawerOpen, setDrawerOpen] = useState(false);
const [drawerRoom, setDrawerRoom] = useState<any>(null);
const [beds, setBeds] = useState<any[]>([]);
const [lockers, setLockers] = useState<any[]>([]);
const [bedModalOpen, setBedModalOpen] = useState(false);
const [bedEditing, setBedEditing] = useState<any>(null);
const [lockerModalOpen, setLockerModalOpen] = useState(false);
const [lockerEditing, setLockerEditing] = useState<any>(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 = () => {
<PermissionButton permission="room:edit" size="small" icon={<UndoOutlined />} type="link">
</PermissionButton>
</Popconfirm>
) : (
<>
<PermissionButton
permission="room:view"
size="small"
type="link"
onClick={() => showDetail(record.id)}
onClick={async () => {
setDrawerRoom(record);
setDrawerOpen(true);
await Promise.all([fetchBeds(record.id), fetchLockers(record.id)]);
}}
>
</PermissionButton>
<PermissionButton
permission="room:edit"
@@ -288,9 +373,7 @@ const RoomsPage: React.FC = () => {
</>
)}
</Space>
),
},
], [showArchived, buildings, handleBatchDelete, handleRestore, handleArchive, showDetail]);
], [showArchived, buildings, handleBatchDelete, handleRestore, handleArchive, fetchBeds, fetchLockers]);
return (
<div>
@@ -478,27 +561,254 @@ const RoomsPage: React.FC = () => {
</Form>
</Modal>
<Modal
title={`宿舍 ${detailModal?.roomNumber} 当前住户`}
open={!!detailModal}
onCancel={() => setDetailModal(null)}
footer={null}
width={600}
<Drawer
title={`${drawerRoom?.roomNumber} 房间详情`}
open={drawerOpen}
onClose={() => { setDrawerOpen(false); setDrawerRoom(null); }}
width={640}
destroyOnClose
>
{detailModal?.currentOccupants?.length > 0 ? (
<Table
dataSource={detailModal.currentOccupants}
rowKey="id"
pagination={false}
columns={[
{ title: '学生', render: (_: any, r: any) => r.student?.name },
{ title: '入住日期', dataIndex: 'checkInDate' },
{ title: '计费起始', dataIndex: 'billingStartDate' },
]}
/>
) : (
<div style={{ textAlign: 'center', padding: 24, color: '#999' }}></div>
)}
<Tabs
defaultActiveKey="info"
items={[
{
key: 'info',
label: '基本信息',
children: drawerRoom && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div><strong></strong>{drawerRoom.roomNumber}</div>
<div><strong></strong>{drawerRoom.building || '-'}</div>
<div><strong></strong>{drawerRoom.floor ?? '-'}</div>
<div><strong></strong>{drawerRoom.roomType || '-'}</div>
<div><strong></strong>{drawerRoom.capacity}</div>
<div><strong></strong>{drawerRoom.rentalCategory === 'long' ? '长租' : '短租'}</div>
<div><strong></strong>{drawerRoom.monthlyRate ? `¥${drawerRoom.monthlyRate}` : '-'}</div>
<div><strong></strong><Tag color={statusMap[drawerRoom.status]?.color}>{statusMap[drawerRoom.status]?.text}</Tag></div>
</div>
),
},
{
key: 'beds',
label: `床位管理 (${beds.length})`,
children: (
<div>
<div style={{ marginBottom: 12, display: 'flex', gap: 8 }}>
<Button
type="primary"
size="small"
icon={<PlusOutlined />}
disabled={drawerRoom?.status === 'archived'}
onClick={() => { setBedEditing(null); bedForm.resetFields(); setBedModalOpen(true); }}
>
</Button>
<Popconfirm
title="批量生成床位"
description={
<InputNumber min={1} max={20} defaultValue={4} id="batch-bed-count" style={{ width: 80 }} />
}
onConfirm={() => {
const input = document.getElementById('batch-bed-count') as HTMLInputElement;
handleBatchBeds(input ? parseInt(input.value) || 4 : 4);
}}
okText="生成"
disabled={drawerRoom?.status === 'archived'}
>
<Button size="small" disabled={drawerRoom?.status === 'archived'}></Button>
</Popconfirm>
</div>
<Table
dataSource={beds}
rowKey="id"
pagination={false}
size="small"
columns={[
{ title: '编号', dataIndex: 'bedNumber', width: 80 },
{
title: '状态', dataIndex: 'status', width: 80,
render: (s: string) => {
const map: Record<string, { text: string; color: string }> = {
available: { text: '空闲', color: 'green' },
occupied: { text: '占用', color: 'blue' },
maintenance: { text: '维修', color: 'orange' },
};
return <Tag color={map[s]?.color}>{map[s]?.text || s}</Tag>;
},
},
{ title: '备注', dataIndex: 'notes', render: (v: string) => v || '-' },
{
title: '操作', width: 120,
render: (_: any, r: any) => (
<Space size="small">
<PermissionButton
permission="room:edit"
size="small"
type="link"
disabled={drawerRoom?.status === 'archived'}
onClick={() => { setBedEditing(r); bedForm.setFieldsValue(r); setBedModalOpen(true); }}
>
</PermissionButton>
{r.status !== 'occupied' && (
<Popconfirm title="确定删除?" onConfirm={() => handleDeleteBed(r.id)}>
<PermissionButton
permission="room:edit"
size="small"
type="link"
danger
disabled={drawerRoom?.status === 'archived'}
>
</PermissionButton>
</Popconfirm>
)}
</Space>
),
},
]}
/>
</div>
),
},
{
key: 'lockers',
label: `柜子管理 (${lockers.length})`,
children: (
<div>
<div style={{ marginBottom: 12, display: 'flex', gap: 8 }}>
<Button
type="primary"
size="small"
icon={<PlusOutlined />}
disabled={drawerRoom?.status === 'archived'}
onClick={() => { setLockerEditing(null); lockerForm.resetFields(); setLockerModalOpen(true); }}
>
</Button>
<Popconfirm
title="批量生成柜子"
description={
<InputNumber min={1} max={20} defaultValue={4} id="batch-locker-count" style={{ width: 80 }} />
}
onConfirm={() => {
const input = document.getElementById('batch-locker-count') as HTMLInputElement;
handleBatchLockers(input ? parseInt(input.value) || 4 : 4);
}}
okText="生成"
disabled={drawerRoom?.status === 'archived'}
>
<Button size="small" disabled={drawerRoom?.status === 'archived'}></Button>
</Popconfirm>
</div>
<Table
dataSource={lockers}
rowKey="id"
pagination={false}
size="small"
columns={[
{ title: '编号', dataIndex: 'lockerNumber', width: 80 },
{
title: '状态', dataIndex: 'status', width: 80,
render: (s: string) => {
const map: Record<string, { text: string; color: string }> = {
available: { text: '空闲', color: 'green' },
occupied: { text: '占用', color: 'blue' },
maintenance: { text: '维修', color: 'orange' },
};
return <Tag color={map[s]?.color}>{map[s]?.text || s}</Tag>;
},
},
{ title: '备注', dataIndex: 'notes', render: (v: string) => v || '-' },
{
title: '操作', width: 120,
render: (_: any, r: any) => (
<Space size="small">
<PermissionButton
permission="room:edit"
size="small"
type="link"
disabled={drawerRoom?.status === 'archived'}
onClick={() => { setLockerEditing(r); lockerForm.setFieldsValue(r); setLockerModalOpen(true); }}
>
</PermissionButton>
{r.status !== 'occupied' && (
<Popconfirm title="确定删除?" onConfirm={() => handleDeleteLocker(r.id)}>
<PermissionButton
permission="room:edit"
size="small"
type="link"
danger
disabled={drawerRoom?.status === 'archived'}
>
</PermissionButton>
</Popconfirm>
)}
</Space>
),
},
]}
/>
</div>
),
},
]}
/>
</Drawer>
<Modal
title={bedEditing ? '编辑床位' : '添加床位'}
open={bedModalOpen}
onOk={handleSaveBed}
onCancel={() => { setBedModalOpen(false); setBedEditing(null); }}
confirmLoading={savingBed}
okText="保存"
>
<Form form={bedForm} layout="vertical">
<Form.Item name="bedNumber" label="床位编号" rules={[{ required: true }]}>
<Input placeholder="如1号床" />
</Form.Item>
<Form.Item name="status" label="状态">
<Select
options={[
{ value: 'available', label: '空闲' },
{ value: 'maintenance', label: '维修中' },
]}
placeholder="默认为空闲"
/>
</Form.Item>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
<Modal
title={lockerEditing ? '编辑柜子' : '添加柜子'}
open={lockerModalOpen}
onOk={handleSaveLocker}
onCancel={() => { setLockerModalOpen(false); setLockerEditing(null); }}
confirmLoading={savingLocker}
okText="保存"
>
<Form form={lockerForm} layout="vertical">
<Form.Item name="lockerNumber" label="柜子编号" rules={[{ required: true }]}>
<Input placeholder="如1号柜" />
</Form.Item>
<Form.Item name="status" label="状态">
<Select
options={[
{ value: 'available', label: '空闲' },
{ value: 'maintenance', label: '维修中' },
]}
placeholder="默认为空闲"
/>
</Form.Item>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
</div>
);