forked from wangziqi/gongxue-base
feat: upgrade Room detail to Drawer with Bed/Locker tabs
This commit is contained in:
@@ -13,6 +13,8 @@ import {
|
|||||||
Popconfirm,
|
Popconfirm,
|
||||||
Badge,
|
Badge,
|
||||||
Upload,
|
Upload,
|
||||||
|
Drawer,
|
||||||
|
Tabs,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { UploadRequestError, UploadRequestOption } from '@rc-component/upload/lib/interface';
|
import type { UploadRequestError, UploadRequestOption } from '@rc-component/upload/lib/interface';
|
||||||
import {
|
import {
|
||||||
@@ -61,12 +63,9 @@ function parseRoomNumber(input: string) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomsPage: React.FC = () => {
|
|
||||||
const [data, setData] = useState<any[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<any>(null);
|
const [editing, setEditing] = useState<any>(null);
|
||||||
const [detailModal, setDetailModal] = useState<any>(null);
|
|
||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
const [archivedCount, setArchivedCount] = useState(0);
|
const [archivedCount, setArchivedCount] = useState(0);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
@@ -75,6 +74,18 @@ const RoomsPage: React.FC = () => {
|
|||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [form] = Form.useForm();
|
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 () => {
|
const handleBatchDelete = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -146,13 +157,86 @@ const RoomsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showDetail = async (id: number) => {
|
const fetchBeds = async (roomId: number) => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(`/rooms/${id}`);
|
const res = await api.get(`/rooms/${roomId}/beds`);
|
||||||
setDetailModal(res);
|
setBeds(res);
|
||||||
} catch (e) {
|
} catch (e) { console.error(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) => {
|
const handleArchive = async (id: number) => {
|
||||||
@@ -253,16 +337,17 @@ const RoomsPage: React.FC = () => {
|
|||||||
<PermissionButton permission="room:edit" size="small" icon={<UndoOutlined />} type="link">
|
<PermissionButton permission="room:edit" size="small" icon={<UndoOutlined />} type="link">
|
||||||
恢复
|
恢复
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
</Popconfirm>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PermissionButton
|
<PermissionButton
|
||||||
permission="room:view"
|
permission="room:view"
|
||||||
size="small"
|
size="small"
|
||||||
type="link"
|
type="link"
|
||||||
onClick={() => showDetail(record.id)}
|
onClick={async () => {
|
||||||
|
setDrawerRoom(record);
|
||||||
|
setDrawerOpen(true);
|
||||||
|
await Promise.all([fetchBeds(record.id), fetchLockers(record.id)]);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
查看住户
|
查看
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
<PermissionButton
|
<PermissionButton
|
||||||
permission="room:edit"
|
permission="room:edit"
|
||||||
@@ -288,9 +373,7 @@ const RoomsPage: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
], [showArchived, buildings, handleBatchDelete, handleRestore, handleArchive, fetchBeds, fetchLockers]);
|
||||||
},
|
|
||||||
], [showArchived, buildings, handleBatchDelete, handleRestore, handleArchive, showDetail]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -478,27 +561,254 @@ const RoomsPage: React.FC = () => {
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Drawer
|
||||||
title={`宿舍 ${detailModal?.roomNumber} 当前住户`}
|
title={`${drawerRoom?.roomNumber} 房间详情`}
|
||||||
open={!!detailModal}
|
open={drawerOpen}
|
||||||
onCancel={() => setDetailModal(null)}
|
onClose={() => { setDrawerOpen(false); setDrawerRoom(null); }}
|
||||||
footer={null}
|
width={640}
|
||||||
width={600}
|
destroyOnClose
|
||||||
>
|
>
|
||||||
{detailModal?.currentOccupants?.length > 0 ? (
|
<Tabs
|
||||||
<Table
|
defaultActiveKey="info"
|
||||||
dataSource={detailModal.currentOccupants}
|
items={[
|
||||||
rowKey="id"
|
{
|
||||||
pagination={false}
|
key: 'info',
|
||||||
columns={[
|
label: '基本信息',
|
||||||
{ title: '学生', render: (_: any, r: any) => r.student?.name },
|
children: drawerRoom && (
|
||||||
{ title: '入住日期', dataIndex: 'checkInDate' },
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
{ title: '计费起始', dataIndex: 'billingStartDate' },
|
<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 style={{ textAlign: 'center', padding: 24, color: '#999' }}>暂无住户</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>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user