add logo
This commit is contained in:
parent
9e7e3aac4a
commit
ef3f54a4fa
@ -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>
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -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" />
|
||||||
|
Loading…
Reference in New Issue
Block a user