huibao/frontend/js/app.js

798 lines
32 KiB
JavaScript
Raw Normal View History

/**
* 眼镜行业情报分析系统前端逻辑
*/
const API_BASE = 'http://localhost:8000/api';
// 配置 Marked.js
if (typeof marked !== 'undefined') {
marked.use({
gfm: true,
breaks: true,
mangle: false,
headerIds: false
});
}
const app = {
// 状态
state: {
companies: [],
reports: [],
currentReport: null
},
// 辅助:增强 Markdown 解析(修复中文粗体失效问题)
parseMarkdown(text) {
if (!text) return '';
// 预处理:修复 **紧贴中文或标点导致无法解析的问题
// 将 **...** 强制替换为 <strong>...</strong>
let fixedText = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
return marked.parse(fixedText);
},
// 初始化
init() {
this.bindEvents();
this.checkStatus();
this.fetchDashboard();
// 初始路由
this.navigateTo('dashboard');
},
// 自定义 Toast
showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
let icon = 'info-circle';
if (type === 'success') icon = 'check-circle';
if (type === 'error') icon = 'exclamation-circle';
// 动态计算CSS变量颜色
let color = 'var(--primary-color)';
if (type === 'success') color = '#10b981';
if (type === 'error') color = '#ef4444';
toast.innerHTML = `
<i class="fas fa-${icon}" style="font-size:1.2rem;color:${color}"></i>
<span class="toast-message">${message}</span>
`;
container.appendChild(toast);
// 3秒后自动消失
setTimeout(() => {
toast.style.animation = 'fadeOut 0.3s forwards';
setTimeout(() => toast.remove(), 300);
}, 3000);
},
// 自定义 Confirm
showConfirm(title, message) {
return new Promise((resolve) => {
const overlay = document.getElementById('confirm-overlay');
const titleEl = document.getElementById('confirm-title');
const msgEl = document.getElementById('confirm-message');
const btnOk = document.getElementById('confirm-ok');
const btnCancel = document.getElementById('confirm-cancel');
titleEl.textContent = title;
msgEl.textContent = message;
overlay.classList.add('active');
const close = (result) => {
overlay.classList.remove('active');
// 移除监听器防止内存泄漏
btnOk.onclick = null;
btnCancel.onclick = null;
resolve(result);
};
btnOk.onclick = () => close(true);
btnCancel.onclick = () => close(false);
});
},
// 绑定事件
bindEvents() {
// 导航切换
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const pageId = e.currentTarget.dataset.page;
this.navigateTo(pageId);
// 移动端点击导航后自动收起
if (window.innerWidth <= 768) {
this.toggleSidebar(false);
}
});
});
// 移动端菜单
const mobileBtn = document.getElementById('mobile-menu-btn');
const overlay = document.getElementById('sidebar-overlay');
if (mobileBtn) {
mobileBtn.addEventListener('click', () => this.toggleSidebar());
}
if (overlay) {
overlay.addEventListener('click', () => this.toggleSidebar(false));
}
// 定时任务按钮
document.getElementById('run-now-btn').addEventListener('click', () => this.runSchedulerNow());
// 添加公司
document.getElementById('btn-add-company').addEventListener('click', () => {
this.openCompanyModal();
});
document.getElementById('add-company-form').addEventListener('submit', (e) => {
e.preventDefault();
this.handleSaveCompany(e.target);
});
// 模态框关闭
document.querySelectorAll('.close').forEach(btn => {
btn.addEventListener('click', (e) => {
const modal = e.target.closest('.modal');
modal.classList.remove('active');
if (modal.id === 'report-modal') {
this.clearDetailPolling();
// 如果详情页显示已完成,关闭时自动刷新列表
if (this.state.currentReport && this.state.currentReport.is_analyzed) {
this.fetchReports(); // 刷新列表
}
}
});
});
// 点击背景关闭模态框
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
if (modal.id === 'report-modal') {
this.clearDetailPolling();
}
}
});
});
// ESC键关闭模态框
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal.active').forEach(modal => {
modal.classList.remove('active');
if (modal.id === 'report-modal') {
this.clearDetailPolling();
}
});
}
});
// 标签页切换
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const tabId = e.target.dataset.tab;
// 更新按钮状态
e.target.parentElement.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
// 更新内容显示
const modalBody = e.target.closest('.modal-body');
modalBody.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
modalBody.querySelector(`#${tabId}`).classList.add('active');
});
});
// 报告筛选
const filters = ['filter-company', 'filter-type', 'filter-status'];
filters.forEach(id => {
document.getElementById(id).addEventListener('change', () => this.fetchReports());
});
// 注:重新分析按钮的事件在 showReportDetail 中动态绑定
},
// 切换侧边栏
toggleSidebar(forceState) {
const sidebar = document.querySelector('.sidebar');
const overlay = document.getElementById('sidebar-overlay');
const btn = document.getElementById('mobile-menu-btn');
if (!sidebar || !overlay) return;
const isActive = sidebar.classList.contains('active');
const newState = forceState !== undefined ? forceState : !isActive;
if (newState) {
sidebar.classList.add('active');
overlay.classList.add('active');
if (btn) btn.style.display = 'none'; // Optional: hide toggle button when open
} else {
sidebar.classList.remove('active');
overlay.classList.remove('active');
if (btn) btn.style.display = '';
}
},
// 路由导航
navigateTo(pageId) {
// 切换页面时清除详情轮询
this.clearDetailPolling();
// 更新导航激活状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.toggle('active', item.dataset.page === pageId);
});
// 更新页面显示
document.querySelectorAll('.page').forEach(page => {
page.classList.remove('active');
});
document.getElementById(`${pageId}-page`).classList.add('active');
// 页面初始化逻辑
if (pageId === 'companies') this.fetchCompanies();
if (pageId === 'reports') {
this.fetchCompanies(); // 用于筛选下拉
this.fetchReports();
}
if (pageId === 'dashboard') this.fetchDashboard();
},
// 获取仪表盘数据
async fetchDashboard() {
try {
// 获取统计数据和日志
const [stats, logs] = await Promise.all([
fetch(`${API_BASE}/stats/dashboard`).then(r => r.json()),
fetch(`${API_BASE}/scheduler/logs`).then(r => r.json())
]);
// 更新计数
document.getElementById('stat-companies').textContent = stats.company_count;
document.getElementById('stat-reports').textContent = stats.report_count;
document.getElementById('stat-analyzed').textContent = stats.analyzed_count;
// 渲染最新洞察
const insightsContainer = document.getElementById('recent-insights-list');
insightsContainer.innerHTML = '';
if (stats.latest_insight) {
const report = stats.latest_insight;
const el = document.createElement('div');
el.className = 'insight-item card';
el.style.marginBottom = '10px';
el.style.padding = '16px';
el.style.borderLeft = '4px solid #3b82f6';
el.innerHTML = `
<div style="font-weight:600;font-size:1rem;margin-bottom:8px">${report.title}</div>
<div style="font-size:0.85rem;color:#64748b;margin-bottom:12px">
<i class="fas fa-check-circle" style="color:#10b981"></i> AI ${report.date}
</div>
<button class="btn-xs" onclick="app.showReportDetail(${report.id}); return false;">
<i class="fas fa-eye"></i>
</button>
`;
insightsContainer.appendChild(el);
} else {
insightsContainer.innerHTML = '<div style="color:#aaa;text-align:center;padding:20px;border:1px dashed #eee;border-radius:8px">暂无分析结果</div>';
}
// 渲染日志
const logsContainer = document.getElementById('logs-list');
logsContainer.innerHTML = '';
logs.forEach(log => {
const el = document.createElement('div');
el.className = 'log-item';
const statusColor = log.status === 'failed' ? 'red' : (log.status === 'completed' ? 'green' : 'blue');
el.innerHTML = `
<div style="display:flex;justify-content:space-between">
<span style="font-weight:500">[${log.task_name}]</span>
<span style="color:${statusColor}">${log.status}</span>
</div>
<div>${log.message || '-'}</div>
<div class="log-time">${new Date(log.started_at).toLocaleString()}</div>
`;
logsContainer.appendChild(el);
});
} catch (error) {
console.error('获取仪表盘数据失败', error);
}
},
// 获取公司
async fetchCompanies() {
try {
const res = await fetch(`${API_BASE}/companies`);
const companies = await res.json();
this.state.companies = companies;
// 渲染表格
const tbody = document.querySelector('#companies-table tbody');
if (tbody) {
tbody.innerHTML = companies.map(c => `
<tr>
<td>${c.stock_code}</td>
<td>${c.short_name || '-'}</td>
<td>${c.company_name}</td>
<td>${c.industry || '-'}</td>
<td>${c.report_count}</td>
<td><span class="tag ${c.is_active ? 'analysis-done' : ''}">${c.is_active ? '监控中' : '暂停'}</span></td>
<td>
<button class="btn-xs edit-btn" data-id="${c.id}" style="margin-right:8px">编辑</button>
<button class="btn-xs remove-btn" data-id="${c.id}" style="color:red;border-color:red">删除</button>
</td>
</tr>
`).join('');
// 绑定事件
tbody.querySelectorAll('.remove-btn').forEach(btn => {
btn.addEventListener('click', (e) => this.deleteCompany(e.target.dataset.id));
});
tbody.querySelectorAll('.edit-btn').forEach(btn => {
btn.addEventListener('click', (e) => this.openCompanyModal(e.target.dataset.id));
});
}
// 更新筛选下拉
const select = document.getElementById('filter-company');
if (select) {
select.innerHTML = '<option value="">所有公司</option>' +
companies.map(c => `<option value="${c.id}">${c.company_name}</option>`).join('');
}
} catch (error) {
console.error('获取公司失败', error);
}
},
// 打开公司模态框 (新增/编辑)
openCompanyModal(id = null) {
const modal = document.getElementById('company-modal');
const form = document.getElementById('add-company-form');
const title = document.getElementById('company-modal-title');
const desc = document.getElementById('company-modal-desc');
const btn = document.getElementById('btn-save-company');
form.reset();
if (id) {
// 编辑模式
const company = this.state.companies.find(c => c.id == id);
if (!company) return;
document.getElementById('company-id').value = company.id;
document.getElementById('company-code').value = company.stock_code;
document.getElementById('company-code').disabled = true; // 禁止修改代码
document.getElementById('company-name').value = company.company_name;
document.getElementById('company-short-name').value = company.short_name;
document.getElementById('company-industry').value = company.industry;
title.textContent = '编辑公司信息';
desc.style.display = 'none';
btn.innerHTML = '<i class="fas fa-save"></i> 更新信息';
} else {
// 新增模式
document.getElementById('company-id').value = '';
document.getElementById('company-code').disabled = false;
title.textContent = '添加公司';
desc.style.display = 'block';
btn.innerHTML = '<i class="fas fa-search"></i> 查询并添加';
}
this.showModal('company-modal');
},
// 保存公司 (新增/更新)
async handleSaveCompany(form) {
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
const id = data.id;
// 移除空值
if (!data.company_name) delete data.company_name;
if (!data.short_name) delete data.short_name;
if (!data.industry) delete data.industry;
try {
let url = `${API_BASE}/companies`;
let method = 'POST';
if (id) {
url = `${API_BASE}/companies/${id}`;
method = 'PUT';
} else {
data.is_active = true;
}
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
this.showToast(id ? '更新成功' : '添加成功,将在下次定时任务中同步报告', 'success');
document.getElementById('company-modal').classList.remove('active');
this.fetchCompanies();
} else {
const err = await res.json();
this.showToast('操作失败: ' + err.detail, 'error');
}
} catch (error) {
console.error('API错误', error);
this.showToast('操作失败: 网络错误', 'error');
}
},
// 删除公司
async deleteCompany(id) {
if (!await this.showConfirm('删除确认', '确定要删除监控吗?已下载的报告将保留。')) return;
try {
await fetch(`${API_BASE}/companies/${id}`, { method: 'DELETE' });
this.showToast('删除成功', 'success');
this.fetchCompanies();
} catch (error) {
this.showToast('删除失败', 'error');
}
},
// 获取报告
async fetchReports() {
const companyId = document.getElementById('filter-company').value;
const type = document.getElementById('filter-type').value;
const status = document.getElementById('filter-status').value;
let url = `${API_BASE}/reports?page_size=50`;
if (companyId) url += `&company_id=${companyId}`;
if (type) url += `&report_type=${type}`;
if (status === 'analyzed') url += `&is_analyzed=true`;
if (status === 'pending') url += `&is_analyzed=false`;
try {
const res = await fetch(url);
const reports = await res.json();
this.state.reports = reports;
const grid = document.getElementById('reports-list');
if (!grid) return;
grid.innerHTML = reports.map(r => {
// 状态标签逻辑
let statusTag = '';
let statusClass = '';
// 优先判断是否正在处理中
const processingStates = ['extracting', 'analyzing', 'summarizing'];
if (processingStates.includes(r.analysis_status)) {
statusTag = '正在处理';
statusClass = 'analysis-processing';
} else if (r.is_analyzed) {
statusTag = 'AI已解读';
statusClass = 'analysis-done';
} else if (r.is_extracted) {
statusTag = '待AI分析';
statusClass = 'analysis-pending';
} else if (r.is_downloaded) {
statusTag = '待提取';
statusClass = 'analysis-pending';
} else {
statusTag = '待下载';
statusClass = '';
}
return `
<tr>
<td style="font-weight:600">${r.company_name}</td>
<td><div style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${r.title}">${r.title}</div></td>
<td><span class="tag">${r.report_type === '年度报告' ? '年报' : '半年报'}</span></td>
<td>${r.report_year}</td>
<td>${new Date(r.announcement_time).toLocaleDateString()}</td>
<td><span class="tag ${statusClass}">${statusTag}</span></td>
<td>
<button class="btn-xs" onclick="app.showReportDetail(${r.id})">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
`;
}).join('');
if (reports.length === 0) {
grid.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:40px;color:#aaa">暂无报告数据</td></tr>';
}
} catch (error) {
console.error('获取报告失败', error);
}
},
// 轮询句柄
detailPollingInterval: null,
// 清除详情轮询
clearDetailPolling() {
if (this.detailPollingInterval) {
clearTimeout(this.detailPollingInterval);
this.detailPollingInterval = null;
}
},
// 显示报告详情
async showReportDetail(id) {
// 清除旧的轮询
this.clearDetailPolling();
try {
const res = await fetch(`${API_BASE}/reports/${id}`);
const report = await res.json();
this.state.currentReport = report;
// 填充基本信息
document.getElementById('modal-title').textContent = report.title;
const tags = document.getElementById('modal-tags');
// 状态显示映射
const statusMap = {
'pending': '待处理',
'extracting': '提取中',
'analyzing': '分析中',
'summarizing': '汇总中',
'completed': '已完成',
'failed': '失败'
};
const statusText = statusMap[report.analysis_status] || report.analysis_status;
const isProcessing = ['extracting', 'analyzing', 'summarizing'].includes(report.analysis_status);
tags.innerHTML = `
<span class="tag">${report.stock_code}</span>
<span class="tag">${report.report_year}年${report.report_period}</span>
<span class="tag ${report.is_analyzed ? 'analysis-done' : (isProcessing ? 'analysis-processing' : 'analysis-pending')}">
${statusText} ${isProcessing ? '<i class="fa-solid fa-circle-notch fa-spin"></i>' : ''}
</span>
`;
// 获取当前激活的 Tab如果不存在则默认为 'ai-summary'
const modal = document.getElementById('report-modal');
const activeTabBtn = modal.querySelector('.tab-btn.active');
const activeTabId = activeTabBtn ? activeTabBtn.dataset.tab : 'ai-summary';
// 设置 Tab 切换逻辑 (使用事件委托,但要防止重复绑定)
if (!modal.hasAttribute('data-tabs-initialized')) {
modal.addEventListener('click', (e) => {
if (e.target.classList.contains('tab-btn')) {
// 移除所有 active
modal.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
modal.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
// 激活当前
e.target.classList.add('active');
const targetId = e.target.dataset.tab;
const targetContent = document.getElementById(targetId);
if (targetContent) targetContent.classList.add('active');
}
});
modal.setAttribute('data-tabs-initialized', 'true');
}
// 恢复 Tab 状态
const tabs = modal.querySelectorAll('.tab-btn');
const contents = modal.querySelectorAll('.tab-content');
tabs.forEach(t => {
if (t.dataset.tab === activeTabId) {
t.classList.add('active');
} else {
t.classList.remove('active');
}
});
contents.forEach(c => {
if (c.id === activeTabId) {
c.classList.add('active');
} else {
c.classList.remove('active');
}
});
// 填充AI总结
const summaryTab = document.getElementById('summary-content');
const summaries = report.analysis_results.filter(a => a.analysis_type === 'summary');
summaries.sort((a, b) => b.id - a.id);
const summary = summaries[0];
if (summary) {
summaryTab.innerHTML = this.parseMarkdown(summary.summary);
} else if (isProcessing) {
summaryTab.innerHTML = `
<div style="padding:40px;text-align:center">
<i class="fa-solid fa-circle-notch fa-spin" style="font-size:2rem;color:#2563eb"></i>
<p style="margin-top:16px">AI正在深度分析中...</p>
<p style="font-size:0.8rem;color:#64748b;margin-top:8px">由于章节较多通常15-25且每个章节分析耗时约30-60整份报告分析可能需要5-10分钟</p>
<p style="font-size:0.8rem;color:#64748b">您可以切换到章节分析标签查看实时进度</p>
</div>`;
} else if (!report.is_downloaded) {
summaryTab.innerHTML = '<div style="padding:40px;text-align:center;color:#f59e0b"><i class="fas fa-exclamation-triangle" style="font-size:2rem"></i><p style="margin-top:16px">报告尚未下载</p></div>';
} else if (!report.is_extracted) {
summaryTab.innerHTML = '<div style="padding:40px;text-align:center;color:#64748b"><i class="fas fa-file-pdf" style="font-size:2rem"></i><p style="margin-top:16px">PDF已下载请点击下方按钮开始分析</p></div>';
} else {
summaryTab.innerHTML = '<div style="padding:40px;text-align:center;color:#64748b"><i class="fas fa-brain" style="font-size:2rem"></i><p style="margin-top:16px">内容已提取,请点击下方按钮开始分析</p></div>';
}
// 填充章节分析
const sectionsList = document.getElementById('sections-list');
const sectionResults = report.analysis_results.filter(a => a.analysis_type === 'section');
if (sectionResults.length > 0) {
sectionsList.innerHTML = sectionResults.map(s => `
<div style="margin-bottom:20px;border-bottom:1px solid #eee;padding-bottom:20px">
<h4 style="margin-bottom:10px;color:#2563eb">${s.section_name}</h4>
<div class="markdown-body">${this.parseMarkdown(s.summary)}</div>
</div>
`).join('');
if (isProcessing) {
sectionsList.innerHTML += `<div style="padding:20px;text-align:center;color:#2563eb"><i class="fa-solid fa-circle-notch fa-spin"></i> 正在分析新的章节...</div>`;
}
} else {
sectionsList.innerHTML = `<div style="padding:20px;text-align:center;color:#aaa">${isProcessing ? '<i class="fa-solid fa-circle-notch fa-spin"></i> 正在处理首个章节...' : '暂无章节分析'}</div>`;
}
// 填充原文
const rawList = document.getElementById('raw-content-list');
if (report.extracted_contents.length > 0) {
rawList.innerHTML = report.extracted_contents.map(c => `
<div style="margin-bottom:20px;background:#f9f9f9;padding:15px;border-radius:8px">
<div style="display:flex;justify-content:space-between;margin-bottom:8px;align-items:flex-start">
<strong style="margin-right:12px;word-break:break-word">${c.section_name}</strong>
<span class="tag" style="flex-shrink:0;white-space:nowrap">匹配: ${c.section_keyword}</span>
</div>
<div style="white-space:pre-wrap;font-size:0.85rem;max-height:300px;overflow-y:auto">${c.content}</div>
</div>
`).join('');
} else {
rawList.innerHTML = '<div style="padding:20px;text-align:center;color:#aaa">未提取到相关内容</div>';
}
// 动态设置按钮
const analyzeBtn = document.getElementById('btn-reanalyze');
const forceResetContainer = document.getElementById('force-reset-container') || document.createElement('div');
// 确保 force-reset-container 存在并紧跟在 analyzeBtn 后面
if (!document.getElementById('force-reset-container')) {
forceResetContainer.id = 'force-reset-container';
forceResetContainer.style.marginTop = '10px';
forceResetContainer.style.fontSize = '0.75rem';
analyzeBtn.parentNode.appendChild(forceResetContainer);
}
forceResetContainer.innerHTML = '';
analyzeBtn.disabled = isProcessing;
if (isProcessing) {
analyzeBtn.textContent = '分析中...';
// 增加强制重置选项
forceResetContainer.innerHTML = `
<a href="#" style="color:#64748b;text-decoration:underline" onclick="event.preventDefault(); app.triggerAnalysis(${report.id}, true)">
发现卡住了点此强制重新开始
</a>`;
} else if (report.analysis_status === 'failed') {
analyzeBtn.textContent = '分析失败,点此重试';
analyzeBtn.disabled = false;
analyzeBtn.onclick = () => this.triggerAnalysis(report.id, true);
} else if (report.is_analyzed) {
analyzeBtn.textContent = '重新分析';
analyzeBtn.onclick = async () => {
if (await this.showConfirm('重新分析', '确定要重新分析此报告吗?旧的分析结果将被清除。')) {
this.triggerAnalysis(report.id, true);
}
};
} else {
analyzeBtn.textContent = '开始分析';
analyzeBtn.onclick = () => {
this.triggerAnalysis(report.id, false);
};
}
// 下载链接
const downloadBtn = document.getElementById('btn-download');
if (report.pdf_url) {
downloadBtn.href = report.pdf_url;
downloadBtn.style.display = 'inline-flex';
} else {
downloadBtn.style.display = 'none';
}
// 显示模态框
this.showModal('report-modal');
// 如果正在处理中,启动轮询
if (isProcessing) {
this.detailPollingInterval = setTimeout(() => this.showReportDetail(id), 5000);
}
} catch (error) {
console.error('获取详情失败', error);
this.showToast('无法获取报告详情', 'error');
}
},
// 触发分析
async triggerAnalysis(reportId, force = false) {
try {
const res = await fetch(`${API_BASE}/analysis/run`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ report_id: reportId, force: force })
});
if (res.ok) {
this.showToast('分析任务已启动', 'success');
// 如果当前正在查看这个报告的详情,不需要关闭模态框,直接进入轮询
if (this.state.currentReport && this.state.currentReport.id === reportId) {
this.showReportDetail(reportId);
} else {
document.getElementById('report-modal').classList.remove('active');
}
this.fetchReports(); // 刷新列表状态
} else {
const err = await res.json();
this.showToast('提交失败: ' + err.detail, 'error');
}
} catch (error) {
this.showToast('网络错误', 'error');
}
},
// 运行调度器
async runSchedulerNow() {
if (!await this.showConfirm('立即同步', '确定要立即开始同步所有公司的报告吗?可能需要较长时间。')) return;
try {
await fetch(`${API_BASE}/scheduler/run-now`, { method: 'POST' });
this.showToast('同步任务已触发,请在日志中查看进度', 'success');
this.checkStatus();
} catch (error) {
this.showToast('操作失败', 'error');
}
},
// 检查状态
async checkStatus() {
try {
const res = await fetch(`${API_BASE}/scheduler/status`);
const status = await res.json();
const indicator = document.getElementById('scheduler-indicator');
indicator.classList.toggle('active', status.is_running);
const nextTime = status.next_run_time ? new Date(status.next_run_time).toLocaleString() : '未调度';
document.getElementById('next-run-time').textContent = `下次: ${nextTime}`;
} catch (error) {
console.error('状态检查失败');
}
},
// 显示模态框
showModal(id) {
document.getElementById(id).classList.add('active');
}
};
// 启动应用
document.addEventListener('DOMContentLoaded', () => {
app.init();
});