概述
随着数字时代的到来,电子签名在各行各业中的应用越来越广泛。今天我将分享一个功能完整的移动端优化签名设计工具,支持文本签名和手写签名,并适配所有设备。这个工具不仅美观实用,还包含了丰富的自定义功能。
功能亮点
核心功能
文本签名:支持添加自定义文本签名,可调整大小、颜色、字体和旋转角度
手写签名:提供画布手写签名功能,支持笔触大小调整
移动端优化:完美适配手机和平板,支持触摸操作
背景自定义:可上传背景图片并调整亮度
签名管理:支持添加、删除、编辑和调整多个签名
导出功能:一键导出设计结果为高质量PNG图片
技术特性
-
响应式设计,适配所有屏幕尺寸
-
触摸友好的界面和操作
-
使用html2canvas实现高质量截图导出
-
纯前端实现,无需后端支持
使用说明
基础操作
添加文本签名:点击"添加文本签名"按钮,在文本框中输入签名内容
手写签名:点击"手写签名"按钮,使用鼠标或触摸屏绘制签名
调整签名:拖动签名调整位置,拖动角落控制点调整大小
修改属性:通过控制面板调整颜色、字体、大小和旋转角度
保存结果:点击"下载签名设计"按钮导出最终设计
移动端特别提示
-
长按签名可拖动位置
-
双指缩放可调整签名大小
-
手写签名时,触摸绘制更自然
技术实现详解
1. 响应式设计
/* 响应式断点 */
@media (max-width: 768px) {
.workspace {
min-height: 350px;
}
.signature-text {
font-size: 1.8rem;
}
}
@media (max-width: 480px) {
.workspace {
min-height: 300px;
}
.signature-text {
font-size: 1.6rem;
}
}
2. 签名拖拽与缩放
// 使元素可拖动(支持触摸事件)
function makeDraggable(element) {
element.onmousedown = dragMouseDown;
element.ontouchstart = dragTouchStart;
// 移动端触摸事件处理
function dragTouchStart(e) {
e.preventDefault();
document.body.style.overflow = 'hidden'; // 防止拖动时页面滚动
// … 其他触摸处理逻辑
}
}
3. 手写签名实现
// 手写签名绘制功能
function startDrawing(e) {
isDrawing = true;
[lastX, lastY] = getCoordinates(e);
}
function draw(e) {
if (!isDrawing) return;
const [x, y] = getCoordinates(e);
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
[lastX, lastY] = [x, y];
}
// 支持触摸事件
handwritingCanvas.addEventListener('touchstart', startDrawing);
handwritingCanvas.addEventListener('touchmove', draw);
handwritingCanvas.addEventListener('touchend', stopDrawing);
4. 图片导出功能
// 使用html2canvas导出图片
downloadBtn.addEventListener('click', function() {
html2canvas(document.querySelector('.workspace'), {
backgroundColor: null,
scale: 2, // 提高导出质量
useCORS: true
}).then(canvas => {
const link = document.createElement('a');
link.download = 'signature-design.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
});
完整代码实现
以下是完整的HTML代码,包含了所有CSS样式和JavaScript功能:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>签名设计工具 – 优化版</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Microsoft YaHei', sans-serif;
touch-action: manipulation;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
min-height: 100vh;
padding: 15px;
display: flex;
justify-content: center;
align-items: center;
overflow-x: hidden;
}
.container {
width: 100%;
max-width: 1400px;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
overflow: hidden;
display: flex;
flex-direction: column;
}
header {
background: linear-gradient(90deg, #2c3e50, #4a6491);
color: white;
padding: 20px 15px;
text-align: center;
}
h1 {
font-size: 2.2rem;
margin-bottom: 8px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
.main-content {
display: flex;
flex-wrap: wrap;
padding: 15px;
gap: 15px;
}
.workspace {
flex: 1;
min-width: 300px;
min-height: 400px;
background: linear-gradient(135deg, #e6f7ff, #f0f9ff);
border: 2px dashed #aaa;
border-radius: 10px;
position: relative;
overflow: hidden;
box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.1);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.background-canvas {
width: 100%;
height: 100%;
position: relative;
}
.signature {
position: absolute;
cursor: move;
user-select: none;
min-width: 120px;
min-height: 60px;
border: 2px solid transparent;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
background: transparent;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transform-origin: center;
resize: none;
transition: border-color 0.3s ease;
touch-action: none;
}
.signature.selected {
border-color: #e74c3c;
z-index: 10;
}
.signature-controls {
position: absolute;
top: -35px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 5px;
background: rgba(255, 255, 255, 0.9);
padding: 5px;
border-radius: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 100;
}
.signature.selected .signature-controls {
opacity: 1;
visibility: visible;
}
.signature-btn {
background: #3498db;
color: white;
border: none;
width: 26px;
height: 26px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
font-size: 12px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
z-index: 10;
transition: all 0.2s;
}
.signature-btn:hover {
transform: scale(1.1);
box-shadow: 0 3px 6px rgba(0,0,0,0.3);
}
.signature-btn.delete {
background: #e74c3c;
}
.signature-btn.rotate {
background: #9b59b6;
}
.signature-btn.edit {
background: #f39c12;
}
.signature-text {
font-size: 2.2rem;
font-weight: solid;
color: #000000;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
cursor: text;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 5px;
transition: all 0.3s ease;
}
.controls {
width: 100%;
max-width: 400px;
background: #ecf0f1;
border-radius: 10px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.control-group {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #bdc3c7;
}
.control-group h3 {
color: #2c3e50;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
font-size: 1.2rem;
}
.btn {
display: inline-block;
width: auto;
padding: 10px 15px;
margin: 5px 5px 5px 0;
background: #3498db;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.btn:hover {
background: #2980b9;
transform: translateY(-3px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
}
.btn i {
margin-right: 8px;
}
.btn.delete {
background: #e74c3c;
}
.btn.delete:hover {
background: #c0392b;
}
.btn.download {
background: #27ae60;
}
.btn.download:hover {
background: #219653;
}
.btn.handwriting {
background: #9b59b6;
}
.btn.handwriting:hover {
background: #8e44ad;
}
.btn.clear {
background: #f39c12;
}
.btn.clear:hover {
background: #d35400;
}
.slider-container {
margin: 15px 0;
}
.slider-container label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #34495e;
font-size: 1rem;
}
.slider {
width: 100%;
height: 8px;
-webkit-appearance: none;
background: #bdc3c7;
border-radius: 5px;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: #3498db;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
}
.slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
background: #2980b9;
}
.color-picker {
display: flex;
gap: 10px;
margin-top: 10px;
flex-wrap: wrap;
}
.color-option {
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
border: 2px solid #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s;
}
.color-option:hover {
transform: scale(1.15);
}
.color-option.active {
transform: scale(1.2);
border: 2px solid #2c3e50;
box-shadow: 0 0 0 2px white, 0 0 0 4px #2c3e50;
}
.font-select {
width: 100%;
padding: 10px;
border-radius: 8px;
border: 1px solid #bdc3c7;
background: white;
margin-top: 10px;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s;
}
.font-select:hover {
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.text-input {
width: 100%;
padding: 10px;
border-radius: 8px;
border: 1px solid #bdc3c7;
background: white;
margin-top: 10px;
font-size: 1rem;
transition: all 0.2s;
}
.text-input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
}
.preview-container {
background: #fff;
border-radius: 10px;
padding: 15px;
margin-top: 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.preview-container h3 {
color: #2c3e50;
margin-bottom: 12px;
font-size: 1.2rem;
}
.preview-box {
height: 100px;
background: #ecf0f1;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border: 1px solid #bdc3c7;
}
.instructions {
background: #e3f2fd;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
}
.instructions h3 {
color: #0d47a1;
margin-bottom: 12px;
font-size: 1.2rem;
display: flex;
align-items: center;
gap: 8px;
}
.instructions ul {
padding-left: 20px;
}
.instructions li {
margin: 10px 0;
color: #37474f;
line-height: 1.5;
font-size: 0.95rem;
}
.instructions li strong {
color: #c0392b;
font-weight: 600;
}
footer {
text-align: center;
padding: 15px;
background: #2c3e50;
color: #ecf0f1;
font-size: 0.9rem;
}
.signature-editor {
background: #fff;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
}
.resize-handle {
position: absolute;
width: 16px;
height: 16px;
background: #3498db;
border-radius: 50%;
cursor: pointer;
z-index: 5;
box-shadow: 0 0 5px rgba(0,0,0,0.5);
border: 2px solid white;
opacity: 0;
transition: opacity 0.3s ease;
}
.signature.selected .resize-handle {
opacity: 1;
}
.resize-handle.tl {
top: -8px;
left: -8px;
cursor: nw-resize;
}
.resize-handle.tr {
top: -8px;
right: -8px;
cursor: ne-resize;
}
.resize-handle.bl {
bottom: -8px;
left: -8px;
cursor: sw-resize;
}
.resize-handle.br {
bottom: -8px;
right: -8px;
cursor: se-resize;
}
.font-preview {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.font-sample {
padding: 8px;
border-radius: 6px;
background: white;
border: 1px solid #ddd;
text-align: center;
flex: 1;
min-width: 100px;
cursor: pointer;
transition: all 0.2s;
}
.font-sample:hover {
transform: translateY(-3px);
box-shadow: 0 5px 10px rgba(0,0,0,0.1);
border-color: #3498db;
}
.font-sample.active {
border: 2px solid #3498db;
background: #e3f2fd;
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.3);
}
.font-sample h4 {
font-size: 0.8rem;
color: #7f8c8d;
margin-bottom: 6px;
}
.font-sample p {
font-size: 1.3rem;
font-weight: bold;
color: #000000;
}
/* 手写签名画布 */
#handwritingCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: crosshair;
display: none;
z-index: 20;
}
.handwriting-controls {
position: absolute;
top: 15px;
right: 15px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
display: none;
z-index: 30;
flex-direction: column;
gap: 8px;
max-width: 240px;
}
.handwriting-controls .btn {
margin: 0;
padding: 8px 12px;
}
.handwriting-mode .signature {
opacity: 0.3;
}
.handwriting-mode .workspace {
background-image: none;
background-color: white;
}
.pen-size-control {
display: flex;
flex-direction: column;
gap: 5px;
}
.pen-size-indicator {
width: 30px;
height: 30px;
border-radius: 50%;
background: #000;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-weight: bold;
font-size: 12px;
margin-top: 5px;
align-self: center;
}
.handwriting-preview {
position: absolute;
bottom: 15px;
left: 15px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
display: none;
z-index: 30;
width: 150px;
height: 60px;
border: 1px dashed #3498db;
}
.handwriting-preview img {
max-width: 100%;
max-height: 100%;
display: block;
margin: 0 auto;
}
/* 移动端提示 */
.mobile-drag-hint {
position: absolute;
bottom: 15px;
left: 0;
right: 0;
text-align: center;
color: #3498db;
font-weight: bold;
background: rgba(255, 255, 255, 0.8);
padding: 8px;
border-radius: 5px;
display: none;
z-index: 10;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
/* 响应式设计 */
@media (max-width: 1200px) {
.main-content {
flex-direction: column;
}
.workspace {
min-height: 500px;
}
.controls {
max-width: 100%;
}
}
@media (max-width: 768px) {
.container {
border-radius: 15px;
}
header {
padding: 15px 10px;
}
h1 {
font-size: 1.7rem;
}
.subtitle {
font-size: 0.9rem;
}
.main-content {
padding: 10px;
gap: 10px;
}
.workspace {
min-height: 350px;
}
.signature-text {
font-size: 1.8rem;
}
.signature-controls {
top: -30px;
}
.signature-btn {
width: 24px;
height: 24px;
}
.controls {
padding: 15px;
}
.control-group {
margin-bottom: 15px;
padding-bottom: 12px;
}
.control-group h3 {
font-size: 1.1rem;
}
.btn {
padding: 8px 12px;
font-size: 0.85rem;
}
.font-sample p {
font-size: 1.2rem;
}
.preview-box {
height: 90px;
}
.handwriting-controls {
max-width: 180px;
padding: 8px;
top: 10px;
right: 10px;
}
.handwriting-controls .btn {
padding: 6px 10px;
font-size: 0.8rem;
}
.pen-size-control {
width: 100%;
}
.pen-size-control label {
font-size: 0.9rem;
}
.pen-size-control input {
width: 100%;
}
.mobile-drag-hint {
display: block;
}
}
@media (max-width: 480px) {
body {
padding: 10px;
}
.container {
border-radius: 12px;
}
.workspace {
min-height: 300px;
}
.signature-text {
font-size: 1.6rem;
}
.signature-controls {
top: -28px;
}
.signature-btn {
width: 22px;
height: 22px;
font-size: 10px;
}
.controls {
padding: 12px;
}
.font-select, .text-input {
font-size: 0.9rem;
padding: 8px;
}
.slider-container label {
font-size: 0.95rem;
}
.color-option {
width: 26px;
height: 26px;
}
.font-preview {
gap: 8px;
}
.font-sample {
min-width: 80px;
padding: 6px;
}
.font-sample p {
font-size: 1.1rem;
}
.instructions li {
font-size: 0.9rem;
}
.handwriting-controls {
max-width: 160px;
padding: 6px;
top: 8px;
right: 8px;
}
.handwriting-controls .btn {
padding: 5px 8px;
font-size: 0.75rem;
}
}
@media (max-width: 360px) {
h1 {
font-size: 1.5rem;
}
.workspace {
min-height: 250px;
}
.signature-text {
font-size: 1.4rem;
}
.font-sample {
min-width: 70px;
}
.font-sample p {
font-size: 1rem;
}
}
/* 中文字体定义 */
.font-yahei { font-family: 'Microsoft YaHei', sans-serif; }
.font-song { font-family: SimSun, serif; }
.font-kai { font-family: KaiTi, cursive; }
.font-hei { font-family: SimHei, sans-serif; }
.font-youyuan { font-family: YouYuan, cursive; }
.font-libian { font-family: LiSu, cursive; }
.font-fangsong { font-family: FangSong, serif; }
.font-xingkai { font-family: STXingkai, cursive; }
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-signature"></i> 专业签名设计工具</h1>
<p class="subtitle">支持文本与手写签名 – 完美适配所有设备</p>
</header>
<div class="main-content">
<div class="workspace">
<div class="mobile-drag-hint">提示:长按签名拖动位置</div>
<div class="background-canvas" id="backgroundCanvas">
<!– 签名元素将通过JavaScript动态添加 –>
</div>
<!– 手写签名画布 –>
<canvas id="handwritingCanvas"></canvas>
<!– 手写控制按钮 –>
<div class="handwriting-controls">
<div class="pen-size-control">
<label>笔触大小:</label>
<input type="range" min="1" max="20" value="5" id="penSizeSlider">
<div class="pen-size-indicator" id="penSizeIndicator">5</div>
</div>
<button class="btn clear" id="clearHandwritingBtn">
<i class="fas fa-eraser"></i> 清除
</button>
<button class="btn download" id="saveHandwritingBtn">
<i class="fas fa-check"></i> 保存签名
</button>
<button class="btn delete" id="cancelHandwritingBtn">
<i class="fas fa-times"></i> 取消
</button>
</div>
<!– 手写预览 –>
<div class="handwriting-preview" id="handwritingPreview"></div>
</div>
<div class="controls">
<div class="control-group">
<h3><i class="fas fa-image"></i> 背景设置</h3>
<button class="btn" id="uploadBgBtn">
<i class="fas fa-upload"></i> 上传背景图片
</button>
<input type="file" id="bgImageInput" accept="image/*" style="display: none;">
<div class="slider-container">
<label for="bgBrightness">背景亮度: <span id="brightnessValue">100%</span></label>
<input type="range" min="0" max="200" value="100" class="slider" id="bgBrightness">
</div>
</div>
<div class="control-group">
<h3><i class="fas fa-signature"></i> 签名设置</h3>
<div class="btn-group">
<button class="btn" id="addSignatureBtn">
<i class="fas fa-plus"></i> 添加文本签名
</button>
<button class="btn handwriting" id="addHandwritingBtn">
<i class="fas fa-pen"></i> 手写签名
</button>
<button class="btn delete" id="removeSignatureBtn">
<i class="fas fa-trash"></i> 删除当前签名
</button>
</div>
<div class="signature-editor">
<div class="slider-container">
<label for="signatureText">签名文本:</label>
<input type="text" id="signatureText" class="text-input" placeholder="输入签名内容" value="张伟">
</div>
<div class="slider-container">
<label for="signatureSize">签名大小: <span id="sizeValue">100%</span></label>
<input type="range" min="20" max="300" value="100" class="slider" id="signatureSize">
</div>
<div class="slider-container">
<label for="signatureRotation">旋转角度: <span id="rotationValue">0°</span></label>
<input type="range" min="0" max="360" value="0" class="slider" id="signatureRotation">
</div>
<label>签名颜色:</label>
<div class="color-picker">
<div class="color-option active" style="background-color: #000000;" data-color="#000000"></div>
<div class="color-option" style="background-color: #2c3e50;" data-color="#2c3e50"></div>
<div class="color-option" style="background-color: #c0392b;" data-color="#c0392b"></div>
<div class="color-option" style="background-color: #27ae60;" data-color="#27ae60"></div>
<div class="color-option" style="background-color: #8e44ad;" data-color="#8e44ad"></div>
<div class="color-option" style="background-color: #2980b9;" data-color="#2980b9"></div>
<div class="color-option" style="background-color: #d35400;" data-color="#d35400"></div>
<div class="color-option" style="background-color: #16a085;" data-color="#16a085"></div>
</div>
<label>签名字体:</label>
<select class="font-select" id="signatureFont">
<option value="font-yahei">微软雅黑</option>
<option value="font-song">宋体</option>
<option value="font-kai">楷体</option>
<option value="font-hei">黑体</option>
<option value="font-youyuan">幼圆</option>
<option value="font-libian">隶书</option>
<option value="font-fangsong">仿宋</option>
<option value="font-xingkai">行楷</option>
</select>
<div class="font-preview">
<div class="font-sample active" data-font="font-yahei">
<h4>微软雅黑</h4>
<p class="font-yahei">张伟</p>
</div>
<div class="font-sample" data-font="font-song">
<h4>宋体</h4>
<p class="font-song">张伟</p>
</div>
<div class="font-sample" data-font="font-kai">
<h4>楷体</h4>
<p class="font-kai">张伟</p>
</div>
<div class="font-sample" data-font="font-hei">
<h4>黑体</h4>
<p class="font-hei">张伟</p>
</div>
</div>
</div>
</div>
<div class="control-group">
<h3><i class="fas fa-download"></i> 导出结果</h3>
<button class="btn download" id="downloadBtn">
<i class="fas fa-download"></i> 下载签名设计
</button>
</div>
<div class="preview-container">
<h3><i class="fas fa-eye"></i> 签名预览</h3>
<div class="preview-box">
<div class="signature-text" id="signaturePreview">张伟</div>
</div>
</div>
<div class="instructions">
<h3><i class="fas fa-info-circle"></i> 使用说明</h3>
<ul>
<li><strong>添加签名</strong>:点击"添加文本签名"或"手写签名"按钮</li>
<li><strong>手写签名</strong>:使用鼠标/触摸屏绘制签名,完成后点击"保存签名"</li>
<li><strong>编辑内容</strong>:在文本框中输入您的姓名</li>
<li><strong>移动位置</strong>:拖动签名到任意位置</li>
<li><strong>自由缩放</strong>:拖动签名四个角的蓝色控制点调整大小</li>
<li><strong>字体选择</strong>:从下拉菜单选择或点击预览卡片切换字体</li>
<li><strong>旋转签名</strong>:使用控制面板滑块或签名右上角旋转按钮</li>
<li><strong>背景设置</strong>:上传您自己的背景图片</li>
<li><strong>保存结果</strong>:完成后点击下载按钮</li>
</ul>
</div>
</div>
</div>
<footer>
<p>© 2026 专业签名设计工具 | 支持文本与手写签名</p>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 元素引用
const workspace = document.getElementById('backgroundCanvas');
const uploadBtn = document.getElementById('uploadBgBtn');
const bgInput = document.getElementById('bgImageInput');
const addSignatureBtn = document.getElementById('addSignatureBtn');
const addHandwritingBtn = document.getElementById('addHandwritingBtn');
const removeSignatureBtn = document.getElementById('removeSignatureBtn');
const sizeSlider = document.getElementById('signatureSize');
const rotationSlider = document.getElementById('signatureRotation');
const brightnessSlider = document.getElementById('bgBrightness');
const colorOptions = document.querySelectorAll('.color-option');
const fontSelect = document.getElementById('signatureFont');
const downloadBtn = document.getElementById('downloadBtn');
const previewBox = document.getElementById('signaturePreview');
const textInput = document.getElementById('signatureText');
const fontSamples = document.querySelectorAll('.font-sample');
const handwritingCanvas = document.getElementById('handwritingCanvas');
const clearHandwritingBtn = document.getElementById('clearHandwritingBtn');
const saveHandwritingBtn = document.getElementById('saveHandwritingBtn');
const cancelHandwritingBtn = document.getElementById('cancelHandwritingBtn');
const handwritingControls = document.querySelector('.handwriting-controls');
const penSizeSlider = document.getElementById('penSizeSlider');
const penSizeIndicator = document.getElementById('penSizeIndicator');
const handwritingPreview = document.getElementById('handwritingPreview');
// 当前选中的签名
let currentSignature = null;
let signatureCount = 0;
let originalSizes = new Map(); // 存储原始尺寸
// 手写签名相关变量
let isDrawing = false;
let lastX = 0;
let lastY = 0;
let ctx = null;
let penSize = 3; // 初始笔触大小
// 初始化画布
function initCanvas() {
const rect = workspace.parentElement.getBoundingClientRect();
handwritingCanvas.width = rect.width;
handwritingCanvas.height = rect.height;
ctx = handwritingCanvas.getContext('2d');
ctx.lineWidth = penSize;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = '#000000'; // 黑色笔触
}
// 笔触大小控制
penSizeSlider.addEventListener('input', function() {
penSize = this.value;
ctx.lineWidth = penSize;
penSizeIndicator.textContent = penSize;
});
// 背景设置 – 修复移动端上传问题
uploadBtn.addEventListener('click', () => bgInput.click());
bgInput.addEventListener('change', function(e) {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onload = function(event) {
workspace.parentElement.style.backgroundImage = `url(${event.target.result})`;
workspace.parentElement.style.backgroundSize = "cover";
workspace.parentElement.style.backgroundRepeat = "no-repeat";
workspace.parentElement.style.backgroundPosition = "center";
}
reader.onerror = function(error) {
console.error('文件读取错误:', error);
alert('背景图片上传失败,请重试');
};
reader.readAsDataURL(e.target.files[0]);
}
});
brightnessSlider.addEventListener('input', function() {
const value = this.value;
document.getElementById('brightnessValue').textContent = `${value}%`;
workspace.parentElement.style.filter = `brightness(${value}%)`;
});
// 签名创建和删除
addSignatureBtn.addEventListener('click', function() {
signatureCount++;
const signature = document.createElement('div');
signature.className = 'signature';
signature.innerHTML = `
<div class="signature-text font-yahei">${textInput.value || '签名' + signatureCount}</div>
<div class="signature-controls">
<button class="signature-btn edit" title="编辑"><i class="fas fa-edit"></i></button>
<button class="signature-btn rotate" title="旋转"><i class="fas fa-sync-alt"></i></button>
<button class="signature-btn delete" title="删除"><i class="fas fa-times"></i></button>
</div>
<div class="resize-handle tl" title="左上角缩放"></div>
<div class="resize-handle tr" title="右上角缩放"></div>
<div class="resize-handle bl" title="左下角缩放"></div>
<div class="resize-handle br" title="右下角缩放"></div>
`;
// 初始位置在画布中央
signature.style.left = '50%';
signature.style.top = '50%';
signature.style.transform = 'translate(-50%, -50%)';
workspace.appendChild(signature);
// 记录原始尺寸
originalSizes.set(signature, {
width: 180,
height: 80
});
selectSignature(signature);
// 添加拖动功能
makeDraggable(signature);
// 添加缩放功能
addResizeFunctionality(signature);
// 添加控制按钮事件
const editBtn = signature.querySelector('.edit');
const rotateBtn = signature.querySelector('.rotate');
const deleteBtn = signature.querySelector('.delete');
// 编辑按钮
editBtn.addEventListener('click', function(e) {
e.stopPropagation();
const text = signature.querySelector('.signature-text').textContent;
textInput.value = text;
textInput.focus();
});
// 旋转按钮
rotateBtn.addEventListener('click', function(e) {
e.stopPropagation();
const currentRotation = parseInt(signature.dataset.rotation || 0);
const newRotation = (currentRotation + 45) % 360;
signature.style.transform = `translate(-50%, -50%) rotate(${newRotation}deg)`;
signature.dataset.rotation = newRotation;
rotationSlider.value = newRotation;
document.getElementById('rotationValue').textContent = `${newRotation}°`;
});
// 删除按钮
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
signature.remove();
originalSizes.delete(signature);
if (currentSignature === signature) {
currentSignature = null;
updateControlsState();
}
});
});
// 默认添加一个签名
setTimeout(() => {
addSignatureBtn.click();
}, 500);
// 手写签名功能
addHandwritingBtn.addEventListener('click', function() {
// 初始化画布
initCanvas();
// 显示画布和控制按钮
handwritingCanvas.style.display = 'block';
handwritingControls.style.display = 'flex';
workspace.parentElement.classList.add('handwriting-mode');
// 显示笔触大小
penSizeIndicator.textContent = penSize;
// 禁用其他控制
document.querySelectorAll('.controls .btn').forEach(btn => {
btn.disabled = true;
});
});
// 清除手写内容
clearHandwritingBtn.addEventListener('click', function() {
ctx.clearRect(0, 0, handwritingCanvas.width, handwritingCanvas.height);
handwritingPreview.style.display = 'none';
});
// 保存手写签名
saveHandwritingBtn.addEventListener('click', function() {
// 创建签名元素
signatureCount++;
const signature = document.createElement('div');
signature.className = 'signature';
// 裁剪签名并放大
const croppedCanvas = cropCanvas(handwritingCanvas);
const scaledCanvas = scaleCanvas(croppedCanvas, 2); // 放大2倍
// 创建图像元素
const img = document.createElement('img');
img.src = scaledCanvas.toDataURL('image/png');
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'contain';
img.style.display = 'block';
signature.appendChild(img);
signature.innerHTML += `
<div class="signature-controls">
<button class="signature-btn rotate" title="旋转"><i class="fas fa-sync-alt"></i></button>
<button class="signature-btn delete" title="删除"><i class="fas fa-times"></i></button>
</div>
<div class="resize-handle tl" title="左上角缩放"></div>
<div class="resize-handle tr" title="右上角缩放"></div>
<div class="resize-handle bl" title="左下角缩放"></div>
<div class="resize-handle br" title="右下角缩放"></div>
`;
// 设置初始位置和大小
signature.style.left = '50%';
signature.style.top = '50%';
signature.style.transform = 'translate(-50%, -50%)';
signature.style.width = '300px';
signature.style.height = '150px';
signature.style.background = 'transparent';
signature.style.padding = '0';
// 添加居中样式
signature.style.display = 'flex';
signature.style.justifyContent = 'center';
signature.style.alignItems = 'center';
workspace.appendChild(signature);
// 记录原始尺寸
originalSizes.set(signature, {
width: 300,
height: 150
});
selectSignature(signature);
// 添加拖动功能
makeDraggable(signature);
// 添加缩放功能
addResizeFunctionality(signature);
// 添加控制按钮事件
const rotateBtn = signature.querySelector('.rotate');
const deleteBtn = signature.querySelector('.delete');
// 旋转按钮
rotateBtn.addEventListener('click', function(e) {
e.stopPropagation();
const currentRotation = parseInt(signature.dataset.rotation || 0);
const newRotation = (currentRotation + 45) % 360;
signature.style.transform = `translate(-50%, -50%) rotate(${newRotation}deg)`;
signature.dataset.rotation = newRotation;
rotationSlider.value = newRotation;
document.getElementById('rotationValue').textContent = `${newRotation}°`;
});
// 删除按钮
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
signature.remove();
originalSizes.delete(signature);
if (currentSignature === signature) {
currentSignature = null;
updateControlsState();
}
});
// 退出手写模式
exitHandwritingMode();
});
// 裁剪画布(去除空白)
function cropCanvas(canvas) {
const ctx = canvas.getContext('2d');
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = pixels.data;
let top = 0, bottom = 0, left = 0, right = 0;
// 查找顶部边界
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const alpha = data[((canvas.width * y) + x) * 4 + 3];
if (alpha > 0) {
top = y;
y = canvas.height;
break;
}
}
}
// 查找底部边界
for (let y = canvas.height – 1; y >= 0; y–) {
for (let x = 0; x < canvas.width; x++) {
const alpha = data[((canvas.width * y) + x) * 4 + 3];
if (alpha > 0) {
bottom = y;
y = -1;
break;
}
}
}
// 查找左侧边界
for (let x = 0; x < canvas.width; x++) {
for (let y = 0; y < canvas.height; y++) {
const alpha = data[((canvas.width * y) + x) * 4 + 3];
if (alpha > 0) {
left = x;
x = canvas.width;
break;
}
}
}
// 查找右侧边界
for (let x = canvas.width – 1; x >= 0; x–) {
for (let y = 0; y < canvas.height; y++) {
const alpha = data[((canvas.width * y) + x) * 4 + 3];
if (alpha > 0) {
right = x;
x = -1;
break;
}
}
}
// 添加一些边距
const padding = 20;
top = Math.max(0, top – padding);
left = Math.max(0, left – padding);
bottom = Math.min(canvas.height, bottom + padding);
right = Math.min(canvas.width, right + padding);
const width = right – left;
const height = bottom – top;
// 创建裁剪后的画布
const croppedCanvas = document.createElement('canvas');
croppedCanvas.width = width;
croppedCanvas.height = height;
const croppedCtx = croppedCanvas.getContext('2d');
croppedCtx.drawImage(canvas, left, top, width, height, 0, 0, width, height);
return croppedCanvas;
}
// 放大画布内容
function scaleCanvas(canvas, scaleFactor) {
const scaledCanvas = document.createElement('canvas');
scaledCanvas.width = canvas.width * scaleFactor;
scaledCanvas.height = canvas.height * scaleFactor;
const scaledCtx = scaledCanvas.getContext('2d');
scaledCtx.imageSmoothingEnabled = true;
scaledCtx.imageSmoothingQuality = 'high';
scaledCtx.drawImage(canvas, 0, 0, scaledCanvas.width, scaledCanvas.height);
return scaledCanvas;
}
// 取消手写签名
cancelHandwritingBtn.addEventListener('click', exitHandwritingMode);
// 退出手写模式
function exitHandwritingMode() {
handwritingCanvas.style.display = 'none';
handwritingControls.style.display = 'none';
handwritingPreview.style.display = 'none';
workspace.parentElement.classList.remove('handwriting-mode');
// 清除画布
ctx.clearRect(0, 0, handwritingCanvas.width, handwritingCanvas.height);
// 启用其他控制
document.querySelectorAll('.controls .btn').forEach(btn => {
btn.disabled = false;
});
}
// 手写签名绘制功能
function startDrawing(e) {
isDrawing = true;
[lastX, lastY] = getCoordinates(e);
}
function draw(e) {
if (!isDrawing) return;
const [x, y] = getCoordinates(e);
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
[lastX, lastY] = [x, y];
// 更新预览
updateHandwritingPreview();
}
function stopDrawing() {
isDrawing = false;
updateHandwritingPreview();
}
// 更新手写预览
function updateHandwritingPreview() {
const croppedCanvas = cropCanvas(handwritingCanvas);
const scaledCanvas = scaleCanvas(croppedCanvas, 1.5); // 预览时放大1.5倍
handwritingPreview.innerHTML = '';
const img = document.createElement('img');
img.src = scaledCanvas.toDataURL('image/png');
img.style.maxWidth = '100%';
img.style.maxHeight = '100%';
handwritingPreview.appendChild(img);
handwritingPreview.style.display = 'block';
}
// 获取坐标(支持触摸事件)
function getCoordinates(e) {
let x, y;
const rect = handwritingCanvas.getBoundingClientRect();
if (e.type.includes('touch')) {
const touch = e.touches[0] || e.changedTouches[0];
x = touch.clientX – rect.left;
y = touch.clientY – rect.top;
} else {
x = e.clientX – rect.left;
y = e.clientY – rect.top;
}
return [x, y];
}
// 添加绘图事件监听器
handwritingCanvas.addEventListener('mousedown', startDrawing);
handwritingCanvas.addEventListener('mousemove', draw);
handwritingCanvas.addEventListener('mouseup', stopDrawing);
handwritingCanvas.addEventListener('mouseout', stopDrawing);
// 触摸事件支持
handwritingCanvas.addEventListener('touchstart', startDrawing);
handwritingCanvas.addEventListener('touchmove', draw);
handwritingCanvas.addEventListener('touchend', stopDrawing);
removeSignatureBtn.addEventListener('click', function() {
if (currentSignature) {
currentSignature.remove();
originalSizes.delete(currentSignature);
currentSignature = null;
updateControlsState();
}
});
// 签名选择
function selectSignature(signature) {
// 移除之前选中的签名边框
document.querySelectorAll('.signature').forEach(sig => {
sig.classList.remove('selected');
});
// 设置当前签名
if (signature) {
signature.classList.add('selected');
currentSignature = signature;
// 更新控制面板的值
const text = signature.querySelector('.signature-text')?.textContent || '';
const size = parseInt(signature.dataset.size || 100);
const rotation = parseInt(signature.dataset.rotation || 0);
const color = signature.dataset.color || '#000000';
const fontClass = signature.querySelector('.signature-text')?.className.split(' ').find(cls => cls.startsWith('font-')) || 'font-yahei';
textInput.value = text;
sizeSlider.value = size;
document.getElementById('sizeValue').textContent = `${size}%`;
rotationSlider.value = rotation;
document.getElementById('rotationValue').textContent = `${rotation}°`;
// 更新颜色选择器
colorOptions.forEach(option => {
option.classList.remove('active');
if (option.dataset.color === color) {
option.classList.add('active');
}
});
// 更新字体选择器
fontSelect.value = fontClass;
// 更新字体预览卡片
fontSamples.forEach(sample => {
sample.classList.remove('active');
if (sample.dataset.font === fontClass) {
sample.classList.add('active');
}
});
} else {
currentSignature = null;
}
// 更新控件状态
updateControlsState();
}
// 更新控件状态
function updateControlsState() {
const disabled = currentSignature === null;
sizeSlider.disabled = disabled;
rotationSlider.disabled = disabled;
textInput.disabled = disabled;
fontSelect.disabled = disabled;
// 更新预览
if (!disabled) {
const signatureText = currentSignature.querySelector('.signature-text');
if (signatureText) {
previewBox.textContent = signatureText.textContent;
previewBox.style.color = signatureText.style.color || '#000000';
previewBox.className = "signature-text " + signatureText.className;
previewBox.style.fontSize = signatureText.style.fontSize || "2rem";
} else {
// 手写签名没有文本元素
previewBox.innerHTML = '<span style="color:#7f8c8d; font-size:1rem;">手写签名预览</span>';
}
} else {
previewBox.textContent = textInput.value || "您的签名";
previewBox.className = "signature-text font-yahei";
previewBox.style.color = '#000000';
}
}
// 使元素可拖动 – 修复移动端拖动问题
function makeDraggable(element) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
let touchOffsetX = 0, touchOffsetY = 0;
element.onmousedown = dragMouseDown;
element.ontouchstart = dragTouchStart;
function dragMouseDown(e) {
// 如果点击的是缩放控制点,则不触发拖动
if (e.target.classList.contains('resize-handle')) return;
e = e || window.event;
e.preventDefault();
// 选择当前签名
selectSignature(element);
// 获取鼠标位置
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function dragTouchStart(e) {
if (e.target.classList.contains('resize-handle')) return;
e.preventDefault();
document.body.style.overflow = 'hidden'; // 防止拖动时页面滚动
selectSignature(element);
const touch = e.touches[0];
const rect = element.getBoundingClientRect();
// 计算触摸点相对于元素左上角的偏移
touchOffsetX = touch.clientX – rect.left;
touchOffsetY = touch.clientY – rect.top;
pos3 = touch.clientX;
pos4 = touch.clientY;
document.ontouchend = closeDragElement;
document.ontouchmove = elementTouchMove;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// 计算新位置
pos1 = pos3 – e.clientX;
pos2 = pos4 – e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// 设置元素新位置
const rect = workspace.getBoundingClientRect();
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
// 计算新位置(像素单位)
let newLeft = element.offsetLeft – pos1;
let newTop = element.offsetTop – pos2;
// 限制在工作区内
newLeft = Math.max(0, Math.min(rect.width – element.offsetWidth, newLeft));
newTop = Math.max(0, Math.min(rect.height – element.offsetHeight, newTop));
element.style.left = `${newLeft}px`;
element.style.top = `${newTop}px`;
}
// 修复移动端垂直拖动问题
function elementTouchMove(e) {
e.preventDefault();
const touch = e.touches[0];
const workspaceRect = workspace.parentElement.getBoundingClientRect();
// 计算元素的新位置(相对于工作区的左上角)
let newLeft = (touch.clientX – workspaceRect.left) – touchOffsetX;
let newTop = (touch.clientY – workspaceRect.top) – touchOffsetY;
// 限制在工作区内
newLeft = Math.max(0, Math.min(workspaceRect.width – element.offsetWidth, newLeft));
newTop = Math.max(0, Math.min(workspaceRect.height – element.offsetHeight, newTop));
element.style.left = `${newLeft}px`;
element.style.top = `${newTop}px`;
}
function closeDragElement() {
// 恢复页面滚动
document.body.style.overflow = '';
document.onmouseup = null;
document.onmousemove = null;
document.ontouchend = null;
document.ontouchmove = null;
}
}
// 添加缩放功能(四个角)
function addResizeFunctionality(element) {
const resizeHandles = element.querySelectorAll('.resize-handle');
resizeHandles.forEach(handle => {
handle.addEventListener('mousedown', initResize);
handle.addEventListener('touchstart', initResize);
});
function initResize(e) {
e.preventDefault();
e.stopPropagation();
// 选择当前元素
selectSignature(element);
const handleClass = e.target.className.split(' ')[1]; // tl, tr, bl, br
let startX, startY;
const startLeft = element.offsetLeft;
const startTop = element.offsetTop;
const startWidth = element.offsetWidth;
const startHeight = element.offsetHeight;
if (e.type === 'mousedown') {
startX = e.clientX;
startY = e.clientY;
} else if (e.type === 'touchstart') {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}
// 添加事件监听
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResize);
document.addEventListener('touchmove', resize);
document.addEventListener('touchend', stopResize);
function resize(e) {
e.preventDefault();
let currentX, currentY;
if (e.type === 'mousemove') {
currentX = e.clientX;
currentY = e.clientY;
} else if (e.type === 'touchmove') {
currentX = e.touches[0].clientX;
currentY = e.touches[0].clientY;
}
const dx = currentX – startX;
const dy = currentY – startY;
let newWidth = startWidth;
let newHeight = startHeight;
let newLeft = startLeft;
let newTop = startTop;
// 根据不同的控制点计算新位置和大小
if (handleClass === 'br') {
newWidth = Math.max(100, startWidth + dx);
newHeight = Math.max(50, startHeight + dy);
} else if (handleClass === 'bl') {
newWidth = Math.max(100, startWidth – dx);
newHeight = Math.max(50, startHeight + dy);
newLeft = startLeft + dx;
} else if (handleClass === 'tr') {
newWidth = Math.max(100, startWidth + dx);
newHeight = Math.max(50, startHeight – dy);
newTop = startTop + dy;
} else if (handleClass === 'tl') {
newWidth = Math.max(100, startWidth – dx);
newHeight = Math.max(50, startHeight – dy);
newLeft = startLeft + dx;
newTop = startTop + dy;
}
// 设置新位置和大小
element.style.width = `${newWidth}px`;
element.style.height = `${newHeight}px`;
element.style.left = `${newLeft}px`;
element.style.top = `${newTop}px`;
// 更新控制面板的滑块值
const sizeValue = Math.min(300, Math.max(20, Math.round((newWidth / originalSizes.get(element).width) * 100)));
sizeSlider.value = sizeValue;
document.getElementById('sizeValue').textContent = `${sizeValue}%`;
// 更新字体大小(保持比例)
const signatureText = element.querySelector('.signature-text');
if (signatureText) {
const fontSize = Math.max(1.2, Math.min(5, (newWidth / originalSizes.get(element).width) * 2));
signatureText.style.fontSize = `${fontSize}rem`;
}
}
function stopResize() {
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stopResize);
document.removeEventListener('touchmove', resize);
document.removeEventListener('touchend', stopResize);
}
}
}
// 签名文本编辑
textInput.addEventListener('input', function() {
if (currentSignature) {
const signatureText = currentSignature.querySelector('.signature-text');
if (signatureText) {
signatureText.textContent = this.value;
// 更新预览
previewBox.textContent = this.value;
}
} else {
previewBox.textContent = this.value;
}
});
// 签名属性控制 – 大小滑块
sizeSlider.addEventListener('input', function() {
const value = this.value;
document.getElementById('sizeValue').textContent = `${value}%`;
if (currentSignature) {
const scale = value / 100;
const originalSize = originalSizes.get(currentSignature);
// 计算新尺寸
const newWidth = originalSize.width * scale;
const newHeight = originalSize.height * scale;
// 设置新尺寸
currentSignature.style.width = `${newWidth}px`;
currentSignature.style.height = `${newHeight}px`;
// 更新字体大小(保持比例)
const signatureText = currentSignature.querySelector('.signature-text');
if (signatureText) {
const fontSize = Math.max(1.2, Math.min(5, scale * 2));
signatureText.style.fontSize = `${fontSize}rem`;
previewBox.style.fontSize = `${fontSize}rem`;
}
} else {
previewBox.style.fontSize = `${value / 50}rem`;
}
});
// 旋转滑块
rotationSlider.addEventListener('input', function() {
const value = this.value;
document.getElementById('rotationValue').textContent = `${value}°`;
if (currentSignature) {
currentSignature.style.transform = `translate(-50%, -50%) rotate(${value}deg)`;
currentSignature.dataset.rotation = value;
}
});
// 颜色选择
colorOptions.forEach(option => {
option.addEventListener('click', function() {
colorOptions.forEach(opt => opt.classList.remove('active'));
this.classList.add('active');
const color = this.dataset.color;
if (currentSignature) {
const signatureText = currentSignature.querySelector('.signature-text');
if (signatureText) {
signatureText.style.color = color;
currentSignature.dataset.color = color;
// 更新预览
previewBox.style.color = color;
}
} else {
previewBox.style.color = color;
}
});
});
// 字体选择
fontSelect.addEventListener('change', function() {
const fontClass = this.value;
if (currentSignature) {
const signatureText = currentSignature.querySelector('.signature-text');
if (signatureText) {
// 移除所有字体类
signatureText.className = signatureText.className.replace(/\\bfont-\\w+\\b/g, '');
// 添加新字体类
signatureText.classList.add(fontClass);
// 更新字体预览卡片
fontSamples.forEach(sample => {
sample.classList.remove('active');
if (sample.dataset.font === fontClass) {
sample.classList.add('active');
}
});
// 更新预览
previewBox.className = "signature-text " + fontClass;
}
} else {
previewBox.className = "signature-text " + fontClass;
}
});
// 字体预览卡片点击事件
fontSamples.forEach(sample => {
sample.addEventListener('click', function() {
const fontClass = this.dataset.font;
fontSelect.value = fontClass;
// 更新字体预览卡片
fontSamples.forEach(s => s.classList.remove('active'));
this.classList.add('active');
if (currentSignature) {
const signatureText = currentSignature.querySelector('.signature-text');
if (signatureText) {
// 移除所有字体类
signatureText.className = signatureText.className.replace(/\\bfont-\\w+\\b/g, '');
// 添加新字体类
signatureText.classList.add(fontClass);
// 更新预览
previewBox.className = "signature-text " + fontClass;
}
} else {
previewBox.className = "signature-text " + fontClass;
}
});
});
// 下载功能 – 修复下载问题
downloadBtn.addEventListener('click', function() {
// 保存当前选中的签名状态
const prevSelected = currentSignature;
// 移除所有签名元素的选中状态(隐藏按钮和边框)
document.querySelectorAll('.signature').forEach(sig => {
sig.classList.remove('selected');
});
// 保存所有签名元素的原始状态
const signatures = document.querySelectorAll('.workspace .signature');
const originalStates = [];
signatures.forEach(signature => {
// 保存原始状态
originalStates.push({
element: signature,
controls: signature.querySelector('.signature-controls')?.outerHTML || '',
handles: Array.from(signature.querySelectorAll('.resize-handle')).map(h => h.outerHTML).join(''),
border: signature.style.border,
boxShadow: signature.style.boxShadow
});
// 临时移除控制按钮
const controls = signature.querySelector('.signature-controls');
if (controls) controls.remove();
// 临时移除缩放点
const resizeHandles = signature.querySelectorAll('.resize-handle');
resizeHandles.forEach(handle => handle.remove());
// 临时移除边框和阴影
signature.style.border = 'none';
signature.style.boxShadow = 'none';
});
// 截图
setTimeout(() => {
html2canvas(document.querySelector('.workspace'), {
backgroundColor: null,
scale: 2,
useCORS: true,
logging: true
}).then(canvas => {
// 创建下载链接
const link = document.createElement('a');
link.download = 'signature-design.png';
link.href = canvas.toDataURL('image/png');
link.click();
// 恢复签名元素的原始状态
originalStates.forEach(state => {
state.element.style.border = state.border;
state.element.style.boxShadow = state.boxShadow;
// 重新添加控制按钮
if (state.controls) {
state.element.insertAdjacentHTML('beforeend', state.controls);
}
// 重新添加缩放点
if (state.handles) {
state.element.insertAdjacentHTML('beforeend', state.handles);
}
});
// 恢复之前的选中状态
if (prevSelected) {
prevSelected.classList.add('selected');
currentSignature = prevSelected;
}
}).catch(error => {
console.error('截图失败:', error);
// 恢复签名元素的原始状态
originalStates.forEach(state => {
state.element.style.border = state.border;
state.element.style.boxShadow = state.boxShadow;
if (state.controls) {
state.element.insertAdjacentHTML('beforeend', state.controls);
}
if (state.handles) {
state.element.insertAdjacentHTML('beforeend', state.handles);
}
});
if (prevSelected) {
prevSelected.classList.add('selected');
currentSignature = prevSelected;
}
});
}, 100); // 给DOM更新一点时间
});
// 点击文档其他区域取消选中签名
document.addEventListener('click', function(e) {
// 如果点击的不是签名元素或签名控制按钮
if (!e.target.closest('.signature') && !e.target.closest('.signature-controls')) {
selectSignature(null);
}
});
// 初始状态更新
updateControlsState();
// 响应窗口大小变化
window.addEventListener('resize', function() {
if (handwritingCanvas.style.display === 'block') {
initCanvas();
}
});
});
</script>
</body>
</html>
部署与使用建议
1. 本地部署
-
将完整代码保存为HTML文件
-
在浏览器中直接打开即可使用
-
无需任何服务器环境
2. 服务器部署
-
上传HTML文件到任意Web服务器
-
确保html2canvas库能正常加载
-
配置正确的MIME类型
3. 性能优化建议
-
对于大量签名元素,考虑使用虚拟滚动
-
手写签名画布可以优化为按需渲染
-
导出图片时可添加加载提示
常见问题解决
1. 移动端触摸问题
问题:在iOS设备上,触摸绘制不流畅 解决:添加touch-action: manipulation样式,优化触摸事件处理
2. 导出图片模糊
解决:在html2canvas配置中设置scale: 2提高分辨率
3. 手写签名保存问题
解决:添加签名裁剪和放大功能,确保手写签名清晰
扩展功能建议
云存储:添加将设计保存到云端的功能
模板系统:预置多种签名模板
AI签名生成:集成AI生成艺术签名
历史记录:保存用户的设计历史
社交分享:一键分享到社交媒体
总结
这个签名设计工具是一个功能完整、移动端优化的前端应用,包含了文本签名和手写签名的所有基本功能。通过响应式设计和触摸友好的界面,它能够在各种设备上提供良好的用户体验。代码结构清晰,易于扩展和维护,是学习前端开发特别是Canvas操作和移动端优化的优秀示例。
希望这个工具对你有所帮助!如果你有任何问题或改进建议,欢迎在评论区讨论。


![[项目] --- 基于 Spring Boot + Redis的多设计模式抽奖系统-171主机测评](https://www.171host.com/wp-content/uploads/2026/11/20261031220757-6ae666bdaa3f5-220x150.png)
