列表项的隐藏操作区
iOS 邮件列表左滑出现「删除、置顶」的交互,在移动端几乎是标配。Vant 用 SwipeCell 一个组件实现:把左右滑出的操作按钮放进左右插槽,单元格本体就是默认内容,滑动、回弹、互斥全由组件处理。
最简示例
html
demo.vue
<template>
<van-swipe-cell>
<template #left>
<van-button square type="primary" text="选择" />
</template>
<van-cell :border="false" title="单元格" value="内容" />
<template #right>
<van-button square type="danger" text="删除" @click="onDelete" />
<van-button square type="primary" text="收藏" />
</template>
</van-swipe-cell>
</template>
<script setup>
import { showConfirmDialog } from "vant";
const onDelete = () => {
showConfirmDialog({ title: "确认删除这条记录吗?" });
};
</script>
插槽规则
#left 是向右滑动露出的区域,#right 是向左滑动露出的区域;不用的方向不写插槽即可禁止该方向滑动。
操作按钮的宽度建议
滑出区域的宽度由按钮自身宽度决定。推荐每个按钮固定宽高,避免内容不同导致滑动距离忽长忽短:
html
demo.vue
<template #right>
<van-button square type="primary" text="置顶" class="action-btn" />
<van-button square type="danger" text="删除" class="action-btn" />
</template>
<style scoped>
.action-btn {
height: 100%; /* 与单元格同高 */
width: 65px; /* 固定宽度,滑动距离可预期 */
}
</style>
受控用法:编程式开关
用 v-model 绑定滑出方向(left / right / 空字符串),可以在外部控制打开与关闭,比如点击其它区域时收起:
html
list.vue
<template>
<van-swipe-cell v-for="item in list" :key="item.id" v-model="item.opened">
<template #right>
<van-button square type="danger" text="删除" @click="onDelete(item)" />
</template>
<van-cell :title="item.title" />
</van-swipe-cell>
</template>
<script setup>
import { ref } from "vue";
import { showToast } from "vant";
const list = ref([
{ id: 1, title: "订单 A", opened: "" },
{ id: 2, title: "订单 B", opened: "" },
]);
const onDelete = (item) => {
// 先收起,再执行业务
item.opened = "";
list.value = list.value.filter((i) => i.id !== item.id);
showToast("已删除");
};
</script>
💡 小技巧:想让"滑开 A 时自动收起 B",watch 所有项的 opened 值,把非当前项重置为空字符串即可,无需额外 API。
before-close:拦截关闭做确认
删除这类高危操作,最好在关闭滑出区前弹确认框。before-close 接收 { position } 参数并返回 Promise,resolve 才允许关闭:
html
demo.vue
<template>
<van-swipe-cell :before-close="beforeClose">
<template #right>
<van-button square type="danger" text="删除" />
</template>
<van-cell title="重要数据" />
</van-swipe-cell>
</template>
<script setup>
import { showConfirmDialog } from "vant";
const beforeClose = ({ position }) => {
switch (position) {
case "left":
case "cell":
case "outside":
return true; // 点本体或空白处直接关闭
case "right":
return showConfirmDialog({
title: "提示",
message: "确定删除吗?删除后不可恢复。",
}); // 用户确认后才关闭并执行
}
};
</script>
position 的取值有 left / right / cell / outside 四种:cell 是点击了单元格本体,outside 是点击了滑出区域以外的空白,right 是点击了右侧滑出按钮。真正触发删除动作要在 right 分支的 Promise resolve 之后处理。
与列表组件组合的常见坑
滑动与页面滚动冲突
SwipeCell 会阻止横向手势传播,纵向滚动不受影响。但若单元格内嵌了横向可滚动元素(如图片横滑条),需要在该元素上调用 stop-propagation:
html
demo.vue
<van-swipe-cell>
<template #right>
<van-button square type="danger" text="删除" />
</template>
<div class="inner-scroll" @touchmove.stop>横向内容</div>
</van-swipe-cell>
点击删除后滑出区没收起
删除导致列表项被 v-for 移除时,组件直接卸载不会走关闭动画,视觉上会"闪一下"。更平滑的做法是先通过 v-model 收起,动画结束后再从数据源删除:
javascript
list.vue
const onDelete = (item) => {
item.opened = ""; // 先收起
setTimeout(() => {
list.value = list.value.filter((i) => i.id !== item.id);
}, 300); // 等收起动画结束再移除
};
属性速查
| 属性 / 插槽 | 类型 | 说明 |
|---|---|---|
| v-model | string | 滑出方向:left / right / ''(关闭) |
| disabled | boolean | 是否禁用滑动 |
| before-close | Function | 关闭前回调,返回 false 或 rejected Promise 阻止关闭 |
| #left / #right | slot | 左右滑出区域内容 |
| #default | slot | 单元格本体内容 |
总结
SwipeCell 的用法本身很简单,真正决定体验的是三个细节:按钮固定宽度、before-close 拦截高危操作、删除项先收起再移除。把这三点做对,滑动菜单的手感和原生 App 基本没有差距。
移动端交互的精细化,就藏在这些 300 毫秒的过渡里。