600 lines
25 KiB
TypeScript
600 lines
25 KiB
TypeScript
'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>
|
||
);
|
||
}
|