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编程兴起后,有些落伍了。程序老兵们转变一下思路,或许还能苟几年。

相关推荐
LawrenceLan1 天前
Flutter 零基础入门(九):构造函数、命名构造函数与 this 关键字
开发语言·flutter·dart
一豆羹1 天前
macOS 环境下 ADB 无线调试连接失败、Protocol Fault 及端口占用的深度排查
flutter
行者961 天前
OpenHarmony上Flutter粒子效果组件的深度适配与实践
flutter·交互·harmonyos·鸿蒙
行者961 天前
Flutter与OpenHarmony深度集成:数据导出组件的实战优化与性能提升
flutter·harmonyos·鸿蒙
小雨下雨的雨1 天前
Flutter 框架跨平台鸿蒙开发 —— Row & Column 布局之轴线控制艺术
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨1 天前
Flutter 框架跨平台鸿蒙开发 —— Center 控件之完美居中之道
flutter·ui·华为·harmonyos·鸿蒙
小雨下雨的雨1 天前
Flutter 框架跨平台鸿蒙开发 —— Icon 控件之图标交互美学
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨1 天前
Flutter 框架跨平台鸿蒙开发 —— Placeholder 控件之布局雏形美学
flutter·ui·华为·harmonyos·鸿蒙系统
行者961 天前
OpenHarmony Flutter弹出菜单组件深度实践:从基础到高级的完整指南
flutter·harmonyos·鸿蒙
前端不太难1 天前
Flutter / RN / iOS,在长期维护下的性能差异本质
flutter·ios