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 - 所有功能保留 (登录、用户管理、批量操作等)
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user