Merge pull request #9726 from allenve/master

feat:amis支持手写签名面板
This commit is contained in:
hsm-lv 2024-03-05 15:56:10 +08:00 committed by GitHub
commit c3a7faf3f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 408 additions and 6 deletions

View File

@ -0,0 +1,115 @@
---
title: inputSignature 签名面板
description:
type: 0
group: null
menuName: inputSignature
icon:
order: 62
---
## 基本用法
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"name": "signature",
"type": "input-signature",
"label": "手写签名",
"height": 200
}
]
}
```
## 水平展示
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"name": "signature",
"type": "input-signature",
"horiz": true,
"height": 300
}
]
}
```
## 自定义按钮名称
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"name": "signature",
"type": "input-signature",
"height": 160,
"confirmText": "确定",
"undoText": "上一步",
"clearText": "重置"
}
]
}
```
## 自定义颜色
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"name": "signature",
"type": "input-signature",
"label": "手写签名",
"height": 200,
"color": "#ff0000",
"bgColor": "#fff"
}
]
}
```
## 配合图片组件实现实时预览
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"name": "signature",
"type": "input-signature",
"label": "手写签名",
"height": 200
},
{
"type": "image",
"name": "signature"
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | --------- | --------- | ------------------ |
| width | `number` | | 组件宽度,最小 300 |
| height | `number` | | 组件高度,最小 160 |
| color | `string` | `#000` | 手写字体颜色 |
| bgColor | `string` | `#EFEFEF` | 面板背景颜色 |
| clearText | `string` | `清空` | 清空按钮名称 |
| undoText | `string` | `撤销` | 撤销按钮名称 |
| confirmText | `string` | `确认` | 确认按钮名称 |
| horiz | `boolean` | | 是否水平展示 |

View File

@ -785,6 +785,16 @@ export const components = [
wrapDoc
)
)
},
{
label: 'InputSignature 签名面板',
path: '/zh-CN/components/form/input-signature',
component: React.lazy(() =>
import('../../docs/zh-CN/components/form/input-signature.md').then(
wrapDoc
)
)
}
]
},

View File

@ -43,7 +43,8 @@
"dependencies": {
"path-to-regexp": "^6.2.0",
"postcss": "^8.4.14",
"qs": "6.9.7"
"qs": "6.9.7",
"smooth-signature": "^1.0.13"
},
"devDependencies": {
"@babel/generator": "^7.22.9",

View File

@ -0,0 +1,35 @@
.#{$ns}Signature {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
&-canvas {
border: 1px dashed #aaa;
}
&-Tool {
text-align: center;
height: 40px;
margin-top: 8px;
button {
margin-left: 5px;
}
}
&.is-horizontal {
flex-direction: row-reverse;
align-items: center;
.#{$ns}Signature-Tool {
width: 32px;
height: 100%;
margin-top: 0;
display: flex;
justify-content: center;
align-items: center;
.actions {
white-space: nowrap;
transform: rotate(90deg);
}
}
}
}

View File

@ -143,5 +143,6 @@
@import '../components/menu';
@import '../components/overflow-tpl';
@import '../components/pdf_viewer';
@import '../components/signature';
@import '../components/print';

View File

@ -0,0 +1,113 @@
/**
* @file Signature.tsx
*
* @created: 2024/03/04
*/
import React from 'react';
import {themeable, ThemeProps} from 'amis-core';
import {LocaleProps, localeable} from 'amis-core';
import {resizeSensor} from 'amis-core';
import SmoothSignature from 'smooth-signature';
import Button from './Button';
export interface ISignatureProps extends LocaleProps, ThemeProps {
width?: number;
height?: number;
color?: string;
bgColor?: string;
clearText?: string;
undoText?: string;
confirmText?: string;
horizontal?: boolean;
onChange?: (value?: string) => void;
}
const Signature: React.FC<ISignatureProps> = props => {
const {classnames: cx, horizontal, width, height} = props;
const [sign, setSign] = React.useState<SmoothSignature | null>(null);
const wrapper = React.useRef<HTMLDivElement>(null);
const canvas = React.useRef<HTMLCanvasElement>(null);
React.useEffect(() => {
if (!wrapper.current || !canvas.current) {
return;
}
initCanvas(canvas.current);
const unSensor = resizeSensor(wrapper.current, resize);
return () => {
setSign(null);
unSensor();
};
}, []);
const clear = React.useCallback(() => {
if (sign) {
sign.clear();
props.onChange?.(undefined);
}
}, [sign]);
const undo = React.useCallback(() => {
if (sign) {
sign.undo();
}
}, [sign]);
const confirm = React.useCallback(() => {
if (sign) {
const base64 = sign.toDataURL();
props.onChange?.(base64);
}
}, [sign]);
const resize = React.useCallback(() => {
setSign(null);
initCanvas(canvas.current!);
}, []);
const initCanvas = React.useCallback(
(element: HTMLCanvasElement) => {
const {width, height} = props;
const rect = element.parentElement!.getBoundingClientRect();
const clientWidth = Math.floor(rect.width);
const defaultWidth = width || clientWidth - (horizontal ? 40 : 0);
const defaultHeight = height || clientWidth / 2 - (horizontal ? 0 : 40);
const signature = new SmoothSignature(element, {
width: Math.max(defaultWidth, 300),
height: Math.max(defaultHeight, 160),
color: props.color || '#000',
bgColor: props.bgColor || '#efefef'
});
setSign(signature);
},
[width, height, horizontal]
);
function renderTool() {
const {translate: __, clearText, undoText, confirmText} = props;
return (
<div className={cx('Signature-Tool')}>
<div className="actions">
<Button onClick={clear}>{clearText || __('Signature.clear')}</Button>
<Button onClick={undo}>{undoText || __('Signature.undo')}</Button>
<Button onClick={confirm}>
{confirmText || __('Signature.confirm')}
</Button>
</div>
</div>
);
}
return (
<div
className={cx('Signature', {
'is-horizontal': horizontal
})}
ref={wrapper}
>
<canvas className={cx('Signature-canvas')} ref={canvas} />
{renderTool()}
</div>
);
};
export default themeable(localeable(Signature));

View File

@ -130,6 +130,7 @@ import InputBoxWithSuggestion from './InputBoxWithSuggestion';
import {CodeMirrorEditor} from './CodeMirror';
import type CodeMirror from 'codemirror';
import OverflowTpl from './OverflowTpl';
import Signature from './Signature';
export {
NotFound,
@ -263,5 +264,6 @@ export {
Menu,
CodeMirror,
CodeMirrorEditor,
OverflowTpl
OverflowTpl,
Signature
};

View File

@ -430,5 +430,8 @@ register('de-DE', {
'TimeNow': 'Jetzt',
'Steps.step': 'Schritt {{index}}',
'FormulaInput.True': 'Treu',
'FormulaInput.False': 'Falsch'
'FormulaInput.False': 'Falsch',
'Signature.clear': 'leer',
'Signature.undo': 'widerrufen',
'Signature.confirm': 'bestätigen'
});

View File

@ -414,5 +414,8 @@ register('en-US', {
'IconSelect.choice': 'Icon selection',
'Steps.step': 'Step {{index}}',
'FormulaInput.True': 'True',
'FormulaInput.False': 'False'
'FormulaInput.False': 'False',
'Signature.clear': 'clear',
'Signature.undo': 'undo',
'Signature.confirm': 'confirm'
});

View File

@ -409,5 +409,8 @@ register('zh-CN', {
'IconSelect.choice': '图标选择',
'Steps.step': '第 {{index}} 步',
'FormulaInput.True': '真',
'FormulaInput.False': '假'
'FormulaInput.False': '假',
'Signature.clear': '清空',
'Signature.undo': '撤销',
'Signature.confirm': '确认'
});

View File

@ -121,6 +121,7 @@ import {TransferPickerControlSchema} from './renderers/Form/TransferPicker';
import {TabsTransferPickerControlSchema} from './renderers/Form/TabsTransferPicker';
import {UserSelectControlSchema} from './renderers/Form/UserSelect';
import {JSONSchemaEditorControlSchema} from './renderers/Form/JSONSchemaEditor';
import {InputSignatureSchema} from './renderers/Form/InputSignature';
import {TableSchema2} from './renderers/Table2';
import {
BaseSchemaWithoutType,
@ -254,6 +255,7 @@ export type SchemaType =
| 'diff-editor'
| 'office-viewer'
| 'pdf-viewer'
| 'input-signature'
// editor 系列
| 'editor'
@ -461,6 +463,7 @@ export type SchemaObject =
| InputGroupControlSchema
| ListControlSchema
| JSONSchemaEditorControlSchema
| InputSignatureSchema
| LocationControlSchema
| UUIDControlSchema
| MatrixControlSchema

View File

@ -94,6 +94,7 @@ import './renderers/Form/TabsTransferPicker';
import './renderers/Form/Group';
import './renderers/Form/InputGroup';
import './renderers/Form/UserSelect';
import './renderers/Form/InputSignature';
import './renderers/Grid';
import './renderers/Grid2D';
import './renderers/HBox';

View File

@ -0,0 +1,112 @@
/**
* @file Signature.tsx
*
* @created: 2024/03/04
*/
import React from 'react';
import {
IScopedContext,
FormItem,
FormControlProps,
ScopedContext
} from 'amis-core';
import {Signature} from 'amis-ui';
import pick from 'lodash/pick';
import {FormBaseControlSchema} from '../../Schema';
export interface InputSignatureSchema extends FormBaseControlSchema {
type: 'input-signature';
/**
*
*/
width?: number;
/**
*
*/
height?: number;
/**
*
* @default #000
*/
color?: string;
/**
*
* @default #efefef
*/
bgColor?: string;
/**
*
* @default
*/
clearText?: string;
/**
*
* @default
*/
undoText?: string;
/**
*
* @default
*/
confirmText?: string;
/**
*
*/
horiz?: boolean;
}
export interface IInputSignatureProps extends FormControlProps {}
interface IInputSignatureState {
loading: boolean;
}
export default class InputSignatureComp extends React.Component<
IInputSignatureProps,
IInputSignatureState
> {
render() {
const {classnames: cx, horiz: horizontal, onChange} = this.props;
const props = pick(this.props, [
'width',
'height',
'mobileUI',
'color',
'bgColor',
'clearText',
'undoText',
'confirmText'
]);
return (
<Signature
classnames={cx}
horizontal={horizontal}
onChange={onChange}
{...props}
/>
);
}
}
@FormItem({
type: 'input-signature',
sizeMutable: false
})
export class InputSignatureRenderer extends InputSignatureComp {
static contextType = ScopedContext;
constructor(props: IInputSignatureProps, context: IScopedContext) {
super(props);
const scoped = context;
scoped.registerComponent(this);
}
componentWillUnmount() {
super.componentWillUnmount?.();
const scoped = this.context as IScopedContext;
scoped.unRegisterComponent(this);
}
}