2025-09-01 17:25:55 +08:00
|
|
|
|
import 'dart:convert';
|
2025-07-11 11:03:21 +08:00
|
|
|
|
import 'dart:io';
|
|
|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
2025-09-01 17:25:55 +08:00
|
|
|
|
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';
|
2025-07-11 11:03:21 +08:00
|
|
|
|
import 'package:qhd_prevention/pages/my_appbar.dart';
|
|
|
|
|
import 'package:image_picker/image_picker.dart';
|
2025-09-01 17:25:55 +08:00
|
|
|
|
import 'package:qhd_prevention/tools/tools.dart';
|
2025-07-11 11:03:21 +08:00
|
|
|
|
|
|
|
|
|
class ScanPage extends StatefulWidget {
|
2025-09-01 17:25:55 +08:00
|
|
|
|
// const ScanPage({Key? key}) : super(key: key,);
|
|
|
|
|
const ScanPage({super.key, required this.totalList});
|
|
|
|
|
final List totalList;
|
2025-07-11 11:03:21 +08:00
|
|
|
|
@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');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 17:25:55 +08:00
|
|
|
|
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 匹配
|
|
|
|
|
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'], mode: FaceMode.auto),
|
|
|
|
|
context,
|
|
|
|
|
);
|
|
|
|
|
if (passed == true) {
|
|
|
|
|
ToastUtil.showSuccess(context, '验证成功');
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
ToastUtil.showError(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);
|
2025-07-11 11:03:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@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),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|