rwadurian/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart

933 lines
30 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}