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

600 lines
25 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, useEffect } from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { PageHeader } from '@/components/layout/page-header';
import { useConfigs, useUpdateConfig, useTransferEnabled, useSetTransferEnabled, useMiningStatus, useActivateMining, useDeactivateMining, useP2pTransferFee, useSetP2pTransferFee, usePoolAccounts, usePoolAccountBalance, usePoolAccountBlockchainWithdraw, usePoolAccountCentralizedDeposit } from '@/features/configs/hooks/use-configs';
import type { PoolAccountInfo, PoolAccountBalance } from '@/features/configs/api/configs.api';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
DialogTrigger,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { Skeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge';
import { Pencil, Save, X, Play, Pause, AlertCircle, CheckCircle2, Loader2, Wallet, PlusCircle, MinusCircle, Copy, Check, Flame, HardHat } from 'lucide-react';
import type { SystemConfig } from '@/types/config';
// 根据 blockchainPoolType 映射视觉属性
const poolVisualMap: Record<string, { icon: typeof Flame; color: string }> = {
BURN_POOL: { icon: Flame, color: 'orange' },
MINING_POOL: { icon: HardHat, color: 'blue' },
};
const categoryLabels: Record<string, string> = {
mining: '挖矿配置',
trading: '交易配置',
distribution: '分配配置',
system: '系统配置',
};
// 池账户卡片子组件
function PoolAccountCard({ pool }: { pool: PoolAccountInfo }) {
const { data: balance, isLoading } = usePoolAccountBalance(pool.walletName);
const blockchainWithdrawMutation = usePoolAccountBlockchainWithdraw();
const centralizedDepositMutation = usePoolAccountCentralizedDeposit();
const [withdrawAddress, setWithdrawAddress] = useState('');
const [withdrawAmount, setWithdrawAmount] = useState('');
const [depositAmount, setDepositAmount] = useState('');
const [copiedAddress, setCopiedAddress] = useState<string | null>(null);
const visual = poolVisualMap[pool.blockchainPoolType] || { icon: Wallet, color: 'gray' };
const Icon = visual.icon;
const colorClass = visual.color === 'orange' ? 'text-orange-500' : visual.color === 'blue' ? 'text-blue-500' : 'text-gray-500';
const handleCopyAddress = async (address: string) => {
await navigator.clipboard.writeText(address);
setCopiedAddress(address);
setTimeout(() => setCopiedAddress(null), 2000);
};
const formatBalance = (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 });
};
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<Icon className={`h-5 w-5 ${colorClass}`} />
{pool.name}
</CardTitle>
<Badge variant="outline" className="text-xs">
2-of-3
</Badge>
</div>
<CardDescription>
: {pool.walletName}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{isLoading ? (
<Skeleton className="h-20 w-full" />
) : (
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">{formatBalance(balance?.balance)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className={`text-2xl font-bold ${colorClass}`}>{formatBalance(balance?.availableBalance)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-lg text-muted-foreground">{formatBalance(balance?.frozenBalance)}</p>
</div>
</div>
)}
<div className="flex gap-2 pt-4 border-t">
{/* 充值(中心化 + 区块链) */}
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<PlusCircle className="h-4 w-4 mr-1" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle> {pool.name}</DialogTitle>
<DialogDescription>{pool.name}</DialogDescription>
</DialogHeader>
<Tabs defaultValue="blockchain" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="centralized" disabled></TabsTrigger>
<TabsTrigger value="blockchain"></TabsTrigger>
</TabsList>
<TabsContent value="centralized" className="space-y-4 pt-4">
<div>
<Label></Label>
<Input
type="number"
value={depositAmount}
onChange={(e) => setDepositAmount(e.target.value)}
placeholder="请输入金额"
/>
</div>
<Button
className="w-full"
onClick={() => {
centralizedDepositMutation.mutate({
walletName: pool.walletName,
amount: depositAmount,
});
setDepositAmount('');
}}
disabled={centralizedDepositMutation.isPending || !depositAmount}
>
{centralizedDepositMutation.isPending ? '处理中...' : '确认充值'}
</Button>
</TabsContent>
<TabsContent value="blockchain" className="space-y-4 pt-4">
<div className="text-sm text-muted-foreground text-center">
<strong>fUSDT</strong>
</div>
{balance?.walletAddress ? (
<div className="flex flex-col items-center space-y-4">
<div className="p-4 bg-white rounded-lg">
<QRCodeSVG value={balance.walletAddress} size={180} />
</div>
<div className="w-full">
<Label className="text-xs text-muted-foreground"> (Kava EVM)</Label>
<div className="flex items-center gap-2 mt-1">
<code className="flex-1 text-xs bg-muted p-2 rounded break-all">
{balance.walletAddress}
</code>
<Button
size="sm"
variant="outline"
onClick={() => handleCopyAddress(balance.walletAddress)}
>
{copiedAddress === balance.walletAddress ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="text-xs text-yellow-600 bg-yellow-50 p-2 rounded w-full">
<AlertCircle className="h-3 w-3 inline mr-1" />
12
</div>
</div>
) : (
<div className="text-center text-muted-foreground py-4">
<AlertCircle className="h-6 w-6 mx-auto mb-2 text-yellow-500" />
<p className="text-sm"></p>
</div>
)}
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
{/* 区块链提现 */}
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<MinusCircle className="h-4 w-4 mr-1" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle> {pool.name}</DialogTitle>
<DialogDescription> fUSDT</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label> (Kava EVM)</Label>
<Input
value={withdrawAddress}
onChange={(e) => setWithdrawAddress(e.target.value)}
placeholder="0x..."
/>
</div>
<div>
<Label></Label>
<Input
type="number"
value={withdrawAmount}
onChange={(e) => setWithdrawAmount(e.target.value)}
placeholder="请输入金额"
/>
</div>
<Button
className="w-full"
onClick={() => {
blockchainWithdrawMutation.mutate({
walletName: pool.walletName,
toAddress: withdrawAddress,
amount: withdrawAmount,
});
setWithdrawAddress('');
setWithdrawAmount('');
}}
disabled={
blockchainWithdrawMutation.isPending ||
!withdrawAddress ||
!withdrawAmount ||
!withdrawAddress.startsWith('0x')
}
>
{blockchainWithdrawMutation.isPending ? '链上转账中...' : '确认区块链提现'}
</Button>
<div className="text-xs text-yellow-600 bg-yellow-50 p-2 rounded">
<AlertCircle className="h-3 w-3 inline mr-1" />
2-of-3 {pool.name}
</div>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</CardContent>
</Card>
);
}
export default function ConfigsPage() {
const { data: configs, isLoading } = useConfigs();
const { data: transferEnabled, isLoading: transferLoading } = useTransferEnabled();
const { data: miningStatus, isLoading: miningLoading } = useMiningStatus();
const updateConfig = useUpdateConfig();
const setTransferEnabled = useSetTransferEnabled();
const activateMining = useActivateMining();
const deactivateMining = useDeactivateMining();
const { data: feeConfig, isLoading: feeLoading } = useP2pTransferFee();
const setP2pTransferFee = useSetP2pTransferFee();
const { data: poolAccounts, isLoading: poolAccountsLoading } = usePoolAccounts();
const [editingConfig, setEditingConfig] = useState<SystemConfig | null>(null);
const [editValue, setEditValue] = useState('');
const [feeValue, setFeeValue] = useState('');
const [minAmountValue, setMinAmountValue] = useState('');
useEffect(() => {
if (feeConfig) {
setFeeValue(feeConfig.fee);
setMinAmountValue(feeConfig.minTransferAmount);
}
}, [feeConfig]);
const handleEdit = (config: SystemConfig) => {
setEditingConfig(config);
setEditValue(config.configValue);
};
const handleSave = () => {
if (editingConfig) {
updateConfig.mutate({ key: editingConfig.configKey, value: editValue });
setEditingConfig(null);
}
};
const handleTransferToggle = (checked: boolean) => {
setTransferEnabled.mutate(checked);
};
const groupedConfigs = configs?.reduce(
(acc, config) => {
const category = config.category || 'system';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(config);
return acc;
},
{} as Record<string, SystemConfig[]>
);
const formatNumber = (value: string) => {
return parseFloat(value).toLocaleString();
};
return (
<div className="space-y-6">
<PageHeader title="配置管理" description="管理系统配置参数" />
{/* 挖矿状态卡片 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</div>
{miningLoading ? (
<Skeleton className="h-6 w-16" />
) : miningStatus?.error ? (
<Badge variant="destructive" className="flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
</Badge>
) : miningStatus?.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>
{miningLoading ? (
<Skeleton className="h-32 w-full" />
) : miningStatus?.error ? (
<div className="text-center py-4 text-muted-foreground">
<AlertCircle className="h-8 w-8 mx-auto mb-2 text-destructive" />
<p></p>
<p className="text-sm">{miningStatus.error}</p>
</div>
) : !miningStatus?.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"> seed </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"> {miningStatus.currentEra} </p>
</div>
<div className="space-y-1">
<p className="text-sm text-muted-foreground"></p>
<p className="text-lg font-semibold">{formatNumber(miningStatus.remainingDistribution)}</p>
</div>
<div className="space-y-1">
<p className="text-sm text-muted-foreground"></p>
<p className="text-lg font-semibold">{formatNumber(miningStatus.secondDistribution)}</p>
</div>
<div className="space-y-1">
<p className="text-sm text-muted-foreground"></p>
<p className="text-lg font-semibold">{miningStatus.accountCount}</p>
</div>
</div>
{/* 算力同步状态提示 */}
{miningStatus.contributionSyncStatus && !miningStatus.contributionSyncStatus.isSynced && (
<div className="flex items-center gap-2 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
<Loader2 className="h-4 w-4 animate-spin text-yellow-600" />
<div className="flex-1">
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">...</p>
<p className="text-xs text-yellow-600 dark:text-yellow-400">
mining-service : {formatNumber(miningStatus.contributionSyncStatus.miningNetworkTotal)} /
contribution-service : {formatNumber(miningStatus.contributionSyncStatus.networkTotalContribution)}
</p>
</div>
</div>
)}
<div className="flex justify-end pt-4 border-t">
{miningStatus.isActive ? (
<Button
variant="destructive"
onClick={() => deactivateMining.mutate()}
disabled={deactivateMining.isPending}
>
<Pause className="h-4 w-4 mr-2" />
{deactivateMining.isPending ? '停用中...' : '停用挖矿'}
</Button>
) : (
<Button
onClick={() => activateMining.mutate()}
disabled={activateMining.isPending || (miningStatus.contributionSyncStatus && !miningStatus.contributionSyncStatus.isSynced)}
>
{miningStatus.contributionSyncStatus && !miningStatus.contributionSyncStatus.isSynced ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
{activateMining.isPending ? '激活中...' : '激活挖矿'}
</>
)}
</Button>
)}
</div>
</div>
)}
</CardContent>
</Card>
{/* 池账户管理 — 动态渲染 */}
{poolAccountsLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card><CardContent className="p-6"><Skeleton className="h-48 w-full" /></CardContent></Card>
<Card><CardContent className="p-6"><Skeleton className="h-48 w-full" /></CardContent></Card>
</div>
) : poolAccounts && poolAccounts.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{poolAccounts.map((pool) => (
<PoolAccountCard key={pool.walletName} pool={pool} />
))}
</div>
) : null}
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div>
<p className="font-medium"></p>
<p className="text-sm text-muted-foreground"></p>
</div>
{transferLoading ? (
<Skeleton className="h-6 w-11" />
) : (
<Switch
checked={transferEnabled}
onCheckedChange={handleTransferToggle}
disabled={setTransferEnabled.isPending}
/>
)}
</div>
</CardContent>
</Card>
{/* P2P划转手续费设置 */}
<Card>
<CardHeader>
<CardTitle className="text-lg">P2P划转手续费设置</CardTitle>
<CardDescription>P2P转账</CardDescription>
</CardHeader>
<CardContent>
{feeLoading ? (
<Skeleton className="h-32 w-full" />
) : (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> ()</Label>
<Input
type="number"
min="0"
step="0.01"
value={feeValue}
onChange={(e) => setFeeValue(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label> ()</Label>
<Input
type="number"
min="0"
step="0.01"
value={minAmountValue}
onChange={(e) => setMinAmountValue(e.target.value)}
/>
</div>
</div>
{parseFloat(minAmountValue) <= parseFloat(feeValue) && feeValue !== '' && minAmountValue !== '' && (
<p className="text-sm text-red-500"></p>
)}
<div className="flex justify-end">
<Button
onClick={() => setP2pTransferFee.mutate({ fee: feeValue, minTransferAmount: minAmountValue })}
disabled={
setP2pTransferFee.isPending ||
parseFloat(minAmountValue) <= parseFloat(feeValue) ||
isNaN(parseFloat(feeValue)) ||
isNaN(parseFloat(minAmountValue))
}
>
<Save className="h-4 w-4 mr-2" />
{setP2pTransferFee.isPending ? '保存中...' : '保存'}
</Button>
</div>
</div>
)}
</CardContent>
</Card>
{isLoading ? (
<Card>
<CardContent className="p-6">
<Skeleton className="h-64 w-full" />
</CardContent>
</Card>
) : (
Object.entries(groupedConfigs || {}).map(([category, configs]) => (
<Card key={category}>
<CardHeader>
<CardTitle className="text-lg">{categoryLabels[category] || category}</CardTitle>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{configs.map((config) => (
<TableRow key={config.configKey}>
<TableCell className="font-mono">{config.configKey}</TableCell>
<TableCell className="text-muted-foreground">{config.description}</TableCell>
<TableCell className="font-mono">{config.configValue}</TableCell>
<TableCell>
{config.isEditable && (
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => handleEdit(config)}>
<Pencil className="h-4 w-4" />
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
))
)}
<Dialog open={!!editingConfig} onOpenChange={() => setEditingConfig(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label></Label>
<Input value={editingConfig?.configKey || ''} disabled />
</div>
<div className="space-y-2">
<Label></Label>
<p className="text-sm text-muted-foreground">{editingConfig?.description}</p>
</div>
<div className="space-y-2">
<Label></Label>
<Input value={editValue} onChange={(e) => setEditValue(e.target.value)} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditingConfig(null)}>
<X className="h-4 w-4 mr-2" />
</Button>
<Button onClick={handleSave} disabled={updateConfig.isPending}>
<Save className="h-4 w-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}