优化模版
This commit is contained in:
parent
52ee91c20b
commit
57ebc424c4
@ -684,4 +684,14 @@
|
|||||||
/* 暗黑模式下的加载动画 */
|
/* 暗黑模式下的加载动画 */
|
||||||
.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 scrollPercentage = textarea.scrollTop / (textarea.scrollHeight - textarea.clientHeight)
|
||||||
const windowMaxScroll = document.documentElement.scrollHeight - window.innerHeight
|
const previewScrollTop = scrollPercentage * (previewContainer.scrollHeight - previewContainer.clientHeight)
|
||||||
window.scrollTo({
|
|
||||||
top: percentage * windowMaxScroll,
|
previewContainer.scrollTop = previewScrollTop
|
||||||
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)
|
|
||||||
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