Эх сурвалжийг харах

Update 优化select组件

Yue 1 долоо хоног өмнө
parent
commit
6d6ea075af

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

@@ -24,6 +24,12 @@ class VberDictSelect extends StatefulWidget {
   /// 是否显示"全部"选项
   final bool showAll;
 
+  /// 是否隐藏下划线
+  final bool hideUnderline;
+
+  /// 是否显示清空按钮
+  final bool showClearButton;
+
   const VberDictSelect({
     super.key,
     required this.dictType,
@@ -32,6 +38,8 @@ class VberDictSelect extends StatefulWidget {
     this.hint,
     this.enabled = true,
     this.showAll = false,
+    this.hideUnderline = false,
+    this.showClearButton = false,
   });
 
   @override
@@ -58,6 +66,8 @@ class _VberDictSelectState extends State<VberDictSelect> {
       hint: widget.hint,
       enabled: widget.enabled,
       showAll: widget.showAll,
+      hideUnderline: widget.hideUnderline,
+      showClearButton: widget.showClearButton,
     );
   }
 }

+ 286 - 252
UI/CF.APP/chicken_farm/lib/components/vb_search_select.dart

@@ -41,6 +41,12 @@ class VberSearchSelect<T> extends StatefulWidget {
   /// 额外参数,用于传递给搜索API
   final Map<String, dynamic>? extraParams;
 
+  /// 是否隐藏下划线
+  final bool hideUnderline;
+
+  /// 是否显示清空按钮
+  final bool showClearButton;
+
   const VberSearchSelect({
     super.key,
     required this.searchApi,
@@ -50,47 +56,41 @@ class VberSearchSelect<T> extends StatefulWidget {
     this.onChanged,
     this.hint,
     this.enabled = true,
-    this.pageSize = 3,
+    this.pageSize = 10,
     this.extraParams,
+    this.hideUnderline = false,
+    this.showClearButton = true,
   });
 
   @override
   State<VberSearchSelect<T>> createState() => _VberSearchSelectState<T>();
 }
 
-class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
-    with SingleTickerProviderStateMixin {
-  final TextEditingController _textController = TextEditingController();
+class _VberSearchSelectState<T> extends State<VberSearchSelect<T>> {
   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;
+  final FocusNode _searchFocusNode = FocusNode();
   String? _selectedValue;
-  SelectOption? _selectedOption; // 新增:用来存储选中的选项
-  int _currentPage = 1;
-  bool _hasMore = true;
-  String _lastSearchText = '';
+  SelectOption? _selectedOption;
+  final List<T> _items = [];
 
   @override
   void initState() {
     super.initState();
     _selectedValue = widget.value;
-    _initTextController();
-    _focusNode.addListener(_onFocusChange);
+    _initSelectedOption();
   }
 
-  /// 初始化文本控制器
-  void _initTextController() async {
+  /// 初始化选中选项
+  void _initSelectedOption() async {
     if (widget.value != null && widget.getValueLabel != null) {
       try {
         final label = await widget.getValueLabel!(widget.value);
         if (mounted) {
           setState(() {
-            _textController.text = label ?? widget.value ?? '';
+            _selectedOption = SelectOption(
+              label: label ?? widget.value ?? '',
+              value: widget.value!,
+            );
           });
         }
       } catch (e) {
@@ -108,7 +108,7 @@ class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
     }
   }
 
-  /// 同步外部value和内部_selectedValue以及_textController.text
+  /// 同步外部value和内部_selectedValue以及_selectedOption
   void _syncValueAndText(String? newValue) async {
     setState(() {
       _selectedValue = newValue;
@@ -140,202 +140,201 @@ class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
       }
     }
 
-    // 更新文本显示控件
+    // 更新选中选项
     if (mounted) {
       setState(() {
         // 优先级: 找到的label > value本身 > 空字符串
-        _textController.text = label ?? newValue ?? '';
+        if (newValue != null) {
+          _selectedOption = SelectOption(
+            label: label ?? newValue,
+            value: newValue,
+          );
+        } else {
+          _selectedOption = null;
+        }
       });
     }
   }
 
   @override
   void dispose() {
-    _textController.dispose();
     _searchController.dispose();
-    _focusNode.removeListener(_onFocusChange);
-    _focusNode.dispose();
-    _searchFocusNode.dispose(); // 释放搜索框的FocusNode
-    _overlayEntry?.remove();
+    _searchFocusNode.dispose();
     super.dispose();
   }
 
-  void _onFocusChange() {
-    // 只有当_focusNode获得焦点且组件启用时才显示弹出层
-    if (_focusNode.hasFocus && widget.enabled) {
-      // 添加一个微小的延迟,避免焦点竞争
-      Future.microtask(() {
-        if (_focusNode.hasFocus && mounted) {
-          _showOverlay();
-        }
+  void _showSearchDialog() async {
+    final result = await showDialog<SelectOption>(
+      context: context,
+      barrierDismissible: true,
+      builder: (BuildContext context) {
+        return _SearchDialog<T>(
+          searchApi: widget.searchApi,
+          converter: widget.converter,
+          hint: widget.hint,
+          pageSize: widget.pageSize,
+          extraParams: widget.extraParams,
+          searchController: _searchController,
+          searchFocusNode: _searchFocusNode,
+        );
+      },
+    );
+
+    if (result != null) {
+      setState(() {
+        _selectedValue = result.value;
+        _selectedOption = result;
       });
+      widget.onChanged?.call(result.value);
+    }
+  }
+
+  void _clearSelection() {
+    setState(() {
+      _selectedValue = null;
+      _selectedOption = null;
+    });
+    widget.onChanged?.call(null);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    List<DropdownMenuItem<String>> dropdownItems = [];
+
+    // 添加当前选中项到下拉列表(如果存在)
+    if (_selectedOption != null) {
+      dropdownItems.add(
+        DropdownMenuItem<String>(
+          value: _selectedOption!.value,
+          child: Text(_selectedOption!.label),
+        ),
+      );
+    }
+
+    // 添加其他已加载的项到下拉列表
+    if (_items.isNotEmpty) {
+      for (var item in _items) {
+        final option = widget.converter(item);
+        // 避免重复添加选中项
+        if (_selectedOption == null || option.value != _selectedOption!.value) {
+          dropdownItems.add(
+            DropdownMenuItem<String>(
+              value: option.value,
+              child: Text(option.label),
+            ),
+          );
+        }
+      }
+    }
+
+    // 创建一个不会真正展开下拉列表的DropdownButton
+    Widget dropdownWidget = DropdownButton<String>(
+      value: _selectedValue,
+      hint: Text('请点击查询${widget.hint ?? ""}...'),
+      items: dropdownItems.isEmpty ? null : dropdownItems,
+      onChanged: null,
+      isExpanded: true,
+      icon: Icon(Icons.search), // 使用搜索图标表明这是搜索选择器
+    );
+
+    Widget selectWidget = widget.hideUnderline
+        ? DropdownButtonHideUnderline(child: dropdownWidget)
+        : dropdownWidget;
+
+    // 使用 GestureDetector 包裹整个组件以确保点击事件能被正确捕获
+    Widget wrappedWidget = GestureDetector(
+      onTap: widget.enabled ? _showSearchDialog : null,
+      child: selectWidget,
+    );
+
+    // 处理清空按钮显示
+    if (widget.showClearButton && _selectedValue != null) {
+      return Row(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          Expanded(child: wrappedWidget),
+          IconButton(
+            icon: const Icon(Icons.clear, size: 20),
+            onPressed: widget.enabled ? _clearSelection : null,
+          ),
+        ],
+      );
     }
+
+    return wrappedWidget;
   }
+}
 
-  void _showOverlay() {
-    if (_isOverlayVisible) return;
+/// 搜索对话框
+class _SearchDialog<T> extends StatefulWidget {
+  final SearchApiFunction<T> searchApi;
+  final SelectOptionConverter<T> converter;
+  final String? hint;
+  final int pageSize;
+  final Map<String, dynamic>? extraParams;
+  final TextEditingController searchController;
+  final FocusNode searchFocusNode;
+
+  const _SearchDialog({
+    required this.searchApi,
+    required this.converter,
+    required this.hint,
+    required this.pageSize,
+    required this.extraParams,
+    required this.searchController,
+    required this.searchFocusNode,
+  });
+
+  @override
+  _SearchDialogState<T> createState() => _SearchDialogState<T>();
+}
 
-    _overlayEntry = _createOverlayEntry();
-    Overlay.of(context).insert(_overlayEntry!);
-    _isOverlayVisible = true;
+class _SearchDialogState<T> extends State<_SearchDialog<T>> {
+  List<T> _items = [];
+  bool _isLoading = false;
+  int _currentPage = 1;
+  bool _hasMore = true;
+  String _lastSearchText = '';
+  final ScrollController _scrollController = ScrollController();
 
+  @override
+  void initState() {
+    super.initState();
     // 清空搜索框
-    _searchController.clear();
+    widget.searchController.clear();
     _items.clear();
     _currentPage = 1;
     _hasMore = true;
     _lastSearchText = '';
 
-    _performSearch('', (callback) => setState(() => callback()));
+    _performSearch('');
 
-    WidgetsBinding.instance.addPostFrameCallback((_) {
-      if (_searchFocusNode.canRequestFocus && mounted) {
-        _searchFocusNode.requestFocus();
-      }
-    });
+    // 移除自动获取焦点的代码,让用户手动点击输入框获取焦点
+    // WidgetsBinding.instance.addPostFrameCallback((_) {
+    //   widget.searchFocusNode.requestFocus();
+    // });
+    
+    _scrollController.addListener(_scrollListener);
   }
 
-  void _hideOverlay() {
-    _overlayEntry?.remove();
-    _overlayEntry = null;
-    _isOverlayVisible = false;
-    _searchFocusNode.unfocus();
-    _lastSearchText = '';
-    _items.clear();
+  @override
+  void dispose() {
+    _scrollController.removeListener(_scrollListener);
+    _scrollController.dispose();
+    super.dispose();
   }
 
-  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 _scrollListener() {
+    if (_scrollController.position.pixels ==
+            _scrollController.position.maxScrollExtent &&
+        _hasMore &&
+        !_isLoading) {
+      _loadMore();
+    }
   }
 
-  void _loadMore(StateSetter overlaySetState) async {
-    if (!_hasMore || _isLoading) return;
-
-    overlaySetState(() {
+  void _loadMore() async {
+    setState(() {
       _isLoading = true;
     });
 
@@ -349,7 +348,7 @@ class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
       final newItems = rows.cast<T>().toList();
 
       if (mounted) {
-        overlaySetState(() {
+        setState(() {
           _items.addAll(newItems);
           _hasMore = _items.length < total;
           _isLoading = false;
@@ -358,7 +357,7 @@ class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
     } catch (e) {
       logger.e('加载更多失败:$e');
       if (mounted) {
-        overlaySetState(() {
+        setState(() {
           _currentPage--; // 回退页码
           _isLoading = false;
         });
@@ -366,8 +365,8 @@ class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
     }
   }
 
-  void _performSearch(String keyword, StateSetter overlaySetState) async {
-    overlaySetState(() {
+  void _performSearch(String keyword) async {
+    setState(() {
       _isLoading = true;
     });
 
@@ -382,7 +381,7 @@ class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
       final newItems = rows.cast<T>().toList();
 
       if (mounted) {
-        overlaySetState(() {
+        setState(() {
           _items = newItems;
           _hasMore = _items.length < total;
           _isLoading = false;
@@ -391,7 +390,7 @@ class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
     } catch (e) {
       logger.e('搜索失败:$e');
       if (mounted) {
-        overlaySetState(() {
+        setState(() {
           _items = [];
           _hasMore = false;
           _isLoading = false;
@@ -409,11 +408,8 @@ class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
       'pageNum': pageNum,
     };
 
-    if (keyword != null) {
+    if (keyword != null && keyword.isNotEmpty) {
       queryParams['keyword'] = keyword;
-    } else if (keyword == null && _searchController.text.isNotEmpty) {
-      // 如果keyword参数为null但_searchController中有内容,则使用该内容
-      queryParams['keyword'] = _searchController.text;
     }
 
     if (widget.extraParams != null) {
@@ -424,73 +420,111 @@ class _VberSearchSelectState<T> extends State<VberSearchSelect<T>>
     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 _onSearchChanged(String value) {
+    Future.delayed(const Duration(milliseconds: 300), () {
+      if (_lastSearchText != widget.searchController.text) {
+        _lastSearchText = widget.searchController.text;
+        _items.clear();
+        _currentPage = 1;
+        _hasMore = true;
+        _performSearch(widget.searchController.text);
       }
     });
   }
 
-  void _clearSelection() {
-    setState(() {
-      _textController.clear();
-      _selectedValue = null;
-      _selectedOption = null; // 清除保存的选项
-    });
-
-    widget.onChanged?.call(null);
+  void _selectItem(SelectOption option, T item) {
+    Navigator.of(context).pop(option);
   }
 
   @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),
-          ),
+    return AlertDialog(
+      title: Text('查询${widget.hint ?? ""}'),
+      content: SizedBox(
+        width: MediaQuery.of(context).size.width * 0.8,
+        height: MediaQuery.of(context).size.height * 0.6,
+        child: Column(
+          children: [
+            TextField(
+              controller: widget.searchController,
+              focusNode: widget.searchFocusNode,
+              decoration: InputDecoration(
+                hintText: '请输入${widget.hint}搜索关键词',
+                contentPadding: EdgeInsets.symmetric(
+                  horizontal: 12,
+                  vertical: 8,
+                ),
+                border: OutlineInputBorder(
+                  borderRadius: BorderRadius.circular(4),
+                ),
+                isDense: true,
+              ),
+              onChanged: _onSearchChanged,
+            ),
+            SizedBox(height: 16),
+            if (_isLoading && _items.isEmpty)
+              Expanded(child: Center(child: CircularProgressIndicator()))
+            else if (!_isLoading && _items.isEmpty)
+              Expanded(
+                child: Center(
+                  child: Text(
+                    '没有查询到结果',
+                    style: TextStyle(color: Colors.grey, fontSize: 14),
+                  ),
+                ),
+              )
+            else
+              Expanded(
+                child: NotificationListener<ScrollNotification>(
+                  onNotification: (notification) {
+                    if (notification.metrics.pixels ==
+                            notification.metrics.maxScrollExtent &&
+                        _hasMore &&
+                        !_isLoading) {
+                      _loadMore();
+                      return true;
+                    }
+                    return false;
+                  },
+                  child: ListView.builder(
+                    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,
+                      );
+                    },
+                  ),
+                ),
+              ),
+          ],
         ),
       ),
+      actions: [
+        TextButton(
+          onPressed: () => Navigator.of(context).pop(),
+          child: Text('取消'),
+        ),
+      ],
     );
   }
 }

+ 74 - 38
UI/CF.APP/chicken_farm/lib/components/vb_select.dart

@@ -40,6 +40,12 @@ class VberSelect<T> extends StatefulWidget {
   /// 是否显示"全部"选项
   final bool showAll;
 
+  /// 是否隐藏下划线
+  final bool hideUnderline;
+
+  /// 是否显示清空按钮
+  final bool showClearButton;
+
   const VberSelect({
     super.key,
     this.items,
@@ -50,6 +56,8 @@ class VberSelect<T> extends StatefulWidget {
     this.hint,
     this.enabled = true,
     this.showAll = false,
+    this.hideUnderline = false,
+    this.showClearButton = false,
   });
 
   @override
@@ -114,7 +122,7 @@ class _VberSelectState<T> extends State<VberSelect<T>> {
   @override
   Widget build(BuildContext context) {
     if (_loading) {
-      return DropdownButton<String>(
+      Widget dropdownWidget = DropdownButton<String>(
         items: [],
         hint: SizedBox(
           width: 16,
@@ -123,65 +131,93 @@ class _VberSelectState<T> extends State<VberSelect<T>> {
         ),
         onChanged: (String? value) {},
       );
-    }
 
-    if (_items == null || _items!.isEmpty) {
-      List<DropdownMenuItem<String>> dropdownItems = [];
-      
-      // 如果showAll为true,添加"全部"选项
-      if (widget.showAll) {
-        dropdownItems.add(
-          DropdownMenuItem<String>(
-            value: '',
-            child: Text('全部'),
-          ),
+      Widget selectWidget = widget.hideUnderline
+          ? DropdownButtonHideUnderline(child: dropdownWidget)
+          : dropdownWidget;
+
+      if (widget.showClearButton) {
+        return Row(
+          children: [
+            Expanded(child: selectWidget),
+            IconButton(
+              icon: const Icon(Icons.clear, size: 20),
+              onPressed: widget.enabled
+                  ? () {
+                      setState(() {
+                        _selectedValue = null;
+                      });
+                      widget.onChanged?.call(null);
+                    }
+                  : null,
+            ),
+          ],
         );
       }
-      
-      return DropdownButton<String>(
-        value: _selectedValue,
-        hint: Text(widget.hint ?? '请选择'),
-        items: dropdownItems,
-        onChanged: widget.enabled ? widget.onChanged : null,
-      );
+
+      return selectWidget;
     }
 
-    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.add(DropdownMenuItem<String>(value: '', child: Text('全部')));
     }
-    
-    // 添加转换后的选项
-    dropdownItems.addAll(options.map((option) {
-      return DropdownMenuItem<String>(
-        value: option.value,
-        child: Text(option.label),
+
+    // 只有当_items不为null且不为空时才添加实际选项
+    if (_items != null && _items!.isNotEmpty) {
+      final options = _items!.map((item) => widget.converter(item)).toList();
+      // 添加转换后的选项
+      dropdownItems.addAll(
+        options.map((option) {
+          return DropdownMenuItem<String>(
+            value: option.value,
+            child: Text(option.label),
+          );
+        }).toList(),
       );
-    }).toList());
+    }
 
-    return DropdownButton<String>(
+    Widget dropdownWidget = DropdownButton<String>(
       value: _selectedValue,
       hint: Text(widget.hint ?? '请选择'),
       items: dropdownItems,
-      onChanged: widget.enabled 
+      onChanged: widget.enabled
           ? (String? newValue) {
               setState(() {
                 _selectedValue = newValue;
               });
               widget.onChanged?.call(newValue);
-            } 
+            }
           : null,
       isExpanded: true,
     );
+
+    Widget selectWidget = widget.hideUnderline
+        ? DropdownButtonHideUnderline(child: dropdownWidget)
+        : dropdownWidget;
+
+    if (widget.showClearButton) {
+      return Row(
+        children: [
+          Expanded(child: selectWidget),
+          IconButton(
+            icon: const Icon(Icons.clear, size: 20),
+            onPressed: widget.enabled
+                ? () {
+                    setState(() {
+                      _selectedValue = null;
+                    });
+                    widget.onChanged?.call(null);
+                  }
+                : null,
+          ),
+        ],
+      );
+    }
+
+    return selectWidget;
   }
 }

+ 2 - 0
UI/CF.APP/chicken_farm/lib/pages/breeding/batch_culling_page.dart

@@ -163,6 +163,7 @@ class _BatchCullingPageState extends State<BatchCullingPage> {
               });
             },
             hint: '请选择淘汰原因',
+            hideUnderline: true,
           ),
         ),
       ],
@@ -190,6 +191,7 @@ class _BatchCullingPageState extends State<BatchCullingPage> {
               });
             },
             hint: '请选择处置方式',
+            hideUnderline: true,
           ),
         ),
       ],

+ 90 - 54
UI/CF.APP/chicken_farm/lib/pages/breeding/bind_wing_tag_page.dart

@@ -46,6 +46,26 @@ class _BindWingTagNumPageState extends State<BindWingTagNumPage> {
 
             const SizedBox(height: 20),
 
+            // 显示扫描的电子编号数量
+            if (_refIdWingTags.isNotEmpty)
+              Container(
+                width: double.infinity,
+                padding: const EdgeInsets.all(16),
+                decoration: BoxDecoration(
+                  border: Border.all(color: Colors.grey),
+                  borderRadius: BorderRadius.circular(8),
+                ),
+                child: Text(
+                  '已扫描电子编号数量: ${_refIdWingTags.length}',
+                  style: const TextStyle(
+                    fontSize: 16,
+                    fontWeight: FontWeight.bold,
+                  ),
+                ),
+              ),
+
+            const SizedBox(height: 20),
+
             // 按钮区域 - 根据不同状态显示不同按钮
             SizedBox(
               width: double.infinity,
@@ -181,38 +201,46 @@ class _BindWingTagNumPageState extends State<BindWingTagNumPage> {
       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 = [];
-              _rfIds = [];
-            });
+        const SizedBox(height: 10),
+        Container(
+          padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.grey),
+            borderRadius: BorderRadius.circular(8),
+          ),
+          child: 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 = [];
+                _rfIds = [];
+              });
 
-            // 获取翅号数据
-            if (value != null) {
-              // 使用Future.microtask确保在下一个微任务中加载数据,避免在构建过程中修改状态
-              Future.microtask(() => _loadWingTags(value));
-            }
-          },
+              // 获取翅号数据
+              if (value != null) {
+                // 使用Future.microtask确保在下一个微任务中加载数据,避免在构建过程中修改状态
+                Future.microtask(() => _loadWingTags(value));
+              }
+            },
+            hideUnderline: true,
+          ),
         ),
       ],
     );
@@ -223,28 +251,36 @@ class _BindWingTagNumPageState extends State<BindWingTagNumPage> {
       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;
-            });
-          },
+        const SizedBox(height: 10),
+        Container(
+          padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.grey),
+            borderRadius: BorderRadius.circular(8),
+          ),
+          child: 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;
+              });
+            },
+            hideUnderline: true,
+          ),
         ),
       ],
     );
@@ -337,4 +373,4 @@ class _BindWingTagNumPageState extends State<BindWingTagNumPage> {
       }
     });
   }
-}
+}

+ 2 - 0
UI/CF.APP/chicken_farm/lib/pages/breeding/individual_culling_page.dart

@@ -123,6 +123,7 @@ class _IndividualCullingPageState extends State<IndividualCullingPage> {
               });
             },
             hint: '请选择淘汰原因',
+            hideUnderline: true,
           ),
         ),
       ],
@@ -150,6 +151,7 @@ class _IndividualCullingPageState extends State<IndividualCullingPage> {
               });
             },
             hint: '请选择处置方式',
+            hideUnderline: true,
           ),
         ),
       ],