diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/backup_mnemonic_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/backup_mnemonic_page.dart index 672f4a48..11567055 100644 --- a/frontend/mobile-app/lib/features/auth/presentation/pages/backup_mnemonic_page.dart +++ b/frontend/mobile-app/lib/features/auth/presentation/pages/backup_mnemonic_page.dart @@ -50,6 +50,10 @@ class _BackupMnemonicPageState extends ConsumerState { // 轮询定时器 Timer? _pollTimer; + // 倒计时定时器 + Timer? _countdownTimer; + // 倒计时剩余秒数 (100秒) + int _countdownSeconds = 100; @override void initState() { @@ -60,8 +64,9 @@ class _BackupMnemonicPageState extends ConsumerState { @override void dispose() { - debugPrint('[BackupMnemonicPage] dispose - 取消轮询定时器'); + debugPrint('[BackupMnemonicPage] dispose - 取消轮询定时器和倒计时'); _pollTimer?.cancel(); + _countdownTimer?.cancel(); super.dispose(); } @@ -106,6 +111,7 @@ class _BackupMnemonicPageState extends ConsumerState { _errorMessage = null; }); _startPolling(); + _startCountdown(); } else if (response.isFailed) { // 钱包生成失败 debugPrint('[BackupMnemonicPage] _loadWalletInfo - 钱包生成失败'); @@ -132,6 +138,34 @@ class _BackupMnemonicPageState extends ConsumerState { return '${address.substring(0, 6)}...${address.substring(address.length - 4)}'; } + /// 开始倒计时 + void _startCountdown() { + _countdownTimer?.cancel(); + _countdownSeconds = 100; + debugPrint('[BackupMnemonicPage] _startCountdown - 启动100秒倒计时'); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) { + timer.cancel(); + return; + } + if (_countdownSeconds > 0) { + setState(() { + _countdownSeconds--; + }); + } else { + timer.cancel(); + debugPrint('[BackupMnemonicPage] _startCountdown - 倒计时结束'); + } + }); + } + + /// 停止倒计时 + void _stopCountdown() { + _countdownTimer?.cancel(); + _countdownTimer = null; + debugPrint('[BackupMnemonicPage] _stopCountdown - 停止倒计时'); + } + /// 开始轮询 int _pollCount = 0; void _startPolling() { @@ -162,6 +196,7 @@ class _BackupMnemonicPageState extends ConsumerState { if (response.isReady) { debugPrint('[BackupMnemonicPage] _startPolling - 钱包就绪! 停止轮询 (共轮询 $_pollCount 次)'); timer.cancel(); + _stopCountdown(); final wordCount = response.mnemonic?.split(' ').length ?? 0; debugPrint('[BackupMnemonicPage] _startPolling - 助记词数量: $wordCount'); setState(() { @@ -176,6 +211,7 @@ class _BackupMnemonicPageState extends ConsumerState { } else if (response.isFailed) { debugPrint('[BackupMnemonicPage] _startPolling - 钱包生成失败,停止轮询'); timer.cancel(); + _stopCountdown(); setState(() { _isLoading = false; _errorMessage = '钱包生成失败,请稍后重试'; @@ -367,15 +403,19 @@ ${DateTime.now().toString()} children: [ // 顶部导航栏 _buildAppBar(), - // 内容区域 + // 内容区域 - 始终显示内容,加载时覆盖蒙版 Expanded( - child: _isLoading - ? _buildLoadingState() - : _errorMessage != null - ? _buildErrorState() - : _buildContent(), + child: Stack( + children: [ + // 底层:内容(始终显示,加载时显示占位数据) + _buildContentWithPlaceholder(), + // 顶层:加载蒙版或错误蒙版 + if (_isLoading) _buildLoadingOverlay(), + if (_errorMessage != null) _buildErrorOverlay(), + ], + ), ), - // 底部按钮区域 (非加载状态时显示) + // 底部按钮区域 (非加载状态且无错误时显示) if (!_isLoading && _errorMessage == null) _buildBottomButtons(), ], ), @@ -384,37 +424,133 @@ ${DateTime.now().toString()} ); } - /// 构建加载状态 - Widget _buildLoadingState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + /// 构建加载蒙版 - 覆盖助记词和地址区域,显示倒计时 + Widget _buildLoadingOverlay() { + return Positioned.fill( + child: Container( + color: const Color(0xCC5D4037), // 半透明深棕色蒙版 + child: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 32), + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: const Color(0xFFFFFDF5), + borderRadius: BorderRadius.circular(20), + boxShadow: const [ + BoxShadow( + color: Color(0x33000000), + blurRadius: 20, + offset: Offset(0, 8), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 安全图标 + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: const Color(0xFFFFF3D6), + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFFD4A84B), + width: 3, + ), + ), + child: const Center( + child: Icon( + Icons.security, + size: 40, + color: Color(0xFFD4A84B), + ), + ), + ), + const SizedBox(height: 24), + // 提示文字 + const Text( + '正在进行钱包的安全计算', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + const Text( + '使用多方安全计算技术生成您的钱包', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + color: Color(0xFF8B7355), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + // 倒计时圆环 + _buildCountdownCircle(), + const SizedBox(height: 16), + // 倒计时文字 + Text( + '预计剩余 $_countdownSeconds 秒', + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + color: Color(0xFF8B7355), + ), + ), + ], + ), + ), + ), + ), + ); + } + + /// 构建倒计时圆环 + Widget _buildCountdownCircle() { + final progress = _countdownSeconds / 100.0; + return SizedBox( + width: 100, + height: 100, + child: Stack( + alignment: Alignment.center, children: [ - const SizedBox( - width: 48, - height: 48, + // 背景圆环 + SizedBox( + width: 100, + height: 100, child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation(Color(0xFFD4AF37)), + value: 1.0, + strokeWidth: 8, + backgroundColor: const Color(0xFFE8E0D0), + valueColor: const AlwaysStoppedAnimation(Color(0xFFE8E0D0)), ), ), - const SizedBox(height: 24), - const Text( - '正在生成您的钱包...', - style: TextStyle( - fontSize: 18, - fontFamily: 'Inter', - fontWeight: FontWeight.w600, - color: Color(0xFF5D4037), + // 进度圆环 + SizedBox( + width: 100, + height: 100, + child: CircularProgressIndicator( + value: progress, + strokeWidth: 8, + backgroundColor: Colors.transparent, + valueColor: const AlwaysStoppedAnimation(Color(0xFFD4A84B)), ), ), - const SizedBox(height: 8), - const Text( - '请稍候,这可能需要几秒钟', - style: TextStyle( - fontSize: 14, + // 倒计时数字 + Text( + '$_countdownSeconds', + style: const TextStyle( + fontSize: 32, fontFamily: 'Inter', - color: Color(0xFF8B7355), + fontWeight: FontWeight.w700, + color: Color(0xFFD4A84B), ), ), ], @@ -422,75 +558,135 @@ ${DateTime.now().toString()} ); } - /// 构建错误状态 - Widget _buildErrorState() { - return Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.error_outline, - size: 64, - color: Color(0xFFCC6B2C), - ), - const SizedBox(height: 16), - Text( - _errorMessage!, - style: const TextStyle( - fontSize: 16, - fontFamily: 'Inter', - color: Color(0xFF5D4037), - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - GestureDetector( - onTap: () { - setState(() { - _isLoading = true; - _errorMessage = null; - }); - _loadWalletInfo(); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - decoration: BoxDecoration( - color: const Color(0xFFD4AF37), - borderRadius: BorderRadius.circular(8), + /// 构建错误蒙版 + Widget _buildErrorOverlay() { + return Positioned.fill( + child: Container( + color: const Color(0xCC5D4037), // 半透明深棕色蒙版 + child: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 32), + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: const Color(0xFFFFFDF5), + borderRadius: BorderRadius.circular(20), + boxShadow: const [ + BoxShadow( + color: Color(0x33000000), + blurRadius: 20, + offset: Offset(0, 8), ), - child: const Text( - '重试', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 错误图标 + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: Color(0xFFFFE4E4), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.error_outline, + size: 48, + color: Color(0xFFCC6B2C), + ), ), ), - ), + const SizedBox(height: 24), + // 错误标题 + const Text( + '钱包生成失败', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + // 错误信息 + Text( + _errorMessage ?? '未知错误', + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + color: Color(0xFF8B7355), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + // 重试按钮 + GestureDetector( + onTap: () { + setState(() { + _isLoading = true; + _errorMessage = null; + _countdownSeconds = 100; + }); + _loadWalletInfo(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14), + decoration: BoxDecoration( + color: const Color(0xFFD4A84B), + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x33D4A84B), + blurRadius: 8, + offset: Offset(0, 4), + ), + ], + ), + child: const Text( + '重新尝试', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ), + ), + ], ), - ], + ), ), ), ); } - /// 构建内容 - Widget _buildContent() { + /// 构建内容(带占位数据) + Widget _buildContentWithPlaceholder() { + // 使用占位数据或真实数据 + final displayWords = _mnemonicWords.isNotEmpty + ? _mnemonicWords + : List.generate(12, (i) => '******'); + final displayKava = _kavaAddress ?? '0x••••••••••••••••••••'; + final displayDst = _dstAddress ?? '0x••••••••••••••••••••'; + final displayBsc = _bscAddress ?? '0x••••••••••••••••••••'; + return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ const SizedBox(height: 8), - // 助记词卡片 - if (_mnemonicWords.isNotEmpty) _buildMnemonicCard(), + // 助记词卡片 (使用占位或真实数据) + _buildMnemonicCardWithData(displayWords), const SizedBox(height: 16), // 警告提示 _buildWarningCard(), const SizedBox(height: 16), - // 钱包地址卡片 - _buildAddressCard(), + // 钱包地址卡片 (使用占位或真实数据) + _buildAddressCardWithData(displayKava, displayDst, displayBsc), const SizedBox(height: 16), ], ), @@ -710,6 +906,329 @@ ${DateTime.now().toString()} ); } + /// 构建助记词卡片(带数据参数) + Widget _buildMnemonicCardWithData(List words) { + final isPlaceholder = words.isNotEmpty && words[0] == '******'; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFFFFDF5), + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题和操作按钮 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '您的助记词', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.25, + letterSpacing: -0.27, + color: Color(0xFF5D4037), + ), + ), + // 仅在非占位状态时显示操作按钮 + if (!isPlaceholder) + Row( + children: [ + // 显示/隐藏按钮 + GestureDetector( + onTap: _toggleVisibility, + child: Container( + padding: const EdgeInsets.all(4), + child: Icon( + _isHidden ? Icons.visibility : Icons.visibility_off, + color: const Color(0xFF8B7355), + size: 22, + ), + ), + ), + const SizedBox(width: 12), + // 下载按钮 + GestureDetector( + onTap: _isDownloading ? null : _downloadMnemonic, + child: Container( + padding: const EdgeInsets.all(4), + child: _isDownloading + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Color(0xFF8B7355), + ), + ), + ) + : const Icon( + Icons.file_download_outlined, + color: Color(0xFF8B7355), + size: 22, + ), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 20), + // 助记词网格 + _buildMnemonicGridWithData(words), + const SizedBox(height: 20), + // 复制全部按钮(仅在非占位状态时可用) + _buildCopyAllButtonWithState(!isPlaceholder), + ], + ), + ); + } + + /// 构建助记词网格(带数据参数) + Widget _buildMnemonicGridWithData(List words) { + return Column( + children: [ + for (int row = 0; row < 4; row++) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + for (int col = 0; col < 3; col++) + Expanded( + child: _buildMnemonicWordWithData( + row * 3 + col + 1, + words.length > row * 3 + col ? words[row * 3 + col] : '', + ), + ), + ], + ), + ), + ], + ); + } + + /// 构建单个助记词项(带数据参数) + Widget _buildMnemonicWordWithData(int index, String word) { + final isPlaceholder = word == '******'; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 序号 + Text( + '$index.', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + height: 1.43, + color: isPlaceholder + ? const Color(0x668B7355) + : const Color(0xCC8B7355), + ), + ), + const SizedBox(width: 6), + // 单词 + Text( + _isHidden && !isPlaceholder ? '******' : word, + style: TextStyle( + fontSize: 15, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + height: 1.5, + color: isPlaceholder + ? const Color(0x665D4037) + : const Color(0xFF5D4037), + ), + ), + ], + ); + } + + /// 构建复制全部按钮(带状态) + Widget _buildCopyAllButtonWithState(bool enabled) { + return GestureDetector( + onTap: enabled ? _copyAllMnemonic : null, + child: Container( + width: double.infinity, + height: 44, + decoration: BoxDecoration( + color: enabled + ? const Color(0xFFF5ECD9) + : const Color(0xFFE8E0D0), + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text( + '复制全部', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + height: 1.5, + color: enabled + ? const Color(0xFF8B7355) + : const Color(0x668B7355), + ), + ), + ), + ), + ); + } + + /// 构建钱包地址卡片(带数据参数) + Widget _buildAddressCardWithData(String kava, String dst, String bsc) { + final isPlaceholder = kava.contains('••••'); + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFFFFFDF5), + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + _buildAddressItemWithData( + iconWidget: _buildChainIcon(), + label: 'KAVA 地址', + address: kava, + showBorder: true, + isPlaceholder: isPlaceholder, + ), + _buildAddressItemWithData( + iconWidget: _buildChainIcon(), + label: 'DST 地址', + address: dst, + showBorder: true, + isPlaceholder: isPlaceholder, + ), + _buildAddressItemWithData( + iconWidget: _buildChainIcon(), + label: 'BSC 地址', + address: bsc, + showBorder: true, + isPlaceholder: isPlaceholder, + ), + _buildAddressItemWithData( + iconWidget: _buildSequenceIcon(), + label: '序列号', + address: widget.userSerialNum.toString(), + showBorder: false, + isPlaceholder: false, + ), + ], + ), + ); + } + + /// 构建地址项(带占位状态) + Widget _buildAddressItemWithData({ + required Widget iconWidget, + required String label, + required String address, + required bool showBorder, + required bool isPlaceholder, + }) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + border: showBorder + ? const Border( + bottom: BorderSide( + color: Color(0x1A5D4037), + width: 1, + ), + ) + : null, + ), + child: Row( + children: [ + // 图标 + iconWidget, + const SizedBox(width: 14), + // 标签和地址 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + height: 1.5, + color: isPlaceholder + ? const Color(0x995D4037) + : const Color(0xFF5D4037), + ), + ), + const SizedBox(height: 2), + Text( + _formatAddress(address), + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.5, + color: isPlaceholder + ? const Color(0x66756452) + : const Color(0x99756452), + ), + ), + ], + ), + ), + // 复制按钮(仅在非占位状态时可用) + if (!isPlaceholder) + GestureDetector( + onTap: () => _copyAddress(address, label), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.copy_rounded, + color: Color(0xCC8B7355), + size: 16, + ), + SizedBox(width: 4), + Text( + '复制', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + height: 1.5, + color: Color(0xCC8B7355), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + /// 构建警告卡片 Widget _buildWarningCard() { return Container( @@ -733,7 +1252,7 @@ ${DateTime.now().toString()} ); } - /// 构建钱包地址卡片 + /// 构建钱包地址卡片 (旧方法,保留向后兼容) Widget _buildAddressCard() { return Container( width: double.infinity,