1.创建src/utils/upload-file.js文件
import COS from 'cos-js-sdk-v5'
import SparkMD5 from 'spark-md5'
import { cosTmpsecret, cosConfig } from '@/api/upload' // 通过后台获取临时密钥
let key = ''
// 配置
// const cosConfig = {
// // Bucket: 'xlcp-tong-1253334579',
// // Region: 'ap-nanjing',
// Bucket: 'mytong-1259414159',
// Region: 'ap-nanjing',
// // Domain: 'mytong-1259414159.cos.ap-nanjing.myqcloud.com'
// }
// 初始化实例
/*** 签名计算放在前端会暴露 SecretId 和 SecretKey* 我们把签名计算过程放在后端实现,前端通过 ajax 向后端获取签名结果* 正式部署时请再后端加一层自己网站本身的权限检验。* 异步获取临时密钥* mytong-1259414159.cos.ap-nanjing.myqcloud.com* mytong-1259414159* SecretKey:A3oKYrI2YrsfFX4LL3VgZBXu7HJETRCf* SecretId:AKIDrYy7VdZIqMaXVDQjaE7lFjGoAFJAmMgj* APPID:1259414159**/
let cos = new COS({async getAuthorization(options, callback) {const res = await cosTmpsecret()const authdata = res.dataconst auth = {TmpSecretId: authdata.tmpSecretId,TmpSecretKey: authdata.tmpSecretKey,XCosSecurityToken: authdata.sessionToken,StartTime: authdata.startTime,ExpiredTime: authdata.expiredTime, // 在ExpiredTime时间前,不会再次调用getAuthorization}callback(auth)},FileParallelLimit: 3, // 文件并发数ChunkParallelLimit: 8, // 同一个上传文件的分块并发数ChunkSize: 1024 * 1024 * 8, // 分块上传时,每块的字节数大小
})// var cos1 = new COS({
// SecretId: 'A3oKYrI2YrsfFX4LL3VgZBXu7HJETRCf',
// SecretKey: 'AKIDrYy7VdZIqMaXVDQjaE7lFjGoAFJAmMgj',
// FileParallelLimit: 3, // 文件并发数
// ChunkParallelLimit: 8, // 同一个上传文件的分块并发数
// ChunkSize: 1024 * 1024 * 8 // 分块上传时,每块的字节数大小
// });// 获取cos存储的图片地址,替换为域名地址
export async function getObjectUrl() {let response = await cosConfig()const url = cos.getObjectUrl({Bucket: response.data.bucket,Region: response.data.region,Key: key,Sign: false,}, (err, data) => {})return url// 腾讯云的地址替换为域名地址// const p = `${cosConfig.Bucket}.cos.${cosConfig.Region}.myqcloud.com`// console.log('p:', p)// console.log('url:', url)// .replace(p, cosConfig.Domain)
}
// 下载对象
export function getObject(path, url) {cosConfig().then(response => {console.log('response', response)let arr = url.split('.com')// console.log('arr', arr[1])cos.getObject({Bucket: response.data.bucket,Region: response.data.region,Key: arr[1], /* 必须 */}, (err, data) => {console.log(err || data.Body)})})
}
// 获得文件md5
function getFileMD5(file, callback) {// 声明必要的变量const fileReader = new FileReader()// 文件每块分割2M,计算分割详情const chunkSize = 2 * 1024 * 1024const chunks = Math.ceil(file.size / chunkSize)let currentChunk = 0// 创建md5对象(基于SparkMD5)const spark = new SparkMD5()// 每块文件读取完毕之后的处理fileReader.onload = function (e) {// 每块交由sparkMD5进行计算spark.appendBinary(e.target.result)currentChunk++// 如果文件处理完成计算MD5,如果还有分片继续处理if (currentChunk < chunks) {loadNext()} else {callback(spark.end())}}// 处理单片文件的上传function loadNext() {const start = currentChunk * chunkSizeconst end = start + chunkSize >= file.size ? file.size : start + chunkSizefileReader.readAsBinaryString(file.slice(start, end))}loadNext()
}
// 大文件分片上传-通过sliceUploadFile上传
export function uploadFile(path, file, callback, progressBc) {return new Promise(resolve => {cosConfig().then(response => {// 得到md5码getFileMD5(file, md5 => {// 存储文件的md5码file.md5 = md5const subfix = file.name.substr(file.name.lastIndexOf('.'))key = path + file.md5 + new Date().getSeconds() + subfixcos.sliceUploadFile({Bucket: response.data.bucket,Region: response.data.region,Key: key,Body: file,onProgress(progressData) {progressBc(progressData.percent)},}, async (err, data) => {if (err) {callback(err)resolve(err)} else {data.fid = await getObjectUrl()console.log(' data.fid1111111111', data)callback(null, data)resolve(data)}})})})})
}
// 小文件直接上传-通过putObject上传
export function uploadFile2(path, file, callback, progressBc) {// 得到md5码try {cosConfig().then(response => {getFileMD5(file, md5 => {// 存储文件的md5码file.md5 = md5const subfix = file.name.substr(file.name.lastIndexOf('.'))key = path + file.md5 + new Date().getSeconds() + subfixcos.putObject({Bucket: response.data.bucket,Region: response.data.region,Key: key,Body: file,onProgress(progressData) {progressBc(progressData.percent)},}, async (err, data) => {if (err) {callback(err)} else {data.fid = await getObjectUrl()console.log(' data.fid1111111111', data.fid)callback(null, data)}})})})} catch (error) {console.log(error)}
}
// 文件直接删除-通过deleteObject上传
export function deleteFile(path, file, callback, keyName) {cosConfig().then(response => {console.log('response', response)if (keyName) {let key = path + keyNamecos.deleteObject({Bucket: response.data.bucket,Region: response.data.region,Key: key,}, (err, data) => {if (err) {callback(err)} else {callback(null, data)}})} else {key = path + file.namecos.deleteObject({Bucket: response.data.bucket,Region: response.data.region,Key: key,}, (err, data) => {if (err) {callback(err)} else {callback(null, data)}})}})
}
2.在vue中上传图片使用
import { uploadFile2 } from '@/utils/upload-file'
import dayjs from 'dayjs'const { proxy } = getCurrentInstance()
function uploadImg({ file }) {const TODAY = dayjs().format('YYYY/MM/DD')uploadFile2(`/upload/company-products/${TODAY}/`, file, (err, datas) => {if (err) {number.value--proxy.$modal.closeLoading()proxy.$modal.msgError(datas.msg)proxy.$refs.imageUpload.handleRemove(file)uploadedSuccessfully()this.$message.error(`上传失败:${err}`)} else {uploadList.value.push({ name: datas.fid, url: datas.fid })uploadedSuccessfully()proxy.$modal.msgSuccess('上传成功!')}})
}// 上传结束处理
function 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 = 0emit('update:modelValue', listToString(fileList.value))proxy.$modal.closeLoading()}
}
完整代码:
<template><div class="component-upload-image"><el-uploadmultipleaction="#":http-request="uploadImg"list-type="picture-card":before-upload="handleBeforeUpload":limit="limit":on-error="handleUploadError":on-exceed="handleExceed"ref="imageUpload":before-remove="handleDelete":show-file-list="true":file-list="fileList":on-preview="handlePictureCardPreview":class="{ hide: fileList.length >= limit }"><el-icon class="avatar-uploader-icon"><plus /></el-icon></el-upload><!-- 上传提示 --><div class="el-upload__tip" v-if="showTip">请上传<template v-if="fileSize">大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b></template><template v-if="fileType">格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b></template>的文件</div><el-dialogv-model="dialogVisible"title="预览"width="800px"append-to-body><img:src="dialogImageUrl"style="display: block; max-width: 100%; margin: 0 auto"/></el-dialog></div>
</template><script setup>
// import { getToken } from '@/utils/auth'
import { defineEmits } from 'vue'
import { uploadFile2 } from '@/utils/upload-file'
import dayjs from 'dayjs'const props = defineProps({modelValue: [String, Object, Array],// 图片数量限制limit: {type: Number,default: 5,},// 大小限制(MB)fileSize: {type: Number,default: 5,},// 文件类型, 例如['png', 'jpg', 'jpeg']fileType: {type: Array,default: () => ['png', 'jpg', 'jpeg'],},// 是否显示提示isShowTip: {type: Boolean,default: true,},
})const { proxy } = getCurrentInstance()
const emit = defineEmits(['update:modelValue'])
const number = ref(0)
const uploadList = ref([])
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const fileList = ref([])
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize),
)
watch(() => props.modelValue,(val) => {if (val) {// 首先将值转为数组const list = Array.isArray(val) ? val : props.modelValue.split(',')// 然后将数组转为对象数组fileList.value = list.map((item) => {console.log(item, 'modelValue item')if (typeof item === 'string') {item = { name: item, url: item }}return item})return fileList.value}fileList.value = []return []},{ deep: true, immediate: true },
)
// 使用腾讯云存储图片
function uploadImg({ file }) {const TODAY = dayjs().format('YYYY/MM/DD')uploadFile2(`/upload/company-products/${TODAY}/`, file, (err, data) => {if (err) {number.value--proxy.$modal.closeLoading()proxy.$modal.msgError(data.msg)proxy.$refs.imageUpload.handleRemove(file)uploadedSuccessfully()this.$message.error(`上传失败:${err}`)} else {const imageUrl = `http://${data.Location}`uploadList.value.push({ name: imageUrl, url: imageUrl })proxy.$modal.msgSuccess('上传成功!')uploadedSuccessfully()}})
}
// 上传前loading加载
function handleBeforeUpload(file) {let isImg = falseif (props.fileType.length) {let fileExtension = ''if (file.name.lastIndexOf('.') > -1) {fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)}isImg = props.fileType.some((type) => {if (file.type.indexOf(type) > -1) return trueif (fileExtension && fileExtension.indexOf(type) > -1) return truereturn false})} else {isImg = file.type.indexOf('image') > -1}if (!isImg) {proxy.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`,)return false}if (props.fileSize) {const isLt = file.size / 1024 / 1024 < props.fileSizeif (!isLt) {proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)return false}}proxy.$modal.loading('正在上传图片,请稍候...')number.value++
}// 文件个数超出
function handleExceed() {proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
}// 删除图片
function handleDelete(file) {console.log(file, 'file')const findex = fileList.value.map((f) => f.name).indexOf(file.name)if (findex > -1 && uploadList.value.length === number.value) {fileList.value.splice(findex, 1)emit('update:modelValue', getUrlToString(fileList.value))return false}
}// 上传结束处理
function 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 = 0emit('update:modelValue', getUrlToString(fileList.value))proxy.$modal.closeLoading()}
}// 上传失败
function handleUploadError() {proxy.$modal.msgError('上传图片失败')proxy.$modal.closeLoading()
}// 预览
function handlePictureCardPreview(file) {dialogImageUrl.value = file.urldialogVisible.value = true
}
// 数组转换字符串,用于传参给后端
function getUrlToString(arr = [], separator = ',') {
if (Array.isArray(arr)) {return arr.map(i => i.url).join(separator)
}
return console.error('不是数组')
}</script><style scoped lang="scss">
// .el-upload--picture-card 控制加号部分
:deep(.hide .el-upload--picture-card) {display: none;
}
</style>
3.在富文本使用
// 编辑器配置
const editorConfig = ref({placeholder: computed(() => props.placeholder),MENU_CONF: {uploadImage: {customUpload(file, insertFn) {console.log(file, insertFn, 'upload')const TODAY = dayjs().format('YYYY/MM/DD')uploadFile2(`/upload/company-products/${TODAY}/`, file, (err, data) => {console.log(data, 'datas')console.log(file, 'datas')if (err) {ElMessage({message: `${file.name} 上传失败`,type: 'error',})} else {// 成功后,调用插入图片的函数const imageUrl = `http://${data.Location}`console.log(data.Location, 'image')console.log(imageUrl, 'image')insertFn(imageUrl, '', '')}})},// server: () => {// },// form-data fieldName ,默认值 'wangeditor-uploaded-image'fieldName: 'file',// 单个文件的最大体积限制,默认为 2MmaxFileSize: 5 * 1024 * 1024, // 3M// 最多可上传几个文件,默认为 100maxNumberOfFiles: 10,// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []allowedFileTypes: ['image/*'],// 自定义增加 http headerheaders: {Authorization: `Bearer ${getToken()}`,},// 跨域是否传递 cookie ,默认为 falsewithCredentials: true,// 超时时间,默认为 10 秒timeout: 5 * 1000, // 5 秒// 上传失败onFailed(file) {ElMessage({message: `${file.name} 上传失败`,type: 'error',})},// 上传出错onError(file, err) {errorTips(err)},// 自定义插入customInsert(res, insertFn) {// 从 res 中找到 url alt href ,然后插入图片insertFn(res.url, '', '')},},uploadVideo: {server: `${import.meta.env.VITE_APP_BASE_API}/common/upload`,// form-data fieldName ,默认值 'wangeditor-uploaded-image'fieldName: 'file',// 单个文件的最大体积限制,默认为 2MmaxFileSize: 20 * 1024 * 1024, // 10M// 最多可上传几个文件,默认为 100maxNumberOfFiles: 10,// 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []allowedFileTypes: ['video/*'],// 自定义增加 http headerheaders: {Authorization: `Bearer ${getToken()}`,},// 跨域是否传递 cookie ,默认为 falsewithCredentials: true,// 超时时间,默认为 10 秒timeout: 15 * 1000, // 5 秒// 上传失败onFailed(file) {ElMessage({message: `${file.name} 上传失败`,type: 'error',})},// 上传出错onError(file, err) {errorTips(err)},// 自定义插入customInsert(res, insertFn) {// 从 res 中找到 url alt href ,然后插入insertFn(res.url, '', '')},},},
})
完整代码:
<!--* @Description: wangEditor* @Author: ZhengQiang Zeng* @Date: 2023-05-17 14:53:37* @LastEditors: ZhengQiang Zeng* @LastEditTime: 2023-05-18 10:35:01
-->
<template><div style="border: 1px solid #ccc"><!-- 工具栏 --><Toolbar:editor="editorRef":defaultConfig="toolbarConfig"style="border-bottom: 1px solid #ccc"/><!-- 编辑器 --><Editorv-model="valueHtml":defaultConfig="editorConfig":style="`height: ${height}px; overflow-y: hidden`"@onCreated="handleCreated"/></div>
</template><script setup>
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue'
// eslint-disable-next-line import/no-extraneous-dependencies
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { ElMessage } from 'element-plus'
import { getToken } from '@/utils/auth'
import { uploadFile2 } from '@/utils/upload-file'
import dayjs from 'dayjs'const props = defineProps({html: {type: String,default: '',},// 富文本高度height: {type: Number,default: 400,},// 是否为禁用状态disabled: {type: Boolean,default: false,},placeholder: {type: String,default: '请输入内容',},
})// 编辑器实例,必须用 shallowRef,重要!
const editorRef = shallowRef()// 内容 HTML
const valueHtml = ref()
watch(() => props.html,() => {valueHtml.value = props.html},
)// 工具栏配置
const toolbarConfig = ref({})// 编辑器配置
const editorConfig = ref({placeholder: computed(() => props.placeholder),MENU_CONF: {uploadImage: {customUpload(file, insertFn) {console.log(file, insertFn, 'upload')const TODAY = dayjs().format('YYYY/MM/DD')uploadFile2(`/upload/company-products/${TODAY}/`, file, (err, data) => {console.log(data, 'datas')console.log(file, 'datas')if (err) {ElMessage({message: `${file.name} 上传失败`,type: 'error',})} else {// 成功后,调用插入图片的函数const imageUrl = `http://${data.Location}`console.log(data.Location, 'image')console.log(imageUrl, 'image')insertFn(imageUrl, '', '')}})},// server: () => {// },// form-data fieldName ,默认值 'wangeditor-uploaded-image'fieldName: 'file',// 单个文件的最大体积限制,默认为 2MmaxFileSize: 5 * 1024 * 1024, // 3M// 最多可上传几个文件,默认为 100maxNumberOfFiles: 10,// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []allowedFileTypes: ['image/*'],// 自定义增加 http headerheaders: {Authorization: `Bearer ${getToken()}`,},// 跨域是否传递 cookie ,默认为 falsewithCredentials: true,// 超时时间,默认为 10 秒timeout: 5 * 1000, // 5 秒// 上传失败onFailed(file) {ElMessage({message: `${file.name} 上传失败`,type: 'error',})},// 上传出错onError(file, err) {errorTips(err)},// 自定义插入customInsert(res, insertFn) {// 从 res 中找到 url alt href ,然后插入图片insertFn(res.url, '', '')},},uploadVideo: {server: `${import.meta.env.VITE_APP_BASE_API}/common/upload`,// form-data fieldName ,默认值 'wangeditor-uploaded-image'fieldName: 'file',// 单个文件的最大体积限制,默认为 2MmaxFileSize: 20 * 1024 * 1024, // 10M// 最多可上传几个文件,默认为 100maxNumberOfFiles: 10,// 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []allowedFileTypes: ['video/*'],// 自定义增加 http headerheaders: {Authorization: `Bearer ${getToken()}`,},// 跨域是否传递 cookie ,默认为 falsewithCredentials: true,// 超时时间,默认为 10 秒timeout: 15 * 1000, // 5 秒// 上传失败onFailed(file) {ElMessage({message: `${file.name} 上传失败`,type: 'error',})},// 上传出错onError(file, err) {errorTips(err)},// 自定义插入customInsert(res, insertFn) {// 从 res 中找到 url alt href ,然后插入insertFn(res.url, '', '')},},},
})function errorTips(err) {err = String(err)err = err.replace(/Error: /g, '错误:')err = err.replace(/exceeds maximum allowed size of/g,'超过了允许的最大上传大小',)ElMessage({message: `${err}`,type: 'error',})
}// 获取 HTML 内容
const getHtml = () => {const res = editorRef.value.getHtml()if (res !== '<p><br></p>') {return res}return ''
}// 暴露出去给父组件获取
defineExpose({getHtml,
})const handleCreated = (editor) => {editorRef.value = editor // 记录 editor 实例,重要!
}// 组件销毁时,及时销毁编辑器
onBeforeUnmount(() => {const editor = editorRef.valueif (editor == null) returneditor.destroy()
})
</script><!-- 引入样式 -->
<style src="@wangeditor/editor/dist/css/style.css"></style>