Browse Source

Add 增加一些下拉框组件,优化代码

Yue 1 week ago
parent
commit
2be428fa64
25 changed files with 1625 additions and 128 deletions
  1. 234 0
      UI/CF.APP/chicken_farm/lib/apis/breeding/_batch.dart
  2. 11 0
      UI/CF.APP/chicken_farm/lib/apis/breeding/index.dart
  3. 3 1
      UI/CF.APP/chicken_farm/lib/apis/index.dart
  4. 15 19
      UI/CF.APP/chicken_farm/lib/components/vb_dict_label.dart
  5. 63 0
      UI/CF.APP/chicken_farm/lib/components/vb_dict_select.dart
  6. 4 4
      UI/CF.APP/chicken_farm/lib/components/vb_qr_scanner.dart
  7. 496 0
      UI/CF.APP/chicken_farm/lib/components/vb_search_select.dart
  8. 186 0
      UI/CF.APP/chicken_farm/lib/components/vb_select.dart
  9. 0 29
      UI/CF.APP/chicken_farm/lib/core/api/api_service.dart
  10. 38 0
      UI/CF.APP/chicken_farm/lib/modes/breeding/batch.dart
  11. 40 0
      UI/CF.APP/chicken_farm/lib/modes/breeding/batch.g.dart
  12. 31 0
      UI/CF.APP/chicken_farm/lib/modes/breeding/family.dart
  13. 30 0
      UI/CF.APP/chicken_farm/lib/modes/breeding/family.g.dart
  14. 17 0
      UI/CF.APP/chicken_farm/lib/modes/breeding/wing_tag_num.dart
  15. 21 0
      UI/CF.APP/chicken_farm/lib/modes/breeding/wing_tag_num.g.dart
  16. 15 0
      UI/CF.APP/chicken_farm/lib/modes/menu_item.dart
  17. 8 0
      UI/CF.APP/chicken_farm/lib/modes/page/page_model.dart
  18. 0 2
      UI/CF.APP/chicken_farm/lib/pages/account/login_page.dart
  19. 341 0
      UI/CF.APP/chicken_farm/lib/pages/breeding/bind_wing_tag_page.dart
  20. 2 2
      UI/CF.APP/chicken_farm/lib/pages/checkin/checkin_page.dart
  21. 15 25
      UI/CF.APP/chicken_farm/lib/pages/home/menu_buttons.dart
  22. 20 44
      UI/CF.APP/chicken_farm/lib/pages/sample/sample_detail_page.dart
  23. 2 2
      UI/CF.APP/chicken_farm/lib/pages/sample/sample_query_page.dart
  24. 7 0
      UI/CF.APP/chicken_farm/lib/routes/app_routes.dart
  25. 26 0
      UI/CF.APP/chicken_farm/lib/stores/menu_store.dart

+ 234 - 0
UI/CF.APP/chicken_farm/lib/apis/breeding/_batch.dart

@@ -0,0 +1,234 @@
+import 'dart:math';
+
+import 'package:chicken_farm/modes/breeding/batch.dart';
+import 'package:chicken_farm/modes/breeding/family.dart';
+import 'package:chicken_farm/modes/breeding/wing_tag_num.dart';
+import 'package:chicken_farm/modes/page/page_model.dart';
+
+class BatchApi {
+  static final BatchApi _instance = BatchApi._internal();
+
+  factory BatchApi() => _instance;
+
+  BatchApi._internal();
+
+  // 预生成的批次模拟数据,仅包含必填字段
+  static final List<BatchModel> _mockBatchData = [
+    BatchModel(batchNum: 'BATCH001', batchName: '批次一号'),
+    BatchModel(batchNum: 'BATCH002', batchName: '批次二号'),
+    BatchModel(batchNum: 'BATCH003', batchName: '批次三号'),
+    BatchModel(batchNum: 'BATCH004', batchName: '批次四号'),
+    BatchModel(batchNum: 'BATCH005', batchName: '批次五号'),
+    BatchModel(batchNum: 'BATCH006', batchName: '批次六号'),
+    BatchModel(batchNum: 'BATCH007', batchName: '批次七号'),
+    BatchModel(batchNum: 'BATCH008', batchName: '批次八号'),
+    BatchModel(batchNum: 'BATCH009', batchName: '批次九号'),
+    BatchModel(batchNum: 'BATCH010', batchName: '批次十号'),
+    BatchModel(batchNum: 'BATCH011', batchName: '批次十一号'),
+    BatchModel(batchNum: 'BATCH012', batchName: '批次十二号'),
+    BatchModel(batchNum: 'BATCH013', batchName: '批次十三号'),
+    BatchModel(batchNum: 'BATCH014', batchName: '批次十四号'),
+    BatchModel(batchNum: 'BATCH015', batchName: '批次十五号'),
+    BatchModel(batchNum: 'BATCH016', batchName: '批次十六号'),
+    BatchModel(batchNum: 'BATCH017', batchName: '批次十七号'),
+    BatchModel(batchNum: 'BATCH018', batchName: '批次十八号'),
+    BatchModel(batchNum: 'BATCH019', batchName: '批次十九号'),
+    BatchModel(batchNum: 'BATCH020', batchName: '批次二十号'),
+  ];
+
+  // 预生成的翅号模拟数据,仅包含必填字段
+  static final List<WingTagNumModel> _mockWingTagData = [
+    WingTagNumModel(wingTagNum: 'WING0001'),
+    WingTagNumModel(wingTagNum: 'WING0002'),
+    WingTagNumModel(wingTagNum: 'WING0003'),
+    WingTagNumModel(wingTagNum: 'WING0004'),
+    WingTagNumModel(wingTagNum: 'WING0005'),
+    WingTagNumModel(wingTagNum: 'WING0006'),
+    WingTagNumModel(wingTagNum: 'WING0007'),
+    WingTagNumModel(wingTagNum: 'WING0008'),
+    WingTagNumModel(wingTagNum: 'WING0009'),
+    WingTagNumModel(wingTagNum: 'WING0010'),
+    WingTagNumModel(wingTagNum: 'WING0011'),
+    WingTagNumModel(wingTagNum: 'WING0012'),
+    WingTagNumModel(wingTagNum: 'WING0013'),
+    WingTagNumModel(wingTagNum: 'WING0014'),
+    WingTagNumModel(wingTagNum: 'WING0015'),
+    WingTagNumModel(wingTagNum: 'WING0016'),
+    WingTagNumModel(wingTagNum: 'WING0017'),
+    WingTagNumModel(wingTagNum: 'WING0018'),
+    WingTagNumModel(wingTagNum: 'WING0019'),
+    WingTagNumModel(wingTagNum: 'WING0020'),
+  ];
+
+  // 预生成的家系模拟数据,仅包含必填字段
+  static final List<FamilyModel> _mockFamilyData = [
+    FamilyModel(id: 1, familyNum: 'FAM001'),
+    FamilyModel(id: 2, familyNum: 'FAM002'),
+    FamilyModel(id: 3, familyNum: 'FAM003'),
+    FamilyModel(id: 4, familyNum: 'FAM004'),
+    FamilyModel(id: 5, familyNum: 'FAM005'),
+    FamilyModel(id: 6, familyNum: 'FAM006'),
+    FamilyModel(id: 7, familyNum: 'FAM007'),
+    FamilyModel(id: 8, familyNum: 'FAM008'),
+    FamilyModel(id: 9, familyNum: 'FAM009'),
+    FamilyModel(id: 10, familyNum: 'FAM010'),
+    FamilyModel(id: 11, familyNum: 'FAM011'),
+    FamilyModel(id: 12, familyNum: 'FAM012'),
+    FamilyModel(id: 13, familyNum: 'FAM013'),
+    FamilyModel(id: 14, familyNum: 'FAM014'),
+    FamilyModel(id: 15, familyNum: 'FAM015'),
+    FamilyModel(id: 16, familyNum: 'FAM016'),
+    FamilyModel(id: 17, familyNum: 'FAM017'),
+    FamilyModel(id: 18, familyNum: 'FAM018'),
+    FamilyModel(id: 19, familyNum: 'FAM019'),
+    FamilyModel(id: 20, familyNum: 'FAM020'),
+  ];
+
+  Future<PageResultModel<BatchModel>> queryPageBatchs(dynamic query) async {
+    // 模拟数据 - 注释掉原来的API调用
+    /*final response = await ApiService().get(
+      '/app/breeding/listBatch',
+      queryParameters: query,
+    );
+    if (response == null) return PageResultModel.empty();
+    final List<BatchModel> rows = (response['rows'] as List)
+        .map((e) => BatchModel.fromJson(e))
+        .toList();
+    return PageResultModel<BatchModel>(rows: rows, total: response['total']);*/
+
+    // 使用预生成的模拟数据
+    // 根据keyword筛选数据
+    List<BatchModel> filteredData = _mockBatchData;
+    if (query != null && query['keyword'] != null && query['keyword'].toString().isNotEmpty) {
+      final keyword = query['keyword'].toString().toLowerCase();
+      filteredData = _mockBatchData.where((batch) => 
+        batch.batchNum.toLowerCase().contains(keyword)).toList();
+    }
+
+    int page = query != null && query['pageNum'] != null
+        ? query['pageNum'] as int
+        : 1;
+    int pageSize = query != null && query['pageSize'] != null
+        ? query['pageSize'] as int
+        : 10;
+    int startIndex = (page - 1) * pageSize;
+    int endIndex = startIndex + pageSize < filteredData.length
+        ? startIndex + pageSize
+        : filteredData.length;
+
+    List<BatchModel> pageRows = startIndex < filteredData.length
+        ? filteredData.sublist(startIndex, endIndex)
+        : [];
+
+    return PageResultModel<BatchModel>(
+      rows: pageRows,
+      total: filteredData.length,
+    );
+  }
+
+  Future<PageResultModel<WingTagNumModel>> queryPageWingTags(
+    dynamic query,
+  ) async {
+    // 模拟数据 - 注释掉原来的API调用
+    /*final response = await ApiService().get(
+      '/app/breeding/listWingTag',
+      queryParameters: query,
+    );
+    if (response == null) return PageResultModel.empty();
+    final List<WingTagNumModel> rows = (response['rows'] as List)
+        .map((e) => WingTagNumModel.fromJson(e))
+        .toList();
+    return PageResultModel<WingTagNumModel>(
+      rows: rows,
+      total: response['total'],
+    );*/
+
+    // 使用预生成的模拟数据
+    int page = query != null && query['pageNum'] != null
+        ? query['pageNum'] as int
+        : 1;
+    int pageSize = query != null && query['pageSize'] != null
+        ? query['pageSize'] as int
+        : 10;
+    int startIndex = (page - 1) * pageSize;
+    int endIndex = startIndex + pageSize < _mockWingTagData.length
+        ? startIndex + pageSize
+        : _mockWingTagData.length;
+
+    List<WingTagNumModel> pageRows = startIndex < _mockWingTagData.length
+        ? _mockWingTagData.sublist(startIndex, endIndex)
+        : [];
+
+    return PageResultModel<WingTagNumModel>(
+      rows: pageRows,
+      total: _mockWingTagData.length,
+    );
+  }
+
+  Future<List<WingTagNumModel>> queryWingTags(dynamic query) async {
+    // 模拟数据 - 注释掉原来的API调用
+    /*final response = await ApiService().get(
+      '/app/breeding/listWingTag',
+      queryParameters: query,
+    );
+    if (response == null) return [];
+    return response
+        .map<WingTagNumModel>((e) => WingTagNumModel.fromJson(e))
+        .toList();*/
+
+    // 使用预生成的模拟数据,随机返回5-10条
+    final random = Random();
+    final count = 5 + random.nextInt(6); // 5到10之间的随机数
+    final indices = <int>{};
+    
+    // 随机选择不重复的索引
+    while (indices.length < count) {
+      indices.add(random.nextInt(_mockWingTagData.length));
+    }
+    
+    // 根据索引获取数据
+    return indices.map((index) => _mockWingTagData[index]).toList();
+  }
+
+  Future<PageResultModel<FamilyModel>> queryPageFamilys(dynamic query) async {
+    // 模拟数据 - 注释掉原来的API调用
+    /*final response = await ApiService().get(
+      '/app/breeding/listFamily',
+      queryParameters: query,
+    );
+    if (response == null) return PageResultModel.empty();
+    final List<FamilyModel> rows = (response['rows'] as List)
+        .map((e) => FamilyModel.fromJson(e))
+        .toList();
+    return PageResultModel<FamilyModel>(rows: rows, total: response['total']);*/
+
+    // 使用预生成的模拟数据
+    // 根据keyword筛选数据
+    List<FamilyModel> filteredData = _mockFamilyData;
+    if (query != null && query['keyword'] != null && query['keyword'].toString().isNotEmpty) {
+      final keyword = query['keyword'].toString().toLowerCase();
+      filteredData = _mockFamilyData.where((family) => 
+        family.familyNum.toLowerCase().contains(keyword)).toList();
+    }
+
+    int page = query != null && query['pageNum'] != null
+        ? query['pageNum'] as int
+        : 1;
+    int pageSize = query != null && query['pageSize'] != null
+        ? query['pageSize'] as int
+        : 10;
+    int startIndex = (page - 1) * pageSize;
+    int endIndex = startIndex + pageSize < filteredData.length
+        ? startIndex + pageSize
+        : filteredData.length;
+
+    List<FamilyModel> pageRows = startIndex < filteredData.length
+        ? filteredData.sublist(startIndex, endIndex)
+        : [];
+
+    return PageResultModel<FamilyModel>(
+      rows: pageRows,
+      total: filteredData.length,
+    );
+  }
+}

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

@@ -0,0 +1,11 @@
+import '_batch.dart';
+
+class BreedingApis {
+  static final BreedingApis _instance = BreedingApis._internal();
+
+  factory BreedingApis() => _instance;
+
+  BreedingApis._internal();
+
+  late final BatchApi batchApi = BatchApi();
+}

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

@@ -2,6 +2,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';
+import 'package:chicken_farm/apis/breeding/index.dart';
 
 class Apis {
   static final Apis _instance = Apis._internal();
@@ -14,6 +15,7 @@ class Apis {
   late final SystemApis system = SystemApis();
   late final DeviceApis device = DeviceApis();
   late final ExperimentApis experiment = ExperimentApis();
+  late final BreedingApis breeding = BreedingApis();
 }
 
-final apis = Apis();
+final apis = Apis();

+ 15 - 19
UI/CF.APP/chicken_farm/lib/components/dict_label.dart → UI/CF.APP/chicken_farm/lib/components/vb_dict_label.dart

@@ -2,15 +2,11 @@ import 'package:flutter/material.dart';
 import 'package:chicken_farm/stores/dict_stroe.dart';
 import 'package:chicken_farm/modes/system/dict.dart';
 
-class DictLabel extends StatelessWidget {
+class VberDictLabel extends StatelessWidget {
   final String dictType;
   final String value;
-  
-  const DictLabel({
-    Key? key,
-    required this.dictType,
-    required this.value,
-  }) : super(key: key);
+
+  const VberDictLabel({super.key, required this.dictType, required this.value});
 
   @override
   Widget build(BuildContext context) {
@@ -24,12 +20,12 @@ class DictLabel extends StatelessWidget {
             child: CircularProgressIndicator(strokeWidth: 2),
           );
         }
-        
+
         if (snapshot.hasData && snapshot.data != null) {
           DictDataModel dict = snapshot.data!;
           Color backgroundColor = _getColorByListClass(dict.listClass);
           Color textColor = _getTextColorByListClass(dict.listClass);
-          
+
           return Container(
             padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
             decoration: BoxDecoration(
@@ -46,7 +42,7 @@ class DictLabel extends StatelessWidget {
             ),
           );
         }
-        
+
         return Container(
           padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
           decoration: BoxDecoration(
@@ -65,24 +61,24 @@ class DictLabel extends StatelessWidget {
       },
     );
   }
-  
+
   Color _getColorByListClass(String? listClass) {
     switch (listClass) {
       case 'primary':
-        return Colors.blue.withOpacity(0.1);
+        return Colors.blue.withValues(alpha: 0.1);
       case 'success':
-        return Colors.green.withOpacity(0.1);
+        return Colors.green.withValues(alpha: 0.1);
       case 'danger':
-        return Colors.red.withOpacity(0.1);
+        return Colors.red.withValues(alpha: 0.1);
       case 'warning':
-        return Colors.orange.withOpacity(0.1);
+        return Colors.orange.withValues(alpha: 0.1);
       case 'info':
-        return Colors.cyan.withOpacity(0.1);
+        return Colors.cyan.withValues(alpha: 0.1);
       default:
-        return Colors.grey.withOpacity(0.1);
+        return Colors.grey.withValues(alpha: 0.1);
     }
   }
-  
+
   Color _getTextColorByListClass(String? listClass) {
     switch (listClass) {
       case 'primary':
@@ -99,4 +95,4 @@ class DictLabel extends StatelessWidget {
         return Colors.grey;
     }
   }
-}
+}

+ 63 - 0
UI/CF.APP/chicken_farm/lib/components/vb_dict_select.dart

@@ -0,0 +1,63 @@
+import 'package:flutter/material.dart';
+import 'package:chicken_farm/components/vb_select.dart';
+import 'package:chicken_farm/stores/dict_stroe.dart';
+import 'package:chicken_farm/modes/system/dict.dart';
+
+/// 基于字典数据的选择器组件
+/// 传入dict_type获取字典数据并显示为下拉选项
+class VberDictSelect extends StatefulWidget {
+  /// 字典类型
+  final String dictType;
+
+  /// 当前选中的值
+  final String? value;
+
+  /// 值改变回调
+  final Function(String?)? onChanged;
+
+  /// 提示文字
+  final String? hint;
+
+  /// 是否启用
+  final bool enabled;
+
+  /// 是否显示"全部"选项
+  final bool showAll;
+
+  const VberDictSelect({
+    super.key,
+    required this.dictType,
+    this.value,
+    this.onChanged,
+    this.hint,
+    this.enabled = true,
+    this.showAll = false,
+  });
+
+  @override
+  State<VberDictSelect> createState() => _VberDictSelectState();
+}
+
+class _VberDictSelectState extends State<VberDictSelect> {
+  @override
+  Widget build(BuildContext context) {
+    return VberSelect<DictDataModel>(
+      fetchData: () async {
+        final dictList = await DictStore().getDictByType(widget.dictType);
+        return dictList ?? [];
+      },
+      converter: (DictDataModel data) {
+        return SelectOption(
+          label: data.dictLabel,
+          value: data.dictValue,
+          extra: data,
+        );
+      },
+      value: widget.value, // 直接传递widget.value以确保更新
+      onChanged: widget.onChanged,
+      hint: widget.hint,
+      enabled: widget.enabled,
+      showAll: widget.showAll,
+    );
+  }
+}

+ 4 - 4
UI/CF.APP/chicken_farm/lib/components/qr_scanner.dart → UI/CF.APP/chicken_farm/lib/components/vb_qr_scanner.dart

@@ -7,13 +7,13 @@ import 'package:audioplayers/audioplayers.dart';
 typedef OnScanCallback = void Function(String scannedContent);
 typedef QRCodeParser = Future<String?> Function(String rawContent);
 
-class QRScannerComponent extends StatefulWidget {
+class VberQRScanner extends StatefulWidget {
   final String? startWithString;
   final OnScanCallback onScanCallback;
   final String? invalidQRMessage;
   final QRCodeParser? qrCodeParser;
 
-  const QRScannerComponent({
+  const VberQRScanner({
     super.key,
     this.startWithString,
     required this.onScanCallback,
@@ -22,10 +22,10 @@ class QRScannerComponent extends StatefulWidget {
   });
 
   @override
-  State<QRScannerComponent> createState() => _QRScannerComponentState();
+  State<VberQRScanner> createState() => _VberQRScannerState();
 }
 
-class _QRScannerComponentState extends State<QRScannerComponent> {
+class _VberQRScannerState extends State<VberQRScanner> {
   MobileScannerController? _cameraController;
   bool _isScanning = false;
   bool _isTorchOn = false;

+ 496 - 0
UI/CF.APP/chicken_farm/lib/components/vb_search_select.dart

@@ -0,0 +1,496 @@
+import 'package:chicken_farm/components/vb_select.dart';
+import 'package:chicken_farm/core/utils/logger.dart';
+import 'package:flutter/material.dart';
+
+/// 搜索API函数签名
+/// T 为数据类型
+typedef SearchApiFunction<T> =
+    Future<Map<String, dynamic>> Function({
+      required Map<String, dynamic> queryParams,
+    });
+
+/// 选项转换器,将数据类型T转换为SelectOption
+typedef SelectOptionConverter<T> = SelectOption Function(T data);
+
+/// 搜索选择组件
+class VberSearchSelect<T> extends StatefulWidget {
+  /// 搜索API函数
+  final SearchApiFunction<T> searchApi;
+
+  /// 数据转换方法,将原始数据转换为SelectOption
+  final SelectOptionConverter<T> converter;
+
+  /// 根据value获取对应label的函数
+  final Future<String?> Function(String?)? getValueLabel;
+
+  /// 当前选中的值
+  final String? value;
+
+  /// 值改变回调
+  final ValueChanged<String?>? onChanged;
+
+  /// 提示文字
+  final String? hint;
+
+  /// 是否启用
+  final bool enabled;
+
+  /// 每页数据条数
+  final int pageSize;
+
+  /// 额外参数,用于传递给搜索API
+  final Map<String, dynamic>? extraParams;
+
+  const VberSearchSelect({
+    super.key,
+    required this.searchApi,
+    required this.converter,
+    this.getValueLabel,
+    this.value,
+    this.onChanged,
+    this.hint,
+    this.enabled = true,
+    this.pageSize = 3,
+    this.extraParams,
+  });
+
+  @override
+  State<VberSearchSelect<T>> createState() => _VberSearchSelectState<T>();
+}
+
+class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
+    with SingleTickerProviderStateMixin {
+  final TextEditingController _textController = TextEditingController();
+  final TextEditingController _searchController = TextEditingController();
+  final FocusNode _focusNode = FocusNode();
+  final FocusNode _searchFocusNode = FocusNode(); // 添加搜索框的FocusNode
+  final LayerLink _layerLink = LayerLink();
+  OverlayEntry? _overlayEntry;
+  List<T> _items = [];
+  bool _isLoading = false;
+  bool _isOverlayVisible = false;
+  String? _selectedValue;
+  SelectOption? _selectedOption; // 新增:用来存储选中的选项
+  int _currentPage = 1;
+  bool _hasMore = true;
+  String _lastSearchText = '';
+
+  @override
+  void initState() {
+    super.initState();
+    _selectedValue = widget.value;
+    _initTextController();
+    _focusNode.addListener(_onFocusChange);
+  }
+
+  /// 初始化文本控制器
+  void _initTextController() async {
+    if (widget.value != null && widget.getValueLabel != null) {
+      try {
+        final label = await widget.getValueLabel!(widget.value);
+        if (mounted) {
+          setState(() {
+            _textController.text = label ?? widget.value ?? '';
+          });
+        }
+      } catch (e) {
+        logger.e('Failed to get label for value: ${widget.value}', e);
+      }
+    }
+  }
+
+  @override
+  void didUpdateWidget(covariant VberSearchSelect<T> oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    // 当外部传入的value发生变化时,同步更新内部状态
+    if (oldWidget.value != widget.value) {
+      _syncValueAndText(widget.value);
+    }
+  }
+
+  /// 同步外部value和内部_selectedValue以及_textController.text
+  void _syncValueAndText(String? newValue) async {
+    setState(() {
+      _selectedValue = newValue;
+    });
+
+    // 优先使用存储的选项
+    String? label;
+    if (_selectedOption != null && _selectedOption!.value == newValue) {
+      label = _selectedOption!.label;
+    }
+
+    // 尝试在现有选项中查找匹配的标签
+    if (label == null && newValue != null) {
+      for (var item in _items) {
+        final option = widget.converter(item);
+        if (option.value == newValue) {
+          label = option.label;
+          break;
+        }
+      }
+    }
+
+    // 如果在现有选项中没找到,且提供了getValueLabel函数,则尝试通过它获取
+    if (label == null && newValue != null && widget.getValueLabel != null) {
+      try {
+        label = await widget.getValueLabel!(newValue);
+      } catch (e) {
+        logger.e('Failed to get label for value: $newValue', e);
+      }
+    }
+
+    // 更新文本显示控件
+    if (mounted) {
+      setState(() {
+        // 优先级: 找到的label > value本身 > 空字符串
+        _textController.text = label ?? newValue ?? '';
+      });
+    }
+  }
+
+  @override
+  void dispose() {
+    _textController.dispose();
+    _searchController.dispose();
+    _focusNode.removeListener(_onFocusChange);
+    _focusNode.dispose();
+    _searchFocusNode.dispose(); // 释放搜索框的FocusNode
+    _overlayEntry?.remove();
+    super.dispose();
+  }
+
+  void _onFocusChange() {
+    // 只有当_focusNode获得焦点且组件启用时才显示弹出层
+    if (_focusNode.hasFocus && widget.enabled) {
+      // 添加一个微小的延迟,避免焦点竞争
+      Future.microtask(() {
+        if (_focusNode.hasFocus && mounted) {
+          _showOverlay();
+        }
+      });
+    }
+  }
+
+  void _showOverlay() {
+    if (_isOverlayVisible) return;
+
+    _overlayEntry = _createOverlayEntry();
+    Overlay.of(context).insert(_overlayEntry!);
+    _isOverlayVisible = true;
+
+    // 清空搜索框
+    _searchController.clear();
+    _items.clear();
+    _currentPage = 1;
+    _hasMore = true;
+    _lastSearchText = '';
+
+    _performSearch('', (callback) => setState(() => callback()));
+
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      if (_searchFocusNode.canRequestFocus && mounted) {
+        _searchFocusNode.requestFocus();
+      }
+    });
+  }
+
+  void _hideOverlay() {
+    _overlayEntry?.remove();
+    _overlayEntry = null;
+    _isOverlayVisible = false;
+    _searchFocusNode.unfocus();
+    _lastSearchText = '';
+    _items.clear();
+  }
+
+  OverlayEntry _createOverlayEntry() {
+    // 获取选择框的大小和位置
+    final renderBox = context.findRenderObject() as RenderBox;
+    final size = renderBox.size;
+    final offset = renderBox.localToGlobal(Offset.zero);
+
+    return OverlayEntry(
+      builder: (context) => Positioned(
+        left: offset.dx,
+        top: offset.dy + size.height + 5,
+        width: size.width,
+        child: Material(
+          elevation: 4,
+          borderRadius: BorderRadius.circular(4),
+          child: Container(
+            constraints: BoxConstraints(
+              maxHeight: MediaQuery.of(context).size.height * 0.4,
+            ),
+            child: StatefulBuilder(
+              builder: (context, setState) {
+                return Column(
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    Padding(
+                      padding: const EdgeInsets.all(16.0),
+                      child: TextField(
+                        key: ValueKey('search_text_field_${widget.hint ?? ''}_${UniqueKey().toString()}'),
+                        controller: _searchController,
+                        focusNode: _searchFocusNode,
+                        autofocus: false,
+                        decoration: InputDecoration(
+                          hintText: '请输入${widget.hint}搜索关键词',
+                          contentPadding: EdgeInsets.symmetric(
+                            horizontal: 12,
+                            vertical: 8,
+                          ),
+                          border: OutlineInputBorder(
+                            borderRadius: BorderRadius.circular(4),
+                          ),
+                          isDense: true,
+                        ),
+                        onChanged: (value) {
+                          Future.delayed(const Duration(milliseconds: 300), () {
+                            if (_lastSearchText != _searchController.text) {
+                              _lastSearchText = _searchController.text;
+                              _items.clear();
+                              _currentPage = 1;
+                              _hasMore = true;
+                              _performSearch(_searchController.text, setState);
+                            } else {}
+                          });
+                        },
+                      ),
+                    ),
+                    if (_isLoading && _items.isEmpty)
+                      Padding(
+                        padding: const EdgeInsets.all(16.0),
+                        child: Center(child: CircularProgressIndicator()),
+                      )
+                    else if (!_isLoading && _items.isEmpty)
+                      Padding(
+                        padding: const EdgeInsets.all(16.0),
+                        child: Center(
+                          child: Text(
+                            '没有查询到结果',
+                            style: TextStyle(color: Colors.grey, fontSize: 14),
+                          ),
+                        ),
+                      )
+                    else
+                      Flexible(
+                        child: NotificationListener<ScrollNotification>(
+                          onNotification: (notification) {
+                            if (notification.metrics.pixels ==
+                                    notification.metrics.maxScrollExtent &&
+                                _hasMore &&
+                                !_isLoading) {
+                              _loadMore(setState);
+                              return true;
+                            }
+                            return false;
+                          },
+                          child: Builder(
+                            builder: (BuildContext context) {
+                              return ListView.builder(
+                                padding: EdgeInsets.zero,
+                                shrinkWrap: true,
+                                itemCount: _items.length + (_hasMore ? 1 : 0),
+                                itemBuilder: (context, index) {
+                                  if (index == _items.length) {
+                                    // 显示加载更多指示器
+                                    return Padding(
+                                      padding: const EdgeInsets.all(12.0),
+                                      child: Center(
+                                        child: _isLoading
+                                            ? CircularProgressIndicator()
+                                            : Text('上拉加载更多'),
+                                      ),
+                                    );
+                                  }
+                                  final item = _items[index];
+                                  final option = widget.converter(item);
+                                  return ListTile(
+                                    title: Text(option.label),
+                                    onTap: () => _selectItem(option, item),
+                                    dense: true,
+                                    contentPadding: EdgeInsets.symmetric(
+                                      horizontal: 16,
+                                      vertical: 4,
+                                    ),
+                                    visualDensity: VisualDensity.compact,
+                                  );
+                                },
+                              );
+                            },
+                          ),
+                        ),
+                      ),
+                  ],
+                );
+              },
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  void _loadMore(StateSetter overlaySetState) async {
+    if (!_hasMore || _isLoading) return;
+
+    overlaySetState(() {
+      _isLoading = true;
+    });
+
+    try {
+      _currentPage++;
+
+      final result = await _fetchData(_currentPage);
+
+      final List<dynamic> rows = result['rows'] is List ? result['rows'] : [];
+      final int total = result['total'] is int ? result['total'] : 0;
+      final newItems = rows.cast<T>().toList();
+
+      if (mounted) {
+        overlaySetState(() {
+          _items.addAll(newItems);
+          _hasMore = _items.length < total;
+          _isLoading = false;
+        });
+      }
+    } catch (e) {
+      logger.e('加载更多失败:$e');
+      if (mounted) {
+        overlaySetState(() {
+          _currentPage--; // 回退页码
+          _isLoading = false;
+        });
+      }
+    }
+  }
+
+  void _performSearch(String keyword, StateSetter overlaySetState) async {
+    overlaySetState(() {
+      _isLoading = true;
+    });
+
+    try {
+      _currentPage = 1;
+      _hasMore = true;
+
+      final result = await _fetchData(_currentPage, keyword: keyword);
+
+      final List<dynamic> rows = result['rows'] is List ? result['rows'] : [];
+      final int total = result['total'] is int ? result['total'] : 0;
+      final newItems = rows.cast<T>().toList();
+
+      if (mounted) {
+        overlaySetState(() {
+          _items = newItems;
+          _hasMore = _items.length < total;
+          _isLoading = false;
+        });
+      }
+    } catch (e) {
+      logger.e('搜索失败:$e');
+      if (mounted) {
+        overlaySetState(() {
+          _items = [];
+          _hasMore = false;
+          _isLoading = false;
+        });
+      }
+    }
+  }
+
+  Future<Map<String, dynamic>> _fetchData(
+    int pageNum, {
+    String? keyword,
+  }) async {
+    final Map<String, dynamic> queryParams = {
+      'pageSize': widget.pageSize,
+      'pageNum': pageNum,
+    };
+
+    if (keyword != null) {
+      queryParams['keyword'] = keyword;
+    } else if (keyword == null && _searchController.text.isNotEmpty) {
+      // 如果keyword参数为null但_searchController中有内容,则使用该内容
+      queryParams['keyword'] = _searchController.text;
+    }
+
+    if (widget.extraParams != null) {
+      queryParams.addAll(widget.extraParams!);
+    }
+
+    final r = await widget.searchApi(queryParams: queryParams);
+    return r;
+  }
+
+  void _selectItem(SelectOption option, T item) {
+    setState(() {
+      _textController.text = option.label;
+      _selectedValue = option.value;
+      _selectedOption = option; // 保存选中的选项
+      
+      // 检查当前选中的项是否已经在列表中,如果不在则添加到列表开头
+      // 这样可以在后续通过value同步label时找到对应的项
+      bool itemExists = _items.any((element) {
+        final elementOption = widget.converter(element);
+        return elementOption.value == option.value;
+      });
+      
+      if (!itemExists) {
+        // 将选中项插入到列表开头,方便快速查找
+        _items.insert(0, item);
+      }
+    });
+
+    widget.onChanged?.call(option.value);
+    _hideOverlay();
+    // 不需要在下一帧失去焦点,立即失去焦点可能会导致一些问题
+    // 使用post frame callback确保overlay完全隐藏后再失去焦点
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      if (mounted) {
+        _focusNode.unfocus();
+      }
+    });
+  }
+
+  void _clearSelection() {
+    setState(() {
+      _textController.clear();
+      _selectedValue = null;
+      _selectedOption = null; // 清除保存的选项
+    });
+
+    widget.onChanged?.call(null);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTap: () {
+        // 点击时请求焦点
+        if (widget.enabled) {
+          _focusNode.requestFocus();
+        }
+      },
+      child: CompositedTransformTarget(
+        link: _layerLink,
+        child: TextField(
+          controller: _textController,
+          focusNode: _focusNode,
+          readOnly: true,
+          enabled: widget.enabled,
+          decoration: InputDecoration(
+            hintText: '请选择${widget.hint ?? ""}...',
+            suffixIcon: _selectedValue != null
+                ? IconButton(
+                    icon: Icon(Icons.clear, size: 20),
+                    onPressed: _clearSelection,
+                  )
+                : Icon(Icons.arrow_drop_down),
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 186 - 0
UI/CF.APP/chicken_farm/lib/components/vb_select.dart

@@ -0,0 +1,186 @@
+import 'package:flutter/material.dart';
+
+/// 数据转换函数签名
+/// 将原始数据转换为SelectOption类型的函数
+typedef SelectOptionConverter<T> = SelectOption Function(T data);
+
+/// 选择器选项数据模型
+class SelectOption {
+  final String label;
+  final String value;
+  final dynamic extra;
+
+  SelectOption({required this.label, required this.value, this.extra});
+}
+
+/// 通用选择器组件
+/// 支持静态数据源和动态API数据源
+class VberSelect<T> extends StatefulWidget {
+  /// 静态数据列表
+  final List<T>? items;
+
+  /// API获取数据的方法
+  final Future<List<T>> Function()? fetchData;
+
+  /// 数据转换方法,将原始数据转换为SelectOption
+  final SelectOptionConverter<T> converter;
+
+  /// 当前选中的值
+  final String? value;
+
+  /// 值改变回调
+  final Function(String?)? onChanged;
+
+  /// 提示文字
+  final String? hint;
+
+  /// 是否启用
+  final bool enabled;
+
+  /// 是否显示"全部"选项
+  final bool showAll;
+
+  const VberSelect({
+    super.key,
+    this.items,
+    this.fetchData,
+    required this.converter,
+    this.value,
+    this.onChanged,
+    this.hint,
+    this.enabled = true,
+    this.showAll = false,
+  });
+
+  @override
+  State<VberSelect<T>> createState() => _VberSelectState<T>();
+}
+
+class _VberSelectState<T> extends State<VberSelect<T>> {
+  List<T>? _items;
+  bool _loading = false;
+  String? _selectedValue;
+
+  @override
+  void initState() {
+    super.initState();
+    _selectedValue = widget.value;
+    // 使用WidgetsBinding.instance.addPostFrameCallback确保在widget构建完成后再加载数据
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      _loadData();
+    });
+  }
+
+  @override
+  void didUpdateWidget(covariant VberSelect<T> oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    // 当外部传入的value发生变化时,同步更新内部状态
+    if (oldWidget.value != widget.value) {
+      setState(() {
+        _selectedValue = widget.value;
+      });
+    }
+  }
+
+  Future<void> _loadData() async {
+    // 如果提供了fetchData方法,则优先使用它获取数据
+    if (widget.fetchData != null) {
+      // 使用Future.microtask确保在下一个微任务中加载数据,避免在构建过程中修改状态
+      Future.microtask(() async {
+        setState(() {
+          _loading = true;
+        });
+
+        try {
+          final data = await widget.fetchData!();
+          setState(() {
+            _items = data;
+            _loading = false;
+          });
+        } catch (e) {
+          setState(() {
+            _loading = false;
+          });
+        }
+      });
+    } else {
+      // 否则使用传入的静态数据
+      setState(() {
+        _items = widget.items;
+      });
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_loading) {
+      return DropdownButton<String>(
+        items: [],
+        hint: SizedBox(
+          width: 16,
+          height: 16,
+          child: CircularProgressIndicator(strokeWidth: 2),
+        ),
+        onChanged: (String? value) {},
+      );
+    }
+
+    if (_items == null || _items!.isEmpty) {
+      List<DropdownMenuItem<String>> dropdownItems = [];
+      
+      // 如果showAll为true,添加"全部"选项
+      if (widget.showAll) {
+        dropdownItems.add(
+          DropdownMenuItem<String>(
+            value: '',
+            child: Text('全部'),
+          ),
+        );
+      }
+      
+      return DropdownButton<String>(
+        value: _selectedValue,
+        hint: Text(widget.hint ?? '请选择'),
+        items: dropdownItems,
+        onChanged: widget.enabled ? widget.onChanged : null,
+      );
+    }
+
+    final options = _items!.map((item) => widget.converter(item)).toList();
+    
+    // 构建最终的下拉选项列表
+    List<DropdownMenuItem<String>> dropdownItems = [];
+    
+    // 如果showAll为true,添加"全部"选项
+    if (widget.showAll) {
+      dropdownItems.add(
+        DropdownMenuItem<String>(
+          value: '',
+          child: Text('全部'),
+        ),
+      );
+    }
+    
+    // 添加转换后的选项
+    dropdownItems.addAll(options.map((option) {
+      return DropdownMenuItem<String>(
+        value: option.value,
+        child: Text(option.label),
+      );
+    }).toList());
+
+    return DropdownButton<String>(
+      value: _selectedValue,
+      hint: Text(widget.hint ?? '请选择'),
+      items: dropdownItems,
+      onChanged: widget.enabled 
+          ? (String? newValue) {
+              setState(() {
+                _selectedValue = newValue;
+              });
+              widget.onChanged?.call(newValue);
+            } 
+          : null,
+    );
+  }
+}

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

@@ -1,6 +1,5 @@
 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';
@@ -21,9 +20,6 @@ class ApiService {
   }) async {
     final option = apiOption ?? ApiOption.noAlert();
     try {
-      if (option.loading) {
-        LoadingUtil.showLoading();
-      }
       final response = await _dio.get(
         path,
         queryParameters: queryParameters,
@@ -33,10 +29,6 @@ class ApiService {
       return _handleResponse(response, option);
     } catch (e) {
       throw ErrorHandler.handleError(e);
-    } finally {
-      if (option.loading) {
-        LoadingUtil.hideLoading();
-      }
     }
   }
 
@@ -50,9 +42,6 @@ class ApiService {
   }) async {
     final option = apiOption ?? ApiOption();
     try {
-      if (option.loading) {
-        LoadingUtil.showLoading();
-      }
       final response = await _dio.post(
         path,
         data: data,
@@ -63,10 +52,6 @@ class ApiService {
       return _handleResponse(response, option);
     } catch (e) {
       throw ErrorHandler.handleError(e);
-    } finally {
-      if (option.loading) {
-        LoadingUtil.hideLoading();
-      }
     }
   }
 
@@ -80,9 +65,6 @@ class ApiService {
   }) async {
     final option = apiOption ?? ApiOption();
     try {
-      if (option.loading) {
-        LoadingUtil.showLoading();
-      }
       final response = await _dio.put(
         path,
         data: data,
@@ -93,10 +75,6 @@ class ApiService {
       return _handleResponse(response, option);
     } catch (e) {
       throw ErrorHandler.handleError(e);
-    } finally {
-      if (option.loading) {
-        LoadingUtil.hideLoading();
-      }
     }
   }
 
@@ -109,9 +87,6 @@ class ApiService {
     CancelToken? cancelToken,
   }) async {
     final option = apiOption ?? ApiOption();
-    if (option.loading) {
-      LoadingUtil.showLoading();
-    }
     try {
       final response = await _dio.delete(
         path,
@@ -123,10 +98,6 @@ class ApiService {
       return _handleResponse(response, option);
     } catch (e) {
       throw ErrorHandler.handleError(e);
-    } finally {
-      if (option.loading) {
-        LoadingUtil.hideLoading();
-      }
     }
   }
 

+ 38 - 0
UI/CF.APP/chicken_farm/lib/modes/breeding/batch.dart

@@ -0,0 +1,38 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'batch.g.dart';
+
+@JsonSerializable()
+class BatchModel {
+  String batchNum;
+  String batchName;
+  int? chickenCount;
+  DateTime? hatchDate;
+  int? status;
+  int? varietyId;
+  String? varietyName;
+  String? lineage;
+  int? generation;
+  int? batchType;
+  String? wingTagNumScope;
+  int? currentAge;
+
+  BatchModel({
+    required this.batchNum,
+    required this.batchName,
+    this.chickenCount,
+    this.hatchDate,
+    this.status,
+    this.varietyId,
+    this.varietyName,
+    this.lineage,
+    this.generation,
+    this.batchType,
+    this.wingTagNumScope,
+    this.currentAge,
+  });
+
+  factory BatchModel.fromJson(Map<String, dynamic> json) => _$BatchModelFromJson(json);
+
+  Map<String, dynamic> toJson() => _$BatchModelToJson(this);
+}

+ 40 - 0
UI/CF.APP/chicken_farm/lib/modes/breeding/batch.g.dart

@@ -0,0 +1,40 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'batch.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+BatchModel _$BatchModelFromJson(Map<String, dynamic> json) => BatchModel(
+  batchNum: json['batchNum'] as String,
+  batchName: json['batchName'] as String,
+  chickenCount: (json['chickenCount'] as num?)?.toInt(),
+  hatchDate: json['hatchDate'] == null
+      ? null
+      : DateTime.parse(json['hatchDate'] as String),
+  status: (json['status'] as num?)?.toInt(),
+  varietyId: (json['varietyId'] as num?)?.toInt(),
+  varietyName: json['varietyName'] as String?,
+  lineage: json['lineage'] as String?,
+  generation: (json['generation'] as num?)?.toInt(),
+  batchType: (json['batchType'] as num?)?.toInt(),
+  wingTagNumScope: json['wingTagNumScope'] as String?,
+  currentAge: (json['currentAge'] as num?)?.toInt(),
+);
+
+Map<String, dynamic> _$BatchModelToJson(BatchModel instance) =>
+    <String, dynamic>{
+      'batchNum': instance.batchNum,
+      'batchName': instance.batchName,
+      'chickenCount': instance.chickenCount,
+      'hatchDate': instance.hatchDate?.toIso8601String(),
+      'status': instance.status,
+      'varietyId': instance.varietyId,
+      'varietyName': instance.varietyName,
+      'lineage': instance.lineage,
+      'generation': instance.generation,
+      'batchType': instance.batchType,
+      'wingTagNumScope': instance.wingTagNumScope,
+      'currentAge': instance.currentAge,
+    };

+ 31 - 0
UI/CF.APP/chicken_farm/lib/modes/breeding/family.dart

@@ -0,0 +1,31 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'family.g.dart';
+
+@JsonSerializable()
+class FamilyModel {
+  int id;
+  String familyNum;
+  int? maleChickenId;
+  String? maleBatchNum;
+  int? femaleChickenId;
+  String? femaleBatchNum;
+  int? breedDate;
+  int? status;
+
+  FamilyModel({
+    required this.id,
+    required this.familyNum,
+    this.maleChickenId,
+    this.maleBatchNum,
+    this.femaleChickenId,
+    this.femaleBatchNum,
+    this.breedDate,
+    this.status,
+  });
+
+  factory FamilyModel.fromJson(Map<String, dynamic> json) =>
+      _$FamilyModelFromJson(json);
+
+  Map<String, dynamic> toJson() => _$FamilyModelToJson(this);
+}

+ 30 - 0
UI/CF.APP/chicken_farm/lib/modes/breeding/family.g.dart

@@ -0,0 +1,30 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'family.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+FamilyModel _$FamilyModelFromJson(Map<String, dynamic> json) => FamilyModel(
+  id: (json['id'] as num).toInt(),
+  familyNum: json['familyNum'] as String,
+  maleChickenId: (json['maleChickenId'] as num?)?.toInt(),
+  maleBatchNum: json['maleBatchNum'] as String?,
+  femaleChickenId: (json['femaleChickenId'] as num?)?.toInt(),
+  femaleBatchNum: json['femaleBatchNum'] as String?,
+  breedDate: (json['breedDate'] as num?)?.toInt(),
+  status: (json['status'] as num?)?.toInt(),
+);
+
+Map<String, dynamic> _$FamilyModelToJson(FamilyModel instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'familyNum': instance.familyNum,
+      'maleChickenId': instance.maleChickenId,
+      'maleBatchNum': instance.maleBatchNum,
+      'femaleChickenId': instance.femaleChickenId,
+      'femaleBatchNum': instance.femaleBatchNum,
+      'breedDate': instance.breedDate,
+      'status': instance.status,
+    };

+ 17 - 0
UI/CF.APP/chicken_farm/lib/modes/breeding/wing_tag_num.dart

@@ -0,0 +1,17 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'wing_tag_num.g.dart';
+
+@JsonSerializable()
+class WingTagNumModel {
+  String wingTagNum;
+  int? chickenId;
+  int? status;
+
+  WingTagNumModel({required this.wingTagNum, this.chickenId, this.status});
+
+  factory WingTagNumModel.fromJson(Map<String, dynamic> json) =>
+      _$WingTagNumModelFromJson(json);
+
+  Map<String, dynamic> toJson() => _$WingTagNumModelToJson(this);
+}

+ 21 - 0
UI/CF.APP/chicken_farm/lib/modes/breeding/wing_tag_num.g.dart

@@ -0,0 +1,21 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'wing_tag_num.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+WingTagNumModel _$WingTagNumModelFromJson(Map<String, dynamic> json) =>
+    WingTagNumModel(
+      wingTagNum: json['wingTagNum'] as String,
+      chickenId: (json['chickenId'] as num?)?.toInt(),
+      status: (json['status'] as num?)?.toInt(),
+    );
+
+Map<String, dynamic> _$WingTagNumModelToJson(WingTagNumModel instance) =>
+    <String, dynamic>{
+      'wingTagNum': instance.wingTagNum,
+      'chickenId': instance.chickenId,
+      'status': instance.status,
+    };

+ 15 - 0
UI/CF.APP/chicken_farm/lib/modes/menu_item.dart

@@ -0,0 +1,15 @@
+import 'package:flutter/material.dart';
+
+class MenuItem {
+  final String name;
+  final String routeName;
+  final IconData? icon;
+  final String? permission;
+
+  const MenuItem({
+    required this.name,
+    required this.routeName,
+    this.icon,
+    this.permission,
+  });
+}

+ 8 - 0
UI/CF.APP/chicken_farm/lib/modes/page/page_model.dart

@@ -0,0 +1,8 @@
+class PageResultModel<T> {
+  int total;
+  List<T> rows;
+
+  PageResultModel({required this.total, required this.rows});
+  PageResultModel.empty({int total = 0, List<T> rows = const []})
+    : this(total: total, rows: rows);
+}

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

@@ -1,4 +1,3 @@
-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';
@@ -34,7 +33,6 @@ class _LoginPageState extends ConsumerState<LoginPage> {
     final authStore = ref.read(authStoreProvider.notifier);
 
     return Scaffold(
-      appBar: const VberAppBar(title: '用户登录', showLeftButton: false),
       body: Stack(
         children: [
           Container(

+ 341 - 0
UI/CF.APP/chicken_farm/lib/pages/breeding/bind_wing_tag_page.dart

@@ -0,0 +1,341 @@
+import 'package:chicken_farm/components/vb_dict_select.dart';
+import 'package:chicken_farm/components/vb_select.dart';
+import 'package:chicken_farm/core/utils/logger.dart';
+import 'package:chicken_farm/stores/dict_stroe.dart';
+import 'package:flutter/material.dart';
+import 'package:chicken_farm/components/vb_app_bar.dart';
+import 'package:chicken_farm/core/utils/toast.dart';
+import 'package:chicken_farm/components/vb_search_select.dart';
+import 'package:chicken_farm/apis/index.dart';
+import 'package:chicken_farm/modes/breeding/batch.dart';
+import 'package:chicken_farm/modes/breeding/family.dart';
+import 'package:chicken_farm/modes/breeding/wing_tag_num.dart';
+
+class BindWingTagNumPage extends StatefulWidget {
+  const BindWingTagNumPage({super.key});
+
+  @override
+  State<BindWingTagNumPage> createState() => _BindWingTagNumPageState();
+}
+
+class _BindWingTagNumPageState extends State<BindWingTagNumPage> {
+  String? _selectedBatchNum;
+  String? _selectedFamilyId;
+
+  // 翅号列表 - 根据选中的批次获取
+  List<WingTagNumModel> _wingTags = [];
+
+  // RefId列表 - 识别后获取的数据
+  List<String> _refIds = [];
+  List<Map<String, dynamic>> _refIdWingTags = [];
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: const VberAppBar(title: '翅号绑定', showLeftButton: true),
+      body: Padding(
+        padding: const EdgeInsets.all(16.0),
+        child: Column(
+          children: [
+            // 批次选择
+            _buildBatchSearchSelect(),
+
+            const SizedBox(height: 10),
+
+            // 家系号选择
+            _buildFamilySearchSelect(),
+
+            const SizedBox(height: 20),
+
+            // 按钮区域 - 根据不同状态显示不同按钮
+            SizedBox(
+              width: double.infinity,
+              child: _refIdWingTags.isEmpty
+                  ? ElevatedButton(
+                      onPressed:
+                          (_selectedBatchNum != null &&
+                              _selectedFamilyId != null)
+                          ? _handleReidentify
+                          : null,
+                      style: ElevatedButton.styleFrom(
+                        backgroundColor:
+                            (_selectedBatchNum != null &&
+                                _selectedFamilyId != null)
+                            ? Colors.blue
+                            : Colors.grey,
+                        foregroundColor: Colors.white,
+                      ),
+                      child: const Text('扫描电子编号'),
+                    )
+                  : Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+                      children: [
+                        Expanded(
+                          child: ElevatedButton(
+                            onPressed: _handleClear,
+                            style: ElevatedButton.styleFrom(
+                              backgroundColor: Colors.orange,
+                              foregroundColor: Colors.white,
+                            ),
+                            child: const Text('清空编号'),
+                          ),
+                        ),
+                        const SizedBox(width: 16),
+                        Expanded(
+                          child: ElevatedButton(
+                            onPressed: _handleSubmit,
+                            style: ElevatedButton.styleFrom(
+                              backgroundColor: Colors.blue,
+                              foregroundColor: Colors.white,
+                            ),
+                            child: const Text('提交'),
+                          ),
+                        ),
+                      ],
+                    ),
+            ),
+
+            const SizedBox(height: 20),
+
+            // 显示refId和翅号的列表
+            Expanded(
+              child: _refIdWingTags.isEmpty
+                  ? const Center(
+                      child: Text(
+                        '请先选择批次和家系号,然后点击扫描电子编号',
+                        style: TextStyle(color: Colors.grey),
+                      ),
+                    )
+                  : ListView.builder(
+                      itemCount: _refIdWingTags.length,
+                      itemBuilder: (context, index) {
+                        final item = _refIdWingTags[index];
+                        return Card(
+                          margin: const EdgeInsets.symmetric(vertical: 4),
+                          child: ListTile(
+                            title: Column(
+                              mainAxisSize: MainAxisSize.min,
+                              crossAxisAlignment: CrossAxisAlignment.start,
+                              children: [
+                                Text('电子编号: ${item['refId']}'),
+                                const SizedBox(height: 3),
+                                Text('翅号: ${item['wingTag']}'),
+                                const SizedBox(height: 3),
+                                FutureBuilder<String>(
+                                  future: DictStore()
+                                      .getLabelByTypeAndValue(
+                                        'chicken_gender',
+                                        item['gender'],
+                                      )
+                                      .then((value) => value ?? '未知'),
+                                  builder:
+                                      (
+                                        BuildContext context,
+                                        AsyncSnapshot<String> snapshot,
+                                      ) {
+                                        if (snapshot.hasData) {
+                                          return Text('性别: ${snapshot.data!}');
+                                        } else if (snapshot.hasError) {
+                                          return Text('性别: 获取失败');
+                                        } else {
+                                          return const Text('性别: 加载中...');
+                                        }
+                                      },
+                                ),
+                                // const SizedBox(height: 8),
+                                // Text('批次: ${item['batchNum']}'),
+                                // const SizedBox(height: 8),
+                                // Text('家系号: ${item['familyId']}'),
+                                // Row(
+                                //   children: [
+                                //     const Text('性别: '),
+                                //     Expanded(
+                                //       child: VberDictSelect(
+                                //         dictType: "chicken_gender",
+                                //         value: item['gender'],
+                                //         onChanged: (value) {
+                                //           // 更新性别值的逻辑可以在这里添加
+                                //           // 例如:
+                                //           // setState(() {
+                                //           //   item['gender'] = value;
+                                //           // });
+                                //         },
+                                //       ),
+                                //     ),
+                                //   ],
+                                // ),
+                              ],
+                            ),
+                          ),
+                        );
+                      },
+                    ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildBatchSearchSelect() {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        const Text('选择批次', style: TextStyle(fontWeight: FontWeight.bold)),
+        const SizedBox(height: 5),
+        VberSearchSelect<BatchModel>(
+          searchApi: ({required dynamic queryParams}) async {
+            final result = await apis.breeding.batchApi.queryPageBatchs(
+              queryParams,
+            );
+            return {'rows': result.rows, 'total': result.total};
+          },
+          converter: (BatchModel data) {
+            return SelectOption(
+              label: '${data.batchNum}(${data.batchName})',
+              value: data.batchNum,
+              extra: data,
+            );
+          },
+          value: _selectedBatchNum,
+          hint: '批次号',
+          onChanged: (String? value) {
+            setState(() {
+              _selectedBatchNum = value;
+              // 当切换批次时,重置后续选项和数据
+              _selectedFamilyId = null;
+              _wingTags = [];
+              _refIds = [];
+            });
+
+            // 获取翅号数据
+            if (value != null) {
+              // 使用Future.microtask确保在下一个微任务中加载数据,避免在构建过程中修改状态
+              Future.microtask(() => _loadWingTags(value));
+            }
+          },
+        ),
+      ],
+    );
+  }
+
+  Widget _buildFamilySearchSelect() {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        const Text('选择家系号', style: TextStyle(fontWeight: FontWeight.bold)),
+        const SizedBox(height: 5),
+        VberSearchSelect<FamilyModel>(
+          searchApi: ({required dynamic queryParams}) async {
+            final result = await apis.breeding.batchApi.queryPageFamilys(
+              queryParams,
+            );
+            return {'rows': result.rows, 'total': result.total};
+          },
+          converter: (FamilyModel data) {
+            return SelectOption(
+              label: data.familyNum,
+              value: data.id.toString(),
+              extra: data,
+            );
+          },
+          value: _selectedFamilyId,
+          hint: '家系号',
+          onChanged: (String? value) {
+            setState(() {
+              _selectedFamilyId = value;
+            });
+          },
+        ),
+      ],
+    );
+  }
+
+  // 根据批次获取翅号
+  void _loadWingTags(String batchId) async {
+    try {
+      final result = await Apis().breeding.batchApi.queryWingTags({
+        'batchNum': batchId,
+      });
+      logger.d('获取翅号成功: $result');
+      setState(() {
+        _wingTags = result;
+      });
+    } catch (e) {
+      ToastUtil.error('获取翅号失败: $e');
+    }
+  }
+
+  // 生成refIds数组
+  List<String> _generateRefIds() {
+    return List.generate(8, (index) {
+      return 'REF-${DateTime.timestamp().toString()}-$index';
+    });
+  }
+
+  // 模拟识别refId
+  void _handleIdentifyRefIds() async {
+    if (_selectedBatchNum == null) {
+      ToastUtil.error('请选择批次号');
+      return;
+    }
+    if (_selectedFamilyId == null) {
+      ToastUtil.error('请选择家系号');
+      return;
+    }
+
+    // 模拟识别过程,生成一些refId
+    final refIds = _generateRefIds();
+
+    // 检查是否有超出翅号数量的refId
+    if (refIds.length > _wingTags.length) {
+      await ToastUtil.errorAlert('错误:RefId数量超过可用翅号数量,请重新识别');
+      FocusManager.instance.primaryFocus?.unfocus();
+    } else {
+      setState(() {
+        _refIdWingTags = refIds
+            .map(
+              (id) => {
+                'refId': id,
+                'wingTag': _wingTags[refIds.indexOf(id)].wingTagNum,
+                'familyId': _selectedFamilyId,
+                'batchNum': _selectedBatchNum,
+                'gender': "2",
+              },
+            )
+            .toList();
+      });
+    }
+  }
+
+  // 扫描电子编号
+  void _handleReidentify() {
+    _refIds.clear();
+    _refIdWingTags.clear();
+    _handleIdentifyRefIds();
+  }
+
+  // 处理清空编号
+  void _handleClear() {
+    setState(() {
+      _refIdWingTags.clear();
+    });
+  }
+
+  // 处理提交
+  void _handleSubmit() {
+    // 实际应用中这里会发送数据到服务器
+    ScaffoldMessenger.of(context).showSnackBar(
+      const SnackBar(content: Text('翅号绑定提交成功'), backgroundColor: Colors.green),
+    );
+
+    // 提交后重置状态
+    setState(() {
+      _refIdWingTags.clear();
+      // 重新获取翅号
+      if (_selectedBatchNum != null) {
+        _loadWingTags(_selectedBatchNum!);
+      }
+    });
+  }
+}

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

@@ -2,7 +2,7 @@ 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/vb_app_bar.dart';
-import 'package:chicken_farm/components/qr_scanner.dart';
+import 'package:chicken_farm/components/vb_qr_scanner.dart';
 import 'package:go_router/go_router.dart';
 
 const String _qrCodePrefix = 'vb@device@/checkin/';
@@ -44,7 +44,7 @@ class _CheckinPageState extends State<CheckinPage> {
   Widget build(BuildContext context) {
     return Scaffold(
       appBar: const VberAppBar(title: '点检签到', showLeftButton: true),
-      body: QRScannerComponent(
+      body: VberQRScanner(
         startWithString: _qrCodePrefix,
         onScanCallback: _performCheckin,
         invalidQRMessage: '签到二维码无效!',

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

@@ -1,5 +1,5 @@
-import 'package:chicken_farm/routes/app_routes.dart';
 import 'package:chicken_farm/stores/auth_store.dart';
+import 'package:chicken_farm/stores/menu_store.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
@@ -14,42 +14,32 @@ class MenuButtons extends ConsumerWidget {
 
     final isSuperAdmin = authStore.isSuperAdmin();
 
-    // 根据权限判断是否显示按钮
-    final showCheckIn =
-        isSuperAdmin ||
-        (authState.permissions?.contains('device:inspection:checkin') ?? false);
-    final showSampleTransfer =
-        isSuperAdmin ||
-        (authState.permissions?.contains('experiment:sample:query') ?? false);
+    // 根据权限过滤菜单项
+    final visibleMenuItems = MenuStore.menuItems.where((item) {
+      if (isSuperAdmin) return true;
+      if (item.permission == null) return true;
+      return authState.permissions?.contains(item.permission) ?? false;
+    }).toList();
 
     return Column(
       mainAxisAlignment: MainAxisAlignment.center,
       children: [
-        if (showCheckIn)
+        for (var i = 0; i < visibleMenuItems.length; i++) ...[
           SizedBox(
             width: 200,
             height: 50,
             child: ElevatedButton.icon(
               onPressed: () {
-                context.pushNamed(AppRouteNames.checkin);
+                context.pushNamed(visibleMenuItems[i].routeName);
               },
-              icon: const Icon(Icons.check_circle_outline),
-              label: const Text('点检签到'),
-            ),
-          ),
-        const SizedBox(height: 20),
-        if (showSampleTransfer)
-          SizedBox(
-            width: 200,
-            height: 50,
-            child: ElevatedButton.icon(
-              onPressed: () {
-                context.pushNamed(AppRouteNames.sample);
-              },
-              icon: const Icon(Icons.swap_horiz),
-              label: const Text('样品查询'),
+              icon: visibleMenuItems[i].icon != null
+                  ? Icon(visibleMenuItems[i].icon)
+                  : const SizedBox.shrink(),
+              label: Text(visibleMenuItems[i].name),
             ),
           ),
+          if (i < visibleMenuItems.length - 1) const SizedBox(height: 20),
+        ],
       ],
     );
   }

+ 20 - 44
UI/CF.APP/chicken_farm/lib/pages/sample/sample_detail_page.dart

@@ -1,4 +1,4 @@
-import 'package:chicken_farm/components/dict_label.dart';
+import 'package:chicken_farm/components/vb_dict_label.dart';
 import 'package:flutter/material.dart';
 import 'package:chicken_farm/components/vb_app_bar.dart';
 import 'package:chicken_farm/modes/experiment/sample/sample.dart';
@@ -102,7 +102,7 @@ class _SampleDetailPageState extends State<SampleDetailPage> {
                     _sample!.sampleName,
                     style: Theme.of(context).textTheme.titleLarge,
                   ),
-                  DictLabel(
+                  VberDictLabel(
                     dictType: "experiment_sample_status",
                     value: _sample!.sampleStatus.toString(),
                   ),
@@ -110,7 +110,13 @@ class _SampleDetailPageState extends State<SampleDetailPage> {
               ),
               const SizedBox(height: 16),
               _buildInfoRow('样品编号', _sample!.id.toString()),
-              _buildInfoRow('样品类型', _getSampleTypeText(_sample!.sampleType)),
+              _buildInfoRow(
+                '样品类型',
+                VberDictLabel(
+                  dictType: "experiment_sample_type",
+                  value: _sample!.sampleType.toString(),
+                ),
+              ),
               _buildInfoRow('取样批次', _sample!.batchNum),
               _buildInfoRow('取样翅号', _sample!.wingTagNum),
               _buildInfoRow(
@@ -127,7 +133,16 @@ class _SampleDetailPageState extends State<SampleDetailPage> {
     );
   }
 
-  Widget _buildInfoRow(String label, String value) {
+  Widget _buildInfoRow(String label, dynamic value) {
+    Widget valueWidget;
+    if (value is String) {
+      valueWidget = Text(value);
+    } else if (value is Widget) {
+      valueWidget = value;
+    } else {
+      valueWidget = Text(value.toString());
+    }
+
     return Padding(
       padding: const EdgeInsets.symmetric(vertical: 4.0),
       child: Row(
@@ -139,7 +154,7 @@ class _SampleDetailPageState extends State<SampleDetailPage> {
               style: const TextStyle(fontWeight: FontWeight.bold),
             ),
           ),
-          Expanded(child: Text(value)),
+          Expanded(child: valueWidget),
         ],
       ),
     );
@@ -190,43 +205,4 @@ class _SampleDetailPageState extends State<SampleDetailPage> {
       ),
     );
   }
-
-  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 '未知类型';
-    }
-  }
 }

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

@@ -1,7 +1,7 @@
 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:chicken_farm/components/vb_qr_scanner.dart';
 import 'package:go_router/go_router.dart';
 
 class SampleQueryPage extends StatefulWidget {
@@ -42,7 +42,7 @@ class _SampleQueryPageState extends State<SampleQueryPage> {
   Widget build(BuildContext context) {
     return Scaffold(
       appBar: const VberAppBar(title: '样品查询', showLeftButton: true),
-      body: QRScannerComponent(
+      body: VberQRScanner(
         startWithString: _qrCodePrefix,
         onScanCallback: _handleSampleScan,
         invalidQRMessage: '样品二维码无效!',

+ 7 - 0
UI/CF.APP/chicken_farm/lib/routes/app_routes.dart

@@ -8,6 +8,7 @@ import '../pages/checkin/checkin_page.dart';
 import '../pages/checkin/checkin_record_page.dart';
 import '../pages/sample/sample_query_page.dart';
 import '../pages/sample/sample_detail_page.dart';
+import '../pages/breeding/bind_wing_tag_page.dart';
 
 class AppRouteNames {
   static const String splash = '/';
@@ -15,6 +16,7 @@ class AppRouteNames {
   static const String home = '/home';
   static const String checkin = '/checkin';
   static const String checkinRecord = '/checkin_record';
+  static const String bindwingTagNum = '/bind_wing_tag_num';
   static const String sample = '/sample';
   static const String sampleDetail = '/sample/detail';
 }
@@ -61,6 +63,11 @@ class AppRoutes {
         return SampleDetailPage(id: id);
       },
     ),
+    GoRoute(
+      path: AppRouteNames.bindwingTagNum,
+      name: AppRouteNames.bindwingTagNum,
+      builder: (context, state) => const BindWingTagNumPage(),
+    ),
   ];
 }
 

+ 26 - 0
UI/CF.APP/chicken_farm/lib/stores/menu_store.dart

@@ -0,0 +1,26 @@
+import 'package:chicken_farm/modes/menu_item.dart';
+import 'package:chicken_farm/routes/app_routes.dart';
+import 'package:flutter/material.dart';
+
+class MenuStore {
+  static const List<MenuItem> menuItems = [
+    MenuItem(
+      name: '翅号绑定',
+      routeName: AppRouteNames.bindwingTagNum,
+      icon: Icons.tag_outlined,
+      permission: 'device:device:query',
+    ),
+    MenuItem(
+      name: '点检签到',
+      routeName: AppRouteNames.checkin,
+      icon: Icons.check_circle_outline,
+      permission: 'device:inspection:checkin',
+    ),
+    MenuItem(
+      name: '样品查询',
+      routeName: AppRouteNames.sample,
+      icon: Icons.swap_horiz,
+      permission: 'experiment:sample:query',
+    ),
+  ];
+}