使用Vue3封裝實現(xiàn)支持Base64導(dǎo)出的電子簽名組件
默認支持簽字回顯,base64壓縮,內(nèi)存釋放
傳參支持禁用簽字也就是查看,組件大小內(nèi)置'small', 'default', 'large'三個大小
效果圖



準(zhǔn)備工作
組件內(nèi)用到elementPlus,vue-esign組件,使用前提前安裝好。
組件代碼
<template>
<!-- 簽名容器 -->
<div class="sign-container" >
<div class="sign-preview" :class="[sizeClass, { 'has-sign': base64Img }]" @click="openDialog">
<img v-if="base64Img" :src="base64Img" class="preview-image" />
<div v-else class="placeholder">
<el-icon><EditPen /></el-icon>
<span>點擊簽名</span>
</div>
</div>
<!-- 簽字彈窗 -->
<el-dialog v-model="dialogVisible" title="電子簽名" width="800px">
<vue-esign ref="esignRef" :width="800" :height="300" :lineWidth="4" :lineColor="'#000000'" :bgColor="'#ffffff'" :id="uuid" />
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button @click="handleReset">清空</el-button>
<el-button type="primary" @click="handleConfirm">確認</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { generateUUID } from '/@/utils/other';
import vueEsign from 'vue-esign';
// 生成組件唯一id
const uuid = ref('id-' + generateUUID());
// 組件尺寸
const sizeClass = computed(() => `sign-size--${props.size}`);
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
modelValue: String, // v-model綁定
disabled: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'default',
validator: (v) => ['small', 'default', 'large'].includes(v),
},
});
const dialogVisible = ref(false);
const esignRef = ref(null);
const base64Img = ref(props.modelValue);
// 打開彈窗時重置畫布
const openDialog = () => {
if (props.disabled) return;
dialogVisible.value = true;
// handleReset();
};
// 清空畫布(保留二次確認)
const handleReset = async () => {
try {
await useMessageBox().confirm('此操作將清空簽名,確定嗎?');
esignRef.value?.reset();
} catch {}
};
// 生成簽名后壓縮
const compressBase64 = (base64) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = base64;
img.onload = () => {
// 創(chuàng)建canvas并設(shè)置縮放尺寸
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 計算壓縮后尺寸(取原圖50%)
const targetWidth = img.width * 0.5;
const targetHeight = img.height * 0.5;
// 設(shè)置畫布尺寸
canvas.width = targetWidth;
canvas.height = targetHeight;
// 繪制壓縮圖像
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
// 生成新base64(自動處理格式)
const mimeType = base64.match(/data:(.*?);/)[1];
canvas.toBlob(
(blob) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
},
mimeType,
0.6
); // 質(zhì)量參數(shù)生效于JPEG/WebP格式
};
img.onerror = (e) => reject('圖片加載失敗');
});
};
// 確認簽名
const handleConfirm = async () => {
esignRef.value
.generate()
.then(async (res) => {
base64Img.value = await compressBase64(res);
// 驗證壓縮效果
const originalSize = Math.round((res.length * 3) / 4 / 1024);
const compressedSize = Math.round((base64Img.value.length * 3) / 4 / 1024);
console.log(` 尺寸變化:${originalSize}KB → ${compressedSize}KB`);
emit('update:modelValue', base64Img.value);
dialogVisible.value = false;
})
.catch(() => {
base64Img.value = '';
emit('update:modelValue', '');
dialogVisible.value = false;
});
};
// watch同步
watch(
() => props.modelValue,
async (val) => {
if (!val) {
base64Img.value = '';
esignRef.value?.reset();
}
console.log(val);
base64Img.value = await compressBase64(val);
}
);
onBeforeUnmount(() => {
// 釋放canvas內(nèi)存
const canvas = esignRef.value?.$el.querySelector('canvas');
canvas.width = 0;
canvas.height = 0;
URL.revokeObjectURL(base64Img.value); // 釋放Blob URL
});
</script>
<style scoped lang="scss">
.sign-container {
display: inline-block;
cursor: pointer;
}
.sign-preview {
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 4px;
&.sign-size--small {
width: 120px;
height: 60px;
}
&.sign-size--default {
width: 180px;
height: 90px;
}
&.sign-size--large {
width: 240px;
height: 120px;
}
&.has-sign {
border-color: var(--el-color-primary);
}
.preview-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.placeholder {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
.el-icon {
margin-right: 8px;
font-size: 18px;
}
}
}
</style>使用組件
<el-form ref="dataFormRef" :model="form" inline :rules="dataRules">
<el-form-item label="經(jīng)辦人簽字" prop="signatureHandler" label-width="8em">
<!-- 簽名組件 -->
<signature-component v-model="form.signatureHandler" />
</el-form-item>
</el-form>注意事項
使用時將組件內(nèi)的提示框替換為elementPlus官方的
generateUUID方法自行修改為生成UUID的方法,也可以去掉。
到此這篇關(guān)于使用Vue3封裝實現(xiàn)支持Base64導(dǎo)出的電子簽名組件的文章就介紹到這了,更多相關(guān)Vue3電子簽名內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue在install時node-sass@4.14.1?postinstall:node?scripts/buil
最近在npm install 的時候遇到了個問題,所以給大家總結(jié)下,下面這篇文章主要給大家介紹了關(guān)于vue在install時node-sass@4.14.1?postinstall:node?scripts/build.js錯誤的解決方法,需要的朋友可以參考下2023-05-05
使用vue-element-admin框架從后端動態(tài)獲取菜單功能的實現(xiàn)
​ vue-element-admin是一個純前端的框架,左側(cè)菜單是根據(jù)路由生成的。實際開發(fā)中經(jīng)常需要根據(jù)當(dāng)前登陸人員的信息從后端獲取菜單進行展示,本文將詳細介紹如何實現(xiàn)該功能2021-04-04
前端vue中實現(xiàn)文件下載的幾種方法總結(jié)
這篇文章主要介紹了前端vue中實現(xiàn)文件下載的幾種方法總結(jié),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04
Vue + Echarts頁面內(nèi)存占用高的問題解決方案
點擊左側(cè)的菜單可以切換不同的看板,有些看板頁面中的報表比較多,用戶多次切換后頁面的內(nèi)存占用可以上升為GB級,嚴重時導(dǎo)致頁面內(nèi)存溢出,使得頁面崩潰,極大影響了用戶體驗,本文給大家介紹Vue + Echarts頁面內(nèi)存占用高的問題解決方案,感興趣的朋友一起看看吧2024-02-02
vue 2.1.3 實時顯示當(dāng)前時間,每秒更新的方法
今天小編就為大家分享一篇vue 2.1.3 實時顯示當(dāng)前時間,每秒更新的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09
element-ui中el-cascader動態(tài)加載和默認值詳解
vue+elementUI項目中el-cascader級聯(lián)選擇器使用頻率非常高,下面這篇文章主要給大家介紹了關(guān)于element-ui中el-cascader動態(tài)加載和默認值的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2023-05-05

