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

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>
 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

numpy知识库:深入理解numpy.resize函数和数组的resize方法

前言 numpy中的resize函数顾名思义&#xff0c;可以用于调整数组的大小。但具体如何调整&#xff1f;数组形状变了&#xff0c;意味着数组中的元素个数发生了变化(增加或减少)&#xff0c;如何确定resize后的新数组中每个元素的数值呢&#xff1f;本次博文就来探讨并试图回答这…

二进制求和

这篇文章会收录到 : 算法通关村第十三关-白银挑战数字与数学高频问题-CSDN博客 二进制求和 描述 : 给你两个二进制字符串 a 和 b &#xff0c;以二进制字符串的形式返回它们的和。 题目 : LeetCode 67.二进制求和 : 67. 二进制求和 分析 : 这个题也是用字符串来表示数据的…

Docker 使用心得

创建一个docker 镜像&#xff0c;相关运行代码&#xff0c;放在docker镜像文件同级&#xff0c; pm2 不能与 docker一起使用&#xff08;&#xff09; # node 服务docker FROM node:10.16.3LABEL author"sj"RUN mkdir -p /var/nodeCOPY ./node /var/nodeWORKDIR /va…

动手学深度学习(六)---权重衰退

文章目录 一、理论知识二、代码实现【相关总结】 主要解决过拟合 一、理论知识 1、使用均方范数作为硬性限制&#xff08;不常用&#xff09; 通过限制参数值的选择范围来控制模型容量 通常不限制偏移b 小的意味着更强的正则项 使用均方范数作为柔性限制 对于每个都可以找到使…

陈嘉庚慈善践行与卓顺发的大爱传承

陈嘉庚慈善践行&#xff0c;了解陈嘉庚后人与卓顺发的大爱传承。 2023年11月25日,卓顺发太平绅士以及陈家后人在分享他们对慈善领域见解的过程中,特别强调了慈善在促进社会和谐以及推动社会进步方面的关键作用。同时,他们深入探讨了如何在当今社会中继续传扬和实践家国情怀以及…

TCP解帧解码、并发送有效数据到FPGA

TCP解帧解码、并发送有效数据到FPGA 工程的功能&#xff1a;使用TCP协议接收到网络调试助手发来的指令&#xff0c;将指令进行解帧&#xff0c;提取出帧头、有限数据、帧尾&#xff1b;再将有效数据发送到FPGA端的BRAM上&#xff0c;实现信息传递。 参考&#xff1a;正点原子启…

使用canvas实现代码雨高级升阶版【附带源码和使用方法】

文章目录 前言基本绿色的彩色版本飘散雪花状后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;前端面试 &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努力填补技术短板。(如果出现错误&…

ProgrammingError: nan can not be used with MySQL

该错误怎么发生的&#xff1f; 我们先在本地创建测试表&#xff1a; CREATE TABLE users_test (id int NOT NULL AUTO_INCREMENT COMMENT 主键,trade_account varchar(50) DEFAULT NULL COMMENT 交易账号,username varchar(50) DEFAULT NULL,email varchar(100) DEFAULT NULL…

哪个软件消除笔好用?我来告诉你

全民自媒体时代&#xff0c;人人都是自媒体人&#xff0c;越来越多的人接触到修图&#xff0c;剪辑&#xff0c;制作&#xff0c;常常在社交媒体上分享美食制作教程&#xff0c;居家好物&#xff0c;影视混剪&#xff0c;小说解说等各种各样的精彩照片,但是在网上找的图片素材往…

数据收集与处理(爬虫技术)

文章目录 1 前言2 网络爬虫2.1 构造自己的Scrapy爬虫2.1.1 items.py2.1.2 spiders子目录2.1.3 pipelines.py 2.2 构造可接受参数的Scrapy爬虫2.3 运行Scrapy爬虫2.3.1 在命令行运行2.3.2 在程序中调用 2.4 运行Scrapy的一些要点 3 大规模非结构化数据的存储与分析4 全部代码 1 …

建文工程项目管理软件 SQL 注入漏洞复现

0x01 产品简介 建文工程管理软件是一个适用于工程投资领域的综合型的多方协作平台。 0x02 漏洞概述 建文工程项目管理软件BusinessManger.ashx、Desktop.ashx等接口处存在SQL注入漏洞&#xff0c;攻击者可通过该漏洞获取数据库中的信息&#xff08;例如&#xff0c;管理员后台…

TypeScript 5.3

导入属性 TypeScript 5.3支持导入属性提案的最新更新。 导入属性的一个用例是向运行库提供有关模块预期格式的信息。 // We only want this to be interpreted as JSON, // not a runnable/malicious JavaScript file with a .json extension. import obj from "./somet…

【综述+自动流量分析A】New Directions in Automated Traffic Analysis

文章目录 论文简介摘要存在的问题论文贡献1. 整体架构2. nPrint3. nPrintML4. 任务 总结论文内容工具数据集可读的引用文献笔记参考文献 论文简介 原文题目&#xff1a;New Directions in Automated Traffic Analysis 中文题目&#xff1a;自动流量分析的新方向 发表会议&#…

Java多线程-第20章

Java多线程-第20章 1.创建线程 Java是一种支持多线程编程的编程语言。多线程是指在同一程序中同时执行多个独立任务的能力。在Java中&#xff0c;线程是一种轻量级的子进程&#xff0c;它是程序中的最小执行单元。Java的多线程编程可以通过两种方式实现&#xff1a;继承Threa…

windows配置使用supervisor

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、使用步骤1.安装supervisor-win2.配置supervisord3.配置program4.启动supervisord.exe5.supervisorctl.exe管控 二、后台启动总结 前言 windows使用supervi…

PromptRank:使用Prompt进行无监督关键词提取

论文题目&#xff1a;PromptRank: Unsupervised Keyphrase Extraction Using Prompt   论文日期&#xff1a;2023/05/15(ACL 2023)   论文地址&#xff1a;https://arxiv.org/abs/2305.04490   GitHub地址&#xff1a;https://github.com/HLT-NLP/PromptRank 文章目录 Ab…

虽然在不同设备上会出现同样的原神错误代码9907,但解决办法因设备而异

你是不是很享受在原神(Genshin Impact)中的神奇旅程,但错误代码9907出现了?与PS4控制台中全面讨论PS4的错误CE-34878-0不同,本文关注的是原神错误本身。本文不仅讨论了这个错误背后的原因,还讨论了每种类型设备的具体解决方案。 在Microsoft Windows/PC上修复错误代码99…

NSSCTF第14页(2)

[UUCTF 2022 新生赛]ezpop 提示说看看反序列化字符串逃逸 PHP反序列化字符串逃逸_php反序列化逃逸-CSDN博客 php反序列化字符逃逸_php反序列化逃逸_Leekos的博客-CSDN博客 buuctf刷题9 (反序列化逃逸&shtml-SSI远程命令执行&idna与utf-8编码漏洞)_extract($_post);…

Linux 基本语句_12_信号

用途&#xff1a; 信号可以直接进行用户进程与内核进程之间的交互 特性&#xff1a; 对于一个进程&#xff0c;其可以注册或者不注册信号&#xff0c;不注册的信号&#xff0c;进程接受后会按默认功能处理&#xff0c;对于注册后的信号&#xff0c;进程会按自定义处理 自定义…

vue3 keep-alive页面切换报错:parentComponent.ctx.deactivate is not a function

问题&#xff1a; <router-view v-slot"{ Component }"><keep-alive ><component :is"Component" v-if"$route.meta.keepAlive" /></keep-alive><component :is"Component" v-if"!$route.meta.keepA…