效果图

准备工作

组件内用到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 = () => {
// 创建canvas并设置缩放尺寸
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); // 计算压缩后尺寸(取原图50%)
const targetWidth = img.width * 0.5;
const targetHeight = img.height * 0.5; // 设置画布尺寸
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
); // 质量参数生效于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内存
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="经办人签字" prop="signatureHandler" label-width="8em">
<!-- 签名组件 -->
<signature-component v-model="form.signatureHandler" />
</el-form-item>
</el-form>

注意事项

使用时将组件内的提示框替换为elementPlus官方的

generateUUID方法自行修改为生成UUID的方法,也可以去掉。

 

有想法和建议欢迎留言讨论

Vue3封装支持Base64导出的电子签名组件的更多相关文章

  1. Vue3 封装 Element Plus Menu 无限级菜单组件

    本文分别使用 SFC(模板方式)和 tsx 方式对 Element Plus el-menu 组件进行二次封装,实现配置化的菜单,有了配置化的菜单,后续便可以根据路由动态渲染菜单. 1 数据结构定义 ...

  2. NPOI读写Excel组件封装Excel导入导出组件

    后台管理系统多数情况会与Excel打交道,常见的就是Excel的导入导出,对于Excel的操作往往是繁琐且容易出错的,对于后台系统的导入导出交互过程往往是固定的,对于这部分操作,我们可以抽离出公共组件 ...

  3. React Native封装Toast与加载Loading组件

    React Native开发封装Toast与加载Loading组件 在App开发中,我们避免不了使用的两个组件,一个Toast,一个网络加载Loading,在RN开发中,也是一样,React Nati ...

  4. 并发编程概述 委托(delegate) 事件(event) .net core 2.0 event bus 一个简单的基于内存事件总线实现 .net core 基于NPOI 的excel导出类,支持自定义导出哪些字段 基于Ace Admin 的菜单栏实现 第五节:SignalR大杂烩(与MVC融合、全局的几个配置、跨域的应用、C/S程序充当Client和Server)

    并发编程概述   前言 说实话,在我软件开发的头两年几乎不考虑并发编程,请求与响应把业务逻辑尽快完成一个星期的任务能两天完成绝不拖三天(剩下时间各种浪),根本不会考虑性能问题(能接受范围内).但随着工 ...

  5. 一个 OpenTiny,Vue2 Vue3 都支持!

    大家好,我是 Kagol,OpenTiny 开源社区运营,TinyVue 跨端.跨框架组件库核心贡献者,专注于前端组件库建设和开源社区运营. 今天给大家介绍如何同时在 Vue2 和 Vue3 项目中使 ...

  6. 基于DotNetCoreNPOI封装特性通用导出excel

    基于DotNetCoreNPOI封装特性通用导出excel 目前根据项目中的要求,支持列名定义,列索引排序,行合并单元格,EXCEL单元格的格式也是随着数据的类型做对应的调整. 效果图: 调用方式 可 ...

  7. 封装Vue Element的table表格组件

    上周分享了几篇关于React组件封装方面的博文,这周就来分享几篇关于Vue组件封装方面的博文,也好让大家能更好地了解React和Vue在组件封装方面的区别. 在封装Vue组件时,我依旧会交叉使用函数式 ...

  8. 封装React AntD的dialog弹窗组件

    前一段时间分享了基于vue和element所封装的弹窗组件(封装Vue Element的dialog弹窗组件),今天就来分享一个基于react和antD所封装的弹窗组件,反正所使用的技术还是那个技术, ...

  9. 整理目前支持 Vue 3 的 UI 组件库 (2021 年)

    最近,让前端圈子振奋的消息莫过于 Vue 3.0 的发布,一个无论是性能还是 API 设计都有了重大升级的新版本.距离 Vue 3.0 正式版发布已经有一段时间了,相信相关生态周边库也正在适配新版本中 ...

  10. vue3 封装简单的 tabs 切换组件

    背景:公司项目要求全部换成 vue3 ,而且也没有应用像 element-ui 一类的UI组件,用到的公共组件都是根据项目需求封装的,下面是使用vue3实现简单的tabs组件,我只是把代码分享出来,实 ...

随机推荐

  1. 阿里云IP遭受DDOS攻击 快速切换IP实践

    阿里云IP遭受DDOS攻击 快速切换IP实践 #1 介绍 运行平台: 阿里云 访问链路: 域名 -> 负载均衡EIP -> 容器 网站无法访问,查询服务运行正常,查询公网流量异常高后断流了 ...

  2. 某苏人社异步JS逆向加解密分析(sm2+sm3+sm4)

    前期准备 网址:aHR0cHM6Ly9ycy5qc2hyc3MuamlhbmdzdS5nb3YuY24vaW5kZXgv 目标: 接口请求内容加密和响应内容解密 请求头加密参数 Web-Encrypt ...

  3. 性能测试工具_nGrinder

    1. ngrinder-controller-3.4.3.war 放置到tomcat的webapps目录下:2. 启动tomcat;3. 访问地址: http://localhost:8080/ngr ...

  4. legged-robot关于locomotion、Navigation任务主要文章速览

    0. 前言 目前legged robot包括locomotion(怎么走).navigation(往哪走).人形机器人的whole body control以及基于机械臂的manipulation的任 ...

  5. 探索AI,拥抱未来,欢迎加入魔乐世界!

    近日,2024开放原子开源生态大会在北京亦庄开幕,大会以"开源赋能产业,生态共筑未来"为主题,来自政府.企业.学术界.研究机构的专家学者汇聚一堂,共同探讨开源在人工智能领域的创新应 ...

  6. golang轻量级版本管理工具g安装使用

    使用 g 可以在 windows 上切换使用不同版本的 golang GitHub仓库地址 https://github.com/voidint/g GitHub下载连接 https://github ...

  7. nacos(二):原生java调用配置与服务实例

    本文主要介绍原生java连接nacos,操作nacos的共享配置与服务实例. 1.引入依赖 <dependencies> <dependency> <groupId> ...

  8. 腾讯云HAI与DeepSeek携手打造私有化高效代码助手

    今天,我们依然以DeepSeek-R1大模型为核心,继续探索其在实际场景中的可用性.今天的重点将放在基于DeepSeek-R1大模型,结合JetBrains IDEA 插件代码助手(CodeGPT)进 ...

  9. Django项目与Vue的集成

    Django项目与Vue的集成 在现代Web开发领域,前后端分离已成为一种主流趋势.Django,作为一个强大的Python Web框架,以其丰富的功能和高度的可扩展性而受到开发者的青睐.而Vue.j ...

  10. Luogu P2540 NOIP2015提高组 斗地主 加强版 题解 [ 紫 ] [ 深搜 ] [ 剪枝 ]

    斗地主:一步一步推性质就能做出来的剪枝题. 这题思路和小木棒的剪枝思路极其相似,剪枝的角度都差不多. 其实大部分搜索剪枝题都是先观察性质,列出性质后选择几个比较关键且代码好写的性质进行剪枝,特别要注意 ...