美文网首页
图片上传并剪裁

图片上传并剪裁

作者: McDu | 来源:发表于2020-11-03 11:41 被阅读0次

图片上传组件

支持功能

  1. 图片上传、图片预览、图片剪裁、图片删除
  2. 默认上传时图片尺寸不符合 750 * 500 大小,自动弹出剪裁框剪裁,如不需要可设置 cropSize=false 关闭此功能
  3. 默认限制上传图片最多50张
  4. 默认限制图片大小最大1M
  5. 图片上传成功会有绿色的打勾角标 file.status = 'success'

使用场景

  1. 上传产品头图,特点:限制图片大小、有设置主图的插槽($scopedSlots.mainImg

    imgList: [{url:'xxx'}]
    
    <image-upload
        :img-list="imgList"
        tip="图片限1M以下,图片宽度750,高度500,数量限50张"
        @image-change="handleImageChange"
    >
        <el-radio
            slot="mainImg"
            slot-scope="{file}"
            v-if="file.status === 'success'"
            v-model="form.main_image"
            :label="file.url"
            @change="handleMainImageChange(file.url)"
        >
            主图
        </el-radio>
    </image-upload>
    
    handleImageChange(data) {
        this.imgList = data;
    }
    
  2. 上传产品图片,不限制尺寸,无主图插槽

    <image-upload
        :img-list="imgList"
        :cropSize="false"
        @image-change="handleImageChange"
    ></image-upload>
    

ImageUpload.js

<!--
https://github.com/xyxiao001/vue-cropper 组件详细信息
description: 图片上传 支持批量上传,图片大小、尺寸校验  (主图插槽 -> mainImg)
@param imgList          Array       eg: [{url: imgUrl}]     必传
@param limit            Number      上传数量限制              50张
@param cropSize         Boolean     是否限制图片尺寸           限制
@param cropOptions      Object      截图选项
@param limitSize        Number      最大尺寸                 1024kb
@param tip              String      上传说明                 ''
 -->
<template>
    <div>
        <el-upload
            :file-list="imgList"
            multiple
            ref="upload"
            :limit="limit"
            list-type="picture-card"
            :accept="accept"
            :auto-upload="false"
            :on-exceed="handleExceed"
            :on-change="debounce(handleChange, 2)"
            :http-request="httpRequest"
            :before-upload="beforeUpload"
        >
            <i slot="default" class="el-icon-plus"></i>
            <div slot="file" slot-scope="{ file }">
                <div :class="$scopedSlots.mainImg ? 'main-wrap' : ''">
                    <img class="el-upload-list__item-thumbnail" :src="file.url" alt />
                    <label
                        class="el-upload-list__item-status-label"
                        v-if="file.status === 'success'"
                    >
                        <i class="el-icon-upload-success el-icon-check"></i>
                    </label>
                    <span class="el-upload-list__item-actions" v-if="showHandle">
                        <span
                            class="el-upload-list__item-preview"
                            @click="showPreviewDialog(file)"
                        >
                            <i class="el-icon-zoom-in"></i>
                        </span>
                        <span
                            class="el-upload-list__item-delete"
                            @click="showCropDialog(file)"
                        >
                            <i class="el-icon-scissors"></i>
                        </span>
                        <span
                            class="el-upload-list__item-delete"
                            @click="handleRemove(file)"
                        >
                            <i class="el-icon-delete"></i>
                        </span>
                    </span>
                </div>

                <div class="main-img-box">
                    <slot name="mainImg" :file="file"></slot>
                </div>
            </div>
            <div slot="tip" class="el-upload__tip">{{tip}}</div>
        </el-upload>

        <slot name="width-tips" :files="widthTipFiles"></slot>

        <slot name="submit-btn" v-if="showSubmitBtn">
            <el-button type="primary" size="small" @click="submitUpload">
                确认上传图片
            </el-button>
        </slot>

        <el-dialog :visible.sync="dialogPreviewVisible" append-to-body>
            <img width="100%" :src="dialogPreviewUrl" alt />
        </el-dialog>

        <div v-for="(item, index) in needCropFiles" :key="item.uid">
            <el-dialog :visible.sync="item.visible" width="875px" append-to-body>
                <div style="width: 100%; height: 550px">
                    <vue-cropper
                        ref="cropper"
                        :auto-crop="cropOptions.autoCrop"
                        :fixed="cropOptions.fixed"
                        :fixed-box="cropOptions.fixedBox"
                        :high="cropOptions.high"
                        :center-box="cropOptions.centerBox"
                        :auto-crop-width="cropOptions.autoCropWidth"
                        :auto-crop-height="cropOptions.autoCropHeight"
                        :img="item.url"
                    />
                    <div style="margin-top: 10px; color: red">
                        *滚动鼠标可以对图片进行缩放操作
                    </div>
                </div>
                <span slot="footer">
                    <el-button type="primary" @click="cropFinish(item, index)">剪裁</el-button>
                    <el-button @click="closeCropDialog(item)">取消</el-button>
                </span>
            </el-dialog>
        </div>
    </div>
</template>

<script>
import {VueCropper} from 'vue-cropper';
import request from 'lib/utils/request';
import Tools from './tools';

export default {
    data() {
        return {
            // 预览弹框
            dialogPreviewUrl: '',
            dialogPreviewVisible: false,

            // 需要剪裁的图片
            needCropFiles: [],

            // 图片宽度不是 750px
            widthTipFiles: []
        };
    },
    props: {
        // 图片列表
        imgList: {
            type: Array,
            default: () => []
        },
        // 图片张数限制
        limit: {
            type: Number,
            default: 50
        },
        limitSize: {
            type: Number,
            default: 1
        },
        // 截图选项
        cropOptions: {
            type: Object,
            default: () => {
                return {
                    // 是否开启截图框宽高固定比例
                    fixed: false,
                    // 固定截图框大小 不允许改变
                    fixedBox: false,
                    // 是否默认生成截图框
                    autoCrop: true,
                    autoCropWidth: 750,
                    autoCropHeight: 500,
                    // 是否按照设备的dpr输出等比例图片
                    high: false,
                    // 截图框是否被限制在图片里面
                    centerBox: false
                };
            }
        },
        // 是否限制尺寸大小
        cropSize: {
            type: Boolean,
            default: true
        },
        showHandle: {
            type: Boolean,
            default: true
        },
        // 是否展示『确认上传图片』按钮
        showSubmitBtn: {
            type: Boolean,
            default: true
        },
        tip: {
            type: String,
            default: ''
        },
        showWidthTips: {
            type: Boolean,
            default: false
        },
        accept: {
            type: String,
            default: 'image/*'
        }
    },

    computed: {
        uploadFiles() {
            return this.$refs.upload.uploadFiles;
        },
        autoW() {
            return this.cropOptions.autoCropWidth;
        },
        autoH() {
            return this.cropOptions.autoCropHeight;
        }
    },
    watch: {
        imgList: {
            handler(val) {
                if (!val.length) {
                    this.widthTipFiles = [];
                }
            },
            immediate: true
        }
    },
    methods: {
        // 判断图片大小是否超过限制
        judgeSizeLt_nM(size) {
            return size < this.limitSize * 1024 * 1024;
        },

        // 错误提示信息
        tipsInfo(file = {}) {
            const {width, height, name = ''} = file;

            return {
                sizeTip: `大小超过 ${this.limitSize}M, 请压缩或剪裁后上传`,
                widthTip: `您上传的 ${name} 图片尺寸为 ${width}px*${height}px, 要求 ${this.autoW}px*${this.autoH}px`,
                typeTip: `文件类型必须为图片`
            };
        },

        // 上传前校验
        async beforeUpload(file) {
            const {typeTip} = this.tipsInfo();

            // 从 uploadFiles 取出的 file 具有更多信息
            let cFile = this.uploadFiles.find((v) => v.uid === file.uid);
            const isNeedCrop = this.judgeNeedCrop(cFile);
            if (isNeedCrop) {
                await this.getCroppedFile(cFile);
            }

            // 图片类型判断
            if (!Tools.isImageType(file.type)) {
                this.$message.error(typeTip);
                return false;
            } else {
                return true;
            }
        },

        // 手动上传图片
        async httpRequest(item) {
            const fd = new FormData();

            const cFile = this.uploadFiles.find((v) => v.uid === item.file.uid);
            fd.append('file', cFile.raw);

            const [err, res] = await this.uploadImg(fd);
            if (err) {
                this.$message.warning('上传失败');
                return false;
            }

            this.handleSuccess(res, cFile, this.uploadFiles);
        },
        uploadImg(formData) {
            return request
                .post('/admin/image/plainUpload.json', formData)
                .then((res) => [null, res])
                .catch((err) => [err, null]);
        },

        // 文件上传成功时的钩子
        handleSuccess(res, file, fileList) {
            if (res && res.ret) {
                file.url = res.msg;
                this.$emit('image-uploaded', fileList);
                this.$emit('image-change', fileList);
            }
        },

        // 添加文件、上传成功和上传失败时都会被调用
        async handleChange(file, fileList) {
            // file 状态为 success,是上传成功的状态
            if (file.status === 'success') {
                // fix 图片成功角标未更新
                this.$forceUpdate();
                return;
            }

            const readyFiles = fileList.filter(file => file.status === 'ready');

            for(let file of readyFiles) {
                const fileInfo = await Tools.getFileInfo(file);
                const isNeedCrop = this.judgeNeedCrop(fileInfo);

                if (isNeedCrop) {
                    file = await this.getCroppedFile(file);
                }
            }
        },

        debounce(fn, delay) {
            let timer = null;

            return function() {
                // eslint-disable-next-line no-invalid-this
                const context = this,
                    // eslint-disable-next-line prefer-rest-params
                    args = arguments;

                if(timer) {
                    clearTimeout(timer);
                }

                timer = setTimeout(() => {
                    fn.apply(context, args);
                    timer = null;
                }, delay);
            };
        },

        // 是否需要剪裁(不需要剪裁或宽高固定的剪裁)
        judgeNeedCrop(fileInfo) {
            const {width, height} = fileInfo;

            if (!this.cropSize) {
                return false;
            }
            return width !== this.autoW || height !== this.autoH;
        },

        judgeWidth750(fileInfo) {
            const index = this.getIndex(this.widthTipFiles, fileInfo);
            if (fileInfo.width !== 750) {
                if (index < 0) {
                    this.widthTipFiles.push(fileInfo);
                }
            } else {
                if(index >= 0) {
                    this.widthTipFiles.splice(index, 1);
                }
            }
        },

        // 弹框确认
        handleConfirmDialog(file) {
            const {widthTip} = this.tipsInfo(file);
            return new Promise((resolve, reject) => {
                this.$confirm(widthTip, {
                    confirmButtonText: '去剪裁',
                    cancelButtonText: '取消上传'
                }).then(
                    () => {
                        resolve(true);
                    },
                    () => {
                        this.uploadFiles.pop();
                        resolve(false);
                    }
                );
            });
        },

        // 获取剪裁后的图片
        async getCroppedFile(file) {
            const fileInfo = await Tools.getFileInfo(file);
            const isGoCrop = await this.handleConfirmDialog(fileInfo);
            // 去剪裁
            if (isGoCrop) {
                return new Promise((resolve, reject) => {
                    this.showCropDialog(file);
                    this.$on('cropFinish', (croppedFile) => {
                        resolve(croppedFile);
                    });
                });
            } else {
                return Promise.resolve(null);
            }
        },

        submitUpload() {
            this.$refs.upload.submit();
        },

        clearFiles() {
            this.$refs.upload.clearFiles();
        },

        // 剪裁
        cropFinish(file, index) {
            const $cropper = this.$refs.cropper[index];
            const $cropW = $cropper.cropW,
                $cropH = $cropper.cropH;

            $cropper.getCropBlob((data) => {
                const imgUrl = window.URL.createObjectURL(data);

                // 更新图片 raw、size、url 等
                file.raw = new Blob([data], {
                    type: data.type
                });
                file.url = imgUrl;
                file.size = data.size;
                // 这里设置 uid,为了 beforeUpload 时的 filter
                file.raw.uid = file.uid;
                file.status = 'ready';
                file.width = $cropW;
                file.height = $cropH;

                this.$emit('cropFinish', file);
                this.closeCropDialog(file);

                if(this.showWidthTips) {
                    this.judgeWidth750(file);
                }
            });
        },

        // 弹出剪裁框剪裁
        showCropDialog(file) {
            file.visible = true;

            const index = this.getIndex(this.needCropFiles, file);
            if (index < 0) {
                this.needCropFiles.push(file);
            }
        },

        getIndex(arr, file) {
            return arr.findIndex((v) => v.uid === file.uid);
        },

        // 关闭剪裁框
        closeCropDialog(file) {
            file.visible = false;
            this.needCropFiles = this.needCropFiles.filter((v) => v.visible);

            // Fix:$emit 一次 cropFinish 事件,$on 回调执行多次
            this.$off('cropFinish');
        },

        // 上传数量超出限制
        handleExceed(files, fileList) {
            const limit = this.limit,
                left = limit - fileList.length;
            this.$message.error(`上传数量限制${limit}张,还可上传${left}张`);
        },

        // 删除图片
        handleRemove(file) {
            const index = this.getIndex(this.uploadFiles, file);
            this.uploadFiles.splice(index, 1);

            if(this.showWidthTips) {
                const tId = this.getIndex(this.widthTipFiles, file);
                if(tId >= 0) {
                    this.widthTipFiles.splice(tId, 1);
                }
            }

            this.$emit('image-change', this.uploadFiles);
            this.$emit('image-removed', this.uploadFiles);
        },

        // 显示预览框
        showPreviewDialog(file) {
            this.dialogPreviewUrl = file.url;
            this.dialogPreviewVisible = true;
        }
    },
    components: {
        VueCropper
    }
};
</script>
<style lang="scss">
.error-tips {
  margin-bottom: 10px;
  line-height: 20px;
  font-size: 12px;
  color: #f56c6c;
}

.el-upload-list--picture-card {
  .el-upload-list__item-actions {
    height: 148px;
  }

  // 有主图插槽,图片高度为100px
  .main-wrap {
    height: 100px;
    overflow: hidden;
    .el-upload-list__item-thumbnail {
      width: 100%;
      height: auto;
    }

    .el-upload-list__item-actions {
      height: 100px;
    }
  }

  .main-img-box {
    line-height: 48px;
    text-align: center;
  }
}
</style>

tools.js


const Tools = {
    isImageType(type) {
        const reg = /^image\//;
        return reg.test(type);
    },

    // 获取 file 信息:width、height、url 等
    getFileInfo(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = (e) => {
                const img = new Image();
                img.src = e.target.result;

                img.onload = () => {
                    file.width = img.width;
                    file.height = img.height;

                    file.url = img.src;

                    resolve(file);
                };
                img.onerror = () => {
                    reject();
                };
            };
            reader.readAsDataURL(file.raw);
        });
    }
};

export default Tools;

相关文章

网友评论

      本文标题:图片上传并剪裁

      本文链接:https://www.haomeiwen.com/subject/rcubvktx.html