Files
gitea-jira-task-bot/public/dashboard-app.js

408 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//标签页切换
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);
});