优化模版
This commit is contained in:
		
							parent
							
								
									52ee91c20b
								
							
						
					
					
						commit
						57ebc424c4
					
				| @ -685,3 +685,13 @@ | |||||||
| .dark .animate-spin { | .dark .animate-spin { | ||||||
|   color: hsl(var(--primary)); |   color: hsl(var(--primary)); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | @layer utilities { | ||||||
|  |   .scrollbar-none { | ||||||
|  |     scrollbar-width: none; | ||||||
|  |     -ms-overflow-style: none; | ||||||
|  |   } | ||||||
|  |   .scrollbar-none::-webkit-scrollbar { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | }  | ||||||
| @ -393,8 +393,10 @@ export default function WechatEditor() { | |||||||
|     setReadingTime(calculateReadingTime(plainText)) |     setReadingTime(calculateReadingTime(plainText)) | ||||||
|   }, [previewContent]) |   }, [previewContent]) | ||||||
| 
 | 
 | ||||||
|  |   const isScrolling = useRef<boolean>(false) | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="h-full flex flex-col relative"> |     <div className="h-screen flex flex-col overflow-hidden"> | ||||||
|       <div className="hidden sm:block"> |       <div className="hidden sm:block"> | ||||||
|         <EditorToolbar  |         <EditorToolbar  | ||||||
|           value={value} |           value={value} | ||||||
| @ -441,7 +443,7 @@ export default function WechatEditor() { | |||||||
|               <div  |               <div  | ||||||
|                 ref={editorRef} |                 ref={editorRef} | ||||||
|                 className={cn( |                 className={cn( | ||||||
|                   "h-full overflow-y-auto", |                   "h-full", | ||||||
|                   selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles |                   selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles | ||||||
|                 )} |                 )} | ||||||
|               > |               > | ||||||
| @ -450,9 +452,26 @@ export default function WechatEditor() { | |||||||
|                   value={value} |                   value={value} | ||||||
|                   onChange={handleInput} |                   onChange={handleInput} | ||||||
|                   onKeyDown={handleKeyDown} |                   onKeyDown={handleKeyDown} | ||||||
|                   className="w-full h-full resize-none outline-none p-4 font-mono text-base leading-relaxed" |                   className="w-full h-full resize-none outline-none p-4 font-mono text-base leading-relaxed overflow-y-scroll scrollbar-none" | ||||||
|                   placeholder="开始写作..." |                   placeholder="开始写作..." | ||||||
|                   spellCheck={false} |                   spellCheck={false} | ||||||
|  |                   onScroll={(e) => { | ||||||
|  |                     if (isScrolling.current) return | ||||||
|  |                     isScrolling.current = true | ||||||
|  |                     try { | ||||||
|  |                       const textarea = e.currentTarget | ||||||
|  |                       const previewContainer = document.querySelector('.preview-container .overflow-y-auto') | ||||||
|  |                       if (!previewContainer) return | ||||||
|  |                        | ||||||
|  |                       const scrollPercentage = textarea.scrollTop / (textarea.scrollHeight - textarea.clientHeight) | ||||||
|  |                       const previewScrollTop = scrollPercentage * (previewContainer.scrollHeight - previewContainer.clientHeight) | ||||||
|  |                       previewContainer.scrollTop = previewScrollTop | ||||||
|  |                     } finally { | ||||||
|  |                       requestAnimationFrame(() => { | ||||||
|  |                         isScrolling.current = false | ||||||
|  |                       }) | ||||||
|  |                     } | ||||||
|  |                   }} | ||||||
|                 /> |                 /> | ||||||
|               </div> |               </div> | ||||||
|             </TabsContent> |             </TabsContent> | ||||||
| @ -476,7 +495,7 @@ export default function WechatEditor() { | |||||||
|           <div  |           <div  | ||||||
|             ref={editorRef} |             ref={editorRef} | ||||||
|             className={cn( |             className={cn( | ||||||
|               "editor-container bg-background transition-all duration-300 ease-in-out flex flex-col h-[calc(100vh-theme(spacing.16)-theme(spacing.10))] min-h-[600px]", |               "editor-container bg-background transition-all duration-300 ease-in-out flex flex-col h-full", | ||||||
|               showPreview  |               showPreview  | ||||||
|                 ? "w-1/2 border-r"  |                 ? "w-1/2 border-r"  | ||||||
|                 : "w-full", |                 : "w-full", | ||||||
| @ -484,15 +503,32 @@ export default function WechatEditor() { | |||||||
|             )} |             )} | ||||||
|           > |           > | ||||||
|             <MarkdownToolbar onInsert={handleToolbarInsert} /> |             <MarkdownToolbar onInsert={handleToolbarInsert} /> | ||||||
|             <div className="flex-1"> |             <div className="flex-1 overflow-hidden"> | ||||||
|               <textarea |               <textarea | ||||||
|                 ref={textareaRef} |                 ref={textareaRef} | ||||||
|                 value={value} |                 value={value} | ||||||
|                 onChange={handleInput} |                 onChange={handleInput} | ||||||
|                 onKeyDown={handleKeyDown} |                 onKeyDown={handleKeyDown} | ||||||
|                 className="w-full h-full resize-none outline-none p-4 font-mono text-base leading-relaxed" |                 className="w-full h-full resize-none outline-none p-4 font-mono text-base leading-relaxed overflow-y-scroll scrollbar-none" | ||||||
|                 placeholder="开始写作..." |                 placeholder="开始写作..." | ||||||
|                 spellCheck={false} |                 spellCheck={false} | ||||||
|  |                 onScroll={(e) => { | ||||||
|  |                   if (isScrolling.current) return | ||||||
|  |                   isScrolling.current = true | ||||||
|  |                   try { | ||||||
|  |                     const textarea = e.currentTarget | ||||||
|  |                     const previewContainer = document.querySelector('.preview-container .overflow-y-auto') | ||||||
|  |                     if (!previewContainer) return | ||||||
|  |                      | ||||||
|  |                     const scrollPercentage = textarea.scrollTop / (textarea.scrollHeight - textarea.clientHeight) | ||||||
|  |                     const previewScrollTop = scrollPercentage * (previewContainer.scrollHeight - previewContainer.clientHeight) | ||||||
|  |                     previewContainer.scrollTop = previewScrollTop | ||||||
|  |                   } finally { | ||||||
|  |                     requestAnimationFrame(() => { | ||||||
|  |                       isScrolling.current = false | ||||||
|  |                     }) | ||||||
|  |                   } | ||||||
|  |                 }} | ||||||
|               /> |               /> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ import { cn } from '@/lib/utils' | |||||||
| import { PREVIEW_SIZES, type PreviewSize } from '../constants' | import { PREVIEW_SIZES, type PreviewSize } from '../constants' | ||||||
| import { Loader2, ZoomIn, ZoomOut, Maximize2, Minimize2 } from 'lucide-react' | import { Loader2, ZoomIn, ZoomOut, Maximize2, Minimize2 } from 'lucide-react' | ||||||
| import { templates } from '@/config/wechat-templates' | import { templates } from '@/config/wechat-templates' | ||||||
| import { useState } from 'react' | import { useState, useRef } from 'react' | ||||||
| 
 | 
 | ||||||
| interface EditorPreviewProps { | interface EditorPreviewProps { | ||||||
|   previewRef: React.RefObject<HTMLDivElement> |   previewRef: React.RefObject<HTMLDivElement> | ||||||
| @ -23,6 +23,7 @@ export function EditorPreview({ | |||||||
| }: EditorPreviewProps) { | }: EditorPreviewProps) { | ||||||
|   const [zoom, setZoom] = useState(100) |   const [zoom, setZoom] = useState(100) | ||||||
|   const [isFullscreen, setIsFullscreen] = useState(false) |   const [isFullscreen, setIsFullscreen] = useState(false) | ||||||
|  |   const isScrolling = useRef<boolean>(false) | ||||||
| 
 | 
 | ||||||
|   const handleZoomIn = () => { |   const handleZoomIn = () => { | ||||||
|     setZoom(prev => Math.min(prev + 10, 200)) |     setZoom(prev => Math.min(prev + 10, 200)) | ||||||
| @ -47,12 +48,12 @@ export function EditorPreview({ | |||||||
|       ref={previewRef} |       ref={previewRef} | ||||||
|       className={cn( |       className={cn( | ||||||
|         "preview-container bg-background transition-all duration-300 ease-in-out flex flex-col", |         "preview-container bg-background transition-all duration-300 ease-in-out flex flex-col", | ||||||
|         "h-[50%] sm:h-full sm:w-1/2", |         "h-full sm:w-1/2", | ||||||
|         "markdown-body", |         "markdown-body relative", | ||||||
|         selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles |         selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles | ||||||
|       )} |       )} | ||||||
|     > |     > | ||||||
|       <div className="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b flex items-center justify-between z-10 sticky top-0"> |       <div className="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b flex items-center justify-between z-10 sticky top-0 left-0 right-0"> | ||||||
|         <div className="text-sm text-muted-foreground px-2 py-1">预览效果</div> |         <div className="text-sm text-muted-foreground px-2 py-1">预览效果</div> | ||||||
|         <div className="flex items-center gap-4 px-2 py-1"> |         <div className="flex items-center gap-4 px-2 py-1"> | ||||||
|           <div className="flex items-center gap-2"> |           <div className="flex items-center gap-2"> | ||||||
| @ -96,8 +97,23 @@ export function EditorPreview({ | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <div className="flex-1 overflow-y-auto"> |       <div className="flex-1 overflow-y-auto" onScroll={(e) => { | ||||||
|         <div className="min-h-full py-8 px-4"> |         const container = e.currentTarget | ||||||
|  |         const textarea = document.querySelector('.editor-container textarea') | ||||||
|  |         if (!textarea || isScrolling.current) return | ||||||
|  |         isScrolling.current = true | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |           const scrollPercentage = container.scrollTop / (container.scrollHeight - container.clientHeight) | ||||||
|  |           const textareaScrollTop = scrollPercentage * (textarea.scrollHeight - textarea.clientHeight) | ||||||
|  |           textarea.scrollTop = textareaScrollTop | ||||||
|  |         } finally { | ||||||
|  |           requestAnimationFrame(() => { | ||||||
|  |             isScrolling.current = false | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |       }}> | ||||||
|  |         <div className="h-full py-8 px-4"> | ||||||
|           <div  |           <div  | ||||||
|             className={cn( |             className={cn( | ||||||
|               "bg-background mx-auto rounded-lg transition-all duration-300", |               "bg-background mx-auto rounded-lg transition-all duration-300", | ||||||
|  | |||||||
| @ -1,43 +1,23 @@ | |||||||
| import { useCallback, useEffect, useRef } from 'react' | import { useCallback, useRef } from 'react' | ||||||
| 
 | 
 | ||||||
| export function useEditorSync(editorRef: React.RefObject<HTMLDivElement>) { | export function useEditorSync(editorRef: React.RefObject<HTMLDivElement>) { | ||||||
|   // 防止滚动事件循环的标志
 |   // 防止滚动事件循环的标志
 | ||||||
|   const isScrolling = useRef(false) |   const isScrolling = useRef(false) | ||||||
| 
 | 
 | ||||||
|   // 同步滚动处理
 |   // 同步滚动处理
 | ||||||
|   const handleScroll = useCallback((event: Event) => { |   const handleScroll = useCallback((event: React.UIEvent<HTMLTextAreaElement>) => { | ||||||
|     const source = event.target |     if (!editorRef.current || isScrolling.current) return | ||||||
|     // 检查是否是编辑器滚动
 |  | ||||||
|     const isEditor = source instanceof Element && source.closest('.editor-container') |  | ||||||
|      |  | ||||||
|     if (!editorRef.current) return |  | ||||||
|      |  | ||||||
|     const editorElement = editorRef.current.querySelector('.bytemd-editor') |  | ||||||
|     if (!editorElement) return |  | ||||||
|      |  | ||||||
|     // 防止滚动事件循环
 |  | ||||||
|     if (isScrolling.current) return |  | ||||||
|     isScrolling.current = true |     isScrolling.current = true | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|       if (isEditor) { |       const textarea = event.currentTarget | ||||||
|         const sourceScrollTop = (source as Element).scrollTop |       const previewContainer = editorRef.current.parentElement?.querySelector('.preview-container') | ||||||
|         const sourceMaxScroll = (source as Element).scrollHeight - (source as Element).clientHeight |       if (!previewContainer) return | ||||||
|         const percentage = sourceScrollTop / sourceMaxScroll |  | ||||||
| 
 | 
 | ||||||
|         const windowMaxScroll = document.documentElement.scrollHeight - window.innerHeight |       const scrollPercentage = textarea.scrollTop / (textarea.scrollHeight - textarea.clientHeight) | ||||||
|         window.scrollTo({ |       const previewScrollTop = scrollPercentage * (previewContainer.scrollHeight - previewContainer.clientHeight) | ||||||
|           top: percentage * windowMaxScroll, |  | ||||||
|           behavior: 'auto' |  | ||||||
|         }) |  | ||||||
|       } else { |  | ||||||
|         const windowScrollTop = window.scrollY |  | ||||||
|         const windowMaxScroll = document.documentElement.scrollHeight - window.innerHeight |  | ||||||
|         const percentage = windowScrollTop / windowMaxScroll |  | ||||||
|        |        | ||||||
|         const targetScrollTop = percentage * (editorElement.scrollHeight - editorElement.clientHeight) |       previewContainer.scrollTop = previewScrollTop | ||||||
|         editorElement.scrollTop = targetScrollTop |  | ||||||
|       } |  | ||||||
|     } finally { |     } finally { | ||||||
|       // 确保在下一帧重置标志
 |       // 确保在下一帧重置标志
 | ||||||
|       requestAnimationFrame(() => { |       requestAnimationFrame(() => { | ||||||
| @ -46,20 +26,5 @@ export function useEditorSync(editorRef: React.RefObject<HTMLDivElement>) { | |||||||
|     } |     } | ||||||
|   }, [editorRef]) |   }, [editorRef]) | ||||||
| 
 | 
 | ||||||
|   // 添加滚动事件监听
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     const editorElement = editorRef.current?.querySelector('.bytemd-editor') |  | ||||||
|      |  | ||||||
|     if (editorElement) { |  | ||||||
|       editorElement.addEventListener('scroll', handleScroll, { passive: true }) |  | ||||||
|       window.addEventListener('scroll', handleScroll, { passive: true }) |  | ||||||
|        |  | ||||||
|       return () => { |  | ||||||
|         editorElement.removeEventListener('scroll', handleScroll) |  | ||||||
|         window.removeEventListener('scroll', handleScroll) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, [handleScroll]) |  | ||||||
| 
 |  | ||||||
|   return { handleScroll } |   return { handleScroll } | ||||||
| }  | }  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 tianyaxiang
						tianyaxiang