feat: 内容数据迁移至数据库,合规审查全链路打通

- 文章 HTML 存储从文件系统迁移至 articles 表,删除 releases 目录
- 合规审查从 DB 读取 HTML,审查结果写回 DB,通过后自动推进至待发布
- 新增 todayCount 筛选按钮,与系统概览统计数据一致
- 全屏预览修复:提升 z-index 超过侧边栏,添加退出全屏/关闭按钮
- 统一 '优化' → '审查' 命名,消除前后端术语不一致
- 调度器创作完成后自动触发审查(生成 → 审查 → 待发布)
- 清理旧备份/调试文件、过期大纲和研究笔记
This commit is contained in:
Yuzhiran Dev
2026-05-13 17:33:56 +08:00
parent bc6a302e59
commit 233e23016c
234 changed files with 5670 additions and 10651 deletions
+249 -144
View File
@@ -5,29 +5,31 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 系统管理</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.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; min-height: calc(100vh - 60px); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.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; display: flex; flex-direction: column; align-items: center; gap: 2px; background: none; border: none; font-size: 10px; color: #909399; cursor: pointer; padding: 4px; }
.mobile-nav-btn.active { color: #409eff; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.card-loading { display: flex; justify-content: center; align-items: center; min-height: 200px; color: #909399; font-size: 14px; }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; color: #909399; }
.empty-state .empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.4; }
.empty-state .empty-text { font-size: 14px; margin-bottom: 16px; }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
.filter-bar { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
.toolbar { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.el-table { width: 100%; }
.el-table .el-table__cell { word-break: break-word; }
.stat-summary { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
.stat-item { background: #f0f5ff; border-radius: 8px; padding: 12px 20px; display: flex; flex-direction: column; align-items: center; min-width: 100px; }
.stat-item .num { font-size: 24px; font-weight: 600; color: #409eff; }
.stat-item .label { font-size: 12px; color: #909399; margin-top: 4px; }
.mobile-card-list { display: none; }
@media (max-width: 768px) {
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
.card { padding: 16px; }
.data-table { display: none; }
@@ -46,6 +48,9 @@
.mobile-card-label { color: #909399; flex-shrink: 0; margin-right: 8px; }
.mobile-card-value { color: #303133; text-align: right; word-break: break-word; }
.mobile-card-actions { display: flex; gap: 8px; justify-content: flex-end; padding-top: 10px; margin-top: 6px; border-top: 1px solid #ebeef5; }
.stat-summary { gap: 8px; }
.stat-item { min-width: 70px; padding: 8px 12px; }
.stat-item .num { font-size: 18px; }
}
</style>
<script src="navigation-component.js"></script>
@@ -57,18 +62,39 @@
<navigation-component current-page="admin" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<el-tabs v-model="activeTab">
<el-tab-pane label="案例管理" name="cases">
<div class="toolbar">
<el-button type="primary" @click="showCaseDialog()">新增案例</el-button>
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">⚙️ 系统管理</h2>
<div class="filter-bar">
<el-button size="default" :type="activeTab === 'cases' ? 'primary' : ''" @click="switchTab('cases')">案例管理</el-button>
<el-button size="default" :type="activeTab === 'tasklogs' ? 'primary' : ''" @click="switchTab('tasklogs')">任务日志</el-button>
<el-button size="default" :type="activeTab === 'llmconfigs' ? 'primary' : ''" @click="switchTab('llmconfigs')">LLM配置</el-button>
<el-button size="default" :type="activeTab === 'systemconfigs' ? 'primary' : ''" @click="switchTab('systemconfigs')">系统配置</el-button>
</div>
</div>
<div v-if="activeTab === 'cases'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showCaseDialog()">新增案例</el-button>
</div>
<div v-if="casesLoading" class="card-loading">加载中...</div>
<template v-else-if="cases.length === 0">
<div class="empty-state">
<div class="empty-icon">📋</div>
<div class="empty-text">暂无案例数据</div>
<el-button type="primary" size="small" @click="showCaseDialog()">新增第一个案例</el-button>
</div>
<el-table :data="cases" border stripe class="data-table">
<el-table-column prop="id" label="ID" width="70"/>
<el-table-column prop="title" label="标题" min-width="150"/>
<el-table-column prop="field" label="领域" width="100"/>
<el-table-column prop="summary" label="概述" min-width="200" :show-overflow-tooltip="true"/>
<el-table-column prop="source" label="来源" width="100"/>
</template>
<template v-else>
<div class="stat-summary">
<div class="stat-item"><span class="num">{{ cases.length }}</span><span class="label">总案例</span></div>
</div>
<el-table :data="cases" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="title" label="标题" min-width="120" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="field" label="领域" width="100"></el-table-column>
<el-table-column prop="summary" label="概述" min-width="200" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="source" label="来源" width="100"></el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="scope">
<el-button size="small" @click="showCaseDialog(scope.row)">编辑</el-button>
@@ -76,52 +102,73 @@
</template>
</el-table-column>
</el-table>
<div class="mobile-card-list">
<div v-for="item in cases" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">标题</span><span class="mobile-card-value">{{ item.title }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">领域</span><span class="mobile-card-value">{{ item.field }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">来源</span><span class="mobile-card-value">{{ item.source }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showCaseDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteCase(item.id)">删除</el-button>
</div>
</template>
<div class="mobile-card-list">
<div v-for="item in cases" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">标题</span><span class="mobile-card-value">{{ item.title }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">领域</span><span class="mobile-card-value">{{ item.field }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">来源</span><span class="mobile-card-value">{{ item.source }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showCaseDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteCase(item.id)">删除</el-button>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="任务日志" name="tasklogs">
<div class="toolbar">
<el-button @click="loadTaskLogs()">刷新</el-button>
</div>
</div>
<div v-if="activeTab === 'tasklogs'">
<div class="toolbar">
<el-button size="small" @click="loadTaskLogs()">刷新</el-button>
</div>
<div v-if="taskLogsLoading" class="card-loading">加载中...</div>
<template v-else-if="taskLogs.length === 0">
<div class="empty-state">
<div class="empty-icon">📝</div>
<div class="empty-text">暂无任务日志</div>
</div>
<el-table :data="taskLogs" border stripe class="data-table">
<el-table-column prop="id" label="ID" width="70"/>
<el-table-column prop="task_name" label="任务名称" min-width="120"/>
<el-table-column prop="topic_id" label="选题" width="100"/>
<el-table-column prop="status" label="状态" width="80"/>
<el-table-column prop="message" label="消息" min-width="150" :show-overflow-tooltip="true"/>
<el-table-column prop="started_at" label="开始" width="150"/>
<el-table-column prop="finished_at" label="结束" width="150"/>
<el-table-column prop="duration" label="耗时" width="80"/>
</template>
<template v-else>
<el-table :data="taskLogs" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="task_name" label="任务名称" min-width="120"></el-table-column>
<el-table-column prop="topic_id" label="选题" width="100"></el-table-column>
<el-table-column prop="status" label="状态" width="80"></el-table-column>
<el-table-column prop="message" label="消息" min-width="150" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="started_at" label="开始" width="150"></el-table-column>
<el-table-column prop="finished_at" label="结束" width="150"></el-table-column>
<el-table-column prop="duration" label="耗时" width="80"></el-table-column>
</el-table>
<div class="mobile-card-list">
<div v-for="item in taskLogs" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">任务</span><span class="mobile-card-value">{{ item.task_name }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">选题</span><span class="mobile-card-value">{{ item.topic_id }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">状态</span><span class="mobile-card-value">{{ item.status }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">消息</span><span class="mobile-card-value">{{ item.message }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">耗时</span><span class="mobile-card-value">{{ item.duration }}s</span></div>
</div>
</template>
<div class="mobile-card-list">
<div v-for="item in taskLogs" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">任务</span><span class="mobile-card-value">{{ item.task_name }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">选题</span><span class="mobile-card-value">{{ item.topic_id }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">状态</span><span class="mobile-card-value">{{ item.status }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">消息</span><span class="mobile-card-value">{{ item.message }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">耗时</span><span class="mobile-card-value">{{ item.duration }}s</span></div>
</div>
</el-tab-pane>
<el-tab-pane label="LLM配置" name="llmconfigs">
<div class="toolbar">
<el-button type="primary" @click="showLLMConfigDialog()">新增配置</el-button>
</div>
</div>
<div v-if="activeTab === 'llmconfigs'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showLLMConfigDialog()">新增配置</el-button>
</div>
<div v-if="llmConfigsLoading" class="card-loading">加载中...</div>
<template v-else-if="llmConfigs.length === 0">
<div class="empty-state">
<div class="empty-icon">🤖</div>
<div class="empty-text">暂无 LLM 配置</div>
<el-button type="primary" size="small" @click="showLLMConfigDialog()">新增配置</el-button>
</div>
<el-table :data="llmConfigs" border stripe class="data-table">
<el-table-column prop="id" label="ID" width="70"/>
<el-table-column prop="name" label="名称" min-width="120"/>
<el-table-column prop="model" label="模型" min-width="180"/>
<el-table-column prop="temperature" label="温度" width="80"/>
<el-table-column prop="max_tokens" label="最大Token" width="110"/>
</template>
<template v-else>
<el-table :data="llmConfigs" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="name" label="名称" min-width="120"></el-table-column>
<el-table-column prop="model" label="模型" min-width="180"></el-table-column>
<el-table-column prop="temperature" label="温度" width="80"></el-table-column>
<el-table-column prop="max_tokens" label="最大Token" width="110"></el-table-column>
<el-table-column prop="is_active" label="激活" width="70">
<template #default="scope">{{ scope.row.is_active ? '是' : '否' }}</template>
</el-table-column>
@@ -132,31 +179,42 @@
</template>
</el-table-column>
</el-table>
<div class="mobile-card-list">
<div v-for="item in llmConfigs" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">名称</span><span class="mobile-card-value">{{ item.name }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">模型</span><span class="mobile-card-value">{{ item.model }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">温度</span><span class="mobile-card-value">{{ item.temperature }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">激活</span><span class="mobile-card-value">{{ item.is_active ? '是' : '否' }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showLLMConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteLLMConfig(item.id)">删除</el-button>
</div>
</template>
<div class="mobile-card-list">
<div v-for="item in llmConfigs" :key="item.id" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label">名称</span><span class="mobile-card-value">{{ item.name }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">模型</span><span class="mobile-card-value">{{ item.model }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">温度</span><span class="mobile-card-value">{{ item.temperature }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">激活</span><span class="mobile-card-value">{{ item.is_active ? '是' : '否' }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showLLMConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteLLMConfig(item.id)">删除</el-button>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="系统配置" name="systemconfigs">
<div class="toolbar">
<el-button type="primary" @click="showSystemConfigDialog()">新增配置</el-button>
</div>
</div>
<div v-if="activeTab === 'systemconfigs'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showSystemConfigDialog()">新增配置</el-button>
</div>
<div v-if="systemConfigsLoading" class="card-loading">加载中...</div>
<template v-else-if="systemConfigs.length === 0">
<div class="empty-state">
<div class="empty-icon">⚙️</div>
<div class="empty-text">暂无系统配置</div>
<el-button type="primary" size="small" @click="showSystemConfigDialog()">新增配置</el-button>
</div>
<el-table :data="systemConfigs" border stripe class="data-table">
<el-table-column prop="key" label="键" min-width="180"/>
</template>
<template v-else>
<el-table :data="systemConfigs" border stripe class="data-table" style="width:100%">
<el-table-column prop="key" label="键" min-width="180"></el-table-column>
<el-table-column prop="value" label="值" min-width="250">
<template #default="scope">
{{ typeof scope.row.value === 'object' ? JSON.stringify(scope.row.value) : scope.row.value }}
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="200"/>
<el-table-column prop="description" label="描述" min-width="200"></el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="scope">
<el-button size="small" @click="showSystemConfigDialog(scope.row)">编辑</el-button>
@@ -164,34 +222,44 @@
</template>
</el-table-column>
</el-table>
<div class="mobile-card-list">
<div v-for="item in systemConfigs" :key="item.key" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label"></span><span class="mobile-card-value">{{ item.key }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label"></span><span class="mobile-card-value">{{ typeof item.value === 'object' ? JSON.stringify(item.value) : item.value }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">描述</span><span class="mobile-card-value">{{ item.description }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showSystemConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteSystemConfig(item.key)">删除</el-button>
</div>
</template>
<div class="mobile-card-list">
<div v-for="item in systemConfigs" :key="item.key" class="mobile-card">
<div class="mobile-card-row"><span class="mobile-card-label"></span><span class="mobile-card-value">{{ item.key }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label"></span><span class="mobile-card-value">{{ typeof item.value === 'object' ? JSON.stringify(item.value) : item.value }}</span></div>
<div class="mobile-card-row"><span class="mobile-card-label">描述</span><span class="mobile-card-value">{{ item.description }}</span></div>
<div class="mobile-card-actions">
<el-button size="small" @click="showSystemConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteSystemConfig(item.key)">删除</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</main>
</div>
<el-dialog v-model="caseDialogVisible" :title="caseDialogTitle" width="600px">
<el-dialog v-model="caseDialogVisible" :title="caseDialogTitle" width="700px" :close-on-click-modal="false">
<el-form :model="caseForm" label-width="80px">
<el-form-item label="标题"><el-input v-model="caseForm.title"/></el-form-item>
<el-form-item label="领域"><el-input v-model="caseForm.field"/></el-form-item>
<el-form-item label="概述"><el-input type="textarea" v-model="caseForm.summary"/></el-form-item>
<el-form-item label="关键指标"><el-input v-model="caseForm.key_metrics"/></el-form-item>
<el-form-item label="日期"><el-input v-model="caseForm.date"/></el-form-item>
<el-form-item label="来源"><el-input v-model="caseForm.source"/></el-form-item>
<el-form-item label="来源URL"><el-input v-model="caseForm.source_url"/></el-form-item>
<el-form-item label="可信度"><el-input v-model="caseForm.credibility_rating"/></el-form-item>
<el-form-item label="国内适用性"><el-input v-model="caseForm.china_applicability"/></el-form-item>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="标题"><el-input v-model="caseForm.title"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="领域"><el-input v-model="caseForm.field"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="概述"><el-input type="textarea" v-model="caseForm.summary" :rows="3"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="关键指标"><el-input v-model="caseForm.key_metrics"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="日期"><el-input v-model="caseForm.date"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="来源"><el-input v-model="caseForm.source"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="来源URL"><el-input v-model="caseForm.source_url"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="可信度"><el-input v-model="caseForm.credibility_rating"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="国内适用性"><el-input v-model="caseForm.china_applicability"/></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="caseDialogVisible=false">取消</el-button>
@@ -199,17 +267,25 @@
</template>
</el-dialog>
<el-dialog v-model="llmConfigDialogVisible" :title="llmConfigDialogTitle" width="600px">
<el-form :model="llmConfigForm" label-width="120px">
<el-form-item label="名称"><el-input v-model="llmConfigForm.name"/></el-form-item>
<el-form-item label="系统提示词"><el-input type="textarea" v-model="llmConfigForm.system_prompt"/></el-form-item>
<el-form-item label="用户提示模板"><el-input type="textarea" v-model="llmConfigForm.user_prompt_template"/></el-form-item>
<el-form-item label="温度"><el-input-number v-model="llmConfigForm.temperature" :min="0" :max="2" :step="0.1"/></el-form-item>
<el-form-item label="最大Token"><el-input-number v-model="llmConfigForm.max_tokens" :min="1" :max="10000"/></el-form-item>
<el-form-item label="模型"><el-input v-model="llmConfigForm.model"/></el-form-item>
<el-form-item label="激活">
<el-switch v-model="llmConfigForm.is_active"/>
</el-form-item>
<el-dialog v-model="llmConfigDialogVisible" :title="llmConfigDialogTitle" width="700px" :close-on-click-modal="false">
<el-form :model="llmConfigForm" label-width="100px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="名称"><el-input v-model="llmConfigForm.name"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="模型"><el-input v-model="llmConfigForm.model"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="温度"><el-input-number v-model="llmConfigForm.temperature" :min="0" :max="2" :step="0.1"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="最大Token"><el-input-number v-model="llmConfigForm.max_tokens" :min="1" :max="10000"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="系统提示词"><el-input type="textarea" v-model="llmConfigForm.system_prompt" :rows="4"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="提示模板"><el-input type="textarea" v-model="llmConfigForm.user_prompt_template" :rows="4"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="激活"><el-switch v-model="llmConfigForm.is_active"/></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="llmConfigDialogVisible=false">取消</el-button>
@@ -217,11 +293,15 @@
</template>
</el-dialog>
<el-dialog v-model="systemConfigDialogVisible" :title="systemConfigDialogTitle" width="500px">
<el-form :model="systemConfigForm" label-width="100px">
<el-form-item label="键"><el-input v-model="systemConfigForm.key" :disabled="!!editingSystemConfigKey"/></el-form-item>
<el-form-item label=""><el-input v-model="systemConfigForm.value" type="textarea"/></el-form-item>
<el-form-item label="描述"><el-input v-model="systemConfigForm.description"/></el-form-item>
<el-dialog v-model="systemConfigDialogVisible" :title="systemConfigDialogTitle" width="600px" :close-on-click-modal="false">
<el-form :model="systemConfigForm" label-width="80px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label=""><el-input v-model="systemConfigForm.key" :disabled="!!editingSystemConfigKey"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="描述"><el-input v-model="systemConfigForm.description"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="值"><el-input v-model="systemConfigForm.value" type="textarea" :rows="4"/></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="systemConfigDialogVisible=false">取消</el-button>
@@ -232,7 +312,7 @@
<script src="vue.global.prod.js"></script>
<script src="element-plus.full.js"></script>
<script>
const { createApp, ref, reactive, onMounted, watch } = Vue;
const { createApp, ref, reactive, onMounted } = Vue;
const { ElMessage, ElMessageBox } = ElementPlus;
const app = createApp({
@@ -240,38 +320,42 @@
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = 'login.html'; return {}; }
const apiBase = '';
const api = {
get: (url) => fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
post: (url, body) => fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
put: (url, body) => fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
delete: (url) => fetch(url, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
get: (url) => fetch(apiBase + url, { headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
post: (url, body) => fetch(apiBase + url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
put: (url, body) => fetch(apiBase + url, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
delete: (url) => fetch(apiBase + url, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
};
const activeTab = ref('cases');
const currentUser = ref({ username: '' });
const isAdmin = ref(false);
const isLoggedIn = ref(false);
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(response => response.ok ? response.json() : Promise.reject())
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => {
const user = data.user || { username: '', role: 'user' };
currentUser.value = user;
isAdmin.value = user.role === 'admin';
isLoggedIn.value = true;
if (!isAdmin.value) { ElMessage.warning('需要管理员权限'); window.location.href = '/login.html'; }
})
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/login.html'; });
const redirectToPage = (page) => { window.location.href = '/' + page; };
const cases = ref([]);
const casesLoading = ref(false);
const caseDialogVisible = ref(false);
const caseDialogTitle = ref('新增案例');
const caseForm = reactive({ id: null, title: '', field: '', summary: '', key_metrics: '', date: '', source: '', source_url: '', credibility_rating: '', china_applicability: '' });
const editingCaseId = ref(null);
const loadCases = async () => { try { cases.value = await api.get('/api/admin/cases'); } catch (e) { ElMessage.error('加载案例失败: ' + e.message); } };
const loadCases = async () => {
casesLoading.value = true;
try { cases.value = await api.get('/api/admin/cases'); } catch (e) { ElMessage.error('加载案例失败: ' + e.message); }
finally { casesLoading.value = false; }
};
const showCaseDialog = (row = null) => {
if (row) { caseDialogTitle.value = '编辑案例'; editingCaseId.value = row.id; Object.assign(caseForm, row); }
else { caseDialogTitle.value = '新增案例'; editingCaseId.value = null; Object.keys(caseForm).forEach(k => { if (k === 'id') caseForm.id = null; else caseForm[k] = ''; }); }
@@ -290,15 +374,25 @@
};
const taskLogs = ref([]);
const loadTaskLogs = async () => { try { taskLogs.value = await api.get('/api/admin/tasklogs'); } catch (e) { ElMessage.error('加载任务日志失败: ' + e.message); } };
const taskLogsLoading = ref(false);
const loadTaskLogs = async () => {
taskLogsLoading.value = true;
try { taskLogs.value = await api.get('/api/admin/tasklogs'); } catch (e) { ElMessage.error('加载任务日志失败: ' + e.message); }
finally { taskLogsLoading.value = false; }
};
const llmConfigs = ref([]);
const llmConfigsLoading = ref(false);
const llmConfigDialogVisible = ref(false);
const llmConfigDialogTitle = ref('新增配置');
const llmConfigForm = reactive({ id: null, name: '', system_prompt: '', user_prompt_template: '', temperature: 0.7, max_tokens: 2000, model: '', is_active: true });
const editingLLMConfigId = ref(null);
const loadLLMConfigs = async () => { try { llmConfigs.value = await api.get('/api/admin/llmconfigs'); } catch (e) { ElMessage.error('加载LLM配置失败: ' + e.message); } };
const loadLLMConfigs = async () => {
llmConfigsLoading.value = true;
try { llmConfigs.value = await api.get('/api/admin/llmconfigs'); } catch (e) { ElMessage.error('加载LLM配置失败: ' + e.message); }
finally { llmConfigsLoading.value = false; }
};
const showLLMConfigDialog = (row = null) => {
if (row) { llmConfigDialogTitle.value = '编辑配置'; editingLLMConfigId.value = row.id; Object.assign(llmConfigForm, row); }
else {
@@ -320,12 +414,17 @@
};
const systemConfigs = ref([]);
const systemConfigsLoading = ref(false);
const systemConfigDialogVisible = ref(false);
const systemConfigDialogTitle = ref('新增配置');
const systemConfigForm = reactive({ key: '', value: '', description: '' });
const editingSystemConfigKey = ref(null);
const loadSystemConfigs = async () => { try { systemConfigs.value = await api.get('/api/admin/systemconfigs'); } catch (e) { ElMessage.error('加载系统配置失败: ' + e.message); } };
const loadSystemConfigs = async () => {
systemConfigsLoading.value = true;
try { systemConfigs.value = await api.get('/api/admin/systemconfigs'); } catch (e) { ElMessage.error('加载系统配置失败: ' + e.message); }
finally { systemConfigsLoading.value = false; }
};
const showSystemConfigDialog = (row = null) => {
if (row) { systemConfigDialogTitle.value = '编辑配置'; editingSystemConfigKey.value = row.key; systemConfigForm.key = row.key; systemConfigForm.value = (typeof row.value === 'object') ? JSON.stringify(row.value) : (row.value || ''); systemConfigForm.description = row.description || ''; }
else { systemConfigDialogTitle.value = '新增配置'; editingSystemConfigKey.value = null; systemConfigForm.key = ''; systemConfigForm.value = ''; systemConfigForm.description = ''; }
@@ -345,26 +444,32 @@
const logout = () => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; };
watch(activeTab, (t) => {
if (t === 'cases' && !cases.value.length) loadCases();
if (t === 'tasklogs' && !taskLogs.value.length) loadTaskLogs();
if (t === 'llmconfigs' && !llmConfigs.value.length) loadLLMConfigs();
if (t === 'systemconfigs' && !systemConfigs.value.length) loadSystemConfigs();
});
const tabLoaders = {
cases: loadCases, tasklogs: loadTaskLogs,
llmconfigs: loadLLMConfigs, systemconfigs: loadSystemConfigs,
};
const loadedTabs = new Set();
const switchTab = (name) => {
activeTab.value = name;
if (!loadedTabs.has(name)) {
loadedTabs.add(name);
tabLoaders[name]();
}
};
onMounted(() => {
if (activeTab.value === 'cases') loadCases();
else if (activeTab.value === 'tasklogs') loadTaskLogs();
else if (activeTab.value === 'llmconfigs') loadLLMConfigs();
else if (activeTab.value === 'systemconfigs') loadSystemConfigs();
loadedTabs.add('cases');
loadCases();
});
return {
activeTab, cases, caseDialogVisible, caseForm, caseDialogTitle, showCaseDialog, saveCase, deleteCase,
taskLogs, loadTaskLogs,
llmConfigs, llmConfigDialogVisible, llmConfigForm, llmConfigDialogTitle, showLLMConfigDialog, saveLLMConfig, deleteLLMConfig,
systemConfigs, systemConfigDialogVisible, systemConfigForm, systemConfigDialogTitle, showSystemConfigDialog, saveSystemConfig, deleteSystemConfig,
logout, currentUser, isAdmin, isLoggedIn, redirectToPage
activeTab, switchTab,
cases, casesLoading, caseDialogVisible, caseForm, caseDialogTitle, showCaseDialog, saveCase, deleteCase,
taskLogs, taskLogsLoading, loadTaskLogs,
llmConfigs, llmConfigsLoading, llmConfigDialogVisible, llmConfigForm, llmConfigDialogTitle, showLLMConfigDialog, saveLLMConfig, deleteLLMConfig,
systemConfigs, systemConfigsLoading, systemConfigDialogVisible, systemConfigForm, systemConfigDialogTitle, showSystemConfigDialog, saveSystemConfig, deleteSystemConfig,
logout, currentUser, isAdmin, redirectToPage
};
}
});
+3 -12
View File
@@ -5,23 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 素材库</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { 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; }
.sidebar { width: 180px; background: white; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.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; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
}
.asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }
@@ -46,7 +37,7 @@
<div class="main-content">
<main class="content-area">
<h2 style="font-size: 24px; font-weight: 700; margin-bottom: 24px; color: #303133;">🖼️ 素材库</h2>
<div class="card">
<div class="card page-fade">
<div class="filter-bar">
<el-input v-model="searchKeyword" placeholder="搜索素材..." style="width: 200px;" clearable @clear="loadAssets" @keyup.enter="loadAssets">
<template #prefix><span>🔍</span></template>
+98 -15
View File
@@ -5,29 +5,29 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 内容日历</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { 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; }
.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; }
.sidebar { width: 180px; background: white; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.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); }
.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; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 12px; padding-bottom: 80px; }
.calendar-day { min-height: 56px !important; padding: 2px; }
.calendar-weekday { padding: 6px; font-size: 12px; }
.day-entry { font-size: 10px; padding: 2px 4px; white-space: normal; }
.calendar-grid { gap: 2px; }
.day-number { font-size: 12px; margin-bottom: 0; }
.day-lunar { font-size: 9px; margin-top: -1px; }
.day-term { font-size: 8px; padding: 0 3px; }
.day-holiday { font-size: 8px; padding: 0 3px; }
}
.calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.calendar-title { font-size: 28px; font-weight: 700; color: #303133; }
.calendar-nav { display: flex; align-items: center; gap: 16px; }
.calendar-nav { display: flex; align-items: center; gap: 16px; white-space: nowrap; }
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; }
.calendar-weekday { text-align: center; font-weight: 600; color: #606266; padding: 12px; background: #f5f7fa; border-radius: 8px; }
.calendar-day { min-height: 100px; background: #fafafa; border-radius: 8px; padding: 8px; border: 1px solid #ebeef5; cursor: pointer; transition: all 0.2s; }
@@ -35,6 +35,9 @@
.calendar-day.other-month { opacity: 0.4; }
.calendar-day.today { border-color: #409EFF; background: #ecf5ff; }
.day-number { font-weight: 600; font-size: 14px; margin-bottom: 8px; color: #303133; }
.day-lunar { font-size: 10px; color: #909399; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.day-term { display: inline-block; font-size: 9px; color: #E6A23C; background: #fdf6ec; border-radius: 3px; padding: 0 4px; margin-top: 1px; font-weight: 500; white-space: nowrap; }
.day-holiday { display: inline-block; font-size: 9px; color: #F56C6C; background: #fef0f0; border-radius: 3px; padding: 0 4px; margin-top: 1px; font-weight: 500; white-space: nowrap; }
.day-entries { display: flex; flex-direction: column; gap: 4px; }
.day-entry { font-size: 11px; padding: 4px 6px; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
.day-entry.planned { background: #fdf6ec; color: #E6A23C; }
@@ -68,7 +71,7 @@
></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<div class="card page-fade">
<div class="calendar-header">
<h2 class="calendar-title">📅 内容日历</h2>
<div class="calendar-nav">
@@ -93,6 +96,9 @@
:class="{ 'other-month': !day.isCurrentMonth, 'today': day.isToday }"
@click="openDayDialog(day)">
<div class="day-number">{{ day.day }}</div>
<div v-if="day.lunar" class="day-lunar">{{ day.lunar }}</div>
<div v-if="day.solarTerm" class="day-term">{{ day.solarTerm }}</div>
<div v-if="day.holiday" class="day-holiday">{{ day.holiday }}</div>
<div class="day-entries">
<div v-for="entry in day.entries" :key="entry.id"
class="day-entry"
@@ -167,6 +173,80 @@
</el-dialog>
</div>
<script>
// ---- 农历/节气/节日数据 ----
const LUNAR_YEARS = [
[2024, 2,10, 0, 30,29,30,29,30,29,30,29,29,30,30,30],
[2025, 1,29, 6, 30,29,30,29,30,29,30,29,30,29,30,29,30],
[2026, 2,17, 0, 29,30,29,30,29,30,29,30,29,30,29,30],
[2027, 2, 6, 0, 30,29,30,29,30,29,30,29,30,29,30,29],
[2028, 1,26, 5, 30,29,30,29,30,29,30,30,29,30,29,30,29],
];
const LUNAR_MONTH_NAMES = ['', '正月','二月','三月','四月','五月','六月','七月','八月','九月','十月','冬月','腊月'];
const LUNAR_DAY_NAMES = ['', '初一','初二','初三','初四','初五','初六','初七','初八','初九','初十','十一','十二','十三','十四','十五','十六','十七','十八','十九','二十','廿一','廿二','廿三','廿四','廿五','廿六','廿七','廿八','廿九','三十'];
// 公历节日 (月-日)
const SOLAR_HOLIDAYS = {
'1-1': '元旦', '2-14': '情人节', '3-8': '妇女节', '3-12': '植树节',
'4-1': '愚人节', '5-1': '劳动节', '5-4': '青年节',
'6-1': '儿童节', '7-1': '建党节', '8-1': '建军节',
'9-10': '教师节', '10-1': '国庆节', '12-25': '圣诞节',
};
// 节气近似日期 (月-日)
const SOLAR_TERMS = {
'1-5':'小寒','1-20':'大寒','2-4':'立春','2-19':'雨水',
'3-6':'惊蛰','3-21':'春分','4-5':'清明','4-20':'谷雨',
'5-6':'立夏','5-21':'小满','6-6':'芒种','6-21':'夏至',
'7-7':'小暑','7-23':'大暑','8-7':'立秋','8-23':'处暑',
'9-7':'白露','9-23':'秋分','10-8':'寒露','10-23':'霜降',
'11-7':'立冬','11-22':'小雪','12-7':'大雪','12-22':'冬至',
};
function gregorianToLunar(year, month, day) {
const target = new Date(year, month - 1, day).getTime();
for (const ly of LUNAR_YEARS) {
const [lyear, sm, sd, leap, ...monthDays] = ly;
const start = new Date(lyear, sm - 1, sd);
const totalDays = monthDays.reduce((a, b) => a + b, 0);
const end = new Date(start);
end.setDate(end.getDate() + totalDays);
if (target < start.getTime() || target >= end.getTime()) continue;
const diff = Math.floor((target - start.getTime()) / 86400000);
let accum = 0;
for (let i = 0; i < monthDays.length; i++) {
if (diff < accum + monthDays[i]) {
let lunarMonth, isLeap = false;
if (leap > 0 && i === leap) {
isLeap = true;
lunarMonth = leap;
} else if (leap > 0 && i > leap) {
lunarMonth = i;
} else {
lunarMonth = i + 1;
}
const lunarDay = diff - accum + 1;
const mName = (isLeap ? '闰' : '') + (LUNAR_MONTH_NAMES[lunarMonth] || lunarMonth + '月');
const dName = LUNAR_DAY_NAMES[lunarDay] || lunarDay + '日';
let lunarStr = mName + dName;
if (lunarStr.length > 6) lunarStr = (isLeap ? '闰' : '') + lunarMonth + '月' + dName;
return lunarStr;
}
accum += monthDays[i];
}
}
return '';
}
function getDayMeta(year, month, day) {
const key = month + '-' + day;
const solarTerm = SOLAR_TERMS[key] || '';
const holiday = SOLAR_HOLIDAYS[key] || '';
const lunar = gregorianToLunar(year, month, day);
return { solarTerm, holiday, lunar };
}
</script>
<script src="vue.global.prod.js"></script>
<script src="element-plus.full.js"></script>
<script>
@@ -212,7 +292,8 @@
for (let i = startWeek - 1; i >= 0; i--) {
const d = new Date(currentYear.value, currentMonth.value - 1, -i);
days.push({ day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear(), isCurrentMonth: false, isToday: false, entries: [] });
const meta = getDayMeta(d.getFullYear(), d.getMonth() + 1, d.getDate());
days.push({ day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear(), isCurrentMonth: false, isToday: false, entries: [], ...meta });
}
for (let i = 1; i <= totalDays; i++) {
const isToday = today.getFullYear() === currentYear.value && today.getMonth() + 1 === currentMonth.value && today.getDate() === i;
@@ -220,12 +301,14 @@
const pd = new Date(e.planned_date);
return pd.getFullYear() === currentYear.value && pd.getMonth() + 1 === currentMonth.value && pd.getDate() === i;
});
days.push({ day: i, month: currentMonth.value, year: currentYear.value, isCurrentMonth: true, isToday, entries: dayEntries });
const meta = getDayMeta(currentYear.value, currentMonth.value, i);
days.push({ day: i, month: currentMonth.value, year: currentYear.value, isCurrentMonth: true, isToday, entries: dayEntries, ...meta });
}
const remaining = 42 - days.length;
for (let i = 1; i <= remaining; i++) {
const d = new Date(currentYear.value, currentMonth.value, i);
days.push({ day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear(), isCurrentMonth: false, isToday: false, entries: [] });
const meta = getDayMeta(d.getFullYear(), d.getMonth() + 1, d.getDate());
days.push({ day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear(), isCurrentMonth: false, isToday: false, entries: [], ...meta });
}
return days;
});
-7
View File
@@ -1,7 +0,0 @@
(function() {
var d = document.createElement('div');
d.style.cssText = 'position:fixed;top:0;left:0;background:rgba(0,0,0,0.9);color:#fff;padding:8px;font-size:12px;z-index:999999;max-width:90vw;overflow:auto;';
d.innerHTML = 'Vue: ' + typeof Vue + '<br>ElementPlus: ' + typeof ElementPlus + '<br>Time: ' + new Date().toLocaleTimeString();
document.body.appendChild(d);
console.log('Debug panel injected', d.innerHTML);
})();
-194
View File
@@ -1,194 +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>宇之然内容创作平台 - Vue调试</title>
<!-- 资源加载 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.3/dist/index.css">
<script src="https://unpkg.com/element-plus@2.4.3/dist/index.full.min.js"></script>
<style>
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; margin-bottom: 24px; }
aside button { width: 100%; text-align: left; border: none; background: transparent; border-radius: 8px; margin-bottom: 4px; }
@media (max-width: 768px) { main { padding-bottom: 70px; } }
.nav-title { text-align: center; }
.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; }
.debug-panel { background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto; }
</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">宇之然内容创作平台 - Vue调试</h1>
</div>
</nav>
<!-- 侧边栏 -->
<div class="page flex gap-6">
<aside class="w-40 flex-shrink-0 hidden md:block">
<button @click="testType='topics'" :class="['px-4 py-2 rounded-lg', testType === 'topics' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📋 选题管理</button>
<button @click="testType='logs'" :class="['px-4 py-2 rounded-lg', testType === 'logs' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📄 系统日志</button>
<button @click="testType='users'" :class="['px-4 py-2 rounded-lg', testType === 'users' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">👥 用户管理</button>
</aside>
<!-- 主内容区 -->
<main class="flex-1">
<div class="card" style="padding: 20px; margin-top: 20px;">
<h2 class="text-2xl font-bold text-gray-800 mb-6">🔍 Vue调试控制台</h2>
<!-- 调试信息显示 -->
<div class="debug-panel mb-4">
<strong>调试输出:</strong><br/>
<span v-for="log in debugLogs" :key="log">{{ log }}</span>
</div>
<!-- 测试按钮 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<button @click="runDebugTest" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">运行调试测试</button>
<button @click="testElementPlus" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">测试Element Plus</button>
<button @click="resetDebug" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">重置调试</button>
</div>
<!-- 测试结果 -->
<div v-if="testResults.length > 0" class="mb-4 p-4 bg-green-50 border-l-4 border-green-400">
<h3 class="font-bold mb-2">测试结果:</h3>
<ul class="list-disc pl-5">
<li v-for="result in testResults">{{ result }}</li>
</ul>
</div>
<!-- 模拟表格 -->
<div v-if="testType === 'topics'" class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-gray-50">
<th class="border p-2"><input type="checkbox"></th>
<th class="border p-2">ID</th>
<th class="border p-2">标题</th>
<th class="border p-2">状态</th>
</tr>
</thead>
<tbody>
<tr v-for="topic in mockTopics" :key="topic.id">
<td class="border p-2"><input type="checkbox"></td>
<td class="border p-2">{{ topic.id }}</td>
<td class="border p-2">{{ topic.title }}</td>
<td class="border p-2">
<span class="status-badge">
<span class="status-dot" :class="getStatusClass(topic.status)"></span>
{{ getStatusText(topic.status) }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<script>
const DebugApp = {
data() {
return {
testType: 'topics',
debugLogs: [
'Vue调试应用已启动',
'请运行调试测试查看详细信息',
''
],
testResults: [],
mockTopics: [
{ id: 'A01', title: '可持续发展趋势分析', status: 'pending' },
{ id: 'B02', title: 'AI在内容创作中的应用', status: 'review' },
{ id: 'C03', title: '数字化转型案例研究', status: 'ready' }
]
}
},
methods: {
addLog(message) {
this.debugLogs.push('[' + new Date().toLocaleTimeString() + '] ' + message);
},
runDebugTest() {
this.addLog('开始运行调试测试...');
// 测试数据绑定
setTimeout(() => {
this.addLog('✅ 数据绑定测试通过');
}, 100);
// 测试方法调用
setTimeout(() => {
this.addLog('✅ 方法调用测试通过');
this.testResults.push('Vue数据绑定正常');
}, 200);
// 测试DOM操作
setTimeout(() => {
this.addLog('✅ DOM操作测试通过');
this.testResults.push('VueDOM渲染正常');
}, 300);
},
testElementPlus() {
this.addLog('正在测试Element Plus集成...');
// 模拟Element Plus功能测试
setTimeout(() => {
this.addLog('✅ Element Plus样式加载成功');
this.addLog('✅ Element Plus组件可用');
this.testResults.push('Element Plus集成正常');
}, 200);
},
resetDebug() {
this.debugLogs = ['Vue调试应用已启动', '请运行调试测试查看详细信息', ''];
this.testResults = [];
this.addLog('调试信息已重置');
},
getStatusClass(status) {
const classes = {
'pending': 'status-dot pending',
'review': 'status-dot review',
'ready': 'status-dot ready',
'published': 'status-dot published'
};
return classes[status] || '';
},
getStatusText(status) {
const texts = {
'pending': '待处理',
'review': '待审查',
'ready': '待发布',
'published': '已发布'
};
return texts[status] || status;
}
},
mounted() {
this.addLog('Vue应用程序挂载完成');
this.addLog('应用状态:', this.$data);
console.log('Vue调试应用已启动');
}
}
Vue.createApp(DebugApp).mount('#app')
</script>
</body>
</html>
-269
View File
@@ -1,269 +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>
<!-- 测试资源加载 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.3/dist/index.css">
<script src="https://unpkg.com/element-plus@2.4.3/dist/index.full.min.js"></script>
<style>
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; margin-bottom: 24px; }
aside button { width: 100%; text-align: left; border: none; background: transparent; border-radius: 8px; margin-bottom: 4px; }
@media (max-width: 768px) { main { padding-bottom: 70px; } }
.nav-title { text-align: center; }
.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; }
</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>
</nav>
<!-- 侧边栏 -->
<div class="page flex gap-6">
<aside class="w-40 flex-shrink-0 hidden md:block">
<button @click="showTest('topics')" :class="['px-4 py-2 rounded-lg', testType === 'topics' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📋 选题管理测试</button>
<button @click="showTest('logs')" :class="['px-4 py-2 rounded-lg', testType === 'logs' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📄 系统日志测试</button>
<button @click="showTest('users')" :class="['px-4 py-2 rounded-lg', testType === 'users' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">👥 用户管理测试</button>
</aside>
<!-- 主内容区 -->
<main class="flex-1">
<div class="card" style="padding: 20px; margin-top: 20px;">
<h2 class="text-2xl font-bold text-gray-800 mb-6">🔍 功能诊断测试</h2>
<!-- 测试结果显示 -->
<div v-if="testResults.length > 0" class="mb-4 p-4 bg-green-50 border-l-4 border-green-400">
<h3 class="font-bold mb-2">✅ 测试结果:</h3>
<ul class="list-disc pl-5">
<li v-for="result in testResults">{{ result }}</li>
</ul>
</div>
<!-- 选题管理测试 -->
<div v-if="testType === 'topics'">
<h3 class="text-xl font-bold mb-4">📋 选题管理功能测试</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div class="p-4 bg-blue-50 rounded">
<h4 class="font-bold mb-2">批量操作测试</h4>
<button @click="testBatchOperations" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">测试批量刷新</button>
<span v-if="batchTested" class="ml-2 text-green-600">✅ 通过</span>
</div>
<div class="p-4 bg-green-50 rounded">
<h4 class="font-bold mb-2">数据加载测试</h4>
<button @click="testDataLoading" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">测试数据加载</button>
<span v-if="dataLoaded" class="ml-2 text-green-600">✅ 通过</span>
</div>
</div>
<!-- 模拟表格 -->
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-gray-50">
<th class="border p-2"><input type="checkbox"></th>
<th class="border p-2">ID</th>
<th class="border p-2">标题</th>
<th class="border p-2">状态</th>
<th class="border p-2">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="topic in mockTopics" :key="topic.id">
<td class="border p-2"><input type="checkbox"></td>
<td class="border p-2">{{ topic.id }}</td>
<td class="border p-2">{{ topic.title }}</td>
<td class="border p-2">
<span class="status-badge">
<span class="status-dot" :class="getStatusClass(topic.status)"></span>
{{ getStatusText(topic.status) }}
</span>
</td>
<td class="border p-2">
<button class="px-2 py-1 bg-blue-500 text-white rounded mr-1 text-xs">预览</button>
<button class="px-2 py-1 bg-green-500 text-white rounded mr-1 text-xs">创作</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 系统日志测试 -->
<div v-if="testType === 'logs'" class="p-6 bg-yellow-50 rounded">
<h3 class="text-xl font-bold mb-4">📄 系统日志功能测试</h3>
<div class="flex flex-wrap gap-4 mb-4">
<select v-model="logType" class="px-3 py-2 border rounded">
<option value="creator">创作日志</option>
<option value="optimizer">优化日志</option>
<option value="collector">收集日志</option>
</select>
<input v-model="logDate" type="date" class="px-3 py-2 border rounded">
<button @click="fetchMockLogs" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">加载日志</button>
</div>
<pre class="bg-white p-4 rounded border min-h-[200px] whitespace-pre-wrap font-mono text-sm">{{ logContent }}</pre>
</div>
<!-- 用户管理测试 -->
<div v-if="testType === 'users'" class="p-6 bg-purple-50 rounded">
<h3 class="text-xl font-bold mb-4">👥 用户管理功能测试</h3>
<div class="flex justify-between items-center mb-4">
<h4 class="font-bold">用户列表</h4>
<button @click="addMockUser" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">+ 新建用户</button>
</div>
<table class="w-full border-collapse">
<thead>
<tr class="bg-gray-50">
<th class="border p-2">ID</th>
<th class="border p-2">用户名</th>
<th class="border p-2">角色</th>
<th class="border p-2">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td class="border p-2">{{ user.id }}</td>
<td class="border p-2">{{ user.username }}</td>
<td class="border p-2">
<span :class="[user.role === 'admin' ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800', 'px-2 py-1 rounded']">
{{ user.role === 'admin' ? '管理员' : '编辑' }}
</span>
</td>
<td class="border p-2">
<button @click="deleteUser(user.id)" class="px-2 py-1 bg-red-500 text-white rounded text-xs" :disabled="user.role === 'admin'">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<script>
const DiagnosticApp = {
data() {
return {
testType: 'topics',
testResults: [],
batchTested: false,
dataLoaded: false,
mockTopics: [
{ id: 'A01', title: '可持续发展趋势分析', status: 'pending' },
{ id: 'B02', title: 'AI在内容创作中的应用', status: 'review' },
{ id: 'C03', title: '数字化转型案例研究', status: 'ready' }
],
logType: 'creator',
logDate: '',
logContent: '请选择日志类型和日期,然后点击加载',
users: [
{ id: 'admin', username: '管理员', role: 'admin' },
{ id: 'editor1', username: '编辑小王', role: 'editor' }
]
}
},
methods: {
showTest(type) {
this.testType = type;
this.testResults = [];
},
// 测试方法
testBatchOperations() {
this.testResults.push('✅ 批量操作按钮点击正常');
this.batchTested = true;
console.log('批量操作测试通过');
},
testDataLoading() {
setTimeout(() => {
this.testResults.push('✅ 数据加载正常 (3个选题)');
this.dataLoaded = true;
console.log('数据加载测试通过');
}, 500);
},
fetchMockLogs() {
const logs = {
creator: `2026-04-27 11:45:23 | 成功生成选题 B02 - AI在内容创作中的应用
2026-04-27 11:30:15 | 开始生成选题 C03 - 数字化转型案例研究`,
optimizer: `2026-04-27 12:05:30 | 优化完成选题 C03 - 合规分提升至78
2026-04-27 11:50:45 | 优化中选题 B02 - 等待人工审核`,
collector: `2026-04-27 10:30:12 | 收集到3个新选题
2026-04-27 09:45:20 | 更新行业热点数据`
}[this.logType] || '暂无日志数据';
this.logContent = `日志类型: ${this.logType}
日期: ${this.logDate || '今天'}
${logs}`;
this.testResults.push(`✅ 日志加载成功 (${this.logType})`);
},
addMockUser() {
const newId = 'user' + Date.now();
this.users.push({ id: newId, username: '新用户', role: 'editor' });
this.testResults.push('✅ 新建用户成功');
},
deleteUser(id) {
if (id !== 'admin') {
this.users = this.users.filter(u => u.id !== id);
this.testResults.push('✅ 删除用户成功');
} else {
this.testResults.push('❌ 不能删除管理员');
}
},
getStatusClass(status) {
const classes = {
'pending': 'status-dot pending',
'review': 'status-dot review',
'ready': 'status-dot ready',
'published': 'status-dot published'
};
return classes[status] || '';
},
getStatusText(status) {
const texts = {
'pending': '待处理',
'review': '待审查',
'ready': '待发布',
'published': '已发布'
};
return texts[status] || status;
}
},
mounted() {
console.log('诊断测试应用已启动');
this.testDataLoading(); // 自动测试数据加载
}
}
Vue.createApp(DiagnosticApp).mount('#app')
</script>
</body>
</html>
-410
View File
@@ -1,410 +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>
<!-- 资源加载 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.3/dist/index.css">
<script src="https://unpkg.com/element-plus@2.4.3/dist/index.full.min.js"></script>
<style>
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; margin-bottom: 24px; }
aside button { width: 100%; text-align: left; border: none; background: transparent; border-radius: 8px; margin-bottom: 4px; }
@media (max-width: 768px) { main { padding-bottom: 70px; } }
.nav-title { text-align: center; }
.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; }
.debug-panel { background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto; }
.btn-primary { padding: 8px 16px; background: #409EFF; color: white; border: none; border-radius: 4px; cursor: pointer; }
.btn-primary:hover { background: #337ecc; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.table th { background-color: #f2f2f2; }
.result-success { color: green; font-weight: bold; }
.result-error { color: red; font-weight: bold; }
</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>
</nav>
<!-- 侧边栏 -->
<div class="page flex gap-6">
<aside class="w-40 flex-shrink-0 hidden md:block">
<button @click="activeTab='diagnostics'" :class="['px-4 py-2 rounded-lg', activeTab === 'diagnostics' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">🔍 诊断测试</button>
<button @click="activeTab='results'" :class="['px-4 py-2 rounded-lg', activeTab === 'results' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📊 测试结果</button>
<button @click="activeTab='solutions'" :class="['px-4 py-2 rounded-lg', activeTab === 'solutions' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">🔧 解决方案</button>
</aside>
<!-- 主内容区 -->
<main class="flex-1">
<div class="card" style="padding: 20px; margin-top: 20px;">
<h2 class="text-2xl font-bold text-gray-800 mb-6">🎯 Vue应用综合诊断与修复</h2>
<!-- 诊断面板 -->
<div v-if="activeTab === 'diagnostics'" class="mb-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<button @click="runComprehensiveTest" class="btn-primary">🔄 运行全面诊断</button>
<button @click="testElementPlusIntegration" class="btn-primary">🧪 测试Element Plus集成</button>
<button @click="testVueCore" class="btn-primary">⚡ 测试Vue核心功能</button>
<button @click="resetAll" class="btn-primary">🔄 重置所有测试</button>
</div>
<!-- 实时调试输出 -->
<div class="debug-panel mb-4">
<strong>诊断日志:</strong><br/>
<span v-for="log in diagnosticLogs" :key="log">{{ log }}</span>
</div>
<!-- 当前状态显示 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="p-4 bg-blue-50 rounded">
<h4 class="font-bold">Vue状态</h4>
<p>初始化: <span :class="vueInitialized ? 'result-success' : 'result-error'">{{ vueInitialized ? '✅' : '❌' }}</span></p>
<p>数据绑定: <span :class="dataBindingWorking ? 'result-success' : 'result-error'">{{ dataBindingWorking ? '✅' : '❌' }}</span></p>
</div>
<div class="p-4 bg-green-50 rounded">
<h4 class="font-bold">Element Plus</h4>
<p>样式加载: <span :class="elementPlusStylesLoaded ? 'result-success' : 'result-error'">{{ elementPlusStylesLoaded ? '✅' : '❌' }}</span></p>
<p>组件可用: <span :class="elementPlusComponentsAvailable ? 'result-success' : 'result-error'">{{ elementPlusComponentsAvailable ? '✅' : '❌' }}</span></p>
</div>
<div class="p-4 bg-yellow-50 rounded">
<h4 class="font-bold">功能状态</h4>
<p>表格渲染: <span :class="tableRenderingWorking ? 'result-success' : 'result-error'">{{ tableRenderingWorking ? '✅' : '❌' }}</span></p>
<p>事件处理: <span :class="eventHandlingWorking ? 'result-success' : 'result-error'">{{ eventHandlingWorking ? '✅' : '❌' }}</span></p>
</div>
</div>
</div>
<!-- 结果面板 -->
<div v-if="activeTab === 'results'" class="space-y-4">
<h3 class="text-xl font-bold">📊 详细测试结果</h3>
<div class="p-4 bg-green-50 rounded" v-for="result in testResults" :key="result.id">
<div class="flex justify-between items-start">
<div>
<h4 class="font-bold">{{ result.title }}</h4>
<p>{{ result.description }}</p>
</div>
<span :class="[result.status === 'passed' ? 'result-success' : 'result-error', 'ml-4']">
{{ result.status === 'passed' ? '✅' : '❌' }}
</span>
</div>
</div>
<div v-if="testResults.length === 0" class="p-4 bg-gray-50 rounded">
<p class="text-gray-500">还没有运行任何测试。请点击上方的"运行全面诊断"开始。</p>
</div>
</div>
<!-- 解决方案面板 -->
<div v-if="activeTab === 'solutions'" class="space-y-4">
<h3 class="text-xl font-bold">🔧 问题解决方案</h3>
<div class="p-4 bg-blue-50 rounded">
<h4 class="font-bold mb-2">方案1: 检查浏览器控制台错误</h4>
<ul class="list-disc pl-5 space-y-1">
<li>打开开发者工具(F12)</li>
<li>切换到Console选项卡</li>
<li>刷新页面并记录所有JavaScript错误</li>
<li>根据错误信息进行针对性修复</li>
</ul>
</div>
<div class="p-4 bg-green-50 rounded">
<h4 class="font-bold mb-2">方案2: 简化Vue应用</h4>
<ul class="list-disc pl-5 space-y-1">
<li>移除所有Element Plus依赖</li>
<li>使用纯HTML/CSS/JS实现基本功能</li>
<li>确保Vue能正常工作</li>
<li>逐步添加复杂功能</li>
</ul>
</div>
<div class="p-4 bg-yellow-50 rounded">
<h4 class="font-bold mb-2">方案3: 本地托管资源</h4>
<ul class="list-disc pl-5 space-y-1">
<li>下载Vue和Element Plus到本地</li>
<li>更新HTML中的CDN链接为本地路径</li>
<li>确保所有资源文件正确放置</li>
<li>重新测试页面功能</li>
</ul>
</div>
<div class="p-4 bg-purple-50 rounded">
<h4 class="font-bold mb-2">方案4: 重构页面结构</h4>
<ul class="list-disc pl-5 space-y-1">
<li>拆分复杂的Vue组件</li>
<li>简化数据结构和状态管理</li>
<li>确保每个功能模块独立工作</li>
<li>分阶段测试和验证</li>
</ul>
</div>
</div>
<!-- 功能演示区域 -->
<div class="mt-8">
<h3 class="text-xl font-bold mb-4">📋 功能演示</h3>
<!-- 模拟选题管理 -->
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th style="width: 55px"><input type="checkbox" @change="toggleSelectAll"></th>
<th style="width: 70px">ID</th>
<th>标题</th>
<th style="width: 100px">领域</th>
<th style="width: 90px">状态</th>
<th style="width: 210px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="topic in topics" :key="topic.id">
<td><input type="checkbox" v-model="selectedTopicIds" :value="topic.id"></td>
<td>{{ topic.id }}</td>
<td>{{ topic.title }}</td>
<td>{{ topic.field }}</td>
<td>
<span class="status-badge">
<span class="status-dot" :class="getStatusClass(topic.status)"></span>
{{ getStatusText(topic.status) }}
</span>
</td>
<td>
<button @click="openPreview(topic)" class="px-2 py-1 bg-blue-500 text-white rounded mr-1 text-xs">预览</button>
<button @click="createTopic(topic)" class="px-2 py-1 bg-green-500 text-white rounded mr-1 text-xs">创作</button>
<button @click="deleteTopic(topic.id)" class="px-2 py-1 bg-red-500 text-white rounded text-xs">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
</div>
<script>
const DiagnosticApp = {
data() {
return {
activeTab: 'diagnostics',
diagnosticLogs: [
'综合诊断应用已启动',
'请运行测试查看详细信息',
''
],
testResults: [],
vueInitialized: false,
dataBindingWorking: false,
elementPlusStylesLoaded: false,
elementPlusComponentsAvailable: false,
tableRenderingWorking: false,
eventHandlingWorking: false,
// 选题数据
topics: [
{ id: 'A01', title: '可持续发展趋势分析', field: '环保', status: 'pending' },
{ id: 'B02', title: 'AI在内容创作中的应用', field: '科技', status: 'review' },
{ id: 'C03', title: '数字化转型案例研究', field: '商业', status: 'ready' }
],
selectedTopicIds: []
}
},
methods: {
addLog(message) {
this.diagnosticLogs.push('[' + new Date().toLocaleTimeString() + '] ' + message);
},
runComprehensiveTest() {
this.addLog('开始运行全面诊断...');
this.testResults = [];
// 测试Vue初始化
setTimeout(() => {
this.vueInitialized = true;
this.addLog('✅ Vue应用程序初始化成功');
this.testResults.push({
id: 'vue-init',
title: 'Vue初始化测试',
description: 'Vue.createApp和mount执行正常',
status: 'passed'
});
}, 100);
// 测试数据绑定
setTimeout(() => {
this.dataBindingWorking = true;
this.addLog('✅ Vue数据绑定测试通过');
this.testResults.push({
id: 'data-binding',
title: '数据绑定测试',
description: '文本插值和变量引用正常',
status: 'passed'
});
}, 200);
// 测试Element Plus样式
setTimeout(() => {
this.elementPlusStylesLoaded = true;
this.addLog('✅ Element Plus样式加载成功');
this.testResults.push({
id: 'element-styles',
title: 'Element Plus样式测试',
description: 'CSS样式文件加载正常',
status: 'passed'
});
}, 300);
// 测试Element Plus组件
setTimeout(() => {
this.elementPlusComponentsAvailable = true;
this.addLog('✅ Element Plus组件模拟可用');
this.testResults.push({
id: 'element-components',
title: 'Element Plus组件测试',
description: '组件API和功能模拟正常',
status: 'passed'
});
}, 400);
// 测试表格渲染
setTimeout(() => {
this.tableRenderingWorking = true;
this.addLog('✅ Vue表格渲染测试通过');
this.testResults.push({
id: 'table-rendering',
title: '表格渲染测试',
description: 'v-for列表渲染和动态数据绑定正常',
status: 'passed'
});
}, 500);
// 测试事件处理
setTimeout(() => {
this.eventHandlingWorking = true;
this.addLog('✅ Vue事件处理测试通过');
this.testResults.push({
id: 'event-handling',
title: '事件处理测试',
description: '@click等事件监听器正常工作',
status: 'passed'
});
}, 600);
},
testElementPlusIntegration() {
this.addLog('正在测试Element Plus集成...');
setTimeout(() => {
this.elementPlusStylesLoaded = true;
this.elementPlusComponentsAvailable = true;
this.addLog('✅ Element Plus集成测试通过');
this.testResults.push({
id: 'element-integration',
title: 'Element Plus集成测试',
description: '样式和组件功能模拟正常',
status: 'passed'
});
}, 300);
},
testVueCore() {
this.addLog('正在测试Vue核心功能...');
setTimeout(() => {
this.vueInitialized = true;
this.dataBindingWorking = true;
this.tableRenderingWorking = true;
this.eventHandlingWorking = true;
this.addLog('✅ Vue核心功能测试通过');
this.testResults.push({
id: 'vue-core',
title: 'Vue核心功能测试',
description: '数据绑定、计算属性、生命周期钩子正常',
status: 'passed'
});
}, 300);
},
resetAll() {
this.diagnosticLogs = ['综合诊断应用已启动', '请运行测试查看详细信息', ''];
this.testResults = [];
this.vueInitialized = false;
this.dataBindingWorking = false;
this.elementPlusStylesLoaded = false;
this.elementPlusComponentsAvailable = false;
this.tableRenderingWorking = false;
this.eventHandlingWorking = false;
this.selectedTopicIds = [];
this.addLog('所有测试已重置');
},
toggleSelectAll(event) {
if (event.target.checked) {
this.selectedTopicIds = this.topics.map(t => t.id);
} else {
this.selectedTopicIds = [];
}
},
openPreview(topic) {
this.addLog('打开选题预览: ' + topic.title);
},
createTopic(topic) {
this.addLog('创作选题: ' + topic.title);
},
deleteTopic(id) {
this.addLog('删除选题: ' + id);
},
getStatusClass(status) {
const classes = {
'pending': 'status-dot pending',
'review': 'status-dot review',
'ready': 'status-dot ready',
'published': 'status-dot published'
};
return classes[status] || '';
},
getStatusText(status) {
const texts = {
'pending': '待处理',
'review': '待审查',
'ready': '待发布',
'published': '已发布'
};
return texts[status] || status;
}
},
mounted() {
this.addLog('综合诊断应用程序挂载完成');
console.log('Vue综合诊断应用已启动');
}
}
Vue.createApp(DiagnosticApp).mount('#app')
</script>
</body>
</html>
-452
View File
@@ -1,452 +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>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.panel { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.btn { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 4px; margin: 5px; cursor: pointer; }
.btn:hover { background: #0056b3; }
.status { font-weight: bold; padding: 5px 10px; border-radius: 4px; }
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
.warning { background: #fff3cd; color: #856404; }
.info { background: #d1ecf1; color: #0c5460; }
</style>
</head>
<body>
<div class="container">
<h1 style="color: #007bff;">宇之然内容创作平台 - Vue问题诊断</h1>
<!-- 问题描述 -->
<div class="panel info">
<h3>📋 问题描述</h3>
<p><strong>症状:</strong> 选题管理、系统日志、用户管理页面点击菜单后只显示标题,没有实际内容</p>
<p><strong>可能原因:</strong> Vue应用初始化失败、Element Plus集成问题、CSS样式冲突等</p>
</div>
<!-- 诊断按钮 -->
<div class="panel">
<h3>🔍 快速诊断</h3>
<button onclick="runQuickTest()" class="btn">运行快速诊断</button>
<button onclick="checkConsole()" class="btn">检查控制台错误</button>
<button onclick="resetPage()" class="btn">重置页面</button>
<div id="testResults" style="margin-top: 15px;"></div>
</div>
<!-- 详细分析 -->
<div class="panel">
<h3>🔬 详细分析</h3>
<div id="detailedAnalysis"></div>
</div>
<!-- 解决方案 -->
<div class="panel">
<h3>💡 解决方案</h3>
<ol id="solutionsList"></ol>
</div>
<!-- 紧急修复 -->
<div class="panel warning">
<h3>🚨 紧急修复方案</h3>
<button onclick="applyEmergencyFix()" class="btn">应用紧急修复</button>
<p id="emergencyResult" style="margin-top: 10px;"></p>
</div>
</div>
<script>
let appData = {
vueReady: false,
elementPlusLoaded: false,
cssLoaded: false,
domReady: false,
errors: [],
warnings: []
};
function runQuickTest() {
document.getElementById('testResults').innerHTML = '<p>正在运行诊断测试...</p>';
// 检查Vue
setTimeout(() => {
if (window.Vue) {
appData.vueReady = true;
addResult('✅ Vue 3库已加载', 'success');
} else {
appData.errors.push('Vue 3库未加载');
addResult('❌ Vue 3库加载失败', 'error');
}
// 检查DOM
const appElement = document.getElementById('app');
if (appElement) {
appData.domReady = true;
addResult('✅ DOM元素存在', 'success');
} else {
appData.errors.push('找不到#app元素');
addResult('❌ DOM元素缺失', 'error');
}
// 检查Tailwind
const tailwindScript = document.querySelector('script[src*="tailwindcss"]');
if (tailwindScript) {
appData.cssLoaded = true;
addResult('✅ Tailwind CSS已加载', 'success');
} else {
appData.warnings.push('Tailwind CSS可能未正确加载');
addResult('⚠️ Tailwind CSS状态未知', 'warning');
}
updateDetailedAnalysis();
generateSolutions();
}, 100);
}
function checkConsole() {
console.log('=== 宇之然Vue应用诊断 ===');
console.log('Vue状态:', appData.vueReady ? 'ready' : 'not ready');
console.log('DOM状态:', appData.domReady ? 'ready' : 'not ready');
console.log('CSS状态:', appData.cssLoaded ? 'loaded' : 'not loaded');
console.log('错误列表:', appData.errors);
console.log('警告列表:', appData.warnings);
addResult('✅ 控制台检查完成,请查看浏览器开发者工具(F12)', 'info');
}
function resetPage() {
location.reload();
}
function applyEmergencyFix() {
document.getElementById('emergencyResult').innerHTML = '<p>正在应用紧急修复...</p>';
setTimeout(() => {
// 创建一个新的极简Vue应用
const emergencyHTML = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 紧急修复版</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.4.3/dist/index.css">
<script src="https://unpkg.com/element-plus@2.4.3/dist/index.full.min.js"></script>
<style>
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }
.nav { background: linear-gradient(to right, #2563eb, #1d4ed8); color: white; padding: 1rem 2rem; }
.sidebar { width: 160px; background: #f3f4f6; padding: 1rem; }
.content { flex: 1; padding: 1.5rem; }
.card { background: white; border-radius: 0.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 1.5rem; margin-bottom: 1.5rem; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border: 1px solid #e5e7eb; padding: 0.75rem; text-align: left; }
.table th { background: #f9fafb; }
.btn { padding: 0.5rem 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.25rem; cursor: pointer; }
.btn:hover { background: #2563eb; }
.btn:disabled { background: #9ca3af; cursor: not-allowed; }
.flex { display: flex; }
.gap-4 { gap: 1rem; }
.mb-4 { margin-bottom: 1rem; }
.hidden.md\:block { display: none; }
@media (min-width: 768px) { .hidden.md\:block { display: block; } }
</style>
</head>
<body>
<div id="app">
<!-- 导航栏 -->
<nav class="nav">
<h1 style="text-align: center; margin: 0;">宇之然内容创作平台</h1>
</nav>
<!-- 侧边栏和主内容区 -->
<div class="flex">
<aside class="sidebar hidden md:block">
<button onclick="setActiveTab('topics')" style="width: 100%; text-align: left; padding: 0.5rem; border: none; background: transparent; border-radius: 0.25rem; margin-bottom: 0.25rem; cursor: pointer;">
📋 选题管理
</button>
<button onclick="setActiveTab('logs')" style="width: 100%; text-align: left; padding: 0.5rem; border: none; background: transparent; border-radius: 0.25rem; margin-bottom: 0.25rem; cursor: pointer;">
📄 系统日志
</button>
<button onclick="setActiveTab('users')" style="width: 100%; text-align: left; padding: 0.5rem; border: none; background: transparent; border-radius: 0.25rem; margin-bottom: 0.25rem; cursor: pointer;">
👥 用户管理
</button>
</aside>
<!-- 主内容区 -->
<main class="content">
<!-- 选题管理 -->
<div v-if="activeTab === 'topics'" class="card">
<h2 style="font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;">📋 选题管理</h2>
<div style="display: inline-block; min-width: fit-content; margin-bottom: 1rem;">
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<button onclick="batchOperation('refresh')" class="btn">🔄 批量刷新</button>
<button onclick="batchOperation('generate')" class="btn">▶ 批量创作</button>
<button onclick="batchOperation('optimize')" class="btn">🔍 批量优化</button>
</div>
</div>
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap;">
<span onclick="filterTopics('all')" style="padding: 0.25rem 0.75rem; background: #dbeafe; color: #1e40af; border-radius: 9999px; cursor: pointer;">全部 (3)</span>
<span onclick="filterTopics('pending')" style="padding: 0.25rem 0.75rem; background: #f3f4f6; color: #374151; border-radius: 9999px; cursor: pointer;">待处理 (1)</span>
<span onclick="filterTopics('review')" style="padding: 0.25rem 0.75rem; background: #f3f4f6; color: #374151; border-radius: 9999px; cursor: pointer;">待审查 (1)</span>
<span onclick="filterTopics('ready')" style="padding: 0.25rem 0.75rem; background: #f3f4f6; color: #374151; border-radius: 9999px; cursor: pointer;">待发布 (1)</span>
</div>
<div style="overflow-x: auto;">
<table class="table">
<thead>
<tr>
<th style="width: 55px"><input type="checkbox" onclick="toggleSelectAll()"></th>
<th style="width: 70px">ID</th>
<th>标题</th>
<th style="width: 100px">领域</th>
<th style="width: 90px">状态</th>
<th style="width: 210px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="topic in filteredTopics" :key="topic.id">
<td><input type="checkbox" v-model="selectedTopicIds" :value="topic.id"></td>
<td>{{ topic.id }}</td>
<td>{{ topic.title }}</td>
<td>{{ topic.field }}</td>
<td>
<span style="display: inline-flex; align-items: center; gap: 0.25rem;">
<span style="width: 6px; height: 6px; border-radius: 50%; background: #eab308;" v-if="topic.status === 'pending'"></span>
<span style="width: 6px; height: 6px; border-radius: 50%; background: #ef4444;" v-if="topic.status === 'review'"></span>
<span style="width: 6px; height: 6px; border-radius: 50%; background: #22c55e;" v-if="topic.status === 'ready'"></span>
{{ getStatusText(topic.status) }}
</span>
</td>
<td>
<button onclick="openPreview(topic)" style="padding: 0.25rem 0.5rem; background: #3b82f6; color: white; border: none; border-radius: 0.25rem; margin-right: 0.25rem; font-size: 0.75rem;">预览</button>
<button onclick="createTopic(topic)" style="padding: 0.25rem 0.5rem; background: #22c55e; color: white; border: none; border-radius: 0.25rem; margin-right: 0.25rem; font-size: 0.75rem;">创作</button>
<button onclick="deleteTopic(topic.id)" style="padding: 0.25rem 0.5rem; background: #ef4444; color: white; border: none; border-radius: 0.25rem; font-size: 0.75rem;">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 系统日志 -->
<div v-if="activeTab === 'logs'" class="card">
<h2 style="font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;">📄 系统日志</h2>
<div style="display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;">
<select v-model="logType" style="padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.25rem;">
<option value="creator">创作日志</option>
<option value="optimizer">优化日志</option>
<option value="collector">收集日志</option>
</select>
<input v-model="logDate" type="date" style="padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.25rem;">
<button onclick="loadLogs()" class="btn">加载日志</button>
</div>
<pre style="background: #f9fafb; padding: 1rem; border-radius: 0.25rem; border: 1px solid #e5e7eb; min-height: 200px; overflow-y: auto;">{{ logContent }}</pre>
</div>
<!-- 用户管理 -->
<div v-if="activeTab === 'users'" class="card">
<h2 style="font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;">👥 用户管理</h2>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3 style="font-size: 1.125rem; font-weight: bold;">用户列表</h3>
<button onclick="addUser()" class="btn">+ 新建用户</button>
</div>
<table class="table">
<thead>
<tr>
<th style="width: 70px">ID</th>
<th>用户名</th>
<th style="width: 100px">角色</th>
<th style="width: 180px">创建时间</th>
<th style="width: 150px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>
<span v-if="user.role === 'admin'" style="padding: 0.25rem 0.5rem; background: #fee2e2; color: #dc2626; border-radius: 0.25rem;">管理员</span>
<span v-if="user.role === 'editor'" style="padding: 0.25rem 0.5rem; background: #dcfce7; color: #16a34a; border-radius: 0.25rem;">编辑</span>
</td>
<td>{{ formatDate(user.created_at) }}</td>
<td>
<button onclick="deleteUser(user.id)" style="padding: 0.25rem 0.5rem; background: #ef4444; color: white; border: none; border-radius: 0.25rem; font-size: 0.75rem;" :disabled="user.role === 'admin'">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</main>
</div>
</div>
<script>
const EmergencyApp = {
data() {
return {
activeTab: 'topics',
topics: [
{ id: 'A01', title: '可持续发展趋势分析', field: '环保', status: 'pending' },
{ id: 'B02', title: 'AI在内容创作中的应用', field: '科技', status: 'review' },
{ id: 'C03', title: '数字化转型案例研究', field: '商业', status: 'ready' }
],
selectedTopicIds: [],
filteredTopics: [],
logType: 'creator',
logDate: '',
logContent: '请选择日志类型和日期,然后点击加载',
users: [
{ id: 'admin', username: '管理员', role: 'admin', created_at: '2026-04-01 09:00' },
{ id: 'editor1', username: '编辑小王', role: 'editor', created_at: '2026-04-05 14:30' }
]
}
},
methods: {
getStatusText(status) {
const texts = { 'pending': '待处理', 'review': '待审查', 'ready': '待发布' };
return texts[status] || status;
},
formatDate(dateStr) {
if (!dateStr) return '-';
return dateStr;
}
},
mounted() {
this.filteredTopics = this.topics;
console.log('紧急修复版Vue应用已启动');
}
}
Vue.createApp(EmergencyApp).mount('#app');
// 全局函数
window.setActiveTab = function(tab) {
appData.activeTab = tab;
};
window.batchOperation = function(type) {
console.log('批量操作:', type);
};
window.filterTopics = function(filter) {
if (filter === 'all') {
appData.filteredTopics = appData.topics;
} else {
appData.filteredTopics = appData.topics.filter(t => t.status === filter);
}
};
window.toggleSelectAll = function() {
// 切换全选逻辑
};
window.openPreview = function(topic) {
console.log('打开预览:', topic);
};
window.createTopic = function(topic) {
console.log('创作选题:', topic);
};
window.deleteTopic = function(id) {
console.log('删除选题:', id);
};
window.loadLogs = function() {
const logs = {
creator: '2026-04-27 11:45:23 | 成功生成选题 B02 - AI在内容创作中的应用\n2026-04-27 11:30:15 | 开始生成选题 C03 - 数字化转型案例研究',
optimizer: '2026-04-27 12:05:30 | 优化完成选题 C03 - 合规分提升至78\n2026-04-27 11:50:45 | 优化中选题 B02 - 等待人工审核',
collector: '2026-04-27 10:30:12 | 收集到3个新选题\n2026-04-27 09:45:20 | 更新行业热点数据'
};
appData.logContent = \`日志类型: \${appData.logType}\n日期: \${appData.logDate || '今天'}\n\n\${logs[appData.logType] || '暂无日志数据'}\`;
};
window.addUser = function() {
console.log('添加用户');
};
window.deleteUser = function(id) {
if (id !== 'admin') {
console.log('删除用户:', id);
}
};
</script>
</body>
</html>
`;
// 替换当前页面内容
document.documentElement.innerHTML = emergencyHTML;
document.getElementById('emergencyResult').innerHTML =
'<p style="color: green; font-weight: bold;">✅ 紧急修复已应用!</p>' +
'<p>页面已更新为简化版本,移除了复杂的依赖。</p>' +
'<p><a href="#" onclick="location.reload()" class="btn" style="background: #28a745;">重新加载</a></p>';
}, 1000);
}
function addResult(message, type = 'info') {
const resultDiv = document.getElementById('testResults');
const colorClass = type === 'success' ? 'success' : type === 'error' ? 'error' : 'warning';
resultDiv.innerHTML +=
'<div class="status ' + colorClass + '" style="margin: 5px 0; padding: 5px 10px; display: inline-block;">' + message + '</div>';
}
function updateDetailedAnalysis() {
const analysisDiv = document.getElementById('detailedAnalysis');
let analysis = '';
analysis += '<h4>当前状态:</h4>';
analysis += '<ul>';
analysis += '<li>Vue就绪: ' + (appData.vueReady ? '✅' : '❌') + '</li>';
analysis += '<li>DOM就绪: ' + (appData.domReady ? '✅' : '❌') + '</li>';
analysis += '<li>CSS就绪: ' + (appData.cssLoaded ? '✅' : '❌') + '</li>';
analysis += '</ul>';
if (appData.errors.length > 0) {
analysis += '<h4 style="color: red;">错误:</h4>';
analysis += '<ul>';
appData.errors.forEach(error => {
analysis += '<li style="color: red;">' + error + '</li>';
});
analysis += '</ul>';
}
analysisDiv.innerHTML = analysis;
}
function generateSolutions() {
const solutionsDiv = document.getElementById('solutionsList');
let solutions = '';
solutions += '<li><strong>检查浏览器控制台</strong>: 按F12查看JavaScript错误</li>';
solutions += '<li><strong>验证CDN资源</strong>: 确保Vue和Element Plus能正常下载</li>';
solutions += '<li><strong>简化页面结构</strong>: 移除复杂依赖,使用纯HTML/CSS/JS</li>';
solutions += '<li><strong>检查网络连接</strong>: 确认能访问外部资源</li>';
solutions += '<li><strong>清除缓存</strong>: 尝试无痕模式或清除浏览器缓存</li>';
solutions += '<li><strong>使用本地托管</strong>: 下载Vue和Element Plus到本地服务器</li>';
solutionsDiv.innerHTML = solutions;
}
// 自动运行初始诊断
setTimeout(runQuickTest, 100);
</script>
</body>
</html>
-451
View File
@@ -1,451 +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>宇之然内容创作平台 - 独立Vue测试</title>
<!-- 仅包含必要的资源 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; margin-bottom: 24px; }
aside button { width: 100%; text-align: left; border: none; background: transparent; border-radius: 8px; margin-bottom: 4px; }
@media (max-width: 768px) { main { padding-bottom: 70px; } }
.nav-title { text-align: center; }
.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; }
.debug-panel { background: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto; }
.btn-primary { padding: 8px 16px; background: #409EFF; color: white; border: none; border-radius: 4px; cursor: pointer; }
.btn-primary:hover { background: #337ecc; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.table th { background-color: #f2f2f2; }
</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">宇之然内容创作平台 - 独立Vue测试</h1>
</div>
</nav>
<!-- 侧边栏 -->
<div class="page flex gap-6">
<aside class="w-40 flex-shrink-0 hidden md:block">
<button @click="testType='topics'" :class="['px-4 py-2 rounded-lg', testType === 'topics' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📋 选题管理</button>
<button @click="testType='logs'" :class="['px-4 py-2 rounded-lg', testType === 'logs' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">📄 系统日志</button>
<button @click="testType='users'" :class="['px-4 py-2 rounded-lg', testType === 'users' ? 'bg-blue-50 text-blue-600 font-semibold' : 'text-gray-600']">👥 用户管理</button>
</aside>
<!-- 主内容区 -->
<main class="flex-1">
<div class="card" style="padding: 20px; margin-top: 20px;">
<h2 class="text-2xl font-bold text-gray-800 mb-6">🔍 独立Vue应用测试</h2>
<!-- 调试信息显示 -->
<div class="debug-panel mb-4">
<strong>实时输出:</strong><br/>
<span v-for="log in debugLogs" :key="log">{{ log }}</span>
</div>
<!-- 测试按钮 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<button @click="runFullTest" class="btn-primary">运行完整测试</button>
<button @click="testElementPlus" class="btn-primary">测试Element Plus模拟</button>
<button @click="resetDebug" class="btn-primary">重置调试</button>
</div>
<!-- 测试结果 -->
<div v-if="testResults.length > 0" class="mb-4 p-4 bg-green-50 border-l-4 border-green-400">
<h3 class="font-bold mb-2">测试结果:</h3>
<ul class="list-disc pl-5">
<li v-for="result in testResults">{{ result }}</li>
</ul>
</div>
<!-- 选题管理测试 -->
<div v-if="testType === 'topics'" class="overflow-x-auto">
<h3 class="text-xl font-bold mb-4">📋 选题管理功能</h3>
<!-- 批量操作 -->
<div class="mb-4 p-4 bg-blue-50 rounded">
<div class="flex flex-wrap gap-2 items-center">
<button @click="refreshAll" class="btn-primary">🔄 批量刷新</button>
<button @click="triggerGenerateSelected" :disabled="selectedTopicIds.length === 0" class="btn-primary">▶ 批量创作</button>
<button @click="triggerOptimizeSelected" :disabled="selectedTopicIds.length === 0" class="btn-primary">🔍 批量优化</button>
<span class="ml-auto text-sm text-gray-500" v-if="selectedTopicIds.length > 0">已选 {{ selectedTopicIds.length }} 项</span>
</div>
</div>
<!-- 筛选标签 -->
<div class="flex flex-wrap gap-2 mb-4">
<span @click="filterStatus = ''" :class="[filterStatus === '' ? 'bg-blue-500' : 'bg-gray-200', 'px-3 py-1 rounded-full cursor-pointer text-white']">全部 ({{ topics.length }})</span>
<span @click="filterStatus = '待处理'" :class="[filterStatus === '待处理' ? 'bg-blue-500' : 'bg-gray-200', 'px-3 py-1 rounded-full cursor-pointer']">待处理 ({{ countByStatus('待处理') }})</span>
<span @click="filterStatus = '待审查'" :class="[filterStatus === '待审查' ? 'bg-blue-500' : 'bg-gray-200', 'px-3 py-1 rounded-full cursor-pointer']">待审查 ({{ countByStatus('待审查') }})</span>
<span @click="filterStatus = '待发布'" :class="[filterStatus === '待发布' ? 'bg-blue-500' : 'bg-gray-200', 'px-3 py-1 rounded-full cursor-pointer']">待发布 ({{ countByStatus('待发布') }})</span>
<span @click="filterStatus = '已发布'" :class="[filterStatus === '已发布' ? 'bg-blue-500' : 'bg-gray-200', 'px-3 py-1 rounded-full cursor-pointer']">已发布 ({{ countByStatus('已发布') }})</span>
</div>
<!-- 表格 -->
<table class="table">
<thead>
<tr>
<th style="width: 55px"><input type="checkbox" @change="toggleSelectAll"></th>
<th style="width: 70px">ID</th>
<th>标题</th>
<th style="width: 100px">领域</th>
<th style="width: 90px">状态</th>
<th style="width: 210px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="topic in filteredTopics" :key="topic.id">
<td><input type="checkbox" v-model="selectedTopicIds" :value="topic.id"></td>
<td>{{ topic.id }}</td>
<td>{{ topic.title }}</td>
<td>{{ topic.field }}</td>
<td>
<span class="status-badge">
<span class="status-dot" :class="getStatusClass(topic.status)"></span>
{{ getStatusText(topic.status) }}
</span>
</td>
<td>
<button @click="openPreview(topic)" class="px-2 py-1 bg-blue-500 text-white rounded mr-1 text-xs">预览</button>
<button @click="createTopic(topic)" :disabled="topic.status !== '待处理'" class="px-2 py-1 bg-green-500 text-white rounded mr-1 text-xs">创作</button>
<button @click="optimizeTopic(topic)" :disabled="topic.status !== '待审查'" class="px-2 py-1 bg-yellow-500 text-white rounded mr-1 text-xs">审查</button>
<button v-if="topic.status === '待发布'" @click="handlePublish(topic)" class="px-2 py-1 bg-blue-500 text-white rounded mr-1 text-xs">发布</button>
<button @click="deleteTopic(topic.id)" class="px-2 py-1 bg-red-500 text-white rounded text-xs">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 系统日志测试 -->
<div v-if="testType === 'logs'" class="p-6 bg-yellow-50 rounded">
<h3 class="text-xl font-bold mb-4">📄 系统日志功能</h3>
<div class="flex flex-wrap gap-4 mb-4">
<select v-model="logType" class="px-3 py-2 border rounded">
<option value="creator">创作日志</option>
<option value="optimizer">优化日志</option>
<option value="collector">收集日志</option>
</select>
<input v-model="logDate" type="date" class="px-3 py-2 border rounded">
<button @click="fetchLogs" class="btn-primary">加载日志</button>
</div>
<pre class="bg-white p-4 rounded border min-h-[200px] whitespace-pre-wrap font-mono text-sm">{{ logContent }}</pre>
</div>
<!-- 用户管理测试 -->
<div v-if="testType === 'users'" class="p-6 bg-purple-50 rounded">
<h3 class="text-xl font-bold mb-4">👥 用户管理功能</h3>
<div class="flex justify-between items-center mb-4">
<h4 class="font-bold">用户列表</h4>
<button @click="addUser" class="btn-primary">+ 新建用户</button>
</div>
<table class="table">
<thead>
<tr>
<th style="width: 70px">ID</th>
<th>用户名</th>
<th style="width: 100px">角色</th>
<th style="width: 180px">创建时间</th>
<th style="width: 150px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>
<span :class="[user.role === 'admin' ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800', 'px-2 py-1 rounded']">
{{ user.role === 'admin' ? '管理员' : '编辑' }}
</span>
</td>
<td>{{ formatDate(user.created_at) }}</td>
<td>
<button @click="deleteUser(user.id)" :disabled="user.role === 'admin'" class="px-2 py-1 bg-red-500 text-white rounded text-xs">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<script>
const IndependentApp = {
data() {
return {
// 基础数据
testType: 'topics',
// 调试相关
debugLogs: [
'独立Vue应用已启动',
'请运行测试查看详细信息',
''
],
testResults: [],
// 选题相关数据
status: {},
topics: [
{
id: 'A01',
title: '可持续发展趋势分析',
field: '环保',
status: 'pending',
compliance_score: 85,
created_at: '2026-04-27 10:30',
generated_at: '-',
published_at: '-',
updated_at: '2026-04-27 10:30',
priority_score: '高'
},
{
id: 'B02',
title: 'AI在内容创作中的应用',
field: '科技',
status: 'review',
compliance_score: 92,
created_at: '2026-04-27 11:15',
generated_at: '2026-04-27 11:45',
published_at: '-',
updated_at: '2026-04-27 11:45',
priority_score: '中'
},
{
id: 'C03',
title: '数字化转型案例研究',
field: '商业',
status: 'ready',
compliance_score: 78,
created_at: '2026-04-27 12:00',
generated_at: '2026-04-27 12:30',
published_at: '2026-04-27 13:00',
updated_at: '2026-04-27 13:00',
priority_score: '高'
}
],
filterStatus: '',
selectedTopicIds: [],
// 日志相关
logType: 'creator',
logDate: '',
logContent: '请选择日志类型和日期,然后点击加载',
// 用户相关
users: [
{ id: 'admin', username: '管理员', role: 'admin', created_at: '2026-04-01 09:00' },
{ id: 'editor1', username: '编辑小王', role: 'editor', created_at: '2026-04-05 14:30' },
{ id: 'editor2', username: '编辑小李', role: 'editor', created_at: '2026-04-10 10:15' }
]
}
},
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: {
addLog(message) {
this.debugLogs.push('[' + new Date().toLocaleTimeString() + '] ' + message);
},
runFullTest() {
this.addLog('开始运行完整测试...');
// 测试数据绑定
setTimeout(() => {
this.addLog('✅ Vue数据绑定测试通过');
}, 100);
// 测试方法调用
setTimeout(() => {
this.addLog('✅ Vue方法调用测试通过');
this.testResults.push('Vue数据绑定正常');
}, 200);
// 测试DOM操作
setTimeout(() => {
this.addLog('✅ VueDOM渲染测试通过');
this.testResults.push('VueDOM操作正常');
}, 300);
// 测试计算属性
setTimeout(() => {
this.addLog('✅ Vue计算属性测试通过');
this.testResults.push('Vue计算属性正常');
}, 400);
},
testElementPlus() {
this.addLog('正在测试Element Plus模拟...');
// 模拟Element Plus功能测试
setTimeout(() => {
this.addLog('✅ Element Plus样式模拟成功');
this.addLog('✅ Element Plus组件模拟可用');
this.testResults.push('Element Plus模拟集成正常');
}, 200);
},
resetDebug() {
this.debugLogs = ['独立Vue应用已启动', '请运行测试查看详细信息', ''];
this.testResults = [];
this.addLog('调试信息已重置');
},
refreshAll() {
this.addLog('执行批量刷新操作');
this.testResults.push('批量刷新操作已触发');
},
triggerGenerateSelected() {
if (!this.selectedTopicIds.length) return
this.addLog('正在批量创作...');
this.testResults.push('批量创作操作已触发');
},
triggerOptimizeSelected() {
if (!this.selectedTopicIds.length) return
this.addLog('正在批量优化...');
this.testResults.push('批量优化操作已触发');
},
toggleSelectAll(event) {
if (event.target.checked) {
this.selectedTopicIds = this.topics.map(t => t.id);
} else {
this.selectedTopicIds = [];
}
},
openPreview(topic) {
this.addLog('打开选题预览: ' + topic.title);
this.testResults.push('预览功能正常');
},
createTopic(topic) {
if (topic && topic.status === '待处理') {
this.addLog('创作选题: ' + topic.title);
this.testResults.push('选题创作功能正常');
}
},
optimizeTopic(topic) {
if (topic && topic.status === '待审查') {
this.addLog('优化选题: ' + topic.title);
this.testResults.push('选题优化功能正常');
}
},
handlePublish(topic) {
this.addLog('发布选题: ' + topic.title);
this.testResults.push('选题发布功能正常');
},
deleteTopic(id) {
this.addLog('删除选题: ' + id);
this.testResults.push('选题删除功能正常');
},
fetchLogs() {
const logs = {
creator: `2026-04-27 11:45:23 | 成功生成选题 B02 - AI在内容创作中的应用
2026-04-27 11:30:15 | 开始生成选题 C03 - 数字化转型案例研究`,
optimizer: `2026-04-27 12:05:30 | 优化完成选题 C03 - 合规分提升至78
2026-04-27 11:50:45 | 优化中选题 B02 - 等待人工审核`,
collector: `2026-04-27 10:30:12 | 收集到3个新选题
2026-04-27 09:45:20 | 更新行业热点数据`
}[this.logType] || '暂无日志数据';
this.logContent = `日志类型: ${this.logType}
日期: ${this.logDate || '今天'}
${logs}`;
this.addLog('日志加载成功');
this.testResults.push('日志加载功能正常');
},
addUser() {
const newId = 'user' + Date.now();
this.users.push({ id: newId, username: '新用户', role: 'editor', created_at: new Date().toISOString().slice(0, 16).replace('T', ' ') });
this.addLog('添加新用户: ' + newId);
this.testResults.push('用户添加功能正常');
},
deleteUser(id) {
if (id !== 'admin') {
this.users = this.users.filter(u => u.id !== id);
this.addLog('删除用户: ' + id);
this.testResults.push('用户删除功能正常');
} else {
this.addLog('不能删除管理员用户');
this.testResults.push('管理员保护功能正常');
}
},
getStatusClass(status) {
const classes = {
'pending': 'status-dot pending',
'review': 'status-dot review',
'ready': 'status-dot ready',
'published': 'status-dot published'
};
return classes[status] || '';
},
getStatusText(status) {
const texts = {
'pending': '待处理',
'review': '待审查',
'ready': '待发布',
'published': '已发布'
};
return texts[status] || status;
},
formatDate(dateStr) {
if (!dateStr || dateStr === '-' || dateStr.trim() === '') return '-'
const date = new Date(dateStr.replace(' ', 'T'))
return date.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
})
}
},
mounted() {
this.addLog('独立Vue应用程序挂载完成');
this.addLog('应用初始状态:', this.$data);
console.log('独立Vue应用已启动');
}
}
Vue.createApp(IndependentApp).mount('#app')
</script>
</body>
</html>
+33 -77
View File
@@ -224,28 +224,8 @@
}
.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);
}
/* 响应式 */
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area {
padding: 16px;
padding-bottom: 80px;
@@ -268,9 +248,6 @@
.sidebar { background: white !important; border-right: 1px solid #ebeef5 !important; backdrop-filter: none !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; }
#page-overview h2, #page-overview h3, #page-overview .module-title { color: #303133 !important; }
#page-overview .module-content { color: #303133 !important; }
@@ -333,50 +310,20 @@
<!-- 模块状态 -->
<h3 style="font-size: 20px; font-weight: 600; margin-bottom: 24px; color: #e0e6ed;">
🔧 模块状态
<span style="font-size: 13px; font-weight: 400; color: #909399; margin-left: 12px;">
定时任务: {{ schedulerStatus }}
</span>
</h3>
<div class="module-grid">
<div class="module-card">
<div v-for="mod in modules" :key="mod.id" class="module-card">
<div class="module-header">
<span class="module-title">🤖 内容创作引擎</span>
<span class="module-status running">运行中</span>
<span class="module-title">{{ mod.title }}</span>
<span :class="['module-status', mod.status === 'running' ? 'running' : '']">{{ mod.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><span>最后运行</span><span>{{ mod.last_run }}</span></div>
<div><span>今日任务</span><span>{{ mod.task_count }}</span></div>
<div v-if="mod.success_rate !== null"><span>成功率</span><span>{{ mod.success_rate }}%</span></div>
</div>
</div>
</div>
@@ -385,20 +332,7 @@
</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>
<!-- 移动端导航由 navigation-component.js 注入 -->
</div>
<script src="vue.global.prod.js"></script>
@@ -418,7 +352,9 @@
ready: 0,
published: 0,
today: 0
}
},
modules: [],
schedulerStatus: ''
};
},
methods: {
@@ -489,6 +425,25 @@
this.stats = { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 };
}
},
async fetchModules() {
try {
const token = localStorage.getItem('authToken');
const resp = await fetch('/api/system/modules/status', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (resp.ok) {
const data = await resp.json();
this.modules = data.modules || [];
const s = data.scheduler || {};
this.schedulerStatus = s.running ? '🟢 运行中' : '🔴 未启动';
if (s.jobs && s.jobs.length) {
this.schedulerStatus += ' · ' + s.jobs.length + ' 个任务';
}
}
} catch (e) {
console.error('获取模块状态失败:', e);
}
},
goToTopics(filter) {
const url = filter ? '/topics.html?filter=' + encodeURIComponent(filter) : '/topics.html';
window.location.href = url;
@@ -513,6 +468,7 @@
this.isLoggedIn = true;
this.currentPage = 'overview';
this.fetchStats();
this.fetchModules();
})
.catch(() => {
localStorage.removeItem('authToken');
+6 -10
View File
@@ -5,19 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 系统日志</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.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; min-height: calc(100vh - 60px); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.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; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
@@ -43,14 +39,14 @@
<navigation-component current-page="logs" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">📄 系统日志</h2>
</div>
<div class="controls">
<el-select v-model="logType" placeholder="日志类型" style="width: 180px;">
<el-option label="创作日志" value="creator"></el-option>
<el-option label="优化日志" value="optimizer"></el-option>
<el-option label="审查日志" value="optimizer"></el-option>
<el-option label="收集日志" value="collector"></el-option>
</el-select>
<el-date-picker v-model="logDate" type="date" placeholder="选择日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
+3 -12
View File
@@ -5,29 +5,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 数据分析</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { 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; }
.sidebar { width: 180px; background: white; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 20px; color: white; text-align: center; }
.stat-card.success { background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%); }
.stat-card.warning { background: linear-gradient(135deg, #e6a23c 0%, #f5c543 100%); }
.stat-card.danger { background: linear-gradient(135deg, #f56c6c 0%, #f78989 100%); }
.stat-value { font-size: 32px; font-weight: 700; }
.stat-label { font-size: 14px; opacity: 0.9; margin-top: 4px; }
.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; }
@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) !important; gap: 12px !important; }
.chart-container { height: 250px !important; }
@@ -67,7 +58,7 @@
<div class="stat-label">平均互动率</div>
</div>
</div>
<div class="card">
<div class="card page-fade">
<h3 style="font-size: 18px; margin-bottom: 16px;">📈 选题状态分布</h3>
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<div v-for="(count, status) in dashboard.topics_by_status" :key="status" style="text-align: center;">
+9 -6
View File
@@ -5,14 +5,17 @@
function injectStyles() {
if (document.getElementById('navbar-styles')) return;
const styles = `
.navbar-component { position: fixed; top: 0; left: 0; right: 0; height: 60px; background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 10000; }
.navbar-component .navbar-title { font-size: 18px; font-weight: 600; margin: 0; }
.navbar-component { position: fixed; top: 0; left: 0; right: 0; height: 60px; background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; box-shadow: 0 4px 20px rgba(37, 99, 235, 0.3); z-index: 10000; }
.navbar-component .navbar-title { font-size: 20px; font-weight: 700; margin: 0; letter-spacing: -0.3px; }
.navbar-component .navbar-user { display: flex; align-items: center; gap: 12px; }
.navbar-component .user-info { display: flex; align-items: center; gap: 8px; }
.navbar-component .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; }
.navbar-component .logout-btn { background: rgba(255,255,255,0.2); border: none; color: white; padding: 6px 12px; border-radius: 6px; cursor: pointer; margin-left: 12px; }
.navbar-component .logout-btn:hover { background: rgba(255,255,255,0.3); }
.navbar-component .admin-badge { margin-left: 8px; font-size: 12px; background: rgba(255,255,255,0.3); padding: 2px 8px; border-radius: 10px; }
.navbar-component .avatar { width: 34px; height: 34px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 15px; font-weight: 600; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
.navbar-component .logout-btn { background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.2); color: white; padding: 6px 14px; border-radius: 8px; cursor: pointer; margin-left: 12px; font-size: 13px; transition: all 0.2s; }
.navbar-component .logout-btn:hover { background: rgba(255,255,255,0.25); border-color: rgba(255,255,255,0.3); transform: translateY(-1px); }
.navbar-component .admin-badge { margin-left: 8px; font-size: 11px; background: rgba(255,255,255,0.2); padding: 2px 10px; border-radius: 12px; font-weight: 500; letter-spacing: 0.3px; }
@media (min-width: 769px) {
.navbar-component { padding: 0 24px 0 180px; }
}
@media (max-width: 768px) {
.navbar-component { padding: 0 16px; }
.navbar-component .navbar-title { font-size: 16px; }
+110 -39
View File
@@ -5,20 +5,31 @@
function injectStyles() {
if (document.getElementById('navigation-styles')) return;
const styles = `
.nav-wrapper, .navigation-wrapper { position: fixed; top: 60px; left: 0; bottom: 0; z-index: 9999; }
.nav-wrapper .sidebar, .navigation-wrapper .sidebar { position: fixed; top: 60px; left: 0; bottom: 0; width: 180px; background: #fff; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.1); overflow-y: auto; z-index: 99999; border-right: 1px solid #ebeef5; }
.nav-wrapper .sidebar-header, .navigation-wrapper .sidebar-header { padding: 12px 8px 16px; border-bottom: 1px solid #ebeef5; margin-bottom: 12px; }
.nav-wrapper .sidebar-header h3, .navigation-wrapper .sidebar-header h3 { margin: 0; font-size: 16px; font-weight: 600; color: #303133; }
.nav-wrapper .sidebar-btn, .navigation-wrapper .sidebar-btn { width: 100%; text-align: left; padding: 10px 12px; border: none; background: transparent; border-radius: 8px; margin-bottom: 6px; cursor: pointer; transition: all 0.3s; color: #606266; font-size: 14px; display: flex; align-items: center; gap: 6px; }
.nav-wrapper .sidebar-btn:hover, .navigation-wrapper .sidebar-btn:hover { background: #f5f7fa; color: #409eff; }
.nav-wrapper .sidebar-btn.active, .navigation-wrapper .sidebar-btn.active { background: #ecf5ff; color: #409eff; font-weight: 600 !important; }
.nav-wrapper .mobile-nav, .navigation-wrapper .mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: #1890ff; box-shadow: 0 -2px 8px rgba(0,0,0,0.2); padding: 8px 0; z-index: 99999; justify-content: space-around; }
.nav-wrapper .mobile-nav-btn, .navigation-wrapper .mobile-nav-btn { flex: 1; border: none; background: transparent; padding: 8px 4px; text-align: center; font-size: 11px; color: white !important; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.nav-wrapper .mobile-nav-btn .nav-icon, .navigation-wrapper .mobile-nav-btn .nav-icon { font-size: 18px; line-height: 1; }
.nav-wrapper .mobile-nav-btn .nav-text, .navigation-wrapper .mobile-nav-btn .nav-text { font-size: 10px; margin-top: 2px; }
.nav-wrapper .mobile-nav-btn:hover, .navigation-wrapper .mobile-nav-btn:hover { background: rgba(255,255,255,0.2); }
.nav-wrapper .mobile-nav-btn.active, .navigation-wrapper .mobile-nav-btn.active { background: rgba(255,255,255,0.3); font-weight: 600 !important; }
@media (max-width: 768px) { .nav-wrapper .sidebar, .navigation-wrapper .sidebar { display: none !important; } .nav-wrapper .mobile-nav, .navigation-wrapper .mobile-nav { display: flex !important; } }
.nav-wrapper, .navigation-wrapper { position: fixed; top: 60px; left: 0; bottom: 0; width: 180px; z-index: 9999; pointer-events: none; }
.nav-wrapper .sidebar, .navigation-wrapper .sidebar { position: fixed; top: 60px; left: 0; bottom: 0; width: 180px; background: #fff; padding: 12px 8px; box-shadow: 2px 0 12px rgba(0,0,0,0.06); overflow-y: auto; z-index: 10000; border-right: 1px solid #ebeef5; pointer-events: auto; }
.nav-wrapper .sidebar-header, .navigation-wrapper .sidebar-header { padding: 12px 12px 16px; border-bottom: 1px solid #f0f2f5; margin-bottom: 12px; }
.nav-wrapper .sidebar-header h3, .navigation-wrapper .sidebar-header h3 { margin: 0; font-size: 15px; font-weight: 700; background: linear-gradient(135deg, #2563eb, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; letter-spacing: -0.3px; }
.nav-wrapper .sidebar-btn, .navigation-wrapper .sidebar-btn { width: 100%; text-align: left; padding: 10px 12px; border: none; background: transparent; border-radius: 10px; margin-bottom: 4px; cursor: pointer; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); color: #606266; font-size: 13px; display: flex; align-items: center; gap: 8px; position: relative; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nav-wrapper .sidebar-btn:hover, .navigation-wrapper .sidebar-btn:hover { background: #f0f4ff; color: #2563eb; transform: translateX(2px); }
.nav-wrapper .sidebar-btn.active, .navigation-wrapper .sidebar-btn.active { background: linear-gradient(135deg, #eef2ff, #e0e7ff); color: #2563eb; font-weight: 600 !important; box-shadow: inset 3px 0 0 #2563eb; }
.nav-wrapper .mobile-nav, .navigation-wrapper .mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: linear-gradient(135deg, #2563eb, #1d4ed8); box-shadow: 0 -4px 20px rgba(37, 99, 235, 0.25); padding: 4px 0; z-index: 99999; justify-content: space-around; pointer-events: auto; }
.nav-wrapper .mobile-nav-btn, .navigation-wrapper .mobile-nav-btn { flex: 1; border: none; background: transparent; padding: 4px 2px; text-align: center; font-size: 10px; color: rgba(255,255,255,0.8) !important; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 0; transition: all 0.2s; border-radius: 8px; margin: 0 1px; min-width: 0; }
.nav-wrapper .mobile-nav-btn .nav-icon, .navigation-wrapper .mobile-nav-btn .nav-icon { font-size: 16px; line-height: 1.2; }
.nav-wrapper .mobile-nav-btn .nav-text, .navigation-wrapper .mobile-nav-btn .nav-text { font-size: 9px; margin-top: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.nav-wrapper .mobile-nav-btn:hover, .navigation-wrapper .mobile-nav-btn:hover { background: rgba(255,255,255,0.15); color: white !important; }
.nav-wrapper .mobile-nav-btn.active, .navigation-wrapper .mobile-nav-btn.active { background: rgba(255,255,255,0.2); color: white !important; font-weight: 600 !important; }
.mobile-more-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); z-index: 100000; animation: fadeIn 0.2s ease-out; }
.mobile-more-sheet { position: fixed; bottom: 0; left: 0; right: 0; background: white; border-radius: 16px 16px 0 0; box-shadow: 0 -8px 32px rgba(0,0,0,0.15); z-index: 100001; padding: 20px 16px calc(env(safe-area-inset-bottom) + 60px); animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); max-height: 70vh; overflow-y: auto; pointer-events: auto; }
.mobile-more-sheet .sheet-handle { width: 36px; height: 4px; background: #e0e0e0; border-radius: 2px; margin: 0 auto 16px; }
.mobile-more-sheet .sheet-title { font-size: 16px; font-weight: 700; color: #303133; margin-bottom: 16px; text-align: center; }
.mobile-more-sheet .sheet-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.mobile-more-sheet .sheet-item { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 12px 4px; border: none; background: transparent; border-radius: 12px; cursor: pointer; transition: all 0.2s; color: #606266; font-size: 12px; white-space: nowrap; }
.mobile-more-sheet .sheet-item:hover { background: #f0f4ff; color: #2563eb; }
.mobile-more-sheet .sheet-item.active { background: #eef2ff; color: #2563eb; font-weight: 600; }
.mobile-more-sheet .sheet-item .item-icon { font-size: 24px; line-height: 1; }
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@media (max-width: 768px) { .nav-wrapper .sidebar, .navigation-wrapper .sidebar { display: none !important; } .nav-wrapper .mobile-nav, .navigation-wrapper .mobile-nav { display: flex !important; } body > #app > .main-content { padding-bottom: 56px !important; } }
body > #app > .main-content { margin-left: 180px !important; padding-top: 60px !important; }
@media (max-width: 768px) { body > #app > .main-content { margin-left: 0 !important; padding-bottom: 60px !important; } }
`;
@@ -55,36 +66,76 @@
mobileNav.innerHTML = `
<button class="mobile-nav-btn ${currentPage==='dashboard'?'active':''}" data-page="/"><span class="nav-icon">📊</span><span class="nav-text">首页</span></button>
<button class="mobile-nav-btn ${currentPage==='topics'?'active':''}" data-page="topics.html"><span class="nav-icon">📋</span><span class="nav-text">选题</span></button>
<button class="mobile-nav-btn ${currentPage==='tasks'?'active':''}" data-page="tasks.html"><span class="nav-icon">🚀</span><span class="nav-text">任务</span></button>
<button class="mobile-nav-btn ${currentPage==='calendar'?'active':''}" data-page="calendar.html"><span class="nav-icon">📅</span><span class="nav-text">日历</span></button>
<button class="mobile-nav-btn ${currentPage==='assets'?'active':''}" data-page="assets.html"><span class="nav-icon">🖼️</span><span class="nav-text">素材</span></button>
<button class="mobile-nav-btn ${currentPage==='tasks'?'active':''}" data-page="tasks.html"><span class="nav-icon">🚀</span><span class="nav-text">任务</span></button>
<button class="mobile-nav-btn" id="mobile-more-btn"><span class="nav-icon"></span><span class="nav-text">更多</span></button>
`;
// "更多"弹出菜单
const sheetItems = [
{ key: 'metrics', label: '数据分析', icon: '📊', page: 'metrics.html' },
{ key: 'platforms', label: '平台配置', icon: '🌐', page: 'platforms.html' },
{ key: 'logs', label: '系统日志', icon: '📄', page: 'logs.html' },
{ key: 'users', label: '用户管理', icon: '👥', page: 'users.html', admin: true },
{ key: 'admin', label: '系统管理', icon: '⚙️', page: 'admin.html', admin: true },
];
const sheetItemsHtml = sheetItems.map(item =>
`<button class="sheet-item ${currentPage===item.key?'active':''}" data-page="${item.page}"${item.admin ? ' data-admin="1"' : ''} style="${item.admin && !isAdmin ? 'display:none' : ''}">
<span class="item-icon">${item.icon}</span><span>${item.label}</span>
</button>`
).join('');
const backdrop = document.createElement('div');
backdrop.className = 'mobile-more-backdrop';
backdrop.id = 'mobile-more-backdrop';
backdrop.style.display = 'none';
const sheet = document.createElement('div');
sheet.className = 'mobile-more-sheet';
sheet.id = 'mobile-more-sheet';
sheet.style.display = 'none';
sheet.innerHTML = `<div class="sheet-handle"></div><div class="sheet-title">所有菜单</div><div class="sheet-grid">${sheetItemsHtml}</div>`;
const showMore = () => {
backdrop.style.display = ''; sheet.style.display = '';
};
const hideMore = () => {
backdrop.style.display = 'none'; sheet.style.display = 'none';
};
// 绑定导航跳转
const handleNavClick = (btn) => {
const page = btn.getAttribute('data-page');
if (!page) return;
console.log('[Nav] 点击导航:', page);
hideMore();
if (onNavigate) { onNavigate(page); return; }
const target = page === '/' ? '/' : (page.startsWith('/') ? page : '/' + page);
window.location.href = target;
};
wrapper.appendChild(sidebar);
wrapper.appendChild(mobileNav);
wrapper.appendChild(backdrop);
wrapper.appendChild(sheet);
// 绑定点击事件
wrapper.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const page = btn.getAttribute('data-page');
console.log('[Nav] 点击导航:', page);
if (onNavigate) {
onNavigate(page);
} else {
// 直接跳转 - 修复路径处理
let target;
if (page === '/' || page === '') {
target = '/';
} else if (page.startsWith('/')) {
target = page;
} else {
target = '/' + page;
}
window.location.href = target;
}
});
// 事件绑定
wrapper.querySelectorAll('.sidebar-btn').forEach(btn => {
btn.addEventListener('click', (e) => { e.preventDefault(); handleNavClick(btn); });
});
wrapper.querySelectorAll('.mobile-nav-btn').forEach(btn => {
if (btn.id === 'mobile-more-btn') {
btn.addEventListener('click', (e) => { e.preventDefault(); showMore(); });
} else {
btn.addEventListener('click', (e) => { e.preventDefault(); handleNavClick(btn); });
}
});
wrapper.querySelectorAll('.sheet-item').forEach(btn => {
btn.addEventListener('click', (e) => { e.preventDefault(); handleNavClick(btn); });
});
backdrop.addEventListener('click', hideMore);
return wrapper;
}
@@ -233,10 +284,18 @@
mobileItems: [
{ key: 'dashboard', label: '首页', icon: '📊', page: '/' },
{ key: 'topics', label: '选题', icon: '📋', page: 'topics.html' },
{ key: 'tasks', label: '任务', icon: '🚀', page: 'tasks.html' },
{ key: 'calendar', label: '日历', icon: '📅', page: 'calendar.html' },
{ key: 'assets', label: '素材', icon: '🖼️', page: 'assets.html' },
{ key: 'tasks', label: '任务', icon: '🚀', page: 'tasks.html' }
]
{ key: 'assets', label: '素材', icon: '🖼️', page: 'assets.html' }
],
sheetItems: [
{ key: 'metrics', label: '数据分析', icon: '📊', page: 'metrics.html' },
{ key: 'platforms', label: '平台配置', icon: '🌐', page: 'platforms.html' },
{ key: 'logs', label: '系统日志', icon: '📄', page: 'logs.html' },
{ key: 'users', label: '用户管理', icon: '👥', page: 'users.html', admin: true },
{ key: 'admin', label: '系统管理', icon: '⚙️', page: 'admin.html', admin: true }
],
showMoreSheet: false
};
},
template: `
@@ -259,7 +318,19 @@
<span class="nav-icon">{{ item.icon }}</span>
<span class="nav-text">{{ item.label }}</span>
</button>
<button class="mobile-nav-btn" @click="showMoreSheet = !showMoreSheet"><span class="nav-icon">⬆</span><span class="nav-text">更多</span></button>
</nav>
<div v-if="showMoreSheet" class="mobile-more-backdrop" @click="showMoreSheet = false"></div>
<div v-if="showMoreSheet" class="mobile-more-sheet">
<div class="sheet-handle"></div>
<div class="sheet-title">所有菜单</div>
<div class="sheet-grid">
<button v-for="item in sheetItems" :key="item.key" v-show="!item.admin || isAdmin" :class="['sheet-item', { active: currentPage === item.key }]" @click="navigate(item.page)">
<span class="item-icon">{{ item.icon }}</span>
<span>{{ item.label }}</span>
</button>
</div>
</div>
</div>
`,
methods: {
+6 -12
View File
@@ -5,20 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 平台配置</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.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; min-height: calc(100vh - 60px); }
.sidebar { width: 180px; background: white; padding: 12px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.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; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
@@ -50,8 +46,6 @@
.loading-state { text-align: center; padding: 60px 20px; color: #909399; font-size: 16px; }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
.card { padding: 16px; }
.platform-card { padding: 16px; }
@@ -79,7 +73,7 @@
<el-button @click="loadPlatforms">🔄 刷新</el-button>
</div>
</div>
<div class="card">
<div class="card page-fade">
<div v-if="loading" class="loading-state">加载中...</div>
<div v-else-if="platforms.length === 0" class="empty-state">
<div class="empty-state-icon">🌐</div>
-315
View File
@@ -1,315 +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>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.diagnostic-panel { margin: 15px 0; padding: 15px; border-radius: 6px; border-left: 4px solid #007bff; }
.success { background-color: #d4edda; border-color: #28a745; color: #155724; }
.warning { background-color: #fff3cd; border-color: #ffc107; color: #856404; }
.error { background-color: #f8d7da; border-color: #dc3545; color: #721c24; }
.info { background-color: #d1ecf1; border-color: #17a2b8; color: #0c5460; }
.test-btn { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }
.test-btn:hover { background: #0056b3; }
.test-btn:disabled { background: #6c757d; cursor: not-allowed; }
pre { background: #f8f9fa; padding: 10px; border-radius: 4px; overflow-x: auto; }
</style>
</head>
<body>
<div id="app">
<h1>宇之然内容创作平台 - 页面渲染诊断</h1>
<!-- 诊断控制面板 -->
<div class="diagnostic-panel info">
<h3>📋 诊断控制</h3>
<button @click="runBasicTest" :disabled="testing" class="test-btn">🔍 基础功能测试</button>
<button @click="runRenderTest" :disabled="testing" class="test-btn">🎨 渲染能力测试</button>
<button @click="runVueTest" :disabled="testing" class="test-btn">⚡ Vue核心测试</button>
<button @click="resetDiagnostic" class="test-btn">🔄 重置诊断</button>
<p v-if="testing">正在运行测试中...</p>
</div>
<!-- 实时输出 -->
<div class="diagnostic-panel" :class="{'success': output.length > 0 && lastResult === 'success', 'error': output.length > 0 && lastResult === 'error'}">
<h3>📊 实时输出</h3>
<div v-for="line in output" :key="line" style="margin: 5px 0;">{{ line }}</div>
</div>
<!-- 详细结果 -->
<div class="diagnostic-panel success" v-if="results.length > 0">
<h3>✅ 测试结果</h3>
<ul>
<li v-for="result in results" :key="result.id">{{ result.message }}</li>
</ul>
</div>
<!-- 问题分析 -->
<div class="diagnostic-panel warning" v-if="issues.length > 0">
<h3>⚠️ 发现的问题</h3>
<ul>
<li v-for="issue in issues" :key="issue">{{ issue }}</li>
</ul>
</div>
<!-- DOM结构检查 -->
<div class="diagnostic-panel info">
<h3>🏗️ DOM结构检查</h3>
<button @click="checkDOMStructure" class="test-btn">检查DOM结构</button>
<pre v-if="domInfo">{{ domInfo }}</pre>
</div>
<!-- 资源加载检查 -->
<div class="diagnostic-panel info">
<h3>🌐 资源加载检查</h3>
<button @click="checkResourceLoading" class="test-btn">检查资源加载</button>
<pre v-if="resourceInfo">{{ resourceInfo }}</pre>
</div>
<!-- 网络状态检查 -->
<div class="diagnostic-panel info">
<h3>📡 网络状态</h3>
<button @click="checkNetworkStatus" class="test-btn">检查网络状态</button>
<p v-if="networkInfo">{{ networkInfo }}</p>
</div>
<!-- 建议操作 -->
<div class="diagnostic-panel success">
<h3>💡 建议操作</h3>
<ol>
<li v-for="suggestion in suggestions" :key="suggestion">{{ suggestion }}</li>
</ol>
</div>
</div>
<script>
const RenderApp = {
data() {
return {
testing: false,
output: [
'页面渲染诊断工具已启动',
'请运行测试查看具体问题',
''
],
results: [],
issues: [],
lastResult: null,
domInfo: '',
resourceInfo: '',
networkInfo: '',
suggestions: []
}
},
methods: {
addOutput(message, type = 'info') {
this.output.push('[' + new Date().toLocaleTimeString() + '] ' + message);
if (type === 'success') this.lastResult = 'success';
if (type === 'error') this.lastResult = 'error';
},
runBasicTest() {
this.testing = true;
this.addOutput('开始基础功能测试...');
setTimeout(() => {
try {
// 测试基本DOM操作
const appElement = document.getElementById('app');
if (!appElement) {
throw new Error('找不到#app元素');
}
this.addOutput('✅ DOM元素检查通过', 'success');
this.results.push({ id: 'dom-element', message: 'DOM元素存在且可访问' });
// 测试Vue实例
if (window.Vue) {
this.addOutput('✅ Vue 3库已加载', 'success');
this.results.push({ id: 'vue-library', message: 'Vue 3库正确加载' });
} else {
throw new Error('Vue 3库未加载');
}
// 测试响应式数据
this.addOutput('✅ 响应式数据绑定正常', 'success');
this.results.push({ id: 'reactive-data', message: 'Vue响应式系统正常工作' });
this.testing = false;
} catch (error) {
this.addOutput('❌ 基础测试失败: ' + error.message, 'error');
this.issues.push('基础功能异常: ' + error.message);
this.testing = false;
}
}, 500);
},
runRenderTest() {
this.testing = true;
this.addOutput('开始渲染能力测试...');
setTimeout(() => {
try {
// 检查CSS样式
const styleElements = document.querySelectorAll('style, link[rel="stylesheet"]');
this.addOutput('✅ 发现 ' + styleElements.length + ' 个样式元素', 'success');
// 检查Tailwind
if (document.querySelector('script[src*="tailwindcss"]')) {
this.addOutput('✅ Tailwind CSS已加载', 'success');
this.results.push({ id: 'tailwind', message: 'Tailwind CSS样式框架正常' });
}
// 检查Vue渲染
this.addOutput('✅ Vue组件渲染测试通过', 'success');
this.results.push({ id: 'vue-render', message: 'Vue组件渲染功能正常' });
this.testing = false;
} catch (error) {
this.addOutput('❌ 渲染测试失败: ' + error.message, 'error');
this.issues.push('渲染功能异常: ' + error.message);
this.testing = false;
}
}, 500);
},
runVueTest() {
this.testing = true;
this.addOutput('开始Vue核心测试...');
setTimeout(() => {
try {
// 测试Vue应用实例
if (this.$data) {
this.addOutput('✅ Vue实例数据访问正常', 'success');
this.results.push({ id: 'vue-instance', message: 'Vue实例正确创建和挂载' });
}
// 测试事件处理
this.addOutput('✅ 事件处理器设置正常', 'success');
this.results.push({ id: 'event-handling', message: 'Vue事件监听器正常工作' });
// 测试计算属性
if (typeof this.countByStatus === 'function') {
this.addOutput('✅ 计算属性功能正常', 'success');
this.results.push({ id: 'computed-properties', message: 'Vue计算属性正常工作' });
}
this.testing = false;
} catch (error) {
this.addOutput('❌ Vue测试失败: ' + error.message, 'error');
this.issues.push('Vue功能异常: ' + error.message);
this.testing = false;
}
}, 500);
},
checkDOMStructure() {
this.addOutput('正在检查DOM结构...');
setTimeout(() => {
try {
const structure = {
'html标签': document.getElementsByTagName('html').length,
'head标签': document.getElementsByTagName('head').length,
'body标签': document.getElementsByTagName('body').length,
'#app元素': document.getElementById('app') ? '存在' : '不存在',
'Vue元素': document.querySelectorAll('[v-if], [v-for], [@click]').length,
'表格元素': document.querySelectorAll('table, th, td').length
};
this.domInfo = JSON.stringify(structure, null, 2);
this.addOutput('✅ DOM结构检查完成', 'success');
} catch (error) {
this.addOutput('❌ DOM检查失败: ' + error.message, 'error');
}
}, 200);
},
checkResourceLoading() {
this.addOutput('正在检查资源加载...');
setTimeout(() => {
try {
const resources = [];
// 检查脚本
document.querySelectorAll('script[src]').forEach(script => {
resources.push({
type: 'script',
src: script.src,
loaded: script.readyState === 'complete' || script.readyState === 'loaded'
});
});
// 检查样式表
document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
resources.push({
type: 'stylesheet',
href: link.href,
loaded: true // 简化处理
});
});
this.resourceInfo = JSON.stringify(resources.slice(0, 5), null, 2); // 只显示前5个
this.addOutput('✅ 资源加载检查完成', 'success');
} catch (error) {
this.addOutput('❌ 资源检查失败: ' + error.message, 'error');
}
}, 200);
},
checkNetworkStatus() {
this.addOutput('正在检查网络状态...');
setTimeout(() => {
try {
// 简化的网络状态检查
const status = {
online: navigator.onLine,
userAgent: navigator.userAgent,
connection: navigator.connection ? navigator.connection.effectiveType : 'unknown'
};
this.networkInfo = JSON.stringify(status, null, 2);
this.addOutput('✅ 网络状态检查完成', 'success');
} catch (error) {
this.addOutput('❌ 网络检查失败: ' + error.message, 'error');
}
}, 200);
},
resetDiagnostic() {
this.output = ['页面渲染诊断工具已启动', '请运行测试查看具体问题', ''];
this.results = [];
this.issues = [];
this.lastResult = null;
this.domInfo = '';
this.resourceInfo = '';
this.networkInfo = '';
this.suggestions = [];
this.addOutput('诊断已重置');
}
},
mounted() {
this.addOutput('Vue渲染诊断应用程序已启动');
console.log('Vue渲染诊断已初始化');
}
}
Vue.createApp(RenderApp).mount('#app')
</script>
</body>
</html>
-476
View File
@@ -1,476 +0,0 @@
<script>
const { ref, reactive, computed, onMounted, watch } = Vue;
const { ElMessage, ElNotification, ElMessageBox } = ElementPlus;
// 图标组件
const CopyDocument = Vue.h('el-icon', { name: 'CopyDocument' });
const FullScreen = Vue.h('el-icon', { name: 'FullScreen' });
const Document = Vue.h('el-icon', { name: 'Document' });
const Upload = Vue.h('el-icon', { name: 'Upload' });
const Promotion = Vue.h('el-icon', { name: 'Promotion' });
const app = Vue.createApp({
name: 'YuZhiRanPlatform',
setup() {
// ========== 变量声明区 ==========
const API_BASE = window.location.origin;
// 状态
const isLoggedIn = ref(false);
const isAdmin = ref(false);
const loginForm = reactive({ username: '', password: '' });
const loginError = ref('');
const status = ref({});
const topics = ref([]);
const selectedTopicIds = ref([]); // 批量操作选中
const filterStatus = ref('');
const generating = ref(false);
const optimizing = ref(false);
const loadingAll = ref(false);
const loadingTable = ref(false);
const loadingLogs = ref(false);
const loadingOverlay = ref(false);
const loadingText = ref('');
const pipeline = ref({ status_distribution: {} });
const pipelineLoading = ref(false);
const pipelineModules = ref([]);
const previewVisible = ref(false);
const previewTopic = ref({ title: '' });
const previewPlatform = ref('zhihu');
const previewHtml = ref('');
const fullScreenPreview = ref(false);
const showLogs = ref(false);
const logType = ref('creator');
const logDate = ref(new Date().toISOString().split('T')[0]);
const logContent = ref('');
// 计算属性
const filteredTopics = computed(() => {
if (!filterStatus.value) return topics.value || [];
return (topics.value || []).filter(t => t && t.status === filterStatus.value);
});
// ========== 工具函数 ==========
const formatDate = (val) => {
if (!val) return '-';
const d = new Date(val);
if (isNaN(d.getTime())) return val;
return d.toLocaleString('zh-CN', { hour12: false });
};
const formatRelativeTime = (val) => {
if (!val) return '-';
const d = new Date(val);
if (isNaN(d.getTime())) return '-';
const now = new Date();
const diff = now - d;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}天前`;
return formatDate(val);
};
// ========== 业务方法 ==========
const countByStatus = (status) => {
return (topics.value || []).filter(t => t.status === status).length;
};
const getPriorityType = (score) => {
if (!score) return '';
if (score >= 20) return 'danger';
if (score >= 15) return 'warning';
return 'success';
};
const getStatusClass = (status) => {
const map = {
'待处理': 'pending',
'待审查': 'review',
'待发布': 'ready',
'已发布': 'published'
};
return map[status] || '';
};
const refresh = async () => {
try {
const [s, t] = await Promise.all([
fetch(API_BASE + '/api/system/status').then(r => r.json()),
fetch(API_BASE + '/api/topics').then(r => r.json())
]);
status.value = s;
topics.value = t;
} catch (e) {
ElMessage.error('刷新失败:' + e.message);
}
};
const refreshPipeline = async () => {
pipelineLoading.value = true;
try {
const res = await fetch(API_BASE + '/api/system/pipeline/status');
if (res.ok) {
const data = await res.json();
pipeline.value = data;
pipelineModules.value = Object.entries(data.pipeline_modules || {}).map(([name, info]) => ({
module: name,
last_run: info.last_run || '未运行',
status_ok: !info.has_error && info.exists,
status_text: info.exists && !info.has_error ? '正常' : info.exists ? '有错误' : '缺失',
error: info.has_error ? '检测到错误' : ''
}));
}
} catch (e) {
ElMessage.error('获取流水线状态失败');
} finally {
pipelineLoading.value = false;
}
};
const refreshAll = async () => {
loadingAll.value = true;
try {
await Promise.all([refresh(), refreshPipeline()]);
ElMessage.success('刷新成功');
} catch (e) {
ElMessage.error('刷新失败');
} finally {
loadingAll.value = false;
}
};
const triggerGenerate = async () => {
generating.value = true;
try {
const res = await fetch(API_BASE + '/api/system/generate/run', { method: 'POST' });
const data = await res.json();
if (data.result && data.result.ok) {
ElMessage.success('创作任务已启动');
setTimeout(refresh, 3000);
} else {
ElMessage.error('启动失败:' + (data.error || '未知错误'));
}
} catch (e) {
ElMessage.error('请求失败:' + e.message);
} finally {
generating.value = false;
}
};
const triggerOptimize = async () => {
optimizing.value = true;
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', { method: 'POST' });
const data = await res.json();
if (data.summary) {
ElNotification({
title: '优化完成',
message: `自动通过 ${data.summary.passed_auto || 0} 篇,需人工 ${data.summary.need_manual || 0}`,
type: 'success'
});
await refresh();
} else {
ElMessage.success('优化完成');
}
} catch (e) {
ElMessage.error('优化失败:' + e.message);
} finally {
optimizing.value = false;
}
};
const openPreview = async (topic) => {
previewTopic.value = { id: topic.id, title: topic.title };
previewPlatform.value = 'zhihu';
previewVisible.value = true;
await loadPreview();
};
const loadPreview = async () => {
previewHtml.value = '';
console.log('[Preview] Loading topic:', previewTopic.value.id, 'platform:', previewPlatform.value);
try {
const res = await fetch(`${API_BASE}/api/articles/${previewTopic.value.id}/preview?platform=${previewPlatform.value}`);
console.log('[Preview] Response status:', res.status);
if (res.ok) {
const data = await res.json();
console.log('[Preview] Got HTML, length:', data.html?.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data.html, 'text/html');
const contentDiv = doc.querySelector('.content');
console.log('[Preview] Found .content:', !!contentDiv);
if (contentDiv) {
previewHtml.value = contentDiv.innerHTML;
console.log('[Preview] Set previewHtml from .content');
} else {
const header = doc.querySelector('.header');
const footer = doc.querySelector('footer');
const tags = doc.querySelector('.tags');
const interaction = doc.querySelector('.interaction');
if (header) header.remove();
if (footer) footer.remove();
if (tags) tags.remove();
if (interaction) interaction.remove();
previewHtml.value = doc.body.innerHTML;
console.log('[Preview] Set previewHtml from body.innerHTML');
}
} else if (res.status === 404) {
previewHtml.value = '<div class="text-center py-12 text-gray-500"><p>暂未创作文章,请先点击创作按钮生成</p></div>';
} else {
ElMessage.error('加载预览失败:' + res.status);
}
} catch (e) {
previewHtml.value = '<div class="text-center py-12 text-gray-500"><p>请求失败,请检查后端服务是否运行</p></div>';
console.error('Preview error:', e);
}
};
const copyPreviewHtml = async () => {
if (!previewHtml.value) return;
try {
const parser = new DOMParser();
const doc = parser.parseFromString(previewHtml.value, 'text/html');
const header = doc.querySelector('.header');
if (header) header.remove();
const footer = doc.querySelector('footer');
if (footer) footer.remove();
const tagsDiv = doc.querySelector('.tags');
if (tagsDiv) tagsDiv.remove();
const interaction = doc.querySelector('.interaction');
if (interaction) interaction.remove();
const contentDiv = doc.querySelector('.content');
let text = '';
if (contentDiv) {
text = contentDiv.innerText.trim();
} else {
text = doc.body.innerText.trim();
}
if (!text) {
ElMessage.warning('未提取到正文内容');
return;
}
await navigator.clipboard.writeText(text);
ElMessage.success('正文已复制到剪贴板');
} catch (e) {
console.error('Copy error:', e);
ElMessage.error('复制失败');
}
};
const expandPreview = () => {
fullScreenPreview.value = true;
};
const handleShowLogs = () => {
showLogs.value = true;
};
const fetchLogs = async () => {
loadingLogs.value = true;
try {
const res = await fetch(`${API_BASE}/api/system/logs/${logDate.value}?log_type=${logType.value}`);
if (res.ok) {
const data = await res.json();
logContent.value = data.content ? data.content.join('\n') : '无内容';
} else {
ElMessage.error('加载日志失败');
}
} catch (e) {
ElMessage.error('请求失败');
} finally {
loadingLogs.value = false;
}
};
const createTopic = async (topic) => {
if (topic.published_urls && Object.keys(topic.published_urls).length > 0) {
try {
await ElMessageBox.alert(
'本文已发布过,重新创作将覆盖原有内容。是否继续?',
'重新创作确认',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning',
}
);
} catch (e) {
return;
}
}
try {
const res = await fetch(`${API_BASE}/api/system/generate/run?topic_id=${topic.id}`, { method: 'POST' });
const data = await res.json();
if (data.result && data.result.ok) {
ElMessage.success(`选题 ${topic.id} 创作任务已启动`);
setTimeout(refresh, 3000);
} else {
ElMessage.error('创作失败:' + (data.error || '未知错误'));
}
} catch (e) {
ElMessage.error('请求失败:' + e.message);
}
};
const optimizeTopic = async (topic) => {
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_ids: [topic.id] })
});
const data = await res.json();
if (data.summary) {
ElMessage.success(`选题 ${topic.id} 优化完成`);
setTimeout(refresh, 2000);
} else {
ElMessage.success('优化完成');
}
} catch (e) {
ElMessage.error('优化失败:' + e.message);
}
};
// 批量操作
const triggerGenerateSelected = async () => {
if (selectedTopicIds.value.length === 0) {
ElMessage.warning('请先选择要创作的选题');
return;
}
generating.value = true;
try {
const res = await fetch(API_BASE + '/api/system/generate/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_ids: selectedTopicIds.value })
});
const data = await res.json();
if (data.result && data.result.ok) {
ElMessage.success(`已启动 ${selectedTopicIds.value.length} 个选题的创作任务`);
selectedTopicIds.value = [];
setTimeout(refresh, 3000);
} else {
ElMessage.error('批量创作失败:' + (data.error || '未知错误'));
}
} catch (e) {
ElMessage.error('请求失败:' + e.message);
} finally {
generating.value = false;
}
};
const triggerOptimizeSelected = async () => {
if (selectedTopicIds.value.length === 0) {
ElMessage.warning('请先选择要优化的选题');
return;
}
optimizing.value = true;
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_ids: selectedTopicIds.value })
});
const data = await res.json();
if (data.summary) {
ElNotification({
title: '批量优化完成',
message: `自动通过 ${data.summary.passed_auto || 0} 篇,需人工 ${data.summary.need_manual || 0}`,
type: 'success'
});
selectedTopicIds.value = [];
await refresh();
} else {
ElMessage.success('批量优化完成');
}
} catch (e) {
ElMessage.error('批量优化失败:' + e.message);
} finally {
optimizing.value = false;
}
};
const handlePublish = async (topic) => {
try {
ElMessage.info(`正在发布选题 ${topic.id}...`);
const res = await fetch(API_BASE + '/api/publishing/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_id: topic.id })
});
if (!res.ok) throw new Error('发布失败');
const data = await res.json();
ElMessage.success(`选题 ${topic.id} 已发布`);
await refresh();
} catch (e) {
ElMessage.error('发布失败:' + e.message);
}
};
const openCreateTopic = () => {
ElMessage.info('新建选题功能待实现');
};
// 页面路由
const currentPage = ref('overview');
const switchPage = (page) => {
currentPage.value = page;
};
const goToTopicsWithFilter = (status) => {
currentPage.value = 'topics';
filterStatus.value = status;
};
// 生命周期
watch(previewPlatform, loadPreview);
onMounted(() => {
const authToken = localStorage.getItem('auth_token');
const role = localStorage.getItem('user_role');
if (authToken) {
isLoggedIn.value = true;
if (role === 'admin') isAdmin.value = true;
}
refresh();
refreshPipeline();
});
// 返回给模板
return {
// 状态
status, topics, filterStatus, filteredTopics,
generating, optimizing, loadingAll, loadingTable, loadingLogs, loadingOverlay, loadingText,
pipeline, pipelineLoading, pipelineModules,
previewVisible, previewTopic, previewPlatform, previewHtml, fullScreenPreview,
showLogs, logType, logDate, logContent,
// 页面路由
currentPage,
// 方法
countByStatus, getPriorityType, getStatusClass,
refresh, refreshPipeline, refreshAll,
triggerGenerate, triggerOptimize,
openPreview, loadPreview, copyPreviewHtml, expandPreview,
fetchLogs,
createTopic, optimizeTopic, handlePublish,
openCreateTopic,
// 工具函数
formatDate, formatRelativeTime,
switchPage, goToTopicsWithFilter,
// 认证(未完整)
isLoggedIn, isAdmin, loginForm, loginError,
// 图标
Document, Upload, CopyDocument, FullScreen, Promotion
};
}
});
app.use(ElementPlus);
app.mount('#app');
</script>
-398
View File
@@ -1,398 +0,0 @@
// 修复后的 Vue 3 setup 函数体
// 所有变量和方法必须在 return 之前定义
const API_BASE = window.location.origin;
// 1. 状态变量
const isLoggedIn = ref(false);
const isAdmin = ref(false);
const loginForm = reactive({ username: '', password: '' });
const loginError = ref('');
const status = ref({});
const topics = ref([]);
const filterStatus = ref('');
const generating = ref(false);
const optimizing = ref(false);
const loadingAll = ref(false);
const loadingTable = ref(false);
const loadingLogs = ref(false);
const loadingOverlay = ref(false);
const loadingText = ref('');
const pipeline = ref({ status_distribution: {} });
const pipelineLoading = ref(false);
const pipelineModules = ref([]);
const previewVisible = ref(false);
const previewTopic = ref({ title: '' });
const previewPlatform = ref('zhihu');
const previewHtml = ref('');
const fullScreenPreview = ref(false);
const showLogs = ref(false);
const logType = ref('creator');
const logDate = ref(new Date().toISOString().split('T')[0]);
const logContent = ref('');
// 2. 计算属性
const filteredTopics = computed(() => {
if (!filterStatus.value) return topics.value || [];
return (topics.value || []).filter(t => t && t.status === filterStatus.value);
});
// 3. 工具函数
const formatDate = (val) => {
if (!val) return '-';
const d = new Date(val);
if (isNaN(d.getTime())) return val;
return d.toLocaleString('zh-CN', { hour12: false });
};
const formatRelativeTime = (val) => {
if (!val) return '-';
const d = new Date(val);
if (isNaN(d.getTime())) return '-';
const now = new Date();
const diff = now - d;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}天前`;
return formatDate(val);
};
// 4. 业务方法
const countByStatus = (status) => {
return (topics.value || []).filter(t => t.status === status).length;
};
const getPriorityType = (score) => {
if (!score) return '';
if (score >= 20) return 'danger';
if (score >= 15) return 'warning';
return 'success';
};
const getStatusClass = (status) => {
const map = {
'待处理': 'pending',
'待审查': 'review',
'待发布': 'ready',
'已发布': 'published'
};
return map[status] || '';
};
const refresh = async () => {
try {
const [s, t] = await Promise.all([
fetch(API_BASE + '/api/system/status').then(r => r.json()),
fetch(API_BASE + '/api/topics').then(r => r.json())
]);
status.value = s;
topics.value = t;
} catch (e) {
ElMessage.error('刷新失败:' + e.message);
}
};
const refreshPipeline = async () => {
pipelineLoading.value = true;
try {
const res = await fetch(API_BASE + '/api/system/pipeline/status');
if (res.ok) {
const data = await res.json();
pipeline.value = data;
pipelineModules.value = Object.entries(data.pipeline_modules || {}).map(([name, info]) => ({
module: name,
last_run: info.last_run || '未运行',
status_ok: !info.has_error && info.exists,
status_text: info.exists && !info.has_error ? '正常' : info.exists ? '有错误' : '缺失',
error: info.has_error ? '检测到错误' : ''
}));
}
} catch (e) {
ElMessage.error('获取流水线状态失败');
} finally {
pipelineLoading.value = false;
}
};
const refreshAll = async () => {
loadingAll.value = true;
try {
await Promise.all([refresh(), refreshPipeline()]);
ElMessage.success('刷新成功');
} catch (e) {
ElMessage.error('刷新失败');
} finally {
loadingAll.value = false;
}
};
const triggerGenerate = async () => {
generating.value = true;
try {
const res = await fetch(API_BASE + '/api/system/generate/run', { method: 'POST' });
const data = await res.json();
if (data.result && data.result.ok) {
ElMessage.success('创作任务已启动');
setTimeout(refresh, 3000);
} else {
ElMessage.error('启动失败:' + (data.error || '未知错误'));
}
} catch (e) {
ElMessage.error('请求失败:' + e.message);
} finally {
generating.value = false;
}
};
const triggerOptimize = async () => {
optimizing.value = true;
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', { method: 'POST' });
const data = await res.json();
if (data.summary) {
ElNotification({
title: '优化完成',
message: `自动通过 ${data.summary.passed_auto || 0} 篇,需人工 ${data.summary.need_manual || 0}`,
type: 'success'
});
await refresh();
} else {
ElMessage.success('优化完成');
}
} catch (e) {
ElMessage.error('优化失败:' + e.message);
} finally {
optimizing.value = false;
}
};
const openPreview = async (topic) => {
previewTopic.value = { id: topic.id, title: topic.title };
previewPlatform.value = 'zhihu';
previewVisible.value = true;
await loadPreview();
};
const loadPreview = async () => {
previewHtml.value = '';
console.log('[Preview] Loading topic:', previewTopic.value.id, 'platform:', previewPlatform.value);
try {
const res = await fetch(`${API_BASE}/api/articles/${previewTopic.value.id}/preview?platform=${previewPlatform.value}`);
console.log('[Preview] Response status:', res.status);
if (res.ok) {
const data = await res.json();
console.log('[Preview] Got HTML, length:', data.html?.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data.html, 'text/html');
const contentDiv = doc.querySelector('.content');
console.log('[Preview] Found .content:', !!contentDiv);
if (contentDiv) {
previewHtml.value = contentDiv.innerHTML;
console.log('[Preview] Set previewHtml from .content');
} else {
const header = doc.querySelector('.header');
const footer = doc.querySelector('footer');
const tags = doc.querySelector('.tags');
const interaction = doc.querySelector('.interaction');
if (header) header.remove();
if (footer) footer.remove();
if (tags) tags.remove();
if (interaction) interaction.remove();
previewHtml.value = doc.body.innerHTML;
console.log('[Preview] Set previewHtml from body.innerHTML');
}
} else if (res.status === 404) {
previewHtml.value = '<div class="text-center py-12 text-gray-500"><p>暂未创作文章,请先点击创作按钮生成</p></div>';
} else {
ElMessage.error('加载预览失败:' + res.status);
}
} catch (e) {
previewHtml.value = '<div class="text-center py-12 text-gray-500"><p>请求失败,请检查后端服务是否运行</p></div>';
console.error('Preview error:', e);
}
};
const copyPreviewHtml = async () => {
if (!previewHtml.value) return;
try {
const parser = new DOMParser();
const doc = parser.parseFromString(previewHtml.value, 'text/html');
const header = doc.querySelector('.header');
if (header) header.remove();
const footer = doc.querySelector('footer');
if (footer) footer.remove();
const tagsDiv = doc.querySelector('.tags');
if (tagsDiv) tagsDiv.remove();
const interaction = doc.querySelector('.interaction');
if (interaction) interaction.remove();
const contentDiv = doc.querySelector('.content');
let text = '';
if (contentDiv) {
text = contentDiv.innerText.trim();
} else {
text = doc.body.innerText.trim();
}
if (!text) {
ElMessage.warning('未提取到正文内容');
return;
}
await navigator.clipboard.writeText(text);
ElMessage.success('正文已复制到剪贴板');
} catch (e) {
console.error('Copy error:', e);
ElMessage.error('复制失败');
}
};
const expandPreview = () => {
fullScreenPreview.value = true;
};
const handleShowLogs = () => {
showLogs.value = true;
};
const fetchLogs = async () => {
loadingLogs.value = true;
try {
const res = await fetch(`${API_BASE}/api/system/logs/${logDate.value}?log_type=${logType.value}`);
if (res.ok) {
const data = await res.json();
logContent.value = data.content ? data.content.join('\n') : '无内容';
} else {
ElMessage.error('加载日志失败');
}
} catch (e) {
ElMessage.error('请求失败');
} finally {
loadingLogs.value = false;
}
};
const createTopic = async (topic) => {
if (topic.published_urls && Object.keys(topic.published_urls).length > 0) {
try {
await ElMessageBox.alert(
'本文已发布过,重新创作将覆盖原有内容。是否继续?',
'重新创作确认',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning',
}
);
} catch (e) {
return;
}
}
try {
const res = await fetch(`${API_BASE}/api/system/generate/run?topic_id=${topic.id}`, { method: 'POST' });
const data = await res.json();
if (data.result && data.result.ok) {
ElMessage.success(`选题 ${topic.id} 创作任务已启动`);
setTimeout(refresh, 3000);
} else {
ElMessage.error('创作失败:' + (data.error || '未知错误'));
}
} catch (e) {
ElMessage.error('请求失败:' + e.message);
}
};
const optimizeTopic = async (topic) => {
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_ids: [topic.id] })
});
const data = await res.json();
if (data.summary) {
ElMessage.success(`选题 ${topic.id} 优化完成`);
setTimeout(refresh, 2000);
} else {
ElMessage.success('优化完成');
}
} catch (e) {
ElMessage.error('优化失败:' + e.message);
}
};
const handlePublish = async (topic) => {
try {
ElMessage.info(`正在发布选题 ${topic.id}...`);
const res = await fetch(API_BASE + '/api/publishing/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic_id: topic.id })
});
if (!res.ok) throw new Error('发布失败');
const data = await res.json();
ElMessage.success(`选题 ${topic.id} 已发布`);
await refresh();
} catch (e) {
ElMessage.error('发布失败:' + e.message);
}
};
const openCreateTopic = () => {
ElMessage.info('新建选题功能待实现');
};
// 5. 页面路由
const currentPage = ref('overview');
const switchPage = (page) => {
currentPage.value = page;
};
const goToTopicsWithFilter = (status) => {
currentPage.value = 'topics';
filterStatus.value = status;
};
// 6. 生命周期(必须在 return 之前)
watch(previewPlatform, loadPreview);
onMounted(() => {
const authToken = localStorage.getItem('auth_token');
const role = localStorage.getItem('user_role');
if (authToken) {
isLoggedIn.value = true;
if (role === 'admin') isAdmin.value = true;
}
refresh();
refreshPipeline();
});
// 7. 返回给模板
return {
// 状态
status, topics, filterStatus, filteredTopics,
generating, optimizing, loadingAll, loadingTable, loadingLogs, loadingOverlay, loadingText,
pipeline, pipelineLoading, pipelineModules,
previewVisible, previewTopic, previewPlatform, previewHtml, fullScreenPreview,
showLogs, logType, logDate, logContent,
// 页面路由
currentPage,
// 方法
countByStatus, getPriorityType, getStatusClass,
refresh, refreshPipeline, refreshAll,
triggerGenerate, triggerOptimize,
openPreview, loadPreview, copyPreviewHtml, expandPreview,
fetchLogs,
createTopic, optimizeTopic, handlePublish,
openCreateTopic,
// 工具函数
formatDate, formatRelativeTime,
switchPage, goToTopicsWithFilter,
// 认证(未完整)
isLoggedIn, isAdmin, loginForm, loginError,
// 图标
Document, Upload, CopyDocument, FullScreen, Promotion
};
-96
View File
@@ -1,96 +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>Vue最简单测试</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.test-result { margin: 10px 0; padding: 10px; border-radius: 4px; }
.success { background-color: #d4edda; color: #155724; }
.error { background-color: #f8d7da; color: #721c24; }
.info { background-color: #d1ecf1; color: #0c5460; }
</style>
</head>
<body>
<div id="app">
<h1>{{ title }}</h1>
<!-- 基础功能测试 -->
<div class="test-result info">
<strong>基础测试:</strong>
<p>当前计数: {{ count }}</p>
<button @click="count++">增加计数</button>
</div>
<!-- Vue初始化状态 -->
<div class="test-result" :class="{'success': vueReady, 'error': !vueReady}">
<strong>Vue状态:</strong>
<p v-if="vueReady">✅ Vue已就绪</p>
<p v-if="!vueReady">❌ Vue未就绪</p>
</div>
<!-- 调试信息 -->
<div class="test-result info">
<strong>调试信息:</strong>
<ul>
<li v-for="log in debugLogs" :key="log">{{ log }}</li>
</ul>
</div>
</div>
<script>
const SimpleApp = {
data() {
return {
title: "Vue最简测试",
count: 0,
vueReady: false,
debugLogs: []
}
},
methods: {
addLog(message) {
this.debugLogs.push('[' + new Date().toLocaleTimeString() + '] ' + message);
}
},
mounted() {
this.addLog('Vue应用已启动');
// 检查Vue是否正确初始化
try {
console.log('Vue实例:', this);
console.log('数据对象:', this.$data);
// 测试基本响应式
setTimeout(() => {
this.vueReady = true;
this.addLog('✅ Vue响应式系统正常工作');
// 测试事件处理
this.addLog('✅ 事件监听器已设置');
// 测试数据绑定
this.addLog('✅ 文本插值正常工作');
}, 100);
} catch (error) {
this.vueReady = false;
this.addLog('❌ Vue初始化失败: ' + error.message);
}
}
}
try {
Vue.createApp(SimpleApp).mount('#app');
console.log('Vue应用程序已成功创建和挂载');
} catch (error) {
console.error('Vue应用程序创建失败:', error);
}
</script>
</body>
</html>
+1 -1
View File
@@ -1,5 +1,5 @@
// Service Worker for 宇之然内容创作平台
const CACHE_NAME = 'yuzhiran-v1';
const CACHE_NAME = 'yuzhiran-v2';
const CACHE_URLS = [
'/',
'/index.html',
+98 -37
View File
@@ -5,19 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 创作任务</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.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; min-height: calc(100vh - 60px); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.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; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
@@ -44,12 +40,28 @@
.error-box { margin-top: 8px; padding: 8px 12px; background: #fef0f0; border-radius: 6px; font-size: 13px; color: #f56c6c; border-left: 3px solid #f56c6c; }
.task-card-list-mobile { display: none; }
.schedule-row { display: flex; align-items: center; gap: 16px; padding: 14px 16px; border-radius: 10px; background: #f8faff; border: 1px solid #e8edf5; margin-bottom: 8px; transition: all 0.2s; }
.schedule-row:hover { background: #f0f4ff; }
.schedule-icon { font-size: 22px; width: 36px; text-align: center; flex-shrink: 0; }
.schedule-info { flex: 1; min-width: 0; }
.schedule-name { font-size: 14px; font-weight: 600; color: #303133; }
.schedule-time { font-size: 12px; color: #909399; margin-top: 2px; }
.schedule-next { font-size: 12px; color: #409eff; white-space: nowrap; }
.schedule-badge { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.schedule-badge.active { background: #67c23a; }
.schedule-badge.inactive { background: #c0c4cc; }
.detail-row { display: flex; padding: 10px 0; border-bottom: 1px solid #f0f2f5; }
.detail-row:last-child { border-bottom: none; }
.detail-label { width: 80px; flex-shrink: 0; font-size: 13px; color: #909399; }
.detail-value { flex: 1; font-size: 13px; color: #303133; word-break: break-all; }
@media (max-width: 768px) {
.content-area { padding: 16px; padding-bottom: 80px; }
.card { padding: 16px; }
.task-table { display: none; }
.task-card-list-mobile { display: block; }
.schedule-row { padding: 10px 12px; gap: 10px; flex-wrap: wrap; }
.schedule-next { width: 100%; margin-left: 46px; }
}
</style>
<script src="navigation-component.js"></script>
@@ -61,7 +73,32 @@
<navigation-component current-page="tasks" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">🚀 定时任务</h2>
<div class="toolbar">
<el-button @click="loadSchedulerStatus">🔄 刷新</el-button>
</div>
</div>
<div v-if="schedulerLoading" style="text-align: center; padding: 20px; color: #909399;">加载中...</div>
<div v-else>
<div v-if="schedulerJobs.length === 0" style="text-align: center; padding: 30px 20px; color: #909399; font-size: 14px;">暂无定时任务</div>
<div v-for="job in schedulerJobs" :key="job.id" class="schedule-row">
<div class="schedule-icon">{{ job.icon }}</div>
<div class="schedule-info">
<div class="schedule-name">{{ job.name }}</div>
<div class="schedule-time">⏰ 每日 {{ job.time }}</div>
</div>
<div class="schedule-next">
<span class="schedule-badge" :class="job.active ? 'active' : 'inactive'"></span>
<span>{{ job.active ? '运行中' : '等待中' }}</span>
<span v-if="job.next_run" style="margin-left: 8px; color: #909399;">下次: {{ job.next_run }}</span>
</div>
</div>
</div>
</div>
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">🚀 创作任务</h2>
<div class="toolbar">
@@ -114,27 +151,23 @@
</div>
</main>
</div>
<el-dialog v-model="showDetailDialog" title="任务详情" width="600px">
<el-dialog v-model="showDetailDialog" title="任务详情" width="600px" class="task-detail-dialog">
<div v-if="detailTask">
<el-descriptions :column="2" border>
<el-descriptions-item label="任务ID">{{ detailTask.task_id }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ getStatusLabel(detailTask.status) }}</el-descriptions-item>
<el-descriptions-item label="阶段">{{ getStageLabel(detailTask.stage) }}</el-descriptions-item>
<el-descriptions-item label="进度">{{ detailTask.progress }}%</el-descriptions-item>
<el-descriptions-item label="选题ID" :span="2">{{ detailTask.topic_id || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(detailTask.created_at) }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ formatDate(detailTask.started_at) }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ formatDate(detailTask.finished_at) }}</el-descriptions-item>
<el-descriptions-item label="耗时">{{ detailTask.duration ? detailTask.duration + '秒' : '-' }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ detailTask.created_by }}</el-descriptions-item>
<el-descriptions-item label="消息" :span="2">{{ detailTask.message || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="detailTask.error_msg" label="错误信息" :span="2">
<span style="color: #f56c6c;">{{ detailTask.error_msg }}</span>
</el-descriptions-item>
</el-descriptions>
<div class="detail-row"><span class="detail-label">任务ID</span><span class="detail-value">{{ detailTask.task_id }}</span></div>
<div class="detail-row"><span class="detail-label">状态</span><span class="detail-value"><span class="status-badge" :class="'status-' + detailTask.status">{{ getStatusLabel(detailTask.status) }}</span></span></div>
<div class="detail-row"><span class="detail-label">阶段</span><span class="detail-value"><span class="task-stage" :class="'stage-' + detailTask.stage">{{ getStageLabel(detailTask.stage) }}</span></span></div>
<div class="detail-row"><span class="detail-label">进度</span><span class="detail-value">{{ detailTask.progress }}%</span></div>
<div class="detail-row"><span class="detail-label">选题ID</span><span class="detail-value">{{ detailTask.topic_id || '-' }}</span></div>
<div class="detail-row"><span class="detail-label">创建时间</span><span class="detail-value">{{ formatDate(detailTask.created_at) }}</span></div>
<div class="detail-row"><span class="detail-label">开始时间</span><span class="detail-value">{{ formatDate(detailTask.started_at) }}</span></div>
<div class="detail-row"><span class="detail-label">完成时间</span><span class="detail-value">{{ formatDate(detailTask.finished_at) }}</span></div>
<div class="detail-row"><span class="detail-label">耗时</span><span class="detail-value">{{ detailTask.duration ? detailTask.duration + '秒' : '-' }}</span></div>
<div class="detail-row"><span class="detail-label">创建人</span><span class="detail-value">{{ detailTask.created_by }}</span></div>
<div class="detail-row"><span class="detail-label">消息</span><span class="detail-value">{{ detailTask.message || '-' }}</span></div>
<div v-if="detailTask.error_msg" class="detail-row"><span class="detail-label">错误信息</span><span class="detail-value" style="color: #f56c6c;">{{ detailTask.error_msg }}</span></div>
<div v-if="detailTask.result_data" style="margin-top: 16px;">
<div style="font-weight: 600; margin-bottom: 8px;">结果数据:</div>
<pre style="background: #f5f7fa; padding: 12px; border-radius: 8px; overflow: auto; max-height: 300px; font-size: 13px;">{{ JSON.stringify(detailTask.result_data, null, 2) }}</pre>
<div style="font-weight: 600; margin-bottom: 8px; font-size: 13px;">结果数据:</div>
<pre style="background: #f5f7fa; padding: 12px; border-radius: 8px; overflow: auto; max-height: 200px; font-size: 12px;">{{ JSON.stringify(detailTask.result_data, null, 2) }}</pre>
</div>
</div>
</el-dialog>
@@ -143,13 +176,20 @@
<script src="element-plus.full.js"></script>
<script>
const TasksApp = {
data() {
return {
currentUser: { username: '' }, isAdmin: false, isLoggedIn: false,
tasks: [], loading: false, filterStatus: '',
showDetailDialog: false, detailTask: null
}
},
data() {
const SCHEDULER_JOBS = {
'scheduled_sync': { icon: '🔄', name: '数据同步', defaultTime: '02:30' },
'scheduled_generate': { icon: '🤖', name: '内容创作', defaultTime: '03:30' },
'scheduled_optimize': { icon: '🔍', name: '合规审查', defaultTime: '04:30' },
};
return {
currentUser: { username: '' }, isAdmin: false, isLoggedIn: false,
tasks: [], loading: false, filterStatus: '',
showDetailDialog: false, detailTask: null,
schedulerJobs: [], schedulerLoading: false,
SCHEDULER_JOBS,
}
},
methods: {
getToken() { return localStorage.getItem('authToken'); },
async api(url, opts = {}) {
@@ -166,11 +206,32 @@ const TasksApp = {
if (!token) { window.location.href = '/login.html'; return; }
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; this.loadTasks(); })
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; this.loadTasks(); this.loadSchedulerStatus(); })
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
},
async loadSchedulerStatus() {
this.schedulerLoading = true;
try {
const data = await this.api('/api/system/scheduler/status');
if (!data) return;
this.schedulerJobs = (data.jobs || []).map(job => {
const info = this.SCHEDULER_JOBS[job.id] || { icon: '⏰', name: job.id, defaultTime: '' };
// Parse cron trigger for time display
let time = info.defaultTime;
const m = job.trigger && job.trigger.match(/hour='?(\d+)'?,\s*minute='?(\d+)'?/);
if (m) time = m[1].padStart(2,'0') + ':' + m[2].padStart(2,'0');
// Format next_run_time
let next_run = '';
if (job.next_run_time) {
try { next_run = new Date(job.next_run_time).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } catch(e) { next_run = job.next_run_time; }
}
return { id: job.id, icon: info.icon, name: info.name, time, active: data.running, next_run };
});
} catch (e) { console.error(e); }
finally { this.schedulerLoading = false; }
},
getStatusLabel(status) { return { 'pending': '等待中', 'running': '进行中', 'completed': '已完成', 'failed': '失败', 'cancelled': '已取消' }[status] || status; },
getStageLabel(stage) { return { 'creator': '创作', 'optimize': '优化', 'review': '审查', 'publish': '发布' }[stage] || stage; },
getStageLabel(stage) { return { 'creator': '创作', 'optimize': '审查', 'review': '审查', 'publish': '发布' }[stage] || stage; },
formatDate(dateStr) {
if (!dateStr) return '-';
try { return new Date(dateStr).toLocaleString('zh-CN'); } catch (e) { return dateStr; }
-90
View File
@@ -1,90 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>前端测试</title>
<script src="vue.global.prod.js"></script>
<link rel="stylesheet" href="element-plus.css" />
<!-- Tailwind CSS -->
<!-- Vue 3 -->
<!-- Element Plus CSS -->
<!-- Element Plus JS -->
<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>
<!-- 本地静态文件 -->
<script src=".vue.global.prod.js?v=20260427"></script>
<link rel="stylesheet" href=".element-plus.css?v=20260427">
<script src="element-plus.full.js?v=20260427"></script>
</head>
<body>
<div id="app">
<h1>测试页面</h1>
<p>Vue 已加载: {{ loaded }}</p>
<el-button type="primary">测试按钮</el-button>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const loaded = ref(true);
return { loaded };
}
}).use(ElementPlus).mount('#app');
</script>
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
/* Modern page fade-in */
.page-fade { animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
/* Enhanced cards */
.card-modern { border-radius: 16px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.card-modern:hover { box-shadow: 0 8px 28px rgba(0,0,0,0.1); }
/* Gradient text */
.text-gradient { background: linear-gradient(135deg, #2563eb, #7c3aed); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.text-gradient-primary { background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.text-gradient-success { background: linear-gradient(135deg, #67c23a, #85e61d); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.text-gradient-warning { background: linear-gradient(135deg, #e6a23c, #f5c543); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.text-gradient-danger { background: linear-gradient(135deg, #f56c6c, #f79296); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.text-gradient-info { background: linear-gradient(135deg, #409eff, #5cd0f3); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
/* Pulse animation */
@keyframes pulse-glow { 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); } }
/* Shimmer hover effect */
.shimmer { position: relative; overflow: hidden; }
.shimmer::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(37, 99, 235, 0.06), transparent); transition: left 0.6s; }
.shimmer:hover::before { left: 100%; }
/* Elastic hover lift */
.hover-lift { transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }
.hover-lift:hover { transform: translateY(-6px) scale(1.02); }
/* Modern stat card */
.stat-card-modern { border-radius: 16px; padding: 24px; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); background: white; border: 1px solid #ebeef5; }
.stat-card-modern:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); border-color: #c6d8ff; }
.stat-card-modern .stat-value { font-size: 32px; font-weight: 800; line-height: 1.2; }
.stat-card-modern .stat-label { font-size: 13px; color: #909399; margin-top: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
/* Status badge */
.status-badge-modern { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; }
.status-badge-modern.running { background: rgba(103, 194, 58, 0.12); color: #67c23a; border: 1px solid rgba(103, 194, 58, 0.25); animation: pulse-glow 2s infinite; }
/* Modern el-table refinements */
.el-table { border-radius: 12px; overflow: hidden; }
/* Smooth page transitions */
.page-enter-active { animation: fadeIn 0.4s ease-out; }
/* ========== H5响应式 ========== */
@media (max-width: 768px) {
/* 对话框自适应宽度 */
.el-dialog { width: calc(100vw - 24px) !important; max-width: none !important; }
.el-dialog__body { padding: 16px !important; }
/* 表单网格在手机上改为单列 */
.el-row .el-col-12 { width: 100% !important; }
/* 手机端表格卡片 */
.mobile-card { position: relative; }
.mobile-card:active { transform: scale(0.98); }
}
/* 触摸优化:增大按钮点击区域 */
@media (max-width: 768px) {
.el-button--small { min-height: 36px; padding: 8px 14px !important; }
.el-input__inner { min-height: 38px; }
.el-table__cell .el-button { padding: 8px 12px !important; }
}
+64 -38
View File
@@ -5,19 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 选题管理</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.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; min-height: calc(100vh - 60px); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.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; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
@@ -39,6 +33,8 @@
.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; }
.preview-dialog-custom.is-fullscreen { z-index: 100001 !important; }
body:has(.preview-dialog-custom.is-fullscreen) > [class*="el-overlay"] { z-index: 100000 !important; }
@media (max-width: 768px) {
.content-area { padding: 12px; padding-bottom: 80px; }
@@ -62,6 +58,7 @@
}
@media (min-width: 769px) {
.preview-iframe { max-height: calc(100vh - 100px) !important; }
.preview-dialog-custom { position: relative; left: 90px; }
}
</style>
<script src="navigation-component.js"></script>
@@ -73,18 +70,19 @@
<navigation-component current-page="topics" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">📋 选题管理</h2>
<div class="toolbar">
<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>
<el-button type="warning" size="small" @click="triggerReviewSelected" :disabled="selectedTopicIds.length === 0">🔍 批量审查</el-button>
<span v-if="selectedTopicIds.length > 0" class="selected-count">已选 {{ selectedTopicIds.length }} 项</span>
</div>
</div>
<div class="filter-bar">
<el-button size="default" :type="filterStatus === '' ? 'primary' : ''" @click="filterStatus = ''">全部 ({{ topics.length }})</el-button>
<el-button size="default" :type="filterStatus === 'today' ? 'primary' : ''" @click="filterStatus = 'today'">今日新增 ({{ todayCount }})</el-button>
<el-button size="default" :type="filterStatus === 'pending' ? 'primary' : ''" @click="filterStatus = 'pending'">待处理 ({{ statusStats.pending }})</el-button>
<el-button size="default" :type="filterStatus === 'review' ? 'primary' : ''" @click="filterStatus = 'review'">待审查 ({{ statusStats.review }})</el-button>
<el-button size="default" :type="filterStatus === 'ready' ? 'primary' : ''" @click="filterStatus = 'ready'">待发布 ({{ statusStats.ready }})</el-button>
@@ -109,7 +107,7 @@
<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 size="small" type="warning" :disabled="!isStatus(scope.row, 'review')" @click="reviewTopic(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>
@@ -134,7 +132,7 @@
<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 size="small" type="warning" :disabled="!isStatus(topic, 'review')" @click="reviewTopic(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>
@@ -143,25 +141,24 @@
</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">
<el-dialog v-model="previewVisible" title="选题预览" width="85%" :modal-props="{ closeOnClickModal: false }" :before-close="() => previewVisible = false" class="preview-dialog-custom" :fullscreen="previewFullscreen" close-on-press-escape>
<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>
<el-button v-if="previewFullscreen" size="small" type="danger" @click="previewVisible = false">关闭</el-button>
</div>
</div>
<div style="margin: 12px 0 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="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; width:100%;" sandbox></iframe>
</div>
@@ -192,7 +189,7 @@ const TopicsApp = {
isLoggedIn: false, isAdmin: false, currentUser: { username: '' },
loadingTable: false, selectedTopicIds: [], filterStatus: '',
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
topics: [],
topics: [], todayCount: 0,
previewVisible: false, previewTopic: null, previewFullscreen: false,
previewPlatform: 'zhihu', platformContents: {}
}
@@ -201,15 +198,23 @@ const TopicsApp = {
filteredTopics() {
if (!this.topics || !this.topics.length) return [];
if (!this.filterStatus) return this.topics;
if (this.filterStatus === 'today') {
const today = new Date();
const todayStr = today.toISOString().slice(0, 10);
return this.topics.filter(t => {
const d = t.created_at;
if (!d) return false;
return d.slice(0, 10) === todayStr;
});
}
const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] };
const allowed = map[this.filterStatus] || [this.filterStatus];
return this.topics.filter(t => allowed.includes(t.status));
},
statusStats() {
const s = { total: this.topics.length, pending: 0, review: 0, ready: 0, published: 0 };
const map = { 'pending': s.pending, 'review': s.review, 'ready': s.ready, 'published': s.published };
const aliases = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] };
this.topics.forEach(t => { for (const [k, v] of Object.entries(aliases)) { if (v.includes(t.status)) { map[k]++; break; } } });
this.topics.forEach(t => { for (const [k, v] of Object.entries(aliases)) { if (v.includes(t.status)) { s[k]++; break; } } });
return s;
},
currentPreviewHtml() {
@@ -249,7 +254,13 @@ const TopicsApp = {
this.topics = [];
} finally { this.loadingTable = false; }
},
refreshAll() { this.$message.info('执行批量刷新'); },
async fetchTodayCount() {
try {
const stats = await this.api('/api/topics/stats');
if (stats) this.todayCount = stats.today_created || 0;
} catch (e) { console.error('获取统计失败:', e); }
},
refreshAll() { this.fetchTopics(); this.fetchTodayCount(); this.$message.success('已刷新'); },
async triggerGenerateSelected() {
if (!this.selectedTopicIds.length) return;
let count = 0;
@@ -263,14 +274,14 @@ const TopicsApp = {
this.selectedTopicIds = [];
setTimeout(() => this.fetchTopics(), 2000);
},
async triggerOptimizeSelected() {
async triggerReviewSelected() {
if (!this.selectedTopicIds.length) return;
try {
const data = await this.api('/api/system/optimize/run', { method: 'POST', body: JSON.stringify({ topic_ids: this.selectedTopicIds }) });
this.$message.success('批量优化完成');
const data = await this.api('/api/system/review/run', { method: 'POST', body: JSON.stringify({ topic_ids: this.selectedTopicIds }) });
this.$message.success('批量审查完成');
this.selectedTopicIds = [];
await this.fetchTopics();
} catch (error) { this.$message.error(`批量优化失败: ${error.message}`); }
} catch (error) { this.$message.error(`批量审查失败: ${error.message}`); }
},
async openPreview(topic) {
this.previewTopic = topic; this.previewPlatform = 'zhihu'; this.previewVisible = true; this.platformContents = {};
@@ -282,7 +293,22 @@ const TopicsApp = {
.then(r => r.ok ? r.json() : null).then(d => { if (d && d.html) this.platformContents[p] = d.html; }).catch(e => console.error(`加载${p}预览失败:`, e))
));
},
togglePreviewFullscreen() { this.previewFullscreen = !this.previewFullscreen; },
togglePreviewFullscreen() {
this.previewFullscreen = !this.previewFullscreen;
this.$nextTick(() => {
const overlays = document.querySelectorAll('.el-overlay');
const overlay = overlays[overlays.length - 1];
if (overlay) overlay.style.zIndex = this.previewFullscreen ? '100000' : '';
const dialog = document.querySelector('.preview-dialog-custom');
if (dialog) {
if (this.previewFullscreen) {
dialog.style.zIndex = '100001';
} else {
dialog.style.zIndex = '';
}
}
});
},
platformName(platform) { return { zhihu: '知乎', wechat: '微信公众号', xiaohongshu: '小红书' }[platform] || platform; },
copyContent(platform) {
if (!this.previewTopic || !this.previewTopic.content) { this.$message.warning('暂无内容可复制'); return; }
@@ -298,18 +324,18 @@ const TopicsApp = {
setTimeout(() => this.fetchTopics(), 2000);
} catch (error) { this.$message.error(`创作失败: ${error.message}`); }
},
async optimizeTopic(topic) {
if (!this.isStatus(topic, 'review')) { this.$message.info('仅待审查选题可优化'); return; }
async reviewTopic(topic) {
if (!this.isStatus(topic, 'review')) { this.$message.info('仅待审查选题可操作'); return; }
try {
const data = await this.api('/api/system/optimize/run', { method: 'POST', body: JSON.stringify({ topic_ids: [topic.id] }) });
this.$message.success(`优化完成: ${topic.title}`);
const data = await this.api('/api/system/review/run', { method: 'POST', body: JSON.stringify({ topic_ids: [topic.id] }) });
this.$message.success(`审查完成: ${topic.title}`);
await this.fetchTopics();
} catch (error) { this.$message.error(`优化失败: ${error.message}`); }
} catch (error) { this.$message.error(`审查失败: ${error.message}`); }
},
async handlePublish(topic) {
if (!this.isStatus(topic, 'ready')) { this.$message.info('仅待发布选题可发布'); return; }
try {
const data = await this.api('/api/publishing/create', { method: 'POST', body: JSON.stringify({ topic_ids: [topic.id] }) });
const data = await this.api('/api/publishing/create', { method: 'POST', body: JSON.stringify({ topic_id: topic.id }) });
this.$message.success(`发布成功: ${topic.title}`);
await this.fetchTopics();
} catch (error) { this.$message.error(`发布失败: ${error.message}`); }
@@ -340,7 +366,7 @@ const TopicsApp = {
if (filter) this.filterStatus = filter;
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; this.fetchTopics(); })
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; this.fetchTopics(); this.fetchTodayCount(); })
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
}
};
+6 -12
View File
@@ -5,20 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 用户管理</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
.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; min-height: calc(100vh - 60px); }
.sidebar { width: 180px; background: white; padding: 16px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.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; }
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
.card-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
@@ -29,8 +25,6 @@
.user-card-list { display: none; }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
.card { padding: 16px; }
.user-table { display: none; }
@@ -79,7 +73,7 @@
<navigation-component current-page="users" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<div class="card page-fade">
<div class="card-header">
<h2 class="card-title">👥 用户管理</h2>
<el-button type="primary" @click="addUser">+ 新建用户</el-button>
-61
View File
@@ -1,61 +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>Vue基础测试</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.test-card { background: #f5f5f5; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
button { padding: 10px 20px; background: #409EFF; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #337ecc; }
.success { color: green; font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<h1>{{ title }}</h1>
<div class="test-card">
<h3>数据绑定测试</h3>
<p>当前计数: {{ count }}</p>
<button @click="count++">增加计数</button>
</div>
<div class="test-card">
<h3>列表渲染测试</h3>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
<div class="test-card">
<h3>条件渲染测试</h3>
<p v-if="showResult" class="success">✅ Vue基础功能正常工作!</p>
<button @click="showResult = true">显示结果</button>
</div>
</div>
<script>
const app = {
data() {
return {
title: "Vue基础功能测试",
count: 0,
items: ["项目 1", "项目 2", "项目 3"],
showResult: false
}
},
mounted() {
console.log("Vue应用已启动");
console.log("数据对象:", this.$data);
}
};
Vue.createApp(app).mount('#app');
</script>
</body>
</html>