列表项的隐藏操作区

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-modelstring滑出方向:left / right / ''(关闭)
disabledboolean是否禁用滑动
before-closeFunction关闭前回调,返回 false 或 rejected Promise 阻止关闭
#left / #rightslot左右滑出区域内容
#defaultslot单元格本体内容

总结

SwipeCell 的用法本身很简单,真正决定体验的是三个细节:按钮固定宽度、before-close 拦截高危操作、删除项先收起再移除。把这三点做对,滑动菜单的手感和原生 App 基本没有差距。

移动端交互的精细化,就藏在这些 300 毫秒的过渡里。