更新记录
1.0.0(2025-08-19) 下载此版本
技术文档 - 手写签名组件
1. 项目架构设计
1.1 整体架构
本项目采用分层架构设计,基于 uni-app 框架构建跨平台手写签名组件。
graph TD
A[用户交互层] --> B[组件层]
B --> C[业务逻辑层]
C --> D[工具服务层]
D --> E[API适配层]
E --> F[UniApp API层]
subgraph "用户交互层"
A1[index.vue - 测试页面]
A2[触摸/鼠标事件]
end
subgraph "组件层"
B1[sign.vue - 核心签名组件]
B2[Canvas绘制区域]
end
subgraph "业务逻辑层"
C1[绘制算法]
C2[历史记录管理]
C3[配置管理]
C4[状态管理]
end
subgraph "工具服务层"
D1[canvasUtils.js]
D2[messageUtils.js]
D3[uploadManager.js]
D4[mathUtils.js]
D5[validationUtils.js]
D6[canvasAdapter.js]
D7[eventManager.js]
D8[drawingEngine.js]
end
subgraph "API适配层"
E1[平台检测]
E2[Canvas适配]
E3[事件适配]
E4[文件操作适配]
end
subgraph "UniApp API层"
F1[uni.createCanvasContext]
F2[uni.canvasToTempFilePath]
F3[uni.saveImageToPhotosAlbum]
F4[uni.showToast]
F5[uni.uploadFile]
end
1.2 技术选型
技术栈 | 选择 | 原因 |
---|---|---|
框架 | uni-app | 跨平台支持,一套代码多端运行 |
构建工具 | Vite | 快速构建,热更新支持 |
语言 | JavaScript (ES6+) | 兼容性好,开发效率高 |
样式 | CSS + uni-app样式 | 原生性能,平台适配 |
Canvas API | uni-app Canvas | 统一的跨平台Canvas接口 |
状态管理 | 组件内部状态 | 轻量级,符合组件化设计 |
1.3 设计原则
- 跨平台一致性: 统一使用 uni-app API,避免平台差异
- 模块化设计: 功能拆分为独立模块,便于维护和测试
- 配置驱动: 通过配置参数控制组件行为
- 事件驱动: 基于事件机制实现组件通信
- 错误容错: 完善的错误处理和降级机制
2. 核心模块实现
2.1 签名组件 (sign.vue)
2.1.1 组件结构
// 核心数据结构
data() {
return {
ctx: null, // Canvas上下文
canvasName: 'handWriting', // Canvas标识
clientHeight: 0, // 客户端高度
clientWidth: 0, // 客户端宽度
startX: 0, // 起始X坐标
startY: 0, // 起始Y坐标
selectColor: '#1A1A1A', // 选中颜色
selectBgColor: 'transparent', // 背景颜色
lineColor: '#1A1A1A', // 线条颜色
bgColor: 'transparent', // 背景色
startPoint: {}, // 起始点
history: [], // 历史记录
currentHistoryStep: 0, // 当前历史步骤
isDrawing: false, // 是否正在绘制
lastPoint: null, // 上一个点
currentStroke: [] // 当前笔画
}
}
2.1.2 生命周期管理
mounted() {
// 初始化Canvas
this.initCanvas()
// 合并配置
this.mergeConfig()
// 检查Canvas上下文
this.checkCanvasContext()
}
2.2 绘制引擎 (drawingEngine.js)
2.2.1 绘制算法
/**
* 平滑线条绘制算法
* 使用贝塞尔曲线实现平滑效果
*/
class SmoothDrawing {
constructor(ctx, config) {
this.ctx = ctx
this.config = config
this.points = []
}
// 添加点并绘制
addPoint(point) {
this.points.push(point)
if (this.points.length >= 3) {
this.drawSmoothLine()
}
}
// 绘制平滑线条
drawSmoothLine() {
const len = this.points.length
if (len < 3) return
const lastTwoPoints = this.points.slice(-3)
const [p0, p1, p2] = lastTwoPoints
// 计算控制点
const cp1x = p0.x + (p1.x - p0.x) * 0.5
const cp1y = p0.y + (p1.y - p0.y) * 0.5
const cp2x = p1.x + (p2.x - p1.x) * 0.5
const cp2y = p1.y + (p2.y - p1.y) * 0.5
// 绘制贝塞尔曲线
this.ctx.beginPath()
this.ctx.moveTo(cp1x, cp1y)
this.ctx.quadraticCurveTo(p1.x, p1.y, cp2x, cp2y)
this.ctx.stroke()
}
}
2.2.2 线条宽度算法
/**
* 根据绘制速度动态调整线条宽度
*/
getLineWidth(speed) {
const { minWidth, maxWidth, minSpeed } = this.config
// 速度越快,线条越细
let lineWidth = Math.max(
minWidth,
maxWidth - (speed - minSpeed) * 0.1
)
return Math.min(lineWidth, maxWidth)
}
2.3 Canvas适配器 (canvasAdapter.js)
2.3.1 平台检测
/**
* 检测当前运行平台
*/
function detectPlatform() {
try {
const systemInfo = uni.getSystemInfoSync()
const { platform, appName } = systemInfo
if (platform === 'devtools') {
return 'devtools'
} else if (appName && appName.includes('微信')) {
return 'mp-weixin'
} else if (appName && appName.includes('支付宝')) {
return 'mp-alipay'
} else if (platform === 'ios' || platform === 'android') {
return 'app'
} else {
return 'h5'
}
} catch (error) {
console.error('平台检测失败:', error)
return 'unknown'
}
}
2.3.2 Canvas初始化适配
/**
* 跨平台Canvas初始化
*/
function initCanvas(canvasId, component) {
const platform = detectPlatform()
switch (platform) {
case 'h5':
return initH5Canvas(canvasId)
case 'mp-weixin':
case 'mp-alipay':
case 'mp-baidu':
return initMpCanvas(canvasId, component)
case 'app':
return initAppCanvas(canvasId, component)
default:
return uni.createCanvasContext(canvasId, component)
}
}
2.4 事件管理器 (eventManager.js)
2.4.1 事件统一处理
/**
* 统一的事件处理器
*/
class EventManager {
constructor(canvas, options = {}) {
this.canvas = canvas
this.options = options
this.handlers = new Map()
this.init()
}
init() {
const platform = detectPlatform()
if (platform === 'h5') {
this.initH5Events()
} else {
this.initMpEvents()
}
}
// H5事件初始化
initH5Events() {
this.canvas.addEventListener('mousedown', this.handleStart.bind(this))
this.canvas.addEventListener('mousemove', this.handleMove.bind(this))
this.canvas.addEventListener('mouseup', this.handleEnd.bind(this))
// 触摸事件
this.canvas.addEventListener('touchstart', this.handleStart.bind(this))
this.canvas.addEventListener('touchmove', this.handleMove.bind(this))
this.canvas.addEventListener('touchend', this.handleEnd.bind(this))
}
// 小程序事件初始化
initMpEvents() {
// 小程序通过组件模板绑定事件
// @touchstart="handleTouchStart"
// @touchmove="handleTouchMove"
// @touchend="handleTouchEnd"
}
}
3. Canvas绘制原理
3.1 绘制流程
sequenceDiagram
participant User as 用户
participant Event as 事件管理器
participant Engine as 绘制引擎
participant Canvas as Canvas上下文
participant History as 历史管理
User->>Event: 触摸开始
Event->>Engine: 处理起始点
Engine->>Canvas: 设置绘制样式
Engine->>History: 保存当前状态
User->>Event: 触摸移动
Event->>Engine: 处理移动点
Engine->>Engine: 计算线条宽度
Engine->>Canvas: 绘制线条
User->>Event: 触摸结束
Event->>Engine: 结束绘制
Engine->>History: 添加历史记录
Engine->>Canvas: 提交绘制
3.2 平滑绘制算法
3.2.1 贝塞尔曲线平滑
/**
* 使用二次贝塞尔曲线实现平滑绘制
*/
drawSmoothLine(points) {
if (points.length < 3) return
this.ctx.beginPath()
this.ctx.moveTo(points[0].x, points[0].y)
for (let i = 1; i < points.length - 1; i++) {
const currentPoint = points[i]
const nextPoint = points[i + 1]
// 计算控制点
const controlX = (currentPoint.x + nextPoint.x) / 2
const controlY = (currentPoint.y + nextPoint.y) / 2
// 绘制二次贝塞尔曲线
this.ctx.quadraticCurveTo(
currentPoint.x, currentPoint.y,
controlX, controlY
)
}
this.ctx.stroke()
}
3.2.2 压感模拟
/**
* 根据绘制速度模拟压感效果
*/
calculatePressure(currentPoint, lastPoint, timestamp) {
if (!lastPoint) return 1
// 计算距离和时间差
const distance = Math.sqrt(
Math.pow(currentPoint.x - lastPoint.x, 2) +
Math.pow(currentPoint.y - lastPoint.y, 2)
)
const timeDiff = timestamp - lastPoint.timestamp
const speed = distance / timeDiff
// 速度越快,压感越小
const pressure = Math.max(0.1, Math.min(1, 1 - speed * 0.01))
return pressure
}
3.3 历史记录管理
/**
* 历史记录管理器
*/
class HistoryManager {
constructor(maxLength = 20) {
this.history = []
this.currentStep = -1
this.maxLength = maxLength
}
// 添加历史记录
push(imageData) {
// 删除当前步骤之后的记录
this.history = this.history.slice(0, this.currentStep + 1)
// 添加新记录
this.history.push(imageData)
this.currentStep++
// 限制历史记录长度
if (this.history.length > this.maxLength) {
this.history.shift()
this.currentStep--
}
}
// 撤销操作
undo() {
if (this.currentStep > 0) {
this.currentStep--
return this.history[this.currentStep]
}
return null
}
// 重做操作
redo() {
if (this.currentStep < this.history.length - 1) {
this.currentStep++
return this.history[this.currentStep]
}
return null
}
}
4. 多平台兼容性方案
4.1 API统一化策略
4.1.1 Canvas API统一
// 统一的Canvas操作接口
class UnifiedCanvas {
constructor(canvasId, component) {
this.ctx = uni.createCanvasContext(canvasId, component)
this.canvasId = canvasId
this.component = component
}
// 统一的样式设置
setStyle(options) {
const { strokeStyle, lineWidth, lineCap, lineJoin } = options
this.ctx.setStrokeStyle(strokeStyle)
this.ctx.setLineWidth(lineWidth)
this.ctx.setLineCap(lineCap)
this.ctx.setLineJoin(lineJoin)
}
// 统一的绘制方法
drawLine(startPoint, endPoint) {
this.ctx.beginPath()
this.ctx.moveTo(startPoint.x, startPoint.y)
this.ctx.lineTo(endPoint.x, endPoint.y)
this.ctx.stroke()
this.ctx.draw(true)
}
// 统一的图片导出
async toTempFilePath(options = {}) {
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvasId: this.canvasId,
...options
}, this.component)
.then(resolve)
.catch(reject)
})
}
}
4.1.2 事件处理统一
// 统一的事件处理
function normalizeEvent(event) {
const platform = detectPlatform()
if (platform === 'h5') {
// H5事件处理
return {
x: event.offsetX || event.layerX,
y: event.offsetY || event.layerY,
type: event.type,
timestamp: Date.now()
}
} else {
// 小程序事件处理
const touch = event.touches[0] || event.changedTouches[0]
return {
x: touch.x,
y: touch.y,
type: event.type,
timestamp: Date.now()
}
}
}
4.2 平台差异处理
4.2.1 文件保存差异
/**
* 跨平台文件保存
*/
async function saveImage(tempFilePath) {
const platform = detectPlatform()
try {
switch (platform) {
case 'h5':
// H5下载文件
return await downloadFile(tempFilePath)
case 'mp-weixin':
case 'mp-alipay':
case 'mp-baidu':
// 小程序保存到相册
return await saveToPhotosAlbum(tempFilePath)
case 'app':
// App保存到相册
return await saveToPhotosAlbum(tempFilePath)
default:
throw new Error('不支持的平台')
}
} catch (error) {
console.error('保存失败:', error)
throw error
}
}
// H5文件下载
function downloadFile(dataUrl) {
const link = document.createElement('a')
link.download = `signature_${Date.now()}.png`
link.href = dataUrl
document.body(link)
link.click()
document.body.removeChild(link)
}
// 保存到相册
function saveToPhotosAlbum(tempFilePath) {
return uni.saveImageToPhotosAlbum({
filePath: tempFilePath
})
}
4.2.2 权限处理
/**
* 权限检查和申请
*/
async function checkPermission(scope) {
return new Promise((resolve) => {
uni.getSetting({
success: (res) => {
if (res.authSetting[scope]) {
resolve(true)
} else {
uni.authorize({
scope,
success: () => resolve(true),
fail: () => resolve(false)
})
}
},
fail: () => resolve(false)
})
})
}
5. 上传管理器设计
5.1 上传管理器架构
/**
* 文件上传管理器
*/
class UploadManager {
constructor(config = {}) {
this.config = {
timeout: 30000,
maxRetries: 3,
retryDelay: 1000,
...config
}
this.uploadQueue = []
this.isUploading = false
}
// 添加上传任务
addTask(file, options = {}) {
const task = {
id: this.generateId(),
file,
options: { ...this.config, ...options },
status: 'pending',
progress: 0,
retryCount: 0,
error: null
}
this.uploadQueue.push(task)
this.processQueue()
return task.id
}
// 处理上传队列
async processQueue() {
if (this.isUploading) return
const pendingTask = this.uploadQueue.find(task => task.status === 'pending')
if (!pendingTask) return
this.isUploading = true
await this.uploadTask(pendingTask)
this.isUploading = false
// 继续处理队列
this.processQueue()
}
// 上传单个任务
async uploadTask(task) {
task.status = 'uploading'
try {
const result = await this.performUpload(task)
task.status = 'completed'
task.result = result
this.onTaskComplete(task)
} catch (error) {
await this.handleUploadError(task, error)
}
}
// 执行上传
performUpload(task) {
return new Promise((resolve, reject) => {
const uploadTask = uni.uploadFile({
url: task.options.url,
filePath: task.file,
name: task.options.name || 'file',
header: task.options.header || {},
formData: task.options.formData || {},
timeout: task.options.timeout,
success: (res) => {
if (res.statusCode === 200) {
resolve(res)
} else {
reject(new Error(`上传失败: ${res.statusCode}`))
}
},
fail: reject
})
// 监听上传进度
uploadTask.onProgressUpdate((res) => {
task.progress = res.progress
this.onTaskProgress(task)
})
})
}
// 处理上传错误
async handleUploadError(task, error) {
task.error = error
task.retryCount++
if (task.retryCount < task.options.maxRetries) {
// 重试
task.status = 'pending'
await this.delay(task.options.retryDelay)
} else {
// 失败
task.status = 'failed'
this.onTaskFailed(task)
}
}
// 延迟函数
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// 生成任务ID
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
// 事件回调
onTaskProgress(task) {
this.emit('progress', task)
}
onTaskComplete(task) {
this.emit('complete', task)
}
onTaskFailed(task) {
this.emit('failed', task)
}
// 简单的事件发射器
emit(event, data) {
if (this.listeners && this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data))
}
}
// 事件监听
on(event, callback) {
if (!this.listeners) this.listeners = {}
if (!this.listeners[event]) this.listeners[event] = []
this.listeners[event].push(callback)
}
}
5.2 错误处理机制
/**
* 错误处理策略
*/
class ErrorHandler {
static handle(error, context = '') {
const errorInfo = {
message: error.message || '未知错误',
stack: error.stack,
context,
timestamp: new Date().toISOString(),
platform: detectPlatform()
}
// 记录错误日志
console.error('错误详情:', errorInfo)
// 根据错误类型进行处理
switch (error.name) {
case 'NetworkError':
return this.handleNetworkError(error)
case 'PermissionError':
return this.handlePermissionError(error)
case 'CanvasError':
return this.handleCanvasError(error)
default:
return this.handleGenericError(error)
}
}
static handleNetworkError(error) {
uni.showToast({
title: '网络连接失败,请检查网络设置',
icon: 'none'
})
}
static handlePermissionError(error) {
uni.showModal({
title: '权限不足',
content: '需要相册访问权限才能保存图片,请在设置中开启权限',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
uni.openSetting()
}
}
})
}
static handleCanvasError(error) {
uni.showToast({
title: 'Canvas操作失败,请重试',
icon: 'none'
})
}
static handleGenericError(error) {
uni.showToast({
title: error.message || '操作失败,请重试',
icon: 'none'
})
}
}
6. 工具类库说明
6.1 Canvas工具类 (canvasUtils.js)
/**
* Canvas相关工具函数
*/
export const canvasUtils = {
// 获取Canvas尺寸
getCanvasSize(canvasId, component) {
return new Promise((resolve) => {
uni.createSelectorQuery()
.in(component)
.select(`#${canvasId}`)
.boundingClientRect((rect) => {
resolve({
width: rect.width,
height: rect.height
})
})
.exec()
})
},
// 检查Canvas是否就绪
isCanvasReady(ctx) {
return ctx && typeof ctx.draw === 'function'
},
// 清空Canvas区域
clearRect(ctx, x, y, width, height) {
ctx.clearRect(x, y, width, height)
ctx.draw()
},
// 设置Canvas样式
setCanvasStyle(ctx, style) {
const {
strokeStyle,
fillStyle,
lineWidth,
lineCap,
lineJoin,
globalAlpha
} = style
if (strokeStyle) ctx.setStrokeStyle(strokeStyle)
if (fillStyle) ctx.setFillStyle(fillStyle)
if (lineWidth) ctx.setLineWidth(lineWidth)
if (lineCap) ctx.setLineCap(lineCap)
if (lineJoin) ctx.setLineJoin(lineJoin)
if (globalAlpha) ctx.setGlobalAlpha(globalAlpha)
},
// 坐标转换
transformCoordinate(point, scale = 1, offset = { x: 0, y: 0 }) {
return {
x: (point.x - offset.x) / scale,
y: (point.y - offset.y) / scale
}
},
// 获取默认Canvas尺寸
getDefaultSize() {
const systemInfo = uni.getSystemInfoSync()
return {
width: Math.min(systemInfo.windowWidth * 0.9, 400),
height: Math.min(systemInfo.windowHeight * 0.4, 300)
}
},
// Canvas转临时文件
canvasToTempFile(canvasId, component, options = {}) {
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvasId,
...options
}, component)
.then(resolve)
.catch(reject)
})
}
}
6.2 数学工具类 (mathUtils.js)
/**
* 数学计算工具函数
*/
export const mathUtils = {
// 计算两点间距离
distance(p1, p2) {
return Math.sqrt(
Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)
)
},
// 计算两点间角度
angle(p1, p2) {
return Math.atan2(p2.y - p1.y, p2.x - p1.x)
},
// 线性插值
lerp(start, end, t) {
return start + (end - start) * t
},
// 贝塞尔曲线插值
bezier(p0, p1, p2, t) {
const u = 1 - t
return {
x: u * u * p0.x + 2 * u * t * p1.x + t * t * p2.x,
y: u * u * p0.y + 2 * u * t * p1.y + t * t * p2.y
}
},
// 限制数值范围
clamp(value, min, max) {
return Math.min(Math.max(value, min), max)
},
// 平滑步进函数
smoothstep(edge0, edge1, x) {
const t = this.clamp((x - edge0) / (edge1 - edge0), 0, 1)
return t * t * (3 - 2 * t)
},
// 计算点到线段的距离
pointToLineDistance(point, lineStart, lineEnd) {
const A = point.x - lineStart.x
const B = point.y - lineStart.y
const C = lineEnd.x - lineStart.x
const D = lineEnd.y - lineStart.y
const dot = A * C + B * D
const lenSq = C * C + D * D
if (lenSq === 0) {
return this.distance(point, lineStart)
}
let param = dot / lenSq
let xx, yy
if (param < 0) {
xx = lineStart.x
yy = lineStart.y
} else if (param > 1) {
xx = lineEnd.x
yy = lineEnd.y
} else {
xx = lineStart.x + param * C
yy = lineStart.y + param * D
}
return this.distance(point, { x: xx, y: yy })
}
}
6.3 验证工具类 (validationUtils.js)
/**
* 数据验证工具函数
*/
export const validationUtils = {
// 验证配置对象
validateConfig(config) {
const errors = []
if (config.canvasWidth && (typeof config.canvasWidth !== 'number' || config.canvasWidth <= 0)) {
errors.push('canvasWidth必须是正数')
}
if (config.canvasHeight && (typeof config.canvasHeight !== 'number' || config.canvasHeight <= 0)) {
errors.push('canvasHeight必须是正数')
}
if (config.lineColor && !this.isValidColor(config.lineColor)) {
errors.push('lineColor必须是有效的颜色值')
}
if (config.bgColor && !this.isValidColor(config.bgColor)) {
errors.push('bgColor必须是有效的颜色值')
}
if (config.minWidth && (typeof config.minWidth !== 'number' || config.minWidth <= 0)) {
errors.push('minWidth必须是正数')
}
if (config.maxWidth && (typeof config.maxWidth !== 'number' || config.maxWidth <= 0)) {
errors.push('maxWidth必须是正数')
}
if (config.minWidth && config.maxWidth && config.minWidth > config.maxWidth) {
errors.push('minWidth不能大于maxWidth')
}
return {
isValid: errors.length === 0,
errors
}
},
// 验证颜色值
isValidColor(color) {
if (typeof color !== 'string') return false
// 支持的颜色格式
const colorRegex = /^(#[0-9A-Fa-f]{3,8}|rgb\(|rgba\(|hsl\(|hsla\(|\w+)$/
return colorRegex.test(color)
},
// 验证URL
isValidUrl(url) {
if (typeof url !== 'string') return false
try {
new URL(url)
return true
} catch {
return false
}
},
// 验证文件路径
isValidFilePath(filePath) {
if (typeof filePath !== 'string') return false
return filePath.length > 0 && !filePath.includes('..')
},
// 验证坐标点
isValidPoint(point) {
return (
point &&
typeof point === 'object' &&
typeof point.x === 'number' &&
typeof point.y === 'number' &&
!isNaN(point.x) &&
!isNaN(point.y)
)
},
// 验证Canvas上下文
isValidCanvasContext(ctx) {
return (
ctx &&
typeof ctx === 'object' &&
typeof ctx.draw === 'function' &&
typeof ctx.beginPath === 'function'
)
}
}
7. 性能优化策略
7.1 绘制性能优化
7.1.1 批量绘制
/**
* 批量绘制优化
*/
class BatchRenderer {
constructor(ctx, batchSize = 10) {
this.ctx = ctx
this.batchSize = batchSize
this.drawQueue = []
this.isDrawing = false
}
// 添加绘制命令
addDrawCommand(command) {
this.drawQueue.push(command)
if (this.drawQueue.length >= this.batchSize) {
this.flush()
}
}
// 执行批量绘制
flush() {
if (this.isDrawing || this.drawQueue.length === 0) return
this.isDrawing = true
// 执行所有绘制命令
this.drawQueue.forEach(command => {
command(this.ctx)
})
// 一次性提交
this.ctx.draw(true)
// 清空队列
this.drawQueue = []
this.isDrawing = false
}
// 强制刷新
forceFlush() {
this.flush()
}
}
7.1.2 节流优化
/**
* 绘制事件节流
*/
function throttle(func, limit) {
let inThrottle
return function() {
const args = arguments
const context = this
if (!inThrottle) {
func.apply(context, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
// 使用节流优化触摸移动事件
const throttledMove = throttle(function(event) {
this.handleTouchMove(event)
}, 16) // 约60fps
7.2 内存优化
7.2.1 历史记录优化
/**
* 内存友好的历史记录管理
*/
class MemoryEfficientHistory {
constructor(maxSize = 20, compressionRatio = 0.5) {
this.maxSize = maxSize
this.compressionRatio = compressionRatio
this.history = []
this.compressedHistory = []
}
// 添加历史记录
push(imageData) {
this.history.push(imageData)
// 超出限制时压缩旧记录
if (this.history.length > this.maxSize) {
const oldRecord = this.history.shift()
this.compressAndStore(oldRecord)
}
}
// 压缩并存储
compressAndStore(imageData) {
// 简单的压缩策略:降低分辨率
const compressed = this.compressImageData(imageData, this.compressionRatio)
this.compressedHistory.push(compressed)
// 限制压缩历史的大小
if (this.compressedHistory.length > this.maxSize) {
this.compressedHistory.shift()
}
}
// 压缩图像数据
compressImageData(imageData, ratio) {
// 实现图像压缩逻辑
// 这里简化处理,实际可以使用Canvas缩放
return {
...imageData,
compressed: true,
ratio
}
}
}
7.2.2 对象池优化
/**
* 点对象池
*/
class PointPool {
constructor(initialSize = 100) {
this.pool = []
this.used = new Set()
// 预创建对象
for (let i = 0; i < initialSize; i++) {
this.pool.push({ x: 0, y: 0, timestamp: 0 })
}
}
// 获取点对象
acquire(x, y, timestamp) {
let point = this.pool.pop()
if (!point) {
point = { x: 0, y: 0, timestamp: 0 }
}
point.x = x
point.y = y
point.timestamp = timestamp
this.used.add(point)
return point
}
// 释放点对象
release(point) {
if (this.used.has(point)) {
this.used.delete(point)
this.pool.push(point)
}
}
// 批量释放
releaseAll(points) {
points.forEach(point => this.release(point))
}
}
7.3 渲染优化
7.3.1 离屏Canvas
/**
* 离屏Canvas优化
*/
class OffscreenCanvasManager {
constructor(width, height) {
this.width = width
this.height = height
this.offscreenCanvas = null
this.offscreenCtx = null
this.init()
}
init() {
// 创建离屏Canvas
if (typeof OffscreenCanvas !== 'undefined') {
this.offscreenCanvas = new OffscreenCanvas(this.width, this.height)
this.offscreenCtx = this.offscreenCanvas.getContext('2d')
}
}
// 在离屏Canvas上绘制
drawOffscreen(drawFunction) {
if (this.offscreenCtx) {
drawFunction(this.offscreenCtx)
}
}
// 将离屏Canvas内容复制到主Canvas
copyToMainCanvas(mainCtx) {
if (this.offscreenCanvas) {
mainCtx.drawImage(this.offscreenCanvas, 0, 0)
}
}
}
8. 扩展开发指南
8.1 自定义绘制工具
/**
* 自定义绘制工具基类
*/
class DrawingTool {
constructor(name, config = {}) {
this.name = name
this.config = config
this.isActive = false
}
// 激活工具
activate() {
this.isActive = true
this.onActivate()
}
// 停用工具
deactivate() {
this.isActive = false
this.onDeactivate()
}
// 处理开始事件
handleStart(point, ctx) {
if (!this.isActive) return
this.onStart(point, ctx)
}
// 处理移动事件
handleMove(point, ctx) {
if (!this.isActive) return
this.onMove(point, ctx)
}
// 处理结束事件
handleEnd(point, ctx) {
if (!this.isActive) return
this.onEnd(point, ctx)
}
// 子类需要实现的方法
() {}
() {}
onStart(point, ctx) {}
onMove(point, ctx) {}
onEnd(point, ctx) {}
}
/**
* 画笔工具示例
*/
class BrushTool extends DrawingTool {
constructor(config) {
super('brush', config)
this.currentStroke = []
}
onStart(point, ctx) {
this.currentStroke = [point]
ctx.beginPath()
ctx.moveTo(point.x, point.y)
}
onMove(point, ctx) {
this.currentStroke.push(point)
ctx.lineTo(point.x, point.y)
ctx.stroke()
}
onEnd(point, ctx) {
this.currentStroke.push(point)
ctx.draw(true)
this.currentStroke = []
}
}
/**
* 橡皮擦工具示例
*/
class EraserTool extends DrawingTool {
constructor(config) {
super('eraser', config)
this.eraserSize = config.size || 20
}
onStart(point, ctx) {
this.erase(point, ctx)
}
onMove(point, ctx) {
this.erase(point, ctx)
}
erase(point, ctx) {
ctx.clearRect(
point.x - this.eraserSize / 2,
point.y - this.eraserSize / 2,
this.eraserSize,
this.eraserSize
)
ctx.draw(true)
}
}
8.2 插件系统
/**
* 插件管理器
*/
class PluginManager {
constructor() {
this.plugins = new Map()
this.hooks = new Map()
}
// 注册插件
register(plugin) {
if (this.plugins.has(plugin.name)) {
throw new Error(`插件 ${plugin.name} 已存在`)
}
this.plugins.set(plugin.name, plugin)
// 注册插件的钩子
if (plugin.hooks) {
Object.keys(plugin.hooks).forEach(hookName => {
this.addHook(hookName, plugin.hooks[hookName])
})
}
// 初始化插件
if (plugin.init) {
plugin.init()
}
}
// 卸载插件
unregister(pluginName) {
const plugin = this.plugins.get(pluginName)
if (!plugin) return
// 清理插件的钩子
if (plugin.hooks) {
Object.keys(plugin.hooks).forEach(hookName => {
this.removeHook(hookName, plugin.hooks[hookName])
})
}
// 销毁插件
if (plugin.destroy) {
plugin.destroy()
}
this.plugins.delete(pluginName)
}
// 添加钩子
addHook(hookName, callback) {
if (!this.hooks.has(hookName)) {
this.hooks.set(hookName, [])
}
this.hooks.get(hookName).push(callback)
}
// 移除钩子
removeHook(hookName, callback) {
const hooks = this.hooks.get(hookName)
if (hooks) {
const index = hooks.indexOf(callback)
if (index > -1) {
hooks.splice(index, 1)
}
}
}
// 执行钩子
executeHook(hookName, ...args) {
const hooks = this.hooks.get(hookName)
if (hooks) {
hooks.forEach(callback => {
try {
callback(...args)
} catch (error) {
console.error(`钩子 ${hookName} 执行失败:`, error)
}
})
}
}
}
/**
* 插件示例:自动保存
*/
class AutoSavePlugin {
constructor(options = {}) {
this.name = 'autoSave'
this.interval = options.interval || 30000 // 30秒
this.timer = null
this.hooks = {
'drawing-end': this.onDrawingEnd.bind(this)
}
}
init() {
console.log('自动保存插件已启用')
}
destroy() {
if (this.timer) {
clearTimeout(this.timer)
}
console.log('自动保存插件已禁用')
}
onDrawingEnd() {
// 重置自动保存计时器
if (this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
this.autoSave()
}, this.interval)
}
autoSave() {
console.log('执行自动保存')
// 实现自动保存逻辑
}
}
8.3 主题系统
/**
* 主题管理器
*/
class ThemeManager {
constructor() {
this.themes = new Map()
this.currentTheme = null
this.loadDefaultThemes()
}
// 加载默认主题
loadDefaultThemes() {
// 默认主题
this.register('default', {
name: '默认',
colors: {
primary: '#1A1A1A',
background: '#FFFFFF',
canvas: 'transparent',
border: '#E0E0E0'
},
styles: {
lineWidth: 2,
lineCap: 'round',
lineJoin: 'round'
}
})
// 深色主题
this.register('dark', {
name: '深色',
colors: {
primary: '#FFFFFF',
background: '#1A1A1A',
canvas: '#2A2A2A',
border: '#404040'
},
styles: {
lineWidth: 2,
lineCap: 'round',
lineJoin: 'round'
}
})
}
// 注册主题
register(id, theme) {
this.themes.set(id, theme)
}
// 应用主题
apply(themeId) {
const theme = this.themes.get(themeId)
if (!theme) {
throw new Error(`主题 ${themeId} 不存在`)
}
this.currentTheme = theme
this.applyThemeStyles(theme)
// 触发主题变更事件
this.onThemeChange(theme)
}
// 应用主题样式
applyThemeStyles(theme) {
// 更新CSS变量
const root = document.documentElement
if (root) {
Object.keys(theme.colors).forEach(key => {
root.style.setProperty(`--theme-${key}`, theme.colors[key])
})
}
}
// 获取当前主题
getCurrentTheme() {
return this.currentTheme
}
// 获取所有主题
getAllThemes() {
return Array.from(this.themes.values())
}
// 主题变更回调
onThemeChange(theme) {
console.log('主题已切换:', theme.name)
}
}
9. 技术债务和未来优化
9.1 当前技术债务
9.1.1 代码层面
- 历史遗留代码: 部分兼容性处理代码可以进一步简化
- 类型定义缺失: 缺少TypeScript类型定义,影响开发体验
- 测试覆盖不足: 单元测试和集成测试覆盖率有待提升
- 文档不完整: 部分API文档需要补充和完善
9.1.2 性能层面
- 内存优化: 长时间使用可能存在内存泄漏风险
- 渲染优化: 复杂绘制场景下的性能有优化空间
- 包体积: 工具类可以进一步模块化,支持按需加载
9.1.3 功能层面
- 撤销重做: 当前实现较为简单,可以支持更复杂的操作
- 多点触控: 缺少多点触控支持
- 矢量化: 当前基于位图,可以考虑矢量化支持
9.2 未来优化方向
9.2.1 技术升级
// 1. TypeScript重构
interface SignatureConfig {
canvasWidth: number
canvasHeight: number
lineColor: string
bgColor: string
minWidth: number
maxWidth: number
openSmooth: boolean
}
interface DrawingPoint {
x: number
y: number
timestamp: number
pressure?: number
}
class TypedSignatureComponent {
private config: SignatureConfig
private ctx: CanvasRenderingContext2D | null
private history: ImageData[]
constructor(config: SignatureConfig) {
this.config = config
this.ctx = null
this.history = []
}
public drawLine(start: DrawingPoint, end: DrawingPoint): void {
// 类型安全的绘制方法
}
}
9.2.2 架构优化
// 2. 微前端架构
class MicroSignatureApp {
constructor() {
this.modules = new Map()
this.eventBus = new EventBus()
}
// 动态加载模块
async loadModule(moduleName) {
const module = await import(`./modules/${moduleName}.js`)
this.modules.set(moduleName, module.default)
return module.default
}
// 模块通信
sendMessage(from, to, message) {
this.eventBus.emit(`${to}:message`, { from, message })
}
}
9.2.3 性能优化
// 3. WebAssembly优化
class WasmDrawingEngine {
constructor() {
this.wasmModule = null
this.init()
}
async init() {
// 加载WebAssembly模块
this.wasmModule = await WebAssembly.instantiateStreaming(
fetch('./drawing-engine.wasm')
)
}
// 使用WASM进行高性能计算
calculateSmoothPath(points) {
if (this.wasmModule) {
return this.wasmModule.instance.exports.smooth_path(points)
}
return this.fallbackCalculation(points)
}
}
9.2.4 功能扩展
// 4. AI辅助功能
class AIAssistant {
constructor(apiKey) {
this.apiKey = apiKey
this.model = null
}
// 笔迹识别
async recognizeHandwriting(imageData) {
const response = await fetch('/api/ocr', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ image: imageData })
})
return response.json()
}
// 签名美化
async beautifySignature(strokeData) {
// 使用AI模型优化签名外观
return this.model.process(strokeData)
}
}
9.3 迁移计划
9.3.1 短期目标(1-3个月)
- [ ] 完善单元测试覆盖率至80%以上
- [ ] 添加TypeScript类型定义
- [ ] 优化内存使用,修复潜在内存泄漏
- [ ] 完善API文档和使用示例
9.3.2 中期目标(3-6个月)
- [ ] 重构为TypeScript项目
- [ ] 实现插件系统和主题系统
- [ ] 添加多点触控支持
- [ ] 优化渲染性能,支持大尺寸Canvas
9.3.3 长期目标(6-12个月)
- [ ] 支持矢量化绘制
- [ ] 集成AI辅助功能
- [ ] 支持协同编辑
- [ ] 实现云端同步
9.4 风险评估
9.4.1 技术风险
- 平台兼容性: uni-app版本更新可能带来的兼容性问题
- 性能瓶颈: 大量绘制操作可能导致的性能问题
- 内存限制: 移动端内存限制对复杂绘制的影响
9.4.2 业务风险
- 用户体验: 跨平台一致性可能存在差异
- 数据安全: 签名数据的存储和传输安全
- 法律合规: 电子签名的法律效力要求
9.4.3 缓解策略
- 持续集成: 建立自动化测试和部署流程
- 性能监控: 实时监控应用性能指标
- 安全审计: 定期进行安全漏洞扫描
- 用户反馈: 建立用户反馈收集机制
10. 开发环境配置
10.1 环境要求
{
"node": ">=14.0.0",
"npm": ">=6.0.0",
"uni-app": ">=3.0.0",
"vite": ">=2.0.0"
}
10.2 开发工具配置
10.2.1 VSCode配置
// .vscode/settings.json
{
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"eslint.autoFixOnSave": true,
"vetur.format.defaultFormatter.html": "prettier",
"vetur.format.defaultFormatter.js": "prettier",
"vetur.format.defaultFormatter.css": "prettier"
}
10.2.2 ESLint配置
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off'
}
}
10.3 调试配置
10.3.1 Chrome DevTools
// 调试工具函数
window.debugSignature = {
// 显示Canvas信息
showCanvasInfo() {
const canvas = document.querySelector('#handWriting')
console.log('Canvas信息:', {
width: canvas.width,
height: canvas.height,
style: canvas.style.cssText
})
},
// 显示绘制历史
showHistory() {
const component = this.getCurrentComponent()
console.log('绘制历史:', component.history)
},
// 性能监控
startPerformanceMonitor() {
performance.mark('signature-start')
console.log('性能监控已启动')
},
endPerformanceMonitor() {
performance.mark('signature-end')
performance.measure('signature-duration', 'signature-start', 'signature-end')
const measure = performance.getEntriesByName('signature-duration')[0]
console.log('操作耗时:', measure.duration, 'ms')
}
}
11. 部署指南
11.1 构建配置
// vite.config.js
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
export default defineConfig({
plugins: [uni()],
build: {
// 生产环境优化
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
// 代码分割
rollupOptions: {
output: {
manualChunks: {
'canvas-utils': ['./src/utils/canvasUtils.js'],
'drawing-engine': ['./src/utils/drawingEngine.js']
}
}
}
},
// 开发服务器配置
server: {
port: 3000,
host: '0.0.0.0'
}
})
11.2 平台特定配置
11.2.1 微信小程序
// manifest.json - 微信小程序配置
{
"mp-weixin": {
"appid": "your-app-id",
"setting": {
"urlCheck": false,
"es6": true,
"minified": true
},
"permission": {
"scope.writePhotosAlbum": {
"desc": "保存签名图片到相册"
}
}
}
}
11.2.2 H5部署
// nginx.conf
server {
listen 80;
server_name your-domain.com;
location / {
root /var/www/signature-app;
index index.html;
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Gzip压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
12. 总结
本技术文档详细介绍了手写签名组件的技术架构、实现原理和开发指南。项目采用uni-app框架实现跨平台兼容,通过模块化设计确保代码的可维护性和可扩展性。
12.1 技术亮点
- 跨平台统一: 基于uni-app实现一套代码多端运行
- 模块化架构: 清晰的分层设计,便于维护和扩展
- 性能优化: 多种优化策略确保流畅的绘制体验
- 错误处理: 完善的错误处理和降级机制
- 可配置性: 丰富的配置选项满足不同需求
12.2 适用场景
- 电子合同签署
- 快递签收确认
- 会议签到系统
- 医疗病历签名
- 教育培训签名
12.3 持续改进
项目将持续关注技术发展趋势,不断优化性能和用户体验,逐步引入新技术和功能,为用户提供更好的签名解决方案。
文档版本: v1.0.0
最后更新: 2025年
维护团队: 开发团队
平台兼容性
uni-app(4.07)
Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
---|---|---|---|---|---|---|---|---|
√ | √ | √ | √ | √ | √ | √ | √ | √ |
微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 快应用-华为 | 快应用-联盟 |
---|---|---|---|---|---|---|---|---|---|---|
√ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ |
手写签名组件 (uni-app)
一个功能完整、跨平台的手写签名组件,基于 uni-app 开发,支持 H5、小程序等多个平台。提供流畅的手写体验、丰富的配置选项和完善的功能特性。
✨ 功能特性
- 🎨 流畅手写体验 - 支持平滑线条绘制,可调节线条粗细和颜色
- 📱 跨平台支持 - 完美适配 H5、微信小程序、支付宝小程序等平台
- 🎯 丰富配置选项 - 支持画布尺寸、背景色、线条样式等多项配置
- 💾 多种保存方式 - 支持保存到相册、下载到本地、上传到服务器
- 🔄 撤销重做功能 - 支持多步撤销操作,可配置历史记录长度
- 🖼️ 预览功能 - 实时预览签名效果
- 📤 文件上传 - 内置上传管理器,支持重试机制和错误处理
- 🎛️ 灵活API - 提供完整的事件回调和方法调用
🛠️ 技术栈
- 框架: uni-app (Vue 2.x)
- 构建工具: Vite
- 样式: 原生CSS + uni-app样式
- 平台支持: H5、微信小程序、支付宝小程序、App等
- 依赖管理: npm/pnpm
📦 安装和运行
环境要求
- Node.js >= 14.0.0
- npm >= 6.0.0 或 pnpm >= 6.0.0
快速开始
# 克隆项目
git clone <repository-url>
cd sign
# 安装依赖
npm install
# 或使用 pnpm
pnpm install
# 运行开发服务器
# H5 平台
npm run dev:h5
# 微信小程序
npm run dev:mp-weixin
# 支付宝小程序
npm run dev:mp-alipay
# App 平台
npm run dev:app
构建生产版本
# 构建 H5
npm run build:h5
# 构建微信小程序
npm run build:mp-weixin
# 构建支付宝小程序
npm run build:mp-alipay
# 构建 App
npm run build:app
🚀 使用方法
基础用法
<template>
<view class="container">
<sign-component
ref="signRef"
:canvas-width="400"
:canvas-height="300"
:line-color="'#000000'"
:bg-color="'#ffffff'"
@complete="onSignComplete"
/>
</view>
</template>
<script>
import SignComponent from '@/components/sign.vue'
export default {
components: {
SignComponent
},
methods: {
onSignComplete(result) {
if (result.success) {
console.log('签名完成:', result.filePath)
} else {
console.error('签名失败:', result.error)
}
},
// 清空画布
clearCanvas() {
this.$refs.signRef.clearCanvas()
},
// 撤销操作
undoCanvas() {
this.$refs.signRef.undo()
},
// 保存签名
saveSignature() {
this.$refs.signRef.saveCanvasAsImg()
},
// 预览签名
previewSignature() {
this.$refs.signRef.previewCanvasImg()
}
}
}
</script>
📋 API 文档
Props 配置选项
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
canvasWidth |
Number | 300 | 画布宽度(px) |
canvasHeight |
Number | 200 | 画布高度(px) |
lineColor |
String | '#1A1A1A' | 线条颜色 |
bgColor |
String | 'transparent' | 背景颜色 |
minWidth |
Number | 2 | 最小线条宽度 |
maxWidth |
Number | 6 | 最大线条宽度 |
minSpeed |
Number | 1.5 | 影响线条粗细的最小速度 |
maxWidthDiffRate |
Number | 20 | 线条宽度变化率(%) |
maxHistoryLength |
Number | 20 | 最大历史记录长度 |
openSmooth |
Boolean | true | 是否开启平滑绘制 |
uploadUrl |
String | '' | 上传接口地址 |
uploadConfig |
Object | {} | 上传配置选项 |
上传配置 (uploadConfig)
{
method: 'POST', // 请求方法
name: 'file', // 文件字段名
header: {}, // 请求头
formData: {}, // 额外表单数据
timeout: 30000, // 超时时间(ms)
maxRetries: 3, // 最大重试次数
retryDelay: 1000 // 重试延迟(ms)
}
方法 (Methods)
方法名 | 参数 | 返回值 | 说明 |
---|---|---|---|
clearCanvas() |
- | - | 清空画布 |
undo() |
- | Boolean | 撤销上一步操作 |
isEmpty() |
- | Boolean | 检查画布是否为空 |
saveCanvasAsImg() |
- | - | 保存签名图片 |
previewCanvasImg() |
- | - | 预览签名图片 |
complete() |
- | - | 完成签名并上传 |
setLineColor(color) |
String | - | 设置线条颜色 |
事件 (Events)
事件名 | 参数 | 说明 |
---|---|---|
complete |
{success, filePath, error, response} |
签名完成事件 |
upload-progress |
{progress, total} |
上传进度事件 |
canvas-ready |
- | 画布初始化完成 |
drawing-start |
{x, y} |
开始绘制 |
drawing-end |
- | 结束绘制 |
🌐 平台兼容性
平台 | 支持状态 | 特殊说明 |
---|---|---|
H5 | ✅ 完全支持 | 保存功能为下载到本地 |
微信小程序 | ✅ 完全支持 | 可保存到相册,需要用户授权 |
支付宝小程序 | ✅ 完全支持 | 可保存到相册,需要用户授权 |
百度小程序 | ✅ 完全支持 | 可保存到相册,需要用户授权 |
字节小程序 | ✅ 完全支持 | 可保存到相册,需要用户授权 |
QQ小程序 | ✅ 完全支持 | 可保存到相册,需要用户授权 |
App (iOS/Android) | ✅ 完全支持 | 可保存到相册,需要权限 |
平台差异说明
H5 平台
- 保存功能:触发浏览器下载
- 文件格式:PNG
- 权限要求:无
小程序平台
- 保存功能:保存到系统相册
- 文件格式:PNG
- 权限要求:需要用户授权相册权限
- 注意事项:首次保存需要用户手动授权
App 平台
- 保存功能:保存到系统相册
- 文件格式:PNG
- 权限要求:需要相册写入权限
- 注意事项:需要在 manifest.json 中配置相关权限
🔧 高级配置
自定义样式
<template>
<sign-component class="custom-sign" />
</template>
<style>
.custom-sign {
border: 2px solid #007AFF;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
</style>
上传配置示例
<template>
<sign-component
:upload-url="uploadUrl"
:upload-config="uploadConfig"
@complete="handleComplete"
/>
</template>
<script>
export default {
data() {
return {
uploadUrl: 'https://api.example.com/upload',
uploadConfig: {
method: 'POST',
name: 'signature',
header: {
'Authorization': 'Bearer your-token'
},
formData: {
userId: '12345',
type: 'signature'
},
timeout: 60000,
maxRetries: 3,
retryDelay: 2000
}
}
},
methods: {
handleComplete(result) {
if (result.success) {
console.log('上传成功:', result.response)
} else {
console.error('上传失败:', result.error)
}
}
}
}
</script>
❓ 常见问题
Q: H5 平台下 canvas.toDataURL 报错?
A: 这是因为 uni-app H5 环境下的 canvas 不是标准 HTML5 canvas 元素。组件已统一使用 uni.canvasToTempFilePath
API 解决此问题。
Q: 小程序保存到相册失败?
A: 请检查以下几点:
- 确保用户已授权相册权限
- 检查小程序是否有相册访问权限
- 确保画布有内容(非空)
Q: 上传功能不工作?
A: 请检查:
uploadUrl
是否正确配置- 服务器接口是否正常
- 网络连接是否正常
- 上传配置是否正确
Q: 画布显示异常或无法绘制?
A: 请确保:
- 画布尺寸设置合理
- 组件已正确挂载
- 检查控制台是否有错误信息
Q: 如何自定义线条样式?
A: 可以通过以下 props 调整:
lineColor
: 线条颜色minWidth
/maxWidth
: 线条粗细范围openSmooth
: 是否平滑绘制
Q: 如何处理不同屏幕尺寸?
A: 建议使用响应式设计:
// 根据屏幕宽度动态设置画布尺寸
const systemInfo = uni.getSystemInfoSync()
const canvasWidth = systemInfo.windowWidth * 0.9
const canvasHeight = canvasWidth * 0.6
📁 项目结构
sign/
├── src/
│ ├── components/
│ │ └── sign.vue # 主签名组件
│ ├── pages/
│ │ └── index/
│ │ └── index.vue # 测试页面
│ └── utils/ # 工具模块
│ ├── canvasAdapter.js # Canvas适配器
│ ├── canvasUtils.js # Canvas工具函数
│ ├── configManager.js # 配置管理
│ ├── drawingEngine.js # 绘制引擎
│ ├── drawingHistory.js # 绘制历史管理
│ ├── eventManager.js # 事件管理
│ ├── mathUtils.js # 数学计算工具
│ ├── messageUtils.js # 消息提示工具
│ ├── uploadManager.js # 上传管理器
│ └── validationUtils.js # 验证工具
├── dist/ # 构建输出目录
├── docs/ # 文档目录
├── package.json # 项目配置
├── vite.config.js # Vite配置
├── manifest.json # uni-app配置
├── pages.json # 页面配置
└── README.md # 项目说明
🤝 贡献指南
- Fork 本仓库
- 创建特性分支 (
git checkout -b feature/AmazingFeature
) - 提交更改 (
git commit -m 'Add some AmazingFeature'
) - 推送到分支 (
git push origin feature/AmazingFeature
) - 打开 Pull Request
📄 许可证
本项目采用 MIT 许可证 - 查看 LICENSE 文件了解详情。
🆘 技术支持
如果您在使用过程中遇到问题,可以通过以下方式获取帮助:
- 查看本文档的常见问题部分
- 提交 Issue 描述问题
- 查看项目源码和注释
- 参考示例代码
注意: 本组件基于 uni-app 框架开发,使用前请确保您的项目环境支持 uni-app。