图片上传并剪裁

<!--
https://github.com/xyxiao001/vue-cropper 组件详细信息
description: 图片上传 支持批量上传,图片大小、尺寸校验  (主图插槽 -> mainImg)
@param imgList          Array       eg: [{url: imgUrl}]     必传
@param limit            Number      上传数量限制              50张
@param fixSize          Boolean     是否限制图片尺寸           限制
@param autoCropWidth    Number      图片要求宽度              750px
@param autoCropHeight   Number      图片要求高度              500px
@param limitSize        Number      最大尺寸                 1024kb
@param tip              String      上传说明                 ''
 -->
<template>
    <div>
        <el-upload
            action="#"
            :file-list="fileList"
            multiple
            ref="upload"
            :limit="limit"
            list-type="picture-card"
            :accept="accept"
            :auto-upload="false"
            :on-exceed="handleExceed"
            :on-success="handleSuccess"
            :on-change="handleChange"
            :http-request="httpRequest"
            :before-upload="beforeUpload"
        >
            <i slot="default" class="el-icon-plus"></i>
            <div slot="file" slot-scope="{file}">
                <img class="el-upload-list__item-thumbnail" :src="file.url" alt />
                <label
                    class="el-upload-list__item-status-label"
                    v-if="file.status === 'success' && !file.overSize "
                >
                    <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="handlePictureCardPreview(file)"
                    >
                        <i class="el-icon-zoom-in"></i>
                    </span>
                    <span class="el-upload-list__item-delete" @click="handleCrop(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>
                <slot name="mainImg" :file="file"></slot>
            </div>
            <div slot="tip" class="el-upload__tip">{{tip}}</div>
        </el-upload>
        <div class="error-tips" v-show="failedList.length > 0">
            <p>本次上传以下图片上传失败:</p>
            {{failedList.join(',')}}
            <p>请重新上传</p>
        </div>

        <el-button type="primary" size="small" @click="submitUpload">确认上传图片</el-button>

        <el-dialog :visible.sync="dialogVisible" append-to-body>
            <img width="100%" :src="dialogImageUrl" alt />
        </el-dialog>
        <el-dialog :visible.sync="dialogCropperVisible" width="880px" append-to-body>
            <div style="width:100%;height:550px;">
                <vue-cropper
                    auto-crop
                    mode="100% auto"
                    :info-true="true"
                    :high="false"
                    :fixed-box="fixedBox"
                    :auto-crop-width="autoCropWidth"
                    :auto-crop-height="autoCropHeight"
                    ref="cropper"
                    :original="true"
                    :img="dialogCropperUrl"
                />
                <div style="margin-top: 10px; color: red">*滚动鼠标可以对图片进行缩放操作</div>
            </div>
            <span slot="footer">
                <el-button type="primary" @click="cropFinish">确定</el-button>
                <el-button @click="cancelCrop">取消</el-button>
            </span>
        </el-dialog>
    </div>
</template>

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

export default {
    data() {
        return {
            fileList: [],
            dialogImageUrl: '',
            dialogVisible: false,
            dialogCropperUrl: '',
            cropFile: '',
            dialogCropperVisible: false,
            showImageDialog: false,
            failedList: []
        };
    },
    props: {
        // 图片列表
        imgList: {
            type: Array,
            default: () => []
        },
        // 图片张数限制
        limit: {
            type: Number,
            default: 5
        },
        limitSize: {
            type: Number,
            default: 1
        },
        autoCropWidth: {
            type: Number,
            default: 750
        },
        autoCropHeight: {
            type: Number,
            default: 500
        },
        // 是否限制尺寸大小
        fixSize: {
            type: Boolean,
            default: true
        },
        showHandle: {
            type: Boolean,
            default: true
        },
        // 是否固定截图框大小,不允许改变
        fixedBox: {
            type: Boolean,
            default: false
        },
        tip: {
            type: String,
            default: ''
        },
        accept: {
            type: String,
            default: 'image/*'
        }
    },
    watch: {
        imgList: {
            handler(val = [], oldVal = []) {
                if(this.arrayEquals(val, oldVal)) {
                    return;
                }

                this.getFileList(val).then(res => {
                    this.fileList = res;
                });
            },
            immediate: true
        }
    },
    computed: {
        files() {
            return this.$refs.upload.uploadFiles;
        }
    },
    methods: {
        judgeSizeLt_nM(size) {
            return size < this.limitSize * 1024 * 1024;
        },
        beforeUpload(file) {
            const fileType = file.type;
            if (!this.judgeSizeLt_nM(file.size)) {
                this.$message.warning(`图片大小不能超过${this.limitSize}M, 上传前请压缩`);
                return false;
            }
            const reg = /image\//g;
            if (!reg.test(fileType) && (this.accept !== 'image/*' || this.accept.indexOf(fileType) === -1)) {
                this.$message.error(`文件类型必须为${this.accept}图片`);
                return false;
            }
            return true;
        },
        getFileList(val) {
            const list = val.map(async v => {
                const {raw} = await this.getRawFile(v.url || v);
                return {
                    raw,
                    ...v
                };
            });

            return Promise.allSettled(list).then(res => {
                return res.filter(p => p.status === 'fulfilled').map(v => {
                    return {
                        v,
                        ...v.value
                    };
                });
            }).catch(err => {
                console.log(err);
            });
        },
        async httpRequest(item) {
            const fd = new FormData();
            fd.append('file', item.file);
            const {size} = item.file;
            if (!this.judgeSizeLt_nM(size)) {
                return false;
            }

            const {width, height} = await this.getImgSize(item.file);

            const isCropSize =
                width === this.autoCropWidth && height === this.autoCropHeight;
            if (!isCropSize && this.fixSize) {
                this.$alert(`当前尺寸${width}*${height}, 请裁剪后上传`);
                const uploadFiles = this.$refs.upload.uploadFiles;
                this.$refs.upload.uploadFiles = uploadFiles.map(v => {
                    if (v.uid === item.file.uid) {
                        v.overSize = true;
                    }
                    return v;
                });
                return false;
            }
            // /admin/image/uploads.json 多张图片上传
            const [err, res] = await this.uploadImg(fd);
            if(err) {
                this.$message.warning('上传失败');
                return false;
            }
            item.onSuccess(res);
        },
        // 比较两个非对象数组是否相等
        arrayEquals(arr1, arr2) {
            return arr1.length == arr2.length && arr1.every((v, i) => {
                return arr2.some(vv => JSON.stringify(vv) === JSON.stringify(v));
            });
        },
        uploadImg(formData) {
            return request.post('/admin/image/plainUpload.json', formData)
                .then(res => [null, res])
                .catch(err => [err, null]);
        },
        submitUpload() {
            this.$refs.upload.submit();
        },
        clearFiles() {
            this.$refs.upload.clearFiles();
            this.fileList = [];
        },
        handleChange(file, fileList) {
            const failedList = [];
            const result = this.files.filter(item=> {
                if(item.status === 'success' || item.status === 'ready') {
                    return true;
                }else {
                    failedList.push(item.name);
                    return false;
                }
            });
            result.forEach(item=> {
                if(item.response && item.response.ret && item.response.msg) {
                    item.url = item.response.msg;
                }
            });
            this.failedList = failedList.slice(0);
            this.$emit('image-change', result);
        },
        handleExceed(files, fileList) {
            this.$message.error(
                `上传数量限制${this.limit}张,还可上传${this.limit -
                    fileList.length}张`
            );
        },
        // 删除图片
        handleRemove(file) {
            const uploadFiles = this.$refs.upload.uploadFiles;
            const index = uploadFiles.findIndex(v => v.uid === file.uid);
            uploadFiles.splice(index, 1);

            this.fileList = uploadFiles;

            this.$emit('image-change', this.fileList);
        },
        handlePictureCardPreview(file) {
            this.dialogImageUrl = file.url;
            this.dialogVisible = true;
        },
        handleCrop(file) {
            this.dialogCropperUrl = file.url;
            this.dialogCropperVisible = true;
            this.cropFile = file;
        },
        cropFinish() {
            this.$refs.cropper.getCropBlob(data => {
                const imgUrl = window.URL.createObjectURL(data);
                this.cropFile.raw = new Blob([data], {
                    type: this.cropFile.raw.type
                });
                // 不设置uid无法触发on-success
                this.cropFile.raw.uid = this.cropFile.uid;
                this.cropFile.url = imgUrl;
                this.cropFile.status = 'ready';
            });
            this.dialogCropperVisible = false;
        },
        cancelCrop() {
            this.dialogCropperVisible = false;
        },
        convertBase64UrlToBlob(base64) {
            const urlData = base64.dataUrl,
                type = base64.type,
                // 去掉url的头,并转换为byte
                bytes = window.atob(urlData.split(',')[1]),
                ab = new ArrayBuffer(bytes.length),
                ia = new Uint8Array(ab);

            // 处理异常,将ascii码小于0的转换为大于0
            for (let i = 0; i < bytes.length; i++) {
                ia[i] = bytes.charCodeAt(i);
            }

            return new Blob([ab], {type});
        },
        getBase64Image(img) {
            const canvas = document.createElement('canvas');

            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, img.width, img.height);
            const ext = img.src
                .substring(img.src.lastIndexOf('.') + 1)
                .toLowerCase();

            const dataUrl = canvas.toDataURL('image/' + ext);

            return {
                dataUrl,
                type: 'image/' + ext
            };
        },
        getRawFile(url) {
            return new Promise((resolve, reject) => {
                const img = new Image();
                img.src = url;
                img.crossOrigin = '';
                img.onload = () => {
                    const base64 = this.getBase64Image(img);
                    const raw = this.convertBase64UrlToBlob(base64);

                    resolve({raw, url});
                };

                img.onerror = () => {
                    const defaultUrl = '//s.qunarzz.com/package/touch/apphome/s/bg.jpg';

                    img.src = defaultUrl;
                    const base64 = this.getBase64Image(img);
                    const raw = this.convertBase64UrlToBlob(base64);

                    resolve({raw, url: defaultUrl});

                    reject(new Error(`图片${url}链接有误,已过滤`));
                };
            });
        },
        getImgSize(file) {
            return new Promise(resolve => {
                const reader = new FileReader();
                reader.onload = e => {
                    const data = e.target.result;
                    const img = new Image();
                    img.src = data;
                    img.onload = () => {
                        resolve({width: img.width, height: img.height});
                    };
                };
                reader.readAsDataURL(file);
            });
        }
    },
    components: {
        VueCropper
    }
};
</script>
<style lang="scss" scoped>
.error-tips{
    margin-bottom: 10px;
    line-height: 20px;
    font-size: 12px;
    color:#F56C6C;
}
</style>

推荐阅读更多精彩内容

  • 《汤姆叔叔的小屋》 世界上有这样一些幸福的人,他们把自己的痛苦化作他人的幸福,他们挥泪埋葬了自己在尘世间的希望,它...
    简微柠Jean阅读 4,156评论 4 164
  • 1.我自是光亮。 2.新世纪温柔成熟知性的独立女青年。 3.生活可以五颜六色,不可以乱七八糟。 4.慢慢理解世界,...
    Tc荼茶阅读 2,511评论 0 93
  • 前段时间,背着老公偷偷存了几千块钱,想着夫妻再亲密,也要存点私封钱以备不时之需,谁想到,自己就是个蠢货,钱没存着,...
    俏郡主阅读 3,378评论 43 124
  • 自从开始尝试写作后,我每天的想法便是:“希望哪天可以靠稿费自给自足!” 不求一夜暴富,但求哪一日靠写写写,就能过上...
    怡安呀阅读 11,196评论 98 584
  • 有种男生,总是能把爱情,活生生处成友情。 还有一种男生,则是情感洁癖,他一生只想爱一个人。 一、总是能把爱情处成友...
    Demo呆某人阅读 4,623评论 36 121