feat: 重构各业务模块管理页面与服务
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
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,
|
||||
@@ -30,6 +34,14 @@ interface WalletRow {
|
||||
roomNumber?: string;
|
||||
}
|
||||
|
||||
interface WalletTransaction {
|
||||
id: number;
|
||||
createdAt: string;
|
||||
type: string;
|
||||
amount: number;
|
||||
balanceAfter: number;
|
||||
}
|
||||
|
||||
const transactionNames: Record<string, string> = {
|
||||
recharge: '充值',
|
||||
adjustment: '调账',
|
||||
@@ -38,14 +50,11 @@ const transactionNames: Record<string, string> = {
|
||||
};
|
||||
|
||||
const WalletsPage: React.FC = () => {
|
||||
const [rows, setRows] = useState<WalletRow[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [debtOnly, setDebtOnly] = useState(false);
|
||||
const [roomType, setRoomType] = useState<string | undefined>();
|
||||
const [roomTypes, setRoomTypes] = useState<string[]>([]);
|
||||
const [selected, setSelected] = useState<WalletRow | null>(null);
|
||||
const [transactions, setTransactions] = useState<any[]>([]);
|
||||
const [transactions, setTransactions] = useState<WalletTransaction[]>([]);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [batchForm] = Form.useForm();
|
||||
@@ -53,38 +62,66 @@ const WalletsPage: React.FC = () => {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [batchModalOpen, setBatchModalOpen] = useState(false);
|
||||
|
||||
const fetchRows = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await api.get('/wallets', {
|
||||
params: { keyword: keyword || undefined, debtOnly, roomType },
|
||||
});
|
||||
setRows(data as WalletRow[]);
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '加载学生余额失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [keyword, debtOnly, roomType]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchRows();
|
||||
}, [fetchRows]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRoomTypes = async () => {
|
||||
const {
|
||||
data: rows = [],
|
||||
isLoading,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useQuery<WalletRow[]>({
|
||||
queryKey: ['wallets', keyword, debtOnly, roomType],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
setRoomTypes((await api.get('/wallets/room-types')) as string[]);
|
||||
return validateResponse<WalletRow[]>(
|
||||
walletsSchema,
|
||||
await api.get('/wallets', {
|
||||
params: { keyword: keyword || undefined, debtOnly, roomType },
|
||||
}),
|
||||
);
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '加载学生余额失败');
|
||||
return [];
|
||||
}
|
||||
},
|
||||
});
|
||||
const { data: roomTypes = [] } = useQuery<string[]>({
|
||||
queryKey: ['wallets', 'room-types'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return validateResponse<string[]>(
|
||||
roomTypesSchema,
|
||||
await api.get('/wallets/room-types'),
|
||||
);
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '加载房型失败');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
void fetchRoomTypes();
|
||||
}, []);
|
||||
},
|
||||
});
|
||||
const loading = isLoading || isFetching;
|
||||
const fetchRows = useCallback(() => refetch(), [refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
const changeMutation = useApiMutation(
|
||||
async (payload: Record<string, unknown>) => api.post('/wallets/change-balance', payload),
|
||||
{ invalidate: [['wallets']] },
|
||||
);
|
||||
const batchChangeMutation = useApiMutation(
|
||||
async (payload: Record<string, unknown>) =>
|
||||
api.post('/wallets/batch-change-balance', payload),
|
||||
{ invalidate: [['wallets']] },
|
||||
);
|
||||
|
||||
const updateKeyword = (value: string) => {
|
||||
setKeyword(value);
|
||||
setSelectedRowKeys([]);
|
||||
}, [keyword, debtOnly, roomType]);
|
||||
};
|
||||
const updateRoomType = (value: string | undefined) => {
|
||||
setRoomType(value);
|
||||
setSelectedRowKeys([]);
|
||||
};
|
||||
const updateDebtOnly = (value: boolean) => {
|
||||
setDebtOnly(value);
|
||||
setSelectedRowKeys([]);
|
||||
};
|
||||
|
||||
const openChange = (row: WalletRow) => {
|
||||
setSelected(row);
|
||||
@@ -101,7 +138,7 @@ const WalletsPage: React.FC = () => {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
const result: any = await api.post('/wallets/change-balance', {
|
||||
const result: any = await changeMutation.mutateAsync({
|
||||
operationId: newOperationId(),
|
||||
studentId: selected.studentId,
|
||||
...values,
|
||||
@@ -112,9 +149,8 @@ const WalletsPage: React.FC = () => {
|
||||
);
|
||||
message.success(paid > 0 ? `余额已更新,并自动补扣历史账单` : '余额已更新');
|
||||
setSelected(null);
|
||||
await fetchRows();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '余额操作失败');
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -124,7 +160,7 @@ const WalletsPage: React.FC = () => {
|
||||
const values = await batchForm.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
const result: any = await api.post('/wallets/batch-change-balance', {
|
||||
const result: any = await batchChangeMutation.mutateAsync({
|
||||
operationId: newOperationId(),
|
||||
studentIds: selectedRowKeys,
|
||||
...values,
|
||||
@@ -146,9 +182,8 @@ const WalletsPage: React.FC = () => {
|
||||
setBatchModalOpen(false);
|
||||
setSelectedRowKeys([]);
|
||||
batchForm.resetFields();
|
||||
await fetchRows();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '批量余额操作失败');
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -159,10 +194,13 @@ const WalletsPage: React.FC = () => {
|
||||
setDrawerOpen(true);
|
||||
try {
|
||||
setTransactions(
|
||||
(await api.get('/wallets/transactions', { params: { studentId: row.studentId } })) as any[],
|
||||
await api.get<WalletTransaction[]>('/wallets/transactions', {
|
||||
params: { studentId: row.studentId },
|
||||
}),
|
||||
);
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '加载流水失败');
|
||||
} catch (error: unknown) {
|
||||
console.error('加载余额流水失败', error);
|
||||
message.error('加载流水失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,8 +222,8 @@ const WalletsPage: React.FC = () => {
|
||||
title: '可用余额',
|
||||
dataIndex: 'balance',
|
||||
render: (value: number) => (
|
||||
<strong style={{ color: Number(value) > 0 ? '#1677ff' : undefined }}>
|
||||
¥{Number(value).toFixed(2)}
|
||||
<strong style={{ color: value > 0 ? '#1677ff' : undefined }}>
|
||||
¥{value.toFixed(2)}
|
||||
</strong>
|
||||
),
|
||||
},
|
||||
@@ -193,8 +231,8 @@ const WalletsPage: React.FC = () => {
|
||||
title: '未付账单',
|
||||
dataIndex: 'outstandingAmount',
|
||||
render: (value: number) =>
|
||||
Number(value) > 0 ? (
|
||||
<Tag color="red">¥{Number(value).toFixed(2)}</Tag>
|
||||
value > 0 ? (
|
||||
<Tag color="red">¥{value.toFixed(2)}</Tag>
|
||||
) : (
|
||||
<Tag color="green">无欠费</Tag>
|
||||
),
|
||||
@@ -238,19 +276,19 @@ const WalletsPage: React.FC = () => {
|
||||
allowClear
|
||||
placeholder="搜索姓名或学号"
|
||||
style={{ width: 240 }}
|
||||
onSearch={setKeyword}
|
||||
onChange={(event) => !event.target.value && setKeyword('')}
|
||||
onSearch={updateKeyword}
|
||||
onChange={(event) => !event.target.value && updateKeyword('')}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="按房型筛选"
|
||||
style={{ width: 180 }}
|
||||
value={roomType}
|
||||
onChange={setRoomType}
|
||||
onChange={updateRoomType}
|
||||
options={roomTypes.map((type) => ({ label: type, value: type }))}
|
||||
/>
|
||||
<span>仅看欠费</span>
|
||||
<Switch checked={debtOnly} onChange={setDebtOnly} />
|
||||
<Switch checked={debtOnly} onChange={updateDebtOnly} />
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<PermissionButton
|
||||
@@ -297,6 +335,7 @@ const WalletsPage: React.FC = () => {
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="amount"
|
||||
label="变动金额"
|
||||
@@ -305,6 +344,7 @@ const WalletsPage: React.FC = () => {
|
||||
>
|
||||
<InputNumber precision={2} style={{ width: '100%' }} addonBefore="¥" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="备注">
|
||||
<Input.TextArea maxLength={300} />
|
||||
</Form.Item>
|
||||
@@ -331,6 +371,7 @@ const WalletsPage: React.FC = () => {
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="amount"
|
||||
label="变动金额(元/人)"
|
||||
@@ -339,6 +380,7 @@ const WalletsPage: React.FC = () => {
|
||||
>
|
||||
<InputNumber precision={2} style={{ width: '100%' }} addonBefore="¥" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="备注">
|
||||
<Input.TextArea maxLength={300} />
|
||||
</Form.Item>
|
||||
@@ -346,7 +388,7 @@ const WalletsPage: React.FC = () => {
|
||||
</Modal>
|
||||
<Drawer
|
||||
title={`${selected?.studentName || ''} - 余额流水`}
|
||||
width={680}
|
||||
size={680}
|
||||
open={drawerOpen}
|
||||
onClose={() => {
|
||||
setDrawerOpen(false);
|
||||
@@ -372,15 +414,15 @@ const WalletsPage: React.FC = () => {
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
render: (value: number) => (
|
||||
<span style={{ color: Number(value) >= 0 ? '#389e0d' : '#cf1322' }}>
|
||||
{Number(value) >= 0 ? '+' : ''}¥{Number(value).toFixed(2)}
|
||||
<span style={{ color: value >= 0 ? '#389e0d' : '#cf1322' }}>
|
||||
{value >= 0 ? '+' : ''}¥{value.toFixed(2)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '变动后余额',
|
||||
dataIndex: 'balanceAfter',
|
||||
render: (value: number) => `¥${Number(value).toFixed(2)}`,
|
||||
render: (value: number) => `¥${value.toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '关联账单',
|
||||
|
||||
Reference in New Issue
Block a user