feat: 完成布局优化 - 操作列固定、批量按钮自适应、分类标签带数量
优化内容: 1. 表格布局: - 使用 calc(100vw - 160px) 确保表格不超出视口 - 操作列 fixed='right' 固定在右侧,宽度 300px - 按钮 3 个后自动换行 (max-width: 200px) - 恢复合理列宽,不再过度压缩 2. 批量操作区域: - 容器改为 inline-block,宽度自适应按钮内容 - 背景宽度与按钮总宽度匹配 3. 分类标签: - 显示数量 (如 '待处理 (20)') - 点击切换筛选,去掉误导的 'X' 图标 4. 删除功能: - 操作列增加删除按钮 - 删除前弹出确认对话框 5. 系统日志: - 修复后端日志路径 (parents[4]) - 404 时显示友好提示 6. 其他: - 左侧菜单宽度 160px - 所有功能保留 (登录、用户管理、批量操作等)
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# 宇之然内容创作平台 - 前端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="企业级内容创作管理系统前端"
|
||||
@@ -0,0 +1,7 @@
|
||||
(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);
|
||||
})();
|
||||
@@ -1,76 +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="/static/vue.global.prod.js"></script>
|
||||
<link rel="stylesheet" href="/static/element-plus.css" />
|
||||
<script src="/static/element-plus.full.js"></script>
|
||||
<style>
|
||||
body { margin: 20px; font-family: sans-serif; }
|
||||
.card { border: 1px solid #ddd; padding: 20px; margin: 10px 0; border-radius: 8px; }
|
||||
.stat-value { font-size: 2rem; color: #409EFF; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>宇之然内容创作平台</h1>
|
||||
<div class="card">
|
||||
<h2>系统概览</h2>
|
||||
<div v-if="status">
|
||||
<p>选题总数: {{ status.total_topics }}</p>
|
||||
<p>待发布: {{ status.topics_by_status?.['待发布'] || 0 }}</p>
|
||||
<p>待处理: {{ status.topics_by_status?.['待处理'] || 0 }}</p>
|
||||
</div>
|
||||
<div v-else>加载中...</div>
|
||||
<button @click="refresh">刷新</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>选题列表</h2>
|
||||
<div v-if="topics.length">
|
||||
<ul>
|
||||
<li v-for="t in topics" :key="t.id">
|
||||
{{ t.id }} - {{ t.title }} - {{ t.status }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else-if="topics">无选题</div>
|
||||
<div v-else>加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp, ref, onMounted } = Vue;
|
||||
createApp({
|
||||
setup() {
|
||||
const API_BASE = '';
|
||||
const status = ref(null);
|
||||
const topics = ref([]);
|
||||
|
||||
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;
|
||||
console.log('数据加载成功', s, t);
|
||||
} catch (e) {
|
||||
console.error('刷新失败:', e);
|
||||
alert('加载失败: ' + e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log('应用启动');
|
||||
refresh();
|
||||
});
|
||||
|
||||
return { status, topics, refresh };
|
||||
}
|
||||
}).use(ElementPlus).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,181 @@
|
||||
<!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="/static/vue.global.prod.js?v=20260421-0830"></script>
|
||||
<link rel="stylesheet" href="/static/element-plus.css?v=20260421-0830" />
|
||||
<script src="/static/element-plus.full.js?v=20260421-0830"></script>
|
||||
<style>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 32px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.login-title {
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.login-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
margin-bottom: 16px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.login-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.login-button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
.login-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h1 class="login-title">宇之然内容创作平台</h1>
|
||||
|
||||
<form @submit.prevent="handleLogin">
|
||||
<input
|
||||
v-model="username"
|
||||
class="login-input"
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
required
|
||||
autocomplete="username"
|
||||
/>
|
||||
<input
|
||||
v-model="password"
|
||||
class="login-input"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<button
|
||||
class="login-button"
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
:class="{'opacity-60 cursor-not-allowed': loading}"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="login-footer">
|
||||
只有管理员用户可登录访问系统
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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('auth_token', data.token);
|
||||
localStorage.setItem('user_role', data.role || 'admin');
|
||||
|
||||
ElMessage.success('登录成功!正在跳转...');
|
||||
|
||||
// 延迟跳转,让用户看到成功消息
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
ElMessage.error(data.message || data.error || '登录失败,请检查用户名和密码');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录请求失败:', error);
|
||||
ElMessage.error('网络连接失败,请检查服务是否正常运行');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
loading,
|
||||
handleLogin
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
app.use(ElementPlus);
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
# 宇之然内容创作平台 - 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,476 @@
|
||||
<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>
|
||||
@@ -0,0 +1,398 @@
|
||||
// 修复后的 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
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 67 B |
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 287 B |
Binary file not shown.
|
After Width: | Height: | Size: 67 B |
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 289 B |
File diff suppressed because one or more lines are too long
@@ -0,0 +1,116 @@
|
||||
// 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...');
|
||||
}
|
||||
Reference in New Issue
Block a user