本文目录导读:

在PHP项目中实现用户自定义签名功能,通常涉及前端编辑、后端存储和前端展示三个环节,以下是一个完整的实现方案,涵盖常见需求(如文本签名、手写签名、图片上传签名)。
功能设计思路
| 功能点 | 技术方案 |
|---|---|
| 文本签名 | 用户输入文本,存储为字符串 |
| 手写签名 | 使用Canvas绘制,生成Base64图片或Blob |
| 图片上传签名 | 上传图片文件,存储路径 |
| 签名存储 | 数据库字段存储JSON或图片路径 |
| 签名展示 | 根据类型动态渲染 |
数据库设计
CREATE TABLE user_signatures (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
signature_type ENUM('text', 'draw', 'image') DEFAULT 'text',
signature_data TEXT, -- 文本内容或Base64或图片路径
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
字段说明:
signature_type:签名类型signature_data:根据类型存储不同数据(文本签名存字符串,手写签名存Base64或路径,图片签名存路径)
后端PHP接口实现
1 保存签名(文本/手写/图片)
<?php
// save_signature.php
session_start();
require_once 'db.php'; // 数据库连接
$user_id = $_SESSION['user_id'];
$type = $_POST['type'] ?? 'text';
$data = $_POST['data'] ?? '';
if (!$user_id) {
die(json_encode(['error' => '用户未登录']));
}
// 1. 文本签名
if ($type === 'text') {
$data = htmlspecialchars(trim($data), ENT_QUOTES, 'UTF-8');
if (mb_strlen($data) > 50) {
die(json_encode(['error' => '签名文本过长']));
}
}
// 2. 手写签名(Base64图片)
elseif ($type === 'draw') {
// 可选:将Base64保存为文件,减少数据库压力
$data = save_base64_image($data, $user_id);
}
// 3. 图片上传签名
elseif ($type === 'image') {
if (!isset($_FILES['file'])) {
die(json_encode(['error' => '未上传文件']));
}
$data = handle_upload($_FILES['file'], $user_id);
}
// 存储或更新签名
$stmt = $db->prepare("
INSERT INTO user_signatures (user_id, signature_type, signature_data)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE signature_type=VALUES(signature_type), signature_data=VALUES(signature_data)
");
$stmt->execute([$user_id, $type, $data]);
echo json_encode(['success' => true, 'data' => $data]);
2 辅助函数
// 保存Base64图片为文件
function save_base64_image($base64_string, $user_id) {
// 去除前缀 data:image/png;base64,
$image_parts = explode(";base64,", $base64_string);
if (count($image_parts) != 2) return false;
$image_base64 = base64_decode($image_parts[1]);
$filename = "signatures/user_{$user_id}_" . time() . ".png";
$filepath = __DIR__ . "/uploads/" . $filename;
if (!file_exists(dirname($filepath))) {
mkdir(dirname($filepath), 0755, true);
}
file_put_contents($filepath, $image_base64);
return $filename;
}
// 处理上传的签名图片
function handle_upload($file, $user_id) {
$allowed_types = ['image/png', 'image/jpeg', 'image/gif'];
if (!in_array($file['type'], $allowed_types)) {
die(json_encode(['error' => '仅支持PNG/JPG/GIF图片']));
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = "signatures/user_{$user_id}_" . time() . "." . $ext;
$filepath = __DIR__ . "/uploads/" . $filename;
if (!move_uploaded_file($file['tmp_name'], $filepath)) {
die(json_encode(['error' => '文件上传失败']));
}
return $filename;
}
前端HTML/CSS/JS实现
1 签名选择与编辑界面
<div class="signature-container">
<h3>设置签名</h3>
<!-- 签名类型切换 -->
<div class="type-selector">
<label><input type="radio" name="sig_type" value="text" checked> 文字签名</label>
<label><input type="radio" name="sig_type" value="draw"> 手写签名</label>
<label><input type="radio" name="sig_type" value="image"> 上传图片</label>
</div>
<!-- 文本签名编辑 -->
<div id="text-signature" class="sig-edit-area">
<input type="text" id="sig-text" placeholder="输入签名文字" maxlength="50">
<button onclick="previewTextSig()">预览</button>
</div>
<!-- 手写签名画布 -->
<div id="draw-signature" class="sig-edit-area" style="display:none;">
<canvas id="sig-canvas" width="300" height="150" style="border:1px solid #ccc;"></canvas>
<div>
<button onclick="clearCanvas()">清除</button>
<button onclick="saveDrawSig()">保存手写签名</button>
</div>
</div>
<!-- 图片上传 -->
<div id="image-signature" class="sig-edit-area" style="display:none;">
<input type="file" id="sig-image" accept="image/png,image/jpeg,image/gif">
</div>
<!-- 签名预览 -->
<div id="signature-preview" class="sig-preview">
<h4>当前签名</h4>
<div id="sig-display"></div>
</div>
<button onclick="saveSignature()">保存签名设置</button>
</div>
2 JavaScript核心逻辑
// 签名类型切换
document.querySelectorAll('input[name="sig_type"]').forEach(radio => {
radio.addEventListener('change', function() {
document.querySelectorAll('.sig-edit-area').forEach(el => el.style.display = 'none');
document.getElementById(this.value + '-signature').style.display = 'block';
});
});
// ----- 手写签名Canvas -----
const canvas = document.getElementById('sig-canvas');
const ctx = canvas.getContext('2d');
let drawing = false;
canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDraw);
canvas.addEventListener('mouseout', stopDraw);
// 触屏支持
canvas.addEventListener('touchstart', e => { e.preventDefault(); startDraw(e); });
canvas.addEventListener('touchmove', e => { e.preventDefault(); draw(e); });
canvas.addEventListener('touchend', stopDraw);
function startDraw(e) {
drawing = true;
ctx.beginPath();
const pos = getPos(e);
ctx.moveTo(pos.x, pos.y);
}
function draw(e) {
if (!drawing) return;
const pos = getPos(e);
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.strokeStyle = '#000';
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
}
function stopDraw() { drawing = false; }
function getPos(e) {
const rect = canvas.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return { x: clientX - rect.left, y: clientY - rect.top };
}
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
// ----- 保存签名到后端 -----
function saveSignature() {
const type = document.querySelector('input[name="sig_type"]:checked').value;
let data = '';
if (type === 'text') {
data = document.getElementById('sig-text').value;
if (!data.trim()) { alert('请输入签名文本'); return; }
} else if (type === 'draw') {
if (isCanvasBlank(canvas)) { alert('请先绘制签名'); return; }
data = canvas.toDataURL('image/png'); // Base64
} else if (type === 'image') {
const fileInput = document.getElementById('sig-image');
if (!fileInput.files[0]) { alert('请选择签名图片'); return; }
// 使用FormData上传
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('type', 'image');
formData.append('data', '');
uploadViaFormData(formData);
return;
}
// 文本或手写签名使用JSON提交
fetch('save_signature.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `type=${type}&data=${encodeURIComponent(data)}`
})
.then(res => res.json())
.then(result => {
if (result.success) {
alert('签名保存成功');
updatePreview(type, data);
} else {
alert('保存失败:' + result.error);
}
});
}
function uploadViaFormData(formData) {
formData.append('type', 'image');
formData.append('data', '');
fetch('save_signature.php', { method: 'POST', body: formData })
.then(res => res.json())
.then(result => {
if (result.success) {
alert('签名图片上传成功');
updatePreview('image', result.data);
} else {
alert('上传失败:' + result.error);
}
});
}
function isCanvasBlank(canvas) {
const pixelData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
return !pixelData.some(channel => channel !== 0);
}
// ----- 预览签名 -----
function updatePreview(type, data) {
const display = document.getElementById('sig-display');
if (type === 'text') {
display.innerHTML = `<span style="font-family: '华文行楷', cursive; font-size: 24px;">${data}</span>`;
} else if (type === 'draw' || type === 'image') {
display.innerHTML = `<img src="${type === 'draw' ? data : 'uploads/' + data}" style="max-height:100px;">`;
}
}
// 从数据库加载已有签名(页面初始化时调用)
function loadExistingSignature() {
fetch('get_signature.php')
.then(res => res.json())
.then(data => {
if (data && data.signature_data) {
document.querySelector(`input[value="${data.signature_type}"]`).checked = true;
// 触发切换显示
document.querySelector(`input[value="${data.signature_type}"]`).dispatchEvent(new Event('change'));
updatePreview(data.signature_type, data.signature_data);
}
});
}
loadExistingSignature();
安全与优化建议
- 输入验证:文本签名限制长度,过滤XSS(如
htmlspecialchars) - 图片处理:
- 验证文件类型(MIME + 扩展名)
- 限制文件大小(建议最大2MB)
- 使用
getimagesize()确认图片合法性 - 对上传图片重命名防路径穿越
- 存储优化:
- 手写签名Base64数据较长(约10-50KB),建议存为文件而非数据库
- 可定期清理未使用的签名图片
- 权限控制:确保用户只能修改自己的签名
- 图片缩放:前端Canvas可设置最大尺寸,避免生成超大图片
扩展功能
- 签名水印:在手写签名Canvas上叠加用户名、日期
- 字体选择器:文本签名支持多种字体(引入Google Fonts)
- 坐标存储:手写签名存储为坐标数组,可无损缩放和重绘
- 签名验证:在重要操作前要求用户重新绘制签名确认身份
这个方案覆盖了主流的用户签名需求,你可以根据项目复杂度选择实现全部或部分功能。