一、列表问题通常来自多个“真相源”纹样目录同时支持关键词、首页分类、纹样类型、文化来源、朝代和色调筛选。若每个控件直接修改一份独立列表常见结果是搜索后切换分类又恢复全部数据、清空筛选却保留旧数量、选中卡片在列表重排后跑到另一项。根因不是 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列表经过过滤后数组下标会改变。若页面保存selectedIndex0筛选条件一变第一项可能从龙纹变成凤纹但 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 循环渲染。