From 6bcb4af02854fdaf917511eec3c575cce1786f58 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 14 Jan 2026 08:22:40 -0800 Subject: [PATCH] feat(mining-app): integrate real APIs for Asset and Profile pages - Asset page now uses trading-service /asset/my endpoint - Profile page integrates auth-service /user/profile and contribution-service - Add new entities: AssetDisplay, PriceInfo, MarketOverview, TradingAccount - Add corresponding models with JSON parsing - Create asset_providers and profile_providers for state management - Update trading_providers with real API integration - Extend UserState and UserInfo with additional profile fields - Remove obsolete buy_shares and sell_shares use cases - Fix compilation errors in get_current_price and trading_page Co-Authored-By: Claude Opus 4.5 --- .../mining-app/lib/core/di/injection.dart | 8 - .../lib/core/network/api_endpoints.dart | 33 +- .../remote/auth_remote_datasource.dart | 18 + .../remote/trading_remote_datasource.dart | 195 +++++-- .../lib/data/models/asset_display_model.dart | 55 ++ .../lib/data/models/contribution_model.dart | 27 +- .../data/models/market_overview_model.dart | 29 + .../lib/data/models/price_info_model.dart | 29 + .../lib/data/models/trade_order_model.dart | 54 +- .../data/models/trading_account_model.dart | 29 + .../repositories/trading_repository_impl.dart | 167 +++--- .../lib/domain/entities/asset_display.dart | 91 ++++ .../lib/domain/entities/contribution.dart | 54 +- .../lib/domain/entities/market_overview.dart | 48 ++ .../lib/domain/entities/price_info.dart | 44 ++ .../lib/domain/entities/trade_order.dart | 37 +- .../lib/domain/entities/trading_account.dart | 48 ++ .../repositories/trading_repository.dart | 54 +- .../domain/usecases/trading/buy_shares.dart | 24 - .../usecases/trading/get_current_price.dart | 3 +- .../domain/usecases/trading/sell_shares.dart | 24 - .../presentation/pages/asset/asset_page.dart | 134 +++-- .../pages/contribution/contribution_page.dart | 10 +- .../pages/profile/profile_page.dart | 200 ++++--- .../pages/trading/trading_page.dart | 498 +++++++++--------- .../providers/asset_providers.dart | 77 +++ .../providers/profile_providers.dart | 91 ++++ .../providers/trading_providers.dart | 189 +++++-- .../providers/user_providers.dart | 42 ++ 29 files changed, 1690 insertions(+), 622 deletions(-) create mode 100644 frontend/mining-app/lib/data/models/asset_display_model.dart create mode 100644 frontend/mining-app/lib/data/models/market_overview_model.dart create mode 100644 frontend/mining-app/lib/data/models/price_info_model.dart create mode 100644 frontend/mining-app/lib/data/models/trading_account_model.dart create mode 100644 frontend/mining-app/lib/domain/entities/asset_display.dart create mode 100644 frontend/mining-app/lib/domain/entities/market_overview.dart create mode 100644 frontend/mining-app/lib/domain/entities/price_info.dart create mode 100644 frontend/mining-app/lib/domain/entities/trading_account.dart delete mode 100644 frontend/mining-app/lib/domain/usecases/trading/buy_shares.dart delete mode 100644 frontend/mining-app/lib/domain/usecases/trading/sell_shares.dart create mode 100644 frontend/mining-app/lib/presentation/providers/asset_providers.dart create mode 100644 frontend/mining-app/lib/presentation/providers/profile_providers.dart diff --git a/frontend/mining-app/lib/core/di/injection.dart b/frontend/mining-app/lib/core/di/injection.dart index 7d94427b..94662c2e 100644 --- a/frontend/mining-app/lib/core/di/injection.dart +++ b/frontend/mining-app/lib/core/di/injection.dart @@ -15,9 +15,6 @@ import '../../domain/repositories/trading_repository.dart'; import '../../domain/repositories/contribution_repository.dart'; import '../../domain/usecases/mining/get_share_account.dart'; import '../../domain/usecases/mining/get_global_state.dart'; -import '../../domain/usecases/trading/get_current_price.dart'; -import '../../domain/usecases/trading/sell_shares.dart'; -import '../../domain/usecases/trading/buy_shares.dart'; import '../../domain/usecases/contribution/get_user_contribution.dart'; final getIt = GetIt.instance; @@ -72,11 +69,6 @@ Future configureDependencies() async { getIt.registerLazySingleton(() => GetShareAccount(getIt())); getIt.registerLazySingleton(() => GetGlobalState(getIt())); - // Use Cases - Trading - getIt.registerLazySingleton(() => GetCurrentPrice(getIt())); - getIt.registerLazySingleton(() => SellShares(getIt())); - getIt.registerLazySingleton(() => BuyShares(getIt())); - // Use Cases - Contribution getIt.registerLazySingleton(() => GetUserContribution(getIt())); } diff --git a/frontend/mining-app/lib/core/network/api_endpoints.dart b/frontend/mining-app/lib/core/network/api_endpoints.dart index 44a85d35..87b9be0d 100644 --- a/frontend/mining-app/lib/core/network/api_endpoints.dart +++ b/frontend/mining-app/lib/core/network/api_endpoints.dart @@ -23,16 +23,31 @@ class ApiEndpoints { '/api/v2/mining/accounts/$accountSequence/realtime'; // Trading Service 2.0 (Kong路由: /api/v2/trading) - static const String currentPrice = '/api/v2/trading/price'; - static const String klineData = '/api/v2/trading/kline'; + // Price endpoints + static const String currentPrice = '/api/v2/trading/price/current'; + static const String latestPrice = '/api/v2/trading/price/latest'; + static const String priceHistory = '/api/v2/trading/price/history'; + + // Trading account endpoints static String tradingAccount(String accountSequence) => - '/api/v2/trading/accounts/$accountSequence'; - static String createOrder(String accountSequence) => - '/api/v2/trading/accounts/$accountSequence/orders'; - static String orders(String accountSequence) => - '/api/v2/trading/accounts/$accountSequence/orders'; - static String transfer(String accountSequence) => - '/api/v2/trading/accounts/$accountSequence/transfer'; + '/api/v2/trading/trading/accounts/$accountSequence'; + static const String orderBook = '/api/v2/trading/trading/orderbook'; + static const String createOrder = '/api/v2/trading/trading/orders'; + static const String orders = '/api/v2/trading/trading/orders'; + static String cancelOrder(String orderNo) => + '/api/v2/trading/trading/orders/$orderNo/cancel'; + + // Asset endpoints + static const String myAsset = '/api/v2/trading/asset/my'; + static String accountAsset(String accountSequence) => + '/api/v2/trading/asset/account/$accountSequence'; + static const String estimateSell = '/api/v2/trading/asset/estimate-sell'; + static const String marketOverview = '/api/v2/trading/asset/market'; + + // Transfer endpoints + static const String transferIn = '/api/v2/trading/transfers/in'; + static const String transferOut = '/api/v2/trading/transfers/out'; + static const String transferHistory = '/api/v2/trading/transfers/history'; // Contribution Service 2.0 (Kong路由: /api/v2/contribution -> /api/v1/contributions) static String contribution(String accountSequence) => diff --git a/frontend/mining-app/lib/data/datasources/remote/auth_remote_datasource.dart b/frontend/mining-app/lib/data/datasources/remote/auth_remote_datasource.dart index c732f4e5..73d1275d 100644 --- a/frontend/mining-app/lib/data/datasources/remote/auth_remote_datasource.dart +++ b/frontend/mining-app/lib/data/datasources/remote/auth_remote_datasource.dart @@ -30,12 +30,20 @@ class UserInfo { final String phone; final String source; final String kycStatus; + final String? status; + final String? realName; + final DateTime? createdAt; + final DateTime? lastLoginAt; UserInfo({ required this.accountSequence, required this.phone, required this.source, required this.kycStatus, + this.status, + this.realName, + this.createdAt, + this.lastLoginAt, }); factory UserInfo.fromJson(Map json) { @@ -44,8 +52,18 @@ class UserInfo { phone: json['phone'] as String, source: json['source'] as String, kycStatus: json['kycStatus'] as String, + status: json['status'] as String?, + realName: json['realName'] as String?, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'].toString()) + : null, + lastLoginAt: json['lastLoginAt'] != null + ? DateTime.parse(json['lastLoginAt'].toString()) + : null, ); } + + bool get isKycVerified => kycStatus == 'VERIFIED'; } abstract class AuthRemoteDataSource { diff --git a/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart b/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart index 187879e6..f30f99c1 100644 --- a/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart +++ b/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart @@ -1,30 +1,52 @@ import '../../models/trade_order_model.dart'; -import '../../models/kline_model.dart'; +import '../../models/price_info_model.dart'; +import '../../models/trading_account_model.dart'; +import '../../models/market_overview_model.dart'; +import '../../models/asset_display_model.dart'; import '../../../core/network/api_client.dart'; import '../../../core/network/api_endpoints.dart'; import '../../../core/error/exceptions.dart'; abstract class TradingRemoteDataSource { - Future getCurrentPrice(); - Future> getKlineData(String period); - Future buyShares({ - required String accountSequence, - required String amount, + /// 获取当前价格信息 + Future getCurrentPrice(); + + /// 获取市场概览 + Future getMarketOverview(); + + /// 获取交易账户信息 + Future getTradingAccount(String accountSequence); + + /// 创建订单 + Future> createOrder({ + required String type, + required String price, + required String quantity, }); - Future sellShares({ - required String accountSequence, - required String amount, - }); - Future> getOrders( - String accountSequence, { + + /// 取消订单 + Future cancelOrder(String orderNo); + + /// 获取用户订单列表 + Future getOrders({ int page = 1, - int limit = 20, - }); - Future transfer({ - required String accountSequence, - required String amount, - required String direction, + int pageSize = 50, }); + + /// 预估卖出收益 + Future> estimateSell(String quantity); + + /// 划入积分股 (从挖矿账户到交易账户) + Future> transferIn(String amount); + + /// 划出积分股 (从交易账户到挖矿账户) + Future> transferOut(String amount); + + /// 获取我的资产显示信息 + Future getMyAsset({String? dailyAllocation}); + + /// 获取指定账户资产显示信息 + Future getAccountAsset(String accountSequence, {String? dailyAllocation}); } class TradingRemoteDataSourceImpl implements TradingRemoteDataSource { @@ -33,90 +55,149 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource { TradingRemoteDataSourceImpl({required this.client}); @override - Future getCurrentPrice() async { + Future getCurrentPrice() async { try { final response = await client.get(ApiEndpoints.currentPrice); - return response.data['price']?.toString() ?? '0'; + return PriceInfoModel.fromJson(response.data); } catch (e) { throw ServerException(e.toString()); } } @override - Future> getKlineData(String period) async { + Future getMarketOverview() async { try { - final response = await client.get( - ApiEndpoints.klineData, - queryParameters: {'period': period}, - ); - final items = response.data as List? ?? []; - return items.map((json) => KlineModel.fromJson(json)).toList(); + final response = await client.get(ApiEndpoints.marketOverview); + return MarketOverviewModel.fromJson(response.data); } catch (e) { throw ServerException(e.toString()); } } @override - Future buyShares({ - required String accountSequence, - required String amount, + Future getTradingAccount(String accountSequence) async { + try { + final response = await client.get(ApiEndpoints.tradingAccount(accountSequence)); + return TradingAccountModel.fromJson(response.data); + } catch (e) { + throw ServerException(e.toString()); + } + } + + @override + Future> createOrder({ + required String type, + required String price, + required String quantity, }) async { try { final response = await client.post( - ApiEndpoints.createOrder(accountSequence), - data: {'orderType': 'BUY', 'quantity': amount}, + ApiEndpoints.createOrder, + data: { + 'type': type, + 'price': price, + 'quantity': quantity, + }, ); - return TradeOrderModel.fromJson(response.data); + return response.data; } catch (e) { throw ServerException(e.toString()); } } @override - Future sellShares({ - required String accountSequence, - required String amount, - }) async { + Future cancelOrder(String orderNo) async { try { - final response = await client.post( - ApiEndpoints.createOrder(accountSequence), - data: {'orderType': 'SELL', 'quantity': amount}, - ); - return TradeOrderModel.fromJson(response.data); + await client.post(ApiEndpoints.cancelOrder(orderNo)); } catch (e) { throw ServerException(e.toString()); } } @override - Future> getOrders( - String accountSequence, { + Future getOrders({ int page = 1, - int limit = 20, + int pageSize = 50, }) async { try { final response = await client.get( - ApiEndpoints.orders(accountSequence), - queryParameters: {'page': page, 'pageSize': limit}, + ApiEndpoints.orders, + queryParameters: {'page': page, 'pageSize': pageSize}, ); - final items = response.data['items'] as List? ?? []; - return items.map((json) => TradeOrderModel.fromJson(json)).toList(); + return OrdersPageModel.fromJson(response.data); } catch (e) { throw ServerException(e.toString()); } } @override - Future transfer({ - required String accountSequence, - required String amount, - required String direction, - }) async { + Future> estimateSell(String quantity) async { try { - await client.post( - ApiEndpoints.transfer(accountSequence), - data: {'amount': amount, 'direction': direction}, + final response = await client.get( + ApiEndpoints.estimateSell, + queryParameters: {'quantity': quantity}, ); + return response.data; + } catch (e) { + throw ServerException(e.toString()); + } + } + + @override + Future> transferIn(String amount) async { + try { + final response = await client.post( + ApiEndpoints.transferIn, + data: {'amount': amount}, + ); + return response.data; + } catch (e) { + throw ServerException(e.toString()); + } + } + + @override + Future> transferOut(String amount) async { + try { + final response = await client.post( + ApiEndpoints.transferOut, + data: {'amount': amount}, + ); + return response.data; + } catch (e) { + throw ServerException(e.toString()); + } + } + + @override + Future getMyAsset({String? dailyAllocation}) async { + try { + final queryParams = {}; + if (dailyAllocation != null) { + queryParams['dailyAllocation'] = dailyAllocation; + } + final response = await client.get( + ApiEndpoints.myAsset, + queryParameters: queryParams.isNotEmpty ? queryParams : null, + ); + return AssetDisplayModel.fromJson(response.data); + } catch (e) { + throw ServerException(e.toString()); + } + } + + @override + Future getAccountAsset(String accountSequence, {String? dailyAllocation}) async { + try { + final queryParams = {}; + if (dailyAllocation != null) { + queryParams['dailyAllocation'] = dailyAllocation; + } + final response = await client.get( + ApiEndpoints.accountAsset(accountSequence), + queryParameters: queryParams.isNotEmpty ? queryParams : null, + ); + return AssetDisplayModel.fromJson(response.data); } catch (e) { throw ServerException(e.toString()); } diff --git a/frontend/mining-app/lib/data/models/asset_display_model.dart b/frontend/mining-app/lib/data/models/asset_display_model.dart new file mode 100644 index 00000000..578f7281 --- /dev/null +++ b/frontend/mining-app/lib/data/models/asset_display_model.dart @@ -0,0 +1,55 @@ +import '../../domain/entities/asset_display.dart'; + +class AssetDisplayModel extends AssetDisplay { + const AssetDisplayModel({ + required super.shareBalance, + required super.cashBalance, + required super.frozenShares, + required super.frozenCash, + required super.availableShares, + required super.availableCash, + required super.currentPrice, + required super.burnMultiplier, + required super.effectiveShares, + required super.displayAssetValue, + required super.assetGrowthPerSecond, + required super.totalBought, + required super.totalSold, + }); + + factory AssetDisplayModel.fromJson(Map json) { + return AssetDisplayModel( + shareBalance: json['shareBalance']?.toString() ?? '0', + cashBalance: json['cashBalance']?.toString() ?? '0', + frozenShares: json['frozenShares']?.toString() ?? '0', + frozenCash: json['frozenCash']?.toString() ?? '0', + availableShares: json['availableShares']?.toString() ?? '0', + availableCash: json['availableCash']?.toString() ?? '0', + currentPrice: json['currentPrice']?.toString() ?? '0', + burnMultiplier: json['burnMultiplier']?.toString() ?? '0', + effectiveShares: json['effectiveShares']?.toString() ?? '0', + displayAssetValue: json['displayAssetValue']?.toString() ?? '0', + assetGrowthPerSecond: json['assetGrowthPerSecond']?.toString() ?? '0', + totalBought: json['totalBought']?.toString() ?? '0', + totalSold: json['totalSold']?.toString() ?? '0', + ); + } + + Map toJson() { + return { + 'shareBalance': shareBalance, + 'cashBalance': cashBalance, + 'frozenShares': frozenShares, + 'frozenCash': frozenCash, + 'availableShares': availableShares, + 'availableCash': availableCash, + 'currentPrice': currentPrice, + 'burnMultiplier': burnMultiplier, + 'effectiveShares': effectiveShares, + 'displayAssetValue': displayAssetValue, + 'assetGrowthPerSecond': assetGrowthPerSecond, + 'totalBought': totalBought, + 'totalSold': totalSold, + }; + } +} diff --git a/frontend/mining-app/lib/data/models/contribution_model.dart b/frontend/mining-app/lib/data/models/contribution_model.dart index 3eb804c0..6ea2cb93 100644 --- a/frontend/mining-app/lib/data/models/contribution_model.dart +++ b/frontend/mining-app/lib/data/models/contribution_model.dart @@ -2,32 +2,51 @@ import '../../domain/entities/contribution.dart'; class ContributionModel extends Contribution { const ContributionModel({ + required super.status, + required super.message, required super.accountSequence, required super.personalContribution, - required super.systemContribution, required super.teamLevelContribution, required super.teamBonusContribution, required super.totalContribution, - required super.effectiveContribution, required super.hasAdopted, required super.directReferralAdoptedCount, required super.unlockedLevelDepth, required super.unlockedBonusTiers, + required super.isCalculated, + super.lastCalculatedAt, }); factory ContributionModel.fromJson(Map json) { return ContributionModel( + status: _parseStatus(json['status']), + message: json['message']?.toString() ?? '', accountSequence: json['accountSequence']?.toString() ?? '', personalContribution: json['personalContribution']?.toString() ?? '0', - systemContribution: json['systemContribution']?.toString() ?? '0', teamLevelContribution: json['teamLevelContribution']?.toString() ?? '0', teamBonusContribution: json['teamBonusContribution']?.toString() ?? '0', totalContribution: json['totalContribution']?.toString() ?? '0', - effectiveContribution: json['effectiveContribution']?.toString() ?? '0', hasAdopted: json['hasAdopted'] == true, directReferralAdoptedCount: json['directReferralAdoptedCount'] ?? 0, unlockedLevelDepth: json['unlockedLevelDepth'] ?? 0, unlockedBonusTiers: json['unlockedBonusTiers'] ?? 0, + isCalculated: json['isCalculated'] == true, + lastCalculatedAt: json['lastCalculatedAt'] != null + ? DateTime.tryParse(json['lastCalculatedAt'].toString()) + : null, ); } + + static ContributionAccountStatus _parseStatus(String? status) { + switch (status) { + case 'ACTIVE': + return ContributionAccountStatus.active; + case 'INACTIVE': + return ContributionAccountStatus.inactive; + case 'USER_NOT_FOUND': + return ContributionAccountStatus.userNotFound; + default: + return ContributionAccountStatus.inactive; + } + } } diff --git a/frontend/mining-app/lib/data/models/market_overview_model.dart b/frontend/mining-app/lib/data/models/market_overview_model.dart new file mode 100644 index 00000000..436ae179 --- /dev/null +++ b/frontend/mining-app/lib/data/models/market_overview_model.dart @@ -0,0 +1,29 @@ +import '../../domain/entities/market_overview.dart'; + +class MarketOverviewModel extends MarketOverview { + const MarketOverviewModel({ + required super.price, + required super.greenPoints, + required super.blackHoleAmount, + required super.circulationPool, + required super.effectiveDenominator, + required super.burnMultiplier, + required super.totalShares, + required super.burnTarget, + required super.burnProgress, + }); + + factory MarketOverviewModel.fromJson(Map json) { + return MarketOverviewModel( + price: json['price']?.toString() ?? '0', + greenPoints: json['greenPoints']?.toString() ?? '0', + blackHoleAmount: json['blackHoleAmount']?.toString() ?? '0', + circulationPool: json['circulationPool']?.toString() ?? '0', + effectiveDenominator: json['effectiveDenominator']?.toString() ?? '0', + burnMultiplier: json['burnMultiplier']?.toString() ?? '0', + totalShares: json['totalShares']?.toString() ?? '0', + burnTarget: json['burnTarget']?.toString() ?? '0', + burnProgress: json['burnProgress']?.toString() ?? '0', + ); + } +} diff --git a/frontend/mining-app/lib/data/models/price_info_model.dart b/frontend/mining-app/lib/data/models/price_info_model.dart new file mode 100644 index 00000000..15ef550b --- /dev/null +++ b/frontend/mining-app/lib/data/models/price_info_model.dart @@ -0,0 +1,29 @@ +import '../../domain/entities/price_info.dart'; + +class PriceInfoModel extends PriceInfo { + const PriceInfoModel({ + required super.price, + required super.greenPoints, + required super.blackHoleAmount, + required super.circulationPool, + required super.effectiveDenominator, + required super.burnMultiplier, + required super.minuteBurnRate, + required super.snapshotTime, + }); + + factory PriceInfoModel.fromJson(Map json) { + return PriceInfoModel( + price: json['price']?.toString() ?? '0', + greenPoints: json['greenPoints']?.toString() ?? '0', + blackHoleAmount: json['blackHoleAmount']?.toString() ?? '0', + circulationPool: json['circulationPool']?.toString() ?? '0', + effectiveDenominator: json['effectiveDenominator']?.toString() ?? '0', + burnMultiplier: json['burnMultiplier']?.toString() ?? '0', + minuteBurnRate: json['minuteBurnRate']?.toString() ?? '0', + snapshotTime: json['snapshotTime'] != null + ? DateTime.parse(json['snapshotTime'].toString()) + : DateTime.now(), + ); + } +} diff --git a/frontend/mining-app/lib/data/models/trade_order_model.dart b/frontend/mining-app/lib/data/models/trade_order_model.dart index 484a38db..21ec87b9 100644 --- a/frontend/mining-app/lib/data/models/trade_order_model.dart +++ b/frontend/mining-app/lib/data/models/trade_order_model.dart @@ -3,36 +3,55 @@ import '../../domain/entities/trade_order.dart'; class TradeOrderModel extends TradeOrder { const TradeOrderModel({ required super.id, - required super.accountSequence, + required super.orderNo, required super.orderType, required super.status, required super.price, required super.quantity, required super.filledQuantity, + required super.remainingQuantity, + required super.averagePrice, required super.totalAmount, required super.createdAt, - super.updatedAt, + super.completedAt, + super.cancelledAt, }); factory TradeOrderModel.fromJson(Map json) { return TradeOrderModel( - id: json['id'] ?? '', - accountSequence: json['accountSequence']?.toString() ?? '', - orderType: json['orderType'] == 'BUY' ? OrderType.buy : OrderType.sell, + id: json['id']?.toString() ?? '', + orderNo: json['orderNo']?.toString() ?? '', + orderType: _parseOrderType(json['type']), status: _parseStatus(json['status']), price: json['price']?.toString() ?? '0', quantity: json['quantity']?.toString() ?? '0', filledQuantity: json['filledQuantity']?.toString() ?? '0', + remainingQuantity: json['remainingQuantity']?.toString() ?? '0', + averagePrice: json['averagePrice']?.toString() ?? '0', totalAmount: json['totalAmount']?.toString() ?? '0', createdAt: json['createdAt'] != null - ? DateTime.parse(json['createdAt']) + ? DateTime.parse(json['createdAt'].toString()) : DateTime.now(), - updatedAt: json['updatedAt'] != null - ? DateTime.parse(json['updatedAt']) + completedAt: json['completedAt'] != null + ? DateTime.parse(json['completedAt'].toString()) + : null, + cancelledAt: json['cancelledAt'] != null + ? DateTime.parse(json['cancelledAt'].toString()) : null, ); } + static OrderType _parseOrderType(String? type) { + switch (type) { + case 'BUY': + return OrderType.buy; + case 'SELL': + return OrderType.sell; + default: + return OrderType.buy; + } + } + static OrderStatus _parseStatus(String? status) { switch (status) { case 'PENDING': @@ -48,3 +67,22 @@ class TradeOrderModel extends TradeOrder { } } } + +/// 订单列表响应 +class OrdersPageModel { + final List data; + final int total; + + const OrdersPageModel({ + required this.data, + required this.total, + }); + + factory OrdersPageModel.fromJson(Map json) { + final dataList = (json['data'] as List?) ?? []; + return OrdersPageModel( + data: dataList.map((e) => TradeOrderModel.fromJson(e)).toList(), + total: json['total'] ?? 0, + ); + } +} diff --git a/frontend/mining-app/lib/data/models/trading_account_model.dart b/frontend/mining-app/lib/data/models/trading_account_model.dart new file mode 100644 index 00000000..e059feaa --- /dev/null +++ b/frontend/mining-app/lib/data/models/trading_account_model.dart @@ -0,0 +1,29 @@ +import '../../domain/entities/trading_account.dart'; + +class TradingAccountModel extends TradingAccount { + const TradingAccountModel({ + required super.accountSequence, + required super.shareBalance, + required super.cashBalance, + required super.availableShares, + required super.availableCash, + required super.frozenShares, + required super.frozenCash, + required super.totalBought, + required super.totalSold, + }); + + factory TradingAccountModel.fromJson(Map json) { + return TradingAccountModel( + accountSequence: json['accountSequence']?.toString() ?? '', + shareBalance: json['shareBalance']?.toString() ?? '0', + cashBalance: json['cashBalance']?.toString() ?? '0', + availableShares: json['availableShares']?.toString() ?? '0', + availableCash: json['availableCash']?.toString() ?? '0', + frozenShares: json['frozenShares']?.toString() ?? '0', + frozenCash: json['frozenCash']?.toString() ?? '0', + totalBought: json['totalBought']?.toString() ?? '0', + totalSold: json['totalSold']?.toString() ?? '0', + ); + } +} diff --git a/frontend/mining-app/lib/data/repositories/trading_repository_impl.dart b/frontend/mining-app/lib/data/repositories/trading_repository_impl.dart index 360a9a16..b9785625 100644 --- a/frontend/mining-app/lib/data/repositories/trading_repository_impl.dart +++ b/frontend/mining-app/lib/data/repositories/trading_repository_impl.dart @@ -1,10 +1,13 @@ import 'package:dartz/dartz.dart'; -import '../../domain/entities/trade_order.dart'; -import '../../domain/entities/kline.dart'; +import '../../domain/entities/price_info.dart'; +import '../../domain/entities/market_overview.dart'; +import '../../domain/entities/trading_account.dart'; +import '../../domain/entities/asset_display.dart'; import '../../domain/repositories/trading_repository.dart'; import '../../core/error/exceptions.dart'; import '../../core/error/failures.dart'; import '../datasources/remote/trading_remote_datasource.dart'; +import '../models/trade_order_model.dart'; class TradingRepositoryImpl implements TradingRepository { final TradingRemoteDataSource remoteDataSource; @@ -12,7 +15,7 @@ class TradingRepositoryImpl implements TradingRepository { TradingRepositoryImpl({required this.remoteDataSource}); @override - Future> getCurrentPrice() async { + Future> getCurrentPrice() async { try { final result = await remoteDataSource.getCurrentPrice(); return Right(result); @@ -24,9 +27,9 @@ class TradingRepositoryImpl implements TradingRepository { } @override - Future>> getKlineData(String period) async { + Future> getMarketOverview() async { try { - final result = await remoteDataSource.getKlineData(period); + final result = await remoteDataSource.getMarketOverview(); return Right(result); } on ServerException catch (e) { return Left(ServerFailure(e.message)); @@ -36,14 +39,28 @@ class TradingRepositoryImpl implements TradingRepository { } @override - Future> buyShares({ - required String accountSequence, - required String amount, + Future> getTradingAccount(String accountSequence) async { + try { + final result = await remoteDataSource.getTradingAccount(accountSequence); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException { + return Left(const NetworkFailure()); + } + } + + @override + Future>> createOrder({ + required String type, + required String price, + required String quantity, }) async { try { - final result = await remoteDataSource.buyShares( - accountSequence: accountSequence, - amount: amount, + final result = await remoteDataSource.createOrder( + type: type, + price: price, + quantity: quantity, ); return Right(result); } on ServerException catch (e) { @@ -54,55 +71,9 @@ class TradingRepositoryImpl implements TradingRepository { } @override - Future> sellShares({ - required String accountSequence, - required String amount, - }) async { + Future> cancelOrder(String orderNo) async { try { - final result = await remoteDataSource.sellShares( - accountSequence: accountSequence, - amount: amount, - ); - return Right(result); - } on ServerException catch (e) { - return Left(ServerFailure(e.message)); - } on NetworkException { - return Left(const NetworkFailure()); - } - } - - @override - Future>> getOrders( - String accountSequence, { - int page = 1, - int limit = 20, - }) async { - try { - final result = await remoteDataSource.getOrders( - accountSequence, - page: page, - limit: limit, - ); - return Right(result); - } on ServerException catch (e) { - return Left(ServerFailure(e.message)); - } on NetworkException { - return Left(const NetworkFailure()); - } - } - - @override - Future> transfer({ - required String accountSequence, - required String amount, - required String direction, - }) async { - try { - await remoteDataSource.transfer( - accountSequence: accountSequence, - amount: amount, - direction: direction, - ); + await remoteDataSource.cancelOrder(orderNo); return const Right(null); } on ServerException catch (e) { return Left(ServerFailure(e.message)); @@ -110,4 +81,82 @@ class TradingRepositoryImpl implements TradingRepository { return Left(const NetworkFailure()); } } + + @override + Future> getOrders({ + int page = 1, + int pageSize = 50, + }) async { + try { + final result = await remoteDataSource.getOrders( + page: page, + pageSize: pageSize, + ); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException { + return Left(const NetworkFailure()); + } + } + + @override + Future>> estimateSell(String quantity) async { + try { + final result = await remoteDataSource.estimateSell(quantity); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException { + return Left(const NetworkFailure()); + } + } + + @override + Future>> transferIn(String amount) async { + try { + final result = await remoteDataSource.transferIn(amount); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException { + return Left(const NetworkFailure()); + } + } + + @override + Future>> transferOut(String amount) async { + try { + final result = await remoteDataSource.transferOut(amount); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException { + return Left(const NetworkFailure()); + } + } + + @override + Future> getMyAsset({String? dailyAllocation}) async { + try { + final result = await remoteDataSource.getMyAsset(dailyAllocation: dailyAllocation); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException { + return Left(const NetworkFailure()); + } + } + + @override + Future> getAccountAsset(String accountSequence, {String? dailyAllocation}) async { + try { + final result = await remoteDataSource.getAccountAsset(accountSequence, dailyAllocation: dailyAllocation); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException { + return Left(const NetworkFailure()); + } + } } diff --git a/frontend/mining-app/lib/domain/entities/asset_display.dart b/frontend/mining-app/lib/domain/entities/asset_display.dart new file mode 100644 index 00000000..20f0856d --- /dev/null +++ b/frontend/mining-app/lib/domain/entities/asset_display.dart @@ -0,0 +1,91 @@ +import 'package:equatable/equatable.dart'; + +/// 用户资产显示信息 +/// 来自后端 trading-service 的 /asset/my 或 /asset/account/:accountSequence 接口 +class AssetDisplay extends Equatable { + /// 账户积分股余额 + final String shareBalance; + + /// 账户现金余额 + final String cashBalance; + + /// 冻结积分股 + final String frozenShares; + + /// 冻结现金 + final String frozenCash; + + /// 可用积分股 + final String availableShares; + + /// 可用现金 + final String availableCash; + + /// 当前价格 + final String currentPrice; + + /// 销毁倍数 + final String burnMultiplier; + + /// 有效积分股(含销毁加成) + final String effectiveShares; + + /// 资产显示值 = (账户积分股 + 账户积分股 × 倍数) × 积分股价 + final String displayAssetValue; + + /// 每秒增长量 + final String assetGrowthPerSecond; + + /// 累计买入 + final String totalBought; + + /// 累计卖出 + final String totalSold; + + const AssetDisplay({ + required this.shareBalance, + required this.cashBalance, + required this.frozenShares, + required this.frozenCash, + required this.availableShares, + required this.availableCash, + required this.currentPrice, + required this.burnMultiplier, + required this.effectiveShares, + required this.displayAssetValue, + required this.assetGrowthPerSecond, + required this.totalBought, + required this.totalSold, + }); + + /// 总积分股余额 = 可用 + 冻结 + String get totalShareBalance { + final available = double.tryParse(availableShares) ?? 0; + final frozen = double.tryParse(frozenShares) ?? 0; + return (available + frozen).toString(); + } + + /// 总现金余额 = 可用 + 冻结 + String get totalCashBalance { + final available = double.tryParse(availableCash) ?? 0; + final frozen = double.tryParse(frozenCash) ?? 0; + return (available + frozen).toString(); + } + + @override + List get props => [ + shareBalance, + cashBalance, + frozenShares, + frozenCash, + availableShares, + availableCash, + currentPrice, + burnMultiplier, + effectiveShares, + displayAssetValue, + assetGrowthPerSecond, + totalBought, + totalSold, + ]; +} diff --git a/frontend/mining-app/lib/domain/entities/contribution.dart b/frontend/mining-app/lib/domain/entities/contribution.dart index 165c5f12..c5ad9c8f 100644 --- a/frontend/mining-app/lib/domain/entities/contribution.dart +++ b/frontend/mining-app/lib/domain/entities/contribution.dart @@ -1,41 +1,83 @@ import 'package:equatable/equatable.dart'; +/// 账户状态枚举 +enum ContributionAccountStatus { + /// 正常 - 账户存在且有算力数据 + active, + /// 未激活 - 用户存在但暂无任何算力 + inactive, + /// 用户不存在 + userNotFound, +} + class Contribution extends Equatable { + /// 账户状态 + final ContributionAccountStatus status; + /// 状态说明 + final String message; + /// 账户序号 final String accountSequence; + /// 个人算力 final String personalContribution; - final String systemContribution; + /// 团队层级算力 final String teamLevelContribution; + /// 团队奖励算力 final String teamBonusContribution; + /// 总算力 final String totalContribution; - final String effectiveContribution; + /// 是否已认种 final bool hasAdopted; + /// 直推认种用户数 final int directReferralAdoptedCount; + /// 已解锁层级深度 final int unlockedLevelDepth; + /// 已解锁奖励档位数 final int unlockedBonusTiers; + /// 是否已完成计算 + final bool isCalculated; + /// 最后计算时间 + final DateTime? lastCalculatedAt; const Contribution({ + required this.status, + required this.message, required this.accountSequence, required this.personalContribution, - required this.systemContribution, required this.teamLevelContribution, required this.teamBonusContribution, required this.totalContribution, - required this.effectiveContribution, required this.hasAdopted, required this.directReferralAdoptedCount, required this.unlockedLevelDepth, required this.unlockedBonusTiers, + required this.isCalculated, + this.lastCalculatedAt, }); + /// 团队总贡献值 (层级 + 奖励) + String get teamContribution { + final teamLevel = double.tryParse(teamLevelContribution) ?? 0; + final teamBonus = double.tryParse(teamBonusContribution) ?? 0; + return (teamLevel + teamBonus).toString(); + } + + /// 是否处于活跃状态 + bool get isActive => status == ContributionAccountStatus.active; + @override List get props => [ + status, + message, accountSequence, personalContribution, - systemContribution, teamLevelContribution, teamBonusContribution, totalContribution, - effectiveContribution, hasAdopted, + directReferralAdoptedCount, + unlockedLevelDepth, + unlockedBonusTiers, + isCalculated, + lastCalculatedAt, ]; } diff --git a/frontend/mining-app/lib/domain/entities/market_overview.dart b/frontend/mining-app/lib/domain/entities/market_overview.dart new file mode 100644 index 00000000..82c8457b --- /dev/null +++ b/frontend/mining-app/lib/domain/entities/market_overview.dart @@ -0,0 +1,48 @@ +import 'package:equatable/equatable.dart'; + +/// 市场概览信息 +class MarketOverview extends Equatable { + /// 当前价格 + final String price; + /// 绿积分池 + final String greenPoints; + /// 黑洞销毁量 + final String blackHoleAmount; + /// 流通池 + final String circulationPool; + /// 有效分母 + final String effectiveDenominator; + /// 销毁倍数 + final String burnMultiplier; + /// 总积分股 + final String totalShares; + /// 销毁目标 + final String burnTarget; + /// 销毁进度 + final String burnProgress; + + const MarketOverview({ + required this.price, + required this.greenPoints, + required this.blackHoleAmount, + required this.circulationPool, + required this.effectiveDenominator, + required this.burnMultiplier, + required this.totalShares, + required this.burnTarget, + required this.burnProgress, + }); + + @override + List get props => [ + price, + greenPoints, + blackHoleAmount, + circulationPool, + effectiveDenominator, + burnMultiplier, + totalShares, + burnTarget, + burnProgress, + ]; +} diff --git a/frontend/mining-app/lib/domain/entities/price_info.dart b/frontend/mining-app/lib/domain/entities/price_info.dart new file mode 100644 index 00000000..55dda6bf --- /dev/null +++ b/frontend/mining-app/lib/domain/entities/price_info.dart @@ -0,0 +1,44 @@ +import 'package:equatable/equatable.dart'; + +/// 价格信息 +class PriceInfo extends Equatable { + /// 当前价格 + final String price; + /// 绿积分池 + final String greenPoints; + /// 黑洞销毁量 + final String blackHoleAmount; + /// 流通池 + final String circulationPool; + /// 有效分母 + final String effectiveDenominator; + /// 销毁倍数 + final String burnMultiplier; + /// 每分钟销毁率 + final String minuteBurnRate; + /// 快照时间 + final DateTime snapshotTime; + + const PriceInfo({ + required this.price, + required this.greenPoints, + required this.blackHoleAmount, + required this.circulationPool, + required this.effectiveDenominator, + required this.burnMultiplier, + required this.minuteBurnRate, + required this.snapshotTime, + }); + + @override + List get props => [ + price, + greenPoints, + blackHoleAmount, + circulationPool, + effectiveDenominator, + burnMultiplier, + minuteBurnRate, + snapshotTime, + ]; +} diff --git a/frontend/mining-app/lib/domain/entities/trade_order.dart b/frontend/mining-app/lib/domain/entities/trade_order.dart index 0db12a28..b4c28240 100644 --- a/frontend/mining-app/lib/domain/entities/trade_order.dart +++ b/frontend/mining-app/lib/domain/entities/trade_order.dart @@ -4,41 +4,64 @@ enum OrderType { buy, sell } enum OrderStatus { pending, partial, filled, cancelled } class TradeOrder extends Equatable { + /// 订单ID final String id; - final String accountSequence; + /// 订单号 + final String orderNo; + /// 订单类型 final OrderType orderType; + /// 订单状态 final OrderStatus status; + /// 价格 final String price; + /// 数量 final String quantity; + /// 已成交数量 final String filledQuantity; + /// 剩余数量 + final String remainingQuantity; + /// 平均成交价格 + final String averagePrice; + /// 总金额 final String totalAmount; + /// 创建时间 final DateTime createdAt; - final DateTime? updatedAt; + /// 完成时间 + final DateTime? completedAt; + /// 取消时间 + final DateTime? cancelledAt; const TradeOrder({ required this.id, - required this.accountSequence, + required this.orderNo, required this.orderType, required this.status, required this.price, required this.quantity, required this.filledQuantity, + required this.remainingQuantity, + required this.averagePrice, required this.totalAmount, required this.createdAt, - this.updatedAt, + this.completedAt, + this.cancelledAt, }); bool get isBuy => orderType == OrderType.buy; bool get isSell => orderType == OrderType.sell; bool get isPending => status == OrderStatus.pending; + bool get isPartial => status == OrderStatus.partial; bool get isFilled => status == OrderStatus.filled; + bool get isCancelled => status == OrderStatus.cancelled; - String get remainingQuantity { + /// 成交进度百分比 + double get fillProgress { final qty = double.tryParse(quantity) ?? 0; final filled = double.tryParse(filledQuantity) ?? 0; - return (qty - filled).toString(); + if (qty == 0) return 0; + return filled / qty; } @override - List get props => [id, accountSequence, orderType, status, price, quantity]; + List get props => [id, orderNo, orderType, status, price, quantity]; } diff --git a/frontend/mining-app/lib/domain/entities/trading_account.dart b/frontend/mining-app/lib/domain/entities/trading_account.dart new file mode 100644 index 00000000..f1977a86 --- /dev/null +++ b/frontend/mining-app/lib/domain/entities/trading_account.dart @@ -0,0 +1,48 @@ +import 'package:equatable/equatable.dart'; + +/// 交易账户信息 +class TradingAccount extends Equatable { + /// 账户序号 + final String accountSequence; + /// 积分股余额 + final String shareBalance; + /// 现金余额(绿积分) + final String cashBalance; + /// 可用积分股 + final String availableShares; + /// 可用现金 + final String availableCash; + /// 冻结积分股 + final String frozenShares; + /// 冻结现金 + final String frozenCash; + /// 累计买入 + final String totalBought; + /// 累计卖出 + final String totalSold; + + const TradingAccount({ + required this.accountSequence, + required this.shareBalance, + required this.cashBalance, + required this.availableShares, + required this.availableCash, + required this.frozenShares, + required this.frozenCash, + required this.totalBought, + required this.totalSold, + }); + + @override + List get props => [ + accountSequence, + shareBalance, + cashBalance, + availableShares, + availableCash, + frozenShares, + frozenCash, + totalBought, + totalSold, + ]; +} diff --git a/frontend/mining-app/lib/domain/repositories/trading_repository.dart b/frontend/mining-app/lib/domain/repositories/trading_repository.dart index d9118baa..9b70bbb1 100644 --- a/frontend/mining-app/lib/domain/repositories/trading_repository.dart +++ b/frontend/mining-app/lib/domain/repositories/trading_repository.dart @@ -1,32 +1,50 @@ import 'package:dartz/dartz.dart'; import '../../core/error/failures.dart'; +import '../entities/price_info.dart'; +import '../entities/market_overview.dart'; +import '../entities/trading_account.dart'; import '../entities/trade_order.dart'; -import '../entities/kline.dart'; +import '../entities/asset_display.dart'; +import '../../data/models/trade_order_model.dart'; abstract class TradingRepository { - Future> getCurrentPrice(); + /// 获取当前价格信息 + Future> getCurrentPrice(); - Future>> getKlineData(String period); + /// 获取市场概览 + Future> getMarketOverview(); - Future> buyShares({ - required String accountSequence, - required String amount, + /// 获取交易账户信息 + Future> getTradingAccount(String accountSequence); + + /// 创建订单 + Future>> createOrder({ + required String type, + required String price, + required String quantity, }); - Future> sellShares({ - required String accountSequence, - required String amount, - }); + /// 取消订单 + Future> cancelOrder(String orderNo); - Future>> getOrders( - String accountSequence, { + /// 获取用户订单列表 + Future> getOrders({ int page = 1, - int limit = 20, + int pageSize = 50, }); - Future> transfer({ - required String accountSequence, - required String amount, - required String direction, - }); + /// 预估卖出收益 + Future>> estimateSell(String quantity); + + /// 划入积分股 (从挖矿账户到交易账户) + Future>> transferIn(String amount); + + /// 划出积分股 (从交易账户到挖矿账户) + Future>> transferOut(String amount); + + /// 获取我的资产显示信息 + Future> getMyAsset({String? dailyAllocation}); + + /// 获取指定账户资产显示信息 + Future> getAccountAsset(String accountSequence, {String? dailyAllocation}); } diff --git a/frontend/mining-app/lib/domain/usecases/trading/buy_shares.dart b/frontend/mining-app/lib/domain/usecases/trading/buy_shares.dart deleted file mode 100644 index 66edeeaa..00000000 --- a/frontend/mining-app/lib/domain/usecases/trading/buy_shares.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:dartz/dartz.dart'; -import '../../../core/error/failures.dart'; -import '../../entities/trade_order.dart'; -import '../../repositories/trading_repository.dart'; - -class BuySharesParams { - final String accountSequence; - final String amount; - - BuySharesParams({required this.accountSequence, required this.amount}); -} - -class BuyShares { - final TradingRepository repository; - - BuyShares(this.repository); - - Future> call(BuySharesParams params) async { - return await repository.buyShares( - accountSequence: params.accountSequence, - amount: params.amount, - ); - } -} diff --git a/frontend/mining-app/lib/domain/usecases/trading/get_current_price.dart b/frontend/mining-app/lib/domain/usecases/trading/get_current_price.dart index f5345b35..98393283 100644 --- a/frontend/mining-app/lib/domain/usecases/trading/get_current_price.dart +++ b/frontend/mining-app/lib/domain/usecases/trading/get_current_price.dart @@ -1,5 +1,6 @@ import 'package:dartz/dartz.dart'; import '../../../core/error/failures.dart'; +import '../../entities/price_info.dart'; import '../../repositories/trading_repository.dart'; class GetCurrentPrice { @@ -7,7 +8,7 @@ class GetCurrentPrice { GetCurrentPrice(this.repository); - Future> call() async { + Future> call() async { return await repository.getCurrentPrice(); } } diff --git a/frontend/mining-app/lib/domain/usecases/trading/sell_shares.dart b/frontend/mining-app/lib/domain/usecases/trading/sell_shares.dart deleted file mode 100644 index e7e37040..00000000 --- a/frontend/mining-app/lib/domain/usecases/trading/sell_shares.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:dartz/dartz.dart'; -import '../../../core/error/failures.dart'; -import '../../entities/trade_order.dart'; -import '../../repositories/trading_repository.dart'; - -class SellSharesParams { - final String accountSequence; - final String amount; - - SellSharesParams({required this.accountSequence, required this.amount}); -} - -class SellShares { - final TradingRepository repository; - - SellShares(this.repository); - - Future> call(SellSharesParams params) async { - return await repository.sellShares( - accountSequence: params.accountSequence, - amount: params.amount, - ); - } -} diff --git a/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart b/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart index 77578e47..087b9fdc 100644 --- a/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart +++ b/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/utils/format_utils.dart'; +import '../../../domain/entities/asset_display.dart'; import '../../providers/user_providers.dart'; -import '../../providers/mining_providers.dart'; +import '../../providers/asset_providers.dart'; import '../../widgets/shimmer_loading.dart'; class AssetPage extends ConsumerWidget { @@ -23,12 +24,11 @@ class AssetPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userNotifierProvider); - final accountSequence = user.accountSequence ?? ''; - final accountAsync = ref.watch(shareAccountProvider(accountSequence)); + final assetAsync = ref.watch(myAssetProvider); // 提取数据和加载状态 - final isLoading = accountAsync.isLoading; - final account = accountAsync.valueOrNull; + final isLoading = assetAsync.isLoading; + final asset = assetAsync.valueOrNull; return Scaffold( backgroundColor: Colors.white, @@ -38,7 +38,7 @@ class AssetPage extends ConsumerWidget { builder: (context, constraints) { return RefreshIndicator( onRefresh: () async { - ref.invalidate(shareAccountProvider(accountSequence)); + ref.invalidate(myAssetProvider); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), @@ -55,19 +55,19 @@ class AssetPage extends ConsumerWidget { children: [ const SizedBox(height: 8), // 总资产卡片 - 始终显示,数字部分闪烁 - _buildTotalAssetCard(account, isLoading), + _buildTotalAssetCard(asset, isLoading), const SizedBox(height: 24), // 快捷操作按钮 _buildQuickActions(), const SizedBox(height: 24), // 资产列表 - 始终显示,数字部分闪烁 - _buildAssetList(account, isLoading), + _buildAssetList(asset, isLoading), const SizedBox(height: 24), - // 收益统计 - _buildEarningsCard(account, isLoading), + // 交易统计 + _buildEarningsCard(asset, isLoading), const SizedBox(height: 24), // 账户列表 - _buildAccountList(account, isLoading), + _buildAccountList(asset, isLoading), const SizedBox(height: 100), ], ), @@ -181,7 +181,12 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildTotalAssetCard(account, bool isLoading) { + Widget _buildTotalAssetCard(AssetDisplay? asset, bool isLoading) { + // 计算每秒增长 + final growthPerSecond = asset != null + ? AssetValueCalculator.calculateGrowthPerSecond(asset.assetGrowthPerSecond) + : 0.0; + return Container( decoration: BoxDecoration( color: Colors.white, @@ -254,7 +259,7 @@ class AssetPage extends ConsumerWidget { const SizedBox(height: 8), // 金额 - 闪烁占位符 AmountText( - amount: account != null ? formatAmount(account.tradingBalance ?? '0') : null, + amount: asset != null ? formatAmount(asset.displayAssetValue) : null, isLoading: isLoading, prefix: '¥ ', style: const TextStyle( @@ -265,18 +270,18 @@ class AssetPage extends ConsumerWidget { ), ), const SizedBox(height: 4), - // USDT估值 + // 有效积分股 DataText( - data: account != null ? '≈ 12,345.67 USDT' : null, + data: asset != null ? '≈ ${formatCompact(asset.effectiveShares)} 积分股 (含倍数)' : null, isLoading: isLoading, - placeholder: '≈ -- USDT', + placeholder: '≈ -- 积分股', style: const TextStyle( fontSize: 14, color: Color(0xFF9CA3AF), ), ), const SizedBox(height: 12), - // 今日收益 + // 每秒增长 Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( @@ -286,12 +291,14 @@ class AssetPage extends ConsumerWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.trending_up, size: 14, color: _green), + const Icon(Icons.bolt, size: 14, color: _green), const SizedBox(width: 6), DataText( - data: account != null ? '+¥ 156.78 今日' : null, + data: asset != null + ? '+${formatDecimal(growthPerSecond.toString(), 8)}/秒' + : null, isLoading: isLoading, - placeholder: '+¥ -- 今日', + placeholder: '+--/秒', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, @@ -346,7 +353,12 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildAssetList(account, bool isLoading) { + Widget _buildAssetList(AssetDisplay? asset, bool isLoading) { + // 计算倍数资产 + final shareBalance = double.tryParse(asset?.shareBalance ?? '0') ?? 0; + final multiplier = double.tryParse(asset?.burnMultiplier ?? '0') ?? 0; + final multipliedAsset = shareBalance * multiplier; + return Column( children: [ // 积分股 @@ -355,41 +367,49 @@ class AssetPage extends ConsumerWidget { iconColor: _orange, iconBgColor: _serenade, title: '积分股', - amount: account?.miningBalance, + amount: asset?.shareBalance, isLoading: isLoading, - valueInCny: '¥15,234.56', - tag: '含倍数资产: 246,913.56', - growthText: '每秒 +0.0015', + valueInCny: asset != null + ? '¥${formatAmount(_calculateValue(asset.shareBalance, asset.currentPrice))}' + : null, + tag: asset != null ? '含倍数资产: ${formatCompact(multipliedAsset.toString())}' : null, + growthText: asset != null ? '每秒 +${formatDecimal(asset.assetGrowthPerSecond, 8)}' : null, ), const SizedBox(height: 16), - // 绿积分 + // 绿积分(现金余额) _buildAssetItem( icon: Icons.eco, iconColor: _green, iconBgColor: _feta, title: '绿积分', - amount: account?.tradingBalance, + amount: asset?.cashBalance, isLoading: isLoading, - valueInCny: '¥10,986.54', + valueInCny: asset != null ? '¥${formatAmount(asset.cashBalance)}' : null, badge: '可提现', badgeColor: _jewel, badgeBgColor: _scandal, ), const SizedBox(height: 16), - // 待分配积分股 + // 冻结积分股 _buildAssetItem( - icon: Icons.hourglass_empty, + icon: Icons.lock_outline, iconColor: _orange, iconBgColor: _serenade, - title: '待分配积分股', - amount: '1,234.56', + title: '冻结积分股', + amount: asset?.frozenShares, isLoading: isLoading, - subtitle: '次日开始参与分配', + subtitle: '交易挂单中', ), ], ); } + String _calculateValue(String balance, String price) { + final b = double.tryParse(balance) ?? 0; + final p = double.tryParse(price) ?? 0; + return (b * p).toString(); + } + Widget _buildAssetItem({ required IconData icon, required Color iconColor, @@ -555,7 +575,7 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildEarningsCard(account, bool isLoading) { + Widget _buildEarningsCard(AssetDisplay? asset, bool isLoading) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -584,7 +604,7 @@ class AssetPage extends ConsumerWidget { ), const SizedBox(width: 8), const Text( - '收益统计', + '交易统计', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -597,19 +617,34 @@ class AssetPage extends ConsumerWidget { // 统计数据 Row( children: [ - _buildEarningsItem('累计收益', isLoading ? null : '12,345.67', _orange, isLoading), + _buildEarningsItem( + '累计买入', + asset != null ? formatCompact(asset.totalBought) : null, + _orange, + isLoading, + ), Container( width: 1, height: 40, color: _serenade, ), - _buildEarningsItem('今日收益', isLoading ? null : '+156.78', _green, isLoading), + _buildEarningsItem( + '累计卖出', + asset != null ? formatCompact(asset.totalSold) : null, + _green, + isLoading, + ), Container( width: 1, height: 40, color: _serenade, ), - _buildEarningsItem('昨日收益', isLoading ? null : '143.21', const Color(0xFF9CA3AF), isLoading), + _buildEarningsItem( + '销毁倍数', + asset != null ? '${formatDecimal(asset.burnMultiplier, 4)}x' : null, + const Color(0xFF9CA3AF), + isLoading, + ), ], ), ], @@ -646,31 +681,31 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildAccountList(account, bool isLoading) { + Widget _buildAccountList(AssetDisplay? asset, bool isLoading) { return Column( children: [ - // 交易账户 + // 交易账户(可用现金) _buildAccountItem( icon: Icons.account_balance_wallet, iconColor: _orange, - title: '交易账户', - balance: account?.tradingBalance, + title: '可用绿积分', + balance: asset?.availableCash, isLoading: isLoading, unit: '绿积分', - status: '正常', + status: '可交易', statusColor: _green, statusBgColor: _feta, ), const SizedBox(height: 16), - // 提现账户 + // 冻结现金 _buildAccountItem( - icon: Icons.savings, + icon: Icons.lock_outline, iconColor: _orange, - title: '提现账户', - balance: '1,234.56', + title: '冻结绿积分', + balance: asset?.frozenCash, isLoading: isLoading, unit: '绿积分', - status: '已绑定', + status: '挂单中', statusColor: const Color(0xFF9CA3AF), statusBgColor: Colors.white, statusBorder: true, @@ -734,7 +769,7 @@ class AssetPage extends ConsumerWidget { Row( children: [ DataText( - data: balance, + data: balance != null ? formatAmount(balance) : null, isLoading: isLoading, placeholder: '--', style: const TextStyle( @@ -778,5 +813,4 @@ class AssetPage extends ConsumerWidget { ), ); } - } diff --git a/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart b/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart index a1686f02..e2a31d53 100644 --- a/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart +++ b/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart @@ -159,7 +159,7 @@ class ContributionPage extends ConsumerWidget { } Widget _buildTotalContributionCard(Contribution? contribution, bool isLoading) { - final total = contribution?.effectiveContribution ?? '0'; + final total = contribution?.totalContribution ?? '0'; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -226,8 +226,8 @@ class ContributionPage extends ConsumerWidget { child: Row( children: [ _buildStatColumn('个人贡献值', contribution?.personalContribution, isLoading, false), - _buildStatColumn('团队贡献值', contribution?.teamLevelContribution, isLoading, true), - _buildStatColumn('省市公司', contribution?.systemContribution, isLoading, true), + _buildStatColumn('团队层级', contribution?.teamLevelContribution, isLoading, true), + _buildStatColumn('团队奖励', contribution?.teamBonusContribution, isLoading, true), ], ), ); @@ -261,10 +261,10 @@ class ContributionPage extends ConsumerWidget { Widget _buildTodayEstimateCard(Contribution? contribution, bool isLoading) { // 基于贡献值计算预估收益(暂时显示占位符,后续可接入实际计算API) - final effectiveContribution = double.tryParse(contribution?.effectiveContribution ?? '0') ?? 0; + final totalContribution = double.tryParse(contribution?.totalContribution ?? '0') ?? 0; // 简单估算:假设每日发放总量为 10000 积分股,按贡献值占比分配 // 这里先显示"--"表示暂无数据,后续可接入实际计算 - final hasContribution = effectiveContribution > 0; + final hasContribution = totalContribution > 0; return Container( padding: const EdgeInsets.all(20), diff --git a/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart b/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart index f3660a64..7f70d672 100644 --- a/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart +++ b/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/router/routes.dart'; import '../../providers/user_providers.dart'; +import '../../providers/profile_providers.dart'; +import '../../widgets/shimmer_loading.dart'; class ProfilePage extends ConsumerWidget { const ProfilePage({super.key}); @@ -20,72 +22,84 @@ class ProfilePage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userNotifierProvider); + final statsAsync = ref.watch(userStatsProvider); + final invitationCode = ref.watch(invitationCodeProvider); + + final isStatsLoading = statsAsync.isLoading; + final stats = statsAsync.valueOrNull; return Scaffold( backgroundColor: _bgGray, body: SafeArea( bottom: false, - child: SingleChildScrollView( - child: Column( - children: [ - // 用户头部信息 - _buildUserHeader(context, user), + child: RefreshIndicator( + onRefresh: () async { + ref.invalidate(userStatsProvider); + await ref.read(userNotifierProvider.notifier).fetchProfile(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + // 用户头部信息 + _buildUserHeader(context, user), - const SizedBox(height: 16), + const SizedBox(height: 16), - // 统计数据行 - _buildStatsRow(), + // 统计数据行 + _buildStatsRow(stats, isStatsLoading), - const SizedBox(height: 16), + const SizedBox(height: 16), - // 邀请码卡片 - _buildInvitationCard(context), + // 邀请码卡片 + _buildInvitationCard(context, invitationCode), - const SizedBox(height: 16), + const SizedBox(height: 16), - // 账户设置 - _buildAccountSettings(context), + // 账户设置 + _buildAccountSettings(context, user), - const SizedBox(height: 16), + const SizedBox(height: 16), - // 记录入口 - _buildRecordsSection(context), + // 记录入口 + _buildRecordsSection(context), - const SizedBox(height: 16), + const SizedBox(height: 16), - // 团队与收益 - _buildTeamEarningsSection(context), + // 团队与收益 + _buildTeamEarningsSection(context), - const SizedBox(height: 16), + const SizedBox(height: 16), - // 其他设置 - _buildOtherSettings(context), + // 其他设置 + _buildOtherSettings(context), - const SizedBox(height: 24), + const SizedBox(height: 24), - // 退出登录 - _buildLogoutButton(context, ref), + // 退出登录 + _buildLogoutButton(context, ref), - const SizedBox(height: 16), + const SizedBox(height: 16), - // 版本信息 - const Text( - 'Version 1.0.0', - style: TextStyle( - fontSize: 12, - color: _lightGray, + // 版本信息 + const Text( + 'Version 1.0.0', + style: TextStyle( + fontSize: 12, + color: _lightGray, + ), ), - ), - const SizedBox(height: 100), - ], + const SizedBox(height: 100), + ], + ), ), ), ), ); } - Widget _buildUserHeader(BuildContext context, dynamic user) { + Widget _buildUserHeader(BuildContext context, UserState user) { return Container( padding: const EdgeInsets.all(20), color: Colors.white, @@ -107,7 +121,9 @@ class ProfilePage extends ConsumerWidget { child: Text( user.nickname?.isNotEmpty == true ? user.nickname!.substring(0, 1).toUpperCase() - : 'U', + : (user.realName?.isNotEmpty == true + ? user.realName!.substring(0, 1).toUpperCase() + : 'U'), style: const TextStyle( fontSize: 36, fontWeight: FontWeight.bold, @@ -127,7 +143,7 @@ class ProfilePage extends ConsumerWidget { Row( children: [ Text( - user.nickname ?? '榴莲用户', + user.realName ?? user.nickname ?? '榴莲用户', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -135,27 +151,33 @@ class ProfilePage extends ConsumerWidget { ), ), const SizedBox(width: 8), - // VIP 徽章 - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFFFFD700), Color(0xFFFF8C00)], + // 实名认证徽章 + if (user.isKycVerified) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, ), - borderRadius: BorderRadius.circular(10), - ), - child: const Text( - 'VIP 3', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Colors.white, + decoration: BoxDecoration( + color: _green.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.verified, size: 12, color: _green), + SizedBox(width: 2), + Text( + '已实名', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: _green, + ), + ), + ], ), ), - ), ], ), const SizedBox(height: 8), @@ -191,6 +213,18 @@ class ProfilePage extends ConsumerWidget { ), ], ), + // 手机号 + if (user.phone != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + '手机: ${user.phone}', + style: const TextStyle( + fontSize: 12, + color: _lightGray, + ), + ), + ), ], ), ), @@ -210,30 +244,48 @@ class ProfilePage extends ConsumerWidget { ); } - Widget _buildStatsRow() { + Widget _buildStatsRow(UserStats? stats, bool isLoading) { return Container( padding: const EdgeInsets.symmetric(vertical: 16), color: Colors.white, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildStatItem('认种数量', '10'), + _buildStatItem( + '认种状态', + stats?.hasAdopted == true ? '已认种' : '未认种', + isLoading, + ), _buildDivider(), - _buildStatItem('直推人数', '5'), + _buildStatItem( + '直推人数', + stats?.directReferralAdoptedCount.toString() ?? '0', + isLoading, + ), _buildDivider(), - _buildStatItem('团队人数', '128'), + _buildStatItem( + '团队层级', + stats?.unlockedLevelDepth.toString() ?? '0', + isLoading, + ), _buildDivider(), - _buildStatItem('VIP等级', 'V3'), + _buildStatItem( + 'VIP等级', + stats?.vipLevel ?? '-', + isLoading, + ), ], ), ); } - Widget _buildStatItem(String label, String value) { + Widget _buildStatItem(String label, String value, bool isLoading) { return Column( children: [ - Text( - value, + DataText( + data: isLoading ? null : value, + isLoading: isLoading, + placeholder: '--', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -260,9 +312,7 @@ class ProfilePage extends ConsumerWidget { ); } - Widget _buildInvitationCard(BuildContext context) { - const invitationCode = 'DUR8888XYZ'; - + Widget _buildInvitationCard(BuildContext context, String invitationCode) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), @@ -293,9 +343,9 @@ class ProfilePage extends ConsumerWidget { color: _bgGray, borderRadius: BorderRadius.circular(8), ), - child: const Text( + child: Text( invitationCode, - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: _darkText, @@ -309,7 +359,7 @@ class ProfilePage extends ConsumerWidget { icon: Icons.copy, label: '复制', onTap: () { - Clipboard.setData(const ClipboardData(text: invitationCode)); + Clipboard.setData(ClipboardData(text: invitationCode)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('邀请码已复制'), @@ -364,7 +414,7 @@ class ProfilePage extends ConsumerWidget { ); } - Widget _buildAccountSettings(BuildContext context) { + Widget _buildAccountSettings(BuildContext context, UserState user) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( @@ -393,11 +443,11 @@ class ProfilePage extends ConsumerWidget { _buildSettingItem( icon: Icons.verified_user, label: '实名认证', - trailing: const Text( - '已认证', + trailing: Text( + user.isKycVerified ? '已认证' : '未认证', style: TextStyle( fontSize: 14, - color: _green, + color: user.isKycVerified ? _green : _lightGray, ), ), onTap: () {}, diff --git a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart index 45342f32..f8514aef 100644 --- a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart +++ b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import '../../../core/constants/app_colors.dart'; import '../../../core/utils/format_utils.dart'; -import '../../providers/mining_providers.dart'; +import '../../../data/models/trade_order_model.dart'; +import '../../../domain/entities/price_info.dart'; +import '../../../domain/entities/market_overview.dart'; +import '../../../domain/entities/trade_order.dart'; import '../../providers/user_providers.dart'; import '../../providers/trading_providers.dart'; import '../../widgets/shimmer_loading.dart'; @@ -28,60 +32,55 @@ class _TradingPageState extends ConsumerState { // 状态 int _selectedTab = 1; // 0: 买入, 1: 卖出 int _selectedTimeRange = 1; // 时间周期选择 - final _amountController = TextEditingController(); + final _quantityController = TextEditingController(); + final _priceController = TextEditingController(); final List _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日']; @override void dispose() { - _amountController.dispose(); + _quantityController.dispose(); + _priceController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final globalState = ref.watch(globalStateProvider); + final priceAsync = ref.watch(currentPriceProvider); + final marketAsync = ref.watch(marketOverviewProvider); + final ordersAsync = ref.watch(ordersProvider); final user = ref.watch(userNotifierProvider); final accountSequence = user.accountSequence ?? ''; - // Extract loading state and data using shimmer placeholder approach - final isLoading = globalState.isLoading; - final state = globalState.valueOrNull; - final hasError = globalState.hasError; - return Scaffold( backgroundColor: const Color(0xFFF5F5F5), body: SafeArea( bottom: false, - child: Column( - children: [ - // 顶部导航栏 - _buildAppBar(), - // 可滚动内容 - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - // 价格卡片 - always render, use shimmer for loading - hasError - ? _buildErrorCard('价格加载失败') - : _buildPriceCard(state, isLoading), - // K线图占位区域 - _buildChartSection(), - // 市场数据 - always render, use shimmer for loading - hasError - ? _buildErrorCard('市场数据加载失败') - : _buildMarketDataCard(state, isLoading), - // 买入/卖出交易面板 - _buildTradingPanel(accountSequence), - // 我的挂单 - _buildMyOrdersCard(), - const SizedBox(height: 100), - ], + child: RefreshIndicator( + onRefresh: () async { + ref.invalidate(currentPriceProvider); + ref.invalidate(marketOverviewProvider); + ref.invalidate(ordersProvider); + }, + child: Column( + children: [ + _buildAppBar(), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + _buildPriceCard(priceAsync), + _buildChartSection(priceAsync), + _buildMarketDataCard(marketAsync), + _buildTradingPanel(priceAsync), + _buildMyOrdersCard(ordersAsync), + const SizedBox(height: 100), + ], + ), ), ), - ), - ], + ], + ), ), ), ); @@ -89,12 +88,11 @@ class _TradingPageState extends ConsumerState { Widget _buildAppBar() { return Container( - color: _bgGray.withOpacity(0.9), + color: _bgGray.withValues(alpha: 0.9), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // 积分股交易按钮 Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( @@ -110,7 +108,6 @@ class _TradingPageState extends ConsumerState { ), ), ), - // 通知图标 Container( width: 40, height: 40, @@ -119,7 +116,7 @@ class _TradingPageState extends ConsumerState { borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ), @@ -150,10 +147,17 @@ class _TradingPageState extends ConsumerState { ); } - Widget _buildPriceCard(dynamic state, bool isLoading) { - final isPriceUp = state?.isPriceUp ?? true; - final currentPrice = state?.currentPrice; - final priceChange = state?.priceChange24h; + Widget _buildPriceCard(AsyncValue priceAsync) { + final isLoading = priceAsync.isLoading; + final priceInfo = priceAsync.valueOrNull; + final hasError = priceAsync.hasError; + + if (hasError && priceInfo == null) { + return _buildErrorCard('价格加载失败'); + } + + final price = priceInfo?.price ?? '0'; + final greenPoints = priceInfo?.greenPoints ?? '0'; return Container( margin: const EdgeInsets.all(16), @@ -165,11 +169,10 @@ class _TradingPageState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 标题行 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( + const Text( '当前积分股价格', style: TextStyle( fontSize: 12, @@ -178,23 +181,19 @@ class _TradingPageState extends ConsumerState { ), ), DataText( - data: '= 156.00 绿积分', + data: priceInfo != null ? '= ${formatCompact(greenPoints)} 绿积分' : null, isLoading: isLoading, placeholder: '= -- 绿积分', - style: TextStyle( - fontSize: 12, - color: _grayText, - ), + style: const TextStyle(fontSize: 12, color: _grayText), ), ], ), const SizedBox(height: 8), - // 价格和涨跌 Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ AmountText( - amount: currentPrice != null ? formatPrice(currentPrice) : null, + amount: priceInfo != null ? formatPrice(price) : null, isLoading: isLoading, prefix: '\u00A5 ', style: const TextStyle( @@ -208,19 +207,15 @@ class _TradingPageState extends ConsumerState { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: _green.withOpacity(0.1), + color: _green.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - isPriceUp ? Icons.trending_up : Icons.trending_down, - size: 16, - color: _green, - ), + const Icon(Icons.trending_up, size: 16, color: _green), DataText( - data: priceChange != null ? '+$priceChange%' : null, + data: isLoading ? null : '+0.00%', isLoading: isLoading, placeholder: '+--.--%', style: const TextStyle( @@ -239,7 +234,10 @@ class _TradingPageState extends ConsumerState { ); } - Widget _buildChartSection() { + Widget _buildChartSection(AsyncValue priceAsync) { + final priceInfo = priceAsync.valueOrNull; + final currentPrice = priceInfo?.price ?? '0.000000'; + return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), @@ -249,7 +247,6 @@ class _TradingPageState extends ConsumerState { ), child: Column( children: [ - // K线图占位 Container( height: 200, decoration: BoxDecoration( @@ -258,12 +255,10 @@ class _TradingPageState extends ConsumerState { ), child: Stack( children: [ - // 模拟K线图 CustomPaint( size: const Size(double.infinity, 200), painter: _CandlestickPainter(), ), - // 当前价格标签 Positioned( right: 0, top: 60, @@ -274,18 +269,15 @@ class _TradingPageState extends ConsumerState { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 2, offset: const Offset(0, 1), ), ], ), - child: const Text( - '0.000156', - style: TextStyle( - fontSize: 10, - color: Colors.white, - ), + child: Text( + formatPrice(currentPrice), + style: const TextStyle(fontSize: 10, color: Colors.white), ), ), ), @@ -293,7 +285,6 @@ class _TradingPageState extends ConsumerState { ), ), const SizedBox(height: 16), - // 时间周期选择 SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -329,11 +320,14 @@ class _TradingPageState extends ConsumerState { ); } - Widget _buildMarketDataCard(dynamic state, bool isLoading) { - final sharesPool = state?.sharesPool; - final circulatingPool = state?.circulatingPool; - final greenPointsPool = state?.greenPointsPool; - final blackHoleBurned = state?.blackHoleBurned; + Widget _buildMarketDataCard(AsyncValue marketAsync) { + final isLoading = marketAsync.isLoading; + final market = marketAsync.valueOrNull; + final hasError = marketAsync.hasError; + + if (hasError && market == null) { + return _buildErrorCard('市场数据加载失败'); + } return Container( margin: const EdgeInsets.all(16), @@ -345,7 +339,6 @@ class _TradingPageState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 标题 Row( children: [ Container( @@ -368,12 +361,11 @@ class _TradingPageState extends ConsumerState { ], ), const SizedBox(height: 24), - // 第一行数据 Row( children: [ _buildMarketDataItem( - '积分股池', - sharesPool ?? '8,888,888,888', + '总积分股', + market != null ? formatCompact(market.totalShares) : null, _orange, isLoading, ), @@ -381,7 +373,7 @@ class _TradingPageState extends ConsumerState { const SizedBox(width: 16), _buildMarketDataItem( '流通池', - circulatingPool ?? '1,234,567', + market != null ? formatCompact(market.circulationPool) : null, _orange, isLoading, ), @@ -390,12 +382,11 @@ class _TradingPageState extends ConsumerState { const SizedBox(height: 24), Container(height: 1, color: _bgGray), const SizedBox(height: 24), - // 第二行数据 Row( children: [ _buildMarketDataItem( '绿积分池', - greenPointsPool ?? '99,999,999', + market != null ? formatCompact(market.greenPoints) : null, _orange, isLoading, ), @@ -403,7 +394,7 @@ class _TradingPageState extends ConsumerState { const SizedBox(width: 16), _buildMarketDataItem( '黑洞销毁量', - blackHoleBurned ?? '50,000,000', + market != null ? formatCompact(market.blackHoleAmount) : null, _red, isLoading, ), @@ -414,18 +405,12 @@ class _TradingPageState extends ConsumerState { ); } - Widget _buildMarketDataItem(String label, String value, Color valueColor, bool isLoading) { + Widget _buildMarketDataItem(String label, String? value, Color valueColor, bool isLoading) { return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: const TextStyle( - fontSize: 12, - color: _grayText, - ), - ), + Text(label, style: const TextStyle(fontSize: 12, color: _grayText)), const SizedBox(height: 4), DataText( data: value, @@ -442,7 +427,15 @@ class _TradingPageState extends ConsumerState { ); } - Widget _buildTradingPanel(String accountSequence) { + Widget _buildTradingPanel(AsyncValue priceAsync) { + final priceInfo = priceAsync.valueOrNull; + final currentPrice = priceInfo?.price ?? '0'; + + // 设置默认价格 + if (_priceController.text.isEmpty && priceInfo != null) { + _priceController.text = currentPrice; + } + return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(20), @@ -452,7 +445,6 @@ class _TradingPageState extends ConsumerState { ), child: Column( children: [ - // 买入/卖出切换 Container( decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: _bgGray)), @@ -513,71 +505,13 @@ class _TradingPageState extends ConsumerState { ), ), const SizedBox(height: 24), - // 数量输入 - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '数量', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: _grayText, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: Container( - height: 44, - decoration: BoxDecoration( - color: _bgGray, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _amountController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - decoration: const InputDecoration( - hintText: '请输入数量', - hintStyle: TextStyle( - fontSize: 14, - color: Color(0xFF9CA3AF), - ), - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(horizontal: 16), - ), - ), - ), - const Icon(Icons.currency_yuan, size: 15, color: _grayText), - const SizedBox(width: 12), - ], - ), - ), - ), - const SizedBox(width: 12), - GestureDetector( - onTap: () { - // TODO: 设置最大数量 - }, - child: const Text( - 'MAX', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: _orange, - ), - ), - ), - ], - ), - ], - ), + // 价格输入 + _buildInputField('价格', _priceController, '请输入价格', '绿积分'), const SizedBox(height: 16), - // 预计获得 + // 数量输入 + _buildInputField('数量', _quantityController, '请输入数量', '积分股'), + const SizedBox(height: 16), + // 预计获得/支出 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( @@ -587,16 +521,13 @@ class _TradingPageState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - '预计获得', - style: TextStyle( - fontSize: 12, - color: _grayText, - ), + Text( + _selectedTab == 0 ? '预计支出' : '预计获得', + style: const TextStyle(fontSize: 12, color: _grayText), ), - const Text( - '0.00 绿积分', - style: TextStyle( + Text( + _calculateEstimate(), + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: _orange, @@ -606,37 +537,35 @@ class _TradingPageState extends ConsumerState { ), ), const SizedBox(height: 16), - // 手续费 - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - Text( - '手续费 (10%)', - style: TextStyle( - fontSize: 12, - color: _grayText, + // 销毁说明 (卖出时显示) + if (_selectedTab == 1) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + Text( + '销毁比例', + style: TextStyle(fontSize: 12, color: _grayText), ), - ), - Text( - '0.00', - style: TextStyle( - fontSize: 12, - color: _grayText, - fontFamily: 'monospace', + Text( + '10% 进入黑洞', + style: TextStyle( + fontSize: 12, + color: _red, + fontFamily: 'monospace', + ), ), - ), - ], + ], + ), ), - ), const SizedBox(height: 24), // 提交按钮 SizedBox( width: double.infinity, height: 48, child: ElevatedButton( - onPressed: () => _handleTrade(accountSequence), + onPressed: _handleTrade, style: ElevatedButton.styleFrom( backgroundColor: _orange, shape: RoundedRectangleBorder( @@ -658,7 +587,79 @@ class _TradingPageState extends ConsumerState { ); } - Widget _buildMyOrdersCard() { + Widget _buildInputField( + String label, + TextEditingController controller, + String hint, + String suffix, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: _grayText, + ), + ), + const SizedBox(height: 8), + Container( + height: 44, + decoration: BoxDecoration( + color: _bgGray, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + hintText: hint, + hintStyle: const TextStyle( + fontSize: 14, + color: Color(0xFF9CA3AF), + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + ), + ), + Text(suffix, style: const TextStyle(fontSize: 12, color: _grayText)), + const SizedBox(width: 12), + ], + ), + ), + ], + ); + } + + String _calculateEstimate() { + final price = double.tryParse(_priceController.text) ?? 0; + final quantity = double.tryParse(_quantityController.text) ?? 0; + final total = price * quantity; + + if (total == 0) { + return '0.00 绿积分'; + } + + if (_selectedTab == 1) { + // 卖出时扣除10%销毁 + final afterBurn = total * 0.9; + return '${formatAmount(afterBurn.toString())} 绿积分'; + } + + return '${formatAmount(total.toString())} 绿积分'; + } + + Widget _buildMyOrdersCard(AsyncValue ordersAsync) { + final isLoading = ordersAsync.isLoading; + final ordersPage = ordersAsync.valueOrNull; + final orders = ordersPage?.data ?? []; + return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), @@ -668,7 +669,6 @@ class _TradingPageState extends ConsumerState { ), child: Column( children: [ - // 标题 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -696,27 +696,54 @@ class _TradingPageState extends ConsumerState { ], ), const SizedBox(height: 16), - // 挂单列表项 - _buildOrderItem( - type: '卖出', - price: '0.000156', - quantity: '1,000 股', - time: '12/05 14:30', - status: '待成交', - ), + if (isLoading) + const Center( + child: Padding( + padding: EdgeInsets.all(20), + child: CircularProgressIndicator(color: _orange), + ), + ) + else if (orders.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Text( + '暂无挂单', + style: TextStyle(fontSize: 14, color: _grayText), + ), + ) + else + Column( + children: orders.take(3).map((order) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildOrderItemFromEntity(order), + )).toList(), + ), ], ), ); } - Widget _buildOrderItem({ - required String type, - required String price, - required String quantity, - required String time, - required String status, - }) { - final isSell = type == '卖出'; + Widget _buildOrderItemFromEntity(TradeOrder order) { + final isSell = order.isSell; + final dateFormat = DateFormat('MM/dd HH:mm'); + final formattedDate = dateFormat.format(order.createdAt); + + String statusText; + switch (order.status) { + case OrderStatus.pending: + statusText = '待成交'; + break; + case OrderStatus.partial: + statusText = '部分成交'; + break; + case OrderStatus.filled: + statusText = '已成交'; + break; + case OrderStatus.cancelled: + statusText = '已取消'; + break; + } + return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( @@ -735,11 +762,11 @@ class _TradingPageState extends ConsumerState { Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: (isSell ? _red : _green).withOpacity(0.1), + color: (isSell ? _red : _green).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Text( - type, + isSell ? '卖出' : '买入', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, @@ -749,7 +776,7 @@ class _TradingPageState extends ConsumerState { ), const SizedBox(width: 8), Text( - price, + formatPrice(order.price), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -760,19 +787,16 @@ class _TradingPageState extends ConsumerState { ), const SizedBox(height: 4), Text( - time, - style: const TextStyle( - fontSize: 12, - color: _grayText, - ), + formattedDate, + style: const TextStyle(fontSize: 12, color: _grayText), ), ], ), Column( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - quantity, + '${formatCompact(order.quantity)} 股', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -783,15 +807,12 @@ class _TradingPageState extends ConsumerState { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: _orange.withOpacity(0.1), + color: _orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(9999), ), child: Text( - status, - style: const TextStyle( - fontSize: 12, - color: _orange, - ), + statusText, + style: const TextStyle(fontSize: 12, color: _orange), ), ), ], @@ -821,8 +842,15 @@ class _TradingPageState extends ConsumerState { ); } - void _handleTrade(String accountSequence) async { - if (_amountController.text.isEmpty) { + void _handleTrade() async { + if (_priceController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('请输入价格')), + ); + return; + } + + if (_quantityController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请输入数量')), ); @@ -835,11 +863,11 @@ class _TradingPageState extends ConsumerState { if (isBuy) { success = await ref .read(tradingNotifierProvider.notifier) - .buyShares(accountSequence, _amountController.text); + .buyShares(_priceController.text, _quantityController.text); } else { success = await ref .read(tradingNotifierProvider.notifier) - .sellShares(accountSequence, _amountController.text); + .sellShares(_priceController.text, _quantityController.text); } if (mounted) { @@ -852,10 +880,11 @@ class _TradingPageState extends ConsumerState { ), ); if (success) { - _amountController.clear(); - // 交易成功后刷新所有相关数据 - ref.invalidate(shareAccountProvider(accountSequence)); - ref.invalidate(globalStateProvider); + _quantityController.clear(); + // 交易成功后刷新订单列表 + ref.invalidate(ordersProvider); + ref.invalidate(currentPriceProvider); + ref.invalidate(marketOverviewProvider); } } } @@ -889,7 +918,7 @@ class _CandlestickPainter extends CustomPainter { ]; final candleWidth = (size.width - 40) / candleData.length; - final padding = 20.0; + const padding = 20.0; for (int i = 0; i < candleData.length; i++) { final data = candleData[i]; @@ -907,14 +936,12 @@ class _CandlestickPainter extends CustomPainter { final yHigh = size.height - (high * size.height * 0.8 + size.height * 0.1); final yLow = size.height - (low * size.height * 0.8 + size.height * 0.1); - // 绘制影线 canvas.drawLine( Offset(x, yHigh), Offset(x, yLow), paint..strokeWidth = 1, ); - // 绘制实体 final bodyTop = isGreen ? yClose : yOpen; final bodyBottom = isGreen ? yOpen : yClose; canvas.drawRect( @@ -923,7 +950,6 @@ class _CandlestickPainter extends CustomPainter { ); } - // 绘制虚线参考线 final dashY = size.height * 0.35; const dashWidth = 5.0; const dashSpace = 3.0; diff --git a/frontend/mining-app/lib/presentation/providers/asset_providers.dart b/frontend/mining-app/lib/presentation/providers/asset_providers.dart new file mode 100644 index 00000000..e86b150c --- /dev/null +++ b/frontend/mining-app/lib/presentation/providers/asset_providers.dart @@ -0,0 +1,77 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../domain/entities/asset_display.dart'; +import '../../domain/repositories/trading_repository.dart'; +import '../../core/di/injection.dart'; +import 'trading_providers.dart'; + +/// 我的资产显示 Provider (需要登录,使用 JWT token) +final myAssetProvider = FutureProvider((ref) async { + final repository = ref.watch(tradingRepositoryProvider); + final result = await repository.getMyAsset(); + + ref.keepAlive(); + final timer = Timer(const Duration(minutes: 2), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) => throw Exception(failure.message), + (asset) => asset, + ); +}); + +/// 指定账户资产显示 Provider (public API) +final accountAssetProvider = FutureProvider.family( + (ref, accountSequence) async { + if (accountSequence.isEmpty) return null; + + final repository = ref.watch(tradingRepositoryProvider); + final result = await repository.getAccountAsset(accountSequence); + + ref.keepAlive(); + final timer = Timer(const Duration(minutes: 2), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) => throw Exception(failure.message), + (asset) => asset, + ); + }, +); + +/// 资产显示值计算器 - 用于实时显示资产估值 +/// displayAssetValue = (账户积分股 + 账户积分股 × 倍数) × 积分股价 +class AssetValueCalculator { + final String shareBalance; + final String burnMultiplier; + final String currentPrice; + + AssetValueCalculator({ + required this.shareBalance, + required this.burnMultiplier, + required this.currentPrice, + }); + + /// 计算有效积分股 = 余额 × (1 + 倍数) + double get effectiveShares { + final balance = double.tryParse(shareBalance) ?? 0; + final multiplier = double.tryParse(burnMultiplier) ?? 0; + return balance * (1 + multiplier); + } + + /// 计算资产显示值 = 有效积分股 × 价格 + double get displayValue { + final price = double.tryParse(currentPrice) ?? 0; + return effectiveShares * price; + } + + /// 计算每秒增长值 + static double calculateGrowthPerSecond(String dailyAllocation) { + final daily = double.tryParse(dailyAllocation) ?? 0; + return daily / 24 / 60 / 60; + } +} diff --git a/frontend/mining-app/lib/presentation/providers/profile_providers.dart b/frontend/mining-app/lib/presentation/providers/profile_providers.dart new file mode 100644 index 00000000..9c102e9f --- /dev/null +++ b/frontend/mining-app/lib/presentation/providers/profile_providers.dart @@ -0,0 +1,91 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../domain/entities/contribution.dart'; +import '../../domain/repositories/contribution_repository.dart'; +import '../../core/di/injection.dart'; +import 'user_providers.dart'; + +/// 用户统计数据 - 来自贡献值服务 +class UserStats { + /// 是否已认种 + final bool hasAdopted; + + /// 直推认种用户数 + final int directReferralAdoptedCount; + + /// 已解锁层级深度(可理解为团队层数) + final int unlockedLevelDepth; + + /// 已解锁奖励档位数(VIP等级) + final int unlockedBonusTiers; + + /// 总贡献值 + final String totalContribution; + + const UserStats({ + this.hasAdopted = false, + this.directReferralAdoptedCount = 0, + this.unlockedLevelDepth = 0, + this.unlockedBonusTiers = 0, + this.totalContribution = '0', + }); + + /// 从贡献值账户数据构建 + factory UserStats.fromContribution(Contribution contribution) { + return UserStats( + hasAdopted: contribution.hasAdopted, + directReferralAdoptedCount: contribution.directReferralAdoptedCount, + unlockedLevelDepth: contribution.unlockedLevelDepth, + unlockedBonusTiers: contribution.unlockedBonusTiers, + totalContribution: contribution.totalContribution, + ); + } + + /// VIP等级显示 + String get vipLevel { + if (unlockedBonusTiers <= 0) return '-'; + return 'V$unlockedBonusTiers'; + } + + /// 认种数量(简化展示直推数) + int get adoptionCount => hasAdopted ? 1 : 0; + + /// 团队人数估算(基于已解锁层级) + int get teamSize { + // 简单估算:每层平均3人 + if (unlockedLevelDepth <= 0) return 0; + int total = 0; + for (int i = 1; i <= unlockedLevelDepth; i++) { + total += (directReferralAdoptedCount > 0 ? directReferralAdoptedCount : 1) * i; + } + return total; + } +} + +/// 用户统计数据 Provider +final userStatsProvider = FutureProvider((ref) async { + final user = ref.watch(userNotifierProvider); + if (!user.isLoggedIn || user.accountSequence == null) { + return null; + } + + final repository = getIt(); + final result = await repository.getUserContribution(user.accountSequence!); + + ref.keepAlive(); + final timer = Timer(const Duration(minutes: 5), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) => const UserStats(), + (contribution) => UserStats.fromContribution(contribution), + ); +}); + +/// 邀请码 Provider - 使用账户序号作为邀请码 +final invitationCodeProvider = Provider((ref) { + final user = ref.watch(userNotifierProvider); + return user.accountSequence ?? '--------'; +}); diff --git a/frontend/mining-app/lib/presentation/providers/trading_providers.dart b/frontend/mining-app/lib/presentation/providers/trading_providers.dart index 6f910458..a26657f9 100644 --- a/frontend/mining-app/lib/presentation/providers/trading_providers.dart +++ b/frontend/mining-app/lib/presentation/providers/trading_providers.dart @@ -1,60 +1,131 @@ +import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../domain/entities/price_info.dart'; +import '../../domain/entities/market_overview.dart'; +import '../../domain/entities/trading_account.dart'; import '../../domain/entities/trade_order.dart'; -import '../../domain/usecases/trading/buy_shares.dart'; -import '../../domain/usecases/trading/sell_shares.dart'; +import '../../domain/repositories/trading_repository.dart'; +import '../../data/models/trade_order_model.dart'; import '../../core/di/injection.dart'; -// Use Cases Providers -final buySharesUseCaseProvider = Provider((ref) { - return getIt(); -}); - -final sellSharesUseCaseProvider = Provider((ref) { - return getIt(); +// Repository Provider +final tradingRepositoryProvider = Provider((ref) { + return getIt(); }); // K线周期选择 final selectedKlinePeriodProvider = StateProvider((ref) => '1h'); +// 当前价格 Provider (5分钟缓存) +final currentPriceProvider = FutureProvider((ref) async { + final repository = ref.watch(tradingRepositoryProvider); + final result = await repository.getCurrentPrice(); + + ref.keepAlive(); + final timer = Timer(const Duration(minutes: 5), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) => throw Exception(failure.message), + (priceInfo) => priceInfo, + ); +}); + +// 市场概览 Provider (5分钟缓存) +final marketOverviewProvider = FutureProvider((ref) async { + final repository = ref.watch(tradingRepositoryProvider); + final result = await repository.getMarketOverview(); + + ref.keepAlive(); + final timer = Timer(const Duration(minutes: 5), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) => throw Exception(failure.message), + (overview) => overview, + ); +}); + +// 交易账户 Provider (带参数) +final tradingAccountProvider = FutureProvider.family( + (ref, accountSequence) async { + if (accountSequence.isEmpty) return null; + + final repository = ref.watch(tradingRepositoryProvider); + final result = await repository.getTradingAccount(accountSequence); + + ref.keepAlive(); + final timer = Timer(const Duration(minutes: 2), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) => throw Exception(failure.message), + (account) => account, + ); + }, +); + +// 订单列表 Provider +final ordersProvider = FutureProvider((ref) async { + final repository = ref.watch(tradingRepositoryProvider); + final result = await repository.getOrders(page: 1, pageSize: 10); + + ref.keepAlive(); + final timer = Timer(const Duration(minutes: 2), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) => throw Exception(failure.message), + (orders) => orders, + ); +}); + // 交易状态 class TradingState { final bool isLoading; final String? error; - final TradeOrder? lastOrder; + final Map? lastOrderResult; TradingState({ this.isLoading = false, this.error, - this.lastOrder, + this.lastOrderResult, }); TradingState copyWith({ bool? isLoading, String? error, - TradeOrder? lastOrder, + Map? lastOrderResult, }) { return TradingState( isLoading: isLoading ?? this.isLoading, error: error, - lastOrder: lastOrder ?? this.lastOrder, + lastOrderResult: lastOrderResult ?? this.lastOrderResult, ); } } class TradingNotifier extends StateNotifier { - final BuyShares buySharesUseCase; - final SellShares sellSharesUseCase; + final TradingRepository repository; - TradingNotifier({ - required this.buySharesUseCase, - required this.sellSharesUseCase, - }) : super(TradingState()); + TradingNotifier({required this.repository}) : super(TradingState()); - Future buyShares(String accountSequence, String amount) async { + /// 创建买入订单 + Future buyShares(String price, String quantity) async { state = state.copyWith(isLoading: true, error: null); - final result = await buySharesUseCase( - BuySharesParams(accountSequence: accountSequence, amount: amount), + final result = await repository.createOrder( + type: 'BUY', + price: price, + quantity: quantity, ); return result.fold( @@ -62,18 +133,21 @@ class TradingNotifier extends StateNotifier { state = state.copyWith(isLoading: false, error: failure.message); return false; }, - (order) { - state = state.copyWith(isLoading: false, lastOrder: order); + (orderResult) { + state = state.copyWith(isLoading: false, lastOrderResult: orderResult); return true; }, ); } - Future sellShares(String accountSequence, String amount) async { + /// 创建卖出订单 + Future sellShares(String price, String quantity) async { state = state.copyWith(isLoading: true, error: null); - final result = await sellSharesUseCase( - SellSharesParams(accountSequence: accountSequence, amount: amount), + final result = await repository.createOrder( + type: 'SELL', + price: price, + quantity: quantity, ); return result.fold( @@ -81,8 +155,62 @@ class TradingNotifier extends StateNotifier { state = state.copyWith(isLoading: false, error: failure.message); return false; }, - (order) { - state = state.copyWith(isLoading: false, lastOrder: order); + (orderResult) { + state = state.copyWith(isLoading: false, lastOrderResult: orderResult); + return true; + }, + ); + } + + /// 取消订单 + Future cancelOrder(String orderNo) async { + state = state.copyWith(isLoading: true, error: null); + + final result = await repository.cancelOrder(orderNo); + + return result.fold( + (failure) { + state = state.copyWith(isLoading: false, error: failure.message); + return false; + }, + (_) { + state = state.copyWith(isLoading: false); + return true; + }, + ); + } + + /// 划入积分股 + Future transferIn(String amount) async { + state = state.copyWith(isLoading: true, error: null); + + final result = await repository.transferIn(amount); + + return result.fold( + (failure) { + state = state.copyWith(isLoading: false, error: failure.message); + return false; + }, + (_) { + state = state.copyWith(isLoading: false); + return true; + }, + ); + } + + /// 划出积分股 + Future transferOut(String amount) async { + state = state.copyWith(isLoading: true, error: null); + + final result = await repository.transferOut(amount); + + return result.fold( + (failure) { + state = state.copyWith(isLoading: false, error: failure.message); + return false; + }, + (_) { + state = state.copyWith(isLoading: false); return true; }, ); @@ -95,7 +223,6 @@ class TradingNotifier extends StateNotifier { final tradingNotifierProvider = StateNotifierProvider( (ref) => TradingNotifier( - buySharesUseCase: ref.watch(buySharesUseCaseProvider), - sellSharesUseCase: ref.watch(sellSharesUseCaseProvider), + repository: ref.watch(tradingRepositoryProvider), ), ); diff --git a/frontend/mining-app/lib/presentation/providers/user_providers.dart b/frontend/mining-app/lib/presentation/providers/user_providers.dart index 99a2aebd..7482a218 100644 --- a/frontend/mining-app/lib/presentation/providers/user_providers.dart +++ b/frontend/mining-app/lib/presentation/providers/user_providers.dart @@ -9,6 +9,10 @@ class UserState { final String? phone; final String? kycStatus; final String? source; + final String? status; + final String? realName; + final DateTime? createdAt; + final DateTime? lastLoginAt; final String? accessToken; final String? refreshToken; final bool isLoggedIn; @@ -21,6 +25,10 @@ class UserState { this.phone, this.kycStatus, this.source, + this.status, + this.realName, + this.createdAt, + this.lastLoginAt, this.accessToken, this.refreshToken, this.isLoggedIn = false, @@ -28,12 +36,18 @@ class UserState { this.error, }); + bool get isKycVerified => kycStatus == 'VERIFIED'; + UserState copyWith({ String? accountSequence, String? nickname, String? phone, String? kycStatus, String? source, + String? status, + String? realName, + DateTime? createdAt, + DateTime? lastLoginAt, String? accessToken, String? refreshToken, bool? isLoggedIn, @@ -46,6 +60,10 @@ class UserState { phone: phone ?? this.phone, kycStatus: kycStatus ?? this.kycStatus, source: source ?? this.source, + status: status ?? this.status, + realName: realName ?? this.realName, + createdAt: createdAt ?? this.createdAt, + lastLoginAt: lastLoginAt ?? this.lastLoginAt, accessToken: accessToken ?? this.accessToken, refreshToken: refreshToken ?? this.refreshToken, isLoggedIn: isLoggedIn ?? this.isLoggedIn, @@ -77,6 +95,8 @@ class UserNotifier extends StateNotifier { phone: phone, isLoggedIn: true, ); + // 登录后自动获取用户详情 + await fetchProfile(); } } @@ -143,6 +163,8 @@ class UserNotifier extends StateNotifier { isLoggedIn: true, isLoading: false, ); + // 登录后获取详细信息 + await fetchProfile(); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); rethrow; @@ -164,6 +186,8 @@ class UserNotifier extends StateNotifier { isLoggedIn: true, isLoading: false, ); + // 登录后获取详细信息 + await fetchProfile(); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); rethrow; @@ -219,6 +243,24 @@ class UserNotifier extends StateNotifier { } } + /// 获取用户详细信息 + Future fetchProfile() async { + if (!state.isLoggedIn) return; + try { + final profile = await _authDataSource.getProfile(); + state = state.copyWith( + phone: profile.phone, + kycStatus: profile.kycStatus, + source: profile.source, + status: profile.status, + realName: profile.realName, + createdAt: profile.createdAt, + lastLoginAt: profile.lastLoginAt, + ); + } catch (e) { + // 静默失败,不影响用户体验 + } + } } final userNotifierProvider = StateNotifierProvider(