import { BankOutlined, DownloadOutlined, UploadOutlined } from '@ant-design/icons' import { Alert, App, Button, Empty, Form, Input, InputNumber, Modal, Pagination, Select, Skeleton, Table } from 'antd' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useMemo, useRef, useState } from 'react' import { downloadApiFile, importExcel } from '../api/files' import http, { apiErrorMessage } from '../api/http' import PageHeader from '../components/PageHeader' import './admin-ledgers.css' interface WalletStudent { id: string; studentNumber: string; name: string; collegeName: string; majorName: string; className: string; balance: number; lastChangedAt?: string } interface Option { id: string; name: string } interface Major extends Option { collegeId: string } interface ClassOption extends Option { grade: number; majorId: string } interface Options { colleges: Option[]; majors: Major[]; classes: ClassOption[] } interface Paged { items: T[]; total: number } interface Filters { grade?: number; collegeId?: string; majorId?: string; administrativeClassId?: string } export default function CampusWalletManagementPage() { const { message } = App.useApp(); const client = useQueryClient(); const fileRef = useRef(null); const [page, setPage] = useState(1); const [keyword, setKeyword] = useState(''); const [search, setSearch] = useState(''); const [filters, setFilters] = useState({}); const [adjusting, setAdjusting] = useState(); const [form] = Form.useForm<{ amount: number; reason: string }>() const options = useQuery({ queryKey: ['wallet-filter-options'], queryFn: async () => (await http.get('/campus-registrations/wallets/filter-options')).data }) const wallets = useQuery({ queryKey: ['student-wallets', page, search, filters], enabled: options.isSuccess, queryFn: async () => (await http.get>('/campus-registrations/wallets/students', { params: { keyword: search || undefined, ...filters, page, pageSize: 20 } })).data }) const majors = useMemo(() => options.data?.majors.filter((item) => !filters.collegeId || item.collegeId === filters.collegeId) ?? [], [filters.collegeId, options.data]) const classes = useMemo(() => options.data?.classes.filter((item) => (!filters.majorId || item.majorId === filters.majorId) && (!filters.grade || item.grade === filters.grade)) ?? [], [filters.grade, filters.majorId, options.data]) const grades = useMemo(() => [...new Set((options.data?.classes ?? []).map((item) => item.grade))].sort((a, b) => b - a), [options.data]) const refresh = () => client.invalidateQueries({ queryKey: ['student-wallets'] }) async function adjust(values: { amount: number; reason: string }) { if (!adjusting) return; try { await http.post(`/campus-registrations/wallets/students/${adjusting.id}/adjust`, values); message.success('余额已调整'); setAdjusting(undefined); form.resetFields(); await refresh() } catch (error) { message.error(apiErrorMessage(error)) } } async function importFile(file?: File) { if (!file) return; try { const { data } = await importExcel('/campus-registrations/wallets/import-adjustments', file); message.success(`已调整 ${data.adjusted} 名学生`); await refresh() } catch (error) { message.error(apiErrorMessage(error)) } finally { if (fileRef.current) fileRef.current.value = '' } } async function download(path: string, name: string) { try { await downloadApiFile(path, name) } catch (error) { message.error(apiErrorMessage(error)) } } if (options.isLoading) return
if (options.isError) return
const optionData = options.data! return
{wallets.data?.total ?? 0}名范围内学生} />
setKeyword(event.target.value)} onPressEnter={() => { setSearch(keyword.trim()); setPage(1) }} /> ({ value: item.id, label: item.name }))} onChange={(value) => { setFilters((item) => ({ ...item, collegeId: value, majorId: undefined, administrativeClassId: undefined })); setPage(1) }} /> ({ value: item.id, label: `${item.grade}级 ${item.name}` }))} onChange={(value) => { setFilters((item) => ({ ...item, administrativeClassId: value })); setPage(1) }} /> void importFile(event.target.files?.[0])} />
{wallets.isError ? : rowKey="id" loading={wallets.isLoading} pagination={false} dataSource={wallets.data?.items} locale={{ emptyText: }} columns={[{ title: '学生', render: (_, item) =>
{item.studentNumber} · {item.name}{item.collegeName} · {item.majorName} · {item.className}
}, { title: '当前余额', dataIndex: 'balance', width: 150, render: (value: number) => ¥{value.toFixed(2)} }, { title: '最后变动', dataIndex: 'lastChangedAt', width: 190, render: (value?: string) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '从未调整' }, { title: '操作', width: 110, fixed: 'right', render: (_, item) => }]} />}{(wallets.data?.total ?? 0) > 20 && }
setAdjusting(undefined)} onOk={() => form.submit()}>
void adjust(values)}>
}