SignaturePad 签名板
pointer events + canvas 平滑笔画签名板。quadratic-bezier 平滑、DPR 自动缩放、暴露 clear/toDataURL/toBlob/isEmpty 方法。
基础用法
通过 ref 暴露 clear / toDataURL / toBlob / isEmpty。绘制平滑度用 quadratic-bezier 中点平均;DPR 自动缩放保证 retina 屏锐利。
背景 视口
<script setup lang="ts">
import { ref } from 'vue';
import { CfSignaturePad, CfButton } from '@chufix-design/vue';
const pad = ref<InstanceType<typeof CfSignaturePad> | null>(null);
const empty = ref(true);
const preview = ref('');
function onChange(isEmpty: boolean) {
empty.value = isEmpty;
if (!isEmpty) preview.value = pad.value?.toDataURL() ?? '';
}
function doClear() {
pad.value?.clear();
preview.value = '';
}
function doExport() {
if (!pad.value) return;
preview.value = pad.value.toDataURL();
}
</script>
<template>
<div class="sig-demo">
<CfSignaturePad
ref="pad"
:height="160"
stroke-color="#f8fafc"
background="oklch(20% 0.012 260)"
@change="onChange"
/>
<div class="sig-demo__bar">
<CfButton variant="tertiary" :disabled="empty" @click="doClear">清空</CfButton>
<CfButton variant="primary" :disabled="empty" @click="doExport">导出 PNG</CfButton>
</div>
<img v-if="preview" :src="preview" class="sig-demo__preview" alt="signature" />
</div>
</template>
<style scoped>
.sig-demo {
display: grid;
gap: 10px;
}
.sig-demo__bar {
display: flex;
gap: 8px;
}
.sig-demo__preview {
max-height: 60px;
background: #fff;
border-radius: var(--r-2);
padding: 4px;
}
</style> <script setup>
import { ref } from 'vue';
import { CfSignaturePad, CfButton } from '@chufix-design/vue';
const pad = ref<InstanceType<typeof CfSignaturePad> | null>(null);
const empty = ref(true);
const preview = ref('');
function onChange(isEmpty) {
empty.value = isEmpty;
if (!isEmpty) preview.value = pad.value?.toDataURL() ?? '';
}
function doClear() {
pad.value?.clear();
preview.value = '';
}
function doExport() {
if (!pad.value) return;
preview.value = pad.value.toDataURL();
}
</script>
<template>
<div class="sig-demo">
<CfSignaturePad
ref="pad"
:height="160"
stroke-color="#f8fafc"
background="oklch(20% 0.012 260)"
@change="onChange"
/>
<div class="sig-demo__bar">
<CfButton variant="tertiary" :disabled="empty" @click="doClear">清空</CfButton>
<CfButton variant="primary" :disabled="empty" @click="doExport">导出 PNG</CfButton>
</div>
<img v-if="preview" :src="preview" class="sig-demo__preview" alt="signature" />
</div>
</template>
<style scoped>
.sig-demo {
display: grid;
gap: 10px;
}
.sig-demo__bar {
display: flex;
gap: 8px;
}
.sig-demo__preview {
max-height: 60px;
background: #fff;
border-radius: var(--r-2);
padding: 4px;
}
</style> import { useState } from 'react';
import { CfButton, CfSignaturePad } from '@chufix-design/react';
export default function Demo() {
const [pad, setPad] = useState<InstanceType<typeof CfSignaturePad> | null>(null);
const [empty, setEmpty] = useState(true);
const [preview, setPreview] = useState('');
function onChange(isEmpty: boolean) {
setEmpty(isEmpty);
if (!isEmpty) setPreview(pad?.toDataURL() ?? '');
}
function doClear() {
pad?.clear();
setPreview('');
}
function doExport() {
if (!pad) return;
setPreview(pad.toDataURL());
}
return (
<>
<div className="sig-demo">
<CfSignaturePad ref="pad" height={160} stroke-color="#f8fafc" background="oklch(20% 0.012 260)" onChange={onChange} />
<div className="sig-demo__bar">
<CfButton variant="tertiary" disabled={empty} onClick={doClear}>清空</CfButton>
<CfButton variant="primary" disabled={empty} onClick={doExport}>导出 PNG</CfButton>
</div>
<img v-if="preview" src={preview} className="sig-demo__preview" alt="signature" />
</div>
</>
);
} import { useState } from 'react';
import { CfButton, CfSignaturePad } from '@chufix-design/react';
export default function Demo() {
const [pad, setPad] = useState<InstanceType<typeof CfSignaturePad> | null>(null);
const [empty, setEmpty] = useState(true);
const [preview, setPreview] = useState('');
function onChange(isEmpty) {
setEmpty(isEmpty);
if (!isEmpty) setPreview(pad?.toDataURL() ?? '');
}
function doClear() {
pad?.clear();
setPreview('');
}
function doExport() {
if (!pad) return;
setPreview(pad.toDataURL());
}
return (
<>
<div className="sig-demo">
<CfSignaturePad ref="pad" height={160} stroke-color="#f8fafc" background="oklch(20% 0.012 260)" onChange={onChange} />
<div className="sig-demo__bar">
<CfButton variant="tertiary" disabled={empty} onClick={doClear}>清空</CfButton>
<CfButton variant="primary" disabled={empty} onClick={doExport}>导出 PNG</CfButton>
</div>
<img v-if="preview" src={preview} className="sig-demo__preview" alt="signature" />
</div>
</>
);
} API
Props
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
width | number | string | '100%' | 画板宽 |
height | number | string | 180 | 画板高 |
strokeWidth | number | 2 | 笔画粗细 |
strokeColor | string | 当前 color | 颜色(默认读 CSS color) |
background | string | 'transparent' | 背景;导出时会作为画板底色 |
disabled | boolean | false | 禁用 |
Events
| 事件 | 载荷 | 说明 |
|---|---|---|
change | empty: boolean | 笔画结束 / 清空后 |
start | — | 一笔开始 |
end | — | 一笔结束 |
Exposed methods
通过 ref / forwardRef 暴露:
clear()toDataURL(type?, quality?)toBlob(type?, quality?): Promise<Blob \| null>isEmpty(): boolean
反馈与讨论
SignaturePad 签名板 的讨论