Skip to content

Latest commit

 

History

History
720 lines (566 loc) · 20.6 KB

D4.SignaturePad.md

File metadata and controls

720 lines (566 loc) · 20.6 KB

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

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    int currentCount = 0;

    [Parameter]
    public int IncrementAmount { get; set; } = 1;

    void IncrementCount()
    {
        currentCount+=IncrementAmount;
    }
}

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

razor

<Counter IncrementAmount="10" />

4.2 C#组件参数实例

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

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

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

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

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

仅获取手写签名结果回调

<SignaturePad OnResult="((e) =>  Result=e)" />

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

自定义按钮文本

<SignaturePad OnResult="((e) =>  Result=e)" SaveBase64BtnTitle="完成"/>
<SignaturePad OnResult="((e) =>  Result=e)" SaveBase64BtnTitle="OK" ClearBtnTitle="Clear"/>
<SignaturePad OnResult="((e) =>  Result=e)" 
              SignAboveLabel="Sign above" 
              UndoBtnTitle="Undo" 
              SaveBase64BtnTitle="OK" 
              ChangeColorBtnTitle="Change color" 
              ClearBtnTitle="Clear" />

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

自定义按钮css

<SignaturePad OnResult="((e) =>  Result=e)" BtnCssClass="btn btn-outline-success"/>

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

4.4 完整代码

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

<div class="signature-pad-body">
    <div @ref="SignaturepadElement" class="signature-pad">
        <div class="signature-pad--body">
            <canvas width="614" style="touch-action: none; user-select: none;" height="242"></canvas>
        </div>
        <div class="signature-pad--footer">
            <div class="description">@SignAboveLabel</div>

            <div class="signature-pad--actions">
                <div>
                    <button type="button" class="@BtnCssClass" data-action="clear">@ClearBtnTitle</button>
                    @if (EnableChangeColorBtn)
                    {
                        <button type="button" class="@BtnCssClass" data-action="change-color">@ChangeColorBtnTitle</button>
                    }
                    <button type="button" class="@BtnCssClass" data-action="undo">@UndoBtnTitle</button>

                </div>
                <div>
                    @if (EnableSaveBase64Btn)
                    {
                        <button type="button" class="@BtnCssClass" data-action="save-base64">@SaveBase64BtnTitle</button>
                    }
                    @if (EnableSavePNGBtn)
                    {
                        <button type="button" class="@BtnCssClass" data-action="save-png">@SavePNGBtnTitle</button>
                    }
                    @if (EnableSaveJPGBtn)
                    {
                        <button type="button" class="@BtnCssClass" data-action="save-jpg">@SaveJPGBtnTitle</button>
                    }
                    @if (EnableSaveSVGBtn)
                    {
                        <button type="button" class="@BtnCssClass" data-action="save-svg">@SaveSVGBtnTitle</button>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

@code {

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

    /// <summary>
    /// 手写签名警告信息回调/SignaturePad alert callback method
    /// </summary>
    [Parameter]
    public EventCallback<string> OnAlert { get; set; }

    /// <summary>
    /// 获得/设置 错误回调方法
    /// </summary>
    [Parameter]
    public Func<string, Task>? OnError { get; set; }

    /// <summary>
    /// 在框内签名标签文本/Sign above label
    /// </summary>
    [Parameter]
    public string SignAboveLabel { get; set; } = "在框内签名";

    /// <summary>
    /// 清除按钮文本/Clear button title
    /// </summary>
    [Parameter]
    public string ClearBtnTitle { get; set; } = "清除";

    /// <summary>
    /// 请先签名提示文本/'Please provide a signature first' alert text
    /// </summary>
    [Parameter]
    public string SignatureAlertText { get; set; } = "请先签名";

    /// <summary>
    /// 换颜色按钮文本/Change color button title
    /// </summary>
    [Parameter]
    public string ChangeColorBtnTitle { get; set; } = "换颜色";

    /// <summary>
    /// 撤消按钮文本/Undo button title
    /// </summary>
    [Parameter]
    public string UndoBtnTitle { get; set; } = "撤消";

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

    /// <summary>
    /// 保存为PNG按钮文本/Save as PNG button title
    /// </summary>
    [Parameter]
    public string SavePNGBtnTitle { get; set; } = "PNG";

    /// <summary>
    /// 保存为JPG按钮文本/Save as JPG button title
    /// </summary>
    [Parameter]
    public string SaveJPGBtnTitle { get; set; } = "JPG";

    /// <summary>
    /// 保存为SVG按钮文本/Save as SVG button title
    /// </summary>
    [Parameter]
    public string SaveSVGBtnTitle { get; set; } = "SVG";

    /// <summary>
    /// 启用换颜色按钮/Enable change color button
    /// </summary>
    [Parameter]
    public bool EnableChangeColorBtn { get; set; } = true;

    /// <summary>
    /// 启用JS错误弹窗/Enable Alert from JS
    /// </summary>
    [Parameter]
    public bool EnableAlertJS { get; set; } = true;

    /// <summary>
    /// 启用保存为base64按钮/Enable save as Base64 button
    /// </summary>
    [Parameter]
    public bool EnableSaveBase64Btn { get; set; } = true;

    /// <summary>
    /// 启用保存为PNG按钮文本/Enable save as PNG button
    /// </summary>
    [Parameter]
    public bool EnableSavePNGBtn { get; set; } = false;

    /// <summary>
    /// 启用保存为JPG按钮文本/Enable save as JPG button
    /// </summary>
    [Parameter]
    public bool EnableSaveJPGBtn { get; set; } = false;

    /// <summary>
    /// 启用保存为SVG按钮文本/Enable save as SVG button
    /// </summary>
    [Parameter]
    public bool EnableSaveSVGBtn { get; set; } = false;

    /// <summary>
    /// 按钮CSS式样/Button css style
    /// </summary>
    [Parameter]
    public string BtnCssClass { get; set; } = "btn btn-light";

    private IJSObjectReference? module;

    /// <summary>
    ///
    /// </summary>
    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<IJSObjectReference>("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"

<h3>SignaturePad 签名</h3>

<SignaturePad OnResult="((e) =>  Result=e)" /> 

@code{

    /// <summary>
    /// 签名Base64
    /// </summary>
    public string? Result { get; set; } 
}


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

@using Blazor100.Components

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

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="signaturepad">
                <span class="oi oi-plus" aria-hidden="true"></span> 手写签名2
            </NavLink>
        </div>

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组件自做系列

Blazor组件自做一 : 使用JS隔离封装viewerjs库

Blazor组件自做二 : 使用JS隔离制作手写签名组件

Blazor组件自做三 : 使用JS隔离封装ZXing扫码

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

Blazor组件自做五: 使用JS隔离封装Google地图

Blazor组件自做六: 使用JS隔离封装Baidu地图

Blazor组件自做七: 使用JS隔离制作定位/持续定位组件

Blazor组件自做八: 使用JS隔离封装屏幕键盘kioskboard.js组件

项目源码 Github | Gitee