huibao/frontend/js/app.js

798 lines
32 KiB
JavaScript
Raw Permalink 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.

/**
* 眼镜行业情报分析系统前端逻辑
*/
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();
});