1
lzhe
2024-09-26 7d59e8e2c727dd49d9552a8febc2af47c5b95a69
src/components/scUpload/index.vue
@@ -1,13 +1,14 @@
<template>
   <div class="sc-upload" :class="{'sc-upload-round':round}" :style="style">
   <div class="sc-upload" :class="{ 'sc-upload-round': round }" :style="style">
      <div v-if="file && file.status != 'success'" class="sc-upload__uploading">
         <div class="sc-upload__progress">
            <el-progress :percentage="file.percentage" :text-inside="true" :stroke-width="16"/>
            <el-progress :percentage="file.percentage" :text-inside="true" :stroke-width="16" />
         </div>
         <el-image class="image" :src="file.tempFile" fit="cover"></el-image>
      </div>
      <div v-if="file && file.status=='success'" class="sc-upload__img">
         <el-image class="image" :src="file.url" :preview-src-list="[file.url]" fit="cover" hide-on-click-modal append-to-body :z-index="9999">
      <div v-if="file && file.status == 'success'" class="sc-upload__img">
         <el-image class="image" :src="file.url" :preview-src-list="[file.url]" fit="cover" hide-on-click-modal
            append-to-body :z-index="9999">
            <template #placeholder>
               <div class="sc-upload__img-slot">
                  Loading...
@@ -18,35 +19,28 @@
            <span class="del" @click="handleRemove()"><el-icon><el-icon-delete /></el-icon></span>
         </div>
      </div>
      <el-upload v-if="!file" class="uploader" ref="uploader"
         :auto-upload="cropper?false:autoUpload"
         :disabled="disabled"
         :show-file-list="showFileList"
         :action="action"
         :name="name"
         :data="data"
         :accept="accept"
         :limit="1"
         :http-request="request"
         :on-change="change"
         :before-upload="before"
         :on-success="success"
         :on-error="error"
         :on-exceed="handleExceed">
      <el-upload v-if="!file" class="uploader" ref="uploader" :auto-upload="cropper ? false : autoUpload"
         :disabled="disabled" :show-file-list="showFileList" :action="action" :name="name" :data="data"
         :accept="accept" :limit="1" :http-request="request" :on-change="change" :before-upload="before"
         :on-success="success" :on-error="error" :on-exceed="handleExceed">
         <slot>
            <div class="el-upload--picture-card">
               <div class="file-empty">
                  <el-icon><component :is="icon" /></el-icon>
                  <h4 v-if="title">{{title}}</h4>
                  <el-icon>
                     <component :is="icon" />
                  </el-icon>
                  <h4 v-if="title">{{ title }}</h4>
               </div>
            </div>
         </slot>
      </el-upload>
      <span style="display:none!important"><el-input v-model="value"></el-input></span>
      <el-dialog title="剪裁" draggable v-model="cropperDialogVisible" :width="580" @closed="cropperClosed" destroy-on-close>
         <sc-cropper :src="cropperFile.tempCropperFile" :compress="compress" :aspectRatio="aspectRatio" ref="cropper"></sc-cropper>
      <el-dialog title="剪裁" draggable v-model="cropperDialogVisible" :width="580" @closed="cropperClosed"
         destroy-on-close>
         <sc-cropper :src="cropperFile.tempCropperFile" :compress="compress" :aspectRatio="aspectRatio"
            ref="cropper"></sc-cropper>
         <template #footer>
            <el-button @click="cropperDialogVisible=false" >取 消</el-button>
            <el-button @click="cropperDialogVisible = false">取 消</el-button>
            <el-button type="primary" @click="cropperSave">确 定</el-button>
         </template>
      </el-dialog>
@@ -54,232 +48,347 @@
</template>
<script>
   import { defineAsyncComponent } from 'vue'
   import { genFileId } from 'element-plus'
   const scCropper = defineAsyncComponent(() => import('@/components/scCropper'))
   import config from "@/config/upload"
import { defineAsyncComponent } from 'vue'
import { genFileId } from 'element-plus'
const scCropper = defineAsyncComponent(() => import('@/components/scCropper'))
import config from "@/config/upload"
   export default {
      props: {
         modelValue: { type: String, default: "" },
         height: {type: Number, default: 148},
         width: {type: Number, default: 148},
         title: { type: String, default: "" },
         icon: { type: String, default: "el-icon-plus" },
         action: { type: String, default: "" },
         apiObj: { type: Object, default: () => {} },
         name: { type: String, default: config.filename },
         data: { type: Object, default: () => {} },
         accept: { type: String, default: "image/gif, image/jpeg, image/png" },
         maxSize: { type: Number, default: config.maxSizeFile },
         limit: { type: Number, default: 1 },
         autoUpload: { type: Boolean, default: true },
         showFileList: { type: Boolean, default: false },
         disabled: { type: Boolean, default: false },
         round: { type: Boolean, default: false },
         onSuccess: { type: Function, default: () => { return true } },
         host: {type: String, default: ''},
         cropper: { type: Boolean, default: false },
         compress: {type: Number, default: 1},
         aspectRatio:  {type: Number, default: NaN}
      },
      components: {
         scCropper
      },
      data() {
         return {
            value: "",
            file: null,
            style: {
               width: this.width + "px",
               height: this.height + "px"
            },
            cropperDialogVisible: false,
            cropperFile: null
         }
      },
      watch:{
         modelValue(val){
            this.value = val
            this.newFile(val)
export default {
   props: {
      modelValue: { type: String, default: "" },
      height: { type: Number, default: 148 },
      width: { type: Number, default: 148 },
      title: { type: String, default: "" },
      icon: { type: String, default: "el-icon-plus" },
      action: { type: String, default: "" },
      apiObj: { type: Object, default: () => { } },
      name: { type: String, default: config.filename },
      data: { type: Object, default: () => { } },
      accept: { type: String, default: "image/gif, image/jpeg, image/png" },
      maxSize: { type: Number, default: config.maxSizeFile },
      limit: { type: Number, default: 1 },
      autoUpload: { type: Boolean, default: true },
      showFileList: { type: Boolean, default: false },
      disabled: { type: Boolean, default: false },
      round: { type: Boolean, default: false },
      onSuccess: { type: Function, default: () => { return true } },
      host: { type: String, default: '' },
      cropper: { type: Boolean, default: false },
      compress: { type: Number, default: 1 },
      aspectRatio: { type: Number, default: NaN }
   },
   components: {
      scCropper
   },
   data() {
      return {
         value: "",
         file: null,
         style: {
            width: this.width + "px",
            height: this.height + "px"
         },
         value(val){
            this.$emit('update:modelValue', val)
         }
         cropperDialogVisible: false,
         cropperFile: null,
         hostNew: ''
      }
   },
   watch: {
      modelValue(val) {
         this.value = val
         this.newFile(val)
      },
      mounted() {
      value(val) {
         this.$emit('update:modelValue', val)
      }
   },
   mounted() {
      this.getHost().then(res => {
         this.value = this.modelValue
         this.newFile(this.modelValue)
      })
   },
   methods: {
      getHost() {
         if (this.host) {
            this.hostNew = this.host
            return Promise.resolve()
         }
         return this.$API.setting.component.getImgHost.get().then(res => {
            this.hostNew = res.data + '/visual/'
         })
      },
      methods: {
         newFile(url){
            if(url){
               this.file = {
                  status: "success",
                  url: `${this.host}${url}`
               }
            }else{
               this.file = null
      newFile(url) {
         if (url) {
            this.file = {
               status: "success",
               url: `${this.hostNew}${url}`
            }
         },
         cropperSave(){
            this.$refs.cropper.getCropFile(file => {
               file.uid = this.cropperFile.uid
               this.cropperFile.raw = file
               this.file = this.cropperFile
               this.file.tempFile = URL.createObjectURL(this.file.raw)
               this.$refs.uploader.submit()
            }, this.cropperFile.name, this.cropperFile.type)
            this.cropperDialogVisible = false
         },
         cropperClosed(){
            URL.revokeObjectURL(this.cropperFile.tempCropperFile)
            delete this.cropperFile.tempCropperFile
         },
         handleRemove(){
            this.clearFiles()
         },
         clearFiles(){
            URL.revokeObjectURL(this.file.tempFile)
            this.value = ""
         } else {
            this.file = null
            this.$nextTick(()=>{
               this.$refs.uploader.clearFiles()
            })
         },
         change(file,files){
            if(files.length > 1){
               files.splice(0, 1)
            }
            if(this.cropper && file.status=='ready'){
               const acceptIncludes = ["image/gif", "image/jpeg", "image/png"].includes(file.raw.type)
               if(!acceptIncludes){
                  this.$notify.warning({
                     title: '上传文件警告',
                     message: '选择的文件非图像类文件'
                  })
                  return false
               }
               this.cropperFile = file
               this.cropperFile.tempCropperFile = URL.createObjectURL(file.raw)
               this.cropperDialogVisible = true
               return false
            }
            this.file = file
            if(file.status=='ready'){
               file.tempFile = URL.createObjectURL(file.raw)
            }
         },
         before(file){
            const acceptIncludes = this.accept.replace(/\s/g,"").split(",").includes(file.type)
            if(!acceptIncludes){
         }
      },
      cropperSave() {
         this.$refs.cropper.getCropFile(file => {
            file.uid = this.cropperFile.uid
            this.cropperFile.raw = file
            this.file = this.cropperFile
            this.file.tempFile = URL.createObjectURL(this.file.raw)
            this.$refs.uploader.submit()
         }, this.cropperFile.name, this.cropperFile.type)
         this.cropperDialogVisible = false
      },
      cropperClosed() {
         URL.revokeObjectURL(this.cropperFile.tempCropperFile)
         delete this.cropperFile.tempCropperFile
      },
      handleRemove() {
         this.clearFiles()
      },
      clearFiles() {
         URL.revokeObjectURL(this.file.tempFile)
         this.value = ""
         this.file = null
         this.$nextTick(() => {
            this.$refs.uploader.clearFiles()
         })
      },
      change(file, files) {
         if (files.length > 1) {
            files.splice(0, 1)
         }
         if (this.cropper && file.status == 'ready') {
            const acceptIncludes = ["image/gif", "image/jpeg", "image/png"].includes(file.raw.type)
            if (!acceptIncludes) {
               this.$notify.warning({
                  title: '上传文件警告',
                  message: '选择的文件非图像类文件'
               })
               this.clearFiles()
               return false
            }
            const maxSize = file.size / 1024 / 1024 < this.maxSize;
            if (!maxSize) {
               this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
               this.clearFiles()
               return false
            }
         },
         handleExceed(files){
            const file = files[0]
            file.uid = genFileId()
            this.$refs.uploader.handleStart(file)
         },
         success(res, file){
            //释放内存删除blob
            URL.revokeObjectURL(file.tempFile)
            try {
               delete file.tempFile
            } catch (error) {
            }
            var os = this.onSuccess(res, file)
            if(os!=undefined && os==false){
               this.$nextTick(() => {
                  this.file = null
                  this.value = ""
               })
               return false
            }
            var response = config.parseData(res)
            file.url = response.src
            this.value = file.url
         },
         error(err){
            this.$nextTick(()=>{
               this.clearFiles()
            })
            this.$notify.error({
               title: '上传文件未成功',
               message: err
            })
         },
         request(param){
            var apiObj = config.apiObj;
            if(this.apiObj){
               apiObj = this.apiObj;
            }
            const data = new FormData();
            data.append(param.filename, param.file);
            for (const key in param.data) {
               data.append(key, param.data[key]);
            }
            apiObj.post(data, {
               onUploadProgress: e => {
                  const complete = parseInt(((e.loaded / e.total) * 100) | 0, 10)
                  param.onProgress({percent: complete})
               }
            }).then(res => {
               var response = config.parseData(res);
               if(response.code == config.successCode){
                  param.onSuccess(res)
               }else{
                  param.onError(response.msg || "未知错误")
               }
            }).catch(err => {
               param.onError(err)
            })
            this.cropperFile = file
            this.cropperFile.tempCropperFile = URL.createObjectURL(file.raw)
            this.cropperDialogVisible = true
            return false
         }
         this.file = file
         if (file.status == 'ready') {
            file.tempFile = URL.createObjectURL(file.raw)
         }
      },
      before(file) {
         const acceptIncludes = this.accept.replace(/\s/g, "").split(",").includes(file.type)
         if (!acceptIncludes) {
            this.$notify.warning({
               title: '上传文件警告',
               message: '选择的文件非图像类文件'
            })
            this.clearFiles()
            return false
         }
         const maxSize = file.size / 1024 / 1024 < this.maxSize;
         if (!maxSize) {
            this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
            this.clearFiles()
            return false
         }
      },
      handleExceed(files) {
         const file = files[0]
         file.uid = genFileId()
         this.$refs.uploader.handleStart(file)
      },
      success(res, file) {
         //释放内存删除blob
         URL.revokeObjectURL(file.tempFile)
         try {
            delete file.tempFile
         } catch (error) {
         }
         var os = this.onSuccess(res, file)
         if (os != undefined && os == false) {
            this.$nextTick(() => {
               this.file = null
               this.value = ""
            })
            return false
         }
         var response = config.parseData(res)
         file.url = response.src
         this.value = file.url
      },
      error(err) {
         this.$nextTick(() => {
            this.clearFiles()
         })
         this.$notify.error({
            title: '上传文件未成功',
            message: err
         })
      },
      request(param) {
         var apiObj = config.apiObj;
         if (this.apiObj) {
            apiObj = this.apiObj;
         }
         const data = new FormData();
         data.append(param.filename, param.file);
         for (const key in param.data) {
            data.append(key, param.data[key]);
         }
         apiObj.post(data, {
            onUploadProgress: e => {
               const complete = parseInt(((e.loaded / e.total) * 100) | 0, 10)
               param.onProgress({ percent: complete })
            }
         }).then(res => {
            var response = config.parseData(res);
            if (response.code == config.successCode) {
               param.onSuccess(res)
            } else {
               param.onError(response.msg || "未知错误")
            }
         }).catch(err => {
            param.onError(err)
         })
      }
   }
}
</script>
<style scoped>
   .el-form-item.is-error .sc-upload .el-upload--picture-card {border-color: var(--el-color-danger);}
   .sc-upload .el-upload--picture-card {border-radius: 0;}
.el-form-item.is-error .sc-upload .el-upload--picture-card {
   border-color: var(--el-color-danger);
}
   .sc-upload .uploader,.sc-upload:deep(.el-upload) {width: 100%;height: 100%;}
.sc-upload .el-upload--picture-card {
   border-radius: 0;
}
   .sc-upload__img {width: 100%;height: 100%;position: relative;}
   .sc-upload__img .image {width: 100%;height: 100%;}
   .sc-upload__img-actions {position: absolute;top:0;right: 0;display: none;}
   .sc-upload__img-actions span {display: flex;justify-content: center;align-items: center;width: 25px;height:25px;cursor: pointer;color: #fff;}
   .sc-upload__img-actions span i {font-size: 12px;}
   .sc-upload__img-actions .del {background: #F56C6C;}
   .sc-upload__img:hover .sc-upload__img-actions {display: block;}
   .sc-upload__img-slot {display: flex;justify-content: center;align-items: center;width: 100%;height: 100%;font-size: 12px;background-color: var(--el-fill-color-lighter);}
.sc-upload .uploader,
.sc-upload:deep(.el-upload) {
   width: 100%;
   height: 100%;
}
   .sc-upload__uploading {width: 100%;height: 100%;position: relative;}
   .sc-upload__progress {position: absolute;width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;background-color: var(--el-overlay-color-lighter);z-index: 1;padding:10px;}
   .sc-upload__progress .el-progress {width: 100%;}
   .sc-upload__uploading .image {width: 100%;height: 100%;}
.sc-upload__img {
   width: 100%;
   height: 100%;
   position: relative;
}
   .sc-upload .file-empty {width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;flex-direction: column;}
   .sc-upload .file-empty i {font-size: 28px;}
   .sc-upload .file-empty h4 {font-size: 12px;font-weight: normal;color: #8c939d;margin-top: 8px;}
.sc-upload__img .image {
   width: 100%;
   height: 100%;
}
   .sc-upload.sc-upload-round {border-radius: 50%;overflow: hidden;}
   .sc-upload.sc-upload-round .el-upload--picture-card {border-radius: 50%;}
   .sc-upload.sc-upload-round .sc-upload__img-actions {top: auto;left: 0;right: 0;bottom: 0;}
   .sc-upload.sc-upload-round .sc-upload__img-actions span {width: 100%;}
.sc-upload__img-actions {
   position: absolute;
   top: 0;
   right: 0;
   display: none;
}
.sc-upload__img-actions span {
   display: flex;
   justify-content: center;
   align-items: center;
   width: 25px;
   height: 25px;
   cursor: pointer;
   color: #fff;
}
.sc-upload__img-actions span i {
   font-size: 12px;
}
.sc-upload__img-actions .del {
   background: #F56C6C;
}
.sc-upload__img:hover .sc-upload__img-actions {
   display: block;
}
.sc-upload__img-slot {
   display: flex;
   justify-content: center;
   align-items: center;
   width: 100%;
   height: 100%;
   font-size: 12px;
   background-color: var(--el-fill-color-lighter);
}
.sc-upload__uploading {
   width: 100%;
   height: 100%;
   position: relative;
}
.sc-upload__progress {
   position: absolute;
   width: 100%;
   height: 100%;
   display: flex;
   justify-content: center;
   align-items: center;
   background-color: var(--el-overlay-color-lighter);
   z-index: 1;
   padding: 10px;
}
.sc-upload__progress .el-progress {
   width: 100%;
}
.sc-upload__uploading .image {
   width: 100%;
   height: 100%;
}
.sc-upload .file-empty {
   width: 100%;
   height: 100%;
   display: flex;
   justify-content: center;
   align-items: center;
   flex-direction: column;
}
.sc-upload .file-empty i {
   font-size: 28px;
}
.sc-upload .file-empty h4 {
   font-size: 12px;
   font-weight: normal;
   color: #8c939d;
   margin-top: 8px;
}
.sc-upload.sc-upload-round {
   border-radius: 50%;
   overflow: hidden;
}
.sc-upload.sc-upload-round .el-upload--picture-card {
   border-radius: 50%;
}
.sc-upload.sc-upload-round .sc-upload__img-actions {
   top: auto;
   left: 0;
   right: 0;
   bottom: 0;
}
.sc-upload.sc-upload-round .sc-upload__img-actions span {
   width: 100%;
}
</style>