691 lines
25 KiB
TypeScript
691 lines
25 KiB
TypeScript
import React, { useEffect, useState, useMemo } from 'react';
|
|
import {
|
|
Table,
|
|
Button,
|
|
Modal,
|
|
Form,
|
|
Select,
|
|
DatePicker,
|
|
InputNumber,
|
|
Input,
|
|
Space,
|
|
message,
|
|
Tag,
|
|
Tabs,
|
|
Popconfirm,
|
|
Upload,
|
|
} from 'antd';
|
|
import {
|
|
PlusOutlined,
|
|
DeleteOutlined,
|
|
EditOutlined,
|
|
UploadOutlined,
|
|
DownloadOutlined,
|
|
ExportOutlined,
|
|
} from '@ant-design/icons';
|
|
import dayjs from 'dayjs';
|
|
import api from '../../api';
|
|
import PermissionButton from '../../components/PermissionButton';
|
|
|
|
const { RangePicker } = DatePicker;
|
|
|
|
const expenseTypeOptions = [
|
|
{ value: 'water', label: '水费' },
|
|
{ value: 'electricity', label: '电费' },
|
|
{ value: 'cleaning', label: '保洁费' },
|
|
{ value: 'damage', label: '损坏赔偿' },
|
|
{ value: 'other', label: '其他' },
|
|
];
|
|
|
|
const personalExpenseTypeOptions = [
|
|
{ value: 'damage', label: '物品损坏' },
|
|
{ value: 'cleaning', label: '保洁费' },
|
|
{ value: 'penalty', label: '罚款' },
|
|
{ value: 'key', label: '钥匙费' },
|
|
{ value: 'remote', label: '空调遥控器' },
|
|
{ value: 'deposit_deduction', label: '押金扣除' },
|
|
{ value: 'other', label: '其他' },
|
|
];
|
|
|
|
const typeMap: Record<string, string> = {
|
|
water: '水费',
|
|
electricity: '电费',
|
|
cleaning: '保洁费',
|
|
damage: '损坏赔偿',
|
|
penalty: '罚款',
|
|
key: '钥匙费',
|
|
remote: '空调遥控器',
|
|
deposit_deduction: '押金扣除',
|
|
other: '其他',
|
|
};
|
|
|
|
const ExpensesPage: React.FC = () => {
|
|
const [roomExpenses, setRoomExpenses] = useState<any[]>([]);
|
|
const [personalExpenses, setPersonalExpenses] = useState<any[]>([]);
|
|
const [rooms, setRooms] = useState<any[]>([]);
|
|
const [students, setStudents] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [roomModal, setRoomModal] = useState(false);
|
|
const [personalModal, setPersonalModal] = useState(false);
|
|
const [editingRoom, setEditingRoom] = useState<any>(null);
|
|
const [editingPersonal, setEditingPersonal] = useState<any>(null);
|
|
const [roomForm] = Form.useForm();
|
|
const [personalForm] = Form.useForm();
|
|
const [roomSearch, setRoomSearch] = useState('');
|
|
const [roomTypeFilter, setRoomTypeFilter] = useState<string | undefined>(undefined);
|
|
const [personalSearch, setPersonalSearch] = useState('');
|
|
const [personalTypeFilter, setPersonalTypeFilter] = useState<string | undefined>(undefined);
|
|
const [selectedRoomKeys, setSelectedRoomKeys] = useState<number[]>([]);
|
|
const [selectedPersonalKeys, setSelectedPersonalKeys] = useState<number[]>([]);
|
|
|
|
const handleBatchDeleteRoom = async () => {
|
|
try {
|
|
const res: any = await api.post('/expenses/room/batch-delete', { ids: selectedRoomKeys });
|
|
message.success(res?.message || `已删除 ${selectedRoomKeys.length} 条`);
|
|
setSelectedRoomKeys([]);
|
|
fetchData();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '批量删除失败');
|
|
}
|
|
};
|
|
|
|
const handleBatchDeletePersonal = async () => {
|
|
try {
|
|
const res: any = await api.post('/expenses/personal/batch-delete', {
|
|
ids: selectedPersonalKeys,
|
|
});
|
|
message.success(res?.message || `已删除 ${selectedPersonalKeys.length} 条`);
|
|
setSelectedPersonalKeys([]);
|
|
fetchData();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '批量删除失败');
|
|
}
|
|
};
|
|
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [re, pe, rm, st]: any[] = await Promise.all([
|
|
api.get('/expenses/room'),
|
|
api.get('/expenses/personal'),
|
|
api.get('/rooms'),
|
|
api.get('/students'),
|
|
]);
|
|
setRoomExpenses(re);
|
|
setPersonalExpenses(pe);
|
|
setRooms(rm);
|
|
setStudents(st);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
const filteredRoomExpenses = useMemo(() => {
|
|
return roomExpenses.filter((r: any) => {
|
|
if (roomSearch) {
|
|
const s = roomSearch.toLowerCase();
|
|
if (!r.room?.roomNumber?.toLowerCase().includes(s)) return false;
|
|
}
|
|
if (roomTypeFilter && r.expenseType !== roomTypeFilter) return false;
|
|
return true;
|
|
});
|
|
}, [roomExpenses, roomSearch, roomTypeFilter]);
|
|
|
|
const filteredPersonalExpenses = useMemo(() => {
|
|
return personalExpenses.filter((p: any) => {
|
|
if (personalSearch) {
|
|
const s = personalSearch.toLowerCase();
|
|
if (!p.student?.name?.toLowerCase().includes(s)) return false;
|
|
}
|
|
if (personalTypeFilter && p.expenseType !== personalTypeFilter) return false;
|
|
return true;
|
|
});
|
|
}, [personalExpenses, personalSearch, personalTypeFilter]);
|
|
|
|
const handleRoomExpense = async () => {
|
|
const values = await roomForm.validateFields();
|
|
const payload = {
|
|
roomId: values.roomId,
|
|
expenseType: values.expenseType,
|
|
amount: values.amount,
|
|
periodStart: values.period[0].format('YYYY-MM-DD'),
|
|
periodEnd: values.period[1].format('YYYY-MM-DD'),
|
|
description: values.description,
|
|
};
|
|
try {
|
|
if (editingRoom) {
|
|
await api.put(`/expenses/room/${editingRoom.id}`, payload);
|
|
message.success('更新成功');
|
|
} else {
|
|
await api.post('/expenses/room', payload);
|
|
message.success('录入成功');
|
|
}
|
|
setRoomModal(false);
|
|
setEditingRoom(null);
|
|
roomForm.resetFields();
|
|
fetchData();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '操作失败');
|
|
}
|
|
};
|
|
|
|
const handlePersonalExpense = async () => {
|
|
const values = await personalForm.validateFields();
|
|
const payload = {
|
|
studentId: values.studentId,
|
|
roomId: values.roomId,
|
|
expenseType: values.expenseType,
|
|
amount: values.amount,
|
|
expenseDate: values.expenseDate.format('YYYY-MM-DD'),
|
|
description: values.description,
|
|
};
|
|
try {
|
|
if (editingPersonal) {
|
|
await api.put(`/expenses/personal/${editingPersonal.id}`, payload);
|
|
message.success('更新成功');
|
|
} else {
|
|
await api.post('/expenses/personal', payload);
|
|
message.success('录入成功');
|
|
}
|
|
setPersonalModal(false);
|
|
setEditingPersonal(null);
|
|
personalForm.resetFields();
|
|
fetchData();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '操作失败');
|
|
}
|
|
};
|
|
|
|
const roomColumns = [
|
|
{ title: '宿舍', render: (_: any, r: any) => r.room?.roomNumber || '-' },
|
|
{
|
|
title: '费用类型',
|
|
dataIndex: 'expenseType',
|
|
render: (v: string) => <Tag>{typeMap[v] || v}</Tag>,
|
|
},
|
|
{ title: '金额', dataIndex: 'amount', render: (v: number) => `¥${Number(v).toFixed(2)}` },
|
|
{ title: '账单周期', render: (_: any, r: any) => `${r.periodStart} ~ ${r.periodEnd}` },
|
|
{ title: '说明', dataIndex: 'description' },
|
|
{
|
|
title: '录入时间',
|
|
dataIndex: 'createdAt',
|
|
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm'),
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 120,
|
|
render: (_: any, record: any) => (
|
|
<Space>
|
|
<PermissionButton
|
|
permission="expense:edit"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => {
|
|
setEditingRoom(record);
|
|
roomForm.setFieldsValue({
|
|
roomId: record.roomId,
|
|
expenseType: record.expenseType,
|
|
amount: Number(record.amount),
|
|
period: [dayjs(record.periodStart), dayjs(record.periodEnd)],
|
|
description: record.description,
|
|
});
|
|
setRoomModal(true);
|
|
}}
|
|
>
|
|
{''}
|
|
</PermissionButton>
|
|
<PermissionButton permission="expense:delete">
|
|
<Popconfirm
|
|
title="确定删除?"
|
|
onConfirm={async () => {
|
|
await api.delete(`/expenses/room/${record.id}`);
|
|
message.success('删除成功');
|
|
fetchData();
|
|
}}
|
|
>
|
|
<Button size="small" danger icon={<DeleteOutlined />} />
|
|
</Popconfirm>
|
|
</PermissionButton>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
const personalColumns = [
|
|
{ title: '学生', render: (_: any, r: any) => r.student?.name || '-' },
|
|
{
|
|
title: '费用类型',
|
|
dataIndex: 'expenseType',
|
|
render: (v: string) => <Tag color="orange">{typeMap[v] || v}</Tag>,
|
|
},
|
|
{ title: '金额', dataIndex: 'amount', render: (v: number) => `¥${Number(v).toFixed(2)}` },
|
|
{ title: '日期', dataIndex: 'expenseDate' },
|
|
{ title: '说明', dataIndex: 'description' },
|
|
{
|
|
title: '操作',
|
|
width: 120,
|
|
render: (_: any, record: any) => (
|
|
<Space>
|
|
<PermissionButton
|
|
permission="expense:edit"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => {
|
|
setEditingPersonal(record);
|
|
personalForm.setFieldsValue({
|
|
studentId: record.studentId,
|
|
roomId: record.roomId,
|
|
expenseType: record.expenseType,
|
|
amount: Number(record.amount),
|
|
expenseDate: dayjs(record.expenseDate),
|
|
description: record.description,
|
|
});
|
|
setPersonalModal(true);
|
|
}}
|
|
>
|
|
{''}
|
|
</PermissionButton>
|
|
<PermissionButton permission="expense:delete">
|
|
<Popconfirm
|
|
title="确定删除?"
|
|
onConfirm={async () => {
|
|
await api.delete(`/expenses/personal/${record.id}`);
|
|
message.success('删除成功');
|
|
fetchData();
|
|
}}
|
|
>
|
|
<Button size="small" danger icon={<DeleteOutlined />} />
|
|
</Popconfirm>
|
|
</PermissionButton>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<Tabs
|
|
items={[
|
|
{
|
|
key: 'room',
|
|
label: '宿舍费用',
|
|
children: (
|
|
<>
|
|
<div
|
|
style={{
|
|
marginBottom: 16,
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
flexWrap: 'wrap',
|
|
gap: 8,
|
|
}}
|
|
>
|
|
<Space wrap>
|
|
<Input.Search
|
|
placeholder="搜索宿舍号"
|
|
allowClear
|
|
style={{ width: 160 }}
|
|
onSearch={(v) => setRoomSearch(v)}
|
|
onChange={(e) => {
|
|
if (!e.target.value) setRoomSearch('');
|
|
}}
|
|
/>
|
|
<Select
|
|
placeholder="费用类型"
|
|
allowClear
|
|
style={{ width: 120 }}
|
|
value={roomTypeFilter}
|
|
onChange={(v) => setRoomTypeFilter(v)}
|
|
options={expenseTypeOptions}
|
|
/>
|
|
<PermissionButton permission="expense:create" type="text">
|
|
<Upload
|
|
accept=".xlsx,.xls"
|
|
showUploadList={false}
|
|
customRequest={async ({ file, onSuccess, onError }: any) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
try {
|
|
const res: any = await api.post('/expenses/utility/import', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
if (res.errors?.length > 0) {
|
|
Modal.warning({
|
|
title: res.message,
|
|
content: res.errors.join('\n'),
|
|
width: 500,
|
|
});
|
|
} else {
|
|
message.success(res.message);
|
|
}
|
|
onSuccess?.(res);
|
|
fetchData();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '导入失败');
|
|
onError?.(e);
|
|
}
|
|
}}
|
|
>
|
|
<Button icon={<UploadOutlined />}>导入水电费Excel</Button>
|
|
</Upload>
|
|
</PermissionButton>
|
|
<PermissionButton
|
|
permission="expense:view"
|
|
icon={<DownloadOutlined />}
|
|
onClick={() => {
|
|
const baseURL = import.meta.env.PROD
|
|
? '/api'
|
|
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
|
const token = localStorage.getItem('token');
|
|
fetch(`${baseURL}/expenses/utility/template`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
.then((res) => res.blob())
|
|
.then((blob) => {
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = '水电费导入模板.xlsx';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
})
|
|
.catch(() => message.error('下载失败'));
|
|
}}
|
|
>
|
|
下载水电费模板
|
|
</PermissionButton>
|
|
</Space>
|
|
<Space>
|
|
<PermissionButton permission="expense:delete">
|
|
<Popconfirm
|
|
title={`确定删除选中的 ${selectedRoomKeys.length} 条费用?`}
|
|
onConfirm={handleBatchDeleteRoom}
|
|
okText="删除"
|
|
cancelText="取消"
|
|
disabled={selectedRoomKeys.length === 0}
|
|
>
|
|
<Button
|
|
danger
|
|
icon={<DeleteOutlined />}
|
|
disabled={selectedRoomKeys.length === 0}
|
|
>
|
|
批量删除
|
|
</Button>
|
|
</Popconfirm>
|
|
</PermissionButton>
|
|
<PermissionButton
|
|
permission="expense:create"
|
|
type="primary"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => {
|
|
setEditingRoom(null);
|
|
roomForm.resetFields();
|
|
setRoomModal(true);
|
|
}}
|
|
>
|
|
录入宿舍费用
|
|
</PermissionButton>
|
|
</Space>
|
|
</div>
|
|
<Table
|
|
columns={roomColumns}
|
|
dataSource={filteredRoomExpenses}
|
|
rowKey="id"
|
|
loading={loading}
|
|
scroll={{ x: 800 }}
|
|
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
|
rowSelection={{
|
|
selectedRowKeys: selectedRoomKeys,
|
|
onChange: (keys) => setSelectedRoomKeys(keys as number[]),
|
|
}}
|
|
/>
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
key: 'personal',
|
|
label: '个人附加费',
|
|
children: (
|
|
<>
|
|
<div
|
|
style={{
|
|
marginBottom: 16,
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
flexWrap: 'wrap',
|
|
gap: 8,
|
|
}}
|
|
>
|
|
<Space wrap>
|
|
<Input.Search
|
|
placeholder="搜索学生姓名"
|
|
allowClear
|
|
style={{ width: 160 }}
|
|
onSearch={(v) => setPersonalSearch(v)}
|
|
onChange={(e) => {
|
|
if (!e.target.value) setPersonalSearch('');
|
|
}}
|
|
/>
|
|
<Select
|
|
placeholder="费用类型"
|
|
allowClear
|
|
style={{ width: 120 }}
|
|
value={personalTypeFilter}
|
|
onChange={(v) => setPersonalTypeFilter(v)}
|
|
options={personalExpenseTypeOptions}
|
|
/>
|
|
<PermissionButton permission="expense:create" type="text">
|
|
<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 api.post('/expenses/personal/import', formData);
|
|
message.success(res.message || '导入完成');
|
|
if (res.errors?.length)
|
|
res.errors.forEach((e: string) => message.warning(e));
|
|
fetchData();
|
|
onSuccess?.(res);
|
|
} catch (e: any) {
|
|
message.error(e?.message || '导入失败');
|
|
onError?.(e);
|
|
}
|
|
}}
|
|
>
|
|
<Button icon={<UploadOutlined />}>导入个人附加费</Button>
|
|
</Upload>
|
|
</PermissionButton>
|
|
<PermissionButton
|
|
permission="expense:view"
|
|
icon={<DownloadOutlined />}
|
|
onClick={() => {
|
|
const baseURL = import.meta.env.PROD
|
|
? '/api'
|
|
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
|
const token = localStorage.getItem('token');
|
|
fetch(`${baseURL}/expenses/personal/template`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
.then((res) => res.blob())
|
|
.then((blob) => {
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = '个人附加费导入模板.xlsx';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
})
|
|
.catch(() => message.error('下载失败'));
|
|
}}
|
|
>
|
|
下载模板
|
|
</PermissionButton>
|
|
<PermissionButton
|
|
permission="expense:view"
|
|
icon={<ExportOutlined />}
|
|
onClick={() => {
|
|
const baseURL = import.meta.env.PROD
|
|
? '/api'
|
|
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
|
const token = localStorage.getItem('token');
|
|
fetch(`${baseURL}/expenses/personal/export`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
.then((res) => res.blob())
|
|
.then((blob) => {
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = '个人附加费导出.xlsx';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
})
|
|
.catch(() => message.error('导出失败'));
|
|
}}
|
|
>
|
|
导出
|
|
</PermissionButton>
|
|
</Space>
|
|
<Space>
|
|
<PermissionButton permission="expense:delete">
|
|
<Popconfirm
|
|
title={`确定删除选中的 ${selectedPersonalKeys.length} 条个人费用?`}
|
|
onConfirm={handleBatchDeletePersonal}
|
|
okText="删除"
|
|
cancelText="取消"
|
|
disabled={selectedPersonalKeys.length === 0}
|
|
>
|
|
<Button
|
|
danger
|
|
icon={<DeleteOutlined />}
|
|
disabled={selectedPersonalKeys.length === 0}
|
|
>
|
|
批量删除
|
|
</Button>
|
|
</Popconfirm>
|
|
</PermissionButton>
|
|
<PermissionButton
|
|
permission="expense:create"
|
|
type="primary"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => {
|
|
setEditingPersonal(null);
|
|
personalForm.resetFields();
|
|
setPersonalModal(true);
|
|
}}
|
|
>
|
|
录入个人费用
|
|
</PermissionButton>
|
|
</Space>
|
|
</div>
|
|
<Table
|
|
columns={personalColumns}
|
|
dataSource={filteredPersonalExpenses}
|
|
rowKey="id"
|
|
loading={loading}
|
|
scroll={{ x: 800 }}
|
|
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
|
rowSelection={{
|
|
selectedRowKeys: selectedPersonalKeys,
|
|
onChange: (keys) => setSelectedPersonalKeys(keys as number[]),
|
|
}}
|
|
/>
|
|
</>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
|
|
<Modal
|
|
title={editingRoom ? '编辑宿舍费用' : '录入宿舍费用'}
|
|
open={roomModal}
|
|
onOk={handleRoomExpense}
|
|
onCancel={() => {
|
|
setRoomModal(false);
|
|
setEditingRoom(null);
|
|
}}
|
|
okText={editingRoom ? '保存' : '确认录入'}
|
|
>
|
|
<Form form={roomForm} 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={expenseTypeOptions} />
|
|
</Form.Item>
|
|
<Form.Item name="amount" label="金额(元)" rules={[{ required: true }]}>
|
|
<InputNumber min={0} 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>
|
|
|
|
<Modal
|
|
title={editingPersonal ? '编辑个人费用' : '录入个人附加费'}
|
|
open={personalModal}
|
|
onOk={handlePersonalExpense}
|
|
onCancel={() => {
|
|
setPersonalModal(false);
|
|
setEditingPersonal(null);
|
|
}}
|
|
okText={editingPersonal ? '保存' : '确认录入'}
|
|
>
|
|
<Form form={personalForm} 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={personalExpenseTypeOptions} />
|
|
</Form.Item>
|
|
<Form.Item name="amount" label="金额(元)" rules={[{ required: true }]}>
|
|
<InputNumber min={0} 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>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ExpensesPage;
|