feat(trading): 交易记录页面添加成交明细Tab,显示手续费
后端: - trading-service 添加 GET /trading/trades API 获取成交记录 - 成交记录包含: 交易总额、手续费(10%)、实际收到金额 前端: - 新增 TradeRecord 实体和 TradesPageModel - 交易记录页面添加 Tab: "订单记录" 和 "成交明细" - 成交明细显示: 价格、数量、交易总额、手续费、实际收到 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
63e02666ea
commit
7da98c248b
|
|
@ -122,4 +122,24 @@ export class TradingController {
|
||||||
total: result.total,
|
total: result.total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('trades')
|
||||||
|
@ApiOperation({ summary: '获取用户成交记录(含手续费明细)' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'pageSize', required: false, type: Number })
|
||||||
|
async getTrades(
|
||||||
|
@Req() req: any,
|
||||||
|
@Query('page') page?: number,
|
||||||
|
@Query('pageSize') pageSize?: number,
|
||||||
|
) {
|
||||||
|
const accountSequence = req.user?.accountSequence;
|
||||||
|
if (!accountSequence) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.orderRepository.findTradesByAccountSequence(accountSequence, {
|
||||||
|
page: page ?? 1,
|
||||||
|
pageSize: pageSize ?? 50,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,84 @@ export class OrderRepository {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户的成交记录(作为买方或卖方)
|
||||||
|
*/
|
||||||
|
async findTradesByAccountSequence(
|
||||||
|
accountSequence: string,
|
||||||
|
options?: { page?: number; pageSize?: number },
|
||||||
|
): Promise<{
|
||||||
|
data: Array<{
|
||||||
|
id: string;
|
||||||
|
tradeNo: string;
|
||||||
|
type: 'BUY' | 'SELL';
|
||||||
|
price: string;
|
||||||
|
quantity: string;
|
||||||
|
amount: string;
|
||||||
|
fee: string;
|
||||||
|
netAmount: string;
|
||||||
|
counterparty: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}>;
|
||||||
|
total: number;
|
||||||
|
}> {
|
||||||
|
const page = options?.page ?? 1;
|
||||||
|
const pageSize = options?.pageSize ?? 50;
|
||||||
|
|
||||||
|
// 查询作为买方和卖方的成交记录
|
||||||
|
const [buyerTrades, sellerTrades, buyerCount, sellerCount] = await Promise.all([
|
||||||
|
this.prisma.trade.findMany({
|
||||||
|
where: { buyerSequence: accountSequence },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
this.prisma.trade.findMany({
|
||||||
|
where: { sellerSequence: accountSequence },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
this.prisma.trade.count({ where: { buyerSequence: accountSequence } }),
|
||||||
|
this.prisma.trade.count({ where: { sellerSequence: accountSequence } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 合并并转换格式
|
||||||
|
const allTrades = [
|
||||||
|
...buyerTrades.map((t) => ({
|
||||||
|
id: t.id,
|
||||||
|
tradeNo: t.tradeNo,
|
||||||
|
type: 'BUY' as const,
|
||||||
|
price: t.price.toString(),
|
||||||
|
quantity: t.quantity.toString(),
|
||||||
|
amount: t.amount.toString(),
|
||||||
|
fee: '0', // 买方不支付手续费
|
||||||
|
netAmount: t.amount.toString(),
|
||||||
|
counterparty: t.sellerSequence,
|
||||||
|
createdAt: t.createdAt,
|
||||||
|
})),
|
||||||
|
...sellerTrades.map((t) => ({
|
||||||
|
id: t.id,
|
||||||
|
tradeNo: t.tradeNo,
|
||||||
|
type: 'SELL' as const,
|
||||||
|
price: t.price.toString(),
|
||||||
|
quantity: t.quantity.toString(),
|
||||||
|
amount: t.amount.toString(), // 这是扣除手续费后的金额
|
||||||
|
fee: t.fee.toString(),
|
||||||
|
netAmount: t.amount.toString(), // amount 已经是 net(扣除手续费后)
|
||||||
|
counterparty: t.buyerSequence,
|
||||||
|
createdAt: t.createdAt,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 按时间排序
|
||||||
|
allTrades.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const paged = allTrades.slice((page - 1) * pageSize, page * pageSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: paged,
|
||||||
|
total: buyerCount + sellerCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private toDomain(record: any): OrderAggregate {
|
private toDomain(record: any): OrderAggregate {
|
||||||
return OrderAggregate.reconstitute({
|
return OrderAggregate.reconstitute({
|
||||||
id: record.id,
|
id: record.id,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ class ApiEndpoints {
|
||||||
static const String orderBook = '/api/v2/trading/trading/orderbook';
|
static const String orderBook = '/api/v2/trading/trading/orderbook';
|
||||||
static const String createOrder = '/api/v2/trading/trading/orders';
|
static const String createOrder = '/api/v2/trading/trading/orders';
|
||||||
static const String orders = '/api/v2/trading/trading/orders';
|
static const String orders = '/api/v2/trading/trading/orders';
|
||||||
|
static const String trades = '/api/v2/trading/trading/trades';
|
||||||
static String cancelOrder(String orderNo) =>
|
static String cancelOrder(String orderNo) =>
|
||||||
'/api/v2/trading/trading/orders/$orderNo/cancel';
|
'/api/v2/trading/trading/orders/$orderNo/cancel';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ abstract class TradingRemoteDataSource {
|
||||||
int pageSize = 50,
|
int pageSize = 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 获取用户成交记录(含手续费明细)
|
||||||
|
Future<TradesPageModel> getTrades({
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
});
|
||||||
|
|
||||||
/// 预估卖出收益
|
/// 预估卖出收益
|
||||||
Future<Map<String, dynamic>> estimateSell(String quantity);
|
Future<Map<String, dynamic>> estimateSell(String quantity);
|
||||||
|
|
||||||
|
|
@ -222,6 +228,22 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TradesPageModel> getTrades({
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await client.get(
|
||||||
|
ApiEndpoints.trades,
|
||||||
|
queryParameters: {'page': page, 'pageSize': pageSize},
|
||||||
|
);
|
||||||
|
return TradesPageModel.fromJson(response.data);
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, dynamic>> estimateSell(String quantity) async {
|
Future<Map<String, dynamic>> estimateSell(String quantity) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import '../../domain/entities/trade_order.dart';
|
import '../../domain/entities/trade_order.dart';
|
||||||
|
import '../../domain/entities/trade_record.dart';
|
||||||
|
|
||||||
class TradeOrderModel extends TradeOrder {
|
class TradeOrderModel extends TradeOrder {
|
||||||
const TradeOrderModel({
|
const TradeOrderModel({
|
||||||
|
|
@ -86,3 +87,55 @@ class OrdersPageModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 成交记录 Model
|
||||||
|
class TradeRecordModel extends TradeRecord {
|
||||||
|
const TradeRecordModel({
|
||||||
|
required super.id,
|
||||||
|
required super.tradeNo,
|
||||||
|
required super.type,
|
||||||
|
required super.price,
|
||||||
|
required super.quantity,
|
||||||
|
required super.amount,
|
||||||
|
required super.fee,
|
||||||
|
required super.netAmount,
|
||||||
|
required super.counterparty,
|
||||||
|
required super.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory TradeRecordModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return TradeRecordModel(
|
||||||
|
id: json['id']?.toString() ?? '',
|
||||||
|
tradeNo: json['tradeNo']?.toString() ?? '',
|
||||||
|
type: json['type'] == 'BUY' ? TradeType.buy : TradeType.sell,
|
||||||
|
price: json['price']?.toString() ?? '0',
|
||||||
|
quantity: json['quantity']?.toString() ?? '0',
|
||||||
|
amount: json['amount']?.toString() ?? '0',
|
||||||
|
fee: json['fee']?.toString() ?? '0',
|
||||||
|
netAmount: json['netAmount']?.toString() ?? '0',
|
||||||
|
counterparty: json['counterparty']?.toString() ?? '',
|
||||||
|
createdAt: json['createdAt'] != null
|
||||||
|
? DateTime.parse(json['createdAt'].toString())
|
||||||
|
: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 成交记录列表响应
|
||||||
|
class TradesPageModel {
|
||||||
|
final List<TradeRecordModel> data;
|
||||||
|
final int total;
|
||||||
|
|
||||||
|
const TradesPageModel({
|
||||||
|
required this.data,
|
||||||
|
required this.total,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory TradesPageModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
final dataList = (json['data'] as List<dynamic>?) ?? [];
|
||||||
|
return TradesPageModel(
|
||||||
|
data: dataList.map((e) => TradeRecordModel.fromJson(e)).toList(),
|
||||||
|
total: json['total'] ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,24 @@ class TradingRepositoryImpl implements TradingRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, TradesPageModel>> getTrades({
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final result = await remoteDataSource.getTrades(
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
);
|
||||||
|
return Right(result);
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} on NetworkException {
|
||||||
|
return Left(const NetworkFailure());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Either<Failure, Map<String, dynamic>>> estimateSell(String quantity) async {
|
Future<Either<Failure, Map<String, dynamic>>> estimateSell(String quantity) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
enum TradeType { buy, sell }
|
||||||
|
|
||||||
|
/// 成交记录实体(包含手续费明细)
|
||||||
|
class TradeRecord extends Equatable {
|
||||||
|
/// 成交ID
|
||||||
|
final String id;
|
||||||
|
/// 成交编号
|
||||||
|
final String tradeNo;
|
||||||
|
/// 交易类型
|
||||||
|
final TradeType type;
|
||||||
|
/// 成交价格
|
||||||
|
final String price;
|
||||||
|
/// 成交数量
|
||||||
|
final String quantity;
|
||||||
|
/// 成交金额(卖方扣除手续费后的金额)
|
||||||
|
final String amount;
|
||||||
|
/// 手续费(卖方支付,买方为0)
|
||||||
|
final String fee;
|
||||||
|
/// 净额(实际收到/支付金额)
|
||||||
|
final String netAmount;
|
||||||
|
/// 对手方账号
|
||||||
|
final String counterparty;
|
||||||
|
/// 成交时间
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const TradeRecord({
|
||||||
|
required this.id,
|
||||||
|
required this.tradeNo,
|
||||||
|
required this.type,
|
||||||
|
required this.price,
|
||||||
|
required this.quantity,
|
||||||
|
required this.amount,
|
||||||
|
required this.fee,
|
||||||
|
required this.netAmount,
|
||||||
|
required this.counterparty,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isBuy => type == TradeType.buy;
|
||||||
|
bool get isSell => type == TradeType.sell;
|
||||||
|
|
||||||
|
/// 是否有手续费
|
||||||
|
bool get hasFee {
|
||||||
|
final feeValue = double.tryParse(fee) ?? 0;
|
||||||
|
return feeValue > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, tradeNo, type, price, quantity, fee];
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,12 @@ abstract class TradingRepository {
|
||||||
int pageSize = 50,
|
int pageSize = 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 获取用户成交记录(含手续费明细)
|
||||||
|
Future<Either<Failure, TradesPageModel>> getTrades({
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
});
|
||||||
|
|
||||||
/// 预估卖出收益
|
/// 预估卖出收益
|
||||||
Future<Either<Failure, Map<String, dynamic>>> estimateSell(String quantity);
|
Future<Either<Failure, Map<String, dynamic>>> estimateSell(String quantity);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../../../core/constants/app_colors.dart';
|
import '../../../core/constants/app_colors.dart';
|
||||||
|
import '../../../core/utils/format_utils.dart';
|
||||||
import '../../../domain/entities/trade_order.dart';
|
import '../../../domain/entities/trade_order.dart';
|
||||||
|
import '../../../domain/entities/trade_record.dart';
|
||||||
import '../../../data/models/trade_order_model.dart';
|
import '../../../data/models/trade_order_model.dart';
|
||||||
import '../../providers/trading_providers.dart';
|
import '../../providers/trading_providers.dart';
|
||||||
|
|
||||||
|
|
@ -14,7 +16,7 @@ class TradingRecordsPage extends ConsumerStatefulWidget {
|
||||||
ConsumerState<TradingRecordsPage> createState() => _TradingRecordsPageState();
|
ConsumerState<TradingRecordsPage> createState() => _TradingRecordsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> with SingleTickerProviderStateMixin {
|
||||||
static const Color _orange = Color(0xFFFF6B00);
|
static const Color _orange = Color(0xFFFF6B00);
|
||||||
static const Color _green = Color(0xFF22C55E);
|
static const Color _green = Color(0xFF22C55E);
|
||||||
static const Color _red = Color(0xFFEF4444);
|
static const Color _red = Color(0xFFEF4444);
|
||||||
|
|
@ -22,10 +24,22 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
static const Color _darkText = Color(0xFF1F2937);
|
static const Color _darkText = Color(0xFF1F2937);
|
||||||
static const Color _lightGray = Color(0xFF9CA3AF);
|
static const Color _lightGray = Color(0xFF9CA3AF);
|
||||||
|
|
||||||
|
late TabController _tabController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tabController = TabController(length: 2, vsync: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ordersAsync = ref.watch(ordersProvider);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
|
@ -44,22 +58,66 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
bottom: TabBar(
|
||||||
body: RefreshIndicator(
|
controller: _tabController,
|
||||||
onRefresh: () async {
|
labelColor: _orange,
|
||||||
ref.invalidate(ordersProvider);
|
unselectedLabelColor: _grayText,
|
||||||
},
|
indicatorColor: _orange,
|
||||||
child: ordersAsync.when(
|
tabs: const [
|
||||||
loading: () => _buildLoadingList(),
|
Tab(text: '订单记录'),
|
||||||
error: (error, stack) => _buildErrorView(error.toString()),
|
Tab(text: '成交明细'),
|
||||||
data: (ordersPage) {
|
],
|
||||||
if (ordersPage == null || ordersPage.data.isEmpty) {
|
|
||||||
return _buildEmptyView();
|
|
||||||
}
|
|
||||||
return _buildOrdersList(ordersPage);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
body: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: [
|
||||||
|
_buildOrdersTab(),
|
||||||
|
_buildTradesTab(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 订单记录 Tab ====================
|
||||||
|
Widget _buildOrdersTab() {
|
||||||
|
final ordersAsync = ref.watch(ordersProvider);
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.invalidate(ordersProvider);
|
||||||
|
},
|
||||||
|
child: ordersAsync.when(
|
||||||
|
loading: () => _buildLoadingList(),
|
||||||
|
error: (error, stack) => _buildErrorView(error.toString(), () => ref.invalidate(ordersProvider)),
|
||||||
|
data: (ordersPage) {
|
||||||
|
if (ordersPage == null || ordersPage.data.isEmpty) {
|
||||||
|
return _buildEmptyView('暂无订单记录', '前往交易页面进行买卖操作');
|
||||||
|
}
|
||||||
|
return _buildOrdersList(ordersPage);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 成交明细 Tab ====================
|
||||||
|
Widget _buildTradesTab() {
|
||||||
|
final tradesAsync = ref.watch(tradesProvider);
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.invalidate(tradesProvider);
|
||||||
|
},
|
||||||
|
child: tradesAsync.when(
|
||||||
|
loading: () => _buildLoadingList(),
|
||||||
|
error: (error, stack) => _buildErrorView(error.toString(), () => ref.invalidate(tradesProvider)),
|
||||||
|
data: (tradesPage) {
|
||||||
|
if (tradesPage == null || tradesPage.data.isEmpty) {
|
||||||
|
return _buildEmptyView('暂无成交记录', '成交后将在此显示明细');
|
||||||
|
}
|
||||||
|
return _buildTradesList(tradesPage);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,34 +183,12 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 120,
|
|
||||||
height: 12,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[300],
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 100,
|
|
||||||
height: 12,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[300],
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildErrorView(String error) {
|
Widget _buildErrorView(String error, VoidCallback onRetry) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|
@ -171,9 +207,7 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: onRetry,
|
||||||
ref.invalidate(ordersProvider);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: _orange),
|
style: ElevatedButton.styleFrom(backgroundColor: _orange),
|
||||||
child: const Text('重试', style: TextStyle(color: Colors.white)),
|
child: const Text('重试', style: TextStyle(color: Colors.white)),
|
||||||
),
|
),
|
||||||
|
|
@ -182,22 +216,16 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyView() {
|
Widget _buildEmptyView(String title, String subtitle) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.receipt_long_outlined, size: 64, color: _grayText.withValues(alpha: 0.5)),
|
Icon(Icons.receipt_long_outlined, size: 64, color: _grayText.withValues(alpha: 0.5)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(title, style: TextStyle(fontSize: 16, color: _grayText)),
|
||||||
'暂无交易记录',
|
|
||||||
style: TextStyle(fontSize: 16, color: _grayText),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(subtitle, style: TextStyle(fontSize: 14, color: _grayText.withValues(alpha: 0.7))),
|
||||||
'前往交易页面进行买卖操作',
|
|
||||||
style: TextStyle(fontSize: 14, color: _grayText.withValues(alpha: 0.7)),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -209,13 +237,26 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
itemCount: ordersPage.data.length + 1,
|
itemCount: ordersPage.data.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == ordersPage.data.length) {
|
if (index == ordersPage.data.length) {
|
||||||
return _buildBottomInfo(ordersPage.total);
|
return _buildBottomInfo('共 ${ordersPage.total} 条订单记录');
|
||||||
}
|
}
|
||||||
return _buildOrderCard(ordersPage.data[index]);
|
return _buildOrderCard(ordersPage.data[index]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTradesList(TradesPageModel tradesPage) {
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: tradesPage.data.length + 1,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == tradesPage.data.length) {
|
||||||
|
return _buildBottomInfo('共 ${tradesPage.total} 条成交记录');
|
||||||
|
}
|
||||||
|
return _buildTradeCard(tradesPage.data[index]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildOrderCard(TradeOrder order) {
|
Widget _buildOrderCard(TradeOrder order) {
|
||||||
final isBuy = order.isBuy;
|
final isBuy = order.isBuy;
|
||||||
final typeColor = isBuy ? _green : _red;
|
final typeColor = isBuy ? _green : _red;
|
||||||
|
|
@ -233,7 +274,6 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 第一行:类型标签 + 状态
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -245,11 +285,7 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
typeText,
|
typeText,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: typeColor),
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: typeColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
|
|
@ -260,18 +296,12 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
statusText,
|
statusText,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: statusColor),
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: statusColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// 第二行:价格和数量
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -280,8 +310,6 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 第三行:成交数量和金额
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -290,26 +318,17 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 第四行:订单号和时间
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('订单号: ', style: TextStyle(fontSize: 11, color: _lightGray)),
|
||||||
'订单号: ',
|
|
||||||
style: TextStyle(fontSize: 11, color: _lightGray),
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
order.orderNo,
|
order.orderNo,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 11, color: _grayText, fontFamily: 'monospace'),
|
||||||
fontSize: 11,
|
|
||||||
color: _grayText,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -334,19 +353,110 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInfoItem(String label, String value) {
|
Widget _buildTradeCard(TradeRecord trade) {
|
||||||
|
final isBuy = trade.isBuy;
|
||||||
|
final typeColor = isBuy ? _green : _red;
|
||||||
|
final typeText = isBuy ? '买入' : '卖出';
|
||||||
|
|
||||||
|
// 计算交易总额(卖出时:netAmount + fee)
|
||||||
|
final grossAmount = isBuy
|
||||||
|
? double.tryParse(trade.amount) ?? 0
|
||||||
|
: (double.tryParse(trade.netAmount) ?? 0) + (double.tryParse(trade.fee) ?? 0);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 第一行:类型标签
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: typeColor.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
typeText,
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: typeColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
DateFormat('MM-dd HH:mm').format(trade.createdAt),
|
||||||
|
style: TextStyle(fontSize: 12, color: _lightGray),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 第二行:价格和数量
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildInfoItem('价格', _formatPrice(trade.price)),
|
||||||
|
_buildInfoItem('数量', _formatQuantity(trade.quantity)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// 第三行:交易总额
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildInfoItem('交易总额', '${formatAmount(grossAmount.toString())} 积分值'),
|
||||||
|
if (trade.hasFee)
|
||||||
|
_buildInfoItem('手续费(10%)', '${formatAmount(trade.fee)} 积分值', valueColor: _red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// 第四行:实际收到(仅卖出时显示)
|
||||||
|
if (trade.isSell && trade.hasFee) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildInfoItem('实际收到', '${formatAmount(trade.netAmount)} 积分值', valueColor: _green),
|
||||||
|
const SizedBox(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// 成交编号
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text('成交编号: ', style: TextStyle(fontSize: 11, color: _lightGray)),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
trade.tradeNo,
|
||||||
|
style: TextStyle(fontSize: 11, color: _grayText, fontFamily: 'monospace'),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoItem(String label, String value, {Color? valueColor}) {
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('$label: ', style: TextStyle(fontSize: 12, color: _grayText)),
|
||||||
'$label: ',
|
|
||||||
style: TextStyle(fontSize: 12, color: _grayText),
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: _darkText,
|
color: valueColor ?? _darkText,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
),
|
),
|
||||||
|
|
@ -402,14 +512,11 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBottomInfo(int total) {
|
Widget _buildBottomInfo(String text) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(text, style: TextStyle(fontSize: 12, color: _grayText)),
|
||||||
'共 $total 条交易记录',
|
|
||||||
style: TextStyle(fontSize: 12, color: _grayText),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,23 @@ final ordersProvider = FutureProvider<OrdersPageModel?>((ref) async {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 成交记录列表 Provider(含手续费明细)
|
||||||
|
final tradesProvider = FutureProvider<TradesPageModel?>((ref) async {
|
||||||
|
final repository = ref.watch(tradingRepositoryProvider);
|
||||||
|
final result = await repository.getTrades(page: 1, pageSize: 50);
|
||||||
|
|
||||||
|
ref.keepAlive();
|
||||||
|
final timer = Timer(const Duration(seconds: 30), () {
|
||||||
|
ref.invalidateSelf();
|
||||||
|
});
|
||||||
|
ref.onDispose(() => timer.cancel());
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) => throw Exception(failure.message),
|
||||||
|
(trades) => trades,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// 交易状态
|
// 交易状态
|
||||||
class TradingState {
|
class TradingState {
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue