chore: commit oxfmt formatting changes and verify artifacts
This commit is contained in:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user