一、功能背景
在基于 Vue 2.x + Element-UI 的管理系统开发中,图片上传是高频需求,而「上传后支持拖拽调整图片顺序」能显著提升用户操作体验。本文以实际场景为例,完整拆解实现思路、核心知识点和避坑方案。
二、核心技术栈
基础框架:Vue 2.x
UI 组件库:Element-UI(核心使用 el-upload 上传组件)
拖拽排序:SortableJS(轻量级、高性能的拖拽排序库)
辅助能力:Vue 生命周期、深度监听、DOM 操作、异步更新队列
三、核心实现步骤与知识点拆解
3.1 环境准备:安装并引入 SortableJS
3.1.1安装依赖
bash
npm install sortablejs --save
3.1.2组件内引入
javascript
import Sortable from "sortablejs";
3.2 基础结构:Element-UI Upload 组件配置
·核心属性说明
| 属性名 | 作用 |
|---|---|
| list-type="picture-card" | 图片以卡片形式展示,为拖拽提供可视化基础 |
| file-list | 绑定图片列表数组,控制已上传图片的展示状态 |
| before-upload | 上传前钩子:校验图片格式、大小等 |
| on-success | 上传成功钩子:更新图片列表和表单数据 |
| on-remove | 移除图片钩子:同步更新列表和表单数据 |
| limit | 限制最大上传数量 |
·核心模板代码
html
<el-form-item label="请上传链接内容:" prop="contentImgs">
<el-upload
ref="upload"
class="image-uploader"
:action="actionUrl"
:headers="headers"
list-type="picture-card"
:limit="6"
:file-list="fileList_contentImgs"
:before-upload="(file) => beforeUpload(file, 'contentImgs')"
:on-success="(res, file, fileList) => handleSuccess(res, file, fileList, 'contentImgs')"
:on-remove="(file, fileList) => handleRemove(file, fileList, 'contentImgs')"
:on-exceed="handleExceed"
:class="fileList_contentImgs.length > 5 ? 'hide_box' : ''"
>
<i slot="default" class="el-icon-plus"></i>
</el-upload>
<span>建议尺寸,支持上传图片,最多可上传6张,宽度1080像素,大小不超过2M,支持JPG、JPEG、PNG等格式。</span>
</el-form-item>
3.3 拖拽排序核心实现
3.3.1 初始化 Sortable 实例(核心方法)
·核心思路:找到 el-upload 生成的图片列表 DOM 容器,创建 Sortable 实例,监听拖拽结束事件同步数组顺序。
javascript
// 初始化拖拽排序
initSortable() {
// 获取 Element-UI 生成的图片卡片列表DOM容器
const uploadList = this.$refs.upload?.$el.querySelector(
".el-upload-list--picture-card"
);
// 边界判断:无列表/仅1张图时无需排序,销毁实例避免冗余
if (!uploadList || this.fileList_contentImgs.length <= 1) {
if (this.sortable) {
this.sortable.destroy();
this.sortable = null;
}
return;
}
// 销毁已有实例,防止重复绑定导致拖拽异常
if (this.sortable) {
this.sortable.destroy();
}
// 创建 Sortable 实例
this.sortable = new Sortable(uploadList, {
animation: 150, // 拖拽动画时长(毫秒),提升体验
ghostClass: "sortable-ghost", // 拖拽时占位符样式类
onEnd: (evt) => {
// 拖拽结束:交换数组元素,同步表单数据和展示列表
// evt.oldIndex:拖拽元素原索引;evt.newIndex:拖拽元素新索引
[this.form.contentImgs[evt.oldIndex], this.form.contentImgs[evt.newIndex]] =
[this.form.contentImgs[evt.newIndex], this.form.contentImgs[evt.oldIndex]];
[this.fileList_contentImgs[evt.oldIndex], this.fileList_contentImgs[evt.newIndex]] =
[this.fileList_contentImgs[evt.newIndex], this.fileList_contentImgs[evt.oldIndex]];
},
});
}
3.3.2 时机控制:确保DOM就绪后初始化
·mounted 阶段初始化:
DOM 渲染完成后才能获取到上传列表容器,结合 $nextTick 确保 DOM 就绪:
javascript
mounted() {
this.$nextTick(() => {
this.initSortable();
});
}
·列表变化时重新初始化:
图片上传 / 删除后列表长度变化,需重新绑定拖拽事件,通过「深度监听」实现:
javascript
watch: {
// 深度监听图片列表,数组内部元素变化也能触发
fileList_contentImgs: {
handler() {
this.$nextTick(() => {
this.initSortable();
});
},
deep: true, // 关键:深度监听数组(数组元素增删/顺序变化都能检测)
},
}
3.3.3 实际场景回显
实际开发中,在调用后端接口,更新图片列表时,需再次初始化排序
javascript
// 获取详情-编辑用
getDetail() {
GetDetailApi({id: this.$route.query.id}).then(res => {
if (res && res.code == 0) {
this.form = res.data
// 格式转换:接口返回的字符串转数组
this.form.contentImgs = JSON.parse(res.data.contentImgs)
// 适配 file-list 格式:[{url: 图片地址}]
this.fileList_contentImgs = this.form.contentImgs.map(item => ({ url: item }))
// 重新初始化排序(确保回显后拖拽功能正常)
this.$nextTick(() => {
this.initSortable();
})
}
})
}
3.3.4 内存泄漏防护:销毁 Sortable 实例
javascript
beforeDestroy() {
if (this.sortable) {
this.sortable.destroy();
this.sortable = null;
}
}
3.3.5 样式优化:提升拖拽体验
css
<style lang="scss" scoped>
// 拖拽光标提示:告知用户可拖拽
::v-deep .el-upload-list--picture-card .el-upload-list__item {
margin-right: 10px;
margin-bottom: 10px;
cursor: move;
}
// 拖拽占位符样式:提升拖拽可视化效果
::v-deep .sortable-ghost {
opacity: 0.5;
background: #f5f5f5;
border: 1px dashed #ddd;
}
// 超出数量隐藏上传按钮
::v-deep .hide_box .el-upload--picture-card {
display: none;
}
// 调整上传卡片尺寸
.image-uploader {
--el-upload-picture-card-width: 100px;
--el-upload-picture-card-height: 100px;
}
</style>
四、关键避坑点
4.1 数组更新不触发排序重新初始化
问题:直接修改数组元素 / 长度,Vue 浅监听无法检测,导致拖拽失效。
解决方案:对 fileList_contentImgs 开启 deep: true 深度监听,确保数组内部变化能触发重新初始化。
4.2 重复创建 Sortable 实例
问题:多次调用 initSortable 会创建多个实例,导致拖拽行为异常(如拖拽卡顿、顺序错乱)。
解决方案:每次创建新实例前,先销毁已有实例;列表长度≤1 时直接销毁实例。
4.3 DOM 未渲染完成就获取元素
问题:mounted 阶段直接获取 el-upload-list--picture-card 可能返回 null,导致 Sortable 初始化失败。
解决方案:使用 this.$nextTick 等待 Vue 异步更新 DOM 完成后再执行初始化。
4.4 编辑回显格式不匹配
问题:接口返回的图片地址是字符串 / 纯数组,与 el-upload 的 file-list(对象数组)格式不匹配,导致图片无法回显。
解决方案:将接口返回的图片地址转换为 [{url: 图片地址}] 格式。
五、完整流程总结
1.配置 el-upload 组件,绑定文件列表、上传 / 移除 / 成功等钩子函数;
2.引入 SortableJS,封装初始化拖拽方法,监听拖拽结束事件同步数组顺序;
3.通过 Vue 生命周期(mounted)和深度监听,确保拖拽实例随列表变化动态更新;
4.处理编辑场景的图片回显,适配 file-list 格式;
5.组件销毁前清理 Sortable 实例,避免内存泄漏;
6.优化样式,提升拖拽操作的可视化体验。
六、扩展场景
1.多组图片上传排序:为不同上传组件绑定不同 ref,分别初始化 Sortable 实例,区分不同图片列表;
2.拖拽权限控制:通过 Sortable 的 filter 属性限制特定元素不可拖拽;
3.后端同步排序:拖拽结束后调用接口,将最新排序的图片地址数组同步到后端,持久化排序结果;
4.拖拽动画定制:通过 Sortable 的 chosenClass dragClass 等属性定制拖拽过程中的样式。