277b13eaae
优化内容: 1. 表格布局: - 使用 calc(100vw - 160px) 确保表格不超出视口 - 操作列 fixed='right' 固定在右侧,宽度 300px - 按钮 3 个后自动换行 (max-width: 200px) - 恢复合理列宽,不再过度压缩 2. 批量操作区域: - 容器改为 inline-block,宽度自适应按钮内容 - 背景宽度与按钮总宽度匹配 3. 分类标签: - 显示数量 (如 '待处理 (20)') - 点击切换筛选,去掉误导的 'X' 图标 4. 删除功能: - 操作列增加删除按钮 - 删除前弹出确认对话框 5. 系统日志: - 修复后端日志路径 (parents[4]) - 404 时显示友好提示 6. 其他: - 左侧菜单宽度 160px - 所有功能保留 (登录、用户管理、批量操作等)
477 lines
18 KiB
JavaScript
477 lines
18 KiB
JavaScript
<script>
|
|
const { ref, reactive, computed, onMounted, watch } = Vue;
|
|
const { ElMessage, ElNotification, ElMessageBox } = ElementPlus;
|
|
|
|
// 图标组件
|
|
const CopyDocument = Vue.h('el-icon', { name: 'CopyDocument' });
|
|
const FullScreen = Vue.h('el-icon', { name: 'FullScreen' });
|
|
const Document = Vue.h('el-icon', { name: 'Document' });
|
|
const Upload = Vue.h('el-icon', { name: 'Upload' });
|
|
const Promotion = Vue.h('el-icon', { name: 'Promotion' });
|
|
|
|
const app = Vue.createApp({
|
|
name: 'YuZhiRanPlatform',
|
|
setup() {
|
|
// ========== 变量声明区 ==========
|
|
const API_BASE = window.location.origin;
|
|
|
|
// 状态
|
|
const isLoggedIn = ref(false);
|
|
const isAdmin = ref(false);
|
|
const loginForm = reactive({ username: '', password: '' });
|
|
const loginError = ref('');
|
|
|
|
const status = ref({});
|
|
const topics = ref([]);
|
|
const selectedTopicIds = 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('');
|
|
|
|
// 计算属性
|
|
const filteredTopics = computed(() => {
|
|
if (!filterStatus.value) return topics.value || [];
|
|
return (topics.value || []).filter(t => t && t.status === filterStatus.value);
|
|
});
|
|
|
|
// ========== 工具函数 ==========
|
|
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);
|
|
};
|
|
|
|
// ========== 业务方法 ==========
|
|
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 = '<div class="text-center py-12 text-gray-500"><p>暂未创作文章,请先点击创作按钮生成</p></div>';
|
|
} else {
|
|
ElMessage.error('加载预览失败:' + res.status);
|
|
}
|
|
} catch (e) {
|
|
previewHtml.value = '<div class="text-center py-12 text-gray-500"><p>请求失败,请检查后端服务是否运行</p></div>';
|
|
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 triggerGenerateSelected = async () => {
|
|
if (selectedTopicIds.value.length === 0) {
|
|
ElMessage.warning('请先选择要创作的选题');
|
|
return;
|
|
}
|
|
generating.value = true;
|
|
try {
|
|
const res = await fetch(API_BASE + '/api/system/generate/run', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ topic_ids: selectedTopicIds.value })
|
|
});
|
|
const data = await res.json();
|
|
if (data.result && data.result.ok) {
|
|
ElMessage.success(`已启动 ${selectedTopicIds.value.length} 个选题的创作任务`);
|
|
selectedTopicIds.value = [];
|
|
setTimeout(refresh, 3000);
|
|
} else {
|
|
ElMessage.error('批量创作失败:' + (data.error || '未知错误'));
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error('请求失败:' + e.message);
|
|
} finally {
|
|
generating.value = false;
|
|
}
|
|
};
|
|
|
|
const triggerOptimizeSelected = async () => {
|
|
if (selectedTopicIds.value.length === 0) {
|
|
ElMessage.warning('请先选择要优化的选题');
|
|
return;
|
|
}
|
|
optimizing.value = true;
|
|
try {
|
|
const res = await fetch(API_BASE + '/api/system/optimize/run', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ topic_ids: selectedTopicIds.value })
|
|
});
|
|
const data = await res.json();
|
|
if (data.summary) {
|
|
ElNotification({
|
|
title: '批量优化完成',
|
|
message: `自动通过 ${data.summary.passed_auto || 0} 篇,需人工 ${data.summary.need_manual || 0} 篇`,
|
|
type: 'success'
|
|
});
|
|
selectedTopicIds.value = [];
|
|
await refresh();
|
|
} else {
|
|
ElMessage.success('批量优化完成');
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error('批量优化失败:' + e.message);
|
|
} finally {
|
|
optimizing.value = false;
|
|
}
|
|
};
|
|
|
|
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('新建选题功能待实现');
|
|
};
|
|
|
|
// 页面路由
|
|
const currentPage = ref('overview');
|
|
const switchPage = (page) => {
|
|
currentPage.value = page;
|
|
};
|
|
const goToTopicsWithFilter = (status) => {
|
|
currentPage.value = 'topics';
|
|
filterStatus.value = status;
|
|
};
|
|
|
|
// 生命周期
|
|
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();
|
|
});
|
|
|
|
// 返回给模板
|
|
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
|
|
};
|
|
}
|
|
});
|
|
|
|
app.use(ElementPlus);
|
|
app.mount('#app');
|
|
</script>
|