|
@@ -0,0 +1,425 @@
|
|
|
|
|
+import 'package:flutter/material.dart';
|
|
|
|
|
+import 'package:go_router/go_router.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,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 16),
|
|
|
|
|
+ Row(
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Expanded(
|
|
|
|
|
+ child: _StakingActionButton(
|
|
|
|
|
+ label: l10n.depositCoin,
|
|
|
|
|
+ primary: true,
|
|
|
|
|
+ onTap: () => context.push('/asset/deposit'),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(width: 8),
|
|
|
|
|
+ Expanded(
|
|
|
|
|
+ child: _StakingActionButton(
|
|
|
|
|
+ label: l10n.withdrawCoin,
|
|
|
|
|
+ onTap: () => context.push('/asset/withdraw'),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(width: 8),
|
|
|
|
|
+ Expanded(
|
|
|
|
|
+ child: _StakingActionButton(
|
|
|
|
|
+ label: l10n.transfer,
|
|
|
|
|
+ onTap: () =>
|
|
|
|
|
+ context.push('/asset/transfer?from=SPOT&to=SWAP'),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ 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 _StakingActionButton extends StatelessWidget {
|
|
|
|
|
+ const _StakingActionButton({
|
|
|
|
|
+ required this.label,
|
|
|
|
|
+ required this.onTap,
|
|
|
|
|
+ this.primary = false,
|
|
|
|
|
+ });
|
|
|
|
|
+ final String label;
|
|
|
|
|
+ final VoidCallback onTap;
|
|
|
|
|
+ final bool primary;
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ Widget build(BuildContext context) {
|
|
|
|
|
+ final cs = Theme.of(context).colorScheme;
|
|
|
|
|
+ return GestureDetector(
|
|
|
|
|
+ onTap: onTap,
|
|
|
|
|
+ child: Container(
|
|
|
|
|
+ height: 40,
|
|
|
|
|
+ alignment: Alignment.center,
|
|
|
|
|
+ decoration: BoxDecoration(
|
|
|
|
|
+ color: primary ? AppColors.brand : cs.onSurface.withAlpha(20),
|
|
|
|
|
+ borderRadius: BorderRadius.circular(20),
|
|
|
|
|
+ ),
|
|
|
|
|
+ child: Text(
|
|
|
|
|
+ label,
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ color: primary ? Colors.white : cs.onSurface,
|
|
|
|
|
+ fontSize: 13,
|
|
|
|
|
+ fontWeight: FontWeight.w600,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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) {
|
|
|
|
|
+ if (obscure) {
|
|
|
|
|
+ return '******';
|
|
|
|
|
+ }
|
|
|
|
|
+ return formatPrice(value, decimalPlaces: 2);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String _statusLabel(AppLocalizations l10n) {
|
|
|
|
|
+ if (order.status == 0) {
|
|
|
|
|
+ return l10n.assetsSubscribing;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (order.status == 2) {
|
|
|
|
|
+ return l10n.completed;
|
|
|
|
|
+ }
|
|
|
|
|
+ return l10n.assetsReleasing;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Color _statusColor() {
|
|
|
|
|
+ if (order.status == 0) {
|
|
|
|
|
+ return AppColors.brand;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (order.status == 2) {
|
|
|
|
|
+ return Colors.grey;
|
|
|
|
|
+ }
|
|
|
|
|
+ return Colors.orange;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @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;
|
|
|
|
|
+
|
|
|
|
|
+ return Container(
|
|
|
|
|
+ margin: const EdgeInsets.fromLTRB(16, 0, 16, 10),
|
|
|
|
|
+ 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: [
|
|
|
|
|
+ Text(
|
|
|
|
|
+ '${l10n.assetsStakingOrderNo}: ORD${order.id}',
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ color: cs.onSurface.withAlpha(153),
|
|
|
|
|
+ fontSize: 12,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 12),
|
|
|
|
|
+ _StakingOrderRow(
|
|
|
|
|
+ label: l10n.assetsStakingTotal,
|
|
|
|
|
+ value: '${_fmtAmount(amount)} $coin',
|
|
|
|
|
+ ),
|
|
|
|
|
+ _StakingOrderRow(
|
|
|
|
|
+ label: l10n.assetsReleased,
|
|
|
|
|
+ value: '${_fmtAmount(released)} $coin',
|
|
|
|
|
+ ),
|
|
|
|
|
+ _StakingOrderRow(
|
|
|
|
|
+ label: l10n.assetsPendingRelease,
|
|
|
|
|
+ value: '${_fmtAmount(order.pendingAmount)} $coin',
|
|
|
|
|
+ ),
|
|
|
|
|
+ _StakingOrderRow(
|
|
|
|
|
+ label: l10n.startTime,
|
|
|
|
|
+ value: order.displayStartTime,
|
|
|
|
|
+ ),
|
|
|
|
|
+ _StakingOrderRow(
|
|
|
|
|
+ label: l10n.statusLabel,
|
|
|
|
|
+ value: _statusLabel(l10n),
|
|
|
|
|
+ valueColor: _statusColor(),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+class _StakingOrderRow extends StatelessWidget {
|
|
|
|
|
+ const _StakingOrderRow({
|
|
|
|
|
+ required this.label,
|
|
|
|
|
+ required this.value,
|
|
|
|
|
+ this.valueColor,
|
|
|
|
|
+ });
|
|
|
|
|
+ final String label;
|
|
|
|
|
+ final String value;
|
|
|
|
|
+ final Color? valueColor;
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ Widget build(BuildContext context) {
|
|
|
|
|
+ final cs = Theme.of(context).colorScheme;
|
|
|
|
|
+ return Padding(
|
|
|
|
|
+ padding: const EdgeInsets.only(bottom: 8),
|
|
|
|
|
+ child: Row(
|
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Text(
|
|
|
|
|
+ label,
|
|
|
|
|
+ style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13),
|
|
|
|
|
+ ),
|
|
|
|
|
+ Flexible(
|
|
|
|
|
+ child: Text(
|
|
|
|
|
+ value,
|
|
|
|
|
+ textAlign: TextAlign.right,
|
|
|
|
|
+ style: TextStyle(
|
|
|
|
|
+ color: valueColor ?? cs.onSurface,
|
|
|
|
|
+ fontSize: 13,
|
|
|
|
|
+ fontWeight: FontWeight.w500,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|