运行截图

演示地址

响应式演示

感谢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. mybatis——逆向工程中 where (条件1)and (条件2 or 条件3 or 条件4)

    where (条件1)and (条件2 or 条件3 or 条件4) = where (条件1 and 条件2)or (条件1 and 条件3) or (条件1 and 条件4) 结果 是这样的 WH ...

  2. ssm 关于mybatis启动报Result Maps collection already contains value for ...的问题总结

    Result Maps collection already contains value for com.zhaike.mapping.ChapterMapper.BaseResultMap Err ...

  3. Knife4j 注解详谈

    Controller层添加注解 @Api:用于类:表示标识这个类是swagger的资源 属性名称 数据类型   默认值  说明  value      String  ""  字段 ...

  4. 添加ico图标

    1. 先添加资源文件XXX.Ico,然后引用的时候用如下代码即可. Icon ico=Properties.Resources.XXX;

  5. 《前端运维》二、Nginx--4代理、负载均衡与其他

    一.代理服务 比较容易理解吧,简单来说.客户端访问服务器并不是直接访问的,而是通过中间代理服务器,代理服务器再去访问服务器.就像一个中转站一样,无论什么,只要从客户端到服务器,你就要通过我. 一)正向 ...

  6. P2P图书馆实践:让知识更好的传播

    人才是每个公司最重要的资产,而人的成长自然就成了最重要的事.苏轼曾经说过:"腹有诗书气自华,代码万行零缺陷",阅读对人成长的影响是巨大的.相信不同的团队都有着自己打造学习氛围.技术 ...

  7. Android Studio Gradle project sync failed

    使用Android Studio 1.1.0创建新项目后,运行报以下错: Error:Unable to start the daemon process. This problem might be ...

  8. vue渐进式?

    小到可以只使用核心功能,比如单文件组件作为一部分嵌入:大到使用整个工程,vue init webpack my-project来构建项目:VUE的核心库及其生态系统也可以满足你的各式需求(core+v ...

  9. Zookeeper 对于 Kafka 的作用是什么?

    Zookeeper 是一个开放源码的.高性能的协调服务,它用于 Kafka 的分布式应用. Zookeeper 主要用于在集群中不同节点之间进行通信 在 Kafka 中,它被用于提交偏移量,因此如果节 ...

  10. Kafka 都有哪些特点?

    高吞吐量.低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作. ...