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

Flutter Firestore多条件过滤数据遇索引报错及功能需求实现

Flutter Firestore 过滤数据问题解决

问题概述

在Flutter项目中使用Firestore的transaction集合,需要按transactionType和date字段过滤数据并展示:

  • 已实现transactionType单字段过滤,但日期过滤存在逻辑错误
  • 同时选择两类过滤时触发FAILED_PRECONDITION错误,提示需创建复合索引
  • 需要实现未选择任何过滤时展示全量数据

解决方案

1. 复合索引创建

Firestore对多字段组合查询要求创建复合索引,按以下配置在Firebase控制台手动创建:

  • 目标集合:transaction
  • 索引字段:transactionType(升序)、date(升序)
  • 注意:索引创建需几分钟生效,期间查询可能仍报错

2. 日期过滤逻辑修正

原代码中日期Radio的value与groupValue类型不匹配,且日期判断逻辑错误(比如"今日"应该查询大于等于当日0点的数据,而非小于昨日时间),修正点:

  • 将selectedDateFilter改为存储日期范围标记(如'today'、'yesterday'、'weekAgo'),避免DateTime对象对比的精度问题
  • 按标记生成正确时间范围:
    • 今日:date >= 当前日期的0点
    • 昨日:date >= 昨日0点 且 date < 当前日期0点
    • 一周前:date >= 7天前的0点

3. 全量数据展示逻辑

原代码中当selectedTransactionType为空且selectedDateRange为null时,filteredQuery直接指向原始集合,已支持全量展示,只需确保过滤条件清空时状态正确即可。


修正后完整代码

import "package:flutter/material.dart";
import "package:cloud_firestore/cloud_firestore.dart";
import "package:intl/intl.dart";

// 扩展方法:获取当日起始时间(00:00:00)
extension DateTimeExtension on DateTime {
  DateTime get startOfDay => DateTime(year, month, day);
}

class FilteringScreen extends StatefulWidget {
  @override
  _FilteringScreenState createState() => _FilteringScreenState();
}

class _FilteringScreenState extends State<FilteringScreen> {
  String selectedTransactionType = '';
  String? selectedDateRange; // 存储日期范围标记:'today'/'yesterday'/'weekAgo'

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('过滤页面'),
        actions: [
          IconButton(
            icon: const Icon(Icons.filter_alt),
            onPressed: () async {
              await _showFilterDialog(context);
            },
          ),
        ],
      ),
      body: _buildStreamBuilder(),
    );
  }

  StreamBuilder<QuerySnapshot> _buildStreamBuilder() {
    CollectionReference transactions = FirebaseFirestore.instance.collection('transaction');

    Query filteredQuery = transactions;

    // 交易类型过滤
    if (selectedTransactionType.isNotEmpty) {
      filteredQuery = filteredQuery.where('transactionType', isEqualTo: selectedTransactionType);
    }

    // 日期范围过滤
    if (selectedDateRange != null) {
      DateTime now = DateTime.now();
      switch (selectedDateRange) {
        case 'today':
          filteredQuery = filteredQuery.where('date', isGreaterThanOrEqualTo: now.startOfDay);
          break;
        case 'yesterday':
          DateTime yesterdayStart = now.subtract(const Duration(days: 1)).startOfDay;
          filteredQuery = filteredQuery
              .where('date', isGreaterThanOrEqualTo: yesterdayStart)
              .where('date', isLessThan: now.startOfDay);
          break;
        case 'weekAgo':
          DateTime weekAgoStart = now.subtract(const Duration(days: 7)).startOfDay;
          filteredQuery = filteredQuery.where('date', isGreaterThanOrEqualTo: weekAgoStart);
          break;
      }
    }

    return StreamBuilder<QuerySnapshot>(
      stream: filteredQuery.snapshots(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }

        if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
          return const Center(child: Text('暂无数据'));
        }

        var docs = snapshot.data!.docs;
        return ListView.builder(
          itemCount: docs.length,
          itemBuilder: (context, index) {
            var data = docs[index];
            DateTime transactionDate = (data['date'] as Timestamp).toDate();
            return ListTile(
              title: Text(data['imei']),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('类型: ${data['transactionType']}'),
                  Text('日期: ${DateFormat('yyyy-MM-dd HH:mm').format(transactionDate)}'),
                ],
              ),
            );
          },
        );
      },
    );
  }

  Future<void> _showFilterDialog(BuildContext context) async {
    await showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('过滤选项'),
          content: SizedBox(
            width: double.maxFinite,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                // 交易类型选择
                const Text('交易类型'),
                Row(
                  children: [
                    Radio(
                      value: 'In',
                      groupValue: selectedTransactionType,
                      onChanged: (value) {
                        setState(() {
                          selectedTransactionType = value.toString();
                        });
                      },
                    ),
                    const Text('入库'),
                    Radio(
                      value: 'Out',
                      groupValue: selectedTransactionType,
                      onChanged: (value) {
                        setState(() {
                          selectedTransactionType = value.toString();
                        });
                      },
                    ),
                    const Text('出库'),
                    Radio(
                      value: '',
                      groupValue: selectedTransactionType,
                      onChanged: (value) {
                        setState(() {
                          selectedTransactionType = value.toString();
                        });
                      },
                    ),
                    const Text('全部'),
                  ],
                ),
                const SizedBox(height: 16),
                // 日期范围选择
                const Text('日期范围'),
                Column(
                  children: [
                    RadioListTile(
                      title: const Text('今日'),
                      value: 'today',
                      groupValue: selectedDateRange,
                      onChanged: (value) {
                        setState(() {
                          selectedDateRange = value;
                        });
                      },
                    ),
                    RadioListTile(
                      title: const Text('昨日'),
                      value: 'yesterday',
                      groupValue: selectedDateRange,
                      onChanged: (value) {
                        setState(() {
                          selectedDateRange = value;
                        });
                      },
                    ),
                    RadioListTile(
                      title: const Text('一周前至今'),
                      value: 'weekAgo',
                      groupValue: selectedDateRange,
                      onChanged: (value) {
                        setState(() {
                          selectedDateRange = value;
                        });
                      },
                    ),
                    RadioListTile(
                      title: const Text('全部日期'),
                      value: null,
                      groupValue: selectedDateRange,
                      onChanged: (value) {
                        setState(() {
                          selectedDateRange = value;
                        });
                      },
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed: () {
                    Navigator.of(context).pop();
                    setState(() {}); // 触发界面刷新
                  },
                  child: const Text('确认'),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

补充说明

  • 新增DateTimeExtension扩展方法,简化当日起始时间的获取
  • 将日期选择改为RadioListTile,提升UI可读性和交互体验
  • 修复日期查询逻辑错误,确保过滤结果符合预期
  • 增加日期格式化展示,让列表项信息更完整

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 05:40:59