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}
/>
-
-
+ />
+
+