| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477 |
- 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<StakingOrderStatusFilter> 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<StakingOrderStatusFilter>(
- 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,
- ),
- ),
- ),
- ],
- ),
- );
- }
- }
|