rwadurian/frontend/mining-app/DEVELOPMENT_GUIDE.md

1398 lines
41 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<Object?> 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<Object?> 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<Either<Failure, ShareAccount>> 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<Either<Failure, TradeOrder>> 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<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)
```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<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)
```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<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)
```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<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 定义
```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<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
```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<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 首页
```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<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 交易页面
```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<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. 路由配置
```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
```