From d43a70de930660e849373aeb5b3dc7716b607418 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 13 Jan 2026 20:27:59 -0800 Subject: [PATCH] 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 --- .../app/(dashboard)/system-accounts/page.tsx | 231 +++++++++--------- .../src/components/ui/alert.tsx | 59 +++++ .../api/system-accounts.api.ts | 43 ++++ .../components/account-card.tsx | 65 +++++ .../components/accounts-table.tsx | 134 ++++++++++ .../system-accounts/components/index.ts | 3 + .../components/summary-cards.tsx | 132 ++++++++++ .../hooks/use-system-accounts.ts | 48 ++++ .../src/features/system-accounts/index.ts | 12 + .../src/types/system-account.ts | 152 ++++++++++++ 10 files changed, 768 insertions(+), 111 deletions(-) create mode 100644 frontend/mining-admin-web/src/components/ui/alert.tsx create mode 100644 frontend/mining-admin-web/src/features/system-accounts/api/system-accounts.api.ts create mode 100644 frontend/mining-admin-web/src/features/system-accounts/components/account-card.tsx create mode 100644 frontend/mining-admin-web/src/features/system-accounts/components/accounts-table.tsx create mode 100644 frontend/mining-admin-web/src/features/system-accounts/components/index.ts create mode 100644 frontend/mining-admin-web/src/features/system-accounts/components/summary-cards.tsx create mode 100644 frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts create mode 100644 frontend/mining-admin-web/src/features/system-accounts/index.ts create mode 100644 frontend/mining-admin-web/src/types/system-account.ts diff --git a/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/page.tsx index 2bef049e..035576c8 100644 --- a/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/page.tsx +++ b/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/page.tsx @@ -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 = { - DISTRIBUTION_POOL: Coins, - BLACK_HOLE: Flame, - CIRCULATION_POOL: ShoppingCart, - SYSTEM_OPERATION: Building2, - SYSTEM_PROVINCE: Building2, - SYSTEM_CITY: Building2, -}; - -const accountColors: Record = { - 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 (
- + + + 刷新 + + } + /> -
- {isLoading - ? [...Array(3)].map((_, i) => ( - - - - - - )) - : mainAccounts?.map((account) => { - const Icon = accountIcons[account.accountType] || Building2; - const color = accountColors[account.accountType] || 'text-gray-500'; - return ( - - -
-
-

{account.accountName}

-

{formatDecimal(account.balance, 4)}

-

{account.description}

-
- -
-
-
- ); - })} -
+ {hasError && ( + + + + 加载系统账户数据失败,请稍后重试。 + + + )} - - - 系统分配账户 - - - - - - 账户类型 - 账户名称 - 余额 - 描述 - - - - {isLoading ? ( - [...Array(3)].map((_, i) => ( - - {[...Array(4)].map((_, j) => ( - - - + {/* Summary Cards */} + + + {/* Main Content with Tabs */} + + + 概览 + 全部账户 + + + + {/* Main Pools - Card Grid */} + {categorized.mainPools.length > 0 && ( +
+

主要资金池

+
+ {accountsLoading + ? [...Array(3)].map((_, i) => ( + + + + + + )) + : categorized.mainPools.map((account) => ( + ))} - - )) - ) : ( - systemAccounts?.map((account) => ( - - {account.accountType} - {account.accountName} - {formatDecimal(account.balance, 4)} - {account.description} - - )) - )} - -
-
-
+
+ + )} + + {/* System Distribution Accounts - Table */} + + + {/* Fixed Accounts if any */} + {categorized.fixedAccounts.length > 0 && ( + + )} + + + + {/* All Accounts in one table */} + + + ); } diff --git a/frontend/mining-admin-web/src/components/ui/alert.tsx b/frontend/mining-admin-web/src/components/ui/alert.tsx new file mode 100644 index 00000000..d2b59ccd --- /dev/null +++ b/frontend/mining-admin-web/src/components/ui/alert.tsx @@ -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 & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = 'Alert'; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = 'AlertTitle'; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = 'AlertDescription'; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/frontend/mining-admin-web/src/features/system-accounts/api/system-accounts.api.ts b/frontend/mining-admin-web/src/features/system-accounts/api/system-accounts.api.ts new file mode 100644 index 00000000..3d76fe24 --- /dev/null +++ b/frontend/mining-admin-web/src/features/system-accounts/api/system-accounts.api.ts @@ -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 => { + const response = await apiClient.get('/system-accounts'); + return response.data.data; + }, + + /** + * Get system accounts summary with mining config and circulation pool + */ + getSummary: async (): Promise => { + 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) + ), + }; +} diff --git a/frontend/mining-admin-web/src/features/system-accounts/components/account-card.tsx b/frontend/mining-admin-web/src/features/system-accounts/components/account-card.tsx new file mode 100644 index 00000000..c42ba2b0 --- /dev/null +++ b/frontend/mining-admin-web/src/features/system-accounts/components/account-card.tsx @@ -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 ( + + +
+
+
+

+ {displayName} +

+ {account.source === 'synced' && ( + + 已同步 + + )} +
+

+ {formatDecimal(balance, 4)} +

+

+ {account.description || displayInfo.description} +

+ {account.contributionNeverExpires && ( + + 永不过期 + + )} +
+
+ +
+
+
+
+ ); +} diff --git a/frontend/mining-admin-web/src/features/system-accounts/components/accounts-table.tsx b/frontend/mining-admin-web/src/features/system-accounts/components/accounts-table.tsx new file mode 100644 index 00000000..48ee9832 --- /dev/null +++ b/frontend/mining-admin-web/src/features/system-accounts/components/accounts-table.tsx @@ -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 ( + + + + {title} + {accounts.length > 0 && ( + + {accounts.length} 个账户 + + )} + + + + + + + 账户类型 + 账户名称 + 余额/算力 + 来源 + {showSyncInfo && 同步时间} + 描述 + + + + {isLoading ? ( + [...Array(3)].map((_, i) => ( + + {[...Array(showSyncInfo ? 6 : 5)].map((_, j) => ( + + + + ))} + + )) + ) : accounts.length === 0 ? ( + + + 暂无数据 + + + ) : ( + accounts.map((account) => { + const displayInfo = getAccountDisplayInfo(account.accountType); + const balance = account.contributionBalance || account.totalContribution || '0'; + + return ( + + +
+ + + {account.accountType} + +
+
+ + {account.name || displayInfo.label} + + + {formatDecimal(balance, 4)} + {account.contributionNeverExpires && ( + + 永久 + + )} + + + + {account.source === 'synced' ? 'CDC同步' : '本地'} + + + {showSyncInfo && ( + + {account.syncedAt + ? formatDistanceToNow(new Date(account.syncedAt), { + addSuffix: true, + locale: zhCN, + }) + : '-'} + + )} + + {account.description || displayInfo.description} + +
+ ); + }) + )} +
+
+
+
+ ); +} diff --git a/frontend/mining-admin-web/src/features/system-accounts/components/index.ts b/frontend/mining-admin-web/src/features/system-accounts/components/index.ts new file mode 100644 index 00000000..dd066406 --- /dev/null +++ b/frontend/mining-admin-web/src/features/system-accounts/components/index.ts @@ -0,0 +1,3 @@ +export { AccountCard } from './account-card'; +export { AccountsTable } from './accounts-table'; +export { SummaryCards } from './summary-cards'; diff --git a/frontend/mining-admin-web/src/features/system-accounts/components/summary-cards.tsx b/frontend/mining-admin-web/src/features/system-accounts/components/summary-cards.tsx new file mode 100644 index 00000000..ef9c0a34 --- /dev/null +++ b/frontend/mining-admin-web/src/features/system-accounts/components/summary-cards.tsx @@ -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 ( +
+ {[...Array(4)].map((_, i) => ( + + + + + + ))} +
+ ); + } + + if (!summary) { + return null; + } + + return ( +
+ {/* System Accounts Count */} + + +
+
+

本地账户

+

{summary.systemAccounts.count}

+

+ 总算力: {formatDecimal(summary.systemAccounts.totalContribution, 4)} +

+
+
+ +
+
+
+
+ + {/* Synced Contributions */} + + +
+
+

同步算力

+

{summary.syncedContributions.count}

+

+ 总余额: {formatDecimal(summary.syncedContributions.totalBalance, 4)} +

+
+
+ +
+
+
+
+ + {/* Mining Config */} + + +
+
+

+ 挖矿配置 + {summary.miningConfig?.isActive && ( + + 运行中 + + )} +

+ {summary.miningConfig ? ( + <> +

+ 第 {summary.miningConfig.currentEra} 纪 +

+

+ 剩余分发: {formatDecimal(summary.miningConfig.remainingDistribution, 2)} +

+ + ) : ( +

未配置

+ )} +
+
+ +
+
+
+
+ + {/* Circulation Pool */} + + +
+
+

流通池

+ {summary.circulationPool ? ( + <> +

+ {formatDecimal(summary.circulationPool.totalShares, 2)} +

+

+ 总金额: {formatDecimal(summary.circulationPool.totalCash, 2)} USDT +

+ + ) : ( +

未初始化

+ )} +
+
+ +
+
+
+
+
+ ); +} diff --git a/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts b/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts new file mode 100644 index 00000000..d70ac08e --- /dev/null +++ b/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts @@ -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, + }; +} diff --git a/frontend/mining-admin-web/src/features/system-accounts/index.ts b/frontend/mining-admin-web/src/features/system-accounts/index.ts new file mode 100644 index 00000000..b2e3c43b --- /dev/null +++ b/frontend/mining-admin-web/src/features/system-accounts/index.ts @@ -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'; diff --git a/frontend/mining-admin-web/src/types/system-account.ts b/frontend/mining-admin-web/src/types/system-account.ts new file mode 100644 index 00000000..066a6f8c --- /dev/null +++ b/frontend/mining-admin-web/src/types/system-account.ts @@ -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 = { + 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', + }; +} \ No newline at end of file