/** * 论坛创建帖子组件 - GitHub Discussions * Forum create post component - GitHub Discussions */ import { useState, useRef, useCallback } from 'react'; import { ArrowLeft, Send, AlertCircle, Eye, Edit3, Bold, Italic, Code, Link, List, Image, Quote, HelpCircle, Upload, Loader2 } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { getForumService } from '../../services/forum'; import type { Category } from '../../services/forum'; import { parseEmoji } from './utils'; import './ForumCreatePost.css'; interface ForumCreatePostProps { categories: Category[]; isEnglish: boolean; onBack: () => void; onCreated: () => void; } type EditorTab = 'write' | 'preview'; export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: ForumCreatePostProps) { const [title, setTitle] = useState(''); const [body, setBody] = useState(''); const [categoryId, setCategoryId] = useState(null); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState('write'); const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [isDragging, setIsDragging] = useState(false); const textareaRef = useRef(null); const fileInputRef = useRef(null); const forumService = getForumService(); /** * 处理图片上传 * Handle image upload */ const handleImageUpload = useCallback(async (file: File) => { if (uploading) return; setUploading(true); setUploadProgress(0); setError(null); try { const imageUrl = await forumService.uploadImage(file, (progress) => { setUploadProgress(progress); }); // 插入 Markdown 图片语法 | Insert Markdown image syntax const textarea = textareaRef.current; if (textarea) { const start = textarea.selectionStart; const end = textarea.selectionEnd; const imageMarkdown = `![${file.name}](${imageUrl})`; const newBody = body.substring(0, start) + imageMarkdown + body.substring(end); setBody(newBody); // 恢复光标位置 | Restore cursor position setTimeout(() => { textarea.focus(); const newPos = start + imageMarkdown.length; textarea.setSelectionRange(newPos, newPos); }, 0); } else { // 如果没有 textarea,直接追加到末尾 | Append to end if no textarea setBody(prev => prev + `\n![${file.name}](${imageUrl})`); } } catch (err) { console.error('[ForumCreatePost] Upload failed:', err); setError(err instanceof Error ? err.message : (isEnglish ? 'Failed to upload image' : '图片上传失败')); } finally { setUploading(false); setUploadProgress(0); } }, [body, forumService, isEnglish, uploading]); /** * 处理拖拽事件 * Handle drag events */ const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const files = Array.from(e.dataTransfer.files); const imageFile = files.find(f => f.type.startsWith('image/')); if (imageFile) { handleImageUpload(imageFile); } }, [handleImageUpload]); /** * 处理粘贴事件 * Handle paste event */ const handlePaste = useCallback((e: React.ClipboardEvent) => { const items = Array.from(e.clipboardData.items); const imageItem = items.find(item => item.type.startsWith('image/')); if (imageItem) { e.preventDefault(); const file = imageItem.getAsFile(); if (file) { handleImageUpload(file); } } }, [handleImageUpload]); /** * 处理文件选择 * Handle file selection */ const handleFileSelect = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { handleImageUpload(file); } // 清空 input 以便重复选择同一文件 | Clear input to allow selecting same file again e.target.value = ''; }, [handleImageUpload]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); // 验证 | Validation if (!title.trim()) { setError(isEnglish ? 'Please enter a title' : '请输入标题'); return; } if (!body.trim()) { setError(isEnglish ? 'Please enter content' : '请输入内容'); return; } if (!categoryId) { setError(isEnglish ? 'Please select a category' : '请选择分类'); return; } setSubmitting(true); try { const post = await forumService.createPost({ title: title.trim(), body: body.trim(), categoryId }); if (post) { onCreated(); } else { setError(isEnglish ? 'Failed to create discussion' : '创建讨论失败,请稍后重试'); } } catch (err) { console.error('[ForumCreatePost] Error:', err); setError(err instanceof Error ? err.message : (isEnglish ? 'An error occurred' : '发生错误,请稍后重试')); } finally { setSubmitting(false); } }; // 插入 Markdown 语法 | Insert Markdown syntax const insertMarkdown = (prefix: string, suffix: string = '', placeholder: string = '') => { const textarea = textareaRef.current; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = body.substring(start, end) || placeholder; const newBody = body.substring(0, start) + prefix + selectedText + suffix + body.substring(end); setBody(newBody); // 恢复光标位置 | Restore cursor position setTimeout(() => { textarea.focus(); const newCursorPos = start + prefix.length + selectedText.length; textarea.setSelectionRange(newCursorPos, newCursorPos); }, 0); }; const toolbarButtons = [ { icon: , action: () => insertMarkdown('**', '**', 'bold'), title: isEnglish ? 'Bold' : '粗体' }, { icon: , action: () => insertMarkdown('*', '*', 'italic'), title: isEnglish ? 'Italic' : '斜体' }, { icon: , action: () => insertMarkdown('`', '`', 'code'), title: isEnglish ? 'Inline code' : '行内代码' }, { icon: , action: () => insertMarkdown('[', '](url)', 'link text'), title: isEnglish ? 'Link' : '链接' }, { icon: , action: () => insertMarkdown('\n- ', '', 'list item'), title: isEnglish ? 'List' : '列表' }, { icon: , action: () => insertMarkdown('\n> ', '', 'quote'), title: isEnglish ? 'Quote' : '引用' }, { icon: , action: () => fileInputRef.current?.click(), title: isEnglish ? 'Upload image' : '上传图片' }, ]; const selectedCategory = categories.find(c => c.id === categoryId); return (
{/* 返回按钮 | Back button */}
{/* 左侧:编辑区 | Left: Editor */}

{isEnglish ? 'Start a Discussion' : '发起讨论'}

{selectedCategory && ( {parseEmoji(selectedCategory.emoji)} {selectedCategory.name} )}
{/* 分类选择 | Category selection */}
{categories.map(cat => ( ))}
{/* 标题 | Title */}
setTitle(e.target.value)} placeholder={isEnglish ? 'Enter a descriptive title...' : '输入一个描述性的标题...'} maxLength={200} /> {title.length}/200
{/* 编辑器 | Editor */}
{activeTab === 'write' && (
{toolbarButtons.map((btn, idx) => ( ))}
)}
{/* 隐藏的文件输入 | Hidden file input */} {/* 上传进度提示 | Upload progress indicator */} {uploading && (
{isEnglish ? 'Uploading...' : '上传中...'} {uploadProgress}%
)} {/* 拖拽提示 | Drag hint */} {isDragging && !uploading && (
{isEnglish ? 'Drop image here' : '拖放图片到这里'}
)} {activeTab === 'write' ? (