206 lines
9.0 KiB
HTML
206 lines
9.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>宇之然内容创作平台</title>
|
|
|
|
|
|
<style>
|
|
body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
|
.sidebar { width: 160px; position: fixed; height: 100vh; left: 0; top: 0; background: #f5f5f5; border-right: 1px solid #e0e0e0; }
|
|
.main-content { margin-left: 160px; width: calc(100vw - 160px); min-height: 100vh; overflow-x: auto; }
|
|
@media (max-width: 768px) { .sidebar { display: none; } .main-content { margin-left: 0; width: 100vw; } }
|
|
</style>
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- Vue 3 -->
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
|
|
<!-- Element Plus CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.3/dist/index.css">
|
|
|
|
<!-- Element Plus JS -->
|
|
<script src="https://unpkg.com/element-plus@2.4.3/dist/index.full.min.js"></script>
|
|
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<nav class="bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg">
|
|
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
|
|
<h1 class="text-2xl font-bold nav-title">宇之然内容创作平台</h1>
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm">{{ currentUser?.username || '管理员' }}</span>
|
|
<el-button type="danger" size="small" @click="handleLogout">退出</el-button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="page flex gap-6">
|
|
<aside class="w-40 flex-shrink-0 hidden md:block sidebar">
|
|
<button @click="currentPage = 'overview'" :class="['px-4 py-2 rounded-lg', currentPage === 'overview' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📊 系统概览</button>
|
|
<button @click="currentPage = 'topics'" :class="['px-4 py-2 rounded-lg', currentPage === 'topics' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📋 选题管理</button>
|
|
<button @click="currentPage = 'logs'" :class="['px-4 py-2 rounded-lg', currentPage === 'logs' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📄 系统日志</button>
|
|
<button v-if="isAdmin" @click="currentPage = 'users'" :class="['px-4 py-2 rounded-lg', currentPage === 'users' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">👥 用户管理</button>
|
|
</aside>
|
|
|
|
<main class="flex-1 main-content">
|
|
<!-- 页面内容将通过条件渲染切换 -->
|
|
<div v-if="!isLoggedIn" class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100">
|
|
<div class="bg-white p-8 rounded-xl shadow-lg w-full max-w-md">
|
|
<h2 class="text-2xl font-bold text-center mb-6 text-blue-600">宇之然内容创作平台</h2>
|
|
<div v-if="loginError" class="mb-4 p-3 bg-red-50 text-red-600 rounded text-sm">{{ loginError }}</div>
|
|
<el-form @submit.prevent="handleLogin">
|
|
<el-form-item label="用户名"><el-input v-model="loginForm.username" placeholder="请输入用户名" prefix-icon="User"></el-input></el-form-item>
|
|
<el-form-item label="密码"><el-input v-model="loginForm.password" type="password" placeholder="请输入密码" prefix-icon="Lock" @keyup.enter="handleLogin"></el-input></el-form-item>
|
|
<el-button type="primary" class="w-full" @click="handleLogin" :loading="loadingLogin">登 录</el-button>
|
|
</el-form>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="isLoggedIn && currentPage === 'overview'">
|
|
<h2 class="text-2xl font-bold mb-6 text-gray-800">📊 系统概览</h2>
|
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
|
|
<div class="stat-card card" @click="goToTopicsWithFilter('total')"><div class="stat-value">{{ status.total_topics || 0 }}</div><div class="stat-label">选题总数</div></div>
|
|
<div class="stat-card card" @click="goToTopicsWithFilter('待处理')"><div class="stat-value">{{ countByStatus('待处理') }}</div><div class="stat-label">待处理</div></div>
|
|
<div class="stat-card card" @click="goToTopicsWithFilter('待审查')"><div class="stat-value">{{ countByStatus('待审查') }}</div><div class="stat-label">待审查</div></div>
|
|
<div class="stat-card card" @click="goToTopicsWithFilter('待发布')"><div class="stat-value">{{ countByStatus('待发布') }}</div><div class="stat-label">待发布</div></div>
|
|
<div class="stat-card card" @click="goToTopicsWithFilter('已发布')"><div class="stat-value">{{ countByStatus('已发布') }}</div><div class="stat-label">已发布</div></div>
|
|
<div class="stat-card card" @click="goToTopicsWithFilter('today')"><div class="stat-value">{{ status.today_articles || 0 }}</div><div class="stat-label">今日新选题</div></div>
|
|
</div>
|
|
<div class="card">
|
|
<h3 class="text-lg font-bold mb-4">🔄 流水线状态</h3>
|
|
<div v-if="pipelineLoading" class="text-center py-4">加载中...</div>
|
|
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div v-for="mod in pipelineModules" :key="mod.module" class="p-4 bg-gray-50 rounded-lg">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<el-tag :type="mod.status_ok ? 'success' : 'danger'" size="small">{{ mod.status_text }}</el-tag>
|
|
<span class="font-semibold">{{ mod.module }}</span>
|
|
</div>
|
|
<div class="text-sm text-gray-500">最后运行:{{ mod.last_run }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="isLoggedIn && currentPage === 'topics'">
|
|
<!-- 选题管理页面内容将在这里 -->
|
|
</div>
|
|
|
|
<div v-if="isLoggedIn && currentPage === 'logs'">
|
|
<!-- 系统日志页面内容将在这里 -->
|
|
</div>
|
|
|
|
<div v-if="isLoggedIn && currentPage === 'users' && isAdmin">
|
|
<!-- 用户管理页面内容将在这里 -->
|
|
</div>
|
|
|
|
<div v-if="loadingOverlay" class="loading-overlay">
|
|
<el-spinner type="spinning" :size="50"></el-spinner>
|
|
<p class="ml-4 text-lg">{{ loadingText }}</p>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/static/vue.global.prod.js?v=20260427"></script>
|
|
<script>
|
|
const App = {
|
|
data() {
|
|
return {
|
|
isLoggedIn: true,
|
|
currentUser: { username: 'admin' },
|
|
isAdmin: true,
|
|
currentPage: 'overview',
|
|
loginForm: { username: '', password: '' },
|
|
loginError: '',
|
|
loadingLogin: false,
|
|
|
|
// 其他数据...
|
|
status: {},
|
|
topics: [],
|
|
filterStatus: '',
|
|
selectedTopicIds: [],
|
|
generating: false,
|
|
optimizing: false,
|
|
loadingAll: false,
|
|
loadingTable: false,
|
|
loadingLogs: false,
|
|
loadingUsers: false,
|
|
loadingOverlay: false,
|
|
loadingText: '',
|
|
pipeline: {},
|
|
pipelineLoading: false,
|
|
pipelineModules: [],
|
|
previewVisible: false,
|
|
previewTopic: null,
|
|
previewPlatform: '',
|
|
previewHtml: '',
|
|
fullScreenPreview: false,
|
|
showLogs: false,
|
|
logType: 'creator',
|
|
logDate: '',
|
|
logContent: '',
|
|
users: [],
|
|
createTopicModalVisible: false,
|
|
newUserForm: {},
|
|
publishModalVisible: false,
|
|
publishForm: {},
|
|
newTopicForm: {}
|
|
}
|
|
},
|
|
computed: {
|
|
filteredTopics() {
|
|
if (!this.topics.length) return []
|
|
if (!this.filterStatus) return this.topics
|
|
return this.topics.filter(t => t.status === this.filterStatus)
|
|
},
|
|
countByStatus() {
|
|
return (status) => this.topics.filter(t => t.status === status).length
|
|
}
|
|
},
|
|
methods: {
|
|
handleLogout() {
|
|
location.reload()
|
|
},
|
|
goToTopicsWithFilter(status) {
|
|
this.currentPage = 'topics'
|
|
this.filterStatus = status
|
|
},
|
|
|
|
// 批量操作方法...
|
|
refreshAll() {},
|
|
triggerGenerateSelected() {},
|
|
triggerOptimizeSelected() {},
|
|
openPreview(topic) {},
|
|
loadPreview() {},
|
|
copyPreviewHtml() {},
|
|
expandPreview() {},
|
|
fetchLogs() {},
|
|
createTopic() {},
|
|
optimizeTopic() {},
|
|
handlePublish() {},
|
|
confirmPublish() {},
|
|
openCreateTopic() {},
|
|
confirmCreateTopic() {},
|
|
openCreateUserModal() {},
|
|
confirmCreateUser() {},
|
|
deleteUser() {},
|
|
loadUsers() {},
|
|
deleteTopic() {},
|
|
|
|
// 工具方法...
|
|
getPriorityType(score) {},
|
|
getStatusClass(status) {},
|
|
formatDate(date) {},
|
|
refresh() {},
|
|
refreshPipeline() {}
|
|
}
|
|
}
|
|
|
|
Vue.createApp(App).mount('#app')
|
|
</script>
|
|
</body>
|
|
</html> |