# Mining App (挖矿用户端 Flutter App) 开发指导 ## 1. 项目概述 ### 1.1 核心职责 Mining App 是面向用户的挖矿移动应用,用户通过此 App 查看算力、挖矿收益、进行积分股买卖交易。 **主要功能:** - 贡献值展示(个人贡献值、团队贡献值) - 积分股挖矿收益实时显示 - 买卖兑换功能(积分股 ↔ 绿积分) - K线图与价格显示 - 资产展示 - 个人中心 ### 1.2 技术栈 ``` 框架: Flutter 3.x 语言: Dart 架构: Clean Architecture (3层) 状态管理: Riverpod 2.x 路由: GoRouter 网络: Dio 本地存储: Hive / SharedPreferences 依赖注入: Riverpod + get_it ``` ### 1.3 架构模式 ``` Flutter 3层 Clean Architecture + Riverpod ``` --- ## 2. 目录结构 ``` mining-app/ ├── lib/ │ ├── core/ # 核心层(基础设施) │ │ ├── di/ │ │ │ └── injection.dart # 依赖注入配置 │ │ ├── error/ │ │ │ ├── exceptions.dart # 自定义异常 │ │ │ └── failures.dart # 失败类型 │ │ ├── network/ │ │ │ ├── api_client.dart # Dio 封装 │ │ │ ├── api_endpoints.dart # API 端点 │ │ │ ├── interceptors.dart # 请求拦截器 │ │ │ └── network_info.dart # 网络状态 │ │ ├── router/ │ │ │ ├── app_router.dart # GoRouter 配置 │ │ │ └── routes.dart # 路由常量 │ │ ├── utils/ │ │ │ ├── decimal_utils.dart # 高精度计算 │ │ │ ├── format_utils.dart # 格式化工具 │ │ │ └── date_utils.dart # 日期处理 │ │ └── constants/ │ │ ├── app_colors.dart # 颜色常量 │ │ ├── app_text_styles.dart # 文字样式 │ │ └── app_constants.dart # 通用常量 │ │ │ ├── data/ # 数据层 │ │ ├── datasources/ │ │ │ ├── local/ │ │ │ │ ├── user_local_datasource.dart │ │ │ │ └── cache_manager.dart │ │ │ └── remote/ │ │ │ ├── contribution_remote_datasource.dart │ │ │ ├── mining_remote_datasource.dart │ │ │ ├── trading_remote_datasource.dart │ │ │ └── user_remote_datasource.dart │ │ ├── models/ │ │ │ ├── contribution_model.dart │ │ │ ├── share_account_model.dart │ │ │ ├── mining_record_model.dart │ │ │ ├── trade_order_model.dart │ │ │ ├── kline_model.dart │ │ │ ├── global_state_model.dart │ │ │ └── user_model.dart │ │ └── repositories/ │ │ ├── contribution_repository_impl.dart │ │ ├── mining_repository_impl.dart │ │ ├── trading_repository_impl.dart │ │ └── user_repository_impl.dart │ │ │ ├── domain/ # 领域层 │ │ ├── entities/ │ │ │ ├── contribution.dart │ │ │ ├── share_account.dart │ │ │ ├── mining_record.dart │ │ │ ├── trade_order.dart │ │ │ ├── kline.dart │ │ │ ├── global_state.dart │ │ │ └── user.dart │ │ ├── repositories/ │ │ │ ├── contribution_repository.dart │ │ │ ├── mining_repository.dart │ │ │ ├── trading_repository.dart │ │ │ └── user_repository.dart │ │ └── usecases/ │ │ ├── contribution/ │ │ │ ├── get_user_contribution.dart │ │ │ └── get_contribution_records.dart │ │ ├── mining/ │ │ │ ├── get_share_account.dart │ │ │ ├── get_mining_records.dart │ │ │ ├── get_global_state.dart │ │ │ └── get_realtime_earning.dart │ │ ├── trading/ │ │ │ ├── buy_shares.dart │ │ │ ├── sell_shares.dart │ │ │ ├── get_kline_data.dart │ │ │ ├── get_current_price.dart │ │ │ └── get_trade_orders.dart │ │ └── user/ │ │ ├── get_user_profile.dart │ │ └── login.dart │ │ │ ├── presentation/ # 表示层 │ │ ├── pages/ │ │ │ ├── splash/ │ │ │ │ └── splash_page.dart │ │ │ ├── home/ │ │ │ │ ├── home_page.dart │ │ │ │ └── widgets/ │ │ │ │ ├── asset_overview_card.dart │ │ │ │ ├── realtime_earning_display.dart │ │ │ │ └── quick_actions.dart │ │ │ ├── contribution/ │ │ │ │ ├── contribution_page.dart │ │ │ │ └── widgets/ │ │ │ │ ├── contribution_summary.dart │ │ │ │ ├── contribution_breakdown.dart │ │ │ │ └── contribution_record_list.dart │ │ │ ├── mining/ │ │ │ │ ├── mining_page.dart │ │ │ │ └── widgets/ │ │ │ │ ├── mining_stats.dart │ │ │ │ └── mining_record_list.dart │ │ │ ├── trading/ │ │ │ │ ├── trading_page.dart │ │ │ │ ├── buy_page.dart │ │ │ │ ├── sell_page.dart │ │ │ │ └── widgets/ │ │ │ │ ├── kline_chart.dart │ │ │ │ ├── price_display.dart │ │ │ │ ├── trade_form.dart │ │ │ │ └── order_list.dart │ │ │ └── profile/ │ │ │ ├── profile_page.dart │ │ │ └── widgets/ │ │ │ └── user_info_card.dart │ │ │ │ │ ├── providers/ # Riverpod Providers │ │ │ ├── contribution_providers.dart │ │ │ ├── mining_providers.dart │ │ │ ├── trading_providers.dart │ │ │ ├── user_providers.dart │ │ │ └── global_providers.dart │ │ │ │ │ └── widgets/ # 共享组件 │ │ ├── common/ │ │ │ ├── loading_widget.dart │ │ │ ├── error_widget.dart │ │ │ ├── empty_widget.dart │ │ │ └── refresh_wrapper.dart │ │ ├── charts/ │ │ │ ├── candlestick_chart.dart │ │ │ ├── line_chart.dart │ │ │ └── chart_period_selector.dart │ │ └── cards/ │ │ ├── stat_card.dart │ │ ├── balance_card.dart │ │ └── record_card.dart │ │ │ └── main.dart # 应用入口 │ ├── assets/ │ ├── images/ │ └── fonts/ │ ├── test/ │ ├── unit/ │ ├── widget/ │ └── integration/ │ ├── pubspec.yaml ├── analysis_options.yaml └── README.md ``` --- ## 3. Clean Architecture 分层 ``` ┌─────────────────────────────────────────────────────────────┐ │ Presentation Layer │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Pages (UI) ←→ Providers (State) ←→ Widgets │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ↓ 依赖 │ ├─────────────────────────────────────────────────────────────┤ │ Domain Layer │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Entities ←→ Use Cases ←→ Repository Interfaces │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ↓ 依赖 │ ├─────────────────────────────────────────────────────────────┤ │ Data Layer │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Models ←→ Repository Impl ←→ Data Sources │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### 3.1 依赖规则 - **Presentation → Domain**: 表示层只依赖领域层 - **Domain**: 不依赖任何层,纯业务逻辑 - **Data → Domain**: 数据层实现领域层定义的接口 --- ## 4. 核心实现 ### 4.1 实体定义 (Domain Layer) ```dart // domain/entities/contribution.dart import 'package:equatable/equatable.dart'; class Contribution extends Equatable { final String accountSequence; final String personalContribution; final String teamLevelContribution; final String teamBonusContribution; final String totalContribution; final String effectiveContribution; final bool hasAdopted; final int directReferralAdoptedCount; final int unlockedLevelDepth; final int unlockedBonusTiers; const Contribution({ required this.accountSequence, required this.personalContribution, required this.teamLevelContribution, required this.teamBonusContribution, required this.totalContribution, required this.effectiveContribution, required this.hasAdopted, required this.directReferralAdoptedCount, required this.unlockedLevelDepth, required this.unlockedBonusTiers, }); @override List get props => [ accountSequence, personalContribution, teamLevelContribution, teamBonusContribution, totalContribution, effectiveContribution, ]; } ``` ```dart // domain/entities/share_account.dart class ShareAccount extends Equatable { final String accountSequence; final String availableBalance; final String frozenBalance; final String totalMined; final String totalSold; final String totalBought; final String perSecondEarning; final String displayAssetValue; const ShareAccount({ required this.accountSequence, required this.availableBalance, required this.frozenBalance, required this.totalMined, required this.totalSold, required this.totalBought, required this.perSecondEarning, required this.displayAssetValue, }); @override List get props => [accountSequence, availableBalance]; } ``` ### 4.2 用例定义 (Domain Layer) ```dart // domain/usecases/mining/get_realtime_earning.dart import 'package:dartz/dartz.dart'; import '../../entities/share_account.dart'; import '../../repositories/mining_repository.dart'; import '../../../core/error/failures.dart'; class GetRealtimeEarning { final MiningRepository repository; GetRealtimeEarning(this.repository); Future> call(String accountSequence) async { return await repository.getRealtimeEarning(accountSequence); } } ``` ```dart // domain/usecases/trading/sell_shares.dart import 'package:dartz/dartz.dart'; import '../../entities/trade_order.dart'; import '../../repositories/trading_repository.dart'; import '../../../core/error/failures.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, ); } } ``` ### 4.3 仓库接口 (Domain Layer) ```dart // domain/repositories/mining_repository.dart import 'package:dartz/dartz.dart'; import '../entities/share_account.dart'; import '../entities/mining_record.dart'; import '../entities/global_state.dart'; import '../../core/error/failures.dart'; abstract class MiningRepository { Future> getShareAccount(String accountSequence); Future> getRealtimeEarning(String accountSequence); Future>> getMiningRecords( String accountSequence, { int page = 1, int limit = 20, }); Future> getGlobalState(); } ``` ### 4.4 数据模型 (Data Layer) ```dart // data/models/share_account_model.dart import '../../domain/entities/share_account.dart'; class ShareAccountModel extends ShareAccount { const ShareAccountModel({ required super.accountSequence, required super.availableBalance, required super.frozenBalance, required super.totalMined, required super.totalSold, required super.totalBought, required super.perSecondEarning, required super.displayAssetValue, }); factory ShareAccountModel.fromJson(Map json) { return ShareAccountModel( accountSequence: json['accountSequence'] ?? '', availableBalance: json['availableBalance'] ?? '0', frozenBalance: json['frozenBalance'] ?? '0', totalMined: json['totalMined'] ?? '0', totalSold: json['totalSold'] ?? '0', totalBought: json['totalBought'] ?? '0', perSecondEarning: json['perSecondEarning'] ?? '0', displayAssetValue: json['displayAssetValue'] ?? '0', ); } Map toJson() { return { 'accountSequence': accountSequence, 'availableBalance': availableBalance, 'frozenBalance': frozenBalance, 'totalMined': totalMined, 'totalSold': totalSold, 'totalBought': totalBought, 'perSecondEarning': perSecondEarning, 'displayAssetValue': displayAssetValue, }; } } ``` ### 4.5 仓库实现 (Data Layer) ```dart // data/repositories/mining_repository_impl.dart import 'package:dartz/dartz.dart'; import '../../domain/entities/share_account.dart'; import '../../domain/entities/mining_record.dart'; import '../../domain/entities/global_state.dart'; import '../../domain/repositories/mining_repository.dart'; import '../../core/error/exceptions.dart'; import '../../core/error/failures.dart'; import '../datasources/remote/mining_remote_datasource.dart'; import '../datasources/local/cache_manager.dart'; class MiningRepositoryImpl implements MiningRepository { final MiningRemoteDataSource remoteDataSource; final CacheManager cacheManager; MiningRepositoryImpl({ required this.remoteDataSource, required this.cacheManager, }); @override Future> getShareAccount( String accountSequence, ) async { try { final result = await remoteDataSource.getShareAccount(accountSequence); return Right(result); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } on NetworkException { return Left(NetworkFailure()); } } @override Future> getRealtimeEarning( String accountSequence, ) async { try { final result = await remoteDataSource.getRealtimeEarning(accountSequence); return Right(result); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } on NetworkException { return Left(NetworkFailure()); } } @override Future>> getMiningRecords( String accountSequence, { int page = 1, int limit = 20, }) async { try { final result = await remoteDataSource.getMiningRecords( accountSequence, page: page, limit: limit, ); return Right(result); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } on NetworkException { return Left(NetworkFailure()); } } @override Future> getGlobalState() async { try { // 先尝试从缓存获取 final cached = await cacheManager.getGlobalState(); if (cached != null && !cached.isExpired) { return Right(cached.data); } // 从远程获取 final result = await remoteDataSource.getGlobalState(); // 缓存结果 await cacheManager.cacheGlobalState(result); return Right(result); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } on NetworkException { // 网络异常时尝试返回缓存 final cached = await cacheManager.getGlobalState(); if (cached != null) { return Right(cached.data); } return Left(NetworkFailure()); } } } ``` ### 4.6 远程数据源 (Data Layer) ```dart // data/datasources/remote/mining_remote_datasource.dart import '../../models/share_account_model.dart'; import '../../models/mining_record_model.dart'; import '../../models/global_state_model.dart'; import '../../../core/network/api_client.dart'; import '../../../core/network/api_endpoints.dart'; import '../../../core/error/exceptions.dart'; abstract class MiningRemoteDataSource { Future getShareAccount(String accountSequence); Future getRealtimeEarning(String accountSequence); Future> getMiningRecords( String accountSequence, { int page, int limit, }); Future getGlobalState(); } class MiningRemoteDataSourceImpl implements MiningRemoteDataSource { final ApiClient client; MiningRemoteDataSourceImpl({required this.client}); @override Future getShareAccount(String accountSequence) async { try { final response = await client.get( ApiEndpoints.shareAccount(accountSequence), ); return ShareAccountModel.fromJson(response.data); } catch (e) { throw ServerException(e.toString()); } } @override Future getRealtimeEarning(String accountSequence) async { try { final response = await client.get( ApiEndpoints.realtimeEarning(accountSequence), ); return ShareAccountModel.fromJson(response.data); } catch (e) { throw ServerException(e.toString()); } } @override Future> getMiningRecords( String accountSequence, { int page = 1, int limit = 20, }) async { try { final response = await client.get( ApiEndpoints.miningRecords(accountSequence), queryParameters: {'page': page, 'limit': limit}, ); return (response.data['items'] as List) .map((json) => MiningRecordModel.fromJson(json)) .toList(); } catch (e) { throw ServerException(e.toString()); } } @override Future getGlobalState() async { try { final response = await client.get(ApiEndpoints.globalState); return GlobalStateModel.fromJson(response.data); } catch (e) { throw ServerException(e.toString()); } } } ``` --- ## 5. Riverpod 状态管理 ### 5.1 Provider 定义 ```dart // presentation/providers/mining_providers.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/entities/share_account.dart'; import '../../domain/entities/global_state.dart'; import '../../domain/usecases/mining/get_share_account.dart'; import '../../domain/usecases/mining/get_realtime_earning.dart'; import '../../domain/usecases/mining/get_global_state.dart'; import '../../core/di/injection.dart'; // Use Cases Providers final getShareAccountUseCaseProvider = Provider((ref) { return getIt(); }); final getRealtimeEarningUseCaseProvider = Provider((ref) { return getIt(); }); final getGlobalStateUseCaseProvider = Provider((ref) { return getIt(); }); // State Providers // 用户积分股账户 final shareAccountProvider = FutureProvider.family( (ref, accountSequence) async { final useCase = ref.watch(getShareAccountUseCaseProvider); final result = await useCase(accountSequence); return result.fold( (failure) => throw Exception(failure.message), (account) => account, ); }, ); // 实时收益(自动刷新) final realtimeEarningProvider = StreamProvider.family( (ref, accountSequence) async* { final useCase = ref.watch(getRealtimeEarningUseCaseProvider); while (true) { final result = await useCase(accountSequence); yield* Stream.value( result.fold( (failure) => throw Exception(failure.message), (account) => account, ), ); await Future.delayed(const Duration(seconds: 5)); // 5秒刷新 } }, ); // 全局状态 final globalStateProvider = FutureProvider((ref) async { final useCase = ref.watch(getGlobalStateUseCaseProvider); final result = await useCase(); return result.fold( (failure) => throw Exception(failure.message), (state) => state, ); }); // 当前价格(从全局状态派生) final currentPriceProvider = Provider((ref) { final globalState = ref.watch(globalStateProvider); return globalState.when( data: (state) => state?.currentPrice ?? '0', loading: () => '0', error: (_, __) => '0', ); }); ``` ### 5.2 交易 Providers ```dart // presentation/providers/trading_providers.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/entities/trade_order.dart'; import '../../domain/entities/kline.dart'; import '../../domain/usecases/trading/buy_shares.dart'; import '../../domain/usecases/trading/sell_shares.dart'; import '../../domain/usecases/trading/get_kline_data.dart'; // K线数据 final klineDataProvider = FutureProvider.family, KlineParams>( (ref, params) async { final useCase = ref.watch(getKlineDataUseCaseProvider); final result = await useCase(params); return result.fold( (failure) => throw Exception(failure.message), (data) => data, ); }, ); // K线周期选择 final selectedKlinePeriodProvider = StateProvider((ref) => '1h'); // 交易状态 class TradingState { final bool isLoading; final String? error; final TradeOrder? lastOrder; TradingState({ this.isLoading = false, this.error, this.lastOrder, }); TradingState copyWith({ bool? isLoading, String? error, TradeOrder? lastOrder, }) { return TradingState( isLoading: isLoading ?? this.isLoading, error: error, lastOrder: lastOrder ?? this.lastOrder, ); } } class TradingNotifier extends StateNotifier { final BuyShares buySharesUseCase; final SellShares sellSharesUseCase; TradingNotifier({ required this.buySharesUseCase, required this.sellSharesUseCase, }) : super(TradingState()); Future buyShares(String accountSequence, String amount) async { state = state.copyWith(isLoading: true, error: null); final result = await buySharesUseCase( BuySharesParams(accountSequence: accountSequence, amount: amount), ); result.fold( (failure) => state = state.copyWith(isLoading: false, error: failure.message), (order) => state = state.copyWith(isLoading: false, lastOrder: order), ); } Future sellShares(String accountSequence, String amount) async { state = state.copyWith(isLoading: true, error: null); final result = await sellSharesUseCase( SellSharesParams(accountSequence: accountSequence, amount: amount), ); result.fold( (failure) => state = state.copyWith(isLoading: false, error: failure.message), (order) => state = state.copyWith(isLoading: false, lastOrder: order), ); } } final tradingNotifierProvider = StateNotifierProvider( (ref) => TradingNotifier( buySharesUseCase: ref.watch(buySharesUseCaseProvider), sellSharesUseCase: ref.watch(sellSharesUseCaseProvider), ), ); ``` --- ## 6. 页面实现 ### 6.1 首页 ```dart // presentation/pages/home/home_page.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'widgets/asset_overview_card.dart'; import 'widgets/realtime_earning_display.dart'; import 'widgets/quick_actions.dart'; import '../../providers/mining_providers.dart'; import '../../providers/user_providers.dart'; class HomePage extends ConsumerWidget { const HomePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(currentUserProvider); return Scaffold( appBar: AppBar( title: const Text('挖矿'), actions: [ IconButton( icon: const Icon(Icons.notifications_outlined), onPressed: () {}, ), ], ), body: RefreshIndicator( onRefresh: () async { ref.invalidate(shareAccountProvider); ref.invalidate(globalStateProvider); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 资产总览卡片 user.when( data: (u) => AssetOverviewCard(accountSequence: u?.accountSequence ?? ''), loading: () => const AssetOverviewCardSkeleton(), error: (_, __) => const AssetOverviewCardError(), ), const SizedBox(height: 16), // 实时收益显示 user.when( data: (u) => RealtimeEarningDisplay(accountSequence: u?.accountSequence ?? ''), loading: () => const SizedBox.shrink(), error: (_, __) => const SizedBox.shrink(), ), const SizedBox(height: 24), // 快捷操作 const QuickActions(), const SizedBox(height: 24), // 当前价格 const PriceCard(), ], ), ), ), ); } } ``` ### 6.2 实时收益显示组件 ```dart // presentation/pages/home/widgets/realtime_earning_display.dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:decimal/decimal.dart'; import '../../../providers/mining_providers.dart'; import '../../../../core/utils/format_utils.dart'; class RealtimeEarningDisplay extends ConsumerStatefulWidget { final String accountSequence; const RealtimeEarningDisplay({ super.key, required this.accountSequence, }); @override ConsumerState createState() => _RealtimeEarningDisplayState(); } class _RealtimeEarningDisplayState extends ConsumerState { Timer? _timer; Decimal _displayEarning = Decimal.zero; Decimal _perSecondEarning = Decimal.zero; @override void initState() { super.initState(); _startTimer(); } @override void dispose() { _timer?.cancel(); super.dispose(); } void _startTimer() { _timer = Timer.periodic(const Duration(seconds: 1), (_) { setState(() { _displayEarning += _perSecondEarning; }); }); } @override Widget build(BuildContext context) { final earningAsync = ref.watch(realtimeEarningProvider(widget.accountSequence)); return earningAsync.when( data: (account) { _perSecondEarning = Decimal.parse(account.perSecondEarning); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Theme.of(context).primaryColor, Theme.of(context).primaryColor.withOpacity(0.8), ], ), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '实时收益', style: TextStyle( color: Colors.white70, fontSize: 14, ), ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( formatDecimal(_displayEarning.toString(), 8), style: const TextStyle( color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 8), const Padding( padding: EdgeInsets.only(bottom: 4), child: Text( '积分股', style: TextStyle( color: Colors.white70, fontSize: 14, ), ), ), ], ), const SizedBox(height: 4), Text( '每秒 +${formatDecimal(account.perSecondEarning, 10)}', style: const TextStyle( color: Colors.white60, fontSize: 12, ), ), ], ), ); }, loading: () => const RealtimeEarningDisplaySkeleton(), error: (_, __) => const SizedBox.shrink(), ); } } ``` ### 6.3 交易页面 ```dart // presentation/pages/trading/trading_page.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'widgets/kline_chart.dart'; import 'widgets/price_display.dart'; import 'widgets/trade_form.dart'; import '../../providers/trading_providers.dart'; class TradingPage extends ConsumerWidget { const TradingPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final selectedPeriod = ref.watch(selectedKlinePeriodProvider); return Scaffold( appBar: AppBar( title: const Text('交易'), ), body: Column( children: [ // 价格显示 const PriceDisplay(), // K线图 Expanded( flex: 2, child: KlineChart(period: selectedPeriod), ), // 周期选择器 ChartPeriodSelector( selected: selectedPeriod, onChanged: (period) { ref.read(selectedKlinePeriodProvider.notifier).state = period; }, ), const Divider(), // 买卖按钮 Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.green, padding: const EdgeInsets.symmetric(vertical: 16), ), onPressed: () => _showBuySheet(context), child: const Text('买入'), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.red, padding: const EdgeInsets.symmetric(vertical: 16), ), onPressed: () => _showSellSheet(context), child: const Text('卖出'), ), ), ], ), ), ], ), ); } void _showBuySheet(BuildContext context) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => const BuyTradeForm(), ); } void _showSellSheet(BuildContext context) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => const SellTradeForm(), ); } } ``` --- ## 7. K线图组件 ```dart // presentation/widgets/charts/candlestick_chart.dart import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import '../../../domain/entities/kline.dart'; class CandlestickChart extends StatelessWidget { final List data; const CandlestickChart({super.key, required this.data}); @override Widget build(BuildContext context) { if (data.isEmpty) { return const Center(child: Text('暂无数据')); } return CustomPaint( painter: CandlestickPainter(data: data), child: Container(), ); } } class CandlestickPainter extends CustomPainter { final List data; CandlestickPainter({required this.data}); @override void paint(Canvas canvas, Size size) { if (data.isEmpty) return; final candleWidth = size.width / data.length * 0.8; final maxPrice = data.map((k) => double.parse(k.high)).reduce((a, b) => a > b ? a : b); final minPrice = data.map((k) => double.parse(k.low)).reduce((a, b) => a < b ? a : b); final priceRange = maxPrice - minPrice; for (int i = 0; i < data.length; i++) { final kline = data[i]; final open = double.parse(kline.open); final close = double.parse(kline.close); final high = double.parse(kline.high); final low = double.parse(kline.low); final x = (i + 0.5) * (size.width / data.length); final isUp = close >= open; final paint = Paint() ..color = isUp ? Colors.green : Colors.red ..style = PaintingStyle.fill; // 绘制影线 final wickPaint = Paint() ..color = isUp ? Colors.green : Colors.red ..strokeWidth = 1; final highY = size.height - ((high - minPrice) / priceRange * size.height); final lowY = size.height - ((low - minPrice) / priceRange * size.height); canvas.drawLine(Offset(x, highY), Offset(x, lowY), wickPaint); // 绘制实体 final openY = size.height - ((open - minPrice) / priceRange * size.height); final closeY = size.height - ((close - minPrice) / priceRange * size.height); final top = isUp ? closeY : openY; final bottom = isUp ? openY : closeY; final bodyHeight = (bottom - top).abs().clamp(1.0, double.infinity); canvas.drawRect( Rect.fromLTWH(x - candleWidth / 2, top, candleWidth, bodyHeight), paint, ); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } ``` --- ## 8. 路由配置 ```dart // core/router/app_router.dart import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../presentation/pages/splash/splash_page.dart'; import '../../presentation/pages/home/home_page.dart'; import '../../presentation/pages/contribution/contribution_page.dart'; import '../../presentation/pages/mining/mining_page.dart'; import '../../presentation/pages/trading/trading_page.dart'; import '../../presentation/pages/profile/profile_page.dart'; import 'routes.dart'; final appRouter = GoRouter( initialLocation: Routes.splash, routes: [ GoRoute( path: Routes.splash, builder: (context, state) => const SplashPage(), ), ShellRoute( builder: (context, state, child) => MainShell(child: child), routes: [ GoRoute( path: Routes.home, builder: (context, state) => const HomePage(), ), GoRoute( path: Routes.contribution, builder: (context, state) => const ContributionPage(), ), GoRoute( path: Routes.trading, builder: (context, state) => const TradingPage(), ), GoRoute( path: Routes.profile, builder: (context, state) => const ProfilePage(), ), ], ), ], ); class MainShell extends StatelessWidget { final Widget child; const MainShell({super.key, required this.child}); @override Widget build(BuildContext context) { return Scaffold( body: child, bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'), BottomNavigationBarItem(icon: Icon(Icons.analytics), label: '贡献值'), BottomNavigationBarItem(icon: Icon(Icons.swap_horiz), label: '兑换'), BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'), ], currentIndex: _calculateSelectedIndex(context), onTap: (index) => _onItemTapped(index, context), ), ); } int _calculateSelectedIndex(BuildContext context) { final location = GoRouterState.of(context).uri.path; if (location.startsWith(Routes.home)) return 0; if (location.startsWith(Routes.contribution)) return 1; if (location.startsWith(Routes.trading)) return 2; if (location.startsWith(Routes.profile)) return 3; return 0; } void _onItemTapped(int index, BuildContext context) { switch (index) { case 0: context.go(Routes.home); break; case 1: context.go(Routes.contribution); break; case 2: context.go(Routes.trading); break; case 3: context.go(Routes.profile); break; } } } ``` --- ## 9. 依赖配置 ```yaml # pubspec.yaml name: mining_app description: 挖矿用户端 App version: 1.0.0+1 environment: sdk: '>=3.0.0 <4.0.0' flutter: '>=3.10.0' dependencies: flutter: sdk: flutter # 状态管理 flutter_riverpod: ^2.4.0 riverpod_annotation: ^2.3.0 # 路由 go_router: ^12.0.0 # 网络 dio: ^5.3.0 connectivity_plus: ^5.0.0 # 本地存储 hive: ^2.2.0 hive_flutter: ^1.1.0 shared_preferences: ^2.2.0 # 工具 dartz: ^0.10.1 equatable: ^2.0.5 get_it: ^7.6.0 injectable: ^2.3.0 decimal: ^2.3.0 # UI flutter_svg: ^2.0.7 cached_network_image: ^3.3.0 shimmer: ^3.0.0 # 图表 fl_chart: ^0.64.0 # 其他 intl: ^0.18.0 logger: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.0 build_runner: ^2.4.0 riverpod_generator: ^2.3.0 injectable_generator: ^2.4.0 hive_generator: ^2.0.0 mocktail: ^1.0.0 flutter: uses-material-design: true assets: - assets/images/ fonts: - family: PingFang fonts: - asset: assets/fonts/PingFang-Regular.ttf - asset: assets/fonts/PingFang-Medium.ttf weight: 500 - asset: assets/fonts/PingFang-Bold.ttf weight: 700 ``` --- ## 10. 关键注意事项 ### 10.1 数据精度 - 使用 `decimal` 包处理金额计算 - API 返回的数值使用字符串类型 - 显示时选择合适的小数位数 ### 10.2 实时更新 - 收益显示每秒更新(本地计时器) - 每5秒从服务器同步真实数据 - 注意内存泄漏,页面离开时取消订阅 ### 10.3 离线支持 - 关键数据本地缓存 - 网络异常时显示缓存数据 - 明确标识数据更新时间 ### 10.4 错误处理 - 统一的 Failure 类型 - UI 友好的错误提示 - 支持重试操作 --- ## 11. 开发检查清单 - [ ] 配置 Flutter 项目 - [ ] 实现 Clean Architecture 分层 - [ ] 配置 Riverpod - [ ] 实现用户认证 - [ ] 实现首页(资产概览、实时收益) - [ ] 实现贡献值页面 - [ ] 实现交易页面(K线图、买卖) - [ ] 实现个人中心 - [ ] 配置路由 - [ ] 实现本地缓存 - [ ] 编写单元测试 - [ ] UI 适配(不同屏幕尺寸) --- ## 12. 启动命令 ```bash # 获取依赖 flutter pub get # 生成代码 flutter pub run build_runner build --delete-conflicting-outputs # 运行开发版 flutter run # 构建 APK flutter build apk --release # 构建 iOS flutter build ios --release ```