202 lines
10 KiB
HTML
202 lines
10 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="zh-CN">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>PPT智能管理系统 V2.0</title>
|
||
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
||
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
|
||
|
|
<style>
|
||
|
|
.log-scroll { max-height: 280px; overflow-y: auto; }
|
||
|
|
.progress-bar-fill { transition: width 0.3s ease; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 min-h-screen text-white">
|
||
|
|
<div class="container mx-auto px-4 py-6 max-w-6xl">
|
||
|
|
<div class="text-center mb-8">
|
||
|
|
<h1 class="text-4xl font-bold bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-500 bg-clip-text text-transparent mb-2">
|
||
|
|
PPT智能管理系统 <span class="text-cyan-300 text-xl">v2.0</span>
|
||
|
|
</h1>
|
||
|
|
<p class="text-slate-400">基于锚点定位 | 原生图表更新 | 插件化架构 | WebSocket实时推送</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="grid lg:grid-cols-3 gap-5">
|
||
|
|
<div class="lg:col-span-2 space-y-5">
|
||
|
|
<div class="bg-slate-800/60 rounded-2xl p-5 border border-slate-700">
|
||
|
|
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||
|
|
<span class="w-8 h-8 bg-cyan-500/20 rounded-lg flex items-center justify-center text-cyan-400">⚙</span>
|
||
|
|
参数配置
|
||
|
|
</h2>
|
||
|
|
|
||
|
|
<div class="grid md:grid-cols-2 gap-4 mb-4">
|
||
|
|
<div>
|
||
|
|
<label class="block text-sm text-slate-400 mb-1">报告年份</label>
|
||
|
|
<input type="number" id="param_year" value="2026"
|
||
|
|
class="w-full bg-slate-700/50 border border-slate-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-cyan-500 outline-none">
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label class="block text-sm text-slate-400 mb-1">季度</label>
|
||
|
|
<select id="param_quarter"
|
||
|
|
class="w-full bg-slate-700/50 border border-slate-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-cyan-500 outline-none">
|
||
|
|
<option value="Q1">Q1 (1-3月)</option>
|
||
|
|
<option value="Q2" selected>Q2 (4-6月)</option>
|
||
|
|
<option value="Q3">Q3 (7-9月)</option>
|
||
|
|
<option value="Q4">Q4 (10-12月)</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button id="btnGenerate" onclick="startGeneration()"
|
||
|
|
class="w-full py-3 bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-600 hover:to-blue-700 rounded-xl font-semibold transition-all transform hover:scale-[1.01] active:scale-[0.99] shadow-lg shadow-cyan-500/20">
|
||
|
|
🚀 开始生成 PPT
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="bg-slate-800/60 rounded-2xl p-5 border border-slate-700">
|
||
|
|
<h2 class="text-lg font-semibold mb-3 flex items-center gap-2">
|
||
|
|
<span class="w-8 h-8 bg-green-500/20 rounded-lg flex items-center justify-center text-green-400">◉</span>
|
||
|
|
实时进度
|
||
|
|
</h2>
|
||
|
|
|
||
|
|
<div class="w-full bg-slate-700/50 rounded-full h-3 mb-4">
|
||
|
|
<div id="progressBar" class="progress-bar-fill bg-gradient-to-r from-green-400 to-cyan-400 h-3 rounded-full" style="width: 0%"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="statusText" class="text-slate-400 text-sm mb-3">等待开始...</div>
|
||
|
|
|
||
|
|
<div class="bg-slate-900/50 rounded-xl overflow-hidden">
|
||
|
|
<div class="bg-slate-800/80 px-3 py-2 text-xs text-slate-500 border-b border-slate-700/50">
|
||
|
|
LOG OUTPUT (实时推送)
|
||
|
|
</div>
|
||
|
|
<div id="logOutput" class="log-scroll p-3 text-xs font-mono space-y-1">
|
||
|
|
<div class="text-slate-600">// 系统已就绪,等待开始</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="space-y-5">
|
||
|
|
<div class="bg-slate-800/60 rounded-2xl p-5 border border-slate-700">
|
||
|
|
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||
|
|
<span class="w-8 h-8 bg-purple-500/20 rounded-lg flex items-center justify-center text-purple-400">📦</span>
|
||
|
|
已加载插件
|
||
|
|
</h2>
|
||
|
|
<div id="pluginList" class="space-y-2">
|
||
|
|
<div class="text-slate-500 text-sm">加载中...</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="bg-slate-800/60 rounded-2xl p-5 border border-slate-700">
|
||
|
|
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||
|
|
<span class="w-8 h-8 bg-amber-500/20 rounded-lg flex items-center justify-center text-amber-400">📋</span>
|
||
|
|
已生成文件
|
||
|
|
</h2>
|
||
|
|
<div id="fileList" class="space-y-2 max-h-64 overflow-y-auto">
|
||
|
|
<div class="text-slate-500 text-sm">暂无</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="bg-gradient-to-br from-cyan-500/10 to-blue-500/10 rounded-2xl p-5 border border-cyan-500/20">
|
||
|
|
<h3 class="font-semibold text-cyan-300 mb-2">✨ 核心增强</h3>
|
||
|
|
<ul class="text-xs text-slate-400 space-y-1.5">
|
||
|
|
<li>• 锚点(Anchor)定位 - 无惧页码变动</li>
|
||
|
|
<li>• 原生图表更新 - 可编辑/保持主题动画</li>
|
||
|
|
<li>• 插件化架构 - 自动扫描注册即插即用</li>
|
||
|
|
<li>• 条件渲染 - 规则判断动态增删页</li>
|
||
|
|
<li>• LLM洞察 - 数据→分析结论自动生成</li>
|
||
|
|
<li>• WebSocket - 进度+日志实时推送</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
let socket = io();
|
||
|
|
let isRunning = false;
|
||
|
|
|
||
|
|
socket.on('connect', () => {
|
||
|
|
appendLog('System', 'WebSocket已连接', '#22c55e');
|
||
|
|
refreshPlugins();
|
||
|
|
refreshFiles();
|
||
|
|
});
|
||
|
|
|
||
|
|
socket.on('progress', (data) => {
|
||
|
|
document.getElementById('progressBar').style.width = `${data.percent}%`;
|
||
|
|
document.getElementById('statusText').textContent = `[${data.percent}%] ${data.message}`;
|
||
|
|
appendLog(data.step, data.message, '#06b6d4');
|
||
|
|
});
|
||
|
|
|
||
|
|
socket.on('complete', (data) => {
|
||
|
|
isRunning = false;
|
||
|
|
document.getElementById('btnGenerate').disabled = false;
|
||
|
|
document.getElementById('btnGenerate').textContent = '🚀 开始生成 PPT';
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
appendLog('DONE', `成功: ${data.output_file}`, '#22c55e');
|
||
|
|
document.getElementById('statusText').innerHTML =
|
||
|
|
`<span class="text-green-400">✓ 完成!</span> <a href="${data.download_url}" class="text-cyan-400 underline ml-1" target="_blank">下载文件</a>`;
|
||
|
|
refreshFiles();
|
||
|
|
} else {
|
||
|
|
appendLog('ERROR', data.error, '#ef4444');
|
||
|
|
document.getElementById('statusText').innerHTML = `<span class="text-red-400">✗ 失败: ${data.error}</span>`;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
function startGeneration() {
|
||
|
|
if (isRunning) return;
|
||
|
|
isRunning = true;
|
||
|
|
|
||
|
|
const btn = document.getElementById('btnGenerate');
|
||
|
|
btn.disabled = true;
|
||
|
|
btn.textContent = '⏳ 生成中...';
|
||
|
|
|
||
|
|
document.getElementById('logOutput').innerHTML = '';
|
||
|
|
|
||
|
|
socket.emit('start_generation', {
|
||
|
|
params: {
|
||
|
|
year: parseInt(document.getElementById('param_year').value),
|
||
|
|
quarter: document.getElementById('param_quarter').value
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function appendLog(tag, message, color = '#94a3b8') {
|
||
|
|
const container = document.getElementById('logOutput');
|
||
|
|
const div = document.createElement('div');
|
||
|
|
div.innerHTML = `<span style="color:${color}">[${tag}]</span> ${message}`;
|
||
|
|
container.appendChild(div);
|
||
|
|
container.scrollTop = container.scrollHeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function refreshPlugins() {
|
||
|
|
const res = await fetch('/api/projects');
|
||
|
|
const data = await res.json();
|
||
|
|
const html = data.plugins.map(p => `
|
||
|
|
<div class="bg-slate-700/30 rounded-lg px-3 py-2.5 border border-slate-600/50">
|
||
|
|
<div class="font-medium text-sm text-white">${p.name}</div>
|
||
|
|
<div class="text-xs text-slate-500">${p.description || ''}</div>
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
document.getElementById('pluginList').innerHTML = html || '<div class="text-slate-500 text-sm">无插件</div>';
|
||
|
|
}
|
||
|
|
|
||
|
|
async function refreshFiles() {
|
||
|
|
const res = await fetch('/api/files');
|
||
|
|
const data = await res.json();
|
||
|
|
const html = data.files.slice(0, 8).map(f => `
|
||
|
|
<a href="/download/${f.name}" target="_blank"
|
||
|
|
class="flex items-center justify-between bg-slate-700/30 rounded-lg px-3 py-2 border border-slate-600/50 hover:bg-slate-700/50 transition-colors">
|
||
|
|
<div>
|
||
|
|
<div class="font-mono text-xs text-white truncate max-w-[150px]">${f.name}</div>
|
||
|
|
<div class="text-[10px] text-slate-500">${f.size} MB</div>
|
||
|
|
</div>
|
||
|
|
<span class="text-cyan-400 text-xs">↓</span>
|
||
|
|
</a>
|
||
|
|
`).join('');
|
||
|
|
document.getElementById('fileList').innerHTML = html || '<div class="text-slate-500 text-sm">暂无</div>';
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|