// 自动检测当前域名和端口,支持不同环境 const API_BASE = `${window.location.origin}/api`; let currentReport = null; let currentListPage = 1; function toggleDateRange() { const useDateRange = document.getElementById('useDateRange').checked; document.getElementById('dateRangeFields').style.display = useDateRange ? 'block' : 'none'; document.getElementById('normalFields').style.display = useDateRange ? 'none' : 'block'; } function toggleDetailDateRange() { const useDetailDateRange = document.getElementById('useDetailDateRange').checked; document.getElementById('detailDateRangeFields').style.display = useDetailDateRange ? 'block' : 'none'; document.getElementById('detailNormalFields').style.display = useDetailDateRange ? 'none' : 'block'; } function switchTab(tabName) { // 隐藏所有标签内容 document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); // 显示选中的标签 document.getElementById(tabName).classList.add('active'); event.target.classList.add('active'); } async function fetchList(pageNum) { const url = document.getElementById('listUrl').value; const page = pageNum || parseInt(document.getElementById('listPage').value) || 1; const loading = document.getElementById('listLoading'); const results = document.getElementById('listResults'); const pagination = document.getElementById('listPagination'); currentListPage = page; document.getElementById('listPage').value = page; loading.classList.add('active'); results.innerHTML = ''; pagination.style.display = 'none'; try { const response = await fetch(`${API_BASE}/list?url=${encodeURIComponent(url)}&page=${page}`); const data = await response.json(); if (data.success) { displayList(data.data, results); updateListPagination(page, data.data.length > 0); } else { results.innerHTML = `
错误: ${data.error}
`; } } catch (error) { results.innerHTML = `
请求失败: ${error.message}
`; } finally { loading.classList.remove('active'); } } function goToListPage(page) { if (page < 1) return; fetchList(page); } function updateListPagination(page, hasData) { const pagination = document.getElementById('listPagination'); const currentPageSpan = document.getElementById('listCurrentPage'); const prevBtn = document.getElementById('listPrevPage'); const firstBtn = document.getElementById('listFirstPage'); const nextBtn = document.getElementById('listNextPage'); if (hasData) { pagination.style.display = 'flex'; currentPageSpan.textContent = page; prevBtn.disabled = page <= 1; firstBtn.disabled = page <= 1; nextBtn.disabled = !hasData; } } function displayList(items, container) { if (items.length === 0) { container.innerHTML = '

没有找到公告

'; return; } const html = `

找到 ${items.length} 条公告

${items.map((item, index) => `

${index + 1}. ${item.title}

发布日期: ${item.date}
查看详情 →
`).join('')}
`; container.innerHTML = html; } async function fetchDetails() { const useDetailDateRange = document.getElementById('useDetailDateRange').checked; const loading = document.getElementById('detailLoading'); const results = document.getElementById('detailResults'); loading.classList.add('active'); results.innerHTML = ''; try { let listData; if (useDetailDateRange) { // 时间范围模式 const startDate = document.getElementById('detailStartDate').value; const endDate = document.getElementById('detailEndDate').value; const maxPages = parseInt(document.getElementById('detailMaxPages').value); if (!startDate && !endDate) { results.innerHTML = '
请至少填写开始日期或结束日期
'; loading.classList.remove('active'); return; } const dateRangeResponse = await fetch(`${API_BASE}/list-daterange`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ startDate, endDate, maxPages }) }); listData = await dateRangeResponse.json(); } else { // 普通模式 - 按数量采集多页 const url = document.getElementById('detailUrl').value; const limit = parseInt(document.getElementById('detailLimit').value); // 采集多页直到获得足够数量 const allItems = []; let page = 1; const maxPagesToFetch = Math.ceil(limit / 10) + 1; // 假设每页约10条 while (allItems.length < limit && page <= maxPagesToFetch) { const listResponse = await fetch(`${API_BASE}/list?url=${encodeURIComponent(url)}&page=${page}`); const pageData = await listResponse.json(); if (!pageData.success) { if (allItems.length === 0) { results.innerHTML = `
错误: ${pageData.error}
`; loading.classList.remove('active'); return; } break; } if (pageData.data.length === 0) { break; } allItems.push(...pageData.data); page++; // 如果还需要更多数据且未到达上限,稍作延迟 if (allItems.length < limit && page <= maxPagesToFetch) { await new Promise(resolve => setTimeout(resolve, 500)); } } listData = { success: true, data: allItems }; } if (!listData.success) { results.innerHTML = `
错误: ${listData.error}
`; return; } // 采集详情 const limit = useDetailDateRange ? listData.data.length : parseInt(document.getElementById('detailLimit').value); const detailResponse = await fetch(`${API_BASE}/details`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: listData.data, limit }) }); const detailData = await detailResponse.json(); if (detailData.success) { displayDetails(detailData.data, results); } else { results.innerHTML = `
错误: ${detailData.error}
`; } } catch (error) { results.innerHTML = `
请求失败: ${error.message}
`; } finally { loading.classList.remove('active'); } } function displayDetails(items, container) { const html = `

采集了 ${items.length} 条详情

${items.map((item, index) => `

${index + 1}. ${item.title}

发布日期: ${item.date}
${item.detail ? `
发布时间: ${item.detail.publishTime || '未知'}
${item.detail.budget ? ` ${item.detail.budget.amount}${item.detail.budget.unit} ` : '
未找到预算信息
'} ` : '
采集失败
'}
查看原文 →
`).join('')}
`; container.innerHTML = html; } async function generateReport() { const useDateRange = document.getElementById('useDateRange').checked; const threshold = parseFloat(document.getElementById('reportThreshold').value); const loading = document.getElementById('reportLoading'); const results = document.getElementById('reportResults'); const exportBtn = document.getElementById('exportBtn'); loading.classList.add('active'); results.innerHTML = ''; exportBtn.style.display = 'none'; try { let response; if (useDateRange) { // 时间范围模式 const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; const maxPages = parseInt(document.getElementById('maxPages').value); if (!startDate && !endDate) { results.innerHTML = '
请至少填写开始日期或结束日期
'; loading.classList.remove('active'); return; } response = await fetch(`${API_BASE}/report-daterange`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ startDate, endDate, threshold, maxPages }) }); } else { // 普通模式 const url = document.getElementById('reportUrl').value; const limit = parseInt(document.getElementById('reportLimit').value); response = await fetch(`${API_BASE}/report`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, limit, threshold }) }); } const data = await response.json(); if (data.success) { currentReport = data.data; displayReport(data.data, results); exportBtn.style.display = 'inline-block'; document.getElementById('sendEmailBtn').style.display = 'inline-block'; } else { results.innerHTML = `
错误: ${data.error}
`; } } catch (error) { results.innerHTML = `
请求失败: ${error.message}
`; } finally { loading.classList.remove('active'); } } function displayReport(report, container) { const html = `

统计摘要

总项目数
${report.summary.total_count}
符合条件
${report.summary.filtered_count}
总金额
${report.summary.total_amount}
阈值
${report.summary.threshold}
${report.projects.length === 0 ? '

暂无符合条件的项目

' : `

项目列表

${report.projects.map((project, index) => `

${index + 1}. ${project.title}

发布日期: ${project.date}
发布时间: ${project.publish_time}
${project.budget.amount}${project.budget.unit}
金额描述: ${project.budget.text}

查看详情 →
`).join('')} `} `; container.innerHTML = html; } async function exportReport() { if (!currentReport) return; // 检查docx库是否加载 if (!window.docx) { alert('Word导出库正在加载中,请稍后再试...'); return; } const report = currentReport; const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType } = window.docx; // 构建文档段落 const paragraphs = []; // 标题 paragraphs.push( new Paragraph({ text: '南京公共工程建设项目报告', heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER, spacing: { after: 200 } }) ); // 生成时间 paragraphs.push( new Paragraph({ children: [ new TextRun({ text: '生成时间: ', bold: true }), new TextRun({ text: new Date(report.summary.generated_at).toLocaleString('zh-CN') }) ], spacing: { after: 200 } }) ); // 统计摘要标题 paragraphs.push( new Paragraph({ text: '统计摘要', heading: HeadingLevel.HEADING_2, spacing: { before: 200, after: 100 } }) ); // 统计数据 paragraphs.push( new Paragraph({ children: [new TextRun({ text: `• 总项目数: ${report.summary.total_count}` })], spacing: { after: 50 } }), new Paragraph({ children: [new TextRun({ text: `• 超过${report.summary.threshold}的项目: ${report.summary.filtered_count}` })], spacing: { after: 50 } }), new Paragraph({ children: [new TextRun({ text: `• 总金额: ${report.summary.total_amount}` })], spacing: { after: 200 } }) ); // 项目列表标题 paragraphs.push( new Paragraph({ text: '项目列表', heading: HeadingLevel.HEADING_2, spacing: { before: 200, after: 100 } }) ); // 项目详情 if (report.projects.length === 0) { paragraphs.push( new Paragraph({ text: '暂无符合条件的项目。', spacing: { after: 100 } }) ); } else { report.projects.forEach((project, index) => { // 项目标题 paragraphs.push( new Paragraph({ text: `${index + 1}. ${project.title}`, heading: HeadingLevel.HEADING_3, spacing: { before: 150, after: 100 } }) ); // 项目详情 paragraphs.push( new Paragraph({ children: [ new TextRun({ text: '发布日期: ', bold: true }), new TextRun({ text: project.date }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: '发布时间: ', bold: true }), new TextRun({ text: project.publish_time }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: '预算金额: ', bold: true }), new TextRun({ text: `${project.budget.amount}${project.budget.unit}` }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: '链接: ', bold: true }), new TextRun({ text: project.url, color: '0000FF' }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: '金额描述: ', bold: true }), new TextRun({ text: project.budget.text }) ], spacing: { after: 100 } }) ); }); } // 创建文档 const doc = new Document({ sections: [{ properties: {}, children: paragraphs }] }); // 生成并下载Word文件 const blob = await Packer.toBlob(doc); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `report_${new Date().getTime()}.docx`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // ========== 邮件功能 ========== // 保存邮件配置到localStorage function saveEmailConfig() { const config = { smtpHost: document.getElementById('smtpHost').value, smtpPort: parseInt(document.getElementById('smtpPort').value) || 587, smtpUser: document.getElementById('smtpUser').value, smtpPass: document.getElementById('smtpPass').value, recipients: document.getElementById('recipients').value }; // 验证配置 if (!config.smtpHost || !config.smtpUser || !config.smtpPass || !config.recipients) { showEmailStatus('请填写所有必填项', 'error'); return; } // 保存到localStorage localStorage.setItem('emailConfig', JSON.stringify(config)); showEmailStatus('邮件配置已保存', 'success'); } // 从localStorage加载邮件配置 function loadEmailConfig() { const configStr = localStorage.getItem('emailConfig'); if (configStr) { try { const config = JSON.parse(configStr); document.getElementById('smtpHost').value = config.smtpHost || ''; document.getElementById('smtpPort').value = config.smtpPort || 587; document.getElementById('smtpUser').value = config.smtpUser || ''; document.getElementById('smtpPass').value = config.smtpPass || ''; document.getElementById('recipients').value = config.recipients || ''; } catch (e) { console.error('加载邮件配置失败:', e); } } } // 测试邮件配置 async function testEmailConfig() { const config = { smtpHost: document.getElementById('smtpHost').value, smtpPort: parseInt(document.getElementById('smtpPort').value) || 587, smtpUser: document.getElementById('smtpUser').value, smtpPass: document.getElementById('smtpPass').value, recipients: document.getElementById('recipients').value }; // 验证配置 if (!config.smtpHost || !config.smtpUser || !config.smtpPass || !config.recipients) { showEmailStatus('请填写所有必填项', 'error'); return; } // 创建测试报告 const testReport = { summary: { total_count: 1, filtered_count: 1, threshold: '50万元', total_amount: '100.00万元', generated_at: new Date().toISOString() }, projects: [{ title: '这是一封测试邮件', date: new Date().toLocaleDateString('zh-CN'), publish_time: new Date().toLocaleString('zh-CN'), budget: { amount: 100, unit: '万元', text: '测试金额', originalUnit: '万元' }, url: 'https://gjzx.nanjing.gov.cn' }] }; showEmailStatus('正在发送测试邮件...', 'info'); try { const response = await fetch(`${API_BASE}/send-email`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emailConfig: config, report: testReport }) }); const data = await response.json(); if (data.success) { showEmailStatus('测试邮件发送成功!请检查收件箱', 'success'); } else { showEmailStatus(`发送失败: ${data.error}`, 'error'); } } catch (error) { showEmailStatus(`请求失败: ${error.message}`, 'error'); } } // 发送报告到邮箱 async function sendReportByEmail() { if (!currentReport) { alert('请先生成报告'); return; } // 从localStorage加载邮件配置 const configStr = localStorage.getItem('emailConfig'); if (!configStr) { alert('请先在"邮件配置"标签页配置邮件服务器'); return; } let emailConfig; try { emailConfig = JSON.parse(configStr); } catch (e) { alert('邮件配置格式错误,请重新配置'); return; } // 验证配置 if (!emailConfig.smtpHost || !emailConfig.smtpUser || !emailConfig.smtpPass || !emailConfig.recipients) { alert('邮件配置不完整,请在"邮件配置"标签页检查配置'); return; } const sendBtn = document.getElementById('sendEmailBtn'); const originalText = sendBtn.textContent; sendBtn.disabled = true; sendBtn.textContent = '正在发送...'; try { const response = await fetch(`${API_BASE}/send-email`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emailConfig: emailConfig, report: currentReport }) }); const data = await response.json(); if (data.success) { alert('报告已成功发送到邮箱!'); } else { alert(`发送失败: ${data.error}`); } } catch (error) { alert(`请求失败: ${error.message}`); } finally { sendBtn.disabled = false; sendBtn.textContent = originalText; } } // 显示邮件配置状态 function showEmailStatus(message, type) { const statusDiv = document.getElementById('emailConfigStatus'); const bgColors = { success: '#d4edda', error: '#f8d7da', info: '#d1ecf1' }; const textColors = { success: '#155724', error: '#721c24', info: '#0c5460' }; statusDiv.innerHTML = `
${message}
`; // 3秒后自动隐藏成功消息 if (type === 'success') { setTimeout(() => { statusDiv.innerHTML = ''; }, 3000); } } // ========== 定时任务功能 ========== // 将Cron表达式转换为友好的时间描述 function cronToFriendlyText(cronTime) { // 常见的预设值映射 const cronMap = { '0 9 * * *': '每天上午9点', '0 6 * * *': '每天上午6点', '0 12 * * *': '每天中午12点', '0 18 * * *': '每天下午18点', '0 9,18 * * *': '每天9点和18点', '0 */6 * * *': '每6小时', '0 */12 * * *': '每12小时', '0 9 * * 1': '每周一上午9点', '0 9 1 * *': '每月1日上午9点' }; // 如果是预设值,直接返回 if (cronMap[cronTime]) { return cronMap[cronTime]; } // 尝试解析自定义时间 "分 时 * * *" 格式 const cronParts = cronTime.split(/\s+/); if (cronParts.length === 5 && cronParts[2] === '*' && cronParts[3] === '*' && cronParts[4] === '*') { const minute = cronParts[0]; const hour = cronParts[1]; // 检查是否是整点 if (minute === '0') { return `每天${hour}点`; } else { return `每天${hour}点${minute}分`; } } // 如果无法解析,返回原始值 return cronTime; } // 加载定时任务配置 async function loadSchedulerConfig() { try { // 从服务器获取配置 const response = await fetch(`${API_BASE}/config`); const data = await response.json(); if (data.success && data.data) { const config = data.data; // 填充表单 if (config.scheduler) { document.getElementById('schedulerEnabled').checked = config.scheduler.enabled || false; const cronTime = config.scheduler.cronTime || '0 9 * * *'; document.getElementById('schedulerCronInput').value = cronTime; document.getElementById('schedulerThresholdInput').value = config.scheduler.threshold || 100000; document.getElementById('schedulerDescription').value = config.scheduler.description || ''; // 时间段配置 document.getElementById('schedulerTimeRange').value = config.scheduler.timeRange || 'thisMonth'; // 反向映射Cron表达式到预设选择器 const presetSelector = document.getElementById('schedulerCronPreset'); const customGroup = document.getElementById('customCronGroup'); // 预设值列表 const presets = [ '0 9 * * *', '0 6 * * *', '0 12 * * *', '0 18 * * *', '0 9,18 * * *', '0 */6 * * *', '0 */12 * * *', '0 9 * * 1', '0 9 1 * *' ]; // 检查是否匹配预设值 if (presets.includes(cronTime)) { presetSelector.value = cronTime; customGroup.style.display = 'none'; } else { // 自定义时间 - 尝试解析为 "分 时 * * *" 格式 presetSelector.value = 'custom'; customGroup.style.display = 'block'; const cronParts = cronTime.split(/\s+/); if (cronParts.length >= 2) { document.getElementById('customMinute').value = cronParts[0]; document.getElementById('customHour').value = cronParts[1]; } } } // 更新状态显示 await updateSchedulerStatus(); } } catch (error) { console.error('加载定时任务配置失败:', error); showSchedulerStatus('加载配置失败: ' + error.message, 'error'); } } // 处理Cron预设选择器变化 function handleCronPresetChange() { const preset = document.getElementById('schedulerCronPreset').value; const customGroup = document.getElementById('customCronGroup'); const cronInput = document.getElementById('schedulerCronInput'); if (preset === 'custom') { // 显示自定义时间选择器 customGroup.style.display = 'block'; updateCustomCron(); // 根据自定义时间生成Cron表达式 } else { // 隐藏自定义时间选择器,使用预设Cron表达式 customGroup.style.display = 'none'; cronInput.value = preset; } } // 根据自定义小时和分钟生成Cron表达式 function updateCustomCron() { const hour = document.getElementById('customHour').value; const minute = document.getElementById('customMinute').value; const cronInput = document.getElementById('schedulerCronInput'); // 生成格式: 分 时 * * * (每天指定时间执行) cronInput.value = `${minute} ${hour} * * *`; } document.addEventListener('DOMContentLoaded', function() { loadEmailConfig(); loadSchedulerConfig(); // 添加自定义时间输入框的事件监听 const customHour = document.getElementById('customHour'); const customMinute = document.getElementById('customMinute'); if (customHour) { customHour.addEventListener('change', updateCustomCron); } if (customMinute) { customMinute.addEventListener('change', updateCustomCron); } }); // 更新定时任务状态显示 async function updateSchedulerStatus() { try { const response = await fetch(`${API_BASE}/scheduler/status`); const data = await response.json(); if (data.success && data.data) { const status = data.data; // 更新运行状态 const statusText = status.isRunning ? '✓ 运行中' : '✗ 未运行'; const statusColor = status.isRunning ? '#28a745' : '#dc3545'; document.getElementById('schedulerRunningStatus').innerHTML = `${statusText}`; // 更新执行计划 if (status.config) { document.getElementById('schedulerCronTime').textContent = cronToFriendlyText(status.config.cronTime); const thresholdBillion = (status.config.threshold / 10000).toFixed(1); document.getElementById('schedulerThreshold').textContent = `${status.config.threshold}万元 (${thresholdBillion}亿)`; } } } catch (error) { console.error('获取定时任务状态失败:', error); } } // 保存定时任务配置 async function saveSchedulerConfig() { const schedulerConfig = { enabled: document.getElementById('schedulerEnabled').checked, cronTime: document.getElementById('schedulerCronInput').value, threshold: parseInt(document.getElementById('schedulerThresholdInput').value), description: document.getElementById('schedulerDescription').value, timeRange: document.getElementById('schedulerTimeRange').value }; // 验证Cron表达式格式(简单验证) const cronParts = schedulerConfig.cronTime.trim().split(/\s+/); if (cronParts.length !== 5) { showSchedulerStatus('Cron表达式格式错误,应为5个部分(分 时 日 月 周)', 'error'); return; } // 从localStorage获取邮件配置 const emailConfigStr = localStorage.getItem('emailConfig'); let emailConfig = {}; if (emailConfigStr) { try { emailConfig = JSON.parse(emailConfigStr); } catch (e) { console.error('解析邮件配置失败:', e); } } // 如果邮件配置为空,提示用户 if (!emailConfig.smtpHost || !emailConfig.smtpUser) { if (confirm('检测到邮件配置未完成,定时任务需要邮件配置才能发送报告。\n\n是否继续保存定时任务配置(不保存邮件配置)?')) { // 继续保存,但不包含邮件配置 } else { return; } } // 构建完整配置对象 const fullConfig = { scheduler: schedulerConfig, email: emailConfig }; showSchedulerStatus('正在保存配置...', 'info'); try { const response = await fetch(`${API_BASE}/config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(fullConfig) }); const data = await response.json(); if (data.success) { showSchedulerStatus('配置已保存,定时任务已重新加载!', 'success'); // 刷新状态显示 await updateSchedulerStatus(); } else { showSchedulerStatus(`保存失败: ${data.error}`, 'error'); } } catch (error) { showSchedulerStatus(`请求失败: ${error.message}`, 'error'); } } // 立即测试运行定时任务 async function testSchedulerNow() { if (!confirm('确定要立即执行定时任务吗?\n\n这将采集本月大于阈值的项目并发送邮件,可能需要几分钟时间。')) { return; } showSchedulerStatus('正在后台执行定时任务,请稍候...', 'info'); try { const response = await fetch(`${API_BASE}/run-scheduled-task`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { showSchedulerStatus('定时任务已在后台开始执行,完成后将发送邮件。请查看服务器控制台日志了解进度。', 'success'); } else { showSchedulerStatus(`执行失败: ${data.error}`, 'error'); } } catch (error) { showSchedulerStatus(`请求失败: ${error.message}`, 'error'); } } // 显示定时任务配置状态 function showSchedulerStatus(message, type) { const statusDiv = document.getElementById('schedulerConfigStatus'); const bgColors = { success: '#d4edda', error: '#f8d7da', info: '#d1ecf1' }; const textColors = { success: '#155724', error: '#721c24', info: '#0c5460' }; statusDiv.innerHTML = `
${message}
`; // 3秒后自动隐藏成功消息 if (type === 'success') { setTimeout(() => { statusDiv.innerHTML = ''; }, 3000); } }