qr_scanner.dart 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import 'package:chicken_farm/core/utils/logger.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:mobile_scanner/mobile_scanner.dart';
  4. import 'package:chicken_farm/core/utils/toast.dart';
  5. import 'package:audioplayers/audioplayers.dart';
  6. typedef OnScanCallback = void Function(String scannedContent);
  7. typedef QRCodeParser = Future<String?> Function(String rawContent);
  8. class QRScannerComponent extends StatefulWidget {
  9. final String? startWithString;
  10. final OnScanCallback onScanCallback;
  11. final String? invalidQRMessage;
  12. final QRCodeParser? qrCodeParser;
  13. const QRScannerComponent({
  14. super.key,
  15. this.startWithString,
  16. required this.onScanCallback,
  17. this.invalidQRMessage,
  18. this.qrCodeParser,
  19. });
  20. @override
  21. State<QRScannerComponent> createState() => _QRScannerComponentState();
  22. }
  23. class _QRScannerComponentState extends State<QRScannerComponent> {
  24. MobileScannerController? _cameraController;
  25. bool _isScanning = false;
  26. bool _isTorchOn = false;
  27. final AudioPlayer _audioPlayer = AudioPlayer();
  28. @override
  29. void initState() {
  30. super.initState();
  31. _cameraController = MobileScannerController();
  32. }
  33. @override
  34. void dispose() {
  35. _cameraController?.dispose();
  36. _audioPlayer.dispose();
  37. super.dispose();
  38. }
  39. void _handleBarcodeScan(BarcodeCapture barcode) async {
  40. // 如果正在扫描或冷却中,则忽略
  41. logger.d('扫描二维码: $barcode');
  42. if (_isScanning) return;
  43. ToastUtil.info("正在扫描二维码...");
  44. _isScanning = true;
  45. final code = barcode.barcodes.first.rawValue;
  46. // 播放提示音
  47. await _audioPlayer.play(AssetSource('sounds/beep.mp3'));
  48. if (code == null) {
  49. _resetScanningState('无法识别二维码');
  50. return;
  51. }
  52. _processQRCode(code);
  53. }
  54. void _processQRCode(String code) async {
  55. // 如果设置了起始字符串,则检查是否匹配
  56. if (widget.startWithString != null &&
  57. !code.startsWith(widget.startWithString!)) {
  58. final errorMessage = widget.invalidQRMessage ?? '非法二维码';
  59. _resetScanningState(errorMessage);
  60. return;
  61. }
  62. if (widget.qrCodeParser != null) {
  63. try {
  64. final parseCode = await widget.qrCodeParser!(code);
  65. if (parseCode == null || parseCode.isEmpty) {
  66. // 解析失败,显示错误并重置状态
  67. _resetScanningState('二维码解析失败');
  68. return;
  69. }
  70. code = parseCode;
  71. } catch (e) {
  72. // 解析过程中出现异常
  73. _resetScanningState('二维码解析异常: $e');
  74. return;
  75. }
  76. }
  77. widget.onScanCallback(code);
  78. _resetScanningState();
  79. }
  80. void _resetScanningState([String? errorMessage]) {
  81. if (errorMessage != null) {
  82. ToastUtil.errorAlert(errorMessage);
  83. }
  84. Future.delayed(const Duration(seconds: 1), () {
  85. if (mounted) {
  86. _isScanning = false;
  87. }
  88. });
  89. }
  90. void _toggleTorch() async {
  91. if (_cameraController != null) {
  92. await _cameraController!.toggleTorch();
  93. setState(() {
  94. _isTorchOn = !_isTorchOn;
  95. });
  96. }
  97. }
  98. @override
  99. Widget build(BuildContext context) {
  100. const scanBoxSize = 250.0;
  101. return Scaffold(
  102. body: SafeArea(
  103. child: LayoutBuilder(
  104. builder: (context, constraints) {
  105. var cw = constraints.maxWidth;
  106. var ch = constraints.maxHeight;
  107. var w = cw / 2 - scanBoxSize / 2;
  108. var h = ch / 2 - scanBoxSize / 2;
  109. var scanRefc = Rect.fromLTWH(w, h, scanBoxSize, scanBoxSize);
  110. return Stack(
  111. children: [
  112. MobileScanner(
  113. controller: _cameraController,
  114. onDetect: _handleBarcodeScan,
  115. scanWindow: scanRefc,
  116. ),
  117. Align(
  118. alignment: Alignment.center,
  119. child: Container(
  120. width: scanBoxSize,
  121. height: scanBoxSize,
  122. decoration: BoxDecoration(
  123. border: Border(
  124. left: BorderSide(color: Colors.white, width: 2),
  125. right: BorderSide(color: Colors.white, width: 2),
  126. top: BorderSide(color: Colors.white, width: 2),
  127. bottom: BorderSide(color: Colors.white, width: 2),
  128. ),
  129. ),
  130. ),
  131. ),
  132. // 顶部黑色遮罩
  133. Positioned(
  134. top: 0,
  135. left: 0,
  136. right: 0,
  137. bottom: h + scanBoxSize,
  138. child: Container(color: Colors.black.withValues(alpha: 0.7)),
  139. ),
  140. // 底部黑色遮罩
  141. Positioned(
  142. top: h + scanBoxSize,
  143. left: 0,
  144. right: 0,
  145. bottom: 0,
  146. child: Container(color: Colors.black.withValues(alpha: 0.7)),
  147. ),
  148. // 左侧黑色遮罩
  149. Positioned(
  150. top: h,
  151. left: 0,
  152. width: w,
  153. height: scanBoxSize,
  154. child: Container(color: Colors.black.withValues(alpha: 0.7)),
  155. ),
  156. // 右侧黑色遮罩
  157. Positioned(
  158. top: h,
  159. right: 0,
  160. width: w,
  161. height: scanBoxSize,
  162. child: Container(color: Colors.black.withValues(alpha: 0.7)),
  163. ),
  164. //底部手电筒按钮
  165. Positioned(
  166. top: h + scanBoxSize + 40,
  167. left: 0,
  168. right: 0,
  169. child: Center(
  170. child: IconButton(
  171. icon: Icon(
  172. _isTorchOn ? Icons.flashlight_on : Icons.flashlight_off,
  173. color: Colors.white,
  174. size: 36,
  175. ),
  176. onPressed: _toggleTorch,
  177. ),
  178. ),
  179. ),
  180. ],
  181. );
  182. },
  183. ),
  184. ),
  185. );
  186. }
  187. }