remove: delete obsolete backend/static directory

This commit is contained in:
lt
2026-05-01 08:05:54 +08:00
parent e1613d2af9
commit 2cbf2909f8
35 changed files with 0 additions and 83624 deletions
-37
View File
@@ -1,37 +0,0 @@
# 宇之然内容创作平台 - 前端Docker镜像
FROM nginx:alpine as builder
# 安装构建工具(用于优化HTML
RUN apk add --no-cache python3 py3-pip
COPY index.html /tmp/index.html
COPY login.html /tmp/login.html
# 简单压缩HTML(实际生产应使用Webpack等构建工具)
RUN cat /tmp/index.html | tr -d '\n' > /tmp/index.min.html && \
mv /tmp/index.min.html /tmp/index.html
WORKDIR /usr/share/nginx/html
# 复制静态资源
COPY . .
# 生产阶段 - 直接使用Nginx
FROM nginx:alpine
# 复制优化后的前端文件
COPY --from=builder /usr/share/nginx/html /usr/share/nginx/html
# 配置Nginx
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
EXPOSE 8000
# 标签信息
LABEL maintainer="宇之然团队"
LABEL version="1.0.0"
LABEL description="企业级内容创作管理系统前端"
-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>
@@ -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>
@@ -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>
-585
View File
@@ -1,585 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台</title>
<link rel="stylesheet" href="/static/element-plus/index.css">
<style>
/* 深色渐变背景主题 */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
/* 深色渐变: #1a1a2e → #16213e → #0f3460 */
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #e0e6ed;
}
/* 导航栏 */
.navbar {
background: rgba(102, 126, 234, 0.15);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(102, 126, 234, 0.2);
padding: 16px 24px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
position: sticky;
top: 0;
z-index: 100;
}
.navbar-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1400px;
margin: 0 auto;
}
.navbar-title {
font-size: 20px;
font-weight: 700;
background: linear-gradient(90deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: -0.5px;
}
.navbar-user {
display: flex;
align-items: center;
gap: 16px;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
color: #a0aec0;
font-size: 14px;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
color: white;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
}
/* 主内容区 */
.main-content {
display: flex;
max-width: 1400px;
margin: 0 auto;
min-height: calc(100vh - 64px);
}
/* 侧边栏 */
.sidebar {
width: 200px;
background: rgba(26, 26, 46, 0.8);
backdrop-filter: blur(20px);
padding: 16px 12px;
border-right: 1px solid rgba(102, 126, 234, 0.1);
display: flex;
flex-direction: column;
gap: 4px;
}
.sidebar-btn {
width: 100%;
text-align: left;
padding: 12px 16px;
border: none;
background: transparent;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: #a0aec0;
font-size: 14px;
font-weight: 500;
position: relative;
overflow: hidden;
}
.sidebar-btn:hover {
background: rgba(102, 126, 234, 0.1);
color: #667eea;
transform: translateX(4px);
}
.sidebar-btn.active {
background: linear-gradient(90deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2));
color: #667eea;
font-weight: 600;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
}
.sidebar-btn.active::after {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(180deg, #667eea, #764ba2);
border-radius: 0 4px 4px 0;
}
/* 内容区域 */
.content-area {
flex: 1;
padding: 32px;
overflow-y: auto;
}
/* 页面切换 */
.page { display: none; animation: fadeIn 0.5s ease-out; }
.page.active { display: block; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* 统计卡片网格 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(102, 126, 234, 0.1);
border-radius: 16px;
padding: 24px;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(102, 126, 234, 0.1), transparent);
transition: left 0.6s;
}
.stat-card:hover::before {
left: 100%;
}
.stat-card:hover {
transform: translateY(-8px) scale(1.02);
border-color: rgba(102, 126, 234, 0.4);
box-shadow: 0 12px 32px rgba(102, 126, 234, 0.2);
}
.stat-title {
font-size: 13px;
color: #a0aec0;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
}
.stat-value {
font-size: 36px;
font-weight: 800;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2;
}
.stat-card.primary .stat-value { background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.stat-card.success .stat-value { background: linear-gradient(135deg, #67c23a, #85e61d); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.stat-card.warning .stat-value { background: linear-gradient(135deg, #e6a23c, #f5c543); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.stat-card.danger .stat-value { background: linear-gradient(135deg, #f56c6c, #f79296); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.stat-card.info .stat-value { background: linear-gradient(135deg, #409eff, #5cd0f3); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
/* 模块卡片 */
.module-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.module-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(102, 126, 234, 0.1);
border-radius: 16px;
padding: 24px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.module-card:hover {
transform: translateY(-6px);
border-color: rgba(102, 126, 234, 0.3);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.module-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.module-title {
font-size: 16px;
font-weight: 600;
color: #e0e6ed;
display: flex;
align-items: center;
gap: 8px;
}
.module-status {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
background: rgba(103, 194, 58, 0.2);
color: #67c23a;
border: 1px solid rgba(103, 194, 58, 0.3);
}
.module-status.running {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.4); }
50% { box-shadow: 0 0 0 8px rgba(103, 194, 58, 0); }
}
.module-content {
font-size: 14px;
color: #a0aec0;
line-height: 1.8;
}
.module-content div {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px dashed rgba(255, 255, 255, 0.05);
}
.module-content div:last-child { border-bottom: none; }
/* 移动端导航 */
.mobile-nav {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(26, 26, 46, 0.95);
backdrop-filter: blur(20px);
border-top: 1px solid rgba(102, 126, 234, 0.2);
padding: 8px 0;
z-index: 1000;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.3);
}
.mobile-nav-btn {
flex: 1;
border: none;
background: transparent;
padding: 12px 8px;
text-align: center;
font-size: 12px;
color: #a0aec0;
cursor: pointer;
transition: all 0.3s;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.mobile-nav-btn.active {
color: #667eea;
font-weight: 600;
}
.mobile-nav-btn.active::before {
content: '';
width: 4px;
height: 4px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
margin-bottom: 2px;
}
/* 响应式 */
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area {
padding: 16px;
padding-bottom: 80px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card { padding: 16px; }
.stat-value { font-size: 24px; }
.module-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div id="app">
<nav class="navbar" v-if="isLoggedIn">
<div class="navbar-content">
<h1 class="navbar-title">宇之然内容创作平台</h1>
<div class="navbar-user">
<div class="user-info">
<div class="avatar">{{ currentUser.username ? currentUser.username.charAt(0).toUpperCase() : '?' }}</div>
<span>{{ currentUser.username }}</span>
<el-tag v-if="isAdmin" size="small" type="danger" style="border: none;">管理员</el-tag>
</div>
<el-button size="small" type="danger" plain @click="handleLogout">退出</el-button>
</div>
</div>
</nav>
<div class="main-content" v-if="isLoggedIn">
<aside class="sidebar">
<button class="sidebar-btn" :class="{ active: currentPage === 'overview' }" @click="redirectToPage('/')">
📊 系统概览
</button>
<button class="sidebar-btn" :class="{ active: currentPage === 'topics' }" @click="redirectToPage('topics.html')">
📋 选题管理
</button>
<button class="sidebar-btn" :class="{ active: currentPage === 'logs' }" @click="redirectToPage('logs.html')">
📄 系统日志
</button>
<button v-if="isAdmin" class="sidebar-btn" :class="{ active: currentPage === 'users' }" @click="redirectToPage('users.html')">
👥 用户管理
</button>
</aside>
<main class="content-area">
<!-- 系统概览页面 -->
<div id="page-overview" class="page" :class="{ active: currentPage === 'overview' }">
<h2 style="font-size: 28px; font-weight: 700; margin-bottom: 32px; color: #e0e6ed;">
📊 系统概览
</h2>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card primary" @click="goToTopics('')">
<div class="stat-title">选题总数</div>
<div class="stat-value">{{ stats.total }}</div>
</div>
<div class="stat-card warning" @click="goToTopics('pending')">
<div class="stat-title">待处理</div>
<div class="stat-value">{{ stats.pending }}</div>
</div>
<div class="stat-card danger" @click="goToTopics('review')">
<div class="stat-title">待审查</div>
<div class="stat-value">{{ stats.review }}</div>
</div>
<div class="stat-card success" @click="goToTopics('ready')">
<div class="stat-title">待发布</div>
<div class="stat-value">{{ stats.ready }}</div>
</div>
<div class="stat-card info" @click="goToTopics('published')">
<div class="stat-title">已发布</div>
<div class="stat-value">{{ stats.published }}</div>
</div>
<div class="stat-card primary" @click="goToTopics('')">
<div class="stat-title">今日新增</div>
<div class="stat-value">{{ stats.today }}</div>
</div>
</div>
<!-- 模块状态 -->
<h3 style="font-size: 20px; font-weight: 600; margin-bottom: 24px; color: #e0e6ed;">
🔧 模块状态
</h3>
<div class="module-grid">
<div class="module-card">
<div class="module-header">
<span class="module-title">🤖 内容创作引擎</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 14:30</span></div>
<div><span>今日任务</span><span>12 个</span></div>
<div><span>成功率</span><span>95%</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">🔍 内容优化器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 14:45</span></div>
<div><span>今日优化</span><span>8 个</span></div>
<div><span>平均提升</span><span>+12 分</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">📡 内容收集器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 14:00</span></div>
<div><span>今日收集</span><span>24 个</span></div>
<div><span>来源平台</span><span>8 个</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">📤 发布管理器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 13:30</span></div>
<div><span>今日发布</span><span>5 个</span></div>
<div><span>成功率</span><span>100%</span></div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- 移动端导航 -->
<nav class="mobile-nav" v-if="isLoggedIn">
<button class="mobile-nav-btn" :class="{ active: currentPage === 'overview' }" @click="redirectToPage('/')">
📊 概览
</button>
<button class="mobile-nav-btn" :class="{ active: currentPage === 'topics' }" @click="redirectToPage('topics.html')">
📋 选题
</button>
<button class="mobile-nav-btn" :class="{ active: currentPage === 'logs' }" @click="redirectToPage('logs.html')">
📄 日志
</button>
<button v-if="isAdmin" class="mobile-nav-btn" :class="{ active: currentPage === 'users' }" @click="redirectToPage('users.html')">
👥 用户
</button>
</nav>
</div>
<script src="/static/vue/vue.global.js"></script>
<script src="/static/element-plus/index.full.min.js"></script>
<script>
const App = {
data() {
return {
isLoggedIn: false,
isAdmin: false,
currentUser: { username: '' },
currentPage: 'overview',
stats: {
total: 0,
pending: 0,
review: 0,
ready: 0,
published: 0,
today: 0
}
};
},
methods: {
async handleLogin() {
this.loginLoading = true;
this.loginError = '';
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.loginForm)
});
if (!response.ok) throw new Error('登录失败');
const data = await response.json();
localStorage.setItem('authToken', data.token);
this.currentUser = data.user;
this.isAdmin = data.user.role === 'admin';
this.isLoggedIn = true;
this.currentPage = 'overview';
this.fetchStats();
} catch (error) {
this.loginError = '用户名或密码错误';
} finally {
this.loginLoading = false;
}
},
handleLogout() {
localStorage.removeItem('authToken');
this.isLoggedIn = false;
this.currentUser = { username: '' };
this.isAdmin = false;
},
async fetchStats() {
try {
const token = localStorage.getItem('authToken');
if (!token) {
window.location.href = '/login.html';
return;
}
const response = await fetch('/api/system/status', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (response.ok) {
const data = await response.json();
// API返回格式: { stats: { total, pending, review, ready, published, today } }
this.stats = {
total: data.stats?.total || 0,
pending: data.stats?.pending || 0,
review: data.stats?.review || 0,
ready: data.stats?.ready || 0,
published: data.stats?.published || 0,
today: data.stats?.today || 0
};
} else if (response.status === 401) {
// Token无效,清除并跳转登录
localStorage.removeItem('authToken');
window.location.href = '/login.html';
} else {
console.error('获取统计信息失败:', response.status, response.statusText);
this.stats = { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 };
}
} catch (error) {
console.error('获取统计信息失败:', error);
// 失败时设置为0,避免页面空白
this.stats = { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 };
}
},
goToTopics(filter) {
const url = filter ? '/topics.html?filter=' + encodeURIComponent(filter) : '/topics.html';
window.location.href = url;
},
redirectToPage(page) {
window.location.href = page.startsWith('/') ? page : '/' + page;
}
},
mounted() {
const token = localStorage.getItem('authToken');
if (!token) {
window.location.href = '/login.html';
return;
}
fetch('/api/auth/me', {
headers: { 'Authorization': 'Bearer ' + token }
})
.then(response => response.ok ? response.json() : Promise.reject())
.then(data => {
this.currentUser = data.user;
this.isAdmin = data.user.role === 'admin';
this.isLoggedIn = true;
this.currentPage = 'overview';
this.fetchStats();
})
.catch(() => {
localStorage.removeItem('authToken');
window.location.href = '/login.html';
});
}
};
const app = Vue.createApp(App);
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>
-350
View File
@@ -1,350 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 登录</title>
<link rel="stylesheet" href="/static/element-plus/index.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: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
/* 背景动态装饰圆 */
.bg-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
animation: float 20s infinite ease-in-out;
}
.bg-circle:nth-child(1) { width: 300px; height: 300px; top: -150px; left: -150px; animation-delay: 0s; }
.bg-circle:nth-child(2) { width: 200px; height: 200px; bottom: -100px; right: -100px; animation-delay: -5s; }
.bg-circle:nth-child(3) { width: 150px; height: 150px; top: 50%; right: 10%; animation-delay: -10s; }
.bg-circle:nth-child(4) { width: 100px; height: 100px; bottom: 20%; left: 5%; animation-delay: -15s; }
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
25% { transform: translate(30px, -30px) scale(1.1); }
50% { transform: translate(-20px, 20px) scale(0.9); }
75% { transform: translate(20px, 30px) scale(1.05); }
}
/* 登录卡片 */
.login-card {
position: relative;
z-index: 10;
width: 100%;
max-width: 420px;
padding: 48px 40px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.2) inset;
animation: slide-up 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(40px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.login-title {
text-align: center;
font-size: 28px;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 40px;
letter-spacing: -0.5px;
}
.login-subtitle {
text-align: center;
color: #9ca3af;
font-size: 14px;
margin-top: -24px;
margin-bottom: 32px;
}
/* 表单样式 */
.form-group {
margin-bottom: 24px;
position: relative;
}
.form-input {
width: 100%;
padding: 14px 16px;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 16px;
color: #1f2937;
background: #f9fafb;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
}
.form-input:focus {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
transform: translateY(-2px);
}
.form-input:not(:placeholder-shown) {
border-color: #764ba2;
}
.form-label {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
pointer-events: none;
transition: all 0.2s ease;
font-size: 16px;
background: transparent;
padding: 0 4px;
}
.form-input:focus ~ .form-label,
.form-input:not(:placeholder-shown) ~ .form-label {
top: 0;
font-size: 12px;
color: #667eea;
background: #ffffff;
}
/* 登录按钮 */
.login-btn {
width: 100%;
padding: 14px;
margin-top: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.login-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.login-btn:active:not(:disabled) {
transform: translateY(0);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* 按钮加载动画 */
.btn-loader {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 底部信息 */
.login-footer {
text-align: center;
margin-top: 24px;
color: #6b7280;
font-size: 13px;
}
.login-footer a {
color: #667eea;
text-decoration: none;
transition: color 0.2s;
}
.login-footer a:hover {
color: #764ba2;
}
/* 响应式 */
@media (max-width: 480px) {
.login-card {
max-width: 90%;
padding: 32px 24px;
margin: 16px;
border-radius: 20px;
}
.login-title {
font-size: 24px;
margin-bottom: 24px;
}
}
</style>
</head>
<body>
<!-- 背景装饰 -->
<div class="bg-circle"></div>
<div class="bg-circle"></div>
<div class="bg-circle"></div>
<div class="bg-circle"></div>
<div class="login-card">
<h1 class="login-title">宇之然内容创作平台</h1>
<p class="login-subtitle">Yuzhiran Content Creation Platform</p>
<form @submit.prevent="handleLogin">
<div class="form-group">
<input
v-model="username"
class="form-input"
type="text"
placeholder=" "
required
autocomplete="username"
/>
<label class="form-label">用户名</label>
</div>
<div class="form-group">
<input
v-model="password"
class="form-input"
type="password"
placeholder=" "
required
autocomplete="current-password"
/>
<label class="form-label">密码</label>
</div>
<button
type="submit"
class="login-btn"
:disabled="loading"
>
<span v-if="loading" class="btn-loader"></span>
{{ loading ? '登录中...' : '立即登录' }}
</button>
</form>
<p class="login-footer">
管理员用户可访问完整系统功能
</p>
</div>
<script src="/static/vue/vue.global.js"></script>
<script src="/static/element-plus/index.full.min.js"></script>
<script>
const { ref } = Vue;
const { ElMessage } = ElementPlus;
const app = Vue.createApp({
name: 'LoginPage',
setup() {
const username = ref('');
const password = ref('');
const loading = ref(false);
const handleLogin = async () => {
if (!username.value.trim() || !password.value) {
ElMessage.warning('请输入用户名和密码');
return;
}
loading.value = true;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username.value.trim(),
password: password.value
})
});
const data = await response.json();
if (response.ok && data.token) {
localStorage.setItem('authToken', data.token);
localStorage.setItem('userRole', data.role || 'admin');
localStorage.setItem('currentUser', JSON.stringify(data.user));
ElMessage({
message: '登录成功!正在跳转...',
type: 'success',
duration: 1500,
onClose: () => {
window.location.href = '/';
}
});
} else {
ElMessage.error(data.message || data.error || '登录失败,请检查用户名和密码');
}
} catch (error) {
console.error('登录请求失败:', error);
ElMessage.error('网络连接失败,请检查服务是否正常运行');
} finally {
loading.value = false;
}
};
// 检查是否已登录
const token = localStorage.getItem('authToken');
if (token) {
window.location.href = '/';
}
return {
username,
password,
loading,
handleLogin
};
}
});
app.use(ElementPlus);
app.mount('body');
</script>
</body>
</html>
-139
View File
@@ -1,139 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 系统日志</title>
<link rel="stylesheet" href="/static/element-plus/index.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; width: 100%; }
.sidebar { width: 180px; background: white; padding: 16px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.sidebar-btn { width: 100%; text-align: left; padding: 12px 16px; border: none; background: transparent; border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.3s; color: #606266; font-size: 14px; }
.sidebar-btn:hover { background: #f5f7fa; color: #409eff; }
.sidebar-btn.active { background: #ecf5ff; color: #409eff; font-weight: 600; }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.mobile-nav-btn { flex: 1; border: none; background: transparent; padding: 12px; text-align: center; font-size: 12px; color: #606266; cursor: pointer; }
.mobile-nav-btn.active { color: #409eff; font-weight: 600; }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
}
/* logs.html 移动端优化 */
@media (max-width: 768px) {
.log-controls { flex-direction: column; gap: 8px; }
.log-controls .el-select, .log-controls .el-date-picker { width: 100% !important; }
.el-card { margin: 0 -16px; border-radius: 0; min-height: calc(100vh - 180px); }
.el-card .el-card__body { padding: 12px; }
pre { font-size: 11px; line-height: 1.4; }
}
</style>
</head>
<body>
<div id="app">
<nav class="navbar">
<div class="navbar-content">
<h1 class="navbar-title">宇之然内容创作平台 - 系统日志</h1>
<div class="navbar-user">
<div class="user-info"><div class="avatar">{{ currentUser.username.charAt(0).toUpperCase() }}</div><span>{{ currentUser.username }}</span></div>
<el-button type="danger" size="small" @click="handleLogout">退出</el-button>
</div>
</div>
</nav>
<div class="main-content">
<aside class="sidebar">
<button class="sidebar-btn" @click="redirectToPage('/')">📊 系统概览</button>
<button class="sidebar-btn" @click="redirectToPage('topics.html')">📋 选题管理</button>
<button class="sidebar-btn active">📄 系统日志</button>
<button v-if="isAdmin" class="sidebar-btn" @click="redirectToPage('users.html')">👥 用户管理</button>
</aside>
<main class="content-area">
<div class="card">
<h2 style="font-size: 24px; font-weight: 600; margin-bottom: 24px;">📄 系统日志</h2>
<div style="display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap;">
<el-select v-model="logType" placeholder="日志类型" size="default" style="width: 180px;">
<el-option label="创作日志" value="creator"></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" size="default"></el-date-picker>
<el-button type="primary" @click="fetchLogs" :loading="loadingLogs">加载日志</el-button>
</div>
<el-card v-if="logContent" class="font-mono text-sm bg-gray-50" style="max-height: 600px; overflow-y: auto; background: #f9fafb; border: 1px solid #e5e7eb;"><pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">{{ logContent }}</pre></el-card>
<el-empty v-else description="请先选择类型和日期,然后点击加载"></el-empty>
</div>
</main>
</div>
<nav class="mobile-nav">
<button class="mobile-nav-btn" @click="redirectToPage('/')">📊 概览</button>
<button class="mobile-nav-btn" @click="redirectToPage('topics.html')">📋 选题</button>
<button class="mobile-nav-btn active">📄 日志</button>
<button v-if="isAdmin" class="mobile-nav-btn" @click="redirectToPage('users.html')">👥 用户</button>
</nav>
</div>
<script src="/static/vue/vue.global.js"></script>
<script src="/static/element-plus/index.full.min.js"></script>
<script>
const LogsApp = {
data() { return { isLoggedIn: false, isAdmin: false, currentUser: { username: '' }, logType: 'creator', logDate: '', logContent: '', loadingLogs: false } },
methods: {
async fetchLogs() {
// 验证输入
if (!this.logType || !this.logDate) {
this.$message.warning('请选择日志类型和日期');
return;
}
this.loadingLogs = true;
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`/api/logs?type=${encodeURIComponent(this.logType)}&date=${encodeURIComponent(this.logDate)}`, {
headers: { 'Authorization': 'Bearer ' + token }
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `请求失败: ${response.status}`);
}
const data = await response.json();
// 后端返回: { type, date, content }
this.logContent = `日志类型:${data.type}\n日期:${data.date}\n\n${data.content || '(日志文件为空)'}`;
this.$message.success('日志加载成功');
} catch (error) {
console.error('获取日志失败:', error);
this.$message.error(`获取日志失败: ${error.message}`);
this.logContent = '';
} finally {
this.loadingLogs = false;
}
},
handleLogout() { localStorage.removeItem('authToken'); window.location.href = '/'; },
redirectToPage(page) { window.location.href = '/' + page; }
},
mounted() {
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = '/'; return; }
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(response => response.ok ? response.json() : Promise.reject())
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; })
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/'; });
}
};
const app = Vue.createApp(LogsApp);
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>
-36
View File
@@ -1,36 +0,0 @@
{
"name": "宇之然内容创作平台",
"short_name": "宇之然",
"description": "可持续性内容创作与管理系统",
"start_url": "/",
"display": "standalone",
"background_color": "#f5f7fa",
"theme_color": "#409EFF",
"orientation": "portrait-primary",
"icons": [
{
"src": "/static/icon-192.svg",
"sizes": "192x192",
"type": "image/svg+xml"
},
{
"src": "/static/icon-512.svg",
"sizes": "512x512",
"type": "image/svg+xml"
}
],
"screenshots": [
{
"src": "/static/screenshot-desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "/static/screenshot-mobile.png",
"sizes": "750x1334",
"type": "image/png",
"form_factor": "narrow"
}
]
}
-128
View File
@@ -1,128 +0,0 @@
# 宇之然内容创作平台 - Nginx配置
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
# 基本设置
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
gzip_comp_level 6;
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: blob: 'unsafe-inline'" always;
# 代理缓存
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m inactive=7d use_temp_path=off;
# 上游服务器
upstream backend {
server app:8001;
keepalive 32;
}
server {
listen 80;
server_name _;
client_max_body_size 100M;
# SSL配置(生产环境)
# listen 443 ssl http2;
# ssl_certificate /etc/nginx/ssl/cert.pem;
# ssl_certificate_key /etc/nginx/ssl/key.pem;
location / {
# 前端静态资源缓存
proxy_cache STATIC;
proxy_cache_valid 200 302 7d;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
# 反向代理到后端API
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
# WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时设置
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# API文档(可选)
location /docs {
proxy_pass http://backend/docs;
proxy_set_header Host $host;
}
location /redoc {
proxy_pass http://backend/redoc;
proxy_set_header Host $host;
}
}
# 静态文件服务(如果需要)
server {
listen 8000;
server_name localhost;
root /usr/share/nginx/html;
index index.html login.html;
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
}
-57
View File
@@ -1,57 +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>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 20px;
text-align: center;
}
.container {
max-width: 400px;
}
.icon {
font-size: 80px;
margin-bottom: 20px;
}
h1 {
font-size: 24px;
margin: 0 0 12px;
}
p {
font-size: 16px;
line-height: 1.6;
opacity: 0.9;
}
.btn {
display: inline-block;
margin-top: 20px;
padding: 12px 24px;
background: white;
color: #667eea;
text-decoration: none;
border-radius: 8px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">📴</div>
<h1>当前处于离线状态</h1>
<p>您似乎已断开网络连接,但可以查看已缓存的内容。</p>
<p>请检查网络后刷新页面以获取最新数据。</p>
<a href="/" class="btn">重试</a>
</div>
</body>
</html>
-13
View File
@@ -1,13 +0,0 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "",
"main": "debug-vue.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}
@@ -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
};
@@ -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>
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
<rect width="192" height="192" fill="#409EFF" rx="24"/>
<text x="96" y="120" font-family="Arial, sans-serif" font-size="80" font-weight="bold" fill="white" text-anchor="middle"></text>
</svg>

Before

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<rect width="512" height="512" fill="#409EFF" rx="48"/>
<text x="256" y="320" font-family="Arial, sans-serif" font-size="200" font-weight="bold" fill="white" text-anchor="middle"></text>
</svg>

Before

Width:  |  Height:  |  Size: 289 B

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
-116
View File
@@ -1,116 +0,0 @@
// Service Worker for 宇之然内容创作平台
const CACHE_NAME = 'yuzhiran-v1';
const CACHE_URLS = [
'/',
'/index.html',
'/offline.html',
'/static/vue.global.prod.js',
'/static/element-plus.css',
'/static/element-plus.full.js',
'/manifest.json'
];
// 安装事件:预缓存核心资源
self.addEventListener('install', (event) => {
console.log('[SW] Installing...');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[SW] Pre-caching core assets');
return cache.addAll(CACHE_URLS.map(url => {
// 忽略同源请求404错误(静态资源可能不存在)
return new Promise((resolve, reject) => {
fetch(url).then(response => {
if (response.ok) {
resolve(url);
} else {
reject(new Error(`Failed to fetch ${url}: ${response.status}`));
}
}).catch(() => {
// 静默失败,不阻止安装
resolve(url);
});
});
}));
}).catch(err => {
console.error('[SW] Install failed:', err);
})
);
self.skipWaiting();
});
// 激活事件:清理旧缓存
self.addEventListener('activate', (event) => {
console.log('[SW] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cache) => {
if (cache !== CACHE_NAME) {
console.log('[SW] Deleting old cache:', cache);
return caches.delete(cache);
}
})
);
})
);
self.clients.claim();
});
// 网络请求拦截:Cache First + Network Fallback
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 只处理同源请求
if (url.origin !== location.origin) {
return;
}
// API 请求:Network Only(不走缓存)
if (url.pathname.startsWith('/api/')) {
event.respondWith(fetch(request));
return;
}
// 静态资源:Cache First
event.respondWith(
caches.match(request).then((cached) => {
if (cached) {
// 返回缓存,并在后台更新
fetch(request).then(response => {
if (response.ok) {
caches.open(CACHE_NAME).then(cache => cache.put(request, response));
}
});
return cached;
}
// 无缓存,发起网络请求
return fetch(request).then(response => {
// 成功且为有效响应,加入缓存
if (response.ok && response.status === 200) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(request, responseClone));
}
return response;
}).catch(() => {
// 网络失败,尝试返回离线页面(如果是文档请求)
if (request.destination === 'document') {
return caches.match('/offline.html');
}
});
})
);
});
// 后台同步(可选:在网络恢复后发送错误日志)
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-logs') {
event.waitUntil(syncLogs());
}
});
async function syncLogs() {
// TODO: 实现日志同步
console.log('[SW] Syncing logs...');
}
-90
View File
@@ -1,90 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>前端测试</title>
<script src="/static/vue.global.prod.js"></script>
<link rel="stylesheet" href="/static/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="./static/vue.global.prod.js?v=20260427"></script>
<link rel="stylesheet" href="./static/element-plus.css?v=20260427">
<script src="./static/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>
-417
View File
@@ -1,417 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 选题管理</title>
<link rel="stylesheet" href="/static/element-plus/index.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; width: 100%; }
.sidebar { width: 180px; background: white; padding: 16px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.sidebar-btn { width: 100%; text-align: left; padding: 12px 16px; border: none; background: transparent; border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.3s; color: #606266; font-size: 14px; }
.sidebar-btn:hover { background: #f5f7fa; color: #409eff; }
.sidebar-btn.active { background: #ecf5ff; color: #409eff; font-weight: 600; }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.status-badge { display: inline-flex; align-items: center; gap: 4px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; }
.status-dot.pending { background: #E6A23C; }
.status-dot.review { background: #F56C6C; }
.status-dot.ready { background: #67C23A; }
.status-dot.published { background: #409EFF; }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.mobile-nav-btn { flex: 1; border: none; background: transparent; padding: 12px; text-align: center; font-size: 12px; color: #606266; cursor: pointer; }
.mobile-nav-btn.active { color: #409eff; font-weight: 600; }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
}
/* 移动端卡片布局 */
@media (max-width: 768px) {
.el-table { font-size: 12px; display: none; }
.el-table .el-button { padding: 4px 8px; font-size: 11px; min-height: auto; }
.el-table .cell { padding: 0 4px; }
.el-table .el-table__cell { padding: 6px 0; }
.topic-card-list { display: block; margin: 0 -16px; }
.topic-card {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.topic-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.topic-card-title { font-size: 16px; font-weight: 600; color: #303133; flex: 1; margin-right: 8px; }
.topic-card-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
.topic-card-meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
font-size: 12px;
color: #606266;
margin-bottom: 12px;
}
.topic-card-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #ebeef5;
}
.topic-card-actions .el-button { flex: 1; min-width: 60px; }
.mobile-nav { display: flex; }
}
</style>
</head>
<body>
<div id="app">
<nav class="navbar">
<div class="navbar-content">
<h1 class="navbar-title">宇之然内容创作平台 - 选题管理</h1>
<div class="navbar-user">
<div class="user-info"><div class="avatar">{{ currentUser.username ? currentUser.username.charAt(0).toUpperCase() : '?' }}</div><span>{{ currentUser.username }}</span></div>
<el-button type="danger" size="small" @click="handleLogout">退出</el-button>
</div>
</div>
</nav>
<div class="main-content">
<aside class="sidebar">
<button class="sidebar-btn" @click="redirectToPage('/')">📊 系统概览</button>
<button class="sidebar-btn active">📋 选题管理</button>
<button class="sidebar-btn" @click="redirectToPage('logs.html')">📄 系统日志</button>
<button v-if="isAdmin" class="sidebar-btn" @click="redirectToPage('users.html')">👥 用户管理</button>
</aside>
<main class="content-area">
<div class="card">
<h2 style="font-size: 24px; font-weight: 600; margin-bottom: 24px;">📋 选题管理</h2>
<div class="card" style="display: inline-block; min-width: fit-content; padding: 16px; margin-bottom: 24px;">
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<el-button type="primary" size="small" @click="refreshAll">🔄 批量刷新</el-button>
<el-button type="success" size="small" @click="triggerGenerateSelected" :disabled="selectedTopicIds.length === 0">▶ 批量创作</el-button>
<el-button type="warning" size="small" @click="triggerOptimizeSelected" :disabled="selectedTopicIds.length === 0">🔍 批量优化</el-button>
<span class="ml-auto text-sm text-gray-500" v-if="selectedTopicIds.length > 0" style="color: #909399; font-size: 14px; margin-left: auto;">已选 {{ selectedTopicIds.length }} 项</span>
</div>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap;">
<el-tag size="large" :type="filterStatus === '' ? 'primary' : ''" @click="filterStatus = ''">全部 ({{ topics.length }})</el-tag>
<el-tag size="large" :type="filterStatus === 'pending' ? 'primary' : ''" @click="filterStatus = 'pending'">待处理 ({{ countByStatus('pending') }})</el-tag>
<el-tag size="large" :type="filterStatus === 'review' ? 'primary' : ''" @click="filterStatus = 'review'">待审查 ({{ countByStatus('review') }})</el-tag>
<el-tag size="large" :type="filterStatus === 'ready' ? 'primary' : ''" @click="filterStatus = 'ready'">待发布 ({{ countByStatus('ready') }})</el-tag>
<el-tag size="large" :type="filterStatus === 'published' ? 'primary' : ''" @click="filterStatus = 'published'">已发布 ({{ countByStatus('published') }})</el-tag>
</div>
<div class="card" style="width: 100%; overflow-x: auto; padding: 16px;">
<el-table :data="filteredTopics" stripe v-loading="loadingTable" @selection-change="selectedTopicIds = $event">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID" width="70" fixed></el-table-column>
<el-table-column prop="title" label="标题" min-width="200"></el-table-column>
<el-table-column prop="field" label="领域" width="100"></el-table-column>
<el-table-column prop="status" label="状态" width="90">
<template #default="scope"><span class="status-badge"><span class="status-dot" :class="scope.row.status"></span>{{ scope.row.status }}</span></template>
</el-table-column>
<el-table-column prop="compliance_score" label="合规分" width="90">
<template #default="scope"><el-progress :percentage="scope.row.compliance_score || 0" :format="() => scope.row.compliance_score || '-'" :stroke-width="15"></el-progress></template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="140"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column>
<el-table-column prop="generated_at" label="创作时间" width="140"><template #default="scope">{{ scope.row.generated_at ? formatDate(scope.row.generated_at) : '-' }}</template></el-table-column>
<el-table-column prop="published_at" label="发布时间" width="140"><template #default="scope">{{ scope.row.published_at ? formatDate(scope.row.published_at) : '-' }}</template></el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="scope">
<div style="display: flex; gap: 4px; flex-wrap: wrap;">
<el-button size="small" @click="openPreview(scope.row)" type="primary">预览</el-button>
<el-button size="small" type="success" :disabled="scope.row.status !== 'pending'" @click="createTopic(scope.row)">创作</el-button>
<el-button size="small" type="warning" :disabled="scope.row.status !== 'review'" @click="optimizeTopic(scope.row)">审查</el-button>
<el-button v-if="scope.row.status === 'ready'" size="small" type="primary" @click="handlePublish(scope.row)">发布</el-button>
<el-button size="small" type="danger" @click="deleteTopic(scope.row.id)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 移动端卡片列表 -->
<div class="topic-card-list" v-if="filteredTopics && filteredTopics.length > 0">
<div v-for="topic in filteredTopics" :key="topic.id" class="topic-card">
<div class="topic-card-header">
<div class="topic-card-title">{{ topic.title }}</div>
<el-tag :type="getStatusType(topic.status)" size="small">{{ topic.status }}</el-tag>
</div>
<div class="topic-card-tags">
<el-tag size="small" type="info">{{ topic.field }}</el-tag>
<el-tag size="small" type="warning">合规{{ topic.compliance_score }}</el-tag>
</div>
<div class="topic-card-meta">
<div>创建: {{ formatDate(topic.created_at) }}</div>
<div>创作: {{ topic.generated_at ? formatDate(topic.generated_at) : '-' }}</div>
<div>发布: {{ topic.published_at ? formatDate(topic.published_at) : '-' }}</div>
</div>
<div class="topic-card-actions">
<el-button size="small" @click="openPreview(topic)" type="primary">预览</el-button>
<el-button size="small" type="success" :disabled="topic.status !== 'pending'" @click="createTopic(topic)">创作</el-button>
<el-button size="small" type="warning" :disabled="topic.status !== 'review'" @click="optimizeTopic(topic)">审查</el-button>
<el-button v-if="topic.status === 'ready'" size="small" type="primary" @click="handlePublish(topic)">发布</el-button>
<el-button size="small" type="danger" @click="deleteTopic(topic.id)">删除</el-button>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<nav class="mobile-nav">
<button class="mobile-nav-btn" @click="redirectToPage('/')">📊 概览</button>
<button class="mobile-nav-btn active">📋 选题</button>
<button class="mobile-nav-btn" @click="redirectToPage('logs.html')">📄 日志</button>
<button v-if="isAdmin" class="mobile-nav-btn" @click="redirectToPage('users.html')">👥 用户</button>
</nav>
</div>
<script src="/static/vue/vue.global.js"></script>
<script src="/static/element-plus/index.full.min.js"></script>
<script>
const TopicsApp = {
data() {
return {
currentPage: 'topics',
isLoggedIn: false,
isAdmin: false,
currentUser: { username: '' },
loadingTable: false,
selectedTopicIds: [],
filterStatus: '',
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
topics: []
}
},
computed: {
filteredTopics() {
if (!this.topics || !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: {
async fetchTopics() {
this.loadingTable = true;
try {
const token = localStorage.getItem('authToken');
if (!token) {
this.$message.error('请先登录');
setTimeout(() => window.location.href = '/', 1500);
this.loadingTable = false;
return;
}
const response = await fetch('/api/topics', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `请求失败: ${response.status}`);
}
const data = await response.json();
this.topics = data || [];
this.$message.success('选题加载成功');
} catch (error) {
console.error('获取选题失败:', error);
this.$message.error(`获取选题失败: ${error.message}`);
this.topics = [];
} finally {
this.loadingTable = false;
}
},
refreshAll() { this.$message.info('执行批量刷新'); },
async triggerGenerateSelected() {
if (!this.selectedTopicIds.length) return;
this.$message.success('批量创作已启动');
this.selectedTopicIds = [];
await this.fetchTopics();
},
async triggerOptimizeSelected() {
if (!this.selectedTopicIds.length) return;
this.$message.success('批量优化已启动');
this.selectedTopicIds = [];
await this.fetchTopics();
},
openPreview(topic) { this.$message.info('预览:' + topic.title); },
async createTopic(topic) {
if (topic.status !== 'pending') {
this.$message.info('仅待处理选题可创作');
return;
}
try {
const token = localStorage.getItem('authToken');
if (!token) {
this.$message.error('请先登录');
return;
}
const response = await fetch('/api/system/generate/run', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ topic_id: topic.id })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `请求失败: ${response.status}`);
}
const data = await response.json();
this.$message.success(`创作完成: ${topic.title}`);
await this.fetchTopics();
} catch (error) {
console.error('创作失败:', error);
this.$message.error(`创作失败: ${error.message}`);
}
},
async optimizeTopic(topic) {
if (topic.status !== 'review') {
this.$message.info('仅待审查选题可优化');
return;
}
try {
const token = localStorage.getItem('authToken');
if (!token) {
this.$message.error('请先登录');
return;
}
const response = await fetch('/api/optimizer/run', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ topic_id: topic.id })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `请求失败: ${response.status}`);
}
const data = await response.json();
this.$message.success(`优化完成: ${topic.title}`);
await this.fetchTopics();
} catch (error) {
console.error('优化失败:', error);
this.$message.error(`优化失败: ${error.message}`);
}
},
async handlePublish(topic) {
if (topic.status !== 'ready') {
this.$message.info('仅待发布选题可发布');
return;
}
try {
const token = localStorage.getItem('authToken');
if (!token) {
this.$message.error('请先登录');
return;
}
const response = await fetch('/api/publishing/create', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ topic_id: topic.id })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `请求失败: ${response.status}`);
}
const data = await response.json();
this.$message.success(`发布成功: ${topic.title}`);
await this.fetchTopics();
} catch (error) {
console.error('发布失败:', error);
this.$message.error(`发布失败: ${error.message}`);
}
},
async deleteTopic(id) {
this.$confirm('确定删除?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
.then(async () => {
try {
const token = localStorage.getItem('authToken');
if (!token) {
this.$message.error('请先登录');
window.location.href = '/';
return;
}
const response = await fetch(`/api/topics/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token }
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `请求失败: ${response.status}`);
}
this.$message.success('删除成功');
await this.fetchTopics();
} catch (error) {
console.error('删除失败:', error);
this.$message.error(`删除失败: ${error.message}`);
}
})
.catch(() => {});
},
handleLogout() { localStorage.removeItem('authToken'); window.location.href = '/'; },
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
formatDate(dateStr) {
if (!dateStr) return '-';
try {
return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
});
} catch (e) { return dateStr; }
},
getStatusType(status) {
const map = { 'pending': 'warning', 'review': 'danger', 'ready': 'success', 'published': 'info' };
return map[status] || 'primary';
}
},
mounted() {
console.log('[DEBUG] TopicsApp mounted');
const token = localStorage.getItem('authToken');
console.log('[DEBUG] Token exists:', !!token);
if (!token) { window.location.href = '/'; return; }
// 解析 URL filter 参数
const urlParams = new URLSearchParams(window.location.search);
const filter = urlParams.get('filter');
console.log('[DEBUG] URL filter:', filter);
if (filter) { this.filterStatus = filter; }
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(response => response.ok ? response.json() : Promise.reject())
.then(data => {
console.log('[DEBUG] Auth success, user:', data.user);
this.currentUser = data.user;
this.isAdmin = data.user.role === 'admin';
this.isLoggedIn = true;
this.fetchTopics();
})
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/'; });
}
};
const app = Vue.createApp(TopicsApp);
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>
-224
View File
@@ -1,224 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 用户管理</title>
<link rel="stylesheet" href="/static/element-plus/index.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; width: 100%; }
.sidebar { width: 180px; background: white; padding: 16px; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.sidebar-btn { width: 100%; text-align: left; padding: 12px 16px; border: none; background: transparent; border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.3s; color: #606266; font-size: 14px; }
.sidebar-btn:hover { background: #f5f7fa; color: #409eff; }
.sidebar-btn.active { background: #ecf5ff; color: #409eff; font-weight: 600; }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.mobile-nav-btn { flex: 1; border: none; background: transparent; padding: 12px; text-align: center; font-size: 12px; color: #606266; cursor: pointer; }
.mobile-nav-btn.active { color: #409eff; font-weight: 600; }
@media (max-width: 768px) {
.sidebar { display: none; }
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
}
/* users.html 移动端优化 */
@media (max-width: 768px) {
.user-table { display: none; }
.user-card-list { display: block; margin: 0 -16px; }
.user-card {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.user-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.user-card-name { font-size: 16px; font-weight: 600; }
.user-card-meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
font-size: 12px;
color: #606266;
margin-bottom: 12px;
}
.user-card-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
padding-top: 8px;
border-top: 1px solid #ebeef5;
}
}
</style>
</head>
<body>
<div id="app">
<nav class="navbar">
<div class="navbar-content">
<h1 class="navbar-title">宇之然内容创作平台 - 用户管理</h1>
<div class="navbar-user">
<div class="user-info"><div class="avatar">{{ currentUser.username.charAt(0).toUpperCase() }}</div><span>{{ currentUser.username }}</span></div>
<el-button type="danger" size="small" @click="handleLogout">退出</el-button>
</div>
</div>
</nav>
<div class="main-content">
<aside class="sidebar">
<button class="sidebar-btn" @click="redirectToPage('/')">📊 系统概览</button>
<button class="sidebar-btn" @click="redirectToPage('topics.html')">📋 选题管理</button>
<button class="sidebar-btn" @click="redirectToPage('logs.html')">📄 系统日志</button>
<button v-if="isAdmin" class="sidebar-btn active">👥 用户管理</button>
</aside>
<main class="content-area">
<div class="card">
<h2 style="font-size: 24px; font-weight: 600; margin-bottom: 24px;">👥 用户管理</h2>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<h3 style="font-size: 18px; font-weight: 600;">用户列表</h3>
<el-button type="primary" @click="addUser">+ 新建用户</el-button>
</div>
<el-table :data="users" stripe :cell-class-name="getMobileUserCellClass" class="user-table">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="username" label="用户名"></el-table-column>
<el-table-column prop="role" label="角色" width="100">
<template #default="scope"><el-tag :type="scope.row.role === 'admin' ? 'danger' : 'info'">{{ scope.row.role === 'admin' ? '管理员' : '编辑' }}</el-tag></template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button size="small" type="danger" @click="deleteUser(scope.row.id)" :disabled="scope.row.role === 'admin'">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</main>
</div>
<nav class="mobile-nav">
<button class="mobile-nav-btn" @click="redirectToPage('/')">📊 概览</button>
<button class="mobile-nav-btn" @click="redirectToPage('topics.html')">📋 选题</button>
<button class="mobile-nav-btn" @click="redirectToPage('logs.html')">📄 日志</button>
<button v-if="isAdmin" class="mobile-nav-btn active">👥 用户</button>
</nav>
</div>
<script src="/static/vue/vue.global.js"></script>
<script src="/static/element-plus/index.full.min.js"></script>
<script>
const UsersApp = {
data() { return { isLoggedIn: false, isAdmin: false, currentUser: { username: '' }, users: [] } },
methods: {
async fetchUsers() {
try {
const token = localStorage.getItem('authToken');
if (!token) {
this.$message.error('请先登录');
return;
}
const response = await fetch('/api/admin/users', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `请求失败: ${response.status}`);
}
const data = await response.json();
this.users = data || [];
this.$message.success('用户列表加载成功');
} catch (error) {
console.error('获取用户失败:', error);
this.$message.error(`获取用户失败: ${error.message}`);
this.users = [];
}
},
async addUser() {
try {
const token = localStorage.getItem('authToken');
if (!token) {
this.$message.error('请先登录');
return;
}
// 使用默认用户名,添加时间戳避免重复
const username = '新用户' + Date.now().toString().slice(-4);
const response = await fetch('/api/admin/users', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ username: username, role: 'editor' })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `请求失败: ${response.status}`);
}
const newUser = await response.json();
this.users.push(newUser);
this.$message.success('添加用户成功');
} catch (error) {
console.error('添加用户失败:', error);
this.$message.error(`添加用户失败: ${error.message}`);
}
},
async deleteUser(id) {
if (id === 'admin') {
this.$message.warning('不能删除管理员用户');
return;
}
this.$confirm('确定删除该用户?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
.then(async () => {
try {
const token = localStorage.getItem('authToken');
if (!token) {
this.$message.error('请先登录');
window.location.href = '/';
return;
}
const response = await fetch(`/api/admin/users/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token }
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `请求失败: ${response.status}`);
}
this.users = this.users.filter(u => u.id !== id);
this.$message.success('删除用户成功');
} catch (error) {
console.error('删除用户失败:', error);
this.$message.error(`删除用户失败: ${error.message}`);
}
})
.catch(() => {});
},
handleLogout() { localStorage.removeItem('authToken'); window.location.href = '/'; },
redirectToPage(page) { window.location.href = '/' + page; },
formatDate(dateStr) { if (!dateStr) return '-'; return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }
},
mounted() {
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = '/'; return; }
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(response => response.ok ? response.json() : Promise.reject())
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; if (!this.isAdmin) { this.$message.warning('需要管理员权限'); window.location.href = '/'; } else { this.fetchUsers(); } })
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/'; });
}
};
const app = Vue.createApp(UsersApp);
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>
-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>