feat(admin-web): 做市商充值记录流水查询功能

在"充值现金(积分值)"弹窗底部新增"查看充值记录"入口按钮,
点击后打开独立的充值流水记录弹窗,支持以下功能:

- 筛选 Tab:全部 / 中心化充值 / 区块链充值(通过 memo 字段区分)
- 表格列:时间、充值方式(Badge)、金额、变动前余额、变动后余额、备注
- 分页控件:上一页/下一页 + 页码显示 + 总记录数

改动文件:
- market-maker.api.ts: 新增 LedgerEntry 类型定义和 getLedgers() API 函数
- use-market-maker.ts: 新增 useCashDepositLedgers() hook
- page.tsx: 充值弹窗底部入口 + 充值记录 Dialog UI

后端 API(GET /admin/market-maker/{name}/ledgers)已存在,无需改动。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-11 00:46:41 -08:00
parent 254796b08d
commit 7564c1151d
3 changed files with 191 additions and 0 deletions

View File

@ -23,7 +23,9 @@ import {
useDepth,
useDepthEnabled,
useSetDepthEnabled,
useCashDepositLedgers,
} from '@/features/market-maker';
import type { LedgerEntry } from '@/features/market-maker';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
@ -57,6 +59,9 @@ import {
MinusCircle,
Copy,
Check,
History,
ChevronLeft,
ChevronRight,
} from 'lucide-react';
export default function MarketMakerPage() {
@ -95,6 +100,11 @@ export default function MarketMakerPage() {
const [blockchainWithdrawCashAmount, setBlockchainWithdrawCashAmount] = useState('');
const [blockchainWithdrawSharesAddress, setBlockchainWithdrawSharesAddress] = useState('');
const [blockchainWithdrawSharesAmount, setBlockchainWithdrawSharesAmount] = useState('');
// 充值记录
const [ledgerDialogOpen, setLedgerDialogOpen] = useState(false);
const [ledgerFilter, setLedgerFilter] = useState<'all' | 'centralized' | 'blockchain'>('all');
const [ledgerPage, setLedgerPage] = useState(1);
const ledgerPageSize = 20;
const handleCopyAddress = async (address: string) => {
await navigator.clipboard.writeText(address);
@ -102,6 +112,21 @@ export default function MarketMakerPage() {
setTimeout(() => setCopiedAddress(false), 2000);
};
const { data: ledgerData, isLoading: ledgerLoading } = useCashDepositLedgers(ledgerPage, ledgerPageSize);
const filteredLedgers = (ledgerData?.data ?? []).filter((entry: LedgerEntry) => {
if (ledgerFilter === 'centralized') return !entry.memo?.includes('区块链');
if (ledgerFilter === 'blockchain') return entry.memo?.includes('区块链');
return true;
});
const ledgerTotalPages = Math.max(1, Math.ceil((ledgerData?.total ?? 0) / ledgerPageSize));
const formatDateTime = (dateStr: string) => {
const d = new Date(dateStr);
return d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
};
const config = configData?.config;
const formatNumber = (value: string | undefined, decimals: number = 2) => {
@ -284,6 +309,21 @@ export default function MarketMakerPage() {
)}
</TabsContent>
</Tabs>
<div className="pt-3 border-t mt-4">
<Button
variant="ghost"
size="sm"
className="w-full text-muted-foreground hover:text-foreground"
onClick={() => {
setLedgerPage(1);
setLedgerFilter('all');
setLedgerDialogOpen(true);
}}
>
<History className="h-4 w-4 mr-1" />
</Button>
</div>
</DialogContent>
</Dialog>
@ -372,6 +412,109 @@ export default function MarketMakerPage() {
</Tabs>
</DialogContent>
</Dialog>
{/* 充值记录 Dialog */}
<Dialog open={ledgerDialogOpen} onOpenChange={setLedgerDialogOpen}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<Tabs value={ledgerFilter} onValueChange={(v) => { setLedgerFilter(v as typeof ledgerFilter); setLedgerPage(1); }}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="all"></TabsTrigger>
<TabsTrigger value="centralized"></TabsTrigger>
<TabsTrigger value="blockchain"></TabsTrigger>
</TabsList>
</Tabs>
{ledgerLoading ? (
<div className="space-y-2">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</div>
) : filteredLedgers.length === 0 ? (
<div className="text-center text-muted-foreground py-12">
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredLedgers.map((entry: LedgerEntry) => {
const isBlockchain = entry.memo?.includes('区块链');
return (
<TableRow key={entry.id}>
<TableCell className="whitespace-nowrap text-sm">
{formatDateTime(entry.createdAt)}
</TableCell>
<TableCell>
{isBlockchain ? (
<Badge variant="outline" className="text-blue-600 border-blue-300"></Badge>
) : (
<Badge variant="outline" className="text-green-600 border-green-300"></Badge>
)}
</TableCell>
<TableCell className="text-right font-mono text-green-600">
+{formatNumber(entry.amount, 2)}
</TableCell>
<TableCell className="text-right font-mono text-muted-foreground">
{formatNumber(entry.balanceBefore, 2)}
</TableCell>
<TableCell className="text-right font-mono">
{formatNumber(entry.balanceAfter, 2)}
</TableCell>
<TableCell className="max-w-[200px] truncate text-xs text-muted-foreground" title={entry.memo ?? ''}>
{entry.memo ?? '-'}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
)}
{/* 分页 */}
{(ledgerData?.total ?? 0) > 0 && (
<div className="flex items-center justify-between pt-4 border-t">
<span className="text-sm text-muted-foreground">
{ledgerData?.total ?? 0}
</span>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={ledgerPage <= 1}
onClick={() => setLedgerPage((p) => p - 1)}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="text-sm">
{ledgerPage} / {ledgerTotalPages}
</span>
<Button
variant="outline"
size="sm"
disabled={ledgerPage >= ledgerTotalPages}
onClick={() => setLedgerPage((p) => p + 1)}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
</div>
</CardContent>

View File

@ -106,6 +106,22 @@ export interface DepthData {
timestamp: number;
}
export interface LedgerEntry {
id: string;
type: string;
assetType: string;
amount: string;
balanceBefore: string;
balanceAfter: string;
tradeNo: string | null;
orderNo: string | null;
counterpartySeq: string | null;
price: string | null;
quantity: string | null;
memo: string | null;
createdAt: string;
}
export const marketMakerApi = {
// 获取做市商配置(包含运行状态)
getConfig: async (name: string = 'MAIN_MARKET_MAKER'): Promise<{
@ -320,4 +336,23 @@ export const marketMakerApi = {
const response = await tradingClient.post('/admin/trading/depth-enabled', { enabled });
return response.data;
},
// 获取做市商分类账流水
getLedgers: async (
name: string,
params?: {
type?: string;
assetType?: string;
page?: number;
pageSize?: number;
},
): Promise<{ success: boolean; data: LedgerEntry[]; total: number }> => {
const searchParams = new URLSearchParams();
if (params?.type) searchParams.append('type', params.type);
if (params?.assetType) searchParams.append('assetType', params.assetType);
if (params?.page) searchParams.append('page', params.page.toString());
if (params?.pageSize) searchParams.append('pageSize', params.pageSize.toString());
const response = await tradingClient.get(`/admin/market-maker/${name}/ledgers?${searchParams.toString()}`);
return response.data;
},
};

View File

@ -324,6 +324,19 @@ export function useMarketMakerStats() {
});
}
export function useCashDepositLedgers(page: number = 1, pageSize: number = 20) {
return useQuery({
queryKey: ['marketMaker', 'ledgers', 'cash-deposit', MARKET_MAKER_NAME, page, pageSize],
queryFn: () =>
marketMakerApi.getLedgers(MARKET_MAKER_NAME, {
type: 'DEPOSIT',
assetType: 'CASH',
page,
pageSize,
}),
});
}
export function useDepthEnabled() {
return useQuery({
queryKey: ['trading', 'depthEnabled'],