chore: commit oxfmt formatting changes and verify artifacts
This commit is contained in:
@@ -1,6 +1,29 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { Table, Button, Modal, Form, Input, InputNumber, Select, Space, message, Tag, Popconfirm, Badge, Upload } from 'antd';
|
||||
import { PlusOutlined, UploadOutlined, DownloadOutlined, UndoOutlined, InboxOutlined, SearchOutlined, ExportOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
message,
|
||||
Tag,
|
||||
Popconfirm,
|
||||
Badge,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
UploadOutlined,
|
||||
DownloadOutlined,
|
||||
UndoOutlined,
|
||||
InboxOutlined,
|
||||
SearchOutlined,
|
||||
ExportOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import api from '../../api';
|
||||
import PermissionButton from '../../components/PermissionButton';
|
||||
|
||||
@@ -30,7 +53,9 @@ const RoomsPage: React.FC = () => {
|
||||
message.success(res?.message || `已批量归档 ${selectedRowKeys.length} 间`);
|
||||
setSelectedRowKeys([]);
|
||||
fetchData();
|
||||
} catch (e: any) { message.error(e?.message || '批量归档失败'); }
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '批量归档失败');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
@@ -42,11 +67,15 @@ const RoomsPage: React.FC = () => {
|
||||
setArchivedCount(archived.length);
|
||||
const filtered = showArchived ? res : res.filter((r: any) => r.status !== 'archived');
|
||||
setData(filtered);
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, [showArchived]);
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [showArchived]);
|
||||
|
||||
// 获取楼栋列表用于筛选
|
||||
const buildings = useMemo(() => {
|
||||
@@ -81,14 +110,18 @@ const RoomsPage: React.FC = () => {
|
||||
form.resetFields();
|
||||
setEditing(null);
|
||||
fetchData();
|
||||
} catch (e: any) { message.error(e?.message || '操作失败'); }
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const showDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await api.get(`/rooms/${id}`);
|
||||
setDetailModal(res);
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleArchive = async (id: number) => {
|
||||
@@ -96,7 +129,9 @@ const RoomsPage: React.FC = () => {
|
||||
await api.delete(`/rooms/${id}`);
|
||||
message.success('已归档');
|
||||
fetchData();
|
||||
} catch (e: any) { message.error(e?.message || '归档失败'); }
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '归档失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestore = async (id: number) => {
|
||||
@@ -104,15 +139,19 @@ const RoomsPage: React.FC = () => {
|
||||
await api.put(`/rooms/${id}/restore`);
|
||||
message.success('已恢复');
|
||||
fetchData();
|
||||
} catch (e: any) { message.error(e?.message || '恢复失败'); }
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '恢复失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadTemplate = () => {
|
||||
const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
||||
const baseURL = import.meta.env.PROD
|
||||
? '/api'
|
||||
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
||||
const token = localStorage.getItem('token');
|
||||
fetch(`${baseURL}/rooms/template`, { headers: { Authorization: `Bearer ${token}` } })
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -124,12 +163,14 @@ const RoomsPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
||||
const baseURL = import.meta.env.PROD
|
||||
? '/api'
|
||||
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
||||
const token = localStorage.getItem('token');
|
||||
const params = showArchived ? '?includeArchived=true' : '';
|
||||
fetch(`${baseURL}/rooms/export${params}`, { headers: { Authorization: `Bearer ${token}` } })
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -141,37 +182,89 @@ const RoomsPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '房间号', dataIndex: 'roomNumber', sorter: (a: any, b: any) => a.roomNumber.localeCompare(b.roomNumber) },
|
||||
{
|
||||
title: '房间号',
|
||||
dataIndex: 'roomNumber',
|
||||
sorter: (a: any, b: any) => a.roomNumber.localeCompare(b.roomNumber),
|
||||
},
|
||||
{ title: '楼栋', dataIndex: 'building' },
|
||||
{ title: '楼层', dataIndex: 'floor' },
|
||||
{ title: '类型', dataIndex: 'roomType', render: (v: any) => v || '-' },
|
||||
{ title: '额定人数', dataIndex: 'capacity' },
|
||||
{
|
||||
title: '当前入住',
|
||||
render: (_: any, r: any) => r.status === 'archived' ? <Tag color="#999">-</Tag> : <Badge count={r.currentCount} showZero overflowCount={99} style={{ backgroundColor: r.currentCount >= r.capacity ? '#ff4d4f' : '#52c41a' }} />,
|
||||
render: (_: any, r: any) =>
|
||||
r.status === 'archived' ? (
|
||||
<Tag color="#999">-</Tag>
|
||||
) : (
|
||||
<Badge
|
||||
count={r.currentCount}
|
||||
showZero
|
||||
overflowCount={99}
|
||||
style={{ backgroundColor: r.currentCount >= r.capacity ? '#ff4d4f' : '#52c41a' }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ title: '性别', dataIndex: 'gender', width: 60, render: (v: any) => v ? <Tag color={v === '男' ? 'blue' : 'pink'}>{v}</Tag> : '-' },
|
||||
{
|
||||
title: '状态', dataIndex: 'status',
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
width: 60,
|
||||
render: (v: any) => (v ? <Tag color={v === '男' ? 'blue' : 'pink'}>{v}</Tag> : '-'),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
render: (s: string) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.text || s}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作', width: 220,
|
||||
title: '操作',
|
||||
width: 220,
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
{record.status === 'archived' ? (
|
||||
<PermissionButton permission="room:edit">
|
||||
<Popconfirm title="确定恢复此宿舍?恢复后将重新出现在宿舍总览中。" onConfirm={() => handleRestore(record.id)} okText="恢复" cancelText="取消">
|
||||
<Button size="small" icon={<UndoOutlined />} type="link">恢复</Button>
|
||||
<Popconfirm
|
||||
title="确定恢复此宿舍?恢复后将重新出现在宿舍总览中。"
|
||||
onConfirm={() => handleRestore(record.id)}
|
||||
okText="恢复"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button size="small" icon={<UndoOutlined />} type="link">
|
||||
恢复
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
) : (
|
||||
<>
|
||||
<PermissionButton permission="room:view" size="small" type="link" onClick={() => showDetail(record.id)}>查看住户</PermissionButton>
|
||||
<PermissionButton permission="room:edit" size="small" onClick={() => { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑</PermissionButton>
|
||||
<PermissionButton
|
||||
permission="room:view"
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => showDetail(record.id)}
|
||||
>
|
||||
查看住户
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
permission="room:edit"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditing(record);
|
||||
form.setFieldsValue(record);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="room:delete">
|
||||
<Popconfirm title="归档后不会删除数据,可随时恢复。有在住人员将无法归档。" onConfirm={() => handleArchive(record.id)} okText="归档" cancelText="取消">
|
||||
<Button size="small" icon={<InboxOutlined />}>归档</Button>
|
||||
<Popconfirm
|
||||
title="归档后不会删除数据,可随时恢复。有在住人员将无法归档。"
|
||||
onConfirm={() => handleArchive(record.id)}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button size="small" icon={<InboxOutlined />}>
|
||||
归档
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</>
|
||||
@@ -183,7 +276,15 @@ const RoomsPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Space wrap>
|
||||
<h3 style={{ margin: 0 }}>宿舍管理</h3>
|
||||
<Input.Search
|
||||
@@ -204,16 +305,35 @@ const RoomsPage: React.FC = () => {
|
||||
type={showArchived ? 'primary' : 'default'}
|
||||
onClick={() => setShowArchived(!showArchived)}
|
||||
>
|
||||
{showArchived ? '隐藏已归档' : `显示已归档${archivedCount > 0 ? ` (${archivedCount})` : ''}`}
|
||||
{showArchived
|
||||
? '隐藏已归档'
|
||||
: `显示已归档${archivedCount > 0 ? ` (${archivedCount})` : ''}`}
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<PermissionButton permission="room:delete">
|
||||
<Popconfirm title={`确定批量归档选中的 ${selectedRowKeys.length} 间宿舍?(有在住人员的会跳过)`} onConfirm={handleBatchDelete} okText="归档" cancelText="取消" disabled={selectedRowKeys.length === 0}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>批量归档</Button>
|
||||
<Popconfirm
|
||||
title={`确定批量归档选中的 ${selectedRowKeys.length} 间宿舍?(有在住人员的会跳过)`}
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>
|
||||
批量归档
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="room:create" type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
<PermissionButton
|
||||
permission="room:create"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
添加宿舍
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="room:create">
|
||||
@@ -224,18 +344,31 @@ const RoomsPage: React.FC = () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const res: any = await api.post('/rooms/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const res: any = await api.post('/rooms/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
message.success(res.message);
|
||||
onSuccess?.(res);
|
||||
fetchData();
|
||||
} catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); }
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '导入失败');
|
||||
onError?.(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||||
</Upload>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="room:view" icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>下载模板</PermissionButton>
|
||||
<PermissionButton permission="room:view" icon={<ExportOutlined />} onClick={handleExport}>导出列表</PermissionButton>
|
||||
<PermissionButton
|
||||
permission="room:view"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleDownloadTemplate}
|
||||
>
|
||||
下载模板
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="room:view" icon={<ExportOutlined />} onClick={handleExport}>
|
||||
导出列表
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
@@ -244,7 +377,7 @@ const RoomsPage: React.FC = () => {
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 间` }}
|
||||
rowClassName={(record) => record.status === 'archived' ? 'archived-row' : ''}
|
||||
rowClassName={(record) => (record.status === 'archived' ? 'archived-row' : '')}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys as number[]),
|
||||
@@ -253,33 +386,62 @@ const RoomsPage: React.FC = () => {
|
||||
/>
|
||||
<style>{`.archived-row { opacity: 0.6; background: #fafafa !important; }`}</style>
|
||||
|
||||
<Modal title={editing ? '编辑宿舍' : '添加宿舍'} open={modalOpen} onOk={handleSave} onCancel={() => { setModalOpen(false); setEditing(null); }} okText="保存">
|
||||
<Modal
|
||||
title={editing ? '编辑宿舍' : '添加宿舍'}
|
||||
open={modalOpen}
|
||||
onOk={handleSave}
|
||||
onCancel={() => {
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
}}
|
||||
okText="保存"
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="roomNumber" label="房间号" rules={[{ required: true }]}><Input placeholder="如:4-102(自动解析楼栋楼层)" /></Form.Item>
|
||||
<Form.Item name="building" label="楼栋"><Input placeholder="如:4号楼(留空自动解析)" /></Form.Item>
|
||||
<Form.Item name="floor" label="楼层"><InputNumber min={1} style={{ width: '100%' }} /></Form.Item>
|
||||
<Form.Item name="capacity" label="额定人数" rules={[{ required: true }]}><InputNumber min={1} max={20} style={{ width: '100%' }} /></Form.Item>
|
||||
<Form.Item name="roomNumber" label="房间号" rules={[{ required: true }]}>
|
||||
<Input placeholder="如:4-102(自动解析楼栋楼层)" />
|
||||
</Form.Item>
|
||||
<Form.Item name="building" label="楼栋">
|
||||
<Input placeholder="如:4号楼(留空自动解析)" />
|
||||
</Form.Item>
|
||||
<Form.Item name="floor" label="楼层">
|
||||
<InputNumber min={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="capacity" label="额定人数" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={20} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="roomType" label="宿舍类型">
|
||||
<Select allowClear options={[
|
||||
{ value: '四人间', label: '四人间' },
|
||||
{ value: '单人间', label: '单人间' },
|
||||
{ value: '家庭房', label: '家庭房' },
|
||||
{ value: '爆改房', label: '爆改房' },
|
||||
]} placeholder="留空自动解析" />
|
||||
<Select
|
||||
allowClear
|
||||
options={[
|
||||
{ value: '四人间', label: '四人间' },
|
||||
{ value: '单人间', label: '单人间' },
|
||||
{ value: '家庭房', label: '家庭房' },
|
||||
{ value: '爆改房', label: '爆改房' },
|
||||
]}
|
||||
placeholder="留空自动解析"
|
||||
/>
|
||||
</Form.Item>
|
||||
{editing && (
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select options={[
|
||||
{ value: 'available', label: '可入住' },
|
||||
{ value: 'full', label: '已满' },
|
||||
{ value: 'maintenance', label: '维修中' },
|
||||
]} />
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'available', label: '可入住' },
|
||||
{ value: 'full', label: '已满' },
|
||||
{ value: 'maintenance', label: '维修中' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal title={`宿舍 ${detailModal?.roomNumber} 当前住户`} open={!!detailModal} onCancel={() => setDetailModal(null)} footer={null} width={600}>
|
||||
<Modal
|
||||
title={`宿舍 ${detailModal?.roomNumber} 当前住户`}
|
||||
open={!!detailModal}
|
||||
onCancel={() => setDetailModal(null)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
{detailModal?.currentOccupants?.length > 0 ? (
|
||||
<Table
|
||||
dataSource={detailModal.currentOccupants}
|
||||
|
||||
Reference in New Issue
Block a user