import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; /// 二维码扫描底部弹窗 /// 扫描成功后自动关闭并返回扫描结果 class QrScannerSheet extends StatefulWidget { const QrScannerSheet({super.key}); /// 显示扫码底部弹窗 /// 返回扫描到的二维码内容,取消或失败返回 null static Future show(BuildContext context) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => const QrScannerSheet(), ); } @override State createState() => _QrScannerSheetState(); } class _QrScannerSheetState extends State { static const Color _orange = Color(0xFFFF6B00); static const Color _darkText = Color(0xFF1F2937); final MobileScannerController _controller = MobileScannerController( detectionSpeed: DetectionSpeed.normal, facing: CameraFacing.back, ); bool _hasScanned = false; @override void dispose() { _controller.dispose(); super.dispose(); } void _onDetect(BarcodeCapture capture) { if (_hasScanned) return; final List barcodes = capture.barcodes; if (barcodes.isEmpty) return; final String? code = barcodes.first.rawValue; if (code == null || code.isEmpty) return; _hasScanned = true; Navigator.of(context).pop(code); } @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; return Container( height: screenHeight * 0.7, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), ), child: Column( children: [ // 顶部拖动条 Container( margin: const EdgeInsets.only(top: 12), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), // 标题栏 Padding( padding: const EdgeInsets.all(16), child: Row( children: [ IconButton( icon: const Icon(Icons.close, color: _darkText), onPressed: () => Navigator.of(context).pop(), ), const Expanded( child: Text( '扫描二维码', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: _darkText, ), ), ), // 闪光灯按钮 IconButton( icon: ValueListenableBuilder( valueListenable: _controller, builder: (context, state, child) { return Icon( state.torchState == TorchState.on ? Icons.flash_on : Icons.flash_off, color: state.torchState == TorchState.on ? _orange : _darkText, ); }, ), onPressed: () => _controller.toggleTorch(), ), ], ), ), // 扫描区域 Expanded( child: Container( margin: const EdgeInsets.symmetric(horizontal: 32), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Stack( children: [ // 相机预览 MobileScanner( controller: _controller, onDetect: _onDetect, ), // 扫描框装饰 Center( child: Container( width: 250, height: 250, decoration: BoxDecoration( border: Border.all( color: _orange.withOpacity(0.8), width: 2, ), borderRadius: BorderRadius.circular(12), ), ), ), // 四角装饰 Center( child: SizedBox( width: 250, height: 250, child: Stack( children: [ // 左上角 Positioned( left: 0, top: 0, child: _buildCorner( borderTop: true, borderLeft: true, ), ), // 右上角 Positioned( right: 0, top: 0, child: _buildCorner( borderTop: true, borderRight: true, ), ), // 左下角 Positioned( left: 0, bottom: 0, child: _buildCorner( borderBottom: true, borderLeft: true, ), ), // 右下角 Positioned( right: 0, bottom: 0, child: _buildCorner( borderBottom: true, borderRight: true, ), ), ], ), ), ), ], ), ), ), ), // 提示文字 Padding( padding: const EdgeInsets.all(24), child: Text( '将二维码放入框内,即可自动扫描', style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), ), ], ), ); } Widget _buildCorner({ bool borderTop = false, bool borderBottom = false, bool borderLeft = false, bool borderRight = false, }) { return Container( width: 24, height: 24, decoration: BoxDecoration( border: Border( top: borderTop ? const BorderSide(color: _orange, width: 4) : BorderSide.none, bottom: borderBottom ? const BorderSide(color: _orange, width: 4) : BorderSide.none, left: borderLeft ? const BorderSide(color: _orange, width: 4) : BorderSide.none, right: borderRight ? const BorderSide(color: _orange, width: 4) : BorderSide.none, ), ), ); } } /// 解析转账二维码 /// 格式: durian://transfer/?phone={phone} 或 durian://transfer?phone={phone} /// 返回手机号,解析失败返回 null String? parseTransferQrCode(String qrCode) { try { final trimmed = qrCode.trim(); // 使用正则表达式匹配,兼容有无 / 的格式 // durian://transfer/?phone=xxx 或 durian://transfer?phone=xxx final regex = RegExp(r'^durian://transfer/?[?]phone=(\d{11})$'); final match = regex.firstMatch(trimmed); if (match != null) { return match.group(1); } // 备用方案:使用 Uri.parse final uri = Uri.parse(trimmed); // 检查 scheme if (uri.scheme != 'durian') return null; // 检查 host (即 authority 部分) if (uri.host != 'transfer') return null; // 获取 phone 参数 final phone = uri.queryParameters['phone']; if (phone == null || phone.isEmpty) return null; // 简单验证手机号格式 if (phone.length != 11 || !RegExp(r'^\d{11}$').hasMatch(phone)) { return null; } return phone; } catch (e) { return null; } }