diff --git a/packages/vant/src/signature/README.md b/packages/vant/src/signature/README.md new file mode 100644 index 000000000..82d526308 --- /dev/null +++ b/packages/vant/src/signature/README.md @@ -0,0 +1,105 @@ +# Signature + +### Intro + +Component for signature scene, based on Canvas. + +### Install + +Register component globally via `app.use`, refer to [Component Registration](#/en-US/advanced-usage#zu-jian-zhu-ce) for more registration ways. + +```js +import { createApp } from 'vue'; +import { Space } from 'vant'; + +const app = createApp(); +app.use(Space); +``` + +## Usage + +### Basic Usage + +```html + + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const demoUrl = ref(''); + + const onSubmit = (data) => { + const { filePath, canvas } = data; + demoUrl.value = filePath; + + console.log('submit', canvas, filePath); + }; + + const onClear = () => console.log('clear'); + + return { + onSubmit, + onClear, + demoUrl, + }; + }, +}; +``` + +### Pen Color + +```html + +``` + +### LineWidth + +```html + +``` + +## API + +### Props + +| Parameter | Description | Type | Default | +| --- | --- | --- | --- | +| type | Export image type | _string_ | `png` | +| penColor | Color of the brush stroke, default is black | _string_ | `#000` | +| lineWidth | Width of the line | _number_ | `3` | +| tips | Text that appears when Canvas is not supported | _string_ | - | + +### Events + +| Event Name | Description | Callback Parameters | +| --- | --- | --- | +| start | Callback for start of signature | - | +| end | Callback for end of signature | - | +| signing | Callback for signature in progress | _event: TouchEvent_ | +| submit | submit button click | _data: {canvas: HTMLCanvasElement, filePath: string}_ | +| clear | clear button click | - | + +### Types + +The component exports the following type definitions: + +```js +import type { SignatureProps } from 'vant'; +``` + +## Theming + +### CSS Variables + +The component provides the following CSS variables, which can be used to customize styles. Please refer to [ConfigProvider component](#/en-US/config-provider). + +| Name | Default Value | Description | +| --- | --- | --- | +| --van-signature-padding | _var(--van-padding-xs)_ | - | +| --van-signature-content-height | _160px_ | Height of the canvas | +| --van-signature-content-background | _var(--van-background-2)_ | Background color of the canvas | +| --van-signature-content-border | _1px dotted #dadada_ | Border style of the canvas | diff --git a/packages/vant/src/signature/README.zh-CN.md b/packages/vant/src/signature/README.zh-CN.md new file mode 100644 index 000000000..04d8f6577 --- /dev/null +++ b/packages/vant/src/signature/README.zh-CN.md @@ -0,0 +1,105 @@ +# Signature 签名 + +### 介绍 + +用于签名场景的组件,基于 Canvas。 + +### 引入 + +通过以下方式来全局注册组件,更多注册方式请参考[组件注册](#/zh-CN/advanced-usage#zu-jian-zhu-ce)。 + +```js +import { createApp } from 'vue'; +import { Signature } from 'vant'; + +const app = createApp(); +app.use(Signature); +``` + +## 代码演示 + +### 基础用法 + +```html + + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const demoUrl = ref(''); + + const onSubmit = (data) => { + const { filePath, canvas } = data; + demoUrl.value = filePath; + + console.log('submit', canvas, filePath); + }; + + const onClear = () => console.log('clear'); + + return { + onSubmit, + onClear, + demoUrl, + }; + }, +}; +``` + +### 自定义颜色 + +```html + +``` + +### 自定义线宽 + +```html + +``` + +## API + +### Props + +| 参数 | 说明 | 类型 | 默认值 | +| --------- | ------------------------------------ | -------- | ------ | +| type | 导出图片类型 | _string_ | `png` | +| penColor | 笔触颜色,默认黑色。 | _string_ | `#000` | +| lineWidth | 线条宽度 | _number_ | `3` | +| tips | 当不支持 Canvas 的时候出现的提示文案 | _string_ | - | + +### Events + +| 事件名 | 说明 | 回调参数 | +| --- | --- | --- | +| start | 签名开始事件回调 | - | +| end | 签名结束事件回调 | - | +| signing | 签名过程事件回调 | _event: TouchEvent_ | +| submit | 确定按钮事件回调 | _data: {canvas: HTMLCanvasElement, filePath: string}_ | +| clear | 取消按钮事件回调 | - | + +### 类型定义 + +组件导出以下类型定义: + +```js +import type { SignatureProps } from 'vant'; +``` + +## 主题定制 + +### 样式变量 + +组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/config-provider)。 + +| 名称 | 默认值 | 描述 | +| --- | --- | --- | +| --van-signature-padding | _var(--van-padding-xs)_ | - | +| --van-signature-content-height | _160px_ | 画布高度 | +| --van-signature-content-background | _var(--van-background-2)_ | 画布背景色 | +| --van-signature-content-border | _1px dotted #dadada_ | 画布边框样式 | diff --git a/packages/vant/src/signature/Signature.tsx b/packages/vant/src/signature/Signature.tsx new file mode 100644 index 000000000..92969c9a7 --- /dev/null +++ b/packages/vant/src/signature/Signature.tsx @@ -0,0 +1,153 @@ +import { + defineComponent, + reactive, + ref, + onMounted, + type ExtractPropTypes, +} from 'vue'; +import { createNamespace, makeNumberProp, makeStringProp } from '../utils'; +import { Button } from '../button'; + +const [name, bem, t] = createNamespace('signature'); + +export const signatureProps = { + type: makeStringProp('png'), + lineWidth: makeNumberProp(3), + penColor: makeStringProp('#000'), + tips: String, +}; + +export type SignatureProps = ExtractPropTypes; + +export default defineComponent({ + name, + + props: signatureProps, + + emits: ['submit', 'clear', 'start', 'end', 'signing'], + + setup(props, { emit }) { + const canvasRef = ref(null); + const wrapRef = ref(null); + + const state = reactive({ + width: 0, + height: 0, + ctx: null as any, + isSupportTouch: 'ontouchstart' in window, + }); + + const hasCanvasSupport = () => { + const canvas = document.createElement('canvas'); + return !!(canvas.getContext && canvas.getContext('2d')); + }; + + const touchMove = (event: TouchEvent) => { + if (!state.ctx) { + return false; + } + const evt = event.changedTouches + ? event.changedTouches[0] + : event.targetTouches[0]; + + emit('signing', evt); + let mouseX = evt.clientX; + let mouseY = evt.clientY; + + if (!state.isSupportTouch) { + const coverPos = canvasRef.value?.getBoundingClientRect(); + mouseX = evt.clientX - (coverPos?.left || 0); + mouseY = evt.clientY - (coverPos?.top || 0); + } + state.ctx.lineCap = 'round'; + state.ctx.lineJoin = 'round'; + state.ctx?.lineTo(mouseX, mouseY); + state.ctx?.stroke(); + }; + + const touchEnd = (event: { preventDefault: () => void }) => { + event.preventDefault(); + emit('end'); + }; + + const touchStart = () => { + if (!state.ctx) { + return false; + } + emit('start'); + state.ctx.beginPath(); + state.ctx.lineWidth = props.lineWidth; + state.ctx.strokeStyle = props.penColor; + }; + + const isCanvasEmpty = (canvas: HTMLCanvasElement) => { + const empty: HTMLCanvasElement = document.createElement('canvas'); + empty.width = canvas.width; + empty.height = canvas.height; + return canvas.toDataURL() === empty.toDataURL(); + }; + + const submit = () => { + const canvas = canvasRef.value; + if (!canvas) { + return; + } + + const isEmpty = isCanvasEmpty(canvas); + const _canvas = isEmpty ? null : canvas; + const _filePath = isEmpty + ? '' + : canvas.toDataURL( + `image/${props.type}`, + props.type === 'jpg' ? 0.9 : null + ); + + const data = { + canvas: _canvas, + filePath: _filePath, + }; + + emit('submit', data); + }; + + const clear = () => { + state.ctx.clearRect(0, 0, state.width, state.height); + state.ctx.closePath(); + emit('clear'); + }; + + onMounted(() => { + if (hasCanvasSupport()) { + state.ctx = canvasRef.value?.getContext('2d'); + state.width = wrapRef.value?.offsetWidth || 0; + state.height = wrapRef.value?.offsetHeight || 0; + } + }); + + return () => ( +
+
+ {(hasCanvasSupport() && ( + + )) ||

{props.tips}

} +
+
+

{props.tips}

+ + +
+
+ ); + }, +}); diff --git a/packages/vant/src/signature/demo/index.vue b/packages/vant/src/signature/demo/index.vue new file mode 100644 index 000000000..c856639c3 --- /dev/null +++ b/packages/vant/src/signature/demo/index.vue @@ -0,0 +1,67 @@ + + + diff --git a/packages/vant/src/signature/index.less b/packages/vant/src/signature/index.less new file mode 100644 index 000000000..8c00a5bbb --- /dev/null +++ b/packages/vant/src/signature/index.less @@ -0,0 +1,27 @@ +:root { + --van-signature-padding: var(--van-padding-xs); + --van-signature-content-height: 160px; + --van-signature-content-background: var(--van-background-2); + --van-signature-content-border: 1px dotted #dadada; +} + +.van-signature { + padding: var(--van-signature-padding); + + &__content { + display: flex; + justify-content: center; + align-items: center; + height: var(--van-signature-content-height); + background-color: var(--van-signature-content-background); + border: var(--van-signature-content-border); + } + &__footer { + display: flex; + justify-content: flex-end; + + .van-button { + margin: 4px; + } + } +} diff --git a/packages/vant/src/signature/index.ts b/packages/vant/src/signature/index.ts new file mode 100644 index 000000000..743ef0787 --- /dev/null +++ b/packages/vant/src/signature/index.ts @@ -0,0 +1,12 @@ +import { withInstall } from '../utils'; +import _Signature from './Signature'; + +export const Signature = withInstall(_Signature); +export default Signature; +export type { SignatureProps } from './Signature'; + +declare module 'vue' { + export interface GlobalComponents { + Signature: typeof Signature; + } +} diff --git a/packages/vant/src/signature/test/__snapshots__/demo.spec.ts.snap b/packages/vant/src/signature/test/__snapshots__/demo.spec.ts.snap new file mode 100644 index 000000000..f7e31777b --- /dev/null +++ b/packages/vant/src/signature/test/__snapshots__/demo.spec.ts.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render demo and match snapshot 1`] = ` +
+
+
+ + +
+ +
+
+
+
+
+ + +
+ +
+
+
+
+
+ + +
+ +
+
+`; diff --git a/packages/vant/src/signature/test/demo.spec.ts b/packages/vant/src/signature/test/demo.spec.ts new file mode 100644 index 000000000..c0e0c95b9 --- /dev/null +++ b/packages/vant/src/signature/test/demo.spec.ts @@ -0,0 +1,4 @@ +import Demo from '../demo/index.vue'; +import { snapshotDemo } from '../../../test/demo'; + +snapshotDemo(Demo); diff --git a/packages/vant/src/signature/test/index.spec.ts b/packages/vant/src/signature/test/index.spec.ts new file mode 100644 index 000000000..5530a43ce --- /dev/null +++ b/packages/vant/src/signature/test/index.spec.ts @@ -0,0 +1,58 @@ +import { Signature } from '..'; +import { mount } from '../../../test'; + +test('renders a canvas element when canvas is supported', async () => { + const wrapper = mount(Signature); + expect(wrapper.find('canvas').exists()).toBe(true); +}); + +it('should emit "start" event when touch starts', async () => { + const wrapper = mount(Signature); + const canvas = wrapper.find('canvas'); + + await canvas.trigger('touchstart'); + + expect(wrapper.emitted('start')).toBeTruthy(); +}); + +test('should emit "signing" event when touch is moving', async () => { + const wrapper = mount(Signature); + const canvas = wrapper.find('canvas'); + + await canvas.trigger('touchstart'); + await canvas.trigger('touchmove', { + changedTouches: [{ clientX: 10, clientY: 20 }], + }); + + expect(wrapper.emitted('signing')).toBeTruthy(); + expect(wrapper.emitted('signing')![0][0]).toMatchObject({ + clientX: 10, + clientY: 20, + }); +}); + +test('should emit `end` event when touchend is triggered', async () => { + const wrapper = mount(Signature); + await wrapper.vm.$nextTick(); + + const canvas = wrapper.find('canvas'); + await canvas.trigger('touchend'); + expect(wrapper.emitted('end')).toBeTruthy(); +}); + +test('submit() should output a valid canvas', async () => { + const wrapper = mount(Signature); + + await wrapper.vm.$nextTick(); + + wrapper.vm.$emit('submit', { canvas: null, filePath: '' }); + + const emitted = wrapper.emitted(); + expect(emitted.submit).toBeTruthy(); + const [data] = emitted.submit[0] as [ + { canvas: HTMLCanvasElement | null; filePath: string } + ]; + + expect(data.canvas).toBeNull(); + expect(data.filePath).toBe(''); +}); diff --git a/packages/vant/vant.config.mjs b/packages/vant/vant.config.mjs index be48f9228..5e8711331 100644 --- a/packages/vant/vant.config.mjs +++ b/packages/vant/vant.config.mjs @@ -434,6 +434,10 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io'); path: 'submit-bar', title: 'SubmitBar 提交订单栏', }, + { + path: 'signature', + title: 'Signature 签名', + }, ], }, { @@ -878,6 +882,10 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io'); path: 'submit-bar', title: 'SubmitBar', }, + { + path: 'signature', + title: 'Signature', + }, ], }, {