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

如何在Flutter中实现带科目、时段高级筛选的家教搜索栏?

实现带高级筛选的Flutter家教搜索栏

整体布局思路

用AppBar承载搜索栏和筛选入口,点击筛选图标弹出底部弹窗(showModalBottomSheet)放置所有筛选组件,完成筛选后点击确认按钮应用条件。

核心实现步骤

1. 定义筛选状态变量

在StatefulWidget的State类里维护所有筛选条件的状态:

// 科目选项与选中列表
List<String> subjects = ['数学', '生物', '英语', '物理', '化学'];
List<String> selectedSubjects = [];

// 日期选项与选中状态(对应每个日期的选中与否)
List<String> weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
List<bool> selectedWeekdays = List.filled(7, false);

// 时段选项与选中列表
List<String> timeSlots = ['9-11点', '11-13点', '14-16点', '17-19点', '19-21点'];
List<String> selectedTimeSlots = [];

// 授课时长(默认1小时,范围1-4小时)
double classDuration = 1.0;

2. 搜索栏+筛选入口布局

在AppBar中实现基础搜索与筛选触发:

AppBar(
  title: TextField(
    decoration: InputDecoration(
      hintText: '搜索家教',
      border: InputBorder.none,
    ),
    onChanged: (value) {
      // 此处可添加实时搜索逻辑
    },
  ),
  actions: [
    IconButton(
      icon: const Icon(Icons.filter_alt),
      onPressed: () {
        // 弹出筛选弹窗
        showModalBottomSheet(
          context: context,
          isScrollControlled: true,
          builder: (context) => FilterBottomSheet(
            // 传递状态与回调
            subjects: subjects,
            selectedSubjects: selectedSubjects,
            onSubjectToggle: (subject) {
              setState(() {
                selectedSubjects.contains(subject) 
                    ? selectedSubjects.remove(subject) 
                    : selectedSubjects.add(subject);
              });
            },
            weekdays: weekdays,
            selectedWeekdays: selectedWeekdays,
            onWeekdayToggle: (index) {
              setState(() {
                selectedWeekdays[index] = !selectedWeekdays[index];
              });
            },
            timeSlots: timeSlots,
            selectedTimeSlots: selectedTimeSlots,
            onTimeSlotToggle: (slot) {
              setState(() {
                selectedTimeSlots.contains(slot) 
                    ? selectedTimeSlots.remove(slot) 
                    : selectedTimeSlots.add(slot);
              });
            },
            classDuration: classDuration,
            onDurationChanged: (value) {
              setState(() {
                classDuration = value;
              });
            },
            onConfirm: () {
              // 应用筛选条件,执行搜索
              Navigator.pop(context);
              // 此处可替换为实际的搜索接口调用
              print('筛选条件:科目=$selectedSubjects,日期=$selectedWeekdays,时段=$selectedTimeSlots,时长=$classDuration');
            },
          ),
        );
      },
    ),
  ],
)

3. 筛选弹窗组件(FilterBottomSheet)

单独封装成可复用组件,接收状态与回调:

class FilterBottomSheet extends StatelessWidget {
  final List<String> subjects;
  final List<String> selectedSubjects;
  final Function(String) onSubjectToggle;
  final List<String> weekdays;
  final List<bool> selectedWeekdays;
  final Function(int) onWeekdayToggle;
  final List<String> timeSlots;
  final List<String> selectedTimeSlots;
  final Function(String) onTimeSlotToggle;
  final double classDuration;
  final Function(double) onDurationChanged;
  final VoidCallback onConfirm;

  const FilterBottomSheet({
    super.key,
    required this.subjects,
    required this.selectedSubjects,
    required this.onSubjectToggle,
    required this.weekdays,
    required this.selectedWeekdays,
    required this.onWeekdayToggle,
    required this.timeSlots,
    required this.selectedTimeSlots,
    required this.onTimeSlotToggle,
    required this.classDuration,
    required this.onDurationChanged,
    required this.onConfirm,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(
        bottom: MediaQuery.of(context).viewInsets.bottom,
        left: 16,
        right: 16,
        top: 16,
      ),
      child: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 科目筛选
            const Text('选择科目', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: subjects.map((subject) {
                return Chip(
                  label: Text(subject),
                  backgroundColor: selectedSubjects.contains(subject) ? Colors.blue : Colors.grey[200],
                  labelColor: selectedSubjects.contains(subject) ? Colors.white : Colors.black,
                  onPressed: () => onSubjectToggle(subject),
                );
              }).toList(),
            ),
            const SizedBox(height: 16),

            // 日期筛选
            const Text('选择授课日期', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            ToggleButtons(
              isSelected: selectedWeekdays,
              onPressed: onWeekdayToggle,
              children: weekdays.map((day) => Text(day)).toList(),
              selectedColor: Colors.white,
              fillColor: Colors.blue,
              borderColor: Colors.grey,
            ),
            const SizedBox(height: 16),

            // 时段筛选
            const Text('选择授课时段', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: timeSlots.map((slot) {
                return Chip(
                  label: Text(slot),
                  backgroundColor: selectedTimeSlots.contains(slot) ? Colors.blue : Colors.grey[200],
                  labelColor: selectedTimeSlots.contains(slot) ? Colors.white : Colors.black,
                  onPressed: () => onTimeSlotToggle(slot),
                );
              }).toList(),
            ),
            const SizedBox(height: 16),

            // 时长滑块
            Text('授课时长:${classDuration.toStringAsFixed(1)}小时', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            Slider(
              value: classDuration,
              min: 1.0,
              max: 4.0,
              divisions: 6, // 1-4小时,每0.5小时一个刻度
              onChanged: onDurationChanged,
              label: '${classDuration.toStringAsFixed(1)}小时',
            ),
            const SizedBox(height: 24),

            // 确认按钮
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: onConfirm,
                child: const Text('应用筛选'),
              ),
            ),
            const SizedBox(height: 16),
          ],
        ),
      ),
    );
  }
}

关键逻辑说明

  • 所有筛选状态通过父组件的setState更新,确保UI实时响应
  • 用Wrap+Chip实现标签式多选,选中状态通过背景色区分,样式易调整
  • ToggleButtons适合日期这类固定数量的选项,布局更规整
  • Slider通过divisions设置刻度,让用户精准选择时长
  • 弹窗设置isScrollControlled: true,避免键盘遮挡(若后续添加输入类筛选)

后续扩展建议

  • 将每个筛选模块拆分为独立小组件,提升代码可读性
  • 添加“重置筛选”按钮,一键清空所有选择
  • 对接后端API,将筛选条件作为请求参数发起搜索

内容的提问来源于stack exchange,提问作者Aidir

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 02:40:53