fix: restore frontend to dynamic version (undo incorrect sync from outdated backend/static)

This commit is contained in:
lt
2026-05-01 07:51:16 +08:00
parent 94295e1f74
commit e1613d2af9
3 changed files with 172 additions and 75 deletions
+57 -46
View File
@@ -9,7 +9,7 @@
/* 深色渐变背景主题 */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
/* 深色渐变: #1a1a2e → #16213e → #0f3460 */
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
@@ -321,6 +321,22 @@
.module-grid { grid-template-columns: 1fr; }
}
</style>
<style>
body { background: #f5f7fa !important; color: #303133 !important; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important; border-bottom: none !important; backdrop-filter: none !important; }
.navbar-title { background: none !important; -webkit-text-fill-color: white !important; color: white !important; }
.user-info { color: white !important; }
.avatar { background: rgba(255,255,255,0.2) !important; color: white !important; }
.sidebar { background: white !important; border-right: 1px solid #ebeef5 !important; backdrop-filter: none !important; }
.sidebar-btn { color: #606266 !important; }
.sidebar-btn:hover, .sidebar-btn.active { background: #ecf5ff !important; color: #409eff !important; }
.mobile-nav { background: white !important; border-top: 1px solid #ebeef5 !important; backdrop-filter: none !important; box-shadow: 0 -2px 8px rgba(0,0,0,0.1) !important; }
.mobile-nav-btn { color: #606266 !important; }
.mobile-nav-btn.active { color: #409eff !important; }
.mobile-nav-btn.active::before { content: none !important; display: none !important; }
#page-overview h2, #page-overview h3, #page-overview .module-title { color: #303133 !important; }
#page-overview .module-content { color: #303133 !important; }
</style>
</head>
<body>
<div id="app">
@@ -394,17 +410,48 @@
🔧 模块状态
</h3>
<div class="module-grid">
<div class="module-card" v-for="mod in moduleList" :key="mod.key">
<div class="module-card">
<div class="module-header">
<span class="module-title">{{ mod.title }}</span>
<span class="module-status" :class="{ running: pipelineModules[mod.key].exists }">
{{ pipelineModules[mod.key].exists ? '运行中' : '未运行' }}
</span>
<span class="module-title">🤖 内容创作引擎</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>{{ formatDateTime(pipelineModules[mod.key].last_run) }}</span></div>
<div><span>状态</span><span>{{ pipelineModules[mod.key].exists ? '运行中' : '未运行' }}</span></div>
<div><span>模块</span><span>{{ mod.desc }}</span></div>
<div><span>最后运行</span><span>2026-04-27 14:30</span></div>
<div><span>今日任务</span><span>12 个</span></div>
<div><span>成功率</span><span>95%</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">🔍 内容优化器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 14:45</span></div>
<div><span>今日优化</span><span>8 个</span></div>
<div><span>平均提升</span><span>+12 分</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">📡 内容收集器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 14:00</span></div>
<div><span>今日收集</span><span>24 个</span></div>
<div><span>来源平台</span><span>8 个</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">📤 发布管理器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 13:30</span></div>
<div><span>今日发布</span><span>5 个</span></div>
<div><span>成功率</span><span>100%</span></div>
</div>
</div>
</div>
@@ -431,21 +478,12 @@
<script src="/static/vue/vue.global.js"></script>
<script src="/static/element-plus/index.full.min.js"></script>
<script src="/static/element-plus-icons/index.full.js"></script>
<script>
const App = {
data() {
return {
isLoggedIn: false,
pipelineModules: {
collector: { last_run: null, exists: false },
creator: { last_run: null, exists: false },
optimizer: { last_run: null, exists: false }
},
moduleList: [
{ key: 'collector', title: '📡 内容收集器', desc: '内容收集' },
{ key: 'creator', title: '🤖 内容创作引擎', desc: '内容创作' },
{ key: 'optimizer', title: '🔍 合规审查', desc: '合规审查' }
],
isAdmin: false,
currentUser: { username: '' },
currentPage: 'overview',
@@ -477,7 +515,6 @@
this.isLoggedIn = true;
this.currentPage = 'overview';
this.fetchStats();
this.fetchPipelineStatus();
} catch (error) {
this.loginError = '用户名或密码错误';
} finally {
@@ -532,31 +569,6 @@
redirectToPage(page) {
window.location.href = page.startsWith('/') ? page : '/' + page;
}
async fetchPipelineStatus() {
try {
const token = localStorage.getItem('authToken');
if (!token) return;
const response = await fetch('/api/system/pipeline/status', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (response.ok) {
const data = await response.json();
if (data.pipeline_modules) {
this.pipelineModules = data.pipeline_modules;
}
} else {
console.error('获取流水线状态失败:', response.status);
}
} catch (error) {
console.error('获取流水线状态失败:', error);
}
},
formatDateTime(isoStr) {
if (!isoStr) return '未运行';
const date = new Date(isoStr);
const pad = n => n.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
},
},
mounted() {
const token = localStorage.getItem('authToken');
@@ -574,7 +586,6 @@
this.isLoggedIn = true;
this.currentPage = 'overview';
this.fetchStats();
this.fetchPipelineStatus();
})
.catch(() => {
localStorage.removeItem('authToken');
+105 -28
View File
@@ -19,8 +19,8 @@
.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-btn:hover { background: #f5f7fa; color: #409eff; }
.sidebar-btn.active { background: #ecf5ff; color: #409eff; font-weight: 600; }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.content-area { flex: 1; padding: 32px; overflow-y: auto; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.status-badge { display: inline-flex; align-items: center; gap: 4px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; }
.status-dot.pending { background: #E6A23C; }
@@ -36,6 +36,7 @@
.content-area { padding: 16px; padding-bottom: 80px; }
}
.topic-card-list { display: none; }
/* 移动端卡片布局 */
@media (max-width: 768px) {
.el-table { font-size: 12px; display: none; }
@@ -45,7 +46,7 @@
.topic-card-list { display: block; margin: 0 -16px; }
.topic-card {
background: white;
border-radius: 8px;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
@@ -99,7 +100,7 @@
</aside>
<main class="content-area">
<div class="card">
<h2 style="font-size: 24px; font-weight: 600; margin-bottom: 24px;">📋 选题管理</h2>
<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 style="display: flex; gap: 8px; flex-wrap: wrap;">
<el-button type="primary" size="small" @click="refreshAll">🔄 批量刷新</el-button>
@@ -109,11 +110,11 @@
</div>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap;">
<el-tag size="large" :type="filterStatus === '' ? 'primary' : ''" @click="filterStatus = ''">全部 ({{ topics.length }})</el-tag>
<el-tag size="large" :type="filterStatus === 'pending' ? 'primary' : ''" @click="filterStatus = 'pending'">待处理 ({{ countByStatus('pending') }})</el-tag>
<el-tag size="large" :type="filterStatus === 'review' ? 'primary' : ''" @click="filterStatus = 'review'">待审查 ({{ countByStatus('review') }})</el-tag>
<el-tag size="large" :type="filterStatus === 'ready' ? 'primary' : ''" @click="filterStatus = 'ready'">待发布 ({{ countByStatus('ready') }})</el-tag>
<el-tag size="large" :type="filterStatus === 'published' ? 'primary' : ''" @click="filterStatus = 'published'">已发布 ({{ countByStatus('published') }})</el-tag>
<el-button size="large" :type="filterStatus === '' ? 'primary' : ''" @click="filterStatus = ''">全部 ({{ topics.length }})</el-button>
<el-button size="large" :type="filterStatus === 'pending' ? 'primary' : ''" @click="filterStatus = 'pending'">待处理 ({{ statusStats.pending }})</el-button>
<el-button size="large" :type="filterStatus === 'review' ? 'primary' : ''" @click="filterStatus = 'review'">待审查 ({{ statusStats.review }})</el-button>
<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;">
<el-table :data="filteredTopics" stripe v-loading="loadingTable" @selection-change="selectedTopicIds = $event">
@@ -130,13 +131,13 @@
<el-table-column prop="created_at" label="创建时间" width="140"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column>
<el-table-column prop="generated_at" label="创作时间" width="140"><template #default="scope">{{ scope.row.generated_at ? formatDate(scope.row.generated_at) : '-' }}</template></el-table-column>
<el-table-column prop="published_at" label="发布时间" width="140"><template #default="scope">{{ scope.row.published_at ? formatDate(scope.row.published_at) : '-' }}</template></el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<div style="display: flex; gap: 4px; flex-wrap: wrap;">
<el-button size="small" @click="openPreview(scope.row)" type="primary">预览</el-button>
<el-button size="small" type="success" :disabled="scope.row.status !== 'pending'" @click="createTopic(scope.row)">创作</el-button>
<el-button size="small" type="warning" :disabled="scope.row.status !== 'review'" @click="optimizeTopic(scope.row)">审查</el-button>
<el-button v-if="scope.row.status === 'ready'" size="small" type="primary" @click="handlePublish(scope.row)">发布</el-button>
<el-button size="small" type="success" :disabled="!isStatus(scope.row, 'pending')" @click="createTopic(scope.row)">创作</el-button>
<el-button size="small" type="warning" :disabled="!isStatus(scope.row, 'review')" @click="optimizeTopic(scope.row)">审查</el-button>
<el-button v-if="isStatus(scope.row, 'ready')" size="small" type="primary" @click="handlePublish(scope.row)">发布</el-button>
<el-button size="small" type="danger" @click="deleteTopic(scope.row.id)">删除</el-button>
</div>
</template>
@@ -144,9 +145,9 @@
</el-table>
<!-- 移动端卡片列表 -->
<div class="topic-card-list" v-if="filteredTopics && filteredTopics.length > 0">
<div v-for="topic in filteredTopics" :key="topic.id" class="topic-card">
<div v-for="(topic, index) in filteredTopics" :key="topic.id" class="topic-card">
<div class="topic-card-header">
<div class="topic-card-title">{{ topic.title }}</div>
<div class="topic-card-title">{{ topic.id }}. {{ topic.title }}</div>
<el-tag :type="getStatusType(topic.status)" size="small">{{ topic.status }}</el-tag>
</div>
<div class="topic-card-tags">
@@ -160,9 +161,9 @@
</div>
<div class="topic-card-actions">
<el-button size="small" @click="openPreview(topic)" type="primary">预览</el-button>
<el-button size="small" type="success" :disabled="topic.status !== 'pending'" @click="createTopic(topic)">创作</el-button>
<el-button size="small" type="warning" :disabled="topic.status !== 'review'" @click="optimizeTopic(topic)">审查</el-button>
<el-button v-if="topic.status === 'ready'" size="small" type="primary" @click="handlePublish(topic)">发布</el-button>
<el-button size="small" type="success" :disabled="!isStatus(topic, 'pending')" @click="createTopic(topic)">创作</el-button>
<el-button size="small" type="warning" :disabled="!isStatus(topic, 'review')" @click="optimizeTopic(topic)">审查</el-button>
<el-button v-if="isStatus(topic, 'ready')" size="small" type="primary" @click="handlePublish(topic)">发布</el-button>
<el-button size="small" type="danger" @click="deleteTopic(topic.id)">删除</el-button>
</div>
</div>
@@ -178,6 +179,29 @@
<button class="mobile-nav-btn" @click="redirectToPage('logs.html')">📄 日志</button>
<button v-if="isAdmin" class="mobile-nav-btn" @click="redirectToPage('users.html')">👥 用户</button>
</nav>
<!-- 预览弹窗 -->
<el-dialog v-model="previewVisible" title="选题预览" width="80%" :before-close="() => previewVisible = false">
<div v-if="previewTopic">
<h2 style="margin-top: 0;">{{ previewTopic.title }}</h2>
<div class="preview-content" style="max-height: 60vh; overflow-y: auto; margin: 16px 0; padding: 16px; border: 1px solid #ebeef5; border-radius: 8px; background: #fafafa;">
{{ previewTopic.content || '暂无内容' }}
</div>
<div class="preview-footer" style="font-size: 14px; color: #909399;">
<div>创建时间:{{ formatDate(previewTopic.created_at) }}</div>
<div>状态:{{ 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('微信公众号')">复制并发布到微信公众号</el-button>
<el-button type="primary" @click="copyContent('知乎')">复制并发布到知乎</el-button>
<el-button type="primary" @click="copyContent('小红书')">复制并发布到小红书</el-button>
</div>
</template>
</el-dialog>
</div>
<script src="/static/vue/vue.global.js"></script>
<script src="/static/element-plus/index.full.min.js"></script>
@@ -194,17 +218,36 @@ const TopicsApp = {
selectedTopicIds: [],
filterStatus: '',
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
topics: []
topics: [],
previewVisible: false,
previewTopic: null
}
},
computed: {
filteredTopics() {
filteredTopics() {
if (!this.topics || !this.topics.length) { return []; }
if (!this.filterStatus) { return this.topics; }
return this.topics.filter(t => t.status === this.filterStatus);
const statusMap = {
'pending': ['pending', '待处理'],
'review': ['review', '待审查'],
'ready': ['ready', '待发布'],
'published': ['published', '已发布']
};
const allowed = statusMap[this.filterStatus] || [this.filterStatus];
return this.topics.filter(t => allowed.includes(t.status));
},
countByStatus() {
return (status) => this.topics.filter(t => t.status === status).length;
statusStats() {
const pending = ['pending', '待处理'];
const review = ['review', '待审查'];
const ready = ['ready', '待发布'];
const published = ['published', '已发布'];
return {
total: this.topics.length,
pending: this.topics.filter(t => pending.includes(t.status)).length,
review: this.topics.filter(t => review.includes(t.status)).length,
ready: this.topics.filter(t => ready.includes(t.status)).length,
published: this.topics.filter(t => published.includes(t.status)).length
};
}
},
methods: {
@@ -249,9 +292,25 @@ const TopicsApp = {
this.selectedTopicIds = [];
await this.fetchTopics();
},
openPreview(topic) { this.$message.info('预览:' + topic.title); },
openPreview(topic) {
this.previewTopic = topic;
this.previewVisible = true;
},
copyContent(platform) {
if (!this.previewTopic || !this.previewTopic.content) {
this.$message.warning('暂无内容可复制');
return;
}
const text = `标题:${this.previewTopic.title}\n\n内容:\n${this.previewTopic.content}`;
navigator.clipboard.writeText(text).then(() => {
this.$message.success(`已复制内容,请前往${platform}粘贴发布`);
}).catch(err => {
console.error('复制失败', err);
this.$message.error('复制失败,请手动复制');
});
},
async createTopic(topic) {
if (topic.status !== 'pending') {
if (!this.isStatus(topic, 'pending')) {
this.$message.info('仅待处理选题可创作');
return;
}
@@ -282,7 +341,7 @@ const TopicsApp = {
}
},
async optimizeTopic(topic) {
if (topic.status !== 'review') {
if (!this.isStatus(topic, 'review')) {
this.$message.info('仅待审查选题可优化');
return;
}
@@ -313,7 +372,7 @@ const TopicsApp = {
}
},
async handlePublish(topic) {
if (topic.status !== 'ready') {
if (!this.isStatus(topic, 'ready')) {
this.$message.info('仅待发布选题可发布');
return;
}
@@ -382,8 +441,26 @@ const TopicsApp = {
} catch (e) { return dateStr; }
},
getStatusType(status) {
const map = { 'pending': 'warning', 'review': 'danger', 'ready': 'success', 'published': 'info' };
const map = {
'pending': 'warning',
'review': 'danger',
'ready': 'success',
'published': 'info',
'待处理': 'warning',
'待审查': 'danger',
'待发布': 'success',
'已发布': 'info'
};
return map[status] || 'primary';
},
isStatus(row, status) {
const map = {
'pending': ['pending', '待处理'],
'review': ['review', '待审查'],
'ready': ['ready', '待发布'],
'published': ['published', '已发布']
};
return map[status] ? map[status].includes(row.status) : row.status === status;
}
},
mounted() {
+10 -1
View File
@@ -30,6 +30,7 @@
.content-area { padding: 16px; padding-bottom: 80px; }
}
.user-card-list { display: none; }
/* users.html 移动端优化 */
@media (max-width: 768px) {
.user-table { display: none; }
@@ -204,7 +205,15 @@
.catch(() => {});
},
handleLogout() { localStorage.removeItem('authToken'); window.location.href = '/'; },
redirectToPage(page) { window.location.href = '/' + page; },
redirectToPage(page) {
if (!page) return;
// 处理绝对URL或相对路径
if (page.startsWith('/') || page.startsWith('http://') || page.startsWith('https://')) {
window.location.href = page;
} else {
window.location.href = '/' + page;
}
},
formatDate(dateStr) { if (!dateStr) return '-'; return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }
},
mounted() {