前面我们已经讲过 Dialog 与 Toast 这对基础反馈(见本站《Vant Dialog 弹窗与 Toast 轻提示:交互反馈利器》),但移动端还有一整层「轻量反馈」没被覆盖:底部操作菜单、分享面板、顶部通知条、全屏图片预览。Vant 反馈层组件的这四个成员各有明确的调用范式,混用起来最容易出的问题不是样式,而是样式没引入、函数式调用找不到实例、关闭时机被拦截。本文按分层模型逐一拆解,最后拼成一个商品详情页的完整交互。
先建立分层模型
Notify 与 ImagePreview 提供函数式调用(showNotify / showImagePreview),不需要在模板里写标签;ActionSheet 与 ShareSheet 是普通组件,用 v-model:show 控制。这个区别直接决定了按需引入时样式要不要手动加。
VantResolver / unplugin-vue-components 自动导入时,函数式组件的样式不会自动带上。如果 showNotify 弹出来了却完全没有样式,在入口补一行 import 'vant/es/notify/style';ImagePreview 同理补 import 'vant/es/image-preview/style'。ActionSheet:actions 数据结构与关闭时机
每一项是 ActionSheetAction:name 主文案,subname 副文案,color 文字色,icon 图标(Vant 图标名或图片 URL,v4.8.6+),disabled / loading 状态,className 自定义类,以及可挂在项上的 callback。
<script setup>
import { ref } from 'vue';
import { showNotify, showToast } from 'vant';
const show = ref(false);
const actions = [
{ name: '收藏商品', icon: 'star-o' },
{ name: '分享给好友', icon: 'share-o', subname: '可获得积分' },
{ name: '举报', color: '#ee0a24' },
{ name: '正在下架中', disabled: true },
];
const onSelect = (action, index) => {
// close-on-click-action 为 true 时组件会自行关闭
showToast(`选择了:${action.name}(第 ${index + 1} 项)`);
showNotify({ type: 'success', message: `${action.name} 成功` });
};
</script>
<template>
<van-cell is-link title="更多操作" @click="show = true" />
<van-action-sheet
v-model:show="show"
:actions="actions"
cancel-text="取消"
description="请选择要执行的操作"
close-on-click-action
@select="onSelect"
@cancel="() => console.log('已取消')"
/>
</template>
close-on-click-action 默认是 false——不显式开启,点完选项面板不会自己收起,这是最常见的「点了没反应」错觉。若要在关闭前做异步确认,用 before-close:接收 action 字符串,返回 false 或 Promise<false> 即可拦住。ActionSheet 也支持默认插槽,可塞任意自定义内容当底部面板用。
ShareSheet:一维数组与二维数组
options 是一维数组时渲染单行;传二维数组则自动分成多行(第一行常用作分享渠道,第二行放复制链接、生成海报等操作)。内置 icon 名包括 wechat、wechat-moments、weibo、qq、link、poster、qrcode、weapp-qrcode,也可以直接传图片 URL。
<script setup>
import { ref } from 'vue';
import { showNotify } from 'vant';
const showShare = ref(false);
const options = [
[
{ name: '微信', icon: 'wechat' },
{ name: '朋友圈', icon: 'wechat-moments' },
{ name: '微博', icon: 'weibo' },
{ name: 'QQ', icon: 'qq' },
],
[
{ name: '复制链接', icon: 'link' },
{ name: '生成海报', icon: 'poster' },
{ name: '二维码', icon: 'qrcode' },
],
];
const onSelect = (option, index) => {
showShare.value = false;
showNotify({ type: 'primary', message: `已选择:${option.name}(${index})` });
};
</script>
<template>
<van-share-sheet
v-model:show="showShare"
title="分享给好友"
description="分享后可获得 10 积分"
:options="options"
cancel-text="取消"
@select="onSelect"
/>
</template>
select 里按 option.icon 分发到 JS-SDK、剪贴板或海报生成函数即可;自定义渠道用图片 URL 当 icon,别去改内置样式变量。Notify:函数式调用与全局默认配置
showNotify 接受字符串或 NotifyOptions 对象,返回 Notify 实例;还有 closeNotify()、setNotifyDefaultOptions()、resetNotifyDefaultOptions() 三个配套方法。常用配置:type(primary/success/danger/warning)、message、duration(默认 3000ms,设为 0 不自动消失)、position(top/bottom)、color、background、onClick、onClose。
import { setNotifyDefaultOptions } from 'vant';
import 'vant/es/notify/style';
setNotifyDefaultOptions({
type: 'primary',
duration: 2000,
position: 'top',
onClose() {
console.log('notify closed');
},
});
import { showNotify, closeNotify } from 'vant';
showNotify('已加入购物车');
showNotify({ type: 'danger', message: '库存不足', duration: 0 });
// 需要常驻时手动关闭
closeNotify();
如果要在通知条里放图标、按钮等自定义内容,改用组件式:在模板里写 <van-notify v-model:show="show" type="success">,用默认插槽填内容,自己控制显隐时机。
ImagePreview:从函数式调用到 beforeClose
最直接的用法是传字符串数组;需要控制起始位置、关闭按钮或监听关闭时,传配置对象。showImagePreview 返回实例,可调用 close()。
import { showImagePreview } from 'vant';
import 'vant/es/image-preview/style';
const images = [
'https://example.com/goods-1.jpg',
'https://example.com/goods-2.jpg',
];
const instance = showImagePreview({
images,
startPosition: 1, // 从第 2 张开始
closeable: true, // 显示右上角关闭图标
showIndex: true,
maxZoom: 3,
onChange: (index) => console.log('current', index),
onClose: () => console.log('preview closed'),
});
// 需要时主动关闭:instance.close();
其它常用项:loop(默认 true)、showIndicators、swipeDuration、closeIconPosition(top-left/bottom-left/bottom-right)、closeOnClickImage、closeOnClickOverlay、beforeClose(返回 false 阻止关闭)、onScale。组件式 <van-image-preview> 则提供 index、cover、image 三个插槽,后者甚至能把图片替换成视频。
showImagePreview 最省代码;要自定义页码、加「保存到相册」按钮或渲染视频,才用组件式 + 插槽。两者的 images 都是纯 URL 数组,不要塞 File 对象。组合实战:一个商品详情页的交互层
<script setup>
import { ref } from 'vue';
import { showNotify, showImagePreview, showToast } from 'vant';
const images = ['/a.jpg', '/b.jpg', '/c.jpg'];
const showActions = ref(false);
const showShare = ref(false);
const actions = [
{ name: '分享', icon: 'share-o' },
{ name: '收藏', icon: 'star-o' },
{ name: '举报', color: '#ee0a24' },
];
const shareOptions = [
[
{ name: '微信', icon: 'wechat' },
{ name: '朋友圈', icon: 'wechat-moments' },
],
[{ name: '复制链接', icon: 'link' }],
];
const onSelectAction = (action) => {
if (action.name === '分享') {
showShare.value = true;
return;
}
showNotify({ type: 'success', message: `${action.name}成功` });
};
const onSelectShare = async (option) => {
showShare.value = false;
if (option.icon === 'link') {
await navigator.clipboard.writeText(location.href);
showNotify({ type: 'success', message: '链接已复制' });
return;
}
showToast(`调起 ${option.name} 分享`);
};
const preview = (index: number) =>
showImagePreview({ images, startPosition: index, closeable: true });
</script>
<template>
<van-swipe :autoplay="0" @change="() => {}">
<van-swipe-item v-for="(src, i) in images" :key="src">
<img :src="src" @click="preview(i)" />
</van-swipe-item>
</van-swipe>
<van-action-sheet
v-model:show="showActions"
:actions="actions"
cancel-text="取消"
close-on-click-action
@select="onSelectAction"
/>
<van-share-sheet
v-model:show="showShare"
title="分享商品"
:options="shareOptions"
@select="onSelectShare"
/>
</template>
常见坑速查
| 现象 | 原因 | 处理 |
|---|---|---|
showNotify 有内容无样式 | 按需引入未带函数式组件样式 | import 'vant/es/notify/style' |
| ActionSheet 点选项后不关闭 | close-on-click-action 默认 false | 显式开启,或在 select 里手动置 false |
| ShareSheet 只显示一行 | options 传成了一维数组 | 改成二维数组分行 |
| 预览层里加不了按钮 | 函数式调用不支持插槽 | 换组件式 van-image-preview + cover 插槽 |
| 弹层被业务浮层盖住 | z-index 默认 2000+,与自定义层冲突 | 传 z-index / zIndex 统一规划层级 |
| 底部按钮被 Home 条挡住 | 未开启安全区 | 保持 safe-area-inset-bottom 默认 true,并在 meta 里加 viewport-fit=cover |
总结
这四个组件其实只回答两个问题:要不要用户做决策(要 → ActionSheet / ShareSheet),要不要独占屏幕(要 → ImagePreview,不要 → Notify)。把函数式与组件式的边界划清楚,再补上按需引入时那两行样式,反馈层基本就不会再出诡异问题。
反馈组件的价值不在动画,而在于「不打断」与「必须打断」之间的取舍。
—— 选错层级,用户就会觉得 App 黏手