Flutter+Cubit实现带Query参数的工单筛选列表开发求助
Flutter+Cubit+Dio工单列表筛选实现修正
核心问题分析
- 状态类
FilterStatus未正确实现Equatable状态对比,初始状态切换存在类型转换风险 - Cubit未将筛选参数传递给接口请求,未实现完整的筛选状态更新逻辑
- UI缺失搜索框、下拉筛选器,按钮切换状态未同步,列表渲染结构存在嵌套问题
修正后代码实现
1. filter_status.dart(状态类修正)
import 'package:equatable/equatable.dart'; abstract class FilterStatus extends Equatable { const FilterStatus(); } class FilterChanged extends FilterStatus { final bool assignedToMe; final bool unassigned; final bool newTicket; final List<String> companies; final List<String> assignees; final String? searchKeyword; const FilterChanged({ this.assignedToMe = false, this.unassigned = false, this.newTicket = false, this.companies = const [], this.assignees = const [], this.searchKeyword, }); FilterChanged copyWith({ bool? assignedToMe, bool? unassigned, bool? newTicket, List<String>? companies, List<String>? assignees, String? searchKeyword, }) { return FilterChanged( assignedToMe: assignedToMe ?? this.assignedToMe, unassigned: unassigned ?? this.unassigned, newTicket: newTicket ?? this.newTicket, companies: companies ?? this.companies, assignees: assignees ?? this.assignees, searchKeyword: searchKeyword ?? this.searchKeyword, ); } @override List<Object?> get props => [ assignedToMe, unassigned, newTicket, companies, assignees, searchKeyword, ]; }
修正说明:
- 移除
FilterInitial,直接用带默认值的FilterChanged作为初始状态,避免类型转换错误 - 所有字段设为非空或带默认值,消除空安全隐患
- 实现完整的
props列表,确保Equatable能正确对比状态变化
2. ticket_cubit.dart(Cubit逻辑修正)
import 'package:bloc/bloc.dart'; import 'package:codes/features/ticket_list/presentation/cubit/ticket_list_state.dart'; import 'package:codes/features/ticket_list/presentation/cubit/ticket_list_status.dart'; import '../../data/repositories/ticket_list_repo.dart'; import 'filter_status.dart'; class TicketListCubit extends Cubit<TicketListState> { final TicketListRepository ticketRepository; TicketListCubit(this.ticketRepository) : super(TicketListState( ticketListStatus: TicketListInitial(), filterStatus: const FilterChanged(), )); // 根据当前筛选条件请求工单列表 void fetchTicketList() async { emit(state.copyWith(ticketListStatus: TicketListLoading())); try { final params = _buildQueryParams(); final ticketList = await ticketRepository.getTicketsList(queryParameters: params); emit(state.copyWith(ticketListStatus: TicketListSuccess(ticketList: ticketList))); } catch (e) { emit(state.copyWith(ticketListStatus: TicketListFailure(message: e.toString()))); } } // 组装接口要求的Query参数 Map<String, dynamic> _buildQueryParams() { final params = <String, dynamic>{}; final filter = state.filterStatus; if (filter.assignedToMe) params['assignedtome'] = true; if (filter.unassigned) params['unassigned'] = true; if (filter.newTicket) params['new'] = true; if (filter.searchKeyword?.isNotEmpty == true) params['search'] = filter.searchKeyword; // 处理公司列表参数:company[0]=xxx&company[1]=xxx for (int i = 0; i < filter.companies.length; i++) { params['company[$i]'] = filter.companies[i]; } // 处理经办人列表参数 for (int i = 0; i < filter.assignees.length; i++) { params['assignee[$i]'] = filter.assignees[i]; } return params; } // 切换单个状态按钮(互斥逻辑:三个按钮只能选一个) void toggleFilterButton(String type) { emit(state.copyWith( filterStatus: state.filterStatus.copyWith( assignedToMe: type == 'assigned', unassigned: type == 'unassigned', newTicket: type == 'new', ), )); fetchTicketList(); } // 更新搜索关键词 void updateSearchKeyword(String keyword) { emit(state.copyWith( filterStatus: state.filterStatus.copyWith(searchKeyword: keyword), )); // 可添加防抖逻辑,避免实时请求 fetchTicketList(); } // 更新公司筛选列表 void updateCompanies(List<String> companies) { emit(state.copyWith( filterStatus: state.filterStatus.copyWith(companies: companies), )); fetchTicketList(); } // 更新经办人筛选列表 void updateAssignees(List<String> assignees) { emit(state.copyWith( filterStatus: state.filterStatus.copyWith(assignees: assignees), )); fetchTicketList(); } }
修正说明:
- 初始化时直接使用
FilterChanged默认状态,消除类型转换风险 - 新增
_buildQueryParams方法,严格按照接口要求组装参数格式 - 实现完整的筛选状态更新方法,包含按钮切换、搜索、下拉筛选
- 每次状态更新后自动触发列表请求(可根据需求添加防抖)
3. TicketsListPage.dart(UI结构修正)
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/service_locator.dart'; import '../cubit/filter_status.dart'; import '../cubit/ticket_list_cubit.dart'; import '../cubit/ticket_list_state.dart'; import '../cubit/ticket_list_status.dart'; import '../widgets/ticket_list_widget.dart'; class TicketsListPage extends StatelessWidget { const TicketsListPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider<TicketListCubit>( create: (context) => TicketListCubit(sl())..fetchTicketList(), child: Scaffold( appBar: AppBar(title: const Text('Tickets List')), body: BlocBuilder<TicketListCubit, TicketListState>( builder: (context, state) { final cubit = context.read<TicketListCubit>(); final filter = state.filterStatus; return Column( children: [ // 顶部搜索框 Padding( padding: const EdgeInsets.all(8.0), child: TextField( decoration: const InputDecoration( hintText: '搜索工单...', prefixIcon: Icon(Icons.search), border: OutlineInputBorder(), ), onChanged: cubit.updateSearchKeyword, ), ), // 筛选按钮栏 Container( height: 50, child: Row( children: [ _buildFilterButton( context, text: 'Assigned to me', isActive: filter.assignedToMe, onTap: () => cubit.toggleFilterButton('assigned'), ), _buildFilterButton( context, text: 'Unassigned', isActive: filter.unassigned, onTap: () => cubit.toggleFilterButton('unassigned'), ), _buildFilterButton( context, text: 'New Ticket', isActive: filter.newTicket, onTap: () => cubit.toggleFilterButton('new'), ), ], ), ), // 下拉筛选器栏 Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ Expanded( child: _buildDropdown( context, hint: '选择公司', items: const ['Company1', 'Company2', 'Company3'], selectedItems: filter.companies, onChanged: cubit.updateCompanies, ), ), const SizedBox(width: 8), Expanded( child: _buildDropdown( context, hint: '选择经办人', items: const ['UserA', 'UserB', 'UserC'], selectedItems: filter.assignees, onChanged: cubit.updateAssignees, ), ), ], ), ), // 工单列表区域 Expanded( child: _buildTicketList(state), ), ], ); }, ), ), ); } // 构建筛选按钮 Widget _buildFilterButton(BuildContext context, {required String text, required bool isActive, required VoidCallback onTap}) { return Expanded( child: TextButton( style: TextButton.styleFrom( backgroundColor: isActive ? Colors.red : Colors.blue, foregroundColor: Colors.white, ), onPressed: onTap, child: Text(text), ), ); } // 构建多选下拉筛选器(示例) Widget _buildDropdown(BuildContext context, {required String hint, required List<String> items, required List<String> selectedItems, required Function(List<String>) onChanged}) { return GestureDetector( onTap: () async { final result = await showDialog<List<String>>( context: context, builder: (context) => MultiSelectDialog( title: Text(hint), items: items.map((item) => MultiSelectItem(item, item)).toList(), initialValue: selectedItems, ), ); if (result != null) onChanged(result); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(4), ), child: Text( selectedItems.isEmpty ? hint : selectedItems.join(', '), overflow: TextOverflow.ellipsis, ), ), ); } // 构建工单列表 Widget _buildTicketList(TicketListState state) { if (state.ticketListStatus is TicketListLoading) { return const Center(child: CircularProgressIndicator()); } else if (state.ticketListStatus is TicketListSuccess) { final ticketList = (state.ticketListStatus as TicketListSuccess).ticketList; return ListView.builder( itemCount: ticketList.length, itemBuilder: (context, index) => TicketListWidget(ticketListModel: ticketList[index]), ); } else if (state.ticketListStatus is TicketListFailure) { return Center(child: Text((state.ticketListStatus as TicketListFailure).message)); } return const SizedBox(); } } // 多选弹窗辅助类(需自行实现或使用第三方包) class MultiSelectDialog extends StatelessWidget { final String title; final List<MultiSelectItem> items; final List<String> initialValue; const MultiSelectDialog({super.key, required this.title, required this.items, required this.initialValue}); @override Widget build(BuildContext context) { final selected = ValueNotifier<List<String>>(List.from(initialValue)); return AlertDialog( title: Text(title), content: ValueListenableBuilder<List<String>>( valueListenable: selected, builder: (context, value, child) { return SingleChildScrollView( child: Column( children: items.map((item) { return CheckboxListTile( value: value.contains(item.value), title: Text(item.label), onChanged: (bool? checked) { if (checked == true) { selected.value = [...value, item.value]; } else { selected.value = value.where((e) => e != item.value).toList(); } }, ); }).toList(), ), ); }, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(context, selected.value), child: const Text('确认'), ), ], ); } } class MultiSelectItem { final String value; final String label; const MultiSelectItem(this.value, this.label); }
修正说明:
- 重构UI结构,添加搜索框、下拉筛选器
- 将列表请求移至Cubit创建时调用,避免重复触发
- 封装按钮、下拉筛选器组件,简化代码
- 处理列表加载、成功、失败三种状态的UI展示
- 实现多选下拉筛选弹窗(可替换为第三方包如
multi_select_flutter)
内容的提问来源于stack exchange,提问作者sianami
相关产品推荐
相关产品推荐

