vue3【组件封装】头像裁剪 S-avatar.vue

最终效果

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

技术要点

图片裁剪

安装依赖 vue-cropper

npm install vue-cropper@next

专用于vue3 项目的图片裁剪,详细使用参考官方文档

页面使用

import "vue-cropper/dist/index.css";
import { VueCropper } from "vue-cropper";
<vue-cropperref="cropper"v-bind="option"@realTime="realTime"
></vue-cropper>
const cropper = ref();
const option = ref({autoCrop: true, // 是否默认生成截图框autoCropHeight: "240px", // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoCropHeight * 1.25autoCropWidth: "240px", // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoWidth * 1.25canMove: true, // 上传图片是否可以移动canScale: true, // 图片是否允许滚轮缩放centerBox: true, // 截图框是否被限制在图片里面fixed: true, // 是否固定截图框的宽高比例fixedBox: true, // 是否固定截图框大小fixedNumber: [1, 1], // 截图框的宽高比例([ 宽度 , 高度 ])img: "", // 裁剪图片的地址(可选值:url 地址, base64, blob)info: true, // 是否显示裁剪框的宽高信息infoTrue: true, // infoTrue为 true 时裁剪框显示的是预览图片的宽高信息,infoTrue为 false 时裁剪框显示的是裁剪框的宽高信息mode: "contain", // 截图框可拖动时的方向(可选值:contain , cover, 100px, 100% auto)origin: false, // 上传的图片是否按照原始比例渲染outputSize: 1, // 裁剪生成图片的质量(可选值:0.1 ~ 1)outputType: "png", // 裁剪生成图片的格式(可选值:png, jpeg, webp)full: true,
});
const previews = ref<any>({url: "",file: null,
});
// 实时预览
const realTime = () => {cropper.value.getCropBlob((blob: Blob) => {previews.value.url = window.URL.createObjectURL(blob);previews.value.file = blobToFile(blob, imageName.value);});
};

裁剪效果预览

        <div class="preview"><img :src="previews.url" /></div>
.preview {width: 150px;height: 150px;margin: 0px auto 20px auto;border-radius: 50%;border: 1px solid #ccc;background-color: #ccc;overflow: hidden;
}

阻止点击冒泡

@click.stop

组件封装 S-avatar.vue

components/SUI/S-avatar.vue

<template><el-uploadclass="avatar-uploader"action="#":show-file-list="false":on-success="handleAvatarSuccess":before-upload="beforeAvatarUpload":accept="imgType":drag="drag":disabled="disabled"><S-msgWin :msg="callbackMessage" :duration="500" /><div v-if="imageUrl" @click.stop class="avatar-container relative group"><el-imageclass="avatar":src="imageUrl"fit="cover":preview-src-list="[imageUrl]"/><divv-if="!disabled"class="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"><el-icon:size="30"color="white"class="edit-icon text-white mr-8 cursor-pointer"@click.stop="handleEditAvatar"><Edit /></el-icon><el-popconfirm title="确定删除吗?" @confirm="handleDeleteAvatar"><template #reference><el-icon@click.stop:size="30"color="white"class="delete-icon text-white cursor-pointer"><Delete /></el-icon></template></el-popconfirm></div></div><el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon></el-upload><el-dialog title="修改头像" v-model="editAvatarDialog" width="600"><el-row type="flex" justify="center" class="nowarp"><div class="cropper"><vue-cropperref="cropper"v-bind="option"@realTime="realTime"></vue-cropper></div><div class="previewBox"><div class="preview"><img :src="previews.url" /></div><el-row type="flex" justify="center"><el-uploadaction="#":show-file-list="false":on-success="handleAvatarSuccess":before-upload="beforeAvatarUpload"><el-button size="small" type="primary"> 更换头像 </el-button></el-upload></el-row><br /><el-row><el-button:icon="ZoomIn"circlesize="small"@click="changeScale(1)"></el-button><el-button:icon="ZoomOut"circlesize="small"@click="changeScale(-1)"></el-button><el-button:icon="Download"circlesize="small"@click="downloadPreView"></el-button><el-button:icon="RefreshLeft"circlesize="small"@click="rotateLeft"></el-button><el-button:icon="RefreshRight"circlesize="small"@click="rotateRight"></el-button></el-row></div></el-row><template #footer><div class="dialog-footer"><el-button @click="editAvatarDialog = false">取 消</el-button><el-button type="primary" @click="editAvatarConfirm">确 定</el-button></div></template></el-dialog>
</template><script setup lang="ts">
import "vue-cropper/dist/index.css";
import { VueCropper } from "vue-cropper";
import {ZoomIn,ZoomOut,Download,RefreshLeft,RefreshRight,
} from "@element-plus/icons-vue";
import { ref } from "vue";
import { Plus, Edit, Delete } from "@element-plus/icons-vue";
import type { UploadProps } from "element-plus";const { imgType, drag, disabled, maxImgSize } = defineProps({imgType: {type: String,default: "image/*",},drag: {type: Boolean,default: false,},disabled: {type: Boolean,default: false,},maxImgSize: {type: Number,default: 2,},
});const imageUrl = defineModel<string>();const { uploadImage } = useImageUpload();
const editAvatarDialog = ref(false);
const imageName = ref("");const previews = ref<any>({url: "",file: null,
});
// 响应式变量
const callbackMessage = useCallbackMessage();
const cropper = ref();const option = ref({autoCrop: true, // 是否默认生成截图框autoCropHeight: "240px", // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoCropHeight * 1.25autoCropWidth: "240px", // 默认生成截图框宽度(默认值:容器的 80%, 可选值:0 ~ max), 真正裁剪出来的图片的宽度为 autoWidth * 1.25canMove: true, // 上传图片是否可以移动canScale: true, // 图片是否允许滚轮缩放centerBox: true, // 截图框是否被限制在图片里面fixed: true, // 是否固定截图框的宽高比例fixedBox: true, // 是否固定截图框大小fixedNumber: [1, 1], // 截图框的宽高比例([ 宽度 , 高度 ])img: "", // 裁剪图片的地址(可选值:url 地址, base64, blob)info: true, // 是否显示裁剪框的宽高信息infoTrue: true, // infoTrue为 true 时裁剪框显示的是预览图片的宽高信息,infoTrue为 false 时裁剪框显示的是裁剪框的宽高信息mode: "contain", // 截图框可拖动时的方向(可选值:contain , cover, 100px, 100% auto)origin: false, // 上传的图片是否按照原始比例渲染outputSize: 1, // 裁剪生成图片的质量(可选值:0.1 ~ 1)outputType: "png", // 裁剪生成图片的格式(可选值:png, jpeg, webp)full: true,
});const beforeAvatarUpload: UploadProps["beforeUpload"] = (rawFile) => {if (rawFile.size / 1024 / 1024 > maxImgSize) {callbackMessage.value = {show: true,valid: false,content: `图片大小不能超过${maxImgSize}MB!`,};return false;}return true;
};const handleAvatarSuccess: UploadProps["onSuccess"] = (response,uploadFile
) => {// ! 为 TS 的非空断言option.value.img = URL.createObjectURL(uploadFile.raw!);editAvatarDialog.value = true;imageName.value = uploadFile.name;
};// 实时预览
const realTime = () => {cropper.value.getCropBlob((blob: Blob) => {previews.value.url = window.URL.createObjectURL(blob);previews.value.file = blobToFile(blob, imageName.value);});
};const editAvatarConfirm = async () => {editAvatarDialog.value = false;const res = await uploadImage(previews.value.file);if (Array.isArray(res?.data) && res.data.length) {imageUrl.value = res.data[0].url;imageName.value = res.data[0].filename;callbackMessage.value = {show: true,valid: true,content: `上传成功`,};} else {callbackMessage.value = {show: true,valid: false,content: `上传失败`,};}
};const downloadPreView = () => {let aLink = document.createElement("a");aLink.download = "头像裁剪后的效果图.png";cropper.value.getCropBlob((blob: Blob) => {aLink.href = window.URL.createObjectURL(blob);aLink.click();});
};const rotateLeft = () => {cropper.value.rotateLeft();
};const rotateRight = () => {cropper.value.rotateRight();
};const changeScale = (scaleSize: number) => {cropper.value.changeScale(scaleSize);
};const handleDeleteAvatar = () => {if (!imageName.value) {imageName.value = imageUrl.value?.split("/").pop() || "";}$fetch(`/api/upload/delete`, {body: { filename: imageName.value },method: "POST",}).then((res) => {callbackMessage.value = {show: true,valid: true,content: `删除成功`,};imageUrl.value = "";});
};const handleEditAvatar = () => {if (imageUrl.value) {imageName.value = imageUrl.value.split("/").pop() || "";}option.value.img = imageUrl.value || "";editAvatarDialog.value = true;
};
</script><style scoped>
.previewBox {text-align: center;margin-left: 60px;
}.preview {width: 150px;height: 150px;margin: 0px auto 20px auto;border-radius: 50%;border: 1px solid #ccc;background-color: #ccc;overflow: hidden;
}.cropper {width: 260px;height: 260px;
}.avatar-uploader .avatar {width: 178px;height: 178px;display: block;
}.avatar-container {position: relative;
}.avatar:hover + .avatar-actions,
.avatar-actions:hover {display: flex;
}
</style><style>
.avatar-uploader .el-upload {border: 1px dashed var(--el-border-color);border-radius: 50% !important;cursor: pointer;position: relative;overflow: hidden;transition: var(--el-transition-duration-fast);
}.avatar-uploader .el-upload:hover {border-color: var(--el-color-primary);
}.el-icon.avatar-uploader-icon {font-size: 28px;color: #8c939d;width: 178px;height: 178px;text-align: center;
}
</style>

相关组件

components/SUI/S-msgWin.vue

<script lang="ts" setup>
const props = defineProps({msg: {type: Object,required: true,},top: {type: String,default: "50%",},duration: {type: Number,default: 3000,},
});
watch(() => props.msg,(newVal, oldVal) => {if (newVal.show) {setTimeout(() => {props.msg.show = false;}, props.duration);}}
);
</script>
<template><transition name="fade"><divv-show="msg.show"class="msgBox":class="{'border-#fde2e2 bg-red-50 text-#f56c6c': !msg.valid,'border-green-800 bg-green-50 text-green-500': msg.valid,}"><S-icon :icon="msg.valid ? 'ep:success-filled' : 'ix:error-filled'" /><div class="whitespace-nowrap">{{ msg.content }}</div><S-iconv-if="msg.closeable"class="c-#a8abb2 cursor-pointer"icon="material-symbols:close-rounded"@click="msg.show = false"/></div></transition>
</template>
<style scoped>
.msgBox {font-size: 14px;position: absolute;display: flex;flex-wrap: nowrap;align-items: center;gap: 6px;padding: 8px 10px;top: v-bind(props.top);left: 50%;transform: translate(-50%, -50%);z-index: 9999;border-radius: 4px;
}.fade-leave-from,
.fade-enter-to {opacity: 1;
}
.fade-leave-to,
.fade-enter-from {opacity: 0;
}
/* 定义过渡的持续时间和动画效果 */
.fade-enter-active,
.fade-leave-active {transition: opacity 0.3s ease;
}
</style>

相关组合式函数

composables/useCallbackMessage.ts

export const useCallbackMessage = () => {const callbackMessage = ref({show: false,valid: true,content: "",});return callbackMessage;
};

composables/useImageUpload.ts

export const useImageUpload = () => {const isLoading = ref(false);const error = ref<string | null>(null);const uploadImage = async (file: File) => {isLoading.value = true;error.value = null;try {const formData = new FormData();formData.append("image", file);const response = await $fetch("/api/upload/image", {method: "POST",body: formData,headers: {Accept: "application/json",},});return response;} catch (err: any) {error.value = err.message || "上传失败,请重试";throw err;} finally {isLoading.value = false;}};return {uploadImage,isLoading,error,};
};

页面使用

      <S-avatar:disabled="action === 'detail'"v-model="formData.avatar"/>

Nuxt 中使用

因 vue-cropper 不支持服务端渲染,所以必须限定其仅在客户端渲染

import { ref, onMounted } from "vue";
import { defineAsyncComponent } from "vue";// 标记客户端环境
const isClient = ref(false);// 动态导入组件,禁用SSR
const AvatarCropper = defineAsyncComponent({loader: () => import("~/components/SUI/S-avatar.vue"),suspensible: false, // 关键:禁止在服务端渲染该组件,使用 suspensible 替代 ssr
});onMounted(() => {isClient.value = true; // 确保在客户端挂载后才显示组件
});
      <AvatarCropper:disabled="action === 'detail'"v-if="isClient"v-model="formData.avatar"/>

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

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

相关文章

铜金矿数据分组优化系统设计与实现

铜金矿数据分组优化系统设计与实现 1. 项目概述 本项目旨在开发一个Python程序,用于根据给定的四组分组规则,优化包含金吨、干吨和铜单价等信息的Excel数据分组,以最大化总金额。系统需要处理的核心计算是每条数据的铜货值,其公式为:结算铜金吨 铜单价 (价格系数 + 奖…

Python动态规划:从基础到高阶优化的全面指南(3)

七、动态规划性能优化实战7.1 矩阵快速幂优化def matrix_mult(A, B):"""矩阵乘法"""n len(A)m len(B[0])p len(B)C [[0]*m for _ in range(n)]for i in range(n):for k in range(p):if A[i][k]:for j in range(m):C[i][j] A[i][k] * B[k][j…

海外红人营销的下一站:APP出海如何布局虚拟网红与UGC生态?

在全球移动互联网竞争日益激烈的今天&#xff0c;APP出海推广的重心正从传统流量采买和真人KOL合作&#xff0c;逐步向更具未来感的方向演进。虚拟网红、AI生成内容以及用户生成内容的融合&#xff0c;正为海外红人营销注入全新活力。这不仅是技术革新&#xff0c;更是用户行为…

CentOS网卡未被托管解决记录

VMWare挂起关机&#xff0c;又重启后&#xff0c;出现一些很奇怪的问题。 我的几台CentOS的网卡都不见了&#xff0c;显示网卡未被托管 [rootlocalhost ~]# nmcli device status DEVICE TYPE STATE CONNECTION virbr0 bridge 未托管 -- ens33 …

Node.js 中的内置模板path

1. path的作用&#xff1a;path 是 Node.js 中的一个内置模块&#xff0c;用于处理文件和目录路径。它提供了一些工具来处理路径字符串&#xff0c;确保路径操作跨平台兼容&#xff08;Windows 和 Unix 风格的路径分隔符&#xff09;2.path的常用方法path.join()和数组的join方…

重生之我在暑假学习微服务第三天《Docker-上篇》

个人主页&#xff1a;VON文章所属专栏&#xff1a;微服务系列文章链接&#xff1a;重生之我在暑假学习微服务第一天《MybatisPlus-上篇》-CSDN博客重生之我在暑假学习微服务第二天《MybatisPlus-下篇》-CSDN博客时间&#xff1a;每天12点前准时更新 特别声明&#xff1a;本篇文…

【硬件】LT3763中文手册

目录 1.简介 1.1 特点 1.2 简述 1.3 典型原理图 1.4 绝对最大额定值 2.电气特性 3.引脚功能 4.框图 4.1 设计电感电流 4.2 电感选择 4.3 开关MOSFET选择 4.4 输入电容选择 4.5 输出电容选择 4.6 CBOOST电容选择 4.7 INTVCC电容器选择 4.8 Soft-Start 4.9 输出电流…

【计算机科学与应用】基于多域变换的视频水印嵌入算法研究

导读&#xff1a; 为提升视频水印在版权保护中的实际应用效果&#xff0c;本文提出一种基于多域变换的视频水印嵌入算法。该算法结合离散小波变换(Discrete Wavelet Transform, DWT)与离散余弦变换(Discrete Cosine Transformation, DCT)&#xff0c;利用其在时频域分析与能量…

Axios基本使用

介绍 Axios 是一个基于promise网络请求库&#xff0c;作用于node.js和浏览器中 特性 从浏览器创建 XMLHttpRequests从 node.js 创建 http 请求支持 Promise API拦截请求和响应转换请求和响应数据取消请求自动转换JSON数据客户端支持防御XSRF 安装 项目中 npm install axi…

【大模型LLM】梯度累积(Gradient Accumulation)原理详解

梯度累积&#xff08;Gradient Accumulation&#xff09;原理详解 梯度累积是一种在深度学习训练中常用的技术&#xff0c;特别适用于显存有限但希望使用较大批量大小&#xff08;batch size&#xff09;的情况。通过梯度累积&#xff0c;可以在不增加单个批次大小的情况下模拟…

阿里云Ubuntu 22.04 ssh隔一段时间自动断开的解决方法

在使用ssh连接阿里云ubuntu22.04隔一段时间之后就自动断开&#xff0c;很影响体验&#xff0c;按照如下配置就可以解决vim /etc/ssh/sshd_config

R中匹配函数

在 R 中&#xff0c;字符串匹配是一个常见的任务&#xff0c;可以使用正则表达式或非正则表达式的方法来完成。以下是对这些方法的总结&#xff0c;包括在向量和数据框中的应用。 正则表达式匹配 常用函数grepl&#xff1a; 功能&#xff1a;检查向量中的每个元素是否匹配某个正…

Ubuntu服务器上JSP运行缓慢怎么办?全面排查与优化方案

随着企业系统越来越多地部署在Linux平台上&#xff0c;Ubuntu成为JSP Web系统常见的部署环境。但不少开发者会遇到一个共同的问题&#xff1a;在Ubuntu服务器上运行的JSP项目访问缓慢、页面加载时间长&#xff0c;甚至出现卡顿现象。这类问题如果不及时解决&#xff0c;容易导致…

web刷题

[极客大挑战 2019]RCE ME 打开环境&#xff0c;代码逻辑还是很简单的 思路是传参code参数&#xff0c;一般传参shell然后用蚁剑连接看flag&#xff0c;但是这题做了之后就会发现思路是没错但是这题多了一些验证&#xff0c;这题就是无字符rce&#xff0c;可以考虑用取反&…

FFmpeg+javacpp中FFmpegFrameGrabber

FFmpegjavacpp中FFmpegFrameGrabber1、FFmpegFrameGrabber1.1 Demo使用1.2 音频相关1.3 视频相关2、Frame属性2.1 视频帧属性2.2 音频帧属性2.3 音频视频区分JavaCV 1.5.12 API JavaCPP Presets for FFmpeg 7.1.1-1.5.12 API1、FFmpegFrameGrabber org\bytedeco\javacv\FFmpeg…

1-FPGA的LUT理解

FPGA的LUT理解 FPGA的4输入LUT中&#xff0c;SRAM存储的16位二进制数&#xff08;如 0110100110010110&#xff09;直接对应真值表的输出值。下面通过具体例子详细解释其含义&#xff1a; 1. 4输入LUT 4输入LUT的本质是一个161的SRAM&#xff0c;它通过存储真值表的方式实现任意…

Vue2文件上传相关

导入弹窗<template><el-dialog:title"title":visible.sync"fileUploadVisible"append-to-bodyclose-on-click-modalclose-on-press-escapewidth"420px"><div v-if"showDatePicker">选择时间&#xff1a;<el-date…

vue使用xlsx库导出excel

引入xlsx库 import XLSX from "xlsx";将后端接口返回的数据和列名&#xff0c;拼接到XLSX.utils.aoa_to_sheet中exportExcel() {debugger;if (!this.feedingTableData || this.feedingTableData.length "0") {this.$message.error("投料信息为空&…

卷积神经网络(CNN)处理流程(简化版)

前言 是看了这个大佬的视频后想进行一下自己的整理&#xff08;流程只到了扁平化&#xff09;&#xff0c;如果有问题希望各位大佬能够给予指正。卷积神经网络&#xff08;CNN&#xff09;到底卷了啥&#xff1f;8分钟带你快速了解&#xff01;_哔哩哔哩_bilibilihttps://www.…

DBSyncer:开源免费的全能数据同步工具,多数据源无缝支持!

DBSyncer&#xff08;英[dbsɪŋkɜː]&#xff0c;美[dbsɪŋkɜː 简称dbs&#xff09;是一款开源的数据同步中间件&#xff0c;提供MySQL、Oracle、SqlServer、PostgreSQL、Elasticsearch(ES)、Kafka、File、SQL等同步场景。支持上传插件自定义同步转换业务&#xff0c;提供…