1398 lines
41 KiB
Markdown
1398 lines
41 KiB
Markdown
# 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
|
||
```
|