feat(mining-app): integrate real APIs for Asset and Profile pages

- Asset page now uses trading-service /asset/my endpoint
- Profile page integrates auth-service /user/profile and contribution-service
- Add new entities: AssetDisplay, PriceInfo, MarketOverview, TradingAccount
- Add corresponding models with JSON parsing
- Create asset_providers and profile_providers for state management
- Update trading_providers with real API integration
- Extend UserState and UserInfo with additional profile fields
- Remove obsolete buy_shares and sell_shares use cases
- Fix compilation errors in get_current_price and trading_page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-14 08:22:40 -08:00
parent 106a287260
commit 6bcb4af028
29 changed files with 1690 additions and 622 deletions

View File

@ -15,9 +15,6 @@ import '../../domain/repositories/trading_repository.dart';
import '../../domain/repositories/contribution_repository.dart';
import '../../domain/usecases/mining/get_share_account.dart';
import '../../domain/usecases/mining/get_global_state.dart';
import '../../domain/usecases/trading/get_current_price.dart';
import '../../domain/usecases/trading/sell_shares.dart';
import '../../domain/usecases/trading/buy_shares.dart';
import '../../domain/usecases/contribution/get_user_contribution.dart';
final getIt = GetIt.instance;
@ -72,11 +69,6 @@ Future<void> configureDependencies() async {
getIt.registerLazySingleton(() => GetShareAccount(getIt<MiningRepository>()));
getIt.registerLazySingleton(() => GetGlobalState(getIt<MiningRepository>()));
// Use Cases - Trading
getIt.registerLazySingleton(() => GetCurrentPrice(getIt<TradingRepository>()));
getIt.registerLazySingleton(() => SellShares(getIt<TradingRepository>()));
getIt.registerLazySingleton(() => BuyShares(getIt<TradingRepository>()));
// Use Cases - Contribution
getIt.registerLazySingleton(() => GetUserContribution(getIt<ContributionRepository>()));
}

View File

@ -23,16 +23,31 @@ class ApiEndpoints {
'/api/v2/mining/accounts/$accountSequence/realtime';
// Trading Service 2.0 (Kong路由: /api/v2/trading)
static const String currentPrice = '/api/v2/trading/price';
static const String klineData = '/api/v2/trading/kline';
// Price endpoints
static const String currentPrice = '/api/v2/trading/price/current';
static const String latestPrice = '/api/v2/trading/price/latest';
static const String priceHistory = '/api/v2/trading/price/history';
// Trading account endpoints
static String tradingAccount(String accountSequence) =>
'/api/v2/trading/accounts/$accountSequence';
static String createOrder(String accountSequence) =>
'/api/v2/trading/accounts/$accountSequence/orders';
static String orders(String accountSequence) =>
'/api/v2/trading/accounts/$accountSequence/orders';
static String transfer(String accountSequence) =>
'/api/v2/trading/accounts/$accountSequence/transfer';
'/api/v2/trading/trading/accounts/$accountSequence';
static const String orderBook = '/api/v2/trading/trading/orderbook';
static const String createOrder = '/api/v2/trading/trading/orders';
static const String orders = '/api/v2/trading/trading/orders';
static String cancelOrder(String orderNo) =>
'/api/v2/trading/trading/orders/$orderNo/cancel';
// Asset endpoints
static const String myAsset = '/api/v2/trading/asset/my';
static String accountAsset(String accountSequence) =>
'/api/v2/trading/asset/account/$accountSequence';
static const String estimateSell = '/api/v2/trading/asset/estimate-sell';
static const String marketOverview = '/api/v2/trading/asset/market';
// Transfer endpoints
static const String transferIn = '/api/v2/trading/transfers/in';
static const String transferOut = '/api/v2/trading/transfers/out';
static const String transferHistory = '/api/v2/trading/transfers/history';
// Contribution Service 2.0 (Kong路由: /api/v2/contribution -> /api/v1/contributions)
static String contribution(String accountSequence) =>

View File

@ -30,12 +30,20 @@ class UserInfo {
final String phone;
final String source;
final String kycStatus;
final String? status;
final String? realName;
final DateTime? createdAt;
final DateTime? lastLoginAt;
UserInfo({
required this.accountSequence,
required this.phone,
required this.source,
required this.kycStatus,
this.status,
this.realName,
this.createdAt,
this.lastLoginAt,
});
factory UserInfo.fromJson(Map<String, dynamic> json) {
@ -44,8 +52,18 @@ class UserInfo {
phone: json['phone'] as String,
source: json['source'] as String,
kycStatus: json['kycStatus'] as String,
status: json['status'] as String?,
realName: json['realName'] as String?,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'].toString())
: null,
lastLoginAt: json['lastLoginAt'] != null
? DateTime.parse(json['lastLoginAt'].toString())
: null,
);
}
bool get isKycVerified => kycStatus == 'VERIFIED';
}
abstract class AuthRemoteDataSource {

View File

@ -1,30 +1,52 @@
import '../../models/trade_order_model.dart';
import '../../models/kline_model.dart';
import '../../models/price_info_model.dart';
import '../../models/trading_account_model.dart';
import '../../models/market_overview_model.dart';
import '../../models/asset_display_model.dart';
import '../../../core/network/api_client.dart';
import '../../../core/network/api_endpoints.dart';
import '../../../core/error/exceptions.dart';
abstract class TradingRemoteDataSource {
Future<String> getCurrentPrice();
Future<List<KlineModel>> getKlineData(String period);
Future<TradeOrderModel> buyShares({
required String accountSequence,
required String amount,
///
Future<PriceInfoModel> getCurrentPrice();
///
Future<MarketOverviewModel> getMarketOverview();
///
Future<TradingAccountModel> getTradingAccount(String accountSequence);
///
Future<Map<String, dynamic>> createOrder({
required String type,
required String price,
required String quantity,
});
Future<TradeOrderModel> sellShares({
required String accountSequence,
required String amount,
});
Future<List<TradeOrderModel>> getOrders(
String accountSequence, {
///
Future<void> cancelOrder(String orderNo);
///
Future<OrdersPageModel> getOrders({
int page = 1,
int limit = 20,
});
Future<void> transfer({
required String accountSequence,
required String amount,
required String direction,
int pageSize = 50,
});
///
Future<Map<String, dynamic>> estimateSell(String quantity);
/// ()
Future<Map<String, dynamic>> transferIn(String amount);
/// ()
Future<Map<String, dynamic>> transferOut(String amount);
///
Future<AssetDisplayModel> getMyAsset({String? dailyAllocation});
///
Future<AssetDisplayModel> getAccountAsset(String accountSequence, {String? dailyAllocation});
}
class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
@ -33,90 +55,149 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
TradingRemoteDataSourceImpl({required this.client});
@override
Future<String> getCurrentPrice() async {
Future<PriceInfoModel> getCurrentPrice() async {
try {
final response = await client.get(ApiEndpoints.currentPrice);
return response.data['price']?.toString() ?? '0';
return PriceInfoModel.fromJson(response.data);
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<List<KlineModel>> getKlineData(String period) async {
Future<MarketOverviewModel> getMarketOverview() async {
try {
final response = await client.get(
ApiEndpoints.klineData,
queryParameters: {'period': period},
);
final items = response.data as List? ?? [];
return items.map((json) => KlineModel.fromJson(json)).toList();
final response = await client.get(ApiEndpoints.marketOverview);
return MarketOverviewModel.fromJson(response.data);
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<TradeOrderModel> buyShares({
required String accountSequence,
required String amount,
Future<TradingAccountModel> getTradingAccount(String accountSequence) async {
try {
final response = await client.get(ApiEndpoints.tradingAccount(accountSequence));
return TradingAccountModel.fromJson(response.data);
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<Map<String, dynamic>> createOrder({
required String type,
required String price,
required String quantity,
}) async {
try {
final response = await client.post(
ApiEndpoints.createOrder(accountSequence),
data: {'orderType': 'BUY', 'quantity': amount},
ApiEndpoints.createOrder,
data: {
'type': type,
'price': price,
'quantity': quantity,
},
);
return TradeOrderModel.fromJson(response.data);
return response.data;
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<TradeOrderModel> sellShares({
required String accountSequence,
required String amount,
}) async {
Future<void> cancelOrder(String orderNo) async {
try {
final response = await client.post(
ApiEndpoints.createOrder(accountSequence),
data: {'orderType': 'SELL', 'quantity': amount},
);
return TradeOrderModel.fromJson(response.data);
await client.post(ApiEndpoints.cancelOrder(orderNo));
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<List<TradeOrderModel>> getOrders(
String accountSequence, {
Future<OrdersPageModel> getOrders({
int page = 1,
int limit = 20,
int pageSize = 50,
}) async {
try {
final response = await client.get(
ApiEndpoints.orders(accountSequence),
queryParameters: {'page': page, 'pageSize': limit},
ApiEndpoints.orders,
queryParameters: {'page': page, 'pageSize': pageSize},
);
final items = response.data['items'] as List? ?? [];
return items.map((json) => TradeOrderModel.fromJson(json)).toList();
return OrdersPageModel.fromJson(response.data);
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<void> transfer({
required String accountSequence,
required String amount,
required String direction,
}) async {
Future<Map<String, dynamic>> estimateSell(String quantity) async {
try {
await client.post(
ApiEndpoints.transfer(accountSequence),
data: {'amount': amount, 'direction': direction},
final response = await client.get(
ApiEndpoints.estimateSell,
queryParameters: {'quantity': quantity},
);
return response.data;
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<Map<String, dynamic>> transferIn(String amount) async {
try {
final response = await client.post(
ApiEndpoints.transferIn,
data: {'amount': amount},
);
return response.data;
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<Map<String, dynamic>> transferOut(String amount) async {
try {
final response = await client.post(
ApiEndpoints.transferOut,
data: {'amount': amount},
);
return response.data;
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<AssetDisplayModel> getMyAsset({String? dailyAllocation}) async {
try {
final queryParams = <String, dynamic>{};
if (dailyAllocation != null) {
queryParams['dailyAllocation'] = dailyAllocation;
}
final response = await client.get(
ApiEndpoints.myAsset,
queryParameters: queryParams.isNotEmpty ? queryParams : null,
);
return AssetDisplayModel.fromJson(response.data);
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<AssetDisplayModel> getAccountAsset(String accountSequence, {String? dailyAllocation}) async {
try {
final queryParams = <String, dynamic>{};
if (dailyAllocation != null) {
queryParams['dailyAllocation'] = dailyAllocation;
}
final response = await client.get(
ApiEndpoints.accountAsset(accountSequence),
queryParameters: queryParams.isNotEmpty ? queryParams : null,
);
return AssetDisplayModel.fromJson(response.data);
} catch (e) {
throw ServerException(e.toString());
}

View File

@ -0,0 +1,55 @@
import '../../domain/entities/asset_display.dart';
class AssetDisplayModel extends AssetDisplay {
const AssetDisplayModel({
required super.shareBalance,
required super.cashBalance,
required super.frozenShares,
required super.frozenCash,
required super.availableShares,
required super.availableCash,
required super.currentPrice,
required super.burnMultiplier,
required super.effectiveShares,
required super.displayAssetValue,
required super.assetGrowthPerSecond,
required super.totalBought,
required super.totalSold,
});
factory AssetDisplayModel.fromJson(Map<String, dynamic> json) {
return AssetDisplayModel(
shareBalance: json['shareBalance']?.toString() ?? '0',
cashBalance: json['cashBalance']?.toString() ?? '0',
frozenShares: json['frozenShares']?.toString() ?? '0',
frozenCash: json['frozenCash']?.toString() ?? '0',
availableShares: json['availableShares']?.toString() ?? '0',
availableCash: json['availableCash']?.toString() ?? '0',
currentPrice: json['currentPrice']?.toString() ?? '0',
burnMultiplier: json['burnMultiplier']?.toString() ?? '0',
effectiveShares: json['effectiveShares']?.toString() ?? '0',
displayAssetValue: json['displayAssetValue']?.toString() ?? '0',
assetGrowthPerSecond: json['assetGrowthPerSecond']?.toString() ?? '0',
totalBought: json['totalBought']?.toString() ?? '0',
totalSold: json['totalSold']?.toString() ?? '0',
);
}
Map<String, dynamic> toJson() {
return {
'shareBalance': shareBalance,
'cashBalance': cashBalance,
'frozenShares': frozenShares,
'frozenCash': frozenCash,
'availableShares': availableShares,
'availableCash': availableCash,
'currentPrice': currentPrice,
'burnMultiplier': burnMultiplier,
'effectiveShares': effectiveShares,
'displayAssetValue': displayAssetValue,
'assetGrowthPerSecond': assetGrowthPerSecond,
'totalBought': totalBought,
'totalSold': totalSold,
};
}
}

View File

@ -2,32 +2,51 @@ import '../../domain/entities/contribution.dart';
class ContributionModel extends Contribution {
const ContributionModel({
required super.status,
required super.message,
required super.accountSequence,
required super.personalContribution,
required super.systemContribution,
required super.teamLevelContribution,
required super.teamBonusContribution,
required super.totalContribution,
required super.effectiveContribution,
required super.hasAdopted,
required super.directReferralAdoptedCount,
required super.unlockedLevelDepth,
required super.unlockedBonusTiers,
required super.isCalculated,
super.lastCalculatedAt,
});
factory ContributionModel.fromJson(Map<String, dynamic> json) {
return ContributionModel(
status: _parseStatus(json['status']),
message: json['message']?.toString() ?? '',
accountSequence: json['accountSequence']?.toString() ?? '',
personalContribution: json['personalContribution']?.toString() ?? '0',
systemContribution: json['systemContribution']?.toString() ?? '0',
teamLevelContribution: json['teamLevelContribution']?.toString() ?? '0',
teamBonusContribution: json['teamBonusContribution']?.toString() ?? '0',
totalContribution: json['totalContribution']?.toString() ?? '0',
effectiveContribution: json['effectiveContribution']?.toString() ?? '0',
hasAdopted: json['hasAdopted'] == true,
directReferralAdoptedCount: json['directReferralAdoptedCount'] ?? 0,
unlockedLevelDepth: json['unlockedLevelDepth'] ?? 0,
unlockedBonusTiers: json['unlockedBonusTiers'] ?? 0,
isCalculated: json['isCalculated'] == true,
lastCalculatedAt: json['lastCalculatedAt'] != null
? DateTime.tryParse(json['lastCalculatedAt'].toString())
: null,
);
}
static ContributionAccountStatus _parseStatus(String? status) {
switch (status) {
case 'ACTIVE':
return ContributionAccountStatus.active;
case 'INACTIVE':
return ContributionAccountStatus.inactive;
case 'USER_NOT_FOUND':
return ContributionAccountStatus.userNotFound;
default:
return ContributionAccountStatus.inactive;
}
}
}

View File

@ -0,0 +1,29 @@
import '../../domain/entities/market_overview.dart';
class MarketOverviewModel extends MarketOverview {
const MarketOverviewModel({
required super.price,
required super.greenPoints,
required super.blackHoleAmount,
required super.circulationPool,
required super.effectiveDenominator,
required super.burnMultiplier,
required super.totalShares,
required super.burnTarget,
required super.burnProgress,
});
factory MarketOverviewModel.fromJson(Map<String, dynamic> json) {
return MarketOverviewModel(
price: json['price']?.toString() ?? '0',
greenPoints: json['greenPoints']?.toString() ?? '0',
blackHoleAmount: json['blackHoleAmount']?.toString() ?? '0',
circulationPool: json['circulationPool']?.toString() ?? '0',
effectiveDenominator: json['effectiveDenominator']?.toString() ?? '0',
burnMultiplier: json['burnMultiplier']?.toString() ?? '0',
totalShares: json['totalShares']?.toString() ?? '0',
burnTarget: json['burnTarget']?.toString() ?? '0',
burnProgress: json['burnProgress']?.toString() ?? '0',
);
}
}

View File

@ -0,0 +1,29 @@
import '../../domain/entities/price_info.dart';
class PriceInfoModel extends PriceInfo {
const PriceInfoModel({
required super.price,
required super.greenPoints,
required super.blackHoleAmount,
required super.circulationPool,
required super.effectiveDenominator,
required super.burnMultiplier,
required super.minuteBurnRate,
required super.snapshotTime,
});
factory PriceInfoModel.fromJson(Map<String, dynamic> json) {
return PriceInfoModel(
price: json['price']?.toString() ?? '0',
greenPoints: json['greenPoints']?.toString() ?? '0',
blackHoleAmount: json['blackHoleAmount']?.toString() ?? '0',
circulationPool: json['circulationPool']?.toString() ?? '0',
effectiveDenominator: json['effectiveDenominator']?.toString() ?? '0',
burnMultiplier: json['burnMultiplier']?.toString() ?? '0',
minuteBurnRate: json['minuteBurnRate']?.toString() ?? '0',
snapshotTime: json['snapshotTime'] != null
? DateTime.parse(json['snapshotTime'].toString())
: DateTime.now(),
);
}
}

View File

@ -3,36 +3,55 @@ import '../../domain/entities/trade_order.dart';
class TradeOrderModel extends TradeOrder {
const TradeOrderModel({
required super.id,
required super.accountSequence,
required super.orderNo,
required super.orderType,
required super.status,
required super.price,
required super.quantity,
required super.filledQuantity,
required super.remainingQuantity,
required super.averagePrice,
required super.totalAmount,
required super.createdAt,
super.updatedAt,
super.completedAt,
super.cancelledAt,
});
factory TradeOrderModel.fromJson(Map<String, dynamic> json) {
return TradeOrderModel(
id: json['id'] ?? '',
accountSequence: json['accountSequence']?.toString() ?? '',
orderType: json['orderType'] == 'BUY' ? OrderType.buy : OrderType.sell,
id: json['id']?.toString() ?? '',
orderNo: json['orderNo']?.toString() ?? '',
orderType: _parseOrderType(json['type']),
status: _parseStatus(json['status']),
price: json['price']?.toString() ?? '0',
quantity: json['quantity']?.toString() ?? '0',
filledQuantity: json['filledQuantity']?.toString() ?? '0',
remainingQuantity: json['remainingQuantity']?.toString() ?? '0',
averagePrice: json['averagePrice']?.toString() ?? '0',
totalAmount: json['totalAmount']?.toString() ?? '0',
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'])
? DateTime.parse(json['createdAt'].toString())
: DateTime.now(),
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'])
completedAt: json['completedAt'] != null
? DateTime.parse(json['completedAt'].toString())
: null,
cancelledAt: json['cancelledAt'] != null
? DateTime.parse(json['cancelledAt'].toString())
: null,
);
}
static OrderType _parseOrderType(String? type) {
switch (type) {
case 'BUY':
return OrderType.buy;
case 'SELL':
return OrderType.sell;
default:
return OrderType.buy;
}
}
static OrderStatus _parseStatus(String? status) {
switch (status) {
case 'PENDING':
@ -48,3 +67,22 @@ class TradeOrderModel extends TradeOrder {
}
}
}
///
class OrdersPageModel {
final List<TradeOrderModel> data;
final int total;
const OrdersPageModel({
required this.data,
required this.total,
});
factory OrdersPageModel.fromJson(Map<String, dynamic> json) {
final dataList = (json['data'] as List<dynamic>?) ?? [];
return OrdersPageModel(
data: dataList.map((e) => TradeOrderModel.fromJson(e)).toList(),
total: json['total'] ?? 0,
);
}
}

View File

@ -0,0 +1,29 @@
import '../../domain/entities/trading_account.dart';
class TradingAccountModel extends TradingAccount {
const TradingAccountModel({
required super.accountSequence,
required super.shareBalance,
required super.cashBalance,
required super.availableShares,
required super.availableCash,
required super.frozenShares,
required super.frozenCash,
required super.totalBought,
required super.totalSold,
});
factory TradingAccountModel.fromJson(Map<String, dynamic> json) {
return TradingAccountModel(
accountSequence: json['accountSequence']?.toString() ?? '',
shareBalance: json['shareBalance']?.toString() ?? '0',
cashBalance: json['cashBalance']?.toString() ?? '0',
availableShares: json['availableShares']?.toString() ?? '0',
availableCash: json['availableCash']?.toString() ?? '0',
frozenShares: json['frozenShares']?.toString() ?? '0',
frozenCash: json['frozenCash']?.toString() ?? '0',
totalBought: json['totalBought']?.toString() ?? '0',
totalSold: json['totalSold']?.toString() ?? '0',
);
}
}

View File

@ -1,10 +1,13 @@
import 'package:dartz/dartz.dart';
import '../../domain/entities/trade_order.dart';
import '../../domain/entities/kline.dart';
import '../../domain/entities/price_info.dart';
import '../../domain/entities/market_overview.dart';
import '../../domain/entities/trading_account.dart';
import '../../domain/entities/asset_display.dart';
import '../../domain/repositories/trading_repository.dart';
import '../../core/error/exceptions.dart';
import '../../core/error/failures.dart';
import '../datasources/remote/trading_remote_datasource.dart';
import '../models/trade_order_model.dart';
class TradingRepositoryImpl implements TradingRepository {
final TradingRemoteDataSource remoteDataSource;
@ -12,7 +15,7 @@ class TradingRepositoryImpl implements TradingRepository {
TradingRepositoryImpl({required this.remoteDataSource});
@override
Future<Either<Failure, String>> getCurrentPrice() async {
Future<Either<Failure, PriceInfo>> getCurrentPrice() async {
try {
final result = await remoteDataSource.getCurrentPrice();
return Right(result);
@ -24,9 +27,9 @@ class TradingRepositoryImpl implements TradingRepository {
}
@override
Future<Either<Failure, List<Kline>>> getKlineData(String period) async {
Future<Either<Failure, MarketOverview>> getMarketOverview() async {
try {
final result = await remoteDataSource.getKlineData(period);
final result = await remoteDataSource.getMarketOverview();
return Right(result);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
@ -36,14 +39,28 @@ class TradingRepositoryImpl implements TradingRepository {
}
@override
Future<Either<Failure, TradeOrder>> buyShares({
required String accountSequence,
required String amount,
Future<Either<Failure, TradingAccount>> getTradingAccount(String accountSequence) async {
try {
final result = await remoteDataSource.getTradingAccount(accountSequence);
return Right(result);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException {
return Left(const NetworkFailure());
}
}
@override
Future<Either<Failure, Map<String, dynamic>>> createOrder({
required String type,
required String price,
required String quantity,
}) async {
try {
final result = await remoteDataSource.buyShares(
accountSequence: accountSequence,
amount: amount,
final result = await remoteDataSource.createOrder(
type: type,
price: price,
quantity: quantity,
);
return Right(result);
} on ServerException catch (e) {
@ -54,55 +71,9 @@ class TradingRepositoryImpl implements TradingRepository {
}
@override
Future<Either<Failure, TradeOrder>> sellShares({
required String accountSequence,
required String amount,
}) async {
Future<Either<Failure, void>> cancelOrder(String orderNo) async {
try {
final result = await remoteDataSource.sellShares(
accountSequence: accountSequence,
amount: amount,
);
return Right(result);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException {
return Left(const NetworkFailure());
}
}
@override
Future<Either<Failure, List<TradeOrder>>> getOrders(
String accountSequence, {
int page = 1,
int limit = 20,
}) async {
try {
final result = await remoteDataSource.getOrders(
accountSequence,
page: page,
limit: limit,
);
return Right(result);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException {
return Left(const NetworkFailure());
}
}
@override
Future<Either<Failure, void>> transfer({
required String accountSequence,
required String amount,
required String direction,
}) async {
try {
await remoteDataSource.transfer(
accountSequence: accountSequence,
amount: amount,
direction: direction,
);
await remoteDataSource.cancelOrder(orderNo);
return const Right(null);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
@ -110,4 +81,82 @@ class TradingRepositoryImpl implements TradingRepository {
return Left(const NetworkFailure());
}
}
@override
Future<Either<Failure, OrdersPageModel>> getOrders({
int page = 1,
int pageSize = 50,
}) async {
try {
final result = await remoteDataSource.getOrders(
page: page,
pageSize: pageSize,
);
return Right(result);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException {
return Left(const NetworkFailure());
}
}
@override
Future<Either<Failure, Map<String, dynamic>>> estimateSell(String quantity) async {
try {
final result = await remoteDataSource.estimateSell(quantity);
return Right(result);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException {
return Left(const NetworkFailure());
}
}
@override
Future<Either<Failure, Map<String, dynamic>>> transferIn(String amount) async {
try {
final result = await remoteDataSource.transferIn(amount);
return Right(result);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException {
return Left(const NetworkFailure());
}
}
@override
Future<Either<Failure, Map<String, dynamic>>> transferOut(String amount) async {
try {
final result = await remoteDataSource.transferOut(amount);
return Right(result);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException {
return Left(const NetworkFailure());
}
}
@override
Future<Either<Failure, AssetDisplay>> getMyAsset({String? dailyAllocation}) async {
try {
final result = await remoteDataSource.getMyAsset(dailyAllocation: dailyAllocation);
return Right(result);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException {
return Left(const NetworkFailure());
}
}
@override
Future<Either<Failure, AssetDisplay>> getAccountAsset(String accountSequence, {String? dailyAllocation}) async {
try {
final result = await remoteDataSource.getAccountAsset(accountSequence, dailyAllocation: dailyAllocation);
return Right(result);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException {
return Left(const NetworkFailure());
}
}
}

View File

@ -0,0 +1,91 @@
import 'package:equatable/equatable.dart';
///
/// trading-service /asset/my /asset/account/:accountSequence
class AssetDisplay extends Equatable {
///
final String shareBalance;
///
final String cashBalance;
///
final String frozenShares;
///
final String frozenCash;
///
final String availableShares;
///
final String availableCash;
///
final String currentPrice;
///
final String burnMultiplier;
///
final String effectiveShares;
/// = ( + × ) ×
final String displayAssetValue;
///
final String assetGrowthPerSecond;
///
final String totalBought;
///
final String totalSold;
const AssetDisplay({
required this.shareBalance,
required this.cashBalance,
required this.frozenShares,
required this.frozenCash,
required this.availableShares,
required this.availableCash,
required this.currentPrice,
required this.burnMultiplier,
required this.effectiveShares,
required this.displayAssetValue,
required this.assetGrowthPerSecond,
required this.totalBought,
required this.totalSold,
});
/// = +
String get totalShareBalance {
final available = double.tryParse(availableShares) ?? 0;
final frozen = double.tryParse(frozenShares) ?? 0;
return (available + frozen).toString();
}
/// = +
String get totalCashBalance {
final available = double.tryParse(availableCash) ?? 0;
final frozen = double.tryParse(frozenCash) ?? 0;
return (available + frozen).toString();
}
@override
List<Object?> get props => [
shareBalance,
cashBalance,
frozenShares,
frozenCash,
availableShares,
availableCash,
currentPrice,
burnMultiplier,
effectiveShares,
displayAssetValue,
assetGrowthPerSecond,
totalBought,
totalSold,
];
}

View File

@ -1,41 +1,83 @@
import 'package:equatable/equatable.dart';
///
enum ContributionAccountStatus {
/// -
active,
/// -
inactive,
///
userNotFound,
}
class Contribution extends Equatable {
///
final ContributionAccountStatus status;
///
final String message;
///
final String accountSequence;
///
final String personalContribution;
final String systemContribution;
///
final String teamLevelContribution;
///
final String teamBonusContribution;
///
final String totalContribution;
final String effectiveContribution;
///
final bool hasAdopted;
///
final int directReferralAdoptedCount;
///
final int unlockedLevelDepth;
///
final int unlockedBonusTiers;
///
final bool isCalculated;
///
final DateTime? lastCalculatedAt;
const Contribution({
required this.status,
required this.message,
required this.accountSequence,
required this.personalContribution,
required this.systemContribution,
required this.teamLevelContribution,
required this.teamBonusContribution,
required this.totalContribution,
required this.effectiveContribution,
required this.hasAdopted,
required this.directReferralAdoptedCount,
required this.unlockedLevelDepth,
required this.unlockedBonusTiers,
required this.isCalculated,
this.lastCalculatedAt,
});
/// ( + )
String get teamContribution {
final teamLevel = double.tryParse(teamLevelContribution) ?? 0;
final teamBonus = double.tryParse(teamBonusContribution) ?? 0;
return (teamLevel + teamBonus).toString();
}
///
bool get isActive => status == ContributionAccountStatus.active;
@override
List<Object?> get props => [
status,
message,
accountSequence,
personalContribution,
systemContribution,
teamLevelContribution,
teamBonusContribution,
totalContribution,
effectiveContribution,
hasAdopted,
directReferralAdoptedCount,
unlockedLevelDepth,
unlockedBonusTiers,
isCalculated,
lastCalculatedAt,
];
}

View File

@ -0,0 +1,48 @@
import 'package:equatable/equatable.dart';
///
class MarketOverview extends Equatable {
///
final String price;
/// 绿
final String greenPoints;
///
final String blackHoleAmount;
///
final String circulationPool;
///
final String effectiveDenominator;
///
final String burnMultiplier;
///
final String totalShares;
///
final String burnTarget;
///
final String burnProgress;
const MarketOverview({
required this.price,
required this.greenPoints,
required this.blackHoleAmount,
required this.circulationPool,
required this.effectiveDenominator,
required this.burnMultiplier,
required this.totalShares,
required this.burnTarget,
required this.burnProgress,
});
@override
List<Object?> get props => [
price,
greenPoints,
blackHoleAmount,
circulationPool,
effectiveDenominator,
burnMultiplier,
totalShares,
burnTarget,
burnProgress,
];
}

View File

@ -0,0 +1,44 @@
import 'package:equatable/equatable.dart';
///
class PriceInfo extends Equatable {
///
final String price;
/// 绿
final String greenPoints;
///
final String blackHoleAmount;
///
final String circulationPool;
///
final String effectiveDenominator;
///
final String burnMultiplier;
///
final String minuteBurnRate;
///
final DateTime snapshotTime;
const PriceInfo({
required this.price,
required this.greenPoints,
required this.blackHoleAmount,
required this.circulationPool,
required this.effectiveDenominator,
required this.burnMultiplier,
required this.minuteBurnRate,
required this.snapshotTime,
});
@override
List<Object?> get props => [
price,
greenPoints,
blackHoleAmount,
circulationPool,
effectiveDenominator,
burnMultiplier,
minuteBurnRate,
snapshotTime,
];
}

View File

@ -4,41 +4,64 @@ enum OrderType { buy, sell }
enum OrderStatus { pending, partial, filled, cancelled }
class TradeOrder extends Equatable {
/// ID
final String id;
final String accountSequence;
///
final String orderNo;
///
final OrderType orderType;
///
final OrderStatus status;
///
final String price;
///
final String quantity;
///
final String filledQuantity;
///
final String remainingQuantity;
///
final String averagePrice;
///
final String totalAmount;
///
final DateTime createdAt;
final DateTime? updatedAt;
///
final DateTime? completedAt;
///
final DateTime? cancelledAt;
const TradeOrder({
required this.id,
required this.accountSequence,
required this.orderNo,
required this.orderType,
required this.status,
required this.price,
required this.quantity,
required this.filledQuantity,
required this.remainingQuantity,
required this.averagePrice,
required this.totalAmount,
required this.createdAt,
this.updatedAt,
this.completedAt,
this.cancelledAt,
});
bool get isBuy => orderType == OrderType.buy;
bool get isSell => orderType == OrderType.sell;
bool get isPending => status == OrderStatus.pending;
bool get isPartial => status == OrderStatus.partial;
bool get isFilled => status == OrderStatus.filled;
bool get isCancelled => status == OrderStatus.cancelled;
String get remainingQuantity {
///
double get fillProgress {
final qty = double.tryParse(quantity) ?? 0;
final filled = double.tryParse(filledQuantity) ?? 0;
return (qty - filled).toString();
if (qty == 0) return 0;
return filled / qty;
}
@override
List<Object?> get props => [id, accountSequence, orderType, status, price, quantity];
List<Object?> get props => [id, orderNo, orderType, status, price, quantity];
}

View File

@ -0,0 +1,48 @@
import 'package:equatable/equatable.dart';
///
class TradingAccount extends Equatable {
///
final String accountSequence;
///
final String shareBalance;
/// (绿)
final String cashBalance;
///
final String availableShares;
///
final String availableCash;
///
final String frozenShares;
///
final String frozenCash;
///
final String totalBought;
///
final String totalSold;
const TradingAccount({
required this.accountSequence,
required this.shareBalance,
required this.cashBalance,
required this.availableShares,
required this.availableCash,
required this.frozenShares,
required this.frozenCash,
required this.totalBought,
required this.totalSold,
});
@override
List<Object?> get props => [
accountSequence,
shareBalance,
cashBalance,
availableShares,
availableCash,
frozenShares,
frozenCash,
totalBought,
totalSold,
];
}

View File

@ -1,32 +1,50 @@
import 'package:dartz/dartz.dart';
import '../../core/error/failures.dart';
import '../entities/price_info.dart';
import '../entities/market_overview.dart';
import '../entities/trading_account.dart';
import '../entities/trade_order.dart';
import '../entities/kline.dart';
import '../entities/asset_display.dart';
import '../../data/models/trade_order_model.dart';
abstract class TradingRepository {
Future<Either<Failure, String>> getCurrentPrice();
///
Future<Either<Failure, PriceInfo>> getCurrentPrice();
Future<Either<Failure, List<Kline>>> getKlineData(String period);
///
Future<Either<Failure, MarketOverview>> getMarketOverview();
Future<Either<Failure, TradeOrder>> buyShares({
required String accountSequence,
required String amount,
///
Future<Either<Failure, TradingAccount>> getTradingAccount(String accountSequence);
///
Future<Either<Failure, Map<String, dynamic>>> createOrder({
required String type,
required String price,
required String quantity,
});
Future<Either<Failure, TradeOrder>> sellShares({
required String accountSequence,
required String amount,
});
///
Future<Either<Failure, void>> cancelOrder(String orderNo);
Future<Either<Failure, List<TradeOrder>>> getOrders(
String accountSequence, {
///
Future<Either<Failure, OrdersPageModel>> getOrders({
int page = 1,
int limit = 20,
int pageSize = 50,
});
Future<Either<Failure, void>> transfer({
required String accountSequence,
required String amount,
required String direction,
});
///
Future<Either<Failure, Map<String, dynamic>>> estimateSell(String quantity);
/// ()
Future<Either<Failure, Map<String, dynamic>>> transferIn(String amount);
/// ()
Future<Either<Failure, Map<String, dynamic>>> transferOut(String amount);
///
Future<Either<Failure, AssetDisplay>> getMyAsset({String? dailyAllocation});
///
Future<Either<Failure, AssetDisplay>> getAccountAsset(String accountSequence, {String? dailyAllocation});
}

View File

@ -1,24 +0,0 @@
import 'package:dartz/dartz.dart';
import '../../../core/error/failures.dart';
import '../../entities/trade_order.dart';
import '../../repositories/trading_repository.dart';
class BuySharesParams {
final String accountSequence;
final String amount;
BuySharesParams({required this.accountSequence, required this.amount});
}
class BuyShares {
final TradingRepository repository;
BuyShares(this.repository);
Future<Either<Failure, TradeOrder>> call(BuySharesParams params) async {
return await repository.buyShares(
accountSequence: params.accountSequence,
amount: params.amount,
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:dartz/dartz.dart';
import '../../../core/error/failures.dart';
import '../../entities/price_info.dart';
import '../../repositories/trading_repository.dart';
class GetCurrentPrice {
@ -7,7 +8,7 @@ class GetCurrentPrice {
GetCurrentPrice(this.repository);
Future<Either<Failure, String>> call() async {
Future<Either<Failure, PriceInfo>> call() async {
return await repository.getCurrentPrice();
}
}

View File

@ -1,24 +0,0 @@
import 'package:dartz/dartz.dart';
import '../../../core/error/failures.dart';
import '../../entities/trade_order.dart';
import '../../repositories/trading_repository.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,
);
}
}

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/utils/format_utils.dart';
import '../../../domain/entities/asset_display.dart';
import '../../providers/user_providers.dart';
import '../../providers/mining_providers.dart';
import '../../providers/asset_providers.dart';
import '../../widgets/shimmer_loading.dart';
class AssetPage extends ConsumerWidget {
@ -23,12 +24,11 @@ class AssetPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userNotifierProvider);
final accountSequence = user.accountSequence ?? '';
final accountAsync = ref.watch(shareAccountProvider(accountSequence));
final assetAsync = ref.watch(myAssetProvider);
//
final isLoading = accountAsync.isLoading;
final account = accountAsync.valueOrNull;
final isLoading = assetAsync.isLoading;
final asset = assetAsync.valueOrNull;
return Scaffold(
backgroundColor: Colors.white,
@ -38,7 +38,7 @@ class AssetPage extends ConsumerWidget {
builder: (context, constraints) {
return RefreshIndicator(
onRefresh: () async {
ref.invalidate(shareAccountProvider(accountSequence));
ref.invalidate(myAssetProvider);
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
@ -55,19 +55,19 @@ class AssetPage extends ConsumerWidget {
children: [
const SizedBox(height: 8),
// -
_buildTotalAssetCard(account, isLoading),
_buildTotalAssetCard(asset, isLoading),
const SizedBox(height: 24),
//
_buildQuickActions(),
const SizedBox(height: 24),
// -
_buildAssetList(account, isLoading),
_buildAssetList(asset, isLoading),
const SizedBox(height: 24),
//
_buildEarningsCard(account, isLoading),
//
_buildEarningsCard(asset, isLoading),
const SizedBox(height: 24),
//
_buildAccountList(account, isLoading),
_buildAccountList(asset, isLoading),
const SizedBox(height: 100),
],
),
@ -181,7 +181,12 @@ class AssetPage extends ConsumerWidget {
);
}
Widget _buildTotalAssetCard(account, bool isLoading) {
Widget _buildTotalAssetCard(AssetDisplay? asset, bool isLoading) {
//
final growthPerSecond = asset != null
? AssetValueCalculator.calculateGrowthPerSecond(asset.assetGrowthPerSecond)
: 0.0;
return Container(
decoration: BoxDecoration(
color: Colors.white,
@ -254,7 +259,7 @@ class AssetPage extends ConsumerWidget {
const SizedBox(height: 8),
// -
AmountText(
amount: account != null ? formatAmount(account.tradingBalance ?? '0') : null,
amount: asset != null ? formatAmount(asset.displayAssetValue) : null,
isLoading: isLoading,
prefix: '¥ ',
style: const TextStyle(
@ -265,18 +270,18 @@ class AssetPage extends ConsumerWidget {
),
),
const SizedBox(height: 4),
// USDT估值
//
DataText(
data: account != null ? '≈ 12,345.67 USDT' : null,
data: asset != null ? '${formatCompact(asset.effectiveShares)} 积分股 (含倍数)' : null,
isLoading: isLoading,
placeholder: '≈ -- USDT',
placeholder: '≈ -- 积分股',
style: const TextStyle(
fontSize: 14,
color: Color(0xFF9CA3AF),
),
),
const SizedBox(height: 12),
//
//
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
@ -286,12 +291,14 @@ class AssetPage extends ConsumerWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.trending_up, size: 14, color: _green),
const Icon(Icons.bolt, size: 14, color: _green),
const SizedBox(width: 6),
DataText(
data: account != null ? '+¥ 156.78 今日' : null,
data: asset != null
? '+${formatDecimal(growthPerSecond.toString(), 8)}/秒'
: null,
isLoading: isLoading,
placeholder: '+¥ -- 今日',
placeholder: '+--/秒',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
@ -346,7 +353,12 @@ class AssetPage extends ConsumerWidget {
);
}
Widget _buildAssetList(account, bool isLoading) {
Widget _buildAssetList(AssetDisplay? asset, bool isLoading) {
//
final shareBalance = double.tryParse(asset?.shareBalance ?? '0') ?? 0;
final multiplier = double.tryParse(asset?.burnMultiplier ?? '0') ?? 0;
final multipliedAsset = shareBalance * multiplier;
return Column(
children: [
//
@ -355,41 +367,49 @@ class AssetPage extends ConsumerWidget {
iconColor: _orange,
iconBgColor: _serenade,
title: '积分股',
amount: account?.miningBalance,
amount: asset?.shareBalance,
isLoading: isLoading,
valueInCny: '¥15,234.56',
tag: '含倍数资产: 246,913.56',
growthText: '每秒 +0.0015',
valueInCny: asset != null
? '¥${formatAmount(_calculateValue(asset.shareBalance, asset.currentPrice))}'
: null,
tag: asset != null ? '含倍数资产: ${formatCompact(multipliedAsset.toString())}' : null,
growthText: asset != null ? '每秒 +${formatDecimal(asset.assetGrowthPerSecond, 8)}' : null,
),
const SizedBox(height: 16),
// 绿
// 绿
_buildAssetItem(
icon: Icons.eco,
iconColor: _green,
iconBgColor: _feta,
title: '绿积分',
amount: account?.tradingBalance,
amount: asset?.cashBalance,
isLoading: isLoading,
valueInCny: '¥10,986.54',
valueInCny: asset != null ? '¥${formatAmount(asset.cashBalance)}' : null,
badge: '可提现',
badgeColor: _jewel,
badgeBgColor: _scandal,
),
const SizedBox(height: 16),
//
//
_buildAssetItem(
icon: Icons.hourglass_empty,
icon: Icons.lock_outline,
iconColor: _orange,
iconBgColor: _serenade,
title: '待分配积分股',
amount: '1,234.56',
title: '冻结积分股',
amount: asset?.frozenShares,
isLoading: isLoading,
subtitle: '次日开始参与分配',
subtitle: '交易挂单中',
),
],
);
}
String _calculateValue(String balance, String price) {
final b = double.tryParse(balance) ?? 0;
final p = double.tryParse(price) ?? 0;
return (b * p).toString();
}
Widget _buildAssetItem({
required IconData icon,
required Color iconColor,
@ -555,7 +575,7 @@ class AssetPage extends ConsumerWidget {
);
}
Widget _buildEarningsCard(account, bool isLoading) {
Widget _buildEarningsCard(AssetDisplay? asset, bool isLoading) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@ -584,7 +604,7 @@ class AssetPage extends ConsumerWidget {
),
const SizedBox(width: 8),
const Text(
'收益统计',
'交易统计',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@ -597,19 +617,34 @@ class AssetPage extends ConsumerWidget {
//
Row(
children: [
_buildEarningsItem('累计收益', isLoading ? null : '12,345.67', _orange, isLoading),
_buildEarningsItem(
'累计买入',
asset != null ? formatCompact(asset.totalBought) : null,
_orange,
isLoading,
),
Container(
width: 1,
height: 40,
color: _serenade,
),
_buildEarningsItem('今日收益', isLoading ? null : '+156.78', _green, isLoading),
_buildEarningsItem(
'累计卖出',
asset != null ? formatCompact(asset.totalSold) : null,
_green,
isLoading,
),
Container(
width: 1,
height: 40,
color: _serenade,
),
_buildEarningsItem('昨日收益', isLoading ? null : '143.21', const Color(0xFF9CA3AF), isLoading),
_buildEarningsItem(
'销毁倍数',
asset != null ? '${formatDecimal(asset.burnMultiplier, 4)}x' : null,
const Color(0xFF9CA3AF),
isLoading,
),
],
),
],
@ -646,31 +681,31 @@ class AssetPage extends ConsumerWidget {
);
}
Widget _buildAccountList(account, bool isLoading) {
Widget _buildAccountList(AssetDisplay? asset, bool isLoading) {
return Column(
children: [
//
//
_buildAccountItem(
icon: Icons.account_balance_wallet,
iconColor: _orange,
title: '交易账户',
balance: account?.tradingBalance,
title: '可用绿积分',
balance: asset?.availableCash,
isLoading: isLoading,
unit: '绿积分',
status: '正常',
status: '可交易',
statusColor: _green,
statusBgColor: _feta,
),
const SizedBox(height: 16),
//
//
_buildAccountItem(
icon: Icons.savings,
icon: Icons.lock_outline,
iconColor: _orange,
title: '提现账户',
balance: '1,234.56',
title: '冻结绿积分',
balance: asset?.frozenCash,
isLoading: isLoading,
unit: '绿积分',
status: '已绑定',
status: '挂单中',
statusColor: const Color(0xFF9CA3AF),
statusBgColor: Colors.white,
statusBorder: true,
@ -734,7 +769,7 @@ class AssetPage extends ConsumerWidget {
Row(
children: [
DataText(
data: balance,
data: balance != null ? formatAmount(balance) : null,
isLoading: isLoading,
placeholder: '--',
style: const TextStyle(
@ -778,5 +813,4 @@ class AssetPage extends ConsumerWidget {
),
);
}
}

View File

@ -159,7 +159,7 @@ class ContributionPage extends ConsumerWidget {
}
Widget _buildTotalContributionCard(Contribution? contribution, bool isLoading) {
final total = contribution?.effectiveContribution ?? '0';
final total = contribution?.totalContribution ?? '0';
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@ -226,8 +226,8 @@ class ContributionPage extends ConsumerWidget {
child: Row(
children: [
_buildStatColumn('个人贡献值', contribution?.personalContribution, isLoading, false),
_buildStatColumn('团队贡献值', contribution?.teamLevelContribution, isLoading, true),
_buildStatColumn('省市公司', contribution?.systemContribution, isLoading, true),
_buildStatColumn('团队层级', contribution?.teamLevelContribution, isLoading, true),
_buildStatColumn('团队奖励', contribution?.teamBonusContribution, isLoading, true),
],
),
);
@ -261,10 +261,10 @@ class ContributionPage extends ConsumerWidget {
Widget _buildTodayEstimateCard(Contribution? contribution, bool isLoading) {
// API
final effectiveContribution = double.tryParse(contribution?.effectiveContribution ?? '0') ?? 0;
final totalContribution = double.tryParse(contribution?.totalContribution ?? '0') ?? 0;
// 10000
// "--"
final hasContribution = effectiveContribution > 0;
final hasContribution = totalContribution > 0;
return Container(
padding: const EdgeInsets.all(20),

View File

@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/router/routes.dart';
import '../../providers/user_providers.dart';
import '../../providers/profile_providers.dart';
import '../../widgets/shimmer_loading.dart';
class ProfilePage extends ConsumerWidget {
const ProfilePage({super.key});
@ -20,72 +22,84 @@ class ProfilePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userNotifierProvider);
final statsAsync = ref.watch(userStatsProvider);
final invitationCode = ref.watch(invitationCodeProvider);
final isStatsLoading = statsAsync.isLoading;
final stats = statsAsync.valueOrNull;
return Scaffold(
backgroundColor: _bgGray,
body: SafeArea(
bottom: false,
child: SingleChildScrollView(
child: Column(
children: [
//
_buildUserHeader(context, user),
child: RefreshIndicator(
onRefresh: () async {
ref.invalidate(userStatsProvider);
await ref.read(userNotifierProvider.notifier).fetchProfile();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
//
_buildUserHeader(context, user),
const SizedBox(height: 16),
const SizedBox(height: 16),
//
_buildStatsRow(),
//
_buildStatsRow(stats, isStatsLoading),
const SizedBox(height: 16),
const SizedBox(height: 16),
//
_buildInvitationCard(context),
//
_buildInvitationCard(context, invitationCode),
const SizedBox(height: 16),
const SizedBox(height: 16),
//
_buildAccountSettings(context),
//
_buildAccountSettings(context, user),
const SizedBox(height: 16),
const SizedBox(height: 16),
//
_buildRecordsSection(context),
//
_buildRecordsSection(context),
const SizedBox(height: 16),
const SizedBox(height: 16),
//
_buildTeamEarningsSection(context),
//
_buildTeamEarningsSection(context),
const SizedBox(height: 16),
const SizedBox(height: 16),
//
_buildOtherSettings(context),
//
_buildOtherSettings(context),
const SizedBox(height: 24),
const SizedBox(height: 24),
// 退
_buildLogoutButton(context, ref),
// 退
_buildLogoutButton(context, ref),
const SizedBox(height: 16),
const SizedBox(height: 16),
//
const Text(
'Version 1.0.0',
style: TextStyle(
fontSize: 12,
color: _lightGray,
//
const Text(
'Version 1.0.0',
style: TextStyle(
fontSize: 12,
color: _lightGray,
),
),
),
const SizedBox(height: 100),
],
const SizedBox(height: 100),
],
),
),
),
),
);
}
Widget _buildUserHeader(BuildContext context, dynamic user) {
Widget _buildUserHeader(BuildContext context, UserState user) {
return Container(
padding: const EdgeInsets.all(20),
color: Colors.white,
@ -107,7 +121,9 @@ class ProfilePage extends ConsumerWidget {
child: Text(
user.nickname?.isNotEmpty == true
? user.nickname!.substring(0, 1).toUpperCase()
: 'U',
: (user.realName?.isNotEmpty == true
? user.realName!.substring(0, 1).toUpperCase()
: 'U'),
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
@ -127,7 +143,7 @@ class ProfilePage extends ConsumerWidget {
Row(
children: [
Text(
user.nickname ?? '榴莲用户',
user.realName ?? user.nickname ?? '榴莲用户',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@ -135,27 +151,33 @@ class ProfilePage extends ConsumerWidget {
),
),
const SizedBox(width: 8),
// VIP
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFFD700), Color(0xFFFF8C00)],
//
if (user.isKycVerified)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
borderRadius: BorderRadius.circular(10),
),
child: const Text(
'VIP 3',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.white,
decoration: BoxDecoration(
color: _green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.verified, size: 12, color: _green),
SizedBox(width: 2),
Text(
'已实名',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: _green,
),
),
],
),
),
),
],
),
const SizedBox(height: 8),
@ -191,6 +213,18 @@ class ProfilePage extends ConsumerWidget {
),
],
),
//
if (user.phone != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'手机: ${user.phone}',
style: const TextStyle(
fontSize: 12,
color: _lightGray,
),
),
),
],
),
),
@ -210,30 +244,48 @@ class ProfilePage extends ConsumerWidget {
);
}
Widget _buildStatsRow() {
Widget _buildStatsRow(UserStats? stats, bool isLoading) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
color: Colors.white,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildStatItem('认种数量', '10'),
_buildStatItem(
'认种状态',
stats?.hasAdopted == true ? '已认种' : '未认种',
isLoading,
),
_buildDivider(),
_buildStatItem('直推人数', '5'),
_buildStatItem(
'直推人数',
stats?.directReferralAdoptedCount.toString() ?? '0',
isLoading,
),
_buildDivider(),
_buildStatItem('团队人数', '128'),
_buildStatItem(
'团队层级',
stats?.unlockedLevelDepth.toString() ?? '0',
isLoading,
),
_buildDivider(),
_buildStatItem('VIP等级', 'V3'),
_buildStatItem(
'VIP等级',
stats?.vipLevel ?? '-',
isLoading,
),
],
),
);
}
Widget _buildStatItem(String label, String value) {
Widget _buildStatItem(String label, String value, bool isLoading) {
return Column(
children: [
Text(
value,
DataText(
data: isLoading ? null : value,
isLoading: isLoading,
placeholder: '--',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@ -260,9 +312,7 @@ class ProfilePage extends ConsumerWidget {
);
}
Widget _buildInvitationCard(BuildContext context) {
const invitationCode = 'DUR8888XYZ';
Widget _buildInvitationCard(BuildContext context, String invitationCode) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16),
@ -293,9 +343,9 @@ class ProfilePage extends ConsumerWidget {
color: _bgGray,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
child: Text(
invitationCode,
style: TextStyle(
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _darkText,
@ -309,7 +359,7 @@ class ProfilePage extends ConsumerWidget {
icon: Icons.copy,
label: '复制',
onTap: () {
Clipboard.setData(const ClipboardData(text: invitationCode));
Clipboard.setData(ClipboardData(text: invitationCode));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('邀请码已复制'),
@ -364,7 +414,7 @@ class ProfilePage extends ConsumerWidget {
);
}
Widget _buildAccountSettings(BuildContext context) {
Widget _buildAccountSettings(BuildContext context, UserState user) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
@ -393,11 +443,11 @@ class ProfilePage extends ConsumerWidget {
_buildSettingItem(
icon: Icons.verified_user,
label: '实名认证',
trailing: const Text(
'认证',
trailing: Text(
user.isKycVerified ? '认证' : '认证',
style: TextStyle(
fontSize: 14,
color: _green,
color: user.isKycVerified ? _green : _lightGray,
),
),
onTap: () {},

View File

@ -1,8 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../../core/constants/app_colors.dart';
import '../../../core/utils/format_utils.dart';
import '../../providers/mining_providers.dart';
import '../../../data/models/trade_order_model.dart';
import '../../../domain/entities/price_info.dart';
import '../../../domain/entities/market_overview.dart';
import '../../../domain/entities/trade_order.dart';
import '../../providers/user_providers.dart';
import '../../providers/trading_providers.dart';
import '../../widgets/shimmer_loading.dart';
@ -28,60 +32,55 @@ class _TradingPageState extends ConsumerState<TradingPage> {
//
int _selectedTab = 1; // 0: , 1:
int _selectedTimeRange = 1; //
final _amountController = TextEditingController();
final _quantityController = TextEditingController();
final _priceController = TextEditingController();
final List<String> _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', ''];
@override
void dispose() {
_amountController.dispose();
_quantityController.dispose();
_priceController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final globalState = ref.watch(globalStateProvider);
final priceAsync = ref.watch(currentPriceProvider);
final marketAsync = ref.watch(marketOverviewProvider);
final ordersAsync = ref.watch(ordersProvider);
final user = ref.watch(userNotifierProvider);
final accountSequence = user.accountSequence ?? '';
// Extract loading state and data using shimmer placeholder approach
final isLoading = globalState.isLoading;
final state = globalState.valueOrNull;
final hasError = globalState.hasError;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
body: SafeArea(
bottom: false,
child: Column(
children: [
//
_buildAppBar(),
//
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
// - always render, use shimmer for loading
hasError
? _buildErrorCard('价格加载失败')
: _buildPriceCard(state, isLoading),
// K线图占位区域
_buildChartSection(),
// - always render, use shimmer for loading
hasError
? _buildErrorCard('市场数据加载失败')
: _buildMarketDataCard(state, isLoading),
// /
_buildTradingPanel(accountSequence),
//
_buildMyOrdersCard(),
const SizedBox(height: 100),
],
child: RefreshIndicator(
onRefresh: () async {
ref.invalidate(currentPriceProvider);
ref.invalidate(marketOverviewProvider);
ref.invalidate(ordersProvider);
},
child: Column(
children: [
_buildAppBar(),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
_buildPriceCard(priceAsync),
_buildChartSection(priceAsync),
_buildMarketDataCard(marketAsync),
_buildTradingPanel(priceAsync),
_buildMyOrdersCard(ordersAsync),
const SizedBox(height: 100),
],
),
),
),
),
],
],
),
),
),
);
@ -89,12 +88,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
Widget _buildAppBar() {
return Container(
color: _bgGray.withOpacity(0.9),
color: _bgGray.withValues(alpha: 0.9),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
//
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
@ -110,7 +108,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
),
),
//
Container(
width: 40,
height: 40,
@ -119,7 +116,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
@ -150,10 +147,17 @@ class _TradingPageState extends ConsumerState<TradingPage> {
);
}
Widget _buildPriceCard(dynamic state, bool isLoading) {
final isPriceUp = state?.isPriceUp ?? true;
final currentPrice = state?.currentPrice;
final priceChange = state?.priceChange24h;
Widget _buildPriceCard(AsyncValue<PriceInfo?> priceAsync) {
final isLoading = priceAsync.isLoading;
final priceInfo = priceAsync.valueOrNull;
final hasError = priceAsync.hasError;
if (hasError && priceInfo == null) {
return _buildErrorCard('价格加载失败');
}
final price = priceInfo?.price ?? '0';
final greenPoints = priceInfo?.greenPoints ?? '0';
return Container(
margin: const EdgeInsets.all(16),
@ -165,11 +169,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
const Text(
'当前积分股价格',
style: TextStyle(
fontSize: 12,
@ -178,23 +181,19 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
),
DataText(
data: '= 156.00 绿积分',
data: priceInfo != null ? '= ${formatCompact(greenPoints)} 绿积分' : null,
isLoading: isLoading,
placeholder: '= -- 绿积分',
style: TextStyle(
fontSize: 12,
color: _grayText,
),
style: const TextStyle(fontSize: 12, color: _grayText),
),
],
),
const SizedBox(height: 8),
//
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AmountText(
amount: currentPrice != null ? formatPrice(currentPrice) : null,
amount: priceInfo != null ? formatPrice(price) : null,
isLoading: isLoading,
prefix: '\u00A5 ',
style: const TextStyle(
@ -208,19 +207,15 @@ class _TradingPageState extends ConsumerState<TradingPage> {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: _green.withOpacity(0.1),
color: _green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isPriceUp ? Icons.trending_up : Icons.trending_down,
size: 16,
color: _green,
),
const Icon(Icons.trending_up, size: 16, color: _green),
DataText(
data: priceChange != null ? '+$priceChange%' : null,
data: isLoading ? null : '+0.00%',
isLoading: isLoading,
placeholder: '+--.--%',
style: const TextStyle(
@ -239,7 +234,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
);
}
Widget _buildChartSection() {
Widget _buildChartSection(AsyncValue<PriceInfo?> priceAsync) {
final priceInfo = priceAsync.valueOrNull;
final currentPrice = priceInfo?.price ?? '0.000000';
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16),
@ -249,7 +247,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
child: Column(
children: [
// K线图占位
Container(
height: 200,
decoration: BoxDecoration(
@ -258,12 +255,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
child: Stack(
children: [
// K线图
CustomPaint(
size: const Size(double.infinity, 200),
painter: _CandlestickPainter(),
),
//
Positioned(
right: 0,
top: 60,
@ -274,18 +269,15 @@ class _TradingPageState extends ConsumerState<TradingPage> {
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: const Text(
'0.000156',
style: TextStyle(
fontSize: 10,
color: Colors.white,
),
child: Text(
formatPrice(currentPrice),
style: const TextStyle(fontSize: 10, color: Colors.white),
),
),
),
@ -293,7 +285,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
),
const SizedBox(height: 16),
//
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
@ -329,11 +320,14 @@ class _TradingPageState extends ConsumerState<TradingPage> {
);
}
Widget _buildMarketDataCard(dynamic state, bool isLoading) {
final sharesPool = state?.sharesPool;
final circulatingPool = state?.circulatingPool;
final greenPointsPool = state?.greenPointsPool;
final blackHoleBurned = state?.blackHoleBurned;
Widget _buildMarketDataCard(AsyncValue<MarketOverview?> marketAsync) {
final isLoading = marketAsync.isLoading;
final market = marketAsync.valueOrNull;
final hasError = marketAsync.hasError;
if (hasError && market == null) {
return _buildErrorCard('市场数据加载失败');
}
return Container(
margin: const EdgeInsets.all(16),
@ -345,7 +339,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Row(
children: [
Container(
@ -368,12 +361,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
],
),
const SizedBox(height: 24),
//
Row(
children: [
_buildMarketDataItem(
'积分股',
sharesPool ?? '8,888,888,888',
'积分股',
market != null ? formatCompact(market.totalShares) : null,
_orange,
isLoading,
),
@ -381,7 +373,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
const SizedBox(width: 16),
_buildMarketDataItem(
'流通池',
circulatingPool ?? '1,234,567',
market != null ? formatCompact(market.circulationPool) : null,
_orange,
isLoading,
),
@ -390,12 +382,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
const SizedBox(height: 24),
Container(height: 1, color: _bgGray),
const SizedBox(height: 24),
//
Row(
children: [
_buildMarketDataItem(
'绿积分池',
greenPointsPool ?? '99,999,999',
market != null ? formatCompact(market.greenPoints) : null,
_orange,
isLoading,
),
@ -403,7 +394,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
const SizedBox(width: 16),
_buildMarketDataItem(
'黑洞销毁量',
blackHoleBurned ?? '50,000,000',
market != null ? formatCompact(market.blackHoleAmount) : null,
_red,
isLoading,
),
@ -414,18 +405,12 @@ class _TradingPageState extends ConsumerState<TradingPage> {
);
}
Widget _buildMarketDataItem(String label, String value, Color valueColor, bool isLoading) {
Widget _buildMarketDataItem(String label, String? value, Color valueColor, bool isLoading) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: _grayText,
),
),
Text(label, style: const TextStyle(fontSize: 12, color: _grayText)),
const SizedBox(height: 4),
DataText(
data: value,
@ -442,7 +427,15 @@ class _TradingPageState extends ConsumerState<TradingPage> {
);
}
Widget _buildTradingPanel(String accountSequence) {
Widget _buildTradingPanel(AsyncValue<PriceInfo?> priceAsync) {
final priceInfo = priceAsync.valueOrNull;
final currentPrice = priceInfo?.price ?? '0';
//
if (_priceController.text.isEmpty && priceInfo != null) {
_priceController.text = currentPrice;
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(20),
@ -452,7 +445,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
child: Column(
children: [
// /
Container(
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: _bgGray)),
@ -513,71 +505,13 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
),
const SizedBox(height: 24),
//
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'数量',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: _grayText,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Container(
height: 44,
decoration: BoxDecoration(
color: _bgGray,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _amountController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
hintText: '请输入数量',
hintStyle: TextStyle(
fontSize: 14,
color: Color(0xFF9CA3AF),
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 16),
),
),
),
const Icon(Icons.currency_yuan, size: 15, color: _grayText),
const SizedBox(width: 12),
],
),
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () {
// TODO:
},
child: const Text(
'MAX',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: _orange,
),
),
),
],
),
],
),
//
_buildInputField('价格', _priceController, '请输入价格', '绿积分'),
const SizedBox(height: 16),
//
//
_buildInputField('数量', _quantityController, '请输入数量', '积分股'),
const SizedBox(height: 16),
// /
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
@ -587,16 +521,13 @@ class _TradingPageState extends ConsumerState<TradingPage> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'预计获得',
style: TextStyle(
fontSize: 12,
color: _grayText,
),
Text(
_selectedTab == 0 ? '预计支出' : '预计获得',
style: const TextStyle(fontSize: 12, color: _grayText),
),
const Text(
'0.00 绿积分',
style: TextStyle(
Text(
_calculateEstimate(),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: _orange,
@ -606,37 +537,35 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
),
const SizedBox(height: 16),
//
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(
'手续费 (10%)',
style: TextStyle(
fontSize: 12,
color: _grayText,
// ()
if (_selectedTab == 1)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(
'销毁比例',
style: TextStyle(fontSize: 12, color: _grayText),
),
),
Text(
'0.00',
style: TextStyle(
fontSize: 12,
color: _grayText,
fontFamily: 'monospace',
Text(
'10% 进入黑洞',
style: TextStyle(
fontSize: 12,
color: _red,
fontFamily: 'monospace',
),
),
),
],
],
),
),
),
const SizedBox(height: 24),
//
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: () => _handleTrade(accountSequence),
onPressed: _handleTrade,
style: ElevatedButton.styleFrom(
backgroundColor: _orange,
shape: RoundedRectangleBorder(
@ -658,7 +587,79 @@ class _TradingPageState extends ConsumerState<TradingPage> {
);
}
Widget _buildMyOrdersCard() {
Widget _buildInputField(
String label,
TextEditingController controller,
String hint,
String suffix,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: _grayText,
),
),
const SizedBox(height: 8),
Container(
height: 44,
decoration: BoxDecoration(
color: _bgGray,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
hintText: hint,
hintStyle: const TextStyle(
fontSize: 14,
color: Color(0xFF9CA3AF),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
),
),
Text(suffix, style: const TextStyle(fontSize: 12, color: _grayText)),
const SizedBox(width: 12),
],
),
),
],
);
}
String _calculateEstimate() {
final price = double.tryParse(_priceController.text) ?? 0;
final quantity = double.tryParse(_quantityController.text) ?? 0;
final total = price * quantity;
if (total == 0) {
return '0.00 绿积分';
}
if (_selectedTab == 1) {
// 10%
final afterBurn = total * 0.9;
return '${formatAmount(afterBurn.toString())} 绿积分';
}
return '${formatAmount(total.toString())} 绿积分';
}
Widget _buildMyOrdersCard(AsyncValue<OrdersPageModel?> ordersAsync) {
final isLoading = ordersAsync.isLoading;
final ordersPage = ordersAsync.valueOrNull;
final orders = ordersPage?.data ?? [];
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
@ -668,7 +669,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
child: Column(
children: [
//
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -696,27 +696,54 @@ class _TradingPageState extends ConsumerState<TradingPage> {
],
),
const SizedBox(height: 16),
//
_buildOrderItem(
type: '卖出',
price: '0.000156',
quantity: '1,000 股',
time: '12/05 14:30',
status: '待成交',
),
if (isLoading)
const Center(
child: Padding(
padding: EdgeInsets.all(20),
child: CircularProgressIndicator(color: _orange),
),
)
else if (orders.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Text(
'暂无挂单',
style: TextStyle(fontSize: 14, color: _grayText),
),
)
else
Column(
children: orders.take(3).map((order) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildOrderItemFromEntity(order),
)).toList(),
),
],
),
);
}
Widget _buildOrderItem({
required String type,
required String price,
required String quantity,
required String time,
required String status,
}) {
final isSell = type == '卖出';
Widget _buildOrderItemFromEntity(TradeOrder order) {
final isSell = order.isSell;
final dateFormat = DateFormat('MM/dd HH:mm');
final formattedDate = dateFormat.format(order.createdAt);
String statusText;
switch (order.status) {
case OrderStatus.pending:
statusText = '待成交';
break;
case OrderStatus.partial:
statusText = '部分成交';
break;
case OrderStatus.filled:
statusText = '已成交';
break;
case OrderStatus.cancelled:
statusText = '已取消';
break;
}
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
@ -735,11 +762,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: (isSell ? _red : _green).withOpacity(0.1),
color: (isSell ? _red : _green).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
type,
isSell ? '卖出' : '买入',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
@ -749,7 +776,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
const SizedBox(width: 8),
Text(
price,
formatPrice(order.price),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@ -760,19 +787,16 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
const SizedBox(height: 4),
Text(
time,
style: const TextStyle(
fontSize: 12,
color: _grayText,
),
formattedDate,
style: const TextStyle(fontSize: 12, color: _grayText),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
quantity,
'${formatCompact(order.quantity)}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
@ -783,15 +807,12 @@ class _TradingPageState extends ConsumerState<TradingPage> {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: _orange.withOpacity(0.1),
color: _orange.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(9999),
),
child: Text(
status,
style: const TextStyle(
fontSize: 12,
color: _orange,
),
statusText,
style: const TextStyle(fontSize: 12, color: _orange),
),
),
],
@ -821,8 +842,15 @@ class _TradingPageState extends ConsumerState<TradingPage> {
);
}
void _handleTrade(String accountSequence) async {
if (_amountController.text.isEmpty) {
void _handleTrade() async {
if (_priceController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入价格')),
);
return;
}
if (_quantityController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入数量')),
);
@ -835,11 +863,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
if (isBuy) {
success = await ref
.read(tradingNotifierProvider.notifier)
.buyShares(accountSequence, _amountController.text);
.buyShares(_priceController.text, _quantityController.text);
} else {
success = await ref
.read(tradingNotifierProvider.notifier)
.sellShares(accountSequence, _amountController.text);
.sellShares(_priceController.text, _quantityController.text);
}
if (mounted) {
@ -852,10 +880,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
);
if (success) {
_amountController.clear();
//
ref.invalidate(shareAccountProvider(accountSequence));
ref.invalidate(globalStateProvider);
_quantityController.clear();
//
ref.invalidate(ordersProvider);
ref.invalidate(currentPriceProvider);
ref.invalidate(marketOverviewProvider);
}
}
}
@ -889,7 +918,7 @@ class _CandlestickPainter extends CustomPainter {
];
final candleWidth = (size.width - 40) / candleData.length;
final padding = 20.0;
const padding = 20.0;
for (int i = 0; i < candleData.length; i++) {
final data = candleData[i];
@ -907,14 +936,12 @@ class _CandlestickPainter extends CustomPainter {
final yHigh = size.height - (high * size.height * 0.8 + size.height * 0.1);
final yLow = size.height - (low * size.height * 0.8 + size.height * 0.1);
// 线
canvas.drawLine(
Offset(x, yHigh),
Offset(x, yLow),
paint..strokeWidth = 1,
);
//
final bodyTop = isGreen ? yClose : yOpen;
final bodyBottom = isGreen ? yOpen : yClose;
canvas.drawRect(
@ -923,7 +950,6 @@ class _CandlestickPainter extends CustomPainter {
);
}
// 线线
final dashY = size.height * 0.35;
const dashWidth = 5.0;
const dashSpace = 3.0;

View File

@ -0,0 +1,77 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/asset_display.dart';
import '../../domain/repositories/trading_repository.dart';
import '../../core/di/injection.dart';
import 'trading_providers.dart';
/// Provider (使 JWT token)
final myAssetProvider = FutureProvider<AssetDisplay?>((ref) async {
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getMyAsset();
ref.keepAlive();
final timer = Timer(const Duration(minutes: 2), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(asset) => asset,
);
});
/// Provider (public API)
final accountAssetProvider = FutureProvider.family<AssetDisplay?, String>(
(ref, accountSequence) async {
if (accountSequence.isEmpty) return null;
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getAccountAsset(accountSequence);
ref.keepAlive();
final timer = Timer(const Duration(minutes: 2), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(asset) => asset,
);
},
);
/// -
/// displayAssetValue = ( + × ) ×
class AssetValueCalculator {
final String shareBalance;
final String burnMultiplier;
final String currentPrice;
AssetValueCalculator({
required this.shareBalance,
required this.burnMultiplier,
required this.currentPrice,
});
/// = × (1 + )
double get effectiveShares {
final balance = double.tryParse(shareBalance) ?? 0;
final multiplier = double.tryParse(burnMultiplier) ?? 0;
return balance * (1 + multiplier);
}
/// = ×
double get displayValue {
final price = double.tryParse(currentPrice) ?? 0;
return effectiveShares * price;
}
///
static double calculateGrowthPerSecond(String dailyAllocation) {
final daily = double.tryParse(dailyAllocation) ?? 0;
return daily / 24 / 60 / 60;
}
}

View File

@ -0,0 +1,91 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/contribution.dart';
import '../../domain/repositories/contribution_repository.dart';
import '../../core/di/injection.dart';
import 'user_providers.dart';
/// -
class UserStats {
///
final bool hasAdopted;
///
final int directReferralAdoptedCount;
///
final int unlockedLevelDepth;
/// VIP等级
final int unlockedBonusTiers;
///
final String totalContribution;
const UserStats({
this.hasAdopted = false,
this.directReferralAdoptedCount = 0,
this.unlockedLevelDepth = 0,
this.unlockedBonusTiers = 0,
this.totalContribution = '0',
});
///
factory UserStats.fromContribution(Contribution contribution) {
return UserStats(
hasAdopted: contribution.hasAdopted,
directReferralAdoptedCount: contribution.directReferralAdoptedCount,
unlockedLevelDepth: contribution.unlockedLevelDepth,
unlockedBonusTiers: contribution.unlockedBonusTiers,
totalContribution: contribution.totalContribution,
);
}
/// VIP等级显示
String get vipLevel {
if (unlockedBonusTiers <= 0) return '-';
return 'V$unlockedBonusTiers';
}
///
int get adoptionCount => hasAdopted ? 1 : 0;
///
int get teamSize {
// 3
if (unlockedLevelDepth <= 0) return 0;
int total = 0;
for (int i = 1; i <= unlockedLevelDepth; i++) {
total += (directReferralAdoptedCount > 0 ? directReferralAdoptedCount : 1) * i;
}
return total;
}
}
/// Provider
final userStatsProvider = FutureProvider<UserStats?>((ref) async {
final user = ref.watch(userNotifierProvider);
if (!user.isLoggedIn || user.accountSequence == null) {
return null;
}
final repository = getIt<ContributionRepository>();
final result = await repository.getUserContribution(user.accountSequence!);
ref.keepAlive();
final timer = Timer(const Duration(minutes: 5), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => const UserStats(),
(contribution) => UserStats.fromContribution(contribution),
);
});
/// Provider - 使
final invitationCodeProvider = Provider<String>((ref) {
final user = ref.watch(userNotifierProvider);
return user.accountSequence ?? '--------';
});

View File

@ -1,60 +1,131 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/price_info.dart';
import '../../domain/entities/market_overview.dart';
import '../../domain/entities/trading_account.dart';
import '../../domain/entities/trade_order.dart';
import '../../domain/usecases/trading/buy_shares.dart';
import '../../domain/usecases/trading/sell_shares.dart';
import '../../domain/repositories/trading_repository.dart';
import '../../data/models/trade_order_model.dart';
import '../../core/di/injection.dart';
// Use Cases Providers
final buySharesUseCaseProvider = Provider<BuyShares>((ref) {
return getIt<BuyShares>();
});
final sellSharesUseCaseProvider = Provider<SellShares>((ref) {
return getIt<SellShares>();
// Repository Provider
final tradingRepositoryProvider = Provider<TradingRepository>((ref) {
return getIt<TradingRepository>();
});
// K线周期选择
final selectedKlinePeriodProvider = StateProvider<String>((ref) => '1h');
// Provider (5)
final currentPriceProvider = FutureProvider<PriceInfo?>((ref) async {
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getCurrentPrice();
ref.keepAlive();
final timer = Timer(const Duration(minutes: 5), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(priceInfo) => priceInfo,
);
});
// Provider (5)
final marketOverviewProvider = FutureProvider<MarketOverview?>((ref) async {
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getMarketOverview();
ref.keepAlive();
final timer = Timer(const Duration(minutes: 5), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(overview) => overview,
);
});
// Provider ()
final tradingAccountProvider = FutureProvider.family<TradingAccount?, String>(
(ref, accountSequence) async {
if (accountSequence.isEmpty) return null;
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getTradingAccount(accountSequence);
ref.keepAlive();
final timer = Timer(const Duration(minutes: 2), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(account) => account,
);
},
);
// Provider
final ordersProvider = FutureProvider<OrdersPageModel?>((ref) async {
final repository = ref.watch(tradingRepositoryProvider);
final result = await repository.getOrders(page: 1, pageSize: 10);
ref.keepAlive();
final timer = Timer(const Duration(minutes: 2), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(orders) => orders,
);
});
//
class TradingState {
final bool isLoading;
final String? error;
final TradeOrder? lastOrder;
final Map<String, dynamic>? lastOrderResult;
TradingState({
this.isLoading = false,
this.error,
this.lastOrder,
this.lastOrderResult,
});
TradingState copyWith({
bool? isLoading,
String? error,
TradeOrder? lastOrder,
Map<String, dynamic>? lastOrderResult,
}) {
return TradingState(
isLoading: isLoading ?? this.isLoading,
error: error,
lastOrder: lastOrder ?? this.lastOrder,
lastOrderResult: lastOrderResult ?? this.lastOrderResult,
);
}
}
class TradingNotifier extends StateNotifier<TradingState> {
final BuyShares buySharesUseCase;
final SellShares sellSharesUseCase;
final TradingRepository repository;
TradingNotifier({
required this.buySharesUseCase,
required this.sellSharesUseCase,
}) : super(TradingState());
TradingNotifier({required this.repository}) : super(TradingState());
Future<bool> buyShares(String accountSequence, String amount) async {
///
Future<bool> buyShares(String price, String quantity) async {
state = state.copyWith(isLoading: true, error: null);
final result = await buySharesUseCase(
BuySharesParams(accountSequence: accountSequence, amount: amount),
final result = await repository.createOrder(
type: 'BUY',
price: price,
quantity: quantity,
);
return result.fold(
@ -62,18 +133,21 @@ class TradingNotifier extends StateNotifier<TradingState> {
state = state.copyWith(isLoading: false, error: failure.message);
return false;
},
(order) {
state = state.copyWith(isLoading: false, lastOrder: order);
(orderResult) {
state = state.copyWith(isLoading: false, lastOrderResult: orderResult);
return true;
},
);
}
Future<bool> sellShares(String accountSequence, String amount) async {
///
Future<bool> sellShares(String price, String quantity) async {
state = state.copyWith(isLoading: true, error: null);
final result = await sellSharesUseCase(
SellSharesParams(accountSequence: accountSequence, amount: amount),
final result = await repository.createOrder(
type: 'SELL',
price: price,
quantity: quantity,
);
return result.fold(
@ -81,8 +155,62 @@ class TradingNotifier extends StateNotifier<TradingState> {
state = state.copyWith(isLoading: false, error: failure.message);
return false;
},
(order) {
state = state.copyWith(isLoading: false, lastOrder: order);
(orderResult) {
state = state.copyWith(isLoading: false, lastOrderResult: orderResult);
return true;
},
);
}
///
Future<bool> cancelOrder(String orderNo) async {
state = state.copyWith(isLoading: true, error: null);
final result = await repository.cancelOrder(orderNo);
return result.fold(
(failure) {
state = state.copyWith(isLoading: false, error: failure.message);
return false;
},
(_) {
state = state.copyWith(isLoading: false);
return true;
},
);
}
///
Future<bool> transferIn(String amount) async {
state = state.copyWith(isLoading: true, error: null);
final result = await repository.transferIn(amount);
return result.fold(
(failure) {
state = state.copyWith(isLoading: false, error: failure.message);
return false;
},
(_) {
state = state.copyWith(isLoading: false);
return true;
},
);
}
///
Future<bool> transferOut(String amount) async {
state = state.copyWith(isLoading: true, error: null);
final result = await repository.transferOut(amount);
return result.fold(
(failure) {
state = state.copyWith(isLoading: false, error: failure.message);
return false;
},
(_) {
state = state.copyWith(isLoading: false);
return true;
},
);
@ -95,7 +223,6 @@ class TradingNotifier extends StateNotifier<TradingState> {
final tradingNotifierProvider = StateNotifierProvider<TradingNotifier, TradingState>(
(ref) => TradingNotifier(
buySharesUseCase: ref.watch(buySharesUseCaseProvider),
sellSharesUseCase: ref.watch(sellSharesUseCaseProvider),
repository: ref.watch(tradingRepositoryProvider),
),
);

View File

@ -9,6 +9,10 @@ class UserState {
final String? phone;
final String? kycStatus;
final String? source;
final String? status;
final String? realName;
final DateTime? createdAt;
final DateTime? lastLoginAt;
final String? accessToken;
final String? refreshToken;
final bool isLoggedIn;
@ -21,6 +25,10 @@ class UserState {
this.phone,
this.kycStatus,
this.source,
this.status,
this.realName,
this.createdAt,
this.lastLoginAt,
this.accessToken,
this.refreshToken,
this.isLoggedIn = false,
@ -28,12 +36,18 @@ class UserState {
this.error,
});
bool get isKycVerified => kycStatus == 'VERIFIED';
UserState copyWith({
String? accountSequence,
String? nickname,
String? phone,
String? kycStatus,
String? source,
String? status,
String? realName,
DateTime? createdAt,
DateTime? lastLoginAt,
String? accessToken,
String? refreshToken,
bool? isLoggedIn,
@ -46,6 +60,10 @@ class UserState {
phone: phone ?? this.phone,
kycStatus: kycStatus ?? this.kycStatus,
source: source ?? this.source,
status: status ?? this.status,
realName: realName ?? this.realName,
createdAt: createdAt ?? this.createdAt,
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
accessToken: accessToken ?? this.accessToken,
refreshToken: refreshToken ?? this.refreshToken,
isLoggedIn: isLoggedIn ?? this.isLoggedIn,
@ -77,6 +95,8 @@ class UserNotifier extends StateNotifier<UserState> {
phone: phone,
isLoggedIn: true,
);
//
await fetchProfile();
}
}
@ -143,6 +163,8 @@ class UserNotifier extends StateNotifier<UserState> {
isLoggedIn: true,
isLoading: false,
);
//
await fetchProfile();
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
rethrow;
@ -164,6 +186,8 @@ class UserNotifier extends StateNotifier<UserState> {
isLoggedIn: true,
isLoading: false,
);
//
await fetchProfile();
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
rethrow;
@ -219,6 +243,24 @@ class UserNotifier extends StateNotifier<UserState> {
}
}
///
Future<void> fetchProfile() async {
if (!state.isLoggedIn) return;
try {
final profile = await _authDataSource.getProfile();
state = state.copyWith(
phone: profile.phone,
kycStatus: profile.kycStatus,
source: profile.source,
status: profile.status,
realName: profile.realName,
createdAt: profile.createdAt,
lastLoginAt: profile.lastLoginAt,
);
} catch (e) {
//
}
}
}
final userNotifierProvider = StateNotifierProvider<UserNotifier, UserState>(