530 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			530 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | ||
|   <section class="uploader">
 | ||
|     <el-upload
 | ||
|         action
 | ||
|         multiple
 | ||
|         ref="upload"
 | ||
|         :class="{validError:!validateState}"
 | ||
|         :http-request="submitUpload"
 | ||
|         :on-remove="handleRemove"
 | ||
|         :on-change="handleChange"
 | ||
|         :before-upload="onBeforeUpload"
 | ||
|         :file-list="fileList"
 | ||
|         :limit="limit"
 | ||
|         :disabled="disabled"
 | ||
|         :list-type="isImg ? 'picture-card' : 'text'"
 | ||
|         :accept="accept"
 | ||
|         :show-file-list="!isSingle"
 | ||
|         :on-preview="handlePictureCardPreview"
 | ||
|         :auto-upload="isAutoUpload"
 | ||
|         :on-exceed="handleExceed">
 | ||
|       <template v-if="!disabled">
 | ||
|         <template v-if="hasUploaded&&isSingle">
 | ||
|           <div class="fileItem">
 | ||
|             <div class="uploadFile" @click.stop>
 | ||
|               <ai-icon type="svg" :icon="uploadFile.icon"/>
 | ||
|               <div class="info">
 | ||
|                 <span v-text="uploadFile.name"/>
 | ||
|                 <span class="size" v-text="uploadFile.size"/>
 | ||
|               </div>
 | ||
|             </div>
 | ||
|             <el-button>重新选择</el-button>
 | ||
|             <el-button v-if="clearable" plain type="danger" @click.stop="handleClear">删除</el-button>
 | ||
|           </div>
 | ||
|         </template>
 | ||
|         <template v-else-if="limit > fileList.length">
 | ||
|           <slot v-if="hasTriggerSlot" name="trigger"/>
 | ||
|           <div v-else class="uploaderBox">
 | ||
|             <span class="iconfont" :class="isImg ? 'iconPhoto' : 'iconAdd'"/>
 | ||
|             <p>上传{{ isImg ? '图片' : '附件' }}</p>
 | ||
|           </div>
 | ||
|         </template>
 | ||
|         <div slot="tip" class="el-upload__tip" v-if="showTips">
 | ||
|           <p v-if="fileType === 'img' && !acceptType && !hasTipsSlot">最多上传{{
 | ||
|               limit
 | ||
|             }}张图片,单个文件最大10MB,支持jpg、jpeg、png格式</p>
 | ||
|           <p v-if="fileType === 'file' && !acceptType && !hasTipsSlot">最多上传{{ limit }}个附件,单个文件最大10MB</p>
 | ||
|           <p v-if="fileType === 'file' && !acceptType && !hasTipsSlot">
 | ||
|             支持.zip、.rar、.doc、.docx、.xls、.xlsx、.ppt、.pptx、.pdf、.txt、.jpg、.png格式</p>
 | ||
|           <p>
 | ||
|             <slot name="tips" v-if="hasTipsSlot"></slot>
 | ||
|           </p>
 | ||
|         </div>
 | ||
|       </template>
 | ||
|     </el-upload>
 | ||
|     <el-dialog :visible.sync="dialog" title="图片预览编辑器" :modal="false" :show-close="false" append-to-body>
 | ||
|       <vue-cropper
 | ||
|           ref="cropper"
 | ||
|           style="height: 400px;"
 | ||
|           :img="fileList.length ? fileList[0].url : ''" v-bind="crop"/>
 | ||
|       <div style="text-align: center;margin-top: 10px;">
 | ||
|         <el-radio-group v-if="crop.fixed" size="small" v-model="currFixedIndex" @change="onFixedChange"
 | ||
|                         style="margin-right: 8px;">
 | ||
|           <el-radio-button
 | ||
|               :label="0"
 | ||
|               size="small">
 | ||
|             1.6:1
 | ||
|           </el-radio-button>
 | ||
|           <el-radio-button
 | ||
|               :label="1"
 | ||
|               size="small">
 | ||
|             4:3
 | ||
|           </el-radio-button>
 | ||
|         </el-radio-group>
 | ||
|         <el-button size="small" circle icon="el-icon-refresh-right" @click="$refs.cropper.rotateRight()"></el-button>
 | ||
|         <el-button size="small" circle icon="el-icon-refresh-left" @click="$refs.cropper.rotateLeft()"></el-button>
 | ||
|       </div>
 | ||
|       <div slot="footer">
 | ||
|         <el-popconfirm title="是否关闭图片预览器,并上传图片?" @confirm="previewCrop">
 | ||
|           <el-button slot="reference" type="primary">保存</el-button>
 | ||
|         </el-popconfirm>
 | ||
|         <el-button @click="onClose">关闭</el-button>
 | ||
|       </div>
 | ||
|     </el-dialog>
 | ||
|     <div class="images" v-viewer="{movable: true}" v-show="false">
 | ||
|       <img v-for="(item, index) in imgList" :src="item" :key="index" alt="">
 | ||
|     </div>
 | ||
|   </section>
 | ||
| </template>
 | ||
| 
 | ||
| <script>
 | ||
| import {VueCropper} from 'vue-cropper'
 | ||
| import 'viewerjs/dist/viewer.css'
 | ||
| import Viewer from 'v-viewer'
 | ||
| import Vue from "vue";
 | ||
| 
 | ||
| Viewer.setDefaults({
 | ||
|   zIndex: 20170
 | ||
| })
 | ||
| Vue.use(Viewer)
 | ||
| export default {
 | ||
|   name: 'AiUploader',
 | ||
|   components: {VueCropper},
 | ||
|   inject: {
 | ||
|     elFormItem: {default: ""},
 | ||
|     elForm: {default: ''},
 | ||
|   },
 | ||
|   model: {
 | ||
|     prop: 'value',
 | ||
|     event: 'change'
 | ||
|   },
 | ||
|   props: {
 | ||
|     value: {default: () => []},
 | ||
|     url: {
 | ||
|       type: String,
 | ||
|       default: '/admin/file/add'
 | ||
|     },
 | ||
|     isShowTip: {
 | ||
|       type: Boolean,
 | ||
|       default: false
 | ||
|     },
 | ||
|     isWechat: {
 | ||
|       type: Boolean,
 | ||
|       default: false
 | ||
|     },
 | ||
|     maxSize: {
 | ||
|       type: Number,
 | ||
|       default: 10
 | ||
|     },
 | ||
|     instance: Function,
 | ||
|     acceptType: {type: String},
 | ||
|     fileType: {type: String, default: 'img'},
 | ||
|     limit: {type: Number, default: 9},
 | ||
|     disabled: {type: Boolean, default: false},
 | ||
|     isCrop: {type: Boolean, default: false},
 | ||
|     cropOps: Object,
 | ||
|     isImport: {
 | ||
|       type: Boolean,
 | ||
|       default: false
 | ||
|     },
 | ||
|     clearable: {default: true},
 | ||
|     valueIsUrl: Boolean
 | ||
|   },
 | ||
|   data() {
 | ||
|     return {
 | ||
|       fileList: [],
 | ||
|       dialog: false,
 | ||
|       currFixedIndex: 0,
 | ||
|     }
 | ||
|   },
 | ||
|   watch: {
 | ||
|     value: {
 | ||
|       handler(v) {
 | ||
|         this.dispatch('ElFormItem', 'el.form.change', [v]);
 | ||
|         if (v?.length > 0) {
 | ||
|           this.fileList = this.valueIsUrl ? v?.split(",")?.map(url => ({url})) : [...v]
 | ||
|         }
 | ||
|       },
 | ||
|       immediate: true,
 | ||
|       deep: true
 | ||
|     }
 | ||
|   },
 | ||
|   computed: {
 | ||
|     isImg() {
 | ||
|       return this.fileType === 'img'
 | ||
|     },
 | ||
|     validateState() {
 | ||
|       return ['', 'success'].includes(this.elFormItem?.validateState)
 | ||
|     },
 | ||
|     isAutoUpload() {
 | ||
|       return !(this.isCrop || this.isImport);
 | ||
|     },
 | ||
|     hasTipsSlot() {
 | ||
|       return this.$slots.tips
 | ||
|     },
 | ||
|     hasTriggerSlot() {
 | ||
|       return this.$slots.trigger
 | ||
|     },
 | ||
|     accept() {
 | ||
|       if (this.acceptType) {
 | ||
|         return this.acceptType
 | ||
|       }
 | ||
| 
 | ||
|       return this.isImg ? '.jpg,.png,.jpeg' : '.zip,.rar,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt,.jpg,.png,.mp4'
 | ||
|     },
 | ||
|     crop() {
 | ||
|       return {
 | ||
|         autoCrop: true,
 | ||
|         outputType: 'png',
 | ||
|         fixedBox: false,
 | ||
|         fixed: true,
 | ||
|         fixedNumber: [1.6, 1],
 | ||
|         width: 0,
 | ||
|         height: 0,
 | ||
|         ...this.cropOps
 | ||
|       }
 | ||
|     },
 | ||
|     imgList() {
 | ||
|       return this.fileList.map(v => v.url)
 | ||
|     },
 | ||
|     isSingle() {
 | ||
|       return this.limit == 1 && this.isImport
 | ||
|     },
 | ||
|     showTips() {
 | ||
|       return this.isShowTip || this.$slots.tips
 | ||
|     },
 | ||
|     hasUploaded() {
 | ||
|       return this.fileList?.length > 0
 | ||
|     },
 | ||
|     uploadFile() {
 | ||
|       let file = this.fileList?.[0],
 | ||
|           size = Number(file.size),
 | ||
|           icon = "iconTxt"
 | ||
|       //显示大小
 | ||
|       if (size > Math.pow(1024, 2)) {
 | ||
|         size = (size / Math.pow(1024, 2)).toFixed(1) + 'MB'
 | ||
|       } else {
 | ||
|         size = (size / 1024).toFixed(1) + 'KB'
 | ||
|       }
 | ||
|       //显示图标
 | ||
|       if (/\.(xls|xlsx)$/.test(file.name)) {
 | ||
|         icon = "iconExcel"
 | ||
|       } else if (/\.(zip)$/.test(file.name)) {
 | ||
|         icon = "iconZip"
 | ||
|       } else if (/\.(rar)$/.test(file.name)) {
 | ||
|         icon = "iconRar"
 | ||
|       } else if (/\.(png)$/.test(file.name)) {
 | ||
|         icon = "iconPng"
 | ||
|       } else if (/\.(pptx|ppt)$/.test(file.name)) {
 | ||
|         icon = "iconPPT"
 | ||
|       } else if (/\.(doc|docx)$/.test(file.name)) {
 | ||
|         icon = "iconWord"
 | ||
|       }
 | ||
| 
 | ||
|       return {...file, size, icon}
 | ||
|     }
 | ||
|   },
 | ||
|   methods: {
 | ||
|     onFixedChange(e) {
 | ||
|       this.fixedNumber = e === 0 ? [1.6, 1] : [4, 3]
 | ||
| 
 | ||
|       this.$nextTick(() => {
 | ||
|         this.$refs.cropper.goAutoCrop()
 | ||
|       })
 | ||
|     },
 | ||
|     handleChange(file, fileList) {
 | ||
|       if (this.isImport) {
 | ||
|         if (!this.onOverSize(file)) {
 | ||
|           this.fileList = []
 | ||
|           return false
 | ||
|         }
 | ||
|         this.fileList = fileList
 | ||
|         this.emitChange(fileList)
 | ||
|         return false
 | ||
|       }
 | ||
|       if (this.isCrop) {
 | ||
|         if (file.raw.type === 'image/gif') {
 | ||
|           this.$message.error(`不支持gif格式的图片`)
 | ||
|           this.fileList = []
 | ||
| 
 | ||
|           return false
 | ||
|         }
 | ||
| 
 | ||
|         if (!this.onOverSize(file)) {
 | ||
|           this.fileList = []
 | ||
| 
 | ||
|           return false
 | ||
|         }
 | ||
|         this.dialog = true
 | ||
|         this.fileList = fileList
 | ||
|       } else {
 | ||
|         this.fileList = fileList
 | ||
|       }
 | ||
|     },
 | ||
|     handleExceed(files) {
 | ||
|       if (this.isSingle && files[0]) {
 | ||
|         this.$refs.upload?.clearFiles()
 | ||
|         this.$refs.upload?.handleStart(files[0])
 | ||
|       } else this.$message.warning(`最多上传${this.limit}个${this.isImg ? '图片' : '文件'}`)
 | ||
|     },
 | ||
|     handlePictureCardPreview(file) {
 | ||
|       if (this.fileType !== 'img') return
 | ||
| 
 | ||
|       const index = this.imgList.indexOf(file.url)
 | ||
|       const viewer = this.$el.querySelector('.images').$viewer
 | ||
|       viewer.view(index)
 | ||
|     },
 | ||
| 
 | ||
|     handleRemove(file, fileList) {
 | ||
|       this.fileList = fileList
 | ||
|       this.emitChange(fileList)
 | ||
|     },
 | ||
|     emitChange(files) {
 | ||
|       this.$emit('change', this.valueIsUrl ? files?.map(e => e.url)?.toString() : files)
 | ||
|     },
 | ||
|     handleClear() {
 | ||
|       this.fileList = []
 | ||
|     },
 | ||
|     getExtension(name) {
 | ||
|       return name.substring(name.lastIndexOf('.'))
 | ||
|     },
 | ||
| 
 | ||
|     onOverSize(e) {
 | ||
|       const isLt10M = e.size / 1024 / 1024 < this.maxSize
 | ||
|       const suffixName = this.getExtension(e.name)
 | ||
|       const suffixNameList = this.accept.split(',')
 | ||
| 
 | ||
|       if (suffixNameList.indexOf(`${suffixName.toLowerCase()}`) === -1) {
 | ||
|         this.$message.error(`不支持该格式`)
 | ||
|         return false
 | ||
|       }
 | ||
| 
 | ||
|       if (!isLt10M) {
 | ||
|         this.$message.error(`${this.isImg ? '图片' : '文件'}大小不超过${this.maxSize}MB!`)
 | ||
|         return false
 | ||
|       }
 | ||
| 
 | ||
|       return true
 | ||
|     },
 | ||
| 
 | ||
|     onBeforeUpload(event) {
 | ||
|       return this.onOverSize(event)
 | ||
|     },
 | ||
| 
 | ||
|     onClose() {
 | ||
|       this.fileList = []
 | ||
|       this.dialog = false
 | ||
|     },
 | ||
| 
 | ||
|     submitUpload(file) {
 | ||
|       let formData = new FormData()
 | ||
|       formData.append('file', file.file)
 | ||
|       this.instance.post(this.url, formData, {
 | ||
|         withCredentials: false
 | ||
|       }).then(res => {
 | ||
|         if (res?.code == 0) {
 | ||
|           if (this.isWechat) {
 | ||
|             this.emitChange([{
 | ||
|               ...res.data.file,
 | ||
|               media: res.data.media
 | ||
|             }])
 | ||
|             this.fileList.forEach(item => {
 | ||
|               if (item.uid === file.file.uid) {
 | ||
|                 item.id = res.data.file.id
 | ||
|                 item.path = res.data.file.url
 | ||
|                 item.url = res.data.file.url,
 | ||
|                 item.media = res.data.media
 | ||
|               }
 | ||
|             })
 | ||
|             this.emitChange(this.fileList)
 | ||
|             this.$message.success('上传成功')
 | ||
|             return false
 | ||
|           }
 | ||
| 
 | ||
|           if (Object.prototype.toString.call(res.data) === '[object Object]') {
 | ||
|             this.fileList.forEach(item => {
 | ||
|               if (item.uid === file.file.uid) {
 | ||
|                 item.id = res.data.id
 | ||
|                 item.path = res.data.url
 | ||
|                 item.url = res.data.url
 | ||
|                 item.url = res.data.url
 | ||
|                 item.fileSizeStr = res.data.fileSizeStr
 | ||
|                 item.postfix = res.data.postfix
 | ||
|               }
 | ||
|             })
 | ||
|           } else {
 | ||
|             let data = res.data[0].split(';')
 | ||
|             this.fileList.forEach(item => {
 | ||
|               if (item.uid === file.file.uid) {
 | ||
|                 item.id = data[1]
 | ||
|                 item.path = data[0]
 | ||
|                 item.url = data[0]
 | ||
|               }
 | ||
|             })
 | ||
|           }
 | ||
| 
 | ||
|           this.emitChange(this.fileList)
 | ||
|           this.$message.success('上传成功')
 | ||
|         }
 | ||
|       })
 | ||
|     },
 | ||
|     previewCrop() {
 | ||
|       this.$refs.cropper.getCropBlob(data => {
 | ||
|         data.name = this.fileList[0].name;
 | ||
|         this.fileList[0].file = new window.File([data], data.name, {type: data.type})
 | ||
|         this.fileList[0].file.uid = this.fileList[0].uid
 | ||
|         this.submitUpload(this.fileList[0])
 | ||
|         this.dialog = false
 | ||
|       })
 | ||
|     },
 | ||
|     /**
 | ||
|      * 表单验证
 | ||
|      * @param componentName
 | ||
|      * @param eventName
 | ||
|      * @param params
 | ||
|      */
 | ||
|     dispatch(componentName, eventName, params) {
 | ||
|       let parent = this.$parent || this.$root;
 | ||
|       let name = parent.$options.componentName;
 | ||
| 
 | ||
|       while (parent && (!name || name !== componentName)) {
 | ||
|         parent = parent.$parent;
 | ||
| 
 | ||
|         if (parent) {
 | ||
|           name = parent.$options.componentName;
 | ||
|         }
 | ||
|       }
 | ||
|       if (parent) {
 | ||
|         parent.$emit.apply(parent, [eventName].concat(params));
 | ||
|       }
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| </script>
 | ||
| 
 | ||
| <style lang="scss" scoped>
 | ||
| .uploader {
 | ||
|   line-height: 1;
 | ||
| 
 | ||
|   :deep(.el-upload ){
 | ||
|     width: 100%;
 | ||
|     text-align: start;
 | ||
|   }
 | ||
| 
 | ||
|   :deep(.validError ){
 | ||
|     .el-button {
 | ||
|       border-color: #f46;
 | ||
|       color: #f46;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   :deep(.el-upload--picture-card ){
 | ||
|     border: none;
 | ||
|   }
 | ||
| 
 | ||
|   :deep( .el-list-leave-active), :deep( .el-upload-list__item ){
 | ||
|     transition: all 0s !important;
 | ||
|   }
 | ||
| 
 | ||
|   :deep(.el-upload-list--picture-card .el-upload-list__item ){
 | ||
|     width: 120px;
 | ||
|     height: 120px;
 | ||
|     border-radius: 4px;
 | ||
|   }
 | ||
| 
 | ||
|   :deep(.el-upload--picture-card ){
 | ||
|     width: auto;
 | ||
|     height: auto;
 | ||
|   }
 | ||
| 
 | ||
|   .el-upload__tip p {
 | ||
|     color: #999;
 | ||
|     line-height: 16px;
 | ||
|   }
 | ||
| 
 | ||
|   :deep(.fileItem ){
 | ||
|     width: 100%;
 | ||
|     height: 60px;
 | ||
|     background: #FFFFFF;
 | ||
|     border-radius: 2px;
 | ||
|     border: 1px solid #D0D4DC;
 | ||
|     display: flex;
 | ||
|     align-items: center;
 | ||
|     padding: 0 16px;
 | ||
|     cursor: default;
 | ||
| 
 | ||
|     .uploadFile {
 | ||
|       text-align: start;
 | ||
|       flex: 1;
 | ||
|       min-width: 0;
 | ||
|       display: flex;
 | ||
|       color: #222;
 | ||
| 
 | ||
|       .AiIcon {
 | ||
|         width: 40px;
 | ||
|         height: 40px;
 | ||
|       }
 | ||
| 
 | ||
|       .info {
 | ||
|         flex: 1;
 | ||
|         min-width: 0;
 | ||
|         display: flex;
 | ||
|         flex-direction: column;
 | ||
|         margin-left: 8px;
 | ||
|         margin-right: 40px;
 | ||
|         line-height: 22px;
 | ||
|       }
 | ||
| 
 | ||
|       .size {
 | ||
|         color: #888;
 | ||
|       }
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   .uploaderBox {
 | ||
|     width: 120px;
 | ||
|     height: 120px;
 | ||
|     line-height: 1;
 | ||
|     background: #F3F4F7;
 | ||
|     border-radius: 2px;
 | ||
|     border: 1px solid #D0D4DC;
 | ||
|     display: flex;
 | ||
|     flex-direction: column;
 | ||
|     justify-content: center;
 | ||
|     align-items: center;
 | ||
| 
 | ||
|     &:hover {
 | ||
|       opacity: 0.6;
 | ||
|     }
 | ||
| 
 | ||
|     span {
 | ||
|       font-size: 32px;
 | ||
|       color: #8899bb;
 | ||
| 
 | ||
|       &:hover {
 | ||
|         color: #8899bb;
 | ||
|       }
 | ||
|     }
 | ||
| 
 | ||
|     p {
 | ||
|       margin: 0;
 | ||
|       padding-top: 4px;
 | ||
|       color: #555;
 | ||
|       font-size: 12px;
 | ||
|       text-align: center;
 | ||
|       line-height: 1;
 | ||
|     }
 | ||
|   }
 | ||
| }
 | ||
| </style>
 |