From 17a5b7de2a8aa24ed49c0273dedc0d25340fa134 Mon Sep 17 00:00:00 2001 From: tianyaxiang Date: Wed, 29 Jan 2025 21:20:40 +0800 Subject: [PATCH] clear code --- package.json | 6 +- src/components/editor/WechatEditor.tsx | 190 ++++++++++++------ .../editor/components/EditorPreview.tsx | 85 ++++++-- .../editor/components/MobileToolbar.tsx | 185 ++++++++++++----- src/components/editor/hooks/useDebounce.ts | 17 ++ 5 files changed, 356 insertions(+), 127 deletions(-) create mode 100644 src/components/editor/hooks/useDebounce.ts diff --git a/package.json b/package.json index f62bff8..acb0920 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", + "dev": "NODE_NO_WARNINGS=1 next dev", + "build": "NODE_NO_WARNINGS=1 next build", + "start": "NODE_NO_WARNINGS=1 next start", "lint": "next lint" }, "dependencies": { diff --git a/src/components/editor/WechatEditor.tsx b/src/components/editor/WechatEditor.tsx index 5039989..0c21872 100644 --- a/src/components/editor/WechatEditor.tsx +++ b/src/components/editor/WechatEditor.tsx @@ -7,7 +7,7 @@ import breaks from '@bytemd/plugin-breaks' import frontmatter from '@bytemd/plugin-frontmatter' import math from '@bytemd/plugin-math' import mermaid from '@bytemd/plugin-mermaid' -import { useState, useCallback, useEffect, useRef, useMemo } from 'react' +import { useState, useCallback, useEffect, useRef, useMemo, Suspense } from 'react' import { templates } from '@/config/wechat-templates' import { cn } from '@/lib/utils' import { useToast } from '@/components/ui/use-toast' @@ -36,11 +36,41 @@ export default function WechatEditor() { const [previewSize, setPreviewSize] = useState('medium') const [isConverting, setIsConverting] = useState(false) const [isDraft, setIsDraft] = useState(false) + const [previewContent, setPreviewContent] = useState('') + const [plugins, setPlugins] = useState([]) // 使用自定义 hooks const { handleScroll } = useEditorSync(editorRef) const { handleEditorChange } = useAutoSave(value, setIsDraft) + // 获取预览内容 + const getPreviewContent = useCallback(() => { + if (!value) return '' + + const template = templates.find(t => t.id === selectedTemplate) + const mergedOptions = { + ...styleOptions, + ...(template?.options || {}) + } + + const html = convertToWechat(value, mergedOptions) + if (!template?.transform) return html + + try { + const transformed = template.transform(html) + if (transformed && typeof transformed === 'object') { + const result = transformed as { html?: string; content?: string } + if (result.html) return result.html + if (result.content) return result.content + return JSON.stringify(transformed) + } + return transformed || html + } catch (error) { + console.error('Template transformation error:', error) + return html + } + }, [value, selectedTemplate, styleOptions]) + // 手动保存 const handleSave = useCallback(() => { try { @@ -91,33 +121,6 @@ export default function WechatEditor() { } }, [handleSave]) - const getPreviewContent = useCallback(() => { - if (!value) return '' - - const template = templates.find(t => t.id === selectedTemplate) - const mergedOptions = { - ...styleOptions, - ...(template?.options || {}) - } - - const html = convertToWechat(value, mergedOptions) - if (!template?.transform) return html - - try { - const transformed = template.transform(html) - if (transformed && typeof transformed === 'object') { - const result = transformed as { html?: string; content?: string } - if (result.html) return result.html - if (result.content) return result.content - return JSON.stringify(transformed) - } - return transformed || html - } catch (error) { - console.error('Template transformation error:', error) - return html - } - }, [value, selectedTemplate, styleOptions]) - const copyContent = useCallback(() => { const content = getPreviewContent() navigator.clipboard.writeText(content) @@ -135,7 +138,7 @@ export default function WechatEditor() { }, [getPreviewContent, toast]) const handleCopy = useCallback(async () => { - const previewContent = editorRef.current?.querySelector('.bytemd-preview .markdown-body') + const previewContent = previewRef.current?.querySelector('.preview-content') as HTMLElement | null if (!previewContent) { toast({ variant: "destructive", @@ -155,6 +158,16 @@ export default function WechatEditor() { tempDiv.className = template.styles } + if (!tempDiv.innerHTML.trim()) { + toast({ + variant: "destructive", + title: "复制失败", + description: "预览内容为空", + duration: 2000 + }) + return + } + const htmlBlob = new Blob([tempDiv.innerHTML], { type: 'text/html' }) const textBlob = new Blob([tempDiv.innerText], { type: 'text/plain' }) @@ -174,14 +187,23 @@ export default function WechatEditor() { }) } catch (err) { console.error('Copy error:', err) - toast({ - variant: "destructive", - title: "复制失败", - description: "无法访问剪贴板,请检查浏览器权限", - action: 重试, - }) + try { + await navigator.clipboard.writeText(previewContent.innerText) + toast({ + title: "复制成功", + description: "已复制预览内容(仅文本)", + duration: 2000 + }) + } catch (fallbackErr) { + toast({ + variant: "destructive", + title: "复制失败", + description: "无法访问剪贴板,请检查浏览器权限", + action: 重试, + }) + } } - }, [selectedTemplate, toast]) + }, [selectedTemplate, toast, previewRef]) // 创建编辑器插件 const createEditorPlugin = useCallback((): BytemdPlugin => { @@ -202,26 +224,63 @@ export default function WechatEditor() { } }, []) - // 使用创建的插件 - const plugins = useMemo(() => [ - gfm(), - breaks(), - frontmatter(), - math({ - katexOptions: { - throwOnError: false, - output: 'html' + // 加载插件 + useEffect(() => { + const loadPlugins = async () => { + try { + const [gfmPlugin, breaksPlugin, frontmatterPlugin, mathPlugin, mermaidPlugin, highlightPlugin] = await Promise.all([ + import('@bytemd/plugin-gfm').then(m => m.default()), + import('@bytemd/plugin-breaks').then(m => m.default()), + import('@bytemd/plugin-frontmatter').then(m => m.default()), + import('@bytemd/plugin-math').then(m => m.default({ + katexOptions: { throwOnError: false, output: 'html' } + })), + import('@bytemd/plugin-mermaid').then(m => m.default({ theme: 'default' })), + import('@bytemd/plugin-highlight').then(m => m.default()) + ]) + + setPlugins([ + gfmPlugin, + breaksPlugin, + frontmatterPlugin, + mathPlugin, + mermaidPlugin, + highlightPlugin, + createEditorPlugin() + ]) + } catch (error) { + console.error('Failed to load editor plugins:', error) + toast({ + variant: "destructive", + title: "加载失败", + description: "编辑器插件加载失败,部分功能可能无法使用", + duration: 5000, + }) } - }), - mermaid({ - theme: 'default' - }), - highlight(), - createEditorPlugin() - ], [createEditorPlugin]) + } + + loadPlugins() + }, [createEditorPlugin, toast]) + + // 延迟更新预览内容 + useEffect(() => { + if (!showPreview) return + + setIsConverting(true) + const updatePreview = async () => { + try { + const content = getPreviewContent() + setPreviewContent(content) + } finally { + setIsConverting(false) + } + } + updatePreview() + }, [value, selectedTemplate, styleOptions, showPreview, getPreviewContent]) // 检测是否为移动设备 const isMobile = useCallback(() => { + if (typeof window === 'undefined') return false return window.innerWidth < 640 }, []) @@ -307,16 +366,21 @@ export default function WechatEditor() { flexDirection: 'column' }} > -
- { - return [] - }} - /> -
+ 加载编辑器...}> +
+ { + setValue(v) + handleEditorChange(v) + }} + uploadImages={async (files: File[]) => { + return [] + }} + /> +
+
{showPreview && ( @@ -325,7 +389,7 @@ export default function WechatEditor() { selectedTemplate={selectedTemplate} previewSize={previewSize} isConverting={isConverting} - previewContent={getPreviewContent()} + previewContent={previewContent} onPreviewSizeChange={setPreviewSize} /> )} diff --git a/src/components/editor/components/EditorPreview.tsx b/src/components/editor/components/EditorPreview.tsx index 78f632a..58e83f9 100644 --- a/src/components/editor/components/EditorPreview.tsx +++ b/src/components/editor/components/EditorPreview.tsx @@ -1,7 +1,8 @@ import { cn } from '@/lib/utils' import { PREVIEW_SIZES, type PreviewSize } from '../constants' -import { Loader2 } from 'lucide-react' +import { Loader2, ZoomIn, ZoomOut, Maximize2, Minimize2 } from 'lucide-react' import { templates } from '@/config/wechat-templates' +import { useState } from 'react' interface EditorPreviewProps { previewRef: React.RefObject @@ -20,6 +21,27 @@ export function EditorPreview({ previewContent, onPreviewSizeChange }: EditorPreviewProps) { + const [zoom, setZoom] = useState(100) + const [isFullscreen, setIsFullscreen] = useState(false) + + const handleZoomIn = () => { + setZoom(prev => Math.min(prev + 10, 200)) + } + + const handleZoomOut = () => { + setZoom(prev => Math.max(prev - 10, 50)) + } + + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + previewRef.current?.requestFullscreen() + setIsFullscreen(true) + } else { + document.exitFullscreen() + setIsFullscreen(false) + } + } + return (
预览效果
-
- +
+
+ + {zoom}% + +
+
+ + +
- +
{isConverting ? ( -
- +
+ + 正在生成预览...
) : (
void onCopy: () => void onCopyPreview: () => void + onImageUpload?: (file: File) => Promise + onLinkInsert?: (url: string) => void } export function MobileToolbar({ @@ -16,53 +20,142 @@ export function MobileToolbar({ onPreviewToggle, onSave, onCopy, - onCopyPreview + onCopyPreview, + onImageUpload, + onLinkInsert }: MobileToolbarProps) { + const [zoom, setZoom] = useState(100) + + const handleZoomIn = () => { + setZoom(prev => Math.min(prev + 10, 200)) + } + + const handleZoomOut = () => { + setZoom(prev => Math.max(prev - 10, 50)) + } + + const handleImageUpload = async () => { + if (!onImageUpload) return + + const input = document.createElement('input') + input.type = 'file' + input.accept = 'image/*' + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0] + if (file) { + try { + const url = await onImageUpload(file) + // 处理上传成功后的图片URL + } catch (error) { + console.error('Image upload failed:', error) + } + } + } + input.click() + } + + const handleLinkInsert = () => { + if (!onLinkInsert) return + + const url = window.prompt('请输入链接地址') + if (url) { + onLinkInsert(url) + } + } + return ( -
- - - - +
+
+
+ + +
+ +
+ {showPreview && ( + <> + + {zoom}% + + + )} + + + + + + +
+ + + + +
+
+
+
+
) } \ No newline at end of file diff --git a/src/components/editor/hooks/useDebounce.ts b/src/components/editor/hooks/useDebounce.ts new file mode 100644 index 0000000..f36eb3b --- /dev/null +++ b/src/components/editor/hooks/useDebounce.ts @@ -0,0 +1,17 @@ +import { useState, useEffect } from 'react' + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => { + clearTimeout(timer) + } + }, [value, delay]) + + return debouncedValue +} \ No newline at end of file