背景
项目基于 vue3 naive 搭建的后台项目,使用 naive 的 upload 不能满足一些合理的特殊需求(拖拽、多张上传的处理),故封装了自定义上传组件
目标
- 基础上传功能
- 图片上的功能:删除 查看
- 拖拽排序
- 多张图片上传 & 限制上传张数
- 限制上传图片的大小
- 列表中展示缩略图,预览展示原图
- 支持查看模式
基础的上传功能
设置 input 的 type="file",即可使用上传功能。当选择完上传文件之后便会触发 change 的回调 handlerFileChange
<input
type="file"
@change="handlerFileChange"
multiple
style="display: none"
ref="fileInputRef"
accept="image/*"
/>
handlerFileChange 将用户选择的文件(们)进行进一步处理
满足了图片数量和图片大小的要求之后将文件流传给上传函数 upload
这里需要注意一点是要将 target 对象清空,可以解决同一个文件二次上传失败的问题
/**
* 用户点击上传按钮
*/
async function handlerFileChange(e: Event) {
const target = e.target as HTMLInputElement
const files = target.files as FileList
// 允许上传的长度
const canUploadMax = props.max - fileList.value.length
// 触发上传,剔除允许上传文件数量之外的文件
for (let i = 0; i < Math.min(files.length, canUploadMax); i++) {
const file = files[i]
if (file.size > UploadConfig.maxSize * 1024 * 1024) {
// 限制图片大小
message.warning(`请选择小于${UploadConfig.maxSize}的文件`)
break
}
await upload(file)
}
// 手动置空,解决同个图片不能上传的问题
target.value = ''
nextTick(() => {
sortFun()
})
update()
}
upload 上传函数
formatStr 表示图片的格式
Key 作为即将放置的图片的路径
这里用的是腾讯云的 cos 上传 sdk
上传成功之后,将图片链接存入组件的图片列表的变量中
function upload(file: File) {
return new Promise((resolve, reject) => {
const formatStr = file.type?.replace('image/', '') // 图片格式
const Key = `/test/static/pic/cat/oai${new Date()
.getTime()
.toString()}.${formatStr}`
imgSTSStore.cos.putObject(
{
Bucket: imgSTSStore.Bucket,
Region: imgSTSStore.Region,
Key,
Body: file as File,
onProgress: function (progressData) {},
},
function (err, data) {
const url = `https://res.mdaren.cn${Key}`
// 将文件传入文件变量
fileList.value.push({
id: new Date().getTime().toString(),
url: url,
file: file,
})
resolve(1)
}
)
})
}
图片上的功能:删除 查看
删除
点击删除后利用id唯一性在回调函数中找到需要删除的项,splice 删除数据,上报数据更新
<n-button
text
style="font-size: 16px"
type="error"
@click="deleteImg(item.id)"
v-if="!viewFlag"
>
<n-icon>
<SvgIcon name="delete"></SvgIcon>
</n-icon>
</n-button>
/**
* 删除图片
* @param id 被删除项ID
*/
function deleteImg(id: string) {
const findIndex = fileList.value.findIndex((v) => v.id === id)
fileList.value.splice(findIndex, 1)
update()
}
查看
<n-button
text
style="font-size: 16px"
type="primary"
@click="previewImg(item.url)"
>
<n-icon>
<SvgIcon name="eye"></SvgIcon>
</n-icon>
</n-button>
点击预览图片
监听到点击预览事件之后,将所要预览的图片链接赋值给变量 previewUrl,同时触发图片组件的预览点击,唤起预览弹窗
const previewUrl: Ref<string> = ref('')
const previewRef: Ref<any> = ref(null)
/**
* 预览图片
* @param url 将要预览的图片的url
*/
function previewImg(url: string) {
previewUrl.value = url
nextTick(() => {
previewRef.value.click()
})
}
图片预览,工具组件
这里利用 naive 的图片预览功能作为本文组件的预览方案,下面是 n-image 的组件写法
<n-image
ref="previewRef"
width="100"
:src="previewUrl"
style="display: none"
/>
拖拽排序
拖拽用的是 sortablejs
拖拽成功之后的回调会得到原位置和新位置两个的索引,这里利用这两个索引对图片列表进行了插队排序,得到拖拽成功后的数据
import Sortable from 'sortablejs'
const sortable: Ref<Sortable | null> = ref(null)
const draggableContainer: Ref<any> = ref(null)
function sortFun() {
const el = draggableContainer.value.$el.querySelectorAll(
'div.imgList'
)[0] as HTMLElement
sortable.value = Sortable.create(el, {
ghostClass: 'sortable-ghost', // Class name for the drop placeholder
onEnd: (evt: any) => {
if (
typeof evt.oldIndex !== 'undefined' &&
typeof evt.newIndex !== 'undefined'
) {
const { oldIndex, newIndex } = evt
const idArr: string[] = []
fileList.value.forEach((v) => {
idArr.push(v.id)
})
// 切割的位置
const sliceIndex = oldIndex > newIndex ? newIndex : newIndex + 1
// 原位置
const spliceIndex = oldIndex > newIndex ? oldIndex + 1 : oldIndex
const newArr = [
...fileList.value.slice(0, sliceIndex),
fileList.value[oldIndex],
...fileList.value.slice(sliceIndex, idArr.length),
]
newArr.splice(spliceIndex, 1)
fileList.value = newArr
update()
}
},
})
}
多张图片上传 & 限制上传张数
input multiple 属性设置之后就能允许多张
限制张数从 handlerFileChange 限制
// 允许上传的长度
const canUploadMax = props.max - fileList.value.length
// 触发上传,剔除允许上传文件数量之外的文件
for (let i = 0; i < Math.min(files.length, canUploadMax); i++) {
const file = files[i]
if (file.size > UploadConfig.maxSize * 1024 * 1024) {
// 限制图片大小
message.warning(`请选择小于${UploadConfig.maxSize}的文件`)
break
}
await upload(file)
}
canUploadMax 表示允许上传的文件数量 = 允许上传总数 - 当前已经拥有的数量。多余的文件将不会被处理
限制上传图片大小
触发上传回调函数 handlerFileChange中进行文件大小判断,将不满足要求的过滤掉。
for (let i = 0; i < Math.min(files.length, canUploadMax); i++) {
const file = files[i]
if (file.size > UploadConfig.maxSize * 1024 * 1024) {
// 限制图片大小
message.warning(`请选择小于${UploadConfig.maxSize}的文件`)
break
}
await upload(file)
}
UploadConfig.maxSize 表示允许上传的文件大小,大于该数值的文件将不会被处理,而且会有一个警告
列表中展示缩略图,预览展示原图
展示图片的时候 src 使用全局压缩方法 compressImage。这里处理的目的是快速展示组件需要展示的图片和节流
<!-- 展示的图片 -->
<img class="img" :src="compressImage(item.url)" />
支持查看模式
对于详情和编辑共用一个页面的情况下,查看模式就很合适
通过父组件传入的 viewFlag 判断是否使用查看模式,默认不使用查看模式。如果是查看模式的话就隐藏上传按钮和删除按钮
const props = defineProps({
// 查看模式
viewFlag: {
type: Boolean,
default: false,
},
})