前言:为什么“先点筛选再搜索”最容易暴露逻辑问题
待办列表只有筛选时,很多人会写一个“进行中数组”;只有搜索时,又写一个“搜索结果数组”。两个功能单独演示都能跑,一旦用户先点“进行中”再输入关键词,或先搜索再切到“已完成”,结果就开始不稳定:搜索结果覆盖了筛选结果,清空搜索词后列表无法恢复,顶部数量和实际卡片数对不上。
这类问题的根源不是条件判断写得不够多,而是把“结果”当成了需要保存的数据。筛选和搜索本质上都是查询条件:任务源数据不变,用户只是改变了观察角度。正确做法是保存条件,把结果从同一个方法中重新推导。
本文以当前鸿蒙 ArkUI 待办面板为例,完整拆解getVisibleTodos()的写法。它把完成状态筛选和关键词搜索组合为一次过滤,并让列表、摘要和空状态共同依赖这一结果。这样无论用户怎样切换操作顺序,页面都有唯一答案。
一、先确定三个输入和一个输出
组合筛选前,先明确输入。当前页面的任务源数据是todos,用户可改变的条件是filter和keyword:
typeTodoFilter='全部'|'进行中'|'已完成';@Stateprivatetodos:TodoItem[]=[];@Stateprivatefilter:TodoFilter='全部';@Stateprivatekeyword:string='';输出只有一个:当前应该渲染到列表中的任务数组。它不是新的@State,而是每次根据上述输入计算出来的结果。
todos + filter + keyword ↓ getVisibleTodos() ↓ 列表 / 当前数量 / 空状态这个结构看似普通,却避免了大量分支。筛选按钮不再负责“把列表换成什么”,它只负责更新filter;搜索框不再负责“保存搜索结果”,它只负责更新keyword。真正决定列表内容的规则集中在同一个位置。
二、把筛选条件写成可读的布尔变量
当前工程中的核心方法如下:
privategetVisibleTodos():TodoItem[]{constquery=this.keyword.trim().toLowerCase();returnthis.todos.filter((item:TodoItem):boolean=>{constmatchesFilter=this.filter==='全部'||(this.filter==='进行中'&&!item.done)||(this.filter==='已完成'&&item.done);constmatchesKeyword=query.length===0||item.title.toLowerCase().includes(query)||item.category.toLowerCase().includes(query);returnmatchesFilter&&matchesKeyword;});}不要被它放在一个方法里迷惑,内部其实分成四步。
第一步是规范化关键词:trim()去掉用户输入前后的空白,toLowerCase()让英文大小写比较更稳定。当前示例任务是中文为主,大小写处理对中文没有副作用;如果以后允许英文分类和标题,它能避免Task与task被当成两个词。
第二步是计算matchesFilter。全部不限制完成状态;进行中要求!item.done;已完成要求item.done。把逻辑拆成有名字的变量,比在return后面写一长串条件更容易阅读,也方便以后增加“高优先级”“今天到期”等新筛选。
第三步是计算matchesKeyword。关键词为空时,所有任务都匹配;不为空时,标题或分类只要有一个包含查询词就视为命中。这里选择“标题 OR 分类”,是因为用户在待办面板中往往记得任务名称,也可能只记得它属于“内容”或“验证”。
第四步才是组合:matchesFilter && matchesKeyword。这意味着一项任务必须同时满足状态和搜索要求。使用&&是本篇最关键的语义;如果错写成||,一个已完成但标题命中的任务会出现在“进行中”页签中,筛选规则就被破坏了。
三、为什么结果不能再保存一次
下面是常见但不推荐的思路:
@StateprivatefilteredTodos:TodoItem[]=[];@StateprivatesearchedTodos:TodoItem[]=[];这样设计后,用户每次输入字符都要决定搜索哪一份数组;切换筛选时又要决定是否在搜索结果上再筛;勾选任务后还要更新至少两份结果。结果数组越来越多,真实数据源反而变得不清楚。
派生方法没有这个问题。getVisibleTodos()永远从todos出发,条件改变后直接重算。它不是缓存,因此不存在“缓存忘记更新”的风险。对于当前的少量任务,这种遍历成本很小,换来的是可靠的行为和更短的状态链。
如果未来列表非常长,优化顺序也不应颠倒。先通过实际分析确认计算真的成为瓶颈,再考虑节流输入、分页、索引或缓存策略;即使加入优化,也要确保缓存的失效条件覆盖todos、filter、keyword三个输入。没有测量就提前维护两份列表,通常只会把复杂度从性能问题变成正确性问题。
四、让按钮和输入框只修改自己的条件
筛选项的点击事件足够简单:
.onClick(():void=>{this.filter=filter;});搜索框同样只写入关键词:
TextInput({placeholder:'搜索任务或分类',text:this.keyword}).onChange((value:string):void=>{this.keyword=value;});这两个事件不调用列表渲染方法、不修改任务数组、不手动清空别的条件。保留搜索词再切换筛选,是一个有意识的选择:用户可能正试图查看“包含文章的已完成任务”,切换页签后仍保留关键词符合连续操作的预期。如果产品希望切换筛选时清空搜索,也可以明确执行this.keyword = '',但那应该是可说明的交互规则,而不是由于状态混乱产生的副作用。
五、同一个派生结果必须被多个区域复用
只让列表使用getVisibleTodos()还不够。顶部摘要和空状态如果改用其他算法,仍可能与列表不一致。当前页面将它们统一在同一来源上:
Text(`当前${this.getVisibleTodos().length}项,尚有${this.getRemainingTodoCount()}项待完成`)if(this.getVisibleTodos().length===0){Column(){Text('没有匹配的任务')Text('试试清空搜索词、切换筛选条件,或新建一条待办。')}}else{List(){ForEach(this.getVisibleTodos(),(item:TodoItem):void=>{ListItem(){this.TodoCard(item);}},(item:TodoItem):string=>`todo-${item.id}`);}}“当前 N 项”和“尚有 N 项待完成”并不是同一种数字。前者来自可见结果,后者来自整个源数组。把两者并排展示能减少误解:用户在“已完成”里搜索时,当前可见数可能是 1,而全局待完成数仍是 2,这并不矛盾。
空状态也必须依赖可见结果,而不是todos.length。只要任务总数大于零却当前条件无匹配项,就应该展示“没有匹配的任务”,而不是让页面空白或错误显示“暂无任务”。
六、筛选与搜索组合时的操作顺序验证
判断实现是否可靠,不能只测试一种顺序。建议至少覆盖以下几组操作。
1. 先筛选,再搜索
进入“进行中”,再输入某个标题的一部分。结果必须同时是未完成且文本命中的任务。若已完成项混进来,检查状态条件是否被||放宽。
2. 先搜索,再筛选
先输入“内容”,再切换“已完成”。关键词应保留,列表在原始任务上重新按完成状态计算。若切换后出现与关键词无关的任务,说明筛选按钮绕开了统一的派生方法。
3. 搜索无结果,再清空
输入一个不存在的词,出现空状态;清空输入后,原任务恢复。这个用例专门检查搜索是否误删或覆盖了todos。
4. 在结果中完成任务
处于“进行中 + 关键词”条件时勾选一项,任务应立即从当前结果消失;切到“已完成”且关键词仍命中时,应能看到该任务。这个用例验证任务写入和派生查询是否真正串联。
5. 新增任务后保留还是重置条件
当前addTodo()明确把筛选切回“全部”,搜索词不自动改动。新增任务是否可见取决于它是否匹配现有关键词。这种行为可以根据产品需要调整,但必须在代码里明确,而不要让它由多个结果数组的偶然顺序决定。
七、为后续筛选条件扩展留好接口
以后要增加“高优先级”“今天到期”“某一分类”,不必再建新的列表状态。可以继续给派生方法增加命名清楚的条件:
constmatchesPriority=this.onlyHighPriority?item.priority==='高':true;constmatchesCategory=this.category==='全部'||item.category===this.category;returnmatchesFilter&&matchesKeyword&&matchesPriority&&matchesCategory;前提是每个条件都有明确归属:onlyHighPriority、category是页面查询状态,priority、item.category是任务事实。条件更多时可以再抽成独立函数,或把查询条件组织成对象;但“只保存输入、统一推导输出”的原则不变。
当前模拟器画面
这张图对应当前模拟器已验证的运行场景;它不替代签名、发布链接或未实现功能的验证。
小结
筛选和搜索不是两套列表功能,而是对同一份任务数据施加的两类条件。当前待办面板用getVisibleTodos()把它们组合为一次查询,并让列表、数量和空状态都复用该结果。
当用户操作顺序变复杂时,这种设计仍然稳定:按钮只改筛选,输入框只改关键词,勾选只改任务事实,界面只读派生结果。先建立这条单向关系,后续再增加更多筛选条件也不会让页面陷入“结果覆盖结果”的循环。