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 = Future> Function({ required Map queryParams, }); /// 选项转换器,将数据类型T转换为SelectOption typedef SelectOptionConverter = SelectOption Function(T data); /// 搜索选择组件 class VberSearchSelect extends StatefulWidget { /// 搜索API函数 final SearchApiFunction searchApi; /// 数据转换方法,将原始数据转换为SelectOption final SelectOptionConverter converter; /// 根据value获取对应label的函数 final Future Function(String?)? getValueLabel; /// 当前选中的值 final String? value; /// 值改变回调 final ValueChanged? onChanged; /// 提示文字 final String? hint; /// 是否启用 final bool enabled; /// 每页数据条数 final int pageSize; /// 额外参数,用于传递给搜索API final Map? extraParams; /// 是否隐藏下划线 final bool hideUnderline; /// 是否显示清空按钮 final bool showClearButton; const VberSearchSelect({ super.key, required this.searchApi, required this.converter, this.getValueLabel, this.value, this.onChanged, this.hint, this.enabled = true, this.pageSize = 10, this.extraParams, this.hideUnderline = false, this.showClearButton = true, }); @override State> createState() => _VberSearchSelectState(); } class _VberSearchSelectState extends State> { final TextEditingController _searchController = TextEditingController(); final FocusNode _searchFocusNode = FocusNode(); String? _selectedValue; SelectOption? _selectedOption; final List _items = []; @override void initState() { super.initState(); _selectedValue = widget.value; _initSelectedOption(); } /// 初始化选中选项 void _initSelectedOption() async { if (widget.value != null && widget.getValueLabel != null) { try { final label = await widget.getValueLabel!(widget.value); if (mounted) { setState(() { _selectedOption = SelectOption( label: label ?? widget.value ?? '', value: widget.value!, ); }); } } catch (e) { logger.e('Failed to get label for value: ${widget.value}', e); } } } @override void didUpdateWidget(covariant VberSearchSelect oldWidget) { super.didUpdateWidget(oldWidget); // 当外部传入的value发生变化时,同步更新内部状态 if (oldWidget.value != widget.value) { _syncValueAndText(widget.value); } } /// 同步外部value和内部_selectedValue以及_selectedOption 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本身 > 空字符串 if (newValue != null) { _selectedOption = SelectOption( label: label ?? newValue, value: newValue, ); } else { _selectedOption = null; } }); } } @override void dispose() { _searchController.dispose(); _searchFocusNode.dispose(); super.dispose(); } void _showSearchDialog() async { final result = await showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return _SearchDialog( 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> dropdownItems = []; // 添加当前选中项到下拉列表(如果存在) if (_selectedOption != null) { dropdownItems.add( DropdownMenuItem( 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( value: option.value, child: Text(option.label), ), ); } } } // 创建一个不会真正展开下拉列表的DropdownButton Widget dropdownWidget = DropdownButton( 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; } } /// 搜索对话框 class _SearchDialog extends StatefulWidget { final SearchApiFunction searchApi; final SelectOptionConverter converter; final String? hint; final int pageSize; final Map? 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 createState() => _SearchDialogState(); } class _SearchDialogState extends State<_SearchDialog> { List _items = []; bool _isLoading = false; int _currentPage = 1; bool _hasMore = true; String _lastSearchText = ''; final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); // 清空搜索框 widget.searchController.clear(); _items.clear(); _currentPage = 1; _hasMore = true; _lastSearchText = ''; _performSearch(''); // 移除自动获取焦点的代码,让用户手动点击输入框获取焦点 // WidgetsBinding.instance.addPostFrameCallback((_) { // widget.searchFocusNode.requestFocus(); // }); _scrollController.addListener(_scrollListener); } @override void dispose() { _scrollController.removeListener(_scrollListener); _scrollController.dispose(); super.dispose(); } void _scrollListener() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && _hasMore && !_isLoading) { _loadMore(); } } void _loadMore() async { setState(() { _isLoading = true; }); try { _currentPage++; final result = await _fetchData(_currentPage); final List rows = result['rows'] is List ? result['rows'] : []; final int total = result['total'] is int ? result['total'] : 0; final newItems = rows.cast().toList(); if (mounted) { setState(() { _items.addAll(newItems); _hasMore = _items.length < total; _isLoading = false; }); } } catch (e) { logger.e('加载更多失败:$e'); if (mounted) { setState(() { _currentPage--; // 回退页码 _isLoading = false; }); } } } void _performSearch(String keyword) async { setState(() { _isLoading = true; }); try { _currentPage = 1; _hasMore = true; final result = await _fetchData(_currentPage, keyword: keyword); final List rows = result['rows'] is List ? result['rows'] : []; final int total = result['total'] is int ? result['total'] : 0; final newItems = rows.cast().toList(); if (mounted) { setState(() { _items = newItems; _hasMore = _items.length < total; _isLoading = false; }); } } catch (e) { logger.e('搜索失败:$e'); if (mounted) { setState(() { _items = []; _hasMore = false; _isLoading = false; }); } } } Future> _fetchData( int pageNum, { String? keyword, }) async { final Map queryParams = { 'pageSize': widget.pageSize, 'pageNum': pageNum, }; if (keyword != null && keyword.isNotEmpty) { queryParams['keyword'] = keyword; } if (widget.extraParams != null) { queryParams.addAll(widget.extraParams!); } final r = await widget.searchApi(queryParams: queryParams); return r; } 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 _selectItem(SelectOption option, T item) { Navigator.of(context).pop(option); } @override Widget build(BuildContext context) { 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( 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('取消'), ), ], ); } }