Vue3封装支持Base64导出的电子签名组件
效果图



准备工作
组件内用到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导出的电子签名组件的更多相关文章
- Vue3 封装 Element Plus Menu 无限级菜单组件
本文分别使用 SFC(模板方式)和 tsx 方式对 Element Plus el-menu 组件进行二次封装,实现配置化的菜单,有了配置化的菜单,后续便可以根据路由动态渲染菜单. 1 数据结构定义 ...
- NPOI读写Excel组件封装Excel导入导出组件
后台管理系统多数情况会与Excel打交道,常见的就是Excel的导入导出,对于Excel的操作往往是繁琐且容易出错的,对于后台系统的导入导出交互过程往往是固定的,对于这部分操作,我们可以抽离出公共组件 ...
- React Native封装Toast与加载Loading组件
React Native开发封装Toast与加载Loading组件 在App开发中,我们避免不了使用的两个组件,一个Toast,一个网络加载Loading,在RN开发中,也是一样,React Nati ...
- 并发编程概述 委托(delegate) 事件(event) .net core 2.0 event bus 一个简单的基于内存事件总线实现 .net core 基于NPOI 的excel导出类,支持自定义导出哪些字段 基于Ace Admin 的菜单栏实现 第五节:SignalR大杂烩(与MVC融合、全局的几个配置、跨域的应用、C/S程序充当Client和Server)
并发编程概述 前言 说实话,在我软件开发的头两年几乎不考虑并发编程,请求与响应把业务逻辑尽快完成一个星期的任务能两天完成绝不拖三天(剩下时间各种浪),根本不会考虑性能问题(能接受范围内).但随着工 ...
- 一个 OpenTiny,Vue2 Vue3 都支持!
大家好,我是 Kagol,OpenTiny 开源社区运营,TinyVue 跨端.跨框架组件库核心贡献者,专注于前端组件库建设和开源社区运营. 今天给大家介绍如何同时在 Vue2 和 Vue3 项目中使 ...
- 基于DotNetCoreNPOI封装特性通用导出excel
基于DotNetCoreNPOI封装特性通用导出excel 目前根据项目中的要求,支持列名定义,列索引排序,行合并单元格,EXCEL单元格的格式也是随着数据的类型做对应的调整. 效果图: 调用方式 可 ...
- 封装Vue Element的table表格组件
上周分享了几篇关于React组件封装方面的博文,这周就来分享几篇关于Vue组件封装方面的博文,也好让大家能更好地了解React和Vue在组件封装方面的区别. 在封装Vue组件时,我依旧会交叉使用函数式 ...
- 封装React AntD的dialog弹窗组件
前一段时间分享了基于vue和element所封装的弹窗组件(封装Vue Element的dialog弹窗组件),今天就来分享一个基于react和antD所封装的弹窗组件,反正所使用的技术还是那个技术, ...
- 整理目前支持 Vue 3 的 UI 组件库 (2021 年)
最近,让前端圈子振奋的消息莫过于 Vue 3.0 的发布,一个无论是性能还是 API 设计都有了重大升级的新版本.距离 Vue 3.0 正式版发布已经有一段时间了,相信相关生态周边库也正在适配新版本中 ...
- vue3 封装简单的 tabs 切换组件
背景:公司项目要求全部换成 vue3 ,而且也没有应用像 element-ui 一类的UI组件,用到的公共组件都是根据项目需求封装的,下面是使用vue3实现简单的tabs组件,我只是把代码分享出来,实 ...
随机推荐
- 状压 DP 做题记录
1.普通状态压缩 DP oi-wiki I.P1896 [SCOI2005] 互不侵犯 \(f_{i,j,st}\) 表示前 \(i\) 行中放置了 \(j\) 个国王,当前行状态为 \(st\) 的 ...
- w3cschool-Python3 高级教程
https://www.w3cschool.cn/python3/python3-reg-expressions.html Python3 正则表达式 re.match 函数 re.match 尝试从 ...
- 分布式事务---2PC和3PC原理TCC事务
分布式事务(1)---2PC和3PC原理 分布式事物基本理论:基本遵循CPA理论,采用柔性事物特征,软状态或者最终一致性特点保证分布式事物一致性问题. 分布式事物常见解决方案: 2PC两段提交协议 3 ...
- 脱离实体类操作数据库(mysql版本)
原理很简单:1.利用mysql的information_schema库,获取对用表的信息: 2.使用DataSource,建立数据库连接,并执行sql脚本: 3.Map的keySet和values集合 ...
- ORM(Object Relational Mapping:对象关系映射)
了解orm,先了解以下概念: 什么是"持久化" 持久(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘).持久化的主要应用是将内存中的数据存 ...
- ORACLE存储过程中使用游标+BULK COLLECT的应用
经过半天的折腾,编译通过调试结果正确,掌握此过程中的知识点,oracle存储过程编写就应用到了90%. CREATE OR REPLACE PROCEDURE JUNAN.P_IPT_QUOTN_IN ...
- Springboot 3.x 使用PageHelper实现MyBatis分页查询
开发环境 SpringBoot 3.0.1 Maven 工程 JDK OpenJdk 17.0.6 引入pom依赖 <dependency> <groupId>com.gith ...
- [POJ3237] 树的维护 题解
一眼树链剖分或 \(LCT\),由于在学后者所以就写了. 取反操作相当于把 \(min,max\) 取反后交换,所以要维护 \(min,max,val\). 时间复杂度 \(O(m\log n)\). ...
- QT5笔记:34. 视口和窗口
 例子: void Widget::paintEvent(QP ...
- linux 删除文件提示 opration not permitted 处理方法(宝塔删除文件提示无法删除)
问题描述:linux系统中使用rm -rf强制删除文件,提示 opration not permitted,无法删除成功(宝塔删除文件提示无法删除),该问题确定为已关闭所有安全软件及防止恶意篡改的软件 ...