feat(mining-admin): implement complete system accounts feature
- Add system account types and display metadata - Create API layer with getList and getSummary endpoints - Add React Query hooks for data fetching - Create AccountCard, AccountsTable, SummaryCards components - Refactor page with tabs, refresh button, and error handling - Add Alert UI component Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
471702d562
commit
d43a70de93
|
|
@ -1,128 +1,137 @@
|
|||
'use client';
|
||||
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { formatDecimal } from '@/lib/utils/format';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Building2, Flame, ShoppingCart, Coins } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
interface SystemAccount {
|
||||
id: string;
|
||||
accountType: string;
|
||||
accountName: string;
|
||||
balance: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const accountIcons: Record<string, typeof Building2> = {
|
||||
DISTRIBUTION_POOL: Coins,
|
||||
BLACK_HOLE: Flame,
|
||||
CIRCULATION_POOL: ShoppingCart,
|
||||
SYSTEM_OPERATION: Building2,
|
||||
SYSTEM_PROVINCE: Building2,
|
||||
SYSTEM_CITY: Building2,
|
||||
};
|
||||
|
||||
const accountColors: Record<string, string> = {
|
||||
DISTRIBUTION_POOL: 'text-green-500',
|
||||
BLACK_HOLE: 'text-orange-500',
|
||||
CIRCULATION_POOL: 'text-blue-500',
|
||||
SYSTEM_OPERATION: 'text-purple-500',
|
||||
SYSTEM_PROVINCE: 'text-indigo-500',
|
||||
SYSTEM_CITY: 'text-pink-500',
|
||||
};
|
||||
import {
|
||||
useSystemAccountsSummary,
|
||||
useCategorizedAccounts,
|
||||
AccountCard,
|
||||
AccountsTable,
|
||||
SummaryCards,
|
||||
} from '@/features/system-accounts';
|
||||
|
||||
export default function SystemAccountsPage() {
|
||||
const { data: accounts, isLoading } = useQuery({
|
||||
queryKey: ['system-accounts'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/system-accounts');
|
||||
return response.data.data as SystemAccount[];
|
||||
},
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mainAccounts = accounts?.filter((a) =>
|
||||
['DISTRIBUTION_POOL', 'BLACK_HOLE', 'CIRCULATION_POOL'].includes(a.accountType)
|
||||
);
|
||||
const systemAccounts = accounts?.filter((a) =>
|
||||
['SYSTEM_OPERATION', 'SYSTEM_PROVINCE', 'SYSTEM_CITY'].includes(a.accountType)
|
||||
);
|
||||
const {
|
||||
data: summary,
|
||||
isLoading: summaryLoading,
|
||||
error: summaryError,
|
||||
} = useSystemAccountsSummary();
|
||||
|
||||
const {
|
||||
data: categorized,
|
||||
isLoading: accountsLoading,
|
||||
error: accountsError,
|
||||
total,
|
||||
} = useCategorizedAccounts();
|
||||
|
||||
const isLoading = summaryLoading || accountsLoading;
|
||||
const hasError = summaryError || accountsError;
|
||||
|
||||
const handleRefresh = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['system-accounts'] });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="系统账户" description="查看系统账户余额" />
|
||||
<PageHeader
|
||||
title="系统账户"
|
||||
description="查看系统账户余额和算力分布"
|
||||
actions={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{isLoading
|
||||
? [...Array(3)].map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<Skeleton className="h-20 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
: mainAccounts?.map((account) => {
|
||||
const Icon = accountIcons[account.accountType] || Building2;
|
||||
const color = accountColors[account.accountType] || 'text-gray-500';
|
||||
return (
|
||||
<Card key={account.id}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">{account.accountName}</p>
|
||||
<p className="text-2xl font-bold font-mono mt-1">{formatDecimal(account.balance, 4)}</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">{account.description}</p>
|
||||
</div>
|
||||
<Icon className={`h-8 w-8 ${color}`} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{hasError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
加载系统账户数据失败,请稍后重试。
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">系统分配账户</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>账户类型</TableHead>
|
||||
<TableHead>账户名称</TableHead>
|
||||
<TableHead className="text-right">余额</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
[...Array(3)].map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{[...Array(4)].map((_, j) => (
|
||||
<TableCell key={j}>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</TableCell>
|
||||
{/* Summary Cards */}
|
||||
<SummaryCards summary={summary} isLoading={summaryLoading} />
|
||||
|
||||
{/* Main Content with Tabs */}
|
||||
<Tabs defaultValue="overview" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">概览</TabsTrigger>
|
||||
<TabsTrigger value="all">全部账户</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
{/* Main Pools - Card Grid */}
|
||||
{categorized.mainPools.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">主要资金池</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{accountsLoading
|
||||
? [...Array(3)].map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<Skeleton className="h-20 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
: categorized.mainPools.map((account) => (
|
||||
<AccountCard key={account.accountType} account={account} />
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
systemAccounts?.map((account) => (
|
||||
<TableRow key={account.id}>
|
||||
<TableCell className="font-mono">{account.accountType}</TableCell>
|
||||
<TableCell>{account.accountName}</TableCell>
|
||||
<TableCell className="text-right font-mono">{formatDecimal(account.balance, 4)}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{account.description}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* System Distribution Accounts - Table */}
|
||||
<AccountsTable
|
||||
title="系统分配账户"
|
||||
accounts={categorized.systemAccounts}
|
||||
isLoading={accountsLoading}
|
||||
showSyncInfo
|
||||
/>
|
||||
|
||||
{/* Fixed Accounts if any */}
|
||||
{categorized.fixedAccounts.length > 0 && (
|
||||
<AccountsTable
|
||||
title="固定账户"
|
||||
accounts={categorized.fixedAccounts}
|
||||
isLoading={accountsLoading}
|
||||
showSyncInfo
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="all" className="space-y-6">
|
||||
{/* All Accounts in one table */}
|
||||
<AccountsTable
|
||||
title={`全部系统账户 (${total})`}
|
||||
accounts={[
|
||||
...categorized.mainPools,
|
||||
...categorized.systemAccounts,
|
||||
...categorized.fixedAccounts,
|
||||
...categorized.otherAccounts,
|
||||
]}
|
||||
isLoading={accountsLoading}
|
||||
showSyncInfo
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Alert.displayName = 'Alert';
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertTitle.displayName = 'AlertTitle';
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-sm [&_p]:leading-relaxed', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDescription.displayName = 'AlertDescription';
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { apiClient } from '@/lib/api/client';
|
||||
import type {
|
||||
SystemAccount,
|
||||
SystemAccountsResponse,
|
||||
SystemAccountsSummary,
|
||||
} from '@/types/system-account';
|
||||
|
||||
export const systemAccountsApi = {
|
||||
/**
|
||||
* Get all system accounts (merged local + synced data)
|
||||
*/
|
||||
getList: async (): Promise<SystemAccountsResponse> => {
|
||||
const response = await apiClient.get('/system-accounts');
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get system accounts summary with mining config and circulation pool
|
||||
*/
|
||||
getSummary: async (): Promise<SystemAccountsSummary> => {
|
||||
const response = await apiClient.get('/system-accounts/summary');
|
||||
return response.data.data;
|
||||
},
|
||||
};
|
||||
|
||||
// Helper to categorize accounts for display
|
||||
export function categorizeAccounts(accounts: SystemAccount[]) {
|
||||
const mainPools = ['DISTRIBUTION_POOL', 'BLACK_HOLE', 'CIRCULATION_POOL'];
|
||||
const systemAccounts = ['SYSTEM_OPERATION', 'SYSTEM_PROVINCE', 'SYSTEM_CITY'];
|
||||
const fixedAccounts = ['COST_ACCOUNT', 'OPERATION_ACCOUNT', 'HQ_COMMUNITY', 'RWAD_POOL_PENDING'];
|
||||
|
||||
return {
|
||||
mainPools: accounts.filter((a) => mainPools.includes(a.accountType)),
|
||||
systemAccounts: accounts.filter((a) => systemAccounts.includes(a.accountType)),
|
||||
fixedAccounts: accounts.filter((a) => fixedAccounts.includes(a.accountType)),
|
||||
otherAccounts: accounts.filter(
|
||||
(a) =>
|
||||
!mainPools.includes(a.accountType) &&
|
||||
!systemAccounts.includes(a.accountType) &&
|
||||
!fixedAccounts.includes(a.accountType)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
'use client';
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { formatDecimal } from '@/lib/utils/format';
|
||||
import { getAccountDisplayInfo, type SystemAccount } from '@/types/system-account';
|
||||
import { Coins, Flame, ShoppingCart, Building2, Wallet, Landmark, Box } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
const iconMap = {
|
||||
coins: Coins,
|
||||
flame: Flame,
|
||||
'shopping-cart': ShoppingCart,
|
||||
building: Building2,
|
||||
wallet: Wallet,
|
||||
landmark: Landmark,
|
||||
box: Box,
|
||||
};
|
||||
|
||||
interface AccountCardProps {
|
||||
account: SystemAccount;
|
||||
}
|
||||
|
||||
export function AccountCard({ account }: AccountCardProps) {
|
||||
const displayInfo = getAccountDisplayInfo(account.accountType);
|
||||
const Icon = iconMap[displayInfo.icon] || Building2;
|
||||
|
||||
// Use contributionBalance if available (synced), otherwise totalContribution (local)
|
||||
const balance = account.contributionBalance || account.totalContribution || '0';
|
||||
const displayName = account.name || displayInfo.label;
|
||||
|
||||
return (
|
||||
<Card className="hover:shadow-md transition-shadow">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="text-sm font-medium text-muted-foreground truncate">
|
||||
{displayName}
|
||||
</p>
|
||||
{account.source === 'synced' && (
|
||||
<Badge variant="outline" className="text-xs shrink-0">
|
||||
已同步
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-2xl font-bold font-mono mt-1">
|
||||
{formatDecimal(balance, 4)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-2 line-clamp-2">
|
||||
{account.description || displayInfo.description}
|
||||
</p>
|
||||
{account.contributionNeverExpires && (
|
||||
<Badge variant="secondary" className="mt-2 text-xs">
|
||||
永不过期
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg ${displayInfo.bgColor} shrink-0 ml-4`}>
|
||||
<Icon className={`h-6 w-6 ${displayInfo.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
'use client';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { formatDecimal } from '@/lib/utils/format';
|
||||
import { getAccountDisplayInfo, type SystemAccount } from '@/types/system-account';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
|
||||
interface AccountsTableProps {
|
||||
title: string;
|
||||
accounts: SystemAccount[];
|
||||
isLoading?: boolean;
|
||||
showSyncInfo?: boolean;
|
||||
}
|
||||
|
||||
export function AccountsTable({
|
||||
title,
|
||||
accounts,
|
||||
isLoading = false,
|
||||
showSyncInfo = false,
|
||||
}: AccountsTableProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
{title}
|
||||
{accounts.length > 0 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{accounts.length} 个账户
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">账户类型</TableHead>
|
||||
<TableHead>账户名称</TableHead>
|
||||
<TableHead className="text-right">余额/算力</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
{showSyncInfo && <TableHead>同步时间</TableHead>}
|
||||
<TableHead>描述</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
[...Array(3)].map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{[...Array(showSyncInfo ? 6 : 5)].map((_, j) => (
|
||||
<TableCell key={j}>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : accounts.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={showSyncInfo ? 6 : 5}
|
||||
className="text-center text-muted-foreground py-8"
|
||||
>
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
accounts.map((account) => {
|
||||
const displayInfo = getAccountDisplayInfo(account.accountType);
|
||||
const balance = account.contributionBalance || account.totalContribution || '0';
|
||||
|
||||
return (
|
||||
<TableRow key={account.accountType}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-block w-2 h-2 rounded-full ${displayInfo.color.replace('text-', 'bg-')}`}
|
||||
/>
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
{account.accountType}
|
||||
</code>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{account.name || displayInfo.label}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{formatDecimal(balance, 4)}
|
||||
{account.contributionNeverExpires && (
|
||||
<Badge variant="outline" className="ml-2 text-xs">
|
||||
永久
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={account.source === 'synced' ? 'default' : 'secondary'}
|
||||
className="text-xs"
|
||||
>
|
||||
{account.source === 'synced' ? 'CDC同步' : '本地'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
{showSyncInfo && (
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
{account.syncedAt
|
||||
? formatDistanceToNow(new Date(account.syncedAt), {
|
||||
addSuffix: true,
|
||||
locale: zhCN,
|
||||
})
|
||||
: '-'}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-muted-foreground text-sm max-w-[200px] truncate">
|
||||
{account.description || displayInfo.description}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { AccountCard } from './account-card';
|
||||
export { AccountsTable } from './accounts-table';
|
||||
export { SummaryCards } from './summary-cards';
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { formatDecimal, formatNumber } from '@/lib/utils/format';
|
||||
import type { SystemAccountsSummary } from '@/types/system-account';
|
||||
import { Database, Coins, Activity, RefreshCcw } from 'lucide-react';
|
||||
|
||||
interface SummaryCardsProps {
|
||||
summary: SystemAccountsSummary | undefined;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function SummaryCards({ summary, isLoading = false }: SummaryCardsProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<Skeleton className="h-20 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!summary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* System Accounts Count */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">本地账户</p>
|
||||
<p className="text-2xl font-bold mt-1">{summary.systemAccounts.count}</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
总算力: {formatDecimal(summary.systemAccounts.totalContribution, 4)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-blue-50">
|
||||
<Database className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Synced Contributions */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">同步算力</p>
|
||||
<p className="text-2xl font-bold mt-1">{summary.syncedContributions.count}</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
总余额: {formatDecimal(summary.syncedContributions.totalBalance, 4)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-green-50">
|
||||
<RefreshCcw className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Mining Config */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
挖矿配置
|
||||
{summary.miningConfig?.isActive && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
运行中
|
||||
</Badge>
|
||||
)}
|
||||
</p>
|
||||
{summary.miningConfig ? (
|
||||
<>
|
||||
<p className="text-2xl font-bold mt-1">
|
||||
第 {summary.miningConfig.currentEra} 纪
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
剩余分发: {formatDecimal(summary.miningConfig.remainingDistribution, 2)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground mt-2">未配置</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-purple-50">
|
||||
<Activity className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Circulation Pool */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">流通池</p>
|
||||
{summary.circulationPool ? (
|
||||
<>
|
||||
<p className="text-2xl font-bold mt-1">
|
||||
{formatDecimal(summary.circulationPool.totalShares, 2)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
总金额: {formatDecimal(summary.circulationPool.totalCash, 2)} USDT
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground mt-2">未初始化</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-amber-50">
|
||||
<Coins className="h-6 w-6 text-amber-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { systemAccountsApi, categorizeAccounts } from '../api/system-accounts.api';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* Hook to fetch all system accounts
|
||||
*/
|
||||
export function useSystemAccounts() {
|
||||
return useQuery({
|
||||
queryKey: ['system-accounts'],
|
||||
queryFn: systemAccountsApi.getList,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch system accounts summary (includes mining config and circulation pool)
|
||||
*/
|
||||
export function useSystemAccountsSummary() {
|
||||
return useQuery({
|
||||
queryKey: ['system-accounts', 'summary'],
|
||||
queryFn: systemAccountsApi.getSummary,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get categorized system accounts for display
|
||||
*/
|
||||
export function useCategorizedAccounts() {
|
||||
const { data, ...rest } = useSystemAccounts();
|
||||
|
||||
const categorized = useMemo(() => {
|
||||
if (!data?.accounts) {
|
||||
return {
|
||||
mainPools: [],
|
||||
systemAccounts: [],
|
||||
fixedAccounts: [],
|
||||
otherAccounts: [],
|
||||
};
|
||||
}
|
||||
return categorizeAccounts(data.accounts);
|
||||
}, [data?.accounts]);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
data: categorized,
|
||||
total: data?.total ?? 0,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// API
|
||||
export { systemAccountsApi, categorizeAccounts } from './api/system-accounts.api';
|
||||
|
||||
// Hooks
|
||||
export {
|
||||
useSystemAccounts,
|
||||
useSystemAccountsSummary,
|
||||
useCategorizedAccounts,
|
||||
} from './hooks/use-system-accounts';
|
||||
|
||||
// Components
|
||||
export { AccountCard, AccountsTable, SummaryCards } from './components';
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
// System account types based on backend SystemAccountsService
|
||||
|
||||
export type SystemAccountType =
|
||||
| 'DISTRIBUTION_POOL'
|
||||
| 'BLACK_HOLE'
|
||||
| 'CIRCULATION_POOL'
|
||||
| 'SYSTEM_OPERATION'
|
||||
| 'SYSTEM_PROVINCE'
|
||||
| 'SYSTEM_CITY'
|
||||
| 'COST_ACCOUNT'
|
||||
| 'OPERATION_ACCOUNT'
|
||||
| 'HQ_COMMUNITY'
|
||||
| 'RWAD_POOL_PENDING';
|
||||
|
||||
export type AccountSource = 'local' | 'synced';
|
||||
|
||||
export interface SystemAccount {
|
||||
accountType: SystemAccountType;
|
||||
name?: string;
|
||||
description?: string;
|
||||
totalContribution?: string;
|
||||
contributionBalance?: string;
|
||||
contributionNeverExpires?: boolean;
|
||||
syncedAt?: string;
|
||||
createdAt?: string;
|
||||
source: AccountSource;
|
||||
}
|
||||
|
||||
export interface SystemAccountsResponse {
|
||||
accounts: SystemAccount[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface MiningConfig {
|
||||
totalShares: string;
|
||||
distributionPool: string;
|
||||
remainingDistribution: string;
|
||||
currentEra: number;
|
||||
isActive: boolean;
|
||||
activatedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CirculationPool {
|
||||
totalShares: string;
|
||||
totalCash: string;
|
||||
}
|
||||
|
||||
export interface SystemAccountsSummary {
|
||||
systemAccounts: {
|
||||
count: number;
|
||||
totalContribution: string;
|
||||
};
|
||||
syncedContributions: {
|
||||
count: number;
|
||||
totalBalance: string;
|
||||
};
|
||||
miningConfig: MiningConfig | null;
|
||||
circulationPool: CirculationPool | null;
|
||||
}
|
||||
|
||||
// Account display metadata
|
||||
export interface AccountDisplayInfo {
|
||||
label: string;
|
||||
description: string;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
icon: 'coins' | 'flame' | 'shopping-cart' | 'building' | 'wallet' | 'landmark' | 'box';
|
||||
}
|
||||
|
||||
export const ACCOUNT_DISPLAY_INFO: Record<string, AccountDisplayInfo> = {
|
||||
DISTRIBUTION_POOL: {
|
||||
label: '分发池',
|
||||
description: '用于挖矿分发的代币池',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
icon: 'coins',
|
||||
},
|
||||
BLACK_HOLE: {
|
||||
label: '黑洞账户',
|
||||
description: '永久销毁的代币',
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50',
|
||||
icon: 'flame',
|
||||
},
|
||||
CIRCULATION_POOL: {
|
||||
label: '流通池',
|
||||
description: '市场流通中的代币',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
icon: 'shopping-cart',
|
||||
},
|
||||
SYSTEM_OPERATION: {
|
||||
label: '系统运营账户',
|
||||
description: '系统运营分配账户',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50',
|
||||
icon: 'building',
|
||||
},
|
||||
SYSTEM_PROVINCE: {
|
||||
label: '省级系统账户',
|
||||
description: '无授权商省级账户',
|
||||
color: 'text-indigo-600',
|
||||
bgColor: 'bg-indigo-50',
|
||||
icon: 'landmark',
|
||||
},
|
||||
SYSTEM_CITY: {
|
||||
label: '市级系统账户',
|
||||
description: '无授权商市级账户',
|
||||
color: 'text-pink-600',
|
||||
bgColor: 'bg-pink-50',
|
||||
icon: 'landmark',
|
||||
},
|
||||
COST_ACCOUNT: {
|
||||
label: '成本账户',
|
||||
description: '成本分配账户',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'bg-slate-50',
|
||||
icon: 'wallet',
|
||||
},
|
||||
OPERATION_ACCOUNT: {
|
||||
label: '运营账户',
|
||||
description: '12% 运营收入账户',
|
||||
color: 'text-teal-600',
|
||||
bgColor: 'bg-teal-50',
|
||||
icon: 'building',
|
||||
},
|
||||
HQ_COMMUNITY: {
|
||||
label: '总部社区账户',
|
||||
description: '总部社区分配账户',
|
||||
color: 'text-cyan-600',
|
||||
bgColor: 'bg-cyan-50',
|
||||
icon: 'building',
|
||||
},
|
||||
RWAD_POOL_PENDING: {
|
||||
label: 'RWAD 待注入池',
|
||||
description: 'RWAD 代币待注入池',
|
||||
color: 'text-amber-600',
|
||||
bgColor: 'bg-amber-50',
|
||||
icon: 'box',
|
||||
},
|
||||
};
|
||||
|
||||
// Helper function to get display info
|
||||
export function getAccountDisplayInfo(accountType: string): AccountDisplayInfo {
|
||||
return ACCOUNT_DISPLAY_INFO[accountType] || {
|
||||
label: accountType,
|
||||
description: '',
|
||||
color: 'text-gray-600',
|
||||
bgColor: 'bg-gray-50',
|
||||
icon: 'building',
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue