From a937d25fcd3d26a9e528b58bb90702cc45952acb Mon Sep 17 00:00:00 2001 From: qinhaoyan Date: Mon, 9 Dec 2024 15:38:54 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E8=BF=9B=E5=85=A5=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=E6=89=8D=E6=89=A7=E8=A1=8C=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/amis-core/src/SchemaRenderer.tsx | 90 ++----------- .../amis-core/src/components/Animations.tsx | 119 ++++++++++++++++++ packages/amis-ui/scss/base/_typography.scss | 3 + 3 files changed, 130 insertions(+), 82 deletions(-) create mode 100644 packages/amis-core/src/components/Animations.tsx diff --git a/packages/amis-core/src/SchemaRenderer.tsx b/packages/amis-core/src/SchemaRenderer.tsx index e4672df6667..efbeef0e637 100644 --- a/packages/amis-core/src/SchemaRenderer.tsx +++ b/packages/amis-core/src/SchemaRenderer.tsx @@ -39,9 +39,7 @@ import {buildStyle} from './utils/style'; import {isExpression} from './utils/formula'; import {StatusScopedProps} from './StatusScoped'; import {evalExpression, filter} from './utils/tpl'; -import {CSSTransition} from 'react-transition-group'; -import {createAnimationStyle} from './utils/animations'; -import styleManager from './StyleManager'; +import Animations from './components/Animations'; interface SchemaRendererProps extends Partial>, @@ -103,16 +101,6 @@ export class SchemaRenderer extends React.Component { schema: any; path: string; - animationTimeout: { - enter?: number; - exit?: number; - } = {}; - animationClassNames: { - appear?: string; - enter?: string; - exit?: string; - } = {}; - toDispose: Array<() => any> = []; unbindEvent: (() => void) | undefined = undefined; unbindGlobalEvent: (() => void) | undefined = undefined; @@ -120,32 +108,12 @@ export class SchemaRenderer extends React.Component { constructor(props: SchemaRendererProps) { super(props); - const animations = props?.schema?.animations; - if (animations) { - let id = props?.schema.id; - id = formateId(id); - if (animations.enter) { - this.animationTimeout.enter = - ((animations.enter.duration || 1) + (animations.enter.delay || 0)) * - 1000; - this.animationClassNames.enter = `${animations.enter.type}-${id}-enter`; - this.animationClassNames.appear = this.animationClassNames.enter; - } - if (animations.exit) { - this.animationTimeout.exit = - ((animations.exit.duration || 1) + (animations.exit.delay || 0)) * - 1000; - this.animationClassNames.exit = `${animations.exit.type}-${id}-exit`; - } - } this.refFn = this.refFn.bind(this); this.renderChild = this.renderChild.bind(this); this.reRender = this.reRender.bind(this); this.resolveRenderer(this.props); this.dispatchEvent = this.dispatchEvent.bind(this); - this.addAnimationAttention = this.addAnimationAttention.bind(this); - this.removeAnimationAttention = this.removeAnimationAttention.bind(this); // 监听statusStore更新 this.toDispose.push( @@ -169,20 +137,11 @@ export class SchemaRenderer extends React.Component { ); } - componentDidMount(): void { - if (this.props.schema.animations) { - let {animations, id} = this.props.schema; - id = formateId(id); - createAnimationStyle(id, animations); - } - } - componentWillUnmount() { this.toDispose.forEach(fn => fn()); this.toDispose = []; this.unbindEvent?.(); this.unbindGlobalEvent?.(); - this.removeAnimationStyle(); } // 限制:只有 schema 除外的 props 变化,或者 schema 里面的某个成员值发生变化才更新。 @@ -213,14 +172,6 @@ export class SchemaRenderer extends React.Component { return false; } - removeAnimationStyle() { - if (this.props.schema.animations) { - let {id} = this.props.schema; - id = formateId(id); - styleManager.removeStyles(id); - } - } - resolveRenderer(props: SchemaRendererProps, force = false): any { let schema = props.schema; let path = props.$path; @@ -367,25 +318,6 @@ export class SchemaRenderer extends React.Component { this.forceUpdate(); } - addAnimationAttention(node: HTMLElement) { - const {schema} = this.props || {}; - const {attention} = schema?.animations || {}; - if (attention) { - let {id} = schema; - id = formateId(id); - node.classList.add(`${attention.type}-${id}-attention`); - } - } - removeAnimationAttention(node: HTMLElement) { - const {schema} = this.props || {}; - const {attention} = schema?.animations || {}; - if (attention) { - let {id} = schema; - id = formateId(id); - node.classList.remove(`${attention.type}-${id}-attention`); - } - } - render(): JSX.Element | null { let { $path: _, @@ -536,7 +468,7 @@ export class SchemaRenderer extends React.Component { } = schema; const Component = renderer.component!; - let animationIn = true; + let animationShow = true; // 原来表单项的 visible: false 和 hidden: true 表单项的值和验证是有效的 // 而 visibleOn 和 hiddenOn 是无效的, @@ -550,7 +482,7 @@ export class SchemaRenderer extends React.Component { (schema.visible !== false && !schema.hidden)) ) { if (schema.animations) { - animationIn = false; + animationShow = false; } else { return null; } @@ -629,17 +561,11 @@ export class SchemaRenderer extends React.Component { if (schema.animations) { component = ( - - {component} - + ); } diff --git a/packages/amis-core/src/components/Animations.tsx b/packages/amis-core/src/components/Animations.tsx new file mode 100644 index 00000000000..9c4b18904a0 --- /dev/null +++ b/packages/amis-core/src/components/Animations.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import {useEffect, useRef} from 'react'; +import {CSSTransition} from 'react-transition-group'; +import {Schema} from '../types'; +import {formateId} from '../utils'; +import {createAnimationStyle} from '../utils/animations'; +import styleManager from '../StyleManager'; + +function Animations({ + schema, + component, + show +}: { + schema: Schema; + component: any; + show: boolean; +}) { + const animationClassNames = useRef<{ + appear?: string; + enter?: string; + exit?: string; + }>({}); + const animationTimeout = useRef<{ + enter?: number; + exit?: number; + }>({}); + const ref = useRef(null); + const [animationShow, setAnimationShow] = React.useState(false); + + useEffect(() => { + const animations = schema?.animations; + if (animations) { + let id = schema.id; + id = formateId(id); + if (animations.enter) { + animationTimeout.current.enter = + ((animations.enter.duration || 1) + (animations.enter.delay || 0)) * + 1000; + animationClassNames.current.enter = `${animations.enter.type}-${id}-enter`; + animationClassNames.current.appear = animationClassNames.current.enter; + } + if (animations.exit) { + animationTimeout.current.exit = + ((animations.exit.duration || 1) + (animations.exit.delay || 0)) * + 1000; + animationClassNames.current.exit = `${animations.exit.type}-${id}-exit`; + } + createAnimationStyle(id, animations); + } + return () => { + if (schema.animations) { + let {id} = schema; + id = formateId(id); + styleManager.removeStyles(id); + } + }; + }, []); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + setAnimationShow(true); + observer.disconnect(); + } + }); + }, + { + root: null, + rootMargin: '0px', + threshold: 0.1 + } + ); + if (ref.current) { + observer.observe(ref.current); + } + }, [show]); + + function addAnimationAttention(node: HTMLElement) { + const {attention} = schema.animations || {}; + if (attention) { + let {id} = schema; + id = formateId(id); + node.classList.add(`${attention.type}-${id}-attention`); + } + } + function removeAnimationAttention(node: HTMLElement) { + const {attention} = schema.animations || {}; + if (attention) { + let {id} = schema; + id = formateId(id); + node.classList.remove(`${attention.type}-${id}-attention`); + } + } + + return ( + <> + {!animationShow && show && ( +
+ {component} +
+ )} + + {component} + + + ); +} + +export default Animations; diff --git a/packages/amis-ui/scss/base/_typography.scss b/packages/amis-ui/scss/base/_typography.scss index 5f23380c65a..6099f885619 100644 --- a/packages/amis-ui/scss/base/_typography.scss +++ b/packages/amis-ui/scss/base/_typography.scss @@ -74,3 +74,6 @@ samp { width: 100%; height: 100%; } +.amis-animation-placeholder { + opacity: 0; +} From f5dff4afa1c9488843326388a37293130ad83953 Mon Sep 17 00:00:00 2001 From: qinhaoyan Date: Mon, 9 Dec 2024 17:27:11 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E5=85=A5=E5=9C=BA=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../amis-core/src/components/Animations.tsx | 133 +++++++++++------- packages/amis-core/src/utils/animations.ts | 4 + packages/amis-core/src/utils/helper.ts | 3 + packages/amis-editor/src/tpl/style.tsx | 37 ++++- 4 files changed, 123 insertions(+), 54 deletions(-) diff --git a/packages/amis-core/src/components/Animations.tsx b/packages/amis-core/src/components/Animations.tsx index 9c4b18904a0..66faf620580 100644 --- a/packages/amis-core/src/components/Animations.tsx +++ b/packages/amis-core/src/components/Animations.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import {useEffect, useRef} from 'react'; +import React, {useState, useEffect, useRef} from 'react'; import {CSSTransition} from 'react-transition-group'; import {Schema} from '../types'; import {formateId} from '../utils'; @@ -15,98 +14,130 @@ function Animations({ component: any; show: boolean; }) { - const animationClassNames = useRef<{ - appear?: string; - enter?: string; - exit?: string; - }>({}); - const animationTimeout = useRef<{ - enter?: number; - exit?: number; - }>({}); - const ref = useRef(null); - const [animationShow, setAnimationShow] = React.useState(false); + const idRef = useRef(formateId(schema.id)); + const id = idRef.current; + const {enter} = schema.animations || {}; + const [animationShow, setAnimationShow] = React.useState(!enter?.inView); + const [placeholderShow, setPlaceholderShow] = React.useState(!!enter?.inView); - useEffect(() => { + const [animationClassNames] = useState(() => { const animations = schema?.animations; + const animationClassNames = { + appear: '', + enter: '', + exit: '' + }; if (animations) { - let id = schema.id; - id = formateId(id); if (animations.enter) { - animationTimeout.current.enter = + animationClassNames.enter = `${animations.enter.type}-${id}-enter`; + animationClassNames.appear = animationClassNames.enter; + } + if (animations.exit) { + animationClassNames.exit = `${animations.exit.type}-${id}-exit`; + } + } + return animationClassNames; + }); + const [animationTimeout] = useState(() => { + const animations = schema?.animations; + const animationTimeout = { + enter: 1000, + exit: 1000 + }; + if (animations) { + if (animations.enter) { + animationTimeout.enter = ((animations.enter.duration || 1) + (animations.enter.delay || 0)) * 1000; - animationClassNames.current.enter = `${animations.enter.type}-${id}-enter`; - animationClassNames.current.appear = animationClassNames.current.enter; } if (animations.exit) { - animationTimeout.current.exit = + animationTimeout.exit = ((animations.exit.duration || 1) + (animations.exit.delay || 0)) * 1000; - animationClassNames.current.exit = `${animations.exit.type}-${id}-exit`; } - createAnimationStyle(id, animations); } + return animationTimeout; + }); + + useEffect(() => { + createAnimationStyle(id, schema.animations!); return () => { if (schema.animations) { - let {id} = schema; - id = formateId(id); styleManager.removeStyles(id); } }; }, []); - useEffect(() => { - const observer = new IntersectionObserver( - (entries, observer) => { - entries.forEach(entry => { + function refFn(ref: HTMLDivElement) { + if (ref) { + const observer = new IntersectionObserver( + ([entry], observer) => { if (entry.isIntersecting) { setAnimationShow(true); + setPlaceholderShow(false); observer.disconnect(); } - }); - }, - { - root: null, - rootMargin: '0px', - threshold: 0.1 + }, + { + root: null, + rootMargin: '0px', + threshold: 0.1 + } + ); + if (ref) { + observer.observe(ref); } - ); - if (ref.current) { - observer.observe(ref.current); } - }, [show]); + } - function addAnimationAttention(node: HTMLElement) { - const {attention} = schema.animations || {}; + function handleEntered(node: HTMLElement) { + const {attention, exit, enter} = schema.animations || {}; if (attention) { - let {id} = schema; - id = formateId(id); node.classList.add(`${attention.type}-${id}-attention`); } + + if (exit?.outView || enter?.repeat) { + const observer = new IntersectionObserver( + ([entry], observer) => { + if (!entry.isIntersecting) { + setAnimationShow(false); + observer.disconnect(); + } + }, + { + root: null, + rootMargin: '0px', + threshold: 0.1 + } + ); + observer.observe(node); + } } - function removeAnimationAttention(node: HTMLElement) { + function handleExit(node: HTMLElement) { const {attention} = schema.animations || {}; if (attention) { - let {id} = schema; - id = formateId(id); node.classList.remove(`${attention.type}-${id}-attention`); } } + function handleExited() { + setPlaceholderShow(true); + } + return ( <> - {!animationShow && show && ( -
+ {!animationShow && show && placeholderShow && ( +
{component}
)} diff --git a/packages/amis-core/src/utils/animations.ts b/packages/amis-core/src/utils/animations.ts index 043d8f732e4..624e8cf1b66 100644 --- a/packages/amis-core/src/utils/animations.ts +++ b/packages/amis-core/src/utils/animations.ts @@ -5,6 +5,8 @@ export interface AnimationsProps { type: string; duration?: number; delay?: number; + repeat?: boolean; + inView?: boolean; }; attention?: { type: string; @@ -16,6 +18,8 @@ export interface AnimationsProps { type: string; duration?: number; delay?: number; + repeat?: boolean; + outView?: boolean; }; } diff --git a/packages/amis-core/src/utils/helper.ts b/packages/amis-core/src/utils/helper.ts index 38e5097ec5c..a6f30c67502 100644 --- a/packages/amis-core/src/utils/helper.ts +++ b/packages/amis-core/src/utils/helper.ts @@ -2437,6 +2437,9 @@ export function supportsMjs() { } export function formateId(id: string) { + if (!id) { + return guid(); + } // 将className非法字符替换为短横线 id = id.replace(/[^a-zA-Z0-9-]/g, '-'); // 将连续的-替换为单个- diff --git a/packages/amis-editor/src/tpl/style.tsx b/packages/amis-editor/src/tpl/style.tsx index d85c4ad966f..90765c89fa6 100644 --- a/packages/amis-editor/src/tpl/style.tsx +++ b/packages/amis-editor/src/tpl/style.tsx @@ -1,4 +1,9 @@ -import {setSchemaTpl, getSchemaTpl, defaultValue} from 'amis-editor-core'; +import { + setSchemaTpl, + getSchemaTpl, + defaultValue, + tipedLabel +} from 'amis-editor-core'; import {createAnimationStyle, formateId, type SchemaCollection} from 'amis'; import kebabCase from 'lodash/kebabCase'; import {styleManager} from 'amis-core'; @@ -1532,7 +1537,26 @@ setSchemaTpl('animation', () => { return { title: '动画', body: [ - ...animation('enter', '进入动画'), + ...animation('enter', '进入动画', [ + { + label: tipedLabel('可见时触发', '组件进入可见区域才触发进入动画'), + type: 'switch', + name: 'animations.enter.inView', + value: true, + onChange: (value: any, oldValue: any, obj: any, props: any) => { + if (value === false) { + props.setValueByName('animations.enter.repeat', false); + } + } + }, + { + label: tipedLabel('重复', '组件再次进入可见区域时重复播放动画'), + type: 'switch', + name: 'animations.enter.repeat', + visibleOn: 'animations.enter.inView', + value: false + } + ]), ...animation('attention', '强调动画', [ { label: '重复', @@ -1548,7 +1572,14 @@ setSchemaTpl('animation', () => { ] } ]), - ...animation('exit', '退出动画') + ...animation('exit', '退出动画', [ + { + label: tipedLabel('不可见时触发', '组件退出可见区域触发进入动画'), + type: 'switch', + name: 'animations.exit.outView', + value: true + } + ]) ] }; }); From 62208ac311adba99cd7f3fa307d606b9a2901c28 Mon Sep 17 00:00:00 2001 From: qinhaoyan Date: Mon, 9 Dec 2024 18:15:44 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../amis-core/src/components/Animations.tsx | 122 ++++++++++-------- 1 file changed, 65 insertions(+), 57 deletions(-) diff --git a/packages/amis-core/src/components/Animations.tsx b/packages/amis-core/src/components/Animations.tsx index 66faf620580..9676e46860c 100644 --- a/packages/amis-core/src/components/Animations.tsx +++ b/packages/amis-core/src/components/Animations.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect, useRef} from 'react'; +import React, {useEffect, useMemo, useState, useCallback} from 'react'; import {CSSTransition} from 'react-transition-group'; import {Schema} from '../types'; import {formateId} from '../utils'; @@ -14,13 +14,54 @@ function Animations({ component: any; show: boolean; }) { - const idRef = useRef(formateId(schema.id)); - const id = idRef.current; const {enter} = schema.animations || {}; - const [animationShow, setAnimationShow] = React.useState(!enter?.inView); - const [placeholderShow, setPlaceholderShow] = React.useState(!!enter?.inView); + const [animationShow, setAnimationShow] = useState(!enter?.inView); + const [placeholderShow, setPlaceholderShow] = useState(!!enter?.inView); - const [animationClassNames] = useState(() => { + const id = useMemo(() => formateId(schema.id), []); + const observer = useMemo(newObserver, []); + const animationClassNames = useMemo(initAnimationClassNames, []); + const animationTimeout = useMemo(initAnimationTimeout, []); + + useEffect(() => { + createAnimationStyle(id, schema.animations!); + return () => { + if (schema.animations) { + styleManager.removeStyles(id); + } + observer.disconnect(); + }; + }, []); + + function newObserver() { + return new IntersectionObserver( + (entries, observer) => { + entries.forEach(entry => { + if ( + entry.target.getAttribute('data-role') === 'animation-placeholder' + ) { + if (entry.isIntersecting) { + setAnimationShow(true); + setPlaceholderShow(false); + observer.unobserve(entry.target); + } + } else { + if (!entry.isIntersecting) { + setAnimationShow(false); + observer.unobserve(entry.target); + } + } + }); + }, + { + root: null, + rootMargin: '0px', + threshold: 0.1 + } + ); + } + + function initAnimationClassNames() { const animations = schema?.animations; const animationClassNames = { appear: '', @@ -37,8 +78,9 @@ function Animations({ } } return animationClassNames; - }); - const [animationTimeout] = useState(() => { + } + + function initAnimationTimeout() { const animations = schema?.animations; const animationTimeout = { enter: 1000, @@ -57,77 +99,43 @@ function Animations({ } } return animationTimeout; - }); - - useEffect(() => { - createAnimationStyle(id, schema.animations!); - return () => { - if (schema.animations) { - styleManager.removeStyles(id); - } - }; - }, []); + } - function refFn(ref: HTMLDivElement) { + function refFn(ref: HTMLElement | null) { if (ref) { - const observer = new IntersectionObserver( - ([entry], observer) => { - if (entry.isIntersecting) { - setAnimationShow(true); - setPlaceholderShow(false); - observer.disconnect(); - } - }, - { - root: null, - rootMargin: '0px', - threshold: 0.1 - } - ); - if (ref) { - observer.observe(ref); - } + observer.observe(ref); } } - function handleEntered(node: HTMLElement) { + const handleEntered = useCallback((node: HTMLElement) => { const {attention, exit, enter} = schema.animations || {}; if (attention) { node.classList.add(`${attention.type}-${id}-attention`); } - if (exit?.outView || enter?.repeat) { - const observer = new IntersectionObserver( - ([entry], observer) => { - if (!entry.isIntersecting) { - setAnimationShow(false); - observer.disconnect(); - } - }, - { - root: null, - rootMargin: '0px', - threshold: 0.1 - } - ); observer.observe(node); } - } - function handleExit(node: HTMLElement) { + }, []); + + const handleExit = useCallback((node: HTMLElement) => { const {attention} = schema.animations || {}; if (attention) { node.classList.remove(`${attention.type}-${id}-attention`); } - } + }, []); - function handleExited() { + const handleExited = useCallback(() => { setPlaceholderShow(true); - } + }, []); return ( <> {!animationShow && show && placeholderShow && ( -
+
{component}
)} From c29f99f7b97ffc8c4c4521f1bc5bf0a13f3ff53a Mon Sep 17 00:00:00 2001 From: qinhaoyan Date: Mon, 9 Dec 2024 18:38:49 +0800 Subject: [PATCH 4/5] bugfxi --- packages/amis-core/src/components/Animations.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/amis-core/src/components/Animations.tsx b/packages/amis-core/src/components/Animations.tsx index 9676e46860c..27bcc7b0dca 100644 --- a/packages/amis-core/src/components/Animations.tsx +++ b/packages/amis-core/src/components/Animations.tsx @@ -83,8 +83,8 @@ function Animations({ function initAnimationTimeout() { const animations = schema?.animations; const animationTimeout = { - enter: 1000, - exit: 1000 + enter: 0, + exit: 0 }; if (animations) { if (animations.enter) { From eda805d7dd20156aaf22c4ccb327b6dd553e4241 Mon Sep 17 00:00:00 2001 From: qinhaoyan Date: Tue, 10 Dec 2024 17:27:03 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=BE=AE=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/amis-editor-core/scss/control/_event-action.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/amis-editor-core/scss/control/_event-action.scss b/packages/amis-editor-core/scss/control/_event-action.scss index d1e8466cf82..9588f8b6c2f 100644 --- a/packages/amis-editor-core/scss/control/_event-action.scss +++ b/packages/amis-editor-core/scss/control/_event-action.scss @@ -446,9 +446,6 @@ overflow-y: auto; height: 100%; padding: 0 8px 0 0; - .event-action-radio { - padding-top: 5px; - } .action-desc { margin-left: 16px; color: var(--Form-item-color);