在这里插入图片描述

前言:为什么“先点筛选再搜索”最容易暴露逻辑问题

待办列表只有筛选时,很多人会写一个“进行中数组”;只有搜索时,又写一个“搜索结果数组”。两个功能单独演示都能跑,一旦用户先点“进行中”再输入关键词,或先搜索再切到“已完成”,结果就开始不稳定:搜索结果覆盖了筛选结果,清空搜索词后列表无法恢复,顶部数量和实际卡片数对不上。

这类问题的根源不是条件判断写得不够多,而是把“结果”当成了需要保存的数据。筛选和搜索本质上都是查询条件:任务源数据不变,用户只是改变了观察角度。正确做法是保存条件,把结果从同一个方法中重新推导。

本文以当前鸿蒙 ArkUI 待办面板为例,完整拆解 getVisibleTodos() 的写法。它把完成状态筛选和关键词搜索组合为一次过滤,并让列表、摘要和空状态共同依赖这一结果。这样无论用户怎样切换操作顺序,页面都有唯一答案。

一、先确定三个输入和一个输出

组合筛选前,先明确输入。当前页面的任务源数据是 todos,用户可改变的条件是 filterkeyword

type TodoFilter = '全部' | '进行中' | '已完成';

@State private todos: TodoItem[] = [];
@State private filter: TodoFilter = '全部';
@State private keyword: string = '';

输出只有一个:当前应该渲染到列表中的任务数组。它不是新的 @State,而是每次根据上述输入计算出来的结果。

todos + filter + keyword
          ↓
   getVisibleTodos()
          ↓
   列表 / 当前数量 / 空状态

这个结构看似普通,却避免了大量分支。筛选按钮不再负责“把列表换成什么”,它只负责更新 filter;搜索框不再负责“保存搜索结果”,它只负责更新 keyword。真正决定列表内容的规则集中在同一个位置。

二、把筛选条件写成可读的布尔变量

当前工程中的核心方法如下:

private getVisibleTodos(): TodoItem[] {
  const query = this.keyword.trim().toLowerCase();
  return this.todos.filter((item: TodoItem): boolean => {
    const matchesFilter = this.filter === '全部' ||
      (this.filter === '进行中' && !item.done) ||
      (this.filter === '已完成' && item.done);
    const matchesKeyword = query.length === 0 ||
      item.title.toLowerCase().includes(query) ||
      item.category.toLowerCase().includes(query);
    return matchesFilter && matchesKeyword;
  });
}

不要被它放在一个方法里迷惑,内部其实分成四步。

第一步是规范化关键词:trim() 去掉用户输入前后的空白,toLowerCase() 让英文大小写比较更稳定。当前示例任务是中文为主,大小写处理对中文没有副作用;如果以后允许英文分类和标题,它能避免 Tasktask 被当成两个词。

第二步是计算 matchesFilter。全部不限制完成状态;进行中要求 !item.done;已完成要求 item.done。把逻辑拆成有名字的变量,比在 return 后面写一长串条件更容易阅读,也方便以后增加“高优先级”“今天到期”等新筛选。

第三步是计算 matchesKeyword。关键词为空时,所有任务都匹配;不为空时,标题或分类只要有一个包含查询词就视为命中。这里选择“标题 OR 分类”,是因为用户在待办面板中往往记得任务名称,也可能只记得它属于“内容”或“验证”。

第四步才是组合:matchesFilter && matchesKeyword。这意味着一项任务必须同时满足状态和搜索要求。使用 && 是本篇最关键的语义;如果错写成 ||,一个已完成但标题命中的任务会出现在“进行中”页签中,筛选规则就被破坏了。

三、为什么结果不能再保存一次

下面是常见但不推荐的思路:

@State private filteredTodos: TodoItem[] = [];
@State private searchedTodos: TodoItem[] = [];

这样设计后,用户每次输入字符都要决定搜索哪一份数组;切换筛选时又要决定是否在搜索结果上再筛;勾选任务后还要更新至少两份结果。结果数组越来越多,真实数据源反而变得不清楚。

派生方法没有这个问题。getVisibleTodos() 永远从 todos 出发,条件改变后直接重算。它不是缓存,因此不存在“缓存忘记更新”的风险。对于当前的少量任务,这种遍历成本很小,换来的是可靠的行为和更短的状态链。

如果未来列表非常长,优化顺序也不应颠倒。先通过实际分析确认计算真的成为瓶颈,再考虑节流输入、分页、索引或缓存策略;即使加入优化,也要确保缓存的失效条件覆盖 todosfilterkeyword 三个输入。没有测量就提前维护两份列表,通常只会把复杂度从性能问题变成正确性问题。

四、让按钮和输入框只修改自己的条件

筛选项的点击事件足够简单:

.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() 明确把筛选切回“全部”,搜索词不自动改动。新增任务是否可见取决于它是否匹配现有关键词。这种行为可以根据产品需要调整,但必须在代码里明确,而不要让它由多个结果数组的偶然顺序决定。

七、为后续筛选条件扩展留好接口

以后要增加“高优先级”“今天到期”“某一分类”,不必再建新的列表状态。可以继续给派生方法增加命名清楚的条件:

const matchesPriority = this.onlyHighPriority ? item.priority === '高' : true;
const matchesCategory = this.category === '全部' || item.category === this.category;
return matchesFilter && matchesKeyword && matchesPriority && matchesCategory;

前提是每个条件都有明确归属:onlyHighPrioritycategory 是页面查询状态,priorityitem.category 是任务事实。条件更多时可以再抽成独立函数,或把查询条件组织成对象;但“只保存输入、统一推导输出”的原则不变。

当前模拟器画面

在这里插入图片描述

这张图对应当前模拟器已验证的运行场景;它不替代签名、发布链接或未实现功能的验证。

小结

筛选和搜索不是两套列表功能,而是对同一份任务数据施加的两类条件。当前待办面板用 getVisibleTodos() 把它们组合为一次查询,并让列表、数量和空状态都复用该结果。

当用户操作顺序变复杂时,这种设计仍然稳定:按钮只改筛选,输入框只改关键词,勾选只改任务事实,界面只读派生结果。先建立这条单向关系,后续再增加更多筛选条件也不会让页面陷入“结果覆盖结果”的循环。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐