feat(pending-contributions): 添加待解锁算力分类账功能
功能说明: - 待解锁算力是因用户未满足解锁条件(如直推数不足)而暂存的层级/奖励算力 - 这部分算力参与挖矿,但收益归入总部账户(HEADQUARTERS) 后端变更: - mining-service: 添加4个待解锁算力相关API - GET /admin/pending-contributions - 获取待解锁算力列表(支持分页和类型筛选) - GET /admin/pending-contributions/summary - 获取汇总统计(按类型统计、总挖矿收益) - GET /admin/pending-contributions/:id/records - 获取单条记录的挖矿明细 - GET /admin/pending-contributions/mining-records - 获取所有挖矿记录汇总视图 - mining-admin-service: 添加代理层 - 新建 PendingContributionsService 调用 mining-service API - 新建 PendingContributionsController 暴露 API 给前端 前端变更: - 新建 pending-contributions feature 模块(API、hooks、类型定义) - 新建 /pending-contributions 页面 - 汇总统计卡片(总量、已归入总部积分股、挖矿记录数) - 按类型统计展示 - 算力列表Tab(来源用户、归属用户、类型、算力、原因、创建时间) - 挖矿记录Tab(时间、类型、算力、占比、每秒分配量、挖得数量、归入) - 在仪表盘的层级算力和团队奖励卡片添加"查看分类账"链接 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d815792deb
commit
63c192e90d
|
|
@ -9,6 +9,7 @@ import { UsersController } from './controllers/users.controller';
|
||||||
import { SystemAccountsController } from './controllers/system-accounts.controller';
|
import { SystemAccountsController } from './controllers/system-accounts.controller';
|
||||||
import { ReportsController } from './controllers/reports.controller';
|
import { ReportsController } from './controllers/reports.controller';
|
||||||
import { ManualMiningController } from './controllers/manual-mining.controller';
|
import { ManualMiningController } from './controllers/manual-mining.controller';
|
||||||
|
import { PendingContributionsController } from './controllers/pending-contributions.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ApplicationModule],
|
imports: [ApplicationModule],
|
||||||
|
|
@ -22,6 +23,7 @@ import { ManualMiningController } from './controllers/manual-mining.controller';
|
||||||
SystemAccountsController,
|
SystemAccountsController,
|
||||||
ReportsController,
|
ReportsController,
|
||||||
ManualMiningController,
|
ManualMiningController,
|
||||||
|
PendingContributionsController,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ApiModule {}
|
export class ApiModule {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiParam,
|
||||||
|
ApiQuery,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { PendingContributionsService } from '../../application/services/pending-contributions.service';
|
||||||
|
|
||||||
|
@ApiTags('Pending Contributions')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('pending-contributions')
|
||||||
|
export class PendingContributionsController {
|
||||||
|
constructor(
|
||||||
|
private readonly pendingContributionsService: PendingContributionsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取待解锁算力列表' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'contributionType',
|
||||||
|
required: false,
|
||||||
|
type: String,
|
||||||
|
description: '算力类型筛选',
|
||||||
|
})
|
||||||
|
async getPendingContributions(
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
@Query('contributionType') contributionType?: string,
|
||||||
|
) {
|
||||||
|
return this.pendingContributionsService.getPendingContributions(
|
||||||
|
page ?? 1,
|
||||||
|
pageSize ?? 20,
|
||||||
|
contributionType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('summary')
|
||||||
|
@ApiOperation({ summary: '获取待解锁算力汇总统计' })
|
||||||
|
async getPendingContributionsSummary() {
|
||||||
|
return this.pendingContributionsService.getPendingContributionsSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('mining-records')
|
||||||
|
@ApiOperation({ summary: '获取所有待解锁算力的挖矿记录' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
async getAllPendingMiningRecords(
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
) {
|
||||||
|
return this.pendingContributionsService.getAllPendingMiningRecords(
|
||||||
|
page ?? 1,
|
||||||
|
pageSize ?? 20,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id/records')
|
||||||
|
@ApiOperation({ summary: '获取某条待解锁算力的挖矿记录' })
|
||||||
|
@ApiParam({ name: 'id', type: String, description: '待解锁算力ID' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
async getPendingContributionMiningRecords(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
) {
|
||||||
|
return this.pendingContributionsService.getPendingContributionMiningRecords(
|
||||||
|
id,
|
||||||
|
page ?? 1,
|
||||||
|
pageSize ?? 20,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import { UsersService } from './services/users.service';
|
||||||
import { SystemAccountsService } from './services/system-accounts.service';
|
import { SystemAccountsService } from './services/system-accounts.service';
|
||||||
import { DailyReportService } from './services/daily-report.service';
|
import { DailyReportService } from './services/daily-report.service';
|
||||||
import { ManualMiningService } from './services/manual-mining.service';
|
import { ManualMiningService } from './services/manual-mining.service';
|
||||||
|
import { PendingContributionsService } from './services/pending-contributions.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [InfrastructureModule],
|
imports: [InfrastructureModule],
|
||||||
|
|
@ -18,6 +19,7 @@ import { ManualMiningService } from './services/manual-mining.service';
|
||||||
SystemAccountsService,
|
SystemAccountsService,
|
||||||
DailyReportService,
|
DailyReportService,
|
||||||
ManualMiningService,
|
ManualMiningService,
|
||||||
|
PendingContributionsService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AuthService,
|
AuthService,
|
||||||
|
|
@ -27,6 +29,7 @@ import { ManualMiningService } from './services/manual-mining.service';
|
||||||
SystemAccountsService,
|
SystemAccountsService,
|
||||||
DailyReportService,
|
DailyReportService,
|
||||||
ManualMiningService,
|
ManualMiningService,
|
||||||
|
PendingContributionsService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ApplicationModule implements OnModuleInit {
|
export class ApplicationModule implements OnModuleInit {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PendingContributionsService {
|
||||||
|
private readonly logger = new Logger(PendingContributionsService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private getMiningServiceUrl(): string {
|
||||||
|
return this.configService.get<string>(
|
||||||
|
'MINING_SERVICE_URL',
|
||||||
|
'http://localhost:3021',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待解锁算力列表
|
||||||
|
*/
|
||||||
|
async getPendingContributions(
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20,
|
||||||
|
contributionType?: string,
|
||||||
|
) {
|
||||||
|
const miningServiceUrl = this.getMiningServiceUrl();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params: any = { page, pageSize };
|
||||||
|
if (contributionType) {
|
||||||
|
params.contributionType = contributionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get(`${miningServiceUrl}/admin/pending-contributions`, {
|
||||||
|
params,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to fetch pending contributions: ${error.message}`,
|
||||||
|
);
|
||||||
|
return { contributions: [], total: 0, page, pageSize };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待解锁算力汇总统计
|
||||||
|
*/
|
||||||
|
async getPendingContributionsSummary() {
|
||||||
|
const miningServiceUrl = this.getMiningServiceUrl();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get(
|
||||||
|
`${miningServiceUrl}/admin/pending-contributions/summary`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to fetch pending contributions summary: ${error.message}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
byType: [],
|
||||||
|
total: { totalAmount: '0', count: 0 },
|
||||||
|
totalMinedToHeadquarters: '0',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取某条待解锁算力的挖矿记录
|
||||||
|
*/
|
||||||
|
async getPendingContributionMiningRecords(
|
||||||
|
id: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20,
|
||||||
|
) {
|
||||||
|
const miningServiceUrl = this.getMiningServiceUrl();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get(
|
||||||
|
`${miningServiceUrl}/admin/pending-contributions/${id}/records`,
|
||||||
|
{
|
||||||
|
params: { page, pageSize },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to fetch pending contribution mining records: ${error.message}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
pendingContribution: null,
|
||||||
|
records: [],
|
||||||
|
total: 0,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有待解锁算力的挖矿记录
|
||||||
|
*/
|
||||||
|
async getAllPendingMiningRecords(page: number = 1, pageSize: number = 20) {
|
||||||
|
const miningServiceUrl = this.getMiningServiceUrl();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get(
|
||||||
|
`${miningServiceUrl}/admin/pending-contributions/mining-records`,
|
||||||
|
{
|
||||||
|
params: { page, pageSize },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to fetch all pending mining records: ${error.message}`,
|
||||||
|
);
|
||||||
|
return { records: [], total: 0, page, pageSize };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -424,4 +424,213 @@ export class AdminController {
|
||||||
await this.manualMiningService.markWalletSynced(recordId);
|
await this.manualMiningService.markWalletSynced(recordId);
|
||||||
return { success: true, message: '已标记为同步完成' };
|
return { success: true, message: '已标记为同步完成' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 待解锁算力(Pending Contributions)====================
|
||||||
|
|
||||||
|
@Get('pending-contributions')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '获取待解锁算力列表' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'contributionType', required: false, type: String, description: '算力类型筛选' })
|
||||||
|
async getPendingContributions(
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
@Query('contributionType') contributionType?: string,
|
||||||
|
) {
|
||||||
|
const pageNum = page ?? 1;
|
||||||
|
const pageSizeNum = pageSize ?? 20;
|
||||||
|
const skip = (pageNum - 1) * pageSizeNum;
|
||||||
|
|
||||||
|
const where: any = { isExpired: false };
|
||||||
|
if (contributionType) {
|
||||||
|
where.contributionType = contributionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [contributions, total] = await Promise.all([
|
||||||
|
this.prisma.pendingContributionMining.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
skip,
|
||||||
|
take: pageSizeNum,
|
||||||
|
}),
|
||||||
|
this.prisma.pendingContributionMining.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contributions: contributions.map((c) => ({
|
||||||
|
id: c.id.toString(),
|
||||||
|
sourceAdoptionId: c.sourceAdoptionId.toString(),
|
||||||
|
sourceAccountSequence: c.sourceAccountSequence,
|
||||||
|
wouldBeAccountSequence: c.wouldBeAccountSequence,
|
||||||
|
contributionType: c.contributionType,
|
||||||
|
amount: c.amount.toString(),
|
||||||
|
reason: c.reason,
|
||||||
|
effectiveDate: c.effectiveDate,
|
||||||
|
expireDate: c.expireDate,
|
||||||
|
isExpired: c.isExpired,
|
||||||
|
lastSyncedAt: c.lastSyncedAt,
|
||||||
|
createdAt: c.createdAt,
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page: pageNum,
|
||||||
|
pageSize: pageSizeNum,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('pending-contributions/summary')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '获取待解锁算力汇总统计' })
|
||||||
|
async getPendingContributionsSummary() {
|
||||||
|
// 按类型分组统计
|
||||||
|
const byType = await this.prisma.pendingContributionMining.groupBy({
|
||||||
|
by: ['contributionType'],
|
||||||
|
where: { isExpired: false },
|
||||||
|
_sum: { amount: true },
|
||||||
|
_count: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 总计
|
||||||
|
const total = await this.prisma.pendingContributionMining.aggregate({
|
||||||
|
where: { isExpired: false },
|
||||||
|
_sum: { amount: true },
|
||||||
|
_count: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 总挖矿收益(归总部)
|
||||||
|
const totalMined = await this.prisma.pendingMiningRecord.aggregate({
|
||||||
|
_sum: { minedAmount: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
byType: byType.map((t) => ({
|
||||||
|
contributionType: t.contributionType,
|
||||||
|
totalAmount: t._sum.amount?.toString() || '0',
|
||||||
|
count: t._count.id,
|
||||||
|
})),
|
||||||
|
total: {
|
||||||
|
totalAmount: total._sum.amount?.toString() || '0',
|
||||||
|
count: total._count.id,
|
||||||
|
},
|
||||||
|
totalMinedToHeadquarters: totalMined._sum.minedAmount?.toString() || '0',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('pending-contributions/:id/records')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '获取某条待解锁算力的挖矿记录' })
|
||||||
|
@ApiParam({ name: 'id', type: String, description: '待解锁算力ID' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
async getPendingContributionMiningRecords(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
) {
|
||||||
|
const pageNum = page ?? 1;
|
||||||
|
const pageSizeNum = pageSize ?? 20;
|
||||||
|
const skip = (pageNum - 1) * pageSizeNum;
|
||||||
|
const pendingId = BigInt(id);
|
||||||
|
|
||||||
|
// 获取待解锁算力信息
|
||||||
|
const pending = await this.prisma.pendingContributionMining.findUnique({
|
||||||
|
where: { id: pendingId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pending) {
|
||||||
|
throw new HttpException('待解锁算力记录不存在', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [records, total] = await Promise.all([
|
||||||
|
this.prisma.pendingMiningRecord.findMany({
|
||||||
|
where: { pendingContributionId: pendingId },
|
||||||
|
orderBy: { miningMinute: 'desc' },
|
||||||
|
skip,
|
||||||
|
take: pageSizeNum,
|
||||||
|
}),
|
||||||
|
this.prisma.pendingMiningRecord.count({
|
||||||
|
where: { pendingContributionId: pendingId },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pendingContribution: {
|
||||||
|
id: pending.id.toString(),
|
||||||
|
sourceAdoptionId: pending.sourceAdoptionId.toString(),
|
||||||
|
sourceAccountSequence: pending.sourceAccountSequence,
|
||||||
|
wouldBeAccountSequence: pending.wouldBeAccountSequence,
|
||||||
|
contributionType: pending.contributionType,
|
||||||
|
amount: pending.amount.toString(),
|
||||||
|
reason: pending.reason,
|
||||||
|
},
|
||||||
|
records: records.map((r) => ({
|
||||||
|
id: r.id.toString(),
|
||||||
|
miningMinute: r.miningMinute,
|
||||||
|
contributionAmount: r.contributionAmount.toString(),
|
||||||
|
networkTotalContribution: r.networkTotalContribution.toString(),
|
||||||
|
contributionRatio: r.contributionRatio.toString(),
|
||||||
|
secondDistribution: r.secondDistribution.toString(),
|
||||||
|
minedAmount: r.minedAmount.toString(),
|
||||||
|
allocatedTo: r.allocatedTo,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page: pageNum,
|
||||||
|
pageSize: pageSizeNum,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('pending-contributions/mining-records')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '获取所有待解锁算力的挖矿记录(汇总视图)' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
async getAllPendingMiningRecords(
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
) {
|
||||||
|
const pageNum = page ?? 1;
|
||||||
|
const pageSizeNum = pageSize ?? 20;
|
||||||
|
const skip = (pageNum - 1) * pageSizeNum;
|
||||||
|
|
||||||
|
const [records, total] = await Promise.all([
|
||||||
|
this.prisma.pendingMiningRecord.findMany({
|
||||||
|
orderBy: { miningMinute: 'desc' },
|
||||||
|
skip,
|
||||||
|
take: pageSizeNum,
|
||||||
|
include: {
|
||||||
|
pendingContribution: {
|
||||||
|
select: {
|
||||||
|
contributionType: true,
|
||||||
|
wouldBeAccountSequence: true,
|
||||||
|
reason: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.prisma.pendingMiningRecord.count(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
records: records.map((r) => ({
|
||||||
|
id: r.id.toString(),
|
||||||
|
pendingContributionId: r.pendingContributionId.toString(),
|
||||||
|
miningMinute: r.miningMinute,
|
||||||
|
sourceAccountSequence: r.sourceAccountSequence,
|
||||||
|
wouldBeAccountSequence: r.wouldBeAccountSequence,
|
||||||
|
contributionType: r.contributionType,
|
||||||
|
contributionAmount: r.contributionAmount.toString(),
|
||||||
|
networkTotalContribution: r.networkTotalContribution.toString(),
|
||||||
|
contributionRatio: r.contributionRatio.toString(),
|
||||||
|
secondDistribution: r.secondDistribution.toString(),
|
||||||
|
minedAmount: r.minedAmount.toString(),
|
||||||
|
allocatedTo: r.allocatedTo,
|
||||||
|
reason: r.pendingContribution?.reason,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page: pageNum,
|
||||||
|
pageSize: pageSizeNum,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,506 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
RefreshCw,
|
||||||
|
Pickaxe,
|
||||||
|
List,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { zhCN } from 'date-fns/locale';
|
||||||
|
|
||||||
|
import {
|
||||||
|
usePendingContributions,
|
||||||
|
usePendingContributionsSummary,
|
||||||
|
useAllPendingMiningRecords,
|
||||||
|
} from '@/features/pending-contributions';
|
||||||
|
import { formatDecimal } from '@/lib/utils/format';
|
||||||
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
|
|
||||||
|
const CONTRIBUTION_TYPE_LABELS: Record<string, { label: string; color: string }> = {
|
||||||
|
LEVEL_1: { label: '一级', color: 'bg-blue-100 text-blue-800' },
|
||||||
|
LEVEL_2: { label: '二级', color: 'bg-green-100 text-green-800' },
|
||||||
|
LEVEL_3: { label: '三级', color: 'bg-purple-100 text-purple-800' },
|
||||||
|
BONUS_1: { label: '奖励一级', color: 'bg-orange-100 text-orange-800' },
|
||||||
|
BONUS_2: { label: '奖励二级', color: 'bg-yellow-100 text-yellow-800' },
|
||||||
|
BONUS_3: { label: '奖励三级', color: 'bg-red-100 text-red-800' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PendingContributionsPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [listPage, setListPage] = useState(1);
|
||||||
|
const [miningPage, setMiningPage] = useState(1);
|
||||||
|
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||||
|
const pageSize = 20;
|
||||||
|
|
||||||
|
// 获取汇总统计
|
||||||
|
const {
|
||||||
|
data: summary,
|
||||||
|
isLoading: summaryLoading,
|
||||||
|
error: summaryError,
|
||||||
|
} = usePendingContributionsSummary();
|
||||||
|
|
||||||
|
// 获取待解锁算力列表
|
||||||
|
const {
|
||||||
|
data: contributions,
|
||||||
|
isLoading: listLoading,
|
||||||
|
error: listError,
|
||||||
|
} = usePendingContributions(
|
||||||
|
listPage,
|
||||||
|
pageSize,
|
||||||
|
typeFilter !== 'all' ? typeFilter : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取所有挖矿记录
|
||||||
|
const {
|
||||||
|
data: miningRecords,
|
||||||
|
isLoading: miningLoading,
|
||||||
|
error: miningError,
|
||||||
|
} = useAllPendingMiningRecords(miningPage, pageSize);
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['pending-contributions'] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const listTotalPages = contributions
|
||||||
|
? Math.ceil(contributions.total / pageSize)
|
||||||
|
: 0;
|
||||||
|
const miningTotalPages = miningRecords
|
||||||
|
? Math.ceil(miningRecords.total / pageSize)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<PageHeader
|
||||||
|
title="待解锁算力分类账"
|
||||||
|
description="查看未满足解锁条件的算力及其挖矿收益(归入总部账户)"
|
||||||
|
actions={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={listLoading || miningLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={`h-4 w-4 mr-2 ${listLoading || miningLoading ? 'animate-spin' : ''}`}
|
||||||
|
/>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 汇总统计卡片 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
待解锁算力总量
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{summaryLoading ? (
|
||||||
|
<Skeleton className="h-8 w-32" />
|
||||||
|
) : summaryError ? (
|
||||||
|
<span className="text-red-500 text-sm">加载失败</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-2xl font-bold text-yellow-600">
|
||||||
|
{formatDecimal(summary?.total?.totalAmount || '0', 2)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
共 {summary?.total?.count || 0} 条记录
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
已归入总部积分股
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{summaryLoading ? (
|
||||||
|
<Skeleton className="h-8 w-32" />
|
||||||
|
) : summaryError ? (
|
||||||
|
<span className="text-red-500 text-sm">加载失败</span>
|
||||||
|
) : (
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{formatDecimal(summary?.totalMinedToHeadquarters || '0', 8)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
挖矿记录数
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{miningLoading ? (
|
||||||
|
<Skeleton className="h-8 w-20" />
|
||||||
|
) : (
|
||||||
|
<div className="text-2xl font-bold">{miningRecords?.total || 0}</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 按类型统计 */}
|
||||||
|
{summary?.byType && summary.byType.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">按类型统计</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
|
{summary.byType.map((item) => {
|
||||||
|
const typeInfo = CONTRIBUTION_TYPE_LABELS[item.contributionType] || {
|
||||||
|
label: item.contributionType,
|
||||||
|
color: 'bg-gray-100 text-gray-800',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.contributionType}
|
||||||
|
className="p-3 rounded-lg border bg-card"
|
||||||
|
>
|
||||||
|
<Badge className={`${typeInfo.color} mb-2`}>
|
||||||
|
{typeInfo.label}
|
||||||
|
</Badge>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
算力: <span className="font-mono text-yellow-600">{formatDecimal(item.totalAmount, 2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
记录数: <span className="font-mono">{item.count}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 分类账 Tabs */}
|
||||||
|
<Tabs defaultValue="list" className="space-y-4">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="list" className="flex items-center gap-2">
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
算力列表
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="mining" className="flex items-center gap-2">
|
||||||
|
<Pickaxe className="h-4 w-4" />
|
||||||
|
挖矿记录
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* 算力列表 Tab */}
|
||||||
|
<TabsContent value="list">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
待解锁算力列表
|
||||||
|
{contributions && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
共 {contributions.total} 条
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||||
|
<SelectTrigger className="w-[150px]">
|
||||||
|
<SelectValue placeholder="选择类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部类型</SelectItem>
|
||||||
|
{Object.entries(CONTRIBUTION_TYPE_LABELS).map(([key, info]) => (
|
||||||
|
<SelectItem key={key} value={key}>
|
||||||
|
{info.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{listError ? (
|
||||||
|
<Alert variant="destructive" className="m-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>加载列表失败</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>来源用户</TableHead>
|
||||||
|
<TableHead>归属用户</TableHead>
|
||||||
|
<TableHead>类型</TableHead>
|
||||||
|
<TableHead className="text-right">算力</TableHead>
|
||||||
|
<TableHead>原因</TableHead>
|
||||||
|
<TableHead>创建时间</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{listLoading ? (
|
||||||
|
[...Array(5)].map((_, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
{[...Array(6)].map((_, j) => (
|
||||||
|
<TableCell key={j}>
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : !contributions?.contributions.length ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={6}
|
||||||
|
className="text-center text-muted-foreground py-8"
|
||||||
|
>
|
||||||
|
暂无数据
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
contributions.contributions.map((item) => {
|
||||||
|
const typeInfo = CONTRIBUTION_TYPE_LABELS[item.contributionType] || {
|
||||||
|
label: item.contributionType,
|
||||||
|
color: 'bg-gray-100 text-gray-800',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell>
|
||||||
|
<code className="text-xs bg-muted px-1 py-0.5 rounded">
|
||||||
|
{item.sourceAccountSequence}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<code className="text-xs bg-muted px-1 py-0.5 rounded">
|
||||||
|
{item.wouldBeAccountSequence}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={`${typeInfo.color} text-xs`}>
|
||||||
|
{typeInfo.label}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono text-yellow-600">
|
||||||
|
{formatDecimal(item.amount, 2)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground text-sm max-w-[200px] truncate">
|
||||||
|
{item.reason || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{format(
|
||||||
|
new Date(item.createdAt),
|
||||||
|
'yyyy-MM-dd HH:mm',
|
||||||
|
{ locale: zhCN }
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{listTotalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-center gap-2 py-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setListPage((p) => Math.max(1, p - 1))}
|
||||||
|
disabled={listPage <= 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-muted-foreground px-4">
|
||||||
|
第 {listPage} / {listTotalPages} 页
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setListPage((p) => Math.min(listTotalPages, p + 1))
|
||||||
|
}
|
||||||
|
disabled={listPage >= listTotalPages}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 挖矿记录 Tab */}
|
||||||
|
<TabsContent value="mining">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
挖矿记录
|
||||||
|
{miningRecords && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
共 {miningRecords.total} 条
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{miningError ? (
|
||||||
|
<Alert variant="destructive" className="m-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>加载挖矿记录失败</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>挖矿时间</TableHead>
|
||||||
|
<TableHead>类型</TableHead>
|
||||||
|
<TableHead className="text-right">算力</TableHead>
|
||||||
|
<TableHead className="text-right">算力占比</TableHead>
|
||||||
|
<TableHead className="text-right">每秒分配量</TableHead>
|
||||||
|
<TableHead className="text-right">挖得数量</TableHead>
|
||||||
|
<TableHead>归入</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{miningLoading ? (
|
||||||
|
[...Array(5)].map((_, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
{[...Array(7)].map((_, j) => (
|
||||||
|
<TableCell key={j}>
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : !miningRecords?.records.length ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={7}
|
||||||
|
className="text-center text-muted-foreground py-8"
|
||||||
|
>
|
||||||
|
暂无挖矿记录
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
miningRecords.records.map((record) => {
|
||||||
|
const typeInfo = CONTRIBUTION_TYPE_LABELS[record.contributionType || ''] || {
|
||||||
|
label: record.contributionType || '-',
|
||||||
|
color: 'bg-gray-100 text-gray-800',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<TableRow key={record.id}>
|
||||||
|
<TableCell>
|
||||||
|
{format(
|
||||||
|
new Date(record.miningMinute),
|
||||||
|
'yyyy-MM-dd HH:mm',
|
||||||
|
{ locale: zhCN }
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={`${typeInfo.color} text-xs`}>
|
||||||
|
{typeInfo.label}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono">
|
||||||
|
{formatDecimal(record.contributionAmount, 2)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono">
|
||||||
|
{(Number(record.contributionRatio) * 100).toFixed(6)}%
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono">
|
||||||
|
{formatDecimal(record.secondDistribution, 8)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono text-green-600">
|
||||||
|
+{formatDecimal(record.minedAmount, 8)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{record.allocatedTo === 'HEADQUARTERS' ? '总部' : record.allocatedTo}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{miningTotalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-center gap-2 py-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setMiningPage((p) => Math.max(1, p - 1))}
|
||||||
|
disabled={miningPage <= 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-muted-foreground px-4">
|
||||||
|
第 {miningPage} / {miningTotalPages} 页
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setMiningPage((p) => Math.min(miningTotalPages, p + 1))
|
||||||
|
}
|
||||||
|
disabled={miningPage >= miningTotalPages}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { useDashboardStats } from '../hooks/use-dashboard-stats';
|
import { useDashboardStats } from '../hooks/use-dashboard-stats';
|
||||||
import { formatCompactNumber } from '@/lib/utils/format';
|
import { formatCompactNumber } from '@/lib/utils/format';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Activity, Users, Building2, Landmark, Layers, Gift, TreePine } from 'lucide-react';
|
import { Activity, Users, Building2, Landmark, Layers, Gift, TreePine, ExternalLink } from 'lucide-react';
|
||||||
|
|
||||||
function ContributionBreakdownSkeleton() {
|
function ContributionBreakdownSkeleton() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -132,10 +134,18 @@ export function ContributionBreakdown() {
|
||||||
{/* 层级算力详情 */}
|
{/* 层级算力详情 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-base flex items-center gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<Layers className="h-5 w-5 text-blue-600" />
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
层级算力详情 (7.5%)
|
<Layers className="h-5 w-5 text-blue-600" />
|
||||||
</CardTitle>
|
层级算力详情 (7.5%)
|
||||||
|
</CardTitle>
|
||||||
|
<Link href="/pending-contributions">
|
||||||
|
<Button variant="ghost" size="sm" className="text-xs h-7">
|
||||||
|
查看分类账
|
||||||
|
<ExternalLink className="h-3 w-3 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
理论: {formatCompactNumber(dc.level.theory)} |
|
理论: {formatCompactNumber(dc.level.theory)} |
|
||||||
<span className="text-green-600"> 已解锁: {formatCompactNumber(dc.level.unlocked)}</span> |
|
<span className="text-green-600"> 已解锁: {formatCompactNumber(dc.level.unlocked)}</span> |
|
||||||
|
|
@ -173,10 +183,18 @@ export function ContributionBreakdown() {
|
||||||
{/* 团队奖励详情 */}
|
{/* 团队奖励详情 */}
|
||||||
<Card className="lg:col-span-2">
|
<Card className="lg:col-span-2">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-base flex items-center gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<Gift className="h-5 w-5 text-purple-600" />
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
团队奖励详情 (7.5%)
|
<Gift className="h-5 w-5 text-purple-600" />
|
||||||
</CardTitle>
|
团队奖励详情 (7.5%)
|
||||||
|
</CardTitle>
|
||||||
|
<Link href="/pending-contributions">
|
||||||
|
<Button variant="ghost" size="sm" className="text-xs h-7">
|
||||||
|
查看分类账
|
||||||
|
<ExternalLink className="h-3 w-3 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
理论: {formatCompactNumber(dc.bonus.theory)} |
|
理论: {formatCompactNumber(dc.bonus.theory)} |
|
||||||
<span className="text-green-600"> 已解锁: {formatCompactNumber(dc.bonus.unlocked)}</span> |
|
<span className="text-green-600"> 已解锁: {formatCompactNumber(dc.bonus.unlocked)}</span> |
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { apiClient } from '@/lib/api/client';
|
||||||
|
|
||||||
|
// 待解锁算力记录(来自 mining-service 的 PendingContributionMining 表)
|
||||||
|
export interface PendingContribution {
|
||||||
|
id: string;
|
||||||
|
sourceAdoptionId: string;
|
||||||
|
sourceAccountSequence: string;
|
||||||
|
wouldBeAccountSequence: string;
|
||||||
|
contributionType: string;
|
||||||
|
amount: string;
|
||||||
|
reason: string;
|
||||||
|
effectiveDate: string;
|
||||||
|
expireDate: string;
|
||||||
|
isExpired: boolean;
|
||||||
|
lastSyncedAt: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 待解锁算力列表响应
|
||||||
|
export interface PendingContributionsResponse {
|
||||||
|
contributions: PendingContribution[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 待解锁算力汇总统计
|
||||||
|
export interface PendingContributionsSummary {
|
||||||
|
byType: Array<{
|
||||||
|
contributionType: string;
|
||||||
|
totalAmount: string;
|
||||||
|
count: number;
|
||||||
|
}>;
|
||||||
|
total: {
|
||||||
|
totalAmount: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
totalMinedToHeadquarters: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 待解锁算力的挖矿记录
|
||||||
|
export interface PendingMiningRecord {
|
||||||
|
id: string;
|
||||||
|
pendingContributionId?: string;
|
||||||
|
miningMinute: string;
|
||||||
|
sourceAccountSequence?: string;
|
||||||
|
wouldBeAccountSequence?: string;
|
||||||
|
contributionType?: string;
|
||||||
|
contributionAmount: string;
|
||||||
|
networkTotalContribution: string;
|
||||||
|
contributionRatio: string;
|
||||||
|
secondDistribution: string;
|
||||||
|
minedAmount: string;
|
||||||
|
allocatedTo: string;
|
||||||
|
reason?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 挖矿记录响应
|
||||||
|
export interface PendingMiningRecordsResponse {
|
||||||
|
records: PendingMiningRecord[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pendingContributionsApi = {
|
||||||
|
// 获取待解锁算力列表
|
||||||
|
getList: async (
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20,
|
||||||
|
contributionType?: string
|
||||||
|
): Promise<PendingContributionsResponse> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('page', page.toString());
|
||||||
|
params.append('pageSize', pageSize.toString());
|
||||||
|
if (contributionType) {
|
||||||
|
params.append('contributionType', contributionType);
|
||||||
|
}
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/pending-contributions?${params.toString()}`
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取汇总统计
|
||||||
|
getSummary: async (): Promise<PendingContributionsSummary> => {
|
||||||
|
const response = await apiClient.get('/pending-contributions/summary');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取所有待解锁算力的挖矿记录
|
||||||
|
getAllMiningRecords: async (
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
): Promise<PendingMiningRecordsResponse> => {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/pending-contributions/mining-records?page=${page}&pageSize=${pageSize}`
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取某条待解锁算力的挖矿记录
|
||||||
|
getMiningRecords: async (
|
||||||
|
id: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
): Promise<PendingMiningRecordsResponse> => {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/pending-contributions/${id}/records?page=${page}&pageSize=${pageSize}`
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { pendingContributionsApi } from '../api/pending-contributions.api';
|
||||||
|
|
||||||
|
export function usePendingContributions(
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20,
|
||||||
|
contributionType?: string
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['pending-contributions', 'list', page, pageSize, contributionType],
|
||||||
|
queryFn: () => pendingContributionsApi.getList(page, pageSize, contributionType),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePendingContributionsSummary() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['pending-contributions', 'summary'],
|
||||||
|
queryFn: () => pendingContributionsApi.getSummary(),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAllPendingMiningRecords(page: number = 1, pageSize: number = 20) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['pending-contributions', 'mining-records', 'all', page, pageSize],
|
||||||
|
queryFn: () => pendingContributionsApi.getAllMiningRecords(page, pageSize),
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePendingContributionMiningRecords(
|
||||||
|
id: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['pending-contributions', 'mining-records', id, page, pageSize],
|
||||||
|
queryFn: () => pendingContributionsApi.getMiningRecords(id, page, pageSize),
|
||||||
|
enabled: !!id,
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// API
|
||||||
|
export {
|
||||||
|
pendingContributionsApi,
|
||||||
|
type PendingContribution,
|
||||||
|
type PendingContributionsResponse,
|
||||||
|
type PendingContributionsSummary,
|
||||||
|
type PendingMiningRecord,
|
||||||
|
type PendingMiningRecordsResponse,
|
||||||
|
} from './api/pending-contributions.api';
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
export {
|
||||||
|
usePendingContributions,
|
||||||
|
usePendingContributionsSummary,
|
||||||
|
useAllPendingMiningRecords,
|
||||||
|
usePendingContributionMiningRecords,
|
||||||
|
} from './hooks/use-pending-contributions';
|
||||||
Loading…
Reference in New Issue