vue3+element-plus+vue-cropper实现裁剪图片上传

news2024/12/24 9:14:19

1.vue3+element-plus+vue-cropper实现裁剪图片

  • element-UI官网
  • element-plus官网
  • vue-cropper
  • vue3使用vue-cropper安装:npm install vue-cropper@next

2.vue-cropper插件:

 <vue-cropper :img="option.img" />
 <script setup>
     import {reactive} from "vue";
     // 裁剪的配置
     const option = reactive({
         img:'https://img1.baidu.com/it/u=4049022245,514596079&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500'
     })
 </script>
 

3.效果图:

在这里插入图片描述

4.实现cropperUpload组件:

<template>
  <div class="uploadMian">
    <div class="img-item" v-for="(item, index) in fileList" :key="index">
      <img :src="item.src" />
      <el-icon class="uploader-close" @click="delFn(index)"><Close /></el-icon>
      <div v-if="item.isSuccess" class="uploader-Check"><el-icon ><Check /></el-icon></div>
      <div class="button-div" v-if="item.file && isCropper">
        <el-button type="success" @click="uploadFileFn(item, index)"
          >上传</el-button
        >
        <el-button type="primary" @click="cropperFn(item, index)"
          >裁剪</el-button
        >
      </div>
    </div>
    <el-upload
      v-if="multiple || (!multiple && fileList.length == 0)"
      class="avatar-uploader"
      action="#"
      :accept="
        acceptArray.length > 0
          ? acceptArray.map((n) => acceptType[n]).join(',')
          : '*'
      "
      :http-request="!isCropper ? uploadFileFn : () => {}"
      :multiple="multiple"
      :show-file-list="false"
      :before-upload="beforeAvatarUpload"
    >
      <el-icon class="avatar-uploader-icon"><Plus /></el-icon>
    </el-upload>
  </div>

  <el-dialog title="裁切图片" v-model="showCropper" width="550px">
    <div class="cropper-content">
      <div class="cropper-box">
        <div class="cropper">
          <vue-cropper
            ref="cropperRefs"
            :img="option.img"
            :output-size="option.outputSize"
            :info="option.info"
            :can-scale="option.canScale"
            :auto-crop="option.autoCrop"
            :auto-crop-width="option.autoCropWidth"
            :auto-crop-height="option.autoCropHeight"
            :fixed="option.fixed"
            :fixed-number="option.fixedNumber"
            :full="option.full"
            :fixed-box="option.fixedBox"
            :can-move="option.canMove"
            :can-move-box="option.canMoveBox"
            :original="option.original"
            :center-box="option.centerBox"
            :height="option.height"
            :info-true="option.infoTrue"
            :max-img-size="option.maxImgSize"
            :enlarge="option.enlarge"
            :mode="option.mode"
            :limit-min-size="option.limitMinSize"
          />
        </div>
      </div>
    </div>
    <span slot="footer">
      <div class="dialog-footer">
        <el-button @click="showCropper = false">取 消</el-button>
        <el-button type="primary" @click="onSubmit">确 定</el-button>
      </div>
    </span>
  </el-dialog>
</template>

<script setup>
import { ref, reactive, watch } from "vue";
import { Plus, Close,Check } from "@element-plus/icons-vue";
import "vue-cropper/dist/index.css";
import { VueCropper } from "vue-cropper";
const props = defineProps({
  // 额外值
  otherData: {
    type: Object,
    default: () => {},
  },
  // 请求头
  headers: {
    type: Object,
    default: () => {},
  },
  //  参数值
  modelValue: {
    type: Array,
    default: () => {
      return [];
    },
  },
  // 多选
  multiple: {
    type: Boolean,
    default: false,
  },
  //   大小限制:10 * 1024 * 1024 = 10MB
  size: {
    type: Number,
    default: 10 * 1024 * 1024,
  },
  // 是否需要裁剪
  isCropper: {
    type: Boolean,
    default: true,
  },
  // 请求的url
  sendUrl: {
    type: String,
    default: "",
  },
});
const emits = defineEmits(["update:modelValue"]);
const cropperRefs = ref();
const cropperCb = ref(null);
const showCropper = ref(false);
let fileList = reactive([]);
const acceptArray = reactive(["png", "jpg", "jpeg"]); //选择类型
const acceptType = reactive({
  doc: "application/msword",
  docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  ppt: "application/vnd.ms-powerpoint",
  pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
  xls: "application/vnd.ms-excel",
  xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  pdf: "application/pdf",
  csv: ".csv",
  txt: "text/plain",
  image: "image/*",
  png: "image/png",
  gif: "image/gif",
  jpg: "image/jpg",
  jpeg: "image/jpeg",
});

// 监听传入的值
watch(
  props.modelValue,
  (value) => {
    const valueList = value || [];
    let newFileList = [];
    valueList.forEach((item) => {
      const indexThis=fileList.findIndex(n=>n.src==item)
      if(indexThis==-1){
        newFileList.push({
            src: item,
            isSuccess: true,
        });
      }
    });
    fileList.unshift(...newFileList);
  },
  { immediate: true, deep: true }
);

// 监听当前页面的fileList
watch(
  fileList,
  (value) => {
    const valueList = value
      .map((n) => {
        if (n.isSuccess) {
          return n.src;
        }
        return null;
      })
      .filter((n) => n != null);
    emits("update:modelValue", valueList);
  },
  { deep: true }
);

// 裁剪的配置
const option = reactive({
  img: "", // 裁剪图片的地址
  outputSize: 1, // 裁剪生成图片的质量(可选0.1 - 1)
  outputType: "jpeg", // 裁剪生成图片的格式(jpeg || png || webp)
  info: false, // 图片大小信息
  canScale: true, // 图片是否允许滚轮缩放
  autoCrop: true, // 是否默认生成截图框
  autoCropWidth: 230, //默认生成截图框宽度
  autoCropHeight: 150, //默认生成截图框高度
  fixed: false, // 是否开启截图框宽高固定比例
  fixedNumber: [1.53, 1], //截图框的宽高比例
  full: false, // false按原比例裁切图片,不失真
  fixedBox: false, // 固定截图框大小,不允许改变
  canMove: true, // 上传图片是否可以移动
  canMoveBox: true, // 截图框能否拖动
  original: true, // 上传图片按照原始比例渲染
  centerBox: true, // 截图框是否被限制在图片里面
  high: false, // 是否按照设备的dpr 输出等比例图片
  infoTrue: false, // true为展示真实输出图片宽高,false展示看到的截图框宽高
  maxImgSize: 3000, // 限制图片最大宽度和高度
  enlarge: 1, // 图片根据截图框输出比例倍数
  mode: "550px 400px", // 图片默认渲染方式
  limitMinSize: [108, 108], // 裁剪框限制最小区域
  minCropBoxWidth: 108, // 设置最小裁切框宽度
  minCropBoxHeight: 108, // 设置最小裁切框高度
});

// 类型,大小判断
const judegFileSize = (file) => {
  const filterSize = (size) => {
    const pow1024 = (num) => {
      return Math.pow(1024, num);
    };
    if (!size) return "";
    if (size < pow1024(1)) return size + " B";
    if (size < pow1024(2)) return (size / pow1024(1)).toFixed(0) + " KB";
    if (size < pow1024(3)) return (size / pow1024(2)).toFixed(0) + " MB";
    if (size < pow1024(4)) return (size / pow1024(3)).toFixed(0) + " GB";
    return (size / pow1024(4)).toFixed(2) + " TB";
  };
  let retunBoolean = true;
  let fileSize = file.size;
  //判断文件类型
  const fileExtArray = file.name.split(".");
  const judegFn = () => {
    if (acceptArray.indexOf(fileExtArray.at(-1)) == -1) {
      alert(`${file.name}上传失败,只能上传${acceptArray.join("、")}`);
      retunBoolean = false;
    }
  };
  if (acceptArray.length > 0) {
    if (acceptArray.indexOf("image") != -1) {
      var pattern = /(\.jpg|\.jpeg|\.png|\.gif)$/i;
      // 判断文件名是否匹配图片格式的正则表达式
      if (!pattern.test(`.${fileExtArray.at(-1)}`)) {
        judegFn();
      }
    } else {
      judegFn();
    }
  }
  if (retunBoolean) {
    if (props.size > 0 && fileSize > props.size) {
      alert(`最大上传${filterSize(props.size)}`);
      retunBoolean = false;
    }
  }
  return retunBoolean;
};
const beforeAvatarUpload = (rawFile) => {
  let retunBoolean = judegFileSize(rawFile);
  if (retunBoolean) {
    fileList.push({
      src: URL.createObjectURL(rawFile),
      file: rawFile,
    });
  }
  return retunBoolean;
};

// 裁剪
const cropperFn = (item, index) => {
  showCropper.value = true;
  option.img = URL.createObjectURL(item.file);
  const reader = new FileReader();
  reader.readAsDataURL(item.file);
  cropperCb.value = (res) => {
    if (res) {
      cropperRefs.value.getCropBlob((data) => {
        const result = new File([data], item.file.name, {
          type: item.file.type,
          lastModified: Date.now(),
        });
        result["uid"] = item.file.uid;
        fileList.splice(index, 1, {
          src: URL.createObjectURL(result),
          file: result,
        });
        showCropper.value = false;
      });
    }
  };
};

// 删除
const delFn = (index) => {
  fileList.splice(index, 1);
};
// 弹窗确定裁剪
const onSubmit = () => {
  if (cropperCb.value) cropperCb.value(true);
};

// 真实上传
const uploadFileFn = (item) => {
  if (props.sendUrl == "") return false;
  const successFn = (url) => {
    const index = fileList.findIndex((n) => {
      if (n.file && n.file.uid == item.file.uid) {
        return true;
      }
      return false;
    });
    if (index != -1) {
      fileList.splice(index, 1, {
        src: url,
        file: item.file,
        isSuccess: true,
      });
    }
  };
  // successFn(item.src);
  const formData = new FormData();
  formData.append("file", item.file);
  if (props.otherData) {
    Object.keys(props.otherData).forEach((key) => {
      formData.append(key, props.otherData[key]);
    });
  }
  fetch(props.sendUrl, {
    method: "POST",
    body: formData,
    headers: props.headers,
    "Content-type": "multipart/form-data",
  })
    .then((respone) => respone.json())
    .then((res) => {
      // 接口成功后替换url
      successFn("成功的url");
    })
    .catch((error) => {
      // 接口失败的
    });
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.el-icon.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  border: 1px solid #ccc;
  width: 178px;
  height: 178px;
  text-align: center;
}

.uploadMian {
  vertical-align: top;

  display: flex;
  flex-wrap: wrap;
}
.avatar-uploader {
}

.img-item {
  display: inline-block;
  width: 178px;
  height: 178px;
  margin-right: 10px;
  border: 1px solid #ccc;
  position: relative;
  img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    position: relative;
    z-index: 9;
  }
  &:hover{
    .el-icon.uploader-close {
        display: flex !important;
    }
  }
  .uploader-Check{
    width: 40px;
    height: 40px;
    position: absolute;
    z-index: 18;
    top: 0;
    left: 0;
    display: flex;
    background-color: #67c23a;
    clip-path: polygon(0 0 ,100% 0, 0 100%);
    -webkit-clip-path:polygon(0 0 ,100% 0,0 100% );
    .el-icon{
        position: absolute;
        top: 4px;
        left: 4px;
        color: #fff;
    }
  }
  .el-icon.uploader-close {
    display: none;
    position: absolute;
    z-index: 20;
    top: -5px;
    right: -5px;
    width: 20px;
    height: 20px;
    background-color: red;
    justify-content: center;
    align-items: center;
    border-radius: 50%;
    color: #fff;
    font-size: 12px;
    cursor: pointer;
  }
  .button-div {
    position: absolute;
    height: 45px;
    z-index: 20;
    bottom: 0;
    left: 0;
    width: 100%;
    background-color: rgba(0, 0, 0, 0.2);
    display: flex;
    justify-content: space-around;
    align-items: center;
  }
}
.cropper-content {
  display: flex;
  display: -webkit-flex;
  justify-content: flex-end;
  .cropper-box {
    width: 550px;
    .cropper {
      width: auto;
      height: 400px;
    }
  }

  .show-preview {
    flex: 1;
    -webkit-flex: 1;
    display: flex;
    display: -webkit-flex;
    justify-content: center;
    .preview {
      overflow: hidden;
      border: 1px solid #67c23a;
      background: #cccccc;
    }
  }
}
.dialog-footer {
  display: flex;
  justify-content: center;
  margin-top: 10px;
}
</style>

5.使用:

 <cropperUpload :otherData="{a:100}" :headers="{}" v-model="urlList" :multiple="true" sendUrl="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" />
 <script setup>
     import cropperUpload from "./cropperUpload.vue";
     // 裁剪的配置
     const urlList = reactive(['https://img1.baidu.com/it/u=4049022245,514596079&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500'])
 </script>
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1273832.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

组合(回溯算法)

77. 组合 - 力扣&#xff08;LeetCode&#xff09; 题目描述 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 样例输入 示例 1&#xff1a; 输入&#xff1a;n 4, k 2 输出&#xff1a; [[2,4],[3,4],[2,3],…

CSP认证2023-03:田地丈量、垦田计划、LDAP,python满分解答代码

CSP认证2023-03&#xff1a;田地丈量、垦田计划、LDAP&#xff0c;python满分解答代码 目录 一、田地丈量 问题描述 输入输出 思路 代码和结果 二、垦田计划 问题描述 输入和输出 思路 代码和结果 三、LDAP 问题描述 思路 代码和结果 一、田地丈量 问题描…

在ubuntu虚拟机上安装不同版本的交叉编译工具链

在之前的章节中&#xff0c;学习了如何安装了4.8.3的交叉编译工具链&#xff1a; 交叉编译 和 软硬链接 的初识&#xff08;面试重点&#xff09;-CSDN博客 但是&#xff0c;在之后学习内核编译时&#xff0c;由于我的树莓派内核版本较高&#xff0c;为6.1&#xff0c;所以在…

贪心 55. 跳跃游戏 45.跳跃游戏 II

55. 跳跃游戏 题目&#xff1a; 给定非负数组&#xff0c;初始位置在数组第一格&#xff0c;数组值是可以选择的最大跳跃步数&#xff0c;判断能不能达到数组末尾。 示例 1: * 输入: [2,3,1,1,4] * 输出: true * 解释: 我们可以先跳 1 步&#xff0c;从位置 0 到达 位置 1,…

【多传感器融合】BEVFusion: 激光雷达和视觉融合框架 NeurIPS 2022

前言 BEVFusion其实有两篇&#xff0c; 【1】BEVFusion: A Simple and Robust LiDAR-Camera Fusion Framework. NeurIPS 2022 | 北大&阿里提出 【2】BEVFusion: Multi-Task Multi-Sensor Fusion with Unified Bird’s-Eye View Representation 2022 | MIT提出 本文先分…

VUE2+THREE.JS点击事件

THREE.JS点击事件 1.增加监听点击事件2.点击事件实现3.记得关闭页面时 销毁此监听事件 1.增加监听点击事件 renderer.domElement.addEventListener("click", this.onClick, false); 注:初始化render时监听 2.点击事件实现 onClick(event) {const raycaster new …

leetcode-142-环形链表(C语言实现)

题目&#xff1a; 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评…

药食同源的食物哪些适合冬季吃?

药食同源的食物是指在中医理论指导下&#xff0c;既是药物又是食物的天然植物和动物&#xff0c;具有营养和药效双重作用。在冬季&#xff0c;由于气候寒冷&#xff0c;人体需要更多的热量和营养来保持温暖和健康&#xff0c;因此药食同源的食物在冬季特别适合食用。以下是几种…

CSS伪类伪元素?:hover,::before,::after使用(举例)

文章目录 什么是CSS伪类&#xff1f;什么是伪元素&#xff1f;怎么用伪元素&#xff1f;可以做些什么&#xff1f;::before&#xff0c;在标签选择器之前添加内容&#xff0c;::after正好与之相反::before&#xff0c;在类选择器之前添加内容&#xff08;:制作一个悬浮提示窗 参…

认识JVM 一个Java文件的JVM之旅

准备 我是一个java文件&#xff0c;如何实现我的功能呢&#xff1f;需要去JVM(Java Virtual Machine)这个地方旅行。 变身 我高高兴兴的来到JVM&#xff0c;想要开始JVM之旅&#xff0c;它确说&#xff1a;“现在的我还不能进去&#xff0c;需要做一次转换&#xff0c;生成c…

Ubuntu镜像与K8S冲突,容器持续Terminating

问题 记录一次软件冲突BUG&#xff1a; eclipse-temurin:11-jdk&#xff08;底层Ubuntu 20.04.3 LTS&#xff09;镜像创建的容器在K8S-1.25.5上无法正常terminating&#xff0c;造成资源浪费&#xff0c;甚至引发K8S资源CPU insufficient报错。具体表现 某些容器镜像在K8S上无…

20个Python源码项目下载

20个很不错的Python项目源码&#xff0c;其中包括适合毕业设计的项目。这些资源中涵盖了Django 3版本的项目&#xff1a; DjangoMysqlBulma实现的商场管理系统源码 PythonDjango实现基于人脸识别的门禁管理系统 PythonFlaskMySQL实现的学生培养计划管理系统 Python大熊猫主题人…

Windows本地搭建Emby媒体库服务器并实现远程访问「内网穿透」

文章目录 1.前言2. Emby网站搭建2.1. Emby下载和安装2.2 Emby网页测试 3. 本地网页发布3.1 注册并安装cpolar内网穿透3.2 Cpolar云端设置3.3 Cpolar内网穿透本地设置 4.公网访问测试5.结语 1.前言 在现代五花八门的网络应用场景中&#xff0c;观看视频绝对是主力应用场景之一&…

Linux基本指令(中篇)

目录 8.cp指令&#xff08;重要&#xff09; 9.mv指令&#xff08;重要&#xff09;&#xff1a; 10.cat指令&#xff08;适合查看小文件内容&#xff09; 11.more指令&#xff08;适合查看大文件内容&#xff09; 12.less指令&#xff08;重要&#xff09; 13.head指令和…

SpringBoot 整合 Neo4j 实战(头歌)

文章目录 第1关&#xff1a;认识 Spring DATA Neo4J任务描述相关知识Spring DATA Neo4J - 简介Spring JDBC / Spring ORM 模块的缺点&#xff1a;Spring 数据模块的优点&#xff1a;Spring 数据模块功能&#xff1a;Spring DATA Neo4j 模块的附加功能&#xff1a; Spring DATA …

【腾讯云云上实验室】个人对腾讯云向量数据库的体验心得

目录 前言Tencent Cloud VectorDB概念使用初体验腾讯云向量数据库的优势应用场景有哪些&#xff1f;未来展望番外篇&#xff1a;腾讯云向量数据库的设计核心结语 前言 还是那句话&#xff0c;不用多说想必大家都能猜到&#xff0c;现在技术圈最火的是什么&#xff1f;非人工智…

HyperBDR云容灾v4.10.1发布,划重点:支持UCloud云平台自动化容灾+新增可灵活定义的备份策略

版本更新 HyperBDR云容灾v4.10.1版本来啦&#xff01; 此次更新为大家带来了多个新功能&#xff0c;下面让我们来看看具体是哪些吧~ 01 策略管理新功能&#xff1a; 多时间段限速功能&#xff1a; 更加灵活的多个时间段限速选择&#xff0c;可以在创建策略时为不同的时间段设…

JavaEE——简单认识CSS

文章目录 一、简单了解什么是 CSS二、CSS 选择器1.标签选择器2.类选择器3.ID 选择器4.后代选择器5.子选择器6.伪类选择器 三、字体属性1.设置字体家族2.设置字体大小3.设置字体粗细4.文字倾斜 四、文本属性1.文本对齐2.文本装饰3.文本缩进4.背景设置 五、圆角矩形六、CSS 盒子模…

【猜数字游戏】用wxPython实现:基本的游戏框架 + 简单的图形用户界面

【猜数字游戏】 写在最前面猜数字游戏 实现【猜数字游戏】安装wxPython全部代码代码解析1. 初始化界面2. 生成随机数3. 处理猜测4. 特殊功能5. 分数计算 游戏小程序呈现结语 写在最前面 看到了一个比较有意思的问题 https://ask.csdn.net/questions/8038039 猜数字游戏 在这…

人工智能_机器学习056_拉格朗日乘子法原理推导_公式由来详解_原理详解---人工智能工作笔记0096

https://blog.csdn.net/Soft_Po/article/details/118332454 这里有老师的一篇文章介绍拉格朗日乘子法的原理推导 结合老师的这篇文章我们来看一下详细的推导过程 可以看到上一节我们说,一个有条件的,函数,可以转换为一个,无条件的函数, 根据拉格朗日乘子法,可以创建出一个等…