Flutter应用开发:多条件搜索

背景

业务开始需求相对简单,搜索条件也比较单一,例如:根据订单号OrderNo查询对应订单。随着业务需求的升级,需要支持的查询条件也越来越多,组合查询、额外条件查询等需求升级,搜索上也要适配同步升级。

开发历程

单一搜索

原始需求是支持根据订单号查询

多条件搜索(互斥)

这些搜索条件也是在使用过程中逐渐加上来的,因为原始设计UI参与后撤离了,导致后续需求跟进缺乏UI设计支持。虽然能满足需求,界面交互设计上真的是一言难尽。

多条件搜索(互斥 + 部分组合)

这一阶段是一个临时的方案,根据进入入口的不同,区分搜索界面进入哪种查询模式。

  1. 由筛选弹框触发组合搜索模式,进入搜索页面组合展示搜索条件
  2. 其它场景进入上步的互斥搜索模式

从功能实现、界面展示、用户交互等角度看,都不是一个好的方案。于是决定参照业界优秀的案例进行改造。

改造

案例

电商产品比较通用的方案,比较适配当前业务

实现

首先将上图贴到豆包尝试搜索到相关现成插件,未发现任何相关插件。又进入gitee尝试找到类似电商开源项目剥离出实现逻辑,也未发现适配项目。不得已开始分析按钮,人工实现一下。

搜索条件Tag展示区域

分析自身需求,提取相似点,分离不同点。因为状态(单选组件)、时间范围(时间选择组件)、其他都是输入框,大体分为三类。

dart 复制代码
// 定义渲染Tag
List<Map<String, String>> _dropdownItems = [
    {'label': '订单号', 'value': 'fuzzyOrderNo'},
    {'label': '揽货人', 'value': 'accUserName'},
    {'label': '发货人', 'labels': '发货人姓名,发货人电话', 'value': 'corUserName,corUserPhone'}, 
    {'label': '收货人', 'labels': '收货人姓名,收货人电话', 'value': 'ceeUserName,ceeUserPhone'}, 
    {'label': '站点', 'value': 'toStation'},
];
  
List<Widget> queryTags = [
    QueryTag(text: '状态', isActive: _order.orderStatus != null && _order.orderStatus!.isNotEmpty),
    QueryTag(text: '时间范围', isActive: _order.createTimeStart != null),
    ..._dropdownItems.map((e) {
    final values = e['value']!.split(',');
    return QueryTag(text: e['label']!, isActive: values.any((v) => _order.getProperty(v) != null));
    }).toList(),
];

渲染部分使用Wrap组件来实现,因为搜索条件动态渲染,除过支持自动换行 外,还需要计算渲染区域的高度来动态设置。

dart 复制代码
    Widget wrapper = LayoutBuilder(
      key: _wrapperKey,
      builder: (context, constraints) {
        // 在布局完成后获取高度
        WidgetsBinding.instance.addPostFrameCallback((_) {
          final RenderBox? renderBox = _wrapperKey.currentContext?.findRenderObject() as RenderBox?;
          if (renderBox != null && mounted) {
            setState(() {
              _wrapperHeight = renderBox.size.height + rpx(8.0); // 增加内边距余量
            });
          }
        });
        return Wrap(
          direction: Axis.horizontal,
          spacing: 6.0,
          runSpacing: 4.0,
          children: queryTags
              .map(
                (e) => InkWell(
                  child: Container(width: rpx(80.0), child: e),
                  onTap: () {
                    // 计算当前标签在queryTags中的索引
                    final tagIndex = queryTags.indexOf(e);
                    _selectedIndexCache = tagIndex;
                    _showSearchBottomSheet();
                  },
                ),
              )
              .toList(),
        );
      },
    );

    return <Widget>[
      SliverPersistentHeader(
        pinned: true,
        floating: true,
        delegate: SliverAppBarDelegate(
          Container(
              height: MediaQuery.of(context).size.height,
              padding: edgeSymmetric(vertical: 4.0),
              child: Column(
                children: [Container(child: wrapper)],
              )),
          _wrapperHeight > 0 ? _wrapperHeight : rpx(90.0),
        ),
      ),
    ];

实现效果和案例上还是有些差距,不过已经美观很多

搜索弹框

搜索弹框需要注意和Tag区域的联动交互,组合查询的表现,三类组件的实现方式。左侧Tab以红点表示对应条件已经应用,顶部Tag区域的红色高亮与其对应。样式参考按钮优化。

代码比较多,整体划分左右两部分,只需要注意使用IndexedStack组件完成左侧Tab点击展示右侧对应锚点内容。右侧业务区域设置一组动态Key来做唯一标识。

js 复制代码
 final Map<int, GlobalKey> _formKeys = Map.fromEntries(List.generate(_dropdownItems.length + 2, (index) => MapEntry(index, GlobalKey())));
js 复制代码
 IndexedStack(index: _selectedIndex, children: [
                // 状态选择(使用_index=0)
                SpecificaButton(
                    key: _formKeys[0],
                    title: '选择状态',
                    defaultValue: _order.orderStatus,
                    vModels: _searchStatusList,
                    canCancel: true,
                    onChanged: (value) {
                      Iterable<vModel> temp = _searchStatusList.where((element) => element.id == value);
                      setState(() {
                        _order.orderStatus = value != null ? temp.first.id : null;
                      });
                      _setFilterAdditional();
                    }),

                Padding(
                    key: _formKeys[1],
                    padding: EdgeInsets.all(rpx(12)),
                    child: Column(children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: [Text('起始时间', style: TextStyle(fontSize: rpx(11.0)))],
                      ),
                      Gaps.vGap5,
                      ElFormItem(
                        '',
                        type: 'select',
                        required: false,
                        hintText: '请选择起始时间',
                        initValue: _order.createTimeStartStr,
                        labelSize: rpx(12.0),
                        onOpenSheet: () async {
                          final result = await showBoardDateTimePicker(
                            context: context,
                            pickerType: DateTimePickerType.datetime,
                            options: BoardDateTimeOptions(
                              languages: const BoardPickerLanguages.zh(),
                              startDayOfWeek: DateTime.sunday,
                              useResetButton: true,
                              pickerFormat: PickerFormat.ymd,
                              withSecond: true,
                            ),
                          );
                          if (result != null) {
                            setState(() {
                              _order.createTimeStartStr = DateFormat('yyyy-MM-dd HH:mm:ss').format(result);
                              _order.createTimeStart = result.millisecondsSinceEpoch;
                            });
                          }
                        },
                        suffixIcon: IconButton(
                            onPressed: () {
                              setState(() {
                                _order.createTimeStartStr = null;
                                _order.createTimeStart = null;
                              });
                            }, // ← 这里添加缺失的逗号
                            padding: edgeAll(0),
                            icon: Icon(Icons.close, size: rpx(14.0))),
                      ),
                      Gaps.vGap10,
                      Row(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: [Text('截止时间', style: TextStyle(fontSize: rpx(11.0)))],
                      ),
                      Gaps.vGap5,
                      ElFormItem(
                        '',
                        type: 'select',
                        required: false,
                        hintText: '请选择截止时间',
                        initValue: _order.createTimeEndStr,
                        labelSize: rpx(12.0),
                        onOpenSheet: () async {
                          final result = await showBoardDateTimePicker(
                            context: context,
                            pickerType: DateTimePickerType.datetime,
                            options: BoardDateTimeOptions(
                              languages: const BoardPickerLanguages.zh(),
                              startDayOfWeek: DateTime.sunday,
                              useResetButton: true,
                              pickerFormat: PickerFormat.ymd,
                              withSecond: true,
                            ),
                          );
                          if (result != null) {
                            setState(() {
                              _order.createTimeEndStr = DateFormat('yyyy-MM-dd HH:mm:ss').format(result);
                              _order.createTimeEnd = result.millisecondsSinceEpoch;
                            });
                          }
                        },
                        suffixIcon: IconButton(
                            onPressed: () {
                              setState(() {
                                _order.createTimeEndStr = null;
                                _order.createTimeEnd = null;
                              });
                            },
                            padding: edgeAll(0),
                            icon: Icon(Icons.close, size: rpx(14.0))),
                      ),
                    ])),

                // 动态生成字段输入(使用_index=1到N)
                ..._dropdownItems.asMap().entries.map((entry) {
                  final values = entry.value['value']!.split(',');
                  final labels = entry.value['labels'] != null ? entry.value['labels']!.split(',') : [entry.value['label']!];
                  return Column(
                      key: _formKeys[2 + entry.key], // 从1开始分配key
                      children: [
                        ...values.map((fValue) => Padding(
                            padding: EdgeInsets.all(rpx(12)),
                            child: Column(
                              children: [
                                Row(
                                  mainAxisAlignment: MainAxisAlignment.start,
                                  children: [Text(labels[values.indexOf(fValue)], style: TextStyle(fontSize: rpx(11.0)))],
                                ),
                                Gaps.vGap5,
                                ElFormItem(
                                  '',
                                  required: false,
                                  hintText: '请输入',
                                  initValue: _order.getProperty(fValue),
                                  labelSize: rpx(12.0),
                                  suffixIcon: IconButton(
                                    onPressed: () {
                                      setState((() {
                                        _order.setProperty(fValue, null);
                                      }));
                                    }, // ← 这里添加缺失的逗号
                                    color: Colours.status_gray,
                                    icon: Icon(
                                      Icons.close,
                                      size: rpx(14.0),
                                    ),
                                  ),
                                  onChanged: (value) {
                                    _order.setProperty(fValue, value);
                                  },
                                ),
                              ],
                            )))
                      ]);
                }),
              ])

后记

在开发思路上在寻找现成插件上浪费太多时间,迟迟没有落地实现。真正没有办法去实现时,借助AI辅助编程发现效率也很高,效果也比较好。之前一直遵循的开发思路在AI编程兴起后,有些落伍了。程序老兵们转变一下思路,或许还能苟几年。

相关推荐
书弋江山7 小时前
flutter 自定义控件RenderObjectWidget使用
前端·javascript·flutter
帅次10 小时前
Flutter 输入组件 Radio 详解
android·flutter·ios·kotlin·android studio
bst@微胖子14 小时前
Flutter项目之页面实现以及路由fluro
android·javascript·flutter
科昂18 小时前
Dart异步编程:一、认识任务
android·flutter·dart
好的佩奇19 小时前
Dart 之异常处理
android·flutter·dart
帅次19 小时前
Flutter 输入组件 Checkbox 详解
android·flutter·ios·kotlin·android studio
折翅鵬1 天前
Flutter运行错误:UG! exception in phase ‘semantic analysis‘
flutter
予安灵1 天前
Vue.js 组件开发全解析:从基础概念到实战应用
javascript·vue.js·flutter·前端框架·vue·组件
掘金酱1 天前
[周二直播] 用 Trae 和 Flutter 开发你的第一款全平台"答案之书" APP|AI For Code工作坊 Vol.8
flutter·trae·ai 编程