278 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			278 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Dart
		
	
	
| import 'dart:convert';
 | ||
| import 'dart:io';
 | ||
| import 'dart:math' as math;
 | ||
| 
 | ||
| import 'package:flutter/material.dart';
 | ||
| import 'package:mobile_scanner/mobile_scanner.dart';
 | ||
| import 'package:qhd_prevention/customWidget/toast_util.dart';
 | ||
| import 'package:qhd_prevention/pages/home/study/face_ecognition_page.dart';
 | ||
| import 'package:qhd_prevention/pages/home/work/risk_list_page.dart';
 | ||
| import 'package:qhd_prevention/pages/my_appbar.dart';
 | ||
| import 'package:image_picker/image_picker.dart';
 | ||
| import 'package:qhd_prevention/tools/tools.dart';
 | ||
| 
 | ||
| class ScanPage extends StatefulWidget {
 | ||
|   // const ScanPage({Key? key}) : super(key: key,);
 | ||
|   const ScanPage({super.key, required this.totalList});
 | ||
|   final List totalList;
 | ||
|   @override
 | ||
|   State<ScanPage> createState() => _ScanPageState();
 | ||
| }
 | ||
| 
 | ||
| class _ScanPageState extends State<ScanPage> {
 | ||
|   final MobileScannerController _controller = MobileScannerController();
 | ||
|   bool _torchOn = false;
 | ||
| 
 | ||
|   @override
 | ||
|   void initState() {
 | ||
|     super.initState();
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   void dispose() {
 | ||
|     _controller.dispose();
 | ||
|     super.dispose();
 | ||
|   }
 | ||
| 
 | ||
|   Future<void> _scanFromGallery() async {
 | ||
|     final picker = ImagePicker();
 | ||
|     final XFile? image = await picker.pickImage(source: ImageSource.gallery);
 | ||
|     if (image == null) return;
 | ||
| 
 | ||
|     try {
 | ||
|       // ★ 新版:返回 BarcodeCapture?
 | ||
|       final capture = await _controller.analyzeImage(image.path);
 | ||
|       if (capture != null && capture.barcodes.isNotEmpty) {
 | ||
|         final code = capture.barcodes.first.rawValue ?? '';
 | ||
|         _showResult(code);
 | ||
|       } else {
 | ||
|         _showResult('未识别到二维码/条码');
 | ||
|       }
 | ||
|     } catch (e) {
 | ||
|       _showResult('扫描失败:$e');
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   void _showResult(String result) {
 | ||
|     try {
 | ||
|       if (result.contains('STUDENT_ID')) {
 | ||
|         final Map<String, dynamic> stuInfo = jsonDecode(result);
 | ||
|         print('stuInfo: $stuInfo');
 | ||
|         // 兼容性提取:res.result.split("@")[1]
 | ||
|         String? stuId;
 | ||
|         final parts = result.split('@');
 | ||
|         if (parts.length > 1) stuId = parts[1];
 | ||
| 
 | ||
|         // userId = res.result.substring(0, res.result.indexOf('%_face'))
 | ||
|         String? userId;
 | ||
|         final idx = result.indexOf('%_face');
 | ||
|         if (idx >= 0) {
 | ||
|           userId = result.substring(0, idx);
 | ||
|         }
 | ||
| 
 | ||
|         print('stuId: $stuId, userId: $userId');
 | ||
| 
 | ||
|         // 比较登录用户 id 与解析到的 stuInfo.USER_ID
 | ||
|         if (SessionService.instance.loginUserId == stuInfo['USER_ID']) {
 | ||
|           goToFace(stuInfo);
 | ||
|         } else {
 | ||
|           ToastUtil.showNormal(context, '当前登录账号不匹配,无法扫码学习,请切换至正确的账号后再尝试人脸识别!');
 | ||
| 
 | ||
|           return;
 | ||
|         }
 | ||
|       } else {
 | ||
|         // 不是 STUDENT_ID 的情况:按列表 id 匹配
 | ||
|         Navigator.pop(context, result);
 | ||
|         // bool found = false;
 | ||
|         // final listId = result;
 | ||
|         // for (final item in widget.totalList) {
 | ||
|         //   if (item['LISTMANAGER_ID'] == listId) {
 | ||
|         //     found = true;
 | ||
|         //     goToList(listId: item['LISTMANAGER_ID'], listName: item['NAME']);
 | ||
|         //     break;
 | ||
|         //   }
 | ||
|         // }
 | ||
|         // if (!found) {
 | ||
|         //   ToastUtil.showError(context, '无法检查该清单');
 | ||
|         // }
 | ||
|       }
 | ||
|     } catch (e, st) {
 | ||
|       // 捕获解析或运行时错误
 | ||
|       print('handleScanResult error: $e\n$st');
 | ||
|       ScaffoldMessenger.of(context).showSnackBar(
 | ||
|         SnackBar(content: Text('扫码处理失败: ${e.toString()}')),
 | ||
|       );
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // 人脸识别跳转
 | ||
|   void goToFace(Map<String, dynamic> stuInfo) async {
 | ||
|     print('navigate to face with $stuInfo');
 | ||
|     final passed = await pushPage<bool>(
 | ||
|       FaceRecognitionPage(studentId: stuInfo['STUDENT_ID'],
 | ||
|           VIDEOCOURSEWARE_ID: stuInfo['VIDEOCOURSEWARE_ID'],CURRICULUM_ID: stuInfo['CURRICULUM_ID'],
 | ||
|           CHAPTER_ID: stuInfo['CHAPTER_ID'],CLASS_ID: stuInfo['CLASS_ID'],
 | ||
|           mode: FaceMode.auto),
 | ||
|       context,
 | ||
|     );
 | ||
|     if (passed == true) {
 | ||
|       ToastUtil.showSuccess(context, '验证成功');
 | ||
|       Navigator.pop(context);
 | ||
|     } else {
 | ||
|       ToastUtil.showError(context, '验证失败');
 | ||
|       Navigator.pop(context);
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // 跳转到清单页面
 | ||
|   void goToList({required String listId, required String listName}) {
 | ||
|     print('navigate to list: $listId, name: $listName');
 | ||
|     Navigator.pop(context,Animation);
 | ||
|     pushPage(RiskListPage(1, listId,""), context);
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   Widget build(BuildContext context) {
 | ||
|     // 中心扫描框大小
 | ||
|     const double scanSize = 250;
 | ||
|     final Size screen = MediaQuery.of(context).size;
 | ||
|     final double left = (screen.width - scanSize) / 2;
 | ||
|     final double top = (screen.height - scanSize) / 3 - kToolbarHeight;
 | ||
|     // 因为 SafeArea + AppBar 占了高度,所以减去 toolbar 高度
 | ||
|     const double cornerSize = 20.0; // 角标正方形区域大小
 | ||
|     const double strokeWidth = 4.0; // 边线宽度
 | ||
|     return Scaffold(
 | ||
|       appBar: MyAppbar(
 | ||
|         title: "二维码/条码扫描",
 | ||
|         actions: [
 | ||
|           TextButton(
 | ||
|             onPressed: _scanFromGallery,
 | ||
|             child: const Text(
 | ||
|               "相册",
 | ||
|               style: TextStyle(color: Colors.white, fontSize: 16),
 | ||
|             ),
 | ||
|           ),
 | ||
|         ],
 | ||
|       ),
 | ||
|       body: Stack(
 | ||
|         children: [
 | ||
|           // 1. 摄像头预览
 | ||
|           MobileScanner(
 | ||
|             controller: _controller,
 | ||
|             onDetect: (capture) {
 | ||
|               for (final barcode in capture.barcodes) {
 | ||
|                 final code = barcode.rawValue;
 | ||
|                 if (code != null && mounted) {
 | ||
|                   _controller.stop();
 | ||
|                   _showResult(code);
 | ||
|                   break;
 | ||
|                 }
 | ||
|               }
 | ||
|             },
 | ||
|           ),
 | ||
| 
 | ||
|           // 2. 半透明遮罩
 | ||
|           // 顶部
 | ||
|           // 1. 顶部遮罩
 | ||
|           Positioned(
 | ||
|             left: 0, right: 0, top: 0,
 | ||
|             height: top,                  // 从顶到底部到扫描框上边缘
 | ||
|             child: Container(color: Colors.black54),
 | ||
|           ),
 | ||
| 
 | ||
|           // 2. 底部遮罩
 | ||
|           Positioned(
 | ||
|             left: 0, right: 0,
 | ||
|             top: top + scanSize,          // 从扫描框下边缘开始
 | ||
|             bottom: 0,
 | ||
|             child: Container(color: Colors.black54),
 | ||
|           ),
 | ||
| 
 | ||
|           // 3. 左侧遮罩
 | ||
|           Positioned(
 | ||
|             left: 0,
 | ||
|             top: top,
 | ||
|             width: left,                  // 从屏幕左侧到扫描框左边缘
 | ||
|             height: scanSize,             // 和扫描框一样高
 | ||
|             child: Container(color: Colors.black54),
 | ||
|           ),
 | ||
| 
 | ||
|           // 4. 右侧遮罩
 | ||
|           Positioned(
 | ||
|             left: left + scanSize,
 | ||
|             top: top,
 | ||
|             right: 0,
 | ||
|             height: scanSize,             // 和扫描框一样高
 | ||
|             child: Container(color: Colors.black54),
 | ||
|           ),
 | ||
| 
 | ||
|           // 3. 扫描框四个角
 | ||
|           // 左上
 | ||
|           Positioned(
 | ||
|             left: left,
 | ||
|             top: top,
 | ||
|             child: _corner(size: cornerSize, stroke: strokeWidth),
 | ||
|           ),
 | ||
|           // 右上
 | ||
|           Positioned(
 | ||
|             left: left + scanSize - cornerSize,
 | ||
|             top: top,
 | ||
|             child: _corner(size: cornerSize, stroke: strokeWidth, rotation: 1),
 | ||
|           ),
 | ||
|           // 左下
 | ||
|           Positioned(
 | ||
|             left: left,
 | ||
|             top: top + scanSize - cornerSize,
 | ||
|             child: _corner(size: cornerSize, stroke: strokeWidth, rotation: 3),
 | ||
|           ),
 | ||
|           // 右下
 | ||
|           Positioned(
 | ||
|             left: left + scanSize - cornerSize,
 | ||
|             top: top + scanSize - cornerSize,
 | ||
|             child: _corner(size: cornerSize, stroke: strokeWidth, rotation: 2),
 | ||
|           ),
 | ||
| 
 | ||
|           // 闪光灯按钮
 | ||
|           Positioned(
 | ||
|             left: (screen.width - 40) / 2,
 | ||
|             top: top + scanSize - 60,
 | ||
|             child: IconButton(
 | ||
|               iconSize: 32,
 | ||
|               color: Colors.white,
 | ||
|               icon: Icon(_torchOn ? Icons.flashlight_off_outlined : Icons.flashlight_on_outlined),
 | ||
|               onPressed: () {
 | ||
|                 _controller.toggleTorch();
 | ||
|                 setState(() {
 | ||
|                 _torchOn = !_torchOn;
 | ||
|                 });
 | ||
|               },
 | ||
|             ),
 | ||
|           ),
 | ||
|         ],
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   /// 角装饰:一个 L 形的蓝色粗边
 | ||
|   Widget _corner({
 | ||
|     double size = 20,
 | ||
|     double stroke = 4,
 | ||
|     int rotation = 0, // 0=左上, 1=右上, 2=右下, 3=左下
 | ||
|   }) {
 | ||
|     return Transform.rotate(
 | ||
|       angle: rotation * math.pi / 2,
 | ||
|       child: Container(
 | ||
|         width: size,
 | ||
|         height: size,
 | ||
|         decoration: BoxDecoration(
 | ||
|           border: Border(
 | ||
|             top: BorderSide(color: Colors.blue, width: stroke),
 | ||
|             left: BorderSide(color: Colors.blue, width: stroke),
 | ||
|           ),
 | ||
|         ),
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
| }
 |