import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/number_format.dart'; import '../../../providers/asset_provider.dart'; import '../../../providers/my_copy_trading_provider.dart'; import '../../widgets/common/app_shimmer.dart'; import '../../widgets/common/app_tab_bar.dart'; import '../../widgets/common/network_error_body.dart'; import 'asset_overview_tab.dart'; import 'asset_futures_tab.dart'; import 'asset_copy_trading_tab.dart'; import 'asset_spot_tab.dart'; import 'asset_spot_trading_tab.dart'; class AssetScreen extends ConsumerStatefulWidget { const AssetScreen({super.key}); @override ConsumerState createState() => _AssetScreenState(); } class _AssetScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; late PageController _pageController; List _getLocalizedTabs(BuildContext context) { final l10n = AppLocalizations.of(context)!; return [ l10n.assetOverview, l10n.fund, l10n.spotTab, l10n.futures, l10n.copyTrading, ]; } @override void initState() { super.initState(); _tabController = TabController(length: 5, vsync: this); _pageController = PageController(); // 拖动 PageView → 实时更新指示器 offset(平滑插值) _pageController.addListener(() { if (!_pageController.hasClients) return; final page = _pageController.page!; final offset = page - _tabController.index; if (offset.abs() <= 1.0 && !_tabController.indexIsChanging) { _tabController.offset = offset.clamp(-1.0, 1.0); } }); _tabController.addListener(_onTabChanged); } void _onTabChanged() { if (_tabController.indexIsChanging) { // Tab 点击 → 驱动 PageView 动画 _pageController.animateToPage( _tabController.index, duration: const Duration(milliseconds: 280), curve: Curves.easeOut, ); } else { // Tab 切换完成:更新当前子 tab 索引并按需刷新 final idx = _tabController.index; final prevIdx = ref.read(currentAssetSubTabProvider); ref.read(currentAssetSubTabProvider.notifier).state = idx; if (prevIdx == 2 && idx != 2) { ref.read(assetProvider.notifier).onSpotTradingTabHidden(); } WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _refreshForTab(idx); }); // 仅合约 tab(索引 3)需要定时拉持仓;现货 tab 首次进入 HTTP,后续走 WS final notifier = ref.read(assetProvider.notifier); if (idx == 3) { notifier.startPositionPolling(); } else { notifier.stopPositionPolling(); } } setState(() {}); } void _refreshForTab(int tabIndex) { final n = ref.read(assetProvider.notifier); if (tabIndex == 2) { n.onSpotTradingTabVisible(); } else { n.silentRefresh(); } if (tabIndex == 4) { // 跟单 tab 额外刷新跟单仓位 ref.read(myCopyTradingProvider.notifier).silentRefresh(); } } @override void dispose() { ref.read(assetProvider.notifier).onSpotTradingTabHidden(); ref.read(assetProvider.notifier).stopPositionPolling(); _tabController.dispose(); _pageController.dispose(); super.dispose(); } String _getTabLabel(int index) { const labels = [ 'asset_tab_overview', 'asset_tab_funds', 'asset_tab_spot_trading', 'asset_tab_futures', 'asset_tab_copy_trading', ]; return labels[index]; } @override Widget build(BuildContext context) { final state = ref.watch(assetProvider); final notifier = ref.read(assetProvider.notifier); return Scaffold( body: SafeArea( child: Column( children: [ // ── Tab 栏 ───────────────────────────────────── TabBar( controller: _tabController, isScrollable: true, indicator: StretchTabIndicator( controller: _tabController, color: AppColors.brand, height: 3, borderRadius: 1.5, ), indicatorSize: TabBarIndicatorSize.label, labelStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), unselectedLabelStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400), tabAlignment: TabAlignment.start, dividerColor: Colors.transparent, labelPadding: const EdgeInsets.symmetric(horizontal: 14), padding: const EdgeInsets.fromLTRB(4, 8, 16, 0), tabs: [ for (int i = 0; i < 5; i++) Semantics( label: _getTabLabel(i), button: true, child: Tab(text: _getLocalizedTabs(context)[i]), ), ], ), // ── Tab 内容 ─────────────────────────────────── Expanded( child: _buildContent(state, notifier), ), ], ), ), ); } Widget _buildContent(AssetState state, AssetNotifier notifier) { if (state.isLoading) return const _AssetShimmer(); if (state.errorMessage != null) { return NetworkErrorBody(onRetry: notifier.loadAssets); } return PageView( controller: _pageController, physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), ), onPageChanged: (index) { if (_tabController.indexIsChanging) return; _tabController.index = index; }, children: [ AssetOverviewTab(state: state, notifier: notifier), AssetSpotTab(state: state, notifier: notifier), AssetSpotTradingTab(state: state, notifier: notifier), AssetFuturesTab(state: state, notifier: notifier), AssetCopyTradingTab(state: state, notifier: notifier), ], ); } } // ── 资产骨架屏 ────────────────────────────────────────────── class _AssetShimmer extends StatelessWidget { const _AssetShimmer(); @override Widget build(BuildContext context) { return AppShimmer( child: SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 资产估值头部 shimmerBox(80, 13), const SizedBox(height: 10), shimmerBox(180, 34), const SizedBox(height: 6), shimmerBox(120, 13), const SizedBox(height: 24), // 账户卡片列表 ...List.generate(3, (_) => Padding( padding: const EdgeInsets.only(bottom: 12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ shimmerCircle(22), const SizedBox(width: 12), shimmerBox(80, 14), const Spacer(), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ shimmerBox(90, 15), const SizedBox(height: 4), shimmerBox(60, 12), ], ), ], ), ), )), const SizedBox(height: 8), // 持仓卡片 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [shimmerBox(100, 14), shimmerBox(60, 14)], ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(3, (_) => shimmerBox(70, 40, radius: 6)), ), ], ), ), ], ), ), ); } } // ══════════════════════════════════════════════════════════════ // 共享组件(供各 Tab 文件使用) // ══════════════════════════════════════════════════════════════ /// 各 Tab 通用的资产估值头部 class AssetHeader extends StatelessWidget { const AssetHeader({super.key, required this.state, required this.notifier}); final AssetState state; final AssetNotifier notifier; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final total = state.totalUsdtValue; final display = state.obscureBalance ? '******' : formatPrice(total, decimalPlaces: 2); return Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text(AppLocalizations.of(context)!.assetValuation, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)), const SizedBox(width: 6), Semantics( label: 'asset_btn_toggle_balance', button: true, enabled: true, onTap: notifier.toggleObscure, child: GestureDetector( onTap: notifier.toggleObscure, child: Icon( state.obscureBalance ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 16, color: cs.onSurface.withAlpha(153), ), ), ), ], ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Semantics( label: 'asset_text_total_value', enabled: true, child: Text(display, style: TextStyle(color: cs.onSurface, fontSize: 32, fontWeight: FontWeight.w700, letterSpacing: -0.5)), ), const SizedBox(width: 6), Semantics( label: 'asset_text_currency', enabled: true, child: Padding( padding: const EdgeInsets.only(bottom: 5), child: Text('USDT', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)), ), ), ], ), ], ), ); } } /// 合约/跟单持仓卡片 class AssetPositionCard extends StatelessWidget { const AssetPositionCard({ super.key, required this.obscure, this.symbol = 'BTCUSDT', this.isLong = true, this.positionType = '', this.leverage = '20X', this.pnl = '+24.35', this.pnlColor, this.roi = '+5.36%', this.rows = const [], this.onShare, }); final bool obscure; final String symbol; final bool isLong; final String positionType; final String leverage; final String pnl; final Color? pnlColor; final String roi; final List> rows; final VoidCallback? onShare; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final sideColor = isLong ? AppColors.rise : AppColors.fall; final effectivePnlColor = pnlColor ?? AppColors.rise; return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary, borderRadius: BorderRadius.circular(12), border: isDark ? null : Border.all(color: AppColors.lightBorder, width: 0.5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 头部:币种 + 标签 Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration(color: sideColor, borderRadius: BorderRadius.circular(8)), child: Center( child: Text(isLong ? AppLocalizations.of(context)!.longLabel : AppLocalizations.of(context)!.shortLabel, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w700)), ), ), const SizedBox(width: 10), Text(symbol, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600)), const SizedBox(width: 8), _Tag(label: AppLocalizations.of(context)!.perpetual), const SizedBox(width: 4), _Tag(label: positionType), const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: cs.onSurface.withAlpha(20), borderRadius: BorderRadius.circular(4), ), child: Text(leverage, style: TextStyle(color: cs.onSurface, fontSize: 11, fontWeight: FontWeight.w600)), ), const Spacer(), GestureDetector( onTap: onShare, child: Icon(Icons.share_outlined, size: 18, color: cs.onSurface.withAlpha(153)), ), ], ), const SizedBox(height: 14), // 未实现盈亏 + 收益率 Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: const BoxDecoration(), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${AppLocalizations.of(context)!.unrealizedPnl} (USDT)', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)), const SizedBox(height: 2), Text( obscure ? '******' : pnl, style: TextStyle(color: effectivePnlColor, fontSize: 20, fontWeight: FontWeight.w700), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(AppLocalizations.of(context)!.returnRate, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)), const SizedBox(height: 2), Text(roi, style: TextStyle(color: effectivePnlColor, fontSize: 16, fontWeight: FontWeight.w600)), ], ), ], ), ), const SizedBox(height: 14), // 数据网格 _DataGrid(obscure: obscure, rows: rows), ], ), ); } } /// 账户行 class AssetAccountRow extends StatelessWidget { const AssetAccountRow({ super.key, required this.icon, required this.label, required this.amount, required this.usdAmount, this.onTransfer, }); final IconData icon; final String label; final String amount; final String usdAmount; final VoidCallback? onTransfer; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: BoxDecoration( color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary, borderRadius: BorderRadius.circular(12), border: isDark ? null : Border.all(color: AppColors.lightBorder, width: 0.5), ), child: Row( children: [ Icon(icon, color: cs.onSurface.withAlpha(153), size: 22), const SizedBox(width: 12), Expanded( child: Text(label, style: TextStyle( color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w500)), ), if (onTransfer != null) ...[ OutlinedButton( onPressed: onTransfer, style: OutlinedButton.styleFrom( side: const BorderSide(color: AppColors.brand), foregroundColor: AppColors.brand, minimumSize: const Size(0, 32), padding: const EdgeInsets.symmetric(horizontal: 12), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( AppLocalizations.of(context)!.transfer, style: const TextStyle(fontSize: 12), ), ), const SizedBox(width: 12), ], Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(amount, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600)), if (usdAmount.isNotEmpty) Text(usdAmount, style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12)), ], ), ], ), ); } } // ── 内部私有组件 ────────────────────────────────────────── class _Tag extends StatelessWidget { const _Tag({required this.label}); final String label; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( border: Border.all(color: cs.onSurface.withAlpha(60)), borderRadius: BorderRadius.circular(4), ), child: Text(label, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 10)), ); } } class _DataGrid extends StatelessWidget { const _DataGrid({required this.obscure, required this.rows}); final bool obscure; final List> rows; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Column( children: rows.map((row) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ for (int i = 0; i < row.length; i += 2) ...[ Expanded( child: Column( crossAxisAlignment: i == 0 ? CrossAxisAlignment.start : i + 2 >= row.length ? CrossAxisAlignment.end : CrossAxisAlignment.center, children: [ Text(row[i], style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 11), textAlign: i == 0 ? TextAlign.left : i + 2 >= row.length ? TextAlign.right : TextAlign.center), const SizedBox(height: 2), Text( obscure ? '***' : row[i + 1], style: TextStyle(color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500), textAlign: i == 0 ? TextAlign.left : i + 2 >= row.length ? TextAlign.right : TextAlign.center, ), ], ), ), ], ], ), ); }).toList(), ); } }