feat(mining-admin-web): 添加全局兑换记录页面
后端(trading-service): - OrderRepository 新增 findAllOrders/findAllTrades 全局查询方法 - AdminController 新增 GET /admin/orders 和 GET /admin/trades 端点 支持 type/status/source/search/日期范围筛选 + 分页 前端(mining-admin-web): - 新增 /exchange-records 页面,包含「订单记录」和「成交明细」两个 Tab - 订单 Tab: 支持按类型/状态/来源筛选,显示订单号/账号/价格/数量等 - 成交 Tab: 支持按来源筛选,显示买卖双方/价格/数量/销毁量/手续费等 - 侧边栏添加「兑换记录」菜单项 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a55201b3b3
commit
41818eb8e2
|
|
@ -1,8 +1,10 @@
|
|||
import { Controller, Get, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { Controller, Get, Post, Body, Query, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
||||
import { IsBoolean } from 'class-validator';
|
||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||
import { TradingConfigRepository } from '../../infrastructure/persistence/repositories/trading-config.repository';
|
||||
import { OrderRepository } from '../../infrastructure/persistence/repositories/order.repository';
|
||||
import { OrderType, OrderStatus, OrderSource } from '../../domain/aggregates/order.aggregate';
|
||||
import { Public } from '../../shared/guards/jwt-auth.guard';
|
||||
|
||||
class SetBuyEnabledDto {
|
||||
|
|
@ -21,6 +23,7 @@ export class AdminController {
|
|||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly tradingConfigRepository: TradingConfigRepository,
|
||||
private readonly orderRepository: OrderRepository,
|
||||
) {}
|
||||
|
||||
@Get('accounts/sync')
|
||||
|
|
@ -184,6 +187,88 @@ export class AdminController {
|
|||
};
|
||||
}
|
||||
|
||||
@Get('orders')
|
||||
@Public()
|
||||
@ApiOperation({ summary: '获取全局订单列表' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||
@ApiQuery({ name: 'type', required: false, enum: ['BUY', 'SELL'] })
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['PENDING', 'PARTIAL', 'FILLED', 'CANCELLED'] })
|
||||
@ApiQuery({ name: 'source', required: false, enum: ['USER', 'MARKET_MAKER', 'DEX_BOT', 'SYSTEM'] })
|
||||
@ApiQuery({ name: 'search', required: false, type: String })
|
||||
@ApiQuery({ name: 'startDate', required: false, type: String })
|
||||
@ApiQuery({ name: 'endDate', required: false, type: String })
|
||||
async getOrders(
|
||||
@Query('page') page?: number,
|
||||
@Query('pageSize') pageSize?: number,
|
||||
@Query('type') type?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('source') source?: string,
|
||||
@Query('search') search?: string,
|
||||
@Query('startDate') startDate?: string,
|
||||
@Query('endDate') endDate?: string,
|
||||
) {
|
||||
const p = page ? Number(page) : 1;
|
||||
const ps = pageSize ? Number(pageSize) : 20;
|
||||
|
||||
const result = await this.orderRepository.findAllOrders({
|
||||
type: type as OrderType | undefined,
|
||||
status: status as OrderStatus | undefined,
|
||||
source: source as OrderSource | undefined,
|
||||
search: search || undefined,
|
||||
startDate: startDate ? new Date(startDate) : undefined,
|
||||
endDate: endDate ? new Date(endDate) : undefined,
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
});
|
||||
|
||||
return {
|
||||
data: result.data,
|
||||
total: result.total,
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
totalPages: Math.ceil(result.total / ps),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('trades')
|
||||
@Public()
|
||||
@ApiOperation({ summary: '获取全局成交记录' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||
@ApiQuery({ name: 'source', required: false, enum: ['USER', 'MARKET_MAKER', 'DEX_BOT', 'SYSTEM'] })
|
||||
@ApiQuery({ name: 'search', required: false, type: String })
|
||||
@ApiQuery({ name: 'startDate', required: false, type: String })
|
||||
@ApiQuery({ name: 'endDate', required: false, type: String })
|
||||
async getTrades(
|
||||
@Query('page') page?: number,
|
||||
@Query('pageSize') pageSize?: number,
|
||||
@Query('source') source?: string,
|
||||
@Query('search') search?: string,
|
||||
@Query('startDate') startDate?: string,
|
||||
@Query('endDate') endDate?: string,
|
||||
) {
|
||||
const p = page ? Number(page) : 1;
|
||||
const ps = pageSize ? Number(pageSize) : 20;
|
||||
|
||||
const result = await this.orderRepository.findAllTrades({
|
||||
source: source || undefined,
|
||||
search: search || undefined,
|
||||
startDate: startDate ? new Date(startDate) : undefined,
|
||||
endDate: endDate ? new Date(endDate) : undefined,
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
});
|
||||
|
||||
return {
|
||||
data: result.data,
|
||||
total: result.total,
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
totalPages: Math.ceil(result.total / ps),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('trading/depth-enabled')
|
||||
@Public() // TODO: 生产环境应添加管理员权限验证
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
|
|
|||
|
|
@ -324,6 +324,154 @@ export class OrderRepository {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局订单查询(管理后台用)
|
||||
*/
|
||||
async findAllOrders(options?: {
|
||||
type?: OrderType;
|
||||
status?: OrderStatus;
|
||||
source?: OrderSource;
|
||||
search?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}): Promise<{ data: any[]; total: number }> {
|
||||
const where: any = {};
|
||||
|
||||
if (options?.type) where.type = options.type;
|
||||
if (options?.status) where.status = options.status;
|
||||
if (options?.source) where.source = options.source;
|
||||
|
||||
if (options?.search) {
|
||||
where.OR = [
|
||||
{ accountSequence: { contains: options.search } },
|
||||
{ orderNo: { contains: options.search } },
|
||||
];
|
||||
}
|
||||
|
||||
if (options?.startDate || options?.endDate) {
|
||||
where.createdAt = {};
|
||||
if (options?.startDate) where.createdAt.gte = options.startDate;
|
||||
if (options?.endDate) where.createdAt.lte = options.endDate;
|
||||
}
|
||||
|
||||
const page = options?.page ?? 1;
|
||||
const pageSize = options?.pageSize ?? 20;
|
||||
|
||||
const [records, total] = await Promise.all([
|
||||
this.prisma.order.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.order.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: records.map((o) => ({
|
||||
id: o.id,
|
||||
orderNo: o.orderNo,
|
||||
accountSequence: o.accountSequence,
|
||||
type: o.type,
|
||||
status: o.status,
|
||||
source: o.source,
|
||||
sourceLabel: o.sourceLabel,
|
||||
price: o.price.toString(),
|
||||
quantity: o.quantity.toString(),
|
||||
filledQuantity: o.filledQuantity.toString(),
|
||||
remainingQuantity: o.remainingQuantity.toString(),
|
||||
averagePrice: o.averagePrice.toString(),
|
||||
totalAmount: o.totalAmount.toString(),
|
||||
burnQuantity: o.burnQuantity.toString(),
|
||||
burnMultiplier: o.burnMultiplier.toString(),
|
||||
effectiveQuantity: o.effectiveQuantity.toString(),
|
||||
createdAt: o.createdAt,
|
||||
completedAt: o.completedAt,
|
||||
cancelledAt: o.cancelledAt,
|
||||
})),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局成交记录查询(管理后台用)
|
||||
*/
|
||||
async findAllTrades(options?: {
|
||||
source?: string;
|
||||
search?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}): Promise<{ data: any[]; total: number }> {
|
||||
const where: any = {};
|
||||
|
||||
if (options?.source) {
|
||||
where.OR = [
|
||||
{ buyerSource: options.source },
|
||||
{ sellerSource: options.source },
|
||||
];
|
||||
}
|
||||
|
||||
if (options?.search) {
|
||||
const searchCondition = {
|
||||
OR: [
|
||||
{ buyerSequence: { contains: options.search } },
|
||||
{ sellerSequence: { contains: options.search } },
|
||||
{ tradeNo: { contains: options.search } },
|
||||
],
|
||||
};
|
||||
if (where.OR) {
|
||||
where.AND = [{ OR: where.OR }, searchCondition];
|
||||
delete where.OR;
|
||||
} else {
|
||||
where.OR = searchCondition.OR;
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.startDate || options?.endDate) {
|
||||
where.createdAt = {};
|
||||
if (options?.startDate) where.createdAt.gte = options.startDate;
|
||||
if (options?.endDate) where.createdAt.lte = options.endDate;
|
||||
}
|
||||
|
||||
const page = options?.page ?? 1;
|
||||
const pageSize = options?.pageSize ?? 20;
|
||||
|
||||
const [records, total] = await Promise.all([
|
||||
this.prisma.trade.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.trade.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: records.map((t) => ({
|
||||
id: t.id,
|
||||
tradeNo: t.tradeNo,
|
||||
buyOrderId: t.buyOrderId,
|
||||
sellOrderId: t.sellOrderId,
|
||||
buyerSequence: t.buyerSequence,
|
||||
sellerSequence: t.sellerSequence,
|
||||
price: t.price.toString(),
|
||||
quantity: t.quantity.toString(),
|
||||
originalQuantity: t.originalQuantity.toString(),
|
||||
burnQuantity: t.burnQuantity.toString(),
|
||||
amount: t.amount.toString(),
|
||||
fee: t.fee.toString(),
|
||||
buyerSource: t.buyerSource,
|
||||
sellerSource: t.sellerSource,
|
||||
createdAt: t.createdAt,
|
||||
})),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
private toDomain(record: any): OrderAggregate {
|
||||
return OrderAggregate.reconstitute({
|
||||
id: record.id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,336 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
import { useAdminOrders, useAdminTrades } from '@/features/trading/hooks/use-trading';
|
||||
import type { AdminOrder, AdminTrade } from '@/features/trading/api/trading.api';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
|
||||
import { formatDecimal } from '@/lib/utils/format';
|
||||
import { formatDateTime } from '@/lib/utils/date';
|
||||
|
||||
const orderStatusLabels: Record<string, { label: string; className: string }> = {
|
||||
PENDING: { label: '待成交', className: 'bg-yellow-100 text-yellow-700' },
|
||||
PARTIAL: { label: '部分成交', className: 'bg-blue-100 text-blue-700' },
|
||||
FILLED: { label: '已成交', className: 'bg-green-100 text-green-700' },
|
||||
CANCELLED: { label: '已取消', className: 'bg-gray-100 text-gray-600' },
|
||||
};
|
||||
|
||||
const sourceLabels: Record<string, { label: string; className: string }> = {
|
||||
USER: { label: '用户', className: 'bg-gray-100 text-gray-700' },
|
||||
MARKET_MAKER: { label: '做市商', className: 'bg-purple-100 text-purple-700' },
|
||||
DEX_BOT: { label: 'DEX Bot', className: 'bg-blue-100 text-blue-700' },
|
||||
SYSTEM: { label: '系统', className: 'bg-gray-100 text-gray-600' },
|
||||
};
|
||||
|
||||
export default function ExchangeRecordsPage() {
|
||||
const [tab, setTab] = useState<'orders' | 'trades'>('orders');
|
||||
|
||||
// 订单 tab 状态
|
||||
const [ordersPage, setOrdersPage] = useState(1);
|
||||
const [orderType, setOrderType] = useState<string>('ALL');
|
||||
const [orderStatus, setOrderStatus] = useState<string>('ALL');
|
||||
const [orderSource, setOrderSource] = useState<string>('ALL');
|
||||
|
||||
// 成交 tab 状态
|
||||
const [tradesPage, setTradesPage] = useState(1);
|
||||
const [tradeSource, setTradeSource] = useState<string>('ALL');
|
||||
|
||||
// 共享搜索状态
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
const { data: ordersData, isLoading: ordersLoading } = useAdminOrders({
|
||||
page: ordersPage,
|
||||
pageSize,
|
||||
type: orderType === 'ALL' ? undefined : orderType as 'BUY' | 'SELL',
|
||||
status: orderStatus === 'ALL' ? undefined : orderStatus as any,
|
||||
source: orderSource === 'ALL' ? undefined : orderSource as any,
|
||||
search: search || undefined,
|
||||
});
|
||||
|
||||
const { data: tradesData, isLoading: tradesLoading } = useAdminTrades({
|
||||
page: tradesPage,
|
||||
pageSize,
|
||||
source: tradeSource === 'ALL' ? undefined : tradeSource as any,
|
||||
search: search || undefined,
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
setSearch(searchInput);
|
||||
setOrdersPage(1);
|
||||
setTradesPage(1);
|
||||
};
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setSearchInput('');
|
||||
setSearch('');
|
||||
setOrdersPage(1);
|
||||
setTradesPage(1);
|
||||
};
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
setTab(value as 'orders' | 'trades');
|
||||
};
|
||||
|
||||
const renderBadge = (text: string, className: string) => (
|
||||
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${className}`}>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
|
||||
const renderSkeletonRows = (cols: number) =>
|
||||
[...Array(5)].map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{[...Array(cols)].map((_, j) => (
|
||||
<TableCell key={j}><Skeleton className="h-4 w-full" /></TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
));
|
||||
|
||||
const renderPagination = (
|
||||
data: { total: number; totalPages: number } | undefined,
|
||||
page: number,
|
||||
setPage: (p: number) => void,
|
||||
) => {
|
||||
if (!data || data.totalPages <= 1) return null;
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 border-t">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
共 {data.total} 条,第 {page} / {data.totalPages} 页
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setPage(page - 1)} disabled={page <= 1}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setPage(page + 1)} disabled={page >= data.totalPages}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="兑换记录" description="查看全部积分股兑换订单与成交明细" />
|
||||
|
||||
{/* 搜索栏 */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="搜索账号、订单号、成交号..."
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="max-w-md"
|
||||
/>
|
||||
<Button onClick={handleSearch} variant="outline">
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
搜索
|
||||
</Button>
|
||||
{search && (
|
||||
<Button onClick={handleClearSearch} variant="ghost">
|
||||
清除
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={tab} onValueChange={handleTabChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="orders">订单记录</TabsTrigger>
|
||||
<TabsTrigger value="trades">成交明细</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 订单记录 Tab */}
|
||||
<TabsContent value="orders" className="space-y-4">
|
||||
{/* 筛选栏 */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
<Select value={orderType} onValueChange={(v) => { setOrderType(v); setOrdersPage(1); }}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">全部类型</SelectItem>
|
||||
<SelectItem value="BUY">买入</SelectItem>
|
||||
<SelectItem value="SELL">卖出</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={orderStatus} onValueChange={(v) => { setOrderStatus(v); setOrdersPage(1); }}>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue placeholder="状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">全部状态</SelectItem>
|
||||
<SelectItem value="PENDING">待成交</SelectItem>
|
||||
<SelectItem value="PARTIAL">部分成交</SelectItem>
|
||||
<SelectItem value="FILLED">已成交</SelectItem>
|
||||
<SelectItem value="CANCELLED">已取消</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={orderSource} onValueChange={(v) => { setOrderSource(v); setOrdersPage(1); }}>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue placeholder="来源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">全部来源</SelectItem>
|
||||
<SelectItem value="USER">用户</SelectItem>
|
||||
<SelectItem value="MARKET_MAKER">做市商</SelectItem>
|
||||
<SelectItem value="DEX_BOT">DEX Bot</SelectItem>
|
||||
<SelectItem value="SYSTEM">系统</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 订单表格 */}
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>订单号</TableHead>
|
||||
<TableHead>账号</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead className="text-right">价格</TableHead>
|
||||
<TableHead className="text-right">数量</TableHead>
|
||||
<TableHead className="text-right">已成交</TableHead>
|
||||
<TableHead className="text-right">金额</TableHead>
|
||||
<TableHead>时间</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ordersLoading ? renderSkeletonRows(10) : !ordersData?.data?.length ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={10} className="text-center py-8 text-muted-foreground">
|
||||
暂无订单记录
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
ordersData.data.map((order: AdminOrder) => {
|
||||
const status = orderStatusLabels[order.status] || { label: order.status, className: '' };
|
||||
const source = sourceLabels[order.source] || { label: order.source, className: '' };
|
||||
return (
|
||||
<TableRow key={order.id}>
|
||||
<TableCell className="font-mono text-xs max-w-[120px] truncate" title={order.orderNo}>
|
||||
{order.orderNo}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">{order.accountSequence}</TableCell>
|
||||
<TableCell>
|
||||
{renderBadge(
|
||||
order.type === 'BUY' ? '买入' : '卖出',
|
||||
order.type === 'BUY' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700',
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{renderBadge(status.label, status.className)}</TableCell>
|
||||
<TableCell>{renderBadge(source.label, source.className)}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">{formatDecimal(order.price, 8)}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">{formatDecimal(order.quantity, 2)}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">{formatDecimal(order.filledQuantity, 2)}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">{formatDecimal(order.totalAmount, 2)}</TableCell>
|
||||
<TableCell className="text-sm whitespace-nowrap">{formatDateTime(order.createdAt)}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{renderPagination(ordersData, ordersPage, setOrdersPage)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 成交明细 Tab */}
|
||||
<TabsContent value="trades" className="space-y-4">
|
||||
{/* 筛选栏 */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex gap-3">
|
||||
<Select value={tradeSource} onValueChange={(v) => { setTradeSource(v); setTradesPage(1); }}>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue placeholder="来源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">全部来源</SelectItem>
|
||||
<SelectItem value="USER">用户</SelectItem>
|
||||
<SelectItem value="MARKET_MAKER">做市商</SelectItem>
|
||||
<SelectItem value="DEX_BOT">DEX Bot</SelectItem>
|
||||
<SelectItem value="SYSTEM">系统</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 成交表格 */}
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>成交号</TableHead>
|
||||
<TableHead>买方</TableHead>
|
||||
<TableHead>卖方</TableHead>
|
||||
<TableHead className="text-right">价格</TableHead>
|
||||
<TableHead className="text-right">有效数量</TableHead>
|
||||
<TableHead className="text-right">原始数量</TableHead>
|
||||
<TableHead className="text-right">销毁量</TableHead>
|
||||
<TableHead className="text-right">成交额</TableHead>
|
||||
<TableHead className="text-right">手续费</TableHead>
|
||||
<TableHead>时间</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tradesLoading ? renderSkeletonRows(10) : !tradesData?.data?.length ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={10} className="text-center py-8 text-muted-foreground">
|
||||
暂无成交记录
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
tradesData.data.map((trade: AdminTrade) => (
|
||||
<TableRow key={trade.id}>
|
||||
<TableCell className="font-mono text-xs max-w-[120px] truncate" title={trade.tradeNo}>
|
||||
{trade.tradeNo}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">{trade.buyerSequence}</TableCell>
|
||||
<TableCell className="font-mono text-xs">{trade.sellerSequence}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">{formatDecimal(trade.price, 8)}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">{formatDecimal(trade.quantity, 2)}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">{formatDecimal(trade.originalQuantity, 2)}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm text-orange-600">{formatDecimal(trade.burnQuantity, 2)}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm">{formatDecimal(trade.amount, 2)}</TableCell>
|
||||
<TableCell className="text-right font-mono text-sm text-orange-600">{formatDecimal(trade.fee, 2)}</TableCell>
|
||||
<TableCell className="text-sm whitespace-nowrap">{formatDateTime(trade.createdAt)}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{renderPagination(tradesData, tradesPage, setTradesPage)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
FileSpreadsheet,
|
||||
SendHorizontal,
|
||||
HardDrive,
|
||||
Repeat,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ const menuItems = [
|
|||
{ name: '仪表盘', href: '/dashboard', icon: LayoutDashboard },
|
||||
{ name: '用户管理', href: '/users', icon: Users },
|
||||
{ name: '交易管理', href: '/trading', icon: ArrowLeftRight },
|
||||
{ name: '兑换记录', href: '/exchange-records', icon: Repeat },
|
||||
{ name: 'P2P划转', href: '/p2p-transfers', icon: SendHorizontal },
|
||||
{ name: '做市商管理', href: '/market-maker', icon: Bot },
|
||||
{ name: 'C2C Bot', href: '/c2c-bot', icon: Zap },
|
||||
|
|
|
|||
|
|
@ -76,6 +76,76 @@ export interface BurnRecordsResponse {
|
|||
total: number;
|
||||
}
|
||||
|
||||
// ==================== Admin 全局兑换记录类型 ====================
|
||||
|
||||
export interface AdminOrder {
|
||||
id: string;
|
||||
orderNo: string;
|
||||
accountSequence: string;
|
||||
type: 'BUY' | 'SELL';
|
||||
status: 'PENDING' | 'PARTIAL' | 'FILLED' | 'CANCELLED';
|
||||
source: 'USER' | 'MARKET_MAKER' | 'DEX_BOT' | 'SYSTEM';
|
||||
sourceLabel: string | null;
|
||||
price: string;
|
||||
quantity: string;
|
||||
filledQuantity: string;
|
||||
remainingQuantity: string;
|
||||
averagePrice: string;
|
||||
totalAmount: string;
|
||||
burnQuantity: string;
|
||||
burnMultiplier: string;
|
||||
effectiveQuantity: string;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
cancelledAt: string | null;
|
||||
}
|
||||
|
||||
export interface AdminTrade {
|
||||
id: string;
|
||||
tradeNo: string;
|
||||
buyOrderId: string;
|
||||
sellOrderId: string;
|
||||
buyerSequence: string;
|
||||
sellerSequence: string;
|
||||
price: string;
|
||||
quantity: string;
|
||||
originalQuantity: string;
|
||||
burnQuantity: string;
|
||||
amount: string;
|
||||
fee: string;
|
||||
buyerSource: string;
|
||||
sellerSource: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AdminPaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface AdminOrdersParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
type?: 'BUY' | 'SELL';
|
||||
status?: 'PENDING' | 'PARTIAL' | 'FILLED' | 'CANCELLED';
|
||||
source?: 'USER' | 'MARKET_MAKER' | 'DEX_BOT' | 'SYSTEM';
|
||||
search?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
export interface AdminTradesParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
source?: 'USER' | 'MARKET_MAKER' | 'DEX_BOT' | 'SYSTEM';
|
||||
search?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
export const tradingApi = {
|
||||
// 获取交易系统状态
|
||||
getTradingStatus: async (): Promise<TradingStatus> => {
|
||||
|
|
@ -133,6 +203,18 @@ export const tradingApi = {
|
|||
return response.data.data || response.data;
|
||||
},
|
||||
|
||||
// 获取全局订单列表 (admin)
|
||||
getAdminOrders: async (params: AdminOrdersParams): Promise<AdminPaginatedResponse<AdminOrder>> => {
|
||||
const response = await tradingClient.get('/admin/orders', { params });
|
||||
return response.data.data || response.data;
|
||||
},
|
||||
|
||||
// 获取全局成交记录 (admin)
|
||||
getAdminTrades: async (params: AdminTradesParams): Promise<AdminPaginatedResponse<AdminTrade>> => {
|
||||
const response = await tradingClient.get('/admin/trades', { params });
|
||||
return response.data.data || response.data;
|
||||
},
|
||||
|
||||
// 获取市场概览
|
||||
getMarketOverview: async (): Promise<{
|
||||
price: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { tradingApi } from '../api/trading.api';
|
||||
import type { AdminOrdersParams, AdminTradesParams } from '../api/trading.api';
|
||||
import { useToast } from '@/lib/hooks/use-toast';
|
||||
|
||||
export function useTradingStatus() {
|
||||
|
|
@ -97,6 +98,20 @@ export function useBurnRecords(
|
|||
});
|
||||
}
|
||||
|
||||
export function useAdminOrders(params: AdminOrdersParams) {
|
||||
return useQuery({
|
||||
queryKey: ['trading', 'admin-orders', params],
|
||||
queryFn: () => tradingApi.getAdminOrders(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useAdminTrades(params: AdminTradesParams) {
|
||||
return useQuery({
|
||||
queryKey: ['trading', 'admin-trades', params],
|
||||
queryFn: () => tradingApi.getAdminTrades(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useMarketOverview() {
|
||||
return useQuery({
|
||||
queryKey: ['trading', 'market-overview'],
|
||||
|
|
|
|||
Loading…
Reference in New Issue