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 { /// 初始页面索引(0-4,默认为0) final int initialPage; const GuidePage({super.key, this.initialPage = 0}); @override ConsumerState createState() => _GuidePageState(); } class _GuidePageState extends ConsumerState { late final PageController _pageController; late int _currentPage; @override void initState() { super.initState(); // 使用传入的初始页面索引 _currentPage = widget.initialPage.clamp(0, 4); _pageController = PageController(initialPage: _currentPage); // 延迟到 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; }