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
	 tianyaxiang
						tianyaxiang