3 - vue图片上传、裁剪、删除、点击放大、拖拽交换位置

723 阅读1分钟

码云地址:gitee.com/mayxue/vue2…

效果:

  1. 上传:

image.png

  1. 裁剪:

image.png

  1. 点击图片预览效果:

image.png

一、需要安装的插件:

1.1 图片放大预览插件:v-viewer

npm install v-viewer --save

1.2 图片裁剪插件:vue-cropper

官方文档:github.com/xyxiao001/v…

npm install vue-cropper
或 yarn add vue-cropper

二、组件:

2.1 图片裁剪弹窗组件:components -> cropper -> index.vue

<template>
  <!-- 图片裁剪弹窗-组件 -->
  <div class="cropper_model">
    <el-dialog
      title="图片剪裁"
      width="700px"
      top="50px"
      class="cropper_model_dlg"
      :visible.sync="dialogVisible"
      append-to-body
      :close-on-click-modal="false"
      :close-on-press-escape="false"
    >
      <div class="cropper_content">
        <div class="cropper" style="text-align: center">
          <vueCropper
            ref="cropper"
            :img="options.img"
            :outputSize="options.outputSize"
            :outputType="options.outputType"
            :info="options.info"
            :canScale="options.canScale"
            :autoCrop="options.autoCrop"
            :autoCropWidth="options.autoCropWidth"
            :autoCropHeight="options.autoCropHeight"
            :fixed="options.fixed"
            :fixedBox="options.fixedBox"
            :fixedNumber="options.fixedNumber"
            @realTime="previewImg"
          >
          </vueCropper>
          <div class="cropper_btns">
            <el-button type="blue" @click="goUpload" size="mini">
              重新上传
            </el-button>
            <el-button
              @click="rotateLeft"
              icon="el-icon-refresh-left"
              size="mini"
              title="左旋转"
            >
            </el-button>
            <el-button
              @click="rotateRight"
              icon="el-icon-refresh-right"
              size="mini"
              title="右旋转"
            >
            </el-button>
            <el-button @click="changeScale(1)" size="mini" title="放大">
              +
            </el-button>
            <el-button @click="changeScale(-1)" size="mini" title="缩小">
              -
            </el-button>
            <el-button
              type="primary"
              size="mini"
              @click="uploadImg"
              :loading="loading"
            >
              确定上传
            </el-button>
          </div>
        </div>
        <!-- <div class="cropper_right">
          <h3>预览</h3>
          <div
            class="cropper_preview"
            :style="{
              width: preview.w + 'px',
              height: preview.h + 'px',
              overflow: 'hidden',
              margin: '5px',
            }"
          >
            <div :style="preview.div">
              <img :src="preview.url" :style="preview.img" alt="" />
            </div>
          </div>
          <div style="margin-top: 100px">
            <el-button type="primary" @click="uploadImg" :loading="loading">
              确定上传
            </el-button>
          </div>
        </div> -->
      </div>
      <!-- <div slot="footer" class="dialog-footer">
        <el-button @click="downLoad('blob')">下载</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="uploadImg" :loading="loading">
          确认
        </el-button>
      </div> -->
    </el-dialog>
  </div>
</template>

<script>
import { VueCropper } from "vue-cropper";
export default {
  components: { VueCropper },
  props: {
    imgCropperOptions: {},
  },
  data() {
    return {
      dialogVisible: false,
      loading: false,
      options: {
        img: "", // 裁剪图片的地址
        outputSize: 1, // 裁剪生成图片的质量
        outputType: "png", // 裁剪生成图片的格式
        info: true, // 裁剪框的大小信息
        canScale: true, // 图片是否允许滚动缩放
        autoCrop: true, // 是否默认生成截图狂
        autoCropWidth: 100, // 默认生成截图框宽度
        autoCropHeight: 100, // 默认生成截图框高度
        fixed: true, // 是否开启截图框宽高固定比例
        fixedNumber: [1, 1], // 截图框的宽高比例
        full: true, // 是否输出原图比例的截图
        fixedBox: true, // 固定截图框大小 不允许改变
        canMove: true, // 上传图片是否可以移动
        canMoveBox: true, // 截图框能否拖动
        original: true, // 上传图片按照原始比例渲染
        centerBox: false, // 截图框是否被限制在图片里面
        high: false, // 是否按照设备的dpr输出等比例图片
        infoTrue: true, // true为展示真实输出图片宽高false展示看到的截图框宽高
        maximgSize: 150, // 限制图片最大宽度和高度
        enlarge: 1, // 图片根据截图框输出比例倍数
        mode: "contain", // 图片默认渲染方式(contain, cover, 100px, 100% auto)
      },
      preview: {},
    };
  },
  methods: {
    open(data) {
      console.log("open-data", data);
      this.options.img = window.URL.createObjectURL(data);
      this.dialogVisible = true;
      console.log("this.imgCropperOptions", this.imgCropperOptions);
      if (this.imgCropperOptions) {
        this.options.autoCropWidth = this.imgCropperOptions.autoCropWidth;
        this.options.autoCropHeight = this.imgCropperOptions.autoCropWidth;
        this.options.maximgSize = this.imgCropperOptions.maximgSize;
      }
    },
    close() {
      this.dialogVisible = false;
    },
    // base64转图片文件
    dataURLtoFile(dataurl, filename) {
      let arr = dataurl.split(",");
      let mime = arr[0].match(/:(.*?);/)[1];
      let bstr = atob(arr[1]);
      let len = bstr.length;
      let u8arr = new Uint8Array(len);
      while (len--) {
        u8arr[len] = bstr.charCodeAt(len);
      }
      return new File([u8arr], filename, { type: mime });
    },

    downLoad(type) {
      event.preventDefault();
      const aLink = document.createElement("a");
      if (type === "blob") {
        this.$refs.cropper.getCropBlob((data) => {
          let downImg = window.URL.createObjectURL(data);
          aLink.download = "photo.png";
          aLink.href = downImg;
          aLink.click();
        });
      } else {
        this.$refs.cropper.getCropData((data) => {
          let file = this.dataURLtoFile(data, "test");
          aLink.href = file;
          aLink.click();
        });
      }
    },

    // 左旋转
    rotateLeft() {
      this.$refs.cropper.rotateLeft();
    },
    // 右旋转
    rotateRight() {
      this.$refs.cropper.rotateRight();
    },
    // 放大缩小
    changeScale(num) {
      num = num || 1;
      this.$refs.cropper.changeScale(num);
    },
    // 实时预览
    previewImg(data) {
      //console.log('data', data);
      this.preview = data;
      //if(this.preview.img)
    },
    goUpload() {
      this.$emit("upAgain");
    },
    // 上传图片
    uploadImg() {
      let self = this;
      //self.loading = true;
      this.$refs.cropper.getCropData((data) => {
        let file = this.dataURLtoFile(data, "photo.png");
        // 生成文件类型
        //self.loading = false;
        this.$emit("getFile", file);
      });
    },
    //自定义上传,裁剪后调用
  },
};
</script>

<style lang="scss" scoped>
.el-dialog__wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
}
.cropper_model_dlg {
  .cropper_content {
    margin: 0 auto;
    width: 650px;
    height: 650px;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
  }
  .cropper {
    width: 600px;
    height: 600px;
    background: yellow;
  }
  .cropper_right {
    width: 300px;
    text-align: center;
  }
  .cropper_preview {
    margin: 0 auto;
    display: inline-block;
    border: 1px solid #ddd;
  }
  .cropper_btns {
    margin-top: 20px;
  }
  .previewImg {
    width: 100%;
    height: 100%;
  }
}
</style>

2.2 图片拖拽交换位置组件:components -> dragImgItem -> index.vue

<template>
  <!-- 图片拖拽交换位置组件 -->
  <div
    class="imgItem"
    @dragstart="dragStartHandler"
    @dragover.prevent="dragOverHandler"
    @dragenter="dragEnterHandler"
    @dragend="dragEndHandler"
    :style="{
      width: imgWidth,
      height: imgHeight,
    }"
  >
    <img
      class="bgImg"
      :src="imgSrc"
      alt=""
      :width="imgWidth"
      :height="imgHeight"
    />
    <em class="imgClose" @click="removeImg"></em>
  </div>
</template>
<script>
export default {
  name: "dragImgItem",
  props: {
    //图片对应的下标
    index: {
      type: Number,
      required: true,
    },
    //图片回显的路径
    imgSrc: {
      type: String,
      required: true,
    },
    //图片回显的宽度
    imgWidth: {
      default: "150px",
    },
    //图片回显的高度
    imgHeight: {
      default: "150px",
    },
  },
  methods: {
    // 用户开始拖动元素时触发
    dragStartHandler() {
      this.$emit("setCurDragImg", this.index);
    },

    //用户完成元素拖动后触发
    dragEndHandler(e) {
      this.$emit("clearCurDragImg");
    },

    // 当某被拖动的对象在另一对象容器范围内拖动时触发此事件
    dragOverHandler(e) {
      e.dataTransfer.dropEffect = "move";
    },

    // 当被鼠标拖动的对象进入其容器范围内时触发此事件
    dragEnterHandler(e) {
      console.log("e", e);
      e.dataTransfer.effectAllowed = "move";
      this.$emit("dragImg", this.index);
    },

    //删除图片事件
    removeImg() {
      this.$emit("removeImg");
    },
  },
};
</script>
<style scoped lang="scss">
.imgItem {
  position: relative;
  height: 150px;
  width: 150px;
  display: inline-block;
  border-radius: 5px;
  float: left;
  margin: 0 15px 15px 0;
  .bgImg {
    height: 150px;
    width: 150px;
    object-fit: cover;
    border: 1px solid #dcdfe6;
    border-radius: 5px;
  }
  .imgClose {
    position: absolute;
    top: 0;
    right: 0;
    width: 30px;
    height: 30px;
    margin-top: -13px;
    margin-right: -13px;
    cursor: pointer;
    background: url("~@/assets/images/imgClose.png") top left
      no-repeat;
    background-size: cover;
  }
}
</style>

三、在页面中上传图片

<template>
  <div class="imgUploadDemo">
    <el-form
      :model="form"
      ref="ruleForm"
      label-width="180px"
      class="form"
      size="small"
    >
      <el-form-item label="商品列表封面图:" prop="default_image">
        <div class="imgUpload">
          <el-upload
            ref="defaultImgUpload"
            action
            :auto-upload="false"
            :show-file-list="false"
            list-type="picture-card"
            :on-change="handleDefaultSuccess"
          >
            <img
              v-if="form.default_image"
              :src="form.default_image"
              class="defaultImg"
            />
            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
          </el-upload>
          <div class="imgSize">
            (建议尺寸为220像素*220像素长宽比例1:1的图片)
          </div>
        </div>
      </el-form-item>

      <el-form-item label="商户详情页头部轮播图:" prop="swiperList">
        <div class="imgUpload">
          <div class="merchantDetailImg imgDiv">
            <viewer :images="form.swiperList">
              <dragImgItem
                class="imgItem"
                v-for="(item, index) of form.swiperList"
                :key="index"
                :index="index"
                :imgSrc="item"
                @setCurDragImg="setCurDragImg(index, 1)"
                @clearCurDragImg="clearCurDragImg(1)"
                @dragImg="dragSwiperImg(index, 1)"
                @removeImg="removeSwiper(item)"
              >
              </dragImgItem>
            </viewer>
          </div>
          <div class="merchantDetailUpload">
            <el-upload
              action
              ref="upload"
              :auto-upload="false"
              :show-file-list="false"
              list-type="picture-card"
              :on-change="handleSwiperSuccess"
            >
              <i class="el-icon-plus avatar-uploader-icon"></i>
            </el-upload>
          </div>
        </div>
        <div style="clear: both"></div>
        <div class="imgSize">
          (建议尺寸为600像素*600像素长宽比例1:1的图片),支持长按鼠标左键拖拽图片交换位置
        </div>
      </el-form-item>
    </el-form>
    <!-- 图片裁剪弹窗-start -->
    <ImgCropper
      ref="myCropper"
      :imgCropperOptions="imgCropperOptions"
      @getFile="getFile"
      @upAgain="upAgain"
    ></ImgCropper>
    <!-- 图片裁剪弹窗-end -->
  </div>
</template>
<script>
import dragImgItem from "@/components/dragImgItem"; //图片拖拽交换位置组件
import ImgCropper from "@/components/cropper"; //图片裁剪弹窗
//图片预览插件
import Vue from "vue";
import Viewer from "v-viewer";
import "viewerjs/dist/viewer.css";
Vue.use(Viewer);

export default {
  name: "imgUploadDemo",
  components: {
    dragImgItem,
    ImgCropper,
  },
  data() {
    return {
      imgCropperOptions: {
        autoCropWidth: 600, // 默认生成截图框宽度
        autoCropHeight: 600, // 默认生成截图框高度
        maximgSize: 600, // 限制图片最大宽度和高度
      },
      // 图片列表
      form: {
        default_image: "",
        swiperList: [
          "https://circle.sutpay.cn/youhuiquanyi/6083782831/goods_image463321.png",
          "https://circle.sutpay.cn/youhuiquanyi/6083782831/goods_image296439.png",
          "https://circle.sutpay.cn/youhuiquanyi/6083782831/goods_image144881.png",
          "https://circle.sutpay.cn/youhuiquanyi/6083782831/goods_image098233.png",
        ],
      },
      curSwiperImgIndex: null, // 当前被拖动图片(商户详情页头部轮播图)的index
      imgUploadType: 1,
    };
  },
  methods: {
    setCurDragImg(index, type) {
      console.log("正在被拖动的图片-index", index);
      this.curSwiperImgIndex = index;
    },
    clearCurDragImg(type) {
      console.log("clearCurDragImg-type", type);
      this.curSwiperImgIndex = null;
    },

    //当前拖拽的图片进入到要交互的图片位置时触发事件
    dragSwiperImg(index) {
      // 判断被拖拽的图片与撞击的图片是否相同(通过图片的下标判断)
      if (index === this.curSwiperImgIndex) return;

      let newList = [...this.form.swiperList];
      let curImg = this.form.swiperList[this.curSwiperImgIndex]; // 当前拖动的图片
      let hitImg = this.form.swiperList[index]; // 被撞击的图片

      console.log("商户详情页头部轮播图-curImg", curImg);
      console.log("商户详情页头部轮播图-hitImg", hitImg);
      // 两张图片交换位置
      if (curImg) {
        newList.splice(this.curSwiperImgIndex, 1, hitImg);
        newList.splice(index, 1, curImg);
        // 修改当前拖动图片的index,此时已经变成被撞击图片的位置了
        this.curSwiperImgIndex = index;
        this.form.swiperList = newList;
        console.log("this.form.swiperList", this.form.swiperList);
      } else {
        this.$message.error("仅支持同类型的的图片拖拽交换位置!");
      }
    },

    //设置图片裁剪弹窗参数
    setImgCropperOptions(size) {
      console.log("设置图片裁剪弹窗参数size", size);
      this.imgCropperOptions.autoCropWidth = size;
      this.imgCropperOptions.autoCropHeight = size;
    },

    //上传商户详情头部轮播图
    async handleSwiperSuccess(res, file) {
      this.imgUploadType = 2;
      await this.setImgCropperOptions(600);
      this.$nextTick(() => {
        this.$refs.myCropper.open(res.raw || res);
      });
    },

    //上传封面图
    async handleDefaultSuccess(res, file) {
      this.imgUploadType = 1;
      await this.setImgCropperOptions(220);
      this.$nextTick(() => {
        this.$refs.myCropper.open(res.raw || res);
      });
    },

    //删除商户详情页头部轮播图
    removeSwiper(itemImg) {
      console.log("删除商品详情页头部轮播图-itemImg", itemImg);
      this.form.swiperList = this.form.swiperList.filter(
        (item, index) => {
          return item != itemImg;
        }
      );
      console.log("this.form.swiperList", this.form.swiperList);
    },

    // 点击弹框重新时触发
    upAgain() {
      if (this.imgUploadType == 1) {
        this.$refs["defaultImgUpload"].$refs[
          "upload-inner"
        ].handleClick();
      } else if (this.imgUploadType == 2) {
        this.$refs["upload"].$refs["upload-inner"].handleClick();
      }
    },

    //裁剪弹窗-确定上传回调事件
    getFile(file) {
      //如果是想拿到本地的图片信息则可通过file拿到
      console.log("file", file);
      console.log("url", URL.createObjectURL(file));
      if (this.imgUploadType == 1) {
        this.form.default_image = URL.createObjectURL(file);
      } else if (this.imgUploadType == 2) {
        this.form.swiperList.push(URL.createObjectURL(file)); //本地的图片回显
      }
      //关闭裁剪弹窗
      this.$refs.myCropper.close();

      //如果是需要拿到图片上传到服务器上后的地址,就需要通过ajax请求
      //this.uploadImage(file,this.imgUploadType);
    },

    //图片上传请求
    uploadImage(res, imgUploadType) {
      const isImgType =
        res.type === "image/bmp" ||
        "image/png" ||
        "image/jpeg" ||
        "image/jpg";
      if (!isImgType) {
        return this.$message.error(
          "上传图片只能是bmp/image/jpeg/png/jpg格式!"
        );
      }
      let formdata = new FormData();
      formdata.append("img", res);
      this.$refs.myCropper.loading = true;
      this.$axios
        .post(`/xx/upload_image/`, formdata)
        .then((res) => {
          if (imgUploadType == 1) {
            this.form.default_image = res.img_id;
          } else if (imgUploadType == 2) {
            this.form.swiperList.push(res.img_id);
          }
          //关闭裁剪弹窗
          this.$refs.myCropper.close();
        })
        .catch((err) => {
          if (err.response) {
            this.$message.error(
              err.response.data.error || err.response.data.detail
            );
          }
        })
        .finally(() => {
          this.$refs.myCropper.loading = false;
        });
    },
  },
};
</script>
<style scoped lang="scss">
.imgUploadDemo {
  padding: 100px;
  .merchantDetailImg {
    margin: 10px 0;
    .imgItem {
      position: relative;
      height: 150px;
      width: 150px;
      display: inline-block;
      border-radius: 5px;
      float: left;
      margin: 0 15px 15px 0;
      .bgImg {
        height: 150px;
        width: 150px;
        object-fit: cover;
        border: 1px solid #dcdfe6;
        border-radius: 5px;
      }
      .imgClose {
        position: absolute;
        top: 0;
        right: 0;
        width: 30px;
        margin-top: -13px;
        margin-right: -13px;
        cursor: pointer;
      }
    }
  }
  .imgSize {
    font-size: 12px;
    line-height: 12px;
    margin-top: 0;
    float: left;
  }
  .defaultImg {
    height: 150px;
    width: 150px;
    object-fit: cover;
    border-radius: 5px;
  }
}
.imgUploadDemo ::v-deep .el-upload--picture-card {
  margin-bottom: 15px;
  border: 1px solid #c0ccda;
  border-radius: 5px;
  overflow: hidden;
}
</style>