qr_scanner.dart 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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. _isScanning = true;
  44. final code = barcode.barcodes.first.rawValue;
  45. // 播放提示音
  46. await _audioPlayer.play(AssetSource('sounds/beep.mp3'));
  47. if (code == null) {
  48. _resetScanningState('无法识别二维码');
  49. return;
  50. }
  51. _processQRCode(code);
  52. }
  53. void _processQRCode(String code) async {
  54. // 如果设置了起始字符串,则检查是否匹配
  55. if (widget.startWithString != null &&
  56. !code.startsWith(widget.startWithString!)) {
  57. final errorMessage = widget.invalidQRMessage ?? '非法二维码';
  58. _resetScanningState(errorMessage);
  59. return;
  60. }
  61. if (widget.qrCodeParser != null) {
  62. try {
  63. final parseCode = await widget.qrCodeParser!(code);
  64. if (parseCode == null || parseCode.isEmpty) {
  65. // 解析失败,显示错误并重置状态
  66. _resetScanningState('二维码解析失败');
  67. return;
  68. }
  69. code = parseCode;
  70. } catch (e) {
  71. // 解析过程中出现异常
  72. _resetScanningState('二维码解析异常: $e');
  73. return;
  74. }
  75. }
  76. widget.onScanCallback(code);
  77. _resetScanningState();
  78. }
  79. void _resetScanningState([String? errorMessage]) {
  80. if (errorMessage != null) {
  81. ToastUtil.errorAlert(errorMessage);
  82. }
  83. Future.delayed(const Duration(seconds: 1), () {
  84. if (mounted) {
  85. _isScanning = false;
  86. }
  87. });
  88. }
  89. void _toggleTorch() async {
  90. if (_cameraController != null) {
  91. await _cameraController!.toggleTorch();
  92. setState(() {
  93. _isTorchOn = !_isTorchOn;
  94. });
  95. }
  96. }
  97. @override
  98. Widget build(BuildContext context) {
  99. const scanBoxSize = 250.0;
  100. return Scaffold(
  101. body: SafeArea(
  102. child: LayoutBuilder(
  103. builder: (context, constraints) {
  104. var cw = constraints.maxWidth;
  105. var ch = constraints.maxHeight;
  106. var w = cw / 2 - scanBoxSize / 2;
  107. var h = ch / 2 - scanBoxSize / 2;
  108. var scanRefc = Rect.fromLTWH(w, h, scanBoxSize, scanBoxSize);
  109. return Stack(
  110. children: [
  111. MobileScanner(
  112. controller: _cameraController,
  113. onDetect: _handleBarcodeScan,
  114. scanWindow: scanRefc,
  115. ),
  116. Align(
  117. alignment: Alignment.center,
  118. child: Container(
  119. width: scanBoxSize,
  120. height: scanBoxSize,
  121. decoration: BoxDecoration(
  122. border: Border(
  123. left: BorderSide(color: Colors.white, width: 2),
  124. right: BorderSide(color: Colors.white, width: 2),
  125. top: BorderSide(color: Colors.white, width: 2),
  126. bottom: BorderSide(color: Colors.white, width: 2),
  127. ),
  128. ),
  129. ),
  130. ),
  131. // 顶部黑色遮罩
  132. Positioned(
  133. top: 0,
  134. left: 0,
  135. right: 0,
  136. bottom: h + scanBoxSize,
  137. child: Container(color: Colors.black.withValues(alpha: 0.7)),
  138. ),
  139. // 底部黑色遮罩
  140. Positioned(
  141. top: h + scanBoxSize,
  142. left: 0,
  143. right: 0,
  144. bottom: 0,
  145. child: Container(color: Colors.black.withValues(alpha: 0.7)),
  146. ),
  147. // 左侧黑色遮罩
  148. Positioned(
  149. top: h,
  150. left: 0,
  151. width: w,
  152. height: scanBoxSize,
  153. child: Container(color: Colors.black.withValues(alpha: 0.7)),
  154. ),
  155. // 右侧黑色遮罩
  156. Positioned(
  157. top: h,
  158. right: 0,
  159. width: w,
  160. height: scanBoxSize,
  161. child: Container(color: Colors.black.withValues(alpha: 0.7)),
  162. ),
  163. //底部手电筒按钮
  164. Positioned(
  165. top: h + scanBoxSize + 40,
  166. left: 0,
  167. right: 0,
  168. child: Center(
  169. child: IconButton(
  170. icon: Icon(
  171. _isTorchOn ? Icons.flashlight_on : Icons.flashlight_off,
  172. color: Colors.white,
  173. size: 36,
  174. ),
  175. onPressed: _toggleTorch,
  176. ),
  177. ),
  178. ),
  179. ],
  180. );
  181. },
  182. ),
  183. ),
  184. );
  185. }
  186. }