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:mobile_scanner/mobile_scanner.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; // 向导页1-4的数据 final List _guidePages = const [ GuidePageData( imagePath: 'assets/images/guide_1.png', title: '认种一棵榴莲树\n拥有真实RWA资产', subtitle: '绑定真实果园20年收益,让区块链与农业完美结合', ), GuidePageData( imagePath: 'assets/images/guide_2.png', title: '认种即可开启算力\n自动挖矿持续收益', subtitle: '每一棵树都对应真实资产注入,为算力提供真实价值支撑', ), GuidePageData( imagePath: 'assets/images/guide_3.png', title: '分享链接\n获得团队算力与收益', subtitle: '真实认种数据透明可信 · 团队越大算力越强', ), GuidePageData( imagePath: 'assets/images/guide_4.png', title: 'MPC多方安全\n所有地址与收益可审计', 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: SafeArea( child: 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) Widget _buildGuidePage(GuidePageData data, int index) { return Padding( padding: EdgeInsets.symmetric(horizontal: 24.w), child: Column( children: [ SizedBox(height: 64.h), // 图片区域 Expanded( flex: 5, child: Container( width: 312.w, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.r), color: const Color(0xFFFFF8E7), ), child: data.imagePath != null ? ClipRRect( borderRadius: BorderRadius.circular(12.r), child: Image.asset( data.imagePath!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return _buildPlaceholderImage(index); }, ), ) : _buildPlaceholderImage(index), ), ), SizedBox(height: 48.h), // 标题 Text( data.title, style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.w700, height: 1.33, color: const Color(0xFF292524), ), textAlign: TextAlign.center, ), SizedBox(height: 16.h), // 副标题 Padding( padding: EdgeInsets.symmetric(horizontal: 24.w), child: Text( data.subtitle, style: TextStyle( fontSize: 14.sp, height: 1.43, color: const Color(0xFF57534E), ), textAlign: TextAlign.center, ), ), SizedBox(height: 48.h), // 页面指示器 _buildPageIndicator(), SizedBox(height: 80.h), ], ), ); } /// 构建占位图片 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, // 退出整个应用 ); } /// 构建页面指示器 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 ? const Color(0xFF8E794A) : const Color(0xFFEAE0CD), ), ); }), ); } } /// 欢迎加入页面内容 (第5页) class _WelcomePageContent extends StatefulWidget { final VoidCallback onNext; final VoidCallback onExit; const _WelcomePageContent({ required this.onNext, required this.onExit, }); @override State<_WelcomePageContent> createState() => _WelcomePageContentState(); } class _WelcomePageContentState extends State<_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); } /// 打开二维码扫描页面 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; }); } } /// 从扫描结果中提取推荐码 /// 支持以下格式: /// - 完整 URL: 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); // 尝试从路径中提取 (如 /r/ABC123 或 /ABC123) final pathSegments = uri.pathSegments; if (pathSegments.isNotEmpty) { // 取最后一个路径段作为推荐码 final lastSegment = pathSegments.last; if (lastSegment.isNotEmpty) { return lastSegment; } // 如果最后一段是 'r',取倒数第二段 if (pathSegments.length >= 2 && lastSegment == 'r') { return pathSegments[pathSegments.length - 2]; } } // 尝试从查询参数中提取 (如 ?ref=ABC123 或 ?code=ABC123) final refCode = uri.queryParameters['ref'] ?? uri.queryParameters['code'] ?? uri.queryParameters['referral']; if (refCode != null && refCode.isNotEmpty) { return refCode; } } catch (e) { // URL 解析失败,返回原始数据 debugPrint('URL 解析失败: $e'); } } // 不是 URL 或解析失败,返回原始数据 return data; } @override Widget build(BuildContext context) { return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), 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: 32.h), child: GestureDetector( onTap: widget.onExit, child: Text( '退出 Exit', style: TextStyle( fontSize: 14.sp, color: const Color(0xFFA99F93), ), ), ), ), ), SizedBox(height: 80.h), // 欢迎标题 Text( '欢迎加入', style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.w700, height: 1.33, color: const Color(0xFF6F6354), ), ), SizedBox(height: 12.h), // 副标题 Text( '创建账号前的最后一步 · 请选择是否有推荐人', style: TextStyle( fontSize: 14.sp, height: 1.43, color: const Color(0xFFA99F93), ), ), SizedBox(height: 48.h), // 选项区域 _buildReferrerOptions(), const Spacer(), // 下一步按钮 GestureDetector( onTap: _canProceed ? widget.onNext : null, child: Container( width: double.infinity, padding: EdgeInsets.symmetric(vertical: 16.h), child: Text( '下一步 (创建账号)', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, height: 1.5, color: _canProceed ? const Color(0xFFD4A84B) // 金色 - 可点击 : const Color(0xFFCCC5B9), // 灰色 - 不可点击 ), textAlign: TextAlign.right, ), ), ), SizedBox(height: 48.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: const Color(0xFF6F6354), ), ), ], ), ), SizedBox(height: 14.h), // 推荐码输入框 if (_hasReferrer) Container( padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w), decoration: const BoxDecoration( border: Border( bottom: BorderSide( width: 1, color: Color(0xFFEAE1D2), ), ), ), child: Row( children: [ Expanded( child: TextField( controller: _referralCodeController, decoration: InputDecoration( hintText: '请输入推荐码 / 序列号', hintStyle: TextStyle( fontSize: 16.sp, color: const Color(0xFFA99F93), ), border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero, ), style: TextStyle( fontSize: 16.sp, color: const Color(0xFF6F6354), ), ), ), // 扫码按钮 GestureDetector( onTap: _openQrScanner, child: Padding( padding: EdgeInsets.only(left: 8.w), child: Icon( Icons.camera_alt_outlined, size: 20.sp, color: const Color(0xFFA99F93), ), ), ), ], ), ), 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: const Color(0xFF6F6354), ), ), ], ), ), ], ); } /// 构建单选按钮 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 ? const Color(0xFF2563EB) : const Color(0xFFA99F93), ), ), ); } } /// 二维码扫描页面 class _QrScannerPage extends StatefulWidget { const _QrScannerPage(); @override State<_QrScannerPage> createState() => _QrScannerPageState(); } class _QrScannerPageState extends State<_QrScannerPage> { MobileScannerController? _controller; bool _hasScanned = false; bool _torchOn = 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; }); } @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: 100.h, left: 0, right: 0, child: Text( '将二维码放入框内,即可自动扫描', style: TextStyle( fontSize: 14.sp, color: Colors.white70, ), textAlign: TextAlign.center, ), ), ], ), ); } /// 构建扫描框遮罩 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; }