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