import 'package:flutter/material.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/number_format.dart'; import '../../../data/models/finance/staking_order_item.dart'; import '../../../providers/asset_provider.dart'; import '../../widgets/common/app_refresh_indicator.dart'; /// 锁仓账户 Tab — 对齐 Web AssetsView STAKING Tab class AssetStakingTab extends StatelessWidget { const AssetStakingTab( {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 l10n = AppLocalizations.of(context)!; final obscure = state.obscureBalance; final locked = double.tryParse(state.stakingOverviewLocked) ?? 0; final available = double.tryParse(state.stakingOverviewAvailable) ?? 0; final totalLocked = locked + available; final lockedDisplay = obscure ? '******' : formatPrice(locked, decimalPlaces: 2); final totalDisplay = obscure ? '******' : formatPrice(totalLocked, decimalPlaces: 2); return AppRefreshIndicator( onRefresh: notifier.refreshStakingTab, child: ListView( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 0), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).brightness == Brightness.dark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary, borderRadius: BorderRadius.circular(12), border: Theme.of(context).brightness == Brightness.dark ? null : Border.all(color: AppColors.lightBorder, width: 0.5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( l10n.assetsReleaseTotal, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13, ), ), const SizedBox(width: 6), GestureDetector( onTap: notifier.toggleObscure, child: Icon( obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 16, color: cs.onSurface.withAlpha(153), ), ), ], ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( lockedDisplay, style: TextStyle( color: cs.onSurface, fontSize: 32, fontWeight: FontWeight.w700, letterSpacing: -0.5, ), ), const SizedBox(width: 6), Padding( padding: const EdgeInsets.only(bottom: 5), child: Text( l10n.assetsIbitUnit, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 14, ), ), ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.assetsTotalLockedBalance, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13, ), ), Text( totalDisplay, style: TextStyle( color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500, ), ), ], ), ], ), ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 8), child: Row( children: [ Expanded( child: Text( l10n.assetsMyStakingOrders, style: TextStyle( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600, ), ), ), _StakingStatusFilter( value: state.stakingOrderStatusFilter, loading: state.stakingOrdersLoading, onChanged: notifier.setStakingOrderStatusFilter, ), ], ), ), if (state.stakingOrdersLoading && state.stakingOrders.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 32), child: Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, color: cs.onSurface.withAlpha(120), ), ), ), ) else if (state.stakingOrders.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 32), child: Center( child: Text( l10n.noData, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 14, ), ), ), ) else ...state.stakingOrders.map( (order) => _StakingOrderCard(order: order, obscure: obscure), ), const SizedBox(height: 32), ], ), ); } } class _StakingStatusFilter extends StatelessWidget { const _StakingStatusFilter({ required this.value, required this.loading, required this.onChanged, }); final StakingOrderStatusFilter value; final bool loading; final ValueChanged onChanged; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final cs = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( border: Border.all(color: cs.outline.withAlpha(80)), borderRadius: BorderRadius.circular(8), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: value, isDense: true, icon: Icon(Icons.expand_more, size: 18, color: cs.onSurface.withAlpha(153)), items: [ DropdownMenuItem( value: StakingOrderStatusFilter.all, child: Text(l10n.all, style: const TextStyle(fontSize: 13)), ), DropdownMenuItem( value: StakingOrderStatusFilter.subscribing, child: Text(l10n.assetsSubscribing, style: const TextStyle(fontSize: 13)), ), DropdownMenuItem( value: StakingOrderStatusFilter.releasing, child: Text(l10n.assetsReleasing, style: const TextStyle(fontSize: 13)), ), DropdownMenuItem( value: StakingOrderStatusFilter.completed, child: Text(l10n.completed, style: const TextStyle(fontSize: 13)), ), ], onChanged: loading ? null : (next) { if (next != null) { onChanged(next); } }, ), ), ); } } class _StakingOrderCard extends StatelessWidget { const _StakingOrderCard({required this.order, required this.obscure}); final StakingOrderItem order; final bool obscure; String _fmtAmount(double value, {int decimals = 2}) { if (obscure) { return '******'; } return formatPrice(value, decimalPlaces: decimals); } String _fmtReleasedCompact(double value) { if (obscure) { return '******'; } if (value == value.truncateToDouble()) { return value.toInt().toString(); } return value .toStringAsFixed(8) .replaceAll(RegExp(r'0+$'), '') .replaceAll(RegExp(r'\.$'), ''); } String _statusLabel(AppLocalizations l10n) { if (order.status == 0) { return l10n.assetsSubscribing; } if (order.status == 2) { return l10n.completed; } return l10n.assetsReleasing; } Color _statusTextColor() { if (order.status == 0) { return AppColors.brand; } if (order.status == 2) { return const Color(0xFF8E949F); } return const Color(0xFF12161C); } Color _statusBackgroundColor() { if (order.status == 0) { return const Color(0xFF12161C); } if (order.status == 2) { return const Color(0xFF12161C); } return AppColors.brand; } String _displayDate(String? raw) { if (raw == null || raw.isEmpty) { return '-'; } final normalized = raw.replaceFirst('T', ' '); return normalized.length >= 10 ? normalized.substring(0, 10) : normalized; } String _lockDaysText() { final startRaw = order.stakingTime ?? order.createTime; final endRaw = order.unlockTime; if (startRaw == null || endRaw == null || startRaw.isEmpty || endRaw.isEmpty) { return '--'; } final start = DateTime.tryParse(startRaw); final end = DateTime.tryParse(endRaw); if (start == null || end == null) { return '--'; } final days = end.difference(start).inDays.abs(); return days > 0 ? '$days天' : '--'; } double _releaseProgress(double amount, double released) { if (!amount.isFinite || amount <= 0) { return 0; } final ratio = released / amount; if (!ratio.isFinite) { return 0; } return ratio.clamp(0.0, 1.0); } Color _progressColor() { if (order.status == 2) { return const Color(0xFF62656C); } if (order.status == 1) { return AppColors.brand; } return Colors.transparent; } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final isDark = Theme.of(context).brightness == Brightness.dark; final coin = order.coinUnit.isNotEmpty ? order.coinUnit : l10n.assetsIbitUnit.toUpperCase(); final amount = double.tryParse(order.amount) ?? 0; final released = double.tryParse(order.releasedAmount) ?? 0; final progress = _releaseProgress(amount, released); return Container( margin: const EdgeInsets.fromLTRB(16, 0, 16, 10), padding: const EdgeInsets.fromLTRB(14, 14, 14, 12), decoration: BoxDecoration( color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary, borderRadius: BorderRadius.circular(16), border: isDark ? null : Border.all(color: AppColors.lightBorder, width: 0.5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( '${l10n.assetsStakingOrderNo}: ORD${order.id}', style: TextStyle( color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w600, ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: _statusBackgroundColor(), borderRadius: BorderRadius.circular(999), ), child: Text( _statusLabel(l10n), style: TextStyle( color: _statusTextColor(), fontSize: 12, fontWeight: FontWeight.w600, height: 1, ), ), ), ], ), const SizedBox(height: 14), _StakingOrderRow( label: l10n.orderTime, value: _displayDate(order.stakingTime ?? order.createTime), ), _StakingOrderRow( label: l10n.assetsStakingTotal, value: '${_fmtAmount(amount, decimals: 0)} $coin', ), _StakingOrderRow( label: l10n.stakingLockDaysLabel, value: _lockDaysText(), ), _StakingOrderRow( label: l10n.assetsReleased, value: '${_fmtReleasedCompact(released)}/${_fmtReleasedCompact(amount)}', ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(999), child: SizedBox( height: 8, child: Stack( fit: StackFit.expand, children: [ Container(color: const Color(0xFF12161C)), FractionallySizedBox( alignment: Alignment.centerLeft, widthFactor: progress, child: Container(color: _progressColor()), ), ], ), ), ), ], ), ); } } class _StakingOrderRow extends StatelessWidget { const _StakingOrderRow({ required this.label, required this.value, }); final String label; final String value; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle(color: cs.onSurface.withAlpha(140), fontSize: 13), ), Expanded( child: Text( value, textAlign: TextAlign.right, style: TextStyle( color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500, ), ), ), ], ), ); } }