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,
|
||||
};
|
||||
}
|
||||
|
||||
@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 {
|
||||
return OrderAggregate.reconstitute({
|
||||
id: record.id,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class ApiEndpoints {
|
|||
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 const String trades = '/api/v2/trading/trading/trades';
|
||||
static String cancelOrder(String orderNo) =>
|
||||
'/api/v2/trading/trading/orders/$orderNo/cancel';
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,12 @@ abstract class TradingRemoteDataSource {
|
|||
int pageSize = 50,
|
||||
});
|
||||
|
||||
/// 获取用户成交记录(含手续费明细)
|
||||
Future<TradesPageModel> getTrades({
|
||||
int page = 1,
|
||||
int pageSize = 50,
|
||||
});
|
||||
|
||||
/// 预估卖出收益
|
||||
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
|
||||
Future<Map<String, dynamic>> estimateSell(String quantity) async {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import '../../domain/entities/trade_order.dart';
|
||||
import '../../domain/entities/trade_record.dart';
|
||||
|
||||
class TradeOrderModel extends TradeOrder {
|
||||
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
|
||||
Future<Either<Failure, Map<String, dynamic>>> estimateSell(String quantity) async {
|
||||
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,
|
||||
});
|
||||
|
||||
/// 获取用户成交记录(含手续费明细)
|
||||
Future<Either<Failure, TradesPageModel>> getTrades({
|
||||
int page = 1,
|
||||
int pageSize = 50,
|
||||
});
|
||||
|
||||
/// 预估卖出收益
|
||||
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:intl/intl.dart';
|
||||
import '../../../core/constants/app_colors.dart';
|
||||
import '../../../core/utils/format_utils.dart';
|
||||
import '../../../domain/entities/trade_order.dart';
|
||||
import '../../../domain/entities/trade_record.dart';
|
||||
import '../../../data/models/trade_order_model.dart';
|
||||
import '../../providers/trading_providers.dart';
|
||||
|
||||
|
|
@ -14,7 +16,7 @@ class TradingRecordsPage extends ConsumerStatefulWidget {
|
|||
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 _green = Color(0xFF22C55E);
|
||||
static const Color _red = Color(0xFFEF4444);
|
||||
|
|
@ -22,10 +24,22 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
|||
static const Color _darkText = Color(0xFF1F2937);
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final ordersAsync = ref.watch(ordersProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: AppBar(
|
||||
|
|
@ -44,22 +58,66 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
|||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(ordersProvider);
|
||||
},
|
||||
child: ordersAsync.when(
|
||||
loading: () => _buildLoadingList(),
|
||||
error: (error, stack) => _buildErrorView(error.toString()),
|
||||
data: (ordersPage) {
|
||||
if (ordersPage == null || ordersPage.data.isEmpty) {
|
||||
return _buildEmptyView();
|
||||
}
|
||||
return _buildOrdersList(ordersPage);
|
||||
},
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: _orange,
|
||||
unselectedLabelColor: _grayText,
|
||||
indicatorColor: _orange,
|
||||
tabs: const [
|
||||
Tab(text: '订单记录'),
|
||||
Tab(text: '成交明细'),
|
||||
],
|
||||
),
|
||||
),
|
||||
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(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
@ -171,9 +207,7 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.invalidate(ordersProvider);
|
||||
},
|
||||
onPressed: onRetry,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: _orange),
|
||||
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(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.receipt_long_outlined, size: 64, color: _grayText.withValues(alpha: 0.5)),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无交易记录',
|
||||
style: TextStyle(fontSize: 16, color: _grayText),
|
||||
),
|
||||
Text(title, style: TextStyle(fontSize: 16, color: _grayText)),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'前往交易页面进行买卖操作',
|
||||
style: TextStyle(fontSize: 14, color: _grayText.withValues(alpha: 0.7)),
|
||||
),
|
||||
Text(subtitle, style: TextStyle(fontSize: 14, color: _grayText.withValues(alpha: 0.7))),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -209,13 +237,26 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
|||
itemCount: ordersPage.data.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == ordersPage.data.length) {
|
||||
return _buildBottomInfo(ordersPage.total);
|
||||
return _buildBottomInfo('共 ${ordersPage.total} 条订单记录');
|
||||
}
|
||||
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) {
|
||||
final isBuy = order.isBuy;
|
||||
final typeColor = isBuy ? _green : _red;
|
||||
|
|
@ -233,7 +274,6 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 第一行:类型标签 + 状态
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
|
@ -245,11 +285,7 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
|||
),
|
||||
child: Text(
|
||||
typeText,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: typeColor,
|
||||
),
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: typeColor),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
|
|
@ -260,18 +296,12 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
|||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: statusColor,
|
||||
),
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: statusColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 第二行:价格和数量
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
|
@ -280,8 +310,6 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 第三行:成交数量和金额
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
|
@ -290,26 +318,17 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 第四行:订单号和时间
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'订单号: ',
|
||||
style: TextStyle(fontSize: 11, color: _lightGray),
|
||||
),
|
||||
Text('订单号: ', style: TextStyle(fontSize: 11, color: _lightGray)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
order.orderNo,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: _grayText,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
style: TextStyle(fontSize: 11, color: _grayText, fontFamily: 'monospace'),
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'$label: ',
|
||||
style: TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
Text('$label: ', style: TextStyle(fontSize: 12, color: _grayText)),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _darkText,
|
||||
color: valueColor ?? _darkText,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
|
|
@ -402,14 +512,11 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> {
|
|||
}
|
||||
}
|
||||
|
||||
Widget _buildBottomInfo(int total) {
|
||||
Widget _buildBottomInfo(String text) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'共 $total 条交易记录',
|
||||
style: TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
child: Text(text, 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 {
|
||||
final bool isLoading;
|
||||
|
|
|
|||
Loading…
Reference in New Issue