API 第一版定型
This commit is contained in:
+396
@@ -0,0 +1,396 @@
|
||||
# 项目管理系统 API 文档
|
||||
|
||||
**依据**:《产品文档》《交互文档》《初始需求》
|
||||
**读者**:后端/前端工程师
|
||||
**版本**:v1
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
- **基础路径**:`/api`(按实际部署约定)
|
||||
- **请求方式**:**全部采用 POST**
|
||||
- **认证方式**:登录后使用 Token(如 Bearer Token 或 Session),请求头携带,具体与现有账号体系对齐
|
||||
- **数据格式**:请求体与响应体均为 JSON,编码 UTF-8,Content-Type: application/json
|
||||
- **角色**:管理员、市场部、工程部、技经部、财务部、物贸部;新建项目仅市场部可调,查看/修改所有角色可调
|
||||
|
||||
---
|
||||
|
||||
## 2. 认证
|
||||
|
||||
### 2.1 登录
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /api/auth/login` |
|
||||
| **说明** | 用户登录,成功返回 Token 及用户信息(含角色) |
|
||||
| **权限** | 无需登录 |
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| username | string | 是 | 账号 |
|
||||
| password | string | 是 | 密码 |
|
||||
|
||||
#### 响应参数(成功,HTTP 200)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| token | string | 登录凭证,后续请求需携带 |
|
||||
| user | object | 当前用户信息 |
|
||||
| user.id | string | 用户 ID |
|
||||
| user.username | string | 账号 |
|
||||
| user.role | string | 角色:管理员 / 市场部 / 工程部 / 技经部 / 财务部 / 物贸部 |
|
||||
| user.displayName | string | 显示名称 |
|
||||
|
||||
#### 响应参数(失败,HTTP 401)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| code | number | 错误码,401 |
|
||||
| message | string | 错误信息,如「账号或密码错误」 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 项目列表
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /api/projects/list` |
|
||||
| **说明** | 分页获取项目列表,支持按名称/编号搜索、按进度/费用筛选 |
|
||||
| **权限** | 已登录,所有角色可访问 |
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| page | number | 否 | 页码,从 1 开始,默认 1 |
|
||||
| pageSize | number | 否 | 每页条数,默认 20 |
|
||||
| searchType | string | 否 | 搜索维度:`name`(项目名称)、`code`(项目编号) |
|
||||
| keyword | string | 否 | 搜索关键词,与 searchType 配合使用 |
|
||||
| progress | string | 否 | 项目进度筛选,取值与业务枚举一致 |
|
||||
| cost | string | 否 | 项目费用筛选,取值与业务枚举一致 |
|
||||
|
||||
#### 响应参数(成功,HTTP 200)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| list | array | 项目列表,无数据时为空数组 |
|
||||
| list[].id | string | 项目 ID |
|
||||
| list[].projectName | string | 项目名称 |
|
||||
| list[].contractCode | string | 合同编号 |
|
||||
| list[].progress | string | 项目进度 |
|
||||
| list[].cost | string | 项目费用 |
|
||||
| list[].updatedAt | string | 最后更新时间,ISO8601 |
|
||||
| total | number | 总条数,无数据时为 0 |
|
||||
| page | number | 当前页码 |
|
||||
| pageSize | number | 每页条数 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 项目详情
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /api/projects/detail` |
|
||||
| **说明** | 获取单个项目完整信息(5 个分类) |
|
||||
| **权限** | 已登录,所有角色可访问 |
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | string | 是 | 项目 ID |
|
||||
|
||||
#### 响应参数(成功,HTTP 200)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | string | 项目 ID |
|
||||
| contract | object | 合同信息,字段见第 8.1 节 |
|
||||
| costControl | object | 成本控制,字段见第 8.2 节 |
|
||||
| receivable | object | 应收款,字段见第 8.3 节 |
|
||||
| payable | object | 应付款,字段见第 8.4 节 |
|
||||
| other | object | 其他,字段见第 8.5 节 |
|
||||
| createdAt | string | 创建时间,ISO8601 |
|
||||
| updatedAt | string | 更新时间,ISO8601 |
|
||||
|
||||
#### 响应参数(失败,HTTP 404)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| code | number | 错误码,404 |
|
||||
| message | string | 错误信息,如「项目不存在」 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 新建项目
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /api/projects/create` |
|
||||
| **说明** | 新建项目,仅市场部可调用 |
|
||||
| **权限** | 已登录,角色为市场部;非市场部返回 403 |
|
||||
|
||||
#### 请求参数
|
||||
|
||||
请求体为项目 5 个分类的完整或部分数据,未传字段由后端赋默认值。结构见第 8 节「项目信息字段(5 个分类)」;**新建时合同编号、项目名称为必填项**。
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| contract | object | 是 | 合同信息,字段见 8.1;其中 contractCode、projectName 必填 |
|
||||
| contract.contractCode | string | 是 | 合同编号 |
|
||||
| contract.projectName | string | 是 | 项目名称 |
|
||||
| costControl | object | 否 | 成本控制,字段见 8.2 |
|
||||
| receivable | object | 否 | 应收款,字段见 8.3 |
|
||||
| payable | object | 否 | 应付款,字段见 8.4 |
|
||||
| other | object | 否 | 其他,字段见 8.5 |
|
||||
|
||||
#### 响应参数(成功,HTTP 201)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | string | 新创建的项目 ID |
|
||||
| message | string | 固定为「创建成功」 |
|
||||
|
||||
#### 响应参数(失败,HTTP 400)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| code | number | 错误码,400 |
|
||||
| message | string | 错误信息,如「合同编号不能为空」「项目名称不能为空」 |
|
||||
| errors | array | 可选,见第 10 节 |
|
||||
|
||||
#### 响应参数(失败,HTTP 403)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| code | number | 错误码,403 |
|
||||
| message | string | 错误信息,如「无权限」 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 更新项目
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /api/projects/update` |
|
||||
| **说明** | 更新项目全部或部分信息;每次保存由后端记录一条操作日志 |
|
||||
| **权限** | 已登录,所有角色可访问 |
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | string | 是 | 项目 ID |
|
||||
| contract | object | 否 | 合同信息,传则按约定全量/合并更新,字段见 8.1 |
|
||||
| costControl | object | 否 | 成本控制,字段见 8.2 |
|
||||
| receivable | object | 否 | 应收款,字段见 8.3 |
|
||||
| payable | object | 否 | 应付款,字段见 8.4 |
|
||||
| other | object | 否 | 其他,字段见 8.5 |
|
||||
|
||||
#### 响应参数(成功,HTTP 200)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | string | 项目 ID |
|
||||
| message | string | 固定为「保存成功」 |
|
||||
|
||||
#### 响应参数(失败)
|
||||
|
||||
| HTTP 状态 | 参数名 | 类型 | 说明 |
|
||||
|-----------|--------|------|------|
|
||||
| 404 | code | number | 错误码,404 |
|
||||
| 404 | message | string | 如「项目不存在」 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 操作日志
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **接口** | `POST /api/projects/logs` |
|
||||
| **说明** | 获取某项目的修改记录(操作人、时间、修改摘要) |
|
||||
| **权限** | 已登录,所有角色可访问 |
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | string | 是 | 项目 ID |
|
||||
| page | number | 否 | 页码,从 1 开始,默认 1 |
|
||||
| pageSize | number | 否 | 每页条数,默认 20 |
|
||||
|
||||
#### 响应参数(成功,HTTP 200)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| list | array | 操作日志列表,无记录时为空数组 |
|
||||
| list[].id | string | 日志 ID |
|
||||
| list[].operatorId | string | 操作人 ID |
|
||||
| list[].operatorName | string | 操作人姓名 |
|
||||
| list[].operatedAt | string | 操作时间,ISO8601 |
|
||||
| list[].summary | string | 修改摘要,如「合同信息」「成本控制」 |
|
||||
| list[].detail | string | 可选,修改内容详情 |
|
||||
| total | number | 总条数,无记录时为 0 |
|
||||
| page | number | 当前页码 |
|
||||
| pageSize | number | 每页条数 |
|
||||
|
||||
#### 响应参数(失败,HTTP 404)
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| code | number | 错误码,404 |
|
||||
| message | string | 如「项目不存在」 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 项目信息字段(5 个分类)
|
||||
|
||||
以下为各分类在 **请求/响应** 中使用的字段定义;单位、是否必填、校验规则由后端统一规定,前端与后端对齐。
|
||||
|
||||
### 8.1 合同信息 `contract`
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| serialNo | string | 序号 |
|
||||
| contractCode | string | 合同编号 |
|
||||
| powerBureauContractCode | string | 供电局项目合同编号 |
|
||||
| projectName | string | 项目名称 |
|
||||
| subItemCount | number | 子项个数 |
|
||||
| subItemCode | string | 子项编码 |
|
||||
| totalInvestment | number | 项目总投资(万元) |
|
||||
| bidContractAmount | number | 中标合同金额(万元) |
|
||||
| warrantyRatio | string \| number | 质保金比例 |
|
||||
| settlementAmount | number | 结算金额(万元) |
|
||||
| totalCostEstimate | number | 总成本测算 |
|
||||
| voltageLevel | string | 工程电压等级 |
|
||||
| projectCategory | string | 工程类别 |
|
||||
| ownerUnit | string | 业主单位 |
|
||||
| ownerContact | string | 业主联系人及电话 |
|
||||
| bidType | string | 中标形式 |
|
||||
| signDate | string | 签订日期(YYYY-MM-DD) |
|
||||
| startDate | string | 开工日期 |
|
||||
| plannedCompletionDate | string | 计划竣工日期 |
|
||||
| actualCompletionDate | string | 实际竣工日期 |
|
||||
| warrantyAmount | number | 质保金(万元) |
|
||||
| warrantyEndDate | string | 质保期截止日 |
|
||||
| actualWarrantyRefundDate | string | 实际退质保金日期 |
|
||||
| projectDepartment | string | 所属项目部 |
|
||||
| projectLeaderContact | string | 项目负责人及电话 |
|
||||
| paymentMethod | string | 工程款拨付方式 |
|
||||
|
||||
### 8.2 成本控制 `costControl`
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| totalCost | number | 总体成本 |
|
||||
| isAdjusted | string | 是否调整:是/否 |
|
||||
| migrantWorkerPlan | string \| number | 农民工工资(按进度计划) |
|
||||
| migrantWorkerActual | number | 农民工工资(实付) |
|
||||
| selfSupplyMaterialControl | number | 乙供材料费(控制) |
|
||||
| materialPayableByRatio | number | 应付材料费(按收款比例) |
|
||||
| materialActualOccurred | number | 实际发生材料费 |
|
||||
| materialActualPaid | number | 实际支付材料费 |
|
||||
| otherCostControl | number | 其他费用(控制) |
|
||||
| otherPayable | number | 应付其他费 |
|
||||
| otherActual | number | 实际其他费用 |
|
||||
| tax | number | 税金 |
|
||||
| profit | number | 利润(万元) |
|
||||
| actualProfit | number | 实际利润(万元) |
|
||||
| costSettlementAmount | number | 成本结算金额(万元) |
|
||||
|
||||
### 8.3 应收款 `receivable`
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| receivableProgress | number | 应收款(完成进度款) |
|
||||
| invoiceAmount | number | 开票金额(万元) |
|
||||
| actualReceiptAmount | number | 实际收款金额(万元) |
|
||||
| actualReceiptRate | string \| number | 实际收款完成率 |
|
||||
|
||||
### 8.4 应付款 `payable`
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| payableAmount | number | 应付款金额(万元) |
|
||||
| actualPaymentAmount | number | 实际付款金额(万元) |
|
||||
| unreceivedAmount | number | 未收款(万元) |
|
||||
| actualPaymentRate | string \| number | 实际付款完成率 |
|
||||
| migrantWorkerArrears | number | 民工工资清欠金额(万元) |
|
||||
|
||||
### 8.5 其他 `other`
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| settlementCostEstimate | number | 结算后成本测算金额 |
|
||||
| settlementLaborCost | number | 结算人工费 |
|
||||
| settlementMaterialCost | number | 结算材料费 |
|
||||
| settlementOtherCost | number | 结算其他费 |
|
||||
| dueSettlementCount | number | 到期应结算项目个数 |
|
||||
| overdueSettlementCount | number | 到期未完成结算个数 |
|
||||
| existingProblems | string | 存在的问题 |
|
||||
| suggestions | string | 建议措施 |
|
||||
| cumulativeProgress | string \| number | 累计进度 |
|
||||
| remark | string | 备注 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 项目列表/筛选枚举
|
||||
|
||||
以下由后端定义并提供给前端(如通过配置接口或文档约定):
|
||||
|
||||
- **项目进度 progress**:枚举值列表,如「未开始 / 进行中 / 已竣工」等
|
||||
- **项目费用 cost**:枚举值列表,如按金额区间的选项
|
||||
|
||||
具体取值以实际业务与后端实现为准。
|
||||
|
||||
---
|
||||
|
||||
## 10. 统一错误响应
|
||||
|
||||
| HTTP 状态 | 说明 |
|
||||
|-----------|------|
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 未登录或 Token 无效 |
|
||||
| 403 | 无权限(如非市场部调用新建项目) |
|
||||
| 404 | 资源不存在(如项目 ID 不存在) |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
#### 错误响应体参数
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| code | number | HTTP 状态码或业务错误码 |
|
||||
| message | string | 错误描述 |
|
||||
| errors | array | 可选,参数校验详情 |
|
||||
| errors[].field | string | 出错字段名 |
|
||||
| errors[].message | string | 该字段错误说明 |
|
||||
|
||||
**示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "参数校验失败",
|
||||
"errors": [
|
||||
{ "field": "projectName", "message": "项目名称不能为空" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 接口一览
|
||||
|
||||
| 接口 | 方法 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| /api/auth/login | POST | 登录 | 公开 |
|
||||
| /api/projects/list | POST | 项目列表(搜索、筛选、分页) | 已登录 |
|
||||
| /api/projects/detail | POST | 项目详情 | 已登录 |
|
||||
| /api/projects/create | POST | 新建项目 | 已登录且市场部 |
|
||||
| /api/projects/update | POST | 更新项目(记操作日志) | 已登录 |
|
||||
| /api/projects/logs | POST | 操作日志列表 | 已登录 |
|
||||
|
||||
字段命名采用驼峰;日期格式建议 ISO8601 或 `YYYY-MM-DD`,前后端约定一致。
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# 交互文档(前端)
|
||||
|
||||
**依据**:《产品文档》
|
||||
**读者**:前端工程师
|
||||
**目标**:明确页面数量、每个页面的功能与用户交互,便于实现与联调。
|
||||
|
||||
---
|
||||
|
||||
## 1. 页面总览
|
||||
|
||||
| 序号 | 页面名称 | 说明 |
|
||||
|------|--------------|------|
|
||||
| 1 | 登录页 | 用户登录入口(若与现有系统统一,可复用现有登录页) |
|
||||
| 2 | 项目列表页 | 项目列表的入口页,支持搜索、筛选、进入详情、新建项目 |
|
||||
| 3 | 项目详情/编辑页 | 查看/编辑单个项目信息,含 5 个 Tab;新建项目时也为本页(空表单) |
|
||||
| 4 | 操作日志 | 查看某项目的修改记录,可为抽屉或独立页 |
|
||||
|
||||
**合计:4 个页面(或 3 个主页面 + 操作日志以抽屉形式承载)。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 角色与权限(影响各页显隐)
|
||||
|
||||
| 角色 | 新建项目 | 查看/编辑项目 |
|
||||
|------|----------|----------------|
|
||||
| 市场部 | ✓ | ✓ |
|
||||
| 管理员、工程部、技经部、财务部、物贸部 | ✗ | ✓ |
|
||||
|
||||
- **新建项目**:仅市场部展示入口;其他角色不展示或入口不可用。
|
||||
- **查看/编辑**:所有角色均可进入项目列表、项目详情,并可编辑、保存;每次保存由后端记录一条操作日志。
|
||||
- 前端需按当前用户角色控制按钮/入口的**显隐与禁用**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 各页面功能与用户交互
|
||||
|
||||
### 页面 1:登录页
|
||||
|
||||
- **功能**:用户登录,进入系统。若项目与现有账号体系统一,则复用现有登录页;否则需单独实现登录页。
|
||||
- **用户交互**:
|
||||
- 用户输入账号、密码(等必填项),点击「登录」。
|
||||
- 成功:跳转至**项目列表页**。
|
||||
- 失败:页内提示错误信息,不跳转。
|
||||
|
||||
---
|
||||
|
||||
### 页面 2:项目列表页
|
||||
|
||||
- **功能**:展示项目列表,支持按名称/编号搜索、按进度/费用筛选;进入项目详情或新建项目。
|
||||
- **用户交互**:
|
||||
|
||||
| 用户操作 | 系统反馈 |
|
||||
|----------|----------|
|
||||
| 进入页面 | 请求列表数据;请求中显示 Loading;成功则展示表格;无数据时展示空状态文案。 |
|
||||
| 选择搜索维度(项目名称 / 项目编号),输入关键词,点击「搜索」或回车 | 按当前维度和关键词请求列表;请求中可保留原列表或局部 Loading;结果替换列表;无结果时展示空状态。 |
|
||||
| 选择「项目进度」筛选 | 按所选进度请求列表并刷新表格;无结果时展示空状态。 |
|
||||
| 选择「项目费用」筛选 | 按所选费用请求列表并刷新表格;无结果时展示空状态。 |
|
||||
| 点击某一行的「查看」或「编辑」 | 跳转至**项目详情/编辑页**,并传入该项目 ID;详情加载中显示 Loading。 |
|
||||
| 点击「新建项目」(仅市场部可见) | 跳转至**项目详情/编辑页**,不传项目 ID(新建模式);页面展示 5 个 Tab 的空表单。 |
|
||||
| 分页操作(若有) | 请求对应页数据并刷新列表。 |
|
||||
|
||||
- **列表展示**:至少包含项目名称、合同编号、进度、费用等关键信息列,以及操作列(查看/编辑)。具体列与后端接口字段一致。
|
||||
|
||||
---
|
||||
|
||||
### 页面 3:项目详情/编辑页
|
||||
|
||||
- **功能**:查看或编辑单个项目的全部信息(5 个分类);新建时为本页空表单。所有有权限角色均可编辑并保存,保存由后端记操作日志。
|
||||
- **用户交互**:
|
||||
|
||||
| 用户操作 | 系统反馈 |
|
||||
|----------|----------|
|
||||
| 进入页面(带项目 ID) | 请求该项目详情;请求中显示 Loading;成功后在当前页展示 5 个 Tab 及表单内容。 |
|
||||
| 进入页面(新建,无项目 ID) | 直接展示 5 个 Tab 的空表单,无 Loading。 |
|
||||
| 切换 Tab(合同信息 / 成本控制 / 应收款 / 应付款 / 其他) | 切换当前展示的 Tab 内容,无请求。 |
|
||||
| 填写或修改表单字段 | 仅前端状态变化;可做前端校验(与后端约定)。 |
|
||||
| 点击「保存」 | 提交中:保存按钮 Loading,禁止再次点击;成功:提示「保存成功」(如 Toast),新建时可选跳转至该详情或列表;失败:提示接口返回错误信息,不关闭当前页。 |
|
||||
| 点击「返回」或等价入口 | 返回**项目列表页**;是否保留列表页上次的搜索/筛选状态与产品约定。 |
|
||||
| 点击「查看操作日志」 | 打开**操作日志**(抽屉或新页),展示当前项目的修改记录。 |
|
||||
|
||||
- **5 个 Tab**:合同信息、成本控制、应收款、应付款、其他。每个 Tab 内为该分类下的表单字段,字段与《产品文档》一致,单位与校验与后端对齐。字段清单见本文第 5 节。
|
||||
|
||||
---
|
||||
|
||||
### 页面 4:操作日志
|
||||
|
||||
- **功能**:展示某一项目的修改记录(操作人、时间、修改摘要等),仅查看,无编辑。
|
||||
- **承载形式**:可为详情页右侧**抽屉**,或**独立页面**;与产品/前端约定一致即可。
|
||||
- **用户交互**:
|
||||
|
||||
| 用户操作 | 系统反馈 |
|
||||
|----------|----------|
|
||||
| 打开操作日志(从详情页入口) | 请求该项目下的操作日志;请求中显示 Loading;成功则展示记录列表;无记录时展示空状态文案。 |
|
||||
| 关闭抽屉 / 返回 | 关闭抽屉或返回详情页/列表页。 |
|
||||
|
||||
- **列表内容**:操作人、操作时间、修改内容摘要(如「合同信息」「成本控制」等),具体字段与后端接口一致。
|
||||
|
||||
---
|
||||
|
||||
## 4. 全局状态与异常
|
||||
|
||||
| 场景 | 交互反馈 |
|
||||
|------|----------|
|
||||
| 列表/详情/日志请求中 | 明确 Loading 态(整页或局部)。 |
|
||||
| 保存提交中 | 保存按钮 Loading,禁止再次点击。 |
|
||||
| 保存成功 | 统一成功提示(如 Toast)。 |
|
||||
| 保存或接口失败 | 页内或 Toast 提示错误信息,不关闭当前页。 |
|
||||
| 无权限(如 403) | 提示无权限,可引导返回列表。 |
|
||||
| 列表无数据 / 无搜索结果 | 空状态文案,无表格内容。 |
|
||||
| 操作日志无记录 | 空状态文案。 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 项目信息字段(5 个 Tab)
|
||||
|
||||
详情/编辑页按 5 个 Tab 组织,字段与《产品文档》一致;单位、校验、是否必填与后端对齐。
|
||||
|
||||
- **合同信息**:序号、合同编号、供电局项目合同编号、项目名称、子项个数、子项编码、项目总投资、中标合同金额、质保金比例、结算金额、总成本测算、工程电压等级、工程类别、业主单位、业主联系人及电话、中标形式、签订日期、开工日期、计划竣工日期、实际竣工日期、质保金、质保期截止日、实际退质保金日期、所属项目部、项目负责人及电话、工程款拨付方式。
|
||||
- **成本控制**:总体成本、是否调整(是/否)、农民工工资(按进度计划/实付)、乙供材料费(控制)、应付材料费(按收款比例)、实际发生/支付材料费、其他费用(控制)、应付其他费、实际其他费用、税金、利润/实际利润、成本结算金额。
|
||||
- **应收款**:应收款(完成进度款)、开票金额、实际收款金额、实际收款完成率。
|
||||
- **应付款**:应付款金额、实际付款金额、未收款、实际付款完成率、民工工资清欠金额。
|
||||
- **其他**:结算后成本测算金额、结算人工费/材料费/其他费、到期应结算项目个数、到期未完成结算个数、存在的问题、建议措施、累计进度、备注。
|
||||
|
||||
---
|
||||
|
||||
## 6. 小结(给前端)
|
||||
|
||||
- **页面数**:4 个(登录、项目列表、项目详情/编辑、操作日志;操作日志可为抽屉)。
|
||||
- **权限**:仅市场部展示「新建项目」;所有角色可查看、编辑、保存;保存由后端记日志,前端仅提供「查看操作日志」入口与展示。
|
||||
- **搜索**:项目名称、项目编号;触发方式与后端约定(如回车/点击搜索)。
|
||||
- **筛选**:项目进度、项目费用;选项与列表接口参数一致。
|
||||
- **详情页**:5 个 Tab 对应 5 类字段,保存时防重复提交、成功/失败有明确反馈。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v2
|
||||
**依据**:产品文档
|
||||
@@ -0,0 +1,30 @@
|
||||
# 产品文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
项目管理系统,多部门协作维护项目信息,支持按名称/编号/进度/费用检索与筛选。
|
||||
|
||||
## 2. 用户与权限
|
||||
|
||||
**角色**:管理员、市场部、工程部、技经部、财务部、物贸部。
|
||||
|
||||
- **创建项目**:仅市场部。
|
||||
- **查看与修改**:所有角色(含市场部)均可查看和修改项目信息。
|
||||
- **修改日志**:对项目信息的修改需记录操作日志。
|
||||
|
||||
## 3. 项目信息(5 个分类)
|
||||
|
||||
**合同信息**:序号、合同编号、供电局项目合同编号、项目名称、子项个数、子项编码、项目总投资、中标合同金额、质保金比例、结算金额、总成本测算、工程电压等级、工程类别、业主单位、业主联系人及电话、中标形式、签订/开工/计划竣工/实际竣工日期、质保金、质保期截止日、实际退质保金日期、所属项目部、项目负责人及电话、工程款拨付方式。
|
||||
|
||||
**成本控制**:总体成本、是否调整(是/否)、农民工工资(按进度计划/实付)、乙供材料费(控制)、应付材料费(按收款比例)、实际发生/支付材料费、其他费用(控制)、应付其他费、实际其他费用、税金、利润/实际利润、成本结算金额。
|
||||
|
||||
**应收款**:应收款(完成进度款)、开票金额、实际收款金额、实际收款完成率。
|
||||
|
||||
**应付款**:应付款金额、实际付款金额、未收款、实际付款完成率、民工工资清欠金额。
|
||||
|
||||
**其他**:结算后成本测算金额、结算人工费/材料费/其他费、到期应结算项目个数、到期未完成结算个数、存在的问题、建议措施、累计进度、备注。
|
||||
|
||||
## 4. 项目统计
|
||||
|
||||
- **搜索**:按项目名称、项目编号。
|
||||
- **筛选**:按项目进度、按项目费用。
|
||||
@@ -0,0 +1,89 @@
|
||||
- 用户系统
|
||||
每个用归属于这些
|
||||
管理员
|
||||
市场部
|
||||
工程部
|
||||
技经部
|
||||
财务部
|
||||
物贸部
|
||||
只有市场部门能创建项目信息,其他用户可以查看和修改项目信息(修改项目信息需要记录日志)。
|
||||
|
||||
- 项目信息
|
||||
有"合同信息","成本控制", "应收款", "应付款", "其他" 这5个分类。
|
||||
"合同信息" 这个分类有以下这些字段
|
||||
序号
|
||||
合同编号
|
||||
供电局项目合同编号
|
||||
项目名称
|
||||
子项个数
|
||||
子项编码
|
||||
项目总投资(万元)
|
||||
中标合同金额(万元)
|
||||
质保金比例
|
||||
结算金额(万元)
|
||||
总成本测算
|
||||
工程电压等级
|
||||
工程类别
|
||||
业主单位
|
||||
业主联系人及电话
|
||||
中标形式
|
||||
签订日期
|
||||
开工日期
|
||||
计划竣工日期
|
||||
实际竣工日期
|
||||
质保金(万元)
|
||||
质保期截止日
|
||||
实际退质保金日期
|
||||
所属项目部
|
||||
项目负责人及电话
|
||||
工程款拨付方式
|
||||
|
||||
"成本控制"这个分类有以下这些字段
|
||||
总体成本
|
||||
是否调整(是/否)
|
||||
农民工工资(按进度计划)
|
||||
农民工工资(实付)
|
||||
乙供材料费(控制)
|
||||
应付材料费(按收款比例)
|
||||
实际发生材料费
|
||||
实际支付材料费
|
||||
其他费用(控制)
|
||||
应付其他费
|
||||
实际其他费用
|
||||
税金
|
||||
利润(万元)
|
||||
实际利润(万元)
|
||||
成本结算金额(万元)
|
||||
|
||||
"应收款"这个分类有以下这些字段
|
||||
应收款(完成进度款)
|
||||
开票金额(万元)
|
||||
实际收款金额(万元)
|
||||
实际收款完成率
|
||||
|
||||
"应付款"这个分类有以下这些字段
|
||||
应付款金额(万元)
|
||||
实际付款金额(万元)
|
||||
未收款(万元)
|
||||
实际付款完成率
|
||||
民工工资清欠金额(万元)
|
||||
|
||||
"其他"这个分类有以下这些字段
|
||||
结算后成本测算金额
|
||||
结算人工费
|
||||
结算材料费
|
||||
结算其他费
|
||||
到期应结算项目个数
|
||||
到期未完成结算个数
|
||||
存在的问题
|
||||
建议措施
|
||||
累计进度
|
||||
备注
|
||||
|
||||
- 项目统计
|
||||
可以根据,项目名称和项目编号搜索项目
|
||||
可以根据,项目精度筛选项目
|
||||
可以根据,项目费用筛选项目
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# 项目管理系统 · 静态页面(前端参考)
|
||||
|
||||
依据《产品文档》与《交互文档》实现的静态效果,供前端工程师实现与联调参考。**单入口** `index.html`,4 个页面/视图在同一文件中切换。
|
||||
|
||||
---
|
||||
|
||||
## 页面与交互文档对应关系
|
||||
|
||||
| 序号 | 页面 | 在静态中的位置 | 交互文档章节 |
|
||||
|------|------------|--------------------------|----------------|
|
||||
| 1 | 登录页 | 首次打开即显示 | § 页面1 |
|
||||
| 2 | 项目列表页 | 登录后主内容区默认视图 | § 页面2 |
|
||||
| 3 | 项目详情/编辑页 | 点击列表「查看/编辑」或「新建项目」 | § 页面3 |
|
||||
| 4 | 操作日志 | 详情页点击「查看操作日志」打开的右侧抽屉 | § 页面4 |
|
||||
|
||||
---
|
||||
|
||||
## 如何打开
|
||||
|
||||
```bash
|
||||
# 方式一:直接打开(部分浏览器可能限制本地请求)
|
||||
xdg-open ui/index.html # Linux
|
||||
open ui/index.html # macOS
|
||||
|
||||
# 方式二:本地 HTTP 服务(推荐)
|
||||
cd ui && python3 -m http.server 8080
|
||||
# 浏览器访问 http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 各页面功能与操作(与交互文档一致)
|
||||
|
||||
**页面1 - 登录页**
|
||||
- 输入账号、密码,点击「登录」。
|
||||
- 成功:进入项目列表页(主应用)。
|
||||
- 失败:页内提示错误(静态演示:密码填 `fail` 可触发失败)。
|
||||
|
||||
**页面2 - 项目列表页**
|
||||
- 进入即请求列表(Mock);Loading → 表格或空状态。
|
||||
- 搜索:选择「项目名称/项目编号」+ 输入关键词,点击「搜索」或回车。
|
||||
- 筛选:选择「项目进度」「项目费用」后自动刷新列表。
|
||||
- 操作列:「查看」「编辑」进入详情页;「新建项目」仅市场部可见(侧栏「当前角色」选市场部)。
|
||||
|
||||
**页面3 - 项目详情/编辑页**
|
||||
- 带项目 ID 进入:Loading → 5 个 Tab 表单;新建(无 ID):直接展示空表单。
|
||||
- 切换 Tab:合同信息、成本控制、应收款、应付款、其他。
|
||||
- 保存:按钮 Loading、防重复点击;成功 Toast「保存成功」。
|
||||
- 返回:回到项目列表。
|
||||
- 查看操作日志:打开页面4 抽屉。
|
||||
|
||||
**页面4 - 操作日志**
|
||||
- 抽屉内展示当前项目的修改记录(Mock);Loading、空状态。
|
||||
|
||||
---
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `index.html`:4 个页面/视图的 DOM(登录、列表、详情、操作日志抽屉)。
|
||||
- `css/style.css`:全局与各页面样式。
|
||||
- `js/app.js`:登录/退出、视图切换、Mock 数据、搜索/筛选、保存与 Toast、操作日志抽屉。
|
||||
|
||||
数据均为前端 Mock,无后端接口。
|
||||
@@ -0,0 +1,849 @@
|
||||
/* ========== 变量与基础 ========== */
|
||||
:root {
|
||||
--sidebar-bg: #0f172a;
|
||||
--sidebar-text: #e2e8f0;
|
||||
--sidebar-text-muted: #94a3b8;
|
||||
--sidebar-accent: #38bdf8;
|
||||
--sidebar-hover: rgba(56, 189, 248, 0.12);
|
||||
--main-bg: #f1f5f9;
|
||||
--card-bg: #ffffff;
|
||||
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
--card-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--border: #e2e8f0;
|
||||
--border-focus: #38bdf8;
|
||||
--text: #1e293b;
|
||||
--text-muted: #64748b;
|
||||
--primary: #0ea5e9;
|
||||
--primary-hover: #0284c7;
|
||||
--primary-light: rgba(14, 165, 233, 0.1);
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--radius: 10px;
|
||||
--radius-sm: 6px;
|
||||
--font-head: "Outfit", "Noto Sans SC", sans-serif;
|
||||
--font-body: "Noto Sans SC", "Outfit", sans-serif;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font-body);
|
||||
color: var(--text);
|
||||
background: var(--main-bg);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ========== 页面1:登录页 ========== */
|
||||
.view-login {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(160deg, #0f172a 0%, #1e293b 50%, #334155 100%);
|
||||
}
|
||||
|
||||
.view-login.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
padding: 2rem 2.25rem;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
text-align: center;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.login-brand .brand-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--primary);
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 600;
|
||||
font-size: 1.35rem;
|
||||
color: var(--text);
|
||||
margin: 0 0 0.35rem 0;
|
||||
}
|
||||
|
||||
.login-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-form .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login-form .form-group label {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.login-form .input {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.85rem;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--danger);
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.btn-login-submit {
|
||||
width: 100%;
|
||||
padding: 0.65rem 1rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* ========== 布局 ========== */
|
||||
.app {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ========== 侧栏 ========== */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
background: var(--sidebar-bg);
|
||||
color: var(--sidebar-text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--sidebar-accent);
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 600;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.65rem 1.25rem;
|
||||
color: var(--sidebar-text-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--sidebar-text);
|
||||
background: var(--sidebar-hover);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--sidebar-accent);
|
||||
background: var(--sidebar-hover);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1rem 1.25rem 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.role-switch {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.role-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--sidebar-text-muted);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.role-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.6rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--sidebar-text);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.role-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--sidebar-accent);
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--sidebar-text-muted);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
color: var(--sidebar-text);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* ========== 主内容区 ========== */
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 1.5rem 2rem 2rem;
|
||||
}
|
||||
|
||||
/* ========== 视图切换 ========== */
|
||||
.view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ========== 页面头部 ========== */
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 600;
|
||||
font-size: 1.35rem;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-title.detail-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0.75rem;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background: #f8fafc;
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* ========== 按钮 ========== */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.35);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.btn-new-project .btn-icon {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ========== 工具栏(搜索与筛选) ========== */
|
||||
.toolbar {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.search-row .input-select {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-row .input-search {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-filter {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* ========== 输入框 ========== */
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: var(--card-bg);
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px var(--primary-light);
|
||||
}
|
||||
|
||||
.input-select,
|
||||
.input-filter {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.6rem center;
|
||||
padding-right: 1.75rem;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
min-height: 60px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* ========== 表格 ========== */
|
||||
.table-wrap {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.table-loading,
|
||||
.table-empty {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
.table-empty .empty-icon {
|
||||
font-size: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.table thead {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.table th {
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table .cell-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.table .link-action {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 0.875rem;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.table .link-action:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ========== 分页 ========== */
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pagination-btns {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pagination .btn-sm:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ========== 详情页 Tabs ========== */
|
||||
.detail-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 240px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0 1rem;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0.75rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
.tab-panels {
|
||||
padding: 1.5rem 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ========== 表单网格 ========== */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.form-group-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ========== 抽屉(操作日志) ========== */
|
||||
.drawer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.drawer-overlay.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
background: var(--card-bg);
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.12);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.25s ease;
|
||||
z-index: 101;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.drawer.visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 600;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.drawer-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.drawer-close:hover {
|
||||
background: #f1f5f9;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 1rem 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.drawer-loading,
|
||||
.drawer-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 160px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.drawer-empty .empty-icon {
|
||||
font-size: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.log-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-item .log-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.log-item .log-desc {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ========== Toast ========== */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
padding: 0.65rem 1.25rem;
|
||||
background: var(--text);
|
||||
color: #fff;
|
||||
font-size: 0.875rem;
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: transform 0.25s ease, opacity 0.25s, visibility 0.25s;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toast.visible {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
/* ========== 响应式 ========== */
|
||||
@media (max-width: 900px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.brand-text,
|
||||
.nav-item span:not(.nav-icon),
|
||||
.role-label,
|
||||
.role-select,
|
||||
.btn-logout {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
justify-content: center;
|
||||
padding: 0 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
justify-content: center;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
项目管理系统 · 静态页面(前端参考)
|
||||
依据:docs/产品文档.md、docs/交互文档.md
|
||||
页面:1 登录页 | 2 项目列表页 | 3 项目详情/编辑页 | 4 操作日志(抽屉)
|
||||
详见 ui/README.md
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>项目管理系统</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- 页面1:登录页(交互文档 § 页面1) -->
|
||||
<section id="view-login" class="view-login">
|
||||
<div class="login-card">
|
||||
<div class="login-brand">
|
||||
<span class="brand-icon">◇</span>
|
||||
<h1 class="login-title">项目管理系统</h1>
|
||||
<p class="login-desc">多部门协作维护项目信息</p>
|
||||
</div>
|
||||
<form id="loginForm" class="login-form" novalidate>
|
||||
<div class="form-group">
|
||||
<label for="loginAccount">账号</label>
|
||||
<input type="text" id="loginAccount" class="input" name="account" placeholder="请输入账号" required autocomplete="username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">密码</label>
|
||||
<input type="password" id="loginPassword" class="input" name="password" placeholder="请输入密码" required autocomplete="current-password" />
|
||||
</div>
|
||||
<div id="loginError" class="login-error" role="alert" style="display: none;"></div>
|
||||
<button type="submit" class="btn btn-primary btn-login-submit" id="btnLoginSubmit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 页面2~4:登录后主应用(侧栏 + 列表/详情 + 操作日志抽屉) -->
|
||||
<div class="app" id="appMain" style="display: none;">
|
||||
<!-- 侧栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<span class="brand-icon">◇</span>
|
||||
<span class="brand-text">项目管理系统</span>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="#" class="nav-item active" id="navList" data-nav="list">
|
||||
<span class="nav-icon">▦</span>
|
||||
<span>项目列表</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="role-switch">
|
||||
<span class="role-label">当前角色</span>
|
||||
<select id="roleSelect" class="role-select" title="切换角色预览权限">
|
||||
<option value="market">市场部</option>
|
||||
<option value="admin">管理员</option>
|
||||
<option value="engineer">工程部</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn-logout">退出</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<main class="main">
|
||||
<!-- 列表视图 -->
|
||||
<section id="view-list" class="view view-list active">
|
||||
<!-- 页面2:项目列表页(交互文档 § 页面2) -->
|
||||
<header class="page-header">
|
||||
<h1 class="page-title">项目列表</h1>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn btn-primary btn-new-project" id="btnNewProject" style="display: none;">
|
||||
<span class="btn-icon">+</span> 新建项目
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="toolbar">
|
||||
<div class="search-row">
|
||||
<select id="searchType" class="input input-select">
|
||||
<option value="name">项目名称</option>
|
||||
<option value="code">项目编号</option>
|
||||
</select>
|
||||
<input type="text" id="searchInput" class="input input-search" placeholder="输入关键词搜索" />
|
||||
<button type="button" class="btn btn-search" id="btnSearch">搜索</button>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<div class="filter-group">
|
||||
<label>项目进度</label>
|
||||
<select id="filterProgress" class="input input-filter">
|
||||
<option value="">全部</option>
|
||||
<option value="planning">筹备中</option>
|
||||
<option value="ongoing">进行中</option>
|
||||
<option value="completed">已竣工</option>
|
||||
<option value="settled">已结算</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>项目费用</label>
|
||||
<select id="filterCost" class="input input-filter">
|
||||
<option value="">全部</option>
|
||||
<option value="normal">正常</option>
|
||||
<option value="over">超支</option>
|
||||
<option value="under">节余</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<div class="table-loading" id="listLoading">加载中…</div>
|
||||
<div class="table-empty" id="listEmpty" style="display: none;">
|
||||
<span class="empty-icon">📋</span>
|
||||
<p>暂无项目数据</p>
|
||||
</div>
|
||||
<table class="table" id="projectTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>项目名称</th>
|
||||
<th>合同编号</th>
|
||||
<th>进度</th>
|
||||
<th>费用状态</th>
|
||||
<th>业主单位</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="projectTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination" id="pagination">
|
||||
<span class="pagination-info">共 0 条</span>
|
||||
<div class="pagination-btns">
|
||||
<button type="button" class="btn btn-sm" disabled>上一页</button>
|
||||
<button type="button" class="btn btn-sm">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 页面3:项目详情/编辑页(交互文档 § 页面3) -->
|
||||
<section id="view-detail" class="view view-detail">
|
||||
<header class="page-header">
|
||||
<button type="button" class="btn-back" id="btnBack">← 返回列表</button>
|
||||
<h1 class="page-title detail-title" id="detailTitle">项目详情</h1>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn btn-ghost" id="btnViewLog">查看操作日志</button>
|
||||
<button type="button" class="btn btn-primary" id="btnSave">
|
||||
<span class="btn-text">保存</span>
|
||||
<span class="btn-loading" style="display: none;">保存中…</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="detail-loading" id="detailLoading">加载中…</div>
|
||||
<div class="detail-content" id="detailContent" style="display: none;">
|
||||
<div class="tabs">
|
||||
<button type="button" class="tab active" data-tab="contract">合同信息</button>
|
||||
<button type="button" class="tab" data-tab="cost">成本控制</button>
|
||||
<button type="button" class="tab" data-tab="receivable">应收款</button>
|
||||
<button type="button" class="tab" data-tab="payable">应付款</button>
|
||||
<button type="button" class="tab" data-tab="other">其他</button>
|
||||
</div>
|
||||
<div class="tab-panels">
|
||||
<div class="tab-panel active" id="panel-contract">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>项目名称</label><input type="text" class="input" name="projectName" /></div>
|
||||
<div class="form-group"><label>合同编号</label><input type="text" class="input" name="contractCode" /></div>
|
||||
<div class="form-group"><label>供电局项目合同编号</label><input type="text" class="input" name="powerContractCode" /></div>
|
||||
<div class="form-group"><label>子项个数</label><input type="number" class="input" name="subCount" /></div>
|
||||
<div class="form-group"><label>子项编码</label><input type="text" class="input" name="subCode" /></div>
|
||||
<div class="form-group"><label>项目总投资(万元)</label><input type="text" class="input" name="totalInvest" /></div>
|
||||
<div class="form-group"><label>中标合同金额(万元)</label><input type="text" class="input" name="bidAmount" /></div>
|
||||
<div class="form-group"><label>质保金比例</label><input type="text" class="input" name="warrantyRatio" /></div>
|
||||
<div class="form-group"><label>结算金额(万元)</label><input type="text" class="input" name="settlementAmount" /></div>
|
||||
<div class="form-group"><label>工程电压等级</label><input type="text" class="input" name="voltageLevel" /></div>
|
||||
<div class="form-group"><label>工程类别</label><input type="text" class="input" name="projectType" /></div>
|
||||
<div class="form-group"><label>业主单位</label><input type="text" class="input" name="ownerUnit" /></div>
|
||||
<div class="form-group"><label>业主联系人及电话</label><input type="text" class="input" name="ownerContact" /></div>
|
||||
<div class="form-group"><label>中标形式</label><input type="text" class="input" name="bidForm" /></div>
|
||||
<div class="form-group"><label>签订日期</label><input type="date" class="input" name="signDate" /></div>
|
||||
<div class="form-group"><label>开工日期</label><input type="date" class="input" name="startDate" /></div>
|
||||
<div class="form-group"><label>计划竣工日期</label><input type="date" class="input" name="planEndDate" /></div>
|
||||
<div class="form-group"><label>实际竣工日期</label><input type="date" class="input" name="actualEndDate" /></div>
|
||||
<div class="form-group"><label>所属项目部</label><input type="text" class="input" name="department" /></div>
|
||||
<div class="form-group"><label>项目负责人及电话</label><input type="text" class="input" name="managerContact" /></div>
|
||||
<div class="form-group form-group-full"><label>工程款拨付方式</label><input type="text" class="input" name="paymentMethod" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-cost">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>总体成本</label><input type="text" class="input" name="totalCost" /></div>
|
||||
<div class="form-group"><label>是否调整</label><select class="input" name="adjusted"><option value="">请选择</option><option value="yes">是</option><option value="no">否</option></select></div>
|
||||
<div class="form-group"><label>农民工工资(按进度计划)</label><input type="text" class="input" name="wagePlan" /></div>
|
||||
<div class="form-group"><label>农民工工资(实付)</label><input type="text" class="input" name="wageActual" /></div>
|
||||
<div class="form-group"><label>乙供材料费(控制)</label><input type="text" class="input" name="materialControl" /></div>
|
||||
<div class="form-group"><label>应付材料费(按收款比例)</label><input type="text" class="input" name="materialPayable" /></div>
|
||||
<div class="form-group"><label>实际发生材料费</label><input type="text" class="input" name="materialActual" /></div>
|
||||
<div class="form-group"><label>实际支付材料费</label><input type="text" class="input" name="materialPaid" /></div>
|
||||
<div class="form-group"><label>其他费用(控制)</label><input type="text" class="input" name="otherControl" /></div>
|
||||
<div class="form-group"><label>应付其他费</label><input type="text" class="input" name="otherPayable" /></div>
|
||||
<div class="form-group"><label>实际其他费用</label><input type="text" class="input" name="otherActual" /></div>
|
||||
<div class="form-group"><label>税金</label><input type="text" class="input" name="tax" /></div>
|
||||
<div class="form-group"><label>利润(万元)</label><input type="text" class="input" name="profit" /></div>
|
||||
<div class="form-group"><label>实际利润(万元)</label><input type="text" class="input" name="profitActual" /></div>
|
||||
<div class="form-group"><label>成本结算金额(万元)</label><input type="text" class="input" name="costSettlement" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-receivable">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>应收款(完成进度款)</label><input type="text" class="input" name="receivable" /></div>
|
||||
<div class="form-group"><label>开票金额(万元)</label><input type="text" class="input" name="invoiceAmount" /></div>
|
||||
<div class="form-group"><label>实际收款金额(万元)</label><input type="text" class="input" name="receivedAmount" /></div>
|
||||
<div class="form-group"><label>实际收款完成率</label><input type="text" class="input" name="receivedRate" placeholder="%" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-payable">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>应付款金额(万元)</label><input type="text" class="input" name="payableAmount" /></div>
|
||||
<div class="form-group"><label>实际付款金额(万元)</label><input type="text" class="input" name="paidAmount" /></div>
|
||||
<div class="form-group"><label>未收款(万元)</label><input type="text" class="input" name="unreceived" /></div>
|
||||
<div class="form-group"><label>实际付款完成率</label><input type="text" class="input" name="paidRate" placeholder="%" /></div>
|
||||
<div class="form-group"><label>民工工资清欠金额(万元)</label><input type="text" class="input" name="wageArrears" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-other">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>结算后成本测算金额</label><input type="text" class="input" name="afterSettlementCost" /></div>
|
||||
<div class="form-group"><label>结算人工费</label><input type="text" class="input" name="settlementLabor" /></div>
|
||||
<div class="form-group"><label>结算材料费</label><input type="text" class="input" name="settlementMaterial" /></div>
|
||||
<div class="form-group"><label>结算其他费</label><input type="text" class="input" name="settlementOther" /></div>
|
||||
<div class="form-group"><label>到期应结算项目个数</label><input type="number" class="input" name="dueSettlementCount" /></div>
|
||||
<div class="form-group"><label>到期未完成结算个数</label><input type="number" class="input" name="dueUnfinishedCount" /></div>
|
||||
<div class="form-group form-group-full"><label>存在的问题</label><textarea class="input input-area" name="problems" rows="2"></textarea></div>
|
||||
<div class="form-group form-group-full"><label>建议措施</label><textarea class="input input-area" name="suggestions" rows="2"></textarea></div>
|
||||
<div class="form-group"><label>累计进度</label><input type="text" class="input" name="totalProgress" placeholder="%" /></div>
|
||||
<div class="form-group form-group-full"><label>备注</label><textarea class="input input-area" name="remark" rows="2"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 页面4:操作日志(抽屉形式,交互文档 § 页面4) -->
|
||||
<div class="drawer-overlay" id="logOverlay" aria-hidden="true"></div>
|
||||
<div class="drawer" id="logDrawer">
|
||||
<div class="drawer-header">
|
||||
<h2 class="drawer-title">操作日志</h2>
|
||||
<button type="button" class="drawer-close" id="logDrawerClose">×</button>
|
||||
</div>
|
||||
<div class="drawer-body">
|
||||
<div class="drawer-loading" id="logLoading">加载中…</div>
|
||||
<div class="drawer-empty" id="logEmpty" style="display: none;">
|
||||
<span class="empty-icon">📜</span>
|
||||
<p>暂无操作记录</p>
|
||||
</div>
|
||||
<ul class="log-list" id="logList"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div id="toast" class="toast" role="alert" aria-live="polite"></div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ---------- 状态 ----------
|
||||
var isLoggedIn = false; // 页面1 登录态,静态演示默认未登录
|
||||
var currentView = 'list'; // list | detail
|
||||
var currentRole = 'market'; // market | admin | engineer
|
||||
var currentProjectId = null; // 当前详情/编辑的项目 ID,null 表示新建
|
||||
var projects = [];
|
||||
var listFiltered = [];
|
||||
|
||||
// ---------- Mock 数据 ----------
|
||||
function getMockProjects() {
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
name: '城东 110kV 输变电工程',
|
||||
contractCode: 'HT-2024-001',
|
||||
progress: '进行中',
|
||||
costStatus: '正常',
|
||||
ownerUnit: '市供电公司',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '高新区配电改造项目',
|
||||
contractCode: 'HT-2024-002',
|
||||
progress: '已竣工',
|
||||
costStatus: '正常',
|
||||
ownerUnit: '高新区管委会',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '西区 35kV 线路迁改',
|
||||
contractCode: 'HT-2024-003',
|
||||
progress: '筹备中',
|
||||
costStatus: '节余',
|
||||
ownerUnit: '西区建设局',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getMockLogs(projectId) {
|
||||
return [
|
||||
{ operator: '张三', time: '2025-01-28 14:30', summary: '修改合同信息' },
|
||||
{ operator: '李四', time: '2025-01-27 10:00', summary: '修改成本控制' },
|
||||
{ operator: '王五', time: '2025-01-26 16:45', summary: '修改应收款' },
|
||||
];
|
||||
}
|
||||
|
||||
// ---------- DOM ----------
|
||||
var roleSelect = document.getElementById('roleSelect');
|
||||
var btnNewProject = document.getElementById('btnNewProject');
|
||||
var viewList = document.getElementById('view-list');
|
||||
var viewDetail = document.getElementById('view-detail');
|
||||
var searchType = document.getElementById('searchType');
|
||||
var searchInput = document.getElementById('searchInput');
|
||||
var btnSearch = document.getElementById('btnSearch');
|
||||
var filterProgress = document.getElementById('filterProgress');
|
||||
var filterCost = document.getElementById('filterCost');
|
||||
var listLoading = document.getElementById('listLoading');
|
||||
var listEmpty = document.getElementById('listEmpty');
|
||||
var projectTable = document.getElementById('projectTable');
|
||||
var projectTableBody = document.getElementById('projectTableBody');
|
||||
var pagination = document.getElementById('pagination');
|
||||
var btnBack = document.getElementById('btnBack');
|
||||
var detailTitle = document.getElementById('detailTitle');
|
||||
var detailLoading = document.getElementById('detailLoading');
|
||||
var detailContent = document.getElementById('detailContent');
|
||||
var btnViewLog = document.getElementById('btnViewLog');
|
||||
var btnSave = document.getElementById('btnSave');
|
||||
var logOverlay = document.getElementById('logOverlay');
|
||||
var logDrawer = document.getElementById('logDrawer');
|
||||
var logDrawerClose = document.getElementById('logDrawerClose');
|
||||
var logLoading = document.getElementById('logLoading');
|
||||
var logEmpty = document.getElementById('logEmpty');
|
||||
var logList = document.getElementById('logList');
|
||||
var toast = document.getElementById('toast');
|
||||
var navList = document.getElementById('navList');
|
||||
var viewLogin = document.getElementById('view-login');
|
||||
var appMain = document.getElementById('appMain');
|
||||
var loginForm = document.getElementById('loginForm');
|
||||
var loginError = document.getElementById('loginError');
|
||||
var btnLoginSubmit = document.getElementById('btnLoginSubmit');
|
||||
var btnLogout = document.querySelector('.btn-logout');
|
||||
|
||||
// ---------- 页面1:登录 / 退出 ----------
|
||||
function showLogin() {
|
||||
isLoggedIn = false;
|
||||
if (viewLogin) viewLogin.classList.remove('hidden');
|
||||
if (appMain) appMain.style.display = 'none';
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
isLoggedIn = true;
|
||||
if (viewLogin) viewLogin.classList.add('hidden');
|
||||
if (appMain) appMain.style.display = 'flex';
|
||||
}
|
||||
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var account = document.getElementById('loginAccount');
|
||||
var password = document.getElementById('loginPassword');
|
||||
if (!account || !password) return;
|
||||
if (loginError) loginError.style.display = 'none';
|
||||
if (btnLoginSubmit) btnLoginSubmit.disabled = true;
|
||||
|
||||
// 静态演示:任意非空账号密码视为成功;可改为模拟失败(如 password === 'fail')
|
||||
setTimeout(function () {
|
||||
if (btnLoginSubmit) btnLoginSubmit.disabled = false;
|
||||
if (password.value === 'fail') {
|
||||
if (loginError) {
|
||||
loginError.textContent = '账号或密码错误,请重试';
|
||||
loginError.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
showApp();
|
||||
renderList();
|
||||
}, 400);
|
||||
});
|
||||
}
|
||||
|
||||
if (btnLogout) {
|
||||
btnLogout.addEventListener('click', function () {
|
||||
showLogin();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 权限:是否市场部 ----------
|
||||
function isMarketRole() {
|
||||
return currentRole === 'market';
|
||||
}
|
||||
|
||||
function updateRoleUI() {
|
||||
if (btnNewProject) btnNewProject.style.display = isMarketRole() ? '' : 'none';
|
||||
}
|
||||
|
||||
if (roleSelect) {
|
||||
roleSelect.addEventListener('change', function () {
|
||||
currentRole = roleSelect.value;
|
||||
updateRoleUI();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 视图切换 ----------
|
||||
function showList() {
|
||||
currentView = 'list';
|
||||
if (viewList) viewList.classList.add('active');
|
||||
if (viewDetail) viewDetail.classList.remove('active');
|
||||
document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.remove('active'); });
|
||||
if (navList) navList.classList.add('active');
|
||||
}
|
||||
|
||||
function showDetail(projectId) {
|
||||
currentView = 'detail';
|
||||
currentProjectId = projectId;
|
||||
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';
|
||||
|
||||
if (projectId) {
|
||||
// 模拟加载详情
|
||||
setTimeout(function () {
|
||||
if (detailLoading) detailLoading.style.display = 'none';
|
||||
if (detailContent) detailContent.style.display = 'block';
|
||||
fillDetailForm(projectId);
|
||||
}, 400);
|
||||
} else {
|
||||
if (detailLoading) detailLoading.style.display = 'none';
|
||||
if (detailContent) detailContent.style.display = 'block';
|
||||
clearDetailForm();
|
||||
}
|
||||
}
|
||||
|
||||
function fillDetailForm(projectId) {
|
||||
var p = projects.find(function (x) { return x.id === projectId; });
|
||||
if (!p) return;
|
||||
var nameInput = detailContent.querySelector('input[name="projectName"]');
|
||||
if (nameInput) nameInput.value = p.name || '';
|
||||
var codeInput = detailContent.querySelector('input[name="contractCode"]');
|
||||
if (codeInput) codeInput.value = p.contractCode || '';
|
||||
var ownerInput = detailContent.querySelector('input[name="ownerUnit"]');
|
||||
if (ownerInput) ownerInput.value = p.ownerUnit || '';
|
||||
}
|
||||
|
||||
function clearDetailForm() {
|
||||
var inputs = detailContent ? detailContent.querySelectorAll('input, select, textarea') : [];
|
||||
inputs.forEach(function (el) {
|
||||
if (el.type === 'checkbox' || el.type === 'radio') el.checked = false;
|
||||
else el.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Tab 切换 ----------
|
||||
function bindTabs() {
|
||||
var tabs = document.querySelectorAll('.tab');
|
||||
var panels = document.querySelectorAll('.tab-panel');
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
var t = tab.getAttribute('data-tab');
|
||||
tabs.forEach(function (x) { x.classList.remove('active'); });
|
||||
panels.forEach(function (p) {
|
||||
var pid = p.id;
|
||||
if (pid === 'panel-' + t) p.classList.add('active');
|
||||
else p.classList.remove('active');
|
||||
});
|
||||
tab.classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 列表:渲染 ----------
|
||||
function filterList() {
|
||||
var type = searchType && searchType.value ? searchType.value : 'name';
|
||||
var keyword = (searchInput && searchInput.value || '').trim().toLowerCase();
|
||||
var progressVal = filterProgress && filterProgress.value ? filterProgress.value : '';
|
||||
var costVal = filterCost && filterCost.value ? filterCost.value : '';
|
||||
|
||||
listFiltered = projects.filter(function (p) {
|
||||
if (keyword) {
|
||||
var name = (p.name || '').toLowerCase();
|
||||
var code = (p.contractCode || '').toLowerCase();
|
||||
if (type === 'name' && name.indexOf(keyword) === -1) return false;
|
||||
if (type === 'code' && code.indexOf(keyword) === -1) return false;
|
||||
}
|
||||
if (progressVal) {
|
||||
if ((p.progressKey || '').toLowerCase() !== progressVal.toLowerCase()) return false;
|
||||
}
|
||||
if (costVal) {
|
||||
if ((p.costKey || '').toLowerCase() !== costVal.toLowerCase()) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function mapProgressToKey(p) {
|
||||
var map = { '筹备中': 'planning', '进行中': 'ongoing', '已竣工': 'completed', '已结算': 'settled' };
|
||||
p.progressKey = map[p.progress] || p.progress;
|
||||
return p;
|
||||
}
|
||||
|
||||
function mapCostToKey(p) {
|
||||
var map = { '正常': 'normal', '超支': 'over', '节余': 'under' };
|
||||
p.costKey = map[p.costStatus] || p.costStatus;
|
||||
return p;
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
if (listLoading) listLoading.style.display = 'block';
|
||||
if (listEmpty) listEmpty.style.display = 'none';
|
||||
if (projectTable) projectTable.style.visibility = 'hidden';
|
||||
|
||||
setTimeout(function () {
|
||||
filterList();
|
||||
if (listLoading) listLoading.style.display = 'none';
|
||||
|
||||
if (listFiltered.length === 0) {
|
||||
if (listEmpty) listEmpty.style.display = 'flex';
|
||||
if (projectTable) projectTable.style.visibility = 'hidden';
|
||||
} else {
|
||||
if (listEmpty) listEmpty.style.display = 'none';
|
||||
if (projectTable) projectTable.style.visibility = 'visible';
|
||||
var html = listFiltered.map(function (p) {
|
||||
return (
|
||||
'<tr>' +
|
||||
'<td>' + (p.name || '—') + '</td>' +
|
||||
'<td>' + (p.contractCode || '—') + '</td>' +
|
||||
'<td>' + (p.progress || '—') + '</td>' +
|
||||
'<td>' + (p.costStatus || '—') + '</td>' +
|
||||
'<td>' + (p.ownerUnit || '—') + '</td>' +
|
||||
'<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 + '">编辑</button>' +
|
||||
'</td>' +
|
||||
'</tr>'
|
||||
);
|
||||
}).join('');
|
||||
if (projectTableBody) projectTableBody.innerHTML = html;
|
||||
|
||||
projectTableBody.querySelectorAll('.link-action').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var id = btn.getAttribute('data-id');
|
||||
showDetail(id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var totalEl = pagination && pagination.querySelector('.pagination-info');
|
||||
if (totalEl) totalEl.textContent = '共 ' + listFiltered.length + ' 条';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// ---------- 搜索与筛选 ----------
|
||||
if (btnSearch) btnSearch.addEventListener('click', renderList);
|
||||
if (searchInput) searchInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') renderList(); });
|
||||
if (filterProgress) filterProgress.addEventListener('change', renderList);
|
||||
if (filterCost) filterCost.addEventListener('change', renderList);
|
||||
|
||||
// ---------- 新建项目 ----------
|
||||
if (btnNewProject) {
|
||||
btnNewProject.addEventListener('click', function () {
|
||||
showDetail(null);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 返回列表 ----------
|
||||
if (btnBack) {
|
||||
btnBack.addEventListener('click', function () {
|
||||
showList();
|
||||
});
|
||||
}
|
||||
|
||||
if (navList) {
|
||||
navList.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
showList();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 保存 ----------
|
||||
if (btnSave) {
|
||||
btnSave.addEventListener('click', function () {
|
||||
var textSpan = btnSave.querySelector('.btn-text');
|
||||
var loadingSpan = btnSave.querySelector('.btn-loading');
|
||||
if (loadingSpan && loadingSpan.style.display === 'inline') return;
|
||||
|
||||
if (textSpan) textSpan.style.display = 'none';
|
||||
if (loadingSpan) loadingSpan.style.display = 'inline';
|
||||
btnSave.disabled = true;
|
||||
|
||||
setTimeout(function () {
|
||||
if (textSpan) textSpan.style.display = 'inline';
|
||||
if (loadingSpan) loadingSpan.style.display = 'none';
|
||||
btnSave.disabled = false;
|
||||
showToast('保存成功', 'success');
|
||||
}, 800);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 操作日志抽屉 ----------
|
||||
function openLogDrawer() {
|
||||
if (logOverlay) logOverlay.classList.add('visible');
|
||||
if (logDrawer) logDrawer.classList.add('visible');
|
||||
if (logOverlay) logOverlay.setAttribute('aria-hidden', 'false');
|
||||
|
||||
if (logLoading) logLoading.style.display = 'block';
|
||||
if (logEmpty) logEmpty.style.display = 'none';
|
||||
if (logList) logList.innerHTML = '';
|
||||
|
||||
setTimeout(function () {
|
||||
if (logLoading) logLoading.style.display = 'none';
|
||||
var logs = getMockLogs(currentProjectId);
|
||||
if (logs.length === 0) {
|
||||
if (logEmpty) logEmpty.style.display = 'flex';
|
||||
} else {
|
||||
logList.innerHTML = logs.map(function (l) {
|
||||
return (
|
||||
'<li class="log-item">' +
|
||||
'<div class="log-meta">' + l.operator + ' · ' + l.time + '</div>' +
|
||||
'<div class="log-desc">' + l.summary + '</div>' +
|
||||
'</li>'
|
||||
);
|
||||
}).join('');
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
|
||||
function closeLogDrawer() {
|
||||
if (logOverlay) logOverlay.classList.remove('visible');
|
||||
if (logDrawer) logDrawer.classList.remove('visible');
|
||||
if (logOverlay) logOverlay.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
if (btnViewLog) btnViewLog.addEventListener('click', openLogDrawer);
|
||||
if (logDrawerClose) logDrawerClose.addEventListener('click', closeLogDrawer);
|
||||
if (logOverlay) logOverlay.addEventListener('click', closeLogDrawer);
|
||||
|
||||
// ---------- Toast ----------
|
||||
function showToast(message, type) {
|
||||
if (!toast) return;
|
||||
toast.textContent = message;
|
||||
toast.className = 'toast visible' + (type ? ' ' + type : '');
|
||||
clearTimeout(toast._tid);
|
||||
toast._tid = setTimeout(function () {
|
||||
toast.classList.remove('visible');
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
// ---------- 初始化 ----------
|
||||
projects = getMockProjects().map(function (p) {
|
||||
mapProgressToKey(p);
|
||||
mapCostToKey(p);
|
||||
return p;
|
||||
});
|
||||
|
||||
bindTabs();
|
||||
updateRoleUI();
|
||||
|
||||
// 未登录时只显示登录页;已登录则显示主应用(可通过 hash 或 localStorage 模拟,此处默认显示登录页)
|
||||
if (viewLogin && appMain) {
|
||||
showLogin();
|
||||
} else {
|
||||
renderList();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user