import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; export interface SignaturePadHandle { clear: () => void; isEmpty: () => boolean; getDataURL: () => string; } interface SignaturePadProps { width?: number; height?: number; onChange?: (dataUrl: string) => void; } const SignaturePad = forwardRef(function SignaturePad( { width = 480, height = 180, onChange }, ref, ) { const canvasRef = useRef(null); const drawingRef = useRef(false); const emptyRef = useRef(true); const lastPointRef = useRef<{ x: number; y: number } | null>(null); const getContext = () => canvasRef.current?.getContext('2d') ?? null; const resetCanvas = () => { const canvas = canvasRef.current; const ctx = getContext(); if (!canvas || !ctx) return; ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.lineWidth = 2.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = '#111827'; emptyRef.current = true; }; useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; // 高 DPI 适配 const ratio = window.devicePixelRatio || 1; canvas.width = width * ratio; canvas.height = height * ratio; canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; const ctx = canvas.getContext('2d'); if (ctx) ctx.scale(ratio, ratio); resetCanvas(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [width, height]); const pointFromEvent = ( e: React.PointerEvent, ): { x: number; y: number } | null => { const canvas = canvasRef.current; if (!canvas) return null; const rect = canvas.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top, }; }; const handlePointerDown = (e: React.PointerEvent) => { e.preventDefault(); const ctx = getContext(); const pt = pointFromEvent(e); if (!ctx || !pt) return; drawingRef.current = true; lastPointRef.current = pt; canvasRef.current?.setPointerCapture(e.pointerId); // 画一个点(防止只点一下没移动看不到痕迹) ctx.beginPath(); ctx.arc(pt.x, pt.y, 1.2, 0, Math.PI * 2); ctx.fillStyle = '#111827'; ctx.fill(); emptyRef.current = false; }; const handlePointerMove = (e: React.PointerEvent) => { if (!drawingRef.current) return; e.preventDefault(); const ctx = getContext(); const pt = pointFromEvent(e); const last = lastPointRef.current; if (!ctx || !pt || !last) return; ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.lineTo(pt.x, pt.y); ctx.stroke(); lastPointRef.current = pt; emptyRef.current = false; }; const handlePointerUp = (e: React.PointerEvent) => { if (!drawingRef.current) return; drawingRef.current = false; lastPointRef.current = null; canvasRef.current?.releasePointerCapture(e.pointerId); if (onChange && canvasRef.current) { onChange(canvasRef.current.toDataURL('image/png')); } }; useImperativeHandle(ref, () => ({ clear: () => { resetCanvas(); onChange?.(''); }, isEmpty: () => emptyRef.current, getDataURL: () => (canvasRef.current ? canvasRef.current.toDataURL('image/png') : ''), })); return ( ); }); export default SignaturePad;