feat(dashboard): add detailed contribution breakdown by category

Backend (contribution-service):
- Add getDetailedContributionStats() to repository
- Add getUnallocatedByLevelTier/BonusTier() to repository
- Extend stats API with level/bonus breakdown by tier
- Add getTotalTrees() to synced-data repository

Backend (mining-admin-service):
- Add detailed contribution stats calculation
- Calculate theoretical vs actual values per category
- Return level/bonus breakdown with unlocked/pending amounts

Frontend (mining-admin-web):
- Add ContributionBreakdown component showing:
  - Personal (70%), Operation (12%), Province (1%), City (2%)
  - Level contribution (7.5%) by tier: 1-5, 6-10, 11-15
  - Bonus contribution (7.5%) by tier: T1, T2, T3
- Update DashboardStats type definition
- Integrate breakdown component into dashboard page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-15 01:43:37 -08:00
parent 1f15daa6c5
commit cfbf1b21f3
9 changed files with 781 additions and 3 deletions

View File

@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import Decimal from 'decimal.js';
import { ContributionAccountRepository } from '../../infrastructure/persistence/repositories/contribution-account.repository';
import { ContributionRecordRepository } from '../../infrastructure/persistence/repositories/contribution-record.repository';
import { UnallocatedContributionRepository } from '../../infrastructure/persistence/repositories/unallocated-contribution.repository';
@ -6,6 +7,15 @@ import { SystemAccountRepository } from '../../infrastructure/persistence/reposi
import { SyncedDataRepository } from '../../infrastructure/persistence/repositories/synced-data.repository';
import { ContributionSourceType } from '../../domain/aggregates/contribution-account.aggregate';
// 基准算力常量
const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617');
const RATE_PERSONAL = new Decimal('0.70');
const RATE_OPERATION = new Decimal('0.12');
const RATE_PROVINCE = new Decimal('0.01');
const RATE_CITY = new Decimal('0.02');
const RATE_LEVEL_TOTAL = new Decimal('0.075');
const RATE_BONUS_TOTAL = new Decimal('0.075');
export interface ContributionStatsDto {
// 用户统计
totalUsers: number;
@ -16,17 +26,57 @@ export interface ContributionStatsDto {
totalAdoptions: number;
processedAdoptions: number;
unprocessedAdoptions: number;
totalTrees: number;
// 算力统计
totalContribution: string;
// 算力分布
// 算力分布(基础)
contributionByType: {
personal: string;
teamLevel: string;
teamBonus: string;
};
// ========== 详细算力分解(按用户需求) ==========
// 全网算力 = 总认种树 * 22617
networkTotalContribution: string;
// 个人用户总算力 = 总认种树 * (22617 * 70%)
personalTotalContribution: string;
// 运营账户总算力 = 总认种树 * (22617 * 12%)
operationTotalContribution: string;
// 省公司总算力 = 总认种树 * (22617 * 1%)
provinceTotalContribution: string;
// 市公司总算力 = 总认种树 * (22617 * 2%)
cityTotalContribution: string;
// 层级算力详情 (7.5%)
levelContribution: {
total: string;
unlocked: string;
pending: string;
byTier: {
// 1档: 1-5级
tier1: { unlocked: string; pending: string };
// 2档: 6-10级
tier2: { unlocked: string; pending: string };
// 3档: 11-15级
tier3: { unlocked: string; pending: string };
};
};
// 团队奖励算力详情 (7.5%)
bonusContribution: {
total: string;
unlocked: string;
pending: string;
byTier: {
tier1: { unlocked: string; pending: string };
tier2: { unlocked: string; pending: string };
tier3: { unlocked: string; pending: string };
};
};
// 系统账户
systemAccounts: {
accountType: string;
@ -61,6 +111,10 @@ export class GetContributionStatsQuery {
systemAccounts,
totalUnallocated,
unallocatedByType,
detailedStats,
unallocatedByLevelTier,
unallocatedByBonusTier,
totalTrees,
] = await Promise.all([
this.syncedDataRepository.countUsers(),
this.accountRepository.countAccounts(),
@ -72,8 +126,33 @@ export class GetContributionStatsQuery {
this.systemAccountRepository.findAll(),
this.unallocatedRepository.getTotalUnallocated(),
this.unallocatedRepository.getTotalUnallocatedByType(),
this.accountRepository.getDetailedContributionStats(),
this.unallocatedRepository.getUnallocatedByLevelTier(),
this.unallocatedRepository.getUnallocatedByBonusTier(),
this.syncedDataRepository.getTotalTrees(),
]);
// 计算理论算力(基于总认种树 * 基准算力)
const networkTotal = BASE_CONTRIBUTION_PER_TREE.mul(totalTrees);
const personalTotal = networkTotal.mul(RATE_PERSONAL);
const operationTotal = networkTotal.mul(RATE_OPERATION);
const provinceTotal = networkTotal.mul(RATE_PROVINCE);
const cityTotal = networkTotal.mul(RATE_CITY);
const levelTotal = networkTotal.mul(RATE_LEVEL_TOTAL);
const bonusTotal = networkTotal.mul(RATE_BONUS_TOTAL);
// 层级算力: 已解锁 + 未解锁
const levelUnlocked = new Decimal(detailedStats.levelUnlocked);
const levelPending = new Decimal(unallocatedByLevelTier.tier1)
.plus(unallocatedByLevelTier.tier2)
.plus(unallocatedByLevelTier.tier3);
// 团队奖励算力: 已解锁 + 未解锁
const bonusUnlocked = new Decimal(detailedStats.bonusUnlocked);
const bonusPending = new Decimal(unallocatedByBonusTier.tier1)
.plus(unallocatedByBonusTier.tier2)
.plus(unallocatedByBonusTier.tier3);
return {
totalUsers,
totalAccounts,
@ -81,12 +160,63 @@ export class GetContributionStatsQuery {
totalAdoptions,
processedAdoptions: totalAdoptions - undistributedAdoptions,
unprocessedAdoptions: undistributedAdoptions,
totalTrees,
totalContribution: totalContribution.value.toString(),
contributionByType: {
personal: (contributionByType.get(ContributionSourceType.PERSONAL)?.value || 0).toString(),
teamLevel: (contributionByType.get(ContributionSourceType.TEAM_LEVEL)?.value || 0).toString(),
teamBonus: (contributionByType.get(ContributionSourceType.TEAM_BONUS)?.value || 0).toString(),
},
// 详细算力分解
networkTotalContribution: networkTotal.toString(),
personalTotalContribution: personalTotal.toString(),
operationTotalContribution: operationTotal.toString(),
provinceTotalContribution: provinceTotal.toString(),
cityTotalContribution: cityTotal.toString(),
// 层级算力详情
levelContribution: {
total: levelTotal.toString(),
unlocked: levelUnlocked.toString(),
pending: levelPending.toString(),
byTier: {
tier1: {
unlocked: detailedStats.levelByTier.tier1.unlocked,
pending: unallocatedByLevelTier.tier1,
},
tier2: {
unlocked: detailedStats.levelByTier.tier2.unlocked,
pending: unallocatedByLevelTier.tier2,
},
tier3: {
unlocked: detailedStats.levelByTier.tier3.unlocked,
pending: unallocatedByLevelTier.tier3,
},
},
},
// 团队奖励算力详情
bonusContribution: {
total: bonusTotal.toString(),
unlocked: bonusUnlocked.toString(),
pending: bonusPending.toString(),
byTier: {
tier1: {
unlocked: detailedStats.bonusByTier.tier1.unlocked,
pending: unallocatedByBonusTier.tier1,
},
tier2: {
unlocked: detailedStats.bonusByTier.tier2.unlocked,
pending: unallocatedByBonusTier.tier2,
},
tier3: {
unlocked: detailedStats.bonusByTier.tier3.unlocked,
pending: unallocatedByBonusTier.tier3,
},
},
},
systemAccounts: systemAccounts.map((a) => ({
accountType: a.accountType,
name: a.name,
@ -98,4 +228,5 @@ export class GetContributionStatsQuery {
),
};
}
}

View File

@ -233,6 +233,107 @@ export class ContributionAccountRepository implements IContributionAccountReposi
return records.map((r) => this.toDomain(r));
}
/**
*
*/
async getDetailedContributionStats(): Promise<{
// 个人算力总计
personalTotal: string;
// 层级算力 - 已解锁(已分配给上线)
levelUnlocked: string;
// 层级算力 - 未解锁待解锁的pending
levelPending: string;
// 层级按档位分解
levelByTier: {
tier1: { unlocked: string; pending: string }; // 1-5级
tier2: { unlocked: string; pending: string }; // 6-10级
tier3: { unlocked: string; pending: string }; // 11-15级
};
// 团队奖励算力 - 已解锁
bonusUnlocked: string;
// 团队奖励算力 - 未解锁
bonusPending: string;
// 团队奖励按档位分解
bonusByTier: {
tier1: { unlocked: string; pending: string };
tier2: { unlocked: string; pending: string };
tier3: { unlocked: string; pending: string };
};
}> {
const result = await this.client.contributionAccount.aggregate({
_sum: {
personalContribution: true,
// 层级 1-5
level1Pending: true,
level2Pending: true,
level3Pending: true,
level4Pending: true,
level5Pending: true,
// 层级 6-10
level6Pending: true,
level7Pending: true,
level8Pending: true,
level9Pending: true,
level10Pending: true,
// 层级 11-15
level11Pending: true,
level12Pending: true,
level13Pending: true,
level14Pending: true,
level15Pending: true,
// 团队奖励
bonusTier1Pending: true,
bonusTier2Pending: true,
bonusTier3Pending: true,
// 汇总
totalLevelPending: true,
totalBonusPending: true,
totalUnlocked: true,
},
});
const sum = result._sum;
// 层级 1-5 已解锁在pending字段中存储的是已分配给该用户的层级算力
const level1to5 = new Decimal(sum.level1Pending || 0)
.plus(sum.level2Pending || 0)
.plus(sum.level3Pending || 0)
.plus(sum.level4Pending || 0)
.plus(sum.level5Pending || 0);
// 层级 6-10
const level6to10 = new Decimal(sum.level6Pending || 0)
.plus(sum.level7Pending || 0)
.plus(sum.level8Pending || 0)
.plus(sum.level9Pending || 0)
.plus(sum.level10Pending || 0);
// 层级 11-15
const level11to15 = new Decimal(sum.level11Pending || 0)
.plus(sum.level12Pending || 0)
.plus(sum.level13Pending || 0)
.plus(sum.level14Pending || 0)
.plus(sum.level15Pending || 0);
return {
personalTotal: (sum.personalContribution || new Decimal(0)).toString(),
levelUnlocked: (sum.totalLevelPending || new Decimal(0)).toString(),
levelPending: '0', // 未解锁的存储在 unallocated 表中
levelByTier: {
tier1: { unlocked: level1to5.toString(), pending: '0' },
tier2: { unlocked: level6to10.toString(), pending: '0' },
tier3: { unlocked: level11to15.toString(), pending: '0' },
},
bonusUnlocked: (sum.totalBonusPending || new Decimal(0)).toString(),
bonusPending: '0', // 未解锁的存储在 unallocated 表中
bonusByTier: {
tier1: { unlocked: (sum.bonusTier1Pending || new Decimal(0)).toString(), pending: '0' },
tier2: { unlocked: (sum.bonusTier2Pending || new Decimal(0)).toString(), pending: '0' },
tier3: { unlocked: (sum.bonusTier3Pending || new Decimal(0)).toString(), pending: '0' },
},
};
}
private toDomain(record: any): ContributionAccountAggregate {
return ContributionAccountAggregate.fromPersistence({
id: record.id,

View File

@ -461,6 +461,16 @@ export class SyncedDataRepository implements ISyncedDataRepository {
});
}
async getTotalTrees(): Promise<number> {
const result = await this.client.syncedAdoption.aggregate({
where: {
status: 'MINING_ENABLED', // 只统计最终成功的认种订单
},
_sum: { treeCount: true },
});
return result._sum.treeCount ?? 0;
}
// ========== 私有方法 ==========
private toSyncedUser(record: any): SyncedUser {

View File

@ -192,6 +192,81 @@ export class UnallocatedContributionRepository {
return records.map((r) => this.toDomain(r));
}
/**
*
*/
async getUnallocatedByLevelTier(): Promise<{
tier1: string; // 1-5级未分配
tier2: string; // 6-10级未分配
tier3: string; // 11-15级未分配
}> {
const results = await this.client.unallocatedContribution.groupBy({
by: ['levelDepth'],
where: {
levelDepth: { not: null },
status: 'PENDING',
},
_sum: { amount: true },
});
let tier1 = new ContributionAmount(0);
let tier2 = new ContributionAmount(0);
let tier3 = new ContributionAmount(0);
for (const item of results) {
const depth = item.levelDepth!;
const amount = new ContributionAmount(item._sum.amount || 0);
if (depth >= 1 && depth <= 5) {
tier1 = tier1.add(amount);
} else if (depth >= 6 && depth <= 10) {
tier2 = tier2.add(amount);
} else if (depth >= 11 && depth <= 15) {
tier3 = tier3.add(amount);
}
}
return {
tier1: tier1.value.toString(),
tier2: tier2.value.toString(),
tier3: tier3.value.toString(),
};
}
/**
*
*/
async getUnallocatedByBonusTier(): Promise<{
tier1: string;
tier2: string;
tier3: string;
}> {
const results = await this.client.unallocatedContribution.groupBy({
by: ['unallocType'],
where: {
unallocType: { startsWith: 'BONUS_TIER_' },
status: 'PENDING',
},
_sum: { amount: true },
});
let tier1 = '0';
let tier2 = '0';
let tier3 = '0';
for (const item of results) {
const amount = (item._sum.amount || 0).toString();
if (item.unallocType === 'BONUS_TIER_1') {
tier1 = amount;
} else if (item.unallocType === 'BONUS_TIER_2') {
tier2 = amount;
} else if (item.unallocType === 'BONUS_TIER_3') {
tier3 = amount;
}
}
return { tier1, tier2, tier3 };
}
private toDomain(record: any): UnallocatedContribution {
return {
id: record.id,

View File

@ -26,15 +26,19 @@ export class DashboardController {
priceChange24h = (close - open) / open;
}
// 详细算力分解数据
const dc = raw.detailedContribution || {};
// 转换为前端期望的格式
return {
// 基础统计
totalUsers: raw.users?.total || 0,
adoptedUsers: raw.users?.adopted || 0,
totalTrees: raw.contribution?.totalTrees || 0,
networkEffectiveContribution: raw.contribution?.effectiveContribution || '0',
networkTotalContribution: raw.contribution?.totalContribution || '0',
networkLevelPending: raw.contribution?.teamLevelContribution || '0',
networkBonusPending: raw.contribution?.teamBonusContribution || '0',
networkLevelPending: dc.levelContribution?.pending || '0',
networkBonusPending: dc.bonusContribution?.pending || '0',
totalDistributed: raw.mining?.totalMined || '0',
totalBurned: raw.mining?.latestDailyStat?.totalBurned || '0',
circulationPool: raw.trading?.circulationPool?.totalShares || '0',
@ -42,6 +46,47 @@ export class DashboardController {
priceChange24h,
totalOrders: raw.trading?.totalAccounts || 0,
totalTrades: raw.trading?.totalAccounts || 0,
// ========== 详细算力分解 ==========
detailedContribution: {
totalTrees: dc.totalTrees || 0,
// 全网算力(理论值)= 总树数 * 22617
networkTotalTheory: dc.networkTotalTheory || '0',
// 个人算力70%
personalTheory: dc.personalTheory || '0',
personalActual: raw.contribution?.personalContribution || '0',
// 运营账户12%
operationTheory: dc.operationTheory || '0',
operationActual: dc.operationActual || '0',
// 省公司1%
provinceTheory: dc.provinceTheory || '0',
provinceActual: dc.provinceActual || '0',
// 市公司2%
cityTheory: dc.cityTheory || '0',
cityActual: dc.cityActual || '0',
// 层级算力7.5%
level: {
theory: dc.levelTheory || '0',
unlocked: dc.levelContribution?.unlocked || '0',
pending: dc.levelContribution?.pending || '0',
// 分档详情
tier1: dc.levelContribution?.byTier?.tier1 || { unlocked: '0', pending: '0' },
tier2: dc.levelContribution?.byTier?.tier2 || { unlocked: '0', pending: '0' },
tier3: dc.levelContribution?.byTier?.tier3 || { unlocked: '0', pending: '0' },
},
// 团队奖励算力7.5%
bonus: {
theory: dc.bonusTheory || '0',
unlocked: dc.bonusContribution?.unlocked || '0',
pending: dc.bonusContribution?.pending || '0',
// 分档详情
tier1: dc.bonusContribution?.byTier?.tier1 || { unlocked: '0', pending: '0' },
tier2: dc.bonusContribution?.byTier?.tier2 || { unlocked: '0', pending: '0' },
tier3: dc.bonusContribution?.byTier?.tier3 || { unlocked: '0', pending: '0' },
},
},
};
}

View File

@ -1,7 +1,17 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Decimal } from 'decimal.js';
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
// 基准算力常量
const BASE_CONTRIBUTION_PER_TREE = new Decimal('22617');
const RATE_PERSONAL = new Decimal('0.70');
const RATE_OPERATION = new Decimal('0.12');
const RATE_PROVINCE = new Decimal('0.01');
const RATE_CITY = new Decimal('0.02');
const RATE_LEVEL_TOTAL = new Decimal('0.075');
const RATE_BONUS_TOTAL = new Decimal('0.075');
@Injectable()
export class DashboardService {
private readonly logger = new Logger(DashboardService.name);
@ -23,6 +33,7 @@ export class DashboardService {
tradingStats,
latestReport,
latestKLine,
detailedContributionStats,
] = await Promise.all([
this.getUserStats(),
this.getContributionStats(),
@ -30,6 +41,7 @@ export class DashboardService {
this.getTradingStats(),
this.prisma.dailyReport.findFirst({ orderBy: { reportDate: 'desc' } }),
this.prisma.syncedDayKLine.findFirst({ orderBy: { klineDate: 'desc' } }),
this.getDetailedContributionStats(),
]);
return {
@ -37,6 +49,7 @@ export class DashboardService {
contribution: contributionStats,
mining: miningStats,
trading: tradingStats,
detailedContribution: detailedContributionStats,
latestReport: latestReport
? this.formatDailyReport(latestReport)
: null,
@ -128,6 +141,7 @@ export class DashboardService {
_count: true,
}),
this.prisma.syncedAdoption.aggregate({
where: { status: 'MINING_ENABLED' },
_sum: { treeCount: true },
_count: true,
}),
@ -152,6 +166,137 @@ export class DashboardService {
};
}
/**
*
*/
private async getDetailedContributionStats() {
// 获取总树数
const adoptionStats = await this.prisma.syncedAdoption.aggregate({
where: { status: 'MINING_ENABLED' },
_sum: { treeCount: true },
});
const totalTrees = adoptionStats._sum.treeCount || 0;
// 按层级统计已分配的层级算力
const levelRecords = await this.prisma.syncedContributionRecord.groupBy({
by: ['levelDepth'],
where: {
sourceType: 'TEAM_LEVEL',
levelDepth: { not: null },
},
_sum: { amount: true },
});
// 按档位统计已分配的团队奖励算力
const bonusRecords = await this.prisma.syncedContributionRecord.groupBy({
by: ['bonusTier'],
where: {
sourceType: 'TEAM_BONUS',
bonusTier: { not: null },
},
_sum: { amount: true },
});
// 获取系统账户按类型的算力
const systemAccounts = await this.prisma.syncedSystemContribution.findMany();
// 汇总层级1-5, 6-10, 11-15
let levelTier1 = new Decimal(0);
let levelTier2 = new Decimal(0);
let levelTier3 = new Decimal(0);
for (const record of levelRecords) {
const depth = record.levelDepth!;
const amount = new Decimal(record._sum.amount || 0);
if (depth >= 1 && depth <= 5) levelTier1 = levelTier1.plus(amount);
else if (depth >= 6 && depth <= 10) levelTier2 = levelTier2.plus(amount);
else if (depth >= 11 && depth <= 15) levelTier3 = levelTier3.plus(amount);
}
// 汇总团队奖励档位
let bonusTier1 = new Decimal(0);
let bonusTier2 = new Decimal(0);
let bonusTier3 = new Decimal(0);
for (const record of bonusRecords) {
const tier = record.bonusTier!;
const amount = new Decimal(record._sum.amount || 0);
if (tier === 1) bonusTier1 = amount;
else if (tier === 2) bonusTier2 = amount;
else if (tier === 3) bonusTier3 = amount;
}
const levelUnlocked = levelTier1.plus(levelTier2).plus(levelTier3);
const bonusUnlocked = bonusTier1.plus(bonusTier2).plus(bonusTier3);
// 计算理论值
const networkTotal = BASE_CONTRIBUTION_PER_TREE.mul(totalTrees);
const personalTheory = networkTotal.mul(RATE_PERSONAL);
const operationTheory = networkTotal.mul(RATE_OPERATION);
const provinceTheory = networkTotal.mul(RATE_PROVINCE);
const cityTheory = networkTotal.mul(RATE_CITY);
const levelTheory = networkTotal.mul(RATE_LEVEL_TOTAL);
const bonusTheory = networkTotal.mul(RATE_BONUS_TOTAL);
// 计算未解锁(理论 - 已解锁)
const levelPending = levelTheory.minus(levelUnlocked).greaterThan(0)
? levelTheory.minus(levelUnlocked)
: new Decimal(0);
const bonusPending = bonusTheory.minus(bonusUnlocked).greaterThan(0)
? bonusTheory.minus(bonusUnlocked)
: new Decimal(0);
// 系统账户按类型汇总
let operationActual = new Decimal(0);
let provinceActual = new Decimal(0);
let cityActual = new Decimal(0);
for (const account of systemAccounts) {
const balance = new Decimal(account.contributionBalance || 0);
if (account.accountType === 'OPERATION') operationActual = operationActual.plus(balance);
else if (account.accountType === 'PROVINCE') provinceActual = provinceActual.plus(balance);
else if (account.accountType === 'CITY') cityActual = cityActual.plus(balance);
}
return {
totalTrees,
// 理论值(基于总树数计算)
networkTotalTheory: networkTotal.toString(),
personalTheory: personalTheory.toString(),
operationTheory: operationTheory.toString(),
provinceTheory: provinceTheory.toString(),
cityTheory: cityTheory.toString(),
levelTheory: levelTheory.toString(),
bonusTheory: bonusTheory.toString(),
// 实际值(从数据库统计)
operationActual: operationActual.toString(),
provinceActual: provinceActual.toString(),
cityActual: cityActual.toString(),
// 层级算力详情
levelContribution: {
total: levelTheory.toString(),
unlocked: levelUnlocked.toString(),
pending: levelPending.toString(),
byTier: {
tier1: { unlocked: levelTier1.toString(), pending: '0' },
tier2: { unlocked: levelTier2.toString(), pending: '0' },
tier3: { unlocked: levelTier3.toString(), pending: '0' },
},
},
// 团队奖励算力详情
bonusContribution: {
total: bonusTheory.toString(),
unlocked: bonusUnlocked.toString(),
pending: bonusPending.toString(),
byTier: {
tier1: { unlocked: bonusTier1.toString(), pending: '0' },
tier2: { unlocked: bonusTier2.toString(), pending: '0' },
tier3: { unlocked: bonusTier3.toString(), pending: '0' },
},
},
};
}
/**
*
*/

View File

@ -4,6 +4,7 @@ import { PageHeader } from '@/components/layout/page-header';
import { StatsCards } from '@/features/dashboard/components/stats-cards';
import { RealtimePanel } from '@/features/dashboard/components/realtime-panel';
import { PriceOverview } from '@/features/dashboard/components/price-overview';
import { ContributionBreakdown } from '@/features/dashboard/components/contribution-breakdown';
export default function DashboardPage() {
return (
@ -12,6 +13,9 @@ export default function DashboardPage() {
<StatsCards />
{/* 详细算力分解 */}
<ContributionBreakdown />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<PriceOverview />

View File

@ -0,0 +1,229 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useDashboardStats } from '../hooks/use-dashboard-stats';
import { formatCompactNumber } from '@/lib/utils/format';
import { Skeleton } from '@/components/ui/skeleton';
import { Activity, Users, Building2, Landmark, Layers, Gift, TreePine } from 'lucide-react';
function ContributionBreakdownSkeleton() {
return (
<div className="space-y-4">
<Card>
<CardHeader>
<Skeleton className="h-5 w-32" />
</CardHeader>
<CardContent className="space-y-3">
{[...Array(6)].map((_, i) => (
<div key={i} className="flex justify-between">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-20" />
</div>
))}
</CardContent>
</Card>
</div>
);
}
interface BreakdownRowProps {
label: string;
theory?: string;
actual?: string;
unlocked?: string;
pending?: string;
icon?: React.ElementType;
color?: string;
}
function BreakdownRow({ label, theory, actual, unlocked, pending, icon: Icon, color = 'text-gray-600' }: BreakdownRowProps) {
return (
<div className="flex items-center justify-between py-2 border-b last:border-0">
<div className="flex items-center gap-2">
{Icon && <Icon className={`h-4 w-4 ${color}`} />}
<span className="text-sm text-muted-foreground">{label}</span>
</div>
<div className="text-right">
{theory && actual && (
<div className="text-sm">
<span className="font-medium">{formatCompactNumber(actual)}</span>
<span className="text-muted-foreground text-xs ml-1">/ {formatCompactNumber(theory)}</span>
</div>
)}
{unlocked !== undefined && pending !== undefined && (
<div className="text-sm">
<span className="font-medium text-green-600">{formatCompactNumber(unlocked)}</span>
<span className="text-muted-foreground mx-1">/</span>
<span className="font-medium text-orange-500">{formatCompactNumber(pending)}</span>
</div>
)}
</div>
</div>
);
}
export function ContributionBreakdown() {
const { data: stats, isLoading } = useDashboardStats();
if (isLoading) {
return <ContributionBreakdownSkeleton />;
}
const dc = stats?.detailedContribution;
if (!dc) {
return null;
}
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* 基础算力分配 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<TreePine className="h-5 w-5 text-green-600" />
</CardTitle>
<p className="text-xs text-muted-foreground">
: {dc.totalTrees} | : {formatCompactNumber(dc.networkTotalTheory)}
</p>
</CardHeader>
<CardContent className="space-y-1">
<BreakdownRow
label="个人算力 (70%)"
theory={dc.personalTheory}
actual={dc.personalActual}
icon={Users}
color="text-blue-500"
/>
<BreakdownRow
label="运营账户 (12%)"
theory={dc.operationTheory}
actual={dc.operationActual}
icon={Building2}
color="text-purple-500"
/>
<BreakdownRow
label="省公司 (1%)"
theory={dc.provinceTheory}
actual={dc.provinceActual}
icon={Landmark}
color="text-indigo-500"
/>
<BreakdownRow
label="市公司 (2%)"
theory={dc.cityTheory}
actual={dc.cityActual}
icon={Building2}
color="text-cyan-500"
/>
<div className="pt-2 mt-2 border-t">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"> (7.5%)</span>
<span className="font-medium">{formatCompactNumber(dc.level.theory)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"> (7.5%)</span>
<span className="font-medium">{formatCompactNumber(dc.bonus.theory)}</span>
</div>
</div>
</CardContent>
</Card>
{/* 层级算力详情 */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Layers className="h-5 w-5 text-blue-600" />
(7.5%)
</CardTitle>
<p className="text-xs text-muted-foreground">
: {formatCompactNumber(dc.level.theory)} |
<span className="text-green-600"> : {formatCompactNumber(dc.level.unlocked)}</span> |
<span className="text-orange-500"> : {formatCompactNumber(dc.level.pending)}</span>
</p>
</CardHeader>
<CardContent className="space-y-1">
<div className="text-xs text-muted-foreground mb-2">
( / )
</div>
<BreakdownRow
label="1档 (1-5级, 需1直推)"
unlocked={dc.level.tier1.unlocked}
pending={dc.level.tier1.pending}
icon={Activity}
color="text-green-500"
/>
<BreakdownRow
label="2档 (6-10级, 需3直推)"
unlocked={dc.level.tier2.unlocked}
pending={dc.level.tier2.pending}
icon={Activity}
color="text-yellow-500"
/>
<BreakdownRow
label="3档 (11-15级, 需5直推)"
unlocked={dc.level.tier3.unlocked}
pending={dc.level.tier3.pending}
icon={Activity}
color="text-orange-500"
/>
</CardContent>
</Card>
{/* 团队奖励详情 */}
<Card className="lg:col-span-2">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Gift className="h-5 w-5 text-purple-600" />
(7.5%)
</CardTitle>
<p className="text-xs text-muted-foreground">
: {formatCompactNumber(dc.bonus.theory)} |
<span className="text-green-600"> : {formatCompactNumber(dc.bonus.unlocked)}</span> |
<span className="text-orange-500"> : {formatCompactNumber(dc.bonus.pending)}</span>
</p>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-3 bg-green-50 rounded-lg">
<div className="text-sm font-medium text-green-800">1 (2.5%)</div>
<div className="text-xs text-green-600 mb-2">条件: 自己认种</div>
<div className="flex justify-between">
<span className="text-xs text-muted-foreground"></span>
<span className="text-sm font-medium text-green-700">{formatCompactNumber(dc.bonus.tier1.unlocked)}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-muted-foreground"></span>
<span className="text-sm font-medium text-orange-600">{formatCompactNumber(dc.bonus.tier1.pending)}</span>
</div>
</div>
<div className="p-3 bg-yellow-50 rounded-lg">
<div className="text-sm font-medium text-yellow-800">2 (2.5%)</div>
<div className="text-xs text-yellow-600 mb-2">条件: 2直推认种</div>
<div className="flex justify-between">
<span className="text-xs text-muted-foreground"></span>
<span className="text-sm font-medium text-green-700">{formatCompactNumber(dc.bonus.tier2.unlocked)}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-muted-foreground"></span>
<span className="text-sm font-medium text-orange-600">{formatCompactNumber(dc.bonus.tier2.pending)}</span>
</div>
</div>
<div className="p-3 bg-orange-50 rounded-lg">
<div className="text-sm font-medium text-orange-800">3 (2.5%)</div>
<div className="text-xs text-orange-600 mb-2">条件: 4直推认种</div>
<div className="flex justify-between">
<span className="text-xs text-muted-foreground"></span>
<span className="text-sm font-medium text-green-700">{formatCompactNumber(dc.bonus.tier3.unlocked)}</span>
</div>
<div className="flex justify-between">
<span className="text-xs text-muted-foreground"></span>
<span className="text-sm font-medium text-orange-600">{formatCompactNumber(dc.bonus.tier3.pending)}</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -1,3 +1,39 @@
export interface TierContribution {
unlocked: string;
pending: string;
}
export interface ContributionBreakdown {
theory: string;
unlocked: string;
pending: string;
tier1: TierContribution;
tier2: TierContribution;
tier3: TierContribution;
}
export interface DetailedContribution {
totalTrees: number;
// 全网算力(理论值)
networkTotalTheory: string;
// 个人算力70%
personalTheory: string;
personalActual: string;
// 运营账户12%
operationTheory: string;
operationActual: string;
// 省公司1%
provinceTheory: string;
provinceActual: string;
// 市公司2%
cityTheory: string;
cityActual: string;
// 层级算力7.5%
level: ContributionBreakdown;
// 团队奖励算力7.5%
bonus: ContributionBreakdown;
}
export interface DashboardStats {
totalUsers: number;
adoptedUsers: number;
@ -13,6 +49,8 @@ export interface DashboardStats {
priceChange24h: number;
totalOrders: number;
totalTrades: number;
// 详细算力分解
detailedContribution?: DetailedContribution;
}
export interface RealtimeData {