rwadurian/frontend/mining-app/DEVELOPMENT_GUIDE.md

41 KiB
Raw Permalink Blame History

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)

// 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<Object?> get props => [
    accountSequence,
    personalContribution,
    teamLevelContribution,
    teamBonusContribution,
    totalContribution,
    effectiveContribution,
  ];
}
// 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<Object?> get props => [accountSequence, availableBalance];
}

4.2 用例定义 (Domain Layer)

// 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<Either<Failure, ShareAccount>> call(String accountSequence) async {
    return await repository.getRealtimeEarning(accountSequence);
  }
}
// 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<Either<Failure, TradeOrder>> call(SellSharesParams params) async {
    return await repository.sellShares(
      accountSequence: params.accountSequence,
      amount: params.amount,
    );
  }
}

4.3 仓库接口 (Domain Layer)

// 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<Either<Failure, ShareAccount>> getShareAccount(String accountSequence);

  Future<Either<Failure, ShareAccount>> getRealtimeEarning(String accountSequence);

  Future<Either<Failure, List<MiningRecord>>> getMiningRecords(
    String accountSequence, {
    int page = 1,
    int limit = 20,
  });

  Future<Either<Failure, GlobalState>> getGlobalState();
}

4.4 数据模型 (Data Layer)

// 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<String, dynamic> 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<String, dynamic> toJson() {
    return {
      'accountSequence': accountSequence,
      'availableBalance': availableBalance,
      'frozenBalance': frozenBalance,
      'totalMined': totalMined,
      'totalSold': totalSold,
      'totalBought': totalBought,
      'perSecondEarning': perSecondEarning,
      'displayAssetValue': displayAssetValue,
    };
  }
}

4.5 仓库实现 (Data Layer)

// 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<Either<Failure, ShareAccount>> 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<Either<Failure, ShareAccount>> 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<Either<Failure, List<MiningRecord>>> 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<Either<Failure, GlobalState>> 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)

// 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<ShareAccountModel> getShareAccount(String accountSequence);
  Future<ShareAccountModel> getRealtimeEarning(String accountSequence);
  Future<List<MiningRecordModel>> getMiningRecords(
    String accountSequence, {
    int page,
    int limit,
  });
  Future<GlobalStateModel> getGlobalState();
}

class MiningRemoteDataSourceImpl implements MiningRemoteDataSource {
  final ApiClient client;

  MiningRemoteDataSourceImpl({required this.client});

  @override
  Future<ShareAccountModel> 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<ShareAccountModel> 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<List<MiningRecordModel>> 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<GlobalStateModel> 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 定义

// 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<GetShareAccount>((ref) {
  return getIt<GetShareAccount>();
});

final getRealtimeEarningUseCaseProvider = Provider<GetRealtimeEarning>((ref) {
  return getIt<GetRealtimeEarning>();
});

final getGlobalStateUseCaseProvider = Provider<GetGlobalState>((ref) {
  return getIt<GetGlobalState>();
});

// State Providers

// 用户积分股账户
final shareAccountProvider = FutureProvider.family<ShareAccount?, String>(
  (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<ShareAccount, String>(
  (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<GlobalState?>((ref) async {
  final useCase = ref.watch(getGlobalStateUseCaseProvider);
  final result = await useCase();
  return result.fold(
    (failure) => throw Exception(failure.message),
    (state) => state,
  );
});

// 当前价格(从全局状态派生)
final currentPriceProvider = Provider<String>((ref) {
  final globalState = ref.watch(globalStateProvider);
  return globalState.when(
    data: (state) => state?.currentPrice ?? '0',
    loading: () => '0',
    error: (_, __) => '0',
  );
});

5.2 交易 Providers

// 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<List<Kline>, 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<String>((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<TradingState> {
  final BuyShares buySharesUseCase;
  final SellShares sellSharesUseCase;

  TradingNotifier({
    required this.buySharesUseCase,
    required this.sellSharesUseCase,
  }) : super(TradingState());

  Future<void> 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<void> 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<TradingNotifier, TradingState>(
  (ref) => TradingNotifier(
    buySharesUseCase: ref.watch(buySharesUseCaseProvider),
    sellSharesUseCase: ref.watch(sellSharesUseCaseProvider),
  ),
);

6. 页面实现

6.1 首页

// 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 实时收益显示组件

// 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<RealtimeEarningDisplay> createState() => _RealtimeEarningDisplayState();
}

class _RealtimeEarningDisplayState extends ConsumerState<RealtimeEarningDisplay> {
  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 交易页面

// 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线图组件

// 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<Kline> 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<Kline> 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. 路由配置

// 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. 依赖配置

# 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. 启动命令

# 获取依赖
flutter pub get

# 生成代码
flutter pub run build_runner build --delete-conflicting-outputs

# 运行开发版
flutter run

# 构建 APK
flutter build apk --release

# 构建 iOS
flutter build ios --release