import { useEffect, useState, useCallback, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Button, Input, Select, Space, message, Spin, Upload } from 'antd'; import { ArrowLeftOutlined, SaveOutlined, SendOutlined, UploadOutlined } from '@ant-design/icons'; import { Editor, Toolbar } from '@wangeditor/editor-for-react'; import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'; import { articleApi, articleCategoryApi, articleTagApi, type Article, type ArticleTagItem, } from '../../api/health/articles'; import { useThemeMode } from '../../hooks/useThemeMode'; import { AuthButton } from '../../components/AuthButton'; import client, { handleApiError } from '../../api/client'; import '@wangeditor/editor/dist/css/style.css'; export default function ArticleEditor() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const isEdit = Boolean(id); const isDark = useThemeMode(); // 表单状态 const [title, setTitle] = useState(''); const [summary, setSummary] = useState(''); const [content, setContent] = useState(''); const [coverImage, setCoverImage] = useState(''); const [slug, setSlug] = useState(''); const [categoryId, setCategoryId] = useState(undefined); const [selectedTagIds, setSelectedTagIds] = useState([]); const [sortOrder, setSortOrder] = useState(0); const [version, setVersion] = useState(0); // 选项数据 const [categories, setCategories] = useState<{ id: string; name: string }[]>([]); const [tags, setTags] = useState([]); // UI 状态 const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [editor, setEditor] = useState(null); // 加载分类和标签 useEffect(() => { const fetchOptions = async () => { try { const [cats, tagList] = await Promise.all([ articleCategoryApi.list(), articleTagApi.list(), ]); setCategories(cats.map((c) => ({ id: c.id, name: c.name }))); setTags(tagList); } catch { // 选项加载失败不阻塞 } }; fetchOptions(); }, []); // 编辑模式:加载现有文章 useEffect(() => { if (!id) return; const fetchArticle = async () => { setLoading(true); try { const article: Article = await articleApi.get(id); setTitle(article.title); setSummary(article.summary || ''); setContent(article.content || ''); setCoverImage(article.cover_image || ''); setSlug(article.slug || ''); setCategoryId(article.category_id); setSelectedTagIds(article.tags?.map((t) => t.id) || []); setSortOrder(article.sort_order); setVersion(article.version); } catch { message.error('加载文章失败'); navigate('/health/articles'); } finally { setLoading(false); } }; fetchArticle(); }, [id, navigate]); // 编辑器配置 const toolbarConfig = useMemo>( () => ({ excludeKeys: [ 'group-video', 'insertLink', 'editLink', 'unLink', 'viewLink', 'codeView', ], }), [], ); const editorConfig = useMemo>( () => ({ placeholder: '请输入文章内容...', MENU_CONF: { uploadImage: { async customUpload(file: File, insertFn: (url: string, alt?: string, href?: string) => void) { try { const formData = new FormData(); formData.append('file', file); const { data: result } = await client.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); const url: string = result.data.url; const token = localStorage.getItem('access_token'); const urlWithToken = token ? `${url}?token=${token}` : url; insertFn(urlWithToken, file.name, urlWithToken); } catch { message.error('图片上传失败'); } }, }, }, }), [], ); // 及时销毁编辑器 useEffect(() => { return () => { if (editor) { editor.destroy(); setEditor(null); } }; }, [editor]); const handleSave = useCallback(async () => { if (!title.trim()) { message.warning('请输入文章标题'); return; } setSaving(true); try { if (isEdit && id) { await articleApi.update(id, { title, summary: summary || undefined, content, cover_image: coverImage || undefined, slug: slug || undefined, category_id: categoryId, tag_ids: selectedTagIds, sort_order: sortOrder, version, }); message.success('文章已保存'); // 重新加载以获取新 version const updated = await articleApi.get(id); setVersion(updated.version); } else { await articleApi.create({ title, summary: summary || undefined, content, cover_image: coverImage || undefined, slug: slug || undefined, category_id: categoryId, tag_ids: selectedTagIds, sort_order: sortOrder, }); message.success('文章已创建'); // 返回列表页,避免 WangEditor 全局 toolbar 注册导致同组件内重复初始化 navigate('/health/articles'); } } catch (err: unknown) { handleApiError(err, '保存失败'); } finally { setSaving(false); } }, [ id, isEdit, title, summary, content, coverImage, slug, categoryId, selectedTagIds, sortOrder, version, navigate, ]); const handleSubmit = useCallback(async () => { if (!title.trim()) { message.warning('请输入文章标题'); return; } setSaving(true); try { // 先保存 let currentVersion = version; if (isEdit && id) { await articleApi.update(id, { title, summary: summary || undefined, content, cover_image: coverImage || undefined, slug: slug || undefined, category_id: categoryId, tag_ids: selectedTagIds, sort_order: sortOrder, version, }); const updated = await articleApi.get(id); currentVersion = updated.version; setVersion(updated.version); } else { const _created = await articleApi.create({ title, summary: summary || undefined, content, cover_image: coverImage || undefined, slug: slug || undefined, category_id: categoryId, tag_ids: selectedTagIds, sort_order: sortOrder, }); currentVersion = _created.version; setVersion(_created.version); await articleApi.submit(_created.id, currentVersion); message.success('已提交审核'); navigate('/health/articles'); return; } // 编辑模式提交审核 if (id) { await articleApi.submit(id, currentVersion); } message.success('已提交审核'); navigate('/health/articles'); } catch (err: unknown) { handleApiError(err, '提交审核失败'); } finally { setSaving(false); } }, [ id, isEdit, title, summary, content, coverImage, slug, categoryId, selectedTagIds, sortOrder, version, navigate, ]); if (loading) { return (
); } return (
{/* 页面标题栏 */}
{/* 主体布局: 左侧编辑区 + 右侧设置面板 */}
{/* 左侧: 富文本编辑器 — key 变化时强制重新挂载,防止 WangEditor toolbar 重复创建 */}
setContent(editorInstance.getHtml())} mode="default" style={{ minHeight: 500, background: isDark ? '#111827' : '#FFFFFF', color: isDark ? '#e2e8f0' : '#1e293b', }} />
{/* 右侧: 设置面板 */}
{/* 标题 */}
setTitle(e.target.value)} placeholder="请输入文章标题" maxLength={200} showCount />
{/* 分类 */}
({ label: t.name, value: t.id }))} maxTagCount={5} />
{/* 摘要 */}
setSummary(e.target.value)} placeholder="请输入文章摘要" rows={3} maxLength={500} showCount />
{/* 封面图 */}
setCoverImage(e.target.value)} placeholder="请输入封面图片 URL 或上传文件" style={{ flex: 1 }} /> { try { const formData = new FormData(); formData.append('file', file); const { data: result } = await client.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); setCoverImage(result.data.url); message.success('封面图上传成功'); } catch { message.error('封面图上传失败'); } return false; }} > {coverImage && (
封面预览 { (e.target as HTMLImageElement).style.display = 'none'; }} />
)}
{/* Slug */}
setSlug(e.target.value)} placeholder="例如: health-tips-for-elderly" />
{/* 排序 */}
setSortOrder(Number(e.target.value))} placeholder="0" />
); }