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:
parent
254796b08d
commit
7564c1151d
|
|
@ -23,7 +23,9 @@ import {
|
||||||
useDepth,
|
useDepth,
|
||||||
useDepthEnabled,
|
useDepthEnabled,
|
||||||
useSetDepthEnabled,
|
useSetDepthEnabled,
|
||||||
|
useCashDepositLedgers,
|
||||||
} from '@/features/market-maker';
|
} from '@/features/market-maker';
|
||||||
|
import type { LedgerEntry } from '@/features/market-maker';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
@ -57,6 +59,9 @@ import {
|
||||||
MinusCircle,
|
MinusCircle,
|
||||||
Copy,
|
Copy,
|
||||||
Check,
|
Check,
|
||||||
|
History,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export default function MarketMakerPage() {
|
export default function MarketMakerPage() {
|
||||||
|
|
@ -95,6 +100,11 @@ export default function MarketMakerPage() {
|
||||||
const [blockchainWithdrawCashAmount, setBlockchainWithdrawCashAmount] = useState('');
|
const [blockchainWithdrawCashAmount, setBlockchainWithdrawCashAmount] = useState('');
|
||||||
const [blockchainWithdrawSharesAddress, setBlockchainWithdrawSharesAddress] = useState('');
|
const [blockchainWithdrawSharesAddress, setBlockchainWithdrawSharesAddress] = useState('');
|
||||||
const [blockchainWithdrawSharesAmount, setBlockchainWithdrawSharesAmount] = 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) => {
|
const handleCopyAddress = async (address: string) => {
|
||||||
await navigator.clipboard.writeText(address);
|
await navigator.clipboard.writeText(address);
|
||||||
|
|
@ -102,6 +112,21 @@ export default function MarketMakerPage() {
|
||||||
setTimeout(() => setCopiedAddress(false), 2000);
|
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 config = configData?.config;
|
||||||
|
|
||||||
const formatNumber = (value: string | undefined, decimals: number = 2) => {
|
const formatNumber = (value: string | undefined, decimals: number = 2) => {
|
||||||
|
|
@ -284,6 +309,21 @@ export default function MarketMakerPage() {
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</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>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
@ -372,6 +412,109 @@ export default function MarketMakerPage() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,22 @@ export interface DepthData {
|
||||||
timestamp: number;
|
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 = {
|
export const marketMakerApi = {
|
||||||
// 获取做市商配置(包含运行状态)
|
// 获取做市商配置(包含运行状态)
|
||||||
getConfig: async (name: string = 'MAIN_MARKET_MAKER'): Promise<{
|
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 });
|
const response = await tradingClient.post('/admin/trading/depth-enabled', { enabled });
|
||||||
return response.data;
|
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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
export function useDepthEnabled() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['trading', 'depthEnabled'],
|
queryKey: ['trading', 'depthEnabled'],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue