// 自动检测当前域名和端口,支持不同环境 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 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 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?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.bidNo}
标段名称: ${item.bidName}
发布日期: ${item.date}
${item.winningBid.amount}${item.winningBid.unit} ${item.href ? `
查看详情 →` : ''}
`).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 limit = parseInt(document.getElementById('reportLimit').value); response = await fetch(`${API_BASE}/report`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ 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.bidNo || '-'}
标段名称: ${project.bidName || '-'}
发布日期: ${project.date}
${project.winningBid.amount}${project.winningBid.unit} ${project.url ? `
查看详情 →` : ''}
`).join('')} `} `; container.innerHTML = html; } async function exportReport() { if (!currentReport) return; // 按需动态加载docx库 if (!window.docx) { try { // 显示加载提示 const loadingMsg = document.createElement('div'); loadingMsg.textContent = '正在加载导出库...'; loadingMsg.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;padding:20px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);z-index:9999;'; document.body.appendChild(loadingMsg); // 动态加载docx库 await new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/docx@7.8.2/build/index.js'; script.onload = resolve; script.onerror = () => { // 降级到unpkg script.src = 'https://unpkg.com/docx@7.8.2/build/index.js'; script.onload = resolve; script.onerror = reject; }; document.head.appendChild(script); }); document.body.removeChild(loadingMsg); } catch (error) { alert('导出库加载失败,请检查网络连接后重试'); 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.bidNo || '-' }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: '标段名称: ', bold: true }), new TextRun({ text: project.bidName || '-' }) ], spacing: { after: 50 } }), 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.winningBid.amount}${project.winningBid.unit}` }) ], spacing: { after: 50 } }), new Paragraph({ children: [ new TextRun({ text: '链接: ', bold: true }), new TextRun({ text: project.url || '-', color: '0000FF' }) ], 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); } // ========== 邮件功能 ========== // 保存邮件配置到服务器 async 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.recipients) { showEmailStatus('请填写SMTP服务器、发件人邮箱和收件人', 'error'); return; } // 如果密码为空,可能是要保持原密码不变 const smtpPassInput = document.getElementById('smtpPass'); if (!config.smtpPass && smtpPassInput.placeholder.includes('已配置')) { // 使用占位符表示保持原密码 config.smtpPass = '***已配置***'; } else if (!config.smtpPass) { showEmailStatus('请填写SMTP密码', 'error'); return; } showEmailStatus('正在保存配置...', 'info'); try { // 先获取当前服务器配置 const getResponse = await fetch(`${API_BASE}/config`); const getData = await getResponse.json(); let fullConfig = { email: config }; // 如果服务器有其他配置(如scheduler),保留它们 if (getData.success && getData.data) { fullConfig = { ...getData.data, email: config }; } // 保存到服务器 const saveResponse = await fetch(`${API_BASE}/config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(fullConfig) }); const saveData = await saveResponse.json(); if (saveData.success) { // 同时保存到localStorage作为备份 localStorage.setItem('emailConfig', JSON.stringify(config)); showEmailStatus('邮件配置已保存到服务器', 'success'); } else { showEmailStatus(`保存失败: ${saveData.error}`, 'error'); } } catch (error) { showEmailStatus(`保存失败: ${error.message}`, 'error'); } } // 从服务器加载邮件配置 async function loadEmailConfig() { try { // 从服务器获取配置 const response = await fetch(`${API_BASE}/config`); const data = await response.json(); if (data.success && data.data && data.data.email) { const config = data.data.email; 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 === '***已配置***' ? '' : (config.smtpPass || ''); if (config.smtpPass === '***已配置***') { document.getElementById('smtpPass').placeholder = '***已配置*** (留空保持不变)'; } document.getElementById('recipients').value = config.recipients || ''; // 同时保存到localStorage作为备份 localStorage.setItem('emailConfig', JSON.stringify(config)); } else { // 如果服务器没有配置,尝试从localStorage加载 const configStr = localStorage.getItem('emailConfig'); if (configStr) { 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 (error) { console.error('从服务器加载邮件配置失败:', error); // 失败时尝试从localStorage加载 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('从localStorage加载邮件配置失败:', 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: '1000万元', total_amount: '10000.00万元', generated_at: new Date().toISOString() }, projects: [{ bidNo: 'TEST001', title: '这是一封测试邮件', bidName: '测试标段', date: new Date().toLocaleDateString('zh-CN'), budget: { amount: 10000, unit: '万元' }, url: 'https://njggzy.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('schedulerWinningThresholdInput').value = config.scheduler.winningThreshold !== undefined ? config.scheduler.winningThreshold : 10000; document.getElementById('schedulerBidThresholdInput').value = config.scheduler.bidThreshold !== undefined ? config.scheduler.bidThreshold : 0; 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() { // 并行加载配置,提高加载速度 Promise.all([ loadEmailConfig().catch(err => console.error('加载邮件配置失败:', err)), loadSchedulerConfig().catch(err => console.error('加载定时任务配置失败:', err)) ]).then(() => { console.log('配置加载完成'); }); // 添加自定义时间输入框的事件监听 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 winningThreshold = status.config.winningThreshold; if (winningThreshold === 0) { document.getElementById('schedulerWinningThreshold').textContent = '不筛选'; } else { const winningBillion = (winningThreshold / 10000).toFixed(1); document.getElementById('schedulerWinningThreshold').textContent = `${winningThreshold}万元 (${winningBillion}亿)`; } const bidThreshold = status.config.bidThreshold; if (bidThreshold === 0) { document.getElementById('schedulerBidThreshold').textContent = '不筛选'; } else { const bidBillion = (bidThreshold / 10000).toFixed(1); document.getElementById('schedulerBidThreshold').textContent = `${bidThreshold}万元 (${bidBillion}亿)`; } } } } catch (error) { console.error('获取定时任务状态失败:', error); } } // 保存定时任务配置 async function saveSchedulerConfig() { const schedulerConfig = { enabled: document.getElementById('schedulerEnabled').checked, cronTime: document.getElementById('schedulerCronInput').value, winningThreshold: parseInt(document.getElementById('schedulerWinningThresholdInput').value), bidThreshold: parseInt(document.getElementById('schedulerBidThresholdInput').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); } } // ========== 招标公告列表功能 ========== let currentBidListPage = 1; // 获取招标公告列表 async function fetchBidList(pageNum) { const page = pageNum || parseInt(document.getElementById('bidListPage').value) || 1; const loading = document.getElementById('bidListLoading'); const results = document.getElementById('bidListResults'); const pagination = document.getElementById('bidListPagination'); currentBidListPage = page; document.getElementById('bidListPage').value = page; loading.classList.add('active'); results.innerHTML = ''; pagination.style.display = 'none'; try { const response = await fetch(`${API_BASE}/bid-announce/list?page=${page}`); const data = await response.json(); if (data.success) { displayBidList(data.data, results); updateBidListPagination(page, data.data.length > 0); } else { results.innerHTML = `
错误: ${data.error}
`; } } catch (error) { results.innerHTML = `
请求失败: ${error.message}
`; } finally { loading.classList.remove('active'); } } function goToBidListPage(page) { if (page < 1) return; fetchBidList(page); } function updateBidListPagination(page, hasData) { const pagination = document.getElementById('bidListPagination'); const currentPageSpan = document.getElementById('bidCurrentPage'); const prevBtn = document.getElementById('bidPrevPage'); const firstBtn = document.getElementById('bidFirstPage'); const nextBtn = document.getElementById('bidNextPage'); if (hasData) { pagination.style.display = 'flex'; currentPageSpan.textContent = page; prevBtn.disabled = page <= 1; firstBtn.disabled = page <= 1; nextBtn.disabled = !hasData; } } function displayBidList(items, container) { if (items.length === 0) { container.innerHTML = '

没有找到招标公告

'; return; } const html = `

找到 ${items.length} 条招标公告

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

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

发布日期: ${item.date}
${item.href ? `查看详情 →` : ''}
`).join('')}
`; container.innerHTML = html; } // ========== 综合报告功能 ========== let currentWinningReport = null; let currentBidReport = null; // 初始化报告日期 function initReportDates() { const today = new Date(); const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); document.getElementById('startDate').value = firstDayOfMonth.toISOString().split('T')[0]; document.getElementById('endDate').value = today.toISOString().split('T')[0]; } // 生成综合报告(同时包含中标和招标) async function generateCombinedReport() { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; const maxPages = parseInt(document.getElementById('maxPages').value) || 10; const winningThreshold = parseFloat(document.getElementById('reportThreshold').value) * 10000 || 0; // 转换为元 const bidThreshold = parseFloat(document.getElementById('bidReportThreshold').value) * 10000 || 0; if (!startDate && !endDate) { alert('请至少填写开始日期或结束日期'); return; } const loading = document.getElementById('reportLoading'); const loadingText = document.getElementById('reportLoadingText'); const results = document.getElementById('reportResults'); const sendBtn = document.getElementById('sendEmailBtn'); loading.classList.add('active'); results.innerHTML = ''; sendBtn.style.display = 'none'; try { // 1. 先获取中标报告 loadingText.textContent = '正在采集中标公示...'; const winningResponse = await fetch(`${API_BASE}/report-daterange`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ startDate, endDate, threshold: winningThreshold, maxPages }) }); const winningData = await winningResponse.json(); // 2. 再获取招标报告 loadingText.textContent = '正在采集招标公告...'; const bidResponse = await fetch(`${API_BASE}/bid-announce/report`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ startDate, endDate, maxPages, threshold: bidThreshold }) }); const bidData = await bidResponse.json(); // 3. 显示综合报告 if (winningData.success && bidData.success) { currentWinningReport = winningData.data; currentBidReport = bidData.data; displayCombinedReport(winningData.data, bidData.data, results); sendBtn.style.display = 'inline-block'; } else { let errorMsg = ''; if (!winningData.success) errorMsg += `中标报告错误: ${winningData.error}\n`; if (!bidData.success) errorMsg += `招标报告错误: ${bidData.error}`; results.innerHTML = `
${errorMsg}
`; } } catch (error) { results.innerHTML = `
请求失败: ${error.message}
`; } finally { loading.classList.remove('active'); loadingText.textContent = '正在生成报告...'; } } // 显示综合报告 function displayCombinedReport(winningReport, bidReport, container) { const html = `

中标公示报告

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

暂无符合条件的中标项目

' : `

中标项目列表 (${winningReport.projects.length} 条)

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

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

标段编号: ${project.bidNo || '-'}
标段名称: ${project.bidName || '-'}
发布日期: ${project.date}
${project.winningBid.amount}${project.winningBid.unit} ${project.url ? `
查看详情 →` : ''}
`).join('')}
`}

招标公告报告

总公告数量
${bidReport.summary.total_count} 条
有金额信息
${bidReport.summary.has_amount_count || bidReport.summary.filtered_count} 条
金额阈值
${bidReport.summary.threshold || '无'}
合同估算总额
${bidReport.summary.total_amount}
${bidReport.projects.length === 0 ? '

暂无符合条件的招标项目

' : `

招标项目详情 (${bidReport.projects.length} 条)

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

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

发布日期: ${item.date}
${item.bidCode ? `
标段编码: ${item.bidCode}
` : ''} ${item.tenderee ? `
招标人: ${item.tenderee}
` : ''} ${item.duration ? `
工期: ${item.duration} 日历天
` : ''} ${item.estimatedAmount ? ` 合同估算价: ${item.estimatedAmount.amountWan} 万元 ` : ''}
查看详情 →
`).join('')}
`} `; container.innerHTML = html; } // 发送综合报告邮件 async function sendCombinedReportByEmail() { if (!currentWinningReport && !currentBidReport) { alert('请先生成报告'); return; } // 从localStorage获取邮件配置 const emailConfigStr = localStorage.getItem('emailConfig'); if (!emailConfigStr) { alert('请先在"邮件配置"页面配置邮件服务器信息'); return; } let emailConfig; try { emailConfig = JSON.parse(emailConfigStr); } catch (e) { alert('邮件配置解析失败,请重新配置'); return; } if (!emailConfig.smtpHost || !emailConfig.smtpUser || !emailConfig.smtpPass || !emailConfig.recipients) { alert('邮件配置不完整,请检查SMTP服务器、用户名、密码和收件人'); return; } if (!confirm(`确定要将综合报告发送到以下邮箱吗?\n\n${emailConfig.recipients}`)) { return; } const sendBtn = document.getElementById('sendEmailBtn'); const originalText = sendBtn.textContent; sendBtn.disabled = true; sendBtn.textContent = '正在发送...'; try { const response = await fetch(`${API_BASE}/send-combined-email`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emailConfig, winningReport: currentWinningReport, bidReport: currentBidReport }) }); 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; } } // 页面加载时初始化报告日期 document.addEventListener('DOMContentLoaded', function() { initReportDates(); });