diff --git a/backend/services/authorization-service/src/application/services/authorization-application.service.ts b/backend/services/authorization-service/src/application/services/authorization-application.service.ts index 894ec232..2ced42a8 100644 --- a/backend/services/authorization-service/src/application/services/authorization-application.service.ts +++ b/backend/services/authorization-service/src/application/services/authorization-application.service.ts @@ -234,6 +234,9 @@ export class AuthorizationApplicationService { const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + // 0. 检查用户是否已认种(授权前置条件) + await this.ensureUserHasPlanted(command.accountSequence) + // 1. 检查用户是否已有社区角色 const existingUserCommunity = await this.authorizationRepository.findByAccountSequenceAndRoleType( command.accountSequence, @@ -274,6 +277,9 @@ export class AuthorizationApplicationService { * - 同一个省份只允许授权给一个账户(按省份唯一) */ async grantProvinceCompany(command: GrantProvinceCompanyCommand): Promise { + // 0. 检查用户是否已认种(授权前置条件) + await this.ensureUserHasPlanted(command.accountSequence) + const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) @@ -329,6 +335,9 @@ export class AuthorizationApplicationService { * - 同一个城市只允许一个市区域角色被授权 */ async grantCityCompany(command: GrantCityCompanyCommand): Promise { + // 0. 检查用户是否已认种(授权前置条件) + await this.ensureUserHasPlanted(command.accountSequence) + const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) @@ -382,6 +391,9 @@ export class AuthorizationApplicationService { * 需要验证团队内唯一性:同一推荐链上不能有重复的相同省份授权 */ async grantAuthProvinceCompany(command: GrantAuthProvinceCompanyCommand): Promise { + // 0. 检查用户是否已认种(授权前置条件) + await this.ensureUserHasPlanted(command.accountSequence) + const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) const regionCode = RegionCode.create(command.provinceCode) @@ -419,6 +431,9 @@ export class AuthorizationApplicationService { * 需要验证团队内唯一性:同一推荐链上不能有重复的相同城市授权 */ async grantAuthCityCompany(command: GrantAuthCityCompanyCommand): Promise { + // 0. 检查用户是否已认种(授权前置条件) + await this.ensureUserHasPlanted(command.accountSequence) + const userId = UserId.create(command.userId, command.accountSequence) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) const regionCode = RegionCode.create(command.cityCode) @@ -918,6 +933,25 @@ export class AuthorizationApplicationService { return null } + /** + * 检查用户是否已认种至少一棵树 + * 授权前置条件:用户必须先认种才能被授权任何角色 + */ + private async ensureUserHasPlanted(accountSequence: string): Promise { + const teamStats = await this.referralServiceClient.findByAccountSequence(accountSequence) + const selfPlantingCount = teamStats?.selfPlantingCount || 0 + + if (selfPlantingCount < 1) { + throw new ApplicationError( + `用户 ${accountSequence} 尚未认种任何树,无法授权。请先至少认种1棵树后再进行授权操作。`, + ) + } + + this.logger.debug( + `[ensureUserHasPlanted] User ${accountSequence} has planted ${selfPlantingCount} tree(s), authorization allowed`, + ) + } + /** * 尝试激活授权权益 * 仅当权益未激活时执行激活操作 diff --git a/backend/services/wallet-service/src/api/controllers/ledger.controller.ts b/backend/services/wallet-service/src/api/controllers/ledger.controller.ts index 90e96a20..659657a9 100644 --- a/backend/services/wallet-service/src/api/controllers/ledger.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/ledger.controller.ts @@ -1,11 +1,11 @@ import { Controller, Get, Query, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger'; import { WalletApplicationService } from '@/application/services'; import { GetMyLedgerQuery } from '@/application/queries'; import { CurrentUser, CurrentUserPayload } from '@/shared/decorators'; import { JwtAuthGuard } from '@/shared/guards/jwt-auth.guard'; import { GetMyLedgerQueryDTO } from '@/api/dto/request'; -import { PaginatedLedgerResponseDTO } from '@/api/dto/response'; +import { PaginatedLedgerResponseDTO, LedgerStatisticsResponseDTO, LedgerTrendResponseDTO } from '@/api/dto/response'; @ApiTags('Ledger') @Controller('wallet/ledger') @@ -32,4 +32,25 @@ export class LedgerController { ); return this.walletService.getMyLedger(query); } + + @Get('statistics') + @ApiOperation({ summary: '获取流水统计', description: '按类型汇总用户的收支统计' }) + @ApiResponse({ status: 200, type: LedgerStatisticsResponseDTO }) + async getStatistics( + @CurrentUser() user: CurrentUserPayload, + ): Promise { + return this.walletService.getLedgerStatistics(user.userId); + } + + @Get('trend') + @ApiOperation({ summary: '获取流水趋势', description: '按日期统计用户的收支趋势' }) + @ApiQuery({ name: 'days', required: false, description: '统计天数(默认30天)' }) + @ApiResponse({ status: 200, type: LedgerTrendResponseDTO }) + async getTrend( + @CurrentUser() user: CurrentUserPayload, + @Query('days') days?: string, + ): Promise { + const numDays = days ? parseInt(days, 10) : 30; + return this.walletService.getLedgerTrend(user.userId, numDays); + } } diff --git a/backend/services/wallet-service/src/api/dto/response/ledger.dto.ts b/backend/services/wallet-service/src/api/dto/response/ledger.dto.ts index ba58db60..773a36f0 100644 --- a/backend/services/wallet-service/src/api/dto/response/ledger.dto.ts +++ b/backend/services/wallet-service/src/api/dto/response/ledger.dto.ts @@ -45,3 +45,76 @@ export class PaginatedLedgerResponseDTO { @ApiProperty({ description: '总页数' }) totalPages: number; } + +// ============ 统计相关 DTO ============ + +export class EntryTypeSummaryDTO { + @ApiProperty({ description: '流水类型' }) + entryType: string; + + @ApiProperty({ description: '流水类型中文名' }) + entryTypeName: string; + + @ApiProperty({ description: '总金额' }) + totalAmount: number; + + @ApiProperty({ description: '笔数' }) + count: number; +} + +export class LedgerStatisticsResponseDTO { + @ApiProperty({ description: '总收入' }) + totalIncome: number; + + @ApiProperty({ description: '总支出' }) + totalExpense: number; + + @ApiProperty({ description: '净收益' }) + netAmount: number; + + @ApiProperty({ description: '总笔数' }) + totalCount: number; + + @ApiProperty({ type: [EntryTypeSummaryDTO], description: '按类型汇总' }) + byEntryType: EntryTypeSummaryDTO[]; + + @ApiProperty({ description: '统计时间范围开始' }) + startDate: string; + + @ApiProperty({ description: '统计时间范围结束' }) + endDate: string; +} + +export class DailyTrendItemDTO { + @ApiProperty({ description: '日期' }) + date: string; + + @ApiProperty({ description: '收入' }) + income: number; + + @ApiProperty({ description: '支出' }) + expense: number; + + @ApiProperty({ description: '净收益' }) + net: number; + + @ApiProperty({ description: '笔数' }) + count: number; +} + +export class LedgerTrendResponseDTO { + @ApiProperty({ type: [DailyTrendItemDTO], description: '每日趋势' }) + dailyTrend: DailyTrendItemDTO[]; + + @ApiProperty({ description: '统计天数' }) + days: number; + + @ApiProperty({ description: '期间总收入' }) + periodIncome: number; + + @ApiProperty({ description: '期间总支出' }) + periodExpense: number; + + @ApiProperty({ description: '期间净收益' }) + periodNet: number; +} diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index 2e1183b4..6f95775b 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -1844,4 +1844,189 @@ export class WalletApplicationService { transferredToHeadquarters: totalExpiredUsdt, }; } + + // =============== Ledger Statistics =============== + + /** + * 获取流水统计信息 + */ + async getLedgerStatistics(userId: string): Promise<{ + totalIncome: number; + totalExpense: number; + netAmount: number; + totalCount: number; + byEntryType: Array<{ + entryType: string; + entryTypeName: string; + totalAmount: number; + count: number; + }>; + startDate: string; + endDate: string; + }> { + const userIdBigInt = BigInt(userId); + + // 获取所有流水 + const entries = await this.prisma.ledgerEntry.findMany({ + where: { userId: userIdBigInt, assetType: 'USDT' }, + orderBy: { createdAt: 'asc' }, + }); + + if (entries.length === 0) { + return { + totalIncome: 0, + totalExpense: 0, + netAmount: 0, + totalCount: 0, + byEntryType: [], + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + }; + } + + // 计算收支 + let totalIncome = 0; + let totalExpense = 0; + const byTypeMap = new Map(); + + for (const entry of entries) { + const amount = Number(entry.amount); + if (amount > 0) { + totalIncome += amount; + } else { + totalExpense += Math.abs(amount); + } + + // 按类型汇总 + const existing = byTypeMap.get(entry.entryType) || { totalAmount: 0, count: 0 }; + existing.totalAmount += amount; + existing.count += 1; + byTypeMap.set(entry.entryType, existing); + } + + // 转换为数组 + const byEntryType = Array.from(byTypeMap.entries()).map(([entryType, data]) => ({ + entryType, + entryTypeName: this.getEntryTypeName(entryType), + totalAmount: data.totalAmount, + count: data.count, + })); + + return { + totalIncome, + totalExpense, + netAmount: totalIncome - totalExpense, + totalCount: entries.length, + byEntryType, + startDate: entries[0].createdAt.toISOString(), + endDate: entries[entries.length - 1].createdAt.toISOString(), + }; + } + + /** + * 获取流水趋势 + */ + async getLedgerTrend(userId: string, days: number = 30): Promise<{ + dailyTrend: Array<{ + date: string; + income: number; + expense: number; + net: number; + count: number; + }>; + days: number; + periodIncome: number; + periodExpense: number; + periodNet: number; + }> { + const userIdBigInt = BigInt(userId); + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + startDate.setHours(0, 0, 0, 0); + + const entries = await this.prisma.ledgerEntry.findMany({ + where: { + userId: userIdBigInt, + assetType: 'USDT', + createdAt: { gte: startDate }, + }, + orderBy: { createdAt: 'asc' }, + }); + + // 按日期分组 + const dailyMap = new Map(); + + // 初始化所有日期 + for (let i = 0; i < days; i++) { + const date = new Date(startDate); + date.setDate(date.getDate() + i); + const dateStr = date.toISOString().split('T')[0]; + dailyMap.set(dateStr, { income: 0, expense: 0, count: 0 }); + } + + // 填充数据 + let periodIncome = 0; + let periodExpense = 0; + + for (const entry of entries) { + const dateStr = entry.createdAt.toISOString().split('T')[0]; + const amount = Number(entry.amount); + const existing = dailyMap.get(dateStr) || { income: 0, expense: 0, count: 0 }; + + if (amount > 0) { + existing.income += amount; + periodIncome += amount; + } else { + existing.expense += Math.abs(amount); + periodExpense += Math.abs(amount); + } + existing.count += 1; + dailyMap.set(dateStr, existing); + } + + // 转换为数组 + const dailyTrend = Array.from(dailyMap.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([date, data]) => ({ + date, + income: data.income, + expense: data.expense, + net: data.income - data.expense, + count: data.count, + })); + + return { + dailyTrend, + days, + periodIncome, + periodExpense, + periodNet: periodIncome - periodExpense, + }; + } + + /** + * 获取流水类型中文名 + */ + private getEntryTypeName(entryType: string): string { + const nameMap: Record = { + DEPOSIT_KAVA: '充值 (KAVA)', + DEPOSIT_BSC: '充值 (BSC)', + PLANT_PAYMENT: '认种支付', + PLANT_FREEZE: '认种冻结', + PLANT_UNFREEZE: '认种解冻', + REWARD_PENDING: '待领取奖励', + REWARD_TO_SETTLEABLE: '奖励转可结算', + REWARD_EXPIRED: '奖励过期', + REWARD_SETTLED: '奖励结算', + TRANSFER_TO_POOL: '转入矿池', + SWAP_EXECUTED: '兑换执行', + WITHDRAWAL: '提现', + TRANSFER_IN: '转入', + TRANSFER_OUT: '转出', + FREEZE: '冻结', + UNFREEZE: '解冻', + SYSTEM_ALLOCATION: '系统分配', + }; + return nameMap[entryType] || entryType; + } } diff --git a/frontend/mobile-app/lib/core/services/wallet_service.dart b/frontend/mobile-app/lib/core/services/wallet_service.dart index 3c27dd86..8f153d04 100644 --- a/frontend/mobile-app/lib/core/services/wallet_service.dart +++ b/frontend/mobile-app/lib/core/services/wallet_service.dart @@ -375,6 +375,116 @@ class WalletService { rethrow; } } + + // =============== 账本流水相关 API =============== + + /// 获取账本流水列表 + /// + /// 调用 GET /wallet/ledger/my-ledger (wallet-service) + Future getLedger({ + int page = 1, + int pageSize = 20, + String? entryType, + String? assetType, + DateTime? startDate, + DateTime? endDate, + }) async { + try { + debugPrint('[WalletService] ========== 获取账本流水 =========='); + debugPrint('[WalletService] 请求: GET /wallet/ledger/my-ledger'); + debugPrint('[WalletService] 参数: page=$page, pageSize=$pageSize, entryType=$entryType'); + + final queryParams = { + 'page': page.toString(), + 'pageSize': pageSize.toString(), + }; + if (entryType != null) queryParams['entryType'] = entryType; + if (assetType != null) queryParams['assetType'] = assetType; + if (startDate != null) queryParams['startDate'] = startDate.toIso8601String(); + if (endDate != null) queryParams['endDate'] = endDate.toIso8601String(); + + final queryString = queryParams.entries.map((e) => '${e.key}=${e.value}').join('&'); + final response = await _apiClient.get('/wallet/ledger/my-ledger?$queryString'); + + debugPrint('[WalletService] 响应状态码: ${response.statusCode}'); + + if (response.statusCode == 200) { + final responseData = response.data as Map; + final data = responseData['data'] as Map? ?? responseData; + final result = PaginatedLedger.fromJson(data); + debugPrint('[WalletService] 获取成功: ${result.data.length} 条流水, 共 ${result.total} 条'); + debugPrint('[WalletService] ================================'); + return result; + } + + throw Exception('获取账本流水失败: ${response.statusCode}'); + } catch (e, stackTrace) { + debugPrint('[WalletService] !!!!!!!!!! 获取账本流水异常 !!!!!!!!!!'); + debugPrint('[WalletService] 错误: $e'); + debugPrint('[WalletService] 堆栈: $stackTrace'); + rethrow; + } + } + + /// 获取流水统计 + /// + /// 调用 GET /wallet/ledger/statistics (wallet-service) + Future getLedgerStatistics() async { + try { + debugPrint('[WalletService] ========== 获取流水统计 =========='); + debugPrint('[WalletService] 请求: GET /wallet/ledger/statistics'); + + final response = await _apiClient.get('/wallet/ledger/statistics'); + + debugPrint('[WalletService] 响应状态码: ${response.statusCode}'); + + if (response.statusCode == 200) { + final responseData = response.data as Map; + final data = responseData['data'] as Map? ?? responseData; + final result = LedgerStatistics.fromJson(data); + debugPrint('[WalletService] 获取成功: 总收入=${result.totalIncome}, 总支出=${result.totalExpense}'); + debugPrint('[WalletService] ================================'); + return result; + } + + throw Exception('获取流水统计失败: ${response.statusCode}'); + } catch (e, stackTrace) { + debugPrint('[WalletService] !!!!!!!!!! 获取流水统计异常 !!!!!!!!!!'); + debugPrint('[WalletService] 错误: $e'); + debugPrint('[WalletService] 堆栈: $stackTrace'); + rethrow; + } + } + + /// 获取流水趋势 + /// + /// 调用 GET /wallet/ledger/trend (wallet-service) + Future getLedgerTrend({int days = 30}) async { + try { + debugPrint('[WalletService] ========== 获取流水趋势 =========='); + debugPrint('[WalletService] 请求: GET /wallet/ledger/trend?days=$days'); + + final response = await _apiClient.get('/wallet/ledger/trend?days=$days'); + + debugPrint('[WalletService] 响应状态码: ${response.statusCode}'); + + if (response.statusCode == 200) { + final responseData = response.data as Map; + final data = responseData['data'] as Map? ?? responseData; + final result = LedgerTrend.fromJson(data); + debugPrint('[WalletService] 获取成功: ${result.dailyTrend.length} 天数据'); + debugPrint('[WalletService] ================================'); + return result; + } + + throw Exception('获取流水趋势失败: ${response.statusCode}'); + } catch (e, stackTrace) { + debugPrint('[WalletService] !!!!!!!!!! 获取流水趋势异常 !!!!!!!!!!'); + debugPrint('[WalletService] 错误: $e'); + debugPrint('[WalletService] 堆栈: $stackTrace'); + rethrow; + } + } } /// 提取响应 @@ -460,3 +570,214 @@ class WithdrawRecord { ); } } + +// =============== 账本流水相关模型 =============== + +/// 账本流水条目 +class LedgerEntry { + final String id; + final String entryType; + final double amount; + final String assetType; + final double? balanceAfter; + final String? refOrderId; + final String? refTxHash; + final String? memo; + final DateTime createdAt; + + LedgerEntry({ + required this.id, + required this.entryType, + required this.amount, + required this.assetType, + this.balanceAfter, + this.refOrderId, + this.refTxHash, + this.memo, + required this.createdAt, + }); + + factory LedgerEntry.fromJson(Map json) { + return LedgerEntry( + id: json['id']?.toString() ?? '', + entryType: json['entryType'] ?? '', + amount: (json['amount'] ?? 0).toDouble(), + assetType: json['assetType'] ?? 'USDT', + balanceAfter: json['balanceAfter']?.toDouble(), + refOrderId: json['refOrderId'], + refTxHash: json['refTxHash'], + memo: json['memo'], + createdAt: json['createdAt'] != null + ? DateTime.tryParse(json['createdAt']) ?? DateTime.now() + : DateTime.now(), + ); + } + + /// 获取流水类型中文名 + String get entryTypeName { + const nameMap = { + 'DEPOSIT_KAVA': '充值 (KAVA)', + 'DEPOSIT_BSC': '充值 (BSC)', + 'PLANT_PAYMENT': '认种支付', + 'PLANT_FREEZE': '认种冻结', + 'PLANT_UNFREEZE': '认种解冻', + 'REWARD_PENDING': '待领取奖励', + 'REWARD_TO_SETTLEABLE': '奖励转可结算', + 'REWARD_EXPIRED': '奖励过期', + 'REWARD_SETTLED': '奖励结算', + 'TRANSFER_TO_POOL': '转入矿池', + 'SWAP_EXECUTED': '兑换执行', + 'WITHDRAWAL': '提现', + 'TRANSFER_IN': '转入', + 'TRANSFER_OUT': '转出', + 'FREEZE': '冻结', + 'UNFREEZE': '解冻', + 'SYSTEM_ALLOCATION': '系统分配', + }; + return nameMap[entryType] ?? entryType; + } + + /// 是否为收入 + bool get isIncome => amount > 0; +} + +/// 分页的账本流水响应 +class PaginatedLedger { + final List data; + final int total; + final int page; + final int pageSize; + final int totalPages; + + PaginatedLedger({ + required this.data, + required this.total, + required this.page, + required this.pageSize, + required this.totalPages, + }); + + factory PaginatedLedger.fromJson(Map json) { + final dataList = json['data'] as List? ?? []; + return PaginatedLedger( + data: dataList.map((e) => LedgerEntry.fromJson(e as Map)).toList(), + total: json['total'] ?? 0, + page: json['page'] ?? 1, + pageSize: json['pageSize'] ?? 20, + totalPages: json['totalPages'] ?? 1, + ); + } +} + +/// 按类型汇总的统计项 +class EntryTypeSummary { + final String entryType; + final String entryTypeName; + final double totalAmount; + final int count; + + EntryTypeSummary({ + required this.entryType, + required this.entryTypeName, + required this.totalAmount, + required this.count, + }); + + factory EntryTypeSummary.fromJson(Map json) { + return EntryTypeSummary( + entryType: json['entryType'] ?? '', + entryTypeName: json['entryTypeName'] ?? '', + totalAmount: (json['totalAmount'] ?? 0).toDouble(), + count: json['count'] ?? 0, + ); + } +} + +/// 流水统计 +class LedgerStatistics { + final double totalIncome; + final double totalExpense; + final double netAmount; + final int totalCount; + final List byEntryType; + final DateTime startDate; + final DateTime endDate; + + LedgerStatistics({ + required this.totalIncome, + required this.totalExpense, + required this.netAmount, + required this.totalCount, + required this.byEntryType, + required this.startDate, + required this.endDate, + }); + + factory LedgerStatistics.fromJson(Map json) { + final byTypeList = json['byEntryType'] as List? ?? []; + return LedgerStatistics( + totalIncome: (json['totalIncome'] ?? 0).toDouble(), + totalExpense: (json['totalExpense'] ?? 0).toDouble(), + netAmount: (json['netAmount'] ?? 0).toDouble(), + totalCount: json['totalCount'] ?? 0, + byEntryType: byTypeList.map((e) => EntryTypeSummary.fromJson(e as Map)).toList(), + startDate: DateTime.tryParse(json['startDate'] ?? '') ?? DateTime.now(), + endDate: DateTime.tryParse(json['endDate'] ?? '') ?? DateTime.now(), + ); + } +} + +/// 每日趋势项 +class DailyTrendItem { + final String date; + final double income; + final double expense; + final double net; + final int count; + + DailyTrendItem({ + required this.date, + required this.income, + required this.expense, + required this.net, + required this.count, + }); + + factory DailyTrendItem.fromJson(Map json) { + return DailyTrendItem( + date: json['date'] ?? '', + income: (json['income'] ?? 0).toDouble(), + expense: (json['expense'] ?? 0).toDouble(), + net: (json['net'] ?? 0).toDouble(), + count: json['count'] ?? 0, + ); + } +} + +/// 流水趋势 +class LedgerTrend { + final List dailyTrend; + final int days; + final double periodIncome; + final double periodExpense; + final double periodNet; + + LedgerTrend({ + required this.dailyTrend, + required this.days, + required this.periodIncome, + required this.periodExpense, + required this.periodNet, + }); + + factory LedgerTrend.fromJson(Map json) { + final trendList = json['dailyTrend'] as List? ?? []; + return LedgerTrend( + dailyTrend: trendList.map((e) => DailyTrendItem.fromJson(e as Map)).toList(), + days: json['days'] ?? 30, + periodIncome: (json['periodIncome'] ?? 0).toDouble(), + periodExpense: (json['periodExpense'] ?? 0).toDouble(), + periodNet: (json['periodNet'] ?? 0).toDouble(), + ); + } +} diff --git a/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart b/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart new file mode 100644 index 00000000..3c63ad52 --- /dev/null +++ b/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart @@ -0,0 +1,983 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import '../../../../core/di/injection_container.dart'; +import '../../../../core/services/wallet_service.dart'; + +/// 账本明细页面 - 显示用户的流水账、统计图表和筛选功能 +class LedgerDetailPage extends ConsumerStatefulWidget { + const LedgerDetailPage({super.key}); + + @override + ConsumerState createState() => _LedgerDetailPageState(); +} + +class _LedgerDetailPageState extends ConsumerState + with SingleTickerProviderStateMixin { + late TabController _tabController; + + // 数据状态 + bool _isLoading = true; + LedgerStatistics? _statistics; + LedgerTrend? _trend; + PaginatedLedger? _ledger; + String? _errorMessage; + + // 筛选条件 + String? _selectedEntryType; + int _currentPage = 1; + static const int _pageSize = 20; + + // 流水类型选项 + final List> _entryTypes = [ + {'value': '', 'label': '全部'}, + {'value': 'DEPOSIT_KAVA', 'label': '充值 (KAVA)'}, + {'value': 'DEPOSIT_BSC', 'label': '充值 (BSC)'}, + {'value': 'PLANT_PAYMENT', 'label': '认种支付'}, + {'value': 'REWARD_TO_SETTLEABLE', 'label': '奖励转可结算'}, + {'value': 'REWARD_EXPIRED', 'label': '奖励过期'}, + {'value': 'WITHDRAWAL', 'label': '提现'}, + {'value': 'TRANSFER_IN', 'label': '转入'}, + {'value': 'TRANSFER_OUT', 'label': '转出'}, + ]; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _loadData(); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + /// 加载所有数据 + Future _loadData() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + final walletService = ref.read(walletServiceProvider); + + // 并行加载统计和趋势 + final results = await Future.wait([ + walletService.getLedgerStatistics(), + walletService.getLedgerTrend(days: 30), + walletService.getLedger( + page: _currentPage, + pageSize: _pageSize, + entryType: _selectedEntryType, + ), + ]); + + if (mounted) { + setState(() { + _statistics = results[0] as LedgerStatistics; + _trend = results[1] as LedgerTrend; + _ledger = results[2] as PaginatedLedger; + _isLoading = false; + }); + } + } catch (e) { + debugPrint('[LedgerDetailPage] 加载数据失败: $e'); + if (mounted) { + setState(() { + _errorMessage = '加载失败: $e'; + _isLoading = false; + }); + } + } + } + + /// 加载更多流水 + Future _loadMoreLedger() async { + if (_ledger == null || _currentPage >= _ledger!.totalPages) return; + + try { + final walletService = ref.read(walletServiceProvider); + final nextPage = _currentPage + 1; + final newLedger = await walletService.getLedger( + page: nextPage, + pageSize: _pageSize, + entryType: _selectedEntryType, + ); + + if (mounted) { + setState(() { + _currentPage = nextPage; + _ledger = PaginatedLedger( + data: [..._ledger!.data, ...newLedger.data], + total: newLedger.total, + page: nextPage, + pageSize: _pageSize, + totalPages: newLedger.totalPages, + ); + }); + } + } catch (e) { + debugPrint('[LedgerDetailPage] 加载更多失败: $e'); + } + } + + /// 筛选流水类型 + Future _filterByEntryType(String? entryType) async { + setState(() { + _selectedEntryType = entryType?.isEmpty == true ? null : entryType; + _currentPage = 1; + _isLoading = true; + }); + + try { + final walletService = ref.read(walletServiceProvider); + final newLedger = await walletService.getLedger( + page: 1, + pageSize: _pageSize, + entryType: _selectedEntryType, + ); + + if (mounted) { + setState(() { + _ledger = newLedger; + _isLoading = false; + }); + } + } catch (e) { + debugPrint('[LedgerDetailPage] 筛选失败: $e'); + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + /// 格式化金额 + String _formatAmount(double amount) { + final formatter = NumberFormat('#,##0.00', 'zh_CN'); + return formatter.format(amount); + } + + /// 格式化日期 + String _formatDate(DateTime date) { + return DateFormat('yyyy-MM-dd HH:mm').format(date); + } + + /// 格式化短日期 + String _formatShortDate(String date) { + try { + final parsed = DateTime.parse(date); + return DateFormat('MM/dd').format(parsed); + } catch (e) { + return date; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFFFF5E6), + Color(0xFFFFE4B5), + ], + ), + ), + child: SafeArea( + child: Column( + children: [ + // 顶部标题栏 + _buildAppBar(), + // Tab 切换 + _buildTabBar(), + // 内容区域 + Expanded( + child: _isLoading + ? const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFFD4AF37)), + ), + ) + : _errorMessage != null + ? _buildErrorView() + : TabBarView( + controller: _tabController, + children: [ + _buildStatisticsTab(), + _buildLedgerListTab(), + ], + ), + ), + ], + ), + ), + ), + ); + } + + /// 构建顶部标题栏 + Widget _buildAppBar() { + return Container( + height: 56, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: const Icon( + Icons.arrow_back_ios, + color: Color(0xFF5D4037), + size: 20, + ), + ), + const SizedBox(width: 8), + const Text( + '账本明细', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + ), + const Spacer(), + // 刷新按钮 + GestureDetector( + onTap: _loadData, + child: const Icon( + Icons.refresh, + color: Color(0xFF5D4037), + size: 24, + ), + ), + ], + ), + ); + } + + /// 构建 Tab 切换栏 + Widget _buildTabBar() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: const Color(0x1A8B5A2B), + borderRadius: BorderRadius.circular(8), + ), + child: TabBar( + controller: _tabController, + indicator: BoxDecoration( + color: const Color(0xFFD4AF37), + borderRadius: BorderRadius.circular(8), + ), + indicatorSize: TabBarIndicatorSize.tab, + labelColor: Colors.white, + unselectedLabelColor: const Color(0xFF5D4037), + labelStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + tabs: const [ + Tab(text: '统计概览'), + Tab(text: '流水明细'), + ], + ), + ); + } + + /// 构建错误视图 + Widget _buildErrorView() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 48, + color: Color(0xFF8B5A2B), + ), + const SizedBox(height: 16), + Text( + _errorMessage ?? '加载失败', + style: const TextStyle( + color: Color(0xFF5D4037), + fontSize: 14, + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadData, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD4AF37), + ), + child: const Text('重试'), + ), + ], + ), + ); + } + + /// 构建统计概览 Tab + Widget _buildStatisticsTab() { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 收支概览卡片 + _buildSummaryCard(), + const SizedBox(height: 16), + // 趋势图表 + _buildTrendChart(), + const SizedBox(height: 16), + // 按类型统计 + _buildTypeBreakdown(), + ], + ), + ); + } + + /// 构建收支概览卡片 + Widget _buildSummaryCard() { + final stats = _statistics; + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + const Text( + '收支概览', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: _buildSummaryItem( + '总收入', + stats?.totalIncome ?? 0, + const Color(0xFF4CAF50), + Icons.arrow_downward, + ), + ), + Container( + width: 1, + height: 60, + color: const Color(0x1A8B5A2B), + ), + Expanded( + child: _buildSummaryItem( + '总支出', + stats?.totalExpense ?? 0, + const Color(0xFFE53935), + Icons.arrow_upward, + ), + ), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0x1AD4AF37), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '净收益', + style: TextStyle( + fontSize: 14, + color: Color(0xFF5D4037), + ), + ), + Text( + '${(stats?.netAmount ?? 0) >= 0 ? '+' : ''}${_formatAmount(stats?.netAmount ?? 0)} 绿积分', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: (stats?.netAmount ?? 0) >= 0 + ? const Color(0xFF4CAF50) + : const Color(0xFFE53935), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + Text( + '共 ${stats?.totalCount ?? 0} 笔交易', + style: const TextStyle( + fontSize: 12, + color: Color(0x995D4037), + ), + ), + ], + ), + ); + } + + /// 构建收支概览项 + Widget _buildSummaryItem(String label, double amount, Color color, IconData icon) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 4), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Color(0x995D4037), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + _formatAmount(amount), + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: color, + ), + ), + const Text( + '绿积分', + style: TextStyle( + fontSize: 10, + color: Color(0x995D4037), + ), + ), + ], + ); + } + + /// 构建趋势图表 + Widget _buildTrendChart() { + final trend = _trend; + if (trend == null || trend.dailyTrend.isEmpty) { + return const SizedBox.shrink(); + } + + // 找出最大值用于计算比例 + double maxValue = 1; + for (final item in trend.dailyTrend) { + if (item.income > maxValue) maxValue = item.income; + if (item.expense > maxValue) maxValue = item.expense; + } + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '近30天趋势', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + ), + Row( + children: [ + _buildLegendItem('收入', const Color(0xFF4CAF50)), + const SizedBox(width: 12), + _buildLegendItem('支出', const Color(0xFFE53935)), + ], + ), + ], + ), + const SizedBox(height: 20), + // 简化的柱状图 + SizedBox( + height: 120, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // 只显示最近7天 + ...trend.dailyTrend.reversed.take(7).toList().reversed.map((item) { + final incomeHeight = (item.income / maxValue) * 100; + final expenseHeight = (item.expense / maxValue) * 100; + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + width: 8, + height: incomeHeight.clamp(2, 100), + decoration: BoxDecoration( + color: const Color(0xFF4CAF50), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 2), + Container( + width: 8, + height: expenseHeight.clamp(2, 100), + decoration: BoxDecoration( + color: const Color(0xFFE53935), + borderRadius: BorderRadius.circular(2), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + _formatShortDate(item.date), + style: const TextStyle( + fontSize: 8, + color: Color(0x995D4037), + ), + ), + ], + ), + ), + ); + }), + ], + ), + ), + const SizedBox(height: 16), + // 期间汇总 + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildPeriodSummary('期间收入', trend.periodIncome, const Color(0xFF4CAF50)), + _buildPeriodSummary('期间支出', trend.periodExpense, const Color(0xFFE53935)), + _buildPeriodSummary('期间净收益', trend.periodNet, + trend.periodNet >= 0 ? const Color(0xFF4CAF50) : const Color(0xFFE53935)), + ], + ), + ], + ), + ); + } + + /// 构建图例项 + Widget _buildLegendItem(String label, Color color) { + return Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 4), + Text( + label, + style: const TextStyle( + fontSize: 10, + color: Color(0x995D4037), + ), + ), + ], + ); + } + + /// 构建期间汇总项 + Widget _buildPeriodSummary(String label, double amount, Color color) { + return Column( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 10, + color: Color(0x995D4037), + ), + ), + const SizedBox(height: 4), + Text( + _formatAmount(amount), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ], + ); + } + + /// 构建按类型统计 + Widget _buildTypeBreakdown() { + final stats = _statistics; + if (stats == null || stats.byEntryType.isEmpty) { + return const SizedBox.shrink(); + } + + // 按金额绝对值排序 + final sortedTypes = List.from(stats.byEntryType) + ..sort((a, b) => b.totalAmount.abs().compareTo(a.totalAmount.abs())); + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '按类型统计', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 16), + ...sortedTypes.take(8).map((item) => _buildTypeItem(item)), + ], + ), + ); + } + + /// 构建类型统计项 + Widget _buildTypeItem(EntryTypeSummary item) { + final isIncome = item.totalAmount > 0; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: isIncome + ? const Color(0x1A4CAF50) + : const Color(0x1AE53935), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + isIncome ? Icons.arrow_downward : Icons.arrow_upward, + size: 16, + color: isIncome ? const Color(0xFF4CAF50) : const Color(0xFFE53935), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.entryTypeName, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF5D4037), + ), + ), + Text( + '${item.count} 笔', + style: const TextStyle( + fontSize: 10, + color: Color(0x995D4037), + ), + ), + ], + ), + ), + Text( + '${isIncome ? '+' : ''}${_formatAmount(item.totalAmount)}', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isIncome ? const Color(0xFF4CAF50) : const Color(0xFFE53935), + ), + ), + ], + ), + ); + } + + /// 构建流水明细 Tab + Widget _buildLedgerListTab() { + return Column( + children: [ + // 筛选栏 + _buildFilterBar(), + // 流水列表 + Expanded( + child: _ledger == null || _ledger!.data.isEmpty + ? _buildEmptyView() + : _buildLedgerList(), + ), + ], + ); + } + + /// 构建筛选栏 + Widget _buildFilterBar() { + return Container( + height: 44, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _entryTypes.length, + itemBuilder: (context, index) { + final type = _entryTypes[index]; + final isSelected = (_selectedEntryType ?? '') == type['value']; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () => _filterByEntryType(type['value']), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFFD4AF37) : const Color(0x1A8B5A2B), + borderRadius: BorderRadius.circular(20), + ), + child: Center( + child: Text( + type['label']!, + style: TextStyle( + fontSize: 12, + color: isSelected ? Colors.white : const Color(0xFF5D4037), + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ), + ), + ); + }, + ), + ); + } + + /// 构建空视图 + Widget _buildEmptyView() { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.receipt_long_outlined, + size: 64, + color: Color(0x808B5A2B), + ), + SizedBox(height: 16), + Text( + '暂无流水记录', + style: TextStyle( + fontSize: 14, + color: Color(0x995D4037), + ), + ), + ], + ), + ); + } + + /// 构建流水列表 + Widget _buildLedgerList() { + return NotificationListener( + onNotification: (notification) { + if (notification is ScrollEndNotification && + notification.metrics.extentAfter < 100) { + _loadMoreLedger(); + } + return false; + }, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _ledger!.data.length + 1, + itemBuilder: (context, index) { + if (index == _ledger!.data.length) { + // 底部加载更多指示器 + if (_currentPage < _ledger!.totalPages) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Color(0xFFD4AF37)), + ), + ), + ), + ); + } + return Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Text( + '共 ${_ledger!.total} 条记录', + style: const TextStyle( + fontSize: 12, + color: Color(0x995D4037), + ), + ), + ), + ); + } + + final entry = _ledger!.data[index]; + return _buildLedgerItem(entry); + }, + ), + ); + } + + /// 构建流水项 + Widget _buildLedgerItem(LedgerEntry entry) { + final isIncome = entry.isIncome; + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // 类型图标 + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isIncome + ? const Color(0x1A4CAF50) + : const Color(0x1AE53935), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + isIncome ? Icons.arrow_downward : Icons.arrow_upward, + size: 20, + color: isIncome ? const Color(0xFF4CAF50) : const Color(0xFFE53935), + ), + ), + const SizedBox(width: 12), + // 类型和时间 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + entry.entryTypeName, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 4), + Text( + _formatDate(entry.createdAt), + style: const TextStyle( + fontSize: 10, + color: Color(0x995D4037), + ), + ), + if (entry.memo != null && entry.memo!.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + entry.memo!, + style: const TextStyle( + fontSize: 10, + color: Color(0x995D4037), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + // 金额 + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${isIncome ? '+' : ''}${_formatAmount(entry.amount)}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: isIncome ? const Color(0xFF4CAF50) : const Color(0xFFE53935), + ), + ), + if (entry.balanceAfter != null) ...[ + const SizedBox(height: 4), + Text( + '余额: ${_formatAmount(entry.balanceAfter!)}', + style: const TextStyle( + fontSize: 10, + color: Color(0x995D4037), + ), + ), + ], + ], + ), + ], + ), + ); + } +} diff --git a/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart b/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart index 92d4465a..5852addf 100644 --- a/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart +++ b/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart @@ -281,18 +281,39 @@ class _TradingPageState extends ConsumerState { return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), - child: const Center( - child: Text( - '兑换', - style: TextStyle( - fontSize: 18, - fontFamily: 'Inter', - fontWeight: FontWeight.w700, - height: 1.25, - letterSpacing: -0.27, - color: Color(0xFF5D4037), + child: Row( + children: [ + const Spacer(), + const Text( + '兑换', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.25, + letterSpacing: -0.27, + color: Color(0xFF5D4037), + ), ), - ), + const Spacer(), + // 账本明细入口 + GestureDetector( + onTap: () => context.push(RoutePaths.ledgerDetail), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0x1A8B5A2B), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.receipt_long_outlined, + color: Color(0xFF5D4037), + size: 20, + ), + ), + ), + ], ), ); } diff --git a/frontend/mobile-app/lib/routes/app_router.dart b/frontend/mobile-app/lib/routes/app_router.dart index b79cb50a..b978b2d8 100644 --- a/frontend/mobile-app/lib/routes/app_router.dart +++ b/frontend/mobile-app/lib/routes/app_router.dart @@ -13,6 +13,7 @@ import '../features/home/presentation/pages/home_shell_page.dart'; import '../features/ranking/presentation/pages/ranking_page.dart'; import '../features/mining/presentation/pages/mining_page.dart'; import '../features/trading/presentation/pages/trading_page.dart'; +import '../features/trading/presentation/pages/ledger_detail_page.dart'; import '../features/profile/presentation/pages/profile_page.dart'; import '../features/profile/presentation/pages/edit_profile_page.dart'; import '../features/share/presentation/pages/share_page.dart'; @@ -257,6 +258,13 @@ final appRouterProvider = Provider((ref) { builder: (context, state) => const BindEmailPage(), ), + // Ledger Detail Page (账本明细) + GoRoute( + path: RoutePaths.ledgerDetail, + name: RouteNames.ledgerDetail, + builder: (context, state) => const LedgerDetailPage(), + ), + // Withdraw USDT Page (USDT 提款) GoRoute( path: RoutePaths.withdrawUsdt, diff --git a/frontend/mobile-app/lib/routes/route_names.dart b/frontend/mobile-app/lib/routes/route_names.dart index 99c0aabf..0b7daa63 100644 --- a/frontend/mobile-app/lib/routes/route_names.dart +++ b/frontend/mobile-app/lib/routes/route_names.dart @@ -32,6 +32,7 @@ class RouteNames { static const changePassword = 'change-password'; static const bindEmail = 'bind-email'; static const transactionHistory = 'transaction-history'; + static const ledgerDetail = 'ledger-detail'; static const withdrawUsdt = 'withdraw-usdt'; static const withdrawConfirm = 'withdraw-confirm'; diff --git a/frontend/mobile-app/lib/routes/route_paths.dart b/frontend/mobile-app/lib/routes/route_paths.dart index 9ff5c92c..180b9e59 100644 --- a/frontend/mobile-app/lib/routes/route_paths.dart +++ b/frontend/mobile-app/lib/routes/route_paths.dart @@ -32,6 +32,7 @@ class RoutePaths { static const changePassword = '/security/password'; static const bindEmail = '/security/email'; static const transactionHistory = '/trading/history'; + static const ledgerDetail = '/trading/ledger'; static const withdrawUsdt = '/withdraw/usdt'; static const withdrawConfirm = '/withdraw/confirm';