// 文章编辑页面 - 支持 Markdown / HTML 双格式 import { useState, useEffect, useRef, useCallback } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { AdminLayout } from '../components-preline/Sidebar' import { authApi } from '../api/auth' import { Card, CardHeader, CardTitle, CardDescription, CardBody, CardFooter } from '../components-preline/Card' import { Button } from '../components-preline/Button' import { Input, Textarea, Label } from '../components-preline/Input' import { postsApi } from '../api/posts' import { categoriesApi } from '../api/categories' import { tagsApi } from '../api/tags' import { API_BASE_URL } from '../api/config' import MDEditor from '@uiw/react-md-editor' import '@uiw/react-md-editor/markdown-editor.css' import ReactQuill from 'react-quill-new' import 'react-quill-new/dist/quill.snow.css' // 图片上传组件 function ImageUploader({ uploadedImages, setUploadedImages, onInsertImage }) { const fileInputRef = useRef(null) const [uploading, setUploading] = useState(false) const handleImageUpload = async (e) => { const files = e.target.files if (!files || files.length === 0) return setUploading(true) const formData = new FormData() for (let i = 0; i < files.length; i++) { formData.append('files', files[i]) } try { const token = localStorage.getItem('token') const res = await fetch(`${API_BASE_URL}/images/upload-multiple`, { method: 'POST', headers: { ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }, credentials: 'include', body: formData }) const data = await res.json() if (data.uploaded && data.uploaded.length > 0) { setUploadedImages(prev => [...prev, ...data.uploaded]) } } catch (error) { alert('上传失败') } finally { setUploading(false) } } return (
{uploadedImages.length > 0 && (

本次上传的图片

{uploadedImages.map((img) => (
onInsertImage(img)} title="点击插入到文章" > {img.original_name}
))}

点击图片插入到文章末尾

)}

使用说明

) } // 标签选择器 function TagSelector({ tags, selectedIds, onToggle }) { return (
{tags.map(tag => ( ))}
) } export function PostEdit() { const { id } = useParams() const navigate = useNavigate() const isEdit = Boolean(id) const quillRef = useRef(null) const [form, setForm] = useState({ title: '', content: '', content_type: 'markdown', summary: '', category_id: '', tag_ids: [], is_published: true, is_draft: false }) const [categories, setCategories] = useState([]) const [tags, setTags] = useState([]) const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [uploadedImages, setUploadedImages] = useState([]) const user = JSON.parse(localStorage.getItem('user') || '{}') useEffect(() => { fetchCategories() fetchTags() if (isEdit) { fetchPost() } else { // 新建文章时重置表单,防止从编辑页切换过来残留旧数据 setForm({ title: '', content: '', content_type: 'markdown', summary: '', category_id: '', tag_ids: [], is_published: true, is_draft: false }) } }, [id]) const fetchCategories = async () => { try { const res = await categoriesApi.getCategories() setCategories(res.data) } catch (error) { console.error('获取分类失败', error) } } const fetchTags = async () => { try { const res = await tagsApi.getTags() setTags(res.data) } catch (error) { console.error('获取标签失败', error) } } const fetchPost = async () => { setLoading(true) try { const res = await postsApi.getPost(id) const post = res.data setForm({ title: post.title, content: post.content, content_type: post.content_type || 'markdown', summary: post.summary || '', category_id: post.category_id || '', tag_ids: post.tags.map(t => t.id), is_published: post.is_published, is_draft: post.is_draft }) } catch (error) { alert('获取文章失败') navigate('/admin/posts') } finally { setLoading(false) } } const handleSubmit = async (e, isDraft = false) => { e.preventDefault() if (!form.title.trim() || !form.content.trim()) { alert('请填写标题和内容') return } setSaving(true) try { const data = { ...form, category_id: form.category_id || null, is_draft: isDraft } if (isEdit) { await postsApi.updatePost(id, data) } else { await postsApi.createPost(data) } navigate('/admin/posts') } catch (error) { alert('保存失败: ' + (error.response?.data?.detail || '未知错误')) } finally { setSaving(false) } } const handleChange = (e) => { const { name, value, type, checked } = e.target setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value })) } const handleTagToggle = (tagId) => { setForm(prev => ({ ...prev, tag_ids: prev.tag_ids.includes(tagId) ? prev.tag_ids.filter(id => id !== tagId) : [...prev.tag_ids, tagId] })) } // 插入图片:根据当前模式决定格式 const insertImageToEditor = useCallback((image) => { if (form.content_type === 'html') { // HTML 模式:通过 Quill API 插入 if (quillRef.current) { const quill = quillRef.current.getEditor() const range = quill.getSelection(true) const index = range ? range.index : quill.getLength() quill.insertEmbed(index, 'image', image.file_path, 'user') quill.setSelection(index + 1, 0, 'silent') } } else { // Markdown 模式:追加 markdown 图片语法 const markdown = `\n![${image.original_name}](${image.file_path})\n` setForm(prev => ({ ...prev, content: prev.content + markdown })) } }, [form.content_type]) const handleLogout = async () => { localStorage.removeItem('token') await authApi.logout() localStorage.removeItem('user') window.location.href = '/login' } const quillModules = { toolbar: [ [{ header: [1, 2, 3, false] }], ['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ indent: '-1' }, { indent: '+1' }], ['link', 'image'], ['blockquote', 'code-block'], [{ color: [] }, { background: [] }], ['clean'] ] } if (loading) { return (

加载中...

) } return (

{isEdit ? '编辑文章' : '写文章'}

{isEdit ? '修改文章内容和设置' : '创建新的博客文章'}

handleSubmit(e, false)}>
{/* 主编辑区 */}
{/* 标题 */}
{/* 格式切换 */}
{/* 摘要 */}