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:
parent
106a287260
commit
6bcb4af028
|
|
@ -15,9 +15,6 @@ import '../../domain/repositories/trading_repository.dart';
|
|||
import '../../domain/repositories/contribution_repository.dart';
|
||||
import '../../domain/usecases/mining/get_share_account.dart';
|
||||
import '../../domain/usecases/mining/get_global_state.dart';
|
||||
import '../../domain/usecases/trading/get_current_price.dart';
|
||||
import '../../domain/usecases/trading/sell_shares.dart';
|
||||
import '../../domain/usecases/trading/buy_shares.dart';
|
||||
import '../../domain/usecases/contribution/get_user_contribution.dart';
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
|
|
@ -72,11 +69,6 @@ Future<void> configureDependencies() async {
|
|||
getIt.registerLazySingleton(() => GetShareAccount(getIt<MiningRepository>()));
|
||||
getIt.registerLazySingleton(() => GetGlobalState(getIt<MiningRepository>()));
|
||||
|
||||
// Use Cases - Trading
|
||||
getIt.registerLazySingleton(() => GetCurrentPrice(getIt<TradingRepository>()));
|
||||
getIt.registerLazySingleton(() => SellShares(getIt<TradingRepository>()));
|
||||
getIt.registerLazySingleton(() => BuyShares(getIt<TradingRepository>()));
|
||||
|
||||
// Use Cases - Contribution
|
||||
getIt.registerLazySingleton(() => GetUserContribution(getIt<ContributionRepository>()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,16 +23,31 @@ class ApiEndpoints {
|
|||
'/api/v2/mining/accounts/$accountSequence/realtime';
|
||||
|
||||
// Trading Service 2.0 (Kong路由: /api/v2/trading)
|
||||
static const String currentPrice = '/api/v2/trading/price';
|
||||
static const String klineData = '/api/v2/trading/kline';
|
||||
// Price endpoints
|
||||
static const String currentPrice = '/api/v2/trading/price/current';
|
||||
static const String latestPrice = '/api/v2/trading/price/latest';
|
||||
static const String priceHistory = '/api/v2/trading/price/history';
|
||||
|
||||
// Trading account endpoints
|
||||
static String tradingAccount(String accountSequence) =>
|
||||
'/api/v2/trading/accounts/$accountSequence';
|
||||
static String createOrder(String accountSequence) =>
|
||||
'/api/v2/trading/accounts/$accountSequence/orders';
|
||||
static String orders(String accountSequence) =>
|
||||
'/api/v2/trading/accounts/$accountSequence/orders';
|
||||
static String transfer(String accountSequence) =>
|
||||
'/api/v2/trading/accounts/$accountSequence/transfer';
|
||||
'/api/v2/trading/trading/accounts/$accountSequence';
|
||||
static const String orderBook = '/api/v2/trading/trading/orderbook';
|
||||
static const String createOrder = '/api/v2/trading/trading/orders';
|
||||
static const String orders = '/api/v2/trading/trading/orders';
|
||||
static String cancelOrder(String orderNo) =>
|
||||
'/api/v2/trading/trading/orders/$orderNo/cancel';
|
||||
|
||||
// Asset endpoints
|
||||
static const String myAsset = '/api/v2/trading/asset/my';
|
||||
static String accountAsset(String accountSequence) =>
|
||||
'/api/v2/trading/asset/account/$accountSequence';
|
||||
static const String estimateSell = '/api/v2/trading/asset/estimate-sell';
|
||||
static const String marketOverview = '/api/v2/trading/asset/market';
|
||||
|
||||
// Transfer endpoints
|
||||
static const String transferIn = '/api/v2/trading/transfers/in';
|
||||
static const String transferOut = '/api/v2/trading/transfers/out';
|
||||
static const String transferHistory = '/api/v2/trading/transfers/history';
|
||||
|
||||
// Contribution Service 2.0 (Kong路由: /api/v2/contribution -> /api/v1/contributions)
|
||||
static String contribution(String accountSequence) =>
|
||||
|
|
|
|||
|
|
@ -30,12 +30,20 @@ class UserInfo {
|
|||
final String phone;
|
||||
final String source;
|
||||
final String kycStatus;
|
||||
final String? status;
|
||||
final String? realName;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? lastLoginAt;
|
||||
|
||||
UserInfo({
|
||||
required this.accountSequence,
|
||||
required this.phone,
|
||||
required this.source,
|
||||
required this.kycStatus,
|
||||
this.status,
|
||||
this.realName,
|
||||
this.createdAt,
|
||||
this.lastLoginAt,
|
||||
});
|
||||
|
||||
factory UserInfo.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -44,8 +52,18 @@ class UserInfo {
|
|||
phone: json['phone'] as String,
|
||||
source: json['source'] as String,
|
||||
kycStatus: json['kycStatus'] as String,
|
||||
status: json['status'] as String?,
|
||||
realName: json['realName'] as String?,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'].toString())
|
||||
: null,
|
||||
lastLoginAt: json['lastLoginAt'] != null
|
||||
? DateTime.parse(json['lastLoginAt'].toString())
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isKycVerified => kycStatus == 'VERIFIED';
|
||||
}
|
||||
|
||||
abstract class AuthRemoteDataSource {
|
||||
|
|
|
|||
|
|
@ -1,30 +1,52 @@
|
|||
import '../../models/trade_order_model.dart';
|
||||
import '../../models/kline_model.dart';
|
||||
import '../../models/price_info_model.dart';
|
||||
import '../../models/trading_account_model.dart';
|
||||
import '../../models/market_overview_model.dart';
|
||||
import '../../models/asset_display_model.dart';
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/network/api_endpoints.dart';
|
||||
import '../../../core/error/exceptions.dart';
|
||||
|
||||
abstract class TradingRemoteDataSource {
|
||||
Future<String> getCurrentPrice();
|
||||
Future<List<KlineModel>> getKlineData(String period);
|
||||
Future<TradeOrderModel> buyShares({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
/// 获取当前价格信息
|
||||
Future<PriceInfoModel> getCurrentPrice();
|
||||
|
||||
/// 获取市场概览
|
||||
Future<MarketOverviewModel> getMarketOverview();
|
||||
|
||||
/// 获取交易账户信息
|
||||
Future<TradingAccountModel> getTradingAccount(String accountSequence);
|
||||
|
||||
/// 创建订单
|
||||
Future<Map<String, dynamic>> createOrder({
|
||||
required String type,
|
||||
required String price,
|
||||
required String quantity,
|
||||
});
|
||||
Future<TradeOrderModel> sellShares({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
});
|
||||
Future<List<TradeOrderModel>> getOrders(
|
||||
String accountSequence, {
|
||||
|
||||
/// 取消订单
|
||||
Future<void> cancelOrder(String orderNo);
|
||||
|
||||
/// 获取用户订单列表
|
||||
Future<OrdersPageModel> getOrders({
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
});
|
||||
Future<void> transfer({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
required String direction,
|
||||
int pageSize = 50,
|
||||
});
|
||||
|
||||
/// 预估卖出收益
|
||||
Future<Map<String, dynamic>> estimateSell(String quantity);
|
||||
|
||||
/// 划入积分股 (从挖矿账户到交易账户)
|
||||
Future<Map<String, dynamic>> transferIn(String amount);
|
||||
|
||||
/// 划出积分股 (从交易账户到挖矿账户)
|
||||
Future<Map<String, dynamic>> transferOut(String amount);
|
||||
|
||||
/// 获取我的资产显示信息
|
||||
Future<AssetDisplayModel> getMyAsset({String? dailyAllocation});
|
||||
|
||||
/// 获取指定账户资产显示信息
|
||||
Future<AssetDisplayModel> getAccountAsset(String accountSequence, {String? dailyAllocation});
|
||||
}
|
||||
|
||||
class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
|
||||
|
|
@ -33,90 +55,149 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
|
|||
TradingRemoteDataSourceImpl({required this.client});
|
||||
|
||||
@override
|
||||
Future<String> getCurrentPrice() async {
|
||||
Future<PriceInfoModel> getCurrentPrice() async {
|
||||
try {
|
||||
final response = await client.get(ApiEndpoints.currentPrice);
|
||||
return response.data['price']?.toString() ?? '0';
|
||||
return PriceInfoModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<KlineModel>> getKlineData(String period) async {
|
||||
Future<MarketOverviewModel> getMarketOverview() async {
|
||||
try {
|
||||
final response = await client.get(
|
||||
ApiEndpoints.klineData,
|
||||
queryParameters: {'period': period},
|
||||
);
|
||||
final items = response.data as List? ?? [];
|
||||
return items.map((json) => KlineModel.fromJson(json)).toList();
|
||||
final response = await client.get(ApiEndpoints.marketOverview);
|
||||
return MarketOverviewModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TradeOrderModel> buyShares({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
Future<TradingAccountModel> getTradingAccount(String accountSequence) async {
|
||||
try {
|
||||
final response = await client.get(ApiEndpoints.tradingAccount(accountSequence));
|
||||
return TradingAccountModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> createOrder({
|
||||
required String type,
|
||||
required String price,
|
||||
required String quantity,
|
||||
}) async {
|
||||
try {
|
||||
final response = await client.post(
|
||||
ApiEndpoints.createOrder(accountSequence),
|
||||
data: {'orderType': 'BUY', 'quantity': amount},
|
||||
ApiEndpoints.createOrder,
|
||||
data: {
|
||||
'type': type,
|
||||
'price': price,
|
||||
'quantity': quantity,
|
||||
},
|
||||
);
|
||||
return TradeOrderModel.fromJson(response.data);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TradeOrderModel> sellShares({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
}) async {
|
||||
Future<void> cancelOrder(String orderNo) async {
|
||||
try {
|
||||
final response = await client.post(
|
||||
ApiEndpoints.createOrder(accountSequence),
|
||||
data: {'orderType': 'SELL', 'quantity': amount},
|
||||
);
|
||||
return TradeOrderModel.fromJson(response.data);
|
||||
await client.post(ApiEndpoints.cancelOrder(orderNo));
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TradeOrderModel>> getOrders(
|
||||
String accountSequence, {
|
||||
Future<OrdersPageModel> getOrders({
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
int pageSize = 50,
|
||||
}) async {
|
||||
try {
|
||||
final response = await client.get(
|
||||
ApiEndpoints.orders(accountSequence),
|
||||
queryParameters: {'page': page, 'pageSize': limit},
|
||||
ApiEndpoints.orders,
|
||||
queryParameters: {'page': page, 'pageSize': pageSize},
|
||||
);
|
||||
final items = response.data['items'] as List? ?? [];
|
||||
return items.map((json) => TradeOrderModel.fromJson(json)).toList();
|
||||
return OrdersPageModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> transfer({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
required String direction,
|
||||
}) async {
|
||||
Future<Map<String, dynamic>> estimateSell(String quantity) async {
|
||||
try {
|
||||
await client.post(
|
||||
ApiEndpoints.transfer(accountSequence),
|
||||
data: {'amount': amount, 'direction': direction},
|
||||
final response = await client.get(
|
||||
ApiEndpoints.estimateSell,
|
||||
queryParameters: {'quantity': quantity},
|
||||
);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> transferIn(String amount) async {
|
||||
try {
|
||||
final response = await client.post(
|
||||
ApiEndpoints.transferIn,
|
||||
data: {'amount': amount},
|
||||
);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> transferOut(String amount) async {
|
||||
try {
|
||||
final response = await client.post(
|
||||
ApiEndpoints.transferOut,
|
||||
data: {'amount': amount},
|
||||
);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AssetDisplayModel> getMyAsset({String? dailyAllocation}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{};
|
||||
if (dailyAllocation != null) {
|
||||
queryParams['dailyAllocation'] = dailyAllocation;
|
||||
}
|
||||
final response = await client.get(
|
||||
ApiEndpoints.myAsset,
|
||||
queryParameters: queryParams.isNotEmpty ? queryParams : null,
|
||||
);
|
||||
return AssetDisplayModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AssetDisplayModel> getAccountAsset(String accountSequence, {String? dailyAllocation}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{};
|
||||
if (dailyAllocation != null) {
|
||||
queryParams['dailyAllocation'] = dailyAllocation;
|
||||
}
|
||||
final response = await client.get(
|
||||
ApiEndpoints.accountAsset(accountSequence),
|
||||
queryParameters: queryParams.isNotEmpty ? queryParams : null,
|
||||
);
|
||||
return AssetDisplayModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -2,32 +2,51 @@ import '../../domain/entities/contribution.dart';
|
|||
|
||||
class ContributionModel extends Contribution {
|
||||
const ContributionModel({
|
||||
required super.status,
|
||||
required super.message,
|
||||
required super.accountSequence,
|
||||
required super.personalContribution,
|
||||
required super.systemContribution,
|
||||
required super.teamLevelContribution,
|
||||
required super.teamBonusContribution,
|
||||
required super.totalContribution,
|
||||
required super.effectiveContribution,
|
||||
required super.hasAdopted,
|
||||
required super.directReferralAdoptedCount,
|
||||
required super.unlockedLevelDepth,
|
||||
required super.unlockedBonusTiers,
|
||||
required super.isCalculated,
|
||||
super.lastCalculatedAt,
|
||||
});
|
||||
|
||||
factory ContributionModel.fromJson(Map<String, dynamic> json) {
|
||||
return ContributionModel(
|
||||
status: _parseStatus(json['status']),
|
||||
message: json['message']?.toString() ?? '',
|
||||
accountSequence: json['accountSequence']?.toString() ?? '',
|
||||
personalContribution: json['personalContribution']?.toString() ?? '0',
|
||||
systemContribution: json['systemContribution']?.toString() ?? '0',
|
||||
teamLevelContribution: json['teamLevelContribution']?.toString() ?? '0',
|
||||
teamBonusContribution: json['teamBonusContribution']?.toString() ?? '0',
|
||||
totalContribution: json['totalContribution']?.toString() ?? '0',
|
||||
effectiveContribution: json['effectiveContribution']?.toString() ?? '0',
|
||||
hasAdopted: json['hasAdopted'] == true,
|
||||
directReferralAdoptedCount: json['directReferralAdoptedCount'] ?? 0,
|
||||
unlockedLevelDepth: json['unlockedLevelDepth'] ?? 0,
|
||||
unlockedBonusTiers: json['unlockedBonusTiers'] ?? 0,
|
||||
isCalculated: json['isCalculated'] == true,
|
||||
lastCalculatedAt: json['lastCalculatedAt'] != null
|
||||
? DateTime.tryParse(json['lastCalculatedAt'].toString())
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
static ContributionAccountStatus _parseStatus(String? status) {
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
return ContributionAccountStatus.active;
|
||||
case 'INACTIVE':
|
||||
return ContributionAccountStatus.inactive;
|
||||
case 'USER_NOT_FOUND':
|
||||
return ContributionAccountStatus.userNotFound;
|
||||
default:
|
||||
return ContributionAccountStatus.inactive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,36 +3,55 @@ import '../../domain/entities/trade_order.dart';
|
|||
class TradeOrderModel extends TradeOrder {
|
||||
const TradeOrderModel({
|
||||
required super.id,
|
||||
required super.accountSequence,
|
||||
required super.orderNo,
|
||||
required super.orderType,
|
||||
required super.status,
|
||||
required super.price,
|
||||
required super.quantity,
|
||||
required super.filledQuantity,
|
||||
required super.remainingQuantity,
|
||||
required super.averagePrice,
|
||||
required super.totalAmount,
|
||||
required super.createdAt,
|
||||
super.updatedAt,
|
||||
super.completedAt,
|
||||
super.cancelledAt,
|
||||
});
|
||||
|
||||
factory TradeOrderModel.fromJson(Map<String, dynamic> json) {
|
||||
return TradeOrderModel(
|
||||
id: json['id'] ?? '',
|
||||
accountSequence: json['accountSequence']?.toString() ?? '',
|
||||
orderType: json['orderType'] == 'BUY' ? OrderType.buy : OrderType.sell,
|
||||
id: json['id']?.toString() ?? '',
|
||||
orderNo: json['orderNo']?.toString() ?? '',
|
||||
orderType: _parseOrderType(json['type']),
|
||||
status: _parseStatus(json['status']),
|
||||
price: json['price']?.toString() ?? '0',
|
||||
quantity: json['quantity']?.toString() ?? '0',
|
||||
filledQuantity: json['filledQuantity']?.toString() ?? '0',
|
||||
remainingQuantity: json['remainingQuantity']?.toString() ?? '0',
|
||||
averagePrice: json['averagePrice']?.toString() ?? '0',
|
||||
totalAmount: json['totalAmount']?.toString() ?? '0',
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'])
|
||||
? DateTime.parse(json['createdAt'].toString())
|
||||
: DateTime.now(),
|
||||
updatedAt: json['updatedAt'] != null
|
||||
? DateTime.parse(json['updatedAt'])
|
||||
completedAt: json['completedAt'] != null
|
||||
? DateTime.parse(json['completedAt'].toString())
|
||||
: null,
|
||||
cancelledAt: json['cancelledAt'] != null
|
||||
? DateTime.parse(json['cancelledAt'].toString())
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
static OrderType _parseOrderType(String? type) {
|
||||
switch (type) {
|
||||
case 'BUY':
|
||||
return OrderType.buy;
|
||||
case 'SELL':
|
||||
return OrderType.sell;
|
||||
default:
|
||||
return OrderType.buy;
|
||||
}
|
||||
}
|
||||
|
||||
static OrderStatus _parseStatus(String? status) {
|
||||
switch (status) {
|
||||
case 'PENDING':
|
||||
|
|
@ -48,3 +67,22 @@ class TradeOrderModel extends TradeOrder {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 订单列表响应
|
||||
class OrdersPageModel {
|
||||
final List<TradeOrderModel> data;
|
||||
final int total;
|
||||
|
||||
const OrdersPageModel({
|
||||
required this.data,
|
||||
required this.total,
|
||||
});
|
||||
|
||||
factory OrdersPageModel.fromJson(Map<String, dynamic> json) {
|
||||
final dataList = (json['data'] as List<dynamic>?) ?? [];
|
||||
return OrdersPageModel(
|
||||
data: dataList.map((e) => TradeOrderModel.fromJson(e)).toList(),
|
||||
total: json['total'] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import 'package:dartz/dartz.dart';
|
||||
import '../../domain/entities/trade_order.dart';
|
||||
import '../../domain/entities/kline.dart';
|
||||
import '../../domain/entities/price_info.dart';
|
||||
import '../../domain/entities/market_overview.dart';
|
||||
import '../../domain/entities/trading_account.dart';
|
||||
import '../../domain/entities/asset_display.dart';
|
||||
import '../../domain/repositories/trading_repository.dart';
|
||||
import '../../core/error/exceptions.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../datasources/remote/trading_remote_datasource.dart';
|
||||
import '../models/trade_order_model.dart';
|
||||
|
||||
class TradingRepositoryImpl implements TradingRepository {
|
||||
final TradingRemoteDataSource remoteDataSource;
|
||||
|
|
@ -12,7 +15,7 @@ class TradingRepositoryImpl implements TradingRepository {
|
|||
TradingRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, String>> getCurrentPrice() async {
|
||||
Future<Either<Failure, PriceInfo>> getCurrentPrice() async {
|
||||
try {
|
||||
final result = await remoteDataSource.getCurrentPrice();
|
||||
return Right(result);
|
||||
|
|
@ -24,9 +27,9 @@ class TradingRepositoryImpl implements TradingRepository {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Kline>>> getKlineData(String period) async {
|
||||
Future<Either<Failure, MarketOverview>> getMarketOverview() async {
|
||||
try {
|
||||
final result = await remoteDataSource.getKlineData(period);
|
||||
final result = await remoteDataSource.getMarketOverview();
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
|
|
@ -36,14 +39,28 @@ class TradingRepositoryImpl implements TradingRepository {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, TradeOrder>> buyShares({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
Future<Either<Failure, TradingAccount>> getTradingAccount(String accountSequence) async {
|
||||
try {
|
||||
final result = await remoteDataSource.getTradingAccount(accountSequence);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException {
|
||||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> createOrder({
|
||||
required String type,
|
||||
required String price,
|
||||
required String quantity,
|
||||
}) async {
|
||||
try {
|
||||
final result = await remoteDataSource.buyShares(
|
||||
accountSequence: accountSequence,
|
||||
amount: amount,
|
||||
final result = await remoteDataSource.createOrder(
|
||||
type: type,
|
||||
price: price,
|
||||
quantity: quantity,
|
||||
);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
|
|
@ -54,55 +71,9 @@ class TradingRepositoryImpl implements TradingRepository {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, TradeOrder>> sellShares({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
}) async {
|
||||
Future<Either<Failure, void>> cancelOrder(String orderNo) async {
|
||||
try {
|
||||
final result = await remoteDataSource.sellShares(
|
||||
accountSequence: accountSequence,
|
||||
amount: amount,
|
||||
);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException {
|
||||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<TradeOrder>>> getOrders(
|
||||
String accountSequence, {
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final result = await remoteDataSource.getOrders(
|
||||
accountSequence,
|
||||
page: page,
|
||||
limit: limit,
|
||||
);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException {
|
||||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> transfer({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
required String direction,
|
||||
}) async {
|
||||
try {
|
||||
await remoteDataSource.transfer(
|
||||
accountSequence: accountSequence,
|
||||
amount: amount,
|
||||
direction: direction,
|
||||
);
|
||||
await remoteDataSource.cancelOrder(orderNo);
|
||||
return const Right(null);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
|
|
@ -110,4 +81,82 @@ class TradingRepositoryImpl implements TradingRepository {
|
|||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, OrdersPageModel>> getOrders({
|
||||
int page = 1,
|
||||
int pageSize = 50,
|
||||
}) async {
|
||||
try {
|
||||
final result = await remoteDataSource.getOrders(
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException {
|
||||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> estimateSell(String quantity) async {
|
||||
try {
|
||||
final result = await remoteDataSource.estimateSell(quantity);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException {
|
||||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> transferIn(String amount) async {
|
||||
try {
|
||||
final result = await remoteDataSource.transferIn(amount);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException {
|
||||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> transferOut(String amount) async {
|
||||
try {
|
||||
final result = await remoteDataSource.transferOut(amount);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException {
|
||||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, AssetDisplay>> getMyAsset({String? dailyAllocation}) async {
|
||||
try {
|
||||
final result = await remoteDataSource.getMyAsset(dailyAllocation: dailyAllocation);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException {
|
||||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, AssetDisplay>> getAccountAsset(String accountSequence, {String? dailyAllocation}) async {
|
||||
try {
|
||||
final result = await remoteDataSource.getAccountAsset(accountSequence, dailyAllocation: dailyAllocation);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException {
|
||||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
@ -1,41 +1,83 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// 账户状态枚举
|
||||
enum ContributionAccountStatus {
|
||||
/// 正常 - 账户存在且有算力数据
|
||||
active,
|
||||
/// 未激活 - 用户存在但暂无任何算力
|
||||
inactive,
|
||||
/// 用户不存在
|
||||
userNotFound,
|
||||
}
|
||||
|
||||
class Contribution extends Equatable {
|
||||
/// 账户状态
|
||||
final ContributionAccountStatus status;
|
||||
/// 状态说明
|
||||
final String message;
|
||||
/// 账户序号
|
||||
final String accountSequence;
|
||||
/// 个人算力
|
||||
final String personalContribution;
|
||||
final String systemContribution;
|
||||
/// 团队层级算力
|
||||
final String teamLevelContribution;
|
||||
/// 团队奖励算力
|
||||
final String teamBonusContribution;
|
||||
/// 总算力
|
||||
final String totalContribution;
|
||||
final String effectiveContribution;
|
||||
/// 是否已认种
|
||||
final bool hasAdopted;
|
||||
/// 直推认种用户数
|
||||
final int directReferralAdoptedCount;
|
||||
/// 已解锁层级深度
|
||||
final int unlockedLevelDepth;
|
||||
/// 已解锁奖励档位数
|
||||
final int unlockedBonusTiers;
|
||||
/// 是否已完成计算
|
||||
final bool isCalculated;
|
||||
/// 最后计算时间
|
||||
final DateTime? lastCalculatedAt;
|
||||
|
||||
const Contribution({
|
||||
required this.status,
|
||||
required this.message,
|
||||
required this.accountSequence,
|
||||
required this.personalContribution,
|
||||
required this.systemContribution,
|
||||
required this.teamLevelContribution,
|
||||
required this.teamBonusContribution,
|
||||
required this.totalContribution,
|
||||
required this.effectiveContribution,
|
||||
required this.hasAdopted,
|
||||
required this.directReferralAdoptedCount,
|
||||
required this.unlockedLevelDepth,
|
||||
required this.unlockedBonusTiers,
|
||||
required this.isCalculated,
|
||||
this.lastCalculatedAt,
|
||||
});
|
||||
|
||||
/// 团队总贡献值 (层级 + 奖励)
|
||||
String get teamContribution {
|
||||
final teamLevel = double.tryParse(teamLevelContribution) ?? 0;
|
||||
final teamBonus = double.tryParse(teamBonusContribution) ?? 0;
|
||||
return (teamLevel + teamBonus).toString();
|
||||
}
|
||||
|
||||
/// 是否处于活跃状态
|
||||
bool get isActive => status == ContributionAccountStatus.active;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
status,
|
||||
message,
|
||||
accountSequence,
|
||||
personalContribution,
|
||||
systemContribution,
|
||||
teamLevelContribution,
|
||||
teamBonusContribution,
|
||||
totalContribution,
|
||||
effectiveContribution,
|
||||
hasAdopted,
|
||||
directReferralAdoptedCount,
|
||||
unlockedLevelDepth,
|
||||
unlockedBonusTiers,
|
||||
isCalculated,
|
||||
lastCalculatedAt,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
@ -4,41 +4,64 @@ enum OrderType { buy, sell }
|
|||
enum OrderStatus { pending, partial, filled, cancelled }
|
||||
|
||||
class TradeOrder extends Equatable {
|
||||
/// 订单ID
|
||||
final String id;
|
||||
final String accountSequence;
|
||||
/// 订单号
|
||||
final String orderNo;
|
||||
/// 订单类型
|
||||
final OrderType orderType;
|
||||
/// 订单状态
|
||||
final OrderStatus status;
|
||||
/// 价格
|
||||
final String price;
|
||||
/// 数量
|
||||
final String quantity;
|
||||
/// 已成交数量
|
||||
final String filledQuantity;
|
||||
/// 剩余数量
|
||||
final String remainingQuantity;
|
||||
/// 平均成交价格
|
||||
final String averagePrice;
|
||||
/// 总金额
|
||||
final String totalAmount;
|
||||
/// 创建时间
|
||||
final DateTime createdAt;
|
||||
final DateTime? updatedAt;
|
||||
/// 完成时间
|
||||
final DateTime? completedAt;
|
||||
/// 取消时间
|
||||
final DateTime? cancelledAt;
|
||||
|
||||
const TradeOrder({
|
||||
required this.id,
|
||||
required this.accountSequence,
|
||||
required this.orderNo,
|
||||
required this.orderType,
|
||||
required this.status,
|
||||
required this.price,
|
||||
required this.quantity,
|
||||
required this.filledQuantity,
|
||||
required this.remainingQuantity,
|
||||
required this.averagePrice,
|
||||
required this.totalAmount,
|
||||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
this.completedAt,
|
||||
this.cancelledAt,
|
||||
});
|
||||
|
||||
bool get isBuy => orderType == OrderType.buy;
|
||||
bool get isSell => orderType == OrderType.sell;
|
||||
bool get isPending => status == OrderStatus.pending;
|
||||
bool get isPartial => status == OrderStatus.partial;
|
||||
bool get isFilled => status == OrderStatus.filled;
|
||||
bool get isCancelled => status == OrderStatus.cancelled;
|
||||
|
||||
String get remainingQuantity {
|
||||
/// 成交进度百分比
|
||||
double get fillProgress {
|
||||
final qty = double.tryParse(quantity) ?? 0;
|
||||
final filled = double.tryParse(filledQuantity) ?? 0;
|
||||
return (qty - filled).toString();
|
||||
if (qty == 0) return 0;
|
||||
return filled / qty;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, accountSequence, orderType, status, price, quantity];
|
||||
List<Object?> get props => [id, orderNo, orderType, status, price, quantity];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
@ -1,32 +1,50 @@
|
|||
import 'package:dartz/dartz.dart';
|
||||
import '../../core/error/failures.dart';
|
||||
import '../entities/price_info.dart';
|
||||
import '../entities/market_overview.dart';
|
||||
import '../entities/trading_account.dart';
|
||||
import '../entities/trade_order.dart';
|
||||
import '../entities/kline.dart';
|
||||
import '../entities/asset_display.dart';
|
||||
import '../../data/models/trade_order_model.dart';
|
||||
|
||||
abstract class TradingRepository {
|
||||
Future<Either<Failure, String>> getCurrentPrice();
|
||||
/// 获取当前价格信息
|
||||
Future<Either<Failure, PriceInfo>> getCurrentPrice();
|
||||
|
||||
Future<Either<Failure, List<Kline>>> getKlineData(String period);
|
||||
/// 获取市场概览
|
||||
Future<Either<Failure, MarketOverview>> getMarketOverview();
|
||||
|
||||
Future<Either<Failure, TradeOrder>> buyShares({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
/// 获取交易账户信息
|
||||
Future<Either<Failure, TradingAccount>> getTradingAccount(String accountSequence);
|
||||
|
||||
/// 创建订单
|
||||
Future<Either<Failure, Map<String, dynamic>>> createOrder({
|
||||
required String type,
|
||||
required String price,
|
||||
required String quantity,
|
||||
});
|
||||
|
||||
Future<Either<Failure, TradeOrder>> sellShares({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
});
|
||||
/// 取消订单
|
||||
Future<Either<Failure, void>> cancelOrder(String orderNo);
|
||||
|
||||
Future<Either<Failure, List<TradeOrder>>> getOrders(
|
||||
String accountSequence, {
|
||||
/// 获取用户订单列表
|
||||
Future<Either<Failure, OrdersPageModel>> getOrders({
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
int pageSize = 50,
|
||||
});
|
||||
|
||||
Future<Either<Failure, void>> transfer({
|
||||
required String accountSequence,
|
||||
required String amount,
|
||||
required String direction,
|
||||
});
|
||||
/// 预估卖出收益
|
||||
Future<Either<Failure, Map<String, dynamic>>> estimateSell(String quantity);
|
||||
|
||||
/// 划入积分股 (从挖矿账户到交易账户)
|
||||
Future<Either<Failure, Map<String, dynamic>>> transferIn(String amount);
|
||||
|
||||
/// 划出积分股 (从交易账户到挖矿账户)
|
||||
Future<Either<Failure, Map<String, dynamic>>> transferOut(String amount);
|
||||
|
||||
/// 获取我的资产显示信息
|
||||
Future<Either<Failure, AssetDisplay>> getMyAsset({String? dailyAllocation});
|
||||
|
||||
/// 获取指定账户资产显示信息
|
||||
Future<Either<Failure, AssetDisplay>> getAccountAsset(String accountSequence, {String? dailyAllocation});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:dartz/dartz.dart';
|
||||
import '../../../core/error/failures.dart';
|
||||
import '../../entities/price_info.dart';
|
||||
import '../../repositories/trading_repository.dart';
|
||||
|
||||
class GetCurrentPrice {
|
||||
|
|
@ -7,7 +8,7 @@ class GetCurrentPrice {
|
|||
|
||||
GetCurrentPrice(this.repository);
|
||||
|
||||
Future<Either<Failure, String>> call() async {
|
||||
Future<Either<Failure, PriceInfo>> call() async {
|
||||
return await repository.getCurrentPrice();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/utils/format_utils.dart';
|
||||
import '../../../domain/entities/asset_display.dart';
|
||||
import '../../providers/user_providers.dart';
|
||||
import '../../providers/mining_providers.dart';
|
||||
import '../../providers/asset_providers.dart';
|
||||
import '../../widgets/shimmer_loading.dart';
|
||||
|
||||
class AssetPage extends ConsumerWidget {
|
||||
|
|
@ -23,12 +24,11 @@ class AssetPage extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final user = ref.watch(userNotifierProvider);
|
||||
final accountSequence = user.accountSequence ?? '';
|
||||
final accountAsync = ref.watch(shareAccountProvider(accountSequence));
|
||||
final assetAsync = ref.watch(myAssetProvider);
|
||||
|
||||
// 提取数据和加载状态
|
||||
final isLoading = accountAsync.isLoading;
|
||||
final account = accountAsync.valueOrNull;
|
||||
final isLoading = assetAsync.isLoading;
|
||||
final asset = assetAsync.valueOrNull;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
|
|
@ -38,7 +38,7 @@ class AssetPage extends ConsumerWidget {
|
|||
builder: (context, constraints) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(shareAccountProvider(accountSequence));
|
||||
ref.invalidate(myAssetProvider);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
|
|
@ -55,19 +55,19 @@ class AssetPage extends ConsumerWidget {
|
|||
children: [
|
||||
const SizedBox(height: 8),
|
||||
// 总资产卡片 - 始终显示,数字部分闪烁
|
||||
_buildTotalAssetCard(account, isLoading),
|
||||
_buildTotalAssetCard(asset, isLoading),
|
||||
const SizedBox(height: 24),
|
||||
// 快捷操作按钮
|
||||
_buildQuickActions(),
|
||||
const SizedBox(height: 24),
|
||||
// 资产列表 - 始终显示,数字部分闪烁
|
||||
_buildAssetList(account, isLoading),
|
||||
_buildAssetList(asset, isLoading),
|
||||
const SizedBox(height: 24),
|
||||
// 收益统计
|
||||
_buildEarningsCard(account, isLoading),
|
||||
// 交易统计
|
||||
_buildEarningsCard(asset, isLoading),
|
||||
const SizedBox(height: 24),
|
||||
// 账户列表
|
||||
_buildAccountList(account, isLoading),
|
||||
_buildAccountList(asset, isLoading),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
|
|
@ -181,7 +181,12 @@ class AssetPage extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildTotalAssetCard(account, bool isLoading) {
|
||||
Widget _buildTotalAssetCard(AssetDisplay? asset, bool isLoading) {
|
||||
// 计算每秒增长
|
||||
final growthPerSecond = asset != null
|
||||
? AssetValueCalculator.calculateGrowthPerSecond(asset.assetGrowthPerSecond)
|
||||
: 0.0;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
|
|
@ -254,7 +259,7 @@ class AssetPage extends ConsumerWidget {
|
|||
const SizedBox(height: 8),
|
||||
// 金额 - 闪烁占位符
|
||||
AmountText(
|
||||
amount: account != null ? formatAmount(account.tradingBalance ?? '0') : null,
|
||||
amount: asset != null ? formatAmount(asset.displayAssetValue) : null,
|
||||
isLoading: isLoading,
|
||||
prefix: '¥ ',
|
||||
style: const TextStyle(
|
||||
|
|
@ -265,18 +270,18 @@ class AssetPage extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// USDT估值
|
||||
// 有效积分股
|
||||
DataText(
|
||||
data: account != null ? '≈ 12,345.67 USDT' : null,
|
||||
data: asset != null ? '≈ ${formatCompact(asset.effectiveShares)} 积分股 (含倍数)' : null,
|
||||
isLoading: isLoading,
|
||||
placeholder: '≈ -- USDT',
|
||||
placeholder: '≈ -- 积分股',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF9CA3AF),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 今日收益
|
||||
// 每秒增长
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -286,12 +291,14 @@ class AssetPage extends ConsumerWidget {
|
|||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.trending_up, size: 14, color: _green),
|
||||
const Icon(Icons.bolt, size: 14, color: _green),
|
||||
const SizedBox(width: 6),
|
||||
DataText(
|
||||
data: account != null ? '+¥ 156.78 今日' : null,
|
||||
data: asset != null
|
||||
? '+${formatDecimal(growthPerSecond.toString(), 8)}/秒'
|
||||
: null,
|
||||
isLoading: isLoading,
|
||||
placeholder: '+¥ -- 今日',
|
||||
placeholder: '+--/秒',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
|
@ -346,7 +353,12 @@ class AssetPage extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildAssetList(account, bool isLoading) {
|
||||
Widget _buildAssetList(AssetDisplay? asset, bool isLoading) {
|
||||
// 计算倍数资产
|
||||
final shareBalance = double.tryParse(asset?.shareBalance ?? '0') ?? 0;
|
||||
final multiplier = double.tryParse(asset?.burnMultiplier ?? '0') ?? 0;
|
||||
final multipliedAsset = shareBalance * multiplier;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 积分股
|
||||
|
|
@ -355,41 +367,49 @@ class AssetPage extends ConsumerWidget {
|
|||
iconColor: _orange,
|
||||
iconBgColor: _serenade,
|
||||
title: '积分股',
|
||||
amount: account?.miningBalance,
|
||||
amount: asset?.shareBalance,
|
||||
isLoading: isLoading,
|
||||
valueInCny: '¥15,234.56',
|
||||
tag: '含倍数资产: 246,913.56',
|
||||
growthText: '每秒 +0.0015',
|
||||
valueInCny: asset != null
|
||||
? '¥${formatAmount(_calculateValue(asset.shareBalance, asset.currentPrice))}'
|
||||
: null,
|
||||
tag: asset != null ? '含倍数资产: ${formatCompact(multipliedAsset.toString())}' : null,
|
||||
growthText: asset != null ? '每秒 +${formatDecimal(asset.assetGrowthPerSecond, 8)}' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 绿积分
|
||||
// 绿积分(现金余额)
|
||||
_buildAssetItem(
|
||||
icon: Icons.eco,
|
||||
iconColor: _green,
|
||||
iconBgColor: _feta,
|
||||
title: '绿积分',
|
||||
amount: account?.tradingBalance,
|
||||
amount: asset?.cashBalance,
|
||||
isLoading: isLoading,
|
||||
valueInCny: '¥10,986.54',
|
||||
valueInCny: asset != null ? '¥${formatAmount(asset.cashBalance)}' : null,
|
||||
badge: '可提现',
|
||||
badgeColor: _jewel,
|
||||
badgeBgColor: _scandal,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 待分配积分股
|
||||
// 冻结积分股
|
||||
_buildAssetItem(
|
||||
icon: Icons.hourglass_empty,
|
||||
icon: Icons.lock_outline,
|
||||
iconColor: _orange,
|
||||
iconBgColor: _serenade,
|
||||
title: '待分配积分股',
|
||||
amount: '1,234.56',
|
||||
title: '冻结积分股',
|
||||
amount: asset?.frozenShares,
|
||||
isLoading: isLoading,
|
||||
subtitle: '次日开始参与分配',
|
||||
subtitle: '交易挂单中',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _calculateValue(String balance, String price) {
|
||||
final b = double.tryParse(balance) ?? 0;
|
||||
final p = double.tryParse(price) ?? 0;
|
||||
return (b * p).toString();
|
||||
}
|
||||
|
||||
Widget _buildAssetItem({
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
|
|
@ -555,7 +575,7 @@ class AssetPage extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildEarningsCard(account, bool isLoading) {
|
||||
Widget _buildEarningsCard(AssetDisplay? asset, bool isLoading) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -584,7 +604,7 @@ class AssetPage extends ConsumerWidget {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'收益统计',
|
||||
'交易统计',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -597,19 +617,34 @@ class AssetPage extends ConsumerWidget {
|
|||
// 统计数据
|
||||
Row(
|
||||
children: [
|
||||
_buildEarningsItem('累计收益', isLoading ? null : '12,345.67', _orange, isLoading),
|
||||
_buildEarningsItem(
|
||||
'累计买入',
|
||||
asset != null ? formatCompact(asset.totalBought) : null,
|
||||
_orange,
|
||||
isLoading,
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: _serenade,
|
||||
),
|
||||
_buildEarningsItem('今日收益', isLoading ? null : '+156.78', _green, isLoading),
|
||||
_buildEarningsItem(
|
||||
'累计卖出',
|
||||
asset != null ? formatCompact(asset.totalSold) : null,
|
||||
_green,
|
||||
isLoading,
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: _serenade,
|
||||
),
|
||||
_buildEarningsItem('昨日收益', isLoading ? null : '143.21', const Color(0xFF9CA3AF), isLoading),
|
||||
_buildEarningsItem(
|
||||
'销毁倍数',
|
||||
asset != null ? '${formatDecimal(asset.burnMultiplier, 4)}x' : null,
|
||||
const Color(0xFF9CA3AF),
|
||||
isLoading,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
@ -646,31 +681,31 @@ class AssetPage extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildAccountList(account, bool isLoading) {
|
||||
Widget _buildAccountList(AssetDisplay? asset, bool isLoading) {
|
||||
return Column(
|
||||
children: [
|
||||
// 交易账户
|
||||
// 交易账户(可用现金)
|
||||
_buildAccountItem(
|
||||
icon: Icons.account_balance_wallet,
|
||||
iconColor: _orange,
|
||||
title: '交易账户',
|
||||
balance: account?.tradingBalance,
|
||||
title: '可用绿积分',
|
||||
balance: asset?.availableCash,
|
||||
isLoading: isLoading,
|
||||
unit: '绿积分',
|
||||
status: '正常',
|
||||
status: '可交易',
|
||||
statusColor: _green,
|
||||
statusBgColor: _feta,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 提现账户
|
||||
// 冻结现金
|
||||
_buildAccountItem(
|
||||
icon: Icons.savings,
|
||||
icon: Icons.lock_outline,
|
||||
iconColor: _orange,
|
||||
title: '提现账户',
|
||||
balance: '1,234.56',
|
||||
title: '冻结绿积分',
|
||||
balance: asset?.frozenCash,
|
||||
isLoading: isLoading,
|
||||
unit: '绿积分',
|
||||
status: '已绑定',
|
||||
status: '挂单中',
|
||||
statusColor: const Color(0xFF9CA3AF),
|
||||
statusBgColor: Colors.white,
|
||||
statusBorder: true,
|
||||
|
|
@ -734,7 +769,7 @@ class AssetPage extends ConsumerWidget {
|
|||
Row(
|
||||
children: [
|
||||
DataText(
|
||||
data: balance,
|
||||
data: balance != null ? formatAmount(balance) : null,
|
||||
isLoading: isLoading,
|
||||
placeholder: '--',
|
||||
style: const TextStyle(
|
||||
|
|
@ -778,5 +813,4 @@ class AssetPage extends ConsumerWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class ContributionPage extends ConsumerWidget {
|
|||
}
|
||||
|
||||
Widget _buildTotalContributionCard(Contribution? contribution, bool isLoading) {
|
||||
final total = contribution?.effectiveContribution ?? '0';
|
||||
final total = contribution?.totalContribution ?? '0';
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -226,8 +226,8 @@ class ContributionPage extends ConsumerWidget {
|
|||
child: Row(
|
||||
children: [
|
||||
_buildStatColumn('个人贡献值', contribution?.personalContribution, isLoading, false),
|
||||
_buildStatColumn('团队贡献值', contribution?.teamLevelContribution, isLoading, true),
|
||||
_buildStatColumn('省市公司', contribution?.systemContribution, isLoading, true),
|
||||
_buildStatColumn('团队层级', contribution?.teamLevelContribution, isLoading, true),
|
||||
_buildStatColumn('团队奖励', contribution?.teamBonusContribution, isLoading, true),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -261,10 +261,10 @@ class ContributionPage extends ConsumerWidget {
|
|||
|
||||
Widget _buildTodayEstimateCard(Contribution? contribution, bool isLoading) {
|
||||
// 基于贡献值计算预估收益(暂时显示占位符,后续可接入实际计算API)
|
||||
final effectiveContribution = double.tryParse(contribution?.effectiveContribution ?? '0') ?? 0;
|
||||
final totalContribution = double.tryParse(contribution?.totalContribution ?? '0') ?? 0;
|
||||
// 简单估算:假设每日发放总量为 10000 积分股,按贡献值占比分配
|
||||
// 这里先显示"--"表示暂无数据,后续可接入实际计算
|
||||
final hasContribution = effectiveContribution > 0;
|
||||
final hasContribution = totalContribution > 0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/router/routes.dart';
|
||||
import '../../providers/user_providers.dart';
|
||||
import '../../providers/profile_providers.dart';
|
||||
import '../../widgets/shimmer_loading.dart';
|
||||
|
||||
class ProfilePage extends ConsumerWidget {
|
||||
const ProfilePage({super.key});
|
||||
|
|
@ -20,12 +22,23 @@ class ProfilePage extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final user = ref.watch(userNotifierProvider);
|
||||
final statsAsync = ref.watch(userStatsProvider);
|
||||
final invitationCode = ref.watch(invitationCodeProvider);
|
||||
|
||||
final isStatsLoading = statsAsync.isLoading;
|
||||
final stats = statsAsync.valueOrNull;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: _bgGray,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(userStatsProvider);
|
||||
await ref.read(userNotifierProvider.notifier).fetchProfile();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
// 用户头部信息
|
||||
|
|
@ -34,17 +47,17 @@ class ProfilePage extends ConsumerWidget {
|
|||
const SizedBox(height: 16),
|
||||
|
||||
// 统计数据行
|
||||
_buildStatsRow(),
|
||||
_buildStatsRow(stats, isStatsLoading),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 邀请码卡片
|
||||
_buildInvitationCard(context),
|
||||
_buildInvitationCard(context, invitationCode),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 账户设置
|
||||
_buildAccountSettings(context),
|
||||
_buildAccountSettings(context, user),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
|
@ -82,10 +95,11 @@ class ProfilePage extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserHeader(BuildContext context, dynamic user) {
|
||||
Widget _buildUserHeader(BuildContext context, UserState user) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
color: Colors.white,
|
||||
|
|
@ -107,7 +121,9 @@ class ProfilePage extends ConsumerWidget {
|
|||
child: Text(
|
||||
user.nickname?.isNotEmpty == true
|
||||
? user.nickname!.substring(0, 1).toUpperCase()
|
||||
: 'U',
|
||||
: (user.realName?.isNotEmpty == true
|
||||
? user.realName!.substring(0, 1).toUpperCase()
|
||||
: 'U'),
|
||||
style: const TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -127,7 +143,7 @@ class ProfilePage extends ConsumerWidget {
|
|||
Row(
|
||||
children: [
|
||||
Text(
|
||||
user.nickname ?? '榴莲用户',
|
||||
user.realName ?? user.nickname ?? '榴莲用户',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -135,26 +151,32 @@ class ProfilePage extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// VIP 徽章
|
||||
// 实名认证徽章
|
||||
if (user.isKycVerified)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFFFD700), Color(0xFFFF8C00)],
|
||||
),
|
||||
color: _green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Text(
|
||||
'VIP 3',
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.verified, size: 12, color: _green),
|
||||
SizedBox(width: 2),
|
||||
Text(
|
||||
'已实名',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -191,6 +213,18 @@ class ProfilePage extends ConsumerWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
// 手机号
|
||||
if (user.phone != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
'手机: ${user.phone}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: _lightGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -210,30 +244,48 @@ class ProfilePage extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsRow() {
|
||||
Widget _buildStatsRow(UserStats? stats, bool isLoading) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildStatItem('认种数量', '10'),
|
||||
_buildStatItem(
|
||||
'认种状态',
|
||||
stats?.hasAdopted == true ? '已认种' : '未认种',
|
||||
isLoading,
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildStatItem('直推人数', '5'),
|
||||
_buildStatItem(
|
||||
'直推人数',
|
||||
stats?.directReferralAdoptedCount.toString() ?? '0',
|
||||
isLoading,
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildStatItem('团队人数', '128'),
|
||||
_buildStatItem(
|
||||
'团队层级',
|
||||
stats?.unlockedLevelDepth.toString() ?? '0',
|
||||
isLoading,
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildStatItem('VIP等级', 'V3'),
|
||||
_buildStatItem(
|
||||
'VIP等级',
|
||||
stats?.vipLevel ?? '-',
|
||||
isLoading,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(String label, String value) {
|
||||
Widget _buildStatItem(String label, String value, bool isLoading) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
DataText(
|
||||
data: isLoading ? null : value,
|
||||
isLoading: isLoading,
|
||||
placeholder: '--',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -260,9 +312,7 @@ class ProfilePage extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildInvitationCard(BuildContext context) {
|
||||
const invitationCode = 'DUR8888XYZ';
|
||||
|
||||
Widget _buildInvitationCard(BuildContext context, String invitationCode) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
|
@ -293,9 +343,9 @@ class ProfilePage extends ConsumerWidget {
|
|||
color: _bgGray,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
invitationCode,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _darkText,
|
||||
|
|
@ -309,7 +359,7 @@ class ProfilePage extends ConsumerWidget {
|
|||
icon: Icons.copy,
|
||||
label: '复制',
|
||||
onTap: () {
|
||||
Clipboard.setData(const ClipboardData(text: invitationCode));
|
||||
Clipboard.setData(ClipboardData(text: invitationCode));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('邀请码已复制'),
|
||||
|
|
@ -364,7 +414,7 @@ class ProfilePage extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildAccountSettings(BuildContext context) {
|
||||
Widget _buildAccountSettings(BuildContext context, UserState user) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -393,11 +443,11 @@ class ProfilePage extends ConsumerWidget {
|
|||
_buildSettingItem(
|
||||
icon: Icons.verified_user,
|
||||
label: '实名认证',
|
||||
trailing: const Text(
|
||||
'已认证',
|
||||
trailing: Text(
|
||||
user.isKycVerified ? '已认证' : '未认证',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: _green,
|
||||
color: user.isKycVerified ? _green : _lightGray,
|
||||
),
|
||||
),
|
||||
onTap: () {},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../core/constants/app_colors.dart';
|
||||
import '../../../core/utils/format_utils.dart';
|
||||
import '../../providers/mining_providers.dart';
|
||||
import '../../../data/models/trade_order_model.dart';
|
||||
import '../../../domain/entities/price_info.dart';
|
||||
import '../../../domain/entities/market_overview.dart';
|
||||
import '../../../domain/entities/trade_order.dart';
|
||||
import '../../providers/user_providers.dart';
|
||||
import '../../providers/trading_providers.dart';
|
||||
import '../../widgets/shimmer_loading.dart';
|
||||
|
|
@ -28,54 +32,48 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
// 状态
|
||||
int _selectedTab = 1; // 0: 买入, 1: 卖出
|
||||
int _selectedTimeRange = 1; // 时间周期选择
|
||||
final _amountController = TextEditingController();
|
||||
final _quantityController = TextEditingController();
|
||||
final _priceController = TextEditingController();
|
||||
|
||||
final List<String> _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日'];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_amountController.dispose();
|
||||
_quantityController.dispose();
|
||||
_priceController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final globalState = ref.watch(globalStateProvider);
|
||||
final priceAsync = ref.watch(currentPriceProvider);
|
||||
final marketAsync = ref.watch(marketOverviewProvider);
|
||||
final ordersAsync = ref.watch(ordersProvider);
|
||||
final user = ref.watch(userNotifierProvider);
|
||||
final accountSequence = user.accountSequence ?? '';
|
||||
|
||||
// Extract loading state and data using shimmer placeholder approach
|
||||
final isLoading = globalState.isLoading;
|
||||
final state = globalState.valueOrNull;
|
||||
final hasError = globalState.hasError;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(currentPriceProvider);
|
||||
ref.invalidate(marketOverviewProvider);
|
||||
ref.invalidate(ordersProvider);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部导航栏
|
||||
_buildAppBar(),
|
||||
// 可滚动内容
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// 价格卡片 - always render, use shimmer for loading
|
||||
hasError
|
||||
? _buildErrorCard('价格加载失败')
|
||||
: _buildPriceCard(state, isLoading),
|
||||
// K线图占位区域
|
||||
_buildChartSection(),
|
||||
// 市场数据 - always render, use shimmer for loading
|
||||
hasError
|
||||
? _buildErrorCard('市场数据加载失败')
|
||||
: _buildMarketDataCard(state, isLoading),
|
||||
// 买入/卖出交易面板
|
||||
_buildTradingPanel(accountSequence),
|
||||
// 我的挂单
|
||||
_buildMyOrdersCard(),
|
||||
_buildPriceCard(priceAsync),
|
||||
_buildChartSection(priceAsync),
|
||||
_buildMarketDataCard(marketAsync),
|
||||
_buildTradingPanel(priceAsync),
|
||||
_buildMyOrdersCard(ordersAsync),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
|
|
@ -84,17 +82,17 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar() {
|
||||
return Container(
|
||||
color: _bgGray.withOpacity(0.9),
|
||||
color: _bgGray.withValues(alpha: 0.9),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 积分股交易按钮
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -110,7 +108,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
// 通知图标
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
|
|
@ -119,7 +116,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
|
|
@ -150,10 +147,17 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceCard(dynamic state, bool isLoading) {
|
||||
final isPriceUp = state?.isPriceUp ?? true;
|
||||
final currentPrice = state?.currentPrice;
|
||||
final priceChange = state?.priceChange24h;
|
||||
Widget _buildPriceCard(AsyncValue<PriceInfo?> priceAsync) {
|
||||
final isLoading = priceAsync.isLoading;
|
||||
final priceInfo = priceAsync.valueOrNull;
|
||||
final hasError = priceAsync.hasError;
|
||||
|
||||
if (hasError && priceInfo == null) {
|
||||
return _buildErrorCard('价格加载失败');
|
||||
}
|
||||
|
||||
final price = priceInfo?.price ?? '0';
|
||||
final greenPoints = priceInfo?.greenPoints ?? '0';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
|
|
@ -165,11 +169,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题行
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
const Text(
|
||||
'当前积分股价格',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
|
|
@ -178,23 +181,19 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
),
|
||||
DataText(
|
||||
data: '= 156.00 绿积分',
|
||||
data: priceInfo != null ? '= ${formatCompact(greenPoints)} 绿积分' : null,
|
||||
isLoading: isLoading,
|
||||
placeholder: '= -- 绿积分',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _grayText,
|
||||
),
|
||||
style: const TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 价格和涨跌
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AmountText(
|
||||
amount: currentPrice != null ? formatPrice(currentPrice) : null,
|
||||
amount: priceInfo != null ? formatPrice(price) : null,
|
||||
isLoading: isLoading,
|
||||
prefix: '\u00A5 ',
|
||||
style: const TextStyle(
|
||||
|
|
@ -208,19 +207,15 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _green.withOpacity(0.1),
|
||||
color: _green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isPriceUp ? Icons.trending_up : Icons.trending_down,
|
||||
size: 16,
|
||||
color: _green,
|
||||
),
|
||||
const Icon(Icons.trending_up, size: 16, color: _green),
|
||||
DataText(
|
||||
data: priceChange != null ? '+$priceChange%' : null,
|
||||
data: isLoading ? null : '+0.00%',
|
||||
isLoading: isLoading,
|
||||
placeholder: '+--.--%',
|
||||
style: const TextStyle(
|
||||
|
|
@ -239,7 +234,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildChartSection() {
|
||||
Widget _buildChartSection(AsyncValue<PriceInfo?> priceAsync) {
|
||||
final priceInfo = priceAsync.valueOrNull;
|
||||
final currentPrice = priceInfo?.price ?? '0.000000';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
|
@ -249,7 +247,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// K线图占位
|
||||
Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -258,12 +255,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// 模拟K线图
|
||||
CustomPaint(
|
||||
size: const Size(double.infinity, 200),
|
||||
painter: _CandlestickPainter(),
|
||||
),
|
||||
// 当前价格标签
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 60,
|
||||
|
|
@ -274,18 +269,15 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Text(
|
||||
'0.000156',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Text(
|
||||
formatPrice(currentPrice),
|
||||
style: const TextStyle(fontSize: 10, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -293,7 +285,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 时间周期选择
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
|
|
@ -329,11 +320,14 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildMarketDataCard(dynamic state, bool isLoading) {
|
||||
final sharesPool = state?.sharesPool;
|
||||
final circulatingPool = state?.circulatingPool;
|
||||
final greenPointsPool = state?.greenPointsPool;
|
||||
final blackHoleBurned = state?.blackHoleBurned;
|
||||
Widget _buildMarketDataCard(AsyncValue<MarketOverview?> marketAsync) {
|
||||
final isLoading = marketAsync.isLoading;
|
||||
final market = marketAsync.valueOrNull;
|
||||
final hasError = marketAsync.hasError;
|
||||
|
||||
if (hasError && market == null) {
|
||||
return _buildErrorCard('市场数据加载失败');
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
|
|
@ -345,7 +339,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
|
|
@ -368,12 +361,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// 第一行数据
|
||||
Row(
|
||||
children: [
|
||||
_buildMarketDataItem(
|
||||
'积分股池',
|
||||
sharesPool ?? '8,888,888,888',
|
||||
'总积分股',
|
||||
market != null ? formatCompact(market.totalShares) : null,
|
||||
_orange,
|
||||
isLoading,
|
||||
),
|
||||
|
|
@ -381,7 +373,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
const SizedBox(width: 16),
|
||||
_buildMarketDataItem(
|
||||
'流通池',
|
||||
circulatingPool ?? '1,234,567',
|
||||
market != null ? formatCompact(market.circulationPool) : null,
|
||||
_orange,
|
||||
isLoading,
|
||||
),
|
||||
|
|
@ -390,12 +382,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
const SizedBox(height: 24),
|
||||
Container(height: 1, color: _bgGray),
|
||||
const SizedBox(height: 24),
|
||||
// 第二行数据
|
||||
Row(
|
||||
children: [
|
||||
_buildMarketDataItem(
|
||||
'绿积分池',
|
||||
greenPointsPool ?? '99,999,999',
|
||||
market != null ? formatCompact(market.greenPoints) : null,
|
||||
_orange,
|
||||
isLoading,
|
||||
),
|
||||
|
|
@ -403,7 +394,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
const SizedBox(width: 16),
|
||||
_buildMarketDataItem(
|
||||
'黑洞销毁量',
|
||||
blackHoleBurned ?? '50,000,000',
|
||||
market != null ? formatCompact(market.blackHoleAmount) : null,
|
||||
_red,
|
||||
isLoading,
|
||||
),
|
||||
|
|
@ -414,18 +405,12 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildMarketDataItem(String label, String value, Color valueColor, bool isLoading) {
|
||||
Widget _buildMarketDataItem(String label, String? value, Color valueColor, bool isLoading) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: _grayText,
|
||||
),
|
||||
),
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: _grayText)),
|
||||
const SizedBox(height: 4),
|
||||
DataText(
|
||||
data: value,
|
||||
|
|
@ -442,7 +427,15 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildTradingPanel(String accountSequence) {
|
||||
Widget _buildTradingPanel(AsyncValue<PriceInfo?> priceAsync) {
|
||||
final priceInfo = priceAsync.valueOrNull;
|
||||
final currentPrice = priceInfo?.price ?? '0';
|
||||
|
||||
// 设置默认价格
|
||||
if (_priceController.text.isEmpty && priceInfo != null) {
|
||||
_priceController.text = currentPrice;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
|
|
@ -452,7 +445,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 买入/卖出切换
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: _bgGray)),
|
||||
|
|
@ -513,71 +505,13 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// 数量输入
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'数量',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _grayText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: _bgGray,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _amountController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(
|
||||
hintText: '请输入数量',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF9CA3AF),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.currency_yuan, size: 15, color: _grayText),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// TODO: 设置最大数量
|
||||
},
|
||||
child: const Text(
|
||||
'MAX',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _orange,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// 价格输入
|
||||
_buildInputField('价格', _priceController, '请输入价格', '绿积分'),
|
||||
const SizedBox(height: 16),
|
||||
// 预计获得
|
||||
// 数量输入
|
||||
_buildInputField('数量', _quantityController, '请输入数量', '积分股'),
|
||||
const SizedBox(height: 16),
|
||||
// 预计获得/支出
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -587,16 +521,13 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'预计获得',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _grayText,
|
||||
Text(
|
||||
_selectedTab == 0 ? '预计支出' : '预计获得',
|
||||
style: const TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'0.00 绿积分',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
_calculateEstimate(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _orange,
|
||||
|
|
@ -606,24 +537,22 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 手续费
|
||||
// 销毁说明 (卖出时显示)
|
||||
if (_selectedTab == 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: const [
|
||||
Text(
|
||||
'手续费 (10%)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _grayText,
|
||||
),
|
||||
'销毁比例',
|
||||
style: TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
Text(
|
||||
'0.00',
|
||||
'10% 进入黑洞',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _grayText,
|
||||
color: _red,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
|
|
@ -636,7 +565,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _handleTrade(accountSequence),
|
||||
onPressed: _handleTrade,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _orange,
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -658,7 +587,79 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildMyOrdersCard() {
|
||||
Widget _buildInputField(
|
||||
String label,
|
||||
TextEditingController controller,
|
||||
String hint,
|
||||
String suffix,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _grayText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: _bgGray,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF9CA3AF),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(suffix, style: const TextStyle(fontSize: 12, color: _grayText)),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _calculateEstimate() {
|
||||
final price = double.tryParse(_priceController.text) ?? 0;
|
||||
final quantity = double.tryParse(_quantityController.text) ?? 0;
|
||||
final total = price * quantity;
|
||||
|
||||
if (total == 0) {
|
||||
return '0.00 绿积分';
|
||||
}
|
||||
|
||||
if (_selectedTab == 1) {
|
||||
// 卖出时扣除10%销毁
|
||||
final afterBurn = total * 0.9;
|
||||
return '${formatAmount(afterBurn.toString())} 绿积分';
|
||||
}
|
||||
|
||||
return '${formatAmount(total.toString())} 绿积分';
|
||||
}
|
||||
|
||||
Widget _buildMyOrdersCard(AsyncValue<OrdersPageModel?> ordersAsync) {
|
||||
final isLoading = ordersAsync.isLoading;
|
||||
final ordersPage = ordersAsync.valueOrNull;
|
||||
final orders = ordersPage?.data ?? [];
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
|
|
@ -668,7 +669,6 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 标题
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
|
@ -696,27 +696,54 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 挂单列表项
|
||||
_buildOrderItem(
|
||||
type: '卖出',
|
||||
price: '0.000156',
|
||||
quantity: '1,000 股',
|
||||
time: '12/05 14:30',
|
||||
status: '待成交',
|
||||
if (isLoading)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: CircularProgressIndicator(color: _orange),
|
||||
),
|
||||
)
|
||||
else if (orders.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 20),
|
||||
child: Text(
|
||||
'暂无挂单',
|
||||
style: TextStyle(fontSize: 14, color: _grayText),
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: orders.take(3).map((order) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _buildOrderItemFromEntity(order),
|
||||
)).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrderItem({
|
||||
required String type,
|
||||
required String price,
|
||||
required String quantity,
|
||||
required String time,
|
||||
required String status,
|
||||
}) {
|
||||
final isSell = type == '卖出';
|
||||
Widget _buildOrderItemFromEntity(TradeOrder order) {
|
||||
final isSell = order.isSell;
|
||||
final dateFormat = DateFormat('MM/dd HH:mm');
|
||||
final formattedDate = dateFormat.format(order.createdAt);
|
||||
|
||||
String statusText;
|
||||
switch (order.status) {
|
||||
case OrderStatus.pending:
|
||||
statusText = '待成交';
|
||||
break;
|
||||
case OrderStatus.partial:
|
||||
statusText = '部分成交';
|
||||
break;
|
||||
case OrderStatus.filled:
|
||||
statusText = '已成交';
|
||||
break;
|
||||
case OrderStatus.cancelled:
|
||||
statusText = '已取消';
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -735,11 +762,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: (isSell ? _red : _green).withOpacity(0.1),
|
||||
color: (isSell ? _red : _green).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
type,
|
||||
isSell ? '卖出' : '买入',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -749,7 +776,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
price,
|
||||
formatPrice(order.price),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -760,19 +787,16 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
time,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: _grayText,
|
||||
),
|
||||
formattedDate,
|
||||
style: const TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
quantity,
|
||||
'${formatCompact(order.quantity)} 股',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
|
@ -783,15 +807,12 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _orange.withOpacity(0.1),
|
||||
color: _orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(9999),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: _orange,
|
||||
),
|
||||
statusText,
|
||||
style: const TextStyle(fontSize: 12, color: _orange),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -821,8 +842,15 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
);
|
||||
}
|
||||
|
||||
void _handleTrade(String accountSequence) async {
|
||||
if (_amountController.text.isEmpty) {
|
||||
void _handleTrade() async {
|
||||
if (_priceController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('请输入价格')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_quantityController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('请输入数量')),
|
||||
);
|
||||
|
|
@ -835,11 +863,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
if (isBuy) {
|
||||
success = await ref
|
||||
.read(tradingNotifierProvider.notifier)
|
||||
.buyShares(accountSequence, _amountController.text);
|
||||
.buyShares(_priceController.text, _quantityController.text);
|
||||
} else {
|
||||
success = await ref
|
||||
.read(tradingNotifierProvider.notifier)
|
||||
.sellShares(accountSequence, _amountController.text);
|
||||
.sellShares(_priceController.text, _quantityController.text);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
|
|
@ -852,10 +880,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
);
|
||||
if (success) {
|
||||
_amountController.clear();
|
||||
// 交易成功后刷新所有相关数据
|
||||
ref.invalidate(shareAccountProvider(accountSequence));
|
||||
ref.invalidate(globalStateProvider);
|
||||
_quantityController.clear();
|
||||
// 交易成功后刷新订单列表
|
||||
ref.invalidate(ordersProvider);
|
||||
ref.invalidate(currentPriceProvider);
|
||||
ref.invalidate(marketOverviewProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -889,7 +918,7 @@ class _CandlestickPainter extends CustomPainter {
|
|||
];
|
||||
|
||||
final candleWidth = (size.width - 40) / candleData.length;
|
||||
final padding = 20.0;
|
||||
const padding = 20.0;
|
||||
|
||||
for (int i = 0; i < candleData.length; i++) {
|
||||
final data = candleData[i];
|
||||
|
|
@ -907,14 +936,12 @@ class _CandlestickPainter extends CustomPainter {
|
|||
final yHigh = size.height - (high * size.height * 0.8 + size.height * 0.1);
|
||||
final yLow = size.height - (low * size.height * 0.8 + size.height * 0.1);
|
||||
|
||||
// 绘制影线
|
||||
canvas.drawLine(
|
||||
Offset(x, yHigh),
|
||||
Offset(x, yLow),
|
||||
paint..strokeWidth = 1,
|
||||
);
|
||||
|
||||
// 绘制实体
|
||||
final bodyTop = isGreen ? yClose : yOpen;
|
||||
final bodyBottom = isGreen ? yOpen : yClose;
|
||||
canvas.drawRect(
|
||||
|
|
@ -923,7 +950,6 @@ class _CandlestickPainter extends CustomPainter {
|
|||
);
|
||||
}
|
||||
|
||||
// 绘制虚线参考线
|
||||
final dashY = size.height * 0.35;
|
||||
const dashWidth = 5.0;
|
||||
const dashSpace = 3.0;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ?? '--------';
|
||||
});
|
||||
|
|
@ -1,60 +1,131 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../domain/entities/price_info.dart';
|
||||
import '../../domain/entities/market_overview.dart';
|
||||
import '../../domain/entities/trading_account.dart';
|
||||
import '../../domain/entities/trade_order.dart';
|
||||
import '../../domain/usecases/trading/buy_shares.dart';
|
||||
import '../../domain/usecases/trading/sell_shares.dart';
|
||||
import '../../domain/repositories/trading_repository.dart';
|
||||
import '../../data/models/trade_order_model.dart';
|
||||
import '../../core/di/injection.dart';
|
||||
|
||||
// Use Cases Providers
|
||||
final buySharesUseCaseProvider = Provider<BuyShares>((ref) {
|
||||
return getIt<BuyShares>();
|
||||
});
|
||||
|
||||
final sellSharesUseCaseProvider = Provider<SellShares>((ref) {
|
||||
return getIt<SellShares>();
|
||||
// Repository Provider
|
||||
final tradingRepositoryProvider = Provider<TradingRepository>((ref) {
|
||||
return getIt<TradingRepository>();
|
||||
});
|
||||
|
||||
// K线周期选择
|
||||
final selectedKlinePeriodProvider = StateProvider<String>((ref) => '1h');
|
||||
|
||||
// 当前价格 Provider (5分钟缓存)
|
||||
final currentPriceProvider = FutureProvider<PriceInfo?>((ref) async {
|
||||
final repository = ref.watch(tradingRepositoryProvider);
|
||||
final result = await repository.getCurrentPrice();
|
||||
|
||||
ref.keepAlive();
|
||||
final timer = Timer(const Duration(minutes: 5), () {
|
||||
ref.invalidateSelf();
|
||||
});
|
||||
ref.onDispose(() => timer.cancel());
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(priceInfo) => priceInfo,
|
||||
);
|
||||
});
|
||||
|
||||
// 市场概览 Provider (5分钟缓存)
|
||||
final marketOverviewProvider = FutureProvider<MarketOverview?>((ref) async {
|
||||
final repository = ref.watch(tradingRepositoryProvider);
|
||||
final result = await repository.getMarketOverview();
|
||||
|
||||
ref.keepAlive();
|
||||
final timer = Timer(const Duration(minutes: 5), () {
|
||||
ref.invalidateSelf();
|
||||
});
|
||||
ref.onDispose(() => timer.cancel());
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(overview) => overview,
|
||||
);
|
||||
});
|
||||
|
||||
// 交易账户 Provider (带参数)
|
||||
final tradingAccountProvider = FutureProvider.family<TradingAccount?, String>(
|
||||
(ref, accountSequence) async {
|
||||
if (accountSequence.isEmpty) return null;
|
||||
|
||||
final repository = ref.watch(tradingRepositoryProvider);
|
||||
final result = await repository.getTradingAccount(accountSequence);
|
||||
|
||||
ref.keepAlive();
|
||||
final timer = Timer(const Duration(minutes: 2), () {
|
||||
ref.invalidateSelf();
|
||||
});
|
||||
ref.onDispose(() => timer.cancel());
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(account) => account,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// 订单列表 Provider
|
||||
final ordersProvider = FutureProvider<OrdersPageModel?>((ref) async {
|
||||
final repository = ref.watch(tradingRepositoryProvider);
|
||||
final result = await repository.getOrders(page: 1, pageSize: 10);
|
||||
|
||||
ref.keepAlive();
|
||||
final timer = Timer(const Duration(minutes: 2), () {
|
||||
ref.invalidateSelf();
|
||||
});
|
||||
ref.onDispose(() => timer.cancel());
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(orders) => orders,
|
||||
);
|
||||
});
|
||||
|
||||
// 交易状态
|
||||
class TradingState {
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final TradeOrder? lastOrder;
|
||||
final Map<String, dynamic>? lastOrderResult;
|
||||
|
||||
TradingState({
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
this.lastOrder,
|
||||
this.lastOrderResult,
|
||||
});
|
||||
|
||||
TradingState copyWith({
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
TradeOrder? lastOrder,
|
||||
Map<String, dynamic>? lastOrderResult,
|
||||
}) {
|
||||
return TradingState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
lastOrder: lastOrder ?? this.lastOrder,
|
||||
lastOrderResult: lastOrderResult ?? this.lastOrderResult,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TradingNotifier extends StateNotifier<TradingState> {
|
||||
final BuyShares buySharesUseCase;
|
||||
final SellShares sellSharesUseCase;
|
||||
final TradingRepository repository;
|
||||
|
||||
TradingNotifier({
|
||||
required this.buySharesUseCase,
|
||||
required this.sellSharesUseCase,
|
||||
}) : super(TradingState());
|
||||
TradingNotifier({required this.repository}) : super(TradingState());
|
||||
|
||||
Future<bool> buyShares(String accountSequence, String amount) async {
|
||||
/// 创建买入订单
|
||||
Future<bool> buyShares(String price, String quantity) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
final result = await buySharesUseCase(
|
||||
BuySharesParams(accountSequence: accountSequence, amount: amount),
|
||||
final result = await repository.createOrder(
|
||||
type: 'BUY',
|
||||
price: price,
|
||||
quantity: quantity,
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
|
|
@ -62,18 +133,21 @@ class TradingNotifier extends StateNotifier<TradingState> {
|
|||
state = state.copyWith(isLoading: false, error: failure.message);
|
||||
return false;
|
||||
},
|
||||
(order) {
|
||||
state = state.copyWith(isLoading: false, lastOrder: order);
|
||||
(orderResult) {
|
||||
state = state.copyWith(isLoading: false, lastOrderResult: orderResult);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> sellShares(String accountSequence, String amount) async {
|
||||
/// 创建卖出订单
|
||||
Future<bool> sellShares(String price, String quantity) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
final result = await sellSharesUseCase(
|
||||
SellSharesParams(accountSequence: accountSequence, amount: amount),
|
||||
final result = await repository.createOrder(
|
||||
type: 'SELL',
|
||||
price: price,
|
||||
quantity: quantity,
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
|
|
@ -81,8 +155,62 @@ class TradingNotifier extends StateNotifier<TradingState> {
|
|||
state = state.copyWith(isLoading: false, error: failure.message);
|
||||
return false;
|
||||
},
|
||||
(order) {
|
||||
state = state.copyWith(isLoading: false, lastOrder: order);
|
||||
(orderResult) {
|
||||
state = state.copyWith(isLoading: false, lastOrderResult: orderResult);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 取消订单
|
||||
Future<bool> cancelOrder(String orderNo) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
final result = await repository.cancelOrder(orderNo);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
state = state.copyWith(isLoading: false, error: failure.message);
|
||||
return false;
|
||||
},
|
||||
(_) {
|
||||
state = state.copyWith(isLoading: false);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 划入积分股
|
||||
Future<bool> transferIn(String amount) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
final result = await repository.transferIn(amount);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
state = state.copyWith(isLoading: false, error: failure.message);
|
||||
return false;
|
||||
},
|
||||
(_) {
|
||||
state = state.copyWith(isLoading: false);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 划出积分股
|
||||
Future<bool> transferOut(String amount) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
final result = await repository.transferOut(amount);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
state = state.copyWith(isLoading: false, error: failure.message);
|
||||
return false;
|
||||
},
|
||||
(_) {
|
||||
state = state.copyWith(isLoading: false);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
|
@ -95,7 +223,6 @@ class TradingNotifier extends StateNotifier<TradingState> {
|
|||
|
||||
final tradingNotifierProvider = StateNotifierProvider<TradingNotifier, TradingState>(
|
||||
(ref) => TradingNotifier(
|
||||
buySharesUseCase: ref.watch(buySharesUseCaseProvider),
|
||||
sellSharesUseCase: ref.watch(sellSharesUseCaseProvider),
|
||||
repository: ref.watch(tradingRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ class UserState {
|
|||
final String? phone;
|
||||
final String? kycStatus;
|
||||
final String? source;
|
||||
final String? status;
|
||||
final String? realName;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? lastLoginAt;
|
||||
final String? accessToken;
|
||||
final String? refreshToken;
|
||||
final bool isLoggedIn;
|
||||
|
|
@ -21,6 +25,10 @@ class UserState {
|
|||
this.phone,
|
||||
this.kycStatus,
|
||||
this.source,
|
||||
this.status,
|
||||
this.realName,
|
||||
this.createdAt,
|
||||
this.lastLoginAt,
|
||||
this.accessToken,
|
||||
this.refreshToken,
|
||||
this.isLoggedIn = false,
|
||||
|
|
@ -28,12 +36,18 @@ class UserState {
|
|||
this.error,
|
||||
});
|
||||
|
||||
bool get isKycVerified => kycStatus == 'VERIFIED';
|
||||
|
||||
UserState copyWith({
|
||||
String? accountSequence,
|
||||
String? nickname,
|
||||
String? phone,
|
||||
String? kycStatus,
|
||||
String? source,
|
||||
String? status,
|
||||
String? realName,
|
||||
DateTime? createdAt,
|
||||
DateTime? lastLoginAt,
|
||||
String? accessToken,
|
||||
String? refreshToken,
|
||||
bool? isLoggedIn,
|
||||
|
|
@ -46,6 +60,10 @@ class UserState {
|
|||
phone: phone ?? this.phone,
|
||||
kycStatus: kycStatus ?? this.kycStatus,
|
||||
source: source ?? this.source,
|
||||
status: status ?? this.status,
|
||||
realName: realName ?? this.realName,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
lastLoginAt: lastLoginAt ?? this.lastLoginAt,
|
||||
accessToken: accessToken ?? this.accessToken,
|
||||
refreshToken: refreshToken ?? this.refreshToken,
|
||||
isLoggedIn: isLoggedIn ?? this.isLoggedIn,
|
||||
|
|
@ -77,6 +95,8 @@ class UserNotifier extends StateNotifier<UserState> {
|
|||
phone: phone,
|
||||
isLoggedIn: true,
|
||||
);
|
||||
// 登录后自动获取用户详情
|
||||
await fetchProfile();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,6 +163,8 @@ class UserNotifier extends StateNotifier<UserState> {
|
|||
isLoggedIn: true,
|
||||
isLoading: false,
|
||||
);
|
||||
// 登录后获取详细信息
|
||||
await fetchProfile();
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
rethrow;
|
||||
|
|
@ -164,6 +186,8 @@ class UserNotifier extends StateNotifier<UserState> {
|
|||
isLoggedIn: true,
|
||||
isLoading: false,
|
||||
);
|
||||
// 登录后获取详细信息
|
||||
await fetchProfile();
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
rethrow;
|
||||
|
|
@ -219,6 +243,24 @@ class UserNotifier extends StateNotifier<UserState> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 获取用户详细信息
|
||||
Future<void> fetchProfile() async {
|
||||
if (!state.isLoggedIn) return;
|
||||
try {
|
||||
final profile = await _authDataSource.getProfile();
|
||||
state = state.copyWith(
|
||||
phone: profile.phone,
|
||||
kycStatus: profile.kycStatus,
|
||||
source: profile.source,
|
||||
status: profile.status,
|
||||
realName: profile.realName,
|
||||
createdAt: profile.createdAt,
|
||||
lastLoginAt: profile.lastLoginAt,
|
||||
);
|
||||
} catch (e) {
|
||||
// 静默失败,不影响用户体验
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final userNotifierProvider = StateNotifierProvider<UserNotifier, UserState>(
|
||||
|
|
|
|||
Loading…
Reference in New Issue