| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530 |
- 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;
- /// 是否隐藏下划线
- 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<VberSearchSelect<T>> createState() => _VberSearchSelectState<T>();
- }
- class _VberSearchSelectState<T> extends State<VberSearchSelect<T>> {
- final TextEditingController _searchController = TextEditingController();
- final FocusNode _searchFocusNode = FocusNode();
- String? _selectedValue;
- SelectOption? _selectedOption;
- final List<T> _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<T> 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<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;
- }
- }
- /// 搜索对话框
- 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>();
- }
- 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();
- // 清空搜索框
- 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<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) {
- 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<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) {
- setState(() {
- _items = newItems;
- _hasMore = _items.length < total;
- _isLoading = false;
- });
- }
- } catch (e) {
- logger.e('搜索失败:$e');
- if (mounted) {
- setState(() {
- _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 && 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<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('取消'),
- ),
- ],
- );
- }
- }
|