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/repositories/contribution_repository.dart';
import '../../domain/usecases/mining/get_share_account.dart'; import '../../domain/usecases/mining/get_share_account.dart';
import '../../domain/usecases/mining/get_global_state.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'; import '../../domain/usecases/contribution/get_user_contribution.dart';
final getIt = GetIt.instance; final getIt = GetIt.instance;
@ -72,11 +69,6 @@ Future<void> configureDependencies() async {
getIt.registerLazySingleton(() => GetShareAccount(getIt<MiningRepository>())); getIt.registerLazySingleton(() => GetShareAccount(getIt<MiningRepository>()));
getIt.registerLazySingleton(() => GetGlobalState(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 // Use Cases - Contribution
getIt.registerLazySingleton(() => GetUserContribution(getIt<ContributionRepository>())); getIt.registerLazySingleton(() => GetUserContribution(getIt<ContributionRepository>()));
} }

View File

@ -23,16 +23,31 @@ class ApiEndpoints {
'/api/v2/mining/accounts/$accountSequence/realtime'; '/api/v2/mining/accounts/$accountSequence/realtime';
// Trading Service 2.0 (Kong路由: /api/v2/trading) // Trading Service 2.0 (Kong路由: /api/v2/trading)
static const String currentPrice = '/api/v2/trading/price'; // Price endpoints
static const String klineData = '/api/v2/trading/kline'; 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) => static String tradingAccount(String accountSequence) =>
'/api/v2/trading/accounts/$accountSequence'; '/api/v2/trading/trading/accounts/$accountSequence';
static String createOrder(String accountSequence) => static const String orderBook = '/api/v2/trading/trading/orderbook';
'/api/v2/trading/accounts/$accountSequence/orders'; static const String createOrder = '/api/v2/trading/trading/orders';
static String orders(String accountSequence) => static const String orders = '/api/v2/trading/trading/orders';
'/api/v2/trading/accounts/$accountSequence/orders'; static String cancelOrder(String orderNo) =>
static String transfer(String accountSequence) => '/api/v2/trading/trading/orders/$orderNo/cancel';
'/api/v2/trading/accounts/$accountSequence/transfer';
// 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) // Contribution Service 2.0 (Kong路由: /api/v2/contribution -> /api/v1/contributions)
static String contribution(String accountSequence) => static String contribution(String accountSequence) =>

View File

@ -30,12 +30,20 @@ class UserInfo {
final String phone; final String phone;
final String source; final String source;
final String kycStatus; final String kycStatus;
final String? status;
final String? realName;
final DateTime? createdAt;
final DateTime? lastLoginAt;
UserInfo({ UserInfo({
required this.accountSequence, required this.accountSequence,
required this.phone, required this.phone,
required this.source, required this.source,
required this.kycStatus, required this.kycStatus,
this.status,
this.realName,
this.createdAt,
this.lastLoginAt,
}); });
factory UserInfo.fromJson(Map<String, dynamic> json) { factory UserInfo.fromJson(Map<String, dynamic> json) {
@ -44,8 +52,18 @@ class UserInfo {
phone: json['phone'] as String, phone: json['phone'] as String,
source: json['source'] as String, source: json['source'] as String,
kycStatus: json['kycStatus'] 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 { abstract class AuthRemoteDataSource {

View File

@ -1,30 +1,52 @@
import '../../models/trade_order_model.dart'; 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_client.dart';
import '../../../core/network/api_endpoints.dart'; import '../../../core/network/api_endpoints.dart';
import '../../../core/error/exceptions.dart'; import '../../../core/error/exceptions.dart';
abstract class TradingRemoteDataSource { abstract class TradingRemoteDataSource {
Future<String> getCurrentPrice(); ///
Future<List<KlineModel>> getKlineData(String period); Future<PriceInfoModel> getCurrentPrice();
Future<TradeOrderModel> buyShares({
required String accountSequence, ///
required String amount, 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<void> cancelOrder(String orderNo);
});
Future<List<TradeOrderModel>> getOrders( ///
String accountSequence, { Future<OrdersPageModel> getOrders({
int page = 1, int page = 1,
int limit = 20, int pageSize = 50,
});
Future<void> transfer({
required String accountSequence,
required String amount,
required String direction,
}); });
///
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 { class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
@ -33,90 +55,149 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
TradingRemoteDataSourceImpl({required this.client}); TradingRemoteDataSourceImpl({required this.client});
@override @override
Future<String> getCurrentPrice() async { Future<PriceInfoModel> getCurrentPrice() async {
try { try {
final response = await client.get(ApiEndpoints.currentPrice); final response = await client.get(ApiEndpoints.currentPrice);
return response.data['price']?.toString() ?? '0'; return PriceInfoModel.fromJson(response.data);
} catch (e) { } catch (e) {
throw ServerException(e.toString()); throw ServerException(e.toString());
} }
} }
@override @override
Future<List<KlineModel>> getKlineData(String period) async { Future<MarketOverviewModel> getMarketOverview() async {
try { try {
final response = await client.get( final response = await client.get(ApiEndpoints.marketOverview);
ApiEndpoints.klineData, return MarketOverviewModel.fromJson(response.data);
queryParameters: {'period': period},
);
final items = response.data as List? ?? [];
return items.map((json) => KlineModel.fromJson(json)).toList();
} catch (e) { } catch (e) {
throw ServerException(e.toString()); throw ServerException(e.toString());
} }
} }
@override @override
Future<TradeOrderModel> buyShares({ Future<TradingAccountModel> getTradingAccount(String accountSequence) async {
required String accountSequence, try {
required String amount, 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 { }) async {
try { try {
final response = await client.post( final response = await client.post(
ApiEndpoints.createOrder(accountSequence), ApiEndpoints.createOrder,
data: {'orderType': 'BUY', 'quantity': amount}, data: {
'type': type,
'price': price,
'quantity': quantity,
},
); );
return TradeOrderModel.fromJson(response.data); return response.data;
} catch (e) { } catch (e) {
throw ServerException(e.toString()); throw ServerException(e.toString());
} }
} }
@override @override
Future<TradeOrderModel> sellShares({ Future<void> cancelOrder(String orderNo) async {
required String accountSequence,
required String amount,
}) async {
try { try {
final response = await client.post( await client.post(ApiEndpoints.cancelOrder(orderNo));
ApiEndpoints.createOrder(accountSequence),
data: {'orderType': 'SELL', 'quantity': amount},
);
return TradeOrderModel.fromJson(response.data);
} catch (e) { } catch (e) {
throw ServerException(e.toString()); throw ServerException(e.toString());
} }
} }
@override @override
Future<List<TradeOrderModel>> getOrders( Future<OrdersPageModel> getOrders({
String accountSequence, {
int page = 1, int page = 1,
int limit = 20, int pageSize = 50,
}) async { }) async {
try { try {
final response = await client.get( final response = await client.get(
ApiEndpoints.orders(accountSequence), ApiEndpoints.orders,
queryParameters: {'page': page, 'pageSize': limit}, queryParameters: {'page': page, 'pageSize': pageSize},
); );
final items = response.data['items'] as List? ?? []; return OrdersPageModel.fromJson(response.data);
return items.map((json) => TradeOrderModel.fromJson(json)).toList();
} catch (e) { } catch (e) {
throw ServerException(e.toString()); throw ServerException(e.toString());
} }
} }
@override @override
Future<void> transfer({ Future<Map<String, dynamic>> estimateSell(String quantity) async {
required String accountSequence,
required String amount,
required String direction,
}) async {
try { try {
await client.post( final response = await client.get(
ApiEndpoints.transfer(accountSequence), ApiEndpoints.estimateSell,
data: {'amount': amount, 'direction': direction}, 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) { } catch (e) {
throw ServerException(e.toString()); 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 { class ContributionModel extends Contribution {
const ContributionModel({ const ContributionModel({
required super.status,
required super.message,
required super.accountSequence, required super.accountSequence,
required super.personalContribution, required super.personalContribution,
required super.systemContribution,
required super.teamLevelContribution, required super.teamLevelContribution,
required super.teamBonusContribution, required super.teamBonusContribution,
required super.totalContribution, required super.totalContribution,
required super.effectiveContribution,
required super.hasAdopted, required super.hasAdopted,
required super.directReferralAdoptedCount, required super.directReferralAdoptedCount,
required super.unlockedLevelDepth, required super.unlockedLevelDepth,
required super.unlockedBonusTiers, required super.unlockedBonusTiers,
required super.isCalculated,
super.lastCalculatedAt,
}); });
factory ContributionModel.fromJson(Map<String, dynamic> json) { factory ContributionModel.fromJson(Map<String, dynamic> json) {
return ContributionModel( return ContributionModel(
status: _parseStatus(json['status']),
message: json['message']?.toString() ?? '',
accountSequence: json['accountSequence']?.toString() ?? '', accountSequence: json['accountSequence']?.toString() ?? '',
personalContribution: json['personalContribution']?.toString() ?? '0', personalContribution: json['personalContribution']?.toString() ?? '0',
systemContribution: json['systemContribution']?.toString() ?? '0',
teamLevelContribution: json['teamLevelContribution']?.toString() ?? '0', teamLevelContribution: json['teamLevelContribution']?.toString() ?? '0',
teamBonusContribution: json['teamBonusContribution']?.toString() ?? '0', teamBonusContribution: json['teamBonusContribution']?.toString() ?? '0',
totalContribution: json['totalContribution']?.toString() ?? '0', totalContribution: json['totalContribution']?.toString() ?? '0',
effectiveContribution: json['effectiveContribution']?.toString() ?? '0',
hasAdopted: json['hasAdopted'] == true, hasAdopted: json['hasAdopted'] == true,
directReferralAdoptedCount: json['directReferralAdoptedCount'] ?? 0, directReferralAdoptedCount: json['directReferralAdoptedCount'] ?? 0,
unlockedLevelDepth: json['unlockedLevelDepth'] ?? 0, unlockedLevelDepth: json['unlockedLevelDepth'] ?? 0,
unlockedBonusTiers: json['unlockedBonusTiers'] ?? 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 { class TradeOrderModel extends TradeOrder {
const TradeOrderModel({ const TradeOrderModel({
required super.id, required super.id,
required super.accountSequence, required super.orderNo,
required super.orderType, required super.orderType,
required super.status, required super.status,
required super.price, required super.price,
required super.quantity, required super.quantity,
required super.filledQuantity, required super.filledQuantity,
required super.remainingQuantity,
required super.averagePrice,
required super.totalAmount, required super.totalAmount,
required super.createdAt, required super.createdAt,
super.updatedAt, super.completedAt,
super.cancelledAt,
}); });
factory TradeOrderModel.fromJson(Map<String, dynamic> json) { factory TradeOrderModel.fromJson(Map<String, dynamic> json) {
return TradeOrderModel( return TradeOrderModel(
id: json['id'] ?? '', id: json['id']?.toString() ?? '',
accountSequence: json['accountSequence']?.toString() ?? '', orderNo: json['orderNo']?.toString() ?? '',
orderType: json['orderType'] == 'BUY' ? OrderType.buy : OrderType.sell, orderType: _parseOrderType(json['type']),
status: _parseStatus(json['status']), status: _parseStatus(json['status']),
price: json['price']?.toString() ?? '0', price: json['price']?.toString() ?? '0',
quantity: json['quantity']?.toString() ?? '0', quantity: json['quantity']?.toString() ?? '0',
filledQuantity: json['filledQuantity']?.toString() ?? '0', filledQuantity: json['filledQuantity']?.toString() ?? '0',
remainingQuantity: json['remainingQuantity']?.toString() ?? '0',
averagePrice: json['averagePrice']?.toString() ?? '0',
totalAmount: json['totalAmount']?.toString() ?? '0', totalAmount: json['totalAmount']?.toString() ?? '0',
createdAt: json['createdAt'] != null createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt']) ? DateTime.parse(json['createdAt'].toString())
: DateTime.now(), : DateTime.now(),
updatedAt: json['updatedAt'] != null completedAt: json['completedAt'] != null
? DateTime.parse(json['updatedAt']) ? DateTime.parse(json['completedAt'].toString())
: null,
cancelledAt: json['cancelledAt'] != null
? DateTime.parse(json['cancelledAt'].toString())
: null, : 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) { static OrderStatus _parseStatus(String? status) {
switch (status) { switch (status) {
case 'PENDING': 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 'package:dartz/dartz.dart';
import '../../domain/entities/trade_order.dart'; import '../../domain/entities/price_info.dart';
import '../../domain/entities/kline.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 '../../domain/repositories/trading_repository.dart';
import '../../core/error/exceptions.dart'; import '../../core/error/exceptions.dart';
import '../../core/error/failures.dart'; import '../../core/error/failures.dart';
import '../datasources/remote/trading_remote_datasource.dart'; import '../datasources/remote/trading_remote_datasource.dart';
import '../models/trade_order_model.dart';
class TradingRepositoryImpl implements TradingRepository { class TradingRepositoryImpl implements TradingRepository {
final TradingRemoteDataSource remoteDataSource; final TradingRemoteDataSource remoteDataSource;
@ -12,7 +15,7 @@ class TradingRepositoryImpl implements TradingRepository {
TradingRepositoryImpl({required this.remoteDataSource}); TradingRepositoryImpl({required this.remoteDataSource});
@override @override
Future<Either<Failure, String>> getCurrentPrice() async { Future<Either<Failure, PriceInfo>> getCurrentPrice() async {
try { try {
final result = await remoteDataSource.getCurrentPrice(); final result = await remoteDataSource.getCurrentPrice();
return Right(result); return Right(result);
@ -24,9 +27,9 @@ class TradingRepositoryImpl implements TradingRepository {
} }
@override @override
Future<Either<Failure, List<Kline>>> getKlineData(String period) async { Future<Either<Failure, MarketOverview>> getMarketOverview() async {
try { try {
final result = await remoteDataSource.getKlineData(period); final result = await remoteDataSource.getMarketOverview();
return Right(result); return Right(result);
} on ServerException catch (e) { } on ServerException catch (e) {
return Left(ServerFailure(e.message)); return Left(ServerFailure(e.message));
@ -36,14 +39,28 @@ class TradingRepositoryImpl implements TradingRepository {
} }
@override @override
Future<Either<Failure, TradeOrder>> buyShares({ Future<Either<Failure, TradingAccount>> getTradingAccount(String accountSequence) async {
required String accountSequence, try {
required String amount, 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 { }) async {
try { try {
final result = await remoteDataSource.buyShares( final result = await remoteDataSource.createOrder(
accountSequence: accountSequence, type: type,
amount: amount, price: price,
quantity: quantity,
); );
return Right(result); return Right(result);
} on ServerException catch (e) { } on ServerException catch (e) {
@ -54,55 +71,9 @@ class TradingRepositoryImpl implements TradingRepository {
} }
@override @override
Future<Either<Failure, TradeOrder>> sellShares({ Future<Either<Failure, void>> cancelOrder(String orderNo) async {
required String accountSequence,
required String amount,
}) async {
try { try {
final result = await remoteDataSource.sellShares( await remoteDataSource.cancelOrder(orderNo);
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,
);
return const Right(null); return const Right(null);
} on ServerException catch (e) { } on ServerException catch (e) {
return Left(ServerFailure(e.message)); return Left(ServerFailure(e.message));
@ -110,4 +81,82 @@ class TradingRepositoryImpl implements TradingRepository {
return Left(const NetworkFailure()); 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'; import 'package:equatable/equatable.dart';
///
enum ContributionAccountStatus {
/// -
active,
/// -
inactive,
///
userNotFound,
}
class Contribution extends Equatable { class Contribution extends Equatable {
///
final ContributionAccountStatus status;
///
final String message;
///
final String accountSequence; final String accountSequence;
///
final String personalContribution; final String personalContribution;
final String systemContribution; ///
final String teamLevelContribution; final String teamLevelContribution;
///
final String teamBonusContribution; final String teamBonusContribution;
///
final String totalContribution; final String totalContribution;
final String effectiveContribution; ///
final bool hasAdopted; final bool hasAdopted;
///
final int directReferralAdoptedCount; final int directReferralAdoptedCount;
///
final int unlockedLevelDepth; final int unlockedLevelDepth;
///
final int unlockedBonusTiers; final int unlockedBonusTiers;
///
final bool isCalculated;
///
final DateTime? lastCalculatedAt;
const Contribution({ const Contribution({
required this.status,
required this.message,
required this.accountSequence, required this.accountSequence,
required this.personalContribution, required this.personalContribution,
required this.systemContribution,
required this.teamLevelContribution, required this.teamLevelContribution,
required this.teamBonusContribution, required this.teamBonusContribution,
required this.totalContribution, required this.totalContribution,
required this.effectiveContribution,
required this.hasAdopted, required this.hasAdopted,
required this.directReferralAdoptedCount, required this.directReferralAdoptedCount,
required this.unlockedLevelDepth, required this.unlockedLevelDepth,
required this.unlockedBonusTiers, 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 @override
List<Object?> get props => [ List<Object?> get props => [
status,
message,
accountSequence, accountSequence,
personalContribution, personalContribution,
systemContribution,
teamLevelContribution, teamLevelContribution,
teamBonusContribution, teamBonusContribution,
totalContribution, totalContribution,
effectiveContribution,
hasAdopted, 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 } enum OrderStatus { pending, partial, filled, cancelled }
class TradeOrder extends Equatable { class TradeOrder extends Equatable {
/// ID
final String id; final String id;
final String accountSequence; ///
final String orderNo;
///
final OrderType orderType; final OrderType orderType;
///
final OrderStatus status; final OrderStatus status;
///
final String price; final String price;
///
final String quantity; final String quantity;
///
final String filledQuantity; final String filledQuantity;
///
final String remainingQuantity;
///
final String averagePrice;
///
final String totalAmount; final String totalAmount;
///
final DateTime createdAt; final DateTime createdAt;
final DateTime? updatedAt; ///
final DateTime? completedAt;
///
final DateTime? cancelledAt;
const TradeOrder({ const TradeOrder({
required this.id, required this.id,
required this.accountSequence, required this.orderNo,
required this.orderType, required this.orderType,
required this.status, required this.status,
required this.price, required this.price,
required this.quantity, required this.quantity,
required this.filledQuantity, required this.filledQuantity,
required this.remainingQuantity,
required this.averagePrice,
required this.totalAmount, required this.totalAmount,
required this.createdAt, required this.createdAt,
this.updatedAt, this.completedAt,
this.cancelledAt,
}); });
bool get isBuy => orderType == OrderType.buy; bool get isBuy => orderType == OrderType.buy;
bool get isSell => orderType == OrderType.sell; bool get isSell => orderType == OrderType.sell;
bool get isPending => status == OrderStatus.pending; bool get isPending => status == OrderStatus.pending;
bool get isPartial => status == OrderStatus.partial;
bool get isFilled => status == OrderStatus.filled; 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 qty = double.tryParse(quantity) ?? 0;
final filled = double.tryParse(filledQuantity) ?? 0; final filled = double.tryParse(filledQuantity) ?? 0;
return (qty - filled).toString(); if (qty == 0) return 0;
return filled / qty;
} }
@override @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 'package:dartz/dartz.dart';
import '../../core/error/failures.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/trade_order.dart';
import '../entities/kline.dart'; import '../entities/asset_display.dart';
import '../../data/models/trade_order_model.dart';
abstract class TradingRepository { 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, Future<Either<Failure, TradingAccount>> getTradingAccount(String accountSequence);
required String amount,
///
Future<Either<Failure, Map<String, dynamic>>> createOrder({
required String type,
required String price,
required String quantity,
}); });
Future<Either<Failure, TradeOrder>> sellShares({ ///
required String accountSequence, Future<Either<Failure, void>> cancelOrder(String orderNo);
required String amount,
});
Future<Either<Failure, List<TradeOrder>>> getOrders( ///
String accountSequence, { Future<Either<Failure, OrdersPageModel>> getOrders({
int page = 1, int page = 1,
int limit = 20, int pageSize = 50,
}); });
Future<Either<Failure, void>> transfer({ ///
required String accountSequence, Future<Either<Failure, Map<String, dynamic>>> estimateSell(String quantity);
required String amount,
required String direction, /// ()
}); 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 'package:dartz/dartz.dart';
import '../../../core/error/failures.dart'; import '../../../core/error/failures.dart';
import '../../entities/price_info.dart';
import '../../repositories/trading_repository.dart'; import '../../repositories/trading_repository.dart';
class GetCurrentPrice { class GetCurrentPrice {
@ -7,7 +8,7 @@ class GetCurrentPrice {
GetCurrentPrice(this.repository); GetCurrentPrice(this.repository);
Future<Either<Failure, String>> call() async { Future<Either<Failure, PriceInfo>> call() async {
return await repository.getCurrentPrice(); 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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/utils/format_utils.dart'; import '../../../core/utils/format_utils.dart';
import '../../../domain/entities/asset_display.dart';
import '../../providers/user_providers.dart'; import '../../providers/user_providers.dart';
import '../../providers/mining_providers.dart'; import '../../providers/asset_providers.dart';
import '../../widgets/shimmer_loading.dart'; import '../../widgets/shimmer_loading.dart';
class AssetPage extends ConsumerWidget { class AssetPage extends ConsumerWidget {
@ -23,12 +24,11 @@ class AssetPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userNotifierProvider); final user = ref.watch(userNotifierProvider);
final accountSequence = user.accountSequence ?? ''; final assetAsync = ref.watch(myAssetProvider);
final accountAsync = ref.watch(shareAccountProvider(accountSequence));
// //
final isLoading = accountAsync.isLoading; final isLoading = assetAsync.isLoading;
final account = accountAsync.valueOrNull; final asset = assetAsync.valueOrNull;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
@ -38,7 +38,7 @@ class AssetPage extends ConsumerWidget {
builder: (context, constraints) { builder: (context, constraints) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
ref.invalidate(shareAccountProvider(accountSequence)); ref.invalidate(myAssetProvider);
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
@ -55,19 +55,19 @@ class AssetPage extends ConsumerWidget {
children: [ children: [
const SizedBox(height: 8), const SizedBox(height: 8),
// - // -
_buildTotalAssetCard(account, isLoading), _buildTotalAssetCard(asset, isLoading),
const SizedBox(height: 24), const SizedBox(height: 24),
// //
_buildQuickActions(), _buildQuickActions(),
const SizedBox(height: 24), const SizedBox(height: 24),
// - // -
_buildAssetList(account, isLoading), _buildAssetList(asset, isLoading),
const SizedBox(height: 24), const SizedBox(height: 24),
// //
_buildEarningsCard(account, isLoading), _buildEarningsCard(asset, isLoading),
const SizedBox(height: 24), const SizedBox(height: 24),
// //
_buildAccountList(account, isLoading), _buildAccountList(asset, isLoading),
const SizedBox(height: 100), 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( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@ -254,7 +259,7 @@ class AssetPage extends ConsumerWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
// - // -
AmountText( AmountText(
amount: account != null ? formatAmount(account.tradingBalance ?? '0') : null, amount: asset != null ? formatAmount(asset.displayAssetValue) : null,
isLoading: isLoading, isLoading: isLoading,
prefix: '¥ ', prefix: '¥ ',
style: const TextStyle( style: const TextStyle(
@ -265,18 +270,18 @@ class AssetPage extends ConsumerWidget {
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
// USDT估值 //
DataText( DataText(
data: account != null ? '≈ 12,345.67 USDT' : null, data: asset != null ? '${formatCompact(asset.effectiveShares)} 积分股 (含倍数)' : null,
isLoading: isLoading, isLoading: isLoading,
placeholder: '≈ -- USDT', placeholder: '≈ -- 积分股',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF9CA3AF), color: Color(0xFF9CA3AF),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// //
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -286,12 +291,14 @@ class AssetPage extends ConsumerWidget {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.trending_up, size: 14, color: _green), const Icon(Icons.bolt, size: 14, color: _green),
const SizedBox(width: 6), const SizedBox(width: 6),
DataText( DataText(
data: account != null ? '+¥ 156.78 今日' : null, data: asset != null
? '+${formatDecimal(growthPerSecond.toString(), 8)}/秒'
: null,
isLoading: isLoading, isLoading: isLoading,
placeholder: '+¥ -- 今日', placeholder: '+--/秒',
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, 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( return Column(
children: [ children: [
// //
@ -355,41 +367,49 @@ class AssetPage extends ConsumerWidget {
iconColor: _orange, iconColor: _orange,
iconBgColor: _serenade, iconBgColor: _serenade,
title: '积分股', title: '积分股',
amount: account?.miningBalance, amount: asset?.shareBalance,
isLoading: isLoading, isLoading: isLoading,
valueInCny: '¥15,234.56', valueInCny: asset != null
tag: '含倍数资产: 246,913.56', ? '¥${formatAmount(_calculateValue(asset.shareBalance, asset.currentPrice))}'
growthText: '每秒 +0.0015', : null,
tag: asset != null ? '含倍数资产: ${formatCompact(multipliedAsset.toString())}' : null,
growthText: asset != null ? '每秒 +${formatDecimal(asset.assetGrowthPerSecond, 8)}' : null,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// 绿 // 绿
_buildAssetItem( _buildAssetItem(
icon: Icons.eco, icon: Icons.eco,
iconColor: _green, iconColor: _green,
iconBgColor: _feta, iconBgColor: _feta,
title: '绿积分', title: '绿积分',
amount: account?.tradingBalance, amount: asset?.cashBalance,
isLoading: isLoading, isLoading: isLoading,
valueInCny: '¥10,986.54', valueInCny: asset != null ? '¥${formatAmount(asset.cashBalance)}' : null,
badge: '可提现', badge: '可提现',
badgeColor: _jewel, badgeColor: _jewel,
badgeBgColor: _scandal, badgeBgColor: _scandal,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
_buildAssetItem( _buildAssetItem(
icon: Icons.hourglass_empty, icon: Icons.lock_outline,
iconColor: _orange, iconColor: _orange,
iconBgColor: _serenade, iconBgColor: _serenade,
title: '待分配积分股', title: '冻结积分股',
amount: '1,234.56', amount: asset?.frozenShares,
isLoading: isLoading, 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({ Widget _buildAssetItem({
required IconData icon, required IconData icon,
required Color iconColor, required Color iconColor,
@ -555,7 +575,7 @@ class AssetPage extends ConsumerWidget {
); );
} }
Widget _buildEarningsCard(account, bool isLoading) { Widget _buildEarningsCard(AssetDisplay? asset, bool isLoading) {
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -584,7 +604,7 @@ class AssetPage extends ConsumerWidget {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text( const Text(
'收益统计', '交易统计',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -597,19 +617,34 @@ class AssetPage extends ConsumerWidget {
// //
Row( Row(
children: [ children: [
_buildEarningsItem('累计收益', isLoading ? null : '12,345.67', _orange, isLoading), _buildEarningsItem(
'累计买入',
asset != null ? formatCompact(asset.totalBought) : null,
_orange,
isLoading,
),
Container( Container(
width: 1, width: 1,
height: 40, height: 40,
color: _serenade, color: _serenade,
), ),
_buildEarningsItem('今日收益', isLoading ? null : '+156.78', _green, isLoading), _buildEarningsItem(
'累计卖出',
asset != null ? formatCompact(asset.totalSold) : null,
_green,
isLoading,
),
Container( Container(
width: 1, width: 1,
height: 40, height: 40,
color: _serenade, 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( return Column(
children: [ children: [
// //
_buildAccountItem( _buildAccountItem(
icon: Icons.account_balance_wallet, icon: Icons.account_balance_wallet,
iconColor: _orange, iconColor: _orange,
title: '交易账户', title: '可用绿积分',
balance: account?.tradingBalance, balance: asset?.availableCash,
isLoading: isLoading, isLoading: isLoading,
unit: '绿积分', unit: '绿积分',
status: '正常', status: '可交易',
statusColor: _green, statusColor: _green,
statusBgColor: _feta, statusBgColor: _feta,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
_buildAccountItem( _buildAccountItem(
icon: Icons.savings, icon: Icons.lock_outline,
iconColor: _orange, iconColor: _orange,
title: '提现账户', title: '冻结绿积分',
balance: '1,234.56', balance: asset?.frozenCash,
isLoading: isLoading, isLoading: isLoading,
unit: '绿积分', unit: '绿积分',
status: '已绑定', status: '挂单中',
statusColor: const Color(0xFF9CA3AF), statusColor: const Color(0xFF9CA3AF),
statusBgColor: Colors.white, statusBgColor: Colors.white,
statusBorder: true, statusBorder: true,
@ -734,7 +769,7 @@ class AssetPage extends ConsumerWidget {
Row( Row(
children: [ children: [
DataText( DataText(
data: balance, data: balance != null ? formatAmount(balance) : null,
isLoading: isLoading, isLoading: isLoading,
placeholder: '--', placeholder: '--',
style: const TextStyle( 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) { Widget _buildTotalContributionCard(Contribution? contribution, bool isLoading) {
final total = contribution?.effectiveContribution ?? '0'; final total = contribution?.totalContribution ?? '0';
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -226,8 +226,8 @@ class ContributionPage extends ConsumerWidget {
child: Row( child: Row(
children: [ children: [
_buildStatColumn('个人贡献值', contribution?.personalContribution, isLoading, false), _buildStatColumn('个人贡献值', contribution?.personalContribution, isLoading, false),
_buildStatColumn('团队贡献值', contribution?.teamLevelContribution, isLoading, true), _buildStatColumn('团队层级', contribution?.teamLevelContribution, isLoading, true),
_buildStatColumn('省市公司', contribution?.systemContribution, isLoading, true), _buildStatColumn('团队奖励', contribution?.teamBonusContribution, isLoading, true),
], ],
), ),
); );
@ -261,10 +261,10 @@ class ContributionPage extends ConsumerWidget {
Widget _buildTodayEstimateCard(Contribution? contribution, bool isLoading) { Widget _buildTodayEstimateCard(Contribution? contribution, bool isLoading) {
// API // API
final effectiveContribution = double.tryParse(contribution?.effectiveContribution ?? '0') ?? 0; final totalContribution = double.tryParse(contribution?.totalContribution ?? '0') ?? 0;
// 10000 // 10000
// "--" // "--"
final hasContribution = effectiveContribution > 0; final hasContribution = totalContribution > 0;
return Container( return Container(
padding: const EdgeInsets.all(20), 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 'package:go_router/go_router.dart';
import '../../../core/router/routes.dart'; import '../../../core/router/routes.dart';
import '../../providers/user_providers.dart'; import '../../providers/user_providers.dart';
import '../../providers/profile_providers.dart';
import '../../widgets/shimmer_loading.dart';
class ProfilePage extends ConsumerWidget { class ProfilePage extends ConsumerWidget {
const ProfilePage({super.key}); const ProfilePage({super.key});
@ -20,72 +22,84 @@ class ProfilePage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userNotifierProvider); 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( return Scaffold(
backgroundColor: _bgGray, backgroundColor: _bgGray,
body: SafeArea( body: SafeArea(
bottom: false, bottom: false,
child: SingleChildScrollView( child: RefreshIndicator(
child: Column( onRefresh: () async {
children: [ ref.invalidate(userStatsProvider);
// await ref.read(userNotifierProvider.notifier).fetchProfile();
_buildUserHeader(context, user), },
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( const Text(
'Version 1.0.0', 'Version 1.0.0',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: _lightGray, color: _lightGray,
),
), ),
),
const SizedBox(height: 100), const SizedBox(height: 100),
], ],
),
), ),
), ),
), ),
); );
} }
Widget _buildUserHeader(BuildContext context, dynamic user) { Widget _buildUserHeader(BuildContext context, UserState user) {
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
color: Colors.white, color: Colors.white,
@ -107,7 +121,9 @@ class ProfilePage extends ConsumerWidget {
child: Text( child: Text(
user.nickname?.isNotEmpty == true user.nickname?.isNotEmpty == true
? user.nickname!.substring(0, 1).toUpperCase() ? user.nickname!.substring(0, 1).toUpperCase()
: 'U', : (user.realName?.isNotEmpty == true
? user.realName!.substring(0, 1).toUpperCase()
: 'U'),
style: const TextStyle( style: const TextStyle(
fontSize: 36, fontSize: 36,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -127,7 +143,7 @@ class ProfilePage extends ConsumerWidget {
Row( Row(
children: [ children: [
Text( Text(
user.nickname ?? '榴莲用户', user.realName ?? user.nickname ?? '榴莲用户',
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -135,27 +151,33 @@ class ProfilePage extends ConsumerWidget {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
// VIP //
Container( if (user.isKycVerified)
padding: const EdgeInsets.symmetric( Container(
horizontal: 8, padding: const EdgeInsets.symmetric(
vertical: 2, horizontal: 8,
), vertical: 2,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFFD700), Color(0xFFFF8C00)],
), ),
borderRadius: BorderRadius.circular(10), decoration: BoxDecoration(
), color: _green.withValues(alpha: 0.1),
child: const Text( borderRadius: BorderRadius.circular(10),
'VIP 3', ),
style: TextStyle( child: const Row(
fontSize: 10, mainAxisSize: MainAxisSize.min,
fontWeight: FontWeight.bold, children: [
color: Colors.white, Icon(Icons.verified, size: 12, color: _green),
SizedBox(width: 2),
Text(
'已实名',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: _green,
),
),
],
), ),
), ),
),
], ],
), ),
const SizedBox(height: 8), 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( return Container(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
color: Colors.white, color: Colors.white,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildStatItem('认种数量', '10'), _buildStatItem(
'认种状态',
stats?.hasAdopted == true ? '已认种' : '未认种',
isLoading,
),
_buildDivider(), _buildDivider(),
_buildStatItem('直推人数', '5'), _buildStatItem(
'直推人数',
stats?.directReferralAdoptedCount.toString() ?? '0',
isLoading,
),
_buildDivider(), _buildDivider(),
_buildStatItem('团队人数', '128'), _buildStatItem(
'团队层级',
stats?.unlockedLevelDepth.toString() ?? '0',
isLoading,
),
_buildDivider(), _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( return Column(
children: [ children: [
Text( DataText(
value, data: isLoading ? null : value,
isLoading: isLoading,
placeholder: '--',
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -260,9 +312,7 @@ class ProfilePage extends ConsumerWidget {
); );
} }
Widget _buildInvitationCard(BuildContext context) { Widget _buildInvitationCard(BuildContext context, String invitationCode) {
const invitationCode = 'DUR8888XYZ';
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -293,9 +343,9 @@ class ProfilePage extends ConsumerWidget {
color: _bgGray, color: _bgGray,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Text( child: Text(
invitationCode, invitationCode,
style: TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _darkText, color: _darkText,
@ -309,7 +359,7 @@ class ProfilePage extends ConsumerWidget {
icon: Icons.copy, icon: Icons.copy,
label: '复制', label: '复制',
onTap: () { onTap: () {
Clipboard.setData(const ClipboardData(text: invitationCode)); Clipboard.setData(ClipboardData(text: invitationCode));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('邀请码已复制'), content: Text('邀请码已复制'),
@ -364,7 +414,7 @@ class ProfilePage extends ConsumerWidget {
); );
} }
Widget _buildAccountSettings(BuildContext context) { Widget _buildAccountSettings(BuildContext context, UserState user) {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -393,11 +443,11 @@ class ProfilePage extends ConsumerWidget {
_buildSettingItem( _buildSettingItem(
icon: Icons.verified_user, icon: Icons.verified_user,
label: '实名认证', label: '实名认证',
trailing: const Text( trailing: Text(
'认证', user.isKycVerified ? '认证' : '认证',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: _green, color: user.isKycVerified ? _green : _lightGray,
), ),
), ),
onTap: () {}, onTap: () {},

View File

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

View File

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