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 { ReportsController } from './controllers/reports.controller';
|
||||
import { ManualMiningController } from './controllers/manual-mining.controller';
|
||||
import { PendingContributionsController } from './controllers/pending-contributions.controller';
|
||||
|
||||
@Module({
|
||||
imports: [ApplicationModule],
|
||||
|
|
@ -22,6 +23,7 @@ import { ManualMiningController } from './controllers/manual-mining.controller';
|
|||
SystemAccountsController,
|
||||
ReportsController,
|
||||
ManualMiningController,
|
||||
PendingContributionsController,
|
||||
],
|
||||
})
|
||||
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 { DailyReportService } from './services/daily-report.service';
|
||||
import { ManualMiningService } from './services/manual-mining.service';
|
||||
import { PendingContributionsService } from './services/pending-contributions.service';
|
||||
|
||||
@Module({
|
||||
imports: [InfrastructureModule],
|
||||
|
|
@ -18,6 +19,7 @@ import { ManualMiningService } from './services/manual-mining.service';
|
|||
SystemAccountsService,
|
||||
DailyReportService,
|
||||
ManualMiningService,
|
||||
PendingContributionsService,
|
||||
],
|
||||
exports: [
|
||||
AuthService,
|
||||
|
|
@ -27,6 +29,7 @@ import { ManualMiningService } from './services/manual-mining.service';
|
|||
SystemAccountsService,
|
||||
DailyReportService,
|
||||
ManualMiningService,
|
||||
PendingContributionsService,
|
||||
],
|
||||
})
|
||||
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);
|
||||
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';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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';
|
||||
import { Activity, Users, Building2, Landmark, Layers, Gift, TreePine, ExternalLink } from 'lucide-react';
|
||||
|
||||
function ContributionBreakdownSkeleton() {
|
||||
return (
|
||||
|
|
@ -132,10 +134,18 @@ export function ContributionBreakdown() {
|
|||
{/* 层级算力详情 */}
|
||||
<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>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Layers className="h-5 w-5 text-blue-600" />
|
||||
层级算力详情 (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">
|
||||
理论: {formatCompactNumber(dc.level.theory)} |
|
||||
<span className="text-green-600"> 已解锁: {formatCompactNumber(dc.level.unlocked)}</span> |
|
||||
|
|
@ -173,10 +183,18 @@ export function ContributionBreakdown() {
|
|||
{/* 团队奖励详情 */}
|
||||
<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>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Gift className="h-5 w-5 text-purple-600" />
|
||||
团队奖励详情 (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">
|
||||
理论: {formatCompactNumber(dc.bonus.theory)} |
|
||||
<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