```
feat: 添加项目管理功能 添加了项目管理标签页,支持按城市、板块、项目名称、金额范围、日期等条件进行过滤查询, 包含完整的前端界面和后端API接口,实现数据去重和分页功能 ```
This commit is contained in:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// ===== 设置 =====
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user