请添加图片描述

案例开源地址:https://gitcode.com/lqjmac/rn_openharmony_todolist

分类的价值

任务可以按很多维度来组织:时间、重要程度、所属领域。分类就是按领域来组织任务。

工作的事情和生活的事情混在一起,会让人感觉混乱。把它们分开,工作时只看工作任务,下班后只看生活任务,心理上会轻松很多。

在我们的 TodoList 应用中,任务有四个分类:工作、生活、学习、其他。分类筛选让用户能快速切换到某个领域的任务视图。


分类列表的定义

首先定义有哪些分类:

const categories = ['工作', '生活', '学习', '其他'];

为什么用数组

用数组而不是硬编码,是因为分类可能会变。如果以后要加一个"健康"分类,只需要在数组里加一项,筛选按钮和添加任务的分类选择器都会自动更新。

分类的选择

四个分类覆盖了大多数人的日常任务:

  • 工作:职场相关的任务
  • 生活:日常琐事、家务、购物等
  • 学习:读书、课程、技能提升
  • 其他:不好归类的任务

"其他"的必要性

“其他"是一个兜底选项。有些任务确实不好归类,强行归类反而增加用户的认知负担。有了"其他”,用户可以快速添加任务而不用纠结分类。


筛选状态的管理

const [categoryFilter, setCategoryFilter] = useState('all');

类型推断

这里没有显式定义类型,TypeScript 会推断 categoryFilterstring 类型。因为分类是中文字符串,不像优先级那样是固定的英文值,用 string 类型更灵活。

默认值

默认是 'all',显示所有分类的任务。和其他筛选一样,用户打开应用时应该能看到全部任务。


筛选按钮组的实现

分类筛选按钮的实现稍有不同:

<View style={styles.filterContainer}>
  <View style={styles.filterGroup}>
    <TouchableOpacity style={[styles.filterBtn, categoryFilter === 'all' && {backgroundColor: theme.accent}]} onPress={() => setCategoryFilter('all')}>
      <Text style={[styles.filterBtnText, {color: categoryFilter === 'all' ? '#fff' : theme.subText}]}>全部</Text>
    </TouchableOpacity>
    {categories.map(c => (
      <TouchableOpacity key={c} style={[styles.filterBtn, categoryFilter === c && {backgroundColor: theme.accent}]} onPress={() => setCategoryFilter(c)}>
        <Text style={[styles.filterBtnText, {color: categoryFilter === c ? '#fff' : theme.subText}]}>{c}</Text>
      </TouchableOpacity>
    ))}
  </View>
</View>

"全部"按钮单独处理

"全部"按钮是单独写的,不在 categories 数组里。这是因为 'all' 是一个特殊值,不是真正的分类。把它和分类混在一起会让数据结构变得不清晰。

分类按钮用 map 生成

四个分类按钮用 map 遍历 categories 数组生成。如果以后分类变了,只需要改数组,按钮会自动更新。

key 的选择

key={c} 用分类名称作为 key。因为分类名称是唯一的,可以作为稳定的标识符。


按钮样式的处理

分类筛选按钮的样式和其他筛选按钮一样:

style={[styles.filterBtn, categoryFilter === c && {backgroundColor: theme.accent}]}

统一的视觉风格

所有筛选按钮都用相同的基础样式 styles.filterBtn,选中时都用主题强调色 theme.accent。这种统一让界面看起来协调。

没有特殊颜色

和优先级筛选不同,分类筛选没有特殊的颜色。工作、生活、学习、其他,这些分类没有天然的颜色对应关系。强行赋予颜色反而会让用户困惑。


筛选逻辑的实现

分类筛选的逻辑很简单:

const filteredTasks = tasks.filter(task => {
  const matchesSearch = task.title.toLowerCase().includes(searchText.toLowerCase());
  const matchesFilter = filter === 'all' || (filter === 'active' && !task.completed) || (filter === 'completed' && task.completed);
  const matchesPriority = priorityFilter === 'all' || task.priority === priorityFilter;
  const matchesCategory = categoryFilter === 'all' || task.category === categoryFilter;
  return matchesSearch && matchesFilter && matchesPriority && matchesCategory;
});

matchesCategory 的逻辑

const matchesCategory = categoryFilter === 'all' || task.category === categoryFilter;

和优先级筛选一样简单:

  • 如果 categoryFilter === 'all',所有任务都匹配
  • 否则,只有分类等于筛选值的任务才匹配

字符串比较

分类是中文字符串,直接用 === 比较。'工作' === '工作' 返回 true'工作' === '生活' 返回 false


四重筛选的组合

分类筛选是第四个筛选条件,和前三个一起工作:

return matchesSearch && matchesFilter && matchesPriority && matchesCategory;

筛选的叠加

四个条件用 && 连接,只有全部满足的任务才会显示。用户可以组合使用:

  • 搜索"报告" + 筛选"工作" = 找到工作分类中包含"报告"的任务
  • 筛选"待办" + 筛选"高优先级" + 筛选"工作" = 找到工作分类中未完成的高优先级任务

筛选的顺序

四个筛选条件的计算顺序不影响结果,因为 && 是可交换的。但从性能角度,把最可能为 false 的条件放前面可以提前短路。不过对于我们的数据量,这种优化没有意义。


分类在任务卡片中的显示

任务卡片上显示分类标签:

<View style={[styles.categoryTag, {backgroundColor: theme.accent + '30'}]}>
  <Text style={[styles.categoryText, {color: theme.accent}]}>{item.category}</Text>
</View>

标签样式

categoryTag: {paddingHorizontal: 8, paddingVertical: 2, borderRadius: 8},
categoryText: {fontSize: 10, fontWeight: '600'},

分类标签是一个小药丸形状,背景是主题强调色的 30% 透明度,文字是主题强调色。

筛选与显示的关联

当用户筛选"工作"分类时,列表里所有任务的分类标签都显示"工作"。这种一致性让用户确认筛选生效了。


添加任务时的分类选择

添加任务弹窗中有分类选择器:

<Text style={[styles.modalLabel, {color: theme.subText}]}>分类</Text>
<View style={styles.categorySelector}>
  {categories.map(c => (
    <TouchableOpacity key={c} style={[styles.categoryOption, {borderColor: theme.accent}, newTaskCategory === c && {backgroundColor: theme.accent}]} onPress={() => setNewTaskCategory(c)}>
      <Text style={{color: newTaskCategory === c ? '#fff' : theme.accent}}>{c}</Text>
    </TouchableOpacity>
  ))}
</View>

复用分类数组

筛选按钮和添加选择器都用同一个 categories 数组。这保证了分类的一致性,不会出现筛选有"工作"但添加没有的情况。

选择器样式

categorySelector: {flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 24},
categoryOption: {paddingHorizontal: 16, paddingVertical: 8, borderRadius: 8, borderWidth: 1},

分类选择器的按钮比筛选按钮大一些,因为弹窗里空间更充裕,大按钮更容易点击。


分类统计

统计页面显示各分类的任务数量:

const categoryStats = categories.map(c => ({name: c, count: tasks.filter(t => t.category === c).length}));

统计数据的计算

遍历 categories 数组,对每个分类计算任务数量。结果是一个对象数组,每个对象包含分类名称和任务数量。

统计的展示

{categoryStats.map((cat, idx) => (
  <View key={idx} style={styles.categoryStatItem}>
    <Text style={[styles.categoryStatName, {color: theme.text}]}>{cat.name}</Text>
    <View style={[styles.categoryStatBar, {backgroundColor: theme.border}]}>
      <View style={[styles.categoryStatFill, {width: `${totalTasks > 0 ? (cat.count / totalTasks) * 100 : 0}%`, backgroundColor: theme.accent}]} />
    </View>
    <Text style={[styles.categoryStatCount, {color: theme.subText}]}>{cat.count}</Text>
  </View>
))}

用进度条的形式展示各分类的任务占比,直观地显示任务在各分类的分布情况。


分类筛选的使用场景

什么时候用户会用分类筛选?

场景切换

上班时筛选"工作",只看工作任务。下班后筛选"生活",处理生活琐事。这种场景切换能帮助用户保持专注。

批量处理

周末想集中学习,筛选"学习"分类,把学习相关的任务一起处理。

回顾分析

想看看最近工作任务是不是太多了,筛选"工作"看看有多少任务。如果太多,可能需要调整工作节奏。


分类的扩展性

如果要支持用户自定义分类怎么办?

数据结构的变化

categories 从常量变成状态:

const [categories, setCategories] = useState(['工作', '生活', '学习', '其他']);

添加分类的功能

提供一个入口让用户添加新分类:

const addCategory = (name: string) => {
  if (!categories.includes(name)) {
    setCategories([...categories, name]);
  }
};

删除分类的考虑

删除分类比较复杂,需要处理已有任务的分类。可以把被删除分类的任务移到"其他",或者禁止删除有任务的分类。


筛选按钮的换行处理

如果分类很多,一行放不下怎么办?

filterGroup: {flexDirection: 'row', gap: 8},

当前的实现

当前的 filterGroup 没有设置 flexWrap,按钮会挤在一行。如果分类太多,按钮会变得很小或者超出屏幕。

支持换行

可以添加 flexWrap: 'wrap' 让按钮自动换行:

filterGroup: {flexDirection: 'row', flexWrap: 'wrap', gap: 8},

这样分类多的时候会自动换到下一行,不会挤在一起。


筛选状态的重置

有时候用户想清除所有筛选,回到初始状态。可以提供一个重置按钮:

const resetFilters = () => {
  setSearchText('');
  setFilter('all');
  setPriorityFilter('all');
  setCategoryFilter('all');
};

重置的时机

可以在筛选区域添加一个"重置"按钮,或者在空状态提示里提供重置链接。当用户筛选后没有结果时,提示"没有匹配的任务,点击重置筛选"会很有帮助。


筛选的持久化

如果想保存用户的筛选偏好:

useEffect(() => {
  AsyncStorage.setItem('categoryFilter', categoryFilter);
}, [categoryFilter]);

useEffect(() => {
  AsyncStorage.getItem('categoryFilter').then(value => {
    if (value) setCategoryFilter(value);
  });
}, []);

是否需要持久化

分类筛选的持久化比状态筛选更有意义。用户可能习惯每天早上先看工作任务,保存这个偏好能节省操作。


小结

分类筛选让用户能按领域组织和查看任务。工作、生活、学习、其他四个分类覆盖了大多数场景。

实现要点:

  • 分类列表用数组定义,方便扩展
  • "全部"按钮单独处理,不混入分类数组
  • 筛选逻辑简单,直接比较字符串
  • 分类在筛选按钮、任务卡片、添加选择器、统计页面保持一致

分类筛选是四个筛选条件中最"软"的一个。状态是客观的(完成或未完成),优先级是主观但有明确标准的(重要程度),而分类完全取决于用户的组织方式。好的分类筛选应该尊重用户的分类习惯,提供灵活的筛选能力。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐