chore: 清理未使用的代码文件和备份
- 删除 backend/main.py (已由 app/main.py 替代) - 删除 backend/api/ 和 backend/core/ (已由 app/api/ 和 app/core/ 替代) - 删除 backend/static 符号链接 - 删除前端测试/调试页面 - 删除未引用的 vendor 子目录 (element-plus/, vue/, axios/) - 删除所有 .bak 备份文件
This commit is contained in:
Vendored
-1
@@ -1 +0,0 @@
|
||||
Redirecting to /axios@1.15.2/dist/axios.min.js
|
||||
@@ -1,39 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>导航组件调试</title>
|
||||
<script src="vue.global.prod.js"></script>
|
||||
<script src="element-plus.full.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<navigation-component current-page="dashboard" :is-admin="true" @navigate="()=>{}"></navigation-component>
|
||||
</div>
|
||||
|
||||
<script src="navigation-component.js"></script>
|
||||
<script>
|
||||
// 手动检查
|
||||
console.log('NavigationComponent:', window.NavigationComponent);
|
||||
console.log('installNavigation:', window.installNavigation);
|
||||
|
||||
const App = { data() { return { isAdmin: true } } };
|
||||
const app = Vue.createApp(App);
|
||||
|
||||
if (window.installNavigation) {
|
||||
window.installNavigation(app);
|
||||
console.log('通过 installNavigation 注册');
|
||||
} else if (window.NavigationComponent) {
|
||||
app.component('navigation-component', window.NavigationComponent);
|
||||
console.log('直接注册组件');
|
||||
// 手动注入样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '.navigation-wrapper .sidebar { position: fixed; top: 0; left: 0; bottom: 0; width: 180px; background: #f0f0f0; z-index: 9999; }';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,601 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>宇之然内容创作平台</title>
|
||||
<link rel="stylesheet" href="element-plus.css">
|
||||
<style>
|
||||
/* 深色渐变背景主题 */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
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;
|
||||
color: #e0e6ed;
|
||||
}
|
||||
|
||||
/* 导航栏 */
|
||||
.navbar {
|
||||
background: rgba(102, 126, 234, 0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba(102, 126, 234, 0.2);
|
||||
padding: 16px 24px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.navbar-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.navbar-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #a0aec0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
display: flex;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
background: rgba(26, 26, 46, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 16px 12px;
|
||||
border-right: 1px solid rgba(102, 126, 234, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.sidebar-btn {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: #a0aec0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sidebar-btn:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
.sidebar-btn.active {
|
||||
background: linear-gradient(90deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2));
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
.sidebar-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: linear-gradient(180deg, #667eea, #764ba2);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 32px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 页面切换 */
|
||||
.page { display: none; animation: fadeIn 0.5s ease-out; }
|
||||
.page.active { display: block; }
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 统计卡片网格 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.stat-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(102, 126, 234, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(102, 126, 234, 0.1), transparent);
|
||||
transition: left 0.6s;
|
||||
}
|
||||
.stat-card:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
border-color: rgba(102, 126, 234, 0.4);
|
||||
box-shadow: 0 12px 32px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
.stat-title {
|
||||
font-size: 13px;
|
||||
color: #a0aec0;
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.stat-card.primary .stat-value { background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.stat-card.success .stat-value { background: linear-gradient(135deg, #67c23a, #85e61d); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.stat-card.warning .stat-value { background: linear-gradient(135deg, #e6a23c, #f5c543); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.stat-card.danger .stat-value { background: linear-gradient(135deg, #f56c6c, #f79296); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.stat-card.info .stat-value { background: linear-gradient(135deg, #409eff, #5cd0f3); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
|
||||
/* 模块卡片 */
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.module-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(102, 126, 234, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
.module-card:hover {
|
||||
transform: translateY(-6px);
|
||||
border-color: rgba(102, 126, 234, 0.3);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.module-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.module-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #e0e6ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.module-status {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: rgba(103, 194, 58, 0.2);
|
||||
color: #67c23a;
|
||||
border: 1px solid rgba(103, 194, 58, 0.3);
|
||||
}
|
||||
.module-status.running {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.4); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(103, 194, 58, 0); }
|
||||
}
|
||||
.module-content {
|
||||
font-size: 14px;
|
||||
color: #a0aec0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.module-content div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px dashed rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.module-content div:last-child { border-bottom: none; }
|
||||
|
||||
/* 移动端导航 */
|
||||
.mobile-nav {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1px solid rgba(102, 126, 234, 0.2);
|
||||
padding: 8px 0;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.mobile-nav-btn {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #a0aec0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.mobile-nav-btn.active {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
.mobile-nav-btn.active::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { display: none; }
|
||||
.mobile-nav { display: flex; }
|
||||
.content-area {
|
||||
padding: 16px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
.stat-card { padding: 16px; }
|
||||
.stat-value { font-size: 24px; }
|
||||
.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">
|
||||
<nav class="navbar" v-if="isLoggedIn">
|
||||
<div class="navbar-content">
|
||||
<h1 class="navbar-title">宇之然内容创作平台</h1>
|
||||
<div class="navbar-user">
|
||||
<div class="user-info">
|
||||
<div class="avatar">{{ currentUser.username ? currentUser.username.charAt(0).toUpperCase() : '?' }}</div>
|
||||
<span>{{ currentUser.username }}</span>
|
||||
<el-tag v-if="isAdmin" size="small" type="danger" style="border: none;">管理员</el-tag>
|
||||
</div>
|
||||
<el-button size="small" type="danger" plain @click="handleLogout">退出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="main-content" v-if="isLoggedIn">
|
||||
<aside class="sidebar">
|
||||
<button class="sidebar-btn" :class="{ active: currentPage === 'overview' }" @click="redirectToPage('/')">
|
||||
📊 系统概览
|
||||
</button>
|
||||
<button class="sidebar-btn" :class="{ active: currentPage === 'topics' }" @click="redirectToPage('topics.html')">
|
||||
📋 选题管理
|
||||
</button>
|
||||
<button class="sidebar-btn" :class="{ active: currentPage === 'logs' }" @click="redirectToPage('logs.html')">
|
||||
📄 系统日志
|
||||
</button>
|
||||
<button v-if="isAdmin" class="sidebar-btn" :class="{ active: currentPage === 'users' }" @click="redirectToPage('users.html')">
|
||||
👥 用户管理
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main class="content-area">
|
||||
<!-- 系统概览页面 -->
|
||||
<div id="page-overview" class="page" :class="{ active: currentPage === 'overview' }">
|
||||
<h2 style="font-size: 28px; font-weight: 700; margin-bottom: 32px; color: #e0e6ed;">
|
||||
📊 系统概览
|
||||
</h2>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card primary" @click="goToTopics('')">
|
||||
<div class="stat-title">选题总数</div>
|
||||
<div class="stat-value">{{ stats.total }}</div>
|
||||
</div>
|
||||
<div class="stat-card warning" @click="goToTopics('pending')">
|
||||
<div class="stat-title">待处理</div>
|
||||
<div class="stat-value">{{ stats.pending }}</div>
|
||||
</div>
|
||||
<div class="stat-card danger" @click="goToTopics('review')">
|
||||
<div class="stat-title">待审查</div>
|
||||
<div class="stat-value">{{ stats.review }}</div>
|
||||
</div>
|
||||
<div class="stat-card success" @click="goToTopics('ready')">
|
||||
<div class="stat-title">待发布</div>
|
||||
<div class="stat-value">{{ stats.ready }}</div>
|
||||
</div>
|
||||
<div class="stat-card info" @click="goToTopics('published')">
|
||||
<div class="stat-title">已发布</div>
|
||||
<div class="stat-value">{{ stats.published }}</div>
|
||||
</div>
|
||||
<div class="stat-card primary" @click="goToTopics('')">
|
||||
<div class="stat-title">今日新增</div>
|
||||
<div class="stat-value">{{ stats.today }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模块状态 -->
|
||||
<h3 style="font-size: 20px; font-weight: 600; margin-bottom: 24px; color: #e0e6ed;">
|
||||
🔧 模块状态
|
||||
</h3>
|
||||
<div class="module-grid">
|
||||
<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: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>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 移动端导航 -->
|
||||
<nav class="mobile-nav" v-if="isLoggedIn">
|
||||
<button class="mobile-nav-btn" :class="{ active: currentPage === 'overview' }" @click="redirectToPage('/')">
|
||||
📊 概览
|
||||
</button>
|
||||
<button class="mobile-nav-btn" :class="{ active: currentPage === 'topics' }" @click="redirectToPage('topics.html')">
|
||||
📋 选题
|
||||
</button>
|
||||
<button class="mobile-nav-btn" :class="{ active: currentPage === 'logs' }" @click="redirectToPage('logs.html')">
|
||||
📄 日志
|
||||
</button>
|
||||
<button v-if="isAdmin" class="mobile-nav-btn" :class="{ active: currentPage === 'users' }" @click="redirectToPage('users.html')">
|
||||
👥 用户
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<script src="vue.global.prod.js"></script>
|
||||
<script src="element-plus.full.js"></script>
|
||||
<script>
|
||||
const App = {
|
||||
data() {
|
||||
return {
|
||||
isLoggedIn: false,
|
||||
isAdmin: false,
|
||||
currentUser: { username: '' },
|
||||
currentPage: 'overview',
|
||||
stats: {
|
||||
total: 0,
|
||||
pending: 0,
|
||||
review: 0,
|
||||
ready: 0,
|
||||
published: 0,
|
||||
today: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
this.loginLoading = true;
|
||||
this.loginError = '';
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.loginForm)
|
||||
});
|
||||
if (!response.ok) throw new Error('登录失败');
|
||||
const data = await response.json();
|
||||
localStorage.setItem('authToken', data.token);
|
||||
this.currentUser = data.user;
|
||||
this.isAdmin = data.user.role === 'admin';
|
||||
this.isLoggedIn = true;
|
||||
this.currentPage = 'overview';
|
||||
this.fetchStats();
|
||||
} catch (error) {
|
||||
this.loginError = '用户名或密码错误';
|
||||
} finally {
|
||||
this.loginLoading = false;
|
||||
}
|
||||
},
|
||||
handleLogout() {
|
||||
localStorage.removeItem('authToken');
|
||||
this.isLoggedIn = false;
|
||||
this.currentUser = { username: '' };
|
||||
this.isAdmin = false;
|
||||
},
|
||||
async fetchStats() {
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/system/status', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// API返回格式: { stats: { total, pending, review, ready, published, today } }
|
||||
this.stats = {
|
||||
total: data.stats?.total || 0,
|
||||
pending: data.stats?.pending || 0,
|
||||
review: data.stats?.review || 0,
|
||||
ready: data.stats?.ready || 0,
|
||||
published: data.stats?.published || 0,
|
||||
today: data.stats?.today || 0
|
||||
};
|
||||
} else if (response.status === 401) {
|
||||
// Token无效,清除并跳转登录
|
||||
localStorage.removeItem('authToken');
|
||||
window.location.href = '/login.html';
|
||||
} else {
|
||||
console.error('获取统计信息失败:', response.status, response.statusText);
|
||||
this.stats = { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error);
|
||||
// 失败时设置为0,避免页面空白
|
||||
this.stats = { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 };
|
||||
}
|
||||
},
|
||||
goToTopics(filter) {
|
||||
const url = filter ? '/topics.html?filter=' + encodeURIComponent(filter) : '/topics.html';
|
||||
window.location.href = url;
|
||||
},
|
||||
redirectToPage(page) {
|
||||
window.location.href = page.startsWith('/') ? page : '/' + page;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
fetch('/api/auth/me', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
})
|
||||
.then(response => response.ok ? response.json() : Promise.reject())
|
||||
.then(data => {
|
||||
this.currentUser = data.user;
|
||||
this.isAdmin = data.user.role === 'admin';
|
||||
this.isLoggedIn = true;
|
||||
this.currentPage = 'overview';
|
||||
this.fetchStats();
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.removeItem('authToken');
|
||||
window.location.href = '/login.html';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const app = Vue.createApp(App);
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="vue.global.prod.js"></script>
|
||||
<script src="navigation-component.js"></script>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; }
|
||||
.navbar { height: 60px; background: #333; color: white; display: flex; align-items: center; padding: 0 20px; }
|
||||
.main-content { padding: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="navbar">Test Navbar</div>
|
||||
<navigation-component current-page="dashboard" :is-admin="true" @navigate="()=>{}"></navigation-component>
|
||||
<div class="main-content">
|
||||
<h1>内容区域</h1>
|
||||
<p>侧边栏应该在左侧显示。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() { return { isAdmin: true } }
|
||||
});
|
||||
if (window.installNavigation) {
|
||||
window.installNavigation(app);
|
||||
console.log('已调用 installNavigation');
|
||||
} else {
|
||||
console.error('installNavigation 未定义');
|
||||
}
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,51 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>导航组件测试</title>
|
||||
<link rel="stylesheet" href="element-plus.css">
|
||||
<script src="vue.global.prod.js"></script>
|
||||
<script src="element-plus.full.js"></script>
|
||||
<script src="navigation-component.js"></script>
|
||||
<style>
|
||||
body { margin: 0; font-family: sans-serif; }
|
||||
.navbar { background: #2563eb; color: white; padding: 16px; }
|
||||
.main-content { padding: 24px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<nav class="navbar">
|
||||
<span>测试页面</span>
|
||||
</nav>
|
||||
|
||||
<navigation-component
|
||||
current-page="topics"
|
||||
:is-admin="true"
|
||||
@navigate="redirectToPage"
|
||||
></navigation-component>
|
||||
|
||||
<div class="main-content">
|
||||
<h1>内容区域</h1>
|
||||
<p>如果看到左边栏,说明组件工作正常。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const App = {
|
||||
data() {
|
||||
return { isAdmin: true };
|
||||
},
|
||||
methods: {
|
||||
redirectToPage(page) {
|
||||
console.log('导航到:', page);
|
||||
}
|
||||
}
|
||||
};
|
||||
const app = Vue.createApp(App);
|
||||
if (window.installNavigation) { window.installNavigation(app); }
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,682 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>宇之然内容创作平台 - 选题管理</title>
|
||||
<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); }
|
||||
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
|
||||
.navbar-title { font-size: 20px; font-weight: 600; }
|
||||
.navbar-user { display: flex; align-items: center; gap: 16px; }
|
||||
.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: 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; }
|
||||
.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; }
|
||||
.status-dot.review { background: #F56C6C; }
|
||||
.status-dot.ready { background: #67C23A; }
|
||||
.status-dot.published { background: #409EFF; }
|
||||
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
|
||||
.mobile-nav-btn { flex: 1; border: none; background: transparent; padding: 12px; text-align: center; font-size: 12px; color: #606266; cursor: pointer; }
|
||||
.mobile-nav-btn.active { color: #409eff; font-weight: 600; }
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { display: none; }
|
||||
.mobile-nav { display: flex; }
|
||||
.content-area { padding: 12px; padding-bottom: 80px; }
|
||||
}
|
||||
|
||||
.topic-card-list { display: none; }
|
||||
/* 移动端卡片布局 */
|
||||
@media (max-width: 768px) {
|
||||
.el-table { font-size: 12px; display: none; }
|
||||
.el-table .el-button { padding: 4px 8px; font-size: 11px; min-height: auto; }
|
||||
.el-table .cell { padding: 0 4px; }
|
||||
.el-table .el-table__cell { padding: 6px 0; }
|
||||
.topic-card-list { display: block; margin: 0 -16px; }
|
||||
.topic-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
.topic-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.topic-card-title { font-size: 16px; font-weight: 600; color: #303133; flex: 1; margin-right: 8px; }
|
||||
.topic-card-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.topic-card-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.topic-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
.topic-card-actions .el-button { flex: 1; min-width: 60px; }
|
||||
.mobile-nav { display: flex; }
|
||||
}
|
||||
|
||||
.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">
|
||||
<nav class="navbar">
|
||||
<div class="navbar-content">
|
||||
<h1 class="navbar-title">宇之然内容创作平台 - 选题管理</h1>
|
||||
<div class="navbar-user">
|
||||
<div class="user-info"><div class="avatar">{{ currentUser.username ? currentUser.username.charAt(0).toUpperCase() : '?' }}</div><span>{{ currentUser.username }}</span></div>
|
||||
<el-button type="danger" size="small" @click="handleLogout">退出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="main-content">
|
||||
<aside class="sidebar">
|
||||
<button class="sidebar-btn" @click="redirectToPage('/')">📊 系统概览</button>
|
||||
<button class="sidebar-btn active">📋 选题管理</button>
|
||||
<button class="sidebar-btn" @click="redirectToPage('logs.html')">📄 系统日志</button>
|
||||
<button v-if="isAdmin" class="sidebar-btn" @click="redirectToPage('users.html')">👥 用户管理</button>
|
||||
<button v-if="isAdmin" class="sidebar-btn" @click="redirectToPage('admin.html')">⚙️ 系统</button>
|
||||
</aside>
|
||||
<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: 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>
|
||||
<el-button type="warning" size="small" @click="triggerOptimizeSelected" :disabled="selectedTopicIds.length === 0">🔍 批量优化</el-button>
|
||||
<span class="ml-auto text-sm text-gray-500" v-if="selectedTopicIds.length > 0" style="color: #909399; font-size: 14px; margin-left: auto;">已选 {{ selectedTopicIds.length }} 项</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap;">
|
||||
<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: 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>
|
||||
<el-table-column prop="title" label="标题" min-width="200"></el-table-column>
|
||||
<el-table-column prop="field" label="领域" width="100"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90">
|
||||
<template #default="scope"><span class="status-badge"><span class="status-dot" :class="scope.row.status"></span>{{ getStatusLabel(scope.row.status) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="compliance_score" label="合规分" width="90">
|
||||
<template #default="scope"><el-progress :percentage="scope.row.compliance_score || 0" :format="() => scope.row.compliance_score || '-'" :stroke-width="15"></el-progress></template>
|
||||
</el-table-column>
|
||||
<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="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="isStatus(scope.row, 'published')" @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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 移动端卡片列表 -->
|
||||
<div class="topic-card-list" v-if="filteredTopics && filteredTopics.length > 0">
|
||||
<div v-for="(topic, index) in filteredTopics" :key="topic.id" class="topic-card">
|
||||
<div class="topic-card-header">
|
||||
<div class="topic-card-title">{{ topic.id }}. {{ topic.title }}</div>
|
||||
<el-tag :type="getStatusType(topic.status)" size="small">{{ getStatusLabel(topic.status) }}</el-tag>
|
||||
</div>
|
||||
<div class="topic-card-tags">
|
||||
<el-tag size="small" type="info">{{ topic.field }}</el-tag>
|
||||
<el-tag size="small" type="warning">合规{{ topic.compliance_score }}</el-tag>
|
||||
</div>
|
||||
<div class="topic-card-meta">
|
||||
<div>创建: {{ formatDate(topic.created_at) }}</div>
|
||||
<div>创作: {{ topic.generated_at ? formatDate(topic.generated_at) : '-' }}</div>
|
||||
<div>发布: {{ topic.published_at ? formatDate(topic.published_at) : '-' }}</div>
|
||||
</div>
|
||||
<div class="topic-card-actions">
|
||||
<el-button size="small" @click="openPreview(topic)" type="primary">预览</el-button>
|
||||
<el-button size="small" type="success" :disabled="isStatus(topic, 'published')" @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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<nav class="mobile-nav">
|
||||
<button class="mobile-nav-btn" @click="redirectToPage('/')">📊 概览</button>
|
||||
<button class="mobile-nav-btn active">📋 选题</button>
|
||||
<button class="mobile-nav-btn" @click="redirectToPage('logs.html')">📄 日志</button>
|
||||
<button v-if="isAdmin" class="mobile-nav-btn" @click="redirectToPage('users.html')">👥 用户</button>
|
||||
<button v-if="isAdmin" class="mobile-nav-btn" @click="redirectToPage('admin.html')">⚙️ 系统</button>
|
||||
</nav>
|
||||
<!-- 预览弹窗 -->
|
||||
<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;">
|
||||
<el-button-group>
|
||||
<el-button :type="previewPlatform === 'zhihu' ? 'primary' : 'default'" @click="previewPlatform = 'zhihu'">知乎</el-button>
|
||||
<el-button :type="previewPlatform === 'wechat' ? 'primary' : 'default'" @click="previewPlatform = 'wechat'">微信公众号</el-button>
|
||||
<el-button :type="previewPlatform === 'xiaohongshu' ? 'primary' : 'default'" @click="previewPlatform = 'xiaohongshu'">小红书</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 预览内容使用 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>
|
||||
<template #footer>
|
||||
<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>
|
||||
</div>
|
||||
<script src="vue.global.prod.js"></script>
|
||||
<script src="element-plus.full.js"></script>
|
||||
|
||||
<script>
|
||||
const TopicsApp = {
|
||||
data() {
|
||||
return {
|
||||
currentPage: 'topics',
|
||||
isLoggedIn: false,
|
||||
isAdmin: false,
|
||||
currentUser: { username: '' },
|
||||
loadingTable: false,
|
||||
selectedTopicIds: [],
|
||||
filterStatus: '',
|
||||
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
|
||||
topics: [],
|
||||
previewVisible: false,
|
||||
previewTopic: null,
|
||||
previewFullscreen: false,
|
||||
previewPlatform: 'zhihu',
|
||||
platformContents: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredTopics() {
|
||||
if (!this.topics || !this.topics.length) { return []; }
|
||||
if (!this.filterStatus) { return this.topics; }
|
||||
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));
|
||||
},
|
||||
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
|
||||
};
|
||||
},
|
||||
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: {
|
||||
async fetchTopics() {
|
||||
this.loadingTable = true;
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
setTimeout(() => window.location.href = '/', 1500);
|
||||
this.loadingTable = false;
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/topics', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.topics = data || [];
|
||||
this.$message.success('选题加载成功');
|
||||
} catch (error) {
|
||||
console.error('获取选题失败:', error);
|
||||
this.$message.error(`获取选题失败: ${error.message}`);
|
||||
this.topics = [];
|
||||
} finally {
|
||||
this.loadingTable = false;
|
||||
}
|
||||
},
|
||||
refreshAll() { this.$message.info('执行批量刷新'); },
|
||||
async triggerGenerateSelected() {
|
||||
if (!this.selectedTopicIds.length) return;
|
||||
this.$message.success('批量创作已启动');
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
// 取第一个选题(当前简单实现)
|
||||
const topicId = this.selectedTopicIds[0];
|
||||
const response = await fetch('/api/system/generate/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_id: topicId })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
await response.json();
|
||||
this.$message.success('批量创作已启动');
|
||||
this.selectedTopicIds = [];
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('批量创作失败:', error);
|
||||
this.$message.error(`批量创作失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async triggerOptimizeSelected() {
|
||||
if (!this.selectedTopicIds.length) return;
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/system/optimize/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_ids: this.selectedTopicIds })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success('批量优化完成');
|
||||
this.selectedTopicIds = [];
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('批量优化失败:', error);
|
||||
this.$message.error(`批量优化失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async openPreview(topic) {
|
||||
this.previewTopic = topic;
|
||||
this.previewPlatform = 'zhihu';
|
||||
this.previewVisible = true;
|
||||
this.platformContents = {};
|
||||
// 并行加载所有平台内容
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.warning('请先登录');
|
||||
return;
|
||||
}
|
||||
const platforms = ['zhihu', 'wechat', 'xiaohongshu'];
|
||||
const promises = platforms.map(p =>
|
||||
fetch(`/api/articles/${topic.id}/preview?platform=${p}`, {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
})
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => {
|
||||
if (d && d.html) {
|
||||
this.platformContents[p] = d.html;
|
||||
}
|
||||
})
|
||||
.catch(e => console.error(`加载${p}预览失败:`, e))
|
||||
);
|
||||
await Promise.all(promises);
|
||||
},
|
||||
togglePreviewFullscreen() {
|
||||
this.previewFullscreen = !this.previewFullscreen;
|
||||
},
|
||||
platformName(platform) {
|
||||
const names = {
|
||||
zhihu: '知乎',
|
||||
wechat: '微信公众号',
|
||||
xiaohongshu: '小红书'
|
||||
};
|
||||
return names[platform] || platform;
|
||||
},
|
||||
// 计算属性:当前平台预览的完整 HTML(响应式更新)
|
||||
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) {
|
||||
console.log('createTopic clicked, topic:', topic);
|
||||
if (this.isStatus(topic, 'published')) {
|
||||
this.$message.info('已发布选题不可创作');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/system/generate/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_id: topic.id })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success(`创作完成: ${topic.title}`);
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('创作失败:', error);
|
||||
this.$message.error(`创作失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async optimizeTopic(topic) {
|
||||
if (!this.isStatus(topic, 'review')) {
|
||||
this.$message.info('仅待审查选题可优化');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/system/optimize/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_ids: [topic.id] })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success(`优化完成: ${topic.title}`);
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('优化失败:', error);
|
||||
this.$message.error(`优化失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async handlePublish(topic) {
|
||||
if (!this.isStatus(topic, 'ready')) {
|
||||
this.$message.info('仅待发布选题可发布');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/publishing/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_ids: [topic.id] })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success(`发布成功: ${topic.title}`);
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('发布失败:', error);
|
||||
this.$message.error(`发布失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async deleteTopic(id) {
|
||||
this.$confirm('确定删除?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
|
||||
.then(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
const response = await fetch(`/api/topics/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
this.$message.success('删除成功');
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
this.$message.error(`删除失败: ${error.message}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
handleLogout() { localStorage.removeItem('authToken'); window.location.href = '/'; },
|
||||
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
|
||||
getStatusLabel(status) {
|
||||
const statusMap = {
|
||||
'pending': '待处理',
|
||||
'review': '待审查',
|
||||
'ready': '待发布',
|
||||
'published': '已发布'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
},
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
try {
|
||||
return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
} catch (e) { return dateStr; }
|
||||
},
|
||||
getStatusType(status) {
|
||||
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() {
|
||||
console.log('[DEBUG] TopicsApp mounted');
|
||||
const token = localStorage.getItem('authToken');
|
||||
console.log('[DEBUG] Token exists:', !!token);
|
||||
if (!token) { window.location.href = '/'; return; }
|
||||
// 解析 URL filter 参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const filter = urlParams.get('filter');
|
||||
console.log('[DEBUG] URL filter:', filter);
|
||||
if (filter) { this.filterStatus = filter; }
|
||||
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
|
||||
.then(response => response.ok ? response.json() : Promise.reject())
|
||||
.then(data => {
|
||||
console.log('[DEBUG] Auth success, user:', data.user);
|
||||
this.currentUser = data.user;
|
||||
this.isAdmin = data.user.role === 'admin';
|
||||
this.isLoggedIn = true;
|
||||
this.fetchTopics();
|
||||
})
|
||||
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/'; });
|
||||
}
|
||||
};
|
||||
const app = Vue.createApp(TopicsApp);
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,649 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>宇之然内容创作平台 - 选题管理</title>
|
||||
<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); }
|
||||
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
|
||||
.navbar-title { font-size: 20px; font-weight: 600; }
|
||||
.navbar-user { display: flex; align-items: center; gap: 16px; }
|
||||
.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%; }
|
||||
|
||||
/* 移动端卡片布局 */
|
||||
@media (max-width: 768px) {
|
||||
.el-table .el-button { padding: 4px 8px; font-size: 11px; min-height: auto; }
|
||||
.el-table .cell { padding: 0 4px; }
|
||||
.el-table .el-table__cell { padding: 6px 0; }
|
||||
.topic-card-list { display: block; margin: 0 -16px; }
|
||||
.topic-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
.topic-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.topic-card-title { font-size: 16px; font-weight: 600; color: #303133; flex: 1; margin-right: 8px; }
|
||||
.topic-card-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.topic-card-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.topic-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
.topic-card-actions .el-button { flex: 1; min-width: 60px; }
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
<script src="navigation-component.js"></script></head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<nav class="navbar">
|
||||
<div class="navbar-content">
|
||||
<h1 class="navbar-title">宇之然内容创作平台 - 选题管理</h1>
|
||||
<div class="navbar-user">
|
||||
<div class="user-info"><div class="avatar">{{ currentUser.username ? currentUser.username.charAt(0).toUpperCase() : '?' }}</div><span>{{ currentUser.username }}</span></div>
|
||||
<el-button type="danger" size="small" @click="handleLogout">退出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="main-content">
|
||||
|
||||
<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: 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>
|
||||
<el-button type="warning" size="small" @click="triggerOptimizeSelected" :disabled="selectedTopicIds.length === 0">🔍 批量优化</el-button>
|
||||
<span class="ml-auto text-sm text-gray-500" v-if="selectedTopicIds.length > 0" style="color: #909399; font-size: 14px; margin-left: auto;">已选 {{ selectedTopicIds.length }} 项</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap;">
|
||||
<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: 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>
|
||||
<el-table-column prop="title" label="标题" min-width="200"></el-table-column>
|
||||
<el-table-column prop="field" label="领域" width="100"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90">
|
||||
<template #default="scope"><span class="status-badge"><span class="status-dot" :class="scope.row.status"></span>{{ getStatusLabel(scope.row.status) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="compliance_score" label="合规分" width="90">
|
||||
<template #default="scope"><el-progress :percentage="scope.row.compliance_score || 0" :format="() => scope.row.compliance_score || '-'" :stroke-width="15"></el-progress></template>
|
||||
</el-table-column>
|
||||
<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="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="isStatus(scope.row, 'published')" @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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 移动端卡片列表 -->
|
||||
<div class="topic-card-list" v-if="filteredTopics && filteredTopics.length > 0">
|
||||
<div v-for="(topic, index) in filteredTopics" :key="topic.id" class="topic-card">
|
||||
<div class="topic-card-header">
|
||||
<div class="topic-card-title">{{ topic.id }}. {{ topic.title }}</div>
|
||||
<el-tag :type="getStatusType(topic.status)" size="small">{{ getStatusLabel(topic.status) }}</el-tag>
|
||||
</div>
|
||||
<div class="topic-card-tags">
|
||||
<el-tag size="small" type="info">{{ topic.field }}</el-tag>
|
||||
<el-tag size="small" type="warning">合规{{ topic.compliance_score }}</el-tag>
|
||||
</div>
|
||||
<div class="topic-card-meta">
|
||||
<div>创建: {{ formatDate(topic.created_at) }}</div>
|
||||
<div>创作: {{ topic.generated_at ? formatDate(topic.generated_at) : '-' }}</div>
|
||||
<div>发布: {{ topic.published_at ? formatDate(topic.published_at) : '-' }}</div>
|
||||
</div>
|
||||
<div class="topic-card-actions">
|
||||
<el-button size="small" @click="openPreview(topic)" type="primary">预览</el-button>
|
||||
<el-button size="small" type="success" :disabled="isStatus(topic, 'published')" @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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 预览弹窗 -->
|
||||
<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;">
|
||||
<el-button-group>
|
||||
<el-button :type="previewPlatform === 'zhihu' ? 'primary' : 'default'" @click="previewPlatform = 'zhihu'">知乎</el-button>
|
||||
<el-button :type="previewPlatform === 'wechat' ? 'primary' : 'default'" @click="previewPlatform = 'wechat'">微信公众号</el-button>
|
||||
<el-button :type="previewPlatform === 'xiaohongshu' ? 'primary' : 'default'" @click="previewPlatform = 'xiaohongshu'">小红书</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 预览内容使用 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>
|
||||
<template #footer>
|
||||
<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>
|
||||
</div>
|
||||
<script src="vue.global.prod.js"></script>
|
||||
<script src="element-plus.full.js"></script>
|
||||
|
||||
<script>
|
||||
const TopicsApp = {
|
||||
data() {
|
||||
return {
|
||||
currentPage: 'topics',
|
||||
isLoggedIn: false,
|
||||
isAdmin: false,
|
||||
currentUser: { username: '' },
|
||||
loadingTable: false,
|
||||
selectedTopicIds: [],
|
||||
filterStatus: '',
|
||||
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
|
||||
topics: [],
|
||||
previewVisible: false,
|
||||
previewTopic: null,
|
||||
previewFullscreen: false,
|
||||
previewPlatform: 'zhihu',
|
||||
platformContents: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredTopics() {
|
||||
if (!this.topics || !this.topics.length) { return []; }
|
||||
if (!this.filterStatus) { return this.topics; }
|
||||
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));
|
||||
},
|
||||
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
|
||||
};
|
||||
},
|
||||
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: {
|
||||
async fetchTopics() {
|
||||
this.loadingTable = true;
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
setTimeout(() => window.location.href = '/', 1500);
|
||||
this.loadingTable = false;
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/topics', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.topics = data || [];
|
||||
this.$message.success('选题加载成功');
|
||||
} catch (error) {
|
||||
console.error('获取选题失败:', error);
|
||||
this.$message.error(`获取选题失败: ${error.message}`);
|
||||
this.topics = [];
|
||||
} finally {
|
||||
this.loadingTable = false;
|
||||
}
|
||||
},
|
||||
refreshAll() { this.$message.info('执行批量刷新'); },
|
||||
async triggerGenerateSelected() {
|
||||
if (!this.selectedTopicIds.length) return;
|
||||
this.$message.success('批量创作已启动');
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
// 取第一个选题(当前简单实现)
|
||||
const topicId = this.selectedTopicIds[0];
|
||||
const response = await fetch('/api/system/generate/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_id: topicId })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
await response.json();
|
||||
this.$message.success('批量创作已启动');
|
||||
this.selectedTopicIds = [];
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('批量创作失败:', error);
|
||||
this.$message.error(`批量创作失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async triggerOptimizeSelected() {
|
||||
if (!this.selectedTopicIds.length) return;
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/system/optimize/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_ids: this.selectedTopicIds })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success('批量优化完成');
|
||||
this.selectedTopicIds = [];
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('批量优化失败:', error);
|
||||
this.$message.error(`批量优化失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async openPreview(topic) {
|
||||
this.previewTopic = topic;
|
||||
this.previewPlatform = 'zhihu';
|
||||
this.previewVisible = true;
|
||||
this.platformContents = {};
|
||||
// 并行加载所有平台内容
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.warning('请先登录');
|
||||
return;
|
||||
}
|
||||
const platforms = ['zhihu', 'wechat', 'xiaohongshu'];
|
||||
const promises = platforms.map(p =>
|
||||
fetch(`/api/articles/${topic.id}/preview?platform=${p}`, {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
})
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => {
|
||||
if (d && d.html) {
|
||||
this.platformContents[p] = d.html;
|
||||
}
|
||||
})
|
||||
.catch(e => console.error(`加载${p}预览失败:`, e))
|
||||
);
|
||||
await Promise.all(promises);
|
||||
},
|
||||
togglePreviewFullscreen() {
|
||||
this.previewFullscreen = !this.previewFullscreen;
|
||||
},
|
||||
platformName(platform) {
|
||||
const names = {
|
||||
zhihu: '知乎',
|
||||
wechat: '微信公众号',
|
||||
xiaohongshu: '小红书'
|
||||
};
|
||||
return names[platform] || platform;
|
||||
},
|
||||
// 计算属性:当前平台预览的完整 HTML(响应式更新)
|
||||
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) {
|
||||
console.log('createTopic clicked, topic:', topic);
|
||||
if (this.isStatus(topic, 'published')) {
|
||||
this.$message.info('已发布选题不可创作');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/system/generate/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_id: topic.id })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success(`创作完成: ${topic.title}`);
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('创作失败:', error);
|
||||
this.$message.error(`创作失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async optimizeTopic(topic) {
|
||||
if (!this.isStatus(topic, 'review')) {
|
||||
this.$message.info('仅待审查选题可优化');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/system/optimize/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_ids: [topic.id] })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success(`优化完成: ${topic.title}`);
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('优化失败:', error);
|
||||
this.$message.error(`优化失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async handlePublish(topic) {
|
||||
if (!this.isStatus(topic, 'ready')) {
|
||||
this.$message.info('仅待发布选题可发布');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/publishing/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_ids: [topic.id] })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success(`发布成功: ${topic.title}`);
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('发布失败:', error);
|
||||
this.$message.error(`发布失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async deleteTopic(id) {
|
||||
this.$confirm('确定删除?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
|
||||
.then(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
const response = await fetch(`/api/topics/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
this.$message.success('删除成功');
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
this.$message.error(`删除失败: ${error.message}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
handleLogout() { localStorage.removeItem('authToken'); window.location.href = '/'; },
|
||||
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
|
||||
getStatusLabel(status) {
|
||||
const statusMap = {
|
||||
'pending': '待处理',
|
||||
'review': '待审查',
|
||||
'ready': '待发布',
|
||||
'published': '已发布'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
},
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
try {
|
||||
return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
} catch (e) { return dateStr; }
|
||||
},
|
||||
getStatusType(status) {
|
||||
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() {
|
||||
console.log('[DEBUG] TopicsApp mounted');
|
||||
const token = localStorage.getItem('authToken');
|
||||
console.log('[DEBUG] Token exists:', !!token);
|
||||
if (!token) { window.location.href = '/'; return; }
|
||||
// 解析 URL filter 参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const filter = urlParams.get('filter');
|
||||
console.log('[DEBUG] URL filter:', filter);
|
||||
if (filter) { this.filterStatus = filter; }
|
||||
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
|
||||
.then(response => response.ok ? response.json() : Promise.reject())
|
||||
.then(data => {
|
||||
console.log('[DEBUG] Auth success, user:', data.user);
|
||||
this.currentUser = data.user;
|
||||
this.isAdmin = data.user.role === 'admin';
|
||||
this.isLoggedIn = true;
|
||||
this.fetchTopics();
|
||||
})
|
||||
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/'; });
|
||||
}
|
||||
};
|
||||
const app = Vue.createApp(TopicsApp);
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,654 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>宇之然内容创作平台 - 选题管理</title>
|
||||
<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); }
|
||||
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
|
||||
.navbar-title { font-size: 20px; font-weight: 600; }
|
||||
.navbar-user { display: flex; align-items: center; gap: 16px; }
|
||||
.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%; }
|
||||
|
||||
/* 移动端卡片布局 */
|
||||
@media (max-width: 768px) {
|
||||
.el-table .el-button { padding: 4px 8px; font-size: 11px; min-height: auto; }
|
||||
.el-table .cell { padding: 0 4px; }
|
||||
.el-table .el-table__cell { padding: 6px 0; }
|
||||
.topic-card-list { display: block; margin: 0 -16px; }
|
||||
.topic-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
.topic-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.topic-card-title { font-size: 16px; font-weight: 600; color: #303133; flex: 1; margin-right: 8px; }
|
||||
.topic-card-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.topic-card-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.topic-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
.topic-card-actions .el-button { flex: 1; min-width: 60px; }
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
<script src="navigation-component.js"></script></head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<nav class="navbar">
|
||||
<div class="navbar-content">
|
||||
<h1 class="navbar-title">宇之然内容创作平台 - 选题管理</h1>
|
||||
<div class="navbar-user">
|
||||
<div class="user-info"><div class="avatar">{{ currentUser.username ? currentUser.username.charAt(0).toUpperCase() : '?' }}</div><span>{{ currentUser.username }}</span></div>
|
||||
<el-button type="danger" size="small" @click="handleLogout">退出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<navigation-component
|
||||
current-page="topics"
|
||||
:is-admin="isAdmin"
|
||||
@navigate="redirectToPage"
|
||||
></navigation-component>
|
||||
<div class="main-content">
|
||||
|
||||
<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: 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>
|
||||
<el-button type="warning" size="small" @click="triggerOptimizeSelected" :disabled="selectedTopicIds.length === 0">🔍 批量优化</el-button>
|
||||
<span class="ml-auto text-sm text-gray-500" v-if="selectedTopicIds.length > 0" style="color: #909399; font-size: 14px; margin-left: auto;">已选 {{ selectedTopicIds.length }} 项</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap;">
|
||||
<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: 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>
|
||||
<el-table-column prop="title" label="标题" min-width="200"></el-table-column>
|
||||
<el-table-column prop="field" label="领域" width="100"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90">
|
||||
<template #default="scope"><span class="status-badge"><span class="status-dot" :class="scope.row.status"></span>{{ getStatusLabel(scope.row.status) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="compliance_score" label="合规分" width="90">
|
||||
<template #default="scope"><el-progress :percentage="scope.row.compliance_score || 0" :format="() => scope.row.compliance_score || '-'" :stroke-width="15"></el-progress></template>
|
||||
</el-table-column>
|
||||
<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="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="isStatus(scope.row, 'published')" @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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 移动端卡片列表 -->
|
||||
<div class="topic-card-list" v-if="filteredTopics && filteredTopics.length > 0">
|
||||
<div v-for="(topic, index) in filteredTopics" :key="topic.id" class="topic-card">
|
||||
<div class="topic-card-header">
|
||||
<div class="topic-card-title">{{ topic.id }}. {{ topic.title }}</div>
|
||||
<el-tag :type="getStatusType(topic.status)" size="small">{{ getStatusLabel(topic.status) }}</el-tag>
|
||||
</div>
|
||||
<div class="topic-card-tags">
|
||||
<el-tag size="small" type="info">{{ topic.field }}</el-tag>
|
||||
<el-tag size="small" type="warning">合规{{ topic.compliance_score }}</el-tag>
|
||||
</div>
|
||||
<div class="topic-card-meta">
|
||||
<div>创建: {{ formatDate(topic.created_at) }}</div>
|
||||
<div>创作: {{ topic.generated_at ? formatDate(topic.generated_at) : '-' }}</div>
|
||||
<div>发布: {{ topic.published_at ? formatDate(topic.published_at) : '-' }}</div>
|
||||
</div>
|
||||
<div class="topic-card-actions">
|
||||
<el-button size="small" @click="openPreview(topic)" type="primary">预览</el-button>
|
||||
<el-button size="small" type="success" :disabled="isStatus(topic, 'published')" @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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 预览弹窗 -->
|
||||
<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;">
|
||||
<el-button-group>
|
||||
<el-button :type="previewPlatform === 'zhihu' ? 'primary' : 'default'" @click="previewPlatform = 'zhihu'">知乎</el-button>
|
||||
<el-button :type="previewPlatform === 'wechat' ? 'primary' : 'default'" @click="previewPlatform = 'wechat'">微信公众号</el-button>
|
||||
<el-button :type="previewPlatform === 'xiaohongshu' ? 'primary' : 'default'" @click="previewPlatform = 'xiaohongshu'">小红书</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 预览内容使用 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>
|
||||
<template #footer>
|
||||
<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>
|
||||
</div>
|
||||
<script src="vue.global.prod.js"></script>
|
||||
<script src="element-plus.full.js"></script>
|
||||
|
||||
<script>
|
||||
const TopicsApp = {
|
||||
data() {
|
||||
return {
|
||||
currentPage: 'topics',
|
||||
isLoggedIn: false,
|
||||
isAdmin: false,
|
||||
currentUser: { username: '' },
|
||||
loadingTable: false,
|
||||
selectedTopicIds: [],
|
||||
filterStatus: '',
|
||||
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
|
||||
topics: [],
|
||||
previewVisible: false,
|
||||
previewTopic: null,
|
||||
previewFullscreen: false,
|
||||
previewPlatform: 'zhihu',
|
||||
platformContents: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredTopics() {
|
||||
if (!this.topics || !this.topics.length) { return []; }
|
||||
if (!this.filterStatus) { return this.topics; }
|
||||
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));
|
||||
},
|
||||
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
|
||||
};
|
||||
},
|
||||
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: {
|
||||
async fetchTopics() {
|
||||
this.loadingTable = true;
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
setTimeout(() => window.location.href = '/', 1500);
|
||||
this.loadingTable = false;
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/topics', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.topics = data || [];
|
||||
this.$message.success('选题加载成功');
|
||||
} catch (error) {
|
||||
console.error('获取选题失败:', error);
|
||||
this.$message.error(`获取选题失败: ${error.message}`);
|
||||
this.topics = [];
|
||||
} finally {
|
||||
this.loadingTable = false;
|
||||
}
|
||||
},
|
||||
refreshAll() { this.$message.info('执行批量刷新'); },
|
||||
async triggerGenerateSelected() {
|
||||
if (!this.selectedTopicIds.length) return;
|
||||
this.$message.success('批量创作已启动');
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
// 取第一个选题(当前简单实现)
|
||||
const topicId = this.selectedTopicIds[0];
|
||||
const response = await fetch('/api/system/generate/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_id: topicId })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
await response.json();
|
||||
this.$message.success('批量创作已启动');
|
||||
this.selectedTopicIds = [];
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('批量创作失败:', error);
|
||||
this.$message.error(`批量创作失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async triggerOptimizeSelected() {
|
||||
if (!this.selectedTopicIds.length) return;
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/system/optimize/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_ids: this.selectedTopicIds })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success('批量优化完成');
|
||||
this.selectedTopicIds = [];
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('批量优化失败:', error);
|
||||
this.$message.error(`批量优化失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async openPreview(topic) {
|
||||
this.previewTopic = topic;
|
||||
this.previewPlatform = 'zhihu';
|
||||
this.previewVisible = true;
|
||||
this.platformContents = {};
|
||||
// 并行加载所有平台内容
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.warning('请先登录');
|
||||
return;
|
||||
}
|
||||
const platforms = ['zhihu', 'wechat', 'xiaohongshu'];
|
||||
const promises = platforms.map(p =>
|
||||
fetch(`/api/articles/${topic.id}/preview?platform=${p}`, {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
})
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => {
|
||||
if (d && d.html) {
|
||||
this.platformContents[p] = d.html;
|
||||
}
|
||||
})
|
||||
.catch(e => console.error(`加载${p}预览失败:`, e))
|
||||
);
|
||||
await Promise.all(promises);
|
||||
},
|
||||
togglePreviewFullscreen() {
|
||||
this.previewFullscreen = !this.previewFullscreen;
|
||||
},
|
||||
platformName(platform) {
|
||||
const names = {
|
||||
zhihu: '知乎',
|
||||
wechat: '微信公众号',
|
||||
xiaohongshu: '小红书'
|
||||
};
|
||||
return names[platform] || platform;
|
||||
},
|
||||
// 计算属性:当前平台预览的完整 HTML(响应式更新)
|
||||
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) {
|
||||
console.log('createTopic clicked, topic:', topic);
|
||||
if (this.isStatus(topic, 'published')) {
|
||||
this.$message.info('已发布选题不可创作');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/system/generate/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_id: topic.id })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success(`创作完成: ${topic.title}`);
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('创作失败:', error);
|
||||
this.$message.error(`创作失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async optimizeTopic(topic) {
|
||||
if (!this.isStatus(topic, 'review')) {
|
||||
this.$message.info('仅待审查选题可优化');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/system/optimize/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_ids: [topic.id] })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success(`优化完成: ${topic.title}`);
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('优化失败:', error);
|
||||
this.$message.error(`优化失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async handlePublish(topic) {
|
||||
if (!this.isStatus(topic, 'ready')) {
|
||||
this.$message.info('仅待发布选题可发布');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/publishing/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ topic_ids: [topic.id] })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.$message.success(`发布成功: ${topic.title}`);
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('发布失败:', error);
|
||||
this.$message.error(`发布失败: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async deleteTopic(id) {
|
||||
this.$confirm('确定删除?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
|
||||
.then(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
this.$message.error('请先登录');
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
const response = await fetch(`/api/topics/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
this.$message.success('删除成功');
|
||||
await this.fetchTopics();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
this.$message.error(`删除失败: ${error.message}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
handleLogout() { localStorage.removeItem('authToken'); window.location.href = '/'; },
|
||||
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
|
||||
getStatusLabel(status) {
|
||||
const statusMap = {
|
||||
'pending': '待处理',
|
||||
'review': '待审查',
|
||||
'ready': '待发布',
|
||||
'published': '已发布'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
},
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
try {
|
||||
return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
} catch (e) { return dateStr; }
|
||||
},
|
||||
getStatusType(status) {
|
||||
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() {
|
||||
console.log('[DEBUG] TopicsApp mounted');
|
||||
const token = localStorage.getItem('authToken');
|
||||
console.log('[DEBUG] Token exists:', !!token);
|
||||
if (!token) { window.location.href = '/'; return; }
|
||||
// 解析 URL filter 参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const filter = urlParams.get('filter');
|
||||
console.log('[DEBUG] URL filter:', filter);
|
||||
if (filter) { this.filterStatus = filter; }
|
||||
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
|
||||
.then(response => response.ok ? response.json() : Promise.reject())
|
||||
.then(data => {
|
||||
console.log('[DEBUG] Auth success, user:', data.user);
|
||||
this.currentUser = data.user;
|
||||
this.isAdmin = data.user.role === 'admin';
|
||||
this.isLoggedIn = true;
|
||||
this.fetchTopics();
|
||||
})
|
||||
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/'; });
|
||||
}
|
||||
};
|
||||
const app = Vue.createApp(TopicsApp);
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user