vue2 头像上传+裁剪组件封装

news2025/5/15 23:11:56

背景:最近在进行公司业务开发时,遇到了头像上传限制尺寸的需求,即限制为一寸证件照(宽295像素,高413像素)。

用到的第三方库: "vue-cropper": "^0.5.5"

完整组件代码:

avatarUpload.vue

<!-- 上传图片并裁剪 -->
<template>
  <div :style="{ marginTop: marginTop + 'px' }">
    <!-- 上传 -->
    <template>
      <div class="preview-img" v-show="previewImg" :style="{ width: uploadWidth + 'px', height: uploadHeight + 'px' }">
        <div class="preview-img-box" :style="{ lineHeight: uploadHeight + 'px' }">
          <i v-if="!disabled" class="el-icon-delete" @click="deleteFn"></i>
          <i class="el-icon-view" @click="viewFn"></i>
          <i v-if="!disabled" class="el-icon-refresh" @click="refreshFn"></i>
        </div>        
        <img :src="previewImg" :style="{ width: '100%', height: '100%', objectFit: objectFit }" />
      </div>

      <el-upload v-show="!previewImg" ref="upload" class="upload-demo"
        :style="{ width: uploadWidth + 'px', height: uploadHeight + 'px' }" :action="actionUrl"
        :on-change="handleChangeUpload" :auto-upload="false" :show-file-list="false" :disabled="disabled">
        <div class="upload-demo-icon" :style="{
          width: uploadWidth + 'px',
          height: uploadHeight + 'px',
          lineHeight: uploadHeight + 'px',
        }">
          +
        </div>
      </el-upload>
    </template>
    <!-- 裁剪 -->
    <el-dialog title="图片剪裁" :visible.sync="dialogVisible" class="crop-dialog" append-to-body>
      <div style="padding: 0 20px">
        <div :style="{
          textAlign: 'center',
          width: cropperWidth != 0 ? cropperWidth + 'px' : 'auto',
          height: cropperHeight + 'px',
        }">
          <VueCropper ref="cropper" :img="cropperImg" :output-size="outputSize" :output-type="outputType" :info="info"
            :full="full" :can-move="canMove" :can-move-box="canMoveBox" :original="original" :auto-crop="autoCrop"
            :can-scale="canScale" :fixed="fixed" :fixed-number="fixedNumber" :fixed-box="fixedBox"
            :center-box="centerBox" :info-true="infoTrue" :auto-crop-width="autoCropWidth"
            :auto-crop-height="autoCropHeight" />
        </div>
      </div>
      <!-- 这里的按钮可以根据自己的需求进行增删-->
      <div class="action-box" v-if="actionButtonFlag">
        <el-upload action="#" :auto-upload="false" :show-file-list="false" :on-change="handleChangeUpload"
          style="margin-right: 15px">
          <el-button title="更换图片" plain circle type="primary" icon="el-icon-refresh"></el-button>
        </el-upload>
        <el-button title="清除图片" plain circle type="primary" icon="el-icon-close" @click="clearImgHandle"></el-button>
        <el-button title="向左旋转" plain circle type="primary" icon="el-icon-refresh-left"
          @click="rotateLeftHandle"></el-button>
        <el-button title="向右旋转" plain circle type="primary" icon="el-icon-refresh-right" @click="rotateRightHandle">
        </el-button>
        <el-button title="放大" plain circle type="primary" @click="changeScaleHandle(1)" icon="el-icon-zoom-in">
        </el-button>
        <el-button title="缩小" plain circle type="primary" @click="changeScaleHandle(-1)" icon="el-icon-zoom-out">
        </el-button>
        <!-- <el-button type="primary" @click="fixed = !fixed">
            {{ fixed ? "固定比例" : "自由比例" }}
          </el-button> -->
        <el-button title="下载" plain circle type="primary" icon="el-icon-download"
          @click="downloadHandle('blob')"></el-button>
      </div>

      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" :loading="loading" @click="finish">
          确认
        </el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { VueCropper } from "vue-cropper";
import { insertImage } from '@/api/file';
export default {
  name: "Cropper",
  components: {
    VueCropper,
  },
  props: {
    marginTop: {
      type: Number,
      default: 0,
    },
    // 上传属性

    // 图片路径
    imgSrc: {
      type: String,
      default: "",
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false,
    },
    // 列表索引
    listIndex: {
      type: Number,
      default: null,
    },
    // 上传路径
    actionUrl: {
      type: String,
      default: "#",
    },
    // 上传宽度
    uploadWidth: {
      type: Number,
      default: 100,
    },
    // 上传高度
    uploadHeight: {
      type: Number,
      default: 100,
    },
    // 图片显示角度 传值详情参考mdn object-fit
    objectFit: {
      type: String,
      default: "fill",
    },

    // 裁剪属性

    // 裁剪弹出框的宽度
    cropperWidth: {
      type: Number,
      default: 0,
    },
    // 裁剪弹出框的高度
    cropperHeight: {
      type: Number,
      default: 600,
    },
    // 裁剪生成图片的质量 0.1-1
    outputSize: {
      type: Number,
      default: 1,
    },
    // 裁剪生成图片的格式
    outputType: {
      type: String,
      default: "png",
    },
    // 裁剪框的大小信息
    info: {
      type: Boolean,
      default: true,
    },
    // 是否输出原图比例的截图
    full: {
      type: Boolean,
      default: false,
    },
    // 截图框能否拖动
    canMove: {
      type: Boolean,
      default: true,
    },
    // 截图框能否拖动
    canMoveBox: {
      type: Boolean,
      default: true,
    },
    // 上传图片按照原始比例渲染
    original: {
      type: Boolean,
      default: true,
    },
    // 是否默认生成截图框
    autoCrop: {
      type: Boolean,
      default: true,
    },
    // 图片是否允许滚轮缩放
    canScale: {
      type: Boolean,
      default: true,
    },
    // 是否开启截图框宽高固定比例
    fixed: {
      type: Boolean,
      default: true,
    },
    // 截图框的宽高比例 开启fixed生效
    fixedNumber: {
      type: Array,
      default: () => [5, 7],
    },
    // 固定截图框大小 不允许改变
    fixedBox: {
      type: Boolean,
      default: true,
    },
    // 截图框是否被限制在图片里面
    centerBox: {
      type: Boolean,
      default: true,
    },
    // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
    infoTrue: {
      type: Boolean,
      default: true,
    },
    // 默认生成截图框宽度
    autoCropWidth: {
      type: Number,
      default: 295,
    },
    // 默认生成截图框高度
    autoCropHeight: {
      type: Number,
      default: 413,
    },
    // 是否出现操作按钮
    actionButtonFlag: {
      type: Boolean,
      default: false,
    },
    // 裁剪路径输出格式 base64:base64; blob:blob;
    cropFormat: {
      type: String,
      default: "blob",
    },
    // 图片最大宽度
    maxImgWidth: {
      type: Number,
      default: 648,
    },
    // 图片最大高度
    maxImgHeight: {
      type: Number,
      default: 1152,
    },
    // 头像扫描件id
    txsmjid: {
      type: String,
      default: ""
    },
    // 是否禁用上传
    disabled:{
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      previewImg: "", // 预览图片地址
      dialogVisible: false, //图片裁剪弹框
      cropperImg: "", // 裁剪图片的地址
      loading: false, // 防止重复提交
      baseCsUrl: process.env.NODE_ENV === 'production' ? window.globalConfig.VUE_APP_BASE_API_CS : process.env.VUE_APP_BASE_API_CS, // 文件服务器地址
      fileName: "",
    };
  },
  watch: {
    imgSrc: {
      handler(newVal, oldVal) {
        this.previewImg = newVal;
      },
      deep: true, // 深度监听
      immediate: true, // 首次进入就监听
    },
  },
  methods: {
    // 上传按钮 限制图片大小和类型
    handleChangeUpload(file) {
      this.fileName = file.name;
      const isJPG =
        file.raw.type === "image/jpeg" || file.raw.type === "image/png";
      const isLt2M = file.size / 1024 / 1024 < 2;
      const min_isLt = file.size / 1024 > 20;
      if (!isJPG) {
        this.$message.error("上传头像图片只能是 JPG/PNG 格式!");
        return false;
      }
      if (!isLt2M) {
        this.$message.error("上传头像图片大小不能超过 2MB!");
        return false;
      }
      if (!min_isLt) {
        this.$modal.msgError("头像大小不能小于 20 K!");
        return false;
      }
      // 上传成功后将图片地址赋值给裁剪框显示图片
      this.$nextTick(async () => {
        // base64方式
        // this.option.img = await fileByBase64(file.raw)
        this.cropperImg = URL.createObjectURL(file.raw);
        this.loading = false;
        this.dialogVisible = true;
      });
    },
    // 放大/缩小
    changeScaleHandle(num) {
      num = num || 1;
      this.$refs.cropper.changeScale(num);
    },
    // 左旋转
    rotateLeftHandle() {
      this.$refs.cropper.rotateLeft();
    },
    // 右旋转
    rotateRightHandle() {
      this.$refs.cropper.rotateRight();
    },
    // 下载
    downloadHandle(type) {
      let aLink = document.createElement("a");
      aLink.download = "author-img";
      if (type === "blob") {
        this.$refs.cropper.getCropBlob((data) => {
          aLink.href = URL.createObjectURL(data);
          aLink.click();
        });
      } else {
        this.$refs.cropper.getCropData((data) => {
          aLink.href = data;
          aLink.click();
        });
      }
    },
    // 清理图片
    clearImgHandle() {
      this.cropperImg = "";
    },
    // 截图
    finish() {
      if (this.cropFormat == "base64") {
        // 获取截图的 base64 数据
        this.$refs.cropper.getCropData((data) => {
          this.loading = true;
          this.dialogVisible = false;

          this.getImgHeight(data, this.maxImgWidth, this.maxImgHeight).then(
            (imgUrl) => {
              // console.log(imgUrl, "base64");
              this.previewImg = imgUrl;
              if (this.listIndex !== null) {
                this.$emit("successCheng", this.previewImg, this.listIndex);
              } else {
                this.$emit("successCheng", this.previewImg);
              }
            }
          );
        });
      } else if (this.cropFormat == "blob") {
        // 获取截图的 blob 数据
        this.$refs.cropper.getCropBlob((blob) => {
          this.loading = true;
          // this.dialogVisible = false;

          this.getImgHeight(
            URL.createObjectURL(blob),
            this.maxImgWidth,
            this.maxImgHeight
          ).then((imgUrl) => {
            // console.log(imgUrl, "blob");
            this.previewImg = imgUrl;
            if (this.listIndex !== null) {
              this.$emit("successCheng", this.previewImg, this.listIndex);
            } else {
              this.$emit("successCheng", this.previewImg);
            }
          });

          this.uploadImage(blob)
        });
      }
    },
    // 上传到后端
    uploadImage(blob) {
      let file = new File([blob], this.fileName, {
        type: "image/png",
        lastModified: Date.now(),
      });
      let fd = new FormData();
      fd.append("file", file);
      insertImage(fd, this.txsmjid).then((res) => {
        // console.log("图片上传", res);
        this.dialogVisible = false;
        if (res.code === 200) {
          this.$modal.msgSuccess("上传成功");
        }
      }).finally(() => {
        this.loading = false;
      });
    },

    // 预览图片
    viewFn() {
      let preIndex = 0;
      this.$viewerApi({
        images: [this.previewImg],
        options: {
          initialViewIndex: preIndex,
        },
      });
    },
    // 删除图片
    deleteFn() {
      this.previewImg = "";
      // this.$emit("deleteCheng");
    },
    // 更换图片
    refreshFn() {
      this.$refs["upload"].$refs["upload-inner"].handleClick();
    },
    // 获取图片高度并修改
    getImgHeight(imgSrc, scaleWidth = 648, scaleHeight = 1152) {
      return new Promise((resolve, reject) => {
        const img = new Image(); // 创建一个img对象
        img.src = imgSrc; // 设置图片地址
        let imgUrl = ""; // 接收图片地址
        img.onload = () => {
          if (img.width > scaleWidth || img.height > scaleHeight) {
            const canvas = document.createElement("canvas");
            const context = canvas.getContext("2d");
            canvas.width = scaleWidth;
            canvas.height = scaleHeight;
            context.drawImage(img, 0, 0, scaleWidth, scaleHeight);
            if (this.cropFormat == "blob") {
              imgUrl = this.base64toBlob(
                canvas.toDataURL("image/png", 1),
                "image/png"
              );
            } else {
              imgUrl = canvas.toDataURL("image/png", 1);
            }
            resolve(imgUrl);
          } else {
            imgUrl = imgSrc;
            resolve(imgUrl);
          }
        };
      });
    },
    // base64转blob
    base64toBlob(base64, type = "application/octet-stream") {
      // 去除base64头部
      const images = base64.replace(/^data:image\/\w+;base64,/, "");
      const bstr = atob(images);
      let n = bstr.length;
      const u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return URL.createObjectURL(new Blob([u8arr], { type }));
    },
  },
};
</script>

<style lang="scss" scoped>
.preview-img {
  position: relative;
  cursor: pointer;

  &:hover {
    .preview-img-box {
      display: block;
    }
  }
}

.preview-img-box {
  width: 100%;
  height: 100%;
  position: absolute;
  left: 0;
  top: 0;
  background-color: rgba(30, 28, 28, 0.5);
  display: none;
  color: #d9d9d9;
  font-size: 24px;
  text-align: center;
}

.preview-img-number {
  width: 20px;
  height: 20px;
  overflow: hidden;
  position: absolute;
  right: 0;
  bottom: 0;
  background-color: rgba(8, 137, 53, 1);
  color: #d9d9d9;
  font-size: 20px;
  text-align: center;
  border-radius: 50%;
}

.upload-demo {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
}

.upload-demo-icon {
  font-size: 60px;
  color: #8c939d;
  text-align: center;
}

.crop-dialog {
  .action-box {
    margin: 20px;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;

    button {
      margin-top: 15px;
      //width: 80px;
      margin-right: 15px;
    }
  }

  .dialog-footer {
    text-align: center;

    button {
      width: 100px;
    }
  }
}
</style>

使用组件:

<template>
    <div>
     <!-- 图片裁剪 -->
     <avatarUpload :key="new Date().getTime()" :imgSrc="imageUrl" :uploadWidth="148" :uploadHeight="207" :actionButtonFlag="true" :objectFit="'cover'" 
class="avatar-uploader" :txsmjid="txsmjid">
    </avatarUpload>
    </div>
</template>
<script>
import avatarUpload from "./avatarUpload.vue";
export default {
  components: {
    avatarUpload,
  },
  data() {
     imageUrl:'',
     txsmjid:'',
  }      
}
</script>

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

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

相关文章

AI-02a5a5.神经网络-与学习相关的技巧-权重初始值

权重的初始值 在神经网络的学习中&#xff0c;权重的初始值特别重要。实际上&#xff0c;设定什么样的权重初始值&#xff0c;经常关系到神经网络的学习能否成功。 不要将权重初始值设为 0 权值衰减&#xff08;weight decay&#xff09;&#xff1a;抑制过拟合、提高泛化能…

【springcloud学习(dalston.sr1)】Eureka单个服务端的搭建(含源代码)(三)

该系列项目整体介绍及源代码请参照前面写的一篇文章【springcloud学习(dalston.sr1)】项目整体介绍&#xff08;含源代码&#xff09;&#xff08;一&#xff09; 这篇文章主要介绍单个eureka服务端的集群环境是如何搭建的。 通过前面的文章【springcloud学习(dalston.sr1)】…

Node.js数据抓取技术实战示例

Node.js常用的库有哪些呢&#xff1f;比如axios或者node-fetch用来发送HTTP请求&#xff0c;cheerio用来解析HTML&#xff0c;如果是动态网页的话可能需要puppeteer这样的无头浏览器。这些工具的组合应该能满足大部分需求。 然后&#xff0c;可能遇到的难点在哪里&#xff1f;…

windows10 安装 QT

本地环境有个qt文件&#xff0c;这里是5.14.2 打开一个cmd窗口并指定到该文件根目录下 .\qt-opensource-windows-x86-5.14.2.exe --mirror https://mirrors.ustc.edu.cn/qtproject 执行上面命令 记住是文件名&#xff0c;记住不要傻 X的直接复制&#xff0c;是你的文件名 点击…

WordPress 和 GPL – 您需要了解的一切

如果您使用 WordPress&#xff0c;GPL 对您来说应该很重要&#xff0c;您也应该了解它。查看有关 WordPress 和 GPL 的最全面指南。 您可能听说过 GPL&#xff08;通常被称为 WordPress 的权利法案&#xff09;&#xff0c;但很可能并不完全了解它。这是有道理的–这是一个复杂…

C++书本摆放 2024年信息素养大赛复赛 C++小学/初中组 算法创意实践挑战赛 真题详细解析

目录 C++书本摆放 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、运行结果 五、考点分析 六、 推荐资料 1、C++资料 2、Scratch资料 3、Python资料 C++书本摆放 2024年信息素养大赛 C++复赛真题 一、题目要求 1、编程实现 中科智慧科技…

RabbitMQ 核心概念与消息模型深度解析(一)

一、RabbitMQ 是什么 在当今分布式系统盛行的时代&#xff0c;消息队列作为一种至关重要的中间件技术&#xff0c;扮演着实现系统之间异步通信、解耦和削峰填谷等关键角色 。RabbitMQ 便是消息队列领域中的佼佼者&#xff0c;是一个开源的消息代理和队列服务器&#xff0c;基于…

论文阅读笔记——双流网络

双流网络论文 视频相比图像包含更多信息&#xff1a;运动信息、时序信息、背景信息等等。 原先处理视频的方法&#xff1a; CNN LSTM&#xff1a;CNN 抽取关键特征&#xff0c;LSTM 做时序逻辑&#xff1b;抽取视频中关键 K 帧输入 CNN 得到图片特征&#xff0c;再输入 LSTM&…

LabVIEW在电子电工教学中的应用

在电子电工教学领域&#xff0c;传统教学模式面临诸多挑战&#xff0c;如实验设备数量有限、实验过程存在安全隐患、教学内容更新滞后等。LabVIEW 作为一款功能强大的图形化编程软件&#xff0c;为解决这些问题提供了创新思路&#xff0c;在电子电工教学的多个关键环节发挥着重…

Vue3 怎么在ElMessage消息提示组件中添加自定义icon图标

1、定义icon组件代码&#xff1a; <template><svg :class"svgClass" aria-hidden"true"><use :xlink:href"iconName" :fill"color"/></svg> </template><script> export default defineComponen…

生活破破烂烂,AI 缝缝补补(附提示词)

写在前面&#xff1a;​【Fire 计算器】已上线&#xff0c;快算算财富自由要多少​ 现实不总温柔&#xff0c;愿你始终自渡。 请永远拯救自己于水火之中。 毛绒风格提示词&#xff08;供参考&#xff09;&#xff1a; 1. 逼真毛绒风 Transform this image into a hyperrealist…

张 。。 通过Token实现Loss调优prompt

词编码模型和 API LLM不匹配,采用本地模型 理性中性案例(针对中性调整比较合理) 代码解释:Qwen2模型的文本编码与生成过程 这段代码展示了如何使用Qwen2模型进行文本的编码和解码操作。 模型加载与初始化 from transformers import AutoModelForCausalLM, AutoTokenizer

JVM学习专题(一)类加载器与双亲委派

目录 1、JVM加载运行全过程梳理 2、JVM Hotspot底层 3、war包、jar包如何加载 4、类加载器 我们来查看一下getLauncher&#xff1a; 1.我们先查看getExtClassLoader() 2、再来看看getAppClassLoader(extcl) 5、双亲委派机制 1.职责明确&#xff0c;路径隔离​&#xff…

PyTorch API 9 - masked, nested, 稀疏, 存储

文章目录 torch.randomtorch.masked简介动机什么是 MaskedTensor&#xff1f; 支持的运算符一元运算符二元运算符归约操作查看与选择函数 torch.nested简介构造方法数据布局与形状支持的操作查看嵌套张量的组成元素填充张量的相互转换形状操作注意力机制 与 torch.compile 的配…

进程相关面试题20道

一、基础概念与原理 1.进程的定义及其与程序的本质区别是什么&#xff1f; 答案&#xff1a;进程是操作系统分配资源的基本单位&#xff0c;是程序在数据集合上的一次动态执行过程。核心区别&#xff1a;​ 动态性&#xff1a;程序是静态文件&#xff0c;进程是动态执行实例…

Linux复习笔记(五) 网络服务配置(dhcp)

二、网络服务配置 2.5 dhcp服务配置&#xff08;不涉及实际操作&#xff09; 要求&#xff1a;知道原理和常见的参数配置就行 2.5.1 概述DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff09; DHCP&#xff08;Dynamic Host Conf…

windows版redis的使用

redis下载 Releases microsoftarchive/redishttps://github.com/microsoftarchive/redis/releases redis的启动和停止 进入路径的cmd 启动&#xff1a;redis-server.exe redis.windows.conf 停止&#xff1a;ctrlc 连接redis 指定要连接的IP和端口号 -h IP地址 -p 端口…

Java版OA管理系统源码 手机版OA系统源码

Java版OA管理系统源码 手机版OA系统源码 一&#xff1a;OA系统的主要优势 1. 提升效率 减少纸质流程和重复性工作&#xff0c;自动化处理常规事务&#xff0c;缩短响应时间。 2. 降低成本 节省纸张、打印、通讯及人力成本&#xff0c;优化资源分配。 3. 规范管理 固化企…

NineData 社区版 V4.1.0 正式发布,新增 4 条迁移链路,本地化数据管理能力再升级

NineData 社区版 V4.1.0 正式更新发布。本次通过新增 4 条迁移链路扩展、国产数据库深度适配、敏感数据保护增强‌等升级&#xff0c;进一步巩固了其作为高效、安全、易用的数据管理工具的定位。无论是开发测试、数据迁移&#xff0c;还是多环境的数据管理&#xff0c;NineData…

进阶2_1:QT5多线程与定时器共生死

1、在widget.ui中使用 LCD Number控件 注意&#xff1a;若 LCD 控件不是多线程&#xff0c;LCD控件则会瞬间自增到最大的数值&#xff0c;如上图&#xff0c;说明两者都是多线程处理 2、实现方式 1、创建 LCD 控件并修改为 LCD1 2、创建任务类 mytask. h&#xff0c;对任务类…