vue 上传图片到腾讯云对象存储
- 1、 引入cos-js-sdk-v5
- 2、封装`uploadcos.js`
- 3、封装图片上传组件、调用上传方法
- 4、页面使用组件
之前总结过 vue
封装图片上传组件到腾讯云对象存储,后来又加了一些功能,在图片过大时进行压缩,压缩完成之后,再上传到腾讯云对象存储;并且,对上传方法进行了优化,所以重新记录一下。
1、 引入cos-js-sdk-v5
安装 JavaScript SDK
:
npm install cos-js-sdk-v5
安装成功后会有如下信息:
2、封装uploadcos.js
新建文件uploadcos.js
,封装上传文件方法。
/**
* 本文件为腾讯云对象存储相关工具类方法
注意:桶的访问权限需要设置指定域名(不然会出现跨域问题),现在设置允许访问的域名是:
http://localhost:8080 https://xxx.com.cn/
所以本地调试时,需要用http://localhost:8080,不可用其他端口。
跨域配置:
桶:指定域名 + 指定子账号能上传;外部不能访问,统一通过cdn访问;
CDN:设置为无跨域限制
---- COD自主诊断工具:https://cloud.tencent.com/login?s_url=https%3A%2F%2Fconsole.cloud.tencent.com%2Fcos5%2Fdiagnose -----
*/
// https://cloud.tencent.com/document/product/436/11459
import COS from 'cos-js-sdk-v5'
import { Message } from 'element-ui'
import { getCOSSecretKey } from '@/api/index'
// 存储桶所在地域
const BUCKET_REGION = 'ap-beijing'
// 使用分片上传阈值10(M)
const SLICE_SIZE = 10
const BUCKET_TYPE_CONFIG = {
video: 'video-b-123456',
image: 'image-b-123456',
file: 'file-b-123456'
}
const BUCKET_DOMAIN = {
video: 'https://abcd-video.xxx.com.cn',
image: 'https://abcd-image.xxx.com.cn'
}
const FOLDER_PATH_NAME = {
// 内容图片
ART: {
prod: '/art/',
test: '/test/art/'
},
// 日常活动图片
ACT: {
prod: '/act/',
test: '/test/act/'
},
// 产品图片
WARE: {
prod: '/ware/',
test: '/test/ware/'
},
// 广告&宣传图片
ADV: {
prod: '/adv/',
test: '/test/adv/'
}
}
/**
* options @param {Object}
* sliceSize:使用切片上传阈值 默认10(M)
* bucketType:桶类型 video,image,file 三种类型
* busiType:业务类型
* needLaoding:是否需要loading遮罩层
* bucketEnv:桶的环境 测试、生产
* bucketName:桶的名称
* bucketDomain:桶的域名 用来拼接key
* bucketPrefix:自定义桶地址前缀片段
* credentials:后台返回凭证信息
* keyBackData:后台返回密钥信息 为credentials父级
*/
class Cos {
constructor(options) {
this.bucketEnv =
window.msBaseUrl === 'https://xxx.com.cn/' ? 'prod' : 'test'
this.bucketType = options?.bucketType || BUCKET_TYPE_CONFIG.file
this.bucketName = BUCKET_TYPE_CONFIG[this.bucketType]
this.bucketDomain = BUCKET_DOMAIN[this.bucketType]
this.sliceSize = options?.sliceSize || SLICE_SIZE
this.busiType = options?.busiType || 'ART'
this.bucketPrefix = FOLDER_PATH_NAME[this.busiType][this.bucketEnv]
this.credentials = null
this.keyBackData = null
}
/**
* 获取密钥
* @returns Object
*/
async getKey () {
try {
const res = await getCOSSecretKey({
bucket: this.bucketName
})
if (
res?.result?.credentials &&
Object.keys(res?.result?.credentials).length
) {
this.keyBackData = res.result
this.credentials = res.result.credentials
return this.credentials
}
return null
} catch (error) {
return null
}
}
/**
* 生成上传资源名称 6位随机数+uid+文件名
*/
generateKey (file) {
const timeStamp = file.uid ? file.uid : new Date().getTime() + ''
const suffix = file.name.split('.')[file.name.split('.').length - 1]
return `${this.randomString()}_${timeStamp}.${suffix}`
}
/**
* 获取随机数
* @param {*} len
* @returns
*/
randomString (len = 6) {
const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz0123456789'
const maxPos = chars.length
let res = ''
for (let i = 0; i < len; i++) {
res += chars.charAt(Math.floor(Math.random() * maxPos))
}
return res
}
/**
* 创建COS对象实例
* @returns Object
*/
async getCosInstance () {
const getKey = await this.getKey()
if (getKey) {
const { tmpSecretId, tmpSecretKey, sessionToken } = this.credentials
const { startTime, expiredTime } = this.keyBackData
const params = {
TmpSecretId: tmpSecretId,
TmpSecretKey: tmpSecretKey,
SecurityToken: sessionToken,
StartTime: startTime,
ExpiredTime: expiredTime
}
const _cos = new COS({
getAuthorization: function (options, callback) {
callback(params)
}
})
return _cos
}
return null
}
/**
* 单个文件上传到腾讯云cos
* @param {*} file
* @returns
*/
async uploadHandle (file) {
const cos = await this.getCosInstance()
if (cos) {
const KEY = `${this.bucketPrefix}${this.generateKey(
file
)}`
console.log('KEY', KEY)
return new Promise((resolve, reject) => {
// if (this.needLoading) {
// var loadingInstance = Loading.service({ fullscreen: true })
// }
cos.uploadFile(
{
Bucket: this.bucketName /* 填入您自己的存储桶,必须字段 */,
Region: BUCKET_REGION /* 存储桶所在地域,例如ap-beijing,必须字段 */,
Key: KEY /* 存储在桶里的对象键(例如1.jpg,a/b/test.txt),必须字段 */,
Body: file /* 必须,上传文件对象,可以是input[type="file"]标签选择本地文件后得到的file对象 */,
SliceSize:
1024 *
1024 *
this
.sliceSize /* 触发分块上传的阈值,超过5MB使用分块上传,非必须 */,
onTaskReady: function (taskId) {
/* 非必须 */
// console.log(taskId)
},
onProgress: function (progressData) {
/* 非必须 */
// console.log(JSON.stringify(progressData))
const percent = parseInt(progressData.percent * 10000) / 100
const speed =
parseInt((progressData.speed / 1024 / 1024) * 100) / 100
console.log('进度:' + percent + '%; 速度:' + speed + 'Mb/s;')
},
onFileFinish: function (err, data, options) {
/* 非必须 */
console.log(options.Key + '上传' + (err ? '失败' : '完成'))
}
},
(err, data) => {
// loadingInstance && loadingInstance.close()
if (err) {
Message.error(err)
reject(err)
}
const url = `${this.bucketDomain}${KEY}`
if (this.bucketType === 'video') {
const fileName = file.name || ''
const name = fileName.split('.').slice(0, fileName.split('.').length - 1).join('.') // 获取文件名称
resolve({ url, name })
} else {
resolve(url)
}
}
)
})
}
}
/**
* 媒体信息接口
*/
async getMediaInfoHandle (key) {
const cos = await this.getCosInstance()
if (cos) {
return new Promise((resolve, reject) => {
cos.request(
{
Bucket: this.bucketName /* 填入您自己的存储桶,必须字段 */,
Region: BUCKET_REGION /* 存储桶所在地域,例如ap-beijing,必须字段 */,
Method: 'GET',
Key: key /* 存储桶内的媒体文件,必须字段 */,
Query: {
'ci-process': 'videoinfo' /** 固定值,必须 */
}
},
function (err, data) {
if (err) {
Message.error(err)
reject(err)
}
resolve(data)
}
)
})
}
}
}
export default Cos
3、封装图片上传组件、调用上传方法
新建image-upload.vue
封装图片上传组件,调用上传方法:
<template>
<div class="common-image-upload-cos-container">
<div class="image-upload-cos-content" :class="{'limit-num': fileList.length>=limit, 'mini': size === 'small'}">
<el-upload ref="upload" :file-list="fileList" list-type="picture-card" action="#" :http-request="uploadImageHandle" v-loading="uploadLoading" :on-preview="handlePictureCardPreview" :on-remove="handleRemove" :on-exceed="exceedTips" :on-success="handeSuccess" :before-upload="beforeAvatarUpload" :on-change="onChangeHandle">
<i class="el-icon-plus"></i>
<p class="el-upload__tip" slot="tip" v-if="tips">{{tips}}</p>
<div slot="file" slot-scope="{file}" class="img-con">
<img crossorigin 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="handleRemove(file)" v-if="!disabled">
<i class="el-icon-delete"></i>
</span>
<span v-if="size === 'small' && !disabled" style="display:block;marginLeft:0px" @click="onChangeHandle(file)">
<i class="el-icon-edit"></i>
</span>
<span v-if="size !== 'small' && !disabled" @click="onChangeHandle(file)">
<i class="el-icon-edit"></i>
</span>
</span>
</div>
</el-upload>
</div>
<div class="img-preview-dialo">
<el-dialog :visible.sync="dialogVisibleShow" :append-to-body="append_to_body" :modal-append-to-body="modal_append_to_body">
<img style="width:100%" :src="dialogImageUrl" crossorigin alt>
</el-dialog>
</div>
</div>
</template>
<script>
import Cos from '@/utils/uploadcos'
import ImageCompressor from '@/assets/js/image-compressor.min'
export default {
// 上传图片到腾讯云对象存储
name: 'ImageUpload',
componentName: 'ImageUpload',
data () {
return {
uploadLoading: false,
imgWidth: 0,
imgHeight: 0,
picIndex: -1,
dialogImageUrl: '',
dialogVisibleShow: false,
fileList: [],
vmodelType: '',
cos: new Cos({
busiType: this.busiType,
bucketType: this.bucketType
})
}
},
props: {
// 接收 String, Array类型,默认为 String 类型
value: {
type: [String, Array],
default: ''
},
tips: {
type: String,
default: ''
},
size: {
type: String,
default: 'medium' // small
},
limit: {
// 限制上传图片张数
type: Number,
default: 1
},
limitSize: {
// 限制上传图片大小
type: Number,
default: 10
},
valueType: {
type: String,
default: 'String' // Object
},
bucketType: {
type: String,
default: 'image'
},
// 是否校验图片尺寸,默认不校验
isCheckPicSize: {
type: Boolean,
default: false
},
checkWidth: {
type: Number,
default: 0 // 图片限制宽度
},
checkHeight: {
type: Number,
default: 0 // 图片限制高度
},
topLimitWidth: {
type: Number,
default: 0 // 图片限制宽度上限(有时需要校验上传图片宽度在一个范围内)
},
topLimitHeight: {
type: Number,
default: 0 // 图片限制高度上限(有时需要校验上传图片高度在一个范围内)
},
index: {
type: Number,
default: -1 // 当前图片index,限制可以上传多张时,针对某一张进行操作,需要知道当前的index
},
limitType: {
type: String,
default: '' // (限制上传图片格式)传入格式:png,jpg,gif png,jpg,webp png,jpg,gif,webp
},
busiType: {
type: String,
default: 'ART'
},
// 禁用开关
disabled: {
type: Boolean,
default: false
},
isGzip: {
// 是否压缩图片,默认不压缩(false);传入 true 时,图片大小大于80KB且不是gif格式时进行压缩
type: Boolean,
default: false
},
append_to_body: {
type: Boolean,
default: false
},
modal_append_to_body: {
type: Boolean,
default: true
}
},
components: {},
created () {
if (this.valueType === 'Object') {
this.vmodelType = 'array'
}
if (this.value) {
this.modifyValue()
}
},
watch: {
value: {
deep: true,
handler: function (val, oldVal) {
if (val) {
this.modifyValue()
} else {
this.fileList = []
}
}
}
},
methods: {
findItem (uid) {
this.fileList.forEach((ele, i) => {
if (uid === ele.uid) {
this.picIndex = i
}
})
},
onChangeHandle (file, fileList) {
// console.log('onChangeHandle file, fileList', file, fileList)
this.findItem(file.uid)
this.$refs.upload.$refs['upload-inner'].handleClick()
},
handleRemove (file) {
// console.log('handleRemove file', file)
this.findItem(file.uid)
this.fileList.splice(this.picIndex, 1)
this.exportImg()
},
exportImg () {
if (this.fileList.length !== 0) {
if (this.imgWidth && this.imgHeight) {
if (this.valueType === 'Object') {
const imgs = this.fileList.map(item => {
return {
url: item.url,
name: item.fileName ? item.fileName : item.name
}
})
this.$emit('input', imgs)
this.$emit('imgChange', this.index)
} else {
if (this.vmodelType === 'array') {
const imgs = this.fileList.map(item => {
if (item.url) {
return item.url
}
})
this.$emit('input', imgs)
this.$emit('imgChange', this.index)
} else {
const resUrl = this.fileList[0].url
this.$emit('input', resUrl)
this.$emit('imgChange', this.index)
}
}
} else {
this.$message.error('当前未获取到图片宽高数据,请重新上传图片!')
}
} else {
this.$emit('input', '')
this.$emit('imgChange', this.index)
}
this.picIndex = -1
},
uploadImageHandle (file) {
if (this.vmodelType === 'string') {
if (this.picIndex !== -1 && this.fileList.length) {
// 如果是单图编辑替换状态,手动删除被替换的文件
this.fileList.splice(this.picIndex, 1)
}
}
// console.log('uploadImageHandle file', file)
// return
// 不需要压缩、图片小于80KB或是gif图,不压缩直接上传;大于80KB,压缩图片后上传
const uploadFile = file.file
if ((!this.isGzip) || (uploadFile.size / 1024 < 80) || (uploadFile.type == 'image/gif')) {
this.uploadToCos(uploadFile)
} else {
let file = uploadFile
if (!file) {
return
}
var options = {
file: file,
quality: 0.6,
mimeType: 'image/jpeg',
maxWidth: 6000,
maxHeight: 6000,
// width: 1000, // 指定压缩图片宽度
// height: 1000, // 指定压缩图片高度
minWidth: 10,
minHeight: 10,
convertSize: Infinity,
loose: true,
redressOrientation: true,
// 压缩前回调
beforeCompress: (result) => {
console.log('压缩之前图片尺寸大小: ', result.size / 1024)
// console.log('mime 类型: ', result.type);
},
// 压缩成功回调
success: (result) => {
console.log(result);
console.log('压缩之后图片尺寸大小: ', result.size / 1024)
console.log('实际压缩率: ', ((file.size - result.size) / file.size * 100).toFixed(2) + '%');
this.uploadToCos(result)
},
// 发生错误
error: (msg) => {
console.error(msg);
this.$message.error(msg)
}
};
/* eslint-disable no-new */
new ImageCompressor(options)
}
},
// 上传文件
uploadToCos (file) {
// console.log('uploadToCos uploadFile', file)
// return
this.uploadLoading = true
this.cos.uploadHandle(file).then((url) => {
if (url) {
if (this.imgWidth && this.imgHeight) {
const resUrl = url + '?width=' + this.imgWidth + '&height=' + this.imgHeight
const obj = { url: resUrl, name: file.name }
if (this.picIndex < 0) {
this.fileList.push(obj)
} else {
this.fileList[this.picIndex] = obj
}
this.exportImg()
} else {
this.$message.error('当前未获取到图片宽高数据,请重新上传图片!')
}
} else {
this.fileList.splice(this.picIndex, 1)
}
this.uploadLoading = false
})
},
modifyValue () {
if (this.valueType === 'Object') {
this.fileList = this.value.map(item => ({
url: item.url,
name: item.name
}))
} else {
// 判断是否是String
const str = this.value
const res = ((str instanceof String) || (typeof str).toLowerCase() === 'string')
if (res === true) {
this.vmodelType = 'string'
} else {
this.vmodelType = 'array'
}
if (this.vmodelType === 'array') {
this.fileList = this.value.map(item => ({ url: item }))
} else {
this.fileList = [{ url: this.value }]
}
}
},
beforeAvatarUpload (file) {
const imgType = file.type
const isLtSize = file.size / 1024 / 1024 < this.limitSize
const TYPE_ALL = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp']
let isType = true
// console.log('this.limitType', this.limitType)
// console.log('imgType', imgType)
if (this.limitType) {
const limitTypeArr = this.limitType.split(',')
const limutTypeFlagArr = []
const IMG_STATUS = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp'
}
limitTypeArr.forEach(item => {
if (IMG_STATUS[item]) limutTypeFlagArr.push(IMG_STATUS[item])
})
if (limutTypeFlagArr.indexOf(imgType) === -1) {
isType = false
this.$message.error(`仅支持上传 ${this.limitType} 格式的图片!`)
}
} else {
// 默认情况,未传入校验类型格式,则默认可以接受全部格式
if (TYPE_ALL.indexOf(imgType) === -1) {
isType = false
this.$message.error('仅支持上传 jpg、png、jpeg、webp、gif 格式的图片!')
}
}
if (!isLtSize) {
this.$message.error(`上传图片大小不能超过${this.limitSize}MB!`)
}
if (this.isCheckPicSize === true) {
const width = this.checkWidth
const height = this.checkHeight
const topWidth = this.topLimitWidth
const topHeight = this.topLimitHeight
const that = this
const isSize = new Promise((resolve, reject) => {
// console.log('Promise')
// window对象,将blob或file读取成一个url
const _URL = window.URL || window.webkitURL
const img = new Image()
img.onload = () => { // image对象的onload事件,当图片加载完成后执行的函数
// console.log('img.onload')
that.imgWidth = img.width
that.imgHeight = img.height
if (width && height) { // 校验图片的宽度和高度
let valid = false
if (topWidth && topHeight) {
// 校验图片宽度和高度范围
valid = ((width <= img.width) && (img.width <= topWidth)) && ((height <= img.height) && (img.height <= topHeight))
} else if (topHeight) {
// 校验图片高度范围
valid = img.width === width && ((height <= img.height) && (img.height <= topHeight))
} else if (topWidth) {
// 校验图片宽度范围
valid = ((width <= img.width) && (img.width <= topWidth)) && img.height === height
} else {
// 校验图片宽度、高度固定值
valid = img.width === width && height === img.height
}
valid ? resolve() : reject(new Error('error'))
} else if (width) { // 只校验图片的宽度
let valid = false
if (topWidth) {
// 校验图片宽度范围
valid = (width <= img.width) && (img.width <= topWidth)
} else {
// 校验图片宽度固定值
valid = img.width === width
}
valid ? resolve() : reject(new Error('error'))
} if (height) { // 只校验图片的高度
let valid = false
if (topHeight) {
// 校验图片高度范围
valid = (height <= img.height) && (img.height <= topHeight)
} else {
// 校验图片高度固定值
valid = img.height === height
}
valid ? resolve() : reject(new Error('error'))
}
}
img.src = _URL.createObjectURL(file)
}).then(() => {
// console.log('then')
return file
}, () => {
// console.log('reject')
let text = ''
if (width && height) {
if (topWidth && topHeight) {
text = `图片尺寸限制为:宽度${width}~${topWidth}px,高度${height}~${topHeight}px!`
} else if (topHeight) {
text = `图片尺寸限制为:宽度${width}px,高度${height}~${topHeight}px!`
} else if (topWidth) {
text = `图片尺寸限制为:宽度${width}~${topWidth}px,高度${height}px!`
} else {
text = `图片尺寸限制为:宽度${width}px,高度${height}px!`
}
} else if (width) {
if (topWidth) {
text = `图片尺寸限制为:宽度${width}~${topWidth}px!`
} else {
text = `图片尺寸限制为:宽度${width}px!`
}
} else if (height) {
if (topHeight) {
text = `图片尺寸限制为:高度${height}~${topHeight}px!`
} else {
text = `图片尺寸限制为:高度${height}px!`
}
}
this.$message.error(text)
return Promise.reject(new Error('error'))
})
return isType && isLtSize && isSize
} else {
// window对象,将blob或file读取成一个url
const _URL = window.URL || window.webkitURL
const img = new Image()
img.onload = () => { // image对象的onload事件,当图片加载完成后执行的函数
this.imgWidth = img.width
this.imgHeight = img.height
}
img.src = _URL.createObjectURL(file)
return isType && isLtSize
}
},
handlePictureCardPreview (file) {
this.dialogImageUrl = file.url
this.dialogVisibleShow = true
},
exceedTips (file, fileList) {
this.$message(`最多上传${fileList.length}个文件!`)
},
handeSuccess (res, file, fileList) {
console.log('handeSuccess')
}
}
}
</script>
<style lang='less'>
@small-size: 80px;
.common-image-upload-cos-container {
.el-dialog {
background: transparent;
-webkit-box-shadow: none;
box-shadow: none;
margin-top: 0 !important;
width: auto;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
.el-dialog__header {
display: none;
}
.el-dialog__body {
text-align: center;
img {
width: auto;
max-width: 1000px;
}
}
}
}
.image-upload-cos-content&&.limit-num {
.el-upload--picture-card {
display: none !important;
}
}
.image-upload-cos-content&&.mini {
.el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.el-upload-list__item {
width: @small-size;
height: @small-size;
text-align: center;
/*去除upload组件过渡效果*/
transition: none !important;
}
.el-upload--picture-card {
width: @small-size;
height: @small-size;
line-height: @small-size;
text-align: center;
}
}
.el-upload-list__item&&.is-success {
.img-con {
width: 100%;
height: 100%;
}
}
</style>
action
:上传地址,必填,直接上传后端接口时填入接口链接;自定义上传方法使用 http-request
,所以,填任意字符串即可,没有实际意义。
http-request
:覆盖默认上传行为,这里自定义上传行为。
4、页面使用组件
<template>
<div>
<img-upload v-model="bgImg" :isGzip="true" :size="'small'" :tips="'建议图片宽度为350,JPG、JPGE、PNG 小于5M,仅限上传一张'" :limit="1" :limitSize="5"></img-upload>
<img-upload v-model="mainImg" tips="宽度750px,高度范围:170px~1334px" :isCheckPicSize="true" :checkWidth="750" :checkHeight="170" :topLimitHeight="1334" :limit="1" size="small"></img-upload>
<img-upload v-model="imgArr" :tips="'多图数组'" :limit="6" :limitSize="1"></img-upload>
<img-upload v-model="imgList" :tips="'多图数组'" :limit="6" :limitSize="1" valueType="Object"></img-upload>
</div>
</template>
<script>
import ImgUpload from '@/components/image-upload'
export default {
name: 'demo',
components: {
ImgUpload
},
data () {
return {
bgImg: '',
mainImg: '',
imgArr: [ 'https://xxx', 'https://xxx']
imgList: [{
name: '1.jpg',
url: 'https://xxx'
}]
}
},
props: {
},
watch: {
},
created () {
},
mounted () {
},
methods: {
}
}
</script>
上传大小小于 80M 的图片,过程中打印进度信息如下:
上传大小大于 80M 的图片,过程中打印进度信息如下: