diff --git a/public/app.js b/public/app.js index dc5d909..3a2ffb3 100644 --- a/public/app.js +++ b/public/app.js @@ -1063,7 +1063,7 @@ async function generateCombinedReport() { function displayCombinedReport(winningReport, bidReport, container) { const html = ` -
+

中标公示报告

总项目数
diff --git a/public/index.html b/public/index.html index e015cb9..3c00456 100644 --- a/public/index.html +++ b/public/index.html @@ -6,136 +6,365 @@ 公告抓取与分析工具 @@ -169,11 +398,12 @@ 城市 提示词 状态 + 浏览器 操作 - 加载中... + 加载中... @@ -304,6 +534,11 @@ 启用
+
+ +
@@ -346,7 +581,7 @@ document.getElementById('taskSummary').textContent = `共 ${tasksList.length} 个任务,${enabled} 个启用`; if (tasksList.length === 0) { - tbody.innerHTML = '暂无任务,点击「新增任务」添加'; + tbody.innerHTML = '暂无任务,点击「新增任务」添加'; return; } @@ -355,6 +590,7 @@ ${t.city || '-'} ${t.prompt || '-'} ${t.enabled ? '启用' : '禁用'} + ${t.useBrowser === true ? '打开' : (t.useBrowser === false ? '关闭' : '继承全局')} @@ -367,11 +603,15 @@ function openTaskModal(id) { const item = id ? tasksList.find(t => t.id === id) : null; + const globalUseBrowser = document.getElementById('cfgUseBrowser').value === 'true'; document.getElementById('taskModalTitle').textContent = item ? '编辑任务' : '新增任务'; document.getElementById('taskEditId').value = item ? item.id : ''; document.getElementById('taskCity').value = item ? item.city : ''; document.getElementById('taskPrompt').value = item ? item.prompt : ''; document.getElementById('taskEnabled').checked = item ? item.enabled : true; + document.getElementById('taskUseBrowser').checked = item + ? (typeof item.useBrowser === 'boolean' ? item.useBrowser : globalUseBrowser) + : globalUseBrowser; document.getElementById('taskModal').classList.add('show'); } @@ -386,6 +626,7 @@ city: document.getElementById('taskCity').value.trim(), prompt: document.getElementById('taskPrompt').value.trim(), enabled: document.getElementById('taskEnabled').checked, + useBrowser: document.getElementById('taskUseBrowser').checked, }; try { const url = id ? `/api/tasks/${id}` : '/api/tasks'; @@ -736,6 +977,7 @@ // ===== 初始化 ===== loadTasks(); + loadSettings(); diff --git a/public/results.html b/public/results.html index 5ff32d2..a008f0d 100644 --- a/public/results.html +++ b/public/results.html @@ -6,6 +6,8 @@ 抓取结果查看 - 公告采集工具 + + + /* ===== Glass Theme Override (No Purple) ===== */ + + :root { + --primary: #0f6ecd; + --primary-soft: rgba(15, 110, 205, 0.18); + --secondary: #0ea5a4; + --accent: #f59e0b; + --text: #112941; + --muted: #536b86; + --line: rgba(17, 41, 65, 0.14); + --glass: rgba(255, 255, 255, 0.62); + --glass-strong: rgba(255, 255, 255, 0.82); + --danger: #d94848; + } + + body { + font-family: 'Fira Sans', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif; + color: var(--text); + background: + radial-gradient(900px 500px at -5% -5%, rgba(15, 110, 205, 0.20), transparent 55%), + radial-gradient(900px 520px at 108% -10%, rgba(14, 165, 164, 0.18), transparent 52%), + linear-gradient(145deg, #edf6ff 0%, #e8fbf6 50%, #f6fbff 100%); + } + + .topbar { + background: var(--glass); + border-bottom: 1px solid var(--line); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + } + + .topbar-logo { + background: linear-gradient(125deg, var(--primary), var(--secondary)); + color: #fff; + font-family: 'Fira Code', sans-serif; + font-size: 14px; + font-weight: 700; + letter-spacing: 0.4px; + } + + .topbar-title { color: #1a3f65; } + + .topbar-link { + color: var(--muted); + border: 1px solid transparent; + } + + .topbar-link:hover { + color: var(--text); + background: rgba(255, 255, 255, 0.7); + border-color: var(--line); + } + + .topbar-link.active { + color: var(--primary); + background: rgba(15, 110, 205, 0.12); + border-color: rgba(15, 110, 205, 0.24); + } + + .page-header h1 { color: #173d62; } + .page-header p { color: var(--muted); } + + .stat-card, + .toolbar, + .result-card, + .dialog-box, + .detail-box { + background: var(--glass); + border-color: var(--line); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + box-shadow: 0 16px 36px rgba(15, 42, 74, 0.12); + } + + .stat-card:hover, + .result-card:hover { + background: var(--glass-strong); + border-color: rgba(15, 110, 205, 0.26); + } + + .stat-card .label, + .stat-card .sub, + .page-info, + .result-time, + .dialog-msg { + color: var(--muted); + } + + .stat-card .value, + .detail-header h3, + .dialog-title, + .result-data-body h4, + .detail-body h4 { + color: #183f65; + } + + .filter-group label { color: #304d6b; } + + .filter-select, + .filter-input { + background: rgba(255, 255, 255, 0.76); + border-color: var(--line); + color: var(--text); + } + + .filter-select:focus, + .filter-input:focus { + border-color: rgba(15, 110, 205, 0.5); + box-shadow: 0 0 0 3px rgba(15, 110, 205, 0.12); + } + + .filter-select option { + background: #ffffff; + color: #1b3f62; + } + + .btn { + border: 1px solid transparent; + transition: all 0.2s ease; + } + + .btn-primary { + background: linear-gradient(125deg, var(--primary), #178ac2); + color: #fff; + box-shadow: 0 8px 16px rgba(15, 110, 205, 0.24); + } + + .btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 12px 24px rgba(15, 110, 205, 0.26); + } + + .btn-ghost { + color: #284867; + background: rgba(255, 255, 255, 0.76); + border-color: var(--line); + } + + .btn-ghost:hover { + color: #163a60; + background: #ffffff; + } + + .btn-danger { + color: #b73d3d; + background: rgba(217, 72, 72, 0.10); + border-color: rgba(217, 72, 72, 0.22); + } + + .btn-danger:hover { + color: #fff; + background: linear-gradient(125deg, #d94848, #e36d61); + } + + .result-card::before { + background: linear-gradient(180deg, var(--primary), var(--secondary)); + } + + .result-card.has-error::before { + background: linear-gradient(180deg, #d94848, #ed7a62); + } + + .tag-city { + background: rgba(15, 110, 205, 0.12); + color: #0f5fae; + border-color: rgba(15, 110, 205, 0.24); + } + + .tag-section { + background: rgba(14, 165, 164, 0.12); + color: #0b7a79; + border-color: rgba(14, 165, 164, 0.24); + } + + .tag-type { + background: rgba(245, 158, 11, 0.16); + color: #b96f06; + border-color: rgba(245, 158, 11, 0.28); + } + + .tag-error { + background: rgba(217, 72, 72, 0.14); + color: #bf3b3b; + border-color: rgba(217, 72, 72, 0.28); + } + + .result-url a, + .data-table td a { + color: var(--primary); + } + + .result-data { + background: rgba(255, 255, 255, 0.68); + border-color: var(--line); + } + + .result-data-toggle { + color: #375677; + } + + .result-data-toggle:hover { + color: #1b4168; + } + + .result-data-body pre, + .detail-body pre { + color: #225068; + } + + .data-table { + color: #1f3f5d; + } + + .data-table th, + .data-table td { + border-color: rgba(17, 41, 65, 0.12); + } + + .data-table th { + background: rgba(15, 110, 205, 0.12); + color: #185a9f; + } + + .data-table tr:nth-child(even) { + background: rgba(255, 255, 255, 0.42); + } + + .data-table tr:hover { + background: rgba(255, 255, 255, 0.72); + } + + .result-card-footer { + border-top-color: rgba(17, 41, 65, 0.10); + } + + .page-btn { + background: rgba(255, 255, 255, 0.72); + border-color: var(--line); + color: #2a4b6c; + } + + .page-btn:hover:not(:disabled) { + background: rgba(15, 110, 205, 0.14); + color: var(--primary); + border-color: rgba(15, 110, 205, 0.25); + } + + .page-btn.active-page { + background: rgba(15, 110, 205, 0.2); + color: #0f5fae; + border-color: rgba(15, 110, 205, 0.35); + } + + .empty-state, + .empty-state h3, + .empty-state p { + color: #4a6481; + } + + .spinner { + border-color: rgba(15, 110, 205, 0.18); + border-top-color: var(--primary); + } + + .toast { + color: #fff; + box-shadow: 0 8px 24px rgba(17, 41, 65, 0.24); + } + + .overlay, + .detail-overlay { + background: rgba(8, 20, 36, 0.34); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + } + + .dialog-title, + .detail-header h3 { + font-family: 'Fira Code', 'Noto Sans SC', sans-serif; + } + + .detail-header { + border-bottom-color: rgba(17, 41, 65, 0.10); + } + + .close-btn { + background: rgba(255, 255, 255, 0.75); + color: #345777; + border: 1px solid var(--line); + } + + .close-btn:hover { + background: #ffffff; + color: #1b4269; + } + + @media (prefers-reduced-motion: reduce) { + * { + transition: none !important; + animation: none !important; + } + } +
@@ -859,8 +1165,8 @@
- - + +
@@ -883,7 +1189,7 @@
-
⚠️ 确认清空
+
确认清空
确定要清空所有抓取结果吗?此操作不可撤销。
@@ -993,7 +1299,7 @@ renderResults(json.data || []); renderPagination(json.total, json.page, json.pageSize); } catch (e) { - listEl.innerHTML = `
⚠️

加载失败

${e.message}

`; + listEl.innerHTML = `

加载失败

${e.message}

`; } finally { maskEl.classList.remove('show'); } @@ -1015,7 +1321,7 @@ if (data.length === 0) { listEl.innerHTML = `
-
🔍
+

暂无抓取记录

运行抓取来源后,结果将自动保存在这里

`; @@ -1029,13 +1335,13 @@ ${r.city ? `${r.city}` : ''} ${r.section ? `${r.section}${r.subsection ? ' · ' + r.subsection : ''}` : ''} ${r.type ? `${r.type}` : ''} - ${r.error ? `❌ 失败` : ''} + ${r.error ? ` 失败` : ''}
${formatTime(r.scrapedAt)}
${r.error ? `
错误信息:${r.error}
` : ''} @@ -1043,7 +1349,7 @@ ${!r.error && r.data ? `
@@ -1052,7 +1358,7 @@
` : ''}
@@ -1245,7 +1551,7 @@ else val = String(val); if ((val.startsWith('http://') || val.startsWith('https://')) && !val.includes(' ')) { - html += `🔗 链接`; + html += ` 链接`; } else { html += `${escHtml(val)}`; } @@ -1270,4 +1576,4 @@ - \ No newline at end of file + diff --git a/src/emailService.js b/src/emailService.js index 4c5f29e..8967cf7 100644 --- a/src/emailService.js +++ b/src/emailService.js @@ -767,23 +767,29 @@ function generateScraperResultsHtml(results) { const failResults = results.filter(r => r.error); const generatedAt = new Date().toLocaleString('zh-CN'); - // 把所有成功来源的 items 展开,附带来源信息 + // Flatten all successful source items into one table. const allRows = []; for (const r of successResults) { - const items = r.data?.result || []; + const items = r.data?.results || r.data?.result || []; for (const item of items) { + const hasAmount = typeof item.amount_yuan === 'number' || !!item.amount; + const amountText = + typeof item.amount_yuan === 'number' + ? `${item.amount_yuan.toLocaleString('zh-CN')} CNY` + : (item.amount || 'N/A'); + allRows.push({ - section: [r.section, r.subsection].filter(Boolean).join(' · ') || r.city || '-', - type: r.type || '-', - title: item.title || '-', + section: [r.section, r.subsection].filter(Boolean).join(' / ') || r.city || '-', + type: item.type || r.type || '-', + title: item.project_name || item.title || '-', date: item.date || '-', - amount: item.amount || '未公开', - url: item.url || '', + amount: amountText, + hasAmount, + url: item.target_link || item.url || '', }); } } - // 按日期降序排列 allRows.sort((a, b) => { if (a.date === b.date) return 0; return a.date > b.date ? -1 : 1; @@ -802,7 +808,7 @@ function generateScraperResultsHtml(results) { ${row.title} ${row.date} - ${row.amount} + ${row.amount} ${row.url ? `查看 →` @@ -851,7 +857,7 @@ function generateScraperResultsHtml(results) {
成功来源
-
${allRows.filter(r => r.amount && r.amount !== '未公开').length}
+
${allRows.filter(r => r.hasAmount).length}
有金额
diff --git a/src/scheduler.js b/src/scheduler.js index 00437d5..b6a94ec 100644 --- a/src/scheduler.js +++ b/src/scheduler.js @@ -46,10 +46,11 @@ function appendResult(result) { // ========== 任务执行 ========== async function runTask(task, agentCfg) { + const useBrowser = typeof task.useBrowser === 'boolean' ? task.useBrowser : agentCfg.useBrowser; console.log(`[定时任务][Agent] ${task.city}:开始执行`); const { results } = await runAgentTask(task.prompt, { baseUrl: agentCfg.baseUrl, - useBrowser: agentCfg.useBrowser, + useBrowser, pollInterval: agentCfg.pollInterval, timeout: agentCfg.timeout, logPrefix: `[定时任务][Agent][${task.city}]`, diff --git a/src/server.js b/src/server.js index ce02b05..4079d85 100644 --- a/src/server.js +++ b/src/server.js @@ -27,6 +27,10 @@ function saveConfig(cfg) { writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf-8'); } +function normalizeUseBrowser(value) { + return value === true || value === 'true'; +} + // ========== 抓取结果存取 ========== function readResults() { @@ -130,6 +134,7 @@ app.post('/api/tasks', (req, res) => { city: req.body.city || '', prompt: req.body.prompt || '', enabled: req.body.enabled !== false, + useBrowser: normalizeUseBrowser(req.body.useBrowser), }; cfg.tasks.push(item); saveConfig(cfg); @@ -144,7 +149,11 @@ app.put('/api/tasks/:id', (req, res) => { const cfg = readConfig(); const idx = (cfg.tasks || []).findIndex(t => t.id === req.params.id); if (idx === -1) return res.status(404).json({ success: false, error: '未找到该配置' }); - cfg.tasks[idx] = { ...cfg.tasks[idx], ...req.body, id: req.params.id }; + const patch = { ...req.body }; + if (Object.prototype.hasOwnProperty.call(patch, 'useBrowser')) { + patch.useBrowser = normalizeUseBrowser(patch.useBrowser); + } + cfg.tasks[idx] = { ...cfg.tasks[idx], ...patch, id: req.params.id }; saveConfig(cfg); res.json({ success: true, data: cfg.tasks[idx] }); } catch (e) { @@ -173,11 +182,12 @@ let runningStatus = null; // { taskId, city, startTime, current, total, finished async function runTask(task) { const cfg = readConfig(); const agentCfg = cfg.agent || {}; + const useBrowser = typeof task.useBrowser === 'boolean' ? task.useBrowser : agentCfg.useBrowser; console.log(`[Agent] ${task.city}:开始执行`); const { results } = await runAgentTask(task.prompt, { baseUrl: agentCfg.baseUrl, - useBrowser: agentCfg.useBrowser, + useBrowser, pollInterval: agentCfg.pollInterval, timeout: agentCfg.timeout, logPrefix: `[Agent][${task.city}]`,