注意:此处使用vuedraggable-es 而不是vuedraggable
或vue.draggable.next
官方版本,因为vuedraggable
与antd vue3
会存在不可预知的bug问题,
报错如下:
TypeError: isFunction is not a function in vue3 esm environment
所以我们采用另一位大佬封装的vuedraggable-es
库
组件如下:
getAssetsFile()
方法是根据图片名称assets
文件中的获取静态资源图片
<template>
<div class="component-upload-image">
<div class="flex items-center">
<Draggable
v-if="fileList.length > 0"
:list="fileList"
:animation="100"
item-key="url"
class="list-group"
:forceFallback="true"
@end="onEnd"
>
<template #item="{ element }">
<div class="items">
<img
class="img"
:src="getAssetsFile('loading.gif')"
:onload="onLoadImg(element.url)"
@error.once="useDefaultImage"
/>
<div class="img-mask">
<a-space>
<c-icon type="EyeOutlined" @click="handlePreview(element)" />
<c-icon type="DeleteOutlined" @click="removeFn(element)" />
a-space>
div>
div>
template>
Draggable>
<a-upload
class="custom-ant-upload"
:multiple="multiple"
:maxCount="count"
:file-list="[]"
:list-type="type"
:before-upload="beforeUpload"
:customRequest="uploadImage"
>
<div v-if="fileList.length < count">
<svg-icon icon-class="icon-uploadFile" :style="{ fontSize: '32px' }" />
div>
a-upload>
div>
<div class="text-xs text-gray-400" v-if="showTip">
<template v-if="fileSize">
大小不超过
<b class="text-red-400">{{ fileSize }}MBb>
template>
<template v-if="fileType">
格式为
<b class="text-red-400">{{ fileType.join('/') }}b>
template>
的文件
div>
<a-modal :visible="previewVisible" :title="previewTitle" width="400px" :footer="null" @cancel="handleCancel">
<img
:src="previewImage"
alt="example"
style="display: block; max-width: 100%; margin: 0 auto"
v-realImg="previewImage"
/>
a-modal>
div>
template>
<script setup>
import Draggable from 'vuedraggable-es';
import { getToken } from '@/utils/auth';
import axios from 'axios';
const props = defineProps({
// 图片数据
listValue: [String, Object, Array],
// 上传列表的内建样式暂只支持card样式, text, picture待定
type: {
type: String,
default: 'picture-card',
},
// 是否支持多文件上传
multiple: {
type: Boolean,
default: false,
},
// 预览标题
previewTitle: {
type: String,
default: '预览',
},
// 数量限制
count: {
type: Number,
default: 5,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
},
// 上传类型
uploadType: {
type: Number,
default: 0,
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ['png', 'jpg', 'jpeg'],
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['change']);
const { proxy } = getCurrentInstance();
const fileList = ref([]);
const uploadList = ref([]);
const number = ref(0);
const previewImage = ref('');
const previewVisible = ref(false);
const fileUrl = ref('');// 图片前缀
const uploadApi = ref('') // 上传的图片服务器地址
const headers = ref({ Authorization: 'Bearer ' + getToken() });
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
watch(
() => props.listValue,
val => {
if (val) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : props.listValue.split(',');
// 然后将数组转为对象数组
fileList.value = list.map(item => {
if (typeof item === 'string') {
if (item.indexOf(fileUrl) === -1) {
item = { name: fileUrl + item, url: fileUrl + item };
} else {
item = { name: item, url: item };
}
}
return item;
});
} else {
fileList.value = [];
return [];
}
},
{ deep: true, immediate: true }
);
/**
* @description: 图片加载中,在加载完成时触发
* @param {*} url 上传的图片地址
* @return {*} 上传的图片地址
*/
const onLoadImg = url => {
return 'this.src=' + '"' + url + '";this.οnlοad=null';
};
/**
* @description: 图片加载失败
* @param {*} event
* @return {*} 图片加载失败默认图片
*/
const useDefaultImage = event => {
event.target.src = proxy.getAssetsFile('loadError.png');
};
/**
* @description: 拖拽结束的事件
* @return {*} 返回图片相对路径
*/
const onEnd = () => {
emit('change', listToString(fileList.value));
};
/**
* @description: 删除按钮
* @param {*} file 删除的文件
* @return {*}
*/
const removeFn = file => {
const findex = fileList.value.map(f => f.url).indexOf(file.url);
if (findex > -1 && uploadList.value.length === number.value) {
fileList.value.splice(findex, 1);
emit('change', listToString(fileList.value));
return false;
}
};
/**
* @description: 上传前状态
* @param {*} file 上传文件
* @return {*}
*/
const beforeUpload = file => {
return new Promise((resolve, reject) => {
let fileExtension = '';
if (file.name.lastIndexOf('.') > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
}
// 文件类型(file.type)
const isTypeOk = props.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
if (!isTypeOk) {
proxy.$modal.error(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`, 3);
return false;
}
// 大小限制(file.size)
const isLt2M = file.size / 1024 / 1024 < props.fileSize;
if (!isLt2M) {
proxy.$modal.error(`图片大小限制${props.fileSize}MB!`, 3);
return reject(false);
}
number.value++;
resolve(isTypeOk, isLt2M);
});
};
/**
* @description: 上传成功
* @param {*} info file fileList event
* @return {*}
*/
const uploadImage = async options => {
const { onSuccess, onError, file, onProgress } = options;
let formData = new FormData();
formData.append('file', file);
formData.append('uploadType', props.uploadType);
onProgress({ percent: 80 });
try {
axios({
url: uploadApi.value,
method: 'post',
headers: headers.value,
data: formData,
}).then(res => {
if (res.status !== 200) {
onError(res?.data?.data, file);
return proxy.$modal.error(`上传失败`, 3);
}
onProgress({ percent: 100 });
onSuccess(res?.data?.data, file);
const { fileName, filePath } = res?.data?.data;
uploadList.value.push({ name: fileName, url: fileUrl.value + filePath });
uploadedSuccessfully();
});
} catch (error) {
console.log(`上传文件出错`, error);
}
};
/**
* @description: 上传结束处理
* @return {*}
*/
const uploadedSuccessfully = () => {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit('change', listToString(fileList.value));
}
};
/**
* @description: 预览图片-开启
* @param {*} file 预览图片
* @return {*}
*/
const handlePreview = file => {
previewImage.value = file.url;
previewVisible.value = true;
};
/**
* @description: 预览弹窗 关闭
* @return {*}
*/
const handleCancel = () => {
previewVisible.value = false;
};
/**
* @description: 对象转成指定字符串分隔
* @param {*} files 转换文件数组
* @param {*} separator 对象转成指定字符串分隔 例如 ","
* @return {*} 指定分隔符 分隔的字符串
*/
const listToString = (files, separator) => {
let list = toRaw(files);
let strs = '';
separator = separator || ',';
for (let i in list) {
if (undefined !== list[i].url && list[i].url.indexOf('blob:') !== 0) {
strs += list[i].url.replace(fileUrl.value, '') + separator;
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
};
script>
<style lang="scss" scoped>
.custom-ant-upload :deep(.ant-upload.ant-upload-select-picture-card) {
width: 64px !important;
height: 64px !important;
margin: 8 !important;
}
.list-group {
display: flex;
padding: 8px;
}
.items {
position: relative;
width: 64px;
height: 64px;
margin: 0 8px 8px 0;
border-radius: 8px;
}
.img {
width: 64px;
height: 64px;
border-radius: 8px;
}
.img-mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
border-radius: 8px;
background: rgba(0, 0, 0, 0.5);
cursor: pointer;
opacity: 0;
transition: opacity 0.3s;
}
.img-mask:hover {
opacity: 1;
}
style>