@@ -156,25 +156,6 @@ const FilePreviewContentInner: React.FC<FilePreviewContentProps> = ({
156156 const [ textHtmlPreview , setTextHtmlPreview ] = useState ( false ) ;
157157 const [ markdownViewMode , setMarkdownViewMode ] = useState < 'preview' | 'source' > ( 'preview' ) ;
158158
159- // 导航箭头自动隐藏
160- const [ navVisible , setNavVisible ] = useState ( true ) ;
161- const navHideTimerRef = useRef < number | null > ( null ) ;
162- const NAV_HIDE_DELAY = 2000 ;
163-
164- const resetNavTimer = useCallback ( ( ) => {
165- setNavVisible ( true ) ;
166- if ( navHideTimerRef . current ) {
167- clearTimeout ( navHideTimerRef . current ) ;
168- }
169- navHideTimerRef . current = window . setTimeout ( ( ) => {
170- setNavVisible ( false ) ;
171- } , NAV_HIDE_DELAY ) ;
172- } , [ ] ) ;
173-
174- const handleMouseMove = useCallback ( ( ) => {
175- resetNavTimer ( ) ;
176- } , [ resetNavTimer ] ) ;
177-
178159 // 标准化文件输入
179160 const normalizedFiles = useMemo ( ( ) => normalizeFiles ( files ) , [ files ] ) ;
180161
@@ -220,7 +201,6 @@ const FilePreviewContentInner: React.FC<FilePreviewContentProps> = ({
220201 setContentNaturalWidth ( 0 ) ;
221202 setContentNaturalHeight ( 0 ) ;
222203 setImageResetKey ( 0 ) ;
223- setNavVisible ( true ) ;
224204 // 重置 epub 状态
225205 setEpubCurrent ( 0 ) ;
226206 setEpubTotal ( 0 ) ;
@@ -236,9 +216,6 @@ const FilePreviewContentInner: React.FC<FilePreviewContentProps> = ({
236216 setTextHtmlPreview ( false ) ;
237217 // 重置 markdown 状态
238218 setMarkdownViewMode ( 'preview' ) ;
239- if ( navHideTimerRef . current ) {
240- clearTimeout ( navHideTimerRef . current ) ;
241- }
242219 } , [ currentIndex ] ) ;
243220
244221 // 图片加载后默认适应窗口
@@ -253,18 +230,6 @@ const FilePreviewContentInner: React.FC<FilePreviewContentProps> = ({
253230 }
254231 } , [ fileType , contentNaturalWidth , contentNaturalHeight ] ) ;
255232
256- // 导航箭头自动隐藏计时器启动 & 清理
257- useEffect ( ( ) => {
258- if ( normalizedFiles . length > 1 ) {
259- resetNavTimer ( ) ;
260- }
261- return ( ) => {
262- if ( navHideTimerRef . current ) {
263- clearTimeout ( navHideTimerRef . current ) ;
264- }
265- } ;
266- } , [ normalizedFiles . length , resetNavTimer ] ) ;
267-
268233 // 键盘导航
269234 // - modal 模式:全局监听(window)
270235 // - embed 模式:只在根容器 focus 时监听,避免影响外部页面交互
@@ -534,7 +499,6 @@ const FilePreviewContentInner: React.FC<FilePreviewContentProps> = ({
534499 < div
535500 ref = { contentRef }
536501 className = "rfp-flex-1 rfp-flex rfp-items-center rfp-justify-center rfp-overflow-auto"
537- onMouseMove = { handleMouseMove }
538502 >
539503 { customRenderer ? (
540504 customRenderer . render ( currentFile , customCtx )
@@ -631,37 +595,16 @@ const FilePreviewContentInner: React.FC<FilePreviewContentProps> = ({
631595 ) }
632596 </ div >
633597
634- { /* 左右导航箭头 - 自动隐藏 */ }
598+ { /* 左右导航箭头 - 自动隐藏(隔离 state,避免拖选时的 mousemove/timer 污染整树 re-render) */ }
635599 { ! headless && normalizedFiles . length > 1 && (
636- < >
637- { currentIndex > 0 && (
638- < motion . button
639- initial = { { opacity : 0 } }
640- animate = { { opacity : navVisible ? 1 : 0 , x : navVisible ? 0 : - 20 } }
641- transition = { { duration : 0.2 } }
642- onClick = { ( ) => onNavigate ?.( currentIndex - 1 ) }
643- onMouseEnter = { ( ) => setNavVisible ( true ) }
644- style = { { pointerEvents : navVisible ? 'auto' : 'none' } }
645- className = "rfp-absolute rfp-z-20 rfp-left-2 md:rfp-left-4 rfp-top-1/2 -rfp-translate-y-1/2 rfp-w-10 rfp-h-10 md:rfp-w-12 md:rfp-h-12 rfp-rounded-full rfp-backdrop-blur-xl rfp-border rfp-flex rfp-items-center rfp-justify-center rfp-transition-colors rfp-shadow-2xl rfp-bg-surface-nav rfp-border-line hover:rfp-bg-surface-nav-hover rfp-text-fg-primary"
646- >
647- < ChevronLeft className = "rfp-w-5 rfp-h-5 md:rfp-w-6 md:rfp-h-6" />
648- </ motion . button >
649- ) }
650-
651- { currentIndex < normalizedFiles . length - 1 && (
652- < motion . button
653- initial = { { opacity : 0 } }
654- animate = { { opacity : navVisible ? 1 : 0 , x : navVisible ? 0 : 20 } }
655- transition = { { duration : 0.2 } }
656- onClick = { ( ) => onNavigate ?.( currentIndex + 1 ) }
657- onMouseEnter = { ( ) => setNavVisible ( true ) }
658- style = { { pointerEvents : navVisible ? 'auto' : 'none' } }
659- className = "rfp-absolute rfp-z-20 rfp-right-2 md:rfp-right-4 rfp-top-1/2 -rfp-translate-y-1/2 rfp-w-10 rfp-h-10 md:rfp-w-12 md:rfp-h-12 rfp-rounded-full rfp-backdrop-blur-xl rfp-border rfp-flex rfp-items-center rfp-justify-center rfp-transition-colors rfp-shadow-2xl rfp-bg-surface-nav rfp-border-line hover:rfp-bg-surface-nav-hover rfp-text-fg-primary"
660- >
661- < ChevronRight className = "rfp-w-5 rfp-h-5 md:rfp-w-6 md:rfp-h-6" />
662- </ motion . button >
663- ) }
664- </ >
600+ < NavArrows
601+ containerRef = { contentRef }
602+ hasPrev = { currentIndex > 0 }
603+ hasNext = { currentIndex < normalizedFiles . length - 1 }
604+ onPrev = { ( ) => onNavigate ?.( currentIndex - 1 ) }
605+ onNext = { ( ) => onNavigate ?.( currentIndex + 1 ) }
606+ resetKey = { currentIndex }
607+ />
665608 ) }
666609 </ div >
667610 </ ThemeProvider >
@@ -696,3 +639,89 @@ const ToolbarButton: React.FC<ToolbarButtonProps> = ({ icon, label, onClick, dis
696639 </ button >
697640 ) ;
698641} ;
642+
643+ // 导航箭头:自带 mousemove 监听 + 2s 自动隐藏定时器,
644+ // state 隔离在本组件,避免 FilePreviewContent 整树因 navVisible 变化而 re-render
645+ interface NavArrowsProps {
646+ containerRef : React . RefObject < HTMLDivElement | null > ;
647+ hasPrev : boolean ;
648+ hasNext : boolean ;
649+ onPrev : ( ) => void ;
650+ onNext : ( ) => void ;
651+ resetKey : number ;
652+ }
653+
654+ const NAV_HIDE_DELAY = 2000 ;
655+
656+ const NavArrows : React . FC < NavArrowsProps > = ( {
657+ containerRef,
658+ hasPrev,
659+ hasNext,
660+ onPrev,
661+ onNext,
662+ resetKey,
663+ } ) => {
664+ const [ visible , setVisible ] = useState ( true ) ;
665+ const timerRef = useRef < number | null > ( null ) ;
666+
667+ const scheduleHide = useCallback ( ( ) => {
668+ if ( timerRef . current ) clearTimeout ( timerRef . current ) ;
669+ timerRef . current = window . setTimeout ( ( ) => setVisible ( false ) , NAV_HIDE_DELAY ) ;
670+ } , [ ] ) ;
671+
672+ const show = useCallback ( ( ) => {
673+ setVisible ( ( prev ) => ( prev ? prev : true ) ) ;
674+ scheduleHide ( ) ;
675+ } , [ scheduleHide ] ) ;
676+
677+ // 监听容器的 mousemove,触发显示+重置隐藏定时器
678+ useEffect ( ( ) => {
679+ const el = containerRef . current ;
680+ if ( ! el ) return ;
681+ const handler = ( ) => show ( ) ;
682+ el . addEventListener ( 'mousemove' , handler ) ;
683+ return ( ) => {
684+ el . removeEventListener ( 'mousemove' , handler ) ;
685+ } ;
686+ } , [ containerRef , show ] ) ;
687+
688+ // currentIndex 切换时,显示一次并重置定时器
689+ useEffect ( ( ) => {
690+ setVisible ( true ) ;
691+ scheduleHide ( ) ;
692+ return ( ) => {
693+ if ( timerRef . current ) clearTimeout ( timerRef . current ) ;
694+ } ;
695+ } , [ resetKey , scheduleHide ] ) ;
696+
697+ return (
698+ < >
699+ { hasPrev && (
700+ < motion . button
701+ initial = { { opacity : 0 } }
702+ animate = { { opacity : visible ? 1 : 0 , x : visible ? 0 : - 20 } }
703+ transition = { { duration : 0.2 } }
704+ onClick = { onPrev }
705+ onMouseEnter = { show }
706+ style = { { pointerEvents : visible ? 'auto' : 'none' } }
707+ className = "rfp-absolute rfp-z-20 rfp-left-2 md:rfp-left-4 rfp-top-1/2 -rfp-translate-y-1/2 rfp-w-10 rfp-h-10 md:rfp-w-12 md:rfp-h-12 rfp-rounded-full rfp-backdrop-blur-xl rfp-border rfp-flex rfp-items-center rfp-justify-center rfp-transition-colors rfp-shadow-2xl rfp-bg-surface-nav rfp-border-line hover:rfp-bg-surface-nav-hover rfp-text-fg-primary"
708+ >
709+ < ChevronLeft className = "rfp-w-5 rfp-h-5 md:rfp-w-6 md:rfp-h-6" />
710+ </ motion . button >
711+ ) }
712+ { hasNext && (
713+ < motion . button
714+ initial = { { opacity : 0 } }
715+ animate = { { opacity : visible ? 1 : 0 , x : visible ? 0 : 20 } }
716+ transition = { { duration : 0.2 } }
717+ onClick = { onNext }
718+ onMouseEnter = { show }
719+ style = { { pointerEvents : visible ? 'auto' : 'none' } }
720+ className = "rfp-absolute rfp-z-20 rfp-right-2 md:rfp-right-4 rfp-top-1/2 -rfp-translate-y-1/2 rfp-w-10 rfp-h-10 md:rfp-w-12 md:rfp-h-12 rfp-rounded-full rfp-backdrop-blur-xl rfp-border rfp-flex rfp-items-center rfp-justify-center rfp-transition-colors rfp-shadow-2xl rfp-bg-surface-nav rfp-border-line hover:rfp-bg-surface-nav-hover rfp-text-fg-primary"
721+ >
722+ < ChevronRight className = "rfp-w-5 rfp-h-5 md:rfp-w-6 md:rfp-h-6" />
723+ </ motion . button >
724+ ) }
725+ </ >
726+ ) ;
727+ } ;
0 commit comments