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';
|
'use client';
|
||||||
|
|
||||||
import { PageHeader } from '@/components/layout/page-header';
|
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 { 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 {
|
import {
|
||||||
id: string;
|
useSystemAccountsSummary,
|
||||||
accountType: string;
|
useCategorizedAccounts,
|
||||||
accountName: string;
|
AccountCard,
|
||||||
balance: string;
|
AccountsTable,
|
||||||
description: string;
|
SummaryCards,
|
||||||
}
|
} from '@/features/system-accounts';
|
||||||
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SystemAccountsPage() {
|
export default function SystemAccountsPage() {
|
||||||
const { data: accounts, isLoading } = useQuery({
|
const queryClient = useQueryClient();
|
||||||
queryKey: ['system-accounts'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await apiClient.get('/system-accounts');
|
|
||||||
return response.data.data as SystemAccount[];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mainAccounts = accounts?.filter((a) =>
|
const {
|
||||||
['DISTRIBUTION_POOL', 'BLACK_HOLE', 'CIRCULATION_POOL'].includes(a.accountType)
|
data: summary,
|
||||||
);
|
isLoading: summaryLoading,
|
||||||
const systemAccounts = accounts?.filter((a) =>
|
error: summaryError,
|
||||||
['SYSTEM_OPERATION', 'SYSTEM_PROVINCE', 'SYSTEM_CITY'].includes(a.accountType)
|
} = 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 (
|
return (
|
||||||
<div className="space-y-6">
|
<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">
|
{hasError && (
|
||||||
{isLoading
|
<Alert variant="destructive">
|
||||||
? [...Array(3)].map((_, i) => (
|
<AlertCircle className="h-4 w-4" />
|
||||||
<Card key={i}>
|
<AlertDescription>
|
||||||
<CardContent className="p-6">
|
加载系统账户数据失败,请稍后重试。
|
||||||
<Skeleton className="h-20 w-full" />
|
</AlertDescription>
|
||||||
</CardContent>
|
</Alert>
|
||||||
</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>
|
|
||||||
|
|
||||||
<Card>
|
{/* Summary Cards */}
|
||||||
<CardHeader>
|
<SummaryCards summary={summary} isLoading={summaryLoading} />
|
||||||
<CardTitle className="text-lg">系统分配账户</CardTitle>
|
|
||||||
</CardHeader>
|
{/* Main Content with Tabs */}
|
||||||
<CardContent className="p-0">
|
<Tabs defaultValue="overview" className="space-y-4">
|
||||||
<Table>
|
<TabsList>
|
||||||
<TableHeader>
|
<TabsTrigger value="overview">概览</TabsTrigger>
|
||||||
<TableRow>
|
<TabsTrigger value="all">全部账户</TabsTrigger>
|
||||||
<TableHead>账户类型</TableHead>
|
</TabsList>
|
||||||
<TableHead>账户名称</TableHead>
|
|
||||||
<TableHead className="text-right">余额</TableHead>
|
<TabsContent value="overview" className="space-y-6">
|
||||||
<TableHead>描述</TableHead>
|
{/* Main Pools - Card Grid */}
|
||||||
</TableRow>
|
{categorized.mainPools.length > 0 && (
|
||||||
</TableHeader>
|
<div>
|
||||||
<TableBody>
|
<h3 className="text-lg font-semibold mb-4">主要资金池</h3>
|
||||||
{isLoading ? (
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
[...Array(3)].map((_, i) => (
|
{accountsLoading
|
||||||
<TableRow key={i}>
|
? [...Array(3)].map((_, i) => (
|
||||||
{[...Array(4)].map((_, j) => (
|
<Card key={i}>
|
||||||
<TableCell key={j}>
|
<CardContent className="p-6">
|
||||||
<Skeleton className="h-4 w-full" />
|
<Skeleton className="h-20 w-full" />
|
||||||
</TableCell>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
: categorized.mainPools.map((account) => (
|
||||||
|
<AccountCard key={account.accountType} account={account} />
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</div>
|
||||||
))
|
</div>
|
||||||
) : (
|
)}
|
||||||
systemAccounts?.map((account) => (
|
|
||||||
<TableRow key={account.id}>
|
{/* System Distribution Accounts - Table */}
|
||||||
<TableCell className="font-mono">{account.accountType}</TableCell>
|
<AccountsTable
|
||||||
<TableCell>{account.accountName}</TableCell>
|
title="系统分配账户"
|
||||||
<TableCell className="text-right font-mono">{formatDecimal(account.balance, 4)}</TableCell>
|
accounts={categorized.systemAccounts}
|
||||||
<TableCell className="text-muted-foreground">{account.description}</TableCell>
|
isLoading={accountsLoading}
|
||||||
</TableRow>
|
showSyncInfo
|
||||||
))
|
/>
|
||||||
)}
|
|
||||||
</TableBody>
|
{/* Fixed Accounts if any */}
|
||||||
</Table>
|
{categorized.fixedAccounts.length > 0 && (
|
||||||
</CardContent>
|
<AccountsTable
|
||||||
</Card>
|
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>
|
</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