ScopePick 的条目、勾选状态、界面与日志均为可复核的虚拟样本。配图不是 DevEco 或真机测试截图;示例不代表真实上架审核或实际平台审批结果。

一、先决定“全选”到底在选什么

素材上架工具里,一个“全选”按钮经常被认为很简单:给所有复选框设成 true 就行。问题是用户通常先做了筛选。假设当前只展示“待校验”的五张素材,而页面背后还有三张已归档记录,其中两张之前就被选中过。用户点下“当前筛选全选”,合理预期是把眼前的五张选中,而不是悄悄覆盖那些看不见的记录。

我把这个情境做成 ScopePick Demo,页面是 AssetReviewPage,演示任务编号 SP-1910。数据总共八项:AS-01 至 AS-08。当前可见项为 AS-02、AS-03、AS-05、AS-07、AS-08,其中 AS-03 原本已经选中;隐藏项是 AS-01、AS-04、AS-06,其中 AS-01 与 AS-06 已选。这样初始全局选中数为三,当前可见选中数为一。

按钮被点击后,当前五项全部选中,隐藏的两项继续保留,所以全局已选是七,而不是五,也不是八。AS-04 仍然保持未选。把这组具体 ID 写清楚很有必要,因为只看“全选成功”四个字,无法证明程序没有误操作隐藏数据。接下来所有 UI 示意和 HiLog 演示都围绕这份固定快照展开。

封面以两层清单和一个可见窗口表达筛选作用域,仅为机制示意。这个案例讨论的是客户端选择集语义,不涉及审核通过率,也不会替任何真实平台做自动提交。我的建议是把“全选”按钮改成“当前筛选全选”,让动作范围成为用户可见的产品契约,而不是把危险操作藏进默认行为。

二、让原始数据、可见列表、选择集各自独立

第一层是原始八条素材;第二层是当前筛选后得出的可见五条;第三层是跨筛选条件保存的已选 ID 集合。三层可以互相推导,但不能互相替代。特别要避免把 visibleItems 当作选择数据源,一旦切换过滤条件,隐藏项将从屏幕消失,错误实现往往顺手把对应选择状态也清空。

这里先用一个固定数组把演示边界钉住。status 只是本地业务标签,不是 HarmonyOS 提供的审核状态常量;同理,AS-* ID 也是为了便于验收而编造的示例主键。上线系统应使用数据库或服务端的稳定主键,不能依赖列表位置,更不能把 ForEach 当前索引当成唯一身份。

interface ReviewAsset {
  id: string;
  label: string;
  status: string;
}

const ASSETS: ReviewAsset[] = [
  { id: 'AS-01', label: '启动封面', status: '已归档' },
  { id: 'AS-02', label: '商店主图', status: '待校验' },
  { id: 'AS-03', label: '预览截图A', status: '待校验' },
  { id: 'AS-04', label: '旧版图标', status: '已归档' },
  { id: 'AS-05', label: '预览截图B', status: '待校验' },
  { id: 'AS-06', label: '授权声明', status: '已归档' },
  { id: 'AS-07', label: '功能介绍图', status: '待校验' },
  { id: 'AS-08', label: '适配说明图', status: '待校验' }
];

有了这种数据区分,可以清楚回答三个经常混淆的问题:“列表里有几项”是五,“当前可见已选几项”起初是一,“全局共有几项被选中”起初是三。这三个数字不是冗余,而是帮助用户判断有没有隐藏选择项。若产品没有把隐藏选择数展示出来,用户可能以为点了“取消全选”就清除了全部勾选,结果后台提交却仍附带之前藏起来的两项。

三、不要把 CheckboxGroup 当成筛选作用域的保证

CheckboxGroup 适合定义一个明确的组件分组,官方文档也说明相同组名会联系到组内复选框。但业务上的“当前筛选结果”不一定等于组件树中的组成员:分页、条件过滤、虚拟化渲染都可能改变屏幕上实际存在的节点。如果希望隐藏项继续保持选中,最稳妥的方式是把选择逻辑放在 ID 集合上,而不是让组件实例数量决定业务意义。

这段代码把筛选与勾选写在 AssetReviewPage 中。@State selectedIds 每次整体赋新数组,确保 UI 能观察到变更;setVisibleAll 只修改当前筛选所得的 ID,不碰其他状态。逻辑选择集合使用数组是为了示例易读,数据量大时可在计算中使用临时集合,但最终仍需要映射到框架可观察的状态模型。

@Entry
@Component
struct AssetReviewPage {
  @State filter: string = '待校验';
  @State selectedIds: string[] = ['AS-01', 'AS-03', 'AS-06'];

  visibleItems(): ReviewAsset[] {
    return ASSETS.filter((item: ReviewAsset) => item.status === this.filter);
  }
  checked(id: string): boolean { return this.selectedIds.includes(id); }

  changeOne(id: string, yes: boolean): void {
    const exists: boolean = ASSETS.some((item: ReviewAsset) => item.id === id);
    if (!exists) return;
    let next: string[] = this.selectedIds.filter((key: string) => key !== id);
    if (yes) next.push(id);
    this.selectedIds = next;
  }

  setVisibleAll(yes: boolean): void {
    const visibleIds: string[] = this.visibleItems().map((item: ReviewAsset) => item.id);
    let next: string[] = this.selectedIds.filter((id: string) => !visibleIds.includes(id));
    if (yes) next = next.concat(visibleIds);
    this.selectedIds = next;
  }

  visibleAllChecked(): boolean {
    const now: ReviewAsset[] = this.visibleItems();
    return now.length > 0 && now.every((item: ReviewAsset) => this.checked(item.id));
  }
  hiddenSelectedCount(): number {
    const visibleIds: string[] = this.visibleItems().map((item: ReviewAsset) => item.id);
    return this.selectedIds.filter((id: string) => !visibleIds.includes(id)).length;
  }
  // 下一节的 build() 方法放在此组件中。
}

需要注意,setVisibleAll(false) 的含义是取消当前筛选范围的选择,而不是清空全局。对于这个演示数据,从点击后全局七项再点“取消当前筛选全选”,会回到只保留隐藏 AS-01、AS-06 的两项,AS-03 原本的勾选也会被取消;这是按钮的明确业务约定,不是恢复操作历史。如果产品需要撤销,必须额外保存操作前快照,不能混用这两个概念。

四、UI 的职责是暴露范围与后果

渲染时,我选择每行一个受控 Checkbox,通过 .select(this.checked(id)) 显示业务真值,通过 .onChange((yes) => this.changeOne(id, yes)) 把交互送回选择集合。ForEach 的键使用稳定 ID,而不是筛选结果的顺序号。按钮明确写“当前筛选全选/取消当前筛选全选”,同时展示全局已选以及隐藏已选数。这样即使换成其他筛选条件,也不会因为显示数量变化而误导用户。

下面是上一节 AssetReviewPage 的 build() 成员,需与前两段代码合并到同一 ETS 文件。这里没有调用审核 API,也没有上传任何文件;确认按钮仅展示本地状态,不会对外发送请求。

// 放入 AssetReviewPage 组件体内。
build() {
  Column({ space: 16 }) {
    Text('ScopePick · 上架素材选择').fontSize(22).fontWeight(FontWeight.Bold)
    Text('任务 SP-1910  |  当前筛选:' + this.filter)
    Text('可见 ' + this.visibleItems().length + ' 项  |  全局已选 ' + this.selectedIds.length + ' 项')
    Text('隐藏已选 ' + this.hiddenSelectedCount() + ' 项')
    Button(this.visibleAllChecked() ? '取消当前筛选全选' : '当前筛选全选')
      .onClick(() => this.setVisibleAll(!this.visibleAllChecked()))
    ForEach(this.visibleItems(), (item: ReviewAsset) => {
      Row({ space: 12 }) {
        Checkbox({ name: item.id }).select(this.checked(item.id))
          .onChange((yes: boolean) => this.changeOne(item.id, yes))
        Text(item.id + ' · ' + item.label).fontSize(16)
      }.width('100%').height(54)
    }, (item: ReviewAsset) => item.id)
  }.padding(20).width('100%')
}

DevEco 风格示意图左侧是 ScopePick 项目树,中间展示 setVisibleAll 对可见 ID 做集合运算,右侧预览为按下按钮之后的五行勾选状态:可见 5/5、全局已选 7、隐藏已选 2。底部 HiLog 的字符串是按示例状态人工编写的,并非系统实际打印。将右侧模拟器放在固定位置,是为了检查 IDE 图和纯手机图是否传达同一业务快照。

五、筛选变化、失败提交与生命周期细节

纯手机示意图显示 ScopePick、任务 SP-1910,屏幕上五条 待校验 素材全部勾选,页脚再次提示隐藏已选 2 项。顶部的时间、5G、Wi-Fi、信号和电量都只是视觉规范的一部分,不能被视作实际测量证据。它不显示后台“审核已通过”,因为示例根本没有提交审核。

这里要给一个容易漏掉的逆向用例:若用户切换到“已归档”筛选,原来五条待校验素材立刻变成隐藏;全局已选数仍然是七,只有可见集及隐藏已选统计发生变化。若此时再点“当前筛选全选”,应只针对三条归档素材操作,不应反过来改变待校验的五条。这一点是作用域模型成立的关键,不能靠在按钮上多加一句提示来替代。

真正提交前还需要二次校验。选择集合里的 ID 可能已经过期、素材可能被他人删除、权限可能变化,尤其在分页场景里用户看见的是某一时刻的快照。可以在确认时先做去重,再与服务端允许操作的资源集求交集,并让用户核对“将操作七项,其中隐藏两项”。服务端必须重新授权并执行幂等校验,客户端勾选并不意味着具备审核权限。提交失败时保留选择集并给出逐项失败原因,比静默清空勾选更利于复查。

生命周期方面,页面退出时无需释放 Checkbox 的底层资源:组件会随页面节点管理;但是订阅了外部素材更新流的业务代码必须解除订阅,挂起的提交请求应隔离旧页面回调。本文没有创建定时器、网络会话、数据库游标或系统文件句柄,因此不存在可展示的 destroy() 调用。凭空补一个释放 API 反而会制造错误知识。若后续接入服务端响应,建议使用独立请求标识并在页面重建时重新读取经过授权的选择集。

六、按 ID 验收,比看勾选动画可靠

这份演示可以用四个明确断言验收。初始 selectedIds 为 AS-01, AS-03, AS-06;选择当前筛选全部之后应得到七项,绝不能包含 AS-04;取消当前筛选全部之后只留下 AS-01, AS-06;在“已归档”筛选下操作时,待校验素材的勾选集合应保持原样。这些判断可以在纯业务函数层做单元测试,毋须先上真机看勾选框是否变色。

最值得留下的工程结论是:业务选择集的作用域必须用数据来定义,组件分组只负责渲染和交互;隐藏不等于取消选择,撤销不等于取消全选,客户端勾选更不等于后台有权提交。把这几对概念拆开,才有可能安全地扩展到分页、多窗口和折叠屏多栏列表。

华为官方资料及边界:

本文仅核对基本组件 API 和业务行为设计,没有在目标 SDK 编译整段 ETS、跑自动化、验证真实上架审核。要上线,还需按应用实际 UI 版本确认组件表现、权限和提交协议。

Logo

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

更多推荐