You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter+Cubit实现带Query参数的工单筛选列表开发求助

Flutter+Cubit+Dio工单列表筛选实现修正

核心问题分析

  1. 状态类FilterStatus未正确实现Equatable状态对比,初始状态切换存在类型转换风险
  2. Cubit未将筛选参数传递给接口请求,未实现完整的筛选状态更新逻辑
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 10:44:57