Implementing Tencent Cloud Image Upload in Vue.js Applications

First, install the required dependencies:

npm install cos-js-sdk-v5 spark-md5 --save

1. Create a utility file for cloud storage operatiosn

Create src/utils/cloud-storage.js:

import COS from 'cos-js-sdk-v5'
import SparkMD5 from 'spark-md5'
import { getTempCredentials, getCloudConfig } from '@/api/storage'

let fileKey = ''

// Initialize Tencent Cloud Object Storage client
const cosClient = new COS({
  async getAuthorization(options, callback) {
    const response = await getTempCredentials()
    const credentials = response.data
    const auth = {
      TmpSecretId: credentials.tmpSecretId,
      TmpSecretKey: credentials.tmpSecretKey,
      XCosSecurityToken: credentials.sessionToken,
      StartTime: credentials.startTime,
      ExpiredTime: credentials.expiredTime,
    }
    callback(auth)
  },
  FileParallelLimit: 3,
  ChunkParallelLimit: 8,
  ChunkSize: 1024 * 1024 * 8,
})

// Get public URL for uploaded file
export async function getFileUrl() {
    const response = await getCloudConfig()
    const url = cosClient.getObjectUrl({
      Bucket: response.data.bucket,
      Region: response.data.region,
      Key: fileKey,
      Sign: false,
    })
    return url
}

// Calculate file MD5 hash
function calculateFileHash(file, callback) {
    const reader = new FileReader()
    const chunkSize = 2 * 1024 * 1024
    const chunks = Math.ceil(file.size / chunkSize)
    let currentChunk = 0
    const spark = new SparkMD5()

    reader.onload = function(e) {
      spark.appendBinary(e.target.result)
      currentChunk++

      if (currentChunk < chunks) {
        loadNext()
      } else {
        callback(spark.end())
      }
    }

    function loadNext() {
      const start = currentChunk * chunkSize
      const end = start + chunkSize >= file.size ? file.size : start + chunkSize
      reader.readAsBinaryString(file.slice(start, end))
    }
    
    loadNext()
}

// Upload large files using multipart upload
export function uploadLargeFile(path, file, callback, progressCallback) {
  return new Promise(resolve => {
    getCloudConfig().then(response => {
      calculateFileHash(file, hash => {
        file.hash = hash
        const extension = file.name.substr(file.name.lastIndexOf('.'))
        fileKey = path + hash + Date.now() + extension
        cosClient.sliceUploadFile({
          Bucket: response.data.bucket,
          Region: response.data.region,
          Key: fileKey,
          Body: file,
          onProgress(progressData) {
            progressCallback(progressData.percent)
          },
        }, async (err, data) => {
          if (err) {
            callback(err)
            resolve(err)
          } else {
            data.url = await getFileUrl()
            callback(null, data)
            resolve(data)
          }
        })
      })
    })
  })
}

// Upload small files using simple upload
export function uploadSmallFile(path, file, callback, progressCallback) {
  try {
    getCloudConfig().then(response => {
      calculateFileHash(file, hash => {
        file.hash = hash
        const extension = file.name.substr(file.name.lastIndexOf('.'))
        fileKey = path + hash + Date.now() + extension
        cosClient.putObject({
          Bucket: response.data.bucket,
          Region: response.data.region,
          Key: fileKey,
          Body: file,
          onProgress(progressData) {
            progressCallback(progressData.percent)
          },
        }, async (err, data) => {
          if (err) {
            callback(err)
          } else {
            data.url = await getFileUrl()
            callback(null, data)
          }
        })
      })
    })
  } catch (error) {
    console.error('Upload error:', error)
  }
}

// Delete file from cloud storage
export function deleteFile(path, fileName, callback, customKey) {
  getCloudConfig().then(response => {
    const key = customKey ? path + customKey : path + fileName
    cosClient.deleteObject({
      Bucket: response.data.bucket,
      Region: response.data.region,
      Key: key,
    }, (err, data) => {
      if (err) {
        callback(err)
      } else {
        callback(null, data)
      }
    })
  })
}

2. Implementing image upload component in Vue

import { uploadSmallFile } from '@/utils/cloud-storage'
import dayjs from 'dayjs'

const { proxy } = getCurrentInstance()

function handleImageUpload({ file }) {
  const currentDate = dayjs().format('YYYY/MM/DD')
  uploadSmallFile(`/uploads/products/${currentDate}/`, file, (error, result) => {
    if (error) {
      uploadCount.value--
      proxy.$modal.closeLoading()
      proxy.$modal.msgError('Upload failed')
      proxy.$refs.imageUpload.handleRemove(file)
      completeUpload()
    } else {
      uploadedFiles.value.push({ name: result.url, url: result.url })
      completeUpload()
      proxy.$modal.msgSuccess('Upload successful!')
    }
  })
}

function completeUpload() {
  if (uploadCount.value > 0 && uploadedFiles.value.length === uploadCount.value) {
    fileList.value = fileList.value
      .filter((f) => f.url !== undefined)
      .concat(uploadedFiles.value)
    uploadedFiles.value = []
    uploadCount.value = 0
    emit('update:modelValue', fileListToString(fileList.value))
    proxy.$modal.closeLoading()
  }
}

Complete component implementation:

<template>
  <div class="image-upload-component">
    <el-upload
      multiple
      action="#"
      :http-request="handleImageUpload"
      list-type="picture-card"
      :before-upload="beforeUpload"
      :limit="maxFiles"
      :on-error="handleUploadError"
      :on-exceed="handleExceed"
      ref="imageUpload"
      :before-remove="handleRemove"
      :show-file-list="true"
      :file-list="fileList"
      :on-preview="previewImage"
      :class="{ hide: fileList.length >= maxFiles }"
    >
      <el-icon class="upload-icon">
        <plus />
      </el-icon>
    </el-upload>
    
    <div class="upload-tip" v-if="showTip">
      Please upload
      <template v-if="maxFileSize">
        files smaller than <b style="color: #f56c6c">{{ maxFileSize }}MB</b>
      </template>
      <template v-if="allowedTypes">
        with format <b style="color: #f56c6c">{{ allowedTypes.join('/') }}</b>
      </template>
    </div>

    <el-dialog
      v-model="previewVisible"
      title="Preview"
      width="800px"
      append-to-body
    >
      <img
        :src="previewUrl"
        style="display: block; max-width: 100%; margin: 0 auto"
      />
    </el-dialog>
  </div>
</template>

<script setup>
import { defineEmits } from 'vue'
import { uploadSmallFile } from '@/utils/cloud-storage'
import dayjs from 'dayjs'

const props = defineProps({
  modelValue: [String, Object, Array],
  maxFiles: {
    type: Number,
    default: 5,
  },
  maxFileSize: {
    type: Number,
    default: 5,
  },
  allowedTypes: {
    type: Array,
    default: () => ['png', 'jpg', 'jpeg'],
  },
  showTip: {
    type: Boolean,
    default: true,
  },
})

const { proxy } = getCurrentInstance()
const emit = defineEmits(['update:modelValue'])
const uploadCount = ref(0)
const uploadedFiles = ref([])
const previewUrl = ref('')
const previewVisible = ref(false)
const fileList = ref([])
const showUploadTip = computed(
  () => props.showTip && (props.allowedTypes || props.maxFileSize),
)

watch(
  () => props.modelValue,
  (val) => {
    if (val) {
      const list = Array.isArray(val) ? val : props.modelValue.split(',')
      fileList.value = list.map((item) => {
        if (typeof item === 'string') {
          item = { name: item, url: item }
        }
        return item
      })
      return fileList.value
    }
      fileList.value = []
      return []
  },
  { deep: true, immediate: true },
)

function handleImageUpload({ file }) {
  const currentDate = dayjs().format('YYYY/MM/DD')
  uploadSmallFile(`/uploads/products/${currentDate}/`, file, (error, result) => {
    if (error) {
      uploadCount.value--
      proxy.$modal.closeLoading()
      proxy.$modal.msgError('Upload failed')
      proxy.$refs.imageUpload.handleRemove(file)
      completeUpload()
    } else {
      const imageUrl = `http://${result.Location}`
      uploadedFiles.value.push({ name: imageUrl, url: imageUrl })
      proxy.$modal.msgSuccess('Upload successful!')
      completeUpload()
    }
  })
}

function beforeUpload(file) {
  let isValidType = false
  if (props.allowedTypes.length) {
    let fileExtension = ''
    if (file.name.lastIndexOf('.') > -1) {
      fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
    }
    isValidType = props.allowedTypes.some((type) => {
      if (file.type.indexOf(type) > -1) return true
      if (fileExtension && fileExtension.indexOf(type) > -1) return true
      return false
    })
  } else {
    isValidType = file.type.indexOf('image') > -1
  }
  
  if (!isValidType) {
    proxy.$modal.msgError(
      `Invalid file format. Please upload ${props.allowedTypes.join('/')} image files.`,
    )
    return false
  }
  
  if (props.maxFileSize) {
    const isValidSize = file.size / 1024 / 1024 < props.maxFileSize
    if (!isValidSize) {
      proxy.$modal.msgError(`File size cannot exceed ${props.maxFileSize} MB!`)
      return false
    }
  }
  
  proxy.$modal.loading('Uploading image, please wait...')
  uploadCount.value++
}

function handleExceed() {
  proxy.$modal.msgError(`Cannot upload more than ${props.maxFiles} files!`)
}

function handleRemove(file) {
  const fileIndex = fileList.value.map((f) => f.name).indexOf(file.name)
  if (fileIndex > -1 && uploadedFiles.value.length === uploadCount.value) {
    fileList.value.splice(fileIndex, 1)
    emit('update:modelValue', fileListToString(fileList.value))
    return false
  }
}

function completeUpload() {
  if (uploadCount.value > 0 && uploadedFiles.value.length === uploadCount.value) {
    fileList.value = fileList.value
      .filter((f) => f.url !== undefined)
      .concat(uploadedFiles.value)
    uploadedFiles.value = []
    uploadCount.value = 0
    emit('update:modelValue', fileListToString(fileList.value))
    proxy.$modal.closeLoading()
  }
}

function handleUploadError() {
  proxy.$modal.msgError('Image upload failed')
  proxy.$modal.closeLoading()
}

function previewImage(file) {
  previewUrl.value = file.url
  previewVisible.value = true
}

function fileListToString(arr = [], separator = ',') {
  if (Array.isArray(arr)) {
    return arr.map(i => i.url).join(separator)
  }
  return console.error('Input is not an array')
}
</script>

<style scoped lang="scss">
:deep(.hide .el-upload--picture-card) {
  display: none;
}
</style>

3. Integratino with rich text editor

// Editor configuration
const editorConfig = ref({
  placeholder: computed(() => props.placeholder),
  MENU_CONF: {
    uploadImage: {
      customUpload(file, insertFn) {
        const currentDate = dayjs().format('YYYY/MM/DD')
        uploadSmallFile(`/uploads/content/${currentDate}/`, file, (error, result) => {
          if (error) {
            ElMessage({
              message: `${file.name} upload failed`,
              type: 'error',
            })
          } else {
            const imageUrl = `http://${result.Location}`
            insertFn(imageUrl, '', '')
          }
        })
      },
      fieldName: 'file',
      maxFileSize: 5 * 1024 * 1024,
      maxNumberOfFiles: 10,
      allowedFileTypes: ['image/*'],
      headers: {
        Authorization: `Bearer ${getAuthToken()}`,
      },
      withCredentials: true,
      timeout: 5 * 1000,
      onFailed(file) {
        ElMessage({
          message: `${file.name} upload failed`,
          type: 'error',
        })
      },
      onError(file, err) {
        handleError(err)
      },
      customInsert(res, insertFn) {
        insertFn(res.url, '', '')
      },
    },
    uploadVideo: {
      customUpload(file, insertFn) {
        const currentDate = dayjs().format('YYYY/MM/DD')
        uploadSmallFile(`/uploads/content/${currentDate}/`, file, (error, result) => {
          if (error) {
            ElMessage({
              message: `${file.name} upload failed`,
              type: 'error',
            })
          } else {
            const videoUrl = `http://${result.Location}`
            insertFn(videoUrl, '', '')
          }
        })
      },
      fieldName: 'file',
      maxFileSize: 20 * 1024 * 1024,
      maxNumberOfFiles: 10,
      allowedFileTypes: ['video/*'],
      headers: {
        Authorization: `Bearer ${getAuthToken()}`,
      },
      withCredentials: true,
      timeout: 15 * 1000,
      onFailed(file) {
        ElMessage({
          message: `${file.name} upload failed`,
          type: 'error',
        })
      },
      onError(file, err) {
        handleError(err)
      },
      customInsert(res, insertFn) {
        insertFn(res.url, '', '')
      },
    },
  },
})

Complete editor component:

<template>
  <div class="editor-container">
    <Toolbar
      :editor="editorRef"
      :defaultConfig="toolbarConfig"
      class="editor-toolbar"
    />
    <Editor
      v-model="contentHtml"
      :defaultConfig="editorConfig"
      :style="`height: ${height}px; overflow-y: hidden`"
      @onCreated="handleCreated"
    />
  </div>
</template>

<script setup>
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { ElMessage } from 'element-plus'
import { getAuthToken } from '@/utils/auth'
import { uploadSmallFile } from '@/utils/cloud-storage'
import dayjs from 'dayjs'

const props = defineProps({
  content: {
    type: String,
    default: '',
  },
  height: {
    type: Number,
    default: 400,
  },
  disabled: {
    type: Boolean,
    default: false,
  },
  placeholder: {
    type: String,
    default: 'Please enter content',
  },
})

const editorRef = shallowRef()
const contentHtml = ref()

watch(
  () => props.content,
  () => {
    contentHtml.value = props.content
  },
)

const toolbarConfig = ref({})

const editorConfig = ref({
  placeholder: computed(() => props.placeholder),
  MENU_CONF: {
    uploadImage: {
      customUpload(file, insertFn) {
        const currentDate = dayjs().format('YYYY/MM/DD')
        uploadSmallFile(`/uploads/content/${currentDate}/`, file, (error, result) => {
          if (error) {
            ElMessage({
              message: `${file.name} upload failed`,
              type: 'error',
            })
          } else {
            const imageUrl = `http://${result.Location}`
            insertFn(imageUrl, '', '')
          }
        })
      },
      fieldName: 'file',
      maxFileSize: 5 * 1024 * 1024,
      maxNumberOfFiles: 10,
      allowedFileTypes: ['image/*'],
      headers: {
        Authorization: `Bearer ${getAuthToken()}`,
      },
      withCredentials: true,
      timeout: 5 * 1000,
      onFailed(file) {
        ElMessage({
          message: `${file.name} upload failed`,
          type: 'error',
        })
      },
      onError(file, err) {
        handleError(err)
      },
      customInsert(res, insertFn) {
        insertFn(res.url, '', '')
      },
    },
    uploadVideo: {
      customUpload(file, insertFn) {
        const currentDate = dayjs().format('YYYY/MM/DD')
        uploadSmallFile(`/uploads/content/${currentDate}/`, file, (error, result) => {
          if (error) {
            ElMessage({
              message: `${file.name} upload failed`,
              type: 'error',
            })
          } else {
            const videoUrl = `http://${result.Location}`
            insertFn(videoUrl, '', '')
          }
        })
      },
      fieldName: 'file',
      maxFileSize: 20 * 1024 * 1024,
      maxNumberOfFiles: 10,
      allowedFileTypes: ['video/*'],
      headers: {
        Authorization: `Bearer ${getAuthToken()}`,
      },
      withCredentials: true,
      timeout: 15 * 1000,
      onFailed(file) {
        ElMessage({
          message: `${file.name} upload failed`,
          type: 'error',
        })
      },
      onError(file, err) {
        handleError(err)
      },
      customInsert(res, insertFn) {
        insertFn(res.url, '', '')
      },
    },
  },
})

function handleError(err) {
  err = String(err)
  err = err.replace(/Error: /g, 'Error: ')
  err = err.replace(
    /exceeds maximum allowed size of/g,
    'exceeds maximum allowed size of',
  )
  ElMessage({
    message: `${err}`,
    type: 'error',
  })
}

const getHtmlContent = () => {
  const result = editorRef.value.getHtml()
  if (result !== '<p><br></p>') {
    return result
  }
  return ''
}

defineExpose({
  getHtmlContent,
})

const handleCreated = (editor) => {
  editorRef.value = editor
}

onBeforeUnmount(() => {
  const editor = editorRef.value
  if (editor == null) return
  editor.destroy()
})
</script>

<style src="@wangeditor/editor/dist/css/style.css"></style>
<style scoped>
.editor-container {
  border: 1px solid #ccc;
}
.editor-toolbar {
  border-bottom: 1px solid #ccc;
}
</style>

4. Avatar cropping and upload

import { uploadSmallFile } from '@/utils/cloud-storage'

function uploadAvatar() {
  proxy.$refs.cropper.getCropBlob(data => {
    const file = convertBlobToFile(data, 'avatar.png')
    const currentDate = dayjs().format('YYYY/MM/DD')
    uploadSmallFile(`/uploads/avatars/${currentDate}/`, file, (error, result) => {
      if (error) {
        proxy.$modal.error(`Upload failed: ${error}`)
      } else {
        dialogVisible.value = false
        const imageUrl = `http://${result.Location}`
        avatarOptions.img = imageUrl
        emit('update:modelValue', avatarOptions.img)
        userStore.avatar = avatarOptions.img
        proxy.$modal.msgSuccess('Avatar updated successfully')
        previewVisible.value = false
      }
    })
  })
}

Complete avatar component:

<template>
  <div class="avatar-container" @click="openCropper()">
    <img :src="avatarOptions.img" title="Click to upload avatar" class="avatar-preview" />
    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
      <el-row>
        <el-col :xs="24" :md="12" :style="{ height: '350px' }">
          <vue-cropper
            ref="cropper"
            :img="avatarOptions.img"
            :info="true"
            :autoCrop="avatarOptions.autoCrop"
            :autoCropWidth="avatarOptions.autoCropWidth"
            :autoCropHeight="avatarOptions.autoCropHeight"
            :fixedBox="avatarOptions.fixedBox"
            :outputType="avatarOptions.outputType"
            @realTime="updatePreview"
            v-if="previewVisible"
          />
        </el-col>
        <el-col :xs="24" :md="12" :style="{ height: '350px' }">
          <div class="avatar-preview-container">
            <img :src="avatarOptions.previews.url" :style="avatarOptions.previews.img" />
          </div>
        </el-col>
      </el-row>
      <br />
      <el-row>
        <el-col :lg="2" :md="2">
          <el-upload
            action="#"
            :http-request="customUpload"
            :show-file-list="false"
            :before-upload="beforeFileUpload"
          >
            <el-button>
              Select
              <el-icon class="el-icon--right"><Upload /></el-icon>
            </el-button>
          </el-upload>
        </el-col>
        <el-col :lg="{ span: 1, offset: 2 }" :md="2">
          <el-button icon="Plus" @click="zoomIn"></el-button>
        </el-col>
        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
          <el-button icon="Minus" @click="zoomOut"></el-button>
        </el-col>
        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
          <el-button icon="RefreshLeft" @click="rotateLeft"></el-button>
        </el-col>
        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
          <el-button icon="RefreshRight" @click="rotateRight"></el-button>
        </el-col>
        <el-col :lg="{ span: 2, offset: 6 }" :md="2">
          <el-button type="primary" @click="uploadAvatar">Submit</el-button>
        </el-col>
      </el-row>
    </el-dialog>
  </div>
</template>

<script setup>
import 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper'
import useUserStore from '@/store/modules/user'
import { uploadSmallFile } from '@/utils/cloud-storage'
import dayjs from 'dayjs'

const userStore = useUserStore()
const { proxy } = getCurrentInstance()

const dialogVisible = ref(false)
const previewVisible = ref(false)
const dialogTitle = ref('Update Avatar')
const emit = defineEmits(['update:modelValue'])

const avatarOptions = reactive({
  img: userStore.avatar,
  autoCrop: true,
  autoCropWidth: 200,
  autoCropHeight: 200,
  fixedBox: true,
  outputType: 'png',
  previews: {},
})

function openCropper() {
  dialogVisible.value = true
}

function modalOpened() {
  previewVisible.value = true
}

function customUpload() {
}

function rotateLeft() {
  proxy.$refs.cropper.rotateLeft()
}

function rotateRight() {
  proxy.$refs.cropper.rotateRight()
}

function zoomIn() {
  proxy.$refs.cropper.changeScale(1)
}

function zoomOut() {
  proxy.$refs.cropper.changeScale(-1)
}

function beforeFileUpload(file) {
  if (file.type.indexOf('image/') == -1) {
    proxy.$modal.msgError('Invalid file format. Please upload an image file with extensions like JPG or PNG.')
  } else {
    const reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onload = () => {
      avatarOptions.img = reader.result
    }
  }
}

function uploadAvatar() {
  proxy.$refs.cropper.getCropBlob(data => {
    const file = convertBlobToFile(data, 'avatar.png')
    const currentDate = dayjs().format('YYYY/MM/DD')
    uploadSmallFile(`/uploads/avatars/${currentDate}/`, file, (error, result) => {
      if (error) {
        proxy.$modal.error(`Upload failed: ${error}`)
      } else {
        dialogVisible.value = false
        const imageUrl = `http://${result.Location}`
        avatarOptions.img = imageUrl
        emit('update:modelValue', avatarOptions.img)
        userStore.avatar = avatarOptions.img
        proxy.$modal.msgSuccess('Avatar updated successfully')
        previewVisible.value = false
      }
    })
  })
}

function convertBlobToFile(blob, filename) {
  return new File([blob], filename, { type: blob.type })
}

function updatePreview(data) {
  avatarOptions.previews = data
}

function closeDialog() {
  avatarOptions.img = userStore.avatar
  previewVisible.value = false
}
</script>

<style lang='scss' scoped>
.avatar-container {
  position: relative;
  display: inline-block;
  height: 120px;
}

.avatar-preview {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  object-fit: cover;
}

.avatar-container:hover:after {
  content: "+";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  color: #eee;
  background: rgba(0, 0, 0, 0.5);
  font-size: 24px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  cursor: pointer;
  line-height: 110px;
  border-radius: 50%;
}

.avatar-preview-container {
  width: 200px;
  height: 200px;
  margin: 0 auto;
  overflow: hidden;
  border-radius: 50%;
  border: 1px solid #ccc;
}
</style>

Tags: Vue.js tencent-cloud image-upload cos-sdk rich-text-editor

Posted on Sat, 12 Sep 2026 16:50:30 +0000 by FastLaneHosting