diff --git a/backend/API.md b/backend/API.md index 87e7bd1..e8e1bbe 100644 --- a/backend/API.md +++ b/backend/API.md @@ -58,7 +58,7 @@ | 项目 | 说明 | |------|------| | **接口** | `POST /api/projects/list` | -| **说明** | 分页获取项目列表,支持按名称/编号搜索、按进度/费用筛选、时间筛选、日期异常筛选 | +| **说明** | 分页获取项目列表,支持按名称/合同编号搜索、按进度/费用/合同金额筛选、时间筛选、日期异常筛选 | | **权限** | 已登录,所有角色可访问 | #### 请求参数 @@ -67,7 +67,7 @@ |--------|------|------|------| | page | number | 否 | 页码,从 1 开始,默认 1 | | pageSize | number | 否 | 每页条数,默认 50 | -| searchType | string | 否 | 搜索维度:`name`(项目名称)、`code`(项目编号) | +| searchType | string | 否 | 搜索维度:`name`(项目名称)、`code`(合同编号) | | keyword | string | 否 | 搜索关键词,与 searchType 配合使用 | | progress | string | 否 | 项目进度筛选,取值与业务枚举一致 | | cost | string | 否 | 项目费用筛选,取值与业务枚举一致 | @@ -75,6 +75,8 @@ | dateFrom | string | 否 | 时间范围起(YYYY-MM-DD),与 dateFilterType 配合 | | dateTo | string | 否 | 时间范围止(YYYY-MM-DD),与 dateFilterType 配合 | | dateAbnormal | boolean | 否 | 为 true 时只返回「日期异常」项目(四个日期中任意一个非「日期正常」) | +| amountMin | number | 否 | 合同金额(万元)下限,按中标合同金额筛选 | +| amountMax | number | 否 | 合同金额(万元)上限,按中标合同金额筛选 | #### 响应参数(成功,HTTP 200) @@ -87,6 +89,12 @@ | list[].progress | string | 项目进度 | | list[].cost | string | 项目费用 | | list[].updatedAt | string | 最后更新时间,ISO8601 | +| list[].bidContractAmount | string | 中标合同金额(万元) | +| list[].ownerUnit | string | 业主单位 | +| list[].signDate | string | 签订日期(原文或 YYYY-MM-DD) | +| list[].projectDepartment | string | 所属项目部 | +| list[].totalCost | string | 总体成本 | +| list[].projectLeaderContact | string | 项目负责人及电话 | | total | number | 总条数,无数据时为 0 | | page | number | 当前页码 | | pageSize | number | 每页条数 | diff --git a/backend/src/app.py b/backend/src/app.py index bb99ccc..b9c96fd 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -33,6 +33,8 @@ class ProjectListBody(BaseModel): dateFrom: Optional[str] = None dateTo: Optional[str] = None dateAbnormal: Optional[bool] = None # True:只查日期异常项目 + amountMin: Optional[float] = None # 合同金额(万元)下限 + amountMax: Optional[float] = None # 合同金额(万元)上限 class ProjectDetailBody(BaseModel): @@ -122,6 +124,8 @@ def project_list(body: ProjectListBody, authorization: Optional[str] = Header(No date_from=body.dateFrom, date_to=body.dateTo, date_abnormal=body.dateAbnormal, + amount_min=body.amountMin, + amount_max=body.amountMax, ) return {"list": list_, "total": total, "page": page, "pageSize": page_size} diff --git a/backend/src/projects_repo.py b/backend/src/projects_repo.py index 52b06c3..29923ed 100644 --- a/backend/src/projects_repo.py +++ b/backend/src/projects_repo.py @@ -169,8 +169,10 @@ def list_projects( date_from: Optional[str] = None, date_to: Optional[str] = None, date_abnormal: Optional[bool] = None, + amount_min: Optional[float] = None, + amount_max: Optional[float] = None, ) -> tuple[list[dict], int]: - """分页列表,支持按项目名称/合同编号搜索、按进度/费用筛选、时间筛选、日期异常筛选。返回 (list, total)。""" + """分页列表,支持按项目名称/合同编号搜索、按进度/费用/合同金额筛选、时间筛选、日期异常筛选。返回 (list, total)。""" with get_connection() as conn: cur = conn.cursor(dictionary=True) where_parts = [] @@ -191,6 +193,12 @@ def list_projects( if cost and cost.strip(): where_parts.append("cost = %s") params.append(cost.strip()) + if amount_min is not None: + where_parts.append("(bid_contract_amount + 0) >= %s") + params.append(amount_min) + if amount_max is not None: + where_parts.append("(bid_contract_amount + 0) <= %s") + params.append(amount_max) if date_abnormal: where_parts.append( "((sign_date_str IS NULL OR sign_date_str != %s) OR (start_date_str IS NULL OR start_date_str != %s) " @@ -216,22 +224,32 @@ def list_projects( total = cur.fetchone()["cnt"] offset = (page - 1) * page_size cur.execute( - f"SELECT id, project_name, contract_code, progress, cost, updated_at FROM projects WHERE {where_sql} ORDER BY updated_at DESC LIMIT %s OFFSET %s", + f"""SELECT id, project_name, contract_code, progress, cost, updated_at, + bid_contract_amount, owner_unit, sign_date, sign_date_str, + project_department, total_cost, project_leader_contact + FROM projects WHERE {where_sql} ORDER BY updated_at DESC LIMIT %s OFFSET %s""", tuple(params) + (page_size, offset), ) rows = cur.fetchall() cur.close() - list_ = [ - { + list_ = [] + for r in rows: + _sd = r.get("sign_date") + sign_val = r.get("sign_date_str") or (_sd.strftime("%Y-%m-%d") if _sd else None) + list_.append({ "id": r["id"], "projectName": r["project_name"] or "", "contractCode": r["contract_code"] or "", "progress": r["progress"] or "", "cost": r["cost"] or "", "updatedAt": r["updated_at"].isoformat() if r.get("updated_at") else "", - } - for r in rows - ] + "bidContractAmount": r.get("bid_contract_amount") or "", + "ownerUnit": r.get("owner_unit") or "", + "signDate": sign_val or "", + "projectDepartment": r.get("project_department") or "", + "totalCost": str(r["total_cost"]) if r.get("total_cost") is not None else "", + "projectLeaderContact": r.get("project_leader_contact") or "", + }) return list_, total diff --git a/frontend/css/style.css b/frontend/css/style.css index 283f4c7..3489272 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -232,7 +232,7 @@ body { } .sidebar-nav { - flex: 1; + flex: 0 0 auto; } .nav-item { @@ -472,6 +472,10 @@ body { align-items: flex-end; } +.filter-row-amount { + margin-top: var(--space-16); +} + .filter-group { display: flex; align-items: center; @@ -501,6 +505,10 @@ body { min-width: 130px; } +.input-amount { + min-width: 100px; +} + .input { width: 100%; padding: 0.5rem 0.75rem; diff --git a/frontend/index.html b/frontend/index.html index 340d9fd..cdccf79 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -64,7 +64,7 @@
@@ -111,6 +111,16 @@
+
+
+ + +
+
+ + +
+
加载中…
@@ -121,11 +131,14 @@ - - - - + + + + + + + diff --git a/frontend/js/app.js b/frontend/js/app.js index aa4e462..a2ffec5 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -30,6 +30,8 @@ var dateFrom = document.getElementById('dateFrom'); var dateTo = document.getElementById('dateTo'); var dateAbnormal = document.getElementById('dateAbnormal'); + var amountMin = document.getElementById('amountMin'); + var amountMax = document.getElementById('amountMax'); var listLoading = document.getElementById('listLoading'); var listEmpty = document.getElementById('listEmpty'); var projectTable = document.getElementById('projectTable'); @@ -96,14 +98,23 @@ if (navList) navList.classList.add('active'); } - function showDetail(projectId) { + var detailMode = 'edit'; // 'view' | 'edit' + + function updateDetailHeaderVisibility(mode) { + if (btnSave) btnSave.style.display = mode === 'edit' ? '' : 'none'; + if (btnViewLog) btnViewLog.style.display = mode === 'edit' ? '' : 'none'; + } + + function showDetail(projectId, mode) { currentProjectId = projectId; + detailMode = mode != null ? mode : (projectId ? 'edit' : 'edit'); if (viewList) viewList.classList.remove('active'); if (viewDetail) viewDetail.classList.add('active'); document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.remove('active'); }); if (detailTitle) detailTitle.textContent = projectId ? '项目详情' : '新建项目'; if (detailLoading) detailLoading.style.display = 'block'; if (detailContent) detailContent.style.display = 'none'; + updateDetailHeaderVisibility(detailMode); if (projectId) { window.API.post('/api/projects/detail', { id: projectId }).then(function (res) { @@ -198,6 +209,8 @@ dateFrom: dateFrom && dateFrom.value ? dateFrom.value : undefined, dateTo: dateTo && dateTo.value ? dateTo.value : undefined, dateAbnormal: dateAbnormal && dateAbnormal.checked ? true : undefined, + amountMin: amountMin && amountMin.value !== '' ? parseFloat(amountMin.value) : undefined, + amountMax: amountMax && amountMax.value !== '' ? parseFloat(amountMax.value) : undefined, }; if (!body.keyword) body.searchType = undefined; if (!body.dateFilterType) body.dateFrom = body.dateTo = undefined; @@ -221,17 +234,19 @@ if (projectTable) projectTable.style.visibility = 'visible'; var html = list .map(function (p) { - var updatedAt = p.updatedAt ? p.updatedAt.slice(0, 10) : '—'; return ( '' + - '' + '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + '' + '' ); @@ -241,7 +256,8 @@ projectTableBody.querySelectorAll('.link-action').forEach(function (btn) { btn.addEventListener('click', function () { var id = btn.getAttribute('data-id'); - showDetail(id); + var mode = btn.getAttribute('data-mode') || 'edit'; + showDetail(id, mode); }); }); } @@ -309,7 +325,7 @@ if (btnPrev) btnPrev.addEventListener('click', function () { if (currentPage > 1) { currentPage--; loadList(); } }); if (btnNext) btnNext.addEventListener('click', function () { if (currentPage * PAGE_SIZE < totalCount) { currentPage++; loadList(); } }); - if (btnNewProject) btnNewProject.addEventListener('click', function () { showDetail(null); }); + if (btnNewProject) btnNewProject.addEventListener('click', function () { showDetail(null, 'edit'); }); if (btnBack) btnBack.addEventListener('click', function () { showList(); loadList(); }); if (navList) navList.addEventListener('click', function (e) { e.preventDefault(); showList(); });
项目名称 合同编号进度费用状态更新时间项目名称合同金额业主单位签订日期所属项目部总体成本项目负责人及电话 操作
' + (p.projectName || '—') + '' + (p.contractCode || '—') + '' + (p.progress || '—') + '' + (p.cost || '—') + '' + updatedAt + '' + (p.projectName || '—') + '' + (p.bidContractAmount != null && p.bidContractAmount !== '' ? p.bidContractAmount : '—') + '' + (p.ownerUnit || '—') + '' + (p.signDate || '—') + '' + (p.projectDepartment || '—') + '' + (p.totalCost != null && p.totalCost !== '' ? p.totalCost : '—') + '' + (p.projectLeaderContact || '—') + '' + - ' ' + - '' + + ' ' + + '' + '