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
英文地址,請注明出處:http://en.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;提供…