运行截图

演示地址

响应式演示

感谢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

Blazor组件自做四 : 使用JS隔离封装signature_pad签名组件的更多相关文章

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

    1. 运行截图 演示地址 2. 在文件夹wwwroot/lib,添加kioskboard子文件夹,添加kioskboards.js文件 2.1 常规操作,懒加载js库, export function ...

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

    Viewer.js库是一个实用的js库,用于图片浏览,放大缩小翻转幻灯片播放等实用操作 本文相关参考链接 JavaScript 模块中的 JavaScript 隔离 Viewer.js工程 Blazo ...

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

    Blazor组件自做三 : 使用JS隔离封装ZXing扫码 本文基础步骤参考前两篇文章 Blazor组件自做一 : 使用JS隔离封装viewerjs库 Blazor组件自做二 : 使用JS隔离制作手写 ...

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

    Blazor组件自做五: 使用JS隔离封装Google地图 运行截图 演示地址 正式开始 1. 谷歌地图API 谷歌开发文档 开始学习 Maps JavaScript API 的最简单方法是查看一个简 ...

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

    1. 运行截图 演示地址 2. 在文件夹wwwroot/lib,添加baidu子文件夹,添加baidumap.js文件 2.1 跟上一篇类似,用代码方式异步加载API,脚本生成新的 body > ...

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

    Blazor组件自做二 : 使用JS隔离制作手写签名组件 本文相关参考链接 JavaScript 模块中的 JavaScript 隔离 Viewer.js工程 Blazor组件自做一 : 使用JS隔离 ...

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

    1. 运行截图 演示地址 2. 在文件夹wwwroot/lib,添加geolocation子文件夹,添加geolocation.js文件 本组件主要是调用浏览器两个API实现基于浏览器的定位功能,现代 ...

  8. Blazor组件自做九: 用20行代码实现文件上传,浏览目录功能 (3)

    接上篇 Blazor组件自做九: 用20行代码实现文件上传,浏览目录功能 (2) 7. 使用配置文件指定监听地址 打开 appsettings.json 文件,加入一行 "UseUrls&q ...

  9. JS组件系列——又一款MVVM组件:Vue(二:构建自己的Vue组件)

    前言:转眼距离上篇 JS组件系列——又一款MVVM组件:Vue(一:30分钟搞定前端增删改查) 已有好几个月了,今天打算将它捡起来,发现好久不用,Vue相关技术点都生疏不少.经过这几个月的时间,Vue ...

随机推荐

  1. 微信小程序下滑时能实现加载更多数据

    wxml代码: <view class="scroll"> <!-- 绑订页面上拉触底事件的处理函数onReachBottom事件 --> <scro ...

  2. Windows原理深入学习系列-Windows内核提权

    这是[信安成长计划]的第 22 篇文章 0x00 目录 0x01 介绍 0x02 替换 Token 0x03 编辑 ACL 0x04 修改 Privileges 0x05 参考文章 继续纠正网上文章中 ...

  3. 解释一下什么是线程池(thread pool)?

    在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收.所以提高服务程序效率的 ...

  4. Net6Configuration & Options 源码分析 Part3 IOptionsMonitor 是如何接收到配置文件变更并同步数据源的

    配置源的同步 IOptionsMonitor 使用 //以下demo演示使用IOptionsMonitor重新加载配置并当重新加载配置是执行回调函数 var configuration = new C ...

  5. ActiveMQ代码-01

    p2p模式 生产者 package com.activemq.activemqdemo.p2p; import org.apache.activemq.ActiveMQConnectionFactor ...

  6. javascript 实现富文本框选中对齐

    需求: 一个可编辑(contenteditable=true)的div,对齐选中内容,左.中,右 ,其实质是:对选中的末梢节点,找到块属性的父元素,设置text-algin:center: MDN:t ...

  7. nginx配置只允许某个IP或某些IP进行访问

    server{ listen 80; listen 443 ssl; server_name ehall.jerry.plus; ssl_certificate "****.crt" ...

  8. 修改vue的url

    1.修改router的index.js文件,增加base url的配置 2.修改config的index.js文件. bulid的index,assetsRoots和assetsPublicPath的 ...

  9. Windows 8下完美使用Virtual PC 2007(virtual pc 2007 64 win8 兼容性)

    Windows 8下完美使用Virtual PC 2007(virtual pc 2007 64 win8 兼容性) 一.从微软的官方网站下载Virtual PC 2007 SP1英文版,文件名为se ...

  10. java打入jar包

    首先在项目下创建一个文件夹,保存我们的jar包. 在项目名上右击,依次点击[New]-->[Floder],打开新建文件夹窗口 输入文件夹名称[lib],点击[ok].我们通常在lib文件夹中存 ...