feat: 重构各业务模块管理页面与服务
This commit is contained in:
340
apps/admin/src/pages/ClassroomRentals/RentalTable.tsx
Normal file
340
apps/admin/src/pages/ClassroomRentals/RentalTable.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Empty,
|
||||
Popconfirm,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
import {
|
||||
CheckOutlined,
|
||||
FileTextOutlined,
|
||||
StopOutlined,
|
||||
UploadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import PermissionButton from '../../components/PermissionButton';
|
||||
import EditableCell from '../../components/EditableCell';
|
||||
import { message } from '../../ui/app-message';
|
||||
|
||||
const RENTAL_FIELDS = {
|
||||
classroomId: 'classroomId',
|
||||
lesseeOrganizationId: 'lesseeOrganizationId',
|
||||
startDate: 'startDate',
|
||||
endDate: 'endDate',
|
||||
dailyRate: 'dailyRate',
|
||||
totalAmount: 'totalAmount',
|
||||
} as const;
|
||||
|
||||
export interface RentalTableProps {
|
||||
data: any[];
|
||||
loading: boolean;
|
||||
classrooms: any[];
|
||||
organizations: any[];
|
||||
canPurgeRental: boolean;
|
||||
hasPermission: (permission: string) => boolean;
|
||||
onSaveCell: (record: any, field: string, value: unknown) => Promise<void> | void;
|
||||
onEdit: (record: any) => void;
|
||||
onAction: (id: number, action: 'cancel' | 'end') => void;
|
||||
onArchive: (id: number) => void;
|
||||
onPurge: (id: number, name: string) => void;
|
||||
onDownloadContract: (id: number, filename?: string) => void;
|
||||
onDeleteContract: (id: number) => void;
|
||||
onUploadContract: (id: number, formData: FormData) => Promise<unknown>;
|
||||
}
|
||||
|
||||
export const RentalTable: React.FC<RentalTableProps> = ({
|
||||
data,
|
||||
loading,
|
||||
classrooms,
|
||||
organizations,
|
||||
canPurgeRental,
|
||||
hasPermission,
|
||||
onSaveCell,
|
||||
onEdit,
|
||||
onAction,
|
||||
onArchive,
|
||||
onPurge,
|
||||
onDownloadContract,
|
||||
onDeleteContract,
|
||||
onUploadContract,
|
||||
}) => {
|
||||
const EditableRentalCell = <R extends { id: number; effectiveStatus?: string }>({
|
||||
value,
|
||||
field,
|
||||
record,
|
||||
editor,
|
||||
min,
|
||||
required,
|
||||
options,
|
||||
children,
|
||||
}: {
|
||||
value: unknown;
|
||||
field: string;
|
||||
record: R;
|
||||
editor?: React.ComponentProps<typeof EditableCell>['editor'];
|
||||
min?: number;
|
||||
required?: boolean;
|
||||
options?: Array<{ value: string | number; label: string }>;
|
||||
children?: React.ReactNode;
|
||||
}) => (
|
||||
<EditableCell
|
||||
value={value}
|
||||
editor={editor}
|
||||
min={min}
|
||||
required={required}
|
||||
options={options}
|
||||
permission="rental:edit"
|
||||
disabled={record.effectiveStatus !== 'active'}
|
||||
onSave={async (next) => {
|
||||
await onSaveCell(record, field, next);
|
||||
}}
|
||||
>
|
||||
{children ?? String(value ?? '-')}
|
||||
</EditableCell>
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '教室',
|
||||
width: 120,
|
||||
dataIndex: 'classroom',
|
||||
render: (c: any, r: any) => (
|
||||
<EditableRentalCell
|
||||
value={r.classroomId}
|
||||
field={RENTAL_FIELDS.classroomId}
|
||||
record={r}
|
||||
editor="select"
|
||||
options={classrooms
|
||||
.filter((item) => item.status !== 'archived')
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.building ? `${item.building} · ${item.name}` : item.name,
|
||||
}))}
|
||||
required
|
||||
>
|
||||
{c ? (
|
||||
<span>
|
||||
{c.building ? `${c.building} · ` : ''}
|
||||
{c.name}
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EditableRentalCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '承租机构',
|
||||
width: 100,
|
||||
dataIndex: 'lesseeOrganization',
|
||||
render: (t: any, r: any) => (
|
||||
<EditableRentalCell
|
||||
value={r.lesseeOrganizationId}
|
||||
field={RENTAL_FIELDS.lesseeOrganizationId}
|
||||
record={r}
|
||||
editor="select"
|
||||
options={organizations
|
||||
.filter((item) => item.status !== 'archived')
|
||||
.map((item) => ({ value: item.id, label: item.name }))}
|
||||
required
|
||||
>
|
||||
{t ? (
|
||||
<Tag
|
||||
color={t.color}
|
||||
style={{ background: t.color, color: '#fff', borderColor: t.color }}
|
||||
>
|
||||
{t.name}
|
||||
</Tag>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EditableRentalCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '开始日期',
|
||||
dataIndex: 'startDate',
|
||||
width: 110,
|
||||
render: (v: string, r: any) => (
|
||||
<EditableRentalCell value={v} field={RENTAL_FIELDS.startDate} record={r} editor="date" required>
|
||||
{v}
|
||||
</EditableRentalCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '结束日期',
|
||||
dataIndex: 'endDate',
|
||||
width: 110,
|
||||
render: (v: string, r: any) => (
|
||||
<EditableRentalCell value={v} field={RENTAL_FIELDS.endDate} record={r} editor="date" required>
|
||||
{v}
|
||||
</EditableRentalCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<EditableRentalCell value={v} field={RENTAL_FIELDS.dailyRate} record={r} editor="money" min={0.01}>
|
||||
{v ? `¥${v}` : '-'}
|
||||
</EditableRentalCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '总额',
|
||||
dataIndex: 'totalAmount',
|
||||
width: 100,
|
||||
render: (v: any, r: any) => (
|
||||
<EditableRentalCell value={v} field={RENTAL_FIELDS.totalAmount} record={r} editor="money" min={0.01}>
|
||||
{v ? `¥${v}` : '-'}
|
||||
</EditableRentalCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
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={() => onDownloadContract(r.id, r.contractOriginalName)}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{hasPermission('rental:edit') ? (
|
||||
<Popconfirm title="移除合同文件?" onConfirm={() => onDeleteContract(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 onUploadContract(r.id, formData);
|
||||
message.success('合同已上传');
|
||||
onSuccess?.({});
|
||||
} catch (e) {
|
||||
onError?.(e as Error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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={() => onEdit(record)}>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<Popconfirm title="确定取消该租赁?" onConfirm={() => onAction(record.id, 'cancel')}>
|
||||
<PermissionButton
|
||||
permission="rental:edit"
|
||||
size="small"
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
>
|
||||
取消
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
{!dayjs(record.startDate).isAfter(dayjs(), 'day') && (
|
||||
<Popconfirm title="确定今天结束该租赁?" onConfirm={() => onAction(record.id, 'end')}>
|
||||
<PermissionButton permission="rental:edit" size="small" icon={<CheckOutlined />}>
|
||||
结束
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{record.effectiveStatus !== 'active' && (
|
||||
<Popconfirm
|
||||
title="确定归档该租赁订单?合同文件会保留。"
|
||||
onConfirm={() => onArchive(record.id)}
|
||||
>
|
||||
<PermissionButton permission="rental:delete" size="small" danger>
|
||||
归档
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{record.status === 'cancelled' && canPurgeRental ? (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
type="link"
|
||||
onClick={() => onPurge(record.id, record.lesseeOrganization?.name || `订单${record.id}`)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||
pagination={{
|
||||
defaultPageSize: 15,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [15, 30, 50, 100],
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user