933 lines
30 KiB
Dart
933 lines
30 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import '../../../core/constants/app_colors.dart';
|
||
import '../../../core/utils/format_utils.dart';
|
||
import '../../providers/mining_providers.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 _amountController = TextEditingController();
|
||
|
||
final List<String> _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日'];
|
||
|
||
@override
|
||
void dispose() {
|
||
_amountController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final globalState = ref.watch(globalStateProvider);
|
||
final user = ref.watch(userNotifierProvider);
|
||
final accountSequence = user.accountSequence ?? '';
|
||
|
||
return Scaffold(
|
||
backgroundColor: const Color(0xFFF5F5F5),
|
||
body: SafeArea(
|
||
bottom: false,
|
||
child: Column(
|
||
children: [
|
||
// 顶部导航栏
|
||
_buildAppBar(),
|
||
// 可滚动内容
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
children: [
|
||
// 价格卡片
|
||
globalState.when(
|
||
data: (state) => _buildPriceCard(state),
|
||
loading: () => _buildLoadingCard(),
|
||
error: (_, __) => _buildErrorCard('价格加载失败'),
|
||
),
|
||
// K线图占位区域
|
||
_buildChartSection(),
|
||
// 市场数据
|
||
globalState.when(
|
||
data: (state) => _buildMarketDataCard(state),
|
||
loading: () => _buildLoadingCard(),
|
||
error: (_, __) => _buildErrorCard('市场数据加载失败'),
|
||
),
|
||
// 买入/卖出交易面板
|
||
_buildTradingPanel(accountSequence),
|
||
// 我的挂单
|
||
_buildMyOrdersCard(),
|
||
const SizedBox(height: 100),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildAppBar() {
|
||
return Container(
|
||
color: _bgGray.withOpacity(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.withOpacity(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(state) {
|
||
final isPriceUp = state?.isPriceUp ?? true;
|
||
final currentPrice = state?.currentPrice ?? '0.000156';
|
||
final priceChange = state?.priceChange24h ?? '8.52';
|
||
|
||
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: [
|
||
Text(
|
||
'当前积分股价格',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: _grayText,
|
||
),
|
||
),
|
||
Text(
|
||
'= 156.00 绿积分',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: _grayText,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
// 价格和涨跌
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
'¥ ${formatPrice(currentPrice)}',
|
||
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.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
isPriceUp ? Icons.trending_up : Icons.trending_down,
|
||
size: 16,
|
||
color: _green,
|
||
),
|
||
Text(
|
||
'+$priceChange%',
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _green,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildChartSection() {
|
||
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: [
|
||
// K线图占位
|
||
Container(
|
||
height: 200,
|
||
decoration: BoxDecoration(
|
||
color: _lightGray,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
// 模拟K线图
|
||
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.withOpacity(0.1),
|
||
blurRadius: 2,
|
||
offset: const Offset(0, 1),
|
||
),
|
||
],
|
||
),
|
||
child: const Text(
|
||
'0.000156',
|
||
style: 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(state) {
|
||
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('积分股池', '8,888,888,888', _orange),
|
||
Container(width: 1, height: 24, color: _bgGray),
|
||
const SizedBox(width: 16),
|
||
_buildMarketDataItem('流通池', '1,234,567', _orange),
|
||
],
|
||
),
|
||
const SizedBox(height: 24),
|
||
Container(height: 1, color: _bgGray),
|
||
const SizedBox(height: 24),
|
||
// 第二行数据
|
||
Row(
|
||
children: [
|
||
_buildMarketDataItem('绿积分池', '99,999,999', _orange),
|
||
Container(width: 1, height: 24, color: _bgGray),
|
||
const SizedBox(width: 16),
|
||
_buildMarketDataItem('黑洞销毁量', '50,000,000', _red),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildMarketDataItem(String label, String value, Color valueColor) {
|
||
return Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
color: _grayText,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
value,
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: valueColor,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTradingPanel(String accountSequence) {
|
||
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),
|
||
// 数量输入
|
||
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,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
// 预计获得
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: _bgGray,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
const Text(
|
||
'预计获得',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: _grayText,
|
||
),
|
||
),
|
||
const Text(
|
||
'0.00 绿积分',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _orange,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
// 手续费
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: const [
|
||
Text(
|
||
'手续费 (10%)',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: _grayText,
|
||
),
|
||
),
|
||
Text(
|
||
'0.00',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: _grayText,
|
||
fontFamily: 'monospace',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
// 提交按钮
|
||
SizedBox(
|
||
width: double.infinity,
|
||
height: 48,
|
||
child: ElevatedButton(
|
||
onPressed: () => _handleTrade(accountSequence),
|
||
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 _buildMyOrdersCard() {
|
||
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),
|
||
// 挂单列表项
|
||
_buildOrderItem(
|
||
type: '卖出',
|
||
price: '0.000156',
|
||
quantity: '1,000 股',
|
||
time: '12/05 14:30',
|
||
status: '待成交',
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildOrderItem({
|
||
required String type,
|
||
required String price,
|
||
required String quantity,
|
||
required String time,
|
||
required String status,
|
||
}) {
|
||
final isSell = type == '卖出';
|
||
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).withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Text(
|
||
type,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.bold,
|
||
color: isSell ? _red : _green,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
price,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: _darkText,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
time,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
color: _grayText,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
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.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(9999),
|
||
),
|
||
child: Text(
|
||
status,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
color: _orange,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildLoadingCard() {
|
||
return ShimmerLoading(
|
||
child: 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: const [
|
||
ShimmerBox(width: 100, height: 16),
|
||
SizedBox(height: 12),
|
||
ShimmerBox(width: 150, height: 28),
|
||
SizedBox(height: 8),
|
||
ShimmerBox(width: 80, height: 14),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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(String accountSequence) async {
|
||
if (_amountController.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(accountSequence, _amountController.text);
|
||
} else {
|
||
success = await ref
|
||
.read(tradingNotifierProvider.notifier)
|
||
.sellShares(accountSequence, _amountController.text);
|
||
}
|
||
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(success
|
||
? (isBuy ? '买入订单已提交' : '卖出订单已提交')
|
||
: (isBuy ? '买入失败' : '卖出失败')),
|
||
backgroundColor: success ? _green : AppColors.error,
|
||
),
|
||
);
|
||
if (success) {
|
||
_amountController.clear();
|
||
// 交易成功后刷新所有相关数据
|
||
ref.invalidate(shareAccountProvider(accountSequence));
|
||
ref.invalidate(globalStateProvider);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
final 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;
|
||
}
|