Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions docs/zh-CN/components/form/input-signature.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,18 @@ order: 62

## 属性表

| 属性名 | 类型 | 默认值 | 说明 |
| ----------------- | --------- | ---------- | -------------------- |
| width | `number` | | 组件宽度,最小 300 |
| height | `number` | | 组件高度,最小 160 |
| color | `string` | `#000` | 手写字体颜色 |
| bgColor | `string` | `#EFEFEF` | 面板背景颜色 |
| clearBtnLabel | `string` | `清空` | 清空按钮名称 |
| undoBtnLabel | `string` | `撤销` | 撤销按钮名称 |
| confirmBtnLabel | `string` | `确认` | 确认按钮名称 |
| embed | `boolean` | | 是否内嵌 |
| embedConfirmLabel | `string` | `确认` | 内嵌容器确认按钮名称 |
| ebmedCancelLabel | `string` | `取消` | 内嵌容器取消按钮名称 |
| embedBtnIcon | `string` | | 内嵌按钮图标 |
| embedBtnLabel | `string` | `点击签名` | 内嵌按钮文案 |
| 属性名 | 类型 | 默认值 | 说明 |
| ----------------- | ------------------------------ | ---------- | ------------------------------------ |
| width | `number` | | 组件宽度,最小 300 |
| height | `number` | | 组件高度,最小 160 |
| color | `string` | `#000` | 手写字体颜色 |
| bgColor | `string` | `#EFEFEF` | 面板背景颜色 |
| clearBtnLabel | `string` | `清空` | 清空按钮名称 |
| undoBtnLabel | `string` | `撤销` | 撤销按钮名称 |
| confirmBtnLabel | `string` | `确认` | 确认按钮名称 |
| embed | `boolean` | | 是否内嵌 |
| embedConfirmLabel | `string` | `确认` | 内嵌容器确认按钮名称 |
| ebmedCancelLabel | `string` | `取消` | 内嵌容器取消按钮名称 |
| embedBtnIcon | `string` | | 内嵌按钮图标 |
| embedBtnLabel | `string` | `点击签名` | 内嵌按钮文案 |
| uploadApi | [API](../../../docs/types/api) | | 上传签名图片接口,仅在内嵌模式下生效 |
11 changes: 11 additions & 0 deletions packages/amis-core/src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,21 @@ export function preventDefault(event: TouchEvent | Event): void {
}
}

// isMobile根据media宽度判断是否是移动端
export function isMobile() {
return (window as any).matchMedia?.('(max-width: 768px)').matches;
}

// isMobileDevice根据userAgent判断是否为移动端设备
export function isMobileDevice() {
const userAgent = navigator.userAgent;
const isMobileUA =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile/i.test(
userAgent
);
return isMobileUA;
}

export function range(num: number, min: number, max: number): number {
return Math.min(Math.max(num, min), max);
}
Expand Down
1 change: 1 addition & 0 deletions packages/amis-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"react-textarea-autosize": "8.3.3",
"react-transition-group": "4.4.2",
"react-visibility-sensor": "5.1.1",
"smooth-signature": "^1.0.15",
"sortablejs": "1.15.0",
"tinymce": "^6.1.2",
"tslib": "^2.3.1",
Expand Down
1 change: 1 addition & 0 deletions packages/amis-ui/scss/components/_signature.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
cursor: pointer;
font-size: 14px;
color: #aaa;
top: 0;
&:hover {
color: #000;
}
Expand Down
86 changes: 52 additions & 34 deletions packages/amis-ui/src/components/Signature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
*/

import React from 'react';
import {themeable, ThemeProps} from 'amis-core';
import {LocaleProps, localeable} from 'amis-core';
import {resizeSensor} from 'amis-core';
import {SmoothSignature} from '../utils';
import {
themeable,
ThemeProps,
isMobileDevice,
LocaleProps,
localeable,
resizeSensor
} from 'amis-core';
import SmoothSignature from 'smooth-signature';
import Button from './Button';
import {Icon} from '../index';
import Modal from './Modal';
import Spinner from './Spinner';

export interface ISignatureProps extends LocaleProps, ThemeProps {
value?: string;
Expand All @@ -32,18 +38,21 @@ export interface ISignatureProps extends LocaleProps, ThemeProps {
ebmedCancelIcon?: string;
embedBtnIcon?: string;
embedBtnLabel?: string;
onChange?: (value?: string) => void;
onChange?: (value?: string) => Promise<void>;
}

const Signature: React.FC<ISignatureProps> = props => {
const {translate: __, classnames: cx, className, width, height} = props;
const embedMobile =
props.embed && isMobileDevice() && window.innerWidth < 768;

const [sign, setSign] = React.useState<SmoothSignature | null>(null);
const [open, setOpen] = React.useState(false);
const [fullScreen, setFullScreen] = React.useState(false);
const [uploading, setUploading] = React.useState(false);
const [fullScreen, setFullScreen] = React.useState(!!embedMobile);
const [embed, setEmbed] = React.useState(props.embed || false);
const [data, setData] = React.useState<string | undefined>(props.value);
const wrapper = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
if (!wrapper.current) {
return;
Expand All @@ -58,6 +67,7 @@ const Signature: React.FC<ISignatureProps> = props => {
}, []);

React.useEffect(() => setData(props.value), [props.value]);

React.useEffect(() => setEmbed(props.embed || false), [props.embed]);

const clear = React.useCallback(() => {
Expand All @@ -71,19 +81,21 @@ const Signature: React.FC<ISignatureProps> = props => {
sign.undo();
}
}, [sign]);

const confirm = React.useCallback(() => {
if (sign) {
if (fullScreen) {
const canvas = sign.getRotateCanvas(-90);
const base64 = canvas.toDataURL();
setData(base64);
props.onChange?.(base64);
} else {
const base64 = sign.toDataURL();
setData(base64);
props.onChange?.(base64);
}
if (!sign) {
return;
}

const base64 = fullScreen
? sign.getRotateCanvas(-90).toDataURL()
: sign.toDataURL();

setData(base64);
setUploading(true);
props.onChange?.(base64).then(() => {
setUploading(false);
});
}, [sign]);
const resize = React.useCallback(() => {
setSign(null);
Expand All @@ -98,7 +110,7 @@ const Signature: React.FC<ISignatureProps> = props => {
}, []);
const handleCloseModal = React.useCallback(() => {
setOpen(false);
setFullScreen(false);
setFullScreen(!!embedMobile);
setSign(null);
}, []);
const handleConfirmModal = React.useCallback(() => {
Expand All @@ -125,12 +137,6 @@ const Signature: React.FC<ISignatureProps> = props => {
[width, height, fullScreen]
);

React.useEffect(() => {
if (data && sign) {
sign.loadFromBase64(data);
}
}, [data, sign]);

function embedCanvasRef(ref: HTMLCanvasElement) {
if (open && ref && !sign) {
initCanvas(ref);
Expand All @@ -156,6 +162,7 @@ const Signature: React.FC<ISignatureProps> = props => {
ebmedCancelLabel,
ebmedCancelIcon
} = props;

return (
<div className={cx('Signature-Tool')}>
<div className="actions">
Expand All @@ -180,8 +187,7 @@ const Signature: React.FC<ISignatureProps> = props => {
className={cx('icon', {'ml-1': undoBtnLabel})}
/>
</Button>

{fullScreen ? (
{embedMobile ? null : fullScreen ? (
<Button onClick={handleUnFullScreen}>
<Icon icon="un-fullscreen" className="icon" />
</Button>
Expand Down Expand Up @@ -230,16 +236,28 @@ const Signature: React.FC<ISignatureProps> = props => {
<div className={cx('Signature-Embed')}>
<Button onClick={() => setOpen(true)}>
<Icon className="icon mr-1" icon={icon || 'fas fa-pen'}></Icon>
{embedBtnLabel || __('Signature.embedLabel')}
{embedBtnLabel || data
? __('Signature.embedUpdateLabel')
: __('Signature.embedLabel')}
</Button>
{data ? (
<div className={cx('Signature-Embed-Preview')}>
<img src={data} />
<Icon
className="preview-close"
icon="fas fa-close"
onClick={clear}
/>
{uploading ? (
<Spinner show={uploading} />
) : (
<>
<img src={data} />
<Icon
className="preview-close icon"
icon="close"
onClick={clear}
/>
</>
)}
</div>
) : uploading ? (
<div className={cx('Signature-Embed-Preview')}>
<Spinner show={uploading} />
</div>
) : null}

Expand Down
1 change: 1 addition & 0 deletions packages/amis-ui/src/locale/de-DE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ register('de-DE', {
'Signature.confirm': 'bestätigen',
'Signature.cancel': 'Abbrechen',
'Signature.embedLabel': 'Klicken Sie zum Signieren',
'Signature.embedUpdateLabel': 'Aktualisiert zum Signieren',
'QRCode.tooLong':
'Der QR-Code-Wert ist zu lang, bitte setzen Sie den Text auf weniger als {{max}} Zeichen.'
});
1 change: 1 addition & 0 deletions packages/amis-ui/src/locale/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ register('en-US', {
'Signature.confirm': 'confirm',
'Signature.cancel': 'cancel',
'Signature.embedLabel': 'Click to sign',
'Signature.embedUpdateLabel': 'Update to sign',
'QRCode.tooLong':
'The QR code value is too long, please set the text to be below {{max}} characters.'
});
1 change: 1 addition & 0 deletions packages/amis-ui/src/locale/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,5 +428,6 @@ register('zh-CN', {
'Signature.confirm': '确认',
'Signature.cancel': '取消',
'Signature.embedLabel': '点击签名',
'Signature.embedUpdateLabel': '更新签名',
'QRCode.tooLong': '二维码值过长,请设置{{max}}个字符以下的文本'
});
1 change: 1 addition & 0 deletions packages/amis/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"echarts-wordcloud": "^2.1.0",
"exceljs": "^4.4.0",
"file-saver": "^2.0.2",
"file64": "^1.0.4",
"hls.js": "1.1.3",
"hoist-non-react-statics": "^3.3.2",
"hotkeys-js": "^3.8.7",
Expand Down
64 changes: 58 additions & 6 deletions packages/amis/src/renderers/Form/InputSignature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ import {
IScopedContext,
FormItem,
FormControlProps,
ScopedContext
ScopedContext,
normalizeApi,
createObject,
Payload,
autobind
} from 'amis-core';
import {Signature} from 'amis-ui';
import pick from 'lodash/pick';
import {FormBaseControlSchema} from '../../Schema';

import {FormBaseControlSchema, SchemaApi} from '../../Schema';
import {base64ToBlob} from 'file64';
export interface InputSignatureSchema extends FormBaseControlSchema {
type: 'input-signature';
/**
Expand Down Expand Up @@ -94,6 +98,10 @@ export interface InputSignatureSchema extends FormBaseControlSchema {
* 弹窗按钮文案
*/
embedBtnLabel?: string;
/**
* 上传签名图片api, 仅在内嵌模式下生效
*/
uploadApi?: SchemaApi;
}

export interface IInputSignatureProps extends FormControlProps {}
Expand All @@ -106,8 +114,51 @@ export default class InputSignatureComp extends React.Component<
IInputSignatureProps,
IInputSignatureState
> {
@autobind
async uploadFile(file: string, uploadApi: string): Promise<Payload> {
const api = normalizeApi(uploadApi, 'post');
if (!api.data) {
const fd = new FormData();
const fileBlob = await base64ToBlob(file);
fd.append('file', fileBlob, 'image/png');
api.data = fd;
}

return this.props.env!.fetcher(
api,
createObject(this.props.data, {
file
})
);
}

@autobind
async handleChange(val: any) {
const {translate: __, uploadApi, embed, onChange} = this.props;
// 非内嵌模式 没有上传api 或是清空直接onChange
if (!embed || !uploadApi || val === undefined) {
onChange?.(val);
return;
}

try {
// 用api进行上传,上传结果回传表单数据
const res = await this.uploadFile(val, uploadApi as string);
if (!res.ok || (res.status && (res as any).status !== '0') || !res.data) {
throw new Error(res.msg || __('File.errorRetry'));
}
const value =
(res.data as any).value || (res.data as any).url || res.data;
onChange?.(value);
} catch (error) {
// 失败清空签名组件内的数据,传空字符串会重新触发amis的渲染,underfined不会被重新渲染(连续的空字符串不会被重新渲染,amis底层会对value值进行diff对比)
onChange?.('');
this.props.env?.alert?.(error.message || __('File.errorRetry'));
}
}

render() {
const {classnames: cx, className, onChange} = this.props;
const {classnames: cx, className} = this.props;
const props = pick(this.props, [
'value',
'width',
Expand All @@ -127,14 +178,15 @@ export default class InputSignatureComp extends React.Component<
'ebmedCancelLabel',
'ebmedCancelIcon',
'embedBtnIcon',
'embedBtnLabel'
'embedBtnLabel',
'uploadApi'
]);

return (
<Signature
classnames={cx}
className={className}
onChange={onChange}
onChange={this.handleChange}
{...props}
/>
);
Expand Down
Loading