纹渊 HarmonyOS 7 工程实战(16):纹样卡片列表:搜索、筛选与选中态刷新
2026/7/23 0:37:36 网站建设 项目流程

一、列表问题通常来自多个“真相源”

纹样目录同时支持关键词、首页分类、纹样类型、文化来源、朝代和色调筛选。若每个控件直接修改一份独立列表,常见结果是搜索后切换分类又恢复全部数据、清空筛选却保留旧数量、选中卡片在列表重排后跑到另一项。根因不是 ArkUI 刷新慢,而是页面维护了多个互相覆盖的数据真相。

更稳定的方式是只保存筛选条件和稳定的selectedPatternId,每次渲染由同一个纯函数从原始目录计算结果。选中态根据 ID 派生,绝不依赖数组下标。

状态是否持久保存计算来源更新触发
原始纹样目录是,只读领域数据应用版本/内容更新
搜索词是,页面状态用户输入文本变化
四组筛选条件是,页面状态筛选面板确认或重置
可见列表否,派生目录 + 全部条件任一条件变化
选中态只保存 ID可见项 ID 比较点击卡片/恢复状态

二、先建立一条固定顺序的过滤管线

所有条件放进一个filter谓词,顺序保持稳定。空搜索和“全部/不限”返回true,让每个匹配函数只负责一个维度。这样新增条件时不会修改已有条件的内部逻辑。

private filteredPatterns(): Pattern[] { const keyword = this.searchText.trim() return PATTERNS.filter((pattern: Pattern) => { return this.matchesHomeCategory(pattern) && this.matchesKeyword(pattern, keyword) && this.matchesType(pattern) && this.matchesCulture(pattern) && this.matchesDynasty(pattern) && this.matchesTone(pattern) }) } private matchesHomeCategory(pattern: Pattern): boolean { if (this.categoryFilter === '全部') return true return this.patternHomeCategories(pattern) .includes(this.categoryFilter) } private matchesType(pattern: Pattern): boolean { return this.filterType === '全部' || pattern.type === this.filterType }

筛选函数不修改任何@State,也不向数组写入临时标记。因此同一组输入总能得到同一结果,便于单元测试和定位问题。

三、关键词要覆盖读者真正会搜索的字段

只搜索名称会漏掉“清代”“皇家御用”“团扇”等自然查询。关键词匹配应覆盖名称、朝代、类别、标签、描述、特征和用途,并先统一大小写与空白。中文不需要分词也能完成基础子串匹配;若目录规模扩大,再把索引放到专用服务中。

function normalizeKeyword(value: string): string { return value.trim().toLocaleLowerCase().replace(/\s+/g, ' ') } function matchesKeyword(pattern: Pattern, raw: string): boolean { const keyword = normalizeKeyword(raw) if (keyword.length === 0) return true const searchable = [ pattern.name, pattern.dynasty, pattern.type, pattern.tag, pattern.desc, pattern.features, pattern.usage.join(' ') ].join(' ').toLocaleLowerCase() return searchable.includes(keyword) }

输入法组合期间可能连续触发变化。几十条本地数据可以直接计算;数百或上千条数据则应增加 150–300 ms 防抖,并保证旧计算结果不能覆盖新关键词。

四、选中态必须绑定稳定 ID

列表经过过滤后,数组下标会改变。若页面保存selectedIndex=0,筛选条件一变,第一项可能从龙纹变成凤纹,但 UI 仍把它视为原选择。保存纹样 ID 后,卡片只比较pattern.id === selectedPatternId;当前结果不包含该 ID 时,选择仍可保留,但确认按钮应明确提示该项暂不可见或要求重新选择。

@State selectedPatternId: number = 0 @Builder PatternCard(pattern: Pattern) { const selected = pattern.id === this.selectedPatternId Column() { Image(getPatternImage(pattern.id)) Text(pattern.name) Text(`${pattern.dynasty} · ${pattern.type}`) if (selected) { Button('确认纹样') .onClick(() => this.confirmPattern(pattern.id)) } } .border({ width: selected ? 2 : 1, color: selected ? '#8A3B1F' : '#DED6C9' }) .onClick(() => { this.selectedPatternId = pattern.id this.syncContinuationSnapshot() }) }

点击选中与点击确认是两个动作。选中只更新视觉状态和快照,确认才推进创作步骤,避免用户只是浏览卡片就被自动带到下一页。

五、LazyForEach 数据源要主动发出重载通知

目录较大时可使用LazyForEach。数据源持有当前派生数组,筛选条件变化后调用setData(),再通知注册的监听器。不要直接替换内部数组却不通知,否则日志中的数据已变化,界面仍显示旧卡片。

export class PatternDataSource implements IDataSource { private listeners: DataChangeListener[] = [] private data: Pattern[] = [] totalCount(): number { return this.data.length } getData(index: number): Pattern { return this.data[index] } registerDataChangeListener(listener: DataChangeListener): void { if (!this.listeners.includes(listener)) { this.listeners.push(listener) } } unregisterDataChangeListener(listener: DataChangeListener): void { const index = this.listeners.indexOf(listener) if (index >= 0) this.listeners.splice(index, 1) } setData(next: Pattern[]): void { this.data = next this.listeners.forEach((listener) => listener.onDataReloaded()) } }

若只新增、删除或移动少量项目,可以发送更细粒度通知;筛选往往会同时改变大量位置,整表重载更简单可靠。性能瓶颈出现后再引入差异计算,而不是提前把状态流复杂化。

六、筛选数量和首页分组都应由条件派生

筛选按钮上的数量不能单独维护计数,否则某一条件重置后容易少减一次。根据当前条件生成活动标签数组,数量就是数组长度。首页分组只在搜索为空、活动筛选为零且首页分类为“全部”时出现;任何条件生效后统一展示结果列表。

private activeFilterLabels(): string[] { const labels: string[] = [] if (this.filterType !== '全部') labels.push(this.filterType) if (this.filterCulture !== '全部') labels.push(this.filterCulture) if (this.filterDynasty !== '不限') labels.push(this.filterDynasty) if (this.filterTone !== '不限') labels.push(this.filterTone) return labels } private filterActionLabel(): string { const count = this.activeFilterLabels().length return count > 0 ? `筛选(${count})` : '筛选' } private shouldShowGroupedSections(): boolean { return this.searchText.trim().length === 0 && this.activeFilterLabels().length === 0 && this.categoryFilter === '全部' } private resetFilters(): void { this.filterType = '全部' this.filterCulture = '全部' this.filterDynasty = '不限' this.filterTone = '不限' }

重置筛选不必清空搜索词,也不必清空首页分类,除非产品把“重置”定义为恢复整个目录。界面文案需要说明作用范围,状态函数也应严格遵守这个范围。

七、空态、恢复与异常矩阵

场景可见列表选中 ID页面反馈
搜索为空、无筛选首页分组保留展示推荐分类
搜索“清代”命中的多字段结果保留显示结果数量
条件无交集空数组保留但不可确认展示清除条件入口
选中项被过滤不显示该卡片仍保留稳定 ID提示当前选择不在结果中
清除筛选重新计算列表原卡片恢复选中不自动推进步骤
目录更新移除 ID列表正常置为未选择要求重新选择
快速输入关键词只呈现最后一次输入保留旧计算不回写
深浅色切换数据不变保留只刷新样式

空态需要告诉用户当前条件,而不是只显示“没有数据”。可列出活动筛选标签,并提供“清除筛选”或“清除搜索”按钮;用户才能判断是目录为空还是条件过窄。

八、验证步骤

1. 分别搜索纹样名称、朝代、类别、特征和用途,确认都能命中对应卡片。 2. 组合首页分类、类型、文化、朝代和色调,确认结果取交集而不是后一个条件覆盖前一个。 3. 选中龙纹后缩小筛选范围,再清除条件,确认龙纹恢复选中且未变成当前第一项。 4. 构造无交集条件,确认出现带活动条件的空态和可操作清除入口。 5. 连续输入并快速删除关键词,确认最终列表只对应最后一次文本。 6. 重置四组高级筛选,确认搜索词和首页分类按产品定义保留,按钮数量回到零。 7. 模拟目录移除已选纹样,确认稳定 ID 被判定失效,确认按钮不可继续。 8. 切换深浅色和窗口宽度,确认只改变样式与列数,不改变筛选结果和选中 ID。

九、总结

搜索、筛选与选中态能够稳定共存,关键是减少状态:原始目录保持只读,页面只保存条件和稳定 ID,可见列表、筛选数量与分组模式都由纯函数派生;LazyForEach 数据源在数组变化后显式通知刷新。列表顺序无论如何变化,选中语义都绑定到纹样本身,而不是当前位置。

长列表按需渲染可参考LazyForEach 循环渲染。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询