场景应用示例
需求分析
签名组件在许多应用场景中都非常有用,比如在线合同签署、电子表格签名等. 如何在网页上实现手写签名模拟功能,主要涉及以下几个方面:
- 创建画布:
- 提供一个可进行手写操作的画布,该画布应具备自动适应父级组件宽度的特性。
- 用户可以自定义画布的填充颜色,以满足不同的界面设计需求。
- 线条绘制(模拟手写操作):
- 允许用户在画布上绘制线条,线条的外观需要支持自定义,包括但不限于:
- 宽度自定义:用户可以调整线条的宽度,根据自己的喜好和需求来控制签名的粗细。
- 颜色自定义:用户可以选择不同的颜色进行签名,以实现个性化效果。
- 样式自定义:用户可以通过
lineStyle
函数对CanvasRenderingContext2D
进行更高级的线条样式设置,如虚线、点线等,以提供更大的灵活性。
- 允许用户在画布上绘制线条,线条的外观需要支持自定义,包括但不限于:
- 设备兼容性:
- 该签名组件需要同时支持 PC 端和移动端,确保用户在不同设备上都能流畅使用。这就需要处理不同的输入事件,包括鼠标事件(如
mousedown
、mousemove
、mouseup
、mouseleave
)和触摸事件(如touchstart
、touchmove
、touchend
),以保证在不同的输入设备上都能正常进行手写操作。
- 该签名组件需要同时支持 PC 端和移动端,确保用户在不同设备上都能流畅使用。这就需要处理不同的输入事件,包括鼠标事件(如
- 操作功能:
- 重置画布:提供一个功能,让用户可以一键清除画布上已有的签名,将画布恢复到初始状态。这对于用户想要重新签名或修改签名时非常有用。
- 获取签名结果:能够获取最终的签名结果,以
image/png
的格式将签名作为图像数据输出,方便后续的处理和存储,例如将签名保存到服务器或在页面上显示签名的预览。
实现细节
定义 Props
首先,我们定义了一个接口 SignaturePadProps
来接收组件的属性,这些属性将影响签名组件的外观和行为:
interface SignaturePadProps {
className?: string;
fillStyle?: string;
strokeStyle?: string;
lineWidth?: number;
lineStyle?: (ctx: CanvasRenderingContext2D) => void
}
这里,className
允许用户为签名组件添加自定义类名,以便应用额外的CSS样式。fillStyle
用于设置画布的填充颜色,strokeStyle
用于设置线条的颜色,lineWidth
用于设置线条的宽度,而 lineStyle
是一个函数,用户可以通过它对 CanvasRenderingContext2D
进行更高级的线条样式设置,例如:
const customLineStyle = (ctx: CanvasRenderingContext2D) => {
// 示例:设置为虚线
ctx.setLineDash([5, 15]);
};
通过 withDefaults(defineProps<SignaturePadProps>(), {...})
为这些属性设置默认值,确保即使用户未传入相应属性,组件也能正常工作:
const props = withDefaults(defineProps<SignaturePadProps>(), {
fillStyle: "#f8f9fa",
strokeStyle: "#dc3545",
lineWidth: 3,
});
绘制画布和画线方法
- 创建画布
const drawSignaturePad = () => {
const canvas = signaturePad.value;
if (canvas) {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
canvas2D.value = canvas.getContext("2d");
if (canvas2D.value) {
canvas2D.value.fillStyle = props.fillStyle;
canvas2D.value.fillRect(0, 0, canvas.width, canvas.height);
}
}
};
在 drawSignaturePad
函数中,首先获取 canvas
元素的引用。然后,将其宽度和高度设置为 offsetWidth
和 offsetHeight
,确保画布自适应父级元素的宽度,并根据父级元素的大小进行调整。接着,通过 getContext("2d")
获取 2D
绘图上下文。如果成功获取到上下文,将填充颜色设置为 props.fillStyle
,并使用 fillRect
方法填充整个画布。
- 绘制线条
const writing = (
beginX: number,
beginY: number,
stopX: number,
stopY: number,
ctx: CanvasRenderingContext2D | null,
) => {
if (!ctx) return;
ctx.beginPath();
ctx.lineWidth = props.lineWidth;
ctx.strokeStyle = props.strokeStyle;
props.lineStyle?.(ctx);
ctx.moveTo(beginX, beginY);
ctx.lineTo(stopX, stopY);
ctx.closePath();
ctx.stroke();
};
writing
函数用于绘制线条。它接收起始坐标(beginX
和 beginY
)和结束坐标(stopX
和 stopY
)以及 CanvasRenderingContext2D
上下文作为参数。首先检查上下文是否存在,如果存在,开始绘制路径,设置线条宽度和颜色,调用 lineStyle
函数进行自定义样式设置(如果有的话),将路径移动到起始坐标,绘制直线到结束坐标,最后通过 stroke
方法绘制线条。
定义 defineExpose
defineExpose
方法用于将内部的方法暴露给父组件,以便在不同的业务需求下可以灵活地定制组件的布局和操作。这为组件的复用性和可扩展性提供了很大的便利。
const resetSignaturePad = () => {
if (canvas2D.value && signaturePad.value) {
canvas2D.value.clearRect(
0,
0,
signaturePad.value.width,
signaturePad.value.height,
);
drawSignaturePad();
}
};
const getSignatureDataURL = () => {
return signaturePad.value?.toDataURL("image/png", 1.0);
};
defineExpose({
resetSignaturePad,
getSignatureDataURL,
});
resetSignaturePad
方法:用于重置画布。它首先检查canvas2D
和signaturePad
是否存在,如果存在,使用clearRect
方法清除整个画布的内容,然后调用drawSignaturePad
函数重新绘制画布,将画布恢复到初始状态。getSignatureDataURL
方法:用于获取签名的图像数据。通过toDataURL
方法将当前画布内容转换为image/png
格式的数据,该数据可以用于在页面上显示签名的图像或存储签名。
处理事件
可以将签名动作封装成三个方法:
handleStart
,handleMove
,handleEnd
const handleStart = (event: Event) => {
event.preventDefault();
if (signaturePad.value === null) return;
const rect = signaturePad.value.getBoundingClientRect();
if (event instanceof TouchEvent) {
beginX.value = event.touches[0].clientX - rect.left;
beginY.value = event.touches[0].clientY - rect.top;
} else {
beginX.value = (event as MouseEvent).clientX - rect.left;
beginY.value = (event as MouseEvent).clientY - rect.top;
}
isWriting.value = true;
};
const handleMove = (event: Event) => {
event.preventDefault();
if (isWriting.value) {
if (signaturePad.value === null) return;
const rect = signaturePad.value.getBoundingClientRect();
let stopX: number, stopY: number;
if (event instanceof TouchEvent) {
stopX = event.touches[0].clientX - rect.left;
stopY = event.touches[0].clientY - rect.top;
} else {
stopX = (event as MouseEvent).clientX - rect.left;
stopY = (event as MouseEvent).clientY - rect.top;
}
writing(beginX.value, beginY.value, stopX, stopY, canvas2D.value);
beginX.value = stopX;
beginY.value = stopY;
}
};
const handleEnd = () => {
isWriting.value = false;
};
handleStart
方法:处理开始绘制的事件(mousedown
或touchstart
)。首先阻止默认行为,标记isWriting
为true
表示开始绘制。根据不同的事件类型(鼠标或触摸)计算起始坐标,考虑了signaturePad
元素的偏移位置。handleMove
方法:处理绘制过程中的移动事件(mousemove
或touchmove
)。在绘制状态下,根据当前事件类型计算当前坐标,调用writing
函数绘制线条,并更新起始坐标为当前坐标,以实现连续绘制。handleEnd
方法:处理结束绘制的事件(mouseup
、mouseleave
或touchend
),将isWriting
标记为false
,表示绘制结束。
监听事件
import useEventListener from "../../hooks/useEventListener";
useEventListener(
signaturePad,
["mousedown", "mousemove"],
[[handleStart], [handleMove]],
);
useEventListener(
signaturePad,
["touchstart", "touchmove"],
[[handleStart], [handleMove]],
{
passive: true,
},
);
useEventListener(
signaturePad,
["mouseup", "mouseleave", "touchend"],
handleEnd,
);
onMounted(() => {
drawSignaturePad();
});
- 在
onMounted
生命周期中,调用drawSignaturePad
绘制初始画布,然后添加各种鼠标和触摸事件的监听器。 - 在
onBeforeMount
生命周期中,移除相应的事件监听器,以防止内存泄漏和不必要的事件触发。 - 使用
useEventListener
hook 添加各种鼠标和触摸事件的监听器,减少冗余监听代码,且 hook 内部处理移除事件监听器逻辑.
组件样式
自动适应父级组件宽度,并保持默认的宽高比展示
.ld-signature-pad {
width: 100%;
height: auto;
cursor: pointer;
}
这里,width: 100%
确保签名组件的宽度自适应父级组件,height: auto
让高度根据宽度自动调整,cursor: pointer
将鼠标指针样式设置为指针,提示用户可以进行交互操作。
感谢阅读,敬请斧正!