feat: upgrade Room detail to Drawer with Bed/Locker tabs
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user