feat: 添加项目管理功能

添加了项目管理标签页,支持按城市、板块、项目名称、金额范围、日期等条件进行过滤查询,
包含完整的前端界面和后端API接口,实现数据去重和分页功能
```
This commit is contained in:
2026-03-19 11:40:14 +08:00
parent d78dc655ee
commit 7bfba04199
4 changed files with 582 additions and 33 deletions

View File

@@ -520,6 +520,7 @@
<div class="tabs">
<button class="tab active" onclick="switchTab('tasks')">任务配置</button>
<button class="tab" onclick="switchTab('results')">抓取结果</button>
<button class="tab" onclick="switchTab('projects')">项目管理</button>
<button class="tab" onclick="switchTab('settings')">系统设置</button>
</div>
@@ -573,13 +574,84 @@
</select>
<button class="btn btn-secondary btn-sm" onclick="loadResults()">刷新</button>
</div>
<button class="btn btn-danger btn-sm" onclick="clearResults()">清空全部</button>
<!-- <button class="btn btn-danger btn-sm" onclick="clearResults()">清空全部</button> -->
</div>
<div id="resultsList"></div>
<div id="resultsPagination" style="text-align:center;margin-top:16px;"></div>
</div>
<!-- ===== 项目管理 Tab ===== -->
<div id="tab-projects" class="tab-content">
<div class="config-section" style="padding-bottom:16px;">
<div class="form-row">
<div class="form-group">
<label>城市</label>
<select id="projectFilterCity">
<option value="">全部城市</option>
</select> </div>
<div class="form-group">
<label>板块</label>
<input type="text" id="projectFilterSection" placeholder="请输入板块名称">
</div>
<div class="form-group">
<label>项目名称</label>
<input type="text" id="projectFilterProjectName" placeholder="请输入项目名称">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>最小金额</label>
<input type="number" id="projectFilterMinAmount" placeholder="最小金额" min="0" step="0.01">
</div>
<div class="form-group">
<label>最大金额</label>
<input type="number" id="projectFilterMaxAmount" placeholder="最大金额" min="0" step="0.01">
</div>
<div class="form-group">
<label>开始日期</label>
<input type="date" id="projectFilterStartDate">
</div>
<div class="form-group">
<label>结束日期</label>
<input type="date" id="projectFilterEndDate">
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;">
<span style="font-size:13px;color:#888;">按项目名称拆分并去重后展示</span>
<div style="display:flex;gap:10px;align-items:center;">
<button class="btn btn-primary btn-sm" onclick="searchProjectResults()">查询</button>
<button class="btn btn-secondary btn-sm" onclick="resetProjectFilters()">重置</button>
<!-- <button class="btn btn-danger btn-sm" onclick="clearResults()">清空全部</button> -->
</div>
</div>
</div>
<div class="config-section" style="padding:0;overflow:hidden;">
<div style="padding:16px 20px;border-bottom:1px solid rgba(15, 35, 58, 0.08);font-size:13px;color:#66768a;" id="projectsSummary">加载中...</div>
<div style="overflow-x:auto;">
<table>
<thead>
<tr>
<th style="min-width:120px;">城市</th>
<th style="min-width:140px;">板块</th>
<th>项目名称</th>
<th style="min-width:150px;">金额</th>
<th style="min-width:120px;">发布日期</th>
<th style="min-width:120px;">详情链接</th>
</tr>
</thead>
<tbody id="projectsTbody">
<tr>
<td colspan="6" class="empty-state">加载中...</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="projectsPagination" style="text-align:center;margin-top:16px;"></div>
</div>
<!-- ===== 系统设置 Tab ===== -->
<div id="tab-settings" class="tab-content">
<!-- Agent 配置 -->
@@ -712,16 +784,18 @@
let currentTasksPage = 1;
const tasksPageSize = 10;
let currentResultsPage = 1;
let currentProjectsPage = 1;
// ===== Tab 切换 =====
function switchTab(name) {
document.querySelectorAll('.tab').forEach((t, i) => {
t.classList.toggle('active', ['tasks', 'results', 'settings'][i] === name);
t.classList.toggle('active', ['tasks', 'results', 'projects', 'settings'][i] === name);
});
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
if (name === 'results') loadResults();
if (name === 'projects') loadProjectResults();
if (name === 'settings') loadSettings();
}
@@ -1030,36 +1104,127 @@
// ===== 结果展示 =====
async function loadResults() {
const city = document.getElementById('filterCity').value;
try {
// 加载筛选选项
const filtersRes = await fetch('/api/results/filters');
const filtersJson = await filtersRes.json();
if (filtersJson.success) {
const sel = document.getElementById('filterCity');
const curVal = sel.value;
sel.innerHTML = '<option value="">全部城市</option>' +
(filtersJson.data.cities || []).map(c => `<option value="${c}" ${c === curVal ? 'selected' : ''}>${c}</option>`).join('');
}
// 加载结果
await loadResultCityOptions('filterCity');
const city = document.getElementById('filterCity').value;
const params = new URLSearchParams({ page: currentResultsPage, pageSize: 10 });
if (city) params.set('city', city);
const res = await fetch('/api/results?' + params);
const json = await res.json();
if (!json.success) throw new Error(json.error);
renderResults(json.data, json.total, json.page, json.pageSize);
renderResults({
data: json.data,
total: json.total,
page: json.page,
pageSize: json.pageSize,
listId: 'resultsList',
paginationId: 'resultsPagination',
});
} catch (e) {
document.getElementById('resultsList').innerHTML = `<div class="status-box status-error">加载失败: ${e.message}</div>`;
document.getElementById('resultsPagination').innerHTML = '';
}
}
function renderResults(data, total, page, pageSize) {
const container = document.getElementById('resultsList');
async function loadProjectResults() {
try {
await loadProjectFilters();
const params = new URLSearchParams({ page: currentProjectsPage, pageSize: 10 });
const projectFilters = getProjectFilterValues();
if (projectFilters.minAmount && projectFilters.maxAmount && Number(projectFilters.minAmount) > Number(projectFilters.maxAmount)) {
throw new Error('最小金额不能大于最大金额');
}
if (projectFilters.startDate && projectFilters.endDate && projectFilters.startDate > projectFilters.endDate) {
throw new Error('开始日期不能晚于结束日期');
}
Object.entries(projectFilters).forEach(([key, value]) => {
if (value !== '') params.set(key, value);
});
const res = await fetch('/api/projects?' + params);
const json = await res.json();
if (!json.success) throw new Error(json.error);
renderProjects({
data: json.data,
total: json.total,
page: json.page,
pageSize: json.pageSize,
});
} catch (e) {
document.getElementById('projectsTbody').innerHTML = `<tr><td colspan="6" class="empty-state">加载失败: ${e.message}</td></tr>`;
document.getElementById('projectsSummary').textContent = '项目列表加载失败';
document.getElementById('projectsPagination').innerHTML = '';
}
}
async function loadProjectFilters() {
const filtersRes = await fetch('/api/projects/filters');
const filtersJson = await filtersRes.json();
if (!filtersJson.success) return;
populateSelectWithPlaceholder('projectFilterCity', '全部城市', filtersJson.data.cities || []);
}
async function loadResultCityOptions(selectId) {
const filtersRes = await fetch('/api/results/filters');
const filtersJson = await filtersRes.json();
if (!filtersJson.success) return;
populateSelectWithPlaceholder(selectId, '全部城市', filtersJson.data.cities || []);
}
function populateSelectWithPlaceholder(selectId, placeholder, items) {
const sel = document.getElementById(selectId);
const curVal = sel.value;
sel.innerHTML = `<option value="">${placeholder}</option>` +
items.map(item => `<option value="${item}" ${item === curVal ? 'selected' : ''}>${item}</option>`).join('');
}
function getProjectFilterValues() {
return {
city: document.getElementById('projectFilterCity').value.trim(),
section: document.getElementById('projectFilterSection').value.trim(),
projectName: document.getElementById('projectFilterProjectName').value.trim(),
minAmount: document.getElementById('projectFilterMinAmount').value.trim(),
maxAmount: document.getElementById('projectFilterMaxAmount').value.trim(),
startDate: document.getElementById('projectFilterStartDate').value.trim(),
endDate: document.getElementById('projectFilterEndDate').value.trim(),
};
}
function searchProjectResults() {
currentProjectsPage = 1;
loadProjectResults();
}
function resetProjectFilters() {
[
'projectFilterCity',
'projectFilterSection',
'projectFilterProjectName',
'projectFilterMinAmount',
'projectFilterMaxAmount',
'projectFilterStartDate',
'projectFilterEndDate',
].forEach((id) => {
document.getElementById(id).value = '';
});
currentProjectsPage = 1;
loadProjectResults();
}
function renderResults({ data, total, page, pageSize, listId, paginationId }) {
const container = document.getElementById(listId);
if (!data || data.length === 0) {
container.innerHTML = '<div class="empty-state">暂无结果</div>';
document.getElementById('resultsPagination').innerHTML = '';
document.getElementById(paginationId).innerHTML = '';
return;
}
@@ -1070,7 +1235,7 @@
const itemsHtml = results.length > 0 ? results.map(item => `
<div class="result-item">
<span class="type">${item.type || '-'}</span>
<span class="name">${item.project_name || '-'}</span>
<span class="name">${item.project_name || item.projectName || item.title || '-'}</span>
<span class="amount">${item.amount_yuan ? item.amount_yuan.toLocaleString() + ' 元' : '-'}</span>
<span class="date">${item.date || '-'}</span>
${(item.detail_link || item.target_link) ? `<a href="${item.detail_link || item.target_link}" target="_blank">查看</a>` : ''}
@@ -1092,26 +1257,69 @@
`;
}).join('');
// 分页
renderPagination({ total, page, pageSize, paginationId, view: 'results' });
}
function renderProjects({ data, total, page, pageSize }) {
const tbody = document.getElementById('projectsTbody');
const summary = document.getElementById('projectsSummary');
if (!data || data.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">暂无去重后的项目</td></tr>';
summary.textContent = '当前筛选条件下没有项目';
document.getElementById('projectsPagination').innerHTML = '';
return;
}
const startIndex = (page - 1) * pageSize + 1;
const endIndex = Math.min(page * pageSize, total);
summary.textContent = `显示第 ${startIndex}${endIndex} 条记录,共 ${total} 条记录`;
tbody.innerHTML = data.map(project => `
<tr>
<td>${escHtml(project.city || '-')}</td>
<td>${escHtml(project.section || project.type || '-')}</td>
<td>${escHtml(project.projectName || '-')}</td>
<td>${formatAmount(project.amountYuan)}</td>
<td>${escHtml(project.date || '-')}</td>
<td>${project.detailLink ? `<a href="${project.detailLink}" target="_blank">查看详情</a>` : '-'}</td>
</tr>
`).join('');
renderPagination({ total, page, pageSize, paginationId: 'projectsPagination', view: 'projects' });
}
function renderPagination({ total, page, pageSize, paginationId, view }) {
const totalPages = Math.ceil(total / pageSize);
const pagination = document.getElementById(paginationId);
if (totalPages > 1) {
let pHtml = '';
for (let i = 1; i <= totalPages; i++) {
pHtml += `<button class="btn btn-sm ${i === page ? 'btn-primary' : 'btn-secondary'}" onclick="goPage(${i})" style="margin:0 3px;">${i}</button>`;
pHtml += `<button class="btn btn-sm ${i === page ? 'btn-primary' : 'btn-secondary'}" onclick="goPage('${view}', ${i})" style="margin:0 3px;">${i}</button>`;
}
document.getElementById('resultsPagination').innerHTML = `<div>第 ${page}/${totalPages} 页,共 ${total} 条</div>${pHtml}`;
pagination.innerHTML = `<div>第 ${page}/${totalPages} 页,共 ${total} 条</div>${pHtml}`;
} else {
document.getElementById('resultsPagination').innerHTML = total > 0 ? `${total}` : '';
pagination.innerHTML = total > 0 ? `${total}` : '';
}
}
function goPage(p) { currentResultsPage = p; loadResults(); }
function goPage(view, p) {
if (view === 'projects') {
currentProjectsPage = p;
loadProjectResults();
return;
}
currentResultsPage = p;
loadResults();
}
async function deleteResult(id) {
if (!confirm('确定删除这条结果?')) return;
try {
await fetch(`/api/results/${id}`, { method: 'DELETE' });
loadResults();
await Promise.all([loadResults(), loadProjectResults()]);
} catch (e) {
alert('删除失败: ' + e.message);
}
@@ -1121,12 +1329,28 @@
if (!confirm('确定要清空全部结果吗?此操作不可恢复。')) return;
try {
await fetch('/api/results', { method: 'DELETE' });
loadResults();
currentResultsPage = 1;
currentProjectsPage = 1;
await Promise.all([loadResults(), loadProjectResults()]);
} catch (e) {
alert('清空失败: ' + e.message);
}
}
function formatAmount(amount) {
if (typeof amount !== 'number' || !Number.isFinite(amount)) return '-';
return `¥ ${amount.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`;
}
function escHtml(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// ===== 设置 =====
async function loadSettings() {
try {
@@ -1215,7 +1439,29 @@
}
}
function initProjectSearchInputs() {
[
'projectFilterCity',
'projectFilterSection',
'projectFilterProjectName',
'projectFilterMinAmount',
'projectFilterMaxAmount',
'projectFilterStartDate',
'projectFilterEndDate',
].forEach((id) => {
const element = document.getElementById(id);
if (!element) return;
element.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
searchProjectResults();
}
});
});
}
// ===== 初始化 =====
initProjectSearchInputs();
loadTasks();
loadSettings();
</script>

View File

@@ -1166,7 +1166,7 @@
<div class="toolbar-actions">
<button class="btn btn-ghost" onclick="resetFilters()">↺ 重置</button>
<button class="btn btn-primary" onclick="loadResults()"> 刷新</button>
<button class="btn btn-danger" onclick="confirmClearAll()"> 清空全部</button>
<!-- <button class="btn btn-danger" onclick="confirmClearAll()"> 清空全部</button> -->
</div>
</div>