feat: 优化
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import '../../core/constants/api_endpoints.dart';
|
||||
import '../../core/network/api_response.dart';
|
||||
import '../../core/network/dio_client.dart';
|
||||
import '../models/account_models.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../../core/constants/api_endpoints.dart';
|
||||
import '../../core/network/api_response.dart';
|
||||
import '../../core/network/dio_client.dart';
|
||||
import '../models/order_models.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../../core/constants/api_endpoints.dart';
|
||||
import '../../core/network/api_response.dart';
|
||||
import '../../core/network/dio_client.dart';
|
||||
import '../models/coin.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../../core/constants/api_endpoints.dart';
|
||||
import '../../core/network/api_response.dart';
|
||||
import '../../core/network/dio_client.dart';
|
||||
import '../models/order_models.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../../core/constants/api_endpoints.dart';
|
||||
import '../../core/network/api_response.dart';
|
||||
import '../../core/network/dio_client.dart';
|
||||
import '../models/user.dart';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:provider/single_child_widget.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'core/network/dio_client.dart';
|
||||
@@ -135,6 +136,13 @@ class _AuthNavigatorState extends State<AuthNavigator> {
|
||||
void _navigateToAuthPage(bool isLoggedIn) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
|
||||
// 退出登录时重置其他 Provider 的状态
|
||||
if (!isLoggedIn) {
|
||||
context.read<AssetProvider>().resetLoadState();
|
||||
context.read<MarketProvider>().resetLoadState();
|
||||
}
|
||||
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => isLoggedIn ? const MainPage() : const LoginPage(),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/network/api_response.dart';
|
||||
import '../core/network/dio_client.dart';
|
||||
import '../data/models/account_models.dart';
|
||||
import '../data/models/order_models.dart';
|
||||
import '../data/services/asset_service.dart';
|
||||
import '../data/services/fund_service.dart';
|
||||
import '../core/network/dio_client.dart';
|
||||
|
||||
/// 资产状态管理
|
||||
class AssetProvider extends ChangeNotifier {
|
||||
@@ -20,6 +21,11 @@ class AssetProvider extends ChangeNotifier {
|
||||
bool _isLoadingOrders = false;
|
||||
String? _error;
|
||||
|
||||
// 加载状态标记,防止重复加载
|
||||
bool _overviewLoaded = false;
|
||||
bool _fundAccountLoaded = false;
|
||||
bool _tradeAccountLoaded = false;
|
||||
|
||||
AssetProvider(this._assetService, this._fundService);
|
||||
|
||||
// Getters
|
||||
@@ -35,7 +41,12 @@ class AssetProvider extends ChangeNotifier {
|
||||
String? get error => _error;
|
||||
|
||||
/// 加载资产总览
|
||||
Future<void> loadOverview() async {
|
||||
Future<void> loadOverview({bool force = false}) async {
|
||||
// 如果已经加载过且不是强制刷新,则跳过
|
||||
if (_overviewLoaded && !force && _overview != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
@@ -44,6 +55,7 @@ class AssetProvider extends ChangeNotifier {
|
||||
final response = await _assetService.getOverview();
|
||||
if (response.success) {
|
||||
_overview = response.data;
|
||||
_overviewLoaded = true;
|
||||
} else {
|
||||
_error = response.message;
|
||||
}
|
||||
@@ -56,11 +68,17 @@ class AssetProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// 加载资金账户
|
||||
Future<void> loadFundAccount() async {
|
||||
Future<void> loadFundAccount({bool force = false}) async {
|
||||
// 如果已经加载过且不是强制刷新,则跳过
|
||||
if (_fundAccountLoaded && !force && _fundAccount != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _assetService.getFundAccount();
|
||||
if (response.success) {
|
||||
_fundAccount = response.data;
|
||||
_fundAccountLoaded = true;
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (_) {
|
||||
@@ -69,11 +87,17 @@ class AssetProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// 加载交易账户
|
||||
Future<void> loadTradeAccount() async {
|
||||
Future<void> loadTradeAccount({bool force = false}) async {
|
||||
// 如果已经加载过且不是强制刷新,则跳过
|
||||
if (_tradeAccountLoaded && !force && _tradeAccounts.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _assetService.getTradeAccount();
|
||||
if (response.success) {
|
||||
_tradeAccounts = response.data ?? [];
|
||||
_tradeAccountLoaded = true;
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (_) {
|
||||
@@ -114,10 +138,10 @@ class AssetProvider extends ChangeNotifier {
|
||||
amount: amount,
|
||||
);
|
||||
if (response.success) {
|
||||
// 刷新数据
|
||||
await loadOverview();
|
||||
await loadFundAccount();
|
||||
await loadTradeAccount();
|
||||
// 强制刷新数据
|
||||
await loadOverview(force: true);
|
||||
await loadFundAccount(force: true);
|
||||
await loadTradeAccount(force: true);
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
@@ -130,8 +154,8 @@ class AssetProvider extends ChangeNotifier {
|
||||
try {
|
||||
final response = await _fundService.deposit(amount: amount, remark: remark);
|
||||
if (response.success) {
|
||||
await loadOverview();
|
||||
await loadFundAccount();
|
||||
await loadOverview(force: true);
|
||||
await loadFundAccount(force: true);
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
@@ -167,8 +191,8 @@ class AssetProvider extends ChangeNotifier {
|
||||
remark: remark,
|
||||
);
|
||||
if (response.success) {
|
||||
await loadOverview();
|
||||
await loadFundAccount();
|
||||
await loadOverview(force: true);
|
||||
await loadFundAccount(force: true);
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
@@ -214,11 +238,25 @@ class AssetProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// 刷新所有资产数据
|
||||
Future<void> refreshAll() async {
|
||||
Future<void> refreshAll({bool force = false}) async {
|
||||
await Future.wait([
|
||||
loadOverview(),
|
||||
loadFundAccount(),
|
||||
loadTradeAccount(),
|
||||
loadOverview(force: force),
|
||||
loadFundAccount(force: force),
|
||||
loadTradeAccount(force: force),
|
||||
]);
|
||||
}
|
||||
|
||||
/// 重置加载状态(用于退出登录时)
|
||||
void resetLoadState() {
|
||||
_overviewLoaded = false;
|
||||
_fundAccountLoaded = false;
|
||||
_tradeAccountLoaded = false;
|
||||
_overview = null;
|
||||
_fundAccount = null;
|
||||
_tradeAccounts = [];
|
||||
_flows = [];
|
||||
_fundOrders = [];
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/network/api_response.dart';
|
||||
import '../core/network/dio_client.dart';
|
||||
import '../core/storage/local_storage.dart';
|
||||
import '../data/models/user.dart';
|
||||
|
||||
@@ -12,6 +12,7 @@ class MarketProvider extends ChangeNotifier {
|
||||
String _searchKeyword = '';
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _coinsLoaded = false; // 标记是否已加载
|
||||
|
||||
MarketProvider(this._marketService);
|
||||
|
||||
@@ -24,7 +25,12 @@ class MarketProvider extends ChangeNotifier {
|
||||
String get searchKeyword => _searchKeyword;
|
||||
|
||||
/// 加载币种列表
|
||||
Future<void> loadCoins() async {
|
||||
Future<void> loadCoins({bool force = false}) async {
|
||||
// 如果已经加载过且不是强制刷新,则跳过
|
||||
if (_coinsLoaded && !force && _allCoins.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
@@ -35,6 +41,7 @@ class MarketProvider extends ChangeNotifier {
|
||||
if (response.success) {
|
||||
_allCoins = response.data ?? [];
|
||||
_filterCoins();
|
||||
_coinsLoaded = true;
|
||||
} else {
|
||||
_error = response.message;
|
||||
}
|
||||
@@ -100,6 +107,15 @@ class MarketProvider extends ChangeNotifier {
|
||||
|
||||
/// 刷新
|
||||
Future<void> refresh() async {
|
||||
await loadCoins();
|
||||
await loadCoins(force: true);
|
||||
}
|
||||
|
||||
/// 重置加载状态(用于退出登录时)
|
||||
void resetLoadState() {
|
||||
_coinsLoaded = false;
|
||||
_allCoins = [];
|
||||
_filteredCoins = [];
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../providers/asset_provider.dart';
|
||||
import '../../../providers/auth_provider.dart';
|
||||
import '../../shared/ui_constants.dart';
|
||||
|
||||
/// 首页 - 使用 shadcn_ui 现代化设计
|
||||
class HomePage extends StatefulWidget {
|
||||
@@ -19,15 +20,13 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadData();
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
|
||||
}
|
||||
|
||||
void _loadData() {
|
||||
final assetProvider = context.read<AssetProvider>();
|
||||
assetProvider.loadOverview();
|
||||
assetProvider.loadTradeAccount();
|
||||
final provider = context.read<AssetProvider>();
|
||||
provider.loadOverview();
|
||||
provider.loadTradeAccount();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -40,7 +39,7 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
body: Consumer<AssetProvider>(
|
||||
builder: (context, provider, _) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => provider.refreshAll(),
|
||||
onRefresh: () => provider.refreshAll(force: true),
|
||||
color: theme.colorScheme.primary,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
@@ -49,13 +48,18 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
_buildHeader(),
|
||||
_Header(),
|
||||
const SizedBox(height: 20),
|
||||
_buildAssetCard(provider),
|
||||
_AssetOverviewCard(overview: provider.overview),
|
||||
const SizedBox(height: 16),
|
||||
_buildQuickActions(),
|
||||
_QuickActions(
|
||||
onDeposit: _showDeposit,
|
||||
onWithdraw: _showWithdraw,
|
||||
onTransfer: _showTransfer,
|
||||
onTrade: _navigateToTrade,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildHoldings(provider),
|
||||
_HoldingsList(holdings: provider.holdings),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -65,7 +69,115 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
void _showDeposit() => _showAmountDialog('充值', (amount) {
|
||||
context.read<AssetProvider>().deposit(amount: amount);
|
||||
});
|
||||
|
||||
void _showWithdraw() {
|
||||
final amountController = TextEditingController();
|
||||
final addressController = TextEditingController();
|
||||
final contactController = TextEditingController();
|
||||
final formKey = GlobalKey<ShadFormState>();
|
||||
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (ctx) => ShadDialog(
|
||||
title: const Text('提现'),
|
||||
child: ShadForm(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadInputFormField(
|
||||
id: 'amount',
|
||||
placeholder: const Text('请输入提现金额(USDT)'),
|
||||
controller: amountController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: Validators.amount,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ShadInputFormField(
|
||||
id: 'address',
|
||||
placeholder: const Text('请输入提现地址'),
|
||||
controller: addressController,
|
||||
validator: (v) => Validators.required(v, '提现地址'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ShadInputFormField(
|
||||
id: 'contact',
|
||||
placeholder: const Text('联系方式(可选)'),
|
||||
controller: contactController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()),
|
||||
ShadButton(
|
||||
child: const Text('确认'),
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.saveAndValidate()) {
|
||||
Navigator.of(ctx).pop();
|
||||
context.read<AssetProvider>().withdraw(
|
||||
amount: amountController.text.trim(),
|
||||
withdrawAddress: addressController.text.trim(),
|
||||
withdrawContact: contactController.text.trim().isEmpty ? null : contactController.text.trim(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTransfer() => _showAmountDialog('划转', (amount) {
|
||||
context.read<AssetProvider>().transfer(direction: 1, amount: amount);
|
||||
});
|
||||
|
||||
void _showAmountDialog(String title, Function(String) onSubmit) {
|
||||
final controller = TextEditingController();
|
||||
final formKey = GlobalKey<ShadFormState>();
|
||||
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (ctx) => ShadDialog(
|
||||
title: Text(title),
|
||||
child: ShadForm(
|
||||
key: formKey,
|
||||
child: ShadInputFormField(
|
||||
id: 'amount',
|
||||
placeholder: Text('请输入${title}金额(USDT)'),
|
||||
controller: controller,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: Validators.amount,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()),
|
||||
ShadButton(
|
||||
child: const Text('确认'),
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.saveAndValidate()) {
|
||||
onSubmit(controller.text);
|
||||
Navigator.of(ctx).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToTrade() {
|
||||
// 切换到交易页 - 通过 MainController
|
||||
}
|
||||
}
|
||||
|
||||
/// 头部组件
|
||||
class _Header extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Consumer<AuthProvider>(
|
||||
@@ -73,17 +185,7 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
final user = auth.user;
|
||||
return Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2),
|
||||
child: Text(
|
||||
user?.avatarText ?? 'U',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
_Avatar(text: user?.avatarText),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -91,15 +193,10 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
children: [
|
||||
Text(
|
||||
'你好,${user?.username ?? '用户'}',
|
||||
style: theme.textTheme.large.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'欢迎来到模拟所',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
Text('欢迎来到模拟所', style: theme.textTheme.muted),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -108,23 +205,45 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAssetCard(AssetProvider provider) {
|
||||
/// 头像组件
|
||||
class _Avatar extends StatelessWidget {
|
||||
final String? text;
|
||||
|
||||
const _Avatar({this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final overview = provider.overview;
|
||||
|
||||
// 自定义渐变色
|
||||
const gradientColors = [
|
||||
Color(0xFF00D4AA),
|
||||
Color(0xFF00B894),
|
||||
];
|
||||
return CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2),
|
||||
child: Text(
|
||||
text ?? 'U',
|
||||
style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 资产总览卡片
|
||||
class _AssetOverviewCard extends StatelessWidget {
|
||||
final dynamic overview;
|
||||
|
||||
const _AssetOverviewCard({required this.overview});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: gradientColors,
|
||||
colors: AppColors.gradientColors,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
@@ -133,95 +252,95 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'总资产(USDT)',
|
||||
style: theme.textTheme.small.copyWith(color: Colors.white70),
|
||||
),
|
||||
Text('总资产(USDT)', style: theme.textTheme.small.copyWith(color: Colors.white70)),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
overview?.totalAsset ?? '0.00',
|
||||
style: theme.textTheme.h2.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: theme.textTheme.h2.copyWith(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildAssetItem('资金账户', overview?.fundBalance ?? '0.00'),
|
||||
_buildAssetItem('交易账户', overview?.tradeBalance ?? '0.00'),
|
||||
_AssetItem(label: '资金账户', value: overview?.fundBalance ?? '0.00'),
|
||||
_AssetItem(label: '交易账户', value: overview?.tradeBalance ?? '0.00'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms).slideY(begin: 0.1, end: 0);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAssetItem(String label, String value) {
|
||||
/// 资产项
|
||||
class _AssetItem extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _AssetItem({required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.white70),
|
||||
),
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: Colors.white70)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildQuickActions() {
|
||||
final theme = ShadTheme.of(context);
|
||||
/// 快捷操作
|
||||
class _QuickActions extends StatelessWidget {
|
||||
final VoidCallback onDeposit;
|
||||
final VoidCallback onWithdraw;
|
||||
final VoidCallback onTransfer;
|
||||
final VoidCallback onTrade;
|
||||
|
||||
const _QuickActions({
|
||||
required this.onDeposit,
|
||||
required this.onWithdraw,
|
||||
required this.onTransfer,
|
||||
required this.onTrade,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShadCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildActionItem(
|
||||
icon: LucideIcons.arrowDownToLine,
|
||||
text: '充值',
|
||||
color: const Color(0xFF00C853),
|
||||
onTap: () => _showDeposit(),
|
||||
),
|
||||
_buildActionItem(
|
||||
icon: LucideIcons.arrowUpFromLine,
|
||||
text: '提现',
|
||||
color: const Color(0xFFFF9800),
|
||||
onTap: () => _showWithdraw(),
|
||||
),
|
||||
_buildActionItem(
|
||||
icon: LucideIcons.arrowRightLeft,
|
||||
text: '划转',
|
||||
color: theme.colorScheme.primary,
|
||||
onTap: () => _showTransfer(),
|
||||
),
|
||||
_buildActionItem(
|
||||
icon: LucideIcons.trendingUp,
|
||||
text: '交易',
|
||||
color: const Color(0xFF2196F3),
|
||||
onTap: () => _navigateToTrade(),
|
||||
),
|
||||
_ActionButton(icon: LucideIcons.arrowDownToLine, text: '充值', color: AppColors.deposit, onTap: onDeposit),
|
||||
_ActionButton(icon: LucideIcons.arrowUpFromLine, text: '提现', color: AppColors.withdraw, onTap: onWithdraw),
|
||||
_ActionButton(icon: LucideIcons.arrowRightLeft, text: '划转', color: AppColors.trade, onTap: onTransfer),
|
||||
_ActionButton(icon: LucideIcons.trendingUp, text: '交易', color: AppColors.trade, onTap: onTrade),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(duration: 500.ms, delay: 100.ms);
|
||||
}
|
||||
}
|
||||
|
||||
/// 操作按钮
|
||||
class _ActionButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionButton({
|
||||
required this.icon,
|
||||
required this.text,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
Widget _buildActionItem({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
@@ -229,28 +348,26 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
decoration: BoxDecoration(color: color.withOpacity(0.15), shape: BoxShape.circle),
|
||||
child: Icon(icon, color: color, size: 22),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: ShadTheme.of(context).colorScheme.foreground,
|
||||
),
|
||||
),
|
||||
Text(text, style: TextStyle(fontSize: 12, color: theme.colorScheme.foreground)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHoldings(AssetProvider provider) {
|
||||
/// 持仓列表
|
||||
class _HoldingsList extends StatelessWidget {
|
||||
final List<dynamic> holdings;
|
||||
|
||||
const _HoldingsList({required this.holdings});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final holdings = provider.holdings;
|
||||
|
||||
return ShadCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -260,70 +377,78 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'我的持仓',
|
||||
style: theme.textTheme.large.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
size: 20,
|
||||
),
|
||||
Text('我的持仓', style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold)),
|
||||
Icon(LucideIcons.chevronRight, color: theme.colorScheme.mutedForeground, size: 20),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (holdings.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.wallet,
|
||||
size: 48,
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'暂无持仓',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'快去交易吧~',
|
||||
style: theme.textTheme.muted.copyWith(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
_EmptyHoldings()
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: holdings.length > 5 ? 5 : holdings.length,
|
||||
separatorBuilder: (_, __) => Divider(
|
||||
color: theme.colorScheme.border,
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final holding = holdings[index];
|
||||
return _buildHoldingItem(holding)
|
||||
.animate()
|
||||
.fadeIn(delay: Duration(milliseconds: 50 * index));
|
||||
},
|
||||
),
|
||||
_HoldingsListView(holdings: holdings),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(duration: 500.ms, delay: 200.ms);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHoldingItem(holding) {
|
||||
/// 空持仓提示
|
||||
class _EmptyHoldings extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(LucideIcons.wallet, size: 48, color: theme.colorScheme.mutedForeground),
|
||||
const SizedBox(height: 12),
|
||||
Text('暂无持仓', style: theme.textTheme.muted),
|
||||
const SizedBox(height: 4),
|
||||
Text('快去交易吧~', style: theme.textTheme.muted.copyWith(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 持仓列表视图
|
||||
class _HoldingsListView extends StatelessWidget {
|
||||
final List<dynamic> holdings;
|
||||
|
||||
const _HoldingsListView({required this.holdings});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final displayHoldings = holdings.length > 5 ? holdings.sublist(0, 5) : holdings;
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: displayHoldings.length,
|
||||
separatorBuilder: (_, __) => Divider(color: theme.colorScheme.border, height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
return _HoldingItem(holding: displayHoldings[index])
|
||||
.animate()
|
||||
.fadeIn(delay: Duration(milliseconds: 50 * index));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 持仓项
|
||||
class _HoldingItem extends StatelessWidget {
|
||||
final dynamic holding;
|
||||
|
||||
const _HoldingItem({required this.holding});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final upColor = const Color(0xFF00C853);
|
||||
final downColor = const Color(0xFFFF5252);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
@@ -337,26 +462,15 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||
child: Text(
|
||||
holding.coinCode.substring(0, 1),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
holding.coinCode,
|
||||
style: theme.textTheme.large.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
holding.quantity,
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
Text(holding.coinCode, style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(holding.quantity, style: theme.textTheme.muted),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -364,16 +478,11 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${holding.currentValue} USDT',
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text('${holding.currentValue} USDT', style: theme.textTheme.small.copyWith(fontWeight: FontWeight.w500)),
|
||||
Text(
|
||||
holding.formattedProfitRate,
|
||||
style: TextStyle(
|
||||
color: holding.isProfit ? upColor : downColor,
|
||||
color: holding.isProfit ? AppColors.up : AppColors.down,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
@@ -383,144 +492,4 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeposit() {
|
||||
_showActionDialog('充值', '请输入充值金额(USDT)', (amount) {
|
||||
context.read<AssetProvider>().deposit(amount: amount);
|
||||
});
|
||||
}
|
||||
|
||||
void _showWithdraw() {
|
||||
final amountController = TextEditingController();
|
||||
final addressController = TextEditingController();
|
||||
final contactController = TextEditingController();
|
||||
final formKey = GlobalKey<ShadFormState>();
|
||||
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (context) => ShadDialog(
|
||||
title: const Text('提现'),
|
||||
child: ShadForm(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadInputFormField(
|
||||
id: 'amount',
|
||||
placeholder: const Text('请输入提现金额(USDT)'),
|
||||
controller: amountController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入金额';
|
||||
}
|
||||
final amount = double.tryParse(value);
|
||||
if (amount == null || amount <= 0) {
|
||||
return '请输入有效金额';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ShadInputFormField(
|
||||
id: 'address',
|
||||
placeholder: const Text('请输入提现地址'),
|
||||
controller: addressController,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入提现地址';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ShadInputFormField(
|
||||
id: 'contact',
|
||||
placeholder: const Text('联系方式(可选)'),
|
||||
controller: contactController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
child: const Text('取消'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
ShadButton(
|
||||
child: const Text('确认'),
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.saveAndValidate()) {
|
||||
final amount = amountController.text.trim();
|
||||
final address = addressController.text.trim();
|
||||
final contact = contactController.text.trim();
|
||||
Navigator.of(context).pop();
|
||||
context.read<AssetProvider>().withdraw(
|
||||
amount: amount,
|
||||
withdrawAddress: address,
|
||||
withdrawContact: contact.isEmpty ? null : contact,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTransfer() {
|
||||
_showActionDialog('划转', '请输入划转金额(USDT)', (amount) {
|
||||
context.read<AssetProvider>().transfer(direction: 1, amount: amount);
|
||||
});
|
||||
}
|
||||
|
||||
void _showActionDialog(String title, String hint, Function(String) onSubmit) {
|
||||
final controller = TextEditingController();
|
||||
final formKey = GlobalKey<ShadFormState>();
|
||||
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (context) => ShadDialog(
|
||||
title: Text(title),
|
||||
child: ShadForm(
|
||||
key: formKey,
|
||||
child: ShadInputFormField(
|
||||
id: 'amount',
|
||||
placeholder: Text(hint),
|
||||
controller: controller,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入金额';
|
||||
}
|
||||
final amount = double.tryParse(value);
|
||||
if (amount == null || amount <= 0) {
|
||||
return '请输入有效金额';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
child: const Text('取消'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
ShadButton(
|
||||
child: const Text('确认'),
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.saveAndValidate()) {
|
||||
onSubmit(controller.text);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToTrade() {
|
||||
// 切换到交易页 - 通过 MainController
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,15 @@ import '../trade/trade_page.dart';
|
||||
import '../asset/asset_page.dart';
|
||||
import '../mine/mine_page.dart';
|
||||
|
||||
/// 底部导航项
|
||||
class _NavItem {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Widget page;
|
||||
|
||||
const _NavItem({required this.label, required this.icon, required this.page});
|
||||
}
|
||||
|
||||
/// 主页面(使用 shadcn_ui 风格)
|
||||
class MainPage extends StatefulWidget {
|
||||
const MainPage({super.key});
|
||||
@@ -16,81 +25,75 @@ class MainPage extends StatefulWidget {
|
||||
|
||||
class _MainPageState extends State<MainPage> {
|
||||
int _currentIndex = 0;
|
||||
final Set<int> _loadedPages = {0};
|
||||
|
||||
final List<Widget> _pages = [
|
||||
const HomePage(),
|
||||
const MarketPage(),
|
||||
const TradePage(),
|
||||
const AssetPage(),
|
||||
const MinePage(),
|
||||
static const _navItems = [
|
||||
_NavItem(label: '首页', icon: LucideIcons.house, page: HomePage()),
|
||||
_NavItem(label: '行情', icon: LucideIcons.trendingUp, page: MarketPage()),
|
||||
_NavItem(label: '交易', icon: LucideIcons.arrowLeftRight, page: TradePage()),
|
||||
_NavItem(label: '资产', icon: LucideIcons.wallet, page: AssetPage()),
|
||||
_NavItem(label: '我的', icon: LucideIcons.user, page: MinePage()),
|
||||
];
|
||||
|
||||
final List<_TabItem> _tabs = [
|
||||
_TabItem('首页', LucideIcons.house, LucideIcons.house),
|
||||
_TabItem('行情', LucideIcons.trendingUp, LucideIcons.trendingUp),
|
||||
_TabItem('交易', LucideIcons.arrowLeftRight, LucideIcons.arrowLeftRight),
|
||||
_TabItem('资产', LucideIcons.wallet, LucideIcons.wallet),
|
||||
_TabItem('我的', LucideIcons.user, LucideIcons.user),
|
||||
];
|
||||
void _onTabChanged(int index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
_loadedPages.add(index);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
body: IndexedStack(
|
||||
body: LazyIndexedStack(
|
||||
index: _currentIndex,
|
||||
children: _pages,
|
||||
loadedIndexes: _loadedPages,
|
||||
children: _navItems.map((item) => item.page).toList(),
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.background,
|
||||
border: Border(
|
||||
top: BorderSide(color: theme.colorScheme.border),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: _tabs.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final tab = entry.value;
|
||||
final isSelected = index == _currentIndex;
|
||||
bottomNavigationBar: _BottomNavBar(
|
||||
items: _navItems,
|
||||
currentIndex: _currentIndex,
|
||||
onTap: _onTabChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _currentIndex = index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
tab.icon,
|
||||
color: isSelected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.mutedForeground,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
tab.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isSelected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.mutedForeground,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
/// 底部导航栏
|
||||
class _BottomNavBar extends StatelessWidget {
|
||||
final List<_NavItem> items;
|
||||
final int currentIndex;
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
const _BottomNavBar({
|
||||
required this.items,
|
||||
required this.currentIndex,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.background,
|
||||
border: Border(top: BorderSide(color: theme.colorScheme.border)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: items.asMap().entries.map((entry) {
|
||||
return _NavItemWidget(
|
||||
item: entry.value,
|
||||
isSelected: entry.key == currentIndex,
|
||||
onTap: () => onTap(entry.key),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -98,40 +101,79 @@ class _MainPageState extends State<MainPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _TabItem {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final IconData selectedIcon;
|
||||
/// 导航项组件
|
||||
class _NavItemWidget extends StatelessWidget {
|
||||
final _NavItem item;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
_TabItem(this.label, this.icon, this.selectedIcon);
|
||||
const _NavItemWidget({
|
||||
required this.item,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final color = isSelected ? theme.colorScheme.primary : theme.colorScheme.mutedForeground;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(item.icon, color: color, size: 24),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// IndexedStack 用于保持页面状态
|
||||
class IndexedStack extends StatefulWidget {
|
||||
/// 懒加载 IndexedStack - 只渲染已访问过的页面
|
||||
class LazyIndexedStack extends StatefulWidget {
|
||||
final int index;
|
||||
final Set<int> loadedIndexes;
|
||||
final List<Widget> children;
|
||||
|
||||
const IndexedStack({
|
||||
const LazyIndexedStack({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.loadedIndexes,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
@override
|
||||
State<IndexedStack> createState() => _IndexedStackState();
|
||||
State<LazyIndexedStack> createState() => _LazyIndexedStackState();
|
||||
}
|
||||
|
||||
class _IndexedStackState extends State<IndexedStack> {
|
||||
class _LazyIndexedStackState extends State<LazyIndexedStack> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: widget.children.asMap().entries.map((entry) {
|
||||
final isVisible = entry.key == widget.index;
|
||||
final isLoaded = widget.loadedIndexes.contains(entry.key);
|
||||
|
||||
return Positioned.fill(
|
||||
child: Offstage(
|
||||
offstage: entry.key != widget.index,
|
||||
offstage: !isVisible,
|
||||
child: TickerMode(
|
||||
enabled: entry.key == widget.index,
|
||||
child: entry.value,
|
||||
enabled: isVisible,
|
||||
child: isLoaded ? entry.value : const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import '../../../data/models/coin.dart';
|
||||
import '../../../providers/market_provider.dart';
|
||||
import '../../../providers/asset_provider.dart';
|
||||
import '../../shared/ui_constants.dart';
|
||||
|
||||
/// 交易页面 - 使用 shadcn_ui 现代化设计
|
||||
class TradePage extends StatefulWidget {
|
||||
@@ -23,21 +24,14 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
|
||||
final _priceController = TextEditingController();
|
||||
final _quantityController = TextEditingController();
|
||||
|
||||
// 颜色常量
|
||||
static const upColor = Color(0xFF00C853);
|
||||
static const downColor = Color(0xFFFF5252);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadData();
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
|
||||
}
|
||||
|
||||
void _loadData() {
|
||||
context.read<MarketProvider>().loadCoins();
|
||||
context.read<AssetProvider>().loadOverview();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -62,13 +56,35 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildCoinSelector(market),
|
||||
_CoinSelector(
|
||||
selectedCoin: _selectedCoin,
|
||||
coins: market.allCoins,
|
||||
onCoinLoaded: (coin) {
|
||||
_selectedCoin = coin;
|
||||
_priceController.text = coin.formattedPrice;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPriceCard(),
|
||||
if (_selectedCoin != null) _PriceCard(coin: _selectedCoin!),
|
||||
const SizedBox(height: 16),
|
||||
_buildTradeForm(asset),
|
||||
_TradeForm(
|
||||
tradeType: _tradeType,
|
||||
selectedCoin: _selectedCoin,
|
||||
priceController: _priceController,
|
||||
quantityController: _quantityController,
|
||||
tradeBalance: asset.overview?.tradeBalance,
|
||||
onTradeTypeChanged: (type) => setState(() => _tradeType = type),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTradeButton(),
|
||||
_TradeButton(
|
||||
isBuy: _tradeType == 0,
|
||||
coinCode: _selectedCoin?.code,
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
_executeTrade();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -78,311 +94,22 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoinSelector(MarketProvider market) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final coins = market.allCoins;
|
||||
|
||||
if (_selectedCoin == null && coins.isNotEmpty) {
|
||||
_selectedCoin = coins.first;
|
||||
_priceController.text = _selectedCoin!.formattedPrice;
|
||||
}
|
||||
|
||||
return ShadCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||
child: Text(
|
||||
_selectedCoin?.displayIcon ?? '?',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_selectedCoin != null ? '${_selectedCoin!.code}/USDT' : '选择币种',
|
||||
style: theme.textTheme.large.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_selectedCoin != null ? _selectedCoin!.name : '点击选择交易对',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceCard() {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
if (_selectedCoin == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final coin = _selectedCoin!;
|
||||
|
||||
return ShadCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'最新价',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'\$${coin.formattedPrice}',
|
||||
style: theme.textTheme.h2.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: coin.isUp ? upColor.withValues(alpha: 0.2) : downColor.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
coin.formattedChange,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: coin.isUp ? upColor : downColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTradeForm(AssetProvider asset) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return ShadCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// 买入/卖出切换
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _tradeType = 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: _tradeType == 0 ? upColor : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: _tradeType != 0 ? Border.all(color: upColor) : null,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'买入',
|
||||
style: TextStyle(
|
||||
color: _tradeType == 0 ? Colors.white : upColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _tradeType = 1),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: _tradeType == 1 ? downColor : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: _tradeType != 1 ? Border.all(color: downColor) : null,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'卖出',
|
||||
style: TextStyle(
|
||||
color: _tradeType == 1 ? Colors.white : downColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 价格输入
|
||||
ShadInputFormField(
|
||||
id: 'price',
|
||||
label: const Text('价格(USDT)'),
|
||||
controller: _priceController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
placeholder: const Text('输入价格'),
|
||||
trailing: const Padding(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
child: Text('USDT'),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入价格';
|
||||
}
|
||||
final price = double.tryParse(value);
|
||||
if (price == null || price <= 0) {
|
||||
return '请输入有效价格';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 数量输入
|
||||
ShadInputFormField(
|
||||
id: 'quantity',
|
||||
label: const Text('数量'),
|
||||
controller: _quantityController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
placeholder: const Text('输入数量'),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Text(_selectedCoin?.code ?? ''),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入数量';
|
||||
}
|
||||
final quantity = double.tryParse(value);
|
||||
if (quantity == null || quantity <= 0) {
|
||||
return '请输入有效数量';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 交易金额
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'交易金额',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
Text(
|
||||
'${_calculateAmount()} USDT',
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 可用余额
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'可用',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
Text(
|
||||
'${asset.overview?.tradeBalance ?? '0.00'} USDT',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _calculateAmount() {
|
||||
final price = double.tryParse(_priceController.text) ?? 0;
|
||||
final quantity = double.tryParse(_quantityController.text) ?? 0;
|
||||
return (price * quantity).toStringAsFixed(2);
|
||||
}
|
||||
|
||||
Widget _buildTradeButton() {
|
||||
final isBuy = _tradeType == 0;
|
||||
final color = isBuy ? upColor : downColor;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ShadButton(
|
||||
backgroundColor: color,
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.saveAndValidate()) {
|
||||
_executeTrade();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isBuy ? LucideIcons.arrowDownToLine : LucideIcons.arrowUpFromLine,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${isBuy ? '买入' : '卖出'} ${_selectedCoin?.code ?? ''}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _executeTrade() {
|
||||
final price = _priceController.text;
|
||||
final quantity = _quantityController.text;
|
||||
final isBuy = _tradeType == 0;
|
||||
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (context) => ShadDialog.alert(
|
||||
title: Text(_tradeType == 0 ? '确认买入' : '确认卖出'),
|
||||
description: Text(
|
||||
'${_tradeType == 0 ? '买入' : '卖出'} $quantity ${_selectedCoin?.code ?? ''} @ $price USDT',
|
||||
),
|
||||
builder: (ctx) => ShadDialog.alert(
|
||||
title: Text(isBuy ? '确认买入' : '确认卖出'),
|
||||
description: Text('${isBuy ? '买入' : '卖出'} $quantity ${_selectedCoin?.code ?? ''} @ $price USDT'),
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
child: const Text('取消'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()),
|
||||
ShadButton(
|
||||
child: const Text('确认'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(ctx).pop();
|
||||
_showTradeResult();
|
||||
},
|
||||
),
|
||||
@@ -393,29 +120,24 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
|
||||
|
||||
void _showTradeResult() {
|
||||
final theme = ShadTheme.of(context);
|
||||
final isBuy = _tradeType == 0;
|
||||
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (context) => ShadDialog.alert(
|
||||
builder: (ctx) => ShadDialog.alert(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.circleCheck,
|
||||
color: theme.colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
Icon(LucideIcons.circleCheck, color: theme.colorScheme.primary, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
const Text('交易成功'),
|
||||
],
|
||||
),
|
||||
description: Text(
|
||||
'已${_tradeType == 0 ? '买入' : '卖出'} ${_quantityController.text} ${_selectedCoin?.code ?? ''}',
|
||||
),
|
||||
description: Text('已${isBuy ? '买入' : '卖出'} ${_quantityController.text} ${_selectedCoin?.code ?? ''}'),
|
||||
actions: [
|
||||
ShadButton(
|
||||
child: const Text('确定'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(ctx).pop();
|
||||
_quantityController.clear();
|
||||
},
|
||||
),
|
||||
@@ -424,3 +146,320 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 币种选择器
|
||||
class _CoinSelector extends StatelessWidget {
|
||||
final Coin? selectedCoin;
|
||||
final List<Coin> coins;
|
||||
final ValueChanged<Coin> onCoinLoaded;
|
||||
|
||||
const _CoinSelector({
|
||||
required this.selectedCoin,
|
||||
required this.coins,
|
||||
required this.onCoinLoaded,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
// 自动选择第一个币种
|
||||
if (selectedCoin == null && coins.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => onCoinLoaded(coins.first));
|
||||
}
|
||||
|
||||
return ShadCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
_CoinAvatar(icon: selectedCoin?.displayIcon),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
selectedCoin != null ? '${selectedCoin!.code}/USDT' : '选择币种',
|
||||
style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(selectedCoin?.name ?? '点击选择交易对', style: theme.textTheme.muted),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(LucideIcons.chevronRight, color: theme.colorScheme.mutedForeground),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 币种头像
|
||||
class _CoinAvatar extends StatelessWidget {
|
||||
final String? icon;
|
||||
|
||||
const _CoinAvatar({this.icon});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||
child: Text(
|
||||
icon ?? '?',
|
||||
style: TextStyle(fontSize: 20, color: theme.colorScheme.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 价格卡片
|
||||
class _PriceCard extends StatelessWidget {
|
||||
final Coin coin;
|
||||
|
||||
const _PriceCard({required this.coin});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final color = coin.isUp ? AppColors.up : AppColors.down;
|
||||
|
||||
return ShadCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('最新价', style: theme.textTheme.muted),
|
||||
const SizedBox(height: 4),
|
||||
Text('\$${coin.formattedPrice}', style: theme.textTheme.h2.copyWith(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
coin.formattedChange,
|
||||
style: TextStyle(fontSize: 16, color: color, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 交易表单
|
||||
class _TradeForm extends StatelessWidget {
|
||||
final int tradeType;
|
||||
final Coin? selectedCoin;
|
||||
final TextEditingController priceController;
|
||||
final TextEditingController quantityController;
|
||||
final String? tradeBalance;
|
||||
final ValueChanged<int> onTradeTypeChanged;
|
||||
|
||||
const _TradeForm({
|
||||
required this.tradeType,
|
||||
required this.selectedCoin,
|
||||
required this.priceController,
|
||||
required this.quantityController,
|
||||
required this.tradeBalance,
|
||||
required this.onTradeTypeChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return ShadCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// 买入/卖出切换
|
||||
_TradeTypeSelector(
|
||||
tradeType: tradeType,
|
||||
onChanged: onTradeTypeChanged,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 价格输入
|
||||
ShadInputFormField(
|
||||
id: 'price',
|
||||
label: const Text('价格(USDT)'),
|
||||
controller: priceController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
placeholder: const Text('输入价格'),
|
||||
trailing: const Padding(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
child: Text('USDT'),
|
||||
),
|
||||
validator: Validators.price,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 数量输入
|
||||
ShadInputFormField(
|
||||
id: 'quantity',
|
||||
label: const Text('数量'),
|
||||
controller: quantityController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
placeholder: const Text('输入数量'),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Text(selectedCoin?.code ?? ''),
|
||||
),
|
||||
validator: Validators.quantity,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 交易金额
|
||||
_InfoRow(label: '交易金额', value: '${_calculateAmount()} USDT'),
|
||||
const SizedBox(height: 8),
|
||||
// 可用余额
|
||||
_InfoRow(label: '可用', value: '${tradeBalance ?? '0.00'} USDT'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _calculateAmount() {
|
||||
final price = double.tryParse(priceController.text) ?? 0;
|
||||
final quantity = double.tryParse(quantityController.text) ?? 0;
|
||||
return (price * quantity).toStringAsFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
/// 交易类型选择器
|
||||
class _TradeTypeSelector extends StatelessWidget {
|
||||
final int tradeType;
|
||||
final ValueChanged<int> onChanged;
|
||||
|
||||
const _TradeTypeSelector({required this.tradeType, required this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _TypeButton(
|
||||
label: '买入',
|
||||
isSelected: tradeType == 0,
|
||||
color: AppColors.up,
|
||||
onTap: () => onChanged(0),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _TypeButton(
|
||||
label: '卖出',
|
||||
isSelected: tradeType == 1,
|
||||
color: AppColors.down,
|
||||
onTap: () => onChanged(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型按钮
|
||||
class _TypeButton extends StatelessWidget {
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _TypeButton({
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? color : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: isSelected ? null : Border.all(color: color),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 信息行
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _InfoRow({required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: theme.textTheme.muted),
|
||||
Text(value, style: theme.textTheme.small.copyWith(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 交易按钮
|
||||
class _TradeButton extends StatelessWidget {
|
||||
final bool isBuy;
|
||||
final String? coinCode;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _TradeButton({
|
||||
required this.isBuy,
|
||||
required this.coinCode,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = isBuy ? AppColors.up : AppColors.down;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ShadButton(
|
||||
backgroundColor: color,
|
||||
onPressed: onPressed,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(isBuy ? LucideIcons.arrowDownToLine : LucideIcons.arrowUpFromLine, size: 18, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${isBuy ? '买入' : '卖出'} ${coinCode ?? ''}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
62
flutter_monisuo/lib/ui/shared/ui_constants.dart
Normal file
62
flutter_monisuo/lib/ui/shared/ui_constants.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 应用颜色常量
|
||||
class AppColors {
|
||||
AppColors._();
|
||||
|
||||
static const Color up = Color(0xFF00C853);
|
||||
static const Color down = Color(0xFFFF5252);
|
||||
static const Color deposit = Color(0xFF00C853);
|
||||
static const Color withdraw = Color(0xFFFF9800);
|
||||
static const Color trade = Color(0xFF2196F3);
|
||||
|
||||
static const List<Color> gradientColors = [
|
||||
Color(0xFF00D4AA),
|
||||
Color(0xFF00B894),
|
||||
];
|
||||
}
|
||||
|
||||
/// 表单验证器
|
||||
class Validators {
|
||||
Validators._();
|
||||
|
||||
static String? amount(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入金额';
|
||||
}
|
||||
final amount = double.tryParse(value);
|
||||
if (amount == null || amount <= 0) {
|
||||
return '请输入有效金额';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? price(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入价格';
|
||||
}
|
||||
final price = double.tryParse(value);
|
||||
if (price == null || price <= 0) {
|
||||
return '请输入有效价格';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? quantity(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入数量';
|
||||
}
|
||||
final quantity = double.tryParse(value);
|
||||
if (quantity == null || quantity <= 0) {
|
||||
return '请输入有效数量';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? required(String? value, String fieldName) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入$fieldName';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user