66 * 透明度与交互性(而非卸载),使页面状态跨 Esc/重入周期保留,避免动画重播。
77 * 隐藏时设置 inert 防止焦点落入不可见页面(React 18 不支持 inert prop,直接操作 DOM)。
88 *
9- * 动画策略:页面切换的 opacity/transform 入场出场使用 CSS transition
10- * (零 JS 开销,运行在 GPU 合成线程),手势/布局动画保留 Framer Motion。
11- *
129 * @ai -context: 浮层/弹窗组件:FunctionalOverlay。
1310 */
14- import { useEffect , useRef , useState } from 'react' ;
11+ import { useEffect , useRef } from 'react' ;
12+ import { motion } from 'framer-motion' ;
1513import { cn } from '@/lib/utils' ;
1614
1715interface FunctionalOverlayProps {
@@ -27,16 +25,6 @@ interface FunctionalOverlayProps {
2725
2826export function FunctionalOverlay ( { children, visible, className, panelClassName, maskClassName } : FunctionalOverlayProps ) {
2927 const rootRef = useRef < HTMLDivElement > ( null ) ;
30- // 延迟 backdrop-blur:动画期间(前 300ms)不加 blur,避免合成层与过渡动画竞争
31- const [ showBlur , setShowBlur ] = useState ( false ) ;
32-
33- useEffect ( ( ) => {
34- if ( visible ) {
35- const timer = setTimeout ( ( ) => setShowBlur ( true ) , 300 ) ;
36- return ( ) => clearTimeout ( timer ) ;
37- }
38- setShowBlur ( false ) ;
39- } , [ visible ] ) ;
4028
4129 // 隐藏时设置 inert:阻断指针事件、焦点与辅助技术访问(React 18 无 inert prop)
4230 useEffect ( ( ) => {
@@ -49,59 +37,59 @@ export function FunctionalOverlay({ children, visible, className, panelClassName
4937 }
5038 } , [ visible ] ) ;
5139
40+ // 已移除 aria-hidden:inert 属性已完整覆盖其语义(阻断辅助技术+焦点),
41+ // 同时避免 aria-hidden 同步生效而 inert 异步生效导致的焦点竞态警告
5242 return (
53- < div
43+ < motion . div
5444 ref = { rootRef }
5545 className = { cn (
5646 "fixed inset-0 z-10 flex items-center justify-center" ,
5747 "p-2 sm:p-4 md:p-8" ,
58- "pb-16 sm:pb-8 md:pb-8" ,
59- // CSS transition: opacity 300ms ease-out(合成器线程,零 JS 开销)
60- "transition-opacity duration-300 ease-out" ,
61- visible ? "opacity-100" : "opacity-0" ,
48+ "pb-16 sm:pb-8 md:pb-8" , // 移动端底部预留 BottomNav 空间
6249 className
6350 ) }
51+ initial = { { opacity : 0 } }
52+ animate = { { opacity : visible ? 1 : 0 } }
53+ /* 修复:外层过渡统一为 spring 类型,与内层面板保持一致,避免 ease 与 spring 混用导致动画节奏不协调 */
54+ transition = { { type : 'spring' , stiffness : 300 , damping : 30 } }
6455 style = { { pointerEvents : visible ? 'auto' : 'none' } }
6556 >
66- { /* 半透明背景遮罩 — 延迟 backdrop-blur 避免动画期间合成层竞争 */ }
57+ { /* 半透明背景遮罩 — 隐藏时移除 backdrop-blur,避免 Electron 中 backdrop-filter 合成层遮挡下方 canvas */ }
6758 < div className = { cn (
6859 "absolute inset-0 bg-black/20" ,
69- "transition-[backdrop-filter] duration-300 ease-out" ,
70- showBlur && visible && "backdrop-blur-sm" ,
60+ visible && "backdrop-blur-sm" ,
7161 maskClassName ,
7262 visible ? "pointer-events-auto" : "pointer-events-none"
7363 ) } />
7464
75- { /* 功能面板 — CSS transition 处理 scale/y/opacity 入场出场 */ }
76- < div
65+ { /* 功能面板 — 隐藏时移除 backdrop-blur,避免 backdrop-filter 合成层遮挡 canvas */ }
66+ < motion . div
7767 data-work-area = "module-content"
7868 className = { cn (
7969 "relative z-10 w-full" ,
8070 "max-w-5xl" ,
81- "max-h-[calc(100vh-5rem)] sm:max-h-[85vh]" ,
71+ "max-h-[calc(100vh-5rem)] sm:max-h-[85vh]" , // 移动端适配底部导航
8272 "overflow-y-auto" ,
83- "rounded-2xl sm:rounded-[24px_12px_20px_16px]" ,
73+ "rounded-2xl sm:rounded-[24px_12px_20px_16px]" , // 移动端统一圆角
8474 "bg-transparent" ,
85- // CSS transition: 面板从 scale(0.9) + y(30px) 缩放入场
86- "transition-all duration-300 ease-out" ,
87- visible
88- ? "opacity-100 scale-100 translate-y-0"
89- : "opacity-0 scale-90 translate-y-[30px]" ,
90- // backdrop-blur 延迟 300ms 后生效,避免与入场动画竞争
91- showBlur && visible && "backdrop-blur-2xl" ,
75+ visible && "backdrop-blur-2xl" ,
9276 "border border-white/20 dark:border-white/10" ,
9377 "shadow-[0_8px_40px_rgba(0,0,0,0.3)]" ,
9478 panelClassName ,
9579 visible ? "pointer-events-auto" : "pointer-events-none" ,
9680 "p-3 sm:p-5 md:p-8"
9781 ) }
82+ /* 修复:提示浏览器提前创建合成层,避免动画时触发重绘/重排,提升动画流畅度 */
9883 style = { {
9984 willChange : 'transform, opacity' ,
10085 background : 'linear-gradient(180deg, var(--kb-dive-top) 0%, var(--kb-dive-mid) 42%, var(--kb-dive-bot) 100%)' ,
10186 } }
87+ initial = { { scale : 0.9 , y : 30 } }
88+ animate = { visible ? { scale : 1 , y : 0 } : { scale : 0.9 , y : 30 } }
89+ transition = { { type : 'spring' , stiffness : 300 , damping : 30 } }
10290 >
10391 { children }
104- </ div >
105- </ div >
92+ </ motion . div >
93+ </ motion . div >
10694 ) ;
107- }
95+ }
0 commit comments