From 04e07bed152644d2cfd86115aadcb0a89a9f206d Mon Sep 17 00:00:00 2001 From: liuwenbo1 <609942901@qq.com> Date: Mon, 3 Mar 2025 15:02:38 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20=E7=AD=BE=E5=90=8D=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=86=85=E5=B5=8C=E6=A8=A1=E5=BC=8F=E7=A7=BB=E5=8A=A8=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E4=B8=8B=E9=BB=98=E8=AE=A4=E5=85=A8=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/amis-ui/src/components/Signature.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/amis-ui/src/components/Signature.tsx b/packages/amis-ui/src/components/Signature.tsx index 099a4b8d59c..7f0c977c5bb 100644 --- a/packages/amis-ui/src/components/Signature.tsx +++ b/packages/amis-ui/src/components/Signature.tsx @@ -5,14 +5,13 @@ */ import React from 'react'; -import {themeable, ThemeProps} from 'amis-core'; +import {themeable, ThemeProps, isMobile} from 'amis-core'; import {LocaleProps, localeable} from 'amis-core'; import {resizeSensor} from 'amis-core'; import {SmoothSignature} from '../utils'; import Button from './Button'; import {Icon} from '../index'; import Modal from './Modal'; - export interface ISignatureProps extends LocaleProps, ThemeProps { value?: string; width?: number; @@ -37,13 +36,13 @@ export interface ISignatureProps extends LocaleProps, ThemeProps { const Signature: React.FC = props => { const {translate: __, classnames: cx, className, width, height} = props; + const embedMobile = props.embed && isMobile(); const [sign, setSign] = React.useState(null); const [open, setOpen] = React.useState(false); - const [fullScreen, setFullScreen] = React.useState(false); + const [fullScreen, setFullScreen] = React.useState(embedMobile || false); const [embed, setEmbed] = React.useState(props.embed || false); const [data, setData] = React.useState(props.value); const wrapper = React.useRef(null); - React.useEffect(() => { if (!wrapper.current) { return; @@ -98,7 +97,11 @@ const Signature: React.FC = props => { }, []); const handleCloseModal = React.useCallback(() => { setOpen(false); - setFullScreen(false); + if (embedMobile) { + setFullScreen(true); + } else { + setFullScreen(false); + } setSign(null); }, []); const handleConfirmModal = React.useCallback(() => { @@ -156,6 +159,7 @@ const Signature: React.FC = props => { ebmedCancelLabel, ebmedCancelIcon } = props; + return (
@@ -180,8 +184,7 @@ const Signature: React.FC = props => { className={cx('icon', {'ml-1': undoBtnLabel})} /> - - {fullScreen ? ( + {embedMobile ? null : fullScreen ? ( From e1fa665e60b733c7b10471155824248e2615a160 Mon Sep 17 00:00:00 2001 From: liuwenbo1 <609942901@qq.com> Date: Mon, 3 Mar 2025 17:28:57 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20=E7=AD=BE=E5=90=8D=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/amis-ui/src/components/Signature.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/amis-ui/src/components/Signature.tsx b/packages/amis-ui/src/components/Signature.tsx index 7f0c977c5bb..24c5cf6a3db 100644 --- a/packages/amis-ui/src/components/Signature.tsx +++ b/packages/amis-ui/src/components/Signature.tsx @@ -39,7 +39,7 @@ const Signature: React.FC = props => { const embedMobile = props.embed && isMobile(); const [sign, setSign] = React.useState(null); const [open, setOpen] = React.useState(false); - const [fullScreen, setFullScreen] = React.useState(embedMobile || false); + const [fullScreen, setFullScreen] = React.useState(!!embedMobile); const [embed, setEmbed] = React.useState(props.embed || false); const [data, setData] = React.useState(props.value); const wrapper = React.useRef(null); @@ -97,11 +97,7 @@ const Signature: React.FC = props => { }, []); const handleCloseModal = React.useCallback(() => { setOpen(false); - if (embedMobile) { - setFullScreen(true); - } else { - setFullScreen(false); - } + setFullScreen(!!embedMobile); setSign(null); }, []); const handleConfirmModal = React.useCallback(() => { From bcb79c021629f587118eeb4e2e7c87df897afc5d Mon Sep 17 00:00:00 2001 From: liuwenbo1 <609942901@qq.com> Date: Tue, 4 Mar 2025 17:14:20 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E7=A7=BB=E5=8A=A8=E7=AB=AF=E5=85=A8=E5=B1=8F?= =?UTF-8?q?=E7=9A=84=E9=80=BB=E8=BE=91=E6=9D=A1=E4=BB=B6=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=98=AF=E5=90=A6=E6=98=AF=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=9A=84=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/amis-core/src/utils/helper.ts | 9 +++++++++ packages/amis-ui/src/components/Signature.tsx | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/amis-core/src/utils/helper.ts b/packages/amis-core/src/utils/helper.ts index 0a384d699a0..dbb595ec212 100644 --- a/packages/amis-core/src/utils/helper.ts +++ b/packages/amis-core/src/utils/helper.ts @@ -66,6 +66,15 @@ export function isMobile() { return (window as any).matchMedia?.('(max-width: 768px)').matches; } +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); } diff --git a/packages/amis-ui/src/components/Signature.tsx b/packages/amis-ui/src/components/Signature.tsx index 24c5cf6a3db..c00971609ca 100644 --- a/packages/amis-ui/src/components/Signature.tsx +++ b/packages/amis-ui/src/components/Signature.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import {themeable, ThemeProps, isMobile} from 'amis-core'; +import {themeable, ThemeProps, isMobileDevice} from 'amis-core'; import {LocaleProps, localeable} from 'amis-core'; import {resizeSensor} from 'amis-core'; import {SmoothSignature} from '../utils'; @@ -36,7 +36,8 @@ export interface ISignatureProps extends LocaleProps, ThemeProps { const Signature: React.FC = props => { const {translate: __, classnames: cx, className, width, height} = props; - const embedMobile = props.embed && isMobile(); + const embedMobile = + props.embed && isMobileDevice() && window.innerWidth < 768; const [sign, setSign] = React.useState(null); const [open, setOpen] = React.useState(false); const [fullScreen, setFullScreen] = React.useState(!!embedMobile); From 1b08d5107ede2a9959cc9d3521678f594aaac68f Mon Sep 17 00:00:00 2001 From: liuwenbo1 <609942901@qq.com> Date: Tue, 4 Mar 2025 19:28:39 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20isMobileDevice=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/amis-core/src/utils/helper.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/amis-core/src/utils/helper.ts b/packages/amis-core/src/utils/helper.ts index dbb595ec212..e3450d0753d 100644 --- a/packages/amis-core/src/utils/helper.ts +++ b/packages/amis-core/src/utils/helper.ts @@ -62,10 +62,12 @@ 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 = From ef6b0703b978f07843e6e6e3459e92540d81723c Mon Sep 17 00:00:00 2001 From: liuwenbo1 <609942901@qq.com> Date: Wed, 5 Mar 2025 11:09:27 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E7=BB=84=E4=BB=B6close=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/amis-ui/scss/components/_signature.scss | 1 + packages/amis-ui/src/components/Signature.tsx | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/amis-ui/scss/components/_signature.scss b/packages/amis-ui/scss/components/_signature.scss index d8fc625b81c..7071810d690 100644 --- a/packages/amis-ui/scss/components/_signature.scss +++ b/packages/amis-ui/scss/components/_signature.scss @@ -59,6 +59,7 @@ cursor: pointer; font-size: 14px; color: #aaa; + top: 0; &:hover { color: #000; } diff --git a/packages/amis-ui/src/components/Signature.tsx b/packages/amis-ui/src/components/Signature.tsx index c00971609ca..20a709326ca 100644 --- a/packages/amis-ui/src/components/Signature.tsx +++ b/packages/amis-ui/src/components/Signature.tsx @@ -235,11 +235,7 @@ const Signature: React.FC = props => { {data ? (
- +
) : null} From 62b2643b7b6d6ea6950422624f78652c722e2472 Mon Sep 17 00:00:00 2001 From: liuwenbo1 <609942901@qq.com> Date: Fri, 7 Mar 2025 19:09:43 +0800 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=E7=AD=BE=E5=90=8D=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E6=96=B0=E5=A2=9E=E4=B8=8A=E4=BC=A0=E5=9B=BE=E7=89=87?= =?UTF-8?q?API,=20=E6=96=B0=E5=A2=9Efile64=E4=BE=9D=E8=B5=96=E5=8C=85,=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9Esmooth-signature=E4=BE=9D=E8=B5=96=E5=8C=85,?= =?UTF-8?q?=20=E5=88=A0=E9=99=A4=E5=86=B2=E7=AA=81=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/form/input-signature.md | 29 ++++---- packages/amis-ui/package.json | 1 + packages/amis-ui/src/components/Signature.tsx | 72 ++++++++++++------- packages/amis-ui/src/locale/de-DE.ts | 1 + packages/amis-ui/src/locale/en-US.ts | 1 + packages/amis-ui/src/locale/zh-CN.ts | 1 + packages/amis/package.json | 1 + .../src/renderers/Form/InputSignature.tsx | 64 +++++++++++++++-- 8 files changed, 125 insertions(+), 45 deletions(-) diff --git a/docs/zh-CN/components/form/input-signature.md b/docs/zh-CN/components/form/input-signature.md index 670da196ec5..b86c7f8510a 100644 --- a/docs/zh-CN/components/form/input-signature.md +++ b/docs/zh-CN/components/form/input-signature.md @@ -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) | | 上传签名图片接口,仅在内嵌模式下生效 | diff --git a/packages/amis-ui/package.json b/packages/amis-ui/package.json index 262b2d00f15..4b15b77db06 100644 --- a/packages/amis-ui/package.json +++ b/packages/amis-ui/package.json @@ -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", diff --git a/packages/amis-ui/src/components/Signature.tsx b/packages/amis-ui/src/components/Signature.tsx index 20a709326ca..d36410d4e19 100644 --- a/packages/amis-ui/src/components/Signature.tsx +++ b/packages/amis-ui/src/components/Signature.tsx @@ -5,13 +5,20 @@ */ import React from 'react'; -import {themeable, ThemeProps, isMobileDevice} 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; width?: number; @@ -31,15 +38,17 @@ export interface ISignatureProps extends LocaleProps, ThemeProps { ebmedCancelIcon?: string; embedBtnIcon?: string; embedBtnLabel?: string; - onChange?: (value?: string) => void; + onChange?: (value?: string) => Promise; } const Signature: React.FC = props => { const {translate: __, classnames: cx, className, width, height} = props; const embedMobile = props.embed && isMobileDevice() && window.innerWidth < 768; + const [sign, setSign] = React.useState(null); const [open, setOpen] = 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(props.value); @@ -58,6 +67,7 @@ const Signature: React.FC = props => { }, []); React.useEffect(() => setData(props.value), [props.value]); + React.useEffect(() => setEmbed(props.embed || false), [props.embed]); const clear = React.useCallback(() => { @@ -71,19 +81,21 @@ const Signature: React.FC = 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); @@ -125,12 +137,6 @@ const Signature: React.FC = props => { [width, height, fullScreen] ); - React.useEffect(() => { - if (data && sign) { - sign.loadFromBase64(data); - } - }, [data, sign]); - function embedCanvasRef(ref: HTMLCanvasElement) { if (open && ref && !sign) { initCanvas(ref); @@ -230,12 +236,28 @@ const Signature: React.FC = props => {
{data ? (
- - + {uploading ? ( + + ) : ( + <> + + + + )} +
+ ) : uploading ? ( +
+
) : null} diff --git a/packages/amis-ui/src/locale/de-DE.ts b/packages/amis-ui/src/locale/de-DE.ts index 68f9e152189..6fb588ff0a6 100644 --- a/packages/amis-ui/src/locale/de-DE.ts +++ b/packages/amis-ui/src/locale/de-DE.ts @@ -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.' }); diff --git a/packages/amis-ui/src/locale/en-US.ts b/packages/amis-ui/src/locale/en-US.ts index 9263f68557c..94f12bcbf42 100644 --- a/packages/amis-ui/src/locale/en-US.ts +++ b/packages/amis-ui/src/locale/en-US.ts @@ -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.' }); diff --git a/packages/amis-ui/src/locale/zh-CN.ts b/packages/amis-ui/src/locale/zh-CN.ts index 511b8c3a501..0eff1fd63b1 100644 --- a/packages/amis-ui/src/locale/zh-CN.ts +++ b/packages/amis-ui/src/locale/zh-CN.ts @@ -428,5 +428,6 @@ register('zh-CN', { 'Signature.confirm': '确认', 'Signature.cancel': '取消', 'Signature.embedLabel': '点击签名', + 'Signature.embedUpdateLabel': '更新签名', 'QRCode.tooLong': '二维码值过长,请设置{{max}}个字符以下的文本' }); diff --git a/packages/amis/package.json b/packages/amis/package.json index 7a4c47ee095..5acb2ff3109 100644 --- a/packages/amis/package.json +++ b/packages/amis/package.json @@ -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", diff --git a/packages/amis/src/renderers/Form/InputSignature.tsx b/packages/amis/src/renderers/Form/InputSignature.tsx index 7b51a867809..38541c2ce81 100644 --- a/packages/amis/src/renderers/Form/InputSignature.tsx +++ b/packages/amis/src/renderers/Form/InputSignature.tsx @@ -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'; /** @@ -94,6 +98,10 @@ export interface InputSignatureSchema extends FormBaseControlSchema { * 弹窗按钮文案 */ embedBtnLabel?: string; + /** + * 上传签名图片api, 仅在内嵌模式下生效 + */ + uploadApi?: SchemaApi; } export interface IInputSignatureProps extends FormControlProps {} @@ -106,8 +114,51 @@ export default class InputSignatureComp extends React.Component< IInputSignatureProps, IInputSignatureState > { + @autobind + async uploadFile(file: string, uploadApi: string): Promise { + 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', @@ -127,14 +178,15 @@ export default class InputSignatureComp extends React.Component< 'ebmedCancelLabel', 'ebmedCancelIcon', 'embedBtnIcon', - 'embedBtnLabel' + 'embedBtnLabel', + 'uploadApi' ]); return ( );