最近做 vue2 项目,用到了 element-ui,其中有个需求是上传多张图片到腾讯云 COS,这里记录一下实现过程。
大致效果如下:
使用方式
这里外层是一个el-form,增删行是其中一个el-form-item,对应formData的一个字段,代码如下所示:
<el-form :model="formData">
<el-form-item label="最低版本号">
<UploadImage v-model="formData.urls" :limit="3"/>
</el-form-item>
<!-- 其他el-form-item -->
</el-form>
UploadImage的组件实现
UploadImage的组件逻辑:
- 使用的是
el-upload组件,核心的数据是fileList,其数据结构最重要的是[{url:'https://xxx.png, status:'success'}],status 的状态需要手动更换。设置自定义上传,新增、删除、上传都会触发fileList的变化,然后通过$emit('input', this.fileList.map(item=>item.url))将fileList传递给父组件。 value:父组件传入的值,即formData.urls,需要在created生命周期中初始化根据 value 来初始化fileList,其实就是value.map(url=>({url,status:'success'}))。- 这里因为需要额外实现图片预览,所以还有一个
el-dialog,点击图片预览,关闭图片预览,不需要的话可以去掉,如果 - 属性解读,
v-bind="this.$attrs",v-on="this.$listeners",这个是为了将父组件传递的属性和事件传递给el-upload,这样就可以使用el-upload的所有属性和事件了。 beforeUpload:上传前的校验,这里只是校验是否是图片,可以根据实际情况来校验。upload:上传文件,这里是上传到腾讯云 COS,需要调用腾讯云 COS 的接口,这里只是简单的实现,具体的上传逻辑可以根据实际情况来实现。onAdd、onRemove:新增、删除文件,这里是同步fileList,然后通过$emit('input', this.fileList.map(item=>item.url))将fileList传递给父组件。handlePictureCardPreview:图片预览,点击图片预览,打开el-dialog,关闭图片预览,关闭el-dialog。onExceed:超出文件个数限制,这里只是简单的提示,可以根据实际情况来实现。
正常情况,其实内部用 innerValue,然后 computed 计算属性,get 和 set,这样就实现了双向绑定。但是因为 fileList 一旦引用发生变化,el-upload 会重新渲染,所以这里直接用 value,然后在 created 中初始化 fileList。innerValue 的用法可以参考这里的组件封装
以下是UploadImage的组件实现:
<template>
<div>
<el-upload
multiple
accept="image/*"
class="upload-img"
action="#"
:http-request="upload"
:show-file-list="true"
list-type="picture-card"
:file-list="fileList"
:on-change="onAdd"
:on-remove="onRemove"
:on-exceed="onExceed"
:before-upload="beforeUpload"
:limit="limit"
v-bind="this.$attrs"
v-on="this.$listeners"
>
<i slot="default" class="el-icon-plus"></i>
</el-upload>
</div>
</template>
<script>
import { uploadFileToCos } from './utils';
import { getCosConfig } from './service.js';
export default {
name: 'UploadImg',
model: {
prop: 'value', // 默认为 'value'
event: 'input', // 默认为 'input'
},
props: {
value: {
type: Array,
default() {
return [];
},
},
},
data() {
return {
fileList: (this.value || []).map((url) => ({ status: 'success', url })),
// 这个太常用了,直接写在data里面
limit: this.$attrs.limit || 1,
};
},
methods: {
/** 新增同步fileList */
onAdd(file, fileList) {
this.fileList = fileList;
this.$emit(
'input',
this.fileList.map((item) => item.url)
);
},
/** 删除同步fileList */
onRemove(file, fileList) {
this.fileList = fileList.filter((item) => item.url !== file.url);
this.$emit(
'input',
this.fileList.map((item) => item.url)
);
},
/** 上传同步fileList */
async upload(res) {
if (res.file) {
// 这里声明状态,fileList能同步更新
res.file.status = 'loading';
const url = await uploadFileToCos(res.file, getCosConfig);
res.file.status = 'success';
res.file.url = url;
// 更新fileList的url
this.fileList.find((item) => item.uid === res.file.uid).url = url;
// 需要向外
this.$emit(
'input',
this.fileList.map((item) => item.url)
);
}
},
onExceed(files, fileList) {
this.$message.error(
`限制 ${this.limit} 个文件,本次最多能选择 ${
this.limit - fileList.length
} 个文件`
);
},
beforeUpload(file) {
const isImg = file.type.startsWith('image/');
// const isLt = file.size / 1024 / 1024 < 2
if (!isImg) {
this.$message.error('上传只能是 图片格式!');
}
// if (!isLt) {
// this.$message.error("上传头像图片大小不能超过 2MB!")
// }
return isImg;
},
},
};
</script>
<style scoped lang="less">
/** 调整添加卡片大小 item是展示卡片大小*/
.upload-img /deep/.el-upload--picture-card,
.upload-img /deep/.el-upload-list--picture-card .el-upload-list__item {
width: 100px;
height: 100px;
}
/** line-height是调整加号位置 */
.upload-img /deep/.el-upload--picture-card {
line-height: 100px;
}
</style>
utils.js
import COS from 'cos-js-sdk-v5';
import uuid from 'react-uuid';
/**
* 上传文件到腾讯云COS
* @param {File} file 文件
* @param {Function} getCosConfig 获取cos配置,大部分时候,上传文件需要一些配置,这里使用了一个异步函数获取cos配置,这边使用临时秘钥,所以每次请求,如果不是临时秘钥,可以直接传入配置.这里是一个异步函数返回Promise对象: bucketName, region, TmpSecretId, TmpSecretKey, XCosSecurityToken, StartTime, ExpiredTime,
* @param {String} path 上传路径
* @returns {String} 文件url
* @example
* const url = await uploadFileToCos(file, getCosConfig, path)
* console.log(url)
*/
export async function uploadFileToCos(file, getCosConfig, path = '/App') {
//
// 获取cos配置
const cosConfig = await getCosConfig();
const cos = new COS({
getAuthorization: (options, callback) => {
callback(cosConfig);
},
});
const params = {
Bucket: cosConfig.bucketName,
Region: cosConfig.region,
// Key 可以理解是文件的路径,这里我使用了一个带有uuid的文件名
Key: `${path}/${addUuidToName(file.name)}`,
Body: file,
onProgress: function (progressData) {
const { loaded, total } = progressData;
console.log('上传进度', { loaded, total });
},
};
const url = await putObject(cos, params);
return url;
}
export function putObject(cos, params) {
return new Promise((resolve, reject) => {
cos.putObject(params, function (err, data) {
if (err) {
console.error('上传失败', err);
reject(err);
}
console.log('上传成功', data.Location);
const url = `https://${data.Location}`;
resolve(url);
});
});
}
/**
*
* @param {*} filename
* @returns string
* @example
* addUuidToName('test.png') // test_6e1fa314-96ef-d686-104c-4b6a67649483.png
*/
function addUuidToName(filename) {
const dotIndex = filename.lastIndexOf('.');
if (dotIndex === -1) {
return `${filename}_${uuid()}`;
}
const base = filename.slice(0, dotIndex);
const suffix = filename.slice(dotIndex);
return `${base}_${uuid()}${suffix}`;
}
图片超过限制不显示加号
如果图片超过限制,不显示加号,那么需要在el-upload加上类名判断:
<el-upload
:class="{ 'upload-img': true, 'is-enough': fileList.length >= limit }"
>
<!-- css部分
.is-enough /deep/.el-upload--picture-card {
display: none;
}
--></el-upload
>
想要加预览功能的话
默认的缩略图不支持预览,一方面需要 slot 重新设计操作图标,一方面需要加预览弹框,这里使用el-dialog,点击图片预览,关闭图片预览,代码如下所示:
<template>
<div>
<el-upload ...>
<i slot="default" class="el-icon-plus"></i>
<div slot="file" slot-scope="{ file }">
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions">
<span
class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)"
>
<i class="el-icon-zoom-in"></i>
</span>
<span
class="el-upload-list__item-delete"
@click="onRemove(file, fileList)"
>
<i class="el-icon-delete"></i>
</span>
</span>
</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
</div>
</template>
<script>
/**
* 目前最大的问题是 重新赋值数组 会一抖一抖的 所以并没有检测外围的value变化,只是内部值发生变化,同步外围数据
*/
import { uploadFileToCos } from './utils';
import { getCosConfig } from './service.js';
export default {
name: 'UploadImg',
// ...
data() {
return {
dialogImageUrl: '',
dialogVisible: false,
// ...
};
},
methods: {
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url;
this.dialogVisible = true;
},
// ...
},
};
</script>
<style scoped lang="less"></style>
上面所有功能的的代码
<template>
<div>
<el-upload
multiple
accept="image/*"
:class="{ 'upload-img': true, 'is-enough': fileList.length >= limit }"
action="#"
:http-request="upload"
:show-file-list="true"
list-type="picture-card"
:file-list="fileList"
:on-change="onAdd"
:on-remove="onRemove"
:on-exceed="onExceed"
:limit="limit"
:before-upload="beforeUpload"
v-bind="this.$attrs"
v-on="this.$listeners"
>
<i slot="default" class="el-icon-plus"></i>
<div slot="file" slot-scope="{ file }">
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions">
<span
class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)"
>
<i class="el-icon-zoom-in"></i>
</span>
<span
v-if="!disabled"
class="el-upload-list__item-delete"
@click="onRemove(file, fileList)"
>
<i class="el-icon-delete"></i>
</span>
</span>
</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
</div>
</template>
<script>
/**
* 目前最大的问题是 重新赋值数组 会一抖一抖的 所以并没有检测外围的value变化,只是内部值发生变化,同步外围数据
*/
import { uploadFileToCos } from './utils';
import { getCosConfig } from './service.js';
function formatFileListToUrlList(fileList) {
return fileList.map((item) => item.url);
}
export default {
name: 'UploadImg',
model: {
prop: 'value', // 默认为 'value'
event: 'input', // 默认为 'input'
},
props: {
value: {
type: Array,
default() {
return [];
},
},
},
data() {
return {
dialogImageUrl: '',
dialogVisible: false,
disabled: false,
fileList: (this.value || []).map((url) => ({ status: 'success', url })),
limit: this.$attrs.limit || 1,
};
},
methods: {
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url;
this.dialogVisible = true;
},
/** 新增同步fileList */
onAdd(file, fileList) {
console.log(file, fileList);
this.fileList = fileList;
this.$emit('input', formatFileListToUrlList(fileList));
},
/** 删除同步fileList */
onRemove(file, fileList) {
console.log('remove', file, fileList);
this.fileList = fileList.filter((item) => item.url !== file.url);
this.$emit('input', formatFileListToUrlList(this.fileList));
},
async upload(res) {
if (res.file) {
// 这里声明状态,fileList能同步更新
res.file.status = 'loading';
const url = await uploadFileToCos(res.file, getCosConfig);
res.file.status = 'success';
res.file.url = url;
this.fileList.find((item) => item.uid === res.file.uid).url = url;
// 需要向外
this.$emit('input', formatFileListToUrlList(this.fileList));
}
},
onExceed(files, fileList) {
this.$message.error(
`限制 ${this.limit} 个文件,本次最多能选择 ${
this.limit - fileList.length
} 个文件`
);
},
beforeUpload(file) {
const isImg = file.type.startsWith('image/');
// const isLt = file.size / 1024 / 1024 < 2
if (!isImg) {
this.$message.error('上传只能是 图片格式!');
}
// if (!isLt) {
// this.$message.error("上传头像图片大小不能超过 2MB!")
// }
return isImg;
},
},
};
</script>
<style scoped lang="less">
/** 调整添加卡片大小 item是展示卡片大小*/
.upload-img {
&.is-enough {
/deep/.el-upload--picture-card {
display: none;
}
}
/deep/.el-upload--picture-card,
/deep/.el-upload-list--picture-card .el-upload-list__item {
width: 100px;
height: 100px;
}
/** line-height是调整加号位置 */
/deep/.el-upload--picture-card {
line-height: 100px;
}
}
</style>
我这边限制10张图片,效果如下