From 0860ff23b83ba4a582388c24b9692834b9cf2e9c Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 14 Dec 2025 09:19:25 -0800 Subject: [PATCH] =?UTF-8?q?fix(guide):=20=E4=BF=AE=E5=A4=8D=E5=90=91?= =?UTF-8?q?=E5=AF=BC=E9=A1=B55=E5=AF=BC=E5=85=A5=E5=8A=A9=E8=AE=B0?= =?UTF-8?q?=E8=AF=8D=E6=8C=89=E9=92=AE=E5=AF=BC=E8=88=AA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 Navigator.of(context).pushNamed() 改为 context.push(), 使用 go_router 进行页面导航,与 onboarding_page 保持一致。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../auth/presentation/pages/guide_page.dart | 2280 ++++++++--------- 1 file changed, 1140 insertions(+), 1140 deletions(-) diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/guide_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/guide_page.dart index dec46317..2e2b2815 100644 --- a/frontend/mobile-app/lib/features/auth/presentation/pages/guide_page.dart +++ b/frontend/mobile-app/lib/features/auth/presentation/pages/guide_page.dart @@ -1,1140 +1,1140 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import '../../../../core/di/injection_container.dart'; -import '../../../../core/storage/storage_keys.dart'; -import '../../../../routes/route_paths.dart'; -import '../providers/auth_provider.dart'; - -/// 向导页数据模型 -class GuidePageData { - final String? imagePath; - final String title; - final String subtitle; - final Widget? customContent; - - const GuidePageData({ - this.imagePath, - required this.title, - required this.subtitle, - this.customContent, - }); -} - -/// 向导页面 - 用户首次打开应用时展示 -/// 支持左右滑动切换页面 -class GuidePage extends ConsumerStatefulWidget { - const GuidePage({super.key}); - - @override - ConsumerState createState() => _GuidePageState(); -} - -class _GuidePageState extends ConsumerState { - final PageController _pageController = PageController(); - int _currentPage = 0; - - @override - void initState() { - super.initState(); - // 延迟到 build 后获取屏幕信息 - WidgetsBinding.instance.addPostFrameCallback((_) { - _logScreenInfo(); - }); - } - - /// 打印屏幕信息用于调试 - void _logScreenInfo() { - final mediaQuery = MediaQuery.of(context); - final screenSize = mediaQuery.size; - final devicePixelRatio = mediaQuery.devicePixelRatio; - final physicalSize = screenSize * devicePixelRatio; - - debugPrint('[GuidePage] ========== 屏幕信息 =========='); - debugPrint('[GuidePage] 逻辑分辨率: ${screenSize.width.toStringAsFixed(1)} x ${screenSize.height.toStringAsFixed(1)}'); - debugPrint('[GuidePage] 设备像素比: $devicePixelRatio'); - debugPrint('[GuidePage] 物理分辨率: ${physicalSize.width.toStringAsFixed(0)} x ${physicalSize.height.toStringAsFixed(0)}'); - debugPrint('[GuidePage] 屏幕宽高比: ${(screenSize.width / screenSize.height).toStringAsFixed(3)} (${screenSize.width.toStringAsFixed(0)}:${screenSize.height.toStringAsFixed(0)})'); - debugPrint('[GuidePage] 图片设计比例: 0.5625 (1080:1920 = 9:16)'); - debugPrint('[GuidePage] ================================'); - } - - // 向导页1-5的数据 (第5页为欢迎加入页) - // 支持 png、jpg、webp 等格式 - final List _guidePages = const [ - GuidePageData( - imagePath: 'assets/images/guide_1.jpg', - title: '认种一棵榴莲树\n拥有真实RWA资产', - subtitle: '绑定真实果园20年收益,让区块链与农业完美结合', - ), - GuidePageData( - imagePath: 'assets/images/guide_2.jpg', - title: '认种即可开启算力\n自动挖矿持续收益', - subtitle: '每一棵树都对应真实资产注入,为算力提供真实价值支撑', - ), - GuidePageData( - imagePath: 'assets/images/guide_3.jpg', - title: '分享链接\n获得团队算力与收益', - subtitle: '真实认种数据透明可信 · 团队越大算力越强', - ), - GuidePageData( - imagePath: 'assets/images/guide_4.jpg', - title: 'MPC多方安全\n所有地址与收益可审计', - subtitle: '你的资产 · 安全透明 · 不可被篡改', - ), - GuidePageData( - imagePath: 'assets/images/guide_5.jpg', - title: '欢迎加入', - subtitle: '创建账号前的最后一步 · 请选择是否有推荐人', - ), - ]; - - @override - void dispose() { - _pageController.dispose(); - super.dispose(); - } - - void _onPageChanged(int page) { - setState(() { - _currentPage = page; - }); - } - - void _goToNextPage() { - if (_currentPage < 4) { - _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - } - - void _goToOnboarding() async { - // 标记已查看向导页 - await ref.read(authProvider.notifier).markGuideAsSeen(); - if (!mounted) return; - context.go(RoutePaths.onboarding); - } - - /// 退出应用 - void _exitApp() { - SystemNavigator.pop(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: PageView.builder( - controller: _pageController, - onPageChanged: _onPageChanged, - itemCount: 5, // 4个介绍页 + 1个欢迎加入页 - itemBuilder: (context, index) { - if (index < 4) { - return _buildGuidePage(_guidePages[index], index); - } else { - return _buildWelcomePage(); - } - }, - ), - ); - } - - /// 构建向导页 (页面1-4) - 全屏背景图片,无文字 - /// 使用 BoxFit.cover 填满屏幕,保持宽高比(会裁剪超出部分) - Widget _buildGuidePage(GuidePageData data, int index) { - debugPrint('[GuidePage] _buildGuidePage() - 页面 ${index + 1}, 图片: ${data.imagePath}'); - return Stack( - fit: StackFit.expand, - children: [ - // 全屏背景图片 - 使用 cover 填满屏幕 - if (data.imagePath != null) - Image.asset( - data.imagePath!, - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - errorBuilder: (context, error, stackTrace) { - debugPrint('[GuidePage] 页面 ${index + 1} 图片加载失败: $error'); - return Container( - color: const Color(0xFFFFF8E7), - child: _buildPlaceholderImage(index), - ); - }, - ) - else - Container( - color: const Color(0xFFFFF8E7), - child: _buildPlaceholderImage(index), - ), - // 底部页面指示器 - 使用 SafeArea 避免被系统 UI 遮挡 - Positioned( - left: 0, - right: 0, - bottom: 0, - child: SafeArea( - child: Padding( - padding: EdgeInsets.only(bottom: 40.h), - child: _buildPageIndicator(), - ), - ), - ), - ], - ); - } - - /// 构建占位图片 - Widget _buildPlaceholderImage(int index) { - final icons = [ - Icons.nature, - Icons.memory, - Icons.people, - Icons.security, - ]; - final colors = [ - const Color(0xFF8BC34A), - const Color(0xFFD4AF37), - const Color(0xFFFF9800), - const Color(0xFF2196F3), - ]; - - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icons[index], - size: 80.sp, - color: colors[index], - ), - SizedBox(height: 16.h), - Text( - '向导页 ${index + 1}', - style: TextStyle( - fontSize: 16.sp, - color: const Color(0xFF57534E), - ), - ), - ], - ), - ); - } - - /// 构建欢迎加入页面 (页面5) - Widget _buildWelcomePage() { - return _WelcomePageContent( - onNext: _goToOnboarding, - onExit: _exitApp, // 退出整个应用 - backgroundImage: _guidePages[4].imagePath, - pageIndicator: _buildPageIndicator(), - ); - } - - /// 构建页面指示器 (所有页面都使用白色系,因为都有背景图) - Widget _buildPageIndicator() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(5, (index) { - final isActive = index == _currentPage; - return Container( - width: 8.w, - height: 8.w, - margin: EdgeInsets.symmetric(horizontal: 4.w), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isActive - ? Colors.white - : Colors.white.withValues(alpha: 0.4), - ), - ); - }), - ); - } -} - -/// 欢迎加入页面内容 (第5页) -class _WelcomePageContent extends ConsumerStatefulWidget { - final VoidCallback onNext; - final VoidCallback onExit; - final String? backgroundImage; - final Widget pageIndicator; - - const _WelcomePageContent({ - required this.onNext, - required this.onExit, - this.backgroundImage, - required this.pageIndicator, - }); - - @override - ConsumerState<_WelcomePageContent> createState() => _WelcomePageContentState(); -} - -class _WelcomePageContentState extends ConsumerState<_WelcomePageContent> { - bool _hasReferrer = true; - final TextEditingController _referralCodeController = TextEditingController(); - - @override - void initState() { - super.initState(); - // 监听输入框变化以更新按钮状态 - _referralCodeController.addListener(_onReferralCodeChanged); - } - - @override - void dispose() { - _referralCodeController.removeListener(_onReferralCodeChanged); - _referralCodeController.dispose(); - super.dispose(); - } - - /// 输入框内容变化时刷新UI - void _onReferralCodeChanged() { - setState(() {}); - } - - /// 验证推荐码是否有效 - /// 支持: 纯推荐码(至少3个字符) 或 合法URL - bool _isValidReferralCode(String code) { - final trimmed = code.trim(); - if (trimmed.isEmpty) return false; - - // 如果是URL格式,检查是否能解析 - if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { - try { - final uri = Uri.parse(trimmed); - // URL必须有路径或查询参数 - return uri.pathSegments.isNotEmpty || uri.queryParameters.isNotEmpty; - } catch (e) { - return false; - } - } - - // 纯推荐码至少3个字符 - return trimmed.length >= 3; - } - - /// 判断按钮是否可点击 - bool get _canProceed { - // 如果选择"没有推荐人",按钮可点击 - if (!_hasReferrer) return true; - - // 如果选择"有推荐人",需要填写有效的推荐码 - return _isValidReferralCode(_referralCodeController.text); - } - - /// 导入助记词恢复账号 - void _importMnemonic() { - debugPrint('[GuidePage] _importMnemonic - 跳转到导入助记词页面'); - Navigator.of(context).pushNamed(RoutePaths.importMnemonic); - } - - /// 保存推荐码并继续下一步 - Future _saveReferralCodeAndProceed() async { - if (!_canProceed) return; - - // 如果有推荐人且推荐码有效,保存到本地存储 - if (_hasReferrer && _referralCodeController.text.trim().isNotEmpty) { - final secureStorage = ref.read(secureStorageProvider); - await secureStorage.write( - key: StorageKeys.inviterReferralCode, - value: _referralCodeController.text.trim(), - ); - debugPrint('[GuidePage] 保存邀请人推荐码: ${_referralCodeController.text.trim()}'); - } - - // 调用下一步回调 - widget.onNext(); - } - - /// 打开二维码扫描页面 - Future _openQrScanner() async { - final result = await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const _QrScannerPage(), - ), - ); - if (result != null && result.isNotEmpty) { - // 从扫描结果中提取推荐码 - final referralCode = _extractReferralCode(result); - setState(() { - _referralCodeController.text = referralCode; - _hasReferrer = true; - }); - } - } - - /// 从扫描结果中提取推荐码 - /// 支持以下格式: - /// - 查询参数: https://app.rwadurian.com/share?ref=ABC123 -> ABC123 - /// - 路径格式: https://app.rwadurian.com/r/ABC123 -> ABC123 - /// - 短链: https://rwa.link/ABC123 -> ABC123 - /// - 纯推荐码: ABC123 -> ABC123 - String _extractReferralCode(String scannedData) { - // 去除首尾空格 - String data = scannedData.trim(); - - // 如果是 URL,尝试提取推荐码 - if (data.startsWith('http://') || data.startsWith('https://')) { - try { - final uri = Uri.parse(data); - - // 优先从查询参数中提取 (如 ?ref=ABC123 或 ?code=ABC123) - final refCode = uri.queryParameters['ref'] ?? - uri.queryParameters['code'] ?? - uri.queryParameters['referral']; - if (refCode != null && refCode.isNotEmpty) { - return refCode; - } - - // 其次从路径中提取 (如 /r/ABC123 或 /ABC123) - final pathSegments = uri.pathSegments; - if (pathSegments.isNotEmpty) { - // 取最后一个路径段作为推荐码 - final lastSegment = pathSegments.last; - // 排除常见的非推荐码路径段 - const excludedSegments = ['share', 'invite', 'r', 'ref', 'referral']; - if (lastSegment.isNotEmpty && !excludedSegments.contains(lastSegment.toLowerCase())) { - return lastSegment; - } - // 如果最后一段是排除项且有倒数第二段,检查倒数第二段 - if (pathSegments.length >= 2) { - final secondLastSegment = pathSegments[pathSegments.length - 2]; - if (secondLastSegment.isNotEmpty && !excludedSegments.contains(secondLastSegment.toLowerCase())) { - return secondLastSegment; - } - } - } - } catch (e) { - // URL 解析失败,返回原始数据 - debugPrint('URL 解析失败: $e'); - } - } - - // 不是 URL 或解析失败,返回原始数据 - return data; - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: Stack( - fit: StackFit.expand, - children: [ - // 全屏背景图片 - 使用 cover 填满屏幕 - if (widget.backgroundImage != null) - Image.asset( - widget.backgroundImage!, - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - errorBuilder: (context, error, stackTrace) { - return Container( - color: const Color(0xFFFFF8E7), - ); - }, - ) - else - Container( - color: const Color(0xFFFFF8E7), - ), - // 半透明遮罩,让内容更清晰 - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black.withValues(alpha: 0.3), - Colors.black.withValues(alpha: 0.6), - ], - ), - ), - ), - // 内容区域 - SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: Column( - children: [ - // 退出按钮 - Align( - alignment: Alignment.topRight, - child: Padding( - padding: EdgeInsets.only(top: 16.h), - child: GestureDetector( - onTap: widget.onExit, - child: Text( - '退出', - style: TextStyle( - fontSize: 14.sp, - color: Colors.white.withValues(alpha: 0.8), - ), - ), - ), - ), - ), - SizedBox(height: 60.h), - // 欢迎标题 - Text( - '欢迎加入', - style: TextStyle( - fontSize: 28.sp, - fontWeight: FontWeight.w700, - height: 1.33, - color: Colors.white, - shadows: [ - Shadow( - color: Colors.black.withValues(alpha: 0.5), - blurRadius: 4, - ), - ], - ), - ), - SizedBox(height: 12.h), - // 副标题 - Text( - '创建账号前的最后一步 · 请选择是否有推荐人', - style: TextStyle( - fontSize: 14.sp, - height: 1.43, - color: Colors.white.withValues(alpha: 0.9), - shadows: [ - Shadow( - color: Colors.black.withValues(alpha: 0.5), - blurRadius: 4, - ), - ], - ), - ), - SizedBox(height: 48.h), - // 选项区域 (透明背景) - _buildReferrerOptions(), - const Spacer(), - // 页面指示器 - widget.pageIndicator, - SizedBox(height: 24.h), - // 下一步按钮 - GestureDetector( - onTap: _canProceed ? _saveReferralCodeAndProceed : null, - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 16.h), - decoration: BoxDecoration( - color: _canProceed - ? const Color(0xFFD4A84B) - : Colors.white.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(12.r), - ), - child: Text( - '下一步 (创建账号)', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - height: 1.5, - color: _canProceed - ? Colors.white - : Colors.white.withValues(alpha: 0.6), - ), - textAlign: TextAlign.center, - ), - ), - ), - SizedBox(height: 32.h), - ], - ), - ), - ), - ), - ); - }, - ), - ), - ], - ), - ); - } - - /// 构建推荐人选项 (透明背景,白色文字) - Widget _buildReferrerOptions() { - return Column( - children: [ - // 有推荐人选项 - GestureDetector( - onTap: () { - setState(() { - _hasReferrer = true; - }); - }, - child: Row( - children: [ - _buildRadio(_hasReferrer), - SizedBox(width: 12.w), - Text( - '我有推荐人', - style: TextStyle( - fontSize: 16.sp, - height: 1.5, - color: Colors.white, - shadows: [ - Shadow( - color: Colors.black.withValues(alpha: 0.5), - blurRadius: 4, - ), - ], - ), - ), - ], - ), - ), - SizedBox(height: 14.h), - // 推荐码输入框 - 使用 AnimatedSize 平滑过渡 - AnimatedSize( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - child: _hasReferrer - ? Container( - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1, - color: Colors.white.withValues(alpha: 0.5), - ), - ), - ), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: _openQrScanner, - child: AbsorbPointer( - child: TextField( - controller: _referralCodeController, - readOnly: true, - decoration: InputDecoration( - hintText: '点击扫描推荐码', - hintStyle: TextStyle( - fontSize: 16.sp, - color: Colors.black.withValues(alpha: 0.4), - ), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, - ), - style: TextStyle( - fontSize: 16.sp, - color: Colors.black, - ), - ), - ), - ), - ), - // 扫码按钮 - GestureDetector( - onTap: _openQrScanner, - child: Padding( - padding: EdgeInsets.only(left: 8.w), - child: Icon( - Icons.camera_alt_outlined, - size: 20.sp, - color: Colors.white.withValues(alpha: 0.8), - ), - ), - ), - ], - ), - ) - : const SizedBox.shrink(), - ), - SizedBox(height: 24.h), - // 没有推荐人选项 - GestureDetector( - onTap: () { - setState(() { - _hasReferrer = false; - }); - }, - child: Row( - children: [ - _buildRadio(!_hasReferrer), - SizedBox(width: 12.w), - Text( - '我没有推荐人', - style: TextStyle( - fontSize: 16.sp, - height: 1.5, - color: Colors.white, - shadows: [ - Shadow( - color: Colors.black.withValues(alpha: 0.5), - blurRadius: 4, - ), - ], - ), - ), - ], - ), - ), - SizedBox(height: 32.h), - // 分隔线 - Row( - children: [ - Expanded( - child: Container( - height: 1, - color: Colors.white.withValues(alpha: 0.3), - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: Text( - '或', - style: TextStyle( - fontSize: 14.sp, - color: Colors.white.withValues(alpha: 0.7), - ), - ), - ), - Expanded( - child: Container( - height: 1, - color: Colors.white.withValues(alpha: 0.3), - ), - ), - ], - ), - SizedBox(height: 24.h), - // 导入助记词入口 - GestureDetector( - onTap: _importMnemonic, - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 14.h), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: Colors.white.withValues(alpha: 0.3), - width: 1, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.restore, - size: 20.sp, - color: Colors.white, - ), - SizedBox(width: 8.w), - Text( - '已有账号?导入助记词恢复', - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - ), - ], - ), - ), - ), - ], - ); - } - - /// 构建单选按钮 (白色系,适配深色背景) - Widget _buildRadio(bool isSelected) { - return Container( - width: 20.w, - height: 20.w, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - width: isSelected ? 6.w : 2.w, - color: isSelected - ? Colors.white - : Colors.white.withValues(alpha: 0.5), - ), - ), - ); - } -} - -/// 二维码扫描页面 -class _QrScannerPage extends StatefulWidget { - const _QrScannerPage(); - - @override - State<_QrScannerPage> createState() => _QrScannerPageState(); -} - -class _QrScannerPageState extends State<_QrScannerPage> { - MobileScannerController? _controller; - bool _hasScanned = false; - bool _torchOn = false; - bool _isProcessingImage = false; - - @override - void initState() { - super.initState(); - _controller = MobileScannerController( - detectionSpeed: DetectionSpeed.normal, - facing: CameraFacing.back, - ); - } - - @override - void dispose() { - _controller?.dispose(); - super.dispose(); - } - - void _onDetect(BarcodeCapture capture) { - if (_hasScanned) return; - - final List barcodes = capture.barcodes; - for (final barcode in barcodes) { - if (barcode.rawValue != null && barcode.rawValue!.isNotEmpty) { - _hasScanned = true; - Navigator.of(context).pop(barcode.rawValue); - return; - } - } - } - - Future _toggleTorch() async { - await _controller?.toggleTorch(); - setState(() { - _torchOn = !_torchOn; - }); - } - - /// 从相册选择图片并扫描二维码 - Future _pickImageAndScan() async { - if (_isProcessingImage || _hasScanned) return; - - setState(() { - _isProcessingImage = true; - }); - - try { - final ImagePicker picker = ImagePicker(); - final XFile? image = await picker.pickImage(source: ImageSource.gallery); - - if (image == null) { - setState(() { - _isProcessingImage = false; - }); - return; - } - - // 使用 MobileScannerController 分析图片 - final BarcodeCapture? result = await _controller?.analyzeImage(image.path); - - if (result != null && result.barcodes.isNotEmpty) { - for (final barcode in result.barcodes) { - if (barcode.rawValue != null && barcode.rawValue!.isNotEmpty) { - _hasScanned = true; - if (mounted) { - Navigator.of(context).pop(barcode.rawValue); - } - return; - } - } - } - - // 未识别到二维码 - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '未能识别图片中的二维码,请重新选择', - style: TextStyle(fontSize: 14.sp), - ), - backgroundColor: const Color(0xFF6F6354), - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.all(16.w), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - ); - } - } catch (e) { - debugPrint('扫描图片失败: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '图片扫描失败,请重试', - style: TextStyle(fontSize: 14.sp), - ), - backgroundColor: Colors.red, - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.all(16.w), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - ); - } - } finally { - if (mounted) { - setState(() { - _isProcessingImage = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - backgroundColor: Colors.black, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.of(context).pop(), - ), - title: Text( - '扫描推荐码', - style: TextStyle( - fontSize: 18.sp, - color: Colors.white, - ), - ), - centerTitle: true, - actions: [ - IconButton( - icon: Icon( - _torchOn ? Icons.flash_on : Icons.flash_off, - color: Colors.white, - ), - onPressed: _toggleTorch, - ), - ], - ), - body: Stack( - children: [ - // 扫描区域 - MobileScanner( - controller: _controller, - onDetect: _onDetect, - ), - // 扫描框遮罩 - _buildScanOverlay(), - // 底部区域:提示文字和相册按钮 - Positioned( - bottom: 60.h, - left: 0, - right: 0, - child: Column( - children: [ - Text( - '将二维码放入框内,即可自动扫描', - style: TextStyle( - fontSize: 14.sp, - color: Colors.white70, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 32.h), - // 相册按钮 - GestureDetector( - onTap: _isProcessingImage ? null : _pickImageAndScan, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(24.r), - border: Border.all( - color: Colors.white.withValues(alpha: 0.3), - width: 1, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_isProcessingImage) - SizedBox( - width: 18.sp, - height: 18.sp, - child: const CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - else - Icon( - Icons.photo_library_outlined, - size: 18.sp, - color: Colors.white, - ), - SizedBox(width: 8.w), - Text( - _isProcessingImage ? '识别中...' : '从相册选择', - style: TextStyle( - fontSize: 14.sp, - color: Colors.white, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ], - ), - ); - } - - /// 构建扫描框遮罩 - Widget _buildScanOverlay() { - return LayoutBuilder( - builder: (context, constraints) { - final scanAreaSize = 250.w; - final left = (constraints.maxWidth - scanAreaSize) / 2; - final top = (constraints.maxHeight - scanAreaSize) / 2 - 50.h; - - return Stack( - children: [ - // 半透明背景 - ColorFiltered( - colorFilter: ColorFilter.mode( - Colors.black.withValues(alpha: 0.6), - BlendMode.srcOut, - ), - child: Stack( - children: [ - Container( - decoration: const BoxDecoration( - color: Colors.black, - backgroundBlendMode: BlendMode.dstOut, - ), - ), - Positioned( - left: left, - top: top, - child: Container( - width: scanAreaSize, - height: scanAreaSize, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12.r), - ), - ), - ), - ], - ), - ), - // 扫描框边角 - Positioned( - left: left, - top: top, - child: _buildCorner(true, true), - ), - Positioned( - right: left, - top: top, - child: _buildCorner(false, true), - ), - Positioned( - left: left, - bottom: constraints.maxHeight - top - scanAreaSize, - child: _buildCorner(true, false), - ), - Positioned( - right: left, - bottom: constraints.maxHeight - top - scanAreaSize, - child: _buildCorner(false, false), - ), - ], - ); - }, - ); - } - - /// 构建边角装饰 - Widget _buildCorner(bool isLeft, bool isTop) { - return SizedBox( - width: 24.w, - height: 24.w, - child: CustomPaint( - painter: _CornerPainter( - isLeft: isLeft, - isTop: isTop, - color: const Color(0xFFD4A84B), - strokeWidth: 3.w, - ), - ), - ); - } -} - -/// 边角绘制器 -class _CornerPainter extends CustomPainter { - final bool isLeft; - final bool isTop; - final Color color; - final double strokeWidth; - - _CornerPainter({ - required this.isLeft, - required this.isTop, - required this.color, - required this.strokeWidth, - }); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..strokeWidth = strokeWidth - ..style = PaintingStyle.stroke - ..strokeCap = StrokeCap.round; - - final path = Path(); - - if (isLeft && isTop) { - path.moveTo(0, size.height); - path.lineTo(0, 0); - path.lineTo(size.width, 0); - } else if (!isLeft && isTop) { - path.moveTo(0, 0); - path.lineTo(size.width, 0); - path.lineTo(size.width, size.height); - } else if (isLeft && !isTop) { - path.moveTo(0, 0); - path.lineTo(0, size.height); - path.lineTo(size.width, size.height); - } else { - path.moveTo(0, size.height); - path.lineTo(size.width, size.height); - path.lineTo(size.width, 0); - } - - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import '../../../../core/di/injection_container.dart'; +import '../../../../core/storage/storage_keys.dart'; +import '../../../../routes/route_paths.dart'; +import '../providers/auth_provider.dart'; + +/// 向导页数据模型 +class GuidePageData { + final String? imagePath; + final String title; + final String subtitle; + final Widget? customContent; + + const GuidePageData({ + this.imagePath, + required this.title, + required this.subtitle, + this.customContent, + }); +} + +/// 向导页面 - 用户首次打开应用时展示 +/// 支持左右滑动切换页面 +class GuidePage extends ConsumerStatefulWidget { + const GuidePage({super.key}); + + @override + ConsumerState createState() => _GuidePageState(); +} + +class _GuidePageState extends ConsumerState { + final PageController _pageController = PageController(); + int _currentPage = 0; + + @override + void initState() { + super.initState(); + // 延迟到 build 后获取屏幕信息 + WidgetsBinding.instance.addPostFrameCallback((_) { + _logScreenInfo(); + }); + } + + /// 打印屏幕信息用于调试 + void _logScreenInfo() { + final mediaQuery = MediaQuery.of(context); + final screenSize = mediaQuery.size; + final devicePixelRatio = mediaQuery.devicePixelRatio; + final physicalSize = screenSize * devicePixelRatio; + + debugPrint('[GuidePage] ========== 屏幕信息 =========='); + debugPrint('[GuidePage] 逻辑分辨率: ${screenSize.width.toStringAsFixed(1)} x ${screenSize.height.toStringAsFixed(1)}'); + debugPrint('[GuidePage] 设备像素比: $devicePixelRatio'); + debugPrint('[GuidePage] 物理分辨率: ${physicalSize.width.toStringAsFixed(0)} x ${physicalSize.height.toStringAsFixed(0)}'); + debugPrint('[GuidePage] 屏幕宽高比: ${(screenSize.width / screenSize.height).toStringAsFixed(3)} (${screenSize.width.toStringAsFixed(0)}:${screenSize.height.toStringAsFixed(0)})'); + debugPrint('[GuidePage] 图片设计比例: 0.5625 (1080:1920 = 9:16)'); + debugPrint('[GuidePage] ================================'); + } + + // 向导页1-5的数据 (第5页为欢迎加入页) + // 支持 png、jpg、webp 等格式 + final List _guidePages = const [ + GuidePageData( + imagePath: 'assets/images/guide_1.jpg', + title: '认种一棵榴莲树\n拥有真实RWA资产', + subtitle: '绑定真实果园20年收益,让区块链与农业完美结合', + ), + GuidePageData( + imagePath: 'assets/images/guide_2.jpg', + title: '认种即可开启算力\n自动挖矿持续收益', + subtitle: '每一棵树都对应真实资产注入,为算力提供真实价值支撑', + ), + GuidePageData( + imagePath: 'assets/images/guide_3.jpg', + title: '分享链接\n获得团队算力与收益', + subtitle: '真实认种数据透明可信 · 团队越大算力越强', + ), + GuidePageData( + imagePath: 'assets/images/guide_4.jpg', + title: 'MPC多方安全\n所有地址与收益可审计', + subtitle: '你的资产 · 安全透明 · 不可被篡改', + ), + GuidePageData( + imagePath: 'assets/images/guide_5.jpg', + title: '欢迎加入', + subtitle: '创建账号前的最后一步 · 请选择是否有推荐人', + ), + ]; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + void _onPageChanged(int page) { + setState(() { + _currentPage = page; + }); + } + + void _goToNextPage() { + if (_currentPage < 4) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + void _goToOnboarding() async { + // 标记已查看向导页 + await ref.read(authProvider.notifier).markGuideAsSeen(); + if (!mounted) return; + context.go(RoutePaths.onboarding); + } + + /// 退出应用 + void _exitApp() { + SystemNavigator.pop(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: PageView.builder( + controller: _pageController, + onPageChanged: _onPageChanged, + itemCount: 5, // 4个介绍页 + 1个欢迎加入页 + itemBuilder: (context, index) { + if (index < 4) { + return _buildGuidePage(_guidePages[index], index); + } else { + return _buildWelcomePage(); + } + }, + ), + ); + } + + /// 构建向导页 (页面1-4) - 全屏背景图片,无文字 + /// 使用 BoxFit.cover 填满屏幕,保持宽高比(会裁剪超出部分) + Widget _buildGuidePage(GuidePageData data, int index) { + debugPrint('[GuidePage] _buildGuidePage() - 页面 ${index + 1}, 图片: ${data.imagePath}'); + return Stack( + fit: StackFit.expand, + children: [ + // 全屏背景图片 - 使用 cover 填满屏幕 + if (data.imagePath != null) + Image.asset( + data.imagePath!, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + debugPrint('[GuidePage] 页面 ${index + 1} 图片加载失败: $error'); + return Container( + color: const Color(0xFFFFF8E7), + child: _buildPlaceholderImage(index), + ); + }, + ) + else + Container( + color: const Color(0xFFFFF8E7), + child: _buildPlaceholderImage(index), + ), + // 底部页面指示器 - 使用 SafeArea 避免被系统 UI 遮挡 + Positioned( + left: 0, + right: 0, + bottom: 0, + child: SafeArea( + child: Padding( + padding: EdgeInsets.only(bottom: 40.h), + child: _buildPageIndicator(), + ), + ), + ), + ], + ); + } + + /// 构建占位图片 + Widget _buildPlaceholderImage(int index) { + final icons = [ + Icons.nature, + Icons.memory, + Icons.people, + Icons.security, + ]; + final colors = [ + const Color(0xFF8BC34A), + const Color(0xFFD4AF37), + const Color(0xFFFF9800), + const Color(0xFF2196F3), + ]; + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icons[index], + size: 80.sp, + color: colors[index], + ), + SizedBox(height: 16.h), + Text( + '向导页 ${index + 1}', + style: TextStyle( + fontSize: 16.sp, + color: const Color(0xFF57534E), + ), + ), + ], + ), + ); + } + + /// 构建欢迎加入页面 (页面5) + Widget _buildWelcomePage() { + return _WelcomePageContent( + onNext: _goToOnboarding, + onExit: _exitApp, // 退出整个应用 + backgroundImage: _guidePages[4].imagePath, + pageIndicator: _buildPageIndicator(), + ); + } + + /// 构建页面指示器 (所有页面都使用白色系,因为都有背景图) + Widget _buildPageIndicator() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(5, (index) { + final isActive = index == _currentPage; + return Container( + width: 8.w, + height: 8.w, + margin: EdgeInsets.symmetric(horizontal: 4.w), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isActive + ? Colors.white + : Colors.white.withValues(alpha: 0.4), + ), + ); + }), + ); + } +} + +/// 欢迎加入页面内容 (第5页) +class _WelcomePageContent extends ConsumerStatefulWidget { + final VoidCallback onNext; + final VoidCallback onExit; + final String? backgroundImage; + final Widget pageIndicator; + + const _WelcomePageContent({ + required this.onNext, + required this.onExit, + this.backgroundImage, + required this.pageIndicator, + }); + + @override + ConsumerState<_WelcomePageContent> createState() => _WelcomePageContentState(); +} + +class _WelcomePageContentState extends ConsumerState<_WelcomePageContent> { + bool _hasReferrer = true; + final TextEditingController _referralCodeController = TextEditingController(); + + @override + void initState() { + super.initState(); + // 监听输入框变化以更新按钮状态 + _referralCodeController.addListener(_onReferralCodeChanged); + } + + @override + void dispose() { + _referralCodeController.removeListener(_onReferralCodeChanged); + _referralCodeController.dispose(); + super.dispose(); + } + + /// 输入框内容变化时刷新UI + void _onReferralCodeChanged() { + setState(() {}); + } + + /// 验证推荐码是否有效 + /// 支持: 纯推荐码(至少3个字符) 或 合法URL + bool _isValidReferralCode(String code) { + final trimmed = code.trim(); + if (trimmed.isEmpty) return false; + + // 如果是URL格式,检查是否能解析 + if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { + try { + final uri = Uri.parse(trimmed); + // URL必须有路径或查询参数 + return uri.pathSegments.isNotEmpty || uri.queryParameters.isNotEmpty; + } catch (e) { + return false; + } + } + + // 纯推荐码至少3个字符 + return trimmed.length >= 3; + } + + /// 判断按钮是否可点击 + bool get _canProceed { + // 如果选择"没有推荐人",按钮可点击 + if (!_hasReferrer) return true; + + // 如果选择"有推荐人",需要填写有效的推荐码 + return _isValidReferralCode(_referralCodeController.text); + } + + /// 导入助记词恢复账号 + void _importMnemonic() { + debugPrint('[GuidePage] _importMnemonic - 跳转到导入助记词页面'); + context.push(RoutePaths.importMnemonic); + } + + /// 保存推荐码并继续下一步 + Future _saveReferralCodeAndProceed() async { + if (!_canProceed) return; + + // 如果有推荐人且推荐码有效,保存到本地存储 + if (_hasReferrer && _referralCodeController.text.trim().isNotEmpty) { + final secureStorage = ref.read(secureStorageProvider); + await secureStorage.write( + key: StorageKeys.inviterReferralCode, + value: _referralCodeController.text.trim(), + ); + debugPrint('[GuidePage] 保存邀请人推荐码: ${_referralCodeController.text.trim()}'); + } + + // 调用下一步回调 + widget.onNext(); + } + + /// 打开二维码扫描页面 + Future _openQrScanner() async { + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const _QrScannerPage(), + ), + ); + if (result != null && result.isNotEmpty) { + // 从扫描结果中提取推荐码 + final referralCode = _extractReferralCode(result); + setState(() { + _referralCodeController.text = referralCode; + _hasReferrer = true; + }); + } + } + + /// 从扫描结果中提取推荐码 + /// 支持以下格式: + /// - 查询参数: https://app.rwadurian.com/share?ref=ABC123 -> ABC123 + /// - 路径格式: https://app.rwadurian.com/r/ABC123 -> ABC123 + /// - 短链: https://rwa.link/ABC123 -> ABC123 + /// - 纯推荐码: ABC123 -> ABC123 + String _extractReferralCode(String scannedData) { + // 去除首尾空格 + String data = scannedData.trim(); + + // 如果是 URL,尝试提取推荐码 + if (data.startsWith('http://') || data.startsWith('https://')) { + try { + final uri = Uri.parse(data); + + // 优先从查询参数中提取 (如 ?ref=ABC123 或 ?code=ABC123) + final refCode = uri.queryParameters['ref'] ?? + uri.queryParameters['code'] ?? + uri.queryParameters['referral']; + if (refCode != null && refCode.isNotEmpty) { + return refCode; + } + + // 其次从路径中提取 (如 /r/ABC123 或 /ABC123) + final pathSegments = uri.pathSegments; + if (pathSegments.isNotEmpty) { + // 取最后一个路径段作为推荐码 + final lastSegment = pathSegments.last; + // 排除常见的非推荐码路径段 + const excludedSegments = ['share', 'invite', 'r', 'ref', 'referral']; + if (lastSegment.isNotEmpty && !excludedSegments.contains(lastSegment.toLowerCase())) { + return lastSegment; + } + // 如果最后一段是排除项且有倒数第二段,检查倒数第二段 + if (pathSegments.length >= 2) { + final secondLastSegment = pathSegments[pathSegments.length - 2]; + if (secondLastSegment.isNotEmpty && !excludedSegments.contains(secondLastSegment.toLowerCase())) { + return secondLastSegment; + } + } + } + } catch (e) { + // URL 解析失败,返回原始数据 + debugPrint('URL 解析失败: $e'); + } + } + + // 不是 URL 或解析失败,返回原始数据 + return data; + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Stack( + fit: StackFit.expand, + children: [ + // 全屏背景图片 - 使用 cover 填满屏幕 + if (widget.backgroundImage != null) + Image.asset( + widget.backgroundImage!, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Container( + color: const Color(0xFFFFF8E7), + ); + }, + ) + else + Container( + color: const Color(0xFFFFF8E7), + ), + // 半透明遮罩,让内容更清晰 + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.3), + Colors.black.withValues(alpha: 0.6), + ], + ), + ), + ), + // 内容区域 + SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + children: [ + // 退出按钮 + Align( + alignment: Alignment.topRight, + child: Padding( + padding: EdgeInsets.only(top: 16.h), + child: GestureDetector( + onTap: widget.onExit, + child: Text( + '退出', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withValues(alpha: 0.8), + ), + ), + ), + ), + ), + SizedBox(height: 60.h), + // 欢迎标题 + Text( + '欢迎加入', + style: TextStyle( + fontSize: 28.sp, + fontWeight: FontWeight.w700, + height: 1.33, + color: Colors.white, + shadows: [ + Shadow( + color: Colors.black.withValues(alpha: 0.5), + blurRadius: 4, + ), + ], + ), + ), + SizedBox(height: 12.h), + // 副标题 + Text( + '创建账号前的最后一步 · 请选择是否有推荐人', + style: TextStyle( + fontSize: 14.sp, + height: 1.43, + color: Colors.white.withValues(alpha: 0.9), + shadows: [ + Shadow( + color: Colors.black.withValues(alpha: 0.5), + blurRadius: 4, + ), + ], + ), + ), + SizedBox(height: 48.h), + // 选项区域 (透明背景) + _buildReferrerOptions(), + const Spacer(), + // 页面指示器 + widget.pageIndicator, + SizedBox(height: 24.h), + // 下一步按钮 + GestureDetector( + onTap: _canProceed ? _saveReferralCodeAndProceed : null, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 16.h), + decoration: BoxDecoration( + color: _canProceed + ? const Color(0xFFD4A84B) + : Colors.white.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + '下一步 (创建账号)', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + height: 1.5, + color: _canProceed + ? Colors.white + : Colors.white.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + ), + ), + SizedBox(height: 32.h), + ], + ), + ), + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + /// 构建推荐人选项 (透明背景,白色文字) + Widget _buildReferrerOptions() { + return Column( + children: [ + // 有推荐人选项 + GestureDetector( + onTap: () { + setState(() { + _hasReferrer = true; + }); + }, + child: Row( + children: [ + _buildRadio(_hasReferrer), + SizedBox(width: 12.w), + Text( + '我有推荐人', + style: TextStyle( + fontSize: 16.sp, + height: 1.5, + color: Colors.white, + shadows: [ + Shadow( + color: Colors.black.withValues(alpha: 0.5), + blurRadius: 4, + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 14.h), + // 推荐码输入框 - 使用 AnimatedSize 平滑过渡 + AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: _hasReferrer + ? Container( + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 1, + color: Colors.white.withValues(alpha: 0.5), + ), + ), + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: _openQrScanner, + child: AbsorbPointer( + child: TextField( + controller: _referralCodeController, + readOnly: true, + decoration: InputDecoration( + hintText: '点击扫描推荐码', + hintStyle: TextStyle( + fontSize: 16.sp, + color: Colors.black.withValues(alpha: 0.4), + ), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + style: TextStyle( + fontSize: 16.sp, + color: Colors.black, + ), + ), + ), + ), + ), + // 扫码按钮 + GestureDetector( + onTap: _openQrScanner, + child: Padding( + padding: EdgeInsets.only(left: 8.w), + child: Icon( + Icons.camera_alt_outlined, + size: 20.sp, + color: Colors.white.withValues(alpha: 0.8), + ), + ), + ), + ], + ), + ) + : const SizedBox.shrink(), + ), + SizedBox(height: 24.h), + // 没有推荐人选项 + GestureDetector( + onTap: () { + setState(() { + _hasReferrer = false; + }); + }, + child: Row( + children: [ + _buildRadio(!_hasReferrer), + SizedBox(width: 12.w), + Text( + '我没有推荐人', + style: TextStyle( + fontSize: 16.sp, + height: 1.5, + color: Colors.white, + shadows: [ + Shadow( + color: Colors.black.withValues(alpha: 0.5), + blurRadius: 4, + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 32.h), + // 分隔线 + Row( + children: [ + Expanded( + child: Container( + height: 1, + color: Colors.white.withValues(alpha: 0.3), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Text( + '或', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withValues(alpha: 0.7), + ), + ), + ), + Expanded( + child: Container( + height: 1, + color: Colors.white.withValues(alpha: 0.3), + ), + ), + ], + ), + SizedBox(height: 24.h), + // 导入助记词入口 + GestureDetector( + onTap: _importMnemonic, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 14.h), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: Colors.white.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.restore, + size: 20.sp, + color: Colors.white, + ), + SizedBox(width: 8.w), + Text( + '已有账号?导入助记词恢复', + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ], + ); + } + + /// 构建单选按钮 (白色系,适配深色背景) + Widget _buildRadio(bool isSelected) { + return Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: isSelected ? 6.w : 2.w, + color: isSelected + ? Colors.white + : Colors.white.withValues(alpha: 0.5), + ), + ), + ); + } +} + +/// 二维码扫描页面 +class _QrScannerPage extends StatefulWidget { + const _QrScannerPage(); + + @override + State<_QrScannerPage> createState() => _QrScannerPageState(); +} + +class _QrScannerPageState extends State<_QrScannerPage> { + MobileScannerController? _controller; + bool _hasScanned = false; + bool _torchOn = false; + bool _isProcessingImage = false; + + @override + void initState() { + super.initState(); + _controller = MobileScannerController( + detectionSpeed: DetectionSpeed.normal, + facing: CameraFacing.back, + ); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + void _onDetect(BarcodeCapture capture) { + if (_hasScanned) return; + + final List barcodes = capture.barcodes; + for (final barcode in barcodes) { + if (barcode.rawValue != null && barcode.rawValue!.isNotEmpty) { + _hasScanned = true; + Navigator.of(context).pop(barcode.rawValue); + return; + } + } + } + + Future _toggleTorch() async { + await _controller?.toggleTorch(); + setState(() { + _torchOn = !_torchOn; + }); + } + + /// 从相册选择图片并扫描二维码 + Future _pickImageAndScan() async { + if (_isProcessingImage || _hasScanned) return; + + setState(() { + _isProcessingImage = true; + }); + + try { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + + if (image == null) { + setState(() { + _isProcessingImage = false; + }); + return; + } + + // 使用 MobileScannerController 分析图片 + final BarcodeCapture? result = await _controller?.analyzeImage(image.path); + + if (result != null && result.barcodes.isNotEmpty) { + for (final barcode in result.barcodes) { + if (barcode.rawValue != null && barcode.rawValue!.isNotEmpty) { + _hasScanned = true; + if (mounted) { + Navigator.of(context).pop(barcode.rawValue); + } + return; + } + } + } + + // 未识别到二维码 + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '未能识别图片中的二维码,请重新选择', + style: TextStyle(fontSize: 14.sp), + ), + backgroundColor: const Color(0xFF6F6354), + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.all(16.w), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), + ), + ); + } + } catch (e) { + debugPrint('扫描图片失败: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '图片扫描失败,请重试', + style: TextStyle(fontSize: 14.sp), + ), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + margin: EdgeInsets.all(16.w), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isProcessingImage = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text( + '扫描推荐码', + style: TextStyle( + fontSize: 18.sp, + color: Colors.white, + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: Icon( + _torchOn ? Icons.flash_on : Icons.flash_off, + color: Colors.white, + ), + onPressed: _toggleTorch, + ), + ], + ), + body: Stack( + children: [ + // 扫描区域 + MobileScanner( + controller: _controller, + onDetect: _onDetect, + ), + // 扫描框遮罩 + _buildScanOverlay(), + // 底部区域:提示文字和相册按钮 + Positioned( + bottom: 60.h, + left: 0, + right: 0, + child: Column( + children: [ + Text( + '将二维码放入框内,即可自动扫描', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white70, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 32.h), + // 相册按钮 + GestureDetector( + onTap: _isProcessingImage ? null : _pickImageAndScan, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(24.r), + border: Border.all( + color: Colors.white.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isProcessingImage) + SizedBox( + width: 18.sp, + height: 18.sp, + child: const CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + else + Icon( + Icons.photo_library_outlined, + size: 18.sp, + color: Colors.white, + ), + SizedBox(width: 8.w), + Text( + _isProcessingImage ? '识别中...' : '从相册选择', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + /// 构建扫描框遮罩 + Widget _buildScanOverlay() { + return LayoutBuilder( + builder: (context, constraints) { + final scanAreaSize = 250.w; + final left = (constraints.maxWidth - scanAreaSize) / 2; + final top = (constraints.maxHeight - scanAreaSize) / 2 - 50.h; + + return Stack( + children: [ + // 半透明背景 + ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withValues(alpha: 0.6), + BlendMode.srcOut, + ), + child: Stack( + children: [ + Container( + decoration: const BoxDecoration( + color: Colors.black, + backgroundBlendMode: BlendMode.dstOut, + ), + ), + Positioned( + left: left, + top: top, + child: Container( + width: scanAreaSize, + height: scanAreaSize, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + ), + ), + ), + ], + ), + ), + // 扫描框边角 + Positioned( + left: left, + top: top, + child: _buildCorner(true, true), + ), + Positioned( + right: left, + top: top, + child: _buildCorner(false, true), + ), + Positioned( + left: left, + bottom: constraints.maxHeight - top - scanAreaSize, + child: _buildCorner(true, false), + ), + Positioned( + right: left, + bottom: constraints.maxHeight - top - scanAreaSize, + child: _buildCorner(false, false), + ), + ], + ); + }, + ); + } + + /// 构建边角装饰 + Widget _buildCorner(bool isLeft, bool isTop) { + return SizedBox( + width: 24.w, + height: 24.w, + child: CustomPaint( + painter: _CornerPainter( + isLeft: isLeft, + isTop: isTop, + color: const Color(0xFFD4A84B), + strokeWidth: 3.w, + ), + ), + ); + } +} + +/// 边角绘制器 +class _CornerPainter extends CustomPainter { + final bool isLeft; + final bool isTop; + final Color color; + final double strokeWidth; + + _CornerPainter({ + required this.isLeft, + required this.isTop, + required this.color, + required this.strokeWidth, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + final path = Path(); + + if (isLeft && isTop) { + path.moveTo(0, size.height); + path.lineTo(0, 0); + path.lineTo(size.width, 0); + } else if (!isLeft && isTop) { + path.moveTo(0, 0); + path.lineTo(size.width, 0); + path.lineTo(size.width, size.height); + } else if (isLeft && !isTop) { + path.moveTo(0, 0); + path.lineTo(0, size.height); + path.lineTo(size.width, size.height); + } else { + path.moveTo(0, size.height); + path.lineTo(size.width, size.height); + path.lineTo(size.width, 0); + } + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +}