// 文章采集编辑页面 - Preline UI 风格 import { useState, useEffect } from 'react' import { useNavigate } 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, FormError } from '../components-preline/Input' import { categoriesApi } from '../api/categories' import { tagsApi } from '../api/tags' import { API_BASE_URL } from '../api/config' // 错误弹窗组件 function ErrorModal({ message, onClose, onClear }) { if (!message) return null return (

采集失败

{message}

) } export function UniversalCrawlEdit() { const navigate = useNavigate() // 获取当前用户信息 const user = JSON.parse(localStorage.getItem('user') || '{}') // 采集步骤 const [step, setStep] = useState('input') // input -> crawling -> edit const [crawlUrl, setCrawlUrl] = useState('') const [crawling, setCrawling] = useState(false) const [crawlError, setCrawlError] = useState('') // 表单数据 const [form, setForm] = useState({ title: '', content: '', summary: '', category_id: '', tag_ids: [], is_published: false, is_draft: true }) // 采集的图片 const [crawledImages, setCrawledImages] = useState([]) // 分类和标签 const [categories, setCategories] = useState([]) const [tags, setTags] = useState([]) // 保存状态 const [saving, setSaving] = useState(false) // 错误弹窗 const [showErrorModal, setShowErrorModal] = useState(false) const [errorModalMessage, setErrorModalMessage] = useState('') // 加载分类和标签,并清空之前的状态 useEffect(() => { // 清空表单状态,防止显示之前缓存的数据 setForm({ title: '', content: '', summary: '', category_id: '', tag_ids: [], is_published: false, is_draft: true }) setCrawledImages([]) setStep('input') setCrawlError('') const loadData = async () => { try { const [catRes, tagRes] = await Promise.all([ categoriesApi.getCategories(), tagsApi.getTags() ]) setCategories(catRes.data) setTags(tagRes.data) } catch (error) { console.error('加载数据失败:', error) } } loadData() }, []) // 开始采集 const handleCrawl = async (e) => { e.preventDefault() if (!crawlUrl.trim()) { alert('请输入文章链接') return } if (!crawlUrl.startsWith('http://') && !crawlUrl.startsWith('https://')) { alert('请输入有效的URL(以 http:// 或 https:// 开头)') return } setCrawling(true) setCrawlError('') try { const token = localStorage.getItem('token') // 添加随机数防止缓存 const nocache = Date.now() // 构造请求头:有 token 则带 Bearer,同时启用 credentials 以支持 SSO Cookie 认证 const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } if (token) { headers['Authorization'] = `Bearer ${token}` } const response = await fetch(`${API_BASE_URL}/universal-crawl/fetch?_=${nocache}`, { method: 'POST', headers, body: JSON.stringify({ url: crawlUrl }), credentials: 'include' }) const data = await response.json() if (!data.success) { const errorMsg = data.error || '采集失败,请检查链接是否有效' setCrawlError(errorMsg) setErrorModalMessage(errorMsg) setShowErrorModal(true) setCrawling(false) return } // 填充表单 setForm({ title: data.title || '', content: data.content || '', summary: data.summary || '', category_id: '', tag_ids: [], is_published: false, is_draft: true }) // 设置图片 setCrawledImages(data.images || []) // 进入编辑步骤 setStep('edit') } catch (error) { const errorMsg = '采集失败: ' + (error.message || '网络错误,请稍后重试') setCrawlError(errorMsg) setErrorModalMessage(errorMsg) setShowErrorModal(true) } finally { setCrawling(false) } } // 保存文章 const handleSubmit = async (e, isDraft = false) => { e.preventDefault() if (!form.title.trim() || !form.content.trim()) { alert('请填写标题和内容') return } setSaving(true) try { const token = localStorage.getItem('token') // 将 tag_ids 转换为 tag 名称数组 const tagNames = form.tag_ids.map(tid => { const tag = tags.find(t => t.id === Number(tid)) return tag ? tag.name : '' }).filter(Boolean) const response = await fetch(`${API_BASE_URL}/universal-crawl/save`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }, body: JSON.stringify({ title: form.title, content: form.content, content_html: '', images: crawledImages, summary: form.summary, category_id: form.category_id || null, is_draft: isDraft, is_published: !isDraft, tags: tagNames, source_url: crawlUrl }), credentials: 'include' }) const data = await response.json() if (!data.success) { throw new Error(data.detail || data.message || '保存失败') } // 提示图片下载情况 if (data.images_failed > 0) { alert(`文章已保存成功!\n图片:${data.images_saved} 张下载成功,${data.images_failed} 张下载失败(保留原始链接)。`) } navigate('/admin/posts') } catch (error) { alert('保存失败: ' + (error.message || error.response?.data?.detail || '未知错误')) } finally { setSaving(false) } } // 在内容中插入图片 const insertImageToContent = (imagePath) => { const imageMarkdown = `\n\n![图片](${imagePath})\n\n` setForm({ ...form, content: form.content + imageMarkdown }) } // 取消/返回 const handleCancel = () => { if (confirm('确定要取消吗?未保存的内容将丢失。')) { navigate('/admin/posts') } } // 退出登录 const handleLogout = async () => { localStorage.removeItem('token') await authApi.logout() localStorage.removeItem('user') window.location.href = '/login' } // 输入URL界面 if (step === 'input') { return ( setShowErrorModal(false)} onClear={() => setCrawlUrl('')} />
{/* 页面标题 */}

通用采集

从网页自动采集文章内容(增强反爬能力)

从网页自动采集 输入文章链接,自动提取标题、正文和图片,自动处理编码和数学公式
setCrawlUrl(e.target.value)} placeholder="https://example.com/article" disabled={crawling} required />

支持各类网站的反爬场景采集

{crawlError && (
采集失败

{crawlError}

)}
{crawling && (

正在分析网页内容...

)}
使用说明 采集功能的使用注意事项
  • 支持各类网站、技术博客、学术论文等公开内容的增强采集
  • 采集后需要人工编辑确认内容准确性
  • 系统会自动提取标题、正文和图片
  • 采集的内容默认保存为草稿,需要手动发布
  • 请尊重原作者版权,仅用于个人学习参考
) } // 编辑界面 return (
{/* 页面标题 */}

编辑通用采集内容

确认并编辑通用采集的文章内容

{/* 左侧:主要内容 */}
文章内容 编辑文章标题、摘要和正文
setForm({...form, title: e.target.value})} placeholder="请输入标题" required />