This commit is contained in:
tianyaxiang 2025-02-01 15:30:13 +08:00
parent 9e7e3aac4a
commit ef3f54a4fa
3 changed files with 154 additions and 67 deletions

View File

@ -2,6 +2,7 @@ import './globals.css'
import { ThemeProvider } from '@/components/theme/ThemeProvider' import { ThemeProvider } from '@/components/theme/ThemeProvider'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Inter } from 'next/font/google' import { Inter } from 'next/font/google'
import { Toaster } from '@/components/ui/toaster'
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] })
@ -49,6 +50,7 @@ export default function RootLayout({
disableTransitionOnChange disableTransitionOnChange
> >
{children} {children}
<Toaster />
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>

View File

@ -15,7 +15,20 @@ import { MarkdownToolbar } from './components/MarkdownToolbar'
import { type PreviewSize } from './constants' import { type PreviewSize } from './constants'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { WechatStylePicker } from '@/components/template/WechatStylePicker' import { WechatStylePicker } from '@/components/template/WechatStylePicker'
import { Copy } from 'lucide-react' import { Copy, Clock, Type } from 'lucide-react'
// 计算阅读时间假设每分钟阅读300字
const calculateReadingTime = (text: string): string => {
const words = text.trim().length
const minutes = Math.ceil(words / 300)
return `${minutes} 分钟`
}
// 计算字数
const calculateWordCount = (text: string): string => {
const count = text.trim().length
return count.toLocaleString()
}
export default function WechatEditor() { export default function WechatEditor() {
const { toast } = useToast() const { toast } = useToast()
@ -32,6 +45,10 @@ export default function WechatEditor() {
const [previewContent, setPreviewContent] = useState('') const [previewContent, setPreviewContent] = useState('')
const [cursorPosition, setCursorPosition] = useState<{ start: number; end: number }>({ start: 0, end: 0 }) const [cursorPosition, setCursorPosition] = useState<{ start: number; end: number }>({ start: 0, end: 0 })
// 添加字数和阅读时间状态
const [wordCount, setWordCount] = useState('0')
const [readingTime, setReadingTime] = useState('1 分钟')
// 使用自定义 hooks // 使用自定义 hooks
const { handleScroll } = useEditorSync(editorRef) const { handleScroll } = useEditorSync(editorRef)
const { handleEditorChange } = useAutoSave(value, setIsDraft) const { handleEditorChange } = useAutoSave(value, setIsDraft)
@ -150,6 +167,49 @@ export default function WechatEditor() {
} }
}, [value, selectedTemplate, styleOptions]) }, [value, selectedTemplate, styleOptions])
// 处理复制
const handleCopy = useCallback(async () => {
try {
const htmlContent = getPreviewContent()
const tempDiv = document.createElement('div')
tempDiv.innerHTML = htmlContent
const plainText = tempDiv.textContent || tempDiv.innerText
await navigator.clipboard.write([
new ClipboardItem({
'text/html': new Blob([htmlContent], { type: 'text/html' }),
'text/plain': new Blob([plainText], { type: 'text/plain' })
})
])
toast({
title: "复制成功",
description: "已复制预览内容",
duration: 2000
})
return true
} catch (err) {
console.error('Copy error:', err)
try {
await navigator.clipboard.writeText(previewContent)
toast({
title: "复制成功",
description: "已复制预览内容(仅文本)",
duration: 2000
})
return true
} catch (fallbackErr) {
toast({
variant: "destructive",
title: "复制失败",
description: "无法访问剪贴板,请检查浏览器权限",
action: <ToastAction altText="重试"></ToastAction>,
})
return false
}
}
}, [previewContent, toast, getPreviewContent])
// 手动保存 // 手动保存
const handleSave = useCallback(() => { const handleSave = useCallback(() => {
try { try {
@ -228,45 +288,6 @@ export default function WechatEditor() {
) )
}, [getPreviewContent]) }, [getPreviewContent])
const handleCopy = useCallback(async () => {
try {
const htmlContent = getPreviewContent()
const tempDiv = document.createElement('div')
tempDiv.innerHTML = htmlContent
const plainText = tempDiv.textContent || tempDiv.innerText
await navigator.clipboard.write([
new ClipboardItem({
'text/html': new Blob([htmlContent], { type: 'text/html' }),
'text/plain': new Blob([plainText], { type: 'text/plain' })
})
])
toast({
title: "复制成功",
description: "已复制预览内容",
duration: 2000
})
} catch (err) {
console.error('Copy error:', err)
try {
await navigator.clipboard.writeText(previewContent)
toast({
title: "复制成功",
description: "已复制预览内容(仅文本)",
duration: 2000
})
} catch (fallbackErr) {
toast({
variant: "destructive",
title: "复制失败",
description: "无法访问剪贴板,请检查浏览器权限",
action: <ToastAction altText="重试"></ToastAction>,
})
}
}
}, [previewRef, toast, getPreviewContent])
// 检测是否为移动设备 // 检测是否为移动设备
const isMobile = useCallback(() => { const isMobile = useCallback(() => {
if (typeof window === 'undefined') return false if (typeof window === 'undefined') return false
@ -365,8 +386,15 @@ export default function WechatEditor() {
setStyleOptions({}) setStyleOptions({})
}, []) }, [])
// 更新字数和阅读时间
useEffect(() => {
const plainText = previewContent.replace(/<[^>]+>/g, '')
setWordCount(calculateWordCount(plainText))
setReadingTime(calculateReadingTime(plainText))
}, [previewContent])
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col relative">
<div className="hidden sm:block"> <div className="hidden sm:block">
<EditorToolbar <EditorToolbar
value={value} value={value}
@ -397,24 +425,7 @@ export default function WechatEditor() {
/> />
</div> </div>
<button <button
onClick={() => { onClick={handleCopy}
handleCopy()
.then(() => {
toast({
title: "复制成功",
description: "已复制预览内容到剪贴板",
duration: 2000
})
})
.catch(() => {
toast({
variant: "destructive",
title: "复制失败",
description: "无法访问剪贴板,请检查浏览器权限",
duration: 2000
})
})
}}
className="flex items-center justify-center gap-1 px-2 py-1 rounded-md text-xs text-primary hover:bg-muted transition-colors" className="flex items-center justify-center gap-1 px-2 py-1 rounded-md text-xs text-primary hover:bg-muted transition-colors"
> >
<Copy className="h-3.5 w-3.5" /> <Copy className="h-3.5 w-3.5" />
@ -465,10 +476,10 @@ 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", "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]",
showPreview showPreview
? "h-full w-1/2 border-r" ? "w-1/2 border-r"
: "h-full w-full", : "w-full",
selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles
)} )}
> >
@ -498,6 +509,21 @@ export default function WechatEditor() {
)} )}
</div> </div>
</div> </div>
{/* 底部工具栏 */}
<div className="fixed bottom-0 left-0 right-0 bg-background border-t h-10 flex items-center justify-end px-4 gap-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Type className="h-4 w-4" />
<span>{wordCount} </span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground mr-4">
<Clock className="h-4 w-4" />
<span> {readingTime}</span>
</div>
</div>
{/* 为底部工具栏添加间距 */}
<div className="h-10" />
</div> </div>
) )
} }

View File

@ -9,6 +9,9 @@ import { type RendererOptions } from '@/lib/markdown'
import { ThemeToggle } from '@/components/theme/ThemeToggle' import { ThemeToggle } from '@/components/theme/ThemeToggle'
import { Logo } from '@/components/icons/Logo' import { Logo } from '@/components/icons/Logo'
import Link from 'next/link' import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/use-toast'
import { ToastAction } from '@/components/ui/toast'
interface EditorToolbarProps { interface EditorToolbarProps {
value: string value: string
@ -16,8 +19,8 @@ interface EditorToolbarProps {
showPreview: boolean showPreview: boolean
selectedTemplate: string selectedTemplate: string
onSave: () => void onSave: () => void
onCopy: () => void onCopy: () => Promise<boolean>
onCopyPreview: () => void onCopyPreview: () => Promise<boolean>
onNewArticle: () => void onNewArticle: () => void
onArticleSelect: (article: Article) => void onArticleSelect: (article: Article) => void
onTemplateSelect: (template: string) => void onTemplateSelect: (template: string) => void
@ -43,6 +46,62 @@ export function EditorToolbar({
onPreviewToggle, onPreviewToggle,
styleOptions styleOptions
}: EditorToolbarProps) { }: EditorToolbarProps) {
const { toast } = useToast()
const handleCopy = async () => {
try {
const result = await onCopy()
if (result) {
toast({
title: "复制成功",
description: "已复制源码内容",
duration: 2000
})
} else {
toast({
variant: "destructive",
title: "复制失败",
description: "无法访问剪贴板,请检查浏览器权限",
action: <ToastAction altText="重试"></ToastAction>,
})
}
} catch (error) {
toast({
variant: "destructive",
title: "复制失败",
description: "发生错误,请重试",
action: <ToastAction altText="重试"></ToastAction>,
})
}
}
const handleCopyPreview = async () => {
try {
const result = await onCopyPreview()
if (result) {
toast({
title: "复制成功",
description: "已复制预览内容",
duration: 2000
})
} else {
toast({
variant: "destructive",
title: "复制失败",
description: "无法访问剪贴板,请检查浏览器权限",
action: <ToastAction altText="重试"></ToastAction>,
})
}
} catch (error) {
toast({
variant: "destructive",
title: "复制失败",
description: "发生错误,请重试",
action: <ToastAction altText="重试"></ToastAction>,
})
}
}
return ( return (
<div className="flex-none border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-20"> <div className="flex-none border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-20">
<div className="px-4"> <div className="px-4">
@ -104,14 +163,14 @@ export function EditorToolbar({
<span></span> <span></span>
</button> </button>
<button <button
onClick={onCopy} onClick={handleCopy}
className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md bg-muted text-muted-foreground hover:bg-muted/90 text-sm transition-colors" className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md hover:bg-muted text-sm transition-colors"
> >
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
<span></span> <span></span>
</button> </button>
<button <button
onClick={onCopyPreview} onClick={handleCopyPreview}
className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 text-sm transition-colors" className="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 text-sm transition-colors"
> >
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />