背景
业务开始需求相对简单,搜索条件也比较单一,例如:根据订单号OrderNo查询对应订单。随着业务需求的升级,需要支持的查询条件也越来越多,组合查询、额外条件查询等需求升级,搜索上也要适配同步升级。
开发历程
单一搜索
原始需求是支持根据订单号查询
多条件搜索(互斥)
这些搜索条件也是在使用过程中逐渐加上来的,因为原始设计UI参与后撤离了,导致后续需求跟进缺乏UI设计支持。虽然能满足需求,界面交互设计上真的是一言难尽。
多条件搜索(互斥 + 部分组合)
这一阶段是一个临时的方案,根据进入入口的不同,区分搜索界面进入哪种查询模式。
- 由筛选弹框触发组合搜索模式,进入搜索页面组合展示搜索条件
- 其它场景进入上步的互斥搜索模式
从功能实现、界面展示、用户交互等角度看,都不是一个好的方案。于是决定参照业界优秀的案例进行改造。
改造
案例
电商产品比较通用的方案,比较适配当前业务
实现
首先将上图贴到豆包
尝试搜索到相关现成插件,未发现任何相关插件。又进入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编程兴起后,有些落伍了。程序老兵们转变一下思路,或许还能苟几年。