970 lines
31 KiB
Dart
970 lines
31 KiB
Dart
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 '../../../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';
|
||
|
||
class TradingPage extends ConsumerStatefulWidget {
|
||
const TradingPage({super.key});
|
||
|
||
@override
|
||
ConsumerState<TradingPage> createState() => _TradingPageState();
|
||
}
|
||
|
||
class _TradingPageState extends ConsumerState<TradingPage> {
|
||
// 设计色彩
|
||
static const Color _orange = Color(0xFFFF6B00);
|
||
static const Color _green = Color(0xFF10B981);
|
||
static const Color _red = Color(0xFFEF4444);
|
||
static const Color _grayText = Color(0xFF6B7280);
|
||
static const Color _darkText = Color(0xFF1F2937);
|
||
static const Color _bgGray = Color(0xFFF3F4F6);
|
||
static const Color _lightGray = Color(0xFFF9FAFB);
|
||
static const Color _borderGray = Color(0xFFE5E7EB);
|
||
|
||
// 状态
|
||
int _selectedTab = 1; // 0: 买入, 1: 卖出
|
||
int _selectedTimeRange = 1; // 时间周期选择
|
||
final _quantityController = TextEditingController();
|
||
final _priceController = TextEditingController();
|
||
|
||
final List<String> _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日'];
|
||
|
||
@override
|
||
void dispose() {
|
||
_quantityController.dispose();
|
||
_priceController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final priceAsync = ref.watch(currentPriceProvider);
|
||
final marketAsync = ref.watch(marketOverviewProvider);
|
||
final ordersAsync = ref.watch(ordersProvider);
|
||
final user = ref.watch(userNotifierProvider);
|
||
final accountSequence = user.accountSequence ?? '';
|
||
|
||
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: [
|
||
_buildPriceCard(priceAsync),
|
||
_buildChartSection(priceAsync),
|
||
_buildMarketDataCard(marketAsync),
|
||
_buildTradingPanel(priceAsync),
|
||
_buildMyOrdersCard(ordersAsync),
|
||
const SizedBox(height: 100),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildAppBar() {
|
||
return Container(
|
||
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(
|
||
color: _orange,
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
child: const Text(
|
||
'积分股交易',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(20),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.1),
|
||
blurRadius: 4,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
const Center(
|
||
child: Icon(Icons.notifications_outlined, color: _grayText),
|
||
),
|
||
Positioned(
|
||
right: 10,
|
||
top: 10,
|
||
child: Container(
|
||
width: 8,
|
||
height: 8,
|
||
decoration: const BoxDecoration(
|
||
color: Colors.red,
|
||
shape: BoxShape.circle,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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),
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
const Text(
|
||
'当前积分股价格',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: _grayText,
|
||
),
|
||
),
|
||
DataText(
|
||
data: priceInfo != null ? '= ${formatCompact(greenPoints)} 绿积分' : null,
|
||
isLoading: isLoading,
|
||
placeholder: '= -- 绿积分',
|
||
style: const TextStyle(fontSize: 12, color: _grayText),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
AmountText(
|
||
amount: priceInfo != null ? formatPrice(price) : null,
|
||
isLoading: isLoading,
|
||
prefix: '\u00A5 ',
|
||
style: const TextStyle(
|
||
fontSize: 30,
|
||
fontWeight: FontWeight.bold,
|
||
color: _orange,
|
||
letterSpacing: -0.75,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: _green.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const Icon(Icons.trending_up, size: 16, color: _green),
|
||
DataText(
|
||
data: isLoading ? null : '+0.00%',
|
||
isLoading: isLoading,
|
||
placeholder: '+--.--%',
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _green,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Container(
|
||
height: 200,
|
||
decoration: BoxDecoration(
|
||
color: _lightGray,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
CustomPaint(
|
||
size: const Size(double.infinity, 200),
|
||
painter: _CandlestickPainter(),
|
||
),
|
||
Positioned(
|
||
right: 0,
|
||
top: 60,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: _orange,
|
||
borderRadius: BorderRadius.circular(4),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.1),
|
||
blurRadius: 2,
|
||
offset: const Offset(0, 1),
|
||
),
|
||
],
|
||
),
|
||
child: Text(
|
||
formatPrice(currentPrice),
|
||
style: const TextStyle(fontSize: 10, color: Colors.white),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
SingleChildScrollView(
|
||
scrollDirection: Axis.horizontal,
|
||
child: Row(
|
||
children: List.generate(_timeRanges.length, (index) {
|
||
final isSelected = _selectedTimeRange == index;
|
||
return Padding(
|
||
padding: const EdgeInsets.only(right: 8),
|
||
child: GestureDetector(
|
||
onTap: () => setState(() => _selectedTimeRange = index),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: isSelected ? _orange : Colors.white,
|
||
borderRadius: BorderRadius.circular(9999),
|
||
border: isSelected ? null : Border.all(color: _borderGray),
|
||
),
|
||
child: Text(
|
||
_timeRanges[index],
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: isSelected ? Colors.white : _grayText,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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),
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Container(
|
||
width: 4,
|
||
height: 16,
|
||
decoration: BoxDecoration(
|
||
color: _orange,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'市场数据',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _darkText,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 24),
|
||
Row(
|
||
children: [
|
||
_buildMarketDataItem(
|
||
'总积分股',
|
||
market != null ? formatCompact(market.totalShares) : null,
|
||
_orange,
|
||
isLoading,
|
||
),
|
||
Container(width: 1, height: 24, color: _bgGray),
|
||
const SizedBox(width: 16),
|
||
_buildMarketDataItem(
|
||
'流通池',
|
||
market != null ? formatCompact(market.circulationPool) : null,
|
||
_orange,
|
||
isLoading,
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 24),
|
||
Container(height: 1, color: _bgGray),
|
||
const SizedBox(height: 24),
|
||
Row(
|
||
children: [
|
||
_buildMarketDataItem(
|
||
'绿积分池',
|
||
market != null ? formatCompact(market.greenPoints) : null,
|
||
_orange,
|
||
isLoading,
|
||
),
|
||
Container(width: 1, height: 24, color: _bgGray),
|
||
const SizedBox(width: 16),
|
||
_buildMarketDataItem(
|
||
'黑洞销毁量',
|
||
market != null ? formatCompact(market.blackHoleAmount) : null,
|
||
_red,
|
||
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)),
|
||
const SizedBox(height: 4),
|
||
DataText(
|
||
data: value,
|
||
isLoading: isLoading,
|
||
placeholder: '--,---,---',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: valueColor,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Container(
|
||
decoration: const BoxDecoration(
|
||
border: Border(bottom: BorderSide(color: _bgGray)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: () => setState(() => _selectedTab = 0),
|
||
child: Container(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
decoration: BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(
|
||
color: _selectedTab == 0 ? _orange : Colors.transparent,
|
||
width: 2,
|
||
),
|
||
),
|
||
),
|
||
child: Text(
|
||
'买入',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _selectedTab == 0 ? _orange : _grayText,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: () => setState(() => _selectedTab = 1),
|
||
child: Container(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
decoration: BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(
|
||
color: _selectedTab == 1 ? _orange : Colors.transparent,
|
||
width: 2,
|
||
),
|
||
),
|
||
),
|
||
child: Text(
|
||
'卖出',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _selectedTab == 1 ? _orange : _grayText,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
// 价格输入
|
||
_buildInputField('价格', _priceController, '请输入价格', '绿积分'),
|
||
const SizedBox(height: 16),
|
||
// 数量输入
|
||
_buildInputField('数量', _quantityController, '请输入数量', '积分股'),
|
||
const SizedBox(height: 16),
|
||
// 预计获得/支出
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: _bgGray,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
_selectedTab == 0 ? '预计支出' : '预计获得',
|
||
style: const TextStyle(fontSize: 12, color: _grayText),
|
||
),
|
||
Text(
|
||
_calculateEstimate(),
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _orange,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
// 销毁说明 (卖出时显示)
|
||
if (_selectedTab == 1)
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: const [
|
||
Text(
|
||
'销毁比例',
|
||
style: TextStyle(fontSize: 12, color: _grayText),
|
||
),
|
||
Text(
|
||
'10% 进入黑洞',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: _red,
|
||
fontFamily: 'monospace',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
// 提交按钮
|
||
SizedBox(
|
||
width: double.infinity,
|
||
height: 48,
|
||
child: ElevatedButton(
|
||
onPressed: _handleTrade,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: _orange,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
),
|
||
child: Text(
|
||
_selectedTab == 0 ? '买入积分股' : '卖出积分股',
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
const Text(
|
||
'我的挂单',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _darkText,
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () {
|
||
// TODO: 查看全部挂单
|
||
},
|
||
child: const Text(
|
||
'全部 >',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: _orange,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
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 _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(
|
||
color: _lightGray,
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: _bgGray),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: (isSell ? _red : _green).withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Text(
|
||
isSell ? '卖出' : '买入',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.bold,
|
||
color: isSell ? _red : _green,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
formatPrice(order.price),
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _darkText,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
formattedDate,
|
||
style: const TextStyle(fontSize: 12, color: _grayText),
|
||
),
|
||
],
|
||
),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
Text(
|
||
'${formatCompact(order.quantity)} 股',
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w500,
|
||
color: _darkText,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: _orange.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
child: Text(
|
||
statusText,
|
||
style: const TextStyle(fontSize: 12, color: _orange),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildErrorCard(String message) {
|
||
return Container(
|
||
margin: const EdgeInsets.all(16),
|
||
padding: const EdgeInsets.all(32),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Center(
|
||
child: Column(
|
||
children: [
|
||
const Icon(Icons.error_outline, size: 48, color: AppColors.error),
|
||
const SizedBox(height: 8),
|
||
Text(message),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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('请输入数量')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
final isBuy = _selectedTab == 0;
|
||
bool success;
|
||
|
||
if (isBuy) {
|
||
success = await ref
|
||
.read(tradingNotifierProvider.notifier)
|
||
.buyShares(_priceController.text, _quantityController.text);
|
||
} else {
|
||
success = await ref
|
||
.read(tradingNotifierProvider.notifier)
|
||
.sellShares(_priceController.text, _quantityController.text);
|
||
}
|
||
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(success
|
||
? (isBuy ? '买入订单已提交' : '卖出订单已提交')
|
||
: (isBuy ? '买入失败' : '卖出失败')),
|
||
backgroundColor: success ? _green : AppColors.error,
|
||
),
|
||
);
|
||
if (success) {
|
||
_quantityController.clear();
|
||
// 交易成功后刷新订单列表
|
||
ref.invalidate(ordersProvider);
|
||
ref.invalidate(currentPriceProvider);
|
||
ref.invalidate(marketOverviewProvider);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// K线图绘制器(简化版本,显示模拟数据)
|
||
class _CandlestickPainter extends CustomPainter {
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final greenPaint = Paint()..color = const Color(0xFF10B981);
|
||
final redPaint = Paint()..color = const Color(0xFFEF4444);
|
||
final dashPaint = Paint()
|
||
..color = const Color(0xFFFF6B00)
|
||
..strokeWidth = 1
|
||
..style = PaintingStyle.stroke;
|
||
|
||
// 模拟K线数据
|
||
final candleData = [
|
||
{'open': 0.6, 'close': 0.5, 'high': 0.7, 'low': 0.45},
|
||
{'open': 0.5, 'close': 0.55, 'high': 0.6, 'low': 0.48},
|
||
{'open': 0.55, 'close': 0.52, 'high': 0.58, 'low': 0.5},
|
||
{'open': 0.52, 'close': 0.6, 'high': 0.65, 'low': 0.5},
|
||
{'open': 0.6, 'close': 0.58, 'high': 0.65, 'low': 0.55},
|
||
{'open': 0.58, 'close': 0.62, 'high': 0.68, 'low': 0.55},
|
||
{'open': 0.62, 'close': 0.55, 'high': 0.65, 'low': 0.52},
|
||
{'open': 0.55, 'close': 0.58, 'high': 0.62, 'low': 0.52},
|
||
{'open': 0.58, 'close': 0.52, 'high': 0.6, 'low': 0.5},
|
||
{'open': 0.52, 'close': 0.65, 'high': 0.7, 'low': 0.5},
|
||
{'open': 0.65, 'close': 0.7, 'high': 0.75, 'low': 0.62},
|
||
{'open': 0.7, 'close': 0.75, 'high': 0.8, 'low': 0.68},
|
||
];
|
||
|
||
final candleWidth = (size.width - 40) / candleData.length;
|
||
const padding = 20.0;
|
||
|
||
for (int i = 0; i < candleData.length; i++) {
|
||
final data = candleData[i];
|
||
final open = data['open']!;
|
||
final close = data['close']!;
|
||
final high = data['high']!;
|
||
final low = data['low']!;
|
||
|
||
final isGreen = close >= open;
|
||
final paint = isGreen ? greenPaint : redPaint;
|
||
|
||
final x = padding + i * candleWidth + candleWidth / 2;
|
||
final yOpen = size.height - (open * size.height * 0.8 + size.height * 0.1);
|
||
final yClose = size.height - (close * size.height * 0.8 + size.height * 0.1);
|
||
final yHigh = size.height - (high * size.height * 0.8 + size.height * 0.1);
|
||
final yLow = size.height - (low * size.height * 0.8 + size.height * 0.1);
|
||
|
||
canvas.drawLine(
|
||
Offset(x, yHigh),
|
||
Offset(x, yLow),
|
||
paint..strokeWidth = 1,
|
||
);
|
||
|
||
final bodyTop = isGreen ? yClose : yOpen;
|
||
final bodyBottom = isGreen ? yOpen : yClose;
|
||
canvas.drawRect(
|
||
Rect.fromLTRB(x - candleWidth * 0.3, bodyTop, x + candleWidth * 0.3, bodyBottom),
|
||
paint..style = PaintingStyle.fill,
|
||
);
|
||
}
|
||
|
||
final dashY = size.height * 0.35;
|
||
const dashWidth = 5.0;
|
||
const dashSpace = 3.0;
|
||
double startX = 0;
|
||
while (startX < size.width - 60) {
|
||
canvas.drawLine(
|
||
Offset(startX, dashY),
|
||
Offset(startX + dashWidth, dashY),
|
||
dashPaint,
|
||
);
|
||
startX += dashWidth + dashSpace;
|
||
}
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||
}
|