feat(admin): 系统账户添加已挖积分股显示和分类账功能
## 后端改动 ### mining-service - 新增 GET /admin/system-accounts/:accountType/records - 获取系统账户挖矿记录(分钟级) - 新增 GET /admin/system-accounts/:accountType/transactions - 获取系统账户交易记录 ### mining-admin-service - 添加 @nestjs/axios 依赖用于 HTTP 调用 - 修改 SystemAccountsService,通过 HTTP 调用 mining-service 获取挖矿数据(totalMined, availableBalance) - 新增挖矿记录和交易记录的代理 API ## 前端改动 ### 类型定义 - SystemAccount 新增 totalMined, availableBalance, miningContribution, miningLastSyncedAt 字段 ### API 层 - 新增 getMiningRecords 和 getTransactions API 方法 - 新增 SystemMiningRecord, SystemTransaction 等类型定义 ### Hooks - 新增 useSystemAccountMiningRecords 和 useSystemAccountTransactions ### 组件 - AccountsTable 新增"已挖积分股"列,显示每个系统账户累计挖到的积分股 - AccountsTable 新增"分类账"按钮,可跳转到账户详情页 ### 新页面 - 新建 /system-accounts/[accountType] 详情页面 - 账户概览卡片:当前算力、已挖积分股、可用余额、挖矿记录数 - 挖矿记录 Tab:分钟级挖矿明细(时间、算力占比、全网算力、每秒分配量、挖得数量) - 交易记录 Tab:所有交易流水(时间、类型、金额、交易前后余额、备注) - 支持分页浏览 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
07498271d3
commit
d957e5a841
|
|
@ -8,12 +8,14 @@
|
||||||
"name": "mining-admin-service",
|
"name": "mining-admin-service",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/axios": "^3.1.3",
|
||||||
"@nestjs/common": "^10.3.0",
|
"@nestjs/common": "^10.3.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.1.1",
|
||||||
"@nestjs/core": "^10.3.0",
|
"@nestjs/core": "^10.3.0",
|
||||||
"@nestjs/platform-express": "^10.3.0",
|
"@nestjs/platform-express": "^10.3.0",
|
||||||
"@nestjs/swagger": "^7.1.17",
|
"@nestjs/swagger": "^7.1.17",
|
||||||
"@prisma/client": "^5.7.1",
|
"@prisma/client": "^5.7.1",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
|
@ -627,6 +629,17 @@
|
||||||
"integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==",
|
"integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/axios": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-RZ/63c1tMxGLqyG3iOCVt7A72oy4x1eM6QEhd4KzCYpaVWW0igq0WSREeRoEZhIxRcZfDfIIkvsOMiM7yfVGZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"axios": "^1.3.1",
|
||||||
|
"rxjs": "^6.0.0 || ^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/cli": {
|
"node_modules/@nestjs/cli": {
|
||||||
"version": "10.4.9",
|
"version": "10.4.9",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz",
|
||||||
|
|
@ -1734,6 +1747,24 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||||
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
|
@ -2212,6 +2243,18 @@
|
||||||
"color-support": "bin.js"
|
"color-support": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
|
|
@ -2433,6 +2476,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/delegates": {
|
"node_modules/delegates": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
|
|
@ -2629,6 +2681,21 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
|
@ -3136,6 +3203,26 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
|
|
@ -3182,6 +3269,22 @@
|
||||||
"webpack": "^5.11.0"
|
"webpack": "^5.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
|
@ -3493,6 +3596,21 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-unicode": {
|
"node_modules/has-unicode": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||||
|
|
@ -4878,6 +4996,12 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,14 @@
|
||||||
"prisma:migrate": "prisma migrate dev"
|
"prisma:migrate": "prisma migrate dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/axios": "^3.1.3",
|
||||||
"@nestjs/common": "^10.3.0",
|
"@nestjs/common": "^10.3.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.1.1",
|
||||||
"@nestjs/core": "^10.3.0",
|
"@nestjs/core": "^10.3.0",
|
||||||
"@nestjs/platform-express": "^10.3.0",
|
"@nestjs/platform-express": "^10.3.0",
|
||||||
"@nestjs/swagger": "^7.1.17",
|
"@nestjs/swagger": "^7.1.17",
|
||||||
"@prisma/client": "^5.7.1",
|
"@prisma/client": "^5.7.1",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||||
import { SystemAccountsService } from '../../application/services/system-accounts.service';
|
import { SystemAccountsService } from '../../application/services/system-accounts.service';
|
||||||
|
|
||||||
@ApiTags('System Accounts')
|
@ApiTags('System Accounts')
|
||||||
|
|
@ -19,4 +19,38 @@ export class SystemAccountsController {
|
||||||
async getSystemAccountsSummary() {
|
async getSystemAccountsSummary() {
|
||||||
return this.systemAccountsService.getSystemAccountsSummary();
|
return this.systemAccountsService.getSystemAccountsSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':accountType/records')
|
||||||
|
@ApiOperation({ summary: '获取系统账户挖矿记录' })
|
||||||
|
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
async getSystemAccountMiningRecords(
|
||||||
|
@Param('accountType') accountType: string,
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
) {
|
||||||
|
return this.systemAccountsService.getSystemAccountMiningRecords(
|
||||||
|
accountType,
|
||||||
|
page ?? 1,
|
||||||
|
pageSize ?? 20,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':accountType/transactions')
|
||||||
|
@ApiOperation({ summary: '获取系统账户交易记录' })
|
||||||
|
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
async getSystemAccountTransactions(
|
||||||
|
@Param('accountType') accountType: string,
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
) {
|
||||||
|
return this.systemAccountsService.getSystemAccountTransactions(
|
||||||
|
accountType,
|
||||||
|
page ?? 1,
|
||||||
|
pageSize ?? 20,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,66 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||||
|
|
||||||
|
interface MiningServiceSystemAccount {
|
||||||
|
accountType: string;
|
||||||
|
name: string;
|
||||||
|
totalMined: string;
|
||||||
|
availableBalance: string;
|
||||||
|
totalContribution: string;
|
||||||
|
lastSyncedAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MiningServiceResponse {
|
||||||
|
accounts: MiningServiceSystemAccount[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SystemAccountsService {
|
export class SystemAccountsService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
private readonly logger = new Logger(SystemAccountsService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 mining-service 获取系统账户挖矿数据
|
||||||
|
*/
|
||||||
|
private async fetchMiningServiceSystemAccounts(): Promise<Map<string, MiningServiceSystemAccount>> {
|
||||||
|
const miningServiceUrl = this.configService.get<string>(
|
||||||
|
'MINING_SERVICE_URL',
|
||||||
|
'http://localhost:3021',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get<MiningServiceResponse>(
|
||||||
|
`${miningServiceUrl}/admin/system-accounts`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const miningDataMap = new Map<string, MiningServiceSystemAccount>();
|
||||||
|
for (const account of response.data.accounts) {
|
||||||
|
miningDataMap.set(account.accountType, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
return miningDataMap;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to fetch mining service system accounts: ${error.message}`,
|
||||||
|
);
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统账户列表
|
* 获取系统账户列表
|
||||||
* 从 CDC 同步的钱包系统账户表读取数据
|
* 从 CDC 同步的钱包系统账户表读取数据,并合并挖矿数据
|
||||||
*/
|
*/
|
||||||
async getSystemAccounts() {
|
async getSystemAccounts() {
|
||||||
// 从 CDC 同步的 SyncedWalletSystemAccount 表获取数据
|
// 从 CDC 同步的 SyncedWalletSystemAccount 表获取数据
|
||||||
|
|
@ -19,6 +72,9 @@ export class SystemAccountsService {
|
||||||
const syncedContributions =
|
const syncedContributions =
|
||||||
await this.prisma.syncedSystemContribution.findMany();
|
await this.prisma.syncedSystemContribution.findMany();
|
||||||
|
|
||||||
|
// 从 mining-service 获取挖矿数据
|
||||||
|
const miningDataMap = await this.fetchMiningServiceSystemAccounts();
|
||||||
|
|
||||||
// 构建算力数据映射
|
// 构建算力数据映射
|
||||||
const contributionMap = new Map<string, any>();
|
const contributionMap = new Map<string, any>();
|
||||||
for (const contrib of syncedContributions) {
|
for (const contrib of syncedContributions) {
|
||||||
|
|
@ -28,6 +84,8 @@ export class SystemAccountsService {
|
||||||
// 构建返回数据
|
// 构建返回数据
|
||||||
const accounts = syncedAccounts.map((account) => {
|
const accounts = syncedAccounts.map((account) => {
|
||||||
const contrib = contributionMap.get(account.accountType);
|
const contrib = contributionMap.get(account.accountType);
|
||||||
|
const miningData = miningDataMap.get(account.accountType);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: account.originalId,
|
id: account.originalId,
|
||||||
accountType: account.accountType,
|
accountType: account.accountType,
|
||||||
|
|
@ -46,6 +104,11 @@ export class SystemAccountsService {
|
||||||
isActive: account.isActive,
|
isActive: account.isActive,
|
||||||
contributionBalance: contrib?.contributionBalance?.toString() || '0',
|
contributionBalance: contrib?.contributionBalance?.toString() || '0',
|
||||||
contributionNeverExpires: contrib?.contributionNeverExpires || false,
|
contributionNeverExpires: contrib?.contributionNeverExpires || false,
|
||||||
|
// 挖矿数据
|
||||||
|
totalMined: miningData?.totalMined || '0',
|
||||||
|
availableBalance: miningData?.availableBalance || '0',
|
||||||
|
miningContribution: miningData?.totalContribution || '0',
|
||||||
|
miningLastSyncedAt: miningData?.lastSyncedAt || null,
|
||||||
syncedAt: account.syncedAt,
|
syncedAt: account.syncedAt,
|
||||||
source: 'cdc',
|
source: 'cdc',
|
||||||
};
|
};
|
||||||
|
|
@ -75,6 +138,15 @@ export class SystemAccountsService {
|
||||||
this.prisma.syncedCirculationPool.findFirst(),
|
this.prisma.syncedCirculationPool.findFirst(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 从 mining-service 获取挖矿数据汇总
|
||||||
|
const miningDataMap = await this.fetchMiningServiceSystemAccounts();
|
||||||
|
|
||||||
|
// 计算总挖矿积分股
|
||||||
|
let totalMined = 0;
|
||||||
|
for (const miningData of miningDataMap.values()) {
|
||||||
|
totalMined += Number(miningData.totalMined || 0);
|
||||||
|
}
|
||||||
|
|
||||||
// 计算总算力
|
// 计算总算力
|
||||||
let totalSyncedContribution = 0n;
|
let totalSyncedContribution = 0n;
|
||||||
for (const contrib of syncedContributions) {
|
for (const contrib of syncedContributions) {
|
||||||
|
|
@ -90,6 +162,7 @@ export class SystemAccountsService {
|
||||||
(sum, acc) => sum + Number(acc.shareBalance),
|
(sum, acc) => sum + Number(acc.shareBalance),
|
||||||
0,
|
0,
|
||||||
).toFixed(8),
|
).toFixed(8),
|
||||||
|
totalMined: totalMined.toFixed(8),
|
||||||
},
|
},
|
||||||
poolAccounts: {
|
poolAccounts: {
|
||||||
count: syncedPoolAccounts.length,
|
count: syncedPoolAccounts.length,
|
||||||
|
|
@ -123,4 +196,68 @@ export class SystemAccountsService {
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统账户挖矿记录
|
||||||
|
*/
|
||||||
|
async getSystemAccountMiningRecords(
|
||||||
|
accountType: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20,
|
||||||
|
) {
|
||||||
|
const miningServiceUrl = this.configService.get<string>(
|
||||||
|
'MINING_SERVICE_URL',
|
||||||
|
'http://localhost:3021',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get(
|
||||||
|
`${miningServiceUrl}/admin/system-accounts/${accountType}/records`,
|
||||||
|
{
|
||||||
|
params: { page, pageSize },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to fetch system account mining records: ${error.message}`,
|
||||||
|
);
|
||||||
|
return { records: [], total: 0, page, pageSize };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统账户交易记录
|
||||||
|
*/
|
||||||
|
async getSystemAccountTransactions(
|
||||||
|
accountType: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20,
|
||||||
|
) {
|
||||||
|
const miningServiceUrl = this.configService.get<string>(
|
||||||
|
'MINING_SERVICE_URL',
|
||||||
|
'http://localhost:3021',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get(
|
||||||
|
`${miningServiceUrl}/admin/system-accounts/${accountType}/transactions`,
|
||||||
|
{
|
||||||
|
params: { page, pageSize },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to fetch system account transactions: ${error.message}`,
|
||||||
|
);
|
||||||
|
return { transactions: [], total: 0, page, pageSize };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
import { Module, Global } from '@nestjs/common';
|
import { Module, Global } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { PrismaModule } from './persistence/prisma/prisma.module';
|
import { PrismaModule } from './persistence/prisma/prisma.module';
|
||||||
import { RedisService } from './redis/redis.service';
|
import { RedisService } from './redis/redis.service';
|
||||||
import { KafkaModule } from './kafka/kafka.module';
|
import { KafkaModule } from './kafka/kafka.module';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, KafkaModule],
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
KafkaModule,
|
||||||
|
HttpModule.register({
|
||||||
|
timeout: 10000,
|
||||||
|
maxRedirects: 5,
|
||||||
|
}),
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: 'REDIS_OPTIONS',
|
provide: 'REDIS_OPTIONS',
|
||||||
|
|
@ -20,6 +28,6 @@ import { KafkaModule } from './kafka/kafka.module';
|
||||||
},
|
},
|
||||||
RedisService,
|
RedisService,
|
||||||
],
|
],
|
||||||
exports: [PrismaModule, RedisService, KafkaModule],
|
exports: [PrismaModule, RedisService, KafkaModule, HttpModule],
|
||||||
})
|
})
|
||||||
export class InfrastructureModule {}
|
export class InfrastructureModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Controller, Get, Post, Body, Query, Param, HttpException, HttpStatus } from '@nestjs/common';
|
import { Controller, Get, Post, Body, Query, Param, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBody, ApiQuery, ApiParam } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBody, ApiQuery, ApiParam } from '@nestjs/swagger';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { SystemAccountType } from '@prisma/client';
|
||||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||||
import { NetworkSyncService } from '../../application/services/network-sync.service';
|
import { NetworkSyncService } from '../../application/services/network-sync.service';
|
||||||
import { ManualMiningService } from '../../application/services/manual-mining.service';
|
import { ManualMiningService } from '../../application/services/manual-mining.service';
|
||||||
|
|
@ -179,6 +180,98 @@ export class AdminController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('system-accounts/:accountType/records')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '获取系统账户挖矿记录' })
|
||||||
|
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型 (OPERATION, PROVINCE, CITY, HEADQUARTERS)' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
async getSystemAccountMiningRecords(
|
||||||
|
@Param('accountType') accountType: string,
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
) {
|
||||||
|
const pageNum = page ?? 1;
|
||||||
|
const pageSizeNum = pageSize ?? 20;
|
||||||
|
const skip = (pageNum - 1) * pageSizeNum;
|
||||||
|
const accountTypeEnum = accountType as SystemAccountType;
|
||||||
|
|
||||||
|
const [records, total] = await Promise.all([
|
||||||
|
this.prisma.systemMiningRecord.findMany({
|
||||||
|
where: { accountType: accountTypeEnum },
|
||||||
|
orderBy: { miningMinute: 'desc' },
|
||||||
|
skip,
|
||||||
|
take: pageSizeNum,
|
||||||
|
}),
|
||||||
|
this.prisma.systemMiningRecord.count({
|
||||||
|
where: { accountType: accountTypeEnum },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
records: records.map((record) => ({
|
||||||
|
id: record.id,
|
||||||
|
accountType: record.accountType,
|
||||||
|
miningMinute: record.miningMinute,
|
||||||
|
contributionRatio: record.contributionRatio.toString(),
|
||||||
|
totalContribution: record.totalContribution.toString(),
|
||||||
|
secondDistribution: record.secondDistribution.toString(),
|
||||||
|
minedAmount: record.minedAmount.toString(),
|
||||||
|
createdAt: record.createdAt,
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page: pageNum,
|
||||||
|
pageSize: pageSizeNum,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('system-accounts/:accountType/transactions')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '获取系统账户交易记录' })
|
||||||
|
@ApiParam({ name: 'accountType', type: String, description: '系统账户类型 (OPERATION, PROVINCE, CITY, HEADQUARTERS)' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
async getSystemAccountTransactions(
|
||||||
|
@Param('accountType') accountType: string,
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
) {
|
||||||
|
const pageNum = page ?? 1;
|
||||||
|
const pageSizeNum = pageSize ?? 20;
|
||||||
|
const skip = (pageNum - 1) * pageSizeNum;
|
||||||
|
const accountTypeEnum = accountType as SystemAccountType;
|
||||||
|
|
||||||
|
const [transactions, total] = await Promise.all([
|
||||||
|
this.prisma.systemMiningTransaction.findMany({
|
||||||
|
where: { accountType: accountTypeEnum },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
skip,
|
||||||
|
take: pageSizeNum,
|
||||||
|
}),
|
||||||
|
this.prisma.systemMiningTransaction.count({
|
||||||
|
where: { accountType: accountTypeEnum },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactions: transactions.map((tx) => ({
|
||||||
|
id: tx.id,
|
||||||
|
accountType: tx.accountType,
|
||||||
|
type: tx.type,
|
||||||
|
amount: tx.amount.toString(),
|
||||||
|
balanceBefore: tx.balanceBefore.toString(),
|
||||||
|
balanceAfter: tx.balanceAfter.toString(),
|
||||||
|
referenceId: tx.referenceId,
|
||||||
|
referenceType: tx.referenceType,
|
||||||
|
memo: tx.memo,
|
||||||
|
createdAt: tx.createdAt,
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page: pageNum,
|
||||||
|
pageSize: pageSizeNum,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Get('mining/status')
|
@Get('mining/status')
|
||||||
@Public()
|
@Public()
|
||||||
@ApiOperation({ summary: '获取挖矿进度状态(类似销毁进度)' })
|
@ApiOperation({ summary: '获取挖矿进度状态(类似销毁进度)' })
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,464 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
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 {
|
||||||
|
AlertCircle,
|
||||||
|
RefreshCw,
|
||||||
|
Pickaxe,
|
||||||
|
Receipt,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { zhCN } from 'date-fns/locale';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useSystemAccounts,
|
||||||
|
useSystemAccountMiningRecords,
|
||||||
|
useSystemAccountTransactions,
|
||||||
|
} from '@/features/system-accounts';
|
||||||
|
import { getAccountDisplayInfo } from '@/types/system-account';
|
||||||
|
import { formatDecimal } from '@/lib/utils/format';
|
||||||
|
|
||||||
|
const TRANSACTION_TYPE_LABELS: Record<string, { label: string; color: string }> = {
|
||||||
|
MINE: { label: '挖矿收益', color: 'bg-green-100 text-green-800' },
|
||||||
|
TRANSFER_OUT: { label: '转出', color: 'bg-red-100 text-red-800' },
|
||||||
|
TRANSFER_IN: { label: '转入', color: 'bg-blue-100 text-blue-800' },
|
||||||
|
ADJUSTMENT: { label: '调整', color: 'bg-yellow-100 text-yellow-800' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SystemAccountDetailPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const accountType = params.accountType as string;
|
||||||
|
|
||||||
|
const [miningPage, setMiningPage] = useState(1);
|
||||||
|
const [transactionPage, setTransactionPage] = useState(1);
|
||||||
|
const pageSize = 20;
|
||||||
|
|
||||||
|
// 获取账户列表以找到当前账户信息
|
||||||
|
const { data: accountsData, isLoading: accountsLoading } = useSystemAccounts();
|
||||||
|
const currentAccount = accountsData?.accounts.find(
|
||||||
|
(a) => a.accountType === accountType
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取挖矿记录
|
||||||
|
const {
|
||||||
|
data: miningRecords,
|
||||||
|
isLoading: miningLoading,
|
||||||
|
error: miningError,
|
||||||
|
} = useSystemAccountMiningRecords(accountType, miningPage, pageSize);
|
||||||
|
|
||||||
|
// 获取交易记录
|
||||||
|
const {
|
||||||
|
data: transactions,
|
||||||
|
isLoading: transactionsLoading,
|
||||||
|
error: transactionsError,
|
||||||
|
} = useSystemAccountTransactions(accountType, transactionPage, pageSize);
|
||||||
|
|
||||||
|
const displayInfo = getAccountDisplayInfo(accountType);
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['system-accounts', accountType],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const miningTotalPages = miningRecords
|
||||||
|
? Math.ceil(miningRecords.total / pageSize)
|
||||||
|
: 0;
|
||||||
|
const transactionTotalPages = transactions
|
||||||
|
? Math.ceil(transactions.total / pageSize)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const accountTitle = currentAccount?.name || displayInfo.label;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 自定义头部 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Link href="/system-accounts">
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
className={`inline-block w-3 h-3 rounded-full ${displayInfo.color.replace('text-', 'bg-')}`}
|
||||||
|
/>
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">{accountTitle}</h1>
|
||||||
|
<code className="text-sm bg-muted px-2 py-1 rounded font-normal">
|
||||||
|
{accountType}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={miningLoading || transactionsLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={`h-4 w-4 mr-2 ${miningLoading || transactionsLoading ? 'animate-spin' : ''}`}
|
||||||
|
/>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 账户概览卡片 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
当前算力
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{accountsLoading ? (
|
||||||
|
<Skeleton className="h-8 w-32" />
|
||||||
|
) : (
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{formatDecimal(
|
||||||
|
currentAccount?.contributionBalance ||
|
||||||
|
currentAccount?.totalContribution ||
|
||||||
|
'0',
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
已挖积分股
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{accountsLoading ? (
|
||||||
|
<Skeleton className="h-8 w-32" />
|
||||||
|
) : (
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{formatDecimal(currentAccount?.totalMined || '0', 8)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
可用余额
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{accountsLoading ? (
|
||||||
|
<Skeleton className="h-8 w-32" />
|
||||||
|
) : (
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{formatDecimal(currentAccount?.availableBalance || '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>
|
||||||
|
|
||||||
|
{/* 分类账 Tabs */}
|
||||||
|
<Tabs defaultValue="mining" className="space-y-4">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="mining" className="flex items-center gap-2">
|
||||||
|
<Pickaxe className="h-4 w-4" />
|
||||||
|
挖矿记录
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="transactions" className="flex items-center gap-2">
|
||||||
|
<Receipt className="h-4 w-4" />
|
||||||
|
交易记录
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* 挖矿记录 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 className="text-right">算力占比</TableHead>
|
||||||
|
<TableHead className="text-right">全网算力</TableHead>
|
||||||
|
<TableHead className="text-right">每秒分配量</TableHead>
|
||||||
|
<TableHead className="text-right">挖得数量</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{miningLoading ? (
|
||||||
|
[...Array(5)].map((_, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
{[...Array(5)].map((_, j) => (
|
||||||
|
<TableCell key={j}>
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : !miningRecords?.records.length ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={5}
|
||||||
|
className="text-center text-muted-foreground py-8"
|
||||||
|
>
|
||||||
|
暂无挖矿记录
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
miningRecords.records.map((record) => (
|
||||||
|
<TableRow key={record.id}>
|
||||||
|
<TableCell>
|
||||||
|
{format(
|
||||||
|
new Date(record.miningMinute),
|
||||||
|
'yyyy-MM-dd HH:mm',
|
||||||
|
{ locale: zhCN }
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono">
|
||||||
|
{(Number(record.contributionRatio) * 100).toFixed(6)}%
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono">
|
||||||
|
{formatDecimal(record.totalContribution, 2)}
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* 交易记录 Tab */}
|
||||||
|
<TabsContent value="transactions">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
交易记录
|
||||||
|
{transactions && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
共 {transactions.total} 条
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{transactionsError ? (
|
||||||
|
<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>备注</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{transactionsLoading ? (
|
||||||
|
[...Array(5)].map((_, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
{[...Array(6)].map((_, j) => (
|
||||||
|
<TableCell key={j}>
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : !transactions?.transactions.length ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={6}
|
||||||
|
className="text-center text-muted-foreground py-8"
|
||||||
|
>
|
||||||
|
暂无交易记录
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
transactions.transactions.map((tx) => {
|
||||||
|
const typeInfo = TRANSACTION_TYPE_LABELS[tx.type] || {
|
||||||
|
label: tx.type,
|
||||||
|
color: 'bg-gray-100 text-gray-800',
|
||||||
|
};
|
||||||
|
const isPositive = Number(tx.amount) > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={tx.id}>
|
||||||
|
<TableCell>
|
||||||
|
{format(
|
||||||
|
new Date(tx.createdAt),
|
||||||
|
'yyyy-MM-dd HH:mm:ss',
|
||||||
|
{ locale: zhCN }
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={`${typeInfo.color} text-xs`}>
|
||||||
|
{typeInfo.label}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
className={`text-right font-mono ${
|
||||||
|
isPositive ? 'text-green-600' : 'text-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isPositive ? '+' : ''}
|
||||||
|
{formatDecimal(tx.amount, 8)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono">
|
||||||
|
{formatDecimal(tx.balanceBefore, 8)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono">
|
||||||
|
{formatDecimal(tx.balanceAfter, 8)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground text-sm max-w-[200px] truncate">
|
||||||
|
{tx.memo || '-'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{transactionTotalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-center gap-2 py-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setTransactionPage((p) => Math.max(1, p - 1))
|
||||||
|
}
|
||||||
|
disabled={transactionPage <= 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-muted-foreground px-4">
|
||||||
|
第 {transactionPage} / {transactionTotalPages} 页
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setTransactionPage((p) =>
|
||||||
|
Math.min(transactionTotalPages, p + 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={transactionPage >= transactionTotalPages}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,44 @@ import type {
|
||||||
SystemAccountsSummary,
|
SystemAccountsSummary,
|
||||||
} from '@/types/system-account';
|
} from '@/types/system-account';
|
||||||
|
|
||||||
|
export interface SystemMiningRecord {
|
||||||
|
id: string;
|
||||||
|
accountType: string;
|
||||||
|
miningMinute: string;
|
||||||
|
contributionRatio: string;
|
||||||
|
totalContribution: string;
|
||||||
|
secondDistribution: string;
|
||||||
|
minedAmount: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemMiningRecordsResponse {
|
||||||
|
records: SystemMiningRecord[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemTransaction {
|
||||||
|
id: string;
|
||||||
|
accountType: string;
|
||||||
|
type: string;
|
||||||
|
amount: string;
|
||||||
|
balanceBefore: string;
|
||||||
|
balanceAfter: string;
|
||||||
|
referenceId?: string;
|
||||||
|
referenceType?: string;
|
||||||
|
memo?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemTransactionsResponse {
|
||||||
|
transactions: SystemTransaction[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const systemAccountsApi = {
|
export const systemAccountsApi = {
|
||||||
/**
|
/**
|
||||||
* Get all system accounts (merged local + synced data)
|
* Get all system accounts (merged local + synced data)
|
||||||
|
|
@ -21,6 +59,36 @@ export const systemAccountsApi = {
|
||||||
const response = await apiClient.get('/system-accounts/summary');
|
const response = await apiClient.get('/system-accounts/summary');
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system account mining records
|
||||||
|
*/
|
||||||
|
getMiningRecords: async (
|
||||||
|
accountType: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
): Promise<SystemMiningRecordsResponse> => {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/system-accounts/${accountType}/records`,
|
||||||
|
{ params: { page, pageSize } }
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system account transactions
|
||||||
|
*/
|
||||||
|
getTransactions: async (
|
||||||
|
accountType: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
): Promise<SystemTransactionsResponse> => {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/system-accounts/${accountType}/transactions`,
|
||||||
|
{ params: { page, pageSize } }
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to categorize accounts for display
|
// Helper to categorize accounts for display
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -11,16 +12,19 @@ import {
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { formatDecimal } from '@/lib/utils/format';
|
import { formatDecimal } from '@/lib/utils/format';
|
||||||
import { getAccountDisplayInfo, type SystemAccount } from '@/types/system-account';
|
import { getAccountDisplayInfo, type SystemAccount } from '@/types/system-account';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { zhCN } from 'date-fns/locale';
|
import { zhCN } from 'date-fns/locale';
|
||||||
|
import { FileText } from 'lucide-react';
|
||||||
|
|
||||||
interface AccountsTableProps {
|
interface AccountsTableProps {
|
||||||
title: string;
|
title: string;
|
||||||
accounts: SystemAccount[];
|
accounts: SystemAccount[];
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
showSyncInfo?: boolean;
|
showSyncInfo?: boolean;
|
||||||
|
showMiningData?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccountsTable({
|
export function AccountsTable({
|
||||||
|
|
@ -28,7 +32,16 @@ export function AccountsTable({
|
||||||
accounts,
|
accounts,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
showSyncInfo = false,
|
showSyncInfo = false,
|
||||||
|
showMiningData = true,
|
||||||
}: AccountsTableProps) {
|
}: AccountsTableProps) {
|
||||||
|
// 计算列数
|
||||||
|
const getColumnCount = () => {
|
||||||
|
let count = 5; // 基础列: 类型, 名称, 算力, 来源, 操作
|
||||||
|
if (showMiningData) count += 1; // 已挖积分股
|
||||||
|
if (showSyncInfo) count += 1; // 同步时间
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
@ -45,19 +58,22 @@ export function AccountsTable({
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[200px]">账户类型</TableHead>
|
<TableHead className="w-[180px]">账户类型</TableHead>
|
||||||
<TableHead>账户名称</TableHead>
|
<TableHead>账户名称</TableHead>
|
||||||
<TableHead className="text-right">余额/算力</TableHead>
|
<TableHead className="text-right">算力</TableHead>
|
||||||
|
{showMiningData && (
|
||||||
|
<TableHead className="text-right">已挖积分股</TableHead>
|
||||||
|
)}
|
||||||
<TableHead>来源</TableHead>
|
<TableHead>来源</TableHead>
|
||||||
{showSyncInfo && <TableHead>同步时间</TableHead>}
|
{showSyncInfo && <TableHead>同步时间</TableHead>}
|
||||||
<TableHead>描述</TableHead>
|
<TableHead className="w-[100px]">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
[...Array(3)].map((_, i) => (
|
[...Array(3)].map((_, i) => (
|
||||||
<TableRow key={i}>
|
<TableRow key={i}>
|
||||||
{[...Array(showSyncInfo ? 6 : 5)].map((_, j) => (
|
{[...Array(getColumnCount())].map((_, j) => (
|
||||||
<TableCell key={j}>
|
<TableCell key={j}>
|
||||||
<Skeleton className="h-4 w-full" />
|
<Skeleton className="h-4 w-full" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -67,7 +83,7 @@ export function AccountsTable({
|
||||||
) : accounts.length === 0 ? (
|
) : accounts.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={showSyncInfo ? 6 : 5}
|
colSpan={getColumnCount()}
|
||||||
className="text-center text-muted-foreground py-8"
|
className="text-center text-muted-foreground py-8"
|
||||||
>
|
>
|
||||||
暂无数据
|
暂无数据
|
||||||
|
|
@ -76,7 +92,8 @@ export function AccountsTable({
|
||||||
) : (
|
) : (
|
||||||
accounts.map((account) => {
|
accounts.map((account) => {
|
||||||
const displayInfo = getAccountDisplayInfo(account.accountType);
|
const displayInfo = getAccountDisplayInfo(account.accountType);
|
||||||
const balance = account.contributionBalance || account.totalContribution || '0';
|
const contribution = account.contributionBalance || account.totalContribution || '0';
|
||||||
|
const totalMined = account.totalMined || '0';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={account.accountType}>
|
<TableRow key={account.accountType}>
|
||||||
|
|
@ -94,13 +111,20 @@ export function AccountsTable({
|
||||||
{account.name || displayInfo.label}
|
{account.name || displayInfo.label}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right font-mono">
|
<TableCell className="text-right font-mono">
|
||||||
{formatDecimal(balance, 8)}
|
{formatDecimal(contribution, 8)}
|
||||||
{account.contributionNeverExpires && (
|
{account.contributionNeverExpires && (
|
||||||
<Badge variant="outline" className="ml-2 text-xs">
|
<Badge variant="outline" className="ml-2 text-xs">
|
||||||
永久
|
永久
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
{showMiningData && (
|
||||||
|
<TableCell className="text-right font-mono">
|
||||||
|
<span className={Number(totalMined) > 0 ? 'text-green-600' : 'text-muted-foreground'}>
|
||||||
|
{formatDecimal(totalMined, 8)}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
variant={account.source === 'synced' ? 'default' : 'secondary'}
|
variant={account.source === 'synced' ? 'default' : 'secondary'}
|
||||||
|
|
@ -119,8 +143,13 @@ export function AccountsTable({
|
||||||
: '-'}
|
: '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
<TableCell className="text-muted-foreground text-sm max-w-[200px] truncate">
|
<TableCell>
|
||||||
{account.description || displayInfo.description}
|
<Link href={`/system-accounts/${account.accountType}`}>
|
||||||
|
<Button variant="ghost" size="sm" className="h-8 px-2">
|
||||||
|
<FileText className="h-4 w-4 mr-1" />
|
||||||
|
分类账
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,33 @@ export function useCategorizedAccounts() {
|
||||||
total: data?.total ?? 0,
|
total: data?.total ?? 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch system account mining records
|
||||||
|
*/
|
||||||
|
export function useSystemAccountMiningRecords(
|
||||||
|
accountType: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['system-accounts', accountType, 'records', page, pageSize],
|
||||||
|
queryFn: () => systemAccountsApi.getMiningRecords(accountType, page, pageSize),
|
||||||
|
enabled: !!accountType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch system account transactions
|
||||||
|
*/
|
||||||
|
export function useSystemAccountTransactions(
|
||||||
|
accountType: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 20
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['system-accounts', accountType, 'transactions', page, pageSize],
|
||||||
|
queryFn: () => systemAccountsApi.getTransactions(accountType, page, pageSize),
|
||||||
|
enabled: !!accountType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
// API
|
// API
|
||||||
export { systemAccountsApi, categorizeAccounts } from './api/system-accounts.api';
|
export {
|
||||||
|
systemAccountsApi,
|
||||||
|
categorizeAccounts,
|
||||||
|
type SystemMiningRecord,
|
||||||
|
type SystemMiningRecordsResponse,
|
||||||
|
type SystemTransaction,
|
||||||
|
type SystemTransactionsResponse,
|
||||||
|
} from './api/system-accounts.api';
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
export {
|
export {
|
||||||
useSystemAccounts,
|
useSystemAccounts,
|
||||||
useSystemAccountsSummary,
|
useSystemAccountsSummary,
|
||||||
useCategorizedAccounts,
|
useCategorizedAccounts,
|
||||||
|
useSystemAccountMiningRecords,
|
||||||
|
useSystemAccountTransactions,
|
||||||
} from './hooks/use-system-accounts';
|
} from './hooks/use-system-accounts';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ export interface SystemAccount {
|
||||||
totalContribution?: string;
|
totalContribution?: string;
|
||||||
contributionBalance?: string;
|
contributionBalance?: string;
|
||||||
contributionNeverExpires?: boolean;
|
contributionNeverExpires?: boolean;
|
||||||
|
// 挖矿数据
|
||||||
|
totalMined?: string;
|
||||||
|
availableBalance?: string;
|
||||||
|
miningContribution?: string;
|
||||||
|
miningLastSyncedAt?: string;
|
||||||
syncedAt?: string;
|
syncedAt?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
source: AccountSource;
|
source: AccountSource;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue