feat(admin): add trading system management UI and API
- Add trading system activate/deactivate endpoints to trading-service - Add trading management page to mining-admin-web with: - Trading system status display and control - Market overview (price, green points, circulation pool) - Burn progress visualization - Burn records list with filtering - Add trading-service proxy configuration to next.config.js - Add trading menu item to sidebar navigation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1e2d8d1df7
commit
8018fa5110
|
|
@ -1,12 +1,16 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { Controller, Get, Post, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||
import { TradingConfigRepository } from '../../infrastructure/persistence/repositories/trading-config.repository';
|
||||
import { Public } from '../../shared/guards/jwt-auth.guard';
|
||||
|
||||
@ApiTags('Admin')
|
||||
@Controller('admin')
|
||||
export class AdminController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly tradingConfigRepository: TradingConfigRepository,
|
||||
) {}
|
||||
|
||||
@Get('accounts/sync')
|
||||
@Public()
|
||||
|
|
@ -41,4 +45,93 @@ export class AdminController {
|
|||
total: accounts.length,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('trading/status')
|
||||
@Public()
|
||||
@ApiOperation({ summary: '获取交易系统状态' })
|
||||
@ApiResponse({ status: 200, description: '返回交易系统配置状态' })
|
||||
async getTradingStatus() {
|
||||
const config = await this.tradingConfigRepository.getConfig();
|
||||
if (!config) {
|
||||
return {
|
||||
initialized: false,
|
||||
isActive: false,
|
||||
activatedAt: null,
|
||||
message: '交易系统未初始化',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
initialized: true,
|
||||
isActive: config.isActive,
|
||||
activatedAt: config.activatedAt,
|
||||
totalShares: config.totalShares.toFixed(8),
|
||||
burnTarget: config.burnTarget.toFixed(8),
|
||||
burnPeriodMinutes: config.burnPeriodMinutes,
|
||||
minuteBurnRate: config.minuteBurnRate.toFixed(18),
|
||||
message: config.isActive ? '交易系统已激活' : '交易系统未激活',
|
||||
};
|
||||
}
|
||||
|
||||
@Post('trading/activate')
|
||||
@Public() // TODO: 生产环境应添加管理员权限验证
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '激活交易/销毁系统' })
|
||||
@ApiResponse({ status: 200, description: '交易系统已激活' })
|
||||
@ApiResponse({ status: 400, description: '交易系统未初始化' })
|
||||
async activateTrading() {
|
||||
const config = await this.tradingConfigRepository.getConfig();
|
||||
if (!config) {
|
||||
return {
|
||||
success: false,
|
||||
message: '交易系统未初始化,请先启动服务',
|
||||
};
|
||||
}
|
||||
|
||||
if (config.isActive) {
|
||||
return {
|
||||
success: true,
|
||||
message: '交易系统已处于激活状态',
|
||||
activatedAt: config.activatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
await this.tradingConfigRepository.activate();
|
||||
const updatedConfig = await this.tradingConfigRepository.getConfig();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '交易系统已激活,每分钟销毁已开始运行',
|
||||
activatedAt: updatedConfig?.activatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('trading/deactivate')
|
||||
@Public() // TODO: 生产环境应添加管理员权限验证
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '关闭交易/销毁系统' })
|
||||
@ApiResponse({ status: 200, description: '交易系统已关闭' })
|
||||
async deactivateTrading() {
|
||||
const config = await this.tradingConfigRepository.getConfig();
|
||||
if (!config) {
|
||||
return {
|
||||
success: false,
|
||||
message: '交易系统未初始化',
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.isActive) {
|
||||
return {
|
||||
success: true,
|
||||
message: '交易系统已处于关闭状态',
|
||||
};
|
||||
}
|
||||
|
||||
await this.tradingConfigRepository.deactivate();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '交易系统已关闭,每分钟销毁已暂停',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
NEXT_PUBLIC_API_URL=https://rwaapi.szaiai.com/api/v2/mining-admin
|
||||
TRADING_SERVICE_URL=https://rwaapi.szaiai.com/api/v2/trading
|
||||
NEXT_PUBLIC_APP_NAME=挖矿管理后台
|
||||
|
|
|
|||
|
|
@ -6,9 +6,17 @@ const nextConfig = {
|
|||
// NEXT_PUBLIC_API_URL 应该是后端服务的基础 URL,如 http://mining-admin-service:3023
|
||||
// 前端请求 /api/xxx 会被转发到 {API_URL}/api/v2/xxx
|
||||
const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3023';
|
||||
const tradingServiceUrl = process.env.TRADING_SERVICE_URL || 'http://localhost:3020';
|
||||
// 移除末尾可能存在的 /api/v2 避免重复
|
||||
const cleanUrl = apiBaseUrl.replace(/\/api\/v2\/?$/, '');
|
||||
const cleanTradingUrl = tradingServiceUrl.replace(/\/api\/v2\/?$/, '');
|
||||
return [
|
||||
// trading-service 路由
|
||||
{
|
||||
source: '/api/trading/:path*',
|
||||
destination: `${cleanTradingUrl}/api/v2/:path*`,
|
||||
},
|
||||
// mining-admin-service 路由 (默认)
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: `${cleanUrl}/api/v2/:path*`,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,373 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
import {
|
||||
useTradingStatus,
|
||||
useActivateTrading,
|
||||
useDeactivateTrading,
|
||||
useBurnStatus,
|
||||
useBurnRecords,
|
||||
useMarketOverview,
|
||||
} from '@/features/trading/hooks/use-trading';
|
||||
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';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import {
|
||||
Play,
|
||||
Pause,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
TrendingUp,
|
||||
Flame,
|
||||
DollarSign,
|
||||
Activity,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
|
||||
export default function TradingPage() {
|
||||
const { data: tradingStatus, isLoading: tradingLoading, refetch: refetchTrading } = useTradingStatus();
|
||||
const { data: burnStatus, isLoading: burnLoading, refetch: refetchBurn } = useBurnStatus();
|
||||
const { data: marketOverview, isLoading: marketLoading, refetch: refetchMarket } = useMarketOverview();
|
||||
const activateTrading = useActivateTrading();
|
||||
const deactivateTrading = useDeactivateTrading();
|
||||
|
||||
const [recordsPage, setRecordsPage] = useState(1);
|
||||
const [recordsFilter, setRecordsFilter] = useState<'ALL' | 'MINUTE_BURN' | 'SELL_BURN'>('ALL');
|
||||
const { data: burnRecords, isLoading: recordsLoading } = useBurnRecords(
|
||||
recordsPage,
|
||||
20,
|
||||
recordsFilter === 'ALL' ? undefined : recordsFilter
|
||||
);
|
||||
|
||||
const formatNumber = (value: string | undefined, decimals: number = 2) => {
|
||||
if (!value) return '0';
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num)) return '0';
|
||||
return num.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
||||
};
|
||||
|
||||
const formatPrice = (value: string | undefined) => {
|
||||
if (!value) return '0';
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num)) return '0';
|
||||
return num.toFixed(18);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
refetchTrading();
|
||||
refetchBurn();
|
||||
refetchMarket();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<PageHeader title="交易系统管理" description="管理交易/销毁系统的运行状态" />
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 交易系统状态卡片 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Activity className="h-5 w-5" />
|
||||
交易系统状态
|
||||
</CardTitle>
|
||||
<CardDescription>控制交易和每分钟自动销毁的运行状态</CardDescription>
|
||||
</div>
|
||||
{tradingLoading ? (
|
||||
<Skeleton className="h-6 w-16" />
|
||||
) : !tradingStatus?.initialized ? (
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
未初始化
|
||||
</Badge>
|
||||
) : tradingStatus?.isActive ? (
|
||||
<Badge variant="default" className="flex items-center gap-1 bg-green-500">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
运行中
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
<Pause className="h-3 w-3" />
|
||||
已停用
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{tradingLoading ? (
|
||||
<Skeleton className="h-32 w-full" />
|
||||
) : !tradingStatus?.initialized ? (
|
||||
<div className="text-center py-4 text-muted-foreground">
|
||||
<AlertCircle className="h-8 w-8 mx-auto mb-2 text-yellow-500" />
|
||||
<p>交易系统未初始化</p>
|
||||
<p className="text-sm">请确保 trading-service 已正常启动</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">总积分股</p>
|
||||
<p className="text-lg font-semibold">{formatNumber(tradingStatus.totalShares, 0)}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">销毁目标</p>
|
||||
<p className="text-lg font-semibold">{formatNumber(tradingStatus.burnTarget, 0)}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">销毁周期</p>
|
||||
<p className="text-lg font-semibold">{tradingStatus.burnPeriodMinutes?.toLocaleString()} 分钟</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">每分钟销毁率</p>
|
||||
<p className="text-lg font-semibold">{formatNumber(tradingStatus.minuteBurnRate, 8)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tradingStatus.activatedAt && (
|
||||
<div className="pt-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
激活时间: {new Date(tradingStatus.activatedAt).toLocaleString()} (
|
||||
{formatDistanceToNow(new Date(tradingStatus.activatedAt), { addSuffix: true, locale: zhCN })})
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end pt-4 border-t">
|
||||
{tradingStatus.isActive ? (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => deactivateTrading.mutate()}
|
||||
disabled={deactivateTrading.isPending}
|
||||
>
|
||||
<Pause className="h-4 w-4 mr-2" />
|
||||
{deactivateTrading.isPending ? '停用中...' : '停用交易系统'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => activateTrading.mutate()} disabled={activateTrading.isPending}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
{activateTrading.isPending ? '激活中...' : '激活交易系统'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 市场概览和销毁进度 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* 市场概览 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
市场概览
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{marketLoading ? (
|
||||
<Skeleton className="h-40 w-full" />
|
||||
) : !marketOverview ? (
|
||||
<div className="text-center py-4 text-muted-foreground">
|
||||
<p>无法获取市场数据</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign className="h-5 w-5 text-green-500" />
|
||||
<span className="text-sm text-muted-foreground">当前积分股价格</span>
|
||||
</div>
|
||||
<span className="font-mono text-lg font-semibold">{formatPrice(marketOverview.price)}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">绿积分</p>
|
||||
<p className="font-semibold">{formatNumber(marketOverview.greenPoints, 8)}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">流通池</p>
|
||||
<p className="font-semibold">{formatNumber(marketOverview.circulationPool, 8)}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">有效分母</p>
|
||||
<p className="font-semibold">{formatNumber(marketOverview.effectiveDenominator, 0)}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">销毁倍数</p>
|
||||
<p className="font-semibold">{formatNumber(marketOverview.burnMultiplier, 8)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 销毁进度 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Flame className="h-5 w-5 text-orange-500" />
|
||||
销毁进度
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{burnLoading ? (
|
||||
<Skeleton className="h-40 w-full" />
|
||||
) : !burnStatus ? (
|
||||
<div className="text-center py-4 text-muted-foreground">
|
||||
<p>无法获取销毁数据</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">销毁进度</span>
|
||||
<span className="font-semibold">{formatNumber(burnStatus.burnProgress, 4)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-3">
|
||||
<div
|
||||
className="bg-orange-500 h-3 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(parseFloat(burnStatus.burnProgress), 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">已销毁</p>
|
||||
<p className="font-semibold text-orange-500">{formatNumber(burnStatus.totalBurned, 8)}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">剩余目标</p>
|
||||
<p className="font-semibold">{formatNumber(burnStatus.remainingBurn, 8)}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">剩余分钟</p>
|
||||
<p className="font-semibold">{burnStatus.remainingMinutes?.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">每分钟销毁</p>
|
||||
<p className="font-semibold">{formatNumber(burnStatus.minuteBurnRate, 8)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{burnStatus.lastBurnMinute && (
|
||||
<p className="text-sm text-muted-foreground pt-2 border-t">
|
||||
最后销毁: {new Date(burnStatus.lastBurnMinute).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 销毁记录 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">销毁记录</CardTitle>
|
||||
<Select
|
||||
value={recordsFilter}
|
||||
onValueChange={(value) => {
|
||||
setRecordsFilter(value as 'ALL' | 'MINUTE_BURN' | 'SELL_BURN');
|
||||
setRecordsPage(1);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="筛选类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">全部</SelectItem>
|
||||
<SelectItem value="MINUTE_BURN">每分钟销毁</SelectItem>
|
||||
<SelectItem value="SELL_BURN">卖出销毁</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{recordsLoading ? (
|
||||
<div className="p-6">
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
) : !burnRecords?.data?.length ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>暂无销毁记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>时间</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>销毁量</TableHead>
|
||||
<TableHead>剩余目标</TableHead>
|
||||
<TableHead>来源账户</TableHead>
|
||||
<TableHead>备注</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{burnRecords.data.map((record) => (
|
||||
<TableRow key={record.id}>
|
||||
<TableCell className="font-mono text-sm">
|
||||
{new Date(record.burnMinute).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={record.sourceType === 'MINUTE_BURN' ? 'secondary' : 'default'}>
|
||||
{record.sourceType === 'MINUTE_BURN' ? '自动销毁' : '卖出销毁'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono">{formatNumber(record.burnAmount, 8)}</TableCell>
|
||||
<TableCell className="font-mono">{formatNumber(record.remainingTarget, 0)}</TableCell>
|
||||
<TableCell className="font-mono text-sm">
|
||||
{record.sourceAccountSeq || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground max-w-[200px] truncate">
|
||||
{record.memo || '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t">
|
||||
<p className="text-sm text-muted-foreground">共 {burnRecords.total} 条记录</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={recordsPage <= 1}
|
||||
onClick={() => setRecordsPage((p) => p - 1)}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={burnRecords.data.length < 20}
|
||||
onClick={() => setRecordsPage((p) => p + 1)}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -13,12 +13,14 @@ import {
|
|||
ClipboardList,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ArrowLeftRight,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const menuItems = [
|
||||
{ name: '仪表盘', href: '/dashboard', icon: LayoutDashboard },
|
||||
{ name: '用户管理', href: '/users', icon: Users },
|
||||
{ name: '交易管理', href: '/trading', icon: ArrowLeftRight },
|
||||
{ name: '配置管理', href: '/configs', icon: Settings },
|
||||
{ name: '系统账户', href: '/system-accounts', icon: Building2 },
|
||||
{ name: '报表统计', href: '/reports', icon: FileBarChart },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import { apiClient } from '@/lib/api/client';
|
||||
|
||||
export interface TradingStatus {
|
||||
initialized: boolean;
|
||||
isActive: boolean;
|
||||
activatedAt: string | null;
|
||||
totalShares?: string;
|
||||
burnTarget?: string;
|
||||
burnPeriodMinutes?: number;
|
||||
minuteBurnRate?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface BurnStatus {
|
||||
totalBurned: string;
|
||||
targetBurn: string;
|
||||
remainingBurn: string;
|
||||
burnProgress: string;
|
||||
minuteBurnRate: string;
|
||||
remainingMinutes: number;
|
||||
lastBurnMinute: string | null;
|
||||
}
|
||||
|
||||
export interface BurnRecord {
|
||||
id: string;
|
||||
burnMinute: string;
|
||||
burnAmount: string;
|
||||
remainingTarget: string;
|
||||
sourceType: 'MINUTE_BURN' | 'SELL_BURN';
|
||||
sourceAccountSeq: string | null;
|
||||
sourceOrderNo: string | null;
|
||||
memo: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface BurnRecordsResponse {
|
||||
data: BurnRecord[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const tradingApi = {
|
||||
// 获取交易系统状态
|
||||
getTradingStatus: async (): Promise<TradingStatus> => {
|
||||
const response = await apiClient.get('/trading/admin/trading/status');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 激活交易系统
|
||||
activateTrading: async (): Promise<{ success: boolean; message: string; activatedAt?: string }> => {
|
||||
const response = await apiClient.post('/trading/admin/trading/activate');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 关闭交易系统
|
||||
deactivateTrading: async (): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await apiClient.post('/trading/admin/trading/deactivate');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 获取销毁状态
|
||||
getBurnStatus: async (): Promise<BurnStatus> => {
|
||||
const response = await apiClient.get('/trading/burn/status');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 获取销毁记录
|
||||
getBurnRecords: async (
|
||||
page: number = 1,
|
||||
pageSize: number = 20,
|
||||
sourceType?: 'MINUTE_BURN' | 'SELL_BURN'
|
||||
): Promise<BurnRecordsResponse> => {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
pageSize: pageSize.toString(),
|
||||
});
|
||||
if (sourceType) {
|
||||
params.append('sourceType', sourceType);
|
||||
}
|
||||
const response = await apiClient.get(`/trading/burn/records?${params.toString()}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// 获取市场概览
|
||||
getMarketOverview: async (): Promise<{
|
||||
price: string;
|
||||
greenPoints: string;
|
||||
blackHoleAmount: string;
|
||||
circulationPool: string;
|
||||
effectiveDenominator: string;
|
||||
burnMultiplier: string;
|
||||
totalShares: string;
|
||||
burnTarget: string;
|
||||
burnProgress: string;
|
||||
}> => {
|
||||
const response = await apiClient.get('/trading/asset/market');
|
||||
return response.data.data;
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { tradingApi } from '../api/trading.api';
|
||||
import { useToast } from '@/lib/hooks/use-toast';
|
||||
|
||||
export function useTradingStatus() {
|
||||
return useQuery({
|
||||
queryKey: ['trading', 'status'],
|
||||
queryFn: () => tradingApi.getTradingStatus(),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useActivateTrading() {
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => tradingApi.activateTrading(),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['trading'] });
|
||||
toast({ title: data.message || '交易系统已激活', variant: 'success' as any });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: '激活失败',
|
||||
description: error?.response?.data?.message || '请稍后重试',
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeactivateTrading() {
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => tradingApi.deactivateTrading(),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['trading'] });
|
||||
toast({ title: data.message || '交易系统已关闭', variant: 'success' as any });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: '关闭失败',
|
||||
description: error?.response?.data?.message || '请稍后重试',
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useBurnStatus() {
|
||||
return useQuery({
|
||||
queryKey: ['trading', 'burn-status'],
|
||||
queryFn: () => tradingApi.getBurnStatus(),
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useBurnRecords(
|
||||
page: number = 1,
|
||||
pageSize: number = 20,
|
||||
sourceType?: 'MINUTE_BURN' | 'SELL_BURN'
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ['trading', 'burn-records', page, pageSize, sourceType],
|
||||
queryFn: () => tradingApi.getBurnRecords(page, pageSize, sourceType),
|
||||
});
|
||||
}
|
||||
|
||||
export function useMarketOverview() {
|
||||
return useQuery({
|
||||
queryKey: ['trading', 'market-overview'],
|
||||
queryFn: () => tradingApi.getMarketOverview(),
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './api/trading.api';
|
||||
export * from './hooks/use-trading';
|
||||
Loading…
Reference in New Issue