fix: 使用完整模板,保留所有网页功能
✅ 包含所有原有功能: - 阈值设置(CPU/内存/磁盘/间隔) - 邮件规则管理 - 自定义链接添加 - 进程列表显示 - Docker容器监控 - Cron任务管理 - 系统资源实时监控 - 配置导入导出 📝 templates_full.txt: 从v3.3.5提取的完整HTML模板(2366行) 📝 templates.py: 动态加载完整模板并替换版本号
This commit is contained in:
595
templates.py
595
templates.py
@@ -1,590 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTML 模板
|
||||
HTML 模板 - 包含所有完整功能
|
||||
"""
|
||||
|
||||
from config import APP_NAME, APP_VERSION
|
||||
|
||||
|
||||
def get_html_template():
|
||||
"""返回完整的HTML模板"""
|
||||
"""返回完整的HTML模板(包含所有原有功能)"""
|
||||
# 读取完整模板文件
|
||||
import os
|
||||
template_file = os.path.join(os.path.dirname(__file__), 'templates_full.txt')
|
||||
|
||||
try:
|
||||
with open(template_file, 'r', encoding='utf-8') as f:
|
||||
template_content = f.read()
|
||||
|
||||
# 替换版本号
|
||||
template_content = template_content.replace('v2.1', APP_VERSION)
|
||||
template_content = template_content.replace('v3.3.1', APP_VERSION)
|
||||
|
||||
return template_content
|
||||
except:
|
||||
# 如果文件不存在,返回基本模板
|
||||
return get_basic_template()
|
||||
|
||||
|
||||
def get_basic_template():
|
||||
"""基本模板(备用)"""
|
||||
return '''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>''' + APP_NAME + '''</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.remarkable.ai/css/remixicon.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); }
|
||||
.card { background: rgba(30, 41, 59, 0.6); backdrop-filter: blur(10px); border: 1px solid rgba(71, 85, 105, 0.4); }
|
||||
.btn { transition: all 0.2s; }
|
||||
.btn:hover { transform: translateY(-1px); }
|
||||
.status-running { background: rgba(34, 197, 94, 0.2); border-color: rgba(34, 197, 94, 0.5); color: #22c55e; }
|
||||
.status-stopped { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.5); color: #ef4444; }
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 50; display: flex; align-items: center; justify-content: center; }
|
||||
.modal-content { max-width: 600px; width: 90%; max-height: 80vh; overflow: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen text-gray-100">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 头部 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold flex items-center gap-3">
|
||||
<i class="ri-dashboard-3-line text-blue-400"></i>
|
||||
''' + APP_NAME + '''
|
||||
<span class="text-sm text-gray-500">''' + APP_VERSION + '''</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-400 text-sm">对外IP:</span>
|
||||
<input type="text" id="externalIp" value="121.40.164.32" class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-sm w-28" onchange="saveExternalIp()">
|
||||
</div>
|
||||
<button onclick="refreshAll()" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i class="ri-refresh-line"></i> 刷新
|
||||
</button>
|
||||
<button onclick="exportConfig()" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i class="ri-download-line"></i> 导出配置
|
||||
</button>
|
||||
<label class="btn bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-lg flex items-center gap-2 cursor-pointer">
|
||||
<i class="ri-upload-line"></i> 导入配置
|
||||
<input type="file" accept=".json" onchange="importConfig(event)" class="hidden">
|
||||
</label>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-gray-400 text-xs">间隔:</span>
|
||||
<input type="number" id="refreshInterval" value="30" min="5" max="300" class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-xs w-12" onchange="updateRefreshInterval()">
|
||||
<span class="text-gray-500 text-xs">s</span>
|
||||
</div>
|
||||
<span id="updateTime" class="text-gray-400 text-sm"></span>
|
||||
<div id="connectionStatus" class="flex items-center gap-1 px-2 py-1 rounded bg-green-500/20">
|
||||
<div class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div>
|
||||
<span class="text-green-400 text-xs">已连接</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="flex gap-4 mb-6 border-b border-gray-700">
|
||||
<button onclick="switchTab('projects')" class="tab-btn active px-4 py-2 text-gray-300 hover:text-white" data-tab="projects">
|
||||
<i class="ri-apps-line"></i> 项目服务
|
||||
</button>
|
||||
<button onclick="switchTab('system')" class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="system">
|
||||
<i class="ri-dashboard-2-line"></i> 系统监控
|
||||
</button>
|
||||
<button onclick="switchTab('cron')" class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="cron">
|
||||
<i class="ri-time-line"></i> Cron管理
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 项目服务 Tab -->
|
||||
<div id="projectsTab" class="tab-content">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- 项目卡片将通过JS动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统监控 Tab -->
|
||||
<div id="systemTab" class="tab-content hidden">
|
||||
<div class="card rounded-xl p-6 mb-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-bold flex items-center gap-2">
|
||||
<i class="ri-activity-line text-blue-400"></i> 系统资源监控
|
||||
</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="realtimeCheck" onchange="toggleRealtimeMonitor()">
|
||||
<span class="text-sm text-gray-400">实时监控</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPU -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-gray-400 flex items-center gap-2">
|
||||
<i class="ri-cpu-line text-blue-400"></i> CPU使用率
|
||||
</span>
|
||||
<span id="cpuPercent" class="text-2xl font-bold text-blue-400">-</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-4">
|
||||
<div id="cpuBar" class="bg-blue-500 h-4 rounded-full transition-all duration-500" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内存 -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-gray-400 flex items-center gap-2">
|
||||
<i class="ri-database-2-line text-green-400"></i> 内存使用率
|
||||
</span>
|
||||
<span id="memPercent" class="text-2xl font-bold text-green-400">-</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-4">
|
||||
<div id="memBar" class="bg-green-500 h-4 rounded-full transition-all duration-500" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 磁盘 -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-gray-400 flex items-center gap-2">
|
||||
<i class="ri-hard-drive-2-line text-orange-400"></i> 磁盘使用率
|
||||
</span>
|
||||
<span id="diskPercent" class="text-2xl font-bold text-orange-400">-</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-4">
|
||||
<div id="diskBar" class="bg-orange-500 h-4 rounded-full transition-all duration-500" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网速 -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i class="ri-upload-line text-cyan-400"></i>
|
||||
<span class="text-gray-400">上传速度</span>
|
||||
</div>
|
||||
<span id="uploadSpeed" class="text-xl font-bold text-cyan-400">-</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i class="ri-download-line text-purple-400"></i>
|
||||
<span class="text-gray-400">下载速度</span>
|
||||
</div>
|
||||
<span id="downloadSpeed" class="text-xl font-bold text-purple-400">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cron管理 Tab -->
|
||||
<div id="cronTab" class="tab-content hidden">
|
||||
<div class="card rounded-xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-bold flex items-center gap-2">
|
||||
<i class="ri-time-line text-blue-400"></i> Cron任务管理
|
||||
</h2>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="syncCronFromSystem()" class="btn bg-gray-600 hover:bg-gray-700 px-3 py-2 rounded-lg flex items-center gap-1">
|
||||
<i class="ri-refresh-line"></i> 从系统同步
|
||||
</button>
|
||||
<button onclick="showAddCronModal()" class="btn bg-blue-600 hover:bg-blue-700 px-3 py-2 rounded-lg flex items-center gap-1">
|
||||
<i class="ri-add-line"></i> 添加任务
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cron任务列表 -->
|
||||
<div id="cronTasksList" class="space-y-3">
|
||||
<!-- 任务将通过JS动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold">''' + APP_NAME + ''' ''' + APP_VERSION + '''</h1>
|
||||
<p>模板加载失败,请检查 templates_full.txt 文件</p>
|
||||
</div>
|
||||
|
||||
<!-- 添加Cron任务模态框 -->
|
||||
<div id="addCronModal" class="modal-overlay hidden">
|
||||
<div class="modal-content card rounded-xl p-6">
|
||||
<h3 class="text-xl font-bold mb-4 flex items-center gap-2">
|
||||
<i class="ri-add-circle-line text-blue-400"></i> 添加Cron任务
|
||||
</h3>
|
||||
<form onsubmit="addCronTask(event)">
|
||||
<div class="mb-4">
|
||||
<label class="text-gray-400 text-sm mb-1 block">任务名称</label>
|
||||
<input type="text" id="cronName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="text-gray-400 text-sm mb-1 block">Cron表达式</label>
|
||||
<input type="text" id="cronExpression" required placeholder="0 4 * * *" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="text-gray-400 text-sm mb-1 block">执行命令</label>
|
||||
<input type="text" id="cronCommand" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="text-gray-400 text-sm mb-1 block">描述</label>
|
||||
<input type="text" id="cronDescription" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg">添加</button>
|
||||
<button type="button" onclick="hideAddCronModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentTab = 'projects';
|
||||
let projects = [];
|
||||
let externalIp = '121.40.164.32';
|
||||
let thresholds = { cpu: 80, memory: 85, disk: 90, interval: 60 };
|
||||
|
||||
// Tab切换
|
||||
function switchTab(tab) {
|
||||
currentTab = tab;
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tab);
|
||||
btn.classList.toggle('border-b-2', btn.dataset.tab === tab);
|
||||
btn.classList.toggle('border-blue-500', btn.dataset.tab === tab);
|
||||
});
|
||||
document.getElementById('projectsTab').classList.toggle('hidden', tab !== 'projects');
|
||||
document.getElementById('systemTab').classList.toggle('hidden', tab !== 'system');
|
||||
document.getElementById('cronTab').classList.toggle('hidden', tab !== 'cron');
|
||||
|
||||
if (tab === 'cron') {
|
||||
loadCronTasks();
|
||||
}
|
||||
if (tab === 'system') {
|
||||
loadThresholds();
|
||||
loadSystemStats();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载项目列表
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const res = await fetch('/api/projects');
|
||||
const data = await res.json();
|
||||
projects = data.projects;
|
||||
renderProjects();
|
||||
updateConnectionStatus(true);
|
||||
} catch (e) {
|
||||
updateConnectionStatus(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染项目卡片
|
||||
function renderProjects() {
|
||||
const container = document.querySelector('#projectsTab .grid');
|
||||
container.innerHTML = projects.map(p => `
|
||||
<div class="card rounded-xl p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-bold flex items-center gap-2">
|
||||
<i class="ri-${p.type === 'web' ? 'app' : 'server'}-line text-blue-400"></i>
|
||||
${p.name}
|
||||
</h3>
|
||||
<span class="px-2 py-1 rounded text-xs ${p.status === 'running' ? 'status-running' : 'status-stopped'}">
|
||||
${p.status === 'running' ? '运行中' : '已停止'}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm mb-3">${p.description || ''}</p>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500 mb-3">
|
||||
<i class="ri-git-branch-line"></i> ${p.version || 'v1.0'}
|
||||
<span class="ml-2">端口: ${p.ports.join(', ')}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
${p.type === 'web' ? `
|
||||
<button onclick="startProject('${p.id}')" class="btn bg-green-600 hover:bg-green-700 px-3 py-1 rounded text-sm">
|
||||
<i class="ri-play-line"></i>
|
||||
</button>
|
||||
<button onclick="stopProject('${p.id}')" class="btn bg-red-600 hover:bg-red-700 px-3 py-1 rounded text-sm">
|
||||
<i class="ri-stop-line"></i>
|
||||
</button>
|
||||
<button onclick="restartProject('${p.id}')" class="btn bg-yellow-600 hover:bg-yellow-700 px-3 py-1 rounded text-sm">
|
||||
<i class="ri-restart-line"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<a href="${p.health_url || '#'}" target="_blank" class="btn bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded text-sm">
|
||||
<i class="ri-external-link-line"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('updateTime').textContent = new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
// 项目操作
|
||||
async function startProject(id) {
|
||||
await fetch(`/api/projects/${id}/start`, { method: 'POST' });
|
||||
setTimeout(loadProjects, 2000);
|
||||
}
|
||||
|
||||
async function stopProject(id) {
|
||||
await fetch(`/api/projects/${id}/stop`, { method: 'POST' });
|
||||
setTimeout(loadProjects, 1000);
|
||||
}
|
||||
|
||||
async function restartProject(id) {
|
||||
await fetch(`/api/projects/${id}/restart`, { method: 'POST' });
|
||||
setTimeout(loadProjects, 3000);
|
||||
}
|
||||
|
||||
// 加载Cron任务
|
||||
async function loadCronTasks() {
|
||||
try {
|
||||
const res = await fetch('/api/cron/tasks');
|
||||
const data = await res.json();
|
||||
renderCronTasks(data.tasks);
|
||||
} catch (e) {
|
||||
console.error('加载Cron任务失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染Cron任务列表
|
||||
function renderCronTasks(tasks) {
|
||||
const container = document.getElementById('cronTasksList');
|
||||
container.innerHTML = tasks.map(t => `
|
||||
<div class="card rounded-lg p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 class="font-bold">${t.name}</h4>
|
||||
<p class="text-gray-400 text-sm">${t.human_time || t.expression}</p>
|
||||
<p class="text-gray-500 text-xs mt-1">${t.command}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-1 rounded text-xs ${t.enabled ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'}">
|
||||
${t.enabled ? '启用' : '禁用'}
|
||||
</span>
|
||||
<button onclick="toggleCron(${t.id})" class="btn bg-gray-600 hover:bg-gray-700 px-2 py-1 rounded text-sm">
|
||||
<i class="ri-toggle-line"></i>
|
||||
</button>
|
||||
<button onclick="deleteCron(${t.id})" class="btn bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-sm">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Cron操作
|
||||
async function toggleCron(id) {
|
||||
await fetch(`/api/cron/tasks/${id}/toggle`, { method: 'POST' });
|
||||
loadCronTasks();
|
||||
}
|
||||
|
||||
async function deleteCron(id) {
|
||||
if (confirm('确定删除此任务?')) {
|
||||
await fetch(`/api/cron/tasks/${id}`, { method: 'DELETE' });
|
||||
loadCronTasks();
|
||||
}
|
||||
}
|
||||
|
||||
async function syncCronFromSystem() {
|
||||
await fetch('/api/cron/sync', { method: 'POST' });
|
||||
loadCronTasks();
|
||||
}
|
||||
|
||||
function showAddCronModal() {
|
||||
document.getElementById('addCronModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideAddCronModal() {
|
||||
document.getElementById('addCronModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function addCronTask(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
name: document.getElementById('cronName').value,
|
||||
expression: document.getElementById('cronExpression').value,
|
||||
command: document.getElementById('cronCommand').value,
|
||||
description: document.getElementById('cronDescription').value
|
||||
};
|
||||
|
||||
await fetch('/api/cron/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
hideAddCronModal();
|
||||
loadCronTasks();
|
||||
}
|
||||
|
||||
// 系统监控
|
||||
async function loadSystemStats() {
|
||||
try {
|
||||
const res = await fetch('/api/system/stats');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.available) {
|
||||
document.getElementById('cpuPercent').textContent = data.cpu.percent + '%';
|
||||
document.getElementById('cpuBar').style.width = data.cpu.percent + '%';
|
||||
|
||||
document.getElementById('memPercent').textContent = data.memory.percent + '%';
|
||||
document.getElementById('memBar').style.width = data.memory.percent + '%';
|
||||
|
||||
document.getElementById('diskPercent').textContent = data.disk.percent + '%';
|
||||
document.getElementById('diskBar').style.width = data.disk.percent + '%';
|
||||
|
||||
document.getElementById('uploadSpeed').textContent = data.network.upload_speed_kb + ' KB/s';
|
||||
document.getElementById('downloadSpeed').textContent = data.network.download_speed_kb + ' KB/s';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载系统状态失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadThresholds() {
|
||||
try {
|
||||
const response = await fetch('/api/alerts');
|
||||
const config = await response.json();
|
||||
|
||||
if (config.thresholds) {
|
||||
thresholds = config.thresholds;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载阈值配置失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出配置
|
||||
async function exportConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/config/export');
|
||||
const config = await response.json();
|
||||
|
||||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `project-panel-config-${new Date().toISOString().slice(0,10)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
alert('配置导出成功!');
|
||||
} catch (e) {
|
||||
alert('导出失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 导入配置
|
||||
async function importConfig(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!confirm('导入配置将覆盖当前所有设置,是否继续?')) {
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const config = JSON.parse(text);
|
||||
|
||||
const response = await fetch('/api/config/import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
alert('导入失败: ' + result.error);
|
||||
} else {
|
||||
alert(`配置导入成功!\\n项目数: ${result.projects_count}\\n外部服务数: ${result.services_count}`);
|
||||
location.reload();
|
||||
}
|
||||
} catch (e) {
|
||||
alert('导入失败: ' + e.message);
|
||||
}
|
||||
|
||||
event.target.value = '';
|
||||
}
|
||||
|
||||
// 网页设置
|
||||
async function saveExternalIp() {
|
||||
externalIp = document.getElementById('externalIp').value.trim();
|
||||
|
||||
try {
|
||||
await fetch('/api/web-settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ external_ip: externalIp, refresh_interval: parseInt(document.getElementById('refreshInterval').value) || 30 })
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('保存失败:', e);
|
||||
}
|
||||
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
function updateRefreshInterval() {
|
||||
const seconds = Math.max(5, Math.min(300, parseInt(document.getElementById('refreshInterval').value) || 30));
|
||||
document.getElementById('refreshInterval').value = seconds;
|
||||
|
||||
fetch('/api/web-settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ external_ip: externalIp, refresh_interval: seconds })
|
||||
}).catch(e => console.error('保存失败:', e));
|
||||
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, seconds * 1000);
|
||||
}
|
||||
|
||||
function refreshAll() {
|
||||
if (currentTab === 'projects') {
|
||||
loadProjects();
|
||||
} else if (currentTab === 'cron') {
|
||||
loadCronTasks();
|
||||
} else {
|
||||
loadSystemStats();
|
||||
}
|
||||
}
|
||||
|
||||
function updateConnectionStatus(ok) {
|
||||
const statusEl = document.getElementById('connectionStatus');
|
||||
if (ok) {
|
||||
statusEl.innerHTML = '<div class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div><span class="text-green-400 text-xs">已连接</span>';
|
||||
statusEl.className = 'flex items-center gap-1 px-2 py-1 rounded bg-green-500/20';
|
||||
} else {
|
||||
statusEl.innerHTML = '<div class="w-2 h-2 rounded-full bg-red-400"></div><span class="text-red-400 text-xs">断开</span>';
|
||||
statusEl.className = 'flex items-center gap-1 px-2 py-1 rounded bg-red-500/20';
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
loadProjects();
|
||||
loadWebSettings();
|
||||
|
||||
let refreshIntervalMs = 30 * 1000;
|
||||
let refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, refreshIntervalMs);
|
||||
|
||||
// 加载网页设置
|
||||
async function loadWebSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/web-settings');
|
||||
const settings = await response.json();
|
||||
|
||||
if (settings.external_ip) {
|
||||
externalIp = settings.external_ip;
|
||||
document.getElementById('externalIp').value = settings.external_ip;
|
||||
}
|
||||
if (settings.refresh_interval) {
|
||||
document.getElementById('refreshInterval').value = settings.refresh_interval;
|
||||
clearInterval(refreshTimer);
|
||||
refreshIntervalMs = settings.refresh_interval * 1000;
|
||||
refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, refreshIntervalMs);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载网页设置失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 实时监控
|
||||
let realtimeTimer = null;
|
||||
function toggleRealtimeMonitor() {
|
||||
const checked = document.getElementById('realtimeCheck').checked;
|
||||
if (checked) {
|
||||
realtimeTimer = setInterval(loadSystemStats, 2000);
|
||||
} else {
|
||||
clearInterval(realtimeTimer);
|
||||
realtimeTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 定期检查连接状态
|
||||
setInterval(() => {
|
||||
fetch('/api/projects').then(() => updateConnectionStatus(true)).catch(() => updateConnectionStatus(false));
|
||||
}, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
</html>'''
|
||||
2366
templates_full.txt
Normal file
2366
templates_full.txt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user