diff --git a/package.json b/package.json index 7b53c0d..ea0ddde 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,14 @@ "@tiptap/react": "^2.2.4", "@tiptap/starter-kit": "^2.2.4", "@types/marked": "^6.0.0", + "@types/prismjs": "^1.26.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.474.0", "marked": "^15.0.6", "next": "14.1.0", "next-themes": "^0.4.4", + "prismjs": "^1.29.0", "react": "^18.2.0", "react-dom": "^18.2.0", "tailwind-merge": "^2.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fff5f1d..61d6233 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: '@types/marked': specifier: ^6.0.0 version: 6.0.0 + '@types/prismjs': + specifier: ^1.26.5 + version: 1.26.5 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -80,6 +83,9 @@ importers: next-themes: specifier: ^0.4.4 version: 0.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + prismjs: + specifier: ^1.29.0 + version: 1.29.0 react: specifier: ^18.2.0 version: 18.3.1 @@ -829,6 +835,9 @@ packages: '@types/node@20.17.16': resolution: {integrity: sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==} + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} @@ -1394,6 +1403,10 @@ packages: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} + prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -2441,6 +2454,8 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/prismjs@1.26.5': {} + '@types/prop-types@15.7.14': {} '@types/react-dom@18.3.5(@types/react@18.3.18)': @@ -2955,6 +2970,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prismjs@1.29.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2636e40..dd890b8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,14 +1,16 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' import './globals.css' +import '@/styles/code-themes.css' import { ThemeProvider } from '@/components/theme/ThemeProvider' import { cn } from '@/lib/utils' -import { Inter } from 'next/font/google' import { Toaster } from '@/components/ui/toaster' const inter = Inter({ subsets: ['latin'] }) -export const metadata = { - title: 'NeuraPress - AI Enhanced Article Editor', - description: 'An intelligent article editor for creating and formatting content', +export const metadata: Metadata = { + title: 'NeuraPress', + description: 'Markdown 转微信公众号内容神器', icons: { icon: [ { diff --git a/src/components/editor/CodeThemeSelector.tsx b/src/components/editor/CodeThemeSelector.tsx new file mode 100644 index 0000000..496c1ac --- /dev/null +++ b/src/components/editor/CodeThemeSelector.tsx @@ -0,0 +1,30 @@ +'use client' + +import { codeThemes, type CodeThemeId } from '@/config/code-themes' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Label } from '@/components/ui/label' + +interface CodeThemeSelectorProps { + value: CodeThemeId + onChange: (value: CodeThemeId) => void +} + +export function CodeThemeSelector({ value, onChange }: CodeThemeSelectorProps) { + return ( +
+ + +
+ ) +} \ No newline at end of file diff --git a/src/components/editor/WechatEditor.tsx b/src/components/editor/WechatEditor.tsx index 15387af..b16de31 100644 --- a/src/components/editor/WechatEditor.tsx +++ b/src/components/editor/WechatEditor.tsx @@ -6,7 +6,7 @@ import { cn } from '@/lib/utils' import { useToast } from '@/components/ui/use-toast' import { ToastAction } from '@/components/ui/toast' import { convertToWechat } from '@/lib/markdown' -import { type RendererOptions } from '@/lib/markdown' +import { type RendererOptions } from '@/lib/types' import { useEditorSync } from './hooks/useEditorSync' import { useAutoSave } from './hooks/useAutoSave' import { EditorToolbar } from './components/EditorToolbar' @@ -16,6 +16,8 @@ import { type PreviewSize } from './constants' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { WechatStylePicker } from '@/components/template/WechatStylePicker' import { Copy, Clock, Type, Trash2 } from 'lucide-react' +import { useLocalStorage } from '@/hooks/use-local-storage' +import { codeThemes, type CodeThemeId } from '@/config/code-themes' // 计算阅读时间(假设每分钟阅读300字) const calculateReadingTime = (text: string): string => { @@ -49,6 +51,9 @@ export default function WechatEditor() { const [wordCount, setWordCount] = useState('0') const [readingTime, setReadingTime] = useState('1 分钟') + // 添加 codeTheme 状态 + const [codeTheme] = useLocalStorage('code-theme', codeThemes[0].id) + // 使用自定义 hooks const { handleScroll } = useEditorSync(editorRef) const { handleEditorChange } = useAutoSave(value, setIsDraft) @@ -151,7 +156,8 @@ export default function WechatEditor() { ...(styleOptions.inline?.listitem || {}), fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px', } - } + }, + codeTheme } const html = convertToWechat(value, mergedOptions) @@ -171,7 +177,7 @@ export default function WechatEditor() { console.error('Template transformation error:', error) return html } - }, [value, selectedTemplate, styleOptions]) + }, [value, selectedTemplate, styleOptions, codeTheme]) // 处理复制 const handleCopy = useCallback(async () => { @@ -249,21 +255,33 @@ export default function WechatEditor() { return () => window.removeEventListener('keydown', handleKeyDown) }, [handleSave]) - // 延迟更新预览内容 + // 更新预览内容 useEffect(() => { - if (!showPreview) return - - setIsConverting(true) const updatePreview = async () => { + if (!value) { + setPreviewContent('') + return + } + + setIsConverting(true) try { const content = getPreviewContent() setPreviewContent(content) + } catch (error) { + console.error('Error updating preview:', error) + toast({ + variant: "destructive", + title: "预览更新失败", + description: "生成预览内容时发生错误", + }) } finally { setIsConverting(false) } } - updatePreview() - }, [value, selectedTemplate, styleOptions, showPreview, getPreviewContent]) + + const timeoutId = setTimeout(updatePreview, 100) + return () => clearTimeout(timeoutId) + }, [value, selectedTemplate, styleOptions, codeTheme, getPreviewContent, toast]) // 加载已保存的内容 useEffect(() => { @@ -285,14 +303,13 @@ export default function WechatEditor() { // 渲染预览内容 const renderPreview = useCallback(() => { - const content = getPreviewContent() return (
) - }, [getPreviewContent]) + }, [previewContent]) // 检测是否为移动设备 const isMobile = useCallback(() => { diff --git a/src/components/editor/components/EditorPreview.tsx b/src/components/editor/components/EditorPreview.tsx index c5fd4ff..8dd8617 100644 --- a/src/components/editor/components/EditorPreview.tsx +++ b/src/components/editor/components/EditorPreview.tsx @@ -2,7 +2,10 @@ import { cn } from '@/lib/utils' import { PREVIEW_SIZES, type PreviewSize } from '../constants' import { Loader2, ZoomIn, ZoomOut, Maximize2, Minimize2 } from 'lucide-react' import { templates } from '@/config/wechat-templates' -import { useState, useRef } from 'react' +import { useState, useRef, useEffect } from 'react' +import { useLocalStorage } from '@/hooks/use-local-storage' +import { codeThemes, type CodeThemeId } from '@/config/code-themes' +import '@/styles/code-themes.css' interface EditorPreviewProps { previewRef: React.RefObject @@ -24,6 +27,7 @@ export function EditorPreview({ const [zoom, setZoom] = useState(100) const [isFullscreen, setIsFullscreen] = useState(false) const isScrolling = useRef(false) + const [codeTheme] = useLocalStorage('code-theme', codeThemes[0].id) const handleZoomIn = () => { setZoom(prev => Math.min(prev + 10, 200)) @@ -50,7 +54,8 @@ export function EditorPreview({ "preview-container bg-background transition-all duration-300 ease-in-out flex flex-col", "h-full sm:w-1/2", "markdown-body relative", - selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles + selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles, + `code-theme-${codeTheme}` )} >
diff --git a/src/components/editor/components/EditorToolbar.tsx b/src/components/editor/components/EditorToolbar.tsx index 1d49ed2..e7fe7d3 100644 --- a/src/components/editor/components/EditorToolbar.tsx +++ b/src/components/editor/components/EditorToolbar.tsx @@ -1,4 +1,7 @@ -import { Copy, Plus, Save, Smartphone } from 'lucide-react' +'use client' + +import { useState } from 'react' +import { Copy, Plus, Save, Smartphone, Settings } from 'lucide-react' import { cn } from '@/lib/utils' import { WechatStylePicker } from '../../template/WechatStylePicker' import { TemplateManager } from '../../template/TemplateManager' @@ -12,6 +15,9 @@ import Link from 'next/link' import { Button } from '@/components/ui/button' import { useToast } from '@/components/ui/use-toast' import { ToastAction } from '@/components/ui/toast' +import { CodeThemeSelector } from '../CodeThemeSelector' +import { useLocalStorage } from '@/hooks/use-local-storage' +import { codeThemes, type CodeThemeId } from '@/config/code-themes' interface EditorToolbarProps { value: string @@ -47,6 +53,7 @@ export function EditorToolbar({ styleOptions }: EditorToolbarProps) { const { toast } = useToast() + const [codeTheme, setCodeTheme] = useLocalStorage('code-theme', codeThemes[0].id) const handleCopy = async () => { try { @@ -117,16 +124,16 @@ export function EditorToolbar({ currentContent={value} onNew={onNewArticle} /> - - + /> + +