ArkUI Swiper:AI证据轮播失效图剔除与稳定卡片定位【鸿蒙心迹】
案例:ProofCarousel / EvidencePage / 任务
PC-1808。本文的数据、日志、DevEco 界面与手机屏幕均为独立构造的技术示意,不是实际编译、识别或真机测试结果。示例展示的是轮播状态治理,不宣称设备内建 AI 证据能力。
一、证据少了一张,当前页不能跟着换人
内容审核工作台很容易出现这样的小矛盾:服务端返回四份候选证据,前端准备轮播展示,但其中一份图片地址失效。直接把四个条目都塞进 Swiper,第二张可能变成空白;若先按下标选中第三张,再把失效项删掉,原来的第三张又会变成第二张。用户刚刚打开的内容忽然换了,问题既不像网络故障,也不像控件崩溃,定位起来格外麻烦。
我把这个问题单独抽成 ProofCarousel。它并不连接真实 AI 模型,只处理一份受控示例清单。任务号固定为 PC-1808,原始记录为 E-01、E-02、E-03、E-04,只有 E-02 标记为 failed;其余三条标记为 ready。初始选中的是 E-03。筛掉 E-02 后,界面应该显示 E-03 与 2 / 3,而不是显示「第三张」这样脱离数据的身份。工程上要承认一个事实:业务记录 ID 是身份,Swiper 的 index 只是当时容器中的位置。
这里还有另一层边界。所谓 failed 在 Demo 中只是静态业务标记,不等于系统图片组件加载回调已经证明资源不可用。正式接入时,图片解码失败、下载超时和内容审核失败应拆成不同原因,必须由上游给出可复核的错误分类。本文刻意不借一个漂亮的截图宣称完成了链路实测。

封面表现的是「证据变少,但选中对象不漂移」的关系:左侧留给工程问题,右侧卡片强调内容之间存在固定身份。后面的实现都围绕一个判断展开——保留的是 E-03,而不是数字 2。
二、把可展示清单变成唯一数据入口
先不要急着调整 Swiper 属性。当前实现把原始清单与展示清单明确分离:source 保存全部来源记录,visible 保存通过条件的记录,activeId 保存当前业务身份。列表索引只在渲染前计算。这样能把「失败项被过滤」与「控件下标变化」拆开测试,也能避免把一张失败证据当成占位页参与页数统计。
下面这段纯函数只解决清单过滤与索引归位,不知道 ArkUI,也不管理网络或图片资源。状态仅取 ready、failed 两类,是为了保持案例足够集中。调用方传入 E-03 时,会得到过滤后的索引 1;如果身份已消失,回退到索引 0;如果没有可用卡片,返回 -1,交由 UI 显示空态。
interface EvidenceItem {
id: string;
title: string;
status: string;
}
function usableCards(source: EvidenceItem[]): EvidenceItem[] {
const unique: Set<string> = new Set<string>();
return source.filter((item: EvidenceItem) => {
if (item.status !== 'ready' || unique.has(item.id)) return false;
unique.add(item.id);
return true;
});
}
function locateCard(cards: EvidenceItem[], wantedId: string): number {
if (cards.length === 0) return -1;
const at = cards.findIndex((item: EvidenceItem) => item.id === wantedId);
return at >= 0 ? at : 0;
}
这里刻意加了一个 ID 去重门禁。生产环境若出现同一证据被两个批次重复推送,ForEach 使用相同 key 会让渲染身份变得不可靠。去重时保留第一条只是 Demo 的业务约定,涉及真实审核时,还应根据版本号选择可信来源。Set 在函数返回后自然释放,不需要开发者显式 dispose;但如果上游持有大尺寸图片缓冲,过滤掉卡片并不意味着相关底层资源一定已释放。
三、Swiper 负责位置,记录 ID 负责连续性
接下来才是页面层。官方 Swiper 文档明确支持 index、onChange、loop 等能力,并说明越界 index 会采用默认位置。它不会替业务辨别 E-03 在新清单中的位置。因此在 EvidencePage 中,我让 onChange 回写 activeId,展示数量根据 visibleCards 计算,不把 onChange 的序号保存成长期业务标识。
@Entry
@Component
struct EvidencePage {
@State private source: EvidenceItem[] = [
{ id: 'E-01', title: '构图参考', status: 'ready' },
{ id: 'E-02', title: '过期缩略图', status: 'failed' },
{ id: 'E-03', title: '主体细节', status: 'ready' },
{ id: 'E-04', title: '场景补充', status: 'ready' }
];
@State private activeId: string = 'E-03';
@State private pageIndex: number = 1;
private visibleCards(): EvidenceItem[] { return usableCards(this.source); }
build() {
Column({ space: 14 }) {
Text('ProofCarousel · PC-1808').fontSize(22)
Text('AI 证据展示(示例数据)').fontSize(16)
Swiper() {
ForEach(this.visibleCards(), (card: EvidenceItem) => {
Column({ space: 12 }) {
Text(card.id).fontSize(26).fontWeight(FontWeight.Bold)
Text(card.title).fontSize(18)
}.width('100%').height(260).justifyContent(FlexAlign.Center)
}, (card: EvidenceItem) => card.id)
}
.index(this.pageIndex).loop(false).indicator(true).duration(0)
.onChange((index: number) => {
const cards = this.visibleCards();
if (index >= 0 && index < cards.length) {
this.pageIndex = index;
this.activeId = cards[index].id;
}
})
Text(`${this.pageIndex + 1} / ${this.visibleCards().length}`)
}.padding(20)
}
}
这段是缩减后的组件骨架,使用了固定高度的文字卡片,并不发起图片请求。把远端 Image、重试按钮和占位渲染接进去之前,先用受控状态确保核心路径清楚。这里将切换时长设为零,专门回避演示时「边播动画边改子项」的干扰;产品若启用动画,应等切换稳定后再应用清单变更。不要从本段示例推导出 Swiper 可以在任意过渡帧安全替换数据源。
ForEach 的 key 使用 E-03 这样的稳定 ID,而不是 index。这样 E-02 被移除后,E-03 虽然从原始清单下标 2 变成展示清单下标 1,组件身份仍能对应业务对象。但这也不是绝对的滚动恢复保证:页切换与状态更新仍由调用顺序管理,不能只靠稳定 key 解掉所有竞态。

DevEco 白色界面图示展示 EvidencePage.ets 的关键状态以及右侧示意模拟器的 E-03 / 2 / 3。底部 HiLog 文字为人工编写的示例事件,并非导出的真实日志。
四、清单更新必须成组提交
假设下一批可用证据只剩 E-03、E-04,仍以 E-03 为中心。危险做法是先给 source 赋值,然后依赖旧 pageIndex 等待 Swiper 自己恢复,用户可能看到瞬时的错误计数。更稳妥的方式是先计算 nextVisible 与 nextIndex,再一次性更新相关业务状态;对于没有证据的空态,pageIndex 保持零只是内部安全默认值,屏幕不应假装存在第 1 张。
// 放在 EvidencePage 组件内;仅在轮播切换结束后调用。
private applyEvidenceBatch(next: EvidenceItem[]): void {
const nextVisible: EvidenceItem[] = usableCards(next);
const nextIndex: number = locateCard(nextVisible, this.activeId);
this.source = next.slice();
this.pageIndex = nextIndex < 0 ? 0 : nextIndex;
this.activeId = nextIndex < 0 ? '' : nextVisible[nextIndex].id;
}
这段代码没有伪造任何后台接口,next 由上层数据适配器提供。它说明的是同一份快照先算完再赋值,不是多次赋值的真正数据库事务。大批量加载时,还要建立请求批次号,只有最新批次可以调用 applyEvidenceBatch;否则一个较慢的旧请求仍可能把用户正在查看的证据重新带回来。页面离开时应取消图片下载订阅和异步回调,避免在已销毁页面上更新状态。
还有一个不太显眼的取舍:如果用户当前看的 E-03 恰好变成 failed,我们选第一张剩余可用证据,而不是强行把下标钳制到末尾。原因在于业务身份已经不在了,任意所谓「最近位置」都会掩盖身份变化。正式产品需要明确提示「当前证据不可展示,已切换到其他证据」,尤其不能让审核人员误认为原图内容被悄悄替换。
五、只对准一个截图验收,不扩大结论
手机示意图固定采用三个 ready 条目、一个 failed 条目;顶部显示项目 ProofCarousel、任务 PC-1808,中间卡片 E-03 / 主体细节,下方页数为 2 / 3,脚注说明 E-02 已排除。它表达的是一个预期状态,不是截图式测试报告。我们应把屏幕文字和文档中的示例值核对一致,而不是靠拟真的模拟器边框冒充完成验证。

如果要把这一页放到真实工程里,我会先写几组自动化状态断言:源数据四条且 E-02 failed 时,usableCards 应返回三条;activeId=E-03 应定位索引 1;删除 E-01 后 activeId 仍是 E-03,但索引应回到 0;全部 failed 时应显示空态;重复 ID 不应产生重复卡片。这些是建议的验收样例,不是已执行的运行结果。
六、发布前需要补齐的地方
首先要决定失败判据。网络请求失败时允许重试,审核规则失败时可能永久禁止展示,两种情况不应共用一个粗糙的 failed。其次要控制图片缓存与页面生命周期,避免已失效卡片继续持有较大的位图。再次要考虑无障碍场景:页码变化应能用清晰的文本表达,不能只依赖轮播圆点。最后是排序规则,如果同一 ID 的多条记录来自不同来源,应先定义版本选择顺序,再执行过滤。
工程上更值得保留的是一套小小的约束:当前对象始终以 ID 识别,当前位置始终在可展示快照内计算,清单更新在过渡稳定后进行,截图和日志只能证明示意数据一致,不能证明真实加载、审核或性能结果。到这里,Demo 的边界就算写清了,下一步才值得接上真正的图片资源和可回溯的错误上报。
七、参考与边界
- 华为官方 Swiper API(index、onChange、loop、duration 等):https://developer.huawei.com/consumer/en/doc/harmonyos-references-V13/ts-container-swiper-V13
- 华为 HarmonyOS 文档中心:https://developer.huawei.com/consumer/cn/doc/
本文代码按 ArkTS 组件示例组织,未在目标 SDK 下进行编译或真机调试;具体属性签名和 API 版本须以目标项目所装 SDK 为准。涉及 AI 证据的名称均为业务示例,没有调用真实 AI 开放能力。
更多推荐





所有评论(0)