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 { cn } from '@/lib/utils' | ||||
| import { Inter } from 'next/font/google' | ||||
| import { Toaster } from '@/components/ui/toaster' | ||||
| 
 | ||||
| const inter = Inter({ subsets: ['latin'] }) | ||||
| 
 | ||||
| @ -49,6 +50,7 @@ export default function RootLayout({ | ||||
|           disableTransitionOnChange | ||||
|         > | ||||
|           {children} | ||||
|           <Toaster /> | ||||
|         </ThemeProvider> | ||||
|       </body> | ||||
|     </html> | ||||
|  | ||||
| @ -15,7 +15,20 @@ import { MarkdownToolbar } from './components/MarkdownToolbar' | ||||
| import { type PreviewSize } from './constants' | ||||
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' | ||||
| 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() { | ||||
|   const { toast } = useToast() | ||||
| @ -32,6 +45,10 @@ export default function WechatEditor() { | ||||
|   const [previewContent, setPreviewContent] = useState('') | ||||
|   const [cursorPosition, setCursorPosition] = useState<{ start: number; end: number }>({ start: 0, end: 0 }) | ||||
| 
 | ||||
|   // 添加字数和阅读时间状态
 | ||||
|   const [wordCount, setWordCount] = useState('0') | ||||
|   const [readingTime, setReadingTime] = useState('1 分钟') | ||||
| 
 | ||||
|   // 使用自定义 hooks
 | ||||
|   const { handleScroll } = useEditorSync(editorRef) | ||||
|   const { handleEditorChange } = useAutoSave(value, setIsDraft) | ||||
| @ -150,6 +167,49 @@ export default function WechatEditor() { | ||||
|     } | ||||
|   }, [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(() => { | ||||
|     try { | ||||
| @ -228,45 +288,6 @@ export default function WechatEditor() { | ||||
|     ) | ||||
|   }, [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(() => { | ||||
|     if (typeof window === 'undefined') return false | ||||
| @ -365,8 +386,15 @@ export default function WechatEditor() { | ||||
|     setStyleOptions({}) | ||||
|   }, []) | ||||
| 
 | ||||
|   // 更新字数和阅读时间
 | ||||
|   useEffect(() => { | ||||
|     const plainText = previewContent.replace(/<[^>]+>/g, '') | ||||
|     setWordCount(calculateWordCount(plainText)) | ||||
|     setReadingTime(calculateReadingTime(plainText)) | ||||
|   }, [previewContent]) | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="h-full flex flex-col"> | ||||
|     <div className="h-full flex flex-col relative"> | ||||
|       <div className="hidden sm:block"> | ||||
|         <EditorToolbar  | ||||
|           value={value} | ||||
| @ -397,24 +425,7 @@ export default function WechatEditor() { | ||||
|               /> | ||||
|             </div> | ||||
|             <button | ||||
|               onClick={() => { | ||||
|                 handleCopy() | ||||
|                   .then(() => { | ||||
|                     toast({ | ||||
|                       title: "复制成功", | ||||
|                       description: "已复制预览内容到剪贴板", | ||||
|                       duration: 2000 | ||||
|                     }) | ||||
|                   }) | ||||
|                   .catch(() => { | ||||
|                     toast({ | ||||
|                       variant: "destructive", | ||||
|                       title: "复制失败", | ||||
|                       description: "无法访问剪贴板,请检查浏览器权限", | ||||
|                       duration: 2000 | ||||
|                     }) | ||||
|                   }) | ||||
|               }} | ||||
|               onClick={handleCopy} | ||||
|               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" /> | ||||
| @ -465,10 +476,10 @@ export default function WechatEditor() { | ||||
|           <div  | ||||
|             ref={editorRef} | ||||
|             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  | ||||
|                 ? "h-full w-1/2 border-r"  | ||||
|                 : "h-full w-full", | ||||
|                 ? "w-1/2 border-r"  | ||||
|                 : "w-full", | ||||
|               selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles | ||||
|             )} | ||||
|           > | ||||
| @ -498,6 +509,21 @@ export default function WechatEditor() { | ||||
|           )} | ||||
|         </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> | ||||
|   ) | ||||
| } | ||||
| @ -9,6 +9,9 @@ import { type RendererOptions } from '@/lib/markdown' | ||||
| import { ThemeToggle } from '@/components/theme/ThemeToggle' | ||||
| import { Logo } from '@/components/icons/Logo' | ||||
| 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 { | ||||
|   value: string | ||||
| @ -16,8 +19,8 @@ interface EditorToolbarProps { | ||||
|   showPreview: boolean | ||||
|   selectedTemplate: string | ||||
|   onSave: () => void | ||||
|   onCopy: () => void | ||||
|   onCopyPreview: () => void | ||||
|   onCopy: () => Promise<boolean> | ||||
|   onCopyPreview: () => Promise<boolean> | ||||
|   onNewArticle: () => void | ||||
|   onArticleSelect: (article: Article) => void | ||||
|   onTemplateSelect: (template: string) => void | ||||
| @ -43,6 +46,62 @@ export function EditorToolbar({ | ||||
|   onPreviewToggle, | ||||
|   styleOptions | ||||
| }: 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 ( | ||||
|     <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"> | ||||
| @ -104,14 +163,14 @@ export function EditorToolbar({ | ||||
|                 <span>保存</span> | ||||
|               </button> | ||||
|               <button | ||||
|                 onClick={onCopy} | ||||
|                 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" | ||||
|                 onClick={handleCopy} | ||||
|                 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" /> | ||||
|                 <span>复制源码</span> | ||||
|               </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" | ||||
|               > | ||||
|                 <Copy className="h-4 w-4" /> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 tianyaxiang
						tianyaxiang