Open xuanweiH opened 4 years ago
实现一个 图片上传组件,支持上传时拖拽排序, 点击预览大图
思路大概是:
handlePreview (file) { if (this.isPreview) { this.dialogVisible = true setTimeout(() => { this.$refs.carouselBox.setActiveItem(this.fileList.indexOf(file)) }, 100) } if ('on-preview' in this.$attrs) { this.$attrs['on-preview'](file) } },
点击更换图片 主要是通过 创造一个 type为file的input标签, input,onchange 把旧的文件放进新文件属性里面,再移除. 调用 handleStart(file) 再调用上传
接着就是基于el-upload完成图片上传的操作 注册一些相关的时间beforeUpload 如果用element的上传直接把路径写action里面,如果要自己自定义httprequest也可以另外写. 同时这个组件还支持上传多张时 拖拽排序 原理也是基于sortablejs 用sortable.create中的onend事件里, 通过
const currRow = this.fileList.splice(oldIndex, 1)[0] this.fileList.splice(newIndex, 0, currRow)
props 暴露出去的属性大致分为, value 文件属性 type/ image 是否可以上传 canUpload 是否可以预览 一些文件限制 limit 对象 以及输出类型outputType 输出类型主要是用于看后端需要什么值:
if (this.outputType === 'string') { this.$emit('input', files.length === 0 ? '' : files[0].picUrl ? files[0].picUrl : '') } else if (this.outputType === 'object') { this.$emit('input', files.length === 0 ? {} : files[0]) } else if (this.outputType === 'array') { this.$emit('input', files) }
封装代码:
<template> <div class="img-upload-preview"> <template v-if="fileList.length === 0 && type == 'image' && !canUpload"> <div class="no-image" :class="{ 'big': size === 'big', 'medium': size === 'medium', 'small': size === 'small', 'mini': size === 'mini', }" > </div> </template> <template v-if="fileList.length > 0 && type == 'image' && limit.maxNumber >= fileList.length" class="upload-type-image"> <ul class="el-upload-list el-upload-list--picture-card"> <li v-for="(file, index) in fileList" :key="`${file.url}-${index}`" class="el-upload-list__item single-image-box" :class="{ 'big': size === 'big', 'medium': size === 'medium', 'small': size === 'small', 'mini': size === 'mini', 'focusing' : focusing === index }" @mouseenter="focusing = index" @mouseleave="focusing = -1" @click="focusing = index" > <el-progress class="single-image-progress" v-if="file.status === 'uploading'" type="circle" :width="size === 'big' ? 120 : size === 'medium' ? 90 : size === 'small' ? 60 : 120" :stroke-width="size === 'big' ? 6 : size === 'medium' ? 4 : size === 'small' ? 3 : 6" :percentage="file.percentage"> </el-progress> <span class="single-image-box__actions"> <span v-if="isPreview" class="single-image-box__preview" @click="handlePreview(file)"> <i class="el-icon-zoom-in"></i> </span> <span v-if="canUpload && size === 'mini'" class="single-image-box__change" @click="handleChangeImg(file)"> <i class="el-icon-refresh"></i> </span> <span v-if="canUpload" class="single-image-box__change" @click="handleDelete(file)"> <i class="el-icon-delete"></i> </span> </span> <span v-if="canUpload && size !== 'mini'" class="single-image-box__changes" @click="handleChangeImg(file)"> 点击更换 </span> <img v-if="file.status === 'success'" :src="file.url"> </li> </ul> </template> <div class="omp-upload-img" :class="{'no_upload': (isPreview && !canUpload) || fileList.length >= limit.maxNumber, 'is_preview': isPreview}"> <input v-model="value" placeholder="请输入内容" @input="value = $event.target.value" v-show="false"/> <el-upload class="upload-box" :class="{ 'big': type === 'image' && size === 'big', 'medium': type === 'image' && size === 'medium', 'small': type === 'image' && size === 'small', 'mini': type === 'image' && size === 'mini' }" ref="upload" v-bind="$attrs" v-on="$listeners" :list-type="listType" :file-list="fileList" :auto-upload="true" :action="uploadUrl" :data="data" :limit="limit.maxNumber" :show-file-list="type !== 'image'" :before-upload="beforeUpload" :on-success="handleSuccess" :on-exceed="handleExceed" :on-change="handleChange" :on-remove="handleRemove" :on-preview="handlePreview" :on-progress="handleProgress" :disabled="!canUpload" > <template v-if="['text', 'picture'].indexOf(listType) !== -1"> <slot name="trigger" slot="trigger"> <el-button slot="trigger" :size="size" type="primary" :disabled="!canUpload">上传<i class="el-icon-upload el-icon--right"></i></el-button> </slot> <slot name="tip" slot="tip"> <div slot="tip" class="el-upload__tip">只能上传{{this.limit.format.join(',')}}文件</div> </slot> <slot></slot> </template> <template v-else> <slot> <div v-if="size === 'big' || size === 'medium'" style="display: flex; justify-content: center; align-items: center; flex-direction: column; height: 100%;"> <i class="el-icon-upload"></i> <label v-if="placeholder !== ''" style="line-height: 20px; font-size: 12px;">{{placeholder}}</label> </div> <i v-else-if="size === 'small'" class="el-icon-upload" style="font-size: 24px;"></i> <i v-else class="el-icon-plus" style="font-size: 18px;"></i> </slot> </template> </el-upload> <el-dialog :visible.sync="dialogVisible" top="10vh" title="图片预览" append-to-body custom-class="omp-upload-preview"> <el-carousel :autoplay="false" height="620px" class="preview-img" ref="carouselBox" :arrow="fileList.length > 1 ? 'hover' : 'never'" :indicator-position="fileList.length > 1 ? 'outside' : 'none'"> <el-carousel-item v-for="(file, key) in fileList" :key="key" class="text-center"> <img height="100%" :src="file.url" :alt="file.name"> </el-carousel-item> </el-carousel> </el-dialog> </div> </div> </template> <script> import helper from '@/utils/helper' import Sortable from 'sortablejs' let listTypeObj = { 'text': 'text', 'button': 'picture', 'image': 'picture-card' } export default { name: 'imgUploadPreview', components: { }, data () { return { focusing: -1, data: { fileName: '', fileSize: 0 }, listType: listTypeObj[this.type], input: '', fileList: [], dialogVisible: false, baseUrl: this.$api.common.imageUrl, uploadUrl: `${this.$api.common.imageUrl}upload` } }, computed: { }, props: { value: { default: () => { return [] }, type: [Array, String, Object] }, type: { default: 'image', type: String }, size: { default: 'medium', type: String }, canUpload: { default: false, type: Boolean }, isPreview: { default: true, type: Boolean }, limit: { default: () => { return { width: undefined, height: undefined, format: ['jpeg', 'png', 'gif'], size: undefined, maxNumber: 1, isStrictSize: false, isSquare: false, overLimitMsg: '上传文件数量超过限制' } }, type: Object }, placeholder: { default: '', type: String }, outputType: { default: 'string', type: String } }, methods: { handleChange (file, fileList) { // 判断是否为替换文件 if (file.raw.oldFile && file.status === 'ready') { const oldFile = file.raw.oldFile fileList.splice(fileList.indexOf(oldFile), 1, file) fileList.pop() } this.changeCommon(file, fileList) if ('on-change' in this.$attrs) { this.$attrs['on-change'](file, fileList) } }, handleRemove (file, fileList) { this.changeCommon(file, fileList) if ('on-remove' in this.$attrs) { this.$attrs['on-remove'](file, fileList) } }, changeCommon (file, fileList) { // 输出上传结果 this.fileList = fileList this.dragTable() this.handleOutput() }, // 文件上传成功status = 200 的回调 handleSuccess (response, file, fileList) { if (response && response.errorCode === 0) { let resData = response.data let { groupName, remoteFileName } = resData let { width, height } = file.raw file.picUrl = `/${groupName}/${remoteFileName}?width=${width}&height=${height}` file.url = file.picUrl file.remoteFileName = resData.remoteFileName file.groupName = resData.groupName } else { this.$message.error(`上传失败!${response.errorMsg}`) // 判断是否为替换,如果是替换,还原旧文件 if (file.raw.oldFile) { const oldFile = file.raw.oldFile fileList.splice(fileList.indexOf(file), 1, oldFile) } else { fileList.splice(fileList.indexOf(file), 1) } } if ('on-success' in this.$attrs) { this.$attrs['on-success'](response, file, fileList) } }, // 上传前判断图片类型, 尺寸,宽高 beforeUpload (file) { // debugger // 校验 let fileSize = file.size / 1024 if (this.limit.format) { const typeList = this.limit.format.map(item => { item = `image/${item}` return item }) if (!typeList.includes(file.type)) { this.$message.error(`上传失败:图片格式只能为${this.limit.format.join(',')}!`) return false } } if (this.limit.size && fileSize > this.limit.size) { this.$message.error(`上传失败:图片大小需小于${this.limit.size}k,请重新上传!`) return false } const isCheck = new Promise((resolve, reject) => { let _URL = window.URL || window.webkitURL let img = new Image() img.onload = () => { file.width = img.width file.height = img.height let inValid, message if (this.limit.isStrictSize) { // 严格模式下存在单边严格,所以要分成三种情况分析 // 只有宽度严格 if (this.limit.width && !this.limit.height) { inValid = img.width !== this.limit.width message = `图片需严格遵守 ${this.limit.width} * 高度不限制` } else if (!this.limit.width && this.limit.height) { // 只有高度严格 inValid = img.height !== this.limit.height message = `图片需严格遵守 宽度不限制 * ${this.limit.height}` } else { // 全严格 inValid = img.width !== this.limit.width || img.height !== this.limit.height message = `图片需严格遵守 ${this.limit.width} * ${this.limit.height}` } } else { // 只有宽度 if (this.limit.width && !this.limit.height) { inValid = img.width > this.limit.width message = `图片需遵守小于 ${this.limit.width} * 高度不限制` } else if (!this.limit.width && this.limit.height) { // 只有高度 inValid = img.height > this.limit.height message = `图片需遵守小于 宽度不限制 * ${this.limit.height}` } else { inValid = img.width > this.limit.width || img.height > this.limit.height message = `图片需遵守小于 ${this.limit.width} * ${this.limit.height}` } } if (this.limit.isSquare) { inValid = img.width !== img.height message = `图片需遵守长宽1:1的正方形切图` } inValid ? reject(message) : resolve() } img.src = _URL.createObjectURL(file) }).then(() => { // 构造额外请求参数 this.data = { fileName: file.name, fileSize: file.size } return file }, (message) => { this.$message.error(`上传失败:${message}`) return Promise.reject(new Error('something bad happened')) }) return isCheck }, handlePreview (file) { if (this.isPreview) { this.dialogVisible = true setTimeout(() => { this.$refs.carouselBox.setActiveItem(this.fileList.indexOf(file)) }, 100) } if ('on-preview' in this.$attrs) { this.$attrs['on-preview'](file) } }, handleExceed (files, fileList) { if ('on-exceed' in this.$attrs) { this.$attrs['on-exceed'](files, fileList) } else { this.$message.error(`上传失败:${this.limit.overLimitMsg}`) } }, handleChangeImg (oldFile) { this.focusing = false // 创建一个承载器 const newInput = document.createElement('input') newInput.type = 'file' newInput.onchange = (e) => { let file = e.target.files[0] // 将旧文件信息加入新文件属性里 file.oldFile = oldFile // this.clearFiles(false) newInput.remove() setTimeout(() => { // 加入上传文件 this.$refs.upload.handleStart(file) // 开始上传 this.$refs.upload.submit() }, 100) } newInput.click() }, handleProgress (event, file, fileList) { event.percent = 0 if (file.percentage === undefined) { file.percentage = 0 } return this.changeProgress(file) }, changeProgress (file) { setTimeout(() => { file.percentage = file.percentage + 10 if (file.percentage < 100) { this.changeProgress(file) } else { file.percentage = 100 } return file }, (file.size / 1024) / 10) }, handleOutput () { const fileList = helper.objDeepClone(this.fileList) const files = [] // 如果是正在上传的图片, 不传值 if (fileList.length !== 0 && fileList.filter(item => item.status === 'success').length !== fileList.length) return fileList.forEach(item => { files.push({ groupName: item.groupName, picUrl: item.picUrl, remoteFileName: item.remoteFileName, size: item.size, width: item.raw ? item.raw.width : item.width, height: item.raw ? item.raw.height : item.height }) }) if (this.outputType === 'string') { this.$emit('input', files.length === 0 ? '' : files[0].picUrl ? files[0].picUrl : '') } else if (this.outputType === 'object') { this.$emit('input', files.length === 0 ? {} : files[0]) } else if (this.outputType === 'array') { this.$emit('input', files) } }, handleDelete (file) { this.fileList.splice(this.fileList.indexOf(file), 1) this.handleRemove(file, this.fileList) }, clearFiles (boolean = true) { this.fileList = [] this.$refs.upload.clearFiles() if (boolean) { this.handleOutput() } }, abort (file) { this.$refs.upload.abort(file) }, submit () { this.$refs.upload.submit() }, // 拖拽 dragTable () { if (this.$el) { const el = this.$el.querySelector('.el-upload-list.el-upload-list--picture-card') if (el !== null) { Sortable.create(el, { animation: 180, delay: 0, onEnd: ({ newIndex, oldIndex }) => { if (newIndex !== oldIndex) { const currRow = this.fileList.splice(oldIndex, 1)[0] this.fileList.splice(newIndex, 0, currRow) this.handleOutput() } } }) } } } }, created () { // this.baseUrl = this.$api.common.imageUrl // this.uploadUrl = this.baseUrl + 'upload' }, mounted () { // 开启排序功能 if (this.canUpload) { setTimeout(() => { this.dragTable() }, 500) } }, watch: { value: { handler (newName, oldName) { this.fileList = [] if (helper.isString(this.value) && this.value.length !== 0) { this.fileList = [{ picUrl: this.value, url: this.baseUrl + this.value, status: 'success' }] } else if (helper.isObject(this.value) && JSON.stringify(this.value) !== '{}') { const object = helper.objDeepClone(this.value) object.url = this.baseUrl + this.value.picUrl object.status = 'success' this.fileList = [object] } else if (helper.isArray(this.value) && this.value.length !== 0) { const array = helper.objDeepClone(this.value) this.fileList = array.map(item => { item.url = this.baseUrl + item.picUrl item.status = 'success' return item }) } }, immediate: true, deep: true } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style lang="stylus"> .img-upload-preview position relative .no-image position relative background-color #fbfdff border 1px dashed #c0ccda border-radius 6px vertical-align top display inline-block &.big width 148px height 148px line-height 148px &.medium width 120px height 120px line-height 120px &.small width 86px height 86px line-height 86px &.mini width 53px height 53px line-height 53px .single-image-box position relative margin 5px !important background rgba(0, 0, 0, 0.3) !important border 1px dashed #c0ccda border-radius 6px -webkit-box-sizing border-box box-sizing border-box cursor pointer vertical-align top overflow hidden &.focusing .single-image-box__actions opacity 1 &.big width 148px height 148px line-height 148px &.medium width 120px height 120px line-height 120px &.small width 86px height 86px line-height 86px &.mini width 53px height 53px line-height 53px .single-image-box__actions font-size 14px .single-image-progress display flex justify-content center align-items center position absolute top 50% left 50% transform translate(-50%, -50%) .single-image-box__actions position absolute width 100% height 100% left 0 top 0 cursor default text-align center color #fff opacity 0; font-size 20px background-color rgba(0, 0, 0, 0.5) transition opacity .3s z-index 10 .single-image-box__changes font-size 12px height 30px line-height 30px display block position absolute bottom 0 width 100% text-align center background rgba(0, 0, 0, 0.65) color #fff cursor pointer z-index 99 img width 100% height 100% position relative object-fit contain object-position center vertical-align middle border-style none background rgba(0, 0, 0, 0.3) margin-top -4px .omp-upload-img display inline-block margin 5px .upload-box &.big .el-upload--picture-card width 148px height 148px line-height 148px .el-upload-list__item width 148px height 148px &.medium .el-upload--picture-card width 120px height 120px line-height 120px .el-upload-list__item width 120px height 120px &.small .el-upload--picture-card width 86px height 86px line-height 86px .el-upload-list__item width 86px height 86px &.mini .el-upload--picture-card width 53px height 53px line-height 53px .el-upload-list__item width 53px height 53px .el-upload-list__item-preview, .el-upload-list__item-delete font-size 16px margin 0 2px .el-icon-close-tip display none /* 屏蔽上传功能 */ &.no_upload margin 0 .el-upload--picture-card display none /* 屏蔽上传成功标签 */ &.is_preview .el-upload-list__item &:hover .el-upload-list__item-status-label display none .el-upload-list__item-status-label display none .omp-upload-preview .preview-img overflow hidden img width 100% height 560px position relative z-index 10 object-fit contain object-position center vertical-align middle border-style none background rgba(0, 0, 0, 0.3) </style>
组件使用
<img-upload-preview style="text-align: center;" type="image" size="medium" :canUpload="true" :isPreview="true" v-model="form.skuHeadPic" :limit="{ width: undefined, height: undefined, size: undefined, maxNumber: 1, format: ['jpeg', 'png','gif'], isStrictSize: false, isSquare: false, overLimitMsg: '上传文件数量超过限制' }" placeholder="请上传商品头图" outputType="object" > </img-upload-preview>
实现一个 图片上传组件,支持上传时拖拽排序, 点击预览大图
思路大概是:
点击更换图片 主要是通过 创造一个 type为file的input标签, input,onchange 把旧的文件放进新文件属性里面,再移除. 调用 handleStart(file) 再调用上传
接着就是基于el-upload完成图片上传的操作 注册一些相关的时间beforeUpload 如果用element的上传直接把路径写action里面,如果要自己自定义httprequest也可以另外写. 同时这个组件还支持上传多张时 拖拽排序 原理也是基于sortablejs 用sortable.create中的onend事件里, 通过
props 暴露出去的属性大致分为, value 文件属性 type/ image 是否可以上传 canUpload 是否可以预览 一些文件限制 limit 对象 以及输出类型outputType 输出类型主要是用于看后端需要什么值:
封装代码:
组件使用