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 = { water: '水费', electricity: '电费', cleaning: '保洁费', damage: '损坏赔偿', penalty: '罚款', key: '钥匙费', remote: '空调遥控器', deposit_deduction: '押金扣除', other: '其他', }; const ExpensesPage: React.FC = () => { const [roomExpenses, setRoomExpenses] = useState([]); const [personalExpenses, setPersonalExpenses] = useState([]); const [rooms, setRooms] = useState([]); const [students, setStudents] = useState([]); const [loading, setLoading] = useState(false); const [roomModal, setRoomModal] = useState(false); const [personalModal, setPersonalModal] = useState(false); const [editingRoom, setEditingRoom] = useState(null); const [editingPersonal, setEditingPersonal] = useState(null); const [roomForm] = Form.useForm(); const [personalForm] = Form.useForm(); const [roomSearch, setRoomSearch] = useState(''); const [roomTypeFilter, setRoomTypeFilter] = useState(undefined); const [personalSearch, setPersonalSearch] = useState(''); const [personalTypeFilter, setPersonalTypeFilter] = useState(undefined); const [selectedRoomKeys, setSelectedRoomKeys] = useState([]); const [selectedPersonalKeys, setSelectedPersonalKeys] = useState([]); 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) => {typeMap[v] || v}, }, { 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) => ( } 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); }} > {''} { await api.delete(`/expenses/room/${record.id}`); message.success('删除成功'); fetchData(); }} > } 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('下载失败')); }} > 下载模板 } 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('导出失败')); }} > 导出 } onClick={() => { setEditingPersonal(null); personalForm.resetFields(); setPersonalModal(true); }} > 录入个人费用 `共 ${total} 条` }} rowSelection={{ selectedRowKeys: selectedPersonalKeys, onChange: (keys) => setSelectedPersonalKeys(keys as number[]), }} /> ), }, ]} /> { setRoomModal(false); setEditingRoom(null); }} okText={editingRoom ? '保存' : '确认录入'} >
{ setPersonalModal(false); setEditingPersonal(null); }} okText={editingPersonal ? '保存' : '确认录入'} >
({ value: r.id, label: r.roomNumber }))} />