chore: commit oxfmt formatting changes and verify artifacts

This commit is contained in:
2026-07-02 15:55:09 +08:00
parent 34726cc46d
commit 4f4cee157a
77 changed files with 5576 additions and 1400 deletions

View File

@@ -1,6 +1,28 @@
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 {
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';
@@ -25,7 +47,17 @@ const personalExpenseTypeOptions = [
{ value: 'other', label: '其他' },
];
const typeMap: Record<string, string> = { water: '水费', electricity: '电费', cleaning: '保洁费', damage: '损坏赔偿', penalty: '罚款', key: '钥匙费', remote: '空调遥控器', deposit_deduction: '押金扣除', other: '其他' };
const typeMap: Record<string, string> = {
water: '水费',
electricity: '电费',
cleaning: '保洁费',
damage: '损坏赔偿',
penalty: '罚款',
key: '钥匙费',
remote: '空调遥控器',
deposit_deduction: '押金扣除',
other: '其他',
};
const ExpensesPage: React.FC = () => {
const [roomExpenses, setRoomExpenses] = useState<any[]>([]);
@@ -52,16 +84,22 @@ const ExpensesPage: React.FC = () => {
message.success(res?.message || `已删除 ${selectedRoomKeys.length}`);
setSelectedRoomKeys([]);
fetchData();
} catch (e: any) { message.error(e?.message || '批量删除失败'); }
} catch (e: any) {
message.error(e?.message || '批量删除失败');
}
};
const handleBatchDeletePersonal = async () => {
try {
const res: any = await api.post('/expenses/personal/batch-delete', { ids: selectedPersonalKeys });
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 || '批量删除失败'); }
} catch (e: any) {
message.error(e?.message || '批量删除失败');
}
};
const fetchData = async () => {
@@ -77,11 +115,15 @@ const ExpensesPage: React.FC = () => {
setPersonalExpenses(pe);
setRooms(rm);
setStudents(st);
} catch (e) { console.error(e); }
} catch (e) {
console.error(e);
}
setLoading(false);
};
useEffect(() => { fetchData(); }, []);
useEffect(() => {
fetchData();
}, []);
const filteredRoomExpenses = useMemo(() => {
return roomExpenses.filter((r: any) => {
@@ -127,7 +169,9 @@ const ExpensesPage: React.FC = () => {
setEditingRoom(null);
roomForm.resetFields();
fetchData();
} catch (e: any) { message.error(e?.message || '操作失败'); }
} catch (e: any) {
message.error(e?.message || '操作失败');
}
};
const handlePersonalExpense = async () => {
@@ -152,33 +196,58 @@ const ExpensesPage: React.FC = () => {
setEditingPersonal(null);
personalForm.resetFields();
fetchData();
} catch (e: any) { message.error(e?.message || '操作失败'); }
} 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: '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,
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: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(); }}>
<Popconfirm
title="确定删除?"
onConfirm={async () => {
await api.delete(`/expenses/room/${record.id}`);
message.success('删除成功');
fetchData();
}}
>
<Button size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</PermissionButton>
@@ -189,28 +258,47 @@ const ExpensesPage: React.FC = () => {
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: '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,
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: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(); }}>
<Popconfirm
title="确定删除?"
onConfirm={async () => {
await api.delete(`/expenses/personal/${record.id}`);
message.success('删除成功');
fetchData();
}}
>
<Button size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</PermissionButton>
@@ -221,175 +309,318 @@ const ExpensesPage: React.FC = () => {
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">
<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); }
<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('');
}}
>
<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} 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">
<Upload
accept=".xlsx,.xls"
showUploadList={false}
customRequest={async ({ file, onSuccess, onError }: any) => {
try {
/>
<Select
placeholder="费用类型"
allowClear
style={{ width: 120 }}
value={roomTypeFilter}
onChange={(v) => setRoomTypeFilter(v)}
options={expenseTypeOptions}
/>
<PermissionButton permission="expense:create">
<Upload
accept=".xlsx,.xls"
showUploadList={false}
customRequest={async ({ file, onSuccess, onError }: any) => {
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); }
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('下载失败'));
}}
>
<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} pagination={{ pageSize: 15, showTotal: (total) => `${total}` }}
rowSelection={{ selectedRowKeys: selectedPersonalKeys, onChange: (keys) => setSelectedPersonalKeys(keys as number[]) }}
/>
</>
),
},
]} />
</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}
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">
<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}
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 ? '保存' : '确认录入'}>
<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 || ''})` }))} />
<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} />
@@ -398,7 +629,11 @@ const ExpensesPage: React.FC = () => {
<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" />
<RangePicker
style={{ width: '100%' }}
placeholder={['开始日期', '结束日期']}
format="YYYY-MM-DD"
/>
</Form.Item>
<Form.Item name="description" label="说明">
<Input.TextArea rows={2} />
@@ -406,13 +641,31 @@ const ExpensesPage: React.FC = () => {
</Form>
</Modal>
<Modal title={editingPersonal ? '编辑个人费用' : '录入个人附加费'} open={personalModal} onOk={handlePersonalExpense} onCancel={() => { setPersonalModal(false); setEditingPersonal(null); }} okText={editingPersonal ? '保存' : '确认录入'}>
<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 }))} />
<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 }))} />
<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} />