根据需求,我们设计了一个功能完整的审批表生成系统,支持根据用户输入条件动态生成审批表,并提供手写签字功能。系统采用 ASP.NET Core 8 作为后端框架,结合 HTML5 Canvas 实现手写签字功能,使用 Bootstrap 构建响应式界面,此篇文章主要出于教学

下面是完整的实现方案,包含项目结构、核心代码和使用说明:
1、项目结构
ApprovalFormGenerator/ ├── Controllers/ │ └── ApprovalController.cs # 审批表生成与签字处理控制器 ├── Models/ │ ├── ApprovalCriteria.cs # 审批条件模型 │ └── ApprovalForm.cs # 审批表数据模型 ├── Views/ │ ├── Approval/ │ │ ├── Index.cshtml # 条件输入页面 │ │ ├── Preview.cshtml # 审批表预览与签字页面 │ │ └── CompLeted.cshtml # 提交完成页面 │ └── Shared/ │ └── _Layout.cshtml # 布局页面 ├── wwwroot/ │ ├── css/ │ │ └── signature-pad.css # 签字板样式 │ └── js/ │ └── signature-pad.js # 手写签字功能脚本 └── Program.cs # 应用程序配置
2. 核心代码实现
2.1 模型定义(Models)
ApprovalCriteria.cs - 审批条件模型
using System.ComponentModel.DataAnnotations;
namespace ApprovalFormGenerator.Models;
public class ApprovalCriteria
{
[Required(ErrorMessage = "请输入申请人姓名")]
[Display(Name = "申请人姓名")]
public string ApplicantName { get; set; } = string.Empty;
[Required(ErrorMessage = "请输入申请部门")]
[Display(Name = "申请部门")]
public string Department { get; set; } = string.Empty;
[Required(ErrorMessage = "请选择申请类型")]
[Display(Name = "申请类型")]
public string ApprovalType { get; set; } = string.Empty;
[Required(ErrorMessage = "请输入申请事由")]
[Display(Name = "申请事由")]
[StringLength(500, ErrorMessage = "申请事由不能超过500字")]
public string Reason { get; set; } = string.Empty;
[Required(ErrorMessage = "请输入申请金额(如有)")]
[Display(Name = "申请金额(元)")]
[DataType(DataType.Currency)]
public decimal Amount { get; set; }
[Display(Name = "紧急程度")]
public string UrgencyLevel { get; set; } = "普通";
[Display(Name = "附加说明")]
public string Remarks { get; set; } = string.Empty;
}ApprovalForm.cs - 审批表数据模型
namespace ApprovalFormGenerator.Models;
public class ApprovalForm
{
public string FormId { get; set; } = Guid.NewGuid().ToString("N");
public string ApplicantName { get; set; } = string.Empty;
public string Department { get; set; } = string.Empty;
public string ApprovalType { get; set; } = string.Empty;
public string Reason { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string UrgencyLevel { get; set; } = string.Empty;
public string Remarks { get; set; } = string.Empty;
public DateTime ApplyDate { get; set; } = DateTime.Now;
public string SignatureData { get; set; } = string.Empty; // 存储签字图片base64数据
}2.2 控制器(Controllers)
ApprovalController.cs
using ApprovalFormGenerator.Models; using Microsoft.AspNetCore.mvc; namespace ApprovalFormGenerator.Controllers; public class ApprovalController : Controller { // 模拟存储已提交的审批表 private static readonly List<ApprovalForm> _submittedForms = new(); // 条件输入页面 public IActionResult Index() { // 提供申请类型选项 ViewBag.ApprovalTypes = new List<string> { "费用报销", "采购申请", "假期申请", "出差申请", "其他" }; // 提供紧急程度选项 ViewBag.UrgencyLevels = new List<string> { "普通", "紧急", "非常紧急" }; return View(new ApprovalCriteria()); } // 生成审批表并显示预览 [HttpPost] public IActionResult Preview(ApprovalCriteria criteria) { if (!ModelState.IsValid) { ViewBag.ApprovalTypes = new List<string> { "费用报销", "采购申请", "假期申请", "出差申请", "其他" }; ViewBag.UrgencyLevels = new List<string> { "普通", "紧急", "非常紧急" }; return View("Index", criteria); } // 根据条件生成审批表 Var approvalForm = new ApprovalForm { ApplicantName = criteria.ApplicantName, Department = criteria.Department, ApprovalType = criteria.ApprovalType, Reason = criteria.Reason, Amount = criteria.Amount, UrgencyLevel = criteria.UrgencyLevel, Remarks = criteria.Remarks }; // 存储到临时会话 TempData["ApprovalForm"] = System.Text.Json.JsonSerializer.Serialize(approvalForm); return View(approvalForm); } // 提交签字后的审批表 [HttpPost] public IActionResult Submit(string signatureData) { var formJson = TempData["ApprovalForm"] as string; if (string.IsNullOrEmpty(formJson) || string.IsNullOrEmpty(signatureData)) { return RedirectToAction("Index"); } // 反序列化审批表数据 var approvalForm = System.Text.Json.JsonSerializer.Deserialize<ApprovalForm>(formJson)!; approvalForm.SignatureData = signatureData; // 保存到提交列表 _submittedForms.Add(approvalForm); // 传递审批表ID到完成页面 TempData["FormId"] = approvalForm.FormId; return RedirectToAction("Completed"); } // 提交完成页面 public IActionResult Completed() { var formId = TempData["FormId"] as string; if (string.IsNullOrEmpty(formId)) { return RedirectToAction("Index"); } ViewBag.FormId = formId; return View(); } // 查看已提交的审批表(可选功能) public IActionResult ViewSubmittedForms() { return View(_submittedForms); } }
2.3 视图页面(Views)
_Layout.cshtml - 布局页面
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - 审批表自动生成系统</title> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" /> <!-- 自定义样式 --> <link href="~/css/signature-pad.css" rel="stylesheet" /> </head> <body> <header class="bg-primary text-white py-3"> <div> <h1 class="h3 mb-0">审批表自动生成与电子签字系统</h1> </div> </header> <main class="container py-4"> @RenderBody() </main> <footer class="bg-light py-3 mt-4"> <div class="container text-center text-muted"> <p>© @DateTime.Now.Year 审批表系统 - 版权所有</p> </div> </footer> <!-- Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <!-- 签字板脚本 --> <script src="~/js/signature-pad.js"></script> @RenderSection("Scripts", required: false) </body> </html>Index.cshtml - 条件输入页面
@model ApprovalFormGenerator.Models.ApprovalCriteria
@{
ViewData["Title"] = "填写审批条件";
}
<div class="card shadow-sm">
<div class="card-header bg-secondary text-white">
<h2 class="h4 mb-0">审批条件输入</h2>
</div>
<div class="card-body">
<form asp-action="Preview" method="post">
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="ApplicantName" class="form-label"></label>
<input asp-for="ApplicantName" class="form-control @(ModelState["ApplicantName"].Errors.Any() ? "is-invalid" : "")" />
<span asp-validation-for="ApplicantName" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Department" class="form-label"></label>
<input asp-for="Department" class="form-control @(ModelState["Department"].Errors.Any() ? "is-invalid" : "")" />
<span asp-validation-for="Department" class="text-danger"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="ApprovalType" class="form-label"></label>
<select asp-for="ApprovalType" class="form-select @(ModelState["ApprovalType"].Errors.Any() ? "is-invalid" : "")">
<option value="">请选择申请类型</option>
@foreach (var type in ViewBag.ApprovalTypes as List<string>)
{
<option value="@type">@type</option>
}
</select>
<span asp-validation-for="ApprovalType" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="UrgencyLevel" class="form-label"></label>
<select asp-for="UrgencyLevel" class="form-select">
@foreach (var level in ViewBag.UrgencyLevels as List<string>)
{
<option value="@level">@level</option>
}
</select>
</div>
</div>
<div class="mb-3">
<label asp-for="Amount" class="form-label"></label>
<input asp-for="Amount" class="form-control @(ModelState["Amount"].Errors.Any() ? "is-invalid" : "")" type="number" step="0.01" min="0" />
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Reason" class="form-label"></label>
<textarea asp-for="Reason" class="form-control @(ModelState["Reason"].Errors.Any() ? "is-invalid" : "")" rows="3"></textarea>
<span asp-validation-for="Reason" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Remarks" class="form-label"></label>
<textarea asp-for="Remarks" class="form-control" rows="2"></textarea>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" class="btn btn-primary btn-lg">生成审批表</button>
</div>
</form>
</div>
</div>
@section Scripts {
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
}Preview.cshtml - 审批表预览与签字页面
@model ApprovalFormGenerator.Models.ApprovalForm
@{
ViewData["Title"] = "审批表预览与签字";
}
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white">
<h2 class="h4 mb-0">@Model.ApprovalType - 审批表</h2>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<strong>申请人:</strong> @Model.ApplicantName
</div>
<div class="col-md-6">
<strong>申请部门:</strong> @Model.Department
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>申请日期:</strong> @Model.ApplyDate.ToString("yyyy-MM-dd HH:mm")
</div>
<div class="col-md-6">
<strong>紧急程度:</strong>
<span class="badge @(Model.UrgencyLevel == "普通" ? "bg-secondary" : Model.UrgencyLevel == "紧急" ? "bg-warning" : "bg-danger")">
@Model.UrgencyLevel
</span>
</div>
</div>
<div class="mb-3">
<strong>申请事由:</strong>
<p class="card-text">@Model.Reason</p>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>申请金额:</strong> ¥@Model.Amount.ToString("N2")
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Remarks))
{
<div class="mb-3">
<strong>附加说明:</strong>
<p class="card-text">@Model.Remarks</p>
</div>
}
</div>
</div>
<!-- 签字区域 -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-secondary text-white">
<h3 class="h5 mb-0">电子签字</h3>
</div>
<div class="card-body">
<div class="alert alert-info">
请在下方签字区域手写签名,支持鼠标或触摸屏操作
</div>
<!-- 签字板 -->
<div class="signature-pad-container">
<canvas id="signaturePad" class="signature-pad border rounded"></canvas>
</div>
<div class="d-flex justify-content-between mt-3">
<button id="clearSignature" class="btn btn-outline-danger">清除签名</button>
<button id="submitForm" class="btn btn-success">确认签字并提交</button>
</div>
</div>
</div>
<form id="submitForm" action="@Url.Action("Submit")" method="post">
<input type="hidden" name="signatureData" id="signatureData" />
</form>
@section Scripts {
<script>
document.addEventListener('DOMContentLoaded', function() {
// 初始化签字板
Const canvas = document.getElementById('signaturePad');
const signatureDataInput = document.getElementById('signatureData');
const submitButton = document.getElementById('submitForm');
const clearButton = document.getElementById('clearSignature');
// 设置签字板尺寸
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth - 30; // 减去内边距
canvas.height = 200;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// 签字板绘制功能
let isDrawing = false;
let lastX = 0;
let lastY = 0;
const ctx = canvas.getContext('2d');
// 设置画笔样式
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.strokeStyle = '#000';
// 开始绘制
function startDrawing(e) {
isDrawing = true;
[lastX, lastY] = getCanvasCoordinates(e);
}
// 绘制中
function draw(e) {
if (!isDrawing) return;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
[lastX, lastY] = getCanvasCoordinates(e);
ctx.lineTo(lastX, lastY);
ctx.stroke();
}
// 结束绘制
function endDrawing() {
isDrawing = false;
}
// 获取鼠标/触摸在画布上的坐标
function getCanvasCoordinates(e) {
const rect = canvas.getBoundingClientRect();
let x, y;
if (e.type.includes('touch')) {
x = e.touches[0].clientX - rect.left;
y = e.touches[0].clientY - rect.top;
} else {
x = e.clientX - rect.left;
y = e.clientY - rect.top;
}
return [x, y];
}
// 绑定事件
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDrawing);
canvas.addEventListener('mouseout', endDrawing);
// 支持触摸设备
canvas.addEventListener('touchstart', function(e) {
e.preventDefault();
startDrawing(e);
});
canvas.addEventListener('touchmove', function(e) {
e.preventDefault();
draw(e);
});
canvas.addEventListener('touchend', function(e) {
e.preventDefault();
endDrawing(e);
});
canvas.addEventListener('touchcancel', function(e) {
e.preventDefault();
endDrawing(e);
});
// 清除签名
clearButton.addEventListener('click', function() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
signatureDataInput.value = '';
});
// 提交表单
submitButton.addEventListener('click', function() {
// 检查是否有签名
const isEmpty = ctx.getImageData(0, 0, canvas.width, canvas.height).data.every(channel => channel === 0);
if (isEmpty) {
alert('请先完成电子签名!');
return;
}
// 将签名转换为Base64格式
const signatureData = canvas.toDataURL('image/png');
signatureDataInput.value = signatureData;
// 提交表单
document.getElementById('submitForm').submit();
});
});
</script>
}Completed.cshtml - 提交完成页面
@{
ViewData["Title"] = "提交成功";
}
<div class="alert alert-success text-center">
<i class="bi bi-check-circle-fill" style="font-size: 3rem; color: #198754;"></i>
<h2 class="h3 mt-3">审批表提交成功!</h2>
<p class="mt-2">您的审批表编号:<strong>@ViewBag.FormId</strong></p>
<p class="mt-2">请妥善保存此编号,以便后续查询</p>
<div class="mt-4">
<a href="@Url.Action("Index")" class="btn btn-primary btn-lg">创建新审批表</a>
<a href="@Url.Action("ViewSubmittedForms")" class="btn btn-outline-secondary btn-lg ms-2">查看已提交审批表</a>
</div>
</div>2.4 静态资源(wwwroot)
css/signature-pad.css
.signature-pad-container {
position: relative;
width: 100%;
margin: 0 auto;
}
.signature-pad {
width: 100%;
height: 200px;
border: 2px dashed #ccc;
border-radius: 8px;
background-color: #f9f9f9;
cursor: crosshair;
}
.signature-pad:hover {
border-color: #666;
}
/* 适配移动端 */
@media (max-width: 768px) {
.signature-pad {
height: 180px;
}
}js/signature-pad.js
// 空文件,主要逻辑已内联在Preview视图中,此处预留扩展空间
3. 应用程序配置(Program.cs)
var builder = WebApplication.CreateBuilder(args);
// 添加MVC服务
builder.Services.AddControllersWithViews();
// 添加会话支持(用于临时存储审批表数据)
builder.Services.AddSession(options =>
{
options.Cookie.Name = ".ApprovalForm.Session";
options.IdleTimeout = TimeSpan.FromMinutes(30); // 会话超时时间
options.Cookie.IsEssential = true;
});
var app = builder.Build();
// 配置HTTP请求管道
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseSession(); // 使用会话
app.UseAuthorization();
// 配置路由
app.MapControllerRoute(
name: "default",
pattern: "{controller=Approval}/{action=Index}/{id?}");
app.Run();4. 功能特点与使用说明
4.1 核心功能
条件驱动的审批表生成:用户输入申请人信息、申请类型、事由等条件,系统自动生成标准审批表
电子手写签字:支持鼠标(PC 端)和触摸(移动端)签字,Canvas 实时绘制
响应式设计:适配 PC、平板和手机等不同设备
表单验证:对必填项进行前端 + 后端双重验证
审批表存储:模拟存储已提交的审批表,支持查看历史记录
4.2 使用流程
访问系统首页(默认跳转到条件输入页面)
填写审批条件(标 * 为必填项),点击 "生成审批表"
预览生成的审批表,确认信息无误后在签字区域手写签名
点击 "确认签字并提交",完成审批表提交
提交成功后可获取审批表编号,或创建新审批表 / 查看历史记录
4.3 技术亮点
使用 ASP.NET Core 8 构建高效后端,支持跨平台部署
采用 HTML5 Canvas 实现无插件电子签字,支持多种设备
结合 Bootstrap 5 实现响应式 UI,提升用户体验
使用 TempData 和 Session 临时存储审批表数据,确保数据安全
前端表单验证与后端模型验证结合,保证数据完整性
5. 扩展建议
持久化存储:将提交的审批表存储到数据库(如 SQL Server、MySQL),替换当前的内存存储
审批流程管理:增加审批节点设置、审批人分配、流程跟踪等功能
导出功能:支持将审批表导出为 PDF 或 Word 格式
签名验证:集成数字签名或生物识别技术,增强签名安全性
批量生成:支持导入 Excel 数据批量生成审批表
通知功能:审批状态变更时通过邮件或短信通知相关人员
6. 部署要求
.NET 8 SDK
浏览器支持:Chrome 88+、Firefox 85+、Edge 88+、Safari 14+
如需持久化存储,需配置相应数据库(SQL Server、MySQL 等)
此系统提供了完整的审批表生成与电子签字解决方案,代码结构清晰,易于扩展,可根据实际业务需求进一步定制化开发。