Files
yu-zhi-ran/platform/frontend/index.html
T

251 lines
11 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 -->
<!-- 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>
<style>
/* 基础重置 */
body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
/* 卡片组件 */
.card { background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 24px; margin-bottom: 24px; transition: all 0.3s; }
.card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
/* 侧边栏 */
.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; }
/* 统计卡片 */
.stat-card { text-align: center; padding: 20px; cursor: pointer; transition: transform 0.2s; }
.stat-card:hover { transform: translateY(-4px); }
.stat-value { font-size: 2.5rem; font-weight: bold; color: #409EFF; line-height: 1.2; }
.stat-label { color: #909399; font-size: 0.9rem; margin-top: 8px; }
/* 操作按钮组 */
.action-btn-group { display: flex; gap: 8px; flex-wrap: wrap; }
/* 快速筛选 */
.quick-filter { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
/* 状态徽章 */
.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; }
/* 加载覆盖层 */
.loading-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.8); display: flex; align-items: center; justify-content: center; z-index: 9999; }
/* 响应式 */
@media (max-width: 768px) {
.sidebar { display: none; }
.main-content { margin-left: 0; width: 100vw; }
}
</style>
</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>