第一版 开发完成
This commit is contained in:
+10
-2
@@ -58,7 +58,7 @@
|
|||||||
| 项目 | 说明 |
|
| 项目 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **接口** | `POST /api/projects/list` |
|
| **接口** | `POST /api/projects/list` |
|
||||||
| **说明** | 分页获取项目列表,支持按名称/编号搜索、按进度/费用筛选、时间筛选、日期异常筛选 |
|
| **说明** | 分页获取项目列表,支持按名称/合同编号搜索、按进度/费用/合同金额筛选、时间筛选、日期异常筛选 |
|
||||||
| **权限** | 已登录,所有角色可访问 |
|
| **权限** | 已登录,所有角色可访问 |
|
||||||
|
|
||||||
#### 请求参数
|
#### 请求参数
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
|--------|------|------|------|
|
|--------|------|------|------|
|
||||||
| page | number | 否 | 页码,从 1 开始,默认 1 |
|
| page | number | 否 | 页码,从 1 开始,默认 1 |
|
||||||
| pageSize | number | 否 | 每页条数,默认 50 |
|
| pageSize | number | 否 | 每页条数,默认 50 |
|
||||||
| searchType | string | 否 | 搜索维度:`name`(项目名称)、`code`(项目编号) |
|
| searchType | string | 否 | 搜索维度:`name`(项目名称)、`code`(合同编号) |
|
||||||
| keyword | string | 否 | 搜索关键词,与 searchType 配合使用 |
|
| keyword | string | 否 | 搜索关键词,与 searchType 配合使用 |
|
||||||
| progress | string | 否 | 项目进度筛选,取值与业务枚举一致 |
|
| progress | string | 否 | 项目进度筛选,取值与业务枚举一致 |
|
||||||
| cost | string | 否 | 项目费用筛选,取值与业务枚举一致 |
|
| cost | string | 否 | 项目费用筛选,取值与业务枚举一致 |
|
||||||
@@ -75,6 +75,8 @@
|
|||||||
| dateFrom | string | 否 | 时间范围起(YYYY-MM-DD),与 dateFilterType 配合 |
|
| dateFrom | string | 否 | 时间范围起(YYYY-MM-DD),与 dateFilterType 配合 |
|
||||||
| dateTo | string | 否 | 时间范围止(YYYY-MM-DD),与 dateFilterType 配合 |
|
| dateTo | string | 否 | 时间范围止(YYYY-MM-DD),与 dateFilterType 配合 |
|
||||||
| dateAbnormal | boolean | 否 | 为 true 时只返回「日期异常」项目(四个日期中任意一个非「日期正常」) |
|
| dateAbnormal | boolean | 否 | 为 true 时只返回「日期异常」项目(四个日期中任意一个非「日期正常」) |
|
||||||
|
| amountMin | number | 否 | 合同金额(万元)下限,按中标合同金额筛选 |
|
||||||
|
| amountMax | number | 否 | 合同金额(万元)上限,按中标合同金额筛选 |
|
||||||
|
|
||||||
#### 响应参数(成功,HTTP 200)
|
#### 响应参数(成功,HTTP 200)
|
||||||
|
|
||||||
@@ -87,6 +89,12 @@
|
|||||||
| list[].progress | string | 项目进度 |
|
| list[].progress | string | 项目进度 |
|
||||||
| list[].cost | string | 项目费用 |
|
| list[].cost | string | 项目费用 |
|
||||||
| list[].updatedAt | string | 最后更新时间,ISO8601 |
|
| 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 |
|
| total | number | 总条数,无数据时为 0 |
|
||||||
| page | number | 当前页码 |
|
| page | number | 当前页码 |
|
||||||
| pageSize | number | 每页条数 |
|
| pageSize | number | 每页条数 |
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class ProjectListBody(BaseModel):
|
|||||||
dateFrom: Optional[str] = None
|
dateFrom: Optional[str] = None
|
||||||
dateTo: Optional[str] = None
|
dateTo: Optional[str] = None
|
||||||
dateAbnormal: Optional[bool] = None # True:只查日期异常项目
|
dateAbnormal: Optional[bool] = None # True:只查日期异常项目
|
||||||
|
amountMin: Optional[float] = None # 合同金额(万元)下限
|
||||||
|
amountMax: Optional[float] = None # 合同金额(万元)上限
|
||||||
|
|
||||||
|
|
||||||
class ProjectDetailBody(BaseModel):
|
class ProjectDetailBody(BaseModel):
|
||||||
@@ -122,6 +124,8 @@ def project_list(body: ProjectListBody, authorization: Optional[str] = Header(No
|
|||||||
date_from=body.dateFrom,
|
date_from=body.dateFrom,
|
||||||
date_to=body.dateTo,
|
date_to=body.dateTo,
|
||||||
date_abnormal=body.dateAbnormal,
|
date_abnormal=body.dateAbnormal,
|
||||||
|
amount_min=body.amountMin,
|
||||||
|
amount_max=body.amountMax,
|
||||||
)
|
)
|
||||||
return {"list": list_, "total": total, "page": page, "pageSize": page_size}
|
return {"list": list_, "total": total, "page": page, "pageSize": page_size}
|
||||||
|
|
||||||
|
|||||||
@@ -169,8 +169,10 @@ def list_projects(
|
|||||||
date_from: Optional[str] = None,
|
date_from: Optional[str] = None,
|
||||||
date_to: Optional[str] = None,
|
date_to: Optional[str] = None,
|
||||||
date_abnormal: Optional[bool] = None,
|
date_abnormal: Optional[bool] = None,
|
||||||
|
amount_min: Optional[float] = None,
|
||||||
|
amount_max: Optional[float] = None,
|
||||||
) -> tuple[list[dict], int]:
|
) -> tuple[list[dict], int]:
|
||||||
"""分页列表,支持按项目名称/合同编号搜索、按进度/费用筛选、时间筛选、日期异常筛选。返回 (list, total)。"""
|
"""分页列表,支持按项目名称/合同编号搜索、按进度/费用/合同金额筛选、时间筛选、日期异常筛选。返回 (list, total)。"""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
cur = conn.cursor(dictionary=True)
|
cur = conn.cursor(dictionary=True)
|
||||||
where_parts = []
|
where_parts = []
|
||||||
@@ -191,6 +193,12 @@ def list_projects(
|
|||||||
if cost and cost.strip():
|
if cost and cost.strip():
|
||||||
where_parts.append("cost = %s")
|
where_parts.append("cost = %s")
|
||||||
params.append(cost.strip())
|
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:
|
if date_abnormal:
|
||||||
where_parts.append(
|
where_parts.append(
|
||||||
"((sign_date_str IS NULL OR sign_date_str != %s) OR (start_date_str IS NULL OR start_date_str != %s) "
|
"((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"]
|
total = cur.fetchone()["cnt"]
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
cur.execute(
|
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),
|
tuple(params) + (page_size, offset),
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
cur.close()
|
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"],
|
"id": r["id"],
|
||||||
"projectName": r["project_name"] or "",
|
"projectName": r["project_name"] or "",
|
||||||
"contractCode": r["contract_code"] or "",
|
"contractCode": r["contract_code"] or "",
|
||||||
"progress": r["progress"] or "",
|
"progress": r["progress"] or "",
|
||||||
"cost": r["cost"] or "",
|
"cost": r["cost"] or "",
|
||||||
"updatedAt": r["updated_at"].isoformat() if r.get("updated_at") else "",
|
"updatedAt": r["updated_at"].isoformat() if r.get("updated_at") else "",
|
||||||
}
|
"bidContractAmount": r.get("bid_contract_amount") or "",
|
||||||
for r in rows
|
"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
|
return list_, total
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-nav {
|
.sidebar-nav {
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
@@ -472,6 +472,10 @@ body {
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-row-amount {
|
||||||
|
margin-top: var(--space-16);
|
||||||
|
}
|
||||||
|
|
||||||
.filter-group {
|
.filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -501,6 +505,10 @@ body {
|
|||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-amount {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
|
|||||||
+18
-5
@@ -64,7 +64,7 @@
|
|||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<select id="searchType" class="input input-select">
|
<select id="searchType" class="input input-select">
|
||||||
<option value="name">项目名称</option>
|
<option value="name">项目名称</option>
|
||||||
<option value="code">项目编号</option>
|
<option value="code">合同编号</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" id="searchKeyword" class="input input-search" placeholder="输入关键词搜索" />
|
<input type="text" id="searchKeyword" class="input input-search" placeholder="输入关键词搜索" />
|
||||||
<button type="button" class="btn btn-search" id="btnSearch">搜索</button>
|
<button type="button" class="btn btn-search" id="btnSearch">搜索</button>
|
||||||
@@ -111,6 +111,16 @@
|
|||||||
<label><input type="checkbox" id="dateAbnormal" /> 仅日期异常</label>
|
<label><input type="checkbox" id="dateAbnormal" /> 仅日期异常</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-row filter-row-amount">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>合同金额(万元)</label>
|
||||||
|
<input type="number" id="amountMin" class="input input-amount" placeholder="最小" step="0.01" min="0" />
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>至</label>
|
||||||
|
<input type="number" id="amountMax" class="input input-amount" placeholder="最大" step="0.01" min="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<div class="table-loading" id="listLoading">加载中…</div>
|
<div class="table-loading" id="listLoading">加载中…</div>
|
||||||
@@ -121,11 +131,14 @@
|
|||||||
<table class="table" id="projectTable">
|
<table class="table" id="projectTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>项目名称</th>
|
|
||||||
<th>合同编号</th>
|
<th>合同编号</th>
|
||||||
<th>进度</th>
|
<th>项目名称</th>
|
||||||
<th>费用状态</th>
|
<th>合同金额</th>
|
||||||
<th>更新时间</th>
|
<th>业主单位</th>
|
||||||
|
<th>签订日期</th>
|
||||||
|
<th>所属项目部</th>
|
||||||
|
<th>总体成本</th>
|
||||||
|
<th>项目负责人及电话</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
+26
-10
@@ -30,6 +30,8 @@
|
|||||||
var dateFrom = document.getElementById('dateFrom');
|
var dateFrom = document.getElementById('dateFrom');
|
||||||
var dateTo = document.getElementById('dateTo');
|
var dateTo = document.getElementById('dateTo');
|
||||||
var dateAbnormal = document.getElementById('dateAbnormal');
|
var dateAbnormal = document.getElementById('dateAbnormal');
|
||||||
|
var amountMin = document.getElementById('amountMin');
|
||||||
|
var amountMax = document.getElementById('amountMax');
|
||||||
var listLoading = document.getElementById('listLoading');
|
var listLoading = document.getElementById('listLoading');
|
||||||
var listEmpty = document.getElementById('listEmpty');
|
var listEmpty = document.getElementById('listEmpty');
|
||||||
var projectTable = document.getElementById('projectTable');
|
var projectTable = document.getElementById('projectTable');
|
||||||
@@ -96,14 +98,23 @@
|
|||||||
if (navList) navList.classList.add('active');
|
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;
|
currentProjectId = projectId;
|
||||||
|
detailMode = mode != null ? mode : (projectId ? 'edit' : 'edit');
|
||||||
if (viewList) viewList.classList.remove('active');
|
if (viewList) viewList.classList.remove('active');
|
||||||
if (viewDetail) viewDetail.classList.add('active');
|
if (viewDetail) viewDetail.classList.add('active');
|
||||||
document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.remove('active'); });
|
document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.remove('active'); });
|
||||||
if (detailTitle) detailTitle.textContent = projectId ? '项目详情' : '新建项目';
|
if (detailTitle) detailTitle.textContent = projectId ? '项目详情' : '新建项目';
|
||||||
if (detailLoading) detailLoading.style.display = 'block';
|
if (detailLoading) detailLoading.style.display = 'block';
|
||||||
if (detailContent) detailContent.style.display = 'none';
|
if (detailContent) detailContent.style.display = 'none';
|
||||||
|
updateDetailHeaderVisibility(detailMode);
|
||||||
|
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
window.API.post('/api/projects/detail', { id: projectId }).then(function (res) {
|
window.API.post('/api/projects/detail', { id: projectId }).then(function (res) {
|
||||||
@@ -198,6 +209,8 @@
|
|||||||
dateFrom: dateFrom && dateFrom.value ? dateFrom.value : undefined,
|
dateFrom: dateFrom && dateFrom.value ? dateFrom.value : undefined,
|
||||||
dateTo: dateTo && dateTo.value ? dateTo.value : undefined,
|
dateTo: dateTo && dateTo.value ? dateTo.value : undefined,
|
||||||
dateAbnormal: dateAbnormal && dateAbnormal.checked ? true : 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.keyword) body.searchType = undefined;
|
||||||
if (!body.dateFilterType) body.dateFrom = body.dateTo = undefined;
|
if (!body.dateFilterType) body.dateFrom = body.dateTo = undefined;
|
||||||
@@ -221,17 +234,19 @@
|
|||||||
if (projectTable) projectTable.style.visibility = 'visible';
|
if (projectTable) projectTable.style.visibility = 'visible';
|
||||||
var html = list
|
var html = list
|
||||||
.map(function (p) {
|
.map(function (p) {
|
||||||
var updatedAt = p.updatedAt ? p.updatedAt.slice(0, 10) : '—';
|
|
||||||
return (
|
return (
|
||||||
'<tr>' +
|
'<tr>' +
|
||||||
'<td>' + (p.projectName || '—') + '</td>' +
|
|
||||||
'<td>' + (p.contractCode || '—') + '</td>' +
|
'<td>' + (p.contractCode || '—') + '</td>' +
|
||||||
'<td>' + (p.progress || '—') + '</td>' +
|
'<td>' + (p.projectName || '—') + '</td>' +
|
||||||
'<td>' + (p.cost || '—') + '</td>' +
|
'<td>' + (p.bidContractAmount != null && p.bidContractAmount !== '' ? p.bidContractAmount : '—') + '</td>' +
|
||||||
'<td>' + updatedAt + '</td>' +
|
'<td>' + (p.ownerUnit || '—') + '</td>' +
|
||||||
|
'<td>' + (p.signDate || '—') + '</td>' +
|
||||||
|
'<td>' + (p.projectDepartment || '—') + '</td>' +
|
||||||
|
'<td>' + (p.totalCost != null && p.totalCost !== '' ? p.totalCost : '—') + '</td>' +
|
||||||
|
'<td>' + (p.projectLeaderContact || '—') + '</td>' +
|
||||||
'<td class="cell-actions">' +
|
'<td class="cell-actions">' +
|
||||||
'<button type="button" class="link-action" data-id="' + p.id + '">查看</button> ' +
|
'<button type="button" class="link-action" data-id="' + p.id + '" data-mode="view">查看</button> ' +
|
||||||
'<button type="button" class="link-action" data-id="' + p.id + '">编辑</button>' +
|
'<button type="button" class="link-action" data-id="' + p.id + '" data-mode="edit">编辑</button>' +
|
||||||
'</td>' +
|
'</td>' +
|
||||||
'</tr>'
|
'</tr>'
|
||||||
);
|
);
|
||||||
@@ -241,7 +256,8 @@
|
|||||||
projectTableBody.querySelectorAll('.link-action').forEach(function (btn) {
|
projectTableBody.querySelectorAll('.link-action').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', function () {
|
||||||
var id = btn.getAttribute('data-id');
|
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 (btnPrev) btnPrev.addEventListener('click', function () { if (currentPage > 1) { currentPage--; loadList(); } });
|
||||||
if (btnNext) btnNext.addEventListener('click', function () { if (currentPage * PAGE_SIZE < totalCount) { 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 (btnBack) btnBack.addEventListener('click', function () { showList(); loadList(); });
|
||||||
if (navList) navList.addEventListener('click', function (e) { e.preventDefault(); showList(); });
|
if (navList) navList.addEventListener('click', function (e) { e.preventDefault(); showList(); });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user