ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vant Uploader删除图标自定义:插槽机制、事件链路与实战踩坑

Vant Uploader删除图标自定义:插槽机制、事件链路与实战踩坑 上个项目里UI 验收的时候设计师给了一张截图上传组件预览图右上角的删除按钮必须是一个红色圆底白色垃圾桶还带一点投影。我看了下 Vant 默认的样子黑色半透明底、白色叉号做得倒是规整可和设计稿就是两套语言。当时第一反应是拿 CSS 强行覆盖点开开发者工具一看这个删除图标是组件内部渲染的van-icon nameclear /样式是写死在.van-uploader__preview-delete上的。硬改确实能改但要是以后想换成文字、换成图片、或者想按文件状态显示不同的图标CSS 方案就完全崩了。后来翻了 Vant 的文档发现 van-uploader 原生提供了preview-delete插槽就是专门用来替换这个删除图标的。这篇文章就从插槽的底层机制讲起把用法、事件链路、以及我在真机调试里踩过的几个坑完整捋一遍给同样被 UI 细节折磨的朋友一份能直接抄的作业。1. 为什么默认删除图标满足不了真实需求一个典型的 UI 验收现场1.1 默认删除图标的真实渲染逻辑Vant 的 uploader 里删除按钮并不是一个孤立的元素它被渲染在预览图的容器内部位置是绝对定位到右上角。默认结构大概是这样的div classvan-uploader__preview img classvan-uploader__preview-image src... / div classvan-uploader__preview-delete click.stoponDelete van-icon nameclear size18 color#fff / /div /div默认的.van-uploader__preview-delete宽高是18px背景是rgba(0, 0, 0, 0.45)左下角还带一个12px的圆角。也就是说视觉上是一个右上角的小黑方块里面放了一个白色的叉号图标。这个设计在样式上是干净的但放到真实项目里就很受限制。设计稿里常见的删除按钮至少有这几种红色圆形背景加白色垃圾桶、纯文字删除、某个运营位的定制图标、甚至有的需求要求删除按钮悬浮在图片边缘之外露出一半来。这些光靠改默认图标的颜色和尺寸做起来很别扭。1.2 用 CSS 覆盖为什么是条死路你可能会想既然只是图标不一样那我把.van-uploader__preview-delete里的内容换掉不就行了问题在于Vant 的样式是组件内部编译出来的默认的van-icon nameclear /是写死在组件模板里的。你没法通过 CSS 把nameclear这个图标改成namedelete因为图标字体的字符编码是固定的。你最多改改背景色、圆角、尺寸改不了图标本身。还有一种偏方是设置display: none隐藏默认删除图标然后在外面自己定位一个新按钮盖上去。这个方案能出效果但位置需要手动对齐而且新按钮和 Vue 组件内部的状态完全隔离比如图片上传失败时的重试状态、多选时的遮罩层这些细节都容易对不上。所以 Vant 官方保留插槽是有道理的替换组件的某一部分内容插槽才是标准答案。1.3 插槽才是标准答案Vant 从 3.x 开始在 uploader 上预留了preview-delete这个插槽。使用方式很简单van-uploader v-modelfileList template #preview-delete{ file } !-- 这里放你自定义的图标完全按自己的设计来 -- /template /van-uploader一旦写了这个插槽Vant 默认的白色叉号就不会再渲染了你的自定义内容会出现在原来的删除按钮位置上。而且因为内容的宿主是组件内部那个preview-delete容器它的点击事件链路、before-delete校验、文件索引的传递都是通的不需要你额外去调方法。2. preview-delete 插槽的底层工作机制组件到底往里画了什么2.1 用一个生活类比理解插槽很多新手朋友一听插槽就觉得玄乎其实可以把它理解成电脑主板上的 PCI-E 插槽。主板上预留了一个物理接口默认可能插着一张网卡你可以拔掉它换上显卡、声卡、扩展卡。组件里的插槽也是这个逻辑组件内部预留了一个渲染位置默认放一份内容你可以把默认内容拿走换一份自己的内容上去组件本身的供电和通信能力还是照常工作。preview-delete这个插槽就是 emmm可以理解成主板右上角专门给删除功能预留的那个 x16 插槽。2.2 Vant 源码里这个插槽的渲染位置为了搞明白自定义内容会不会影响点击和删除我专门去翻了一下 Vant 4 的 uploader 源码。在UploaderPreviewItem.vue里面渲染逻辑大致是下面这样的div classvan-uploader__preview clickonPreview van-image v-if!isVideo :srcfile.url classvan-uploader__preview-image / video v-else ... / div v-ifdeletable classvan-uploader__preview-delete click.stoponDelete slot namepreview-delete :filefile van-icon nameclear size18 color#fff / /slot /div div v-ifstatus classvan-uploader__mask ... / /div几个关键信息删除按钮的外层容器是.van-uploader__preview-delete它绑定了click.stoponDelete。插槽的默认内容是那个白色叉号图标你只要提供了插槽内容默认的 icon 就不会渲染。插槽作用域里至少能拿到file对象这个对象里带了url、status、message、uid等信息。也就是说自定义内容无论长什么样点击时都会先走外层容器的onDelete这个事件内部会处理删除逻辑不会冒泡到预览图导致打开预览。2.3 插槽作用域里的 file 对象能干什么file对象是 Vant 内部维护的文件描述字段大致包括字段说明常见用途uid文件唯一标识定位到具体文件url文件地址预览图用status上传状态uploading/done/failed根据状态显示不同图标message错误信息上传失败时提示deletable单个文件是否可删除控制删除按钮显隐isImage是否为图片区分图片和视频isVideo是否为视频同上比如我想让上传失败的文件显示一个重试图标而不是删除图标就可以在插槽里用file.status failed来判断。注意一点这里说的显示什么图标只是视觉变化点击事件依然走的是删除逻辑真正的重试动作需要在别的环节另做处理这个我后面会细说。3. 动手实现自定义删除图标从一行替换到完整交互3.1 最小可用版本换成 Vant 自带的垃圾桶图标如果你的设计稿只是不想要那个叉号想换成垃圾桶可以这么写van-uploader v-modelfileList :max-count6 template #preview-delete van-icon namedelete size18 color#fff / /template /van-uploader这里只用到了 Vant 图标库里的delete图标。默认的外层容器依然保留rgba(0,0,0,0.45)的黑色半透明背景白色垃圾桶放上去视觉上就是黑底白垃圾桶大部分浅色调设计稿都能接受。3.2 换成设计稿里的红色圆底垃圾桶接下来是真正的常见需求删除按钮是红色圆底、白色垃圾桶可能还带一个微小的投影。代码是这样van-uploader v-modelfileList :max-count9 template #preview-delete span classcustom-delete-btn van-icon namedelete size14 color#fff / /span /template /van-uploader对应的样式.custom-delete-btn { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; background: #ee0a24; box-shadow: 0 2px 6px rgba(238, 10, 36, 0.4); transform: translate(-2px, 2px); } :deep(.van-uploader__preview-delete) { background: transparent; border-radius: 0; width: 24px; height: 24px; }注意这里的两处深调整外层容器.van-uploader__preview-delete默认是18px × 18px如果你的自定义按钮比它大就会溢出造成点击区域不准。我习惯把外层容器调成和自定义内容差不多大的尺寸然后把自带的背景去掉。默认容器是绝对定位在top: 0; right: 0的如果你的设计稿要求按钮悬浮在图片之外可以再配合transform微调。3.3 换成图片或文字删除按钮有些时候设计稿里用的不是图标库的图标而是一张 PNG、一个 Base64 图片或者干脆是删除两个字。这些情况插槽也都支持template #preview-delete img classcustom-delete-img srchttps://your-cdn.com/delete-btn.png alt删除 / /template template #preview-delete span classcustom-delete-text删除/span /template图片方案要注意自定义图片一定要显式设置宽高否则在小程序环境和部分 WebView 里可能出现无法预料的尺寸渲染问题。文字方案要注意外层容器默认面积不大文字稍微长一点就溢出需要同步调整外层容器的宽高和文字字号。3.4 根据文件状态显示不同内容这个是我自己项目里用得最多的场景。文件上传中显示一个灰圈加载上传失败显示一个红色删除按钮上传成功显示普通的垃圾桶template #preview-delete{ file } span classcustom-delete-btn :class{ custom-delete-btn--danger: file.status failed, custom-delete-btn--loading: file.status uploading } van-icon :namefile.status uploading ? replay : delete size14 color#fff / /span /template这里演示的是视觉根据状态变化但你要清楚一点这几种状态下点击删除按钮组件内部的删除逻辑是一样的。如果上传失败时需要触发重传而不是删除就不要依赖这个插槽的点击链路了应该考虑把组件整体的上传流程重新设计或者在文件状态变化时用别的交互入口去承接重传动作。把视觉切换和业务逻辑混在一起是这个需求里最容易踩的坑。4. 点击事件与删除逻辑自定义内容到底要不要自己绑事件4.1 源码层面确认不用自己绑删除事件很多开发者在自定义插槽时习惯性地给内容加上click来处理删除结果发现删除了两次。这里我从源码角度明确说一次不需要。组件内部.van-uploader__preview-delete这个容器本身已经绑定了click.stoponDelete。你写的自定义内容是这个容器的子节点点击子节点会冒泡到容器上由 Vant 内部的onDelete统一处理。这个内部处理逻辑会完成三件事校验该文件是否满足删除条件如果配置了before-delete会先执行它。从fileList中移除当前文件。触发delete事件让你在父组件里可以拿到被删除的文件和索引。所以你自定义的内容只需要关心长什么样不需要关心怎么删。这反而是好消息因为你省掉了很多状态同步的麻烦。4.2 before-delete 如何与自定义删除图标联动既然自定义内容自动走内部删除链路那么配置在组件上的before-delete也会自动生效。举个例子import { showConfirmDialog } from vant; const beforeDelete async () { await showConfirmDialog({ title: 删除确认, message: 删除后无法恢复确定要继续吗, }); };模板里只需要把before-delete传进组件van-uploader v-modelfileList :before-deletebeforeDelete :max-count6 template #preview-delete span classcustom-delete-btn van-icon namedelete size14 color#fff / /span /template /van-uploader这样点击任何一个自定义删除图标时都会先弹确认框点击确认后才真正删除。before-delete支持返回布尔值、Promise、或者直接抛出异常来中断删除弹窗确认是最常见的方案。4.3 什么时候真的需要自己处理删除逻辑有一种特殊情况建议手动处理删除逻辑你在插槽内容里又加了一个独立的按钮而这个按钮的业务不是删除而是重传下载分享这类操作。这时候千万不要把它放进preview-delete插槽里因为无论你在这个插槽里放多少个按钮点击都会冒泡到外层容器触发删除。正确做法有两种方案一在自定义内容里写click.stop截断冒泡然后执行自己的逻辑。方案二干脆不用preview-delete插槽而是在van-uploader外面用footer插槽或者自定义操作列完全脱离删除容器。我实际项目里用的是方案一因为设计稿要求删除按钮和重传按钮都叠在图片右上角紧凑布局下只能放在同一个位置。核心是给不想触发删除的按钮加上click.stoptemplate #preview-delete{ file } span classcustom-actions van-icon v-iffile.status failed namereplay click.stophandleRetry(file) / van-icon namedelete / /span /template这样设计既享受了插槽自带删除逻辑的便利又不会误删。5. 踩坑实录自定义删除图标实战中的五个高频问题5.1 自定义内容不显示八成是外层容器尺寸不够和背景遮挡很多朋友替换了插槽之后发现自定义图标不见了其实不是不见了是被默认容器盖住或者限定了尺寸。默认.van-uploader__preview-delete只有18px × 18px如果自定义内容是一个20px的圆形按钮它的一部分会溢出到容器外面但容器本身没有overflow: hidden所以溢出部分一般还是可见的。真正看不见的原因往往是你在插槽里写了一个空的div但没有给它任何宽高和内容渲染出来占位是 0而外层容器默认是透明背景的视觉上自然什么都没有。解决办法也是两条给自定义内容一个确定的宽高或者同步用深选择器放大外层容器。我个人建议两条都做避免容器太小导致点击热区不足。5.2 点击删除图标却触发了预览图冒泡链路出了问题我前面说过Vant 内部绑的是click.stop正常情况下点击删除不会触发预览。但如果你在自定义内容上自己又绑了click并且在事件处理里调用了一些不阻止冒泡的方法就可能出现图片预览也被打开的诡异现象。排查思路很明确先打开控制台看点击自定义按钮时预览是否被触发。检查自定义内容上有没有多余的click绑定。如果有加上.stop修饰符。有一种比较隐蔽的情况是你的自定义内容是label标签label 的点击行为在某些移动端浏览器里会触发关联组件从而引发额外事件。所以我建议自定义内容优先用span、i、button这类语义和事件模型都简单的标签。5.3 小程序端和 H5 端的差异插槽写法与样式作用域Vant 在 H5 端用的是#preview-delete这种 Vue 3 插槽语法如果项目还在用 Vue 2 的 Vant 2写法要换成slotpreview-delete。Vant Weapp微信小程序版则完全是另一套。Vant Weapp 的 uploader 同样支持preview-delete插槽但小程序端是slotslot-scope的旧式写法van-uploader file-list{{ fileList }} bind:deleteonDelete view slotpreview-delete slot-scopeprops van-icon namedelete size18 / /view /van-uploader另外小程序端的样式隔离规则比 H5 严格page级样式和组件内部样式是隔离的。自定义内容要覆盖 Vant 组件内部的类名在 H5 端可以用:deep()在小程序端可能要配合externalClasses或者直接写在自定义内容自身的类名上不要去试图覆盖.van-uploader__preview-delete的样式很容易不生效。5.4 多文件场景下 index 定位混乱preview-delete插槽的作用域里只传了file没有直接给index。如果你需要知道删除的是第几张图有两种做法在插槽里通过fileList.indexOf(file)自己算索引。在delete事件里直接拿返回的index。我更推荐第二种因为删除事件里返回的索引是精确的、事件触发时实时计算的。插槽渲染时算好的index一旦文件列表结构变化容易取到旧值。尤其是你同时支持多选删除和排序功能时旧索引会让你删错文件。5.5 单个文件禁止删除和 deletable 属性配合有时候不是所有文件都允许删除比如相册封面图不允许删除其余图片可以。Vant 的fileList里每个文件对象可以单独配置deletableconst fileList ref([ { url: https://example.com/cover.jpg, deletable: false }, { url: https://example.com/photo1.jpg, deletable: true }, ]);当file.deletable false时外层.van-uploader__preview-delete容器根本不会渲染preview-delete插槽里的内容也不会出现。这个优先级是单文件配置 组件级配置不需要你在插槽里写v-if来手动判断。我在项目里第一次遇到这个问题时还傻乎乎地写了一个判断后来发现完全多余。6. 封装一个可复用的自定义删除图标组件少写三遍重复代码6.1 组件封装思路自定义删除图标这个需求通常会在多个页面出现但不同页面的按钮颜色、图标可能有细微差别。我习惯封装成一个轻量组件对外暴露几个 props内部统一处理样式和事件。设计思路是这样的type控制按钮风格默认danger可选dark。size控制删除按钮尺寸需要同步放大外层容器。custom-text如果设计稿要显示文字传入文字内容。这样每个页面传个type和size就能用不会在多个页面复制十几行 CSS。6.2 完整代码新建一个CustomDeleteIcon.vuetemplate span classcustom-delete-icon :classcustom-delete-icon--${type} :stylewidth: ${size}px; height: ${size}px; van-icon v-if!text namedelete :sizesize * 0.7 color#fff / span v-else classcustom-delete-icon__text{{ text }}/span /span /template script setup defineProps({ type: { type: String, default: danger, }, size: { type: Number, default: 20, }, text: { type: String, default: , }, }); /script style scoped .custom-delete-icon { display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; background: #ee0a24; box-shadow: 0 2px 6px rgba(238, 10, 36, 0.4); } .custom-delete-icon--dark { background: rgba(0, 0, 0, 0.45); box-shadow: none; border-radius: 0 0 0 12px; } .custom-delete-icon__text { font-size: 12px; color: #fff; line-height: 1; } /style然后在van-uploader里这样用van-uploader v-modelfileList :max-count9 template #preview-delete CustomDeleteIcon :size24 typedanger / /template /van-uploader如果项目里多个页面都用了同一个删除按钮视觉风格这一步能减少大量重复样式代码。需要注意封装组件里的深选择器依然要写成这样:deep(.van-uploader__preview-delete) { width: v-bind(size 4 px); height: v-bind(size 4 px); background: transparent; border-radius: 0; }但v-bind在scoped样式里的写法要配合vueuse或者 CSS 变量为了简洁我这里就不展开实际复制代码时手动调整一下外层容器尺寸即可。6.3 配合 file 级配置实现复杂业务封装组件的最终形态是配合插槽作用域和单文件配置完成复杂业务。举一个我在真实项目里做过的场景前 3 张图是必传图删除时必须二次确认。后 6 张图是补充图可以随便删不需要确认。上传失败的图片删除按钮要变成红色警示色。实现时单文件上标记一个isRequired字段before-delete里判断当前文件索引决定是否弹确认框。删除按钮的视觉通过preview-delete插槽和file.status动态控制。这套组合完整覆盖了大多数上传场景的交互需求而且全部基于 Vant 的既有机制不会产生额外的状态维护成本。我在实际使用中还有一个体会自定义删除图标绝对不是单纯改个 icon 那么简单它牵涉到点击事件链路、文件状态、单文件可删配置、以及跨端差异。建议你在动手之前先想清楚自己的业务里删除这个动作是纯删除、还是要联动重传、是否要二次确认。想清楚这些再回头看插槽很多奇奇怪怪的报错和 bug 其实都能在源码层面找到答案。
返回列表