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的方法,也可以去掉。

原文链接:https://www.cnblogs.com/Wei-notes/p/18797838

 

posted @ 2025-04-24 15:50  Shimily  阅读(10)  评论(0)    收藏  举报