// 修复后的 Vue 3 setup 函数体 // 所有变量和方法必须在 return 之前定义 const API_BASE = window.location.origin; // 1. 状态变量 const isLoggedIn = ref(false); const isAdmin = ref(false); const loginForm = reactive({ username: '', password: '' }); const loginError = ref(''); const status = ref({}); const topics = ref([]); const filterStatus = ref(''); const generating = ref(false); const optimizing = ref(false); const loadingAll = ref(false); const loadingTable = ref(false); const loadingLogs = ref(false); const loadingOverlay = ref(false); const loadingText = ref(''); const pipeline = ref({ status_distribution: {} }); const pipelineLoading = ref(false); const pipelineModules = ref([]); const previewVisible = ref(false); const previewTopic = ref({ title: '' }); const previewPlatform = ref('zhihu'); const previewHtml = ref(''); const fullScreenPreview = ref(false); const showLogs = ref(false); const logType = ref('creator'); const logDate = ref(new Date().toISOString().split('T')[0]); const logContent = ref(''); // 2. 计算属性 const filteredTopics = computed(() => { if (!filterStatus.value) return topics.value || []; return (topics.value || []).filter(t => t && t.status === filterStatus.value); }); // 3. 工具函数 const formatDate = (val) => { if (!val) return '-'; const d = new Date(val); if (isNaN(d.getTime())) return val; return d.toLocaleString('zh-CN', { hour12: false }); }; const formatRelativeTime = (val) => { if (!val) return '-'; const d = new Date(val); if (isNaN(d.getTime())) return '-'; const now = new Date(); const diff = now - d; const minutes = Math.floor(diff / 60000); if (minutes < 1) return '刚刚'; if (minutes < 60) return `${minutes}分钟前`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}小时前`; const days = Math.floor(hours / 24); if (days < 7) return `${days}天前`; return formatDate(val); }; // 4. 业务方法 const countByStatus = (status) => { return (topics.value || []).filter(t => t.status === status).length; }; const getPriorityType = (score) => { if (!score) return ''; if (score >= 20) return 'danger'; if (score >= 15) return 'warning'; return 'success'; }; const getStatusClass = (status) => { const map = { '待处理': 'pending', '待审查': 'review', '待发布': 'ready', '已发布': 'published' }; return map[status] || ''; }; const refresh = async () => { try { const [s, t] = await Promise.all([ fetch(API_BASE + '/api/system/status').then(r => r.json()), fetch(API_BASE + '/api/topics').then(r => r.json()) ]); status.value = s; topics.value = t; } catch (e) { ElMessage.error('刷新失败:' + e.message); } }; const refreshPipeline = async () => { pipelineLoading.value = true; try { const res = await fetch(API_BASE + '/api/system/pipeline/status'); if (res.ok) { const data = await res.json(); pipeline.value = data; pipelineModules.value = Object.entries(data.pipeline_modules || {}).map(([name, info]) => ({ module: name, last_run: info.last_run || '未运行', status_ok: !info.has_error && info.exists, status_text: info.exists && !info.has_error ? '正常' : info.exists ? '有错误' : '缺失', error: info.has_error ? '检测到错误' : '' })); } } catch (e) { ElMessage.error('获取流水线状态失败'); } finally { pipelineLoading.value = false; } }; const refreshAll = async () => { loadingAll.value = true; try { await Promise.all([refresh(), refreshPipeline()]); ElMessage.success('刷新成功'); } catch (e) { ElMessage.error('刷新失败'); } finally { loadingAll.value = false; } }; const triggerGenerate = async () => { generating.value = true; try { const res = await fetch(API_BASE + '/api/system/generate/run', { method: 'POST' }); const data = await res.json(); if (data.result && data.result.ok) { ElMessage.success('创作任务已启动'); setTimeout(refresh, 3000); } else { ElMessage.error('启动失败:' + (data.error || '未知错误')); } } catch (e) { ElMessage.error('请求失败:' + e.message); } finally { generating.value = false; } }; const triggerOptimize = async () => { optimizing.value = true; try { const res = await fetch(API_BASE + '/api/system/optimize/run', { method: 'POST' }); const data = await res.json(); if (data.summary) { ElNotification({ title: '优化完成', message: `自动通过 ${data.summary.passed_auto || 0} 篇,需人工 ${data.summary.need_manual || 0} 篇`, type: 'success' }); await refresh(); } else { ElMessage.success('优化完成'); } } catch (e) { ElMessage.error('优化失败:' + e.message); } finally { optimizing.value = false; } }; const openPreview = async (topic) => { previewTopic.value = { id: topic.id, title: topic.title }; previewPlatform.value = 'zhihu'; previewVisible.value = true; await loadPreview(); }; const loadPreview = async () => { previewHtml.value = ''; console.log('[Preview] Loading topic:', previewTopic.value.id, 'platform:', previewPlatform.value); try { const res = await fetch(`${API_BASE}/api/articles/${previewTopic.value.id}/preview?platform=${previewPlatform.value}`); console.log('[Preview] Response status:', res.status); if (res.ok) { const data = await res.json(); console.log('[Preview] Got HTML, length:', data.html?.length); const parser = new DOMParser(); const doc = parser.parseFromString(data.html, 'text/html'); const contentDiv = doc.querySelector('.content'); console.log('[Preview] Found .content:', !!contentDiv); if (contentDiv) { previewHtml.value = contentDiv.innerHTML; console.log('[Preview] Set previewHtml from .content'); } else { const header = doc.querySelector('.header'); const footer = doc.querySelector('footer'); const tags = doc.querySelector('.tags'); const interaction = doc.querySelector('.interaction'); if (header) header.remove(); if (footer) footer.remove(); if (tags) tags.remove(); if (interaction) interaction.remove(); previewHtml.value = doc.body.innerHTML; console.log('[Preview] Set previewHtml from body.innerHTML'); } } else if (res.status === 404) { previewHtml.value = '

暂未创作文章,请先点击创作按钮生成

'; } else { ElMessage.error('加载预览失败:' + res.status); } } catch (e) { previewHtml.value = '

请求失败,请检查后端服务是否运行

'; console.error('Preview error:', e); } }; const copyPreviewHtml = async () => { if (!previewHtml.value) return; try { const parser = new DOMParser(); const doc = parser.parseFromString(previewHtml.value, 'text/html'); const header = doc.querySelector('.header'); if (header) header.remove(); const footer = doc.querySelector('footer'); if (footer) footer.remove(); const tagsDiv = doc.querySelector('.tags'); if (tagsDiv) tagsDiv.remove(); const interaction = doc.querySelector('.interaction'); if (interaction) interaction.remove(); const contentDiv = doc.querySelector('.content'); let text = ''; if (contentDiv) { text = contentDiv.innerText.trim(); } else { text = doc.body.innerText.trim(); } if (!text) { ElMessage.warning('未提取到正文内容'); return; } await navigator.clipboard.writeText(text); ElMessage.success('正文已复制到剪贴板'); } catch (e) { console.error('Copy error:', e); ElMessage.error('复制失败'); } }; const expandPreview = () => { fullScreenPreview.value = true; }; const handleShowLogs = () => { showLogs.value = true; }; const fetchLogs = async () => { loadingLogs.value = true; try { const res = await fetch(`${API_BASE}/api/system/logs/${logDate.value}?log_type=${logType.value}`); if (res.ok) { const data = await res.json(); logContent.value = data.content ? data.content.join('\n') : '无内容'; } else { ElMessage.error('加载日志失败'); } } catch (e) { ElMessage.error('请求失败'); } finally { loadingLogs.value = false; } }; const createTopic = async (topic) => { if (topic.published_urls && Object.keys(topic.published_urls).length > 0) { try { await ElMessageBox.alert( '本文已发布过,重新创作将覆盖原有内容。是否继续?', '重新创作确认', { confirmButtonText: '继续', cancelButtonText: '取消', type: 'warning', } ); } catch (e) { return; } } try { const res = await fetch(`${API_BASE}/api/system/generate/run?topic_id=${topic.id}`, { method: 'POST' }); const data = await res.json(); if (data.result && data.result.ok) { ElMessage.success(`选题 ${topic.id} 创作任务已启动`); setTimeout(refresh, 3000); } else { ElMessage.error('创作失败:' + (data.error || '未知错误')); } } catch (e) { ElMessage.error('请求失败:' + e.message); } }; const optimizeTopic = async (topic) => { try { const res = await fetch(API_BASE + '/api/system/optimize/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic_ids: [topic.id] }) }); const data = await res.json(); if (data.summary) { ElMessage.success(`选题 ${topic.id} 优化完成`); setTimeout(refresh, 2000); } else { ElMessage.success('优化完成'); } } catch (e) { ElMessage.error('优化失败:' + e.message); } }; const handlePublish = async (topic) => { try { ElMessage.info(`正在发布选题 ${topic.id}...`); const res = await fetch(API_BASE + '/api/publishing/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic_id: topic.id }) }); if (!res.ok) throw new Error('发布失败'); const data = await res.json(); ElMessage.success(`选题 ${topic.id} 已发布`); await refresh(); } catch (e) { ElMessage.error('发布失败:' + e.message); } }; const openCreateTopic = () => { ElMessage.info('新建选题功能待实现'); }; // 5. 页面路由 const currentPage = ref('overview'); const switchPage = (page) => { currentPage.value = page; }; const goToTopicsWithFilter = (status) => { currentPage.value = 'topics'; filterStatus.value = status; }; // 6. 生命周期(必须在 return 之前) watch(previewPlatform, loadPreview); onMounted(() => { const authToken = localStorage.getItem('auth_token'); const role = localStorage.getItem('user_role'); if (authToken) { isLoggedIn.value = true; if (role === 'admin') isAdmin.value = true; } refresh(); refreshPipeline(); }); // 7. 返回给模板 return { // 状态 status, topics, filterStatus, filteredTopics, generating, optimizing, loadingAll, loadingTable, loadingLogs, loadingOverlay, loadingText, pipeline, pipelineLoading, pipelineModules, previewVisible, previewTopic, previewPlatform, previewHtml, fullScreenPreview, showLogs, logType, logDate, logContent, // 页面路由 currentPage, // 方法 countByStatus, getPriorityType, getStatusClass, refresh, refreshPipeline, refreshAll, triggerGenerate, triggerOptimize, openPreview, loadPreview, copyPreviewHtml, expandPreview, fetchLogs, createTopic, optimizeTopic, handlePublish, openCreateTopic, // 工具函数 formatDate, formatRelativeTime, switchPage, goToTopicsWithFilter, // 认证(未完整) isLoggedIn, isAdmin, loginForm, loginError, // 图标 Document, Upload, CopyDocument, FullScreen, Promotion };