'use client' import { useState, useCallback, useRef, useEffect } from 'react' import { useToast } from '@/components/ui/use-toast' import { ToastAction } from '@/components/ui/toast' import { type RendererOptions } from '@/lib/markdown' import { useAutoSave } from './hooks/useAutoSave' import { EditorToolbar } from './components/EditorToolbar' import { EditorPreview } from './components/EditorPreview' import { MarkdownToolbar } from './components/MarkdownToolbar' import { type PreviewSize } from './constants' import { useLocalStorage } from '@/hooks/use-local-storage' import { codeThemes, type CodeThemeId } from '@/config/code-themes' import '@/styles/code-themes.css' import { templates } from '@/config/wechat-templates' import { cn } from '@/lib/utils' import { usePreviewContent } from './hooks/usePreviewContent' import { useEditorKeyboard } from './hooks/useEditorKeyboard' import { useScrollSync } from './hooks/useScrollSync' import { useWordStats } from './hooks/useWordStats' import { useCopy } from './hooks/useCopy' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { Button } from '@/components/ui/button' import { Copy } from 'lucide-react' import { MobileEditor } from './components/MobileEditor' import { DesktopEditor } from './components/DesktopEditor' export default function WechatEditor() { const { toast } = useToast() const editorRef = useRef(null) const textareaRef = useRef(null) const previewRef = useRef(null) // 状态管理 const [value, setValue] = useState('') const [selectedTemplate, setSelectedTemplate] = useState('default') const [showPreview, setShowPreview] = useState(true) const [styleOptions, setStyleOptions] = useState({}) const [previewSize, setPreviewSize] = useState('medium') const [isDraft, setIsDraft] = useState(false) const [codeTheme, setCodeTheme] = useLocalStorage('code-theme', codeThemes[0].id) // 使用自定义 hooks const { handleEditorChange } = useAutoSave(value, setIsDraft) const { handleEditorScroll } = useScrollSync() // 清除编辑器内容 const handleClear = useCallback(() => { if (window.confirm('确定要清除所有内容吗?')) { setValue('') handleEditorChange('') toast({ title: "已清除", description: "编辑器内容已清除", duration: 2000 }) } }, [handleEditorChange, toast]) // 手动保存 const handleSave = useCallback(() => { try { localStorage.setItem('wechat_editor_content', value) setIsDraft(false) toast({ title: "保存成功", description: "内容已保存到本地", duration: 3000 }) } catch (error) { toast({ variant: "destructive", title: "保存失败", description: "无法保存内容,请检查浏览器存储空间", action: 重试, }) } }, [value, toast]) const { isConverting, previewContent } = usePreviewContent({ value, selectedTemplate, styleOptions, codeTheme }) const { handleKeyDown } = useEditorKeyboard({ value, onChange: (newValue) => { setValue(newValue) handleEditorChange(newValue) }, onSave: handleSave }) // 处理编辑器输入 const handleInput = useCallback((e: React.ChangeEvent) => { const newValue = e.target.value const currentPosition = { start: e.target.selectionStart, end: e.target.selectionEnd, scrollTop: e.target.scrollTop } setValue(newValue) handleEditorChange(newValue) // 使用 requestAnimationFrame 确保在下一帧恢复滚动位置和光标位置 requestAnimationFrame(() => { if (textareaRef.current) { textareaRef.current.scrollTop = currentPosition.scrollTop textareaRef.current.setSelectionRange(currentPosition.start, currentPosition.end) } }) }, [handleEditorChange]) const { copyToClipboard } = useCopy() const handleCopy = useCallback(async (): Promise => { const contentElement = previewRef.current?.querySelector('.preview-content') as HTMLElement | null if (!contentElement) return false const success = await copyToClipboard(contentElement) if (success) { toast({ title: "复制成功", description: "内容已复制,可直接粘贴到公众号编辑器", duration: 2000 }) } else { toast({ variant: "destructive", title: "复制失败", description: "无法访问剪贴板,请检查浏览器权限", duration: 2000 }) } return success }, [copyToClipboard, toast, previewRef]) // 处理放弃草稿 const handleDiscardDraft = useCallback(() => { const savedContent = localStorage.getItem('wechat_editor_content') localStorage.removeItem('wechat_editor_draft') setValue(savedContent || '') setIsDraft(false) toast({ title: "已放弃草稿", description: "已恢复到上次保存的内容", duration: 2000 }) }, [toast]) // 处理文章选择 const handleArticleSelect = useCallback((article: { content: string, template: string }) => { setValue(article.content) setSelectedTemplate(article.template) setIsDraft(false) toast({ title: "加载成功", description: "已加载选中的文章", duration: 2000 }) }, [toast]) // 处理新建文章 const handleNewArticle = useCallback(() => { if (isDraft) { toast({ title: "提示", description: "当前文章未保存,是否继续?", action: ( { setValue('# 新文章\n\n开始写作...') setIsDraft(false) }}> 继续 ), duration: 5000, }) return } setValue('# 新文章\n\n开始写作...') setIsDraft(false) }, [isDraft, toast]) // 处理工具栏插入文本 const handleToolbarInsert = useCallback((text: string, options?: { wrap?: boolean; placeholder?: string; suffix?: string }) => { const textarea = textareaRef.current if (!textarea) return const start = textarea.selectionStart const end = textarea.selectionEnd const selectedText = value.substring(start, end) let newText = '' let newCursorPos = 0 if (options?.wrap && selectedText) { newText = value.substring(0, start) + text + selectedText + (options.suffix || text) + value.substring(end) newCursorPos = start + text.length + selectedText.length + (options.suffix?.length || text.length) } else { const insertText = selectedText || options?.placeholder || '' newText = value.substring(0, start) + text + insertText + (options?.suffix || '') + value.substring(end) newCursorPos = start + text.length + insertText.length + (options?.suffix?.length || 0) } setValue(newText) handleEditorChange(newText) requestAnimationFrame(() => { textarea.focus() textarea.setSelectionRange(newCursorPos, newCursorPos) }) }, [value, handleEditorChange]) // 处理模版选择 const handleTemplateSelect = useCallback((templateId: string) => { setSelectedTemplate(templateId) setStyleOptions({}) }, []) // 检测是否为移动设备 const isMobile = useCallback(() => { if (typeof window === 'undefined') return false return window.innerWidth < 640 }, []) // 自动切换预览模式 useEffect(() => { const handleResize = () => { if (isMobile()) { setPreviewSize('full') } } handleResize() window.addEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize) }, [isMobile]) // 加载已保存的内容 useEffect(() => { const draftContent = localStorage.getItem('wechat_editor_draft') const savedContent = localStorage.getItem('wechat_editor_content') if (draftContent) { setValue(draftContent) setIsDraft(true) toast({ description: "已恢复未保存的草稿", action: 放弃草稿, duration: 5000, }) } else if (savedContent) { setValue(savedContent) } }, [toast, handleDiscardDraft]) const { wordCount, readingTime } = useWordStats(value) return (
{/* 工具栏 */} setSelectedTemplate(templateId)} onTemplateChange={() => {}} onStyleOptionsChange={setStyleOptions} onPreviewToggle={() => setShowPreview(!showPreview)} onCodeThemeChange={setCodeTheme} onClear={handleClear} /> {/* 编辑器主体 */}
{/* 移动设备编辑器 */} {/* 桌面设备编辑器 */}
{/* 底部状态栏 */}
{wordCount} 字
约 {readingTime}
) }