Files
yu-zhi-ran/platform/frontend/script-content.js
T
lt 277b13eaae feat: 完成布局优化 - 操作列固定、批量按钮自适应、分类标签带数量
优化内容:
1. 表格布局:
   - 使用 calc(100vw - 160px) 确保表格不超出视口
   - 操作列 fixed='right' 固定在右侧,宽度 300px
   - 按钮 3 个后自动换行 (max-width: 200px)
   - 恢复合理列宽,不再过度压缩

2. 批量操作区域:
   - 容器改为 inline-block,宽度自适应按钮内容
   - 背景宽度与按钮总宽度匹配

3. 分类标签:
   - 显示数量 (如 '待处理 (20)')
   - 点击切换筛选,去掉误导的 'X' 图标

4. 删除功能:
   - 操作列增加删除按钮
   - 删除前弹出确认对话框

5. 系统日志:
   - 修复后端日志路径 (parents[4])
   - 404 时显示友好提示

6. 其他:
   - 左侧菜单宽度 160px
   - 所有功能保留 (登录、用户管理、批量操作等)
2026-04-27 11:32:17 +08:00

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>