import React, { useCallback, useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useApiMutation } from '../../hooks/useApiMutation'; import { validateResponse } from '../../utils/validate'; import { roomTypesSchema, walletsSchema } from '../../api/schemas'; import { Button, Drawer, Form, Input, InputNumber, Modal, Radio, Select, Space, Switch, Table, Tag, } from 'antd'; import { HistoryOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; import { message } from '../../ui/app-message'; import { newOperationId } from '../../utils/operation-id'; interface WalletRow { studentId: number; studentName: string; studentNo?: string; balance: number; outstandingAmount: number; roomType?: string; roomNumber?: string; } interface WalletTransaction { id: number; createdAt: string; type: string; amount: number; balanceAfter: number; } const transactionNames: Record = { recharge: '充值', adjustment: '调账', bill_payment: '账单扣款', bill_refund: '账单冲正', }; const WalletsPage: React.FC = () => { const [keyword, setKeyword] = useState(''); const [debtOnly, setDebtOnly] = useState(false); const [roomType, setRoomType] = useState(); const [selected, setSelected] = useState(null); const [transactions, setTransactions] = useState([]); const [drawerOpen, setDrawerOpen] = useState(false); const [form] = Form.useForm(); const [batchForm] = Form.useForm(); const [saving, setSaving] = useState(false); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchModalOpen, setBatchModalOpen] = useState(false); const { data: rows = [], isLoading, isFetching, refetch, } = useQuery({ queryKey: ['wallets', keyword, debtOnly, roomType], queryFn: async () => { try { return validateResponse( walletsSchema, await api.get('/wallets', { params: { keyword: keyword || undefined, debtOnly, roomType }, }), ); } catch (error: any) { message.error(error?.message || '加载学生余额失败'); return []; } }, }); const { data: roomTypes = [] } = useQuery({ queryKey: ['wallets', 'room-types'], queryFn: async () => { try { return validateResponse( roomTypesSchema, await api.get('/wallets/room-types'), ); } catch (error: any) { message.error(error?.message || '加载房型失败'); return []; } }, }); const loading = isLoading || isFetching; const fetchRows = useCallback(() => refetch(), [refetch]); const changeMutation = useApiMutation( async (payload: Record) => api.post('/wallets/change-balance', payload), { invalidate: [['wallets']] }, ); const batchChangeMutation = useApiMutation( async (payload: Record) => api.post('/wallets/batch-change-balance', payload), { invalidate: [['wallets']] }, ); const updateKeyword = (value: string) => { setKeyword(value); setSelectedRowKeys([]); }; const updateRoomType = (value: string | undefined) => { setRoomType(value); setSelectedRowKeys([]); }; const updateDebtOnly = (value: boolean) => { setDebtOnly(value); setSelectedRowKeys([]); }; const openChange = (row: WalletRow) => { setSelected(row); form.setFieldsValue({ type: 'recharge', amount: undefined, description: '' }); }; const openBatchChange = () => { batchForm.setFieldsValue({ type: 'recharge', amount: undefined, description: '' }); setBatchModalOpen(true); }; const submitChange = async () => { if (!selected) return; const values = await form.validateFields(); setSaving(true); try { const result: any = await changeMutation.mutateAsync({ operationId: newOperationId(), studentId: selected.studentId, ...values, }); const paid = (result.payments || []).reduce( (sum: number, bill: any) => sum + Number(bill.paidAmount || 0), 0, ); message.success(paid > 0 ? `余额已更新,并自动补扣历史账单` : '余额已更新'); setSelected(null); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setSaving(false); } }; const submitBatchChange = async () => { const values = await batchForm.validateFields(); setSaving(true); try { const result: any = await batchChangeMutation.mutateAsync({ operationId: newOperationId(), studentIds: selectedRowKeys, ...values, }); const paid = (result.results || []).reduce((sum: number, item: any) => { return ( sum + (item.payments || []).reduce( (paymentSum: number, bill: any) => paymentSum + Number(bill.paidAmount || 0), 0, ) ); }, 0); message.success( paid > 0 ? `已批量更新 ${selectedRowKeys.length} 名学生余额,并自动补扣历史账单` : `已批量更新 ${selectedRowKeys.length} 名学生余额`, ); setBatchModalOpen(false); setSelectedRowKeys([]); batchForm.resetFields(); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setSaving(false); } }; const showTransactions = async (row: WalletRow) => { setSelected(row); setDrawerOpen(true); try { setTransactions( await api.get('/wallets/transactions', { params: { studentId: row.studentId }, }), ); } catch (error: unknown) { console.error('加载余额流水失败', error); message.error('加载流水失败'); } }; const columns = useMemo( () => [ { title: '学生', render: (_: unknown, row: WalletRow) => ( <> {row.studentName}
{row.studentNo || `#${row.studentId}`}
{row.roomType ? `${row.roomType}${row.roomNumber ? ` · ${row.roomNumber}` : ''}` : '未入住'}
), }, { title: '可用余额', dataIndex: 'balance', render: (value: number) => ( 0 ? '#1677ff' : undefined }}> ¥{value.toFixed(2)} ), }, { title: '未付账单', dataIndex: 'outstandingAmount', render: (value: number) => value > 0 ? ( ¥{value.toFixed(2)} ) : ( 无欠费 ), }, { title: '操作', render: (_: unknown, row: WalletRow) => ( } onClick={() => openChange(row)} > 充值/调账 ), }, ], [], ); return (
!event.target.value && updateKeyword('')} />