rwadurian/frontend/mining-admin-web/src/app/(dashboard)/c2c-bot/page.tsx

340 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState } from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { PageHeader } from '@/components/layout/page-header';
import {
useC2cBotStatus,
useEnableC2cBot,
useDisableC2cBot,
useC2cBotOrders,
} from '@/features/c2c-bot/hooks/use-c2c-bot';
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 { Switch } from '@/components/ui/switch';
import {
RefreshCw,
Wallet,
BarChart3,
Copy,
Check,
ChevronLeft,
ChevronRight,
AlertCircle,
CheckCircle2,
Zap,
} from 'lucide-react';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
function formatNumber(value: string | undefined | null, decimals: number = 2) {
if (!value) return '0';
const num = parseFloat(value);
if (isNaN(num)) return '0';
return num.toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
}
function truncateAddress(addr: string | null | undefined, chars: number = 8) {
if (!addr) return '-';
if (addr.length <= chars * 2 + 2) return addr;
return `${addr.slice(0, chars + 2)}...${addr.slice(-chars)}`;
}
export default function C2cBotPage() {
const [orderPage, setOrderPage] = useState(1);
const [copied, setCopied] = useState(false);
const pageSize = 20;
const { data: status, isLoading: statusLoading, refetch } = useC2cBotStatus();
const { data: ordersData, isLoading: ordersLoading } = useC2cBotOrders(orderPage, pageSize);
const enableMutation = useEnableC2cBot();
const disableMutation = useDisableC2cBot();
const totalPages = ordersData ? Math.ceil(ordersData.total / pageSize) : 0;
const handleToggle = (checked: boolean) => {
if (checked) {
enableMutation.mutate();
} else {
disableMutation.mutate();
}
};
const handleCopyAddress = () => {
if (status?.hotWallet?.address) {
navigator.clipboard.writeText(status.hotWallet.address);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
if (statusLoading) {
return (
<div className="space-y-6">
<PageHeader title="C2C Bot 管理" description="管理 C2C 自动购买机器人" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Skeleton className="h-48" />
<Skeleton className="h-48" />
<Skeleton className="h-48" />
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<PageHeader title="C2C Bot 管理" description="管理 C2C 自动购买机器人" />
<Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 状态卡片 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Bot 开关 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Zap className="h-5 w-5" />
Bot
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<div className="flex items-center gap-3">
{status?.enabled ? (
<Badge className="bg-green-500"></Badge>
) : (
<Badge variant="secondary"></Badge>
)}
<Switch
checked={status?.enabled ?? false}
onCheckedChange={handleToggle}
disabled={enableMutation.isPending || disableMutation.isPending}
/>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
{status?.blockchainAvailable ? (
<div className="flex items-center gap-1 text-green-600">
<CheckCircle2 className="h-4 w-4" />
<span className="text-sm"></span>
</div>
) : (
<div className="flex items-center gap-1 text-red-500">
<AlertCircle className="h-4 w-4" />
<span className="text-sm"></span>
</div>
)}
</div>
<div className="text-xs text-muted-foreground bg-muted p-2 rounded">
Bot 10 dUSDT
</div>
</CardContent>
</Card>
{/* 热钱包 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Wallet className="h-5 w-5" />
</CardTitle>
<CardDescription>Bot 使 dUSDT </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="text-sm text-muted-foreground">dUSDT </p>
<p className="text-2xl font-bold">
{formatNumber(status?.hotWallet?.balance, 4)}
</p>
</div>
{status?.hotWallet?.address ? (
<>
<div>
<p className="text-sm text-muted-foreground mb-1"> (Kava EVM)</p>
<div className="flex items-center gap-2">
<code className="flex-1 text-xs bg-muted p-2 rounded break-all">
{status.hotWallet.address}
</code>
<Button variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={handleCopyAddress}>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="flex justify-center">
<div className="p-3 bg-white rounded-lg">
<QRCodeSVG value={status.hotWallet.address} size={120} />
</div>
</div>
<div className="text-xs text-yellow-600 bg-yellow-50 p-2 rounded">
<AlertCircle className="h-3 w-3 inline mr-1" />
dUSDT (Kava链) Bot
</div>
</>
) : (
<div className="text-sm text-muted-foreground"></div>
)}
</CardContent>
</Card>
{/* 统计 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
</CardTitle>
<CardDescription>Bot </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">
{status?.stats?.totalBotOrders ?? 0}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">
{formatNumber(status?.stats?.totalBotAmount, 4)}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 订单历史 */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
Bot
{ordersData && (
<Badge variant="secondary" className="text-xs">
{ordersData.total}
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{ordersLoading ? (
<div className="p-6 space-y-3">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-full" />
</div>
) : ordersData && ordersData.data.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead>Kava </TableHead>
<TableHead>TxHash</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ordersData.data.map((order) => (
<TableRow key={order.orderNo}>
<TableCell className="font-mono text-xs">
{order.orderNo}
</TableCell>
<TableCell>
<div className="text-sm">
{order.makerNickname || order.makerPhone || order.makerAccountSequence}
</div>
</TableCell>
<TableCell className="text-right font-mono">
{formatNumber(order.totalAmount, 4)}
</TableCell>
<TableCell className="text-right font-mono">
{formatNumber(order.quantity, 4)}
</TableCell>
<TableCell className="font-mono text-xs">
{truncateAddress(order.sellerKavaAddress, 6)}
</TableCell>
<TableCell className="font-mono text-xs">
{order.paymentTxHash ? (
<span className="text-blue-600" title={order.paymentTxHash}>
{truncateAddress(order.paymentTxHash, 6)}
</span>
) : (
'-'
)}
</TableCell>
<TableCell className="text-sm">
{order.completedAt
? format(new Date(order.completedAt), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })
: '-'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="p-8 text-center text-muted-foreground">
Bot
</div>
)}
{/* 分页 */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => setOrderPage((p) => Math.max(1, p - 1))}
disabled={orderPage <= 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="text-sm text-muted-foreground px-4">
{orderPage} / {totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setOrderPage((p) => Math.min(totalPages, p + 1))}
disabled={orderPage >= totalPages}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
)}
</CardContent>
</Card>
</div>
);
}