feat: 重构各业务模块管理页面与服务
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useImmer } from 'use-immer';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
App,
|
||||
Modal,
|
||||
Form,
|
||||
Select,
|
||||
@@ -9,39 +9,32 @@ import {
|
||||
InputNumber,
|
||||
Input,
|
||||
Space,
|
||||
Tag,
|
||||
Popconfirm,
|
||||
Upload,
|
||||
Tooltip,
|
||||
Empty,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
UploadOutlined,
|
||||
FileTextOutlined,
|
||||
StopOutlined,
|
||||
CheckOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import api from '../../api';
|
||||
import { downloadBlob } from '../../utils/download';
|
||||
import PermissionButton from '../../components/PermissionButton';
|
||||
import EditableCell from '../../components/EditableCell';
|
||||
import { message } from '../../ui/app-message';
|
||||
import { usePermission } from '../../hooks/usePermission';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApiMutation } from '../../hooks/useApiMutation';
|
||||
import { getErrorMessage } from '../../utils/error';
|
||||
import { validateResponse } from '../../utils/validate';
|
||||
import { classroomsSchema, organizationsSchema, rentalsSchema } from '../../api/schemas';
|
||||
import { RentalTable } from './RentalTable';
|
||||
|
||||
interface UnavailableDatesResponse {
|
||||
dates: string[];
|
||||
}
|
||||
|
||||
export const unavailableDatesCacheKey = (classroomId: number, date: Dayjs) =>
|
||||
`${classroomId}:${date.format('YYYY-MM')}`;
|
||||
|
||||
const ClassroomRentalsPage: React.FC = () => {
|
||||
const { modal } = App.useApp();
|
||||
const { hasPermission, hasAnyPermission } = usePermission();
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [classrooms, setClassrooms] = useState<any[]>([]);
|
||||
const [organizations, setOrganizations] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const canPurgeRental = hasPermission('rental:purge');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<any>(null);
|
||||
const [form] = Form.useForm();
|
||||
@@ -49,12 +42,110 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
const [filterStatus, setFilterStatus] = useState<string | undefined>();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [unavailableDates, setUnavailableDates] = useState<Set<string>>(new Set());
|
||||
const [unavailableDates, setUnavailableDates] = useImmer<Set<string>>(new Set());
|
||||
const loadedUnavailableMonths = useRef<Set<string>>(new Set());
|
||||
const unavailableRequestVersion = useRef(0);
|
||||
const [unavailableDatesLoading, setUnavailableDatesLoading] = useState(false);
|
||||
const selectedClassroomId = Form.useWatch('classroomId', form);
|
||||
|
||||
const {
|
||||
data = [],
|
||||
isLoading,
|
||||
isFetching,
|
||||
} = useQuery<any[]>({
|
||||
queryKey: ['classroom-rentals', filterMonth],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (filterMonth) params.month = filterMonth.format('YYYY-MM');
|
||||
params.includeEnded = true;
|
||||
return validateResponse<any[]>(
|
||||
rentalsSchema,
|
||||
await api.get('/classroom-rentals', { params }),
|
||||
);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载失败,请稍后重试');
|
||||
return [];
|
||||
}
|
||||
},
|
||||
});
|
||||
const {
|
||||
data: meta = { classrooms: [], organizations: [] },
|
||||
} = useQuery<{ classrooms: any[]; organizations: any[] }>({
|
||||
queryKey: ['classroom-rentals', 'meta'],
|
||||
enabled: hasAnyPermission('rental:create', 'rental:edit'),
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const [cr, tn]: any = await Promise.all([
|
||||
api.get('/classrooms'),
|
||||
api.get('/organizations', { params: { scope: 'all' } }),
|
||||
]);
|
||||
return {
|
||||
classrooms: validateResponse<any[]>(classroomsSchema, cr),
|
||||
organizations: validateResponse<any[]>(organizationsSchema, tn),
|
||||
};
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载教室列表失败');
|
||||
return { classrooms: [], organizations: [] };
|
||||
}
|
||||
},
|
||||
});
|
||||
const classrooms = meta.classrooms;
|
||||
const organizations = meta.organizations;
|
||||
const loading = isLoading || isFetching;
|
||||
|
||||
const saveMutation = useApiMutation(
|
||||
async (payload: Record<string, unknown>) =>
|
||||
editing
|
||||
? api.put(`/classroom-rentals/${editing.id}`, payload)
|
||||
: api.post('/classroom-rentals', payload),
|
||||
{
|
||||
invalidate: [['classroom-rentals']],
|
||||
onError: (error: unknown) => {
|
||||
const e = error as {
|
||||
conflicts?: Array<{ organizationName?: string; startDate?: string; endDate?: string }>;
|
||||
};
|
||||
if (e?.conflicts?.length) {
|
||||
const list = e.conflicts
|
||||
.map((c) => `${c.organizationName}(${c.startDate}~${c.endDate})`)
|
||||
.join('、');
|
||||
message.error(`时间段冲突:${list}`);
|
||||
} else {
|
||||
message.error(getErrorMessage(error, '操作失败'));
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
const saveCellMutation = useApiMutation(
|
||||
async ({ record, field, value }: { record: any; field: string; value: unknown }) =>
|
||||
api.put(`/classroom-rentals/${record.id}`, { [field]: value }),
|
||||
{ invalidate: [['classroom-rentals']] },
|
||||
);
|
||||
const deleteMutation = useApiMutation(
|
||||
async (id: number) => api.delete(`/classroom-rentals/${id}`),
|
||||
{ invalidate: [['classroom-rentals']] },
|
||||
);
|
||||
const purgeMutation = useApiMutation(
|
||||
async (id: number) => api.delete(`/classroom-rentals/${id}/permanent`),
|
||||
{ invalidate: [['classroom-rentals']] },
|
||||
);
|
||||
const actionMutation = useApiMutation(
|
||||
async ({ id, action }: { id: number; action: 'cancel' | 'end' }) =>
|
||||
api.put(`/classroom-rentals/${id}/${action}`),
|
||||
{ invalidate: [['classroom-rentals']] },
|
||||
);
|
||||
const deleteContractMutation = useApiMutation(
|
||||
async (id: number) => api.delete(`/classroom-rentals/${id}/contract`),
|
||||
{ invalidate: [['classroom-rentals']] },
|
||||
);
|
||||
const uploadContractMutation = useApiMutation(
|
||||
async ({ id, formData }: { id: number; formData: FormData }) =>
|
||||
api.post(`/classroom-rentals/${id}/contract`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
}),
|
||||
{ invalidate: [['classroom-rentals']] },
|
||||
);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter((r: any) => {
|
||||
if (filterStatus && r.effectiveStatus !== filterStatus) return false;
|
||||
@@ -66,40 +157,6 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
});
|
||||
}, [data, searchText, filterStatus]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: any = {};
|
||||
if (filterMonth) params.month = filterMonth.format('YYYY-MM');
|
||||
params.includeEnded = true;
|
||||
const res: any = await api.get('/classroom-rentals', { params });
|
||||
setData(res);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载失败,请稍后重试');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const fetchMeta = async () => {
|
||||
try {
|
||||
const [cr, tn]: any = await Promise.all([
|
||||
api.get('/classrooms'),
|
||||
api.get('/organizations', { params: { scope: 'all' } }),
|
||||
]);
|
||||
setClassrooms(cr);
|
||||
setOrganizations(tn);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载教室列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAnyPermission('rental:create', 'rental:edit')) fetchMeta();
|
||||
}, [hasAnyPermission]);
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [filterMonth]);
|
||||
|
||||
const resetUnavailableDates = () => {
|
||||
unavailableRequestVersion.current += 1;
|
||||
loadedUnavailableMonths.current.clear();
|
||||
@@ -127,10 +184,8 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
},
|
||||
);
|
||||
if (requestVersion !== unavailableRequestVersion.current) return;
|
||||
setUnavailableDates((current) => {
|
||||
const next = new Set(current);
|
||||
response.dates.forEach((item) => next.add(item));
|
||||
return next;
|
||||
setUnavailableDates((draft) => {
|
||||
response.dates.forEach((item) => draft.add(item));
|
||||
});
|
||||
} catch (e: any) {
|
||||
loadedUnavailableMonths.current.delete(key);
|
||||
@@ -190,75 +245,85 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
notes: values.notes,
|
||||
};
|
||||
try {
|
||||
if (editing) {
|
||||
await api.put(`/classroom-rentals/${editing.id}`, payload);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await api.post('/classroom-rentals', payload);
|
||||
message.success('创建成功');
|
||||
}
|
||||
await saveMutation.mutateAsync(payload);
|
||||
message.success(editing ? '更新成功' : '创建成功');
|
||||
setModalOpen(false);
|
||||
form.resetFields();
|
||||
setEditing(null);
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
if (e?.conflicts?.length) {
|
||||
const list = e.conflicts
|
||||
.map((c: any) => `${c.organizationName}(${c.startDate}~${c.endDate})`)
|
||||
.join('、');
|
||||
message.error(`时间段冲突:${list}`);
|
||||
} else {
|
||||
message.error(e?.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveCell = async (record: any, field: string, value: unknown) => {
|
||||
await api.put(`/classroom-rentals/${record.id}`, { [field]: value });
|
||||
message.success('已保存');
|
||||
await fetchData();
|
||||
try {
|
||||
await saveCellMutation.mutateAsync({ record, field, value });
|
||||
message.success('已保存');
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await api.delete(`/classroom-rentals/${id}`);
|
||||
await deleteMutation.mutateAsync(id);
|
||||
message.success('已归档');
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '归档失败');
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurge = (id: number, name: string) => {
|
||||
modal.confirm({
|
||||
title: `永久删除租赁订单(${name})?`,
|
||||
content: '删除后不可恢复,排课与合同文件将被清除(存在考勤记录时将无法删除)。确定继续?',
|
||||
okText: '永久删除',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await purgeMutation.mutateAsync(id);
|
||||
message.success('已永久删除(不可恢复)');
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRentalAction = async (id: number, action: 'cancel' | 'end') => {
|
||||
try {
|
||||
await api.put(`/classroom-rentals/${id}/${action}`);
|
||||
await actionMutation.mutateAsync({ id, action });
|
||||
message.success(action === 'cancel' ? '租赁已取消' : '租赁已结束');
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败');
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadContract = async (id: number, filename?: string) => {
|
||||
try {
|
||||
await downloadBlob(`/classroom-rentals/${id}/contract`, filename || `contract-${id}.pdf`);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error('下载合同失败', e);
|
||||
message.error('下载失败(可能文件已丢失)');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteContract = async (id: number) => {
|
||||
try {
|
||||
await api.delete(`/classroom-rentals/${id}/contract`);
|
||||
await deleteContractMutation.mutateAsync(id);
|
||||
message.success('合同已移除');
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '移除失败');
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadContract = async (id: number, formData: FormData) => {
|
||||
return uploadContractMutation.mutateAsync({ id, formData });
|
||||
};
|
||||
|
||||
const openEdit = (record: any) => {
|
||||
setEditing(record);
|
||||
resetUnavailableDates();
|
||||
@@ -280,271 +345,6 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: '教室',
|
||||
width: 120,
|
||||
dataIndex: 'classroom',
|
||||
render: (c: any, r: any) => (
|
||||
<EditableCell
|
||||
value={r.classroomId}
|
||||
editor="select"
|
||||
options={classrooms
|
||||
.filter((item) => item.status !== 'archived')
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.building ? `${item.building} · ${item.name}` : item.name,
|
||||
}))}
|
||||
permission="rental:edit"
|
||||
disabled={r.effectiveStatus !== 'active'}
|
||||
required
|
||||
onSave={(next) => saveCell(r, 'classroomId', next)}
|
||||
>
|
||||
{c ? (
|
||||
<span>
|
||||
{c.building ? `${c.building} · ` : ''}
|
||||
{c.name}
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EditableCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '承租机构',
|
||||
width: 100,
|
||||
dataIndex: 'lesseeOrganization',
|
||||
render: (t: any, r: any) => (
|
||||
<EditableCell
|
||||
value={r.lesseeOrganizationId}
|
||||
editor="select"
|
||||
options={organizations
|
||||
.filter((item) => item.status !== 'archived')
|
||||
.map((item) => ({ value: item.id, label: item.name }))}
|
||||
permission="rental:edit"
|
||||
disabled={r.effectiveStatus !== 'active'}
|
||||
required
|
||||
onSave={(next) => saveCell(r, 'lesseeOrganizationId', next)}
|
||||
>
|
||||
{t ? (
|
||||
<Tag
|
||||
color={t.color}
|
||||
style={{ background: t.color, color: '#fff', borderColor: t.color }}
|
||||
>
|
||||
{t.name}
|
||||
</Tag>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EditableCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '开始日期',
|
||||
dataIndex: 'startDate',
|
||||
width: 110,
|
||||
render: (v: string, r: any) => (
|
||||
<EditableCell
|
||||
value={v}
|
||||
editor="date"
|
||||
permission="rental:edit"
|
||||
disabled={r.effectiveStatus !== 'active'}
|
||||
required
|
||||
onSave={(next) => saveCell(r, 'startDate', next)}
|
||||
>
|
||||
{v}
|
||||
</EditableCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '结束日期',
|
||||
dataIndex: 'endDate',
|
||||
width: 110,
|
||||
render: (v: string, r: any) => (
|
||||
<EditableCell
|
||||
value={v}
|
||||
editor="date"
|
||||
permission="rental:edit"
|
||||
disabled={r.effectiveStatus !== 'active'}
|
||||
required
|
||||
onSave={(next) => saveCell(r, 'endDate', next)}
|
||||
>
|
||||
{v}
|
||||
</EditableCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '时长',
|
||||
width: 80,
|
||||
render: (_: any, r: any) => {
|
||||
const d = dayjs(r.endDate).diff(dayjs(r.startDate), 'day') + 1;
|
||||
return `${d}天`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '日租金',
|
||||
dataIndex: 'dailyRate',
|
||||
width: 100,
|
||||
render: (v: any, r: any) => (
|
||||
<EditableCell
|
||||
value={v}
|
||||
editor="money"
|
||||
min={0.01}
|
||||
permission="rental:edit"
|
||||
disabled={r.effectiveStatus !== 'active'}
|
||||
onSave={(next) => saveCell(r, 'dailyRate', next)}
|
||||
>
|
||||
{v ? `¥${v}` : '-'}
|
||||
</EditableCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '总额',
|
||||
dataIndex: 'totalAmount',
|
||||
width: 100,
|
||||
render: (v: any, r: any) => (
|
||||
<EditableCell
|
||||
value={v}
|
||||
editor="money"
|
||||
min={0.01}
|
||||
permission="rental:edit"
|
||||
disabled={r.effectiveStatus !== 'active'}
|
||||
onSave={(next) => saveCell(r, 'totalAmount', next)}
|
||||
>
|
||||
{v ? `¥${v}` : '-'}
|
||||
</EditableCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'effectiveStatus',
|
||||
width: 90,
|
||||
render: (status: string) => {
|
||||
const config: Record<string, { text: string; color: string }> = {
|
||||
active: { text: '进行中', color: 'green' },
|
||||
ended: { text: '已结束', color: 'default' },
|
||||
cancelled: { text: '已取消', color: 'red' },
|
||||
};
|
||||
return <Tag color={config[status]?.color}>{config[status]?.text || status}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '合同',
|
||||
width: 120,
|
||||
dataIndex: 'contractPath',
|
||||
render: (v: string, r: any) =>
|
||||
v ? (
|
||||
<Space>
|
||||
<Tooltip title={r.contractOriginalName}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={() => handleDownloadContract(r.id, r.contractOriginalName)}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{hasPermission('rental:edit') ? (
|
||||
<Popconfirm title="移除合同文件?" onConfirm={() => handleDeleteContract(r.id)}>
|
||||
<Button size="small" danger icon={<StopOutlined />} aria-label="移除合同文件" />
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
</Space>
|
||||
) : hasPermission('rental:edit') ? (
|
||||
<Upload
|
||||
accept="application/pdf"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
message.error('文件不能超过 10MB');
|
||||
onError?.(new Error('size'));
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
await api.post(`/classroom-rentals/${r.id}/contract`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
message.success('合同已上传');
|
||||
onSuccess?.({});
|
||||
fetchData();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '上传失败');
|
||||
onError?.(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button size="small" icon={<UploadOutlined />}>
|
||||
上传PDF
|
||||
</Button>
|
||||
</Upload>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
{record.effectiveStatus === 'active' && (
|
||||
<>
|
||||
<PermissionButton
|
||||
permission="rental:edit"
|
||||
size="small"
|
||||
onClick={() => openEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<Popconfirm
|
||||
title="确定取消该租赁?"
|
||||
onConfirm={() => handleRentalAction(record.id, 'cancel')}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="rental:edit"
|
||||
size="small"
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
>
|
||||
取消
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
{!dayjs(record.startDate).isAfter(dayjs(), 'day') && (
|
||||
<Popconfirm
|
||||
title="确定今天结束该租赁?"
|
||||
onConfirm={() => handleRentalAction(record.id, 'end')}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="rental:edit"
|
||||
size="small"
|
||||
icon={<CheckOutlined />}
|
||||
>
|
||||
结束
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{record.effectiveStatus !== 'active' && (
|
||||
<Popconfirm
|
||||
title="确定归档该租赁订单?合同文件会保留。"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<PermissionButton permission="rental:delete" size="small" danger>
|
||||
归档
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
],
|
||||
[classrooms, organizations, hasPermission],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@@ -601,19 +401,21 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
新增租赁
|
||||
</PermissionButton>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredData}
|
||||
rowKey="id"
|
||||
<RentalTable
|
||||
data={filteredData}
|
||||
loading={loading}
|
||||
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||
pagination={{
|
||||
defaultPageSize: 15,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [15, 30, 50, 100],
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
classrooms={classrooms}
|
||||
organizations={organizations}
|
||||
canPurgeRental={canPurgeRental}
|
||||
hasPermission={hasPermission}
|
||||
onSaveCell={saveCell}
|
||||
onEdit={openEdit}
|
||||
onAction={handleRentalAction}
|
||||
onArchive={handleDelete}
|
||||
onPurge={handlePurge}
|
||||
onDownloadContract={handleDownloadContract}
|
||||
onDeleteContract={handleDeleteContract}
|
||||
onUploadContract={handleUploadContract}
|
||||
/>
|
||||
<Modal
|
||||
title={editing ? '编辑租赁' : '新增租赁'}
|
||||
|
||||
Reference in New Issue
Block a user