From f5dff4afa1c9488843326388a37293130ad83953 Mon Sep 17 00:00:00 2001 From: qinhaoyan Date: Mon, 9 Dec 2024 17:27:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=A5=E5=9C=BA=E5=8A=A8=E7=94=BB=E6=94=AF?= =?UTF-8?q?=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 + } + ]) ] }; });