408 lines
14 KiB
JavaScript
408 lines
14 KiB
JavaScript
//标签页切换
|
||
function switchTab(tab) {
|
||
//更新侧边栏按钮状态
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.classList.remove('bg-indigo-600', 'text-white');
|
||
btn.classList.add('hover:bg-slate-800', 'text-slate-400', 'hover:text-white');
|
||
});
|
||
|
||
const activeBtn = document.getElementById(`tab-${tab}`);
|
||
if (activeBtn) {
|
||
activeBtn.classList.add('bg-indigo-600', 'text-white');
|
||
activeBtn.classList.remove('hover:bg-slate-800', 'text-slate-400', 'hover:text-white');
|
||
}
|
||
|
||
//切换内容区
|
||
document.querySelectorAll('.tab-content').forEach(content => {
|
||
content.classList.add('hidden');
|
||
});
|
||
|
||
const activeContent = document.getElementById(`content-${tab}`);
|
||
if (activeContent) {
|
||
activeContent.classList.remove('hidden');
|
||
}
|
||
|
||
//如果切换到日志页,开始实时加载
|
||
if (tab === 'logs') {
|
||
startLogStreaming();
|
||
} else {
|
||
stopLogStreaming();
|
||
}
|
||
|
||
//如果切换到设置页,加载 .env 文件
|
||
if (tab === 'settings') {
|
||
loadEnvFile();
|
||
}
|
||
|
||
//如果切换到使用指南页,加载 README
|
||
if (tab === 'guide') {
|
||
loadGuide();
|
||
}
|
||
}
|
||
|
||
//日志流控制
|
||
let logInterval = null;
|
||
let lastLogSize = 0;
|
||
|
||
async function startLogStreaming() {
|
||
//立即加载一次
|
||
await loadLogs();
|
||
|
||
//每2秒刷新一次
|
||
logInterval = setInterval(loadLogs, 2000);
|
||
}
|
||
|
||
function stopLogStreaming() {
|
||
if (logInterval) {
|
||
clearInterval(logInterval);
|
||
logInterval = null;
|
||
}
|
||
}
|
||
|
||
async function loadLogs() {
|
||
try {
|
||
const res = await fetch('/api/logs');
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
const logViewer = document.getElementById('log-viewer');
|
||
document.getElementById('log-filename').textContent = data.filename || 'sync_service.log';
|
||
|
||
if (data.logs && data.logs.length > 0) {
|
||
//只在日志有变化时更新
|
||
const newContent = data.logs.map((log, index) =>
|
||
`<div class="log-line border-l-2 border-transparent hover:border-slate-700 hover:bg-slate-800/50 pl-2 py-0.5">
|
||
<span class="opacity-50 select-none mr-2">${index + 1}</span>${escapeHtml(log)}
|
||
</div>`
|
||
).join('');
|
||
|
||
if (logViewer.innerHTML !== newContent) {
|
||
logViewer.innerHTML = newContent;
|
||
//自动滚动到底部
|
||
logViewer.scrollTop = logViewer.scrollHeight;
|
||
}
|
||
} else {
|
||
logViewer.innerHTML = '<div class="text-slate-500 text-center py-8">暂无日志</div>';
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('加载日志失败:', e);
|
||
}
|
||
}
|
||
|
||
//HTML 转义
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
//加载仪表盘数据
|
||
async function loadDashboardData() {
|
||
try {
|
||
const res = await fetch('/api/status');
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
//更新统计数据
|
||
document.getElementById('today-syncs').textContent = data.todaySyncs || '--';
|
||
document.getElementById('repo-count').textContent = data.repoCount || '--';
|
||
document.getElementById('error-count').textContent = (data.errorCount + data.fatalCount) || '--';
|
||
document.getElementById('uptime').textContent = data.uptime ? `系统运行时间: ${data.uptime}` : '加载中...';
|
||
|
||
//更新服务状态
|
||
updateServiceStatus(data.status);
|
||
}
|
||
|
||
//加载历史记录
|
||
loadHistory();
|
||
} catch (e) {
|
||
console.error('加载仪表盘数据失败:', e);
|
||
}
|
||
}
|
||
|
||
async function loadHistory() {
|
||
try {
|
||
const res = await fetch('/api/history');
|
||
const data = await res.json();
|
||
|
||
if (data.success && data.history) {
|
||
const tbody = document.getElementById('history-table');
|
||
|
||
if (data.history.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">暂无历史记录</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = data.history.map(day => `
|
||
<tr class="hover:bg-slate-50">
|
||
<td class="px-4 py-3 text-sm text-slate-900">${day.date}</td>
|
||
<td class="px-4 py-3 text-sm text-slate-900 text-right">${day.syncs}</td>
|
||
<td class="px-4 py-3 text-sm ${day.errors > 0 ? 'text-rose-600 font-medium' : 'text-slate-900'} text-right">${day.errors}</td>
|
||
<td class="px-4 py-3 text-sm ${day.fatals > 0 ? 'text-rose-700 font-bold' : 'text-slate-900'} text-right">${day.fatals}</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
} catch (e) {
|
||
console.error('加载历史数据失败:', e);
|
||
const tbody = document.getElementById('history-table');
|
||
tbody.innerHTML = '<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-rose-500">加载失败</td></tr>';
|
||
}
|
||
}
|
||
|
||
function updateServiceStatus(status) {
|
||
const badge = document.getElementById('status-badge');
|
||
const statusText = document.getElementById('service-status');
|
||
|
||
if (status === 'running') {
|
||
badge.className = 'px-3 py-1 rounded-full text-xs font-medium flex items-center border bg-emerald-50 text-emerald-600 border-emerald-200';
|
||
badge.innerHTML = `
|
||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||
</svg>
|
||
运行中
|
||
`;
|
||
statusText.textContent = '运行中';
|
||
} else {
|
||
badge.className = 'px-3 py-1 rounded-full text-xs font-medium flex items-center border bg-slate-100 text-slate-600 border-slate-200';
|
||
badge.innerHTML = `
|
||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||
</svg>
|
||
已停止
|
||
`;
|
||
statusText.textContent = '已停止';
|
||
}
|
||
}
|
||
|
||
//控制机器人
|
||
async function controlBot(action) {
|
||
try {
|
||
const res = await fetch('/api/restart', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action })
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
alert(`操作成功: ${data.message || action}`);
|
||
//服务重启后延迟刷新页面
|
||
if (action === 'restart') {
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 3000);
|
||
} else {
|
||
loadDashboardData();
|
||
}
|
||
} else {
|
||
alert(`操作失败: ${data.error}`);
|
||
}
|
||
} catch (e) {
|
||
alert(`操作失败: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
//清空日志
|
||
async function clearLogs() {
|
||
if (!confirm('确定要清空日志吗?此操作不可恢复。')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/logs/clear', {
|
||
method: 'POST'
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
alert('日志已清空');
|
||
if (document.getElementById('content-logs').classList.contains('hidden') === false) {
|
||
loadLogs();
|
||
}
|
||
} else {
|
||
alert(`清空失败: ${data.error}`);
|
||
}
|
||
} catch (e) {
|
||
alert(`清空失败: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
//刷新状态
|
||
function refreshStatus() {
|
||
loadDashboardData();
|
||
}
|
||
|
||
async function loadEnvFile() {
|
||
const container = document.getElementById('envEditor');
|
||
|
||
try {
|
||
const res = await fetch('/api/env');
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
const envContent = data.content;
|
||
const envItems = parseEnvContent(envContent);
|
||
renderEnvForm(envItems);
|
||
} else {
|
||
container.innerHTML = `<div class="text-center py-8 text-rose-400">${data.error}</div>`;
|
||
}
|
||
} catch (e) {
|
||
container.innerHTML = `<div class="text-center py-8 text-rose-400">加载失败: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function parseEnvContent(content) {
|
||
const lines = content.split('\n');
|
||
const items = [];
|
||
|
||
for (const line of lines) {
|
||
const trimmed = line.trim();
|
||
|
||
if (trimmed === '' || trimmed.startsWith('#')) {
|
||
items.push({ type: 'comment', value: line });
|
||
} else if (trimmed.includes('=')) {
|
||
const equalIndex = trimmed.indexOf('=');
|
||
const key = trimmed.substring(0, equalIndex).trim();
|
||
const value = trimmed.substring(equalIndex + 1).trim();
|
||
items.push({ type: 'var', key, value });
|
||
} else {
|
||
items.push({ type: 'comment', value: line });
|
||
}
|
||
}
|
||
|
||
return items;
|
||
}
|
||
|
||
function renderEnvForm(items) {
|
||
const container = document.getElementById('envEditor');
|
||
container.innerHTML = items.map((item, index) => {
|
||
if (item.type === 'comment') {
|
||
return `<div class="text-slate-400 text-xs font-mono py-1">${escapeHtml(item.value)}</div>`;
|
||
} else {
|
||
const isSecret = item.key.toLowerCase().includes('token') ||
|
||
item.key.toLowerCase().includes('secret') ||
|
||
item.key.toLowerCase().includes('password') ||
|
||
item.key.toLowerCase().includes('pat');
|
||
return `
|
||
<div class="flex items-center gap-3 bg-slate-50 hover:bg-slate-100 p-3 rounded border border-slate-200 transition-colors">
|
||
<label class="text-slate-700 font-mono text-sm flex-shrink-0 w-48 font-medium">${escapeHtml(item.key)}</label>
|
||
<span class="text-slate-400">=</span>
|
||
<input type="${isSecret ? 'password' : 'text'}"
|
||
data-key="${escapeHtml(item.key)}"
|
||
value="${escapeHtml(item.value)}"
|
||
class="flex-1 bg-white border border-slate-300 rounded px-3 py-2 text-slate-900 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||
placeholder="未设置">
|
||
</div>
|
||
`;
|
||
}
|
||
}).join('');
|
||
}
|
||
|
||
async function saveEnvFile() {
|
||
const btn = document.getElementById('saveEnvBtn');
|
||
|
||
if (!confirm('确定要保存配置吗?保存后需要重启服务才能生效。')) {
|
||
return;
|
||
}
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = '保存中...';
|
||
|
||
try {
|
||
const content = buildEnvContent();
|
||
|
||
const res = await fetch('/api/env', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ content })
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
alert(`保存成功!\n\n${data.message}`);
|
||
loadEnvFile();
|
||
} else {
|
||
alert(`保存失败: ${data.error}`);
|
||
}
|
||
} catch (e) {
|
||
alert(`保存失败: ${e.message}`);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '保存配置';
|
||
}
|
||
}
|
||
|
||
function buildEnvContent() {
|
||
const container = document.getElementById('envEditor');
|
||
const lines = [];
|
||
|
||
container.childNodes.forEach(node => {
|
||
if (node.classList && node.classList.contains('text-slate-400')) {
|
||
lines.push(node.textContent);
|
||
} else if (node.classList && node.classList.contains('bg-slate-50')) {
|
||
const input = node.querySelector('input');
|
||
const key = input.dataset.key;
|
||
const value = input.value;
|
||
lines.push(`${key}=${value}`);
|
||
}
|
||
});
|
||
|
||
return lines.join('\n');
|
||
}
|
||
|
||
async function loadGuide() {
|
||
const container = document.getElementById('guide-content');
|
||
|
||
try {
|
||
const res = await fetch('/api/guide');
|
||
const data = await res.json();
|
||
|
||
if (data.success && data.content) {
|
||
//使用marked.js渲染markdown
|
||
if (typeof marked !== 'undefined') {
|
||
container.innerHTML = marked.parse(data.content);
|
||
} else {
|
||
//如果marked未加载,显示原始文本
|
||
container.innerHTML = `<pre>${data.content}</pre>`;
|
||
}
|
||
} else {
|
||
container.innerHTML = `
|
||
<div class="text-center py-8 text-slate-500">
|
||
<svg class="w-12 h-12 mx-auto mb-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||
</svg>
|
||
<p class="font-medium">无法加载使用指南</p>
|
||
<p class="text-sm mt-1">${data.error || '未知错误'}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
} catch (e) {
|
||
console.error('加载使用指南失败:', e);
|
||
container.innerHTML = `
|
||
<div class="text-center py-8 text-rose-500">
|
||
<svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||
</svg>
|
||
<p class="font-medium">加载失败</p>
|
||
<p class="text-sm mt-1">${e.message}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
//页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
//默认显示dashboard
|
||
switchTab('dashboard');
|
||
|
||
//加载初始数据
|
||
loadDashboardData();
|
||
|
||
//定期刷新仪表盘数据(每30秒)
|
||
setInterval(loadDashboardData, 30000);
|
||
});
|