Browse Source

Add 完成扫码点检签到,样品查询功能

Yue 2 weeks ago
parent
commit
ce3b4776e4
44 changed files with 1839 additions and 110 deletions
  1. 2 1
      UI/CF.APP/chicken_farm/android/app/src/main/AndroidManifest.xml
  2. BIN
      UI/CF.APP/chicken_farm/assets/sounds/beep.mp3
  3. 3 0
      UI/CF.APP/chicken_farm/devtools_options.yaml
  4. 36 0
      UI/CF.APP/chicken_farm/lib/apis/device/_inspection_rule.dart
  5. 11 0
      UI/CF.APP/chicken_farm/lib/apis/device/index.dart
  6. 27 0
      UI/CF.APP/chicken_farm/lib/apis/experiment/_sample.dart
  7. 11 0
      UI/CF.APP/chicken_farm/lib/apis/experiment/index.dart
  8. 5 1
      UI/CF.APP/chicken_farm/lib/apis/index.dart
  9. 204 0
      UI/CF.APP/chicken_farm/lib/components/qr_scanner.dart
  10. 17 13
      UI/CF.APP/chicken_farm/lib/components/vb_app_bar.dart
  11. 249 0
      UI/CF.APP/chicken_farm/lib/components/watermarked_camera.dart
  12. 11 9
      UI/CF.APP/chicken_farm/lib/core/api/api_service.dart
  13. 2 1
      UI/CF.APP/chicken_farm/lib/core/config/app_config.dart
  14. 7 0
      UI/CF.APP/chicken_farm/lib/core/services/navigation_service.dart
  15. 57 0
      UI/CF.APP/chicken_farm/lib/core/utils/datetime_util.dart
  16. 164 3
      UI/CF.APP/chicken_farm/lib/core/utils/toast.dart
  17. 33 0
      UI/CF.APP/chicken_farm/lib/modes/device/inspection_rule/checkin_log.dart
  18. 35 0
      UI/CF.APP/chicken_farm/lib/modes/device/inspection_rule/checkin_log.g.dart
  19. 29 0
      UI/CF.APP/chicken_farm/lib/modes/device/inspection_rule/inspection_rule.dart
  20. 30 0
      UI/CF.APP/chicken_farm/lib/modes/device/inspection_rule/inspection_rule.g.dart
  21. 37 0
      UI/CF.APP/chicken_farm/lib/modes/experiment/sample/sample.dart
  22. 36 0
      UI/CF.APP/chicken_farm/lib/modes/experiment/sample/sample.g.dart
  23. 25 0
      UI/CF.APP/chicken_farm/lib/modes/experiment/sample/sample_flow_log.dart
  24. 25 0
      UI/CF.APP/chicken_farm/lib/modes/experiment/sample/sample_flow_log.g.dart
  25. 5 21
      UI/CF.APP/chicken_farm/lib/pages/account/login_page.dart
  26. 44 6
      UI/CF.APP/chicken_farm/lib/pages/checkin/checkin_page.dart
  27. 39 0
      UI/CF.APP/chicken_farm/lib/pages/checkin/checkin_page_test.dart
  28. 277 0
      UI/CF.APP/chicken_farm/lib/pages/checkin/checkin_record_page.dart
  29. 4 4
      UI/CF.APP/chicken_farm/lib/pages/home/home_page.dart
  30. 3 3
      UI/CF.APP/chicken_farm/lib/pages/home/menu_buttons.dart
  31. 0 1
      UI/CF.APP/chicken_farm/lib/pages/home/profile.dart
  32. 237 0
      UI/CF.APP/chicken_farm/lib/pages/sample/sample_detail_page.dart
  33. 53 0
      UI/CF.APP/chicken_farm/lib/pages/sample/sample_query_page.dart
  34. 39 0
      UI/CF.APP/chicken_farm/lib/pages/sample/sample_query_page_test.dart
  35. 0 17
      UI/CF.APP/chicken_farm/lib/pages/sample_transfer/sample_transfer_page.dart
  36. 38 18
      UI/CF.APP/chicken_farm/lib/routes/app_routes.dart
  37. 3 6
      UI/CF.APP/chicken_farm/lib/routes/route_provider.dart
  38. 6 4
      UI/CF.APP/chicken_farm/lib/stores/auth_store.dart
  39. 8 0
      UI/CF.APP/chicken_farm/linux/flutter/generated_plugin_registrant.cc
  40. 2 0
      UI/CF.APP/chicken_farm/linux/flutter/generated_plugins.cmake
  41. 8 0
      UI/CF.APP/chicken_farm/macos/Flutter/GeneratedPluginRegistrant.swift
  42. 9 2
      UI/CF.APP/chicken_farm/pubspec.yaml
  43. 6 0
      UI/CF.APP/chicken_farm/windows/flutter/generated_plugin_registrant.cc
  44. 2 0
      UI/CF.APP/chicken_farm/windows/flutter/generated_plugins.cmake

+ 2 - 1
UI/CF.APP/chicken_farm/android/app/src/main/AndroidManifest.xml

@@ -1,4 +1,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-permission android:name="android.permission.CAMERA" />
     <application
         android:label="chicken_farm"
         android:name="${applicationName}"
@@ -42,4 +43,4 @@
             <data android:mimeType="text/plain"/>
         </intent>
     </queries>
-</manifest>
+</manifest>

BIN
UI/CF.APP/chicken_farm/assets/sounds/beep.mp3


+ 3 - 0
UI/CF.APP/chicken_farm/devtools_options.yaml

@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:

+ 36 - 0
UI/CF.APP/chicken_farm/lib/apis/device/_inspection_rule.dart

@@ -0,0 +1,36 @@
+import 'package:chicken_farm/core/api/api_service.dart';
+import 'package:chicken_farm/modes/device/inspection_rule/checkin_log.dart';
+import 'package:chicken_farm/modes/device/inspection_rule/inspection_rule.dart';
+
+class InspectionRuleApi {
+  static final InspectionRuleApi _instance = InspectionRuleApi._internal();
+
+  factory InspectionRuleApi() => _instance;
+
+  InspectionRuleApi._internal();
+
+  Future<InspectionRuleModel> queryRule(String id) async {
+    final response = await ApiService().get('/device/inspectionRule/$id');
+    return InspectionRuleModel.fromJson(response);
+  }
+
+  Future<List<CheckinLogModel>> queryCheckinList(String id) async {
+    final response = await ApiService().get(
+      '/device/inspectionRule/queryCheckinList/$id',
+    );
+    return response
+        .map<CheckinLogModel>((e) => CheckinLogModel.fromJson(e))
+        .toList();
+  }
+
+  Future<dynamic> checkIn(String id) async {
+    return await ApiService().post('/device/inspectionRule/checkIn/$id');
+  }
+
+  Future<dynamic> checkInWithPhoto(String id, dynamic data) async {
+    return await ApiService().post(
+      '/device/inspectionRule/checkInWithPhoto/$id',
+      data: data,
+    );
+  }
+}

+ 11 - 0
UI/CF.APP/chicken_farm/lib/apis/device/index.dart

@@ -0,0 +1,11 @@
+import '_inspection_rule.dart';
+
+class DeviceApis {
+  static final DeviceApis _instance = DeviceApis._internal();
+
+  factory DeviceApis() => _instance;
+
+  DeviceApis._internal();
+
+  late final InspectionRuleApi inspectionRuleApi = InspectionRuleApi();
+}

+ 27 - 0
UI/CF.APP/chicken_farm/lib/apis/experiment/_sample.dart

@@ -0,0 +1,27 @@
+import 'package:chicken_farm/core/api/api_service.dart';
+import 'package:chicken_farm/modes/experiment/sample/sample.dart';
+import 'package:chicken_farm/modes/experiment/sample/sample_flow_log.dart';
+
+class SampleApi {
+  static final SampleApi _instance = SampleApi._internal();
+
+  factory SampleApi() => _instance;
+
+  SampleApi._internal();
+
+  Future<SampleModel> querySample(String id) async {
+    final response = await ApiService().get(
+      '/experiment/sample/querySample/$id',
+    );
+    return SampleModel.fromJson(response);
+  }
+
+  Future<List<SampleFlowLogModel>> queryFlowLogs(String id) async {
+    final response = await ApiService().get(
+      '/experiment/sample/queryFlowLogs/$id',
+    );
+    return response
+        .map<SampleFlowLogModel>((e) => SampleFlowLogModel.fromJson(e))
+        .toList();
+  }
+}

+ 11 - 0
UI/CF.APP/chicken_farm/lib/apis/experiment/index.dart

@@ -0,0 +1,11 @@
+import '_sample.dart';
+
+class ExperimentApis {
+  static final ExperimentApis _instance = ExperimentApis._internal();
+
+  factory ExperimentApis() => _instance;
+
+  ExperimentApis._internal();
+
+  late final SampleApi sampleApi = SampleApi();
+}

+ 5 - 1
UI/CF.APP/chicken_farm/lib/apis/index.dart

@@ -1,5 +1,7 @@
 import 'package:chicken_farm/apis/_login.dart';
 import 'package:chicken_farm/apis/system/index.dart';
+import 'package:chicken_farm/apis/device/index.dart';
+import 'package:chicken_farm/apis/experiment/index.dart';
 
 class Apis {
   static final Apis _instance = Apis._internal();
@@ -10,6 +12,8 @@ class Apis {
 
   late final LoginApi loginApi = LoginApi();
   late final SystemApis system = SystemApis();
+  late final DeviceApis device = DeviceApis();
+  late final ExperimentApis experiment = ExperimentApis();
 }
 
-final apis = Apis();
+final apis = Apis();

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

@@ -0,0 +1,204 @@
+import 'package:chicken_farm/core/utils/logger.dart';
+import 'package:flutter/material.dart';
+import 'package:mobile_scanner/mobile_scanner.dart';
+import 'package:chicken_farm/core/utils/toast.dart';
+import 'package:audioplayers/audioplayers.dart';
+
+typedef OnScanCallback = void Function(String scannedContent);
+typedef QRCodeParser = Future<String?> Function(String rawContent);
+
+class QRScannerComponent extends StatefulWidget {
+  final String? startWithString;
+  final OnScanCallback onScanCallback;
+  final String? invalidQRMessage;
+  final QRCodeParser? qrCodeParser;
+
+  const QRScannerComponent({
+    super.key,
+    this.startWithString,
+    required this.onScanCallback,
+    this.invalidQRMessage,
+    this.qrCodeParser,
+  });
+
+  @override
+  State<QRScannerComponent> createState() => _QRScannerComponentState();
+}
+
+class _QRScannerComponentState extends State<QRScannerComponent> {
+  MobileScannerController? _cameraController;
+  bool _isScanning = false;
+  bool _isTorchOn = false;
+  final AudioPlayer _audioPlayer = AudioPlayer();
+
+  @override
+  void initState() {
+    super.initState();
+    _cameraController = MobileScannerController();
+  }
+
+  @override
+  void dispose() {
+    _cameraController?.dispose();
+    _audioPlayer.dispose();
+    super.dispose();
+  }
+
+  void _handleBarcodeScan(BarcodeCapture barcode) async {
+    // 如果正在扫描或冷却中,则忽略
+    logger.d('扫描二维码: $barcode');
+    if (_isScanning) return;
+    ToastUtil.info("正在扫描二维码...");
+    _isScanning = true;
+    final code = barcode.barcodes.first.rawValue;
+
+    // 播放提示音
+    await _audioPlayer.play(AssetSource('sounds/beep.mp3'));
+
+    if (code == null) {
+      _resetScanningState('无法识别二维码');
+      return;
+    }
+
+    _processQRCode(code);
+  }
+
+  void _processQRCode(String code) async {
+    // 如果设置了起始字符串,则检查是否匹配
+    if (widget.startWithString != null &&
+        !code.startsWith(widget.startWithString!)) {
+      final errorMessage = widget.invalidQRMessage ?? '非法二维码';
+      _resetScanningState(errorMessage);
+      return;
+    }
+    if (widget.qrCodeParser != null) {
+      try {
+        final parseCode = await widget.qrCodeParser!(code);
+        if (parseCode == null || parseCode.isEmpty) {
+          // 解析失败,显示错误并重置状态
+          _resetScanningState('二维码解析失败');
+          return;
+        }
+        code = parseCode;
+      } catch (e) {
+        // 解析过程中出现异常
+        _resetScanningState('二维码解析异常: $e');
+        return;
+      }
+    }
+    widget.onScanCallback(code);
+    _resetScanningState();
+  }
+
+  void _resetScanningState([String? errorMessage]) {
+    if (errorMessage != null) {
+      ToastUtil.errorAlert(errorMessage);
+    }
+    Future.delayed(const Duration(seconds: 1), () {
+      if (mounted) {
+        _isScanning = false;
+      }
+    });
+  }
+
+  void _toggleTorch() async {
+    if (_cameraController != null) {
+      await _cameraController!.toggleTorch();
+      setState(() {
+        _isTorchOn = !_isTorchOn;
+      });
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    const scanBoxSize = 250.0;
+
+    return Scaffold(
+      body: SafeArea(
+        child: LayoutBuilder(
+          builder: (context, constraints) {
+            var cw = constraints.maxWidth;
+            var ch = constraints.maxHeight;
+            var w = cw / 2 - scanBoxSize / 2;
+            var h = ch / 2 - scanBoxSize / 2;
+            var scanRefc = Rect.fromLTWH(w, h, scanBoxSize, scanBoxSize);
+
+            return Stack(
+              children: [
+                MobileScanner(
+                  controller: _cameraController,
+                  onDetect: _handleBarcodeScan,
+                  scanWindow: scanRefc,
+                ),
+                Align(
+                  alignment: Alignment.center,
+                  child: Container(
+                    width: scanBoxSize,
+                    height: scanBoxSize,
+                    decoration: BoxDecoration(
+                      border: Border(
+                        left: BorderSide(color: Colors.white, width: 2),
+                        right: BorderSide(color: Colors.white, width: 2),
+                        top: BorderSide(color: Colors.white, width: 2),
+                        bottom: BorderSide(color: Colors.white, width: 2),
+                      ),
+                    ),
+                  ),
+                ),
+                // 顶部黑色遮罩
+                Positioned(
+                  top: 0,
+                  left: 0,
+                  right: 0,
+                  bottom: h + scanBoxSize,
+                  child: Container(color: Colors.black.withValues(alpha: 0.7)),
+                ),
+                // 底部黑色遮罩
+                Positioned(
+                  top: h + scanBoxSize,
+                  left: 0,
+                  right: 0,
+                  bottom: 0,
+                  child: Container(color: Colors.black.withValues(alpha: 0.7)),
+                ),
+                // 左侧黑色遮罩
+                Positioned(
+                  top: h,
+                  left: 0,
+                  width: w,
+                  height: scanBoxSize,
+                  child: Container(color: Colors.black.withValues(alpha: 0.7)),
+                ),
+                // 右侧黑色遮罩
+                Positioned(
+                  top: h,
+                  right: 0,
+                  width: w,
+                  height: scanBoxSize,
+                  child: Container(color: Colors.black.withValues(alpha: 0.7)),
+                ),
+                //底部手电筒按钮
+                Positioned(
+                  top: h + scanBoxSize + 40,
+                  left: 0,
+                  right: 0,
+                  child: Center(
+                    child: IconButton(
+                      icon: Icon(
+                        _isTorchOn ? Icons.flashlight_on : Icons.flashlight_off,
+                        color: Colors.white,
+                        size: 36,
+                      ),
+                      onPressed: _toggleTorch,
+                    ),
+                  ),
+                ),
+              ],
+            );
+          },
+        ),
+      ),
+    );
+  }
+}

+ 17 - 13
UI/CF.APP/chicken_farm/lib/components/custom_app_bar.dart → UI/CF.APP/chicken_farm/lib/components/vb_app_bar.dart

@@ -1,18 +1,21 @@
+import 'package:chicken_farm/routes/app_routes.dart';
 import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 
-class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
+class VberAppBar extends StatelessWidget implements PreferredSizeWidget {
   final String title;
-  final bool showBackButton;
+  final bool showLeftButton;
   final List<Widget>? actions;
-  final VoidCallback? onBack;
+  final VoidCallback? onLeftPressed;
+  final bool alwaysGoToHome;
 
-  const CustomAppBar({
+  const VberAppBar({
     super.key,
     required this.title,
-    this.showBackButton = true,
+    this.showLeftButton = true,
     this.actions,
-    this.onBack,
+    this.onLeftPressed,
+    this.alwaysGoToHome = false,
   });
 
   @override
@@ -20,18 +23,19 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
     return AppBar(
       title: Text(title),
       centerTitle: true,
-      leading: showBackButton
+      leading: showLeftButton
           ? IconButton(
-              icon: const Icon(Icons.arrow_back),
+              icon: Icon(alwaysGoToHome ? Icons.home : Icons.arrow_back),
               onPressed: () {
-                if (onBack != null) {
-                  onBack!();
+                if (onLeftPressed != null) {
+                  onLeftPressed!();
+                } else if (alwaysGoToHome) {
+                  context.go(AppRouteNames.home);
                 } else if (context.canPop()) {
                   // 如果可以返回,则返回上一页
                   context.pop();
                 } else {
-                  // 如果无法返回,则导航到首页
-                  context.go('/');
+                  context.go(AppRouteNames.home);
                 }
               },
             )
@@ -42,4 +46,4 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
 
   @override
   Size get preferredSize => const Size.fromHeight(kToolbarHeight);
-}
+}

+ 249 - 0
UI/CF.APP/chicken_farm/lib/components/watermarked_camera.dart

@@ -0,0 +1,249 @@
+import 'dart:io';
+import 'dart:typed_data';
+import 'dart:ui' as ui;
+
+import 'package:flutter/material.dart';
+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';
+
+class WatermarkedCamera extends StatefulWidget {
+  final Function(File)? onImageCaptured;
+  final String? watermarkText;
+
+  const WatermarkedCamera({
+    super.key,
+    this.onImageCaptured,
+    this.watermarkText,
+  });
+
+  @override
+  State<WatermarkedCamera> createState() => _WatermarkedCameraState();
+}
+
+class _WatermarkedCameraState extends State<WatermarkedCamera> {
+  XFile? _capturedImage;
+  File? _watermarkedImage;
+  bool _isProcessing = false;
+
+  Future<void> _takePicture() async {
+    try {
+      final ImagePicker picker = ImagePicker();
+      final XFile? photo = await picker.pickImage(
+        source: ImageSource.camera,
+        imageQuality: 80,
+      );
+
+      if (photo != null) {
+        setState(() {
+          _capturedImage = photo;
+          _isProcessing = true;
+        });
+
+        // 添加水印并处理图像
+        await _addWatermark(photo);
+      }
+    } catch (e) {
+      if (mounted) {
+        ScaffoldMessenger.of(
+          context,
+        ).showSnackBar(SnackBar(content: Text('拍照失败: $e')));
+      }
+      logger.e('拍照失败: $e');
+    }
+  }
+
+  Future<void> _addWatermark(XFile imageFile) async {
+    try {
+      // 获取水印文本
+      String watermark =
+          widget.watermarkText ??
+          DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
+
+      // 将图片转换为字节以便处理
+      final Uint8List imageBytes = await imageFile.readAsBytes();
+
+      // 创建图片对象
+      final ui.Codec codec = await ui.instantiateImageCodec(imageBytes);
+      final ui.FrameInfo frameInfo = await codec.getNextFrame();
+      final ui.Image image = frameInfo.image;
+
+      // 创建画布
+      final ui.PictureRecorder recorder = ui.PictureRecorder();
+      final ui.Canvas canvas = ui.Canvas(recorder);
+
+      // 绘制原始图片
+      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,
+        ),
+      );
+
+      paragraphBuilder.pushStyle(
+        ui.TextStyle(
+          color: const ui.Color(0x80FFFFFF), // 半透明白色
+          fontSize: 24.0,
+        ),
+      );
+
+      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,
+        ),
+      );
+
+      // 完成绘制
+      final ui.Picture picture = recorder.endRecording();
+      final ui.Image watermarkedImage = await picture.toImage(
+        image.width,
+        image.height,
+      );
+
+      // 转换为字节数据
+      final ByteData? byteData = await watermarkedImage.toByteData(
+        format: ui.ImageByteFormat.png,
+      );
+
+      if (byteData != null) {
+        // 保存到临时文件
+        final tempDir = await getTemporaryDirectory();
+        final file = File(
+          '${tempDir.path}/watermarked_${DateTime.now().millisecondsSinceEpoch}.png',
+        );
+        await file.writeAsBytes(byteData.buffer.asUint8List());
+
+        setState(() {
+          _watermarkedImage = file;
+          _isProcessing = false;
+        });
+      } else {
+        setState(() {
+          _isProcessing = false;
+        });
+
+        if (mounted) {
+          ScaffoldMessenger.of(
+            context,
+          ).showSnackBar(const SnackBar(content: Text('处理图片失败')));
+        }
+        logger.e('处理图片失败');
+      }
+    } catch (e) {
+      setState(() {
+        _isProcessing = false;
+      });
+
+      if (mounted) {
+        ScaffoldMessenger.of(
+          context,
+        ).showSnackBar(SnackBar(content: Text('添加水印失败: $e')));
+      }
+      logger.e('添加水印失败: $e');
+    }
+  }
+
+  void _confirmImage() {
+    if (_watermarkedImage != null && widget.onImageCaptured != null) {
+      widget.onImageCaptured!(_watermarkedImage!);
+    }
+
+    Navigator.of(context).pop();
+  }
+
+  void _retakePicture() {
+    setState(() {
+      _capturedImage = null;
+      _watermarkedImage = null;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('拍照签到'),
+        leading: IconButton(
+          icon: const Icon(Icons.close),
+          onPressed: () => Navigator.of(context).pop(),
+        ),
+      ),
+      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
+                ? const Center(child: CircularProgressIndicator())
+                : Center(
+                    child: _watermarkedImage != null
+                        ? Image.file(_watermarkedImage!)
+                        : Image.file(File(_capturedImage!.path)),
+                  ),
+          ),
+          Padding(
+            padding: const EdgeInsets.all(16.0),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+              children: [
+                if (_capturedImage == null)
+                  FloatingActionButton(
+                    onPressed: _takePicture,
+                    child: const Icon(Icons.camera),
+                  )
+                else if (!_isProcessing)
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+                    children: [
+                      ElevatedButton(
+                        onPressed: _retakePicture,
+                        style: ElevatedButton.styleFrom(
+                          backgroundColor: Colors.grey,
+                          foregroundColor: Colors.white,
+                        ),
+                        child: const Text('重新拍照'),
+                      ),
+                      ElevatedButton(
+                        onPressed: _confirmImage,
+                        style: ElevatedButton.styleFrom(
+                          backgroundColor: Colors.green,
+                          foregroundColor: Colors.white,
+                        ),
+                        child: const Text('确认提交'),
+                      ),
+                    ],
+                  ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

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

@@ -1,9 +1,9 @@
 import 'package:chicken_farm/core/api/api_option.dart';
+import 'package:chicken_farm/core/services/navigation_service.dart';
 import 'package:chicken_farm/core/utils/loading.dart';
 import 'package:chicken_farm/core/utils/logger.dart';
 import 'package:chicken_farm/core/utils/toast.dart';
 import 'package:chicken_farm/routes/app_routes.dart';
-import 'package:chicken_farm/routes/route_provider.dart';
 import 'package:dio/dio.dart';
 import 'package:go_router/go_router.dart';
 import 'api_client.dart';
@@ -19,7 +19,7 @@ class ApiService {
     Options? options,
     CancelToken? cancelToken,
   }) async {
-    final option = apiOption ?? ApiOption();
+    final option = apiOption ?? ApiOption.noAlert();
     try {
       if (option.loading) {
         LoadingUtil.showLoading();
@@ -142,36 +142,38 @@ class ApiService {
               ? data['data']
               : data;
           if (apiOption.alert) {
-            ToastUtils.success(msg ?? '操作成功');
+            ToastUtil.success(msg ?? '操作成功');
           }
           return rData;
         } else if (code == 401) {
           if (apiOption.alert) {
-            ToastUtils.error(msg ?? '请先登录!');
+            ToastUtil.error(msg ?? '请先登录!');
           }
-          rootNavigatorKey.currentContext?.goNamed(AppRoutePaths.login);
+          NavigationService.navigatorKey.currentContext?.goNamed(
+            AppRouteNames.login,
+          );
           return null;
         } else if (code == 403) {
           if (apiOption.alert) {
-            ToastUtils.error(msg ?? '没有权限!');
+            ToastUtil.error(msg ?? '没有权限!');
           }
           return null;
         } else {
           if (apiOption.alert) {
-            ToastUtils.error(data['msg'] ?? '操作失败');
+            ToastUtil.error(data['msg'] ?? '操作失败');
           }
           return null;
         }
       } else {
         if (apiOption.alert) {
-          ToastUtils.error(response.statusMessage ?? '请求失败');
+          ToastUtil.error(response.statusMessage ?? '请求失败');
         }
         return null;
       }
     } catch (e) {
       logger.e("数据解析失败:$e");
       if (apiOption.alert) {
-        ToastUtils.error('数据解析失败');
+        ToastUtil.error('数据解析失败');
       }
       return null;
     }

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

@@ -1,7 +1,8 @@
 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.81:8380'; // 默认值
+  static String baseUrl = 'http://192.168.0.109:6071/'; // 默认值
   static String clientId = '9579f8780cf24ae2959d03d11482b18a'; // 默认值
   static const String baseUrlKey = 'base_url';
   static const String clientIdKey = 'client_id';

+ 7 - 0
UI/CF.APP/chicken_farm/lib/core/services/navigation_service.dart

@@ -0,0 +1,7 @@
+import 'package:flutter/material.dart';
+
+/// 全局导航服务,提供在没有BuildContext的情况下进行导航的能力
+class NavigationService {
+  /// 全局导航键,用于在没有BuildContext的情况下进行导航
+  static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
+}

+ 57 - 0
UI/CF.APP/chicken_farm/lib/core/utils/datetime_util.dart

@@ -0,0 +1,57 @@
+import 'package:intl/intl.dart';
+
+/// 时间转换工具类
+class DateTimeUtil {
+  /// 将 DateTime 对象转换为指定格式的字符串
+  /// 默认格式为: yyyy-MM-dd HH:mm:ss
+  static String format(DateTime? dateTime, {String pattern = 'yyyy-MM-dd HH:mm:ss'}) {
+    if (dateTime == null) {
+      return '';
+    }
+    
+    try {
+      final formatter = DateFormat(pattern);
+      return formatter.format(dateTime);
+    } catch (e) {
+      // 如果格式化失败,返回默认格式
+      final defaultFormatter = DateFormat('yyyy-MM-dd HH:mm:ss');
+      return defaultFormatter.format(dateTime);
+    }
+  }
+
+  /// 将字符串解析为 DateTime 对象
+  /// 默认格式为: yyyy-MM-dd HH:mm:ss
+  static DateTime? parse(String dateTimeString, {String pattern = 'yyyy-MM-dd HH:mm:ss'}) {
+    if (dateTimeString.isEmpty) {
+      return null;
+    }
+    
+    try {
+      final formatter = DateFormat(pattern);
+      return formatter.parse(dateTimeString);
+    } catch (e) {
+      // 如果解析失败,尝试使用默认格式
+      try {
+        final defaultFormatter = DateFormat('yyyy-MM-dd HH:mm:ss');
+        return defaultFormatter.parse(dateTimeString);
+      } catch (e) {
+        return null;
+      }
+    }
+  }
+
+  /// 格式化为标准时间格式 (yyyy-MM-dd HH:mm:ss)
+  static String toStandardString(DateTime? dateTime) {
+    return format(dateTime);
+  }
+
+  /// 格式化为日期格式 (yyyy-MM-dd)
+  static String toDateString(DateTime? dateTime) {
+    return format(dateTime, pattern: 'yyyy-MM-dd');
+  }
+
+  /// 格式化为时间格式 (HH:mm:ss)
+  static String toTimeString(DateTime? dateTime) {
+    return format(dateTime, pattern: 'HH:mm:ss');
+  }
+}

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

@@ -1,7 +1,8 @@
+import 'package:chicken_farm/core/services/navigation_service.dart';
 import 'package:flutter/material.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 
-class ToastUtils {
+class ToastUtil {
   static void success(
     String message, [
     Toast? toastLength = Toast.LENGTH_SHORT,
@@ -12,7 +13,7 @@ class ToastUtils {
 
   static void error(
     String message, [
-    Toast? toastLength = Toast.LENGTH_LONG,
+    Toast? toastLength = Toast.LENGTH_SHORT,
     ToastGravity? gravity = ToastGravity.CENTER,
     Color? textColor = Colors.white,
     double? fontSize = 16.0,
@@ -20,12 +21,172 @@ class ToastUtils {
 
   static void info(
     String message, [
-    Toast? toastLength = Toast.LENGTH_LONG,
+    Toast? toastLength = Toast.LENGTH_SHORT,
     ToastGravity? gravity = ToastGravity.CENTER,
     Color? textColor = Colors.white,
     double? fontSize = 16.0,
   ]) => show(message, Colors.blue, toastLength, gravity, textColor, fontSize);
 
+  static Future<void> errorAlert(
+    String message, {
+    String confirmText = '确定',
+    BuildContext? context,
+  }) async {
+    // 这是一个需要用户确认的错误弹窗(只有确认按钮,没有取消按钮)
+    final BuildContext? dialogContext =
+        context ?? NavigationService.navigatorKey.currentContext;
+    if (dialogContext == null) {
+      return;
+    }
+    return await showDialog(
+      context: dialogContext,
+      builder: (BuildContext context) {
+        return AlertDialog(
+          title: const Text(
+            '错误',
+            style: TextStyle(
+              fontSize: 20,
+              fontWeight: FontWeight.bold,
+              color: Colors.red,
+            ),
+          ),
+          content: Text(message, style: const TextStyle(fontSize: 16)),
+          backgroundColor: Colors.white,
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(15),
+          ),
+          elevation: 24,
+          actions: <Widget>[
+            Container(
+              padding: const EdgeInsets.only(right: 16, bottom: 16),
+              alignment: Alignment.centerRight,
+              child: ElevatedButton(
+                style: ElevatedButton.styleFrom(
+                  backgroundColor: Colors.red,
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(8),
+                  ),
+                  padding: const EdgeInsets.symmetric(
+                    horizontal: 24,
+                    vertical: 12,
+                  ),
+                ),
+                onPressed: () {
+                  Navigator.of(context).pop(true); // 返回true表示确认
+                },
+                child: Text(
+                  confirmText,
+                  style: const TextStyle(
+                    fontSize: 16,
+                    fontWeight: FontWeight.w500,
+                    color: Colors.white,
+                  ),
+                ),
+              ),
+            ),
+          ],
+        );
+      },
+    );
+  }
+
+  static Future<void> confirm(
+    String message,
+    VoidCallback onConfirm, {
+    String confirmText = '确定',
+    String cancelText = '取消',
+    BuildContext? context,
+  }) async {
+    final BuildContext? dialogContext =
+        context ?? NavigationService.navigatorKey.currentContext;
+    if (dialogContext == null) {
+      return;
+    }
+    final bool? confirmed = await showDialog<bool>(
+      context: dialogContext,
+      builder: (BuildContext context) {
+        return AlertDialog(
+          title: const Text(
+            '确认',
+            style: TextStyle(
+              fontSize: 20,
+              fontWeight: FontWeight.bold,
+              color: Colors.blue,
+            ),
+          ),
+          content: Text(message, style: const TextStyle(fontSize: 16)),
+          backgroundColor: Colors.white,
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(15),
+          ),
+          elevation: 24,
+          actions: <Widget>[
+            Container(
+              padding: const EdgeInsets.only(right: 16, bottom: 16),
+              alignment: Alignment.centerRight,
+              child: Row(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  OutlinedButton(
+                    style: OutlinedButton.styleFrom(
+                      side: const BorderSide(color: Colors.grey),
+                      shape: RoundedRectangleBorder(
+                        borderRadius: BorderRadius.circular(8),
+                      ),
+                      padding: const EdgeInsets.symmetric(
+                        horizontal: 20,
+                        vertical: 12,
+                      ),
+                    ),
+                    onPressed: () {
+                      Navigator.of(context).pop(false); // 取消,关闭确认框
+                    },
+                    child: Text(
+                      cancelText,
+                      style: const TextStyle(
+                        fontSize: 16,
+                        fontWeight: FontWeight.w500,
+                        color: Colors.grey,
+                      ),
+                    ),
+                  ),
+                  const SizedBox(width: 12),
+                  ElevatedButton(
+                    style: ElevatedButton.styleFrom(
+                      backgroundColor: Colors.blue,
+                      shape: RoundedRectangleBorder(
+                        borderRadius: BorderRadius.circular(8),
+                      ),
+                      padding: const EdgeInsets.symmetric(
+                        horizontal: 20,
+                        vertical: 12,
+                      ),
+                    ),
+                    onPressed: () {
+                      Navigator.of(context).pop(true); // 确定,关闭确认框
+                    },
+                    child: Text(
+                      confirmText,
+                      style: const TextStyle(
+                        fontSize: 16,
+                        fontWeight: FontWeight.w500,
+                        color: Colors.white,
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ],
+        );
+      },
+    );
+
+    if (confirmed == true) {
+      onConfirm();
+    }
+  }
+
   static void show(
     String message, [
     Color? backgroundColor = Colors.blue,

+ 33 - 0
UI/CF.APP/chicken_farm/lib/modes/device/inspection_rule/checkin_log.dart

@@ -0,0 +1,33 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'checkin_log.g.dart';
+
+@JsonSerializable()
+class CheckinLogModel {
+  int id;
+  int inspectionRuleId;
+  int inspectorId;
+  String inspectorName;
+  int plannedSequence;
+  DateTime? executeTime;
+  DateTime planTime;
+  String? imageUrl;
+  int checkinStatus;
+
+  CheckinLogModel({
+    required this.id,
+    required this.inspectionRuleId,
+    required this.inspectorId,
+    required this.inspectorName,
+    required this.plannedSequence,
+    required this.planTime,
+    required this.checkinStatus,
+    this.executeTime,
+    this.imageUrl,
+  });
+
+  factory CheckinLogModel.fromJson(Map<String, dynamic> json) =>
+      _$CheckinLogModelFromJson(json);
+
+  Map<String, dynamic> toJson() => _$CheckinLogModelToJson(this);
+}

+ 35 - 0
UI/CF.APP/chicken_farm/lib/modes/device/inspection_rule/checkin_log.g.dart

@@ -0,0 +1,35 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'checkin_log.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+CheckinLogModel _$CheckinLogModelFromJson(Map<String, dynamic> json) =>
+    CheckinLogModel(
+      id: (json['id'] as num).toInt(),
+      inspectionRuleId: (json['inspectionRuleId'] as num).toInt(),
+      inspectorId: (json['inspectorId'] as num).toInt(),
+      inspectorName: json['inspectorName'] as String,
+      plannedSequence: (json['plannedSequence'] as num).toInt(),
+      planTime: DateTime.parse(json['planTime'] as String),
+      checkinStatus: (json['checkinStatus'] as num).toInt(),
+      executeTime: json['executeTime'] == null
+          ? null
+          : DateTime.parse(json['executeTime'] as String),
+      imageUrl: json['imageUrl'] as String?,
+    );
+
+Map<String, dynamic> _$CheckinLogModelToJson(CheckinLogModel instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'inspectionRuleId': instance.inspectionRuleId,
+      'inspectorId': instance.inspectorId,
+      'inspectorName': instance.inspectorName,
+      'plannedSequence': instance.plannedSequence,
+      'executeTime': instance.executeTime?.toIso8601String(),
+      'planTime': instance.planTime.toIso8601String(),
+      'imageUrl': instance.imageUrl,
+      'checkinStatus': instance.checkinStatus,
+    };

+ 29 - 0
UI/CF.APP/chicken_farm/lib/modes/device/inspection_rule/inspection_rule.dart

@@ -0,0 +1,29 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'inspection_rule.g.dart';
+
+@JsonSerializable()
+class InspectionRuleModel {
+  int id;
+  String taskName;
+  String location;
+  double cycleHours;
+  DateTime startTime;
+  DateTime endTime;
+  String executorName;
+
+  InspectionRuleModel({
+    required this.id,
+    required this.taskName,
+    required this.location,
+    required this.cycleHours,
+    required this.startTime,
+    required this.endTime,
+    required this.executorName,
+  });
+
+  factory InspectionRuleModel.fromJson(Map<String, dynamic> json) =>
+      _$InspectionRuleModelFromJson(json);
+
+  Map<String, dynamic> toJson() => _$InspectionRuleModelToJson(this);
+}

+ 30 - 0
UI/CF.APP/chicken_farm/lib/modes/device/inspection_rule/inspection_rule.g.dart

@@ -0,0 +1,30 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'inspection_rule.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+InspectionRuleModel _$InspectionRuleModelFromJson(Map<String, dynamic> json) =>
+    InspectionRuleModel(
+      id: (json['id'] as num).toInt(),
+      taskName: json['taskName'] as String,
+      location: json['location'] as String,
+      cycleHours: (json['cycleHours'] as num).toDouble(),
+      startTime: DateTime.parse(json['startTime'] as String),
+      endTime: DateTime.parse(json['endTime'] as String),
+      executorName: json['executorName'] as String,
+    );
+
+Map<String, dynamic> _$InspectionRuleModelToJson(
+  InspectionRuleModel instance,
+) => <String, dynamic>{
+  'id': instance.id,
+  'taskName': instance.taskName,
+  'location': instance.location,
+  'cycleHours': instance.cycleHours,
+  'startTime': instance.startTime.toIso8601String(),
+  'endTime': instance.endTime.toIso8601String(),
+  'executorName': instance.executorName,
+};

+ 37 - 0
UI/CF.APP/chicken_farm/lib/modes/experiment/sample/sample.dart

@@ -0,0 +1,37 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'sample.g.dart';
+
+@JsonSerializable()
+class SampleModel {
+  int id;
+  String sampleName;
+  String batchNum;
+  int chickenId;
+  String wingTagNum;
+  DateTime sampleTime;
+  String description;
+  int sampleType;
+  int sampleStatus;
+  int createBy;
+  String createByName;
+
+  SampleModel({
+    required this.id,
+    required this.sampleName,
+    required this.batchNum,
+    required this.chickenId,
+    required this.wingTagNum,
+    required this.sampleTime,
+    required this.description,
+    required this.sampleType,
+    required this.sampleStatus,
+    required this.createBy,
+    required this.createByName,
+  });
+
+  factory SampleModel.fromJson(Map<String, dynamic> json) =>
+      _$SampleModelFromJson(json);
+
+  Map<String, dynamic> toJson() => _$SampleModelToJson(this);
+}

+ 36 - 0
UI/CF.APP/chicken_farm/lib/modes/experiment/sample/sample.g.dart

@@ -0,0 +1,36 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'sample.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+SampleModel _$SampleModelFromJson(Map<String, dynamic> json) => SampleModel(
+  id: (json['id'] as num).toInt(),
+  sampleName: json['sampleName'] as String,
+  batchNum: json['batchNum'] as String,
+  chickenId: (json['chickenId'] as num).toInt(),
+  wingTagNum: json['wingTagNum'] as String,
+  sampleTime: DateTime.parse(json['sampleTime'] as String),
+  description: json['description'] as String,
+  sampleType: (json['sampleType'] as num).toInt(),
+  sampleStatus: (json['sampleStatus'] as num).toInt(),
+  createBy: (json['createBy'] as num).toInt(),
+  createByName: json['createByName'] as String,
+);
+
+Map<String, dynamic> _$SampleModelToJson(SampleModel instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'sampleName': instance.sampleName,
+      'batchNum': instance.batchNum,
+      'chickenId': instance.chickenId,
+      'wingTagNum': instance.wingTagNum,
+      'sampleTime': instance.sampleTime.toIso8601String(),
+      'description': instance.description,
+      'sampleType': instance.sampleType,
+      'sampleStatus': instance.sampleStatus,
+      'createBy': instance.createBy,
+      'createByName': instance.createByName,
+    };

+ 25 - 0
UI/CF.APP/chicken_farm/lib/modes/experiment/sample/sample_flow_log.dart

@@ -0,0 +1,25 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'sample_flow_log.g.dart';
+
+@JsonSerializable()
+class SampleFlowLogModel {
+  int id;
+  int sampleId;
+  int handler;
+  String handlerName;
+  DateTime handleTime;
+
+  SampleFlowLogModel({
+    required this.id,
+    required this.sampleId,
+    required this.handler,
+    required this.handlerName,
+    required this.handleTime,
+  });
+
+  factory SampleFlowLogModel.fromJson(Map<String, dynamic> json) =>
+      _$SampleFlowLogModelFromJson(json);
+
+  Map<String, dynamic> toJson() => _$SampleFlowLogModelToJson(this);
+}

+ 25 - 0
UI/CF.APP/chicken_farm/lib/modes/experiment/sample/sample_flow_log.g.dart

@@ -0,0 +1,25 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'sample_flow_log.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+SampleFlowLogModel _$SampleFlowLogModelFromJson(Map<String, dynamic> json) =>
+    SampleFlowLogModel(
+      id: (json['id'] as num).toInt(),
+      sampleId: (json['sampleId'] as num).toInt(),
+      handler: (json['handler'] as num).toInt(),
+      handlerName: json['handlerName'] as String,
+      handleTime: DateTime.parse(json['handleTime'] as String),
+    );
+
+Map<String, dynamic> _$SampleFlowLogModelToJson(SampleFlowLogModel instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'sampleId': instance.sampleId,
+      'handler': instance.handler,
+      'handlerName': instance.handlerName,
+      'handleTime': instance.handleTime.toIso8601String(),
+    };

+ 5 - 21
UI/CF.APP/chicken_farm/lib/pages/account/login.dart → UI/CF.APP/chicken_farm/lib/pages/account/login_page.dart

@@ -1,4 +1,4 @@
-import 'package:chicken_farm/components/custom_app_bar.dart';
+import 'package:chicken_farm/components/vb_app_bar.dart';
 import 'package:chicken_farm/core/utils/logger.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -33,24 +33,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
     final authState = ref.watch(authStoreProvider);
     final authStore = ref.read(authStoreProvider.notifier);
 
-    // 监听认证状态变化,如果已认证则跳转到主页
-    WidgetsBinding.instance.addPostFrameCallback((_) {
-      if (authState.state == AuthState.authenticated && context.mounted) {
-        context.goNamed(AppRoutePaths.home);
-      }
-    });
-
-    ref.listen<AuthInfo>(authStoreProvider, (previous, next) {
-      if (next.state == AuthState.authenticated) {
-        // 登录成功,跳转到主页
-        if (context.mounted) {
-          context.goNamed(AppRoutePaths.home);
-        }
-      }
-    });
-
     return Scaffold(
-      appBar: const CustomAppBar(title: '用户登录', showBackButton: false),
+      appBar: const VberAppBar(title: '用户登录', showLeftButton: false),
       body: Stack(
         children: [
           Container(
@@ -137,7 +121,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
                                               if (context.mounted) {
                                                 logger.d('登录成功');
                                                 context.goNamed(
-                                                  AppRoutePaths.home,
+                                                  AppRouteNames.home,
                                                 );
                                               }
                                             })
@@ -150,7 +134,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
                                                   errorMessage = error
                                                       .toString();
                                                 }
-                                                ToastUtils.error(errorMessage);
+                                                ToastUtil.error(errorMessage);
                                               }
                                             });
                                       }
@@ -194,7 +178,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
                   //     duration: Duration(seconds: 2),
                   //   ),
                   // );
-                  ToastUtils.success('配置已保存');
+                  ToastUtil.success('配置已保存');
                 }
               },
             ),

+ 44 - 6
UI/CF.APP/chicken_farm/lib/pages/checkin/checkin_page.dart

@@ -1,17 +1,55 @@
+import 'package:chicken_farm/core/utils/logger.dart';
+import 'package:chicken_farm/routes/app_routes.dart';
 import 'package:flutter/material.dart';
-import 'package:chicken_farm/components/custom_app_bar.dart';
+import 'package:chicken_farm/components/vb_app_bar.dart';
+import 'package:chicken_farm/components/qr_scanner.dart';
+import 'package:go_router/go_router.dart';
 
-class CheckinPage extends StatelessWidget {
+const String _qrCodePrefix = 'vb@device@/checkin/';
+
+class CheckinPage extends StatefulWidget {
   const CheckinPage({super.key});
 
+  @override
+  State<CheckinPage> createState() => _CheckinPageState();
+}
+
+class _CheckinPageState extends State<CheckinPage> {
+  Future<String?> _parseCheckinQRCode(String rawContent) async {
+    try {
+      // 替换掉 _qrCodePrefix
+      final content = rawContent.replaceFirst(_qrCodePrefix, '');
+      logger.d('parseCheckinQRCode: $content  rawContent: $rawContent');
+      if (content.isNotEmpty) {
+        // 检查是否能转成int,能转就返回conetent,不能返回null
+        return int.parse(content).toString();
+      }
+      return null;
+    } catch (e) {
+      return null;
+    }
+  }
+
+  Future<void> _performCheckin(String scannedContent) async {
+    // 跳转到签到记录页面 ,传递id参数为scannedContent(查询这个id的签到记录)
+    if (context.mounted) {
+      context.pushNamed(
+        AppRouteNames.checkinRecord,
+        pathParameters: {'id': scannedContent},
+      );
+    }
+  }
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(
-      appBar: const CustomAppBar(
-        title: '点检签到',
-        showBackButton: true, // 子页面显示返回按钮
+      appBar: const VberAppBar(title: '点检签到', showLeftButton: true),
+      body: QRScannerComponent(
+        startWithString: _qrCodePrefix,
+        onScanCallback: _performCheckin,
+        invalidQRMessage: '非法二维码',
+        qrCodeParser: _parseCheckinQRCode,
       ),
-      body: const Center(child: Text('点检签到页面')),
     );
   }
 }

+ 39 - 0
UI/CF.APP/chicken_farm/lib/pages/checkin/checkin_page_test.dart

@@ -0,0 +1,39 @@
+import 'package:chicken_farm/routes/app_routes.dart';
+import 'package:flutter/material.dart';
+import 'package:chicken_farm/components/vb_app_bar.dart';
+import 'package:go_router/go_router.dart';
+
+class CheckinPage extends StatefulWidget {
+  const CheckinPage({super.key});
+
+  @override
+  State<CheckinPage> createState() => _CheckinPageState();
+}
+
+class _CheckinPageState extends State<CheckinPage> {
+  Future<void> _simulateScanAndNavigate() async {
+    // 模拟扫码成功,使用测试ID "1"
+    final String simulatedScannedContent = "1";
+
+    // 跳转到签到记录页面,传递ID参数
+    if (context.mounted) {
+      context.pushNamed(
+        AppRouteNames.checkinRecord,
+        pathParameters: {'id': simulatedScannedContent},
+      );
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: const VberAppBar(title: '点检签到', showLeftButton: true),
+      body: Center(
+        child: ElevatedButton(
+          onPressed: _simulateScanAndNavigate,
+          child: const Text('模拟扫码成功'),
+        ),
+      ),
+    );
+  }
+}

+ 277 - 0
UI/CF.APP/chicken_farm/lib/pages/checkin/checkin_record_page.dart

@@ -0,0 +1,277 @@
+import 'package:chicken_farm/core/utils/logger.dart';
+import 'package:chicken_farm/routes/app_routes.dart';
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:chicken_farm/components/vb_app_bar.dart';
+import 'package:chicken_farm/apis/device/_inspection_rule.dart';
+import 'package:chicken_farm/modes/device/inspection_rule/checkin_log.dart';
+import 'package:chicken_farm/core/utils/toast.dart';
+import 'package:chicken_farm/core/utils/datetime_util.dart';
+import 'package:chicken_farm/modes/device/inspection_rule/inspection_rule.dart';
+import 'package:chicken_farm/components/watermarked_camera.dart';
+
+import 'package:go_router/go_router.dart';
+
+class CheckinRecordPage extends StatefulWidget {
+  final String id;
+
+  const CheckinRecordPage({super.key, required this.id});
+
+  @override
+  State<CheckinRecordPage> createState() => _CheckinRecordPageState();
+}
+
+class _CheckinRecordPageState extends State<CheckinRecordPage> {
+  List<CheckinLogModel> _records = [];
+  InspectionRuleModel? _rule;
+  bool _isLoading = true;
+
+  @override
+  void initState() {
+    super.initState();
+    // 将 _loadData 调用推迟到下一帧,避免在构建过程中修改状态
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      _loadData();
+    });
+  }
+
+  Future<void> _loadData() async {
+    // 在更新状态前检查组件是否仍然挂载
+    if (!mounted) return;
+
+    setState(() {
+      _isLoading = true;
+    });
+
+    try {
+      logger.w('查询设备签到记录: ${widget.id}');
+
+      final ruleFuture = InspectionRuleApi().queryRule(widget.id);
+      final recordsFuture = InspectionRuleApi().queryCheckinList(widget.id);
+
+      final results = await Future.wait([ruleFuture, recordsFuture]);
+
+      setState(() {
+        _rule = results[0] as InspectionRuleModel;
+        _records = results[1] as List<CheckinLogModel>;
+        _isLoading = false;
+      });
+    } catch (e, stackTrace) {
+      logger.e('加载记录失败: $e, error: e, stackTrace: $stackTrace');
+
+      setState(() {
+        _isLoading = false;
+      });
+      ToastUtil.error('加载记录失败: $e');
+    }
+  }
+
+  Future<void> _performCheckin() async {
+    // 打开拍照界面
+    await Navigator.of(context).push(
+      MaterialPageRoute(
+        builder: (context) => WatermarkedCamera(
+          onImageCaptured: (imageFile) async {
+            try {
+              // 创建 FormData 对象用于文件上传
+              final formData = FormData.fromMap({
+                'phone': await MultipartFile.fromFile(
+                  imageFile.path,
+                  filename:
+                      'isCheckin_${widget.id}_${DateTime.now().millisecondsSinceEpoch}.jpg',
+                ),
+              });
+
+              await InspectionRuleApi().checkInWithPhoto(widget.id, formData);
+              if (mounted) {
+                ToastUtil.success('签到成功');
+                _loadData(); // 刷新数据
+              }
+            } catch (e) {
+              logger.e('签到失败: $e');
+              if (mounted) {
+                ToastUtil.error('签到失败: $e');
+              }
+            }
+          },
+        ),
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: const VberAppBar(
+        title: '签到记录',
+        showLeftButton: true,
+        alwaysGoToHome: true,
+      ),
+      body: _buildBody(),
+    );
+  }
+
+  Widget _buildBody() {
+    if (_isLoading) {
+      return const Center(child: CircularProgressIndicator());
+    }
+
+    return SingleChildScrollView(
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          if (_rule != null) _buildRuleInfo(),
+          const Divider(),
+          Padding(
+            padding: const EdgeInsets.all(16.0),
+            child: Text('签到记录', style: Theme.of(context).textTheme.titleLarge),
+          ),
+          if (_records.isEmpty)
+            const Center(
+              child: Padding(
+                padding: EdgeInsets.all(32.0),
+                child: Text(
+                  '暂无签到记录',
+                  style: TextStyle(fontSize: 16, color: Colors.grey),
+                ),
+              ),
+            )
+          else
+            ListView.builder(
+              shrinkWrap: true,
+              physics: const NeverScrollableScrollPhysics(),
+              itemCount: _records.length,
+              itemBuilder: (context, index) {
+                final record = _records[index];
+                return CheckinRecordItem(record: record);
+              },
+            ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildRuleInfo() {
+    return Container(
+      margin: const EdgeInsets.all(16),
+      width: double.infinity,
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          SizedBox(
+            width: double.infinity,
+            child: Card(
+              child: Padding(
+                padding: const EdgeInsets.all(16.0),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Text(
+                      _rule!.taskName,
+                      style: Theme.of(context).textTheme.titleLarge,
+                    ),
+                    const SizedBox(height: 8),
+                    Text('地点: ${_rule!.location}'),
+                    const SizedBox(height: 4),
+                    Text('周期: ${_rule!.cycleHours}小时'),
+                    const SizedBox(height: 4),
+                    Text('执行人: ${_rule!.executorName}'),
+                    const SizedBox(height: 4),
+                    Text(
+                      '开始时间: ${DateTimeUtil.toStandardString(_rule!.startTime)}',
+                    ),
+                    const SizedBox(height: 4),
+                    Text(
+                      '结束时间: ${DateTimeUtil.toStandardString(_rule!.endTime)}',
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          ),
+          const SizedBox(height: 16),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              Expanded(
+                child: SizedBox(
+                  height: 48,
+                  child: ElevatedButton.icon(
+                    onPressed: _performCheckin,
+                    style: ElevatedButton.styleFrom(
+                      textStyle: const TextStyle(
+                        fontSize: 16,
+                        fontWeight: FontWeight.bold,
+                      ),
+                    ),
+                    icon: const Icon(Icons.check, size: 20),
+                    label: const Text('立即签到'),
+                  ),
+                ),
+              ),
+              const SizedBox(width: 16),
+              Expanded(
+                child: SizedBox(
+                  height: 48,
+                  child: ElevatedButton.icon(
+                    onPressed: () {
+                      context.pushNamed(AppRouteNames.checkin);
+                    },
+                    style: ElevatedButton.styleFrom(
+                      textStyle: const TextStyle(
+                        fontSize: 16,
+                        fontWeight: FontWeight.bold,
+                      ),
+                    ),
+                    icon: const Icon(Icons.qr_code_scanner, size: 20),
+                    label: const Text('扫一扫'),
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class CheckinRecordItem extends StatelessWidget {
+  final CheckinLogModel record;
+
+  const CheckinRecordItem({super.key, required this.record});
+
+  @override
+  Widget build(BuildContext context) {
+    return Card(
+      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+      child: ListTile(
+        title: Text('${record.plannedSequence}. ${record.inspectorName}'),
+        subtitle: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text('计划时间: ${DateTimeUtil.toStandardString(record.planTime)}'),
+            const SizedBox(height: 4),
+            Text(_getExecuteTimeText()),
+          ],
+        ),
+        trailing: Chip(
+          label: Text(
+            record.checkinStatus == 1 ? '已签到' : '未签到',
+            style: TextStyle(
+              color: record.checkinStatus == 1 ? Colors.green : Colors.red,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  String _getExecuteTimeText() {
+    if (record.executeTime != null) {
+      return '签到时间: ${DateTimeUtil.toStandardString(record.executeTime)}';
+    } else {
+      return '签到时间: 未签到';
+    }
+  }
+}

+ 4 - 4
UI/CF.APP/chicken_farm/lib/pages/home/home.dart → UI/CF.APP/chicken_farm/lib/pages/home/home_page.dart

@@ -1,4 +1,4 @@
-import 'package:chicken_farm/components/custom_app_bar.dart';
+import 'package:chicken_farm/components/vb_app_bar.dart';
 import 'package:chicken_farm/pages/home/menu_buttons.dart';
 import 'package:chicken_farm/stores/auth_store.dart';
 import 'package:flutter/material.dart';
@@ -22,9 +22,9 @@ class _HomePageState extends ConsumerState<HomePage> {
     final authState = ref.watch(authStoreProvider);
 
     return Scaffold(
-      appBar: CustomAppBar(
+      appBar: VberAppBar(
         title: _titles[_selectedIndex],
-        showBackButton: false, // 主页不显示返回按钮
+        showLeftButton: false, // 主页不显示返回按钮
       ),
       body: _buildBody(_selectedIndex, authState),
       bottomNavigationBar: BottomNavigationBar(
@@ -61,4 +61,4 @@ class _HomePageState extends ConsumerState<HomePage> {
         return const Center(child: Text('页面不存在'));
     }
   }
-}
+}

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

@@ -30,7 +30,7 @@ class MenuButtons extends ConsumerWidget {
             height: 50,
             child: ElevatedButton.icon(
               onPressed: () {
-                context.pushNamed(AppRoutePaths.checkin);
+                context.pushNamed(AppRouteNames.checkin);
               },
               icon: const Icon(Icons.check_circle_outline),
               label: const Text('点检签到'),
@@ -43,10 +43,10 @@ class MenuButtons extends ConsumerWidget {
             height: 50,
             child: ElevatedButton.icon(
               onPressed: () {
-                context.pushNamed(AppRoutePaths.sampleTransfer);
+                context.pushNamed(AppRouteNames.sample);
               },
               icon: const Icon(Icons.swap_horiz),
-              label: const Text('样品流转'),
+              label: const Text('样品查询'),
             ),
           ),
       ],

+ 0 - 1
UI/CF.APP/chicken_farm/lib/pages/home/profile.dart

@@ -1,7 +1,6 @@
 import 'package:chicken_farm/stores/auth_store.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:getwidget/getwidget.dart';
 
 class ProfilePage extends ConsumerWidget {
   const ProfilePage({super.key});

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

@@ -0,0 +1,237 @@
+import 'package:flutter/material.dart';
+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';
+
+class SampleDetailPage extends StatefulWidget {
+  final String id;
+
+  const SampleDetailPage({super.key, required this.id});
+
+  @override
+  State<SampleDetailPage> createState() => _SampleDetailPageState();
+}
+
+class _SampleDetailPageState extends State<SampleDetailPage> {
+  SampleModel? _sample;
+  List<SampleFlowLogModel> _flowLogs = [];
+  bool _isLoading = true;
+
+  @override
+  void initState() {
+    super.initState();
+    // 将 _loadSampleData 调用推迟到下一帧,避免在构建过程中修改状态
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      _loadSampleData();
+    });
+  }
+
+  Future<void> _loadSampleData() async {
+    try {
+      final sampleFuture = SampleApi().querySample(widget.id);
+      final flowLogsFuture = SampleApi().queryFlowLogs(widget.id);
+
+      final results = await Future.wait([sampleFuture, flowLogsFuture]);
+
+      setState(() {
+        _sample = results[0] as SampleModel;
+        _flowLogs = results[1] as List<SampleFlowLogModel>;
+        _isLoading = false;
+      });
+    } catch (e, stackTrace) {
+      logger.e('加载样品详情失败: $e, error: e, stackTrace: $stackTrace');
+      setState(() {
+        _isLoading = false;
+      });
+      ToastUtil.error('加载样品详情失败: $e');
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: const VberAppBar(title: '样品详情', showLeftButton: true),
+      body: _buildBody(),
+    );
+  }
+
+  Widget _buildBody() {
+    if (_isLoading) {
+      return const Center(child: CircularProgressIndicator());
+    }
+
+    if (_sample == null) {
+      return const Center(
+        child: Text(
+          '无法加载样品信息',
+          style: TextStyle(fontSize: 16, color: Colors.grey),
+        ),
+      );
+    }
+
+    return SingleChildScrollView(
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          _buildSampleInfo(),
+          const Divider(),
+          _buildFlowLogsTitle(),
+          _buildFlowLogsList(),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildSampleInfo() {
+    return Container(
+      width: double.infinity,
+      padding: const EdgeInsets.all(16.0),
+      child: Card(
+        child: Padding(
+          padding: const EdgeInsets.all(16.0),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  Text(
+                    _sample!.sampleName,
+                    style: Theme.of(context).textTheme.titleLarge,
+                  ),
+                  Chip(
+                    label: Text(
+                      _getSampleStatusText(_sample!.sampleStatus),
+                      style: TextStyle(
+                        color: _getSampleStatusColor(_sample!.sampleStatus),
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+              const SizedBox(height: 16),
+              _buildInfoRow('样品编号', _sample!.id.toString()),
+              _buildInfoRow('样品类型', _getSampleTypeText(_sample!.sampleType)),
+              _buildInfoRow('取样批次', _sample!.batchNum),
+              _buildInfoRow('取样翅号', _sample!.wingTagNum),
+              _buildInfoRow(
+                '取样时间',
+                DateTimeUtil.toStandardString(_sample!.sampleTime),
+              ),
+              _buildInfoRow('取样人', _sample!.createByName),
+              const SizedBox(height: 8),
+              Text('样品描述: ${_sample!.description}'),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildInfoRow(String label, String value) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: 4.0),
+      child: Row(
+        children: [
+          SizedBox(
+            width: 80,
+            child: Text(
+              '$label:',
+              style: const TextStyle(fontWeight: FontWeight.bold),
+            ),
+          ),
+          Expanded(child: Text(value)),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildFlowLogsTitle() {
+    return Padding(
+      padding: const EdgeInsets.all(16.0),
+      child: Text('流转记录', style: Theme.of(context).textTheme.titleLarge),
+    );
+  }
+
+  Widget _buildFlowLogsList() {
+    if (_flowLogs.isEmpty) {
+      return const Center(
+        child: Padding(
+          padding: EdgeInsets.all(32.0),
+          child: Text(
+            '暂无流转记录',
+            style: TextStyle(fontSize: 16, color: Colors.grey),
+          ),
+        ),
+      );
+    }
+
+    return ListView.builder(
+      shrinkWrap: true,
+      physics: const NeverScrollableScrollPhysics(),
+      itemCount: _flowLogs.length,
+      itemBuilder: (context, index) {
+        final log = _flowLogs[index];
+        return _buildFlowLogItem(log);
+      },
+    );
+  }
+
+  Widget _buildFlowLogItem(SampleFlowLogModel log) {
+    return Card(
+      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+      child: ListTile(
+        title: Text(log.handlerName),
+        subtitle: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text('查询时间: ${DateTimeUtil.toStandardString(log.handleTime)}'),
+          ],
+        ),
+      ),
+    );
+  }
+
+  String _getSampleStatusText(int status) {
+    switch (status) {
+      case 0:
+        return '待处理';
+      case 1:
+        return '处理中';
+      case 2:
+        return '已完成';
+      default:
+        return '未知状态';
+    }
+  }
+
+  Color _getSampleStatusColor(int status) {
+    switch (status) {
+      case 0:
+        return Colors.orange;
+      case 1:
+        return Colors.blue;
+      case 2:
+        return Colors.green;
+      default:
+        return Colors.grey;
+    }
+  }
+
+  String _getSampleTypeText(int type) {
+    switch (type) {
+      case 1:
+        return '血液样品';
+      case 2:
+        return '组织样品';
+      case 3:
+        return '粪便样品';
+      default:
+        return '未知类型';
+    }
+  }
+}

+ 53 - 0
UI/CF.APP/chicken_farm/lib/pages/sample/sample_query_page.dart

@@ -0,0 +1,53 @@
+import 'package:chicken_farm/routes/app_routes.dart';
+import 'package:flutter/material.dart';
+import 'package:chicken_farm/components/vb_app_bar.dart';
+import 'package:chicken_farm/components/qr_scanner.dart';
+import 'package:go_router/go_router.dart';
+
+class SampleQueryPage extends StatefulWidget {
+  const SampleQueryPage({super.key});
+
+  @override
+  State<SampleQueryPage> createState() => _SampleQueryPageState();
+}
+
+const String _qrCodePrefix = 'vb@sample@/sample/';
+
+class _SampleQueryPageState extends State<SampleQueryPage> {
+  Future<String?> _parseSampleQRCode(String rawContent) async {
+    try {
+      // 替换掉 _qrCodePrefix
+      final content = rawContent.replaceFirst(_qrCodePrefix, '');
+      if (content.isNotEmpty) {
+        // 检查是否能转成int,能转就返回conetent,不能返回null
+        return int.parse(content).toString();
+      }
+      return null;
+    } catch (e) {
+      return null;
+    }
+  }
+
+  void _handleSampleScan(String scannedContent) async {
+    // 跳转到样品详情页面,传递id参数为scannedContent
+    if (mounted) {
+      context.pushNamed(
+        AppRouteNames.sampleDetail,
+        pathParameters: {'id': scannedContent},
+      );
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: const VberAppBar(title: '样品查询', showLeftButton: true),
+      body: QRScannerComponent(
+        startWithString: _qrCodePrefix,
+        onScanCallback: _handleSampleScan,
+        invalidQRMessage: '这不是一个有效的样品二维码',
+        qrCodeParser: _parseSampleQRCode,
+      ),
+    );
+  }
+}

+ 39 - 0
UI/CF.APP/chicken_farm/lib/pages/sample/sample_query_page_test.dart

@@ -0,0 +1,39 @@
+import 'package:chicken_farm/routes/app_routes.dart';
+import 'package:flutter/material.dart';
+import 'package:chicken_farm/components/vb_app_bar.dart';
+import 'package:go_router/go_router.dart';
+
+class SampleQueryPage extends StatefulWidget {
+  const SampleQueryPage({super.key});
+
+  @override
+  State<SampleQueryPage> createState() => _SampleQueryPageState();
+}
+
+class _SampleQueryPageState extends State<SampleQueryPage> {
+  Future<void> _simulateScanAndNavigate() async {
+    // 模拟扫码成功,使用测试ID "1"
+    final String simulatedScannedContent = "1";
+
+    // 跳转到签到记录页面,传递ID参数
+    if (context.mounted) {
+      context.pushNamed(
+        AppRouteNames.sampleDetail,
+        pathParameters: {'id': simulatedScannedContent},
+      );
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: const VberAppBar(title: '样品详情', showLeftButton: true),
+      body: Center(
+        child: ElevatedButton(
+          onPressed: _simulateScanAndNavigate,
+          child: const Text('模拟扫码成功'),
+        ),
+      ),
+    );
+  }
+}

+ 0 - 17
UI/CF.APP/chicken_farm/lib/pages/sample_transfer/sample_transfer_page.dart

@@ -1,17 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:chicken_farm/components/custom_app_bar.dart';
-
-class SampleTransferPage extends StatelessWidget {
-  const SampleTransferPage({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      appBar: const CustomAppBar(
-        title: '样品流转',
-        showBackButton: true, // 子页面显示返回按钮
-      ),
-      body: const Center(child: Text('样品流转页面')),
-    );
-  }
-}

+ 38 - 18
UI/CF.APP/chicken_farm/lib/routes/app_routes.dart

@@ -2,44 +2,64 @@ import 'package:chicken_farm/stores/auth_store.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
-import '../pages/account/login.dart';
-import '../pages/home/home.dart';
+import '../pages/account/login_page.dart';
+import '../pages/home/home_page.dart';
 import '../pages/checkin/checkin_page.dart';
-import '../pages/sample_transfer/sample_transfer_page.dart';
+import '../pages/checkin/checkin_record_page.dart';
+import '../pages/sample/sample_query_page.dart';
+import '../pages/sample/sample_detail_page.dart';
 
-class AppRoutePaths {
+class AppRouteNames {
   static const String splash = '/';
   static const String login = '/login';
   static const String home = '/home';
   static const String checkin = '/checkin';
-  static const String sampleTransfer = '/sample_transfer';
+  static const String checkinRecord = '/checkin_record';
+  static const String sample = '/sample';
+  static const String sampleDetail = '/sample/detail';
 }
 
 class AppRoutes {
   static final List<GoRoute> routes = [
     GoRoute(
-      path: AppRoutePaths.splash,
+      path: AppRouteNames.splash,
       builder: (context, state) => const SplashScreen(),
     ),
     GoRoute(
-      path: AppRoutePaths.login,
-      name: AppRoutePaths.login,
+      path: AppRouteNames.login,
+      name: AppRouteNames.login,
       builder: (context, state) => const LoginPage(),
     ),
     GoRoute(
-      path: AppRoutePaths.home,
-      name: AppRoutePaths.home,
+      path: AppRouteNames.home,
+      name: AppRouteNames.home,
       builder: (context, state) => const HomePage(),
     ),
     GoRoute(
-      path: AppRoutePaths.checkin,
-      name: AppRoutePaths.checkin,
+      path: AppRouteNames.checkin,
+      name: AppRouteNames.checkin,
       builder: (context, state) => const CheckinPage(),
     ),
     GoRoute(
-      path: AppRoutePaths.sampleTransfer,
-      name: AppRoutePaths.sampleTransfer,
-      builder: (context, state) => const SampleTransferPage(),
+      path: '${AppRouteNames.checkinRecord}/:id',
+      name: AppRouteNames.checkinRecord,
+      builder: (context, state) {
+        final id = state.pathParameters['id'] ?? '';
+        return CheckinRecordPage(id: id);
+      },
+    ),
+    GoRoute(
+      path: AppRouteNames.sample,
+      name: AppRouteNames.sample,
+      builder: (context, state) => const SampleQueryPage(),
+    ),
+    GoRoute(
+      path: '${AppRouteNames.sampleDetail}/:id',
+      name: AppRouteNames.sampleDetail,
+      builder: (context, state) {
+        final id = state.pathParameters['id'] ?? '';
+        return SampleDetailPage(id: id);
+      },
     ),
   ];
 }
@@ -52,11 +72,11 @@ class SplashScreen extends ConsumerWidget {
   Widget build(BuildContext context, WidgetRef ref) {
     ref.listen(authStoreProvider, (previous, next) {
       if (next.state == AuthState.authenticated) {
-        context.goNamed(AppRoutePaths.home);
+        context.goNamed(AppRouteNames.home);
       } else if (next.state != AuthState.loading) {
-        context.goNamed(AppRoutePaths.login);
+        context.goNamed(AppRouteNames.login);
       }
     });
     return const Scaffold(body: Center(child: CircularProgressIndicator()));
   }
-}
+}

+ 3 - 6
UI/CF.APP/chicken_farm/lib/routes/route_provider.dart

@@ -1,17 +1,14 @@
-import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'app_routes.dart';
 import 'route_guard.dart';
-
-/// 全局导航键,用于在没有BuildContext的情况下进行导航
-final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
+import '../core/services/navigation_service.dart';
 
 final goRouterProvider = Provider<GoRouter>((ref) {
   return GoRouter(
-    navigatorKey: rootNavigatorKey,
+    navigatorKey: NavigationService.navigatorKey,
     routes: AppRoutes.routes,
     redirect: RouteGuard.redirectLogic,
-    initialLocation: AppRoutePaths.splash,
+    initialLocation: AppRouteNames.splash,
   );
 });

+ 6 - 4
UI/CF.APP/chicken_farm/lib/stores/auth_store.dart

@@ -1,11 +1,11 @@
 import 'package:chicken_farm/apis/index.dart';
 import 'package:chicken_farm/core/config/app_config.dart';
+import 'package:chicken_farm/core/services/navigation_service.dart';
 import 'package:chicken_farm/core/utils/jwt_token.dart';
 import 'package:chicken_farm/core/utils/logger.dart';
 import 'package:chicken_farm/modes/auth/login_model.dart';
 import 'package:chicken_farm/modes/user/user_model.dart';
 import 'package:chicken_farm/routes/app_routes.dart';
-import 'package:chicken_farm/routes/route_provider.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 
@@ -148,9 +148,11 @@ class AuthStore extends StateNotifier<AuthInfo> {
     } finally {
       await JwtToken.clear();
       state = AuthInfo.unauthenticated();
-      if (rootNavigatorKey.currentState != null &&
-          rootNavigatorKey.currentContext != null) {
-        rootNavigatorKey.currentContext!.goNamed(AppRoutePaths.login);
+      if (NavigationService.navigatorKey.currentState != null &&
+          NavigationService.navigatorKey.currentContext != null) {
+        NavigationService.navigatorKey.currentContext!.goNamed(
+          AppRouteNames.login,
+        );
       }
     }
   }

+ 8 - 0
UI/CF.APP/chicken_farm/linux/flutter/generated_plugin_registrant.cc

@@ -6,6 +6,14 @@
 
 #include "generated_plugin_registrant.h"
 
+#include <audioplayers_linux/audioplayers_linux_plugin.h>
+#include <file_selector_linux/file_selector_plugin.h>
 
 void fl_register_plugins(FlPluginRegistry* registry) {
+  g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
+      fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
+  audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
+  g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
+      fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
+  file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
 }

+ 2 - 0
UI/CF.APP/chicken_farm/linux/flutter/generated_plugins.cmake

@@ -3,6 +3,8 @@
 #
 
 list(APPEND FLUTTER_PLUGIN_LIST
+  audioplayers_linux
+  file_selector_linux
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST

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

@@ -5,8 +5,16 @@
 import FlutterMacOS
 import Foundation
 
+import audioplayers_darwin
+import file_selector_macos
+import mobile_scanner
+import path_provider_foundation
 import shared_preferences_foundation
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+  AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
+  FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
+  MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
+  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
 }

+ 9 - 2
UI/CF.APP/chicken_farm/pubspec.yaml

@@ -49,6 +49,12 @@ dependencies:
   fluttertoast: ^8.2.2   # 弹窗
   shared_preferences: ^2.2.2 # 本地存储
   getwidget: ^7.0.0
+  flutter_scankit: ^2.0.6
+  mobile_scanner: ^7.1.3
+  audioplayers: ^6.5.1
+  intl: ^0.19.0
+  image_picker: ^1.2.1
+  path_provider: ^2.1.5
 
 dev_dependencies:
   flutter_test:
@@ -74,12 +80,13 @@ flutter:
   # included with your application, so that you can use the icons in
   # the material Icons class.
   uses-material-design: true
-
+  
   # To add assets to your application, add an assets section, like this:
   # assets:
   #   - images/a_dot_burr.jpeg
   #   - images/a_dot_ham.jpeg
-
+  assets:
+    - assets/sounds/beep.mp3
   # An image asset can refer to one or more resolution-specific "variants", see
   # https://flutter.dev/to/resolution-aware-images
 

+ 6 - 0
UI/CF.APP/chicken_farm/windows/flutter/generated_plugin_registrant.cc

@@ -6,6 +6,12 @@
 
 #include "generated_plugin_registrant.h"
 
+#include <audioplayers_windows/audioplayers_windows_plugin.h>
+#include <file_selector_windows/file_selector_windows.h>
 
 void RegisterPlugins(flutter::PluginRegistry* registry) {
+  AudioplayersWindowsPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
+  FileSelectorWindowsRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("FileSelectorWindows"));
 }

+ 2 - 0
UI/CF.APP/chicken_farm/windows/flutter/generated_plugins.cmake

@@ -3,6 +3,8 @@
 #
 
 list(APPEND FLUTTER_PLUGIN_LIST
+  audioplayers_windows
+  file_selector_windows
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST