
前言
邮件、通知、收藏夹这类列表,很容易做出“左滑一下就删除”的爽感,也很容易让用户后悔。手指一滑,内容没了,如果页面没有恢复入口,用户会立刻紧张。
我的习惯是:只要删除会影响用户数据,就给一次撤销机会。它不一定要复杂,但必须存在。
HarmonyOS7 里可以用 ListItem.swipeAction() 做侧滑入口。真正要想清楚的是删除之后的状态流转:先从列表移除,再保留最近删除项,给用户一个可见的撤销入口。
为什么这个问题经常被写乱
侧滑删除要给撤销机会 这类内容很容易被写成“代码能跑就算讲完了”,但对初学者来说,这恰恰是最不够的地方。真正让人卡住的,往往不是某个组件名记不住,而是不知道这段代码为什么要这样拆、状态为什么要这样放、以后需求变化时应该从哪里改。
所以这篇文章不只想给你一个能跑的例子,更想把背后的判断过程讲清楚。你只要把这个判断过程吃透,后面自己改页面、补需求、查问题时,心里会稳很多。
删除不是一个动作,是一段流程
侧滑删除看起来只有一下,其实至少包含四个阶段。
| 阶段 | 页面表现 | 状态变化 |
|---|---|---|
| 侧滑 | 露出删除按钮 | 不改数据 |
| 点击删除 | 项目从列表消失 | 写入 lastDeleted |
| 点击撤销 | 项目回到原位置 | 清空 lastDeleted |
| 再删一条 | 新记录覆盖旧记录 | 只保留最近一次 |
我不建议普通列表一次保留很多撤销记录。移动端页面空间有限,“救回最近一次误删”已经能覆盖大多数场景。
状态要保存 item,也要保存位置
只保存被删的数据还不够。撤销时如果只能追加到末尾,用户会觉得列表顺序乱了。更好的做法是同时保存原始 index。
| 字段 | 作用 |

|—|—|
| mails | 当前列表数据 |
| lastDeleted.item | 最近被删除的邮件 |
| lastDeleted.index | 删除前所在位置 |
| notice | 顶部提示文案 |
删除时用 filter 生成新数组,撤销时用 splice 插回原位置。代码不复杂,但状态语义很清楚。
ArkUI/ArkTS 完整示例
interface MailItem42 {
id: number
sender: string
subject: string
time: string
}
interface DeletedMail42 {
item: MailItem42
index: number
}
@Entry
@Component
struct SwipeDeleteMailPage {
@State mails: MailItem42[] = [
{ id: 101, sender: '系统通知', subject: '你的设备登录提醒', time: '09:30' },
{ id: 102, sender: '账单助手', subject: '本月云空间扣费成功', time: '昨天' },
{ id: 103, sender: '项目成员', subject: '评审会议改到下午三点', time: '周一' }
]
@State lastDeleted: DeletedMail42 | null = null
@State notice: string = '左滑邮件可以删除'
private deleteMail(item: MailItem42): void {
const index: number = this.mails.findIndex((mail: MailItem42) => mail.id === item.id)
if (index < 0) {
return
}
this.lastDeleted = { item: item, index: index }
this.mails = this.mails.filter((mail: MailItem42) => mail.id !== item.id)
this.notice = `已删除:${item.subject}`
}
private undoDelete(): void {
const deleted: DeletedMail42 | null = this.lastDeleted
if (deleted === null) {
return
}
const next: MailItem42[] = this.mails.slice()
const insertIndex: number = Math.min(deleted.index, next.length)
next.splice(insertIndex, 0, deleted.item)
this.mails = next
this.lastDeleted = null
this.notice = `已恢复:${deleted.item.subject}`
}
@Builder
DeleteButton(item: MailItem42) {
Button('删除')
.fontColor(Color.White)
.backgroundColor('#E5484D')
.height('100%')
.onClick(() => {
this.deleteMail(item)
})
}
build() {
Column({ space: 12 }) {
Text('收件箱')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.width('100%')
Text(this.notice)
.fontSize(13)
.fontColor('#666666')
.width('100%')
List({ space: 8 }) {
ForEach(this.mails, (item: MailItem42) => {
ListItem() {
Column({ space: 6 }) {
Row() {
Text(item.sender).fontSize(16).fontWeight(FontWeight.Medium)
Blank()
Text(item.time).fontSize(12).fontColor('#888888')
}
Text(item.subject).fontSize(14).fontColor('#555555')
}
.padding(14)
.backgroundColor(Color.White)
.borderRadius(8)
}
.swipeAction({ end: this.DeleteButton(item) })
}, (item: MailItem42) => item.id.toString())
}
.layoutWeight(1)
if (this.lastDeleted !== null) {
Row() {
Text('已删除一封邮件')
.fontSize(14)
.fontColor(Color.White)
Blank()
Button('撤销')
.height(32)
.onClick(() => {
this.undoDelete()
})
}
.padding(12)
.backgroundColor('#303133')
.borderRadius(8)
}
}
.padding(16)
.backgroundColor('#F5F7FA')
.height('100%')
}
}

把关键代码一段段拆开
DeletedMail42 保存原始位置。 撤销不是简单地把项目加回来,而是尽量回到用户刚才看到的位置。用户的视觉记忆还在,位置恢复会让操作更可信。
删除时重新赋值 this.mails。 用 filter 生成新数组,状态变化更直观。不要直接依赖数组下标删除,因为列表后续排序、筛选以后很容易删错。
swipeAction 只提供入口。 侧滑按钮不适合承载复杂确认流程。普通邮件删除可以“删后撤销”;账号注销、永久清空这类高风险动作,才需要更重的确认。
撤销策略怎么选
| 策略 | 适合场景 | 注意点 |
|---|---|---|
| 只撤销最近一条 | 邮件、通知、浏览记录 | 状态简单,页面占用少 |
| 批量撤销 | 文件管理、批量清理 | 要保存多条记录和顺序 |
| 删除前二次确认 | 账户注销、永久删除 | 交互更重,但风险更低 |
| 软删除后后台同步 | 服务端数据 | 要处理接口失败和恢复 |
普通业务先做“最近一条撤销”就够实用。它能覆盖误触,又不会让页面底部堆满提示条。
容易踩坑的地方
- 只保存
item不保存index。 撤销后只能追加到末尾,顺序会让用户困惑。 - 删除后立刻永久提交。 服务端已经彻底删掉,再撤销就会变得很复杂。可以先软删除,或延迟同步。
- 多个撤销条同时出现。 手机空间不够,用户也很难判断每个撤销对应哪条数据。
- 撤销按钮太小。 删除是高风险动作,恢复入口要足够明显。
小结
侧滑删除的重点不是手势,而是错误恢复。HarmonyOS7 的 ArkUI 能很快搭出交互,但产品体验要靠状态设计兜住。给一次撤销机会,列表才不会让用户紧张。

1万+

被折叠的 条评论
为什么被折叠?



