Browse Source

Update 完善优化扫码点签功能

Yue 2 weeks ago
parent
commit
4ebde54874

+ 2 - 1
UI/CF.APP/chicken_farm/lib/apis/_login.dart

@@ -23,8 +23,9 @@ class LoginApi {
     return AuthResultModel.fromJson(response);
   }
 
-  Future<UserInfoModel> getInfo() async {
+  Future<UserInfoModel?> getInfo() async {
     final response = await ApiService().get('/system/user/getInfo');
+    if (response == null) return null;
     return UserInfoModel.fromJson(response);
   }
 

+ 3 - 1
UI/CF.APP/chicken_farm/lib/apis/device/_inspection_rule.dart

@@ -9,8 +9,9 @@ class InspectionRuleApi {
 
   InspectionRuleApi._internal();
 
-  Future<InspectionRuleModel> queryRule(String id) async {
+  Future<InspectionRuleModel?> queryRule(String id) async {
     final response = await ApiService().get('/device/inspectionRule/$id');
+    if (response == null) return null;
     return InspectionRuleModel.fromJson(response);
   }
 
@@ -18,6 +19,7 @@ class InspectionRuleApi {
     final response = await ApiService().get(
       '/device/inspectionRule/queryCheckinList/$id',
     );
+    if (response == null) return List.empty();
     return response
         .map<CheckinLogModel>((e) => CheckinLogModel.fromJson(e))
         .toList();

+ 3 - 1
UI/CF.APP/chicken_farm/lib/apis/experiment/_sample.dart

@@ -9,10 +9,11 @@ class SampleApi {
 
   SampleApi._internal();
 
-  Future<SampleModel> querySample(String id) async {
+  Future<SampleModel?> querySample(String id) async {
     final response = await ApiService().get(
       '/experiment/sample/querySample/$id',
     );
+    if (response == null) return null;
     return SampleModel.fromJson(response);
   }
 
@@ -20,6 +21,7 @@ class SampleApi {
     final response = await ApiService().get(
       '/experiment/sample/queryFlowLogs/$id',
     );
+    if (response == null) return [];
     return response
         .map<SampleFlowLogModel>((e) => SampleFlowLogModel.fromJson(e))
         .toList();

+ 0 - 1
UI/CF.APP/chicken_farm/lib/components/qr_scanner.dart

@@ -48,7 +48,6 @@ class _QRScannerComponentState extends State<QRScannerComponent> {
     // 如果正在扫描或冷却中,则忽略
     logger.d('扫描二维码: $barcode');
     if (_isScanning) return;
-    ToastUtil.info("正在扫描二维码...");
     _isScanning = true;
     final code = barcode.barcodes.first.rawValue;
 

+ 211 - 46
UI/CF.APP/chicken_farm/lib/components/watermarked_camera.dart

@@ -7,15 +7,18 @@ import 'package:image_picker/image_picker.dart';
 import 'package:intl/intl.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:chicken_farm/core/utils/logger.dart';
+import 'package:flutter_image_compress/flutter_image_compress.dart';
 
 class WatermarkedCamera extends StatefulWidget {
   final Function(File)? onImageCaptured;
   final String? watermarkText;
+  final String? watermarkDateText;
 
   const WatermarkedCamera({
     super.key,
     this.onImageCaptured,
     this.watermarkText,
+    this.watermarkDateText,
   });
 
   @override
@@ -27,6 +30,19 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
   File? _watermarkedImage;
   bool _isProcessing = false;
 
+  // 添加状态标记,用于判断是否已初始化拍照
+  bool _initialized = false;
+
+  @override
+  void initState() {
+    super.initState();
+    // 在初始化时自动启动拍照
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      _takePicture();
+      _initialized = true;
+    });
+  }
+
   Future<void> _takePicture() async {
     try {
       final ImagePicker picker = ImagePicker();
@@ -41,8 +57,15 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
           _isProcessing = true;
         });
 
+        // 压缩图片后再添加水印
+        final compressedImage = await _compressImage(photo);
         // 添加水印并处理图像
-        await _addWatermark(photo);
+        await _addWatermark(compressedImage);
+      } else {
+        // 如果用户取消拍照,则关闭页面
+        if (mounted && _initialized) {
+          Navigator.of(context).pop();
+        }
       }
     } catch (e) {
       if (mounted) {
@@ -51,6 +74,114 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
         ).showSnackBar(SnackBar(content: Text('拍照失败: $e')));
       }
       logger.e('拍照失败: $e');
+
+      // 出错时也关闭页面
+      if (mounted && _initialized) {
+        Navigator.of(context).pop();
+      }
+    }
+  }
+
+  /// 压缩图片以确保文件大小在500KB以内
+  Future<XFile> _compressImage(XFile imageFile) async {
+    try {
+      final tempDir = await getTemporaryDirectory();
+      final originalFile = File(imageFile.path);
+      final originalSize = await originalFile.length();
+
+      logger.i('原始图片大小: ${originalSize / 1024} KB');
+
+      // 如果原始文件已经小于500KB,直接返回
+      if (originalSize <= 500 * 1024) {
+        return imageFile;
+      }
+
+      // 初始压缩参数
+      int quality = 50;
+      int minWidth = 1024;
+      int minHeight = 768;
+
+      XFile? bestCompressedFile;
+      int bestFileSize = originalSize.toInt();
+      String bestFilePath = '';
+
+      // 迭代压缩直到文件大小符合要求
+      while (bestFileSize > 500 * 1024 && (quality >= 20 || minWidth >= 640)) {
+        final targetPath =
+            '${tempDir.path}/compressed_${DateTime.now().millisecondsSinceEpoch}_${quality}_$minWidth.jpg';
+
+        final compressedFile = await FlutterImageCompress.compressAndGetFile(
+          imageFile.path,
+          targetPath,
+          quality: quality,
+          minWidth: minWidth,
+          minHeight: minHeight,
+        );
+
+        if (compressedFile != null) {
+          final compressedFileSize = await File(compressedFile.path).length();
+          logger.i(
+            '压缩后图片大小 ($quality% 质量, ${minWidth}x$minHeight): ${compressedFileSize / 1024} KB',
+          );
+
+          if (compressedFileSize <= 500 * 1024) {
+            // 找到符合大小要求的文件
+            if (compressedFileSize < bestFileSize) {
+              bestCompressedFile = compressedFile;
+              bestFileSize = compressedFileSize.toInt();
+              bestFilePath = targetPath;
+            }
+            break;
+          } else if (compressedFileSize < bestFileSize) {
+            // 记录目前为止最小的文件
+            bestCompressedFile = compressedFile;
+            bestFileSize = compressedFileSize.toInt();
+            bestFilePath = targetPath;
+          }
+
+          // 调整参数继续尝试
+          if (quality > 20) {
+            // 优先降低质量
+            quality -= 10;
+          } else if (minWidth > 640) {
+            // 质量不能再低了,减小尺寸
+            minWidth -= 128;
+            minHeight = (minHeight * minWidth / (minWidth + 128))
+                .toInt(); // 按比例缩小
+            quality = 30; // 恢复一定质量
+          } else {
+            // 已经达到极限
+            break;
+          }
+        } else {
+          // 压缩失败,跳出循环
+          break;
+        }
+      }
+
+      // 清理其他临时文件,只保留最佳文件
+      final dir = Directory(tempDir.path);
+      await for (final file in dir.list()) {
+        if (file.path.contains('compressed_') && file.path != bestFilePath) {
+          try {
+            await file.delete();
+          } catch (e) {
+            logger.w('删除临时文件失败: $e');
+          }
+        }
+      }
+
+      if (bestCompressedFile != null) {
+        logger.i('最终图片大小: ${bestFileSize / 1024} KB');
+        return bestCompressedFile;
+      } else {
+        // 如果所有尝试都失败,返回原始文件
+        return imageFile;
+      }
+    } catch (e) {
+      logger.e('图片压缩失败: $e');
+      // 如果压缩过程中出错,返回原始文件
+      return imageFile;
     }
   }
 
@@ -58,7 +189,7 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
     try {
       // 获取水印文本
       String watermark =
-          widget.watermarkText ??
+          widget.watermarkDateText ??
           DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
 
       // 将图片转换为字节以便处理
@@ -76,33 +207,68 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
       // 绘制原始图片
       canvas.drawImage(image, Offset.zero, ui.Paint());
 
-      // 添加水印文字
-      final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
-        ui.ParagraphStyle(
-          textAlign: TextAlign.left,
-          fontSize: 24.0,
-          textDirection: ui.TextDirection.ltr,
-        ),
+      // 创建段落样式
+      final ui.ParagraphStyle paragraphStyle = ui.ParagraphStyle(
+        textAlign: TextAlign.right,
+        fontSize: 36.0,
+        textDirection: ui.TextDirection.ltr,
       );
 
-      paragraphBuilder.pushStyle(
-        ui.TextStyle(
-          color: const ui.Color(0x80FFFFFF), // 半透明白色
-          fontSize: 24.0,
-        ),
+      // 创建文字样式
+      final ui.TextStyle textStyle = ui.TextStyle(
+        color: const ui.Color(0xFFFFFFFF), // 纯白色
+        fontSize: 36.0,
+        fontWeight: ui.FontWeight.bold, // 加粗
+        shadows: [
+          ui.Shadow(
+            blurRadius: 3.0,
+            color: const ui.Color(0xFF000000), // 黑色阴影增强可读性
+            offset: const Offset(2.0, 2.0),
+          ),
+        ],
       );
 
+      double verticalOffset = image.height.toDouble() - 100;
+
+      // 如果有watermarkText,则先绘制watermarkText
+      if (widget.watermarkText != null && widget.watermarkText!.isNotEmpty) {
+        final ui.ParagraphBuilder additionalParagraphBuilder =
+            ui.ParagraphBuilder(paragraphStyle);
+        additionalParagraphBuilder.pushStyle(textStyle);
+        additionalParagraphBuilder.addText(widget.watermarkText!);
+
+        final ui.Paragraph additionalParagraph =
+            additionalParagraphBuilder.build()
+              ..layout(ui.ParagraphConstraints(width: image.width.toDouble()));
+
+        verticalOffset -= 70;
+
+        // 绘制watermarkText(在时间戳上方)
+        canvas.drawParagraph(
+          additionalParagraph,
+          Offset(
+            image.width.toDouble() - additionalParagraph.width - 90,
+            verticalOffset,
+          ),
+        );
+      }
+
+      // 绘制时间戳水印
+      final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
+        paragraphStyle,
+      );
+      paragraphBuilder.pushStyle(textStyle);
       paragraphBuilder.addText(watermark);
 
       final ui.Paragraph paragraph = paragraphBuilder.build()
         ..layout(ui.ParagraphConstraints(width: image.width.toDouble()));
 
-      // 在右下角绘制水印
+      // 在右下角绘制时间戳水印
       canvas.drawParagraph(
         paragraph,
         Offset(
-          image.width.toDouble() - paragraph.width - 20,
-          image.height.toDouble() - 40,
+          image.width.toDouble() - paragraph.width - 90,
+          verticalOffset + 60,
         ),
       );
 
@@ -113,7 +279,7 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
         image.height,
       );
 
-      // 转换为字节数据
+      // 转换为字节数据(PNG格式)
       final ByteData? byteData = await watermarkedImage.toByteData(
         format: ui.ImageByteFormat.png,
       );
@@ -126,10 +292,23 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
         );
         await file.writeAsBytes(byteData.buffer.asUint8List());
 
-        setState(() {
-          _watermarkedImage = file;
-          _isProcessing = false;
-        });
+        // 检查文件大小,如超过500KB则再压缩一次
+        final fileSize = await file.length();
+        logger.i('添加水印后图片大小: ${fileSize / 1024} KB');
+
+        if (fileSize > 500 * 1024) {
+          // 再次压缩
+          final reCompressedFile = await _compressImage(XFile(file.path));
+          setState(() {
+            _watermarkedImage = File(reCompressedFile.path);
+            _isProcessing = false;
+          });
+        } else {
+          setState(() {
+            _watermarkedImage = file;
+            _isProcessing = false;
+          });
+        }
       } else {
         setState(() {
           _isProcessing = false;
@@ -169,13 +348,16 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
       _capturedImage = null;
       _watermarkedImage = null;
     });
+
+    // 重新拍照
+    _takePicture();
   }
 
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       appBar: AppBar(
-        title: const Text('拍照签到'),
+        title: const Text('拍照'),
         leading: IconButton(
           icon: const Icon(Icons.close),
           onPressed: () => Navigator.of(context).pop(),
@@ -184,27 +366,14 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
       body: Column(
         children: [
           Expanded(
-            child: _capturedImage == null
-                ? Center(
-                    child: Column(
-                      mainAxisAlignment: MainAxisAlignment.center,
-                      children: [
-                        const Icon(
-                          Icons.camera_alt,
-                          size: 100,
-                          color: Colors.grey,
-                        ),
-                        const SizedBox(height: 20),
-                        const Text('点击按钮拍照', style: TextStyle(fontSize: 18)),
-                      ],
-                    ),
-                  )
-                : _isProcessing
+            child: _isProcessing
                 ? const Center(child: CircularProgressIndicator())
                 : Center(
                     child: _watermarkedImage != null
                         ? Image.file(_watermarkedImage!)
-                        : Image.file(File(_capturedImage!.path)),
+                        : _capturedImage != null
+                        ? Image.file(File(_capturedImage!.path))
+                        : const SizedBox(), // 避免显示默认提示信息
                   ),
           ),
           Padding(
@@ -212,12 +381,7 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
             child: Row(
               mainAxisAlignment: MainAxisAlignment.spaceEvenly,
               children: [
-                if (_capturedImage == null)
-                  FloatingActionButton(
-                    onPressed: _takePicture,
-                    child: const Icon(Icons.camera),
-                  )
-                else if (!_isProcessing)
+                if (_capturedImage != null && !_isProcessing)
                   Row(
                     mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                     children: [
@@ -229,6 +393,7 @@ class _WatermarkedCameraState extends State<WatermarkedCamera> {
                         ),
                         child: const Text('重新拍照'),
                       ),
+                      const SizedBox(width: 16.0),
                       ElevatedButton(
                         onPressed: _confirmImage,
                         style: ElevatedButton.styleFrom(

+ 4 - 4
UI/CF.APP/chicken_farm/lib/core/api/api_service.dart

@@ -147,7 +147,7 @@ class ApiService {
           return rData;
         } else if (code == 401) {
           if (apiOption.alert) {
-            ToastUtil.error(msg ?? '请先登录!');
+            ToastUtil.errorAlert(msg ?? '请先登录!');
           }
           NavigationService.navigatorKey.currentContext?.goNamed(
             AppRouteNames.login,
@@ -155,18 +155,18 @@ class ApiService {
           return null;
         } else if (code == 403) {
           if (apiOption.alert) {
-            ToastUtil.error(msg ?? '没有权限!');
+            ToastUtil.errorAlert(msg ?? '没有权限!');
           }
           return null;
         } else {
           if (apiOption.alert) {
-            ToastUtil.error(data['msg'] ?? '操作失败');
+            ToastUtil.errorAlert(data['msg'] ?? '操作失败');
           }
           return null;
         }
       } else {
         if (apiOption.alert) {
-          ToastUtil.error(response.statusMessage ?? '请求失败');
+          ToastUtil.errorAlert(response.statusMessage ?? '请求失败');
         }
         return null;
       }

+ 3 - 3
UI/CF.APP/chicken_farm/lib/core/config/app_config.dart

@@ -1,9 +1,9 @@
 import 'package:chicken_farm/core/utils/storage.dart';
 
 class AppConfig {
-  // static String baseUrl = 'http://192.168.0.81:8380'; // 默认值
-  static String baseUrl = 'http://192.168.0.109:6071/'; // 默认值
-  static String clientId = '9579f8780cf24ae2959d03d11482b18a'; // 默认值
+  static String baseUrl = 'http://192.168.0.81:8380'; // 默认值
+  // static String baseUrl = 'http://192.168.0.109:6071/'; // 默认值
+  static String clientId = '35aee70ae7224eb9a48bc527955ddedc'; // 默认值
   static const String baseUrlKey = 'base_url';
   static const String clientIdKey = 'client_id';
 

+ 2 - 2
UI/CF.APP/chicken_farm/lib/core/utils/toast.dart

@@ -58,7 +58,7 @@ class ToastUtil {
           elevation: 24,
           actions: <Widget>[
             Container(
-              padding: const EdgeInsets.only(right: 16, bottom: 16),
+              padding: const EdgeInsets.only(right: 0, bottom: 0),
               alignment: Alignment.centerRight,
               child: ElevatedButton(
                 style: ElevatedButton.styleFrom(
@@ -122,7 +122,7 @@ class ToastUtil {
           elevation: 24,
           actions: <Widget>[
             Container(
-              padding: const EdgeInsets.only(right: 16, bottom: 16),
+              padding: const EdgeInsets.only(right: 0, bottom: 0),
               alignment: Alignment.centerRight,
               child: Row(
                 mainAxisSize: MainAxisSize.min,

+ 2 - 2
UI/CF.APP/chicken_farm/lib/pages/account/login_page.dart

@@ -18,8 +18,8 @@ class LoginPage extends ConsumerStatefulWidget {
 
 class _LoginPageState extends ConsumerState<LoginPage> {
   final _formKey = GlobalKey<FormState>();
-  final _usernameCtrl = TextEditingController(text: 'admin');
-  final _passwordCtrl = TextEditingController(text: '123iwb');
+  final _usernameCtrl = TextEditingController(text: '');
+  final _passwordCtrl = TextEditingController(text: '');
 
   @override
   void dispose() {

+ 4 - 5
UI/CF.APP/chicken_farm/lib/pages/checkin/checkin_record_page.dart

@@ -62,7 +62,6 @@ class _CheckinRecordPageState extends State<CheckinRecordPage> {
       setState(() {
         _isLoading = false;
       });
-      ToastUtil.error('加载记录失败: $e');
     }
   }
 
@@ -71,11 +70,12 @@ class _CheckinRecordPageState extends State<CheckinRecordPage> {
     await Navigator.of(context).push(
       MaterialPageRoute(
         builder: (context) => WatermarkedCamera(
+          watermarkText: '${_rule?.taskName}',
           onImageCaptured: (imageFile) async {
             try {
               // 创建 FormData 对象用于文件上传
               final formData = FormData.fromMap({
-                'phone': await MultipartFile.fromFile(
+                'photo': await MultipartFile.fromFile(
                   imageFile.path,
                   filename:
                       'isCheckin_${widget.id}_${DateTime.now().millisecondsSinceEpoch}.jpg',
@@ -84,7 +84,6 @@ class _CheckinRecordPageState extends State<CheckinRecordPage> {
 
               await InspectionRuleApi().checkInWithPhoto(widget.id, formData);
               if (mounted) {
-                ToastUtil.success('签到成功');
                 _loadData(); // 刷新数据
               }
             } catch (e) {
@@ -102,8 +101,8 @@ class _CheckinRecordPageState extends State<CheckinRecordPage> {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
-      appBar: const VberAppBar(
-        title: '签到记录',
+      appBar: VberAppBar(
+        title: '${_rule?.taskName ?? ''}签到',
         showLeftButton: true,
         alwaysGoToHome: true,
       ),

+ 3 - 2
UI/CF.APP/chicken_farm/lib/pages/home/menu_buttons.dart

@@ -16,10 +16,11 @@ class MenuButtons extends ConsumerWidget {
 
     // 根据权限判断是否显示按钮
     final showCheckIn =
-        isSuperAdmin || (authState.permissions?.contains('checkin') ?? false);
+        isSuperAdmin ||
+        (authState.permissions?.contains('device:inspection:checkin') ?? false);
     final showSampleTransfer =
         isSuperAdmin ||
-        (authState.permissions?.contains('sample_transfer') ?? false);
+        (authState.permissions?.contains('experiment:sample:query') ?? false);
 
     return Column(
       mainAxisAlignment: MainAxisAlignment.center,

+ 0 - 2
UI/CF.APP/chicken_farm/lib/pages/sample/sample_detail_page.dart

@@ -3,7 +3,6 @@ import 'package:chicken_farm/components/vb_app_bar.dart';
 import 'package:chicken_farm/modes/experiment/sample/sample.dart';
 import 'package:chicken_farm/modes/experiment/sample/sample_flow_log.dart';
 import 'package:chicken_farm/apis/experiment/_sample.dart';
-import 'package:chicken_farm/core/utils/toast.dart';
 import 'package:chicken_farm/core/utils/logger.dart';
 import 'package:chicken_farm/core/utils/datetime_util.dart';
 
@@ -47,7 +46,6 @@ class _SampleDetailPageState extends State<SampleDetailPage> {
       setState(() {
         _isLoading = false;
       });
-      ToastUtil.error('加载样品详情失败: $e');
     }
   }
 

+ 9 - 0
UI/CF.APP/chicken_farm/lib/stores/auth_store.dart

@@ -95,6 +95,9 @@ class AuthStore extends StateNotifier<AuthInfo> {
         // 尝试获取用户信息以验证token有效性
         try {
           final userInfo = await apis.loginApi.getInfo();
+          if (userInfo == null) {
+            throw Exception('用户信息获取失败');
+          }
           state = AuthInfo.authenticated(
             token: token,
             user: userInfo.user!,
@@ -125,6 +128,9 @@ class AuthStore extends StateNotifier<AuthInfo> {
       await JwtToken.setToken(token, authResult.refreshToken);
       // 获取用户信息
       final userInfo = await apis.loginApi.getInfo();
+      if (userInfo == null) {
+        throw Exception('用户信息获取失败');
+      }
       state = AuthInfo.authenticated(
         token: token,
         user: userInfo.user!,
@@ -178,6 +184,9 @@ class AuthStore extends StateNotifier<AuthInfo> {
   Future<void> getUserInfo() async {
     try {
       final userInfo = await apis.loginApi.getInfo();
+      if (userInfo == null) {
+        throw Exception('用户信息获取失败');
+      }
       state = state.copyWith(
         user: userInfo.user,
         permissions: userInfo.permissions,

+ 2 - 0
UI/CF.APP/chicken_farm/macos/Flutter/GeneratedPluginRegistrant.swift

@@ -7,6 +7,7 @@ import Foundation
 
 import audioplayers_darwin
 import file_selector_macos
+import flutter_image_compress_macos
 import mobile_scanner
 import path_provider_foundation
 import shared_preferences_foundation
@@ -14,6 +15,7 @@ import shared_preferences_foundation
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
   FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
+  FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
   MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

+ 1 - 0
UI/CF.APP/chicken_farm/pubspec.yaml

@@ -55,6 +55,7 @@ dependencies:
   intl: ^0.19.0
   image_picker: ^1.2.1
   path_provider: ^2.1.5
+  flutter_image_compress: ^2.3.0
 
 dev_dependencies:
   flutter_test: