41 KiB
41 KiB
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