feat: 管理员页面整合与侧边栏导航优化

- 完成系统管理页面与导航整合
- 在 users.html/topics.html/logs.html 侧边栏与移动导航添加系统管理入口
- 创建 admin.html 管理页面(案例/任务日志/LLM配置/系统配置)
- 新增核心模块:collector.py(数据采集器)、scheduler.py(任务调度器)
- 新增脚本:collector_db_integration.py(采集器数据库整合)
- 更新项目文档并验证路由注册
This commit is contained in:
lt
2026-05-08 09:26:06 +08:00
parent 31d6306e3b
commit df5a9db3ad
8 changed files with 341 additions and 34 deletions
+9 -2
View File
@@ -255,6 +255,7 @@ const app = createApp({
const activeTab = ref('cases');
const currentUser = ref({ username: '' });
const redirectToPage = (page) => { window.location.href = '/' + page; }; const currentUser = ref({ username: '' });
const isAdmin = ref(false);
const isLoggedIn = ref(false);
@@ -262,8 +263,9 @@ const app = createApp({
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(response => response.ok ? response.json() : Promise.reject())
.then(data => {
currentUser.value = data.user;
isAdmin.value = data.user.role === 'admin';
const user = data.user || { username: '', role: 'user' };
currentUser.value = user;
isAdmin.value = user.role === 'admin';
isLoggedIn.value = true;
if (!isAdmin.value) {
ElMessage.warning('需要管理员权限');
@@ -275,6 +277,11 @@ const app = createApp({
window.location.href = 'login.html';
});
// 页面跳转
const redirectToPage = (page) => {
window.location.href = '/' + page;
};
// Cases
const cases = ref([]);
const caseDialogVisible = ref(false);
+18 -2
View File
@@ -71,7 +71,7 @@
<el-date-picker v-model="logDate" type="date" placeholder="选择日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" size="default"></el-date-picker>
<el-button type="primary" @click="fetchLogs" :loading="loadingLogs">加载日志</el-button>
</div>
<el-card v-if="logContent" class="font-mono text-sm bg-gray-50" style="max-height: 600px; overflow-y: auto; background: #f9fafb; border: 1px solid #e5e7eb;"><pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">{{ logContent }}</pre></el-card>
<el-card v-if="logContent" ref="logContainer" class="font-mono text-sm bg-gray-50" style="max-height: 600px; overflow-y: auto; background: #f9fafb; border: 1px solid #e5e7eb;"><pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">{{ logContent }}</pre></el-card>
<el-empty v-else description="请先选择类型和日期,然后点击加载"></el-empty>
</div>
</main>
@@ -88,7 +88,17 @@
<script src="element-plus.full.js"></script>
<script>
const LogsApp = {
data() { return { isLoggedIn: false, isAdmin: false, currentUser: { username: '' }, logType: 'creator', logDate: '', logContent: '', loadingLogs: false } },
data() {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
return {
isLoggedIn: false,
isAdmin: false,
currentUser: { username: '' },
logType: 'creator',
logDate: today,
logContent: '',
loadingLogs: false
} },
methods: {
async fetchLogs() {
// 验证输入
@@ -113,6 +123,12 @@
// 后端返回: { type, date, content }
this.logContent = `日志类型:${data.type}\n日期:${data.date}\n\n${data.content || '(日志文件为空)'}`;
this.$message.success('日志加载成功');
this.$nextTick(() => {
const container = this.$refs.logContainer;
if (container && container.$el) {
container.$el.scrollTop = container.$el.scrollHeight;
}
});
} catch (error) {
console.error('获取日志失败:', error);
this.$message.error(`获取日志失败: ${error.message}`);
+5 -5
View File
@@ -3,8 +3,8 @@
<head>
<meta charset="UTF-8">
<title>前端测试</title>
<script src="/static/vue.global.prod.js"></script>
<link rel="stylesheet" href="/static/element-plus.css" />
<script src="vue.global.prod.js"></script>
<link rel="stylesheet" href="element-plus.css" />
<!-- Tailwind CSS -->
@@ -66,9 +66,9 @@
<!-- 本地静态文件 -->
<script src="./static/vue.global.prod.js?v=20260427"></script>
<link rel="stylesheet" href="./static/element-plus.css?v=20260427">
<script src="./static/element-plus.full.js?v=20260427"></script>
<script src=".vue.global.prod.js?v=20260427"></script>
<link rel="stylesheet" href=".element-plus.css?v=20260427">
<script src="element-plus.full.js?v=20260427"></script>
</head>
<body>
+106 -24
View File
@@ -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('暂无内容可复制');