Blazor组件自做四 : 使用JS隔离封装signature_pad签名组件


运行截图

演示地址

响应式演示

感谢szimek写的棒棒的signature_pad.js项目, 来源: https://github.com/szimek/signature_pad

正式开始

1. 在文件夹wwwroot/lib,添加signature_pad子文件夹,里面下载库文件(文件文末源码里可复制) signature_pad.umd.js复制到此文件夹. 最终版本参考如下

+signature_pad
  |-signature_pad.umd.js

2. 添加app.js文件

+signature_pad
  |-app.js

代码里 wrapperc.invokeMethodAsync("signatureResult", imgBase64) 为签名canvas结果回调到c#

js代码
import '/lib/signature_pad/signature_pad.umd.js';

export function init(wrapperc, element, alertText,) {
    //Code modify from https://github.com/szimek/signature_pad
    var wrapper = element;//document.getElementById("signature-pad");
    var clearButton = wrapper.querySelector("[data-action=clear]");
    var changeColorButton = wrapper.querySelector("[data-action=change-color]");
    var undoButton = wrapper.querySelector("[data-action=undo]");
    var saveBase64Button = wrapper.querySelector("[data-action=save-base64]");
    var savePNGButton = wrapper.querySelector("[data-action=save-png]");
    var saveJPGButton = wrapper.querySelector("[data-action=save-jpg]");
    var saveSVGButton = wrapper.querySelector("[data-action=save-svg]");
    var canvas = wrapper.querySelector("canvas");
    var signaturePad = new SignaturePad(canvas, {
        // It's Necessary to use an opaque color when saving image as JPEG;
        // this option can be omitted if only saving as PNG or SVG
        backgroundColor: 'rgb(255, 255, 255)'
    });

    // Adjust canvas coordinate space taking into account pixel ratio,
    // to make it look crisp on mobile devices.
    // This also causes canvas to be cleared.
    function resizeCanvas() {
        // When zoomed out to less than 100%, for some very strange reason,
        // some browsers report devicePixelRatio as less than 1
        // and only part of the canvas is cleared then.
        var ratio = Math.max(window.devicePixelRatio || 1, 1);

        // This part causes the canvas to be cleared
        canvas.width = canvas.offsetWidth * ratio;
        canvas.height = canvas.offsetHeight * ratio;
        canvas.getContext("2d").scale(ratio, ratio);

        // This library does not listen for canvas changes, so after the canvas is automatically
        // cleared by the browser, SignaturePad#isEmpty might still return false, even though the
        // canvas looks empty, because the internal data of this library wasn't cleared. To make sure
        // that the state of this library is consistent with visual state of the canvas, you
        // have to clear it manually.
        signaturePad.clear();
    }

    // On mobile devices it might make more sense to listen to orientation change,
    // rather than window resize events.
    window.onresize = resizeCanvas;
    resizeCanvas();

    function download(dataURL, filename) {
        if (navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") === -1) {
            window.open(dataURL);
        } else {
            var blob = dataURLToBlob(dataURL);
            var url = window.URL.createObjectURL(blob);

            var a = document.createElement("a");
            a.style = "display: none";
            a.href = url;
            a.download = filename;

            document.body.appendChild(a);
            a.click();

            window.URL.revokeObjectURL(url);
        }
    }

    // One could simply use Canvas#toBlob method instead, but it's just to show
    // that it can be done using result of SignaturePad#toDataURL.
    function dataURLToBlob(dataURL) {
        // Code taken from https://github.com/ebidel/filer.js
        var parts = dataURL.split(';base64,');
        var contentType = parts[0].split(":")[1];
        var raw = window.atob(parts[1]);
        var rawLength = raw.length;
        var uInt8Array = new Uint8Array(rawLength);

        for (var i = 0; i < rawLength; ++i) {
            uInt8Array[i] = raw.charCodeAt(i);
        }

        return new Blob([uInt8Array], { type: contentType });
    }

    if (clearButton) clearButton.addEventListener("click", function (event) {
        signaturePad.clear();
        return wrapperc.invokeMethodAsync("signatureResult", null);
    });

    if (undoButton) undoButton.addEventListener("click", function (event) {
        var data = signaturePad.toData();

        if (data) {
            data.pop(); // remove the last dot or line
            signaturePad.fromData(data);
        }
    });

    if (changeColorButton) changeColorButton.addEventListener("click", function (event) {
        var r = Math.round(Math.random() * 255);
        var g = Math.round(Math.random() * 255);
        var b = Math.round(Math.random() * 255);
        var color = "rgb(" + r + "," + g + "," + b + ")";

        signaturePad.penColor = color;
    });

    if (saveBase64Button) saveBase64Button.addEventListener("click", function (event) {
        if (signaturePad.isEmpty()) {
            alertMessage();
        } else {
            var imgBase64 = signaturePad.toDataURL("image/jpeg");
            //console.log(imgBase64);
            return wrapperc.invokeMethodAsync("signatureResult", imgBase64);
        }
    });

    if (savePNGButton) savePNGButton.addEventListener("click", function (event) {
        if (signaturePad.isEmpty()) {
            alertMessage();
        } else {
            var dataURL = signaturePad.toDataURL();
            download(dataURL, "signature.png");
        }
    });

    if (saveJPGButton) saveJPGButton.addEventListener("click", function (event) {
        if (signaturePad.isEmpty()) {
            alertMessage();
        } else {
            var dataURL = signaturePad.toDataURL("image/jpeg");
            download(dataURL, "signature.jpg");
        }
    });

    if (saveSVGButton) saveSVGButton.addEventListener("click", function (event) {
        if (signaturePad.isEmpty()) {
            alertMessage();
        } else {
            var dataURL = signaturePad.toDataURL('image/svg+xml');
            download(dataURL, "signature.svg");
        }
    });

    function alertMessage() {
        if (alertText) alert(alertText);
        wrapperc.invokeMethodAsync("signatureAlert");
    }
}

3. 打开Components文件夹 , 新建SignaturePad.razor.css文件

css代码
*,
*::before,
*::after {
    box-sizing: border-box;
}

.signature-pad-body {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-pack: center;
    -ms-flex-pack: center;
    justify-content: center;
    -webkit-box-align: center;
    -ms-flex-align: center;
    align-items: center;
    height: 400px;
    width: 100%;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    margin: 0;
    padding: 32px 16px;
    font-family: Helvetica, Sans-Serif;
}

.signature-pad {
    position: relative;
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-orient: vertical;
    -webkit-box-direction: normal;
    -ms-flex-direction: column;
    flex-direction: column;
    font-size: 10px;
    width: 100%;
    height: 100%;
    max-width: 650px;
    max-height: 400px;
    border: 1px solid #e8e8e8;
    background-color: #fff;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.27), 0 0 40px rgba(0, 0, 0, 0.08) inset;
    border-radius: 4px;
    padding: 16px;
}

    .signature-pad::before,
    .signature-pad::after {
        position: absolute;
        z-index: -1;
        content: "";
        width: 40%;
        height: 10px;
        bottom: 10px;
        background: transparent;
        box-shadow: 0 8px 12px rgba(0, 0, 0, 0.4);
    }

    .signature-pad::before {
        left: 20px;
        -webkit-transform: skew(-3deg) rotate(-3deg);
        transform: skew(-3deg) rotate(-3deg);
    }

    .signature-pad::after {
        right: 20px;
        -webkit-transform: skew(3deg) rotate(3deg);
        transform: skew(3deg) rotate(3deg);
    }

.signature-pad--body {
    position: relative;
    -webkit-box-flex: 1;
    -ms-flex: 1;
    flex: 1;
    border: 1px solid #f4f4f4;
}

    .signature-pad--body
    canvas {
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        border-radius: 4px;
        box-shadow: 0 0 5px rgba(0, 0, 0, 0.02) inset;
    }

.signature-pad--footer {
    color: #C3C3C3;
    text-align: center;
    font-size: 1.2em;
    margin-top: 8px;
}

.signature-pad--actions {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-pack: justify;
    -ms-flex-pack: justify;
    justify-content: space-between;
    margin-top: 8px;
}

#github img {
    border: 0;
}

@media (max-width: 940px) {
    #github img {
        width: 90px;
        height: 90px;
    }
}


4. 打开Components文件夹 , 新建SignaturePad.razor组件

参考阅读:Blazor组件参数

4.1 组件参数

在 ASP.NET Web Forms 中,可以使用公共属性将参数和数据传递到控件。 这些属性可以使用特性在标记中进行设置,也可以直接在代码中设置。 Razor 组件以类似的方式工作,尽管组件属性还必须使用 [Parameter] 特性进行标记才能被视为组件参数。

以下 Counter 组件定义名为 IncrementAmount 的组件参数,该参数可用于指定每次单击按钮时 Counter 应该递增的数量。

razor

Counter

Current count: @currentCount

@code { int currentCount = 0; [Parameter] public int IncrementAmount { get; set; } = 1; void IncrementCount() { currentCount+=IncrementAmount; } }

若要在 Blazor 中指定组件参数,请像在 ASP.NET Web Forms 中一样使用特性:

razor


4.2 C#组件参数实例

定义名为 SaveBase64BtnTitle 的组件参数,该参数可用于设置或者获取 [保存为base64]按钮的文本。

定义名为 OnResult 的组件参数,该参数可用于手写签名结果回调。

    /// 
    /// 保存为base64按钮文本/Save as Base64 button title
    /// 
    [Parameter]
    public string SaveBase64BtnTitle { get; set; } = "确定";

    /// 
    /// 手写签名结果回调/SignaturePad result callback method
    /// 
    [Parameter]
    public EventCallback OnResult { get; set; }

4.3 在 Blazor 调用组件页面中指定组件参数

仅获取手写签名结果回调



@code{ 
    public string? Result { get; set; }
}

自定义按钮文本





@code{ 
    public string? Result { get; set; }
}

自定义按钮css



@code{ 
    public string? Result { get; set; }
}

4.4 完整代码
razor代码
@implements IAsyncDisposable
@namespace Blazor100.Components
@inject IJSRuntime JS

@code { /// /// 手写签名结果回调/SignaturePad result callback method /// [Parameter] public EventCallback OnResult { get; set; } /// /// 手写签名警告信息回调/SignaturePad alert callback method /// [Parameter] public EventCallback OnAlert { get; set; } /// /// 获得/设置 错误回调方法 /// [Parameter] public Func? OnError { get; set; } /// /// 在框内签名标签文本/Sign above label /// [Parameter] public string SignAboveLabel { get; set; } = "在框内签名"; /// /// 清除按钮文本/Clear button title /// [Parameter] public string ClearBtnTitle { get; set; } = "清除"; /// /// 请先签名提示文本/'Please provide a signature first' alert text /// [Parameter] public string SignatureAlertText { get; set; } = "请先签名"; /// /// 换颜色按钮文本/Change color button title /// [Parameter] public string ChangeColorBtnTitle { get; set; } = "换颜色"; /// /// 撤消按钮文本/Undo button title /// [Parameter] public string UndoBtnTitle { get; set; } = "撤消"; /// /// 保存为base64按钮文本/Save as Base64 button title /// [Parameter] public string SaveBase64BtnTitle { get; set; } = "确定"; /// /// 保存为PNG按钮文本/Save as PNG button title /// [Parameter] public string SavePNGBtnTitle { get; set; } = "PNG"; /// /// 保存为JPG按钮文本/Save as JPG button title /// [Parameter] public string SaveJPGBtnTitle { get; set; } = "JPG"; /// /// 保存为SVG按钮文本/Save as SVG button title /// [Parameter] public string SaveSVGBtnTitle { get; set; } = "SVG"; /// /// 启用换颜色按钮/Enable change color button /// [Parameter] public bool EnableChangeColorBtn { get; set; } = true; /// /// 启用JS错误弹窗/Enable Alert from JS /// [Parameter] public bool EnableAlertJS { get; set; } = true; /// /// 启用保存为base64按钮/Enable save as Base64 button /// [Parameter] public bool EnableSaveBase64Btn { get; set; } = true; /// /// 启用保存为PNG按钮文本/Enable save as PNG button /// [Parameter] public bool EnableSavePNGBtn { get; set; } = false; /// /// 启用保存为JPG按钮文本/Enable save as JPG button /// [Parameter] public bool EnableSaveJPGBtn { get; set; } = false; /// /// 启用保存为SVG按钮文本/Enable save as SVG button /// [Parameter] public bool EnableSaveSVGBtn { get; set; } = false; /// /// 按钮CSS式样/Button css style /// [Parameter] public string BtnCssClass { get; set; } = "btn btn-light"; private IJSObjectReference? module; /// /// /// protected ElementReference SignaturepadElement { get; set; } // To prevent making JavaScript interop calls during prerendering protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; try { module = await JS.InvokeAsync("import", "./lib/signature_pad/app.js"); await module.InvokeVoidAsync("init", DotNetObjectReference.Create(this), SignaturepadElement, EnableAlertJS ? SignatureAlertText : null); } catch (Exception e) { if (OnError != null) await OnError.Invoke(e.Message); } } [JSInvokable("signatureResult")] public async Task SignatureResult(string val) { if (OnResult.HasDelegate) await OnResult.InvokeAsync(val); } [JSInvokable("signatureAlert")] public async Task SignatureAlert() { if (OnResult.HasDelegate) await OnAlert.InvokeAsync(SignatureAlertText); } async ValueTask IAsyncDisposable.DisposeAsync() { if (module is not null) { //await module.InvokeVoidAsync("destroy",null); await module.DisposeAsync(); } } }
5. Pages文件添加SignaturePadPage.razor文件,用于演示组件调用.
SignaturePadPage.razor代码
@page "/signaturepad"

SignaturePad 签名

@code{ /// /// 签名Base64 /// public string? Result { get; set; } }

6. _Imports.razor加入一行引用组件的命名空间.

@using Blazor100.Components

7. 首页引用组件演示页 或者Shared/NavMenu.razor添加导航

        

8. F5运行程序

9. Tips: 复杂签名会导致传输数据量大ssr会出现断流显示reload错误,启用以下配置解决这个问题.

        builder.Services.AddServerSideBlazor(a =>
        {
            //异步调用JavaScript函数的最大等待时间
            a.JSInteropDefaultCallTimeout = TimeSpan.FromMinutes(2);
        }).AddHubOptions(o =>
        {
            //单个传入集线器消息的最大大小。默认 32 KB
            o.MaximumReceiveMessageSize = null;
            //可为客户端上载流缓冲的最大项数。 如果达到此限制,则会阻止处理调用,直到服务器处理流项。
            o.StreamBufferCapacity = 20;
        });

至此,使用JS隔离封装signature_pad签名组件大功告成! Happy coding!

Blazor组件自做系列








项目源码 Github | Gitee