feat: 管理员页面整合与侧边栏导航优化
- 完成系统管理页面与导航整合 - 在 users.html/topics.html/logs.html 侧边栏与移动导航添加系统管理入口 - 创建 admin.html 管理页面(案例/任务日志/LLM配置/系统配置) - 新增核心模块:collector.py(数据采集器)、scheduler.py(任务调度器) - 新增脚本:collector_db_integration.py(采集器数据库整合) - 更新项目文档并验证路由注册
This commit is contained in:
+106
-24
@@ -4,8 +4,10 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>宇之然内容创作平台 - 选题管理</title>
|
||||
<link rel="stylesheet" href="element-plus.css">
|
||||
<link rel="stylesheet" href="element-plus.css">
|
||||
<style>
|
||||
.preview-iframe { box-sizing: border-box; }
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; }
|
||||
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||
@@ -15,8 +17,8 @@
|
||||
.user-info { display: flex; align-items: center; gap: 8px; }
|
||||
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
|
||||
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; width: 100%; }
|
||||
.sidebar { width: 180px; background: white; padding: 16px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
|
||||
.sidebar-btn { width: 100%; text-align: left; padding: 12px 16px; border: none; background: transparent; border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.3s; color: #606266; font-size: 14px; }
|
||||
.sidebar { width: 180px; background: white; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
|
||||
.sidebar-btn { width: 100%; text-align: left; padding: 8px 12px; border: none; background: transparent; border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.3s; color: #606266; font-size: 14px; }
|
||||
.sidebar-btn:hover { background: #f5f7fa; color: #409eff; }
|
||||
.sidebar-btn.active { background: #ecf5ff; color: #409eff; font-weight: 600; }
|
||||
.content-area { flex: 1; padding: 32px; overflow-y: auto; }
|
||||
@@ -33,7 +35,7 @@
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { display: none; }
|
||||
.mobile-nav { display: flex; }
|
||||
.content-area { padding: 16px; padding-bottom: 80px; }
|
||||
.content-area { padding: 12px; padding-bottom: 80px; }
|
||||
}
|
||||
|
||||
.topic-card-list { display: none; }
|
||||
@@ -47,7 +49,7 @@
|
||||
.topic-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
@@ -78,7 +80,41 @@
|
||||
.topic-card-actions .el-button { flex: 1; min-width: 60px; }
|
||||
.mobile-nav { display: flex; }
|
||||
}
|
||||
</style>
|
||||
|
||||
.preview-iframe { box-sizing: border-box; }
|
||||
/* 预览弹窗响应式高度 */
|
||||
@media (min-width: 769px) {
|
||||
.preview-iframe { max-height: calc(100vh - 100px) !important; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.preview-iframe { max-height: calc(100vh - 250px) !important; }
|
||||
}
|
||||
|
||||
/* 预览弹窗自定义高度(非全屏时) */
|
||||
.preview-dialog-custom.el-dialog {
|
||||
max-height: calc(100vh - 90px) !important;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.preview-dialog-custom.el-dialog .el-dialog__header {
|
||||
padding: 8px 12px;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.preview-dialog-custom.el-dialog .el-dialog__body {
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.preview-dialog-custom.el-dialog .el-dialog__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
@@ -102,7 +138,7 @@
|
||||
<main class="content-area">
|
||||
<div class="card">
|
||||
<h2 style="font-size: 28px; font-weight: 700; margin-bottom: 32px; color: #303133;">📋 选题管理</h2>
|
||||
<div class="card" style="display: inline-block; min-width: fit-content; padding: 16px; margin-bottom: 24px;">
|
||||
<div class="card" style="display: inline-block; min-width: fit-content; padding: 12px; margin-bottom: 24px;">
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<el-button type="primary" size="small" @click="refreshAll">🔄 批量刷新</el-button>
|
||||
<el-button type="success" size="small" @click="triggerGenerateSelected" :disabled="selectedTopicIds.length === 0">▶ 批量创作</el-button>
|
||||
@@ -117,7 +153,7 @@
|
||||
<el-button size="large" :type="filterStatus === 'ready' ? 'primary' : ''" @click="filterStatus = 'ready'">待发布 ({{ statusStats.ready }})</el-button>
|
||||
<el-button size="large" :type="filterStatus === 'published' ? 'primary' : ''" @click="filterStatus = 'published'">已发布 ({{ statusStats.published }})</el-button>
|
||||
</div>
|
||||
<div class="card" style="width: 100%; overflow-x: auto; padding: 16px;">
|
||||
<div class="card" style="width: 100%; overflow-x: auto; padding: 12px;">
|
||||
<el-table :data="filteredTopics" stripe v-loading="loadingTable" @selection-change="selectedTopicIds = $event">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column prop="id" label="ID" width="70" fixed></el-table-column>
|
||||
@@ -182,7 +218,7 @@
|
||||
<button v-if="isAdmin" class="mobile-nav-btn" @click="redirectToPage('admin.html')">⚙️ 系统</button>
|
||||
</nav>
|
||||
<!-- 预览弹窗 -->
|
||||
<el-dialog v-model="previewVisible" title="选题预览" width="80%" :before-close="() => previewVisible = false">
|
||||
<el-dialog v-model="previewVisible" title="选题预览" width="85%" :modal-props="{ closeOnClickModal: false }" :before-close="() => previewVisible = false" class="preview-dialog-custom" :fullscreen="previewFullscreen">
|
||||
<div v-if="previewTopic">
|
||||
<!-- 平台切换按钮 -->
|
||||
<div style="margin-bottom: 16px; display: flex; justify-content: flex-end; gap: 8px;">
|
||||
@@ -193,24 +229,46 @@
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top: 0;">{{ previewTopic.title }}</h2>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px;">
|
||||
<h2 style="margin:0; font-size:16px;">{{ previewTopic.title }}</h2>
|
||||
<div style="display:flex; align-items:center; gap:12px; font-size:13px; color:#909399;">
|
||||
<span>创建:{{ formatDate(previewTopic.created_at) }}</span>
|
||||
<span>状态:{{ getStatusLabel(previewTopic.status) }}</span>
|
||||
<span v-if="previewTopic.generated_at">创作:{{ formatDate(previewTopic.generated_at) }}</span>
|
||||
<span v-if="previewTopic.published_at">发布:{{ formatDate(previewTopic.published_at) }}</span>
|
||||
<el-button size="small" @click="togglePreviewFullscreen">
|
||||
{{ previewFullscreen ? '退出全屏' : '全屏' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 富文本内容预览(v-html 渲染) -->
|
||||
<div class="preview-content" v-html="getPreviewHtml(previewPlatform)"
|
||||
style="max-height: 60vh; overflow-y: auto; margin: 16px 0; padding: 16px; border: 1px solid #ebeef5; border-radius: 8px; background: #fafafa;">
|
||||
<!-- 预览内容使用 iframe 隔离样式 -->
|
||||
<div style="max-width: 1000px; margin: 0 auto; width: 100%; height: 100%; display: flex; flex-direction: column;">
|
||||
<iframe :srcdoc="currentPreviewHtml"
|
||||
class="preview-iframe"
|
||||
style="flex: 1; min-height: 500px; border:1px solid #ebeef5; border-radius:8px; background:#fff; overflow:auto; padding: 0 0; width: 100%;"
|
||||
sandbox>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<div class="preview-footer" style="font-size: 14px; color: #909399;">
|
||||
<div>创建时间:{{ formatDate(previewTopic.created_at) }}</div>
|
||||
<div>状态:{{ getStatusLabel(previewTopic.status) }}</div>
|
||||
<div v-if="previewTopic.generated_at">创作时间:{{ formatDate(previewTopic.generated_at) }}</div>
|
||||
<div v-if="previewTopic.published_at">发布时间:{{ formatDate(previewTopic.published_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="previewVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="copyContent(previewPlatform)">复制并发布到{{ platformName(previewPlatform) }}</el-button>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; width:100%; font-size:14px; color:#909399;">
|
||||
<div class="preview-info">
|
||||
<span>创建:{{ formatDate(previewTopic.created_at) }}</span>
|
||||
<span style="margin: 0 8px;">|</span>
|
||||
<span>状态:{{ getStatusLabel(previewTopic.status) }}</span>
|
||||
<span v-if="previewTopic.generated_at" style="margin-left:8px;">
|
||||
创作:{{ formatDate(previewTopic.generated_at) }}
|
||||
</span>
|
||||
<span v-if="previewTopic.published_at" style="margin-left:8px;">
|
||||
发布:{{ formatDate(previewTopic.published_at) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<el-button @click="previewVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="copyContent(previewPlatform)">复制并发布到{{ platformName(previewPlatform) }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -233,6 +291,7 @@ const TopicsApp = {
|
||||
topics: [],
|
||||
previewVisible: false,
|
||||
previewTopic: null,
|
||||
previewFullscreen: false,
|
||||
previewPlatform: 'zhihu',
|
||||
platformContents: {}
|
||||
}
|
||||
@@ -262,6 +321,28 @@ const TopicsApp = {
|
||||
ready: this.topics.filter(t => ready.includes(t.status)).length,
|
||||
published: this.topics.filter(t => published.includes(t.status)).length
|
||||
};
|
||||
},
|
||||
currentPreviewHtml() {
|
||||
const html = this.platformContents[this.previewPlatform];
|
||||
if (!html) return '';
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const body = doc.body;
|
||||
if (!body) return html;
|
||||
|
||||
body.querySelectorAll('script, nav, .header, footer, .interaction').forEach(el => el.remove());
|
||||
|
||||
const head = doc.querySelector('head');
|
||||
const headHtml = head ? head.innerHTML : '';
|
||||
const bodyHtml = body.innerHTML;
|
||||
const ending = '<p style="margin-top:24px;padding-top:16px;border-top:1px solid #eee;color:#666;font-size:14px;">感兴趣可以收藏关注我们,欢迎在评论区分享你的实践经验和改进建议!</p>';
|
||||
|
||||
return `<!DOCTYPE html><html><head>${headHtml}</head><body style="margin:0;padding:0;">${bodyHtml}${ending}</body></html>`;
|
||||
} catch (e) {
|
||||
console.error('生成预览 HTML 失败:', e);
|
||||
return html;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -381,8 +462,8 @@ const TopicsApp = {
|
||||
);
|
||||
await Promise.all(promises);
|
||||
},
|
||||
getPreviewHtml(platform) {
|
||||
return this.platformContents[platform] || '暂无内容';
|
||||
togglePreviewFullscreen() {
|
||||
this.previewFullscreen = !this.previewFullscreen;
|
||||
},
|
||||
platformName(platform) {
|
||||
const names = {
|
||||
@@ -392,6 +473,7 @@ const TopicsApp = {
|
||||
};
|
||||
return names[platform] || platform;
|
||||
},
|
||||
// 计算属性:当前平台预览的完整 HTML(响应式更新)
|
||||
copyContent(platform) {
|
||||
if (!this.previewTopic || !this.previewTopic.content) {
|
||||
this.$message.warning('暂无内容可复制');
|
||||
|
||||
Reference in New Issue
Block a user