ArkUI ListItem:平行视界工单延迟删除撤销窗口与跨栏恢复【鸿蒙心迹】
双栏工单页里,用户往往左边翻列表、右边看详情。一次左滑“删除”,如果立即从数据库移走记录,右栏会在下一帧找不到对象;如果只是把左栏卡片藏起来,右栏可能继续显示已经准备删除的内容。等用户后悔再按“撤销”,列表和详情又会各自恢复到不同位置。看起来是一个小按钮,背后却牵涉三件事:业务记录何时真正删除、跨栏选择属于谁,以及撤销资格在哪个时间点失效。
这次使用UndoDesk演示一个本地待删除队列,不直接调用远端工单接口。任务UD-909,数据集9条工单,用户选中TKT-004后在17:46:21触发待删除,拥有5秒撤销期;17:46:24选择撤销,最终状态为RESTORED,可见9条、待提交删除0条。页面处于宽度720vp的SPLIT业务布局。这是固定脚本推演,不是真机录屏,也不代表EasyGo系统级平行视界自动完成了业务撤销。

一、不要把“划走列表项”直接等于“删掉记录”
ArkUI的ListItem支持swipeAction划出操作组件,正适合放一个“移入待删除”按钮。但是组件负责的是交互入口,并不负责工单的事务模型。真实删除可能涉及本地库、云端服务、其他窗口以及后台同步;因此业务必须先定义单独的待删除状态,不能从ListItem的视觉消失倒推出删除已经成功。
UndoDesk为每条工单保留稳定的id,例如TKT-004,并用ACTIVE、PENDING_DELETE、DELETED三个状态解释生命周期。进入待删除时记录原始位置、右栏选中ID、时间截止点和操作令牌;撤销只改变业务状态,之后根据稳定ID恢复列表选中项。最终提交删除必须再次校验令牌,避免旧定时器把已经恢复的记录删除掉。
这里的5秒是产品设定,不是ListItem内置属性。用户可能在第四秒把应用切到后台,普通计时器未必按时运行;因此截止资格不能只依赖倒计时动画。尤其不应该把“剩余秒数”作为数据库真相。后台恢复时,应根据记录的截止时间和业务版本重新判断,再决定提交或放弃,必要时交给明确支持后台执行的能力。
二、先用稳定ID建立可恢复的工单对象
实现时先把数据更新与UI隔离。下列类型和函数是应用自定义业务代码,ArkUI不会自动提供UndoTicket或UndoLedger。用复制数组触发组件层状态更新,比直接在旧数组上悄悄修改对象更容易审计。对于正式产品,待删除记录需要落到持久化存储,不能仅留在内存。
export interface UndoTicket {
id: string;
title: string;
state: 'ACTIVE' | 'PENDING_DELETE' | 'DELETED';
}
export interface UndoLease {
id: string;
token: number;
deadline: number;
}
export class UndoLedger {
tickets: UndoTicket[] = [];
lease: UndoLease | null = null;
private serial: number = 0;
stage(id: string, now: number): UndoLease | null {
const item = this.tickets.find((v: UndoTicket) => v.id === id);
if (!item || item.state !== 'ACTIVE' || this.lease) return null;
this.tickets = this.tickets.map((v: UndoTicket) =>
v.id === id ? { ...v, state: 'PENDING_DELETE' } : v);
this.lease = { id, token: ++this.serial, deadline: now + 5000 };
return this.lease;
}
}
只允许一条正在撤销期内的记录,是这个Demo为了方便核对做出的限制,不是系统限制。批量删除产品可以把lease改为按ID索引的Map,并为每个操作维护独立截止点。真正重要的是令牌单调递增:即使用户在同一ID上删、撤销、再删两次,第一次的延迟回调也不能影响第二次操作。
有些实现会在stage()里立即过滤掉记录,之后用数组下标记住删除位置。列表切换排序规则、展开折叠栏或异步刷新后,原下标可能指向别的工单。UndoDesk坚持用TKT-004作为身份,原索引只作为恢复滚动位置的提示,不作为选择和删除的唯一依据。右栏详情也只绑定selectedId,不持有可能过期的数组对象引用。
三、用定时器竞争检查决定最后一步
用户按下撤销按钮和5秒截止回调有可能排在同一个事件循环附近。只比较时间不足以保证正确,因为clearTimeout无法取消已经进入队列的回调。我们需要在回调内部再次检查令牌和状态;撤销同样要验证是否仍处于待删除,不能从DELETED逆向恢复成无审计记录的ACTIVE。
export class UndoCoordinator {
private timerId: number = -1;
constructor(private store: UndoLedger) {}
schedule(lease: UndoLease): void {
this.timerId = setTimeout(() => {
const current = this.store.lease;
if (!current || current.token !== lease.token) return;
if (Date.now() < current.deadline) return;
this.store.tickets = this.store.tickets.map((v: UndoTicket) =>
v.id === lease.id ? { ...v, state: 'DELETED' } : v);
this.store.lease = null;
this.timerId = -1;
}, Math.max(0, lease.deadline - Date.now()));
}
undo(id: string): boolean {
const lease = this.store.lease;
if (!lease || lease.id !== id || Date.now() >= lease.deadline) return false;
if (this.timerId >= 0) clearTimeout(this.timerId);
this.timerId = -1;
this.store.tickets = this.store.tickets.map((v: UndoTicket) =>
v.id === id ? { ...v, state: 'ACTIVE' } : v);
this.store.lease = null;
return true;
}
}
这里的DELETED还只是内存业务状态,正式删除动作应在提交分支统一处理事务与后端返回,成功后再从持久化仓库去除;失败则记录可恢复错误,不能先在界面上声称永久删除。定时器在页面销毁时必须取消、转交给更可靠的业务层或进行持久化补偿,具体选哪一种取决于产品是否承诺后台也要完成删除。本Demo选择离开页面时放弃提交、恢复ACTIVE,避免没有持续执行保证却声称按时执行。

四、把划出操作接到单一业务入口
ListItem是展示节点,真正拥有数据的是上面的UndoLedger。页面可以采用稳定ID生成ForEach的键,避免不同排序下复用到错误节点。右栏也无需直接向左栏发消息,只要两边依据相同的状态快照重建即可。下例省略了布局断点监听等外围代码,重点展示从划出按钮进入受控业务方法的连接方式。
@Entry
@Component
struct DeskPage {
@State visible: UndoTicket[] = [];
@State selectedId: string = 'TKT-004';
@Builder
deleteAction(id: string) {
Button('移入待删除').onClick(() => this.stageDelete(id));
}
private stageDelete(id: string): void {
// 对接当前页面持有的UndoLedger与UndoCoordinator
console.info(`[UndoDesk] UD-909 stage=${id}`);
}
build() {
List() {
ForEach(this.visible, (item: UndoTicket) => {
ListItem() { Text(item.title) }
.swipeAction({ end: this.deleteAction(item.id) });
}, (item: UndoTicket) => item.id)
}
}
}
正式项目会把stageDelete接入存储与UI刷新,并在划出按钮之外提供可访问的删除入口;手势不是唯一操作方式。对于PC、平板、折叠屏,swipeAction可能受输入设备与可用宽度影响,鼠标和键盘用户需要可见按钮或菜单。右栏显示的是“待删除,可撤销”而不是突然退出详情,能够减少用户以为操作没有成功而反复滑动的情况。
在宽度720vp的SPLIT页面,左栏显示9条工单,右栏保留TKT-004摘要。按下待删除时左栏可以将它移入分组底部,并在右栏显示撤销条;撤销完成再把卡片按当前排序规则归位。此过程不需要强行恢复旧数组下标。平行视界只是界面形态背景,不表示本地状态机已经完成跨设备分布式一致性。
五、把撤销证据从动画里独立出来
本轮三段演示日志固定为:17:46:21 [UndoDesk] UD-909 ticket=TKT-004 stage=PENDING_DELETE;17:46:24 [UndoDesk] UD-909 undo=TKT-004 accepted=1;17:46:24 [UndoDesk] UD-909 state=RESTORED visible=9。演示界面的撤销窗口为5秒,实际在第3秒撤销,最后没有进入永久删除,也没有任何网络删除请求。

真实验收至少要补齐几个反例:用户连续左滑同一条工单、到期瞬间点撤销、页面切换时未处理的定时器、后台恢复后跨过截止点、列表数据刷新以及右栏换成另一条工单时的状态隔离。应检查提交是否恰好一次、撤销是否具有唯一令牌、状态改变后UI是否重新计算,而不是只录一段按钮变绿的视频。
针对SPLIT形态,还需要把宽度切换当成一个验收事件。假设用户在720vp时发起删除,撤销期内把窗口缩到紧凑布局,右栏可能合并进详情路由,但TKT-004的删除令牌、截止时刻和选中ID都不应随组件销毁消失。若项目暂时没有持久化的跨页面协调器,最安全的简化方案就是明确取消待删除动作并恢复ACTIVE,同时记录一次CANCEL_ON_LEAVE。一旦承诺后台仍会删除,就必须升级为独立业务仓库,而不只是增加一个setTimeout。这一点在多设备平行视界中尤其重要:窗口形态切换与业务提交不是同一事件,二者不能通过猜测发生顺序来绑定。
日志也要有克制。删除对象ID可以作为排查主键,但不应在诊断日志写入客户姓名、订单地址或工单全文;撤销资格只需要令牌、状态和时间戳。如果演示页记录了RESTORED,验收就必须确保右栏显示的详情确实是恢复后的TKT-004,而不是恰好同名的缓存副本。
设计上的取舍是明确的:短撤销窗让用户有回退机会,但也增加了一层待提交状态;若后端已永久删除,则必须提供服务端撤销协议,不可能靠本地@State还原已删除数据。对于客服工单、审核清单等有责任归属的业务,我宁愿让用户看见一次短暂的“待删除”,也不愿出现左栏消失、右栏仍可操作、后台又报删除失败的三种相互矛盾状态。
参考:华为ArkUI《ListItem》https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/api/ts-container-listitem。示例中的撤销时钟、业务状态和工单仓库都是自定义设计,不属于官方swipeAction直接提供的能力。
更多推荐




所有评论(0)