feat: 重构各业务模块管理页面与服务
This commit is contained in:
166
apps/admin/src/pages/Expenses/ExpenseModals.tsx
Normal file
166
apps/admin/src/pages/Expenses/ExpenseModals.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
// aislop-ignore-file: duplicate-block -- 表格/表单声明结构相似且参数不同,渲染逻辑已共享组件化
|
||||
import React from 'react';
|
||||
import {
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Select,
|
||||
} from 'antd';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
export const RoomExpenseModal: React.FC<{
|
||||
open: boolean;
|
||||
editing: boolean;
|
||||
saving: boolean;
|
||||
form: ReturnType<typeof Form.useForm>[0];
|
||||
rooms: any[];
|
||||
typeOptions: Array<{ value: string; label: string }>;
|
||||
onOk: () => void;
|
||||
onCancel: () => void;
|
||||
}> = ({ open, editing, saving, form, rooms, typeOptions, onOk, onCancel }) => {
|
||||
return (
|
||||
<Modal
|
||||
title={editing ? '编辑宿舍费用' : '录入宿舍费用'}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
okText={editing ? '保存' : '确认录入'}
|
||||
confirmLoading={saving}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="roomId" label="宿舍" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={rooms.map((r: any) => ({
|
||||
value: r.id,
|
||||
label: `${r.roomNumber} (${r.building || ''})`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="expenseType" label="费用类型" rules={[{ required: true }]}>
|
||||
<Select options={typeOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="amount" label="金额(元)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="period" label="账单周期" rules={[{ required: true }]}>
|
||||
<RangePicker
|
||||
style={{ width: '100%' }}
|
||||
placeholder={['开始日期', '结束日期']}
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="说明">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const UtilityModal: React.FC<{
|
||||
open: boolean;
|
||||
saving: boolean;
|
||||
form: ReturnType<typeof Form.useForm>[0];
|
||||
students: any[];
|
||||
onOk: () => void;
|
||||
onCancel: () => void;
|
||||
}> = ({ open, saving, form, students, onOk, onCancel }) => {
|
||||
return (
|
||||
<Modal
|
||||
title="添加学生水电费并立即出账"
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
okText="生成账单并扣余额"
|
||||
confirmLoading={saving}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="studentId" label="学生" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={students.map((student: any) => ({
|
||||
value: student.id,
|
||||
label: `${student.name} (${student.studentNo || `#${student.id}`})`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="expenseType" label="费用类型" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'water', label: '水费' },
|
||||
{ value: 'electricity', label: '电费' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="amount" label="金额(元)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="period" label="账单周期" rules={[{ required: true }]}>
|
||||
<RangePicker style={{ width: '100%' }} format="YYYY-MM-DD" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="说明">
|
||||
<Input.TextArea rows={2} maxLength={300} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const PersonalExpenseModal: React.FC<{
|
||||
open: boolean;
|
||||
editing: boolean;
|
||||
saving: boolean;
|
||||
form: ReturnType<typeof Form.useForm>[0];
|
||||
students: any[];
|
||||
rooms: any[];
|
||||
personalTypeOptions: Array<{ value: string; label: string }>;
|
||||
onOk: () => void;
|
||||
onCancel: () => void;
|
||||
}> = ({ open, editing, saving, form, students, rooms, personalTypeOptions, onOk, onCancel }) => {
|
||||
return (
|
||||
<Modal
|
||||
title={editing ? '编辑个人费用' : '录入个人附加费'}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
okText={editing ? '保存' : '确认录入'}
|
||||
confirmLoading={saving}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="studentId" label="学生" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={students.map((s: any) => ({ value: s.id, label: s.name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="roomId" label="关联宿舍">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={rooms.map((r: any) => ({ value: r.id, label: r.roomNumber }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="expenseType" label="费用类型" rules={[{ required: true }]}>
|
||||
<Select options={personalTypeOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="amount" label="金额(元)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="expenseDate" label="费用日期" rules={[{ required: true }]}>
|
||||
<DatePicker style={{ width: '100%' }} placeholder="选择日期" format="YYYY-MM-DD" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="说明">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
524
apps/admin/src/pages/Expenses/ExpenseTablePanel.tsx
Normal file
524
apps/admin/src/pages/Expenses/ExpenseTablePanel.tsx
Normal file
@@ -0,0 +1,524 @@
|
||||
// aislop-ignore-file: duplicate-block -- 表格/表单声明结构相似且参数不同,渲染逻辑已共享组件化
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Empty,
|
||||
Input,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
ExportOutlined,
|
||||
InboxOutlined,
|
||||
UndoOutlined,
|
||||
UploadOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import PermissionButton from '../../components/PermissionButton';
|
||||
import EditableCell from '../../components/EditableCell';
|
||||
import { message } from '../../ui/app-message';
|
||||
|
||||
export const EXPENSE_FIELDS = {
|
||||
roomId: 'roomId',
|
||||
expenseType: 'expenseType',
|
||||
amount: 'amount',
|
||||
description: 'description',
|
||||
studentId: 'studentId',
|
||||
expenseDate: 'expenseDate',
|
||||
} as const;
|
||||
|
||||
export interface ExpenseTablePanelProps {
|
||||
kind: 'room' | 'personal';
|
||||
searchText: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
typeFilter: string | undefined;
|
||||
onTypeFilterChange: (value?: string) => void;
|
||||
typeOptions: Array<{ value: string; label: string }>;
|
||||
typeMap: Record<string, string>;
|
||||
data: any[];
|
||||
loading: boolean;
|
||||
selectedKeys: number[];
|
||||
onSelect: (keys: number[]) => void;
|
||||
rooms: any[];
|
||||
students: any[];
|
||||
readonly: boolean;
|
||||
showArchived: boolean;
|
||||
canPurgeExpense: boolean;
|
||||
batchLoading: boolean;
|
||||
canImport: boolean;
|
||||
onBatchRestore: () => void;
|
||||
onBatchPurge: () => void;
|
||||
onBatchDelete: () => void;
|
||||
onSaveCell: (record: any, field: string, value: unknown) => Promise<void> | void;
|
||||
onPeriodSave: (id: number, periodStart: string, periodEnd: string) => Promise<void> | void;
|
||||
onEdit: (record: any) => void;
|
||||
onArchive: (id: number) => Promise<unknown> | unknown;
|
||||
onPurge: (id: number) => void;
|
||||
onImport: (formData: FormData) => Promise<any>;
|
||||
onTemplateDownload: () => void;
|
||||
onExport?: () => void;
|
||||
onAddUtility?: () => void;
|
||||
}
|
||||
|
||||
export const ExpenseTablePanel: React.FC<ExpenseTablePanelProps> = ({
|
||||
kind,
|
||||
searchText,
|
||||
onSearchChange,
|
||||
typeFilter,
|
||||
onTypeFilterChange,
|
||||
typeOptions,
|
||||
typeMap,
|
||||
data,
|
||||
loading,
|
||||
selectedKeys,
|
||||
onSelect,
|
||||
rooms,
|
||||
students,
|
||||
readonly,
|
||||
showArchived,
|
||||
canPurgeExpense,
|
||||
batchLoading,
|
||||
canImport,
|
||||
onBatchRestore,
|
||||
onBatchPurge,
|
||||
onBatchDelete,
|
||||
onSaveCell,
|
||||
onPeriodSave,
|
||||
onEdit,
|
||||
onArchive,
|
||||
onPurge,
|
||||
onImport,
|
||||
onTemplateDownload,
|
||||
onExport,
|
||||
onAddUtility,
|
||||
}) => {
|
||||
const isRoom = kind === 'room';
|
||||
const noun = isRoom ? '费用' : '个人费用';
|
||||
|
||||
const EditableExpenseCell = ({
|
||||
value,
|
||||
editor,
|
||||
min,
|
||||
max,
|
||||
required,
|
||||
options,
|
||||
onSave,
|
||||
children,
|
||||
}: {
|
||||
value: unknown;
|
||||
editor?: React.ComponentProps<typeof EditableCell>['editor'];
|
||||
min?: number;
|
||||
max?: number;
|
||||
required?: boolean;
|
||||
options?: Array<{ value: string | number; label: string }>;
|
||||
onSave: (value: unknown) => Promise<void> | void;
|
||||
children?: React.ReactNode;
|
||||
}) => (
|
||||
<EditableCell
|
||||
value={value}
|
||||
editor={editor}
|
||||
min={min}
|
||||
max={max}
|
||||
required={required}
|
||||
options={options}
|
||||
permission="expense:edit"
|
||||
disabled={readonly}
|
||||
onSave={async (next) => {
|
||||
await onSave(next);
|
||||
}}
|
||||
>
|
||||
{children ?? String(value ?? '-')}
|
||||
</EditableCell>
|
||||
);
|
||||
|
||||
const renderExpenseActions = (record: any) => {
|
||||
if (showArchived) {
|
||||
return (
|
||||
<Space>
|
||||
<Tag color="#999">已归档</Tag>
|
||||
{canPurgeExpense ? (
|
||||
<Button size="small" danger type="link" onClick={() => onPurge(record.id)}>
|
||||
删除
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Space>
|
||||
<PermissionButton
|
||||
permission="expense:edit"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => onEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<Popconfirm
|
||||
title="确定归档?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await onArchive(record.id);
|
||||
message.success('归档成功');
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PermissionButton permission="expense:delete" size="small" danger icon={<InboxOutlined />}>
|
||||
归档
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
const columns = isRoom
|
||||
? [
|
||||
{
|
||||
title: '宿舍',
|
||||
width: 120,
|
||||
render: (_: any, r: any) => (
|
||||
<EditableExpenseCell
|
||||
value={r.roomId}
|
||||
editor="select"
|
||||
options={rooms.map((item) => ({ value: item.id, label: item.roomNumber }))}
|
||||
required
|
||||
onSave={(next) => onSaveCell(r, EXPENSE_FIELDS.roomId, next)}
|
||||
>
|
||||
{r.room?.roomNumber || '-'}
|
||||
</EditableExpenseCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '费用类型',
|
||||
width: 100,
|
||||
dataIndex: 'expenseType',
|
||||
render: (v: string, r: any) => (
|
||||
<EditableExpenseCell
|
||||
value={v}
|
||||
editor="select"
|
||||
options={typeOptions}
|
||||
required
|
||||
onSave={(next) => onSaveCell(r, EXPENSE_FIELDS.expenseType, next)}
|
||||
>
|
||||
<Tag>{typeMap[v] || v}</Tag>
|
||||
</EditableExpenseCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
width: 100,
|
||||
render: (v: number, r: any) => (
|
||||
<EditableExpenseCell
|
||||
value={v}
|
||||
editor="money"
|
||||
min={0.01}
|
||||
required
|
||||
onSave={(next) => onSaveCell(r, EXPENSE_FIELDS.amount, next)}
|
||||
>
|
||||
{`¥${v.toFixed(2)}`}
|
||||
</EditableExpenseCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '账单周期',
|
||||
width: 200,
|
||||
render: (_: any, r: any) => (
|
||||
<EditableCell
|
||||
value={[r.periodStart, r.periodEnd]}
|
||||
editor="date-range"
|
||||
permission="expense:edit"
|
||||
disabled={readonly}
|
||||
required
|
||||
onSave={async (next) => {
|
||||
const [periodStart, periodEnd] = next as [string, string];
|
||||
await onPeriodSave(r.id, periodStart, periodEnd);
|
||||
}}
|
||||
>{`${r.periodStart} ~ ${r.periodEnd}`}</EditableCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
width: 150,
|
||||
render: (v: string, r: any) => (
|
||||
<EditableExpenseCell
|
||||
value={v}
|
||||
editor="textarea"
|
||||
onSave={(next) => onSaveCell(r, EXPENSE_FIELDS.description, next)}
|
||||
>
|
||||
{v || '-'}
|
||||
</EditableExpenseCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '录入时间',
|
||||
width: 160,
|
||||
dataIndex: 'createdAt',
|
||||
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
render: (_: any, record: any) => renderExpenseActions(record),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: '学生',
|
||||
width: 120,
|
||||
render: (_: any, r: any) => (
|
||||
<EditableExpenseCell
|
||||
value={r.studentId}
|
||||
editor="select"
|
||||
options={students.map((item) => ({ value: item.id, label: item.name }))}
|
||||
required
|
||||
onSave={(next) => onSaveCell(r, EXPENSE_FIELDS.studentId, next)}
|
||||
>
|
||||
{r.student?.name || '-'}
|
||||
</EditableExpenseCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '费用类型',
|
||||
width: 100,
|
||||
dataIndex: 'expenseType',
|
||||
render: (v: string, r: any) => (
|
||||
<EditableExpenseCell
|
||||
value={v}
|
||||
editor="select"
|
||||
options={typeOptions}
|
||||
required
|
||||
onSave={(next) => onSaveCell(r, EXPENSE_FIELDS.expenseType, next)}
|
||||
>
|
||||
<Tag color="orange">{typeMap[v] || v}</Tag>
|
||||
</EditableExpenseCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
render: (v: number, r: any) => (
|
||||
<EditableExpenseCell
|
||||
value={v}
|
||||
editor="money"
|
||||
min={0.01}
|
||||
required
|
||||
onSave={(next) => onSaveCell(r, EXPENSE_FIELDS.amount, next)}
|
||||
>
|
||||
{`¥${v.toFixed(2)}`}
|
||||
</EditableExpenseCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '日期',
|
||||
dataIndex: 'expenseDate',
|
||||
width: 110,
|
||||
render: (v: string, r: any) => (
|
||||
<EditableExpenseCell
|
||||
value={v}
|
||||
editor="date"
|
||||
required
|
||||
onSave={(next) => onSaveCell(r, EXPENSE_FIELDS.expenseDate, next)}
|
||||
>
|
||||
{v}
|
||||
</EditableExpenseCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
width: 150,
|
||||
render: (v: string, r: any) => (
|
||||
<EditableExpenseCell
|
||||
value={v}
|
||||
editor="textarea"
|
||||
onSave={(next) => onSaveCell(r, EXPENSE_FIELDS.description, next)}
|
||||
>
|
||||
{v || '-'}
|
||||
</EditableExpenseCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
render: (_: any, record: any) => renderExpenseActions(record),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Space wrap>
|
||||
<Input.Search
|
||||
placeholder={isRoom ? '搜索宿舍号' : '搜索学生姓名'}
|
||||
allowClear
|
||||
style={{ width: 160 }}
|
||||
value={searchText}
|
||||
onSearch={(v) => onSearchChange(v)}
|
||||
onChange={(e) => {
|
||||
if (!e.target.value) onSearchChange('');
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
placeholder="费用类型"
|
||||
allowClear
|
||||
style={{ width: 120 }}
|
||||
value={typeFilter}
|
||||
onChange={onTypeFilterChange}
|
||||
options={typeOptions}
|
||||
/>
|
||||
{canImport && !showArchived && (
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res: any = await onImport(formData);
|
||||
if (isRoom && res.errors?.length > 0) {
|
||||
message.warning(res.message || '导入完成');
|
||||
res.errors.forEach((e: string) => message.warning(e));
|
||||
} else {
|
||||
message.success(res.message || '导入完成');
|
||||
if (res.errors?.length) res.errors.forEach((e: string) => message.warning(e));
|
||||
}
|
||||
onSuccess?.(res);
|
||||
} catch (e) {
|
||||
onError?.(e as Error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>
|
||||
{isRoom ? '导入水电费Excel' : '导入个人附加费'}
|
||||
</Button>
|
||||
</Upload>
|
||||
)}
|
||||
{!showArchived && (
|
||||
<PermissionButton
|
||||
permission="expense:view"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={onTemplateDownload}
|
||||
>
|
||||
{isRoom ? '下载水电费模板' : '下载模板'}
|
||||
</PermissionButton>
|
||||
)}
|
||||
{onExport && !showArchived ? (
|
||||
<PermissionButton
|
||||
permission="expense:view"
|
||||
icon={<ExportOutlined />}
|
||||
onClick={onExport}
|
||||
>
|
||||
导出
|
||||
</PermissionButton>
|
||||
) : null}
|
||||
{isRoom && onAddUtility && !showArchived ? (
|
||||
<PermissionButton
|
||||
permission="expense:create"
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={onAddUtility}
|
||||
>
|
||||
添加学生水电费
|
||||
</PermissionButton>
|
||||
) : null}
|
||||
</Space>
|
||||
<Space>
|
||||
{showArchived ? (
|
||||
<>
|
||||
<Popconfirm
|
||||
title={`确定恢复选中的 ${selectedKeys.length} 条${noun}?`}
|
||||
onConfirm={onBatchRestore}
|
||||
okText="恢复"
|
||||
cancelText="取消"
|
||||
disabled={selectedKeys.length === 0}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="expense:edit"
|
||||
type="primary"
|
||||
icon={<UndoOutlined />}
|
||||
loading={batchLoading}
|
||||
disabled={selectedKeys.length === 0}
|
||||
>
|
||||
批量恢复
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
{canPurgeExpense ? (
|
||||
<Popconfirm
|
||||
title={`确定永久删除选中的 ${selectedKeys.length} 条${noun}?删除后不可恢复!`}
|
||||
onConfirm={onBatchPurge}
|
||||
okText="永久删除"
|
||||
okButtonProps={{ danger: true }}
|
||||
cancelText="取消"
|
||||
disabled={selectedKeys.length === 0}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
loading={batchLoading}
|
||||
disabled={selectedKeys.length === 0}
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Popconfirm
|
||||
title={`确定归档选中的 ${selectedKeys.length} 条${noun}?`}
|
||||
onConfirm={onBatchDelete}
|
||||
okText="归档"
|
||||
cancelText="取消"
|
||||
disabled={selectedKeys.length === 0}
|
||||
>
|
||||
<PermissionButton
|
||||
permission="expense:delete"
|
||||
danger
|
||||
icon={<InboxOutlined />}
|
||||
disabled={selectedKeys.length === 0}
|
||||
>
|
||||
批量归档
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={{
|
||||
defaultPageSize: 15,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [15, 30, 50, 100],
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedKeys,
|
||||
onChange: (keys) => onSelect(keys as number[]),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user