机构端、监管端首页修改
parent
3e682fd0aa
commit
53d7617b94
|
|
@ -0,0 +1,464 @@
|
|||
openapi: 3.0.3
|
||||
info:
|
||||
title: 机构端-首页驾驶舱 API
|
||||
description: |
|
||||
重庆市安全评价服务平台 - 机构端首页(驾驶舱)接口(`/container/driver?pageMode=3`)。
|
||||
|
||||
- 统一前缀:`/safetyEval/institution/dashboard`
|
||||
- 全部为**新增**聚合接口(落在 `InstitutionDashboardController`),不依赖/不改动既有机构端接口
|
||||
- 鉴权:`orgId` 取自机构端请求上下文;未取到时返回空数据(不报错)
|
||||
- 成功响应 `code = "0"`
|
||||
- 大屏建议对 5 个接口加 30~60s 短 TTL 缓存,面板级相互独立
|
||||
|
||||
**Apifox 导入方式**:项目设置 → 导入数据 → 选择本文件(OpenAPI/Swagger 格式)
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: safety-eval-service
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8095
|
||||
description: 本地开发
|
||||
- url: http://192.168.20.100:30140
|
||||
description: 测试网关(100)
|
||||
|
||||
tags:
|
||||
- name: 机构端-首页驾驶舱
|
||||
description: 机构端首页(驾驶舱)相关聚合接口
|
||||
|
||||
paths:
|
||||
/safetyEval/institution/dashboard/project-node-stats:
|
||||
get:
|
||||
tags:
|
||||
- 机构端-首页驾驶舱
|
||||
summary: 当前项目节点统计
|
||||
description: |
|
||||
当前机构项目节点统计:项目总数/法定项目 + 8 个流程节点待办数 + 已归档/项目延期。
|
||||
|
||||
- 数据源:`eval_project`(orgId)、`EvalProjectNodeOverviewAssembler.buildForInstitution`、延期计数
|
||||
- 节点待办口径:节点状态为「未开始」
|
||||
- 单项目装配异常跳过,不影响其余节点
|
||||
operationId: projectNodeStats
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseInstitutionDashboardProjectNodeStatsCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
totalProjects: 12
|
||||
statutoryProjects: 8
|
||||
nodeStats:
|
||||
- nodeCode: NODE_01
|
||||
nodeName: 待风险分析
|
||||
pendingCount: 3
|
||||
- nodeCode: NODE_02
|
||||
nodeName: 待合同录入
|
||||
pendingCount: 3
|
||||
- nodeCode: NODE_03
|
||||
nodeName: 待项目组成立
|
||||
pendingCount: 2
|
||||
- nodeCode: NODE_04
|
||||
nodeName: 待现场踏勘
|
||||
pendingCount: 4
|
||||
- nodeCode: NODE_05
|
||||
nodeName: 待报告编制
|
||||
pendingCount: 2
|
||||
- nodeCode: NODE_06
|
||||
nodeName: 待内部审核
|
||||
pendingCount: 1
|
||||
- nodeCode: NODE_07
|
||||
nodeName: 待技术审核
|
||||
pendingCount: 1
|
||||
- nodeCode: NODE_08
|
||||
nodeName: 待过程控制审核
|
||||
pendingCount: 2
|
||||
archivedProjectCount: 5
|
||||
delayedProjectCount: 1
|
||||
|
||||
/safetyEval/institution/dashboard/notices:
|
||||
get:
|
||||
tags:
|
||||
- 机构端-首页驾驶舱
|
||||
summary: 通知提醒
|
||||
description: |
|
||||
通知提醒:资质现场审查通知 / 监督检查通知 / 未读监管通知。
|
||||
|
||||
- 数据源:`safety_message`(按 orgId + sendType;未读=sendState!=1)
|
||||
- `qualOnSiteReviewCount`:sendType=ON_SITE_REVIEW_NOTICE
|
||||
- `inspectionCount`:sendType=INSP_NOTICE_ORG
|
||||
- `unreadRegulatoryCount`:本机构 sendState!=1 的消息数
|
||||
operationId: notices
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseInstitutionDashboardNoticeCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
qualOnSiteReviewCount: 2
|
||||
inspectionCount: 5
|
||||
unreadRegulatoryCount: 7
|
||||
|
||||
/safetyEval/institution/dashboard/industry-stat:
|
||||
get:
|
||||
tags:
|
||||
- 机构端-首页驾驶舱
|
||||
summary: 服务行业项目统计
|
||||
description: |
|
||||
按行业统计项目数与法定项目数(柱状图)。
|
||||
|
||||
- 数据源:`eval_project`(orgId)按 industry_code 分组
|
||||
- 行业名称取自 IndustryEnum;仅返回有项目的行业(前端按固定类目轴合并补 0)
|
||||
operationId: industryStat
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseInstitutionDashboardIndustryStatCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
list:
|
||||
- industryCode: METAL_SMELTING
|
||||
industryName: 金属冶炼
|
||||
projectCount: 4
|
||||
statutoryProjectCount: 3
|
||||
- industryCode: PETROCHEMICAL_CHEMICAL_PHARMACEUTICAL
|
||||
industryName: 石油加工业,化学原料、化学品及医药制造业
|
||||
projectCount: 6
|
||||
statutoryProjectCount: 5
|
||||
- industryCode: FIREWORKS_FIRECRACKERS
|
||||
industryName: 烟花爆竹制造业和仓储业
|
||||
projectCount: 2
|
||||
statutoryProjectCount: 0
|
||||
|
||||
/safetyEval/institution/dashboard/eval-type-ratio:
|
||||
get:
|
||||
tags:
|
||||
- 机构端-首页驾驶舱
|
||||
summary: 评价类别占比
|
||||
description: |
|
||||
按评价类型统计项目数(环形图)。
|
||||
|
||||
- 数据源:`eval_project`(orgId)按 eval_type_code 分组
|
||||
- 名称取自 EvalTypeEnum(PRE/ACCEPT/STATUS)
|
||||
operationId: evalTypeRatio
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseInstitutionDashboardEvalTypeRatioCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
totalProjectCount: 12
|
||||
items:
|
||||
- evalTypeCode: PRE
|
||||
evalTypeName: 安全预评价
|
||||
count: 5
|
||||
- evalTypeCode: ACCEPT
|
||||
evalTypeName: 安全设施竣工验收评价
|
||||
count: 4
|
||||
- evalTypeCode: STATUS
|
||||
evalTypeName: 安全现状评价
|
||||
count: 3
|
||||
|
||||
/safetyEval/institution/dashboard/project-execution:
|
||||
get:
|
||||
tags:
|
||||
- 机构端-首页驾驶舱
|
||||
summary: 项目执行情况
|
||||
description: |
|
||||
项目执行情况列表(表格)。
|
||||
|
||||
- 数据源:`eval_project`(orgId)、`eval_customer`(被评价企业名称)
|
||||
- 按 planEndDate 升序取前 limit 条(最近到期在前,空日期置尾)
|
||||
- 被评价企业名称解析失败置空,不影响其余行
|
||||
operationId: projectExecution
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
description: 返回条数(默认 10,范围 1~50)
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
default: 10
|
||||
minimum: 1
|
||||
maximum: 50
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseInstitutionDashboardProjectExecutionCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
list:
|
||||
- projectId: 101
|
||||
projectNo: P-2026-001
|
||||
projectName: 某化工企业安全预评价
|
||||
customerName: 某化工有限公司
|
||||
evalTypeCode: PRE
|
||||
evalTypeName: 安全预评价
|
||||
projectLeaderName: 张工
|
||||
planEndDate: "2026-08-15"
|
||||
- projectId: 102
|
||||
projectNo: P-2026-002
|
||||
projectName: 某金属冶炼验收评价
|
||||
customerName: 某冶炼厂
|
||||
evalTypeCode: ACCEPT
|
||||
evalTypeName: 安全设施竣工验收评价
|
||||
projectLeaderName: 李工
|
||||
planEndDate: "2026-09-01"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
SingleResponseInstitutionDashboardProjectNodeStatsCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/InstitutionDashboardProjectNodeStatsCO'
|
||||
|
||||
SingleResponseInstitutionDashboardNoticeCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/InstitutionDashboardNoticeCO'
|
||||
|
||||
SingleResponseInstitutionDashboardIndustryStatCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/InstitutionDashboardIndustryStatCO'
|
||||
|
||||
SingleResponseInstitutionDashboardEvalTypeRatioCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/InstitutionDashboardEvalTypeRatioCO'
|
||||
|
||||
SingleResponseInstitutionDashboardProjectExecutionCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/InstitutionDashboardProjectExecutionCO'
|
||||
|
||||
InstitutionDashboardProjectNodeStatsCO:
|
||||
type: object
|
||||
description: 当前项目节点统计
|
||||
properties:
|
||||
totalProjects:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 项目总数
|
||||
statutoryProjects:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 法定项目数
|
||||
nodeStats:
|
||||
type: array
|
||||
description: 8 个流程节点待办数(待办=节点状态未开始)
|
||||
items:
|
||||
$ref: '#/components/schemas/NodeStat'
|
||||
archivedProjectCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 已归档项目数
|
||||
delayedProjectCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 项目延期数
|
||||
|
||||
NodeStat:
|
||||
type: object
|
||||
properties:
|
||||
nodeCode:
|
||||
type: string
|
||||
description: 节点编码(NODE_01..NODE_08)
|
||||
nodeName:
|
||||
type: string
|
||||
description: 节点展示名称(待X)
|
||||
pendingCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 待办数
|
||||
|
||||
InstitutionDashboardNoticeCO:
|
||||
type: object
|
||||
description: 通知提醒
|
||||
properties:
|
||||
qualOnSiteReviewCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 资质现场审查通知数(sendType=ON_SITE_REVIEW_NOTICE)
|
||||
inspectionCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 监督检查通知数(sendType=INSP_NOTICE_ORG)
|
||||
unreadRegulatoryCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 未读监管通知数(sendState!=1)
|
||||
|
||||
InstitutionDashboardIndustryStatCO:
|
||||
type: object
|
||||
description: 服务行业项目统计
|
||||
properties:
|
||||
list:
|
||||
type: array
|
||||
description: 行业分布统计(仅返回有项目的行业)
|
||||
items:
|
||||
$ref: '#/components/schemas/IndustryStat'
|
||||
|
||||
IndustryStat:
|
||||
type: object
|
||||
properties:
|
||||
industryCode:
|
||||
type: string
|
||||
description: 行业编码(IndustryEnum)
|
||||
industryName:
|
||||
type: string
|
||||
description: 行业名称
|
||||
projectCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 项目数
|
||||
statutoryProjectCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 法定项目数
|
||||
|
||||
InstitutionDashboardEvalTypeRatioCO:
|
||||
type: object
|
||||
description: 评价类别占比
|
||||
properties:
|
||||
totalProjectCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 项目总数
|
||||
items:
|
||||
type: array
|
||||
description: 各评价类别项目数
|
||||
items:
|
||||
$ref: '#/components/schemas/EvalTypeRatio'
|
||||
|
||||
EvalTypeRatio:
|
||||
type: object
|
||||
properties:
|
||||
evalTypeCode:
|
||||
type: string
|
||||
description: 评价类型编码(EvalTypeEnum)
|
||||
evalTypeName:
|
||||
type: string
|
||||
description: 评价类型名称
|
||||
count:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 项目数
|
||||
|
||||
InstitutionDashboardProjectExecutionCO:
|
||||
type: object
|
||||
description: 项目执行情况
|
||||
properties:
|
||||
list:
|
||||
type: array
|
||||
description: 项目执行情况列表(按 planEndDate 升序取前 N 条)
|
||||
items:
|
||||
$ref: '#/components/schemas/ProjectExecutionItem'
|
||||
|
||||
ProjectExecutionItem:
|
||||
type: object
|
||||
properties:
|
||||
projectId:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 项目ID
|
||||
projectNo:
|
||||
type: string
|
||||
description: 项目编号
|
||||
projectName:
|
||||
type: string
|
||||
description: 项目名称
|
||||
customerName:
|
||||
type: string
|
||||
description: 被评价企业
|
||||
evalTypeCode:
|
||||
type: string
|
||||
description: 评价类型编码
|
||||
evalTypeName:
|
||||
type: string
|
||||
description: 评价类型名称
|
||||
projectLeaderName:
|
||||
type: string
|
||||
description: 项目负责人
|
||||
planEndDate:
|
||||
type: string
|
||||
description: 项目结束日期(yyyy-MM-dd)
|
||||
|
|
@ -0,0 +1,818 @@
|
|||
openapi: 3.0.3
|
||||
info:
|
||||
title: 监管端-驾驶舱大屏 API
|
||||
description: |
|
||||
重庆市安全评价服务平台 - 监管端「驾驶舱大屏」页面接口(`/container/driver` → 监管端 Cockpit)。
|
||||
|
||||
- 统一前缀:`/safetyEval/regulator/cockpit`
|
||||
- 全部为**新增**聚合接口(落在 `RegulatorCockpitController`),不依赖/不改动既有监管端接口
|
||||
- 成功响应 `code = "0"`
|
||||
- 大屏建议对 7 个接口加 30~60s 短 TTL 缓存,降低 DB 压力;面板级接口相互独立,单接口异常不影响其余面板
|
||||
|
||||
**Apifox 导入方式**:项目设置 → 导入数据 → 选择本文件(OpenAPI/Swagger 格式)
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: safety-eval-service
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8095
|
||||
description: 本地开发
|
||||
- url: http://192.168.20.100:30140
|
||||
description: 测试网关(100)
|
||||
|
||||
tags:
|
||||
- name: 监管端-驾驶舱大屏
|
||||
description: 监管端驾驶舱大屏相关聚合接口
|
||||
|
||||
paths:
|
||||
/safetyEval/regulator/cockpit/qualification-overview:
|
||||
get:
|
||||
tags:
|
||||
- 监管端-驾驶舱大屏
|
||||
summary: 资质全生命周期管理
|
||||
description: |
|
||||
资质全生命周期统计:机构备案、评价师、行业分布。
|
||||
|
||||
- 数据源:`org_info`、`org_personnel`、`IndustryEnum`
|
||||
- 边界:仅机构/人员/资质维度,不含项目与风险
|
||||
- 已知缺口:`logoutOrgCount` 现有字典无「注销」状态,暂置 0(待补来源)
|
||||
operationId: qualificationOverview
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/yearParam'
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseCockpitQualificationOverviewCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
lifecycleStats:
|
||||
newFilingOrgCount: 12
|
||||
currentFilingOrgCount: 86
|
||||
logoutOrgCount: 0
|
||||
totalFilingOrgCount: 120
|
||||
exitedEvaluatorCount: 5
|
||||
currentEvaluatorCount: 340
|
||||
progress:
|
||||
filingCompleteRate: 71.67
|
||||
bizActiveRate: 63.95
|
||||
newGrowthRate: 13.95
|
||||
industryList:
|
||||
- industryCode: "1"
|
||||
industryName: 危险化学品
|
||||
orgCount: 32
|
||||
typeCount: 32
|
||||
- industryCode: "2"
|
||||
industryName: 非煤矿山
|
||||
orgCount: 24
|
||||
typeCount: 24
|
||||
- industryCode: "3"
|
||||
industryName: 金属冶炼
|
||||
orgCount: 18
|
||||
typeCount: 18
|
||||
|
||||
/safetyEval/regulator/cockpit/kpi:
|
||||
get:
|
||||
tags:
|
||||
- 监管端-驾驶舱大屏
|
||||
summary: 核心KPI指标
|
||||
description: |
|
||||
核心 KPI 统计,仅提供「备案项目开展评价率」(现有数据源可支撑)。
|
||||
|
||||
- 数据源:`eval_project`(`is_statutory=1`)、`eval_risk_analysis`(风险分析已建立=已开展评价)
|
||||
- 边界:**不提供**「企业评价项目合格率」(无合格状态字段)、「隐患整改率」(无隐患表)
|
||||
- `value`=率值(百分比);`yoy`=当前周期率 − 上一周期率(百分点)
|
||||
operationId: kpi
|
||||
parameters:
|
||||
- name: cycle
|
||||
in: query
|
||||
description: 统计周期
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- year
|
||||
- quarter
|
||||
- month
|
||||
default: year
|
||||
- $ref: '#/components/parameters/yearParam'
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseCockpitKpiCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
items:
|
||||
- code: EVAL_RATE
|
||||
name: 备案项目开展评价率
|
||||
value: 78.5
|
||||
yoy: 4.2
|
||||
|
||||
/safetyEval/regulator/cockpit/region-distribution:
|
||||
get:
|
||||
tags:
|
||||
- 监管端-驾驶舱大屏
|
||||
summary: 区域地图分布
|
||||
description: |
|
||||
按区县统计评价项目数与备案机构数。
|
||||
|
||||
- 数据源:`eval_project.district_code`(法定项目按区县计数)、`org_info.district_code`(已备案机构按区县计数)
|
||||
- 边界:仅「评价项目数」「备案机构数」两维度;隐患/整改率无数据源,前端可用项目数替代
|
||||
- 两路按 `district_code` 分组后对齐,缺侧补 0
|
||||
operationId: regionDistribution
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseCockpitRegionDistributionCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
list:
|
||||
- districtCode: "500101"
|
||||
districtName: 万州区
|
||||
evalProjectCount: 45
|
||||
filingOrgCount: 8
|
||||
- districtCode: "500103"
|
||||
districtName: 渝中区
|
||||
evalProjectCount: 32
|
||||
filingOrgCount: 6
|
||||
summary:
|
||||
totalEvalProject: 520
|
||||
totalFilingOrg: 86
|
||||
|
||||
/safetyEval/regulator/cockpit/process-overview:
|
||||
get:
|
||||
tags:
|
||||
- 监管端-驾驶舱大屏
|
||||
summary: 执业全过程管控 + 项目流程(节点聚合)
|
||||
description: |
|
||||
执业全过程管控与项目流程节点聚合统计。
|
||||
|
||||
- 数据源:复用 `EvalProjectNodeOverviewAssembler.buildForRegulator(projectId)` 逐项目取节点状态,全局累加
|
||||
- `totalCount`=到达该节点项目数(状态≠未开始);`pendingCount`=未开始(待办)项目数
|
||||
- 覆盖 8 个真实节点(风险分析/合同签订/成立项目组/现场勘查/过程管控/报告草稿/内审/技审);大屏额外的「初勘/从业告知/编制检查表/制定工作计划/归档」无独立实体,由前端并入相邻节点或留空
|
||||
operationId: processOverview
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseCockpitProcessOverviewCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
nodes:
|
||||
- nodeCode: RISK_ANALYSIS
|
||||
nodeName: 风险分析
|
||||
totalCount: 480
|
||||
pendingCount: 40
|
||||
- nodeCode: CONTRACT_SIGN
|
||||
nodeName: 合同签订
|
||||
totalCount: 455
|
||||
pendingCount: 25
|
||||
- nodeCode: GROUP_SETUP
|
||||
nodeName: 成立项目组
|
||||
totalCount: 450
|
||||
pendingCount: 15
|
||||
- nodeCode: SITE_SURVEY
|
||||
nodeName: 现场勘查
|
||||
totalCount: 430
|
||||
pendingCount: 30
|
||||
- nodeCode: PROCESS_CONTROL
|
||||
nodeName: 过程管控
|
||||
totalCount: 400
|
||||
pendingCount: 50
|
||||
- nodeCode: REPORT_DRAFT
|
||||
nodeName: 报告草稿
|
||||
totalCount: 360
|
||||
pendingCount: 40
|
||||
- nodeCode: INTERNAL_REVIEW
|
||||
nodeName: 内审
|
||||
totalCount: 330
|
||||
pendingCount: 20
|
||||
- nodeCode: TECH_REVIEW
|
||||
nodeName: 技审
|
||||
totalCount: 310
|
||||
pendingCount: 10
|
||||
|
||||
/safetyEval/regulator/cockpit/eval-type-trend:
|
||||
get:
|
||||
tags:
|
||||
- 监管端-驾驶舱大屏
|
||||
summary: 评价类型趋势
|
||||
description: |
|
||||
按评价类型统计各时间桶的项目数与监督检查数。
|
||||
|
||||
- 数据源:`eval_project.eval_type_code`/`create_time`、`insp_notice.create_time`/`project_id`
|
||||
- `projectCounts` 来自 `eval_project` 按 `eval_type_code`+时间桶;`inspCounts` 来自 `insp_notice` 经 `project_id` 关联 `eval_project` 的 `eval_type_code`+时间桶
|
||||
- 时间桶:月=01..12;季=Q1..Q4;月粒度=`period=month` 时按 `year` 的 1..12
|
||||
- 仅统计能关联到项目的检查
|
||||
operationId: evalTypeTrend
|
||||
parameters:
|
||||
- name: period
|
||||
in: query
|
||||
required: true
|
||||
description: 时间粒度
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- year
|
||||
- quarter
|
||||
- month
|
||||
- $ref: '#/components/parameters/yearParam'
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseCockpitEvalTypeTrendCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
buckets:
|
||||
- "01"
|
||||
- "02"
|
||||
- "03"
|
||||
- "04"
|
||||
series:
|
||||
- evalTypeCode: "1"
|
||||
evalTypeName: 安全预评价
|
||||
projectCounts: [12, 15, 10, 18]
|
||||
inspCounts: [20, 25, 18, 30]
|
||||
- evalTypeCode: "2"
|
||||
evalTypeName: 安全验收评价
|
||||
projectCounts: [8, 9, 11, 7]
|
||||
inspCounts: [15, 18, 20, 12]
|
||||
|
||||
/safetyEval/regulator/cockpit/review-summary:
|
||||
get:
|
||||
tags:
|
||||
- 监管端-驾驶舱大屏
|
||||
summary: 复盘评估改进提效
|
||||
description: |
|
||||
复盘评估统计:监督检查分类、发现问题、超期/处罚分布。
|
||||
|
||||
- 数据源:`insp_notice`、`eval_project`(`plan_end_date` 逾期、未归档)
|
||||
- 检查类按 `insp_type_code` 与 `result_code` 分组:`insp_type_code` ∈ PROJECT_PROCESS/QUAL_KEEP/SPECIAL;`result_code` 1未发现问题/2责令整改/3转入风险预警
|
||||
- `pie` 仅输出可用的 OVERDUE(超期未完成数)、PUNISH(监管处罚数=resultCode=3);原大屏「现场确认风险隐患数/复查评估报告数/项目主要负责人变更数」无现成数据源,本次不输出
|
||||
operationId: reviewSummary
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/yearParam'
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseCockpitReviewSummaryCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
stats:
|
||||
inspCount: 320
|
||||
onsiteCheckCount: 180
|
||||
qualKeepCount: 90
|
||||
specialCount: 50
|
||||
checkedOrgCount: 60
|
||||
foundProblemCount: 75
|
||||
pie:
|
||||
- code: OVERDUE
|
||||
name: 超期未完成
|
||||
value: 28
|
||||
- code: PUNISH
|
||||
name: 监管处罚
|
||||
value: 12
|
||||
|
||||
/safetyEval/regulator/cockpit/project-monitor:
|
||||
get:
|
||||
tags:
|
||||
- 监管端-驾驶舱大屏
|
||||
summary: 项目实时监控
|
||||
description: |
|
||||
项目实时监控与踏勘/打卡统计。
|
||||
|
||||
- 数据源:`eval_project`、`eval_project_process_waring`、复用既有 `selectRegulatorSurveyStat()`
|
||||
- `warningCount`=存在过程预警的项目数(**真实计算,替代原硬编码 0**),调用 `EvalProjectProcessWaringGateway.checkProjectHasWaring(projectId)`
|
||||
- `survey` 四项(告知书/打卡)复用既有聚合 SQL;监控/预警/计划完成数为本次新实现
|
||||
operationId: projectMonitor
|
||||
parameters:
|
||||
- name: month
|
||||
in: query
|
||||
required: false
|
||||
description: 统计月份(yyyy-MM,默认当前月)
|
||||
schema:
|
||||
type: string
|
||||
example: "2026-07"
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SingleResponseCockpitProjectMonitorCO'
|
||||
example:
|
||||
success: true
|
||||
code: "0"
|
||||
data:
|
||||
monitor:
|
||||
activeProjectCount: 520
|
||||
normalExecCount: 480
|
||||
warningCount: 36
|
||||
monthPlanFinishCount: 45
|
||||
survey:
|
||||
noticeArchived: 500
|
||||
noticeAbnormal: 12
|
||||
clockInNormal: 980
|
||||
clockInAbnormal: 25
|
||||
|
||||
components:
|
||||
parameters:
|
||||
yearParam:
|
||||
name: year
|
||||
in: query
|
||||
required: false
|
||||
description: 统计年份(默认当前年)
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
example: 2026
|
||||
|
||||
schemas:
|
||||
# ---------- 通用响应 ----------
|
||||
SingleResponseError:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: false
|
||||
code:
|
||||
type: string
|
||||
example: "404"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
|
||||
SingleResponseCockpitQualificationOverviewCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/CockpitQualificationOverviewCO'
|
||||
|
||||
SingleResponseCockpitKpiCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/CockpitKpiCO'
|
||||
|
||||
SingleResponseCockpitRegionDistributionCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/CockpitRegionDistributionCO'
|
||||
|
||||
SingleResponseCockpitProcessOverviewCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/CockpitProcessOverviewCO'
|
||||
|
||||
SingleResponseCockpitEvalTypeTrendCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/CockpitEvalTypeTrendCO'
|
||||
|
||||
SingleResponseCockpitReviewSummaryCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/CockpitReviewSummaryCO'
|
||||
|
||||
SingleResponseCockpitProjectMonitorCO:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
code:
|
||||
type: string
|
||||
example: "0"
|
||||
message:
|
||||
type: string
|
||||
errMessage:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/CockpitProjectMonitorCO'
|
||||
|
||||
# ---------- 业务模型 ----------
|
||||
CockpitQualificationOverviewCO:
|
||||
type: object
|
||||
description: 资质全生命周期管理
|
||||
properties:
|
||||
lifecycleStats:
|
||||
$ref: '#/components/schemas/LifecycleStat'
|
||||
progress:
|
||||
$ref: '#/components/schemas/Progress'
|
||||
industryList:
|
||||
type: array
|
||||
description: 行业分布(机构数)
|
||||
items:
|
||||
$ref: '#/components/schemas/IndustryItem'
|
||||
|
||||
LifecycleStat:
|
||||
type: object
|
||||
description: 生命周期核心指标
|
||||
properties:
|
||||
newFilingOrgCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 本年新增备案机构数
|
||||
currentFilingOrgCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 当前备案机构数
|
||||
logoutOrgCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 本年注销机构数(现有字典无注销状态,暂置 0,待补源)
|
||||
totalFilingOrgCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 全部备案机构数
|
||||
exitedEvaluatorCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 退出备案评价师数
|
||||
currentEvaluatorCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 当前备案评价师数
|
||||
|
||||
Progress:
|
||||
type: object
|
||||
description: 完成率/开展率/新增率(百分比)
|
||||
properties:
|
||||
filingCompleteRate:
|
||||
type: number
|
||||
format: double
|
||||
description: 备案完成率
|
||||
bizActiveRate:
|
||||
type: number
|
||||
format: double
|
||||
description: 开展业务率
|
||||
newGrowthRate:
|
||||
type: number
|
||||
format: double
|
||||
description: 新增率
|
||||
|
||||
IndustryItem:
|
||||
type: object
|
||||
description: 行业分布项
|
||||
properties:
|
||||
industryCode:
|
||||
type: string
|
||||
description: 行业编码
|
||||
industryName:
|
||||
type: string
|
||||
description: 行业名称
|
||||
orgCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 机构数
|
||||
typeCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 类型数(口径待定,暂与机构数一致)
|
||||
|
||||
CockpitKpiCO:
|
||||
type: object
|
||||
description: 核心KPI指标
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
description: KPI指标列表
|
||||
items:
|
||||
$ref: '#/components/schemas/KpiItem'
|
||||
|
||||
KpiItem:
|
||||
type: object
|
||||
description: KPI指标项
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 指标编码
|
||||
example: EVAL_RATE
|
||||
name:
|
||||
type: string
|
||||
description: 指标名称
|
||||
example: 备案项目开展评价率
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
description: 当前值(百分比)
|
||||
yoy:
|
||||
type: number
|
||||
format: double
|
||||
description: 同比(百分点)
|
||||
|
||||
CockpitRegionDistributionCO:
|
||||
type: object
|
||||
description: 区域地图分布
|
||||
properties:
|
||||
list:
|
||||
type: array
|
||||
description: 各区县分布
|
||||
items:
|
||||
$ref: '#/components/schemas/DistrictItem'
|
||||
summary:
|
||||
$ref: '#/components/schemas/RegionSummary'
|
||||
|
||||
DistrictItem:
|
||||
type: object
|
||||
description: 区县分布项
|
||||
properties:
|
||||
districtCode:
|
||||
type: string
|
||||
description: 区县编码
|
||||
districtName:
|
||||
type: string
|
||||
description: 区县名称
|
||||
evalProjectCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 评价项目数
|
||||
filingOrgCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 备案机构数
|
||||
|
||||
RegionSummary:
|
||||
type: object
|
||||
description: 汇总
|
||||
properties:
|
||||
totalEvalProject:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 评价项目总数
|
||||
totalFilingOrg:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 备案机构总数
|
||||
|
||||
CockpitProcessOverviewCO:
|
||||
type: object
|
||||
description: 执业全过程管控 + 项目流程(节点聚合)
|
||||
properties:
|
||||
nodes:
|
||||
type: array
|
||||
description: 节点聚合(totalCount=到达该节点项目数;pendingCount=未开始/待办项目数)
|
||||
items:
|
||||
$ref: '#/components/schemas/NodeItem'
|
||||
|
||||
NodeItem:
|
||||
type: object
|
||||
description: 节点项
|
||||
properties:
|
||||
nodeCode:
|
||||
type: string
|
||||
description: 节点编码
|
||||
nodeName:
|
||||
type: string
|
||||
description: 节点名称
|
||||
totalCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 到达该节点项目数
|
||||
pendingCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 待办(未开始)项目数
|
||||
|
||||
CockpitEvalTypeTrendCO:
|
||||
type: object
|
||||
description: 评价类型趋势
|
||||
properties:
|
||||
buckets:
|
||||
type: array
|
||||
description: 时间桶(月=01..12;季=Q1..Q4;月粒度=01..月末)
|
||||
items:
|
||||
type: string
|
||||
series:
|
||||
type: array
|
||||
description: 各评价类型序列
|
||||
items:
|
||||
$ref: '#/components/schemas/EvalTypeSeries'
|
||||
|
||||
EvalTypeSeries:
|
||||
type: object
|
||||
description: 评价类型趋势序列
|
||||
properties:
|
||||
evalTypeCode:
|
||||
type: string
|
||||
description: 评价类型编码
|
||||
evalTypeName:
|
||||
type: string
|
||||
description: 评价类型名称
|
||||
projectCounts:
|
||||
type: array
|
||||
description: 项目数(与 buckets 对齐)
|
||||
items:
|
||||
type: integer
|
||||
format: int32
|
||||
inspCounts:
|
||||
type: array
|
||||
description: 监督检查数(与 buckets 对齐)
|
||||
items:
|
||||
type: integer
|
||||
format: int32
|
||||
|
||||
CockpitReviewSummaryCO:
|
||||
type: object
|
||||
description: 复盘评估改进提效
|
||||
properties:
|
||||
stats:
|
||||
$ref: '#/components/schemas/ReviewStat'
|
||||
pie:
|
||||
type: array
|
||||
description: 饼图分布
|
||||
items:
|
||||
$ref: '#/components/schemas/ReviewPieItem'
|
||||
|
||||
ReviewStat:
|
||||
type: object
|
||||
description: 检查类统计
|
||||
properties:
|
||||
inspCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 监督检查总数
|
||||
onsiteCheckCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 项目过程检查数
|
||||
qualKeepCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 资质保持检查数
|
||||
specialCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 专项检查数
|
||||
checkedOrgCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 检查机构数(去重)
|
||||
foundProblemCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 发现问题数(resultCode=2/3)
|
||||
|
||||
ReviewPieItem:
|
||||
type: object
|
||||
description: 饼图分布项
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 编码
|
||||
enum:
|
||||
- OVERDUE
|
||||
- PUNISH
|
||||
name:
|
||||
type: string
|
||||
description: 名称
|
||||
enum:
|
||||
- 超期未完成
|
||||
- 监管处罚
|
||||
value:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 数值
|
||||
|
||||
CockpitProjectMonitorCO:
|
||||
type: object
|
||||
description: 项目实时监控
|
||||
properties:
|
||||
monitor:
|
||||
$ref: '#/components/schemas/ProjectMonitor'
|
||||
survey:
|
||||
$ref: '#/components/schemas/SurveyStat'
|
||||
|
||||
ProjectMonitor:
|
||||
type: object
|
||||
description: 项目监控核心指标
|
||||
properties:
|
||||
activeProjectCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 在监项目数
|
||||
normalExecCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 正常执行数
|
||||
warningCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 预警数(基于过程预警真实计算)
|
||||
monthPlanFinishCount:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 本月计划完成数
|
||||
|
||||
SurveyStat:
|
||||
type: object
|
||||
description: 踏勘/打卡统计
|
||||
properties:
|
||||
noticeArchived:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 告知书已归档数
|
||||
noticeAbnormal:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 告知异常数
|
||||
clockInNormal:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 人员打卡正常数
|
||||
clockInAbnormal:
|
||||
type: integer
|
||||
format: int32
|
||||
description: 打卡异常数
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
const getApiHost = () => {
|
||||
if (typeof window !== 'undefined' && window.process?.env?.app?.API_HOST) {
|
||||
return window.process.env.app.API_HOST;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
async function request(path, params = {}) {
|
||||
const apiHost = getApiHost();
|
||||
const url = new URL(path, apiHost);
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== null && v !== '') {
|
||||
url.searchParams.append(k, v);
|
||||
}
|
||||
});
|
||||
const response = await fetch(url.toString());
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const json = await response.json();
|
||||
if (json.code !== '0') {
|
||||
throw new Error(json.errMessage || json.message || 'API error');
|
||||
}
|
||||
return json.data;
|
||||
}
|
||||
|
||||
// 当前项目节点统计
|
||||
export function fetchProjectNodeStats() {
|
||||
return request('/safetyEval/institution/dashboard/project-node-stats');
|
||||
}
|
||||
|
||||
// 通知提醒
|
||||
export function fetchNotices() {
|
||||
return request('/safetyEval/institution/dashboard/notices');
|
||||
}
|
||||
|
||||
// 服务行业项目统计
|
||||
export function fetchIndustryStat() {
|
||||
return request('/safetyEval/institution/dashboard/industry-stat');
|
||||
}
|
||||
|
||||
// 评价类别占比
|
||||
export function fetchEvalTypeRatio() {
|
||||
return request('/safetyEval/institution/dashboard/eval-type-ratio');
|
||||
}
|
||||
|
||||
// 项目执行情况(limit 默认 10)
|
||||
export function fetchProjectExecution(limit = 10) {
|
||||
return request('/safetyEval/institution/dashboard/project-execution', { limit });
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
const getApiHost = () => {
|
||||
if (typeof window !== 'undefined' && window.process?.env?.app?.API_HOST) {
|
||||
return window.process.env.app.API_HOST;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
async function request(path, params = {}) {
|
||||
const apiHost = getApiHost();
|
||||
const url = new URL(path, apiHost);
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== null && v !== '') {
|
||||
url.searchParams.append(k, v);
|
||||
}
|
||||
});
|
||||
const response = await fetch(url.toString());
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const json = await response.json();
|
||||
if (json.code !== '0') {
|
||||
throw new Error(json.errMessage || json.message || 'API error');
|
||||
}
|
||||
return json.data;
|
||||
}
|
||||
|
||||
// 资质全生命周期管理
|
||||
export function fetchQualificationOverview(year) {
|
||||
return request('/safetyEval/regulator/cockpit/qualification-overview', { year });
|
||||
}
|
||||
|
||||
// 核心KPI指标
|
||||
export function fetchKpi(cycle, year) {
|
||||
return request('/safetyEval/regulator/cockpit/kpi', { cycle, year });
|
||||
}
|
||||
|
||||
// 区域地图分布
|
||||
export function fetchRegionDistribution() {
|
||||
return request('/safetyEval/regulator/cockpit/region-distribution');
|
||||
}
|
||||
|
||||
// 执业全过程管控 + 项目流程
|
||||
export function fetchProcessOverview() {
|
||||
return request('/safetyEval/regulator/cockpit/process-overview');
|
||||
}
|
||||
|
||||
// 评价类型趋势
|
||||
export function fetchEvalTypeTrend(period, year) {
|
||||
return request('/safetyEval/regulator/cockpit/eval-type-trend', { period, year });
|
||||
}
|
||||
|
||||
// 复盘评估改进提效
|
||||
export function fetchReviewSummary(year) {
|
||||
return request('/safetyEval/regulator/cockpit/review-summary', { year });
|
||||
}
|
||||
|
||||
// 项目实时监控
|
||||
export function fetchProjectMonitor(month) {
|
||||
return request('/safetyEval/regulator/cockpit/project-monitor', { month });
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { NS_DRIVER } from "~/enumerate/namespace";
|
|||
import DriverBack from "~/pages/Container/Driver/components/DriverBack";
|
||||
import Institution from "~/pages/Container/Institution/Dashboard";
|
||||
import Cockpit from "~/pages/Container/Supervision/Cockpit";
|
||||
import { getDriverToken, resolveDriverAuthParams } from "~/utils/resolveDriverAuthParams";
|
||||
import { getDriverToken, getPageMode, resolveDriverAuthParams } from "~/utils/resolveDriverAuthParams";
|
||||
import { getApiErrorMessage } from "~/utils/handleApiResponse";
|
||||
|
||||
import "./index.less";
|
||||
|
|
@ -27,7 +27,11 @@ function DriverErrorPanel({ message }) {
|
|||
);
|
||||
}
|
||||
|
||||
function resolveTerminalType(driverConfig, actionResult) {
|
||||
function resolveTerminalType(driverConfig, actionResult, pageMode = 1) {
|
||||
// pageMode 直接指定时,跳过 store / actionResult 取值
|
||||
if (pageMode === 2) return 2;
|
||||
if (pageMode === 3) return 1;
|
||||
|
||||
const fromStore = Number(driverConfig);
|
||||
if (fromStore === 1 || fromStore === 2) {
|
||||
return fromStore;
|
||||
|
|
@ -43,6 +47,8 @@ function resolveTerminalType(driverConfig, actionResult) {
|
|||
|
||||
const Driver = (props) => {
|
||||
const authParams = useMemo(() => resolveDriverAuthParams(), []);
|
||||
const pageMode = useMemo(() => getPageMode(), []);
|
||||
const bypassApi = pageMode === 2 || pageMode === 3;
|
||||
const requestStartedRef = useRef(false);
|
||||
const actionResultRef = useRef(null);
|
||||
const [pageError, setPageError] = useState("");
|
||||
|
|
@ -56,6 +62,12 @@ const Driver = (props) => {
|
|||
}
|
||||
requestStartedRef.current = true;
|
||||
|
||||
// pageMode 2/3:跳过 checkTerminalType 请求,直接展示指定页面
|
||||
if (bypassApi) {
|
||||
setRequestDone(true);
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const token = getDriverToken();
|
||||
|
|
@ -73,19 +85,25 @@ const Driver = (props) => {
|
|||
setRequestDone(true);
|
||||
}
|
||||
})();
|
||||
}, [queryDriverConfig]);
|
||||
}, [queryDriverConfig, bypassApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestDone || queryDriverConfigLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// pageMode 2/3:跳过 token / 终端类型校验
|
||||
if (bypassApi) {
|
||||
setPageError("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authParams.token) {
|
||||
setPageError("登录凭证获取失败,请重新从基座登录进入");
|
||||
return;
|
||||
}
|
||||
|
||||
const terminalType = resolveTerminalType(driverConfig, actionResultRef.current);
|
||||
const terminalType = resolveTerminalType(driverConfig, actionResultRef.current, pageMode);
|
||||
if (terminalType === 1 || terminalType === 2) {
|
||||
setPageError("");
|
||||
return;
|
||||
|
|
@ -94,16 +112,16 @@ const Driver = (props) => {
|
|||
setPageError(
|
||||
getApiErrorMessage(actionResultRef.current, "终端类型获取失败,请重新从基座登录进入"),
|
||||
);
|
||||
}, [requestDone, queryDriverConfigLoading, driverConfig, authParams.token]);
|
||||
}, [requestDone, queryDriverConfigLoading, driverConfig, authParams.token, bypassApi, pageMode]);
|
||||
|
||||
const terminalType = resolveTerminalType(driverConfig, actionResultRef.current);
|
||||
const terminalType = resolveTerminalType(driverConfig, actionResultRef.current, pageMode);
|
||||
const isInstitution = terminalType === 1;
|
||||
const isSupervision = terminalType === 2;
|
||||
const isValidTerminal = isInstitution || isSupervision;
|
||||
const hasToken = Boolean(authParams.token);
|
||||
const isLoading = !requestDone || queryDriverConfigLoading;
|
||||
const showError = requestDone && !queryDriverConfigLoading && Boolean(pageError);
|
||||
const showContent = requestDone && !queryDriverConfigLoading && hasToken && isValidTerminal && !pageError;
|
||||
const showContent = requestDone && !queryDriverConfigLoading && (hasToken || bypassApi) && isValidTerminal && !pageError;
|
||||
|
||||
if (showError) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import * as echarts from 'echarts';
|
||||
import { Card } from 'antd';
|
||||
|
||||
const BLUE = '#5b8ff9';
|
||||
const ORANGE = '#ffb33e';
|
||||
const BLUE = '#4285f4';
|
||||
const ORANGE = '#ffab31';
|
||||
|
||||
function EChart({ option, className }) {
|
||||
export default function EnterpriseTypeChart({ data }) {
|
||||
const chartRef = useRef(null);
|
||||
const instanceRef = useRef(null);
|
||||
|
||||
|
|
@ -16,7 +15,6 @@ function EChart({ option, className }) {
|
|||
const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
|
||||
if (observer) observer.observe(chartRef.current);
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
if (observer) observer.disconnect();
|
||||
|
|
@ -25,67 +23,47 @@ function EChart({ option, className }) {
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (instanceRef.current) instanceRef.current.setOption(option, true);
|
||||
}, [option]);
|
||||
const list = data && data.list ? data.list : [];
|
||||
const categories = list.map(item => item.industryName);
|
||||
const projectData = list.map(item => Number(item.projectCount) || 0);
|
||||
const statutoryData = list.map(item => Number(item.statutoryProjectCount) || 0);
|
||||
const maxVal = Math.max(...projectData, ...statutoryData, 1);
|
||||
const yMax = Math.ceil(maxVal / 10) * 10 || 10;
|
||||
|
||||
return <div className={className} ref={chartRef} />;
|
||||
}
|
||||
|
||||
export default function EnterpriseTypeChart({ data }) {
|
||||
const option = useMemo(() => ({
|
||||
color: [BLUE, ORANGE],
|
||||
grid: { left: 38, right: 10, top: 26, bottom: 46 },
|
||||
grid: { left: 36, right: 10, top: 12, bottom: 28 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow', shadowStyle: { color: 'rgba(91, 143, 249, .08)' } },
|
||||
backgroundColor: '#fff',
|
||||
borderColor: '#e9edf4',
|
||||
borderColor: '#e2e8f0',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333', fontSize: 12 },
|
||||
textStyle: { color: '#1e293b', fontSize: 12 },
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.categories,
|
||||
data: categories,
|
||||
axisTick: { show: false },
|
||||
axisLine: { lineStyle: { color: '#edf0f5' } },
|
||||
axisLabel: {
|
||||
color: '#666',
|
||||
fontSize: 12,
|
||||
interval: 0,
|
||||
width: 58,
|
||||
overflow: 'break',
|
||||
lineHeight: 14,
|
||||
},
|
||||
axisLine: { lineStyle: { color: '#e2e8f0' } },
|
||||
axisLabel: { color: '#64748b', fontSize: 11, interval: 0 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 60,
|
||||
splitNumber: 6,
|
||||
axisLabel: { color: '#8792a2', fontSize: 12 },
|
||||
splitLine: { lineStyle: { color: '#e9edf4', type: 'dashed' } },
|
||||
max: yMax,
|
||||
interval: yMax > 10 ? yMax / 3 : 5,
|
||||
axisLabel: { color: '#8492a6', fontSize: 10, margin: 4 },
|
||||
splitLine: { lineStyle: { color: '#dce5ef', type: 'dashed' } },
|
||||
},
|
||||
series: [
|
||||
{ name: '项目数', type: 'bar', barWidth: 12, data: data.projectCount, itemStyle: { borderRadius: [2, 2, 0, 0] } },
|
||||
{ name: '金额', type: 'bar', barWidth: 12, data: data.amount, itemStyle: { borderRadius: [2, 2, 0, 0] } },
|
||||
{ name: '项目数', type: 'bar', barWidth: 14, barGap: '30%', data: projectData, itemStyle: { borderRadius: [2, 2, 0, 0] } },
|
||||
{ name: '法定项目', type: 'bar', barWidth: 14, data: statutoryData, itemStyle: { borderRadius: [2, 2, 0, 0] } },
|
||||
],
|
||||
}), [data]);
|
||||
}), [categories, projectData, statutoryData, yMax]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={<span className="institution-card-title">服务企业类型统计</span>}
|
||||
size="small"
|
||||
className="institution-panel-card institution-enterprise-card"
|
||||
styles={{ body: { padding: '20px 26px 15px' } }}
|
||||
extra={
|
||||
<div className="institution-chart-legend institution-chart-legend--top">
|
||||
<span><i style={{ backgroundColor: BLUE }} />项目数</span>
|
||||
<span><i style={{ backgroundColor: ORANGE }} />金额</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<EChart className="institution-bar-chart" option={option} />
|
||||
</Card>
|
||||
);
|
||||
useEffect(() => {
|
||||
if (instanceRef.current) instanceRef.current.setOption(option, true);
|
||||
}, [option]);
|
||||
|
||||
return <div className="institution-bar-chart" ref={chartRef} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,16 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
AuditOutlined,
|
||||
StarFilled,
|
||||
UserDeleteOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const iconMap = {
|
||||
Star: StarFilled,
|
||||
Audit: AuditOutlined,
|
||||
UserDelete: UserDeleteOutlined
|
||||
};
|
||||
|
||||
function AlertItem({ item }) {
|
||||
const Icon = iconMap[item.icon] || AuditOutlined;
|
||||
|
||||
return (
|
||||
<div className="institution-alert-item" style={{ backgroundColor: item.bgColor }}>
|
||||
<div className="institution-alert-item__icon" style={{ backgroundColor: item.color }}>
|
||||
<Icon />
|
||||
</div>
|
||||
<div className="institution-alert-item__content">
|
||||
<div className="institution-alert-item__title">{item.title}</div>
|
||||
<div className="institution-alert-item__value">{item.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InfoAlerts({ data }) {
|
||||
return (
|
||||
<div className="institution-alert-grid">
|
||||
{data.map((item) => (
|
||||
<AlertItem key={item.key} item={item} />
|
||||
<div className="dashboard-reminder-grid">
|
||||
{data.map(item => (
|
||||
<article key={item.key} className={item.colorClass}>
|
||||
<i>{item.char}</i>
|
||||
<div>
|
||||
<span>{item.title}</span>
|
||||
<strong>{item.value}</strong>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,35 +1,54 @@
|
|||
import React from 'react';
|
||||
import { Card, Table } from 'antd';
|
||||
|
||||
const columns = [
|
||||
{ title: '序号', dataIndex: 'id', key: 'id', width: 70, align: 'center' },
|
||||
{ title: '项目名称', dataIndex: 'projectName', key: 'projectName', ellipsis: true },
|
||||
{ title: '项目状态', dataIndex: 'status', key: 'status', width: 120, align: 'center' },
|
||||
{ title: '项目负责人', dataIndex: 'projectLeader', key: 'projectLeader', width: 120, align: 'center' },
|
||||
{ title: '客户负责人', dataIndex: 'clientLeader', key: 'clientLeader', width: 120, align: 'center' },
|
||||
{ title: '项目开始时间', dataIndex: 'startDate', key: 'startDate', width: 180, align: 'center' },
|
||||
{ title: '项目结束时间', dataIndex: 'endDate', key: 'endDate', width: 180, align: 'center' },
|
||||
{ title: '项目阶段', dataIndex: 'acceptanceDate', key: 'acceptanceDate', width: 180, align: 'center' },
|
||||
{ title: '操作', key: 'action', width: 110, align: 'center', render: () => <a className="institution-table-link">查看详情</a> }
|
||||
];
|
||||
export default function ProjectCompletionTable({ data, onView }) {
|
||||
const list = data && data.list ? data.list : [];
|
||||
|
||||
export default function ProjectCompletionTable({ data }) {
|
||||
return (
|
||||
<Card
|
||||
title={<span className="institution-card-title">项目完成情况统计</span>}
|
||||
size="small"
|
||||
className="institution-panel-card institution-table-card"
|
||||
styles={{ body: { padding: '12px 14px 14px' } }}
|
||||
>
|
||||
<Table
|
||||
className="institution-completion-table"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ x: 1180 }}
|
||||
/>
|
||||
</Card>
|
||||
<section className="dashboard-block dashboard-project-block">
|
||||
<div className="dashboard-block-head">
|
||||
<h3>项目执行情况</h3>
|
||||
<button className="dashboard-btn-ghost" type="button">
|
||||
查看全部
|
||||
</button>
|
||||
</div>
|
||||
<div className="data-table dashboard-project-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>序号</th>
|
||||
<th>项目编号</th>
|
||||
<th>项目名称</th>
|
||||
<th>被评价企业</th>
|
||||
<th>评价类别</th>
|
||||
<th>项目负责人</th>
|
||||
<th>项目结束日期</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{list.map((record, index) => (
|
||||
<tr key={record.projectId || index}>
|
||||
<td>{index + 1}</td>
|
||||
<td>{record.projectNo}</td>
|
||||
<td>{record.projectName}</td>
|
||||
<td>{record.customerName}</td>
|
||||
<td>{record.evalTypeName}</td>
|
||||
<td>{record.projectLeaderName}</td>
|
||||
<td>{record.planEndDate}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
type="button"
|
||||
onClick={() => onView && onView(record)}
|
||||
>
|
||||
查看
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import * as echarts from 'echarts';
|
||||
import { Card } from 'antd';
|
||||
|
||||
const COLORS = ['#4285f4', '#58b874', '#ff9f2f'];
|
||||
|
||||
function EChart({ option, className }) {
|
||||
const chartRef = useRef(null);
|
||||
|
|
@ -13,7 +14,6 @@ function EChart({ option, className }) {
|
|||
const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
|
||||
if (observer) observer.observe(chartRef.current);
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
if (observer) observer.disconnect();
|
||||
|
|
@ -29,89 +29,80 @@ function EChart({ option, className }) {
|
|||
return <div className={className} ref={chartRef} />;
|
||||
}
|
||||
|
||||
function Legend({ data }) {
|
||||
return (
|
||||
<div className="institution-project-legend">
|
||||
{data.map((item) => (
|
||||
<span key={item.name}>
|
||||
<i style={{ backgroundColor: item.color }} />
|
||||
{item.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default function ProjectTypeChart({ data }) {
|
||||
const items = data && data.items ? data.items : [];
|
||||
const totalProjectCount = data ? Number(data.totalProjectCount) || 0 : 0;
|
||||
const computedTotal = totalProjectCount || items.reduce((sum, item) => sum + (Number(item.count) || 0), 0);
|
||||
|
||||
export default function ProjectTypeChart({ data, total }) {
|
||||
const totalValue = total || data.reduce((sum, item) => sum + item.value, 0);
|
||||
const option = useMemo(() => ({
|
||||
color: data.map(item => item.color),
|
||||
color: COLORS.slice(0, items.length),
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: '#fff',
|
||||
borderColor: '#e9edf4',
|
||||
borderColor: '#e2e8f0',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#333', fontSize: 12 },
|
||||
textStyle: { color: '#1e293b', fontSize: 12 },
|
||||
formatter: '{b}<br/>项目数:{c}<br/>占比:{d}%',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '项目类型占比',
|
||||
name: '评价类别占比',
|
||||
type: 'pie',
|
||||
radius: ['46%', '74%'],
|
||||
center: ['50%', '50%'],
|
||||
radius: ['48%', '72%'],
|
||||
center: ['50%', '48%'],
|
||||
avoidLabelOverlap: true,
|
||||
minAngle: 5,
|
||||
label: { show: false },
|
||||
labelLine: { show: false },
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
emphasis: {
|
||||
scale: true,
|
||||
scaleSize: 4,
|
||||
},
|
||||
data: data.map(item => ({ name: item.name, value: item.value })),
|
||||
data: items.map((item) => ({
|
||||
name: item.evalTypeName,
|
||||
value: Number(item.count) || 0,
|
||||
})),
|
||||
},
|
||||
],
|
||||
graphic: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '42%',
|
||||
top: '38%',
|
||||
style: {
|
||||
text: '项目总数',
|
||||
textAlign: 'center',
|
||||
fill: '#a0a7b2',
|
||||
fontSize: 15,
|
||||
fill: '#94a3b8',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '53%',
|
||||
top: '50%',
|
||||
style: {
|
||||
text: String(totalValue),
|
||||
text: String(computedTotal),
|
||||
textAlign: 'center',
|
||||
fill: '#111827',
|
||||
fontSize: 24,
|
||||
fill: '#1e293b',
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
},
|
||||
},
|
||||
],
|
||||
}), [data, totalValue]);
|
||||
}), [items, computedTotal]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={<span className="institution-card-title">项目类型占比</span>}
|
||||
size="small"
|
||||
className="institution-panel-card institution-project-card"
|
||||
styles={{ body: { padding: '24px 22px 18px' } }}
|
||||
>
|
||||
<>
|
||||
<EChart className="institution-donut-chart" option={option} />
|
||||
<Legend data={data} />
|
||||
</Card>
|
||||
<div className="dashboard-type-legend">
|
||||
{items.map((item, i) => (
|
||||
<span key={item.evalTypeCode || i}>
|
||||
<i style={{ backgroundColor: COLORS[i % COLORS.length] }} />
|
||||
{item.evalTypeName} {Number(item.count) || 0}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,13 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
AuditOutlined,
|
||||
CompassOutlined,
|
||||
DatabaseOutlined,
|
||||
FileDoneOutlined,
|
||||
FileProtectOutlined,
|
||||
LineChartOutlined,
|
||||
NotificationOutlined,
|
||||
ProfileOutlined,
|
||||
ReadOutlined,
|
||||
SafetyCertificateOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const iconMap = {
|
||||
FileDone: FileDoneOutlined,
|
||||
LineChart: LineChartOutlined,
|
||||
Appstore: AppstoreOutlined,
|
||||
FileProtect: FileProtectOutlined,
|
||||
Safety: SafetyCertificateOutlined,
|
||||
Profile: ProfileOutlined,
|
||||
Notification: NotificationOutlined,
|
||||
Read: ReadOutlined,
|
||||
Compass: CompassOutlined,
|
||||
Database: DatabaseOutlined,
|
||||
Audit: AuditOutlined
|
||||
};
|
||||
|
||||
function StatisticCard({ item }) {
|
||||
const Icon = iconMap[item.icon] || FileDoneOutlined;
|
||||
|
||||
return (
|
||||
<div className="institution-stat-card">
|
||||
<div className="institution-stat-card__icon" style={{ backgroundColor: item.bgColor }}>
|
||||
<Icon />
|
||||
<div
|
||||
className="institution-stat-card__icon"
|
||||
style={{ backgroundColor: item.color, color: item.charColor }}
|
||||
>
|
||||
{item.char}
|
||||
</div>
|
||||
<div className="institution-stat-card__content">
|
||||
<div className="institution-stat-card__title">{item.title}</div>
|
||||
|
|
|
|||
|
|
@ -1,260 +1,379 @@
|
|||
.institution-dashboard {
|
||||
/* ============ 机构端首页 — 1:1 还原原型 dashboard-classic ============ */
|
||||
|
||||
.institution-dashboard {
|
||||
min-height: 100%;
|
||||
padding: 0 2px 0;
|
||||
background: #f3f3f3;
|
||||
color: #333;
|
||||
background: #f8fafc;
|
||||
color: #1e293b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 顶部工具栏 */
|
||||
.institution-dashboard__toolbar {
|
||||
padding: 4px 0 8px;
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
|
||||
.institution-dashboard__top,
|
||||
.institution-dashboard__charts {
|
||||
/* ============ dashboard-classic 布局 ============ */
|
||||
.dashboard-classic {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2.12fr) 397px;
|
||||
gap: 20px;
|
||||
margin-bottom: 15px;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.institution-panel-card {
|
||||
border: 0 !important;
|
||||
border-radius: 2px !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.institution-panel-card > :is(.ant-card-head, .micro-temp-card-head) {
|
||||
min-height: 45px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.institution-panel-card > :is(.ant-card-head, .micro-temp-card-head) :is(.ant-card-head-title, .micro-temp-card-head-title) {
|
||||
padding: 14px 0 8px;
|
||||
}
|
||||
|
||||
.institution-card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.institution-dashboard__summary {
|
||||
font-size: 13px;
|
||||
color: #3d3d3d;
|
||||
}
|
||||
|
||||
.institution-dashboard__summary strong {
|
||||
margin-left: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.institution-dashboard__summary .is-blue {
|
||||
color: #1684ff;
|
||||
}
|
||||
|
||||
.institution-stat-grid {
|
||||
.dashboard-classic-top,
|
||||
.dashboard-classic-middle {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(120px, 1fr));
|
||||
gap: 26px 28px;
|
||||
grid-template-columns: minmax(0, 3.2fr) minmax(300px, 1fr);
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.institution-stat-card {
|
||||
/* ============ dashboard-block 卡片 ============ */
|
||||
.dashboard-block {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 0.95rem;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.dashboard-block-head {
|
||||
min-height: 28px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.8rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.dashboard-block-head h3 {
|
||||
font-size: 0.88rem;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.dashboard-block-head > div {
|
||||
display: flex;
|
||||
gap: 1.4rem;
|
||||
font-size: 0.7rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dashboard-block-head b {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
/* ============ 当前项目节点统计 ============ */
|
||||
.dashboard-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dashboard-status-grid article {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
padding: 10px 13px;
|
||||
gap: 0.65rem;
|
||||
min-height: 62px;
|
||||
padding: 0.65rem;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 17px rgba(33, 57, 98, 0.09);
|
||||
box-shadow: 0 4px 16px rgba(30, 64, 100, 0.07);
|
||||
}
|
||||
|
||||
.institution-stat-card__icon,
|
||||
.institution-alert-item__icon {
|
||||
.dashboard-status-grid article > i {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
color: #fff;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
flex: 0 0 34px;
|
||||
}
|
||||
|
||||
.dashboard-status-grid .blue { background: #4098f7; }
|
||||
.dashboard-status-grid .violet { background: #8268ef; }
|
||||
.dashboard-status-grid .green { background: #23bfa5; }
|
||||
.dashboard-status-grid .orange { background: #ff9f2f; }
|
||||
.dashboard-status-grid .cyan { background: #0eafbd; }
|
||||
.dashboard-status-grid .red { background: #f0645b; }
|
||||
|
||||
.dashboard-status-grid span,
|
||||
.dashboard-status-grid strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dashboard-status-grid span {
|
||||
font-size: 0.7rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dashboard-status-grid strong {
|
||||
font-size: 1rem;
|
||||
margin-top: 0.15rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* ============ 通知提醒 ============ */
|
||||
.dashboard-reminder-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dashboard-reminder-grid article {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
min-height: 64px;
|
||||
padding: 0.65rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashboard-reminder-grid article:last-child {
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
|
||||
.dashboard-reminder-grid article.yellow { background: #fff9e8; }
|
||||
.dashboard-reminder-grid article.red { background: #fff2ef; }
|
||||
.dashboard-reminder-grid article.blue { background: #edf4ff; }
|
||||
|
||||
.dashboard-reminder-grid i {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.institution-stat-card__content {
|
||||
min-width: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.institution-stat-card__title,
|
||||
.institution-alert-item__title {
|
||||
overflow: hidden;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
flex: 0 0 34px;
|
||||
}
|
||||
|
||||
.dashboard-reminder-grid .yellow i { background: #f7bd08; }
|
||||
.dashboard-reminder-grid .red i { background: #ff725c; }
|
||||
.dashboard-reminder-grid .blue i { background: #5b8def; }
|
||||
|
||||
.dashboard-reminder-grid span,
|
||||
.dashboard-reminder-grid strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dashboard-reminder-grid span {
|
||||
font-size: 0.68rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dashboard-reminder-grid strong {
|
||||
font-size: 1rem;
|
||||
margin-top: 0.12rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* ============ 服务行业项目统计 / 评价类别占比 ============ */
|
||||
.dashboard-industry-block,
|
||||
.dashboard-type-block {
|
||||
min-height: 285px;
|
||||
}
|
||||
|
||||
/* 图例 */
|
||||
.chart-legend span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.chart-legend i {
|
||||
width: 14px;
|
||||
height: 6px;
|
||||
border-radius: 5px;
|
||||
background: #4285f4;
|
||||
}
|
||||
|
||||
.chart-legend span:last-child i {
|
||||
background: #ffab31;
|
||||
}
|
||||
|
||||
/* 柱状图容器 */
|
||||
.dashboard-bar-chart {
|
||||
height: 225px;
|
||||
}
|
||||
|
||||
/* ECharts 柱状图 */
|
||||
.institution-bar-chart {
|
||||
width: 100%;
|
||||
height: 225px;
|
||||
}
|
||||
|
||||
/* ============ 评价类别占比 ============ */
|
||||
.institution-donut-chart {
|
||||
width: 100%;
|
||||
height: 185px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-type-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.62rem;
|
||||
color: #64748b;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dashboard-type-legend span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-type-legend i {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* ============ 项目执行情况表格 ============ */
|
||||
.dashboard-project-block .data-table {
|
||||
background: #fff;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dashboard-project-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.dashboard-project-table th,
|
||||
.dashboard-project-table td {
|
||||
padding: 0.65rem 0.55rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.dashboard-project-table th {
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.institution-stat-card__value {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 21px;
|
||||
.dashboard-project-table td {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.institution-alert-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 18px;
|
||||
.dashboard-project-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.institution-alert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 66px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 2px;
|
||||
.dashboard-project-table tr:hover td {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.institution-alert-item__icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
box-shadow: 0 5px 12px rgba(75, 91, 131, 0.2);
|
||||
}
|
||||
|
||||
.institution-alert-item__content {
|
||||
min-width: 0;
|
||||
margin-left: 13px;
|
||||
}
|
||||
|
||||
.institution-alert-item__value {
|
||||
color: #222;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.institution-enterprise-card,
|
||||
.institution-project-card {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.institution-chart-legend,
|
||||
.institution-chart-legend span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.institution-chart-legend {
|
||||
gap: 10px;
|
||||
color: #6f7785;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.institution-chart-legend i,
|
||||
.institution-project-legend i {
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
width: 20px;
|
||||
height: 8px;
|
||||
margin-right: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.institution-bar-chart {
|
||||
width: 100%;
|
||||
height: 232px;
|
||||
}
|
||||
|
||||
.institution-project-card :is(.ant-card-body, .micro-temp-card-body) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.institution-donut-chart {
|
||||
width: 218px;
|
||||
height: 218px;
|
||||
margin: -1px auto 4px;
|
||||
}
|
||||
|
||||
.institution-project-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px 16px;
|
||||
color: #697386;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.institution-project-legend span {
|
||||
/* ============ 原生按钮(匹配原型) ============ */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.42rem 0.85rem;
|
||||
border: 1px solid var(--color-border, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.institution-project-legend i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
.btn-primary {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
.btn-primary:hover { background: #1d4ed8; border-color: #1d4ed8; }
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
border: none;
|
||||
}
|
||||
.btn-ghost:hover { background: #f1f5f9; color: #1e293b; }
|
||||
|
||||
.btn-sm { padding: 0.3rem 0.75rem; font-size: 0.7rem; }
|
||||
|
||||
.dashboard-btn-ghost {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.dashboard-btn-ghost:hover {
|
||||
background: #f1f5f9;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.institution-table-card {
|
||||
min-height: 202px;
|
||||
/* 表格操作按钮 */
|
||||
.dashboard-table-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.institution-completion-table :is(.ant-table, .micro-temp-table) {
|
||||
color: #222;
|
||||
font-size: 12px;
|
||||
.dashboard-table-link:hover {
|
||||
background: #1d4ed8;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.institution-completion-table :is(.ant-table-thead, .micro-temp-table-thead) > tr > th {
|
||||
height: 27px;
|
||||
padding: 6px 10px;
|
||||
background: #eef2f8 !important;
|
||||
border-bottom: 0;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.institution-completion-table :is(.ant-table-tbody, .micro-temp-table-tbody) > tr > td {
|
||||
height: 29px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.institution-completion-table :is(.ant-table-tbody, .micro-temp-table-tbody) > tr:nth-child(even) > td {
|
||||
background: #eaf2ff;
|
||||
}
|
||||
|
||||
.institution-completion-table :is(.ant-table-tbody, .micro-temp-table-tbody) > tr:hover > td {
|
||||
background: #dfeeff !important;
|
||||
}
|
||||
|
||||
.institution-table-link {
|
||||
color: #1684ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.institution-dashboard__top,
|
||||
.institution-dashboard__charts {
|
||||
/* ============ 响应式 ============ */
|
||||
@media (max-width: 1280px) {
|
||||
.dashboard-classic-top,
|
||||
.dashboard-classic-middle {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.institution-stat-grid {
|
||||
grid-template-columns: repeat(3, minmax(120px, 1fr));
|
||||
.dashboard-status-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.institution-stat-grid,
|
||||
.institution-alert-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
.dashboard-status-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.dashboard-reminder-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Space, Card } from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import DriverBack from '~/pages/Container/Driver/components/DriverBack';
|
||||
|
||||
import StatisticCards from './components/StatisticCards';
|
||||
import InfoAlerts from './components/InfoAlerts';
|
||||
import EnterpriseTypeChart from './components/EnterpriseTypeChart';
|
||||
import ProjectTypeChart from './components/ProjectTypeChart';
|
||||
import ProjectCompletionTable from './components/ProjectCompletionTable';
|
||||
|
|
@ -16,51 +12,237 @@ import {
|
|||
ENTERPRISE_TYPE_STATS,
|
||||
PROJECT_TYPE_DISTRIBUTION,
|
||||
PROJECT_TOTAL,
|
||||
PROJECT_COMPLETION_LIST
|
||||
PROJECT_COMPLETION_LIST,
|
||||
} from './mockData';
|
||||
|
||||
import {
|
||||
fetchProjectNodeStats,
|
||||
fetchNotices,
|
||||
fetchIndustryStat,
|
||||
fetchEvalTypeRatio,
|
||||
fetchProjectExecution,
|
||||
} from '~/api/institution';
|
||||
|
||||
// ==================== UI 展示常量(与原型保持一致,非数据映射) ====================
|
||||
const NODE_CHARS = ['险', '合', '组', '场', '报', '内', '技', '控'];
|
||||
const NODE_COLORS = ['blue', 'violet', 'green', 'orange', 'blue', 'violet', 'orange', 'green'];
|
||||
|
||||
function mockIndustryToApiFormat() {
|
||||
return {
|
||||
list: ENTERPRISE_TYPE_STATS.categories.map((name, i) => ({
|
||||
industryCode: '',
|
||||
industryName: name,
|
||||
projectCount: ENTERPRISE_TYPE_STATS.projectCount[i],
|
||||
statutoryProjectCount: ENTERPRISE_TYPE_STATS.statutoryCount[i],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function mockEvalTypeToApiFormat() {
|
||||
return {
|
||||
totalProjectCount: PROJECT_TOTAL,
|
||||
items: PROJECT_TYPE_DISTRIBUTION.map(item => ({
|
||||
evalTypeCode: '',
|
||||
evalTypeName: item.name,
|
||||
count: item.value,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function mockExecutionToApiFormat() {
|
||||
return {
|
||||
list: PROJECT_COMPLETION_LIST.map(item => ({
|
||||
projectId: item.id,
|
||||
projectNo: item.projectCode,
|
||||
projectName: item.projectName,
|
||||
customerName: item.evalCompany,
|
||||
evalTypeCode: '',
|
||||
evalTypeName: item.evalType,
|
||||
projectLeaderName: item.projectLeader,
|
||||
planEndDate: item.endDate,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 子组件 ====================
|
||||
function StatusCard({ item }) {
|
||||
return (
|
||||
<article>
|
||||
<i className={item.colorClass}>{item.char}</i>
|
||||
<div>
|
||||
<span>{item.title}</span>
|
||||
<strong>{item.value}</strong>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function ReminderCard({ item }) {
|
||||
return (
|
||||
<article className={item.colorClass}>
|
||||
<i>{item.char}</i>
|
||||
<div>
|
||||
<span>{item.title}</span>
|
||||
<strong>{item.value}</strong>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 主组件 ====================
|
||||
export default function InstitutionDashboard() {
|
||||
const handleReminderClick = useCallback(() => {}, []);
|
||||
|
||||
// 各 API 的原始返回数据(null 表示接口未成功,走 mock)
|
||||
const [nodeStatsApi, setNodeStatsApi] = useState(null);
|
||||
const [noticesApi, setNoticesApi] = useState(null);
|
||||
const [industryStatApi, setIndustryStatApi] = useState(null);
|
||||
const [evalTypeRatioApi, setEvalTypeRatioApi] = useState(null);
|
||||
const [projectExecutionApi, setProjectExecutionApi] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjectNodeStats()
|
||||
.then(setNodeStatsApi)
|
||||
.catch(() => {});
|
||||
|
||||
fetchNotices()
|
||||
.then(setNoticesApi)
|
||||
.catch(() => {});
|
||||
|
||||
fetchIndustryStat()
|
||||
.then(setIndustryStatApi)
|
||||
.catch(() => {});
|
||||
|
||||
fetchEvalTypeRatio()
|
||||
.then(setEvalTypeRatioApi)
|
||||
.catch(() => {});
|
||||
|
||||
fetchProjectExecution()
|
||||
.then(setProjectExecutionApi)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// 衍生展示数据:API 成功则用 API 字段直接构建,失败则回退 mock
|
||||
const statusCards = useMemo(() => {
|
||||
if (!nodeStatsApi) return STATISTIC_CARDS;
|
||||
const { nodeStats, archivedProjectCount, delayedProjectCount } = nodeStatsApi;
|
||||
const nodes = (nodeStats || []).map((node, i) => ({
|
||||
key: node.nodeCode || `NODE_${i}`,
|
||||
title: node.nodeName,
|
||||
value: Number(node.pendingCount) || 0,
|
||||
char: NODE_CHARS[i] || node.nodeName.charAt(1) || '-',
|
||||
colorClass: NODE_COLORS[i] || 'blue',
|
||||
}));
|
||||
nodes.push(
|
||||
{ key: 'archived', title: '已归档项目', value: Number(archivedProjectCount) || 0, char: '档', colorClass: 'cyan' },
|
||||
{ key: 'delayed', title: '项目延期', value: Number(delayedProjectCount) || 0, char: '延', colorClass: 'red' },
|
||||
);
|
||||
return nodes;
|
||||
}, [nodeStatsApi]);
|
||||
|
||||
const projectSummary = useMemo(() => {
|
||||
if (!nodeStatsApi) return PROJECT_SUMMARY;
|
||||
return {
|
||||
total: Number(nodeStatsApi.totalProjects) || 0,
|
||||
statutory: Number(nodeStatsApi.statutoryProjects) || 0,
|
||||
};
|
||||
}, [nodeStatsApi]);
|
||||
|
||||
const alertItems = useMemo(() => {
|
||||
if (!noticesApi) return INFO_ALERTS;
|
||||
return [
|
||||
{ key: 'qualCheck', title: '资质现场审查通知', value: Number(noticesApi.qualOnSiteReviewCount) || 0, char: '审', colorClass: 'yellow' },
|
||||
{ key: 'supervisionCheck', title: '监督检查通知', value: Number(noticesApi.inspectionCount) || 0, char: '检', colorClass: 'red' },
|
||||
{ key: 'unreadNotice', title: '未读监管通知', value: Number(noticesApi.unreadRegulatoryCount) || 0, char: '未', colorClass: 'blue' },
|
||||
];
|
||||
}, [noticesApi]);
|
||||
|
||||
const industryStatData = useMemo(() => {
|
||||
return (industryStatApi && industryStatApi.list && industryStatApi.list.length > 0)
|
||||
? industryStatApi
|
||||
: mockIndustryToApiFormat();
|
||||
}, [industryStatApi]);
|
||||
|
||||
const evalTypeRatioData = useMemo(() => {
|
||||
return (evalTypeRatioApi && evalTypeRatioApi.items && evalTypeRatioApi.items.length > 0)
|
||||
? evalTypeRatioApi
|
||||
: mockEvalTypeToApiFormat();
|
||||
}, [evalTypeRatioApi]);
|
||||
|
||||
const projectExecutionData = useMemo(() => {
|
||||
return (projectExecutionApi && projectExecutionApi.list && projectExecutionApi.list.length > 0)
|
||||
? projectExecutionApi
|
||||
: mockExecutionToApiFormat();
|
||||
}, [projectExecutionApi]);
|
||||
|
||||
const legendColors = useMemo(() => ({
|
||||
project: '#4285f4',
|
||||
statutory: '#ffab31',
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<div className="institution-dashboard">
|
||||
<div className="institution-dashboard__toolbar">
|
||||
<DriverBack />
|
||||
</div>
|
||||
<div className="institution-dashboard__top">
|
||||
<Card
|
||||
title={<span className="institution-card-title">当前评价状态状态统计</span>}
|
||||
size="small"
|
||||
className="institution-dashboard__status-card institution-panel-card"
|
||||
extra={
|
||||
<Space size={30} className="institution-dashboard__summary">
|
||||
<span>
|
||||
项目总数: <strong className="is-blue">{PROJECT_SUMMARY.total}</strong>
|
||||
</span>
|
||||
<span>
|
||||
超期数: <strong className="is-blue">{PROJECT_SUMMARY.overdue}</strong>
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
styles={{ body: { padding: '16px 34px 17px' } }}
|
||||
>
|
||||
<StatisticCards data={STATISTIC_CARDS} />
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={<span className="institution-card-title">信息提醒</span>}
|
||||
size="small"
|
||||
className="institution-dashboard__alerts-card institution-panel-card"
|
||||
styles={{ body: { padding: '20px 24px' } }}
|
||||
>
|
||||
<InfoAlerts data={INFO_ALERTS} />
|
||||
</Card>
|
||||
<div className="dashboard-classic">
|
||||
{/* ===== 第一行:项目节点统计 + 通知提醒 ===== */}
|
||||
<div className="dashboard-classic-top">
|
||||
<section className="dashboard-block dashboard-status-block">
|
||||
<div className="dashboard-block-head">
|
||||
<h3>当前项目节点统计</h3>
|
||||
<div>
|
||||
<span>项目总数:<b>{projectSummary.total}</b></span>
|
||||
<span>法定项目:<b>{projectSummary.statutory}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dashboard-status-grid">
|
||||
{statusCards.map(item => (
|
||||
<StatusCard key={item.key} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="dashboard-block dashboard-reminder-block">
|
||||
<div className="dashboard-block-head">
|
||||
<h3>通知提醒</h3>
|
||||
</div>
|
||||
<div className="dashboard-reminder-grid">
|
||||
{alertItems.map(item => (
|
||||
<ReminderCard key={item.key} item={item} onClick={handleReminderClick} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* ===== 第二行:服务行业项目统计 + 评价类别占比 ===== */}
|
||||
<div className="dashboard-classic-middle">
|
||||
<section className="dashboard-block dashboard-industry-block">
|
||||
<div className="dashboard-block-head">
|
||||
<h3>服务行业项目统计</h3>
|
||||
<div className="chart-legend">
|
||||
<span><i style={{ backgroundColor: legendColors.project }} />项目数</span>
|
||||
<span><i style={{ backgroundColor: legendColors.statutory }} />法定项目</span>
|
||||
</div>
|
||||
</div>
|
||||
<EnterpriseTypeChart data={industryStatData} />
|
||||
</section>
|
||||
|
||||
<section className="dashboard-block dashboard-type-block">
|
||||
<div className="dashboard-block-head">
|
||||
<h3>评价类别占比</h3>
|
||||
</div>
|
||||
<ProjectTypeChart data={evalTypeRatioData} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* ===== 第三行:项目执行情况 ===== */}
|
||||
<ProjectCompletionTable
|
||||
data={projectExecutionData}
|
||||
onView={() => {}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="institution-dashboard__charts">
|
||||
<EnterpriseTypeChart data={ENTERPRISE_TYPE_STATS} />
|
||||
<ProjectTypeChart data={PROJECT_TYPE_DISTRIBUTION} total={PROJECT_TOTAL} />
|
||||
</div>
|
||||
|
||||
<ProjectCompletionTable data={PROJECT_COMPLETION_LIST} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,86 +1,79 @@
|
|||
/**
|
||||
* 机构端首页 - 模拟数据
|
||||
* 机构端首页 - 模拟数据(1:1 还原原型 V1.4)
|
||||
*/
|
||||
export const STATISTIC_CARDS = [
|
||||
{ key: 'contract', title: '项目合同签订', value: 5632, icon: 'FileDone', color: '#409eff', bgColor: '#409eff' },
|
||||
{ key: 'riskAnalysis', title: '待风险分析', value: 951, icon: 'LineChart', color: '#8b7cf6', bgColor: '#8b7cf6' },
|
||||
{ key: 'projectTeam', title: '待成立项目组', value: 5632, icon: 'Appstore', color: '#48d1bd', bgColor: '#48d1bd' },
|
||||
{ key: 'workPlan', title: '待制定工作计划', value: 5632, icon: 'FileProtect', color: '#ff9f43', bgColor: '#ff9f43' },
|
||||
{ key: 'initialEval', title: '待初提', value: 5632, icon: 'Safety', color: '#8b7cf6', bgColor: '#8b7cf6' },
|
||||
{ key: 'checklist', title: '待编制检查表', value: 456, icon: 'Profile', color: '#8b7cf6', bgColor: '#8b7cf6' },
|
||||
{ key: 'industryNotice', title: '待从业告知', value: 5632, icon: 'Notification', color: '#ffb12a', bgColor: '#ffb12a' },
|
||||
{ key: 'siteSurvey', title: '待现场勘查', value: 5632, icon: 'Read', color: '#8b7cf6', bgColor: '#8b7cf6' },
|
||||
{ key: 'processControl', title: '过程管控', value: 951, icon: 'Compass', color: '#409eff', bgColor: '#409eff' },
|
||||
{ key: 'archive', title: '归档', value: 651, icon: 'Database', color: '#08bea7', bgColor: '#08bea7' }
|
||||
{ key: 'riskAnalysis', title: '待风险分析', value: 3, char: '险', colorClass: 'blue' },
|
||||
{ key: 'contractInput', title: '待合同录入', value: 2, char: '合', colorClass: 'violet' },
|
||||
{ key: 'projectTeam', title: '待项目组成立', value: 2, char: '组', colorClass: 'green' },
|
||||
{ key: 'siteSurvey', title: '待现场踏勘', value: 4, char: '场', colorClass: 'orange' },
|
||||
{ key: 'reportDraft', title: '待报告编制', value: 5, char: '报', colorClass: 'blue' },
|
||||
{ key: 'internalReview', title: '待内部审核', value: 3, char: '内', colorClass: 'violet' },
|
||||
{ key: 'techReview', title: '待技术审核', value: 2, char: '技', colorClass: 'orange' },
|
||||
{ key: 'processReview', title: '待过程控制审核', value: 1, char: '控', colorClass: 'green' },
|
||||
{ key: 'archived', title: '已归档项目', value: 9, char: '档', colorClass: 'cyan' },
|
||||
{ key: 'delayed', title: '项目延期', value: 2, char: '延', colorClass: 'red' },
|
||||
];
|
||||
|
||||
export const INFO_ALERTS = [
|
||||
{ key: 'orgQualification', title: '机构资质到期', value: 5, icon: 'Star', color: '#ffca0a', bgColor: '#fff9e7' },
|
||||
{ key: 'personnelQualification', title: '人员资质到期', value: 35, icon: 'Audit', color: '#ff7a59', bgColor: '#fff3ef' },
|
||||
{ key: 'personnelResignation', title: '人员离岗信息', value: 56, icon: 'UserDelete', color: '#6395f9', bgColor: '#f1f6ff' }
|
||||
{ key: 'qualCheck', title: '资质现场审查通知', value: 1, char: '审', colorClass: 'yellow' },
|
||||
{ key: 'supervisionCheck', title: '监督检查通知', value: 3, char: '检', colorClass: 'red' },
|
||||
{ key: 'unreadNotice', title: '未读监管通知', value: 2, char: '未', colorClass: 'blue' },
|
||||
];
|
||||
|
||||
export const PROJECT_SUMMARY = {
|
||||
total: 5632,
|
||||
overdue: 56
|
||||
total: 31,
|
||||
statutory: 24,
|
||||
};
|
||||
|
||||
export const ENTERPRISE_TYPE_STATS = {
|
||||
categories: ['矿山开采业', '危险化学品行业', '烟花爆竹行业', '金属冶炼与加工', '建筑施工', '交通运输业', '消防重点单位', '特种设备相关企业', '涉爆粉尘企业'],
|
||||
projectCount: [42, 35, 33, 35, 24, 42, 36, 42, 32],
|
||||
amount: [52, 54, 29, 48, 39, 36, 56, 35, 45]
|
||||
categories: ['危险化学品', '非煤矿山', '金属冶炼', '工贸', '烟花爆竹', '石油天然气', '其他行业'],
|
||||
projectCount: [24, 20, 15, 18, 12, 14, 10],
|
||||
statutoryCount: [19, 16, 12, 14, 9, 11, 7],
|
||||
};
|
||||
|
||||
export const PROJECT_TYPE_DISTRIBUTION = [
|
||||
{ name: '定期检测', value: 5054, color: '#3f7df4' },
|
||||
{ name: '现状评价', value: 1540, color: '#63c174' },
|
||||
{ name: '控制效果评价', value: 1600, color: '#ffa533' },
|
||||
{ name: '预评价', value: 900, color: '#ff694f' },
|
||||
{ name: '设计专篇', value: 430, color: '#23c6c8' },
|
||||
{ name: '委托检测', value: 330, color: '#55c653' }
|
||||
{ name: '安全预评价', value: 12, color: '#4285f4' },
|
||||
{ name: '安全验收评价', value: 8, color: '#58b874' },
|
||||
{ name: '安全现状评价', value: 11, color: '#ff9f2f' },
|
||||
];
|
||||
|
||||
export const PROJECT_TOTAL = 9854;
|
||||
export const PROJECT_TOTAL = 31;
|
||||
|
||||
export const PROJECT_COMPLETION_LIST = [
|
||||
{
|
||||
id: 1,
|
||||
projectName: '玉田县志达贸易有限公司蓝兴加油站、LNG加气设施合并项目',
|
||||
status: '检测完成',
|
||||
projectLeader: '梁永利',
|
||||
clientLeader: '赵天祥',
|
||||
startDate: '2025-5-4 16:12:23',
|
||||
endDate: '2025-5-4 16:12:23',
|
||||
acceptanceDate: '2025-5-4 16:12:23'
|
||||
projectCode: 'XP-2026-001',
|
||||
projectName: '华安化工园区安全预评价',
|
||||
evalCompany: '重庆华安安全科技有限公司',
|
||||
evalType: '安全预评价',
|
||||
projectLeader: '张建国',
|
||||
endDate: '2026-09-30',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
projectName: '北京首钢铁合金有限公司迁安分公司包芯线生产线扩建项目',
|
||||
status: '检测完成',
|
||||
projectLeader: '梁永利',
|
||||
clientLeader: '赵天祥',
|
||||
startDate: '2025-5-4 16:12:23',
|
||||
endDate: '2025-5-4 16:12:23',
|
||||
acceptanceDate: '2025-5-4 16:12:23'
|
||||
projectCode: 'XP-2026-002',
|
||||
projectName: '鼎信建设安全验收评价',
|
||||
evalCompany: '重庆鼎信安全咨询有限公司',
|
||||
evalType: '安全验收评价',
|
||||
projectLeader: '李明华',
|
||||
endDate: '2026-08-15',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
projectName: '中特伟业科技有限公司沧州金固废回收利用项目',
|
||||
status: '检测完成',
|
||||
projectLeader: '梁永利',
|
||||
clientLeader: '赵天祥',
|
||||
startDate: '2025-5-4 16:12:23',
|
||||
endDate: '2025-5-4 16:12:23',
|
||||
acceptanceDate: '2025-5-4 16:12:23'
|
||||
projectCode: 'XP-2026-003',
|
||||
projectName: '西南矿业安全现状评价',
|
||||
evalCompany: '重庆西南矿业有限公司',
|
||||
evalType: '安全现状评价',
|
||||
projectLeader: '陈志强',
|
||||
endDate: '2026-10-20',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
projectName: '荣信钢铁有限公司整合重组装备更新一期工程项目安全预评价',
|
||||
status: '检测完成',
|
||||
projectLeader: '梁永利',
|
||||
clientLeader: '赵天祥',
|
||||
startDate: '2025-5-4 16:12:23',
|
||||
endDate: '2025-5-4 16:12:23',
|
||||
acceptanceDate: '2025-5-4 16:12:23'
|
||||
}
|
||||
projectCode: 'XP-2026-004',
|
||||
projectName: '两江装备制造安全现状评价',
|
||||
evalCompany: '重庆两江装备制造有限公司',
|
||||
evalType: '安全现状评价',
|
||||
projectLeader: '王海峰',
|
||||
endDate: '2026-11-08',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,22 +3,28 @@ import * as echarts from 'echarts';
|
|||
import {
|
||||
AuditOutlined,
|
||||
BellOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
ContainerOutlined,
|
||||
FileDoneOutlined,
|
||||
FileProtectOutlined,
|
||||
FundProjectionScreenOutlined,
|
||||
HourglassOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
ScheduleOutlined,
|
||||
SnippetsOutlined,
|
||||
TeamOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import DriverBack from '~/pages/Container/Driver/components/DriverBack';
|
||||
import './index.css';
|
||||
import { cockpitMockData } from './mockData';
|
||||
import chongqingGeoJson from './data/chongqingGeoJson.json';
|
||||
import {
|
||||
fetchQualificationOverview,
|
||||
fetchKpi,
|
||||
fetchRegionDistribution,
|
||||
fetchProcessOverview,
|
||||
fetchEvalTypeTrend,
|
||||
fetchReviewSummary,
|
||||
fetchProjectMonitor,
|
||||
} from '~/api/supervision';
|
||||
|
||||
echarts.registerMap('chongqing', chongqingGeoJson);
|
||||
|
||||
|
|
@ -34,6 +40,87 @@ const iconMap = [
|
|||
ContainerOutlined,
|
||||
];
|
||||
|
||||
// 将 mock 数据转为 API 结构,作为初始值
|
||||
function buildInitialFromMock(m) {
|
||||
return {
|
||||
title: m.title,
|
||||
currentTime: m.currentTime,
|
||||
lifecycleStats: {
|
||||
newFilingOrgCount: m.lifecycleStats[0]?.value || 0,
|
||||
currentFilingOrgCount: m.lifecycleStats[1]?.value || 0,
|
||||
logoutOrgCount: m.lifecycleStats[2]?.value || 0,
|
||||
totalFilingOrgCount: m.lifecycleStats[3]?.value || 0,
|
||||
exitedEvaluatorCount: m.lifecycleStats[4]?.value || 0,
|
||||
currentEvaluatorCount: m.lifecycleStats[5]?.value || 0,
|
||||
},
|
||||
progress: {
|
||||
filingCompleteRate: m.progress[0]?.value || 0,
|
||||
bizActiveRate: m.progress[1]?.value || 0,
|
||||
newGrowthRate: m.progress[2]?.value || 0,
|
||||
},
|
||||
industryList: m.industry.map((item, i) => ({
|
||||
industryCode: String(i + 1),
|
||||
industryName: item.name,
|
||||
orgCount: item.institution,
|
||||
typeCount: item.rectification,
|
||||
})),
|
||||
riskItems: m.riskItems,
|
||||
riskPie: m.riskPie,
|
||||
kpiItems: m.kpis.map(k => ({
|
||||
code: '',
|
||||
name: k.title,
|
||||
value: parseFloat(k.value),
|
||||
yoy: parseFloat(k.change),
|
||||
})),
|
||||
regionList: m.mapAreaStats
|
||||
.filter(item => item.name !== '重庆市')
|
||||
.map(item => ({
|
||||
districtCode: '',
|
||||
districtName: item.name,
|
||||
evalProjectCount: item.projectCount,
|
||||
filingOrgCount: item.institutionCount,
|
||||
})),
|
||||
regionSummary: {
|
||||
totalEvalProject: m.mapAreaStats.find(s => s.name === '重庆市')?.projectCount || 0,
|
||||
totalFilingOrg: m.mapAreaStats.find(s => s.name === '重庆市')?.institutionCount || 0,
|
||||
},
|
||||
mapScatterPoints: m.mapScatterPoints,
|
||||
processNodes: m.todo.map((item, i) => ({
|
||||
nodeCode: `NODE_${i}`,
|
||||
nodeName: item.label,
|
||||
totalCount: 0,
|
||||
pendingCount: item.value,
|
||||
})),
|
||||
trendBuckets: m.reviewLine.xAxis,
|
||||
trendSeries: [
|
||||
{ evalTypeCode: '', evalTypeName: '项目数', projectCounts: m.reviewLine.project, inspCounts: [] },
|
||||
{ evalTypeCode: '', evalTypeName: '监督检查数', projectCounts: m.reviewLine.supervision, inspCounts: [] },
|
||||
],
|
||||
reviewStats: {
|
||||
inspCount: m.reviewProgress[0]?.value || 0,
|
||||
onsiteCheckCount: m.reviewProgress[0]?.value || 0,
|
||||
qualKeepCount: 0,
|
||||
specialCount: 0,
|
||||
checkedOrgCount: m.reviewProgress[4]?.value || 0,
|
||||
foundProblemCount: m.reviewProgress[5]?.value || 0,
|
||||
},
|
||||
reviewPieItems: m.reviewPie.map(item => ({
|
||||
code: '',
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
})),
|
||||
// 项目流程 mock 所用(基于 processOverview API nodes 结构)
|
||||
processMockNodes: m.process.map((item, i) => ({
|
||||
nodeCode: `NODE_${i}`,
|
||||
nodeName: item.label,
|
||||
totalCount: item.value,
|
||||
pendingCount: 0,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 子组件 ====================
|
||||
|
||||
function EChart({ option, className, events }) {
|
||||
const chartRef = useRef(null);
|
||||
const instanceRef = useRef(null);
|
||||
|
|
@ -96,7 +183,25 @@ function Panel({ title, children, className = '' }) {
|
|||
);
|
||||
}
|
||||
|
||||
// ==================== 资质全生命周期管理 ====================
|
||||
function LifecyclePanel({ data }) {
|
||||
const { lifecycleStats, progress, industryList } = data;
|
||||
|
||||
const statItems = useMemo(() => [
|
||||
{ label: '本年新增备案机构数', value: lifecycleStats.newFilingOrgCount },
|
||||
{ label: '当前备案机构数', value: lifecycleStats.currentFilingOrgCount },
|
||||
{ label: '本年注销机构数', value: lifecycleStats.logoutOrgCount },
|
||||
{ label: '全部备案机构数', value: lifecycleStats.totalFilingOrgCount },
|
||||
{ label: '退出备案评价师数', value: lifecycleStats.exitedEvaluatorCount },
|
||||
{ label: '当前备案评价师数', value: lifecycleStats.currentEvaluatorCount },
|
||||
], [lifecycleStats]);
|
||||
|
||||
const progressItems = useMemo(() => [
|
||||
{ label: '评价机构备案完成率', value: progress.filingCompleteRate, color: '#21d8ff' },
|
||||
{ label: '备案机构开展业务率', value: progress.bizActiveRate, color: '#18f7d2' },
|
||||
{ label: '备案机构新增率', value: progress.newGrowthRate, color: '#ffd11a' },
|
||||
], [progress]);
|
||||
|
||||
const barOption = useMemo(() => ({
|
||||
color: ['#1aa7ff', '#d7a92c'],
|
||||
grid: { left: 28, right: 10, top: 20, bottom: 42 },
|
||||
|
|
@ -104,7 +209,7 @@ function LifecyclePanel({ data }) {
|
|||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(3, 25, 66, .92)', borderColor: '#1aa7ff', textStyle: { color: '#dff7ff' } },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.industry.map(item => item.name),
|
||||
data: industryList.map(item => item.industryName),
|
||||
axisLabel: { color: '#b9cfff', fontSize: 10, interval: 0, rotate: 0, width: 38, overflow: 'break' },
|
||||
axisLine: { lineStyle: { color: '#17416e' } },
|
||||
axisTick: { show: false },
|
||||
|
|
@ -115,15 +220,15 @@ function LifecyclePanel({ data }) {
|
|||
splitLine: { lineStyle: { color: 'rgba(78, 143, 219, .2)', type: 'dashed' } },
|
||||
},
|
||||
series: [
|
||||
{ name: '类型数', type: 'bar', stack: 'total', barWidth: 14, data: data.industry.map(item => item.rectification) },
|
||||
{ name: '机构数', type: 'bar', stack: 'total', barWidth: 14, data: data.industry.map(item => item.institution) },
|
||||
{ name: '类型数', type: 'bar', stack: 'total', barWidth: 14, data: industryList.map(item => item.typeCount) },
|
||||
{ name: '机构数', type: 'bar', stack: 'total', barWidth: 14, data: industryList.map(item => item.orgCount) },
|
||||
],
|
||||
}), [data.industry]);
|
||||
}), [industryList]);
|
||||
|
||||
return (
|
||||
<Panel title="资质全生命周期管理" className="lifecycle-panel">
|
||||
<div className="life-grid">
|
||||
{data.lifecycleStats.map((item, index) => (
|
||||
{statItems.map((item) => (
|
||||
<div className="life-stat" key={item.label}>
|
||||
<span className="life-stat__icon"><FileProtectOutlined /></span>
|
||||
<div><p>{item.label}</p><strong>{item.value}</strong></div>
|
||||
|
|
@ -131,7 +236,7 @@ function LifecyclePanel({ data }) {
|
|||
))}
|
||||
</div>
|
||||
<div className="progress-list">
|
||||
{data.progress.map(item => (
|
||||
{progressItems.map(item => (
|
||||
<div className="progress-row" key={item.label}>
|
||||
<span>{item.label}</span>
|
||||
<div className="progress-track"><i style={{ width: `${item.value}%`, background: item.color }} /></div>
|
||||
|
|
@ -144,6 +249,7 @@ function LifecyclePanel({ data }) {
|
|||
);
|
||||
}
|
||||
|
||||
// ==================== 风险预警(仍用 mock,无 API) ====================
|
||||
function RiskPanel({ data }) {
|
||||
const pieOption = useMemo(() => ({
|
||||
color: ['#5f8cff', '#e96fff', '#5fd8ae', '#b6a0ff'],
|
||||
|
|
@ -164,29 +270,64 @@ function RiskPanel({ data }) {
|
|||
);
|
||||
}
|
||||
|
||||
// ==================== KPI 指标卡片 ====================
|
||||
function KpiCards({ data }) {
|
||||
return <div className="kpi-row">{data.kpis.map(item => <div className="kpi-card" key={item.title}><p>{item.title}</p><strong>{item.value}</strong><span>同比 <b>↗ {item.change}</b></span></div>)}</div>;
|
||||
const { kpiItems } = data;
|
||||
return (
|
||||
<div className="kpi-row">
|
||||
{kpiItems.map(item => (
|
||||
<div className="kpi-card" key={item.name}>
|
||||
<p>{item.name}</p>
|
||||
<strong>{item.value}%</strong>
|
||||
<span>同比 <b>↗ {item.yoy}%</b></span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 地图区域分布 ====================
|
||||
function MapSection({ data }) {
|
||||
const [selectedAreaName, setSelectedAreaName] = useState('重庆市');
|
||||
const statMap = useMemo(() => new Map(data.mapAreaStats.map(item => [item.name, item])), [data.mapAreaStats]);
|
||||
const { regionList, regionSummary, mapScatterPoints } = data;
|
||||
|
||||
const statMap = useMemo(() => {
|
||||
const map = new Map();
|
||||
regionList.forEach(item => {
|
||||
map.set(item.districtName, {
|
||||
name: item.districtName,
|
||||
districtCode: item.districtCode,
|
||||
evalProjectCount: item.evalProjectCount,
|
||||
filingOrgCount: item.filingOrgCount,
|
||||
});
|
||||
});
|
||||
return map;
|
||||
}, [regionList]);
|
||||
|
||||
const mapData = useMemo(() => {
|
||||
return (chongqingGeoJson.features || []).map((feature, index) => {
|
||||
const name = feature.properties && feature.properties.name;
|
||||
const stat = statMap.get(name) || {
|
||||
name,
|
||||
value: 18 + index * 7,
|
||||
projectCount: 18 + index * 7,
|
||||
hiddenDanger: 0,
|
||||
institutionCount: 0,
|
||||
rectificationRate: '100%',
|
||||
evalProjectCount: 18 + index * 7,
|
||||
filingOrgCount: 0,
|
||||
};
|
||||
return {
|
||||
name,
|
||||
districtCode: stat.districtCode || '',
|
||||
evalProjectCount: stat.evalProjectCount,
|
||||
filingOrgCount: stat.filingOrgCount,
|
||||
};
|
||||
return { ...stat, name };
|
||||
});
|
||||
}, [statMap]);
|
||||
const selectedArea = statMap.get(selectedAreaName) || mapData[0] || data.mapAreaStats[0];
|
||||
const values = mapData.map(item => Number(item.value) || 0);
|
||||
|
||||
const selectedArea = useMemo(() => {
|
||||
if (selectedAreaName === '重庆市') {
|
||||
return { name: '重庆市', evalProjectCount: regionSummary.totalEvalProject, filingOrgCount: regionSummary.totalFilingOrg };
|
||||
}
|
||||
return statMap.get(selectedAreaName) || { name: selectedAreaName, evalProjectCount: 0, filingOrgCount: 0 };
|
||||
}, [selectedAreaName, statMap, regionSummary]);
|
||||
const values = mapData.map(item => Number(item.evalProjectCount) || 0);
|
||||
const maxValue = Math.max(...values, 100);
|
||||
|
||||
const mapOption = useMemo(() => ({
|
||||
|
|
@ -202,10 +343,8 @@ function MapSection({ data }) {
|
|||
if (!item) return params.name;
|
||||
return [
|
||||
`<strong>${params.name}</strong>`,
|
||||
`评价项目数:${item.projectCount || item.value || 0}`,
|
||||
`备案机构数:${item.institutionCount || 0}`,
|
||||
`隐患数量:${item.hiddenDanger || 0}`,
|
||||
`整改率:${item.rectificationRate || '-'}`,
|
||||
`评价项目数:${item.evalProjectCount || 0}`,
|
||||
`备案机构数:${item.filingOrgCount || 0}`,
|
||||
].join('<br/>');
|
||||
},
|
||||
},
|
||||
|
|
@ -249,7 +388,11 @@ function MapSection({ data }) {
|
|||
map: 'chongqing',
|
||||
geoIndex: 0,
|
||||
selectedMode: 'single',
|
||||
data: mapData.map(item => ({ ...item, selected: item.name === selectedAreaName })),
|
||||
data: mapData.map(item => ({
|
||||
...item,
|
||||
value: item.evalProjectCount,
|
||||
selected: item.name === selectedAreaName,
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: '重点乡镇',
|
||||
|
|
@ -259,10 +402,10 @@ function MapSection({ data }) {
|
|||
symbolSize: value => Math.max(8, Math.min(18, value[2] / 4)),
|
||||
itemStyle: { color: '#12f4ff', shadowBlur: 10, shadowColor: '#12f4ff' },
|
||||
label: { show: false },
|
||||
data: data.mapScatterPoints.map(item => ({ name: item.name, value: [...item.coord, item.value] })),
|
||||
data: mapScatterPoints.map(item => ({ name: item.name, value: [...item.coord, item.value] })),
|
||||
},
|
||||
],
|
||||
}), [data.mapScatterPoints, mapData, maxValue, selectedAreaName, statMap]);
|
||||
}), [mapData, maxValue, selectedAreaName, statMap, mapScatterPoints]);
|
||||
|
||||
const mapEvents = useMemo(() => ({
|
||||
click: params => {
|
||||
|
|
@ -273,84 +416,229 @@ function MapSection({ data }) {
|
|||
return (
|
||||
<div className="map-shell">
|
||||
<EChart className="map-chart" option={mapOption} events={mapEvents} />
|
||||
{data.mapScatterPoints.map((item, index) => <div className={`map-card map-card--${index + 1}`} key={item.name}><strong>{item.name}</strong><span>正在开展评价项目数:{item.value}</span></div>)}
|
||||
{mapScatterPoints.map((item, index) => <div className={`map-card map-card--${index + 1}`} key={item.name}><strong>{item.name}</strong><span>正在开展评价项目数:{item.value}</span></div>)}
|
||||
<div className="map-summary">
|
||||
<strong>{selectedArea && selectedArea.name}</strong>
|
||||
<span>项目数:{selectedArea && (selectedArea.projectCount || selectedArea.value)}</span>
|
||||
<span>隐患:{selectedArea && selectedArea.hiddenDanger}</span>
|
||||
<span>整改率:{selectedArea && selectedArea.rectificationRate}</span>
|
||||
<strong>{selectedArea.name}</strong>
|
||||
<span>项目数:{selectedArea.evalProjectCount}</span>
|
||||
<span>备案机构数:{selectedArea.filingOrgCount}</span>
|
||||
</div>
|
||||
<ul className="map-legend"><li>0-10</li><li>11-30</li><li>31-50</li><li>51-100</li></ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 执业全过程管控 ====================
|
||||
function TodoPanel({ data }) {
|
||||
const { processNodes } = data;
|
||||
return (
|
||||
<Panel title="执业全过程管控" className="todo-panel">
|
||||
<div className="todo-grid">
|
||||
{data.todo.map((item, index) => {
|
||||
{processNodes.map((item, index) => {
|
||||
const Icon = iconMap[index] || FileDoneOutlined;
|
||||
return <div className="todo-item" key={item.label}><span><Icon /></span><p>{item.label}</p><b>{item.value}</b></div>;
|
||||
return <div className="todo-item" key={item.nodeName}><span><Icon /></span><p>{item.nodeName}</p><b>{item.pendingCount}</b></div>;
|
||||
})}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 评价类型趋势 ====================
|
||||
function ReviewLinePanel({ data }) {
|
||||
const lineOption = useMemo(() => ({
|
||||
color: ['#0da5ff', '#29e487'],
|
||||
grid: { left: 34, right: 12, top: 28, bottom: 32 },
|
||||
legend: { right: 0, top: 0, itemWidth: 14, itemHeight: 4, textStyle: { color: '#c6dcff', fontSize: 10 } },
|
||||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(3, 25, 66, .92)', borderColor: '#1aa7ff', textStyle: { color: '#dff7ff' } },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: data.reviewLine.xAxis, axisLabel: { color: '#c6dcff', fontSize: 10 }, axisLine: { lineStyle: { color: '#17416e' } }, axisTick: { show: false } },
|
||||
yAxis: { type: 'value', min: 0, max: 100, splitNumber: 5, axisLabel: { color: '#c6dcff', fontSize: 10 }, splitLine: { lineStyle: { color: 'rgba(78, 143, 219, .22)', type: 'dashed' } } },
|
||||
series: [
|
||||
{ name: '项目数', type: 'line', smooth: true, symbolSize: 5, areaStyle: { opacity: .22 }, data: data.reviewLine.project },
|
||||
{ name: '监督检查数', type: 'line', smooth: true, symbolSize: 5, areaStyle: { opacity: .14 }, data: data.reviewLine.supervision },
|
||||
],
|
||||
}), [data.reviewLine]);
|
||||
return <Panel title="评价类型趋势" className="line-panel"><div className="period-tabs"><span>年</span><span>季</span><span>月</span></div><EChart className="line-chart" option={lineOption} /></Panel>;
|
||||
const { trendBuckets, trendSeries } = data;
|
||||
|
||||
const lineOption = useMemo(() => {
|
||||
const seriesConfig = trendSeries.map(s => ({
|
||||
name: s.evalTypeName,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbolSize: 5,
|
||||
areaStyle: { opacity: .22 },
|
||||
data: s.projectCounts,
|
||||
}));
|
||||
return {
|
||||
color: ['#0da5ff', '#29e487', '#ffb74d', '#ce93d8', '#4dd0e1', '#f48fb1'],
|
||||
grid: { left: 34, right: 12, top: 28, bottom: 32 },
|
||||
legend: { right: 0, top: 0, itemWidth: 14, itemHeight: 4, textStyle: { color: '#c6dcff', fontSize: 10 } },
|
||||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(3, 25, 66, .92)', borderColor: '#1aa7ff', textStyle: { color: '#dff7ff' } },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: trendBuckets, axisLabel: { color: '#c6dcff', fontSize: 10 }, axisLine: { lineStyle: { color: '#17416e' } }, axisTick: { show: false } },
|
||||
yAxis: { type: 'value', min: 0, max: 100, splitNumber: 5, axisLabel: { color: '#c6dcff', fontSize: 10 }, splitLine: { lineStyle: { color: 'rgba(78, 143, 219, .22)', type: 'dashed' } } },
|
||||
series: seriesConfig,
|
||||
};
|
||||
}, [trendBuckets, trendSeries]);
|
||||
|
||||
return (
|
||||
<Panel title="评价类型趋势" className="line-panel">
|
||||
<div className="period-tabs"><span>年</span><span>季</span><span>月</span></div>
|
||||
<EChart className="line-chart" option={lineOption} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 复盘评估改进提效 ====================
|
||||
function ReviewProgressPanel({ data }) {
|
||||
const { reviewStats, reviewPieItems } = data;
|
||||
|
||||
const statItems = useMemo(() => [
|
||||
{ label: '监督检查总数', value: reviewStats.inspCount },
|
||||
{ label: '项目过程检查数', value: reviewStats.onsiteCheckCount },
|
||||
{ label: '资质保持检查数', value: reviewStats.qualKeepCount },
|
||||
{ label: '专项检查数', value: reviewStats.specialCount },
|
||||
{ label: '检查机构数', value: reviewStats.checkedOrgCount },
|
||||
{ label: '发现问题数', value: reviewStats.foundProblemCount },
|
||||
], [reviewStats]);
|
||||
|
||||
const pieOption = useMemo(() => ({
|
||||
color: ['#1677ff', '#00c089', '#ff7a1a', '#f7cb16', '#ab5fde'],
|
||||
tooltip: { trigger: 'item', backgroundColor: 'rgba(3, 25, 66, .92)', borderColor: '#1aa7ff', textStyle: { color: '#dff7ff' } },
|
||||
legend: { orient: 'vertical', right: 0, top: 'center', itemWidth: 10, itemHeight: 8, textStyle: { color: '#c6dcff', fontSize: 10 } },
|
||||
series: [{ type: 'pie', radius: ['32%', '68%'], center: ['28%', '56%'], label: { show: false }, data: data.reviewPie }],
|
||||
}), [data.reviewPie]);
|
||||
series: [{ type: 'pie', radius: ['32%', '68%'], center: ['28%', '56%'], label: { show: false }, data: reviewPieItems.map(item => ({ name: item.name, value: item.value })) }],
|
||||
}), [reviewPieItems]);
|
||||
|
||||
return (
|
||||
<Panel title="复盘评估改进提效" className="review-panel">
|
||||
<div className="review-stats">{data.reviewProgress.map(item => <div className="review-stat" key={item.label}><span><FundProjectionScreenOutlined /></span><p>{item.label}:<b className={item.danger ? 'danger' : ''}>{item.value}</b></p></div>)}</div>
|
||||
<div className="review-stats">
|
||||
{statItems.map(item => <div className="review-stat" key={item.label}><span><FundProjectionScreenOutlined /></span><p>{item.label}:<b>{item.value}</b></p></div>)}
|
||||
</div>
|
||||
<EChart className="review-pie" option={pieOption} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 项目流程 ====================
|
||||
function ProcessPanel({ data }) {
|
||||
const { processNodes } = data;
|
||||
return (
|
||||
<Panel title="项目流程" className="process-panel">
|
||||
<div className="process-grid">{data.process.map((item, index) => <div className="process-node" key={item.label}><strong>{item.value}</strong><span>{item.label}</span>{index < data.process.length - 1 && <i />}</div>)}</div>
|
||||
<div className="process-grid">
|
||||
{processNodes.map((item, index) => (
|
||||
<div className="process-node" key={item.nodeName}>
|
||||
<strong>{item.totalCount}</strong>
|
||||
<span>{item.nodeName}</span>
|
||||
{index < processNodes.length - 1 && <i />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 主组件 ====================
|
||||
export default function SupervisionCockpit() {
|
||||
const data = cockpitMockData;
|
||||
const [data, setData] = useState(() => buildInitialFromMock(cockpitMockData));
|
||||
|
||||
// 取当前年份/月份
|
||||
const currentYear = useMemo(() => new Date().getFullYear(), []);
|
||||
const currentMonth = useMemo(() => {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
||||
}, []);
|
||||
|
||||
// 标记是否使用 API 数据(仅当对应 API 成功时为 true)
|
||||
const [apiFlags, setApiFlags] = useState({
|
||||
qualification: false,
|
||||
process: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// 1. 资质全生命周期管理
|
||||
fetchQualificationOverview(currentYear)
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
lifecycleStats: apiData.lifecycleStats || prev.lifecycleStats,
|
||||
progress: apiData.progress || prev.progress,
|
||||
industryList: apiData.industryList || prev.industryList,
|
||||
}));
|
||||
setApiFlags(prev => ({ ...prev, qualification: true }));
|
||||
})
|
||||
.catch(() => { /* 接口异常,沿用 mock 数据 */ });
|
||||
|
||||
// 2. 核心 KPI
|
||||
fetchKpi('year', currentYear)
|
||||
.then((apiData) => {
|
||||
if (apiData.items && apiData.items.length > 0) {
|
||||
setData(prev => ({ ...prev, kpiItems: apiData.items }));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// 3. 区域地图分布
|
||||
fetchRegionDistribution()
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
regionList: apiData.list || prev.regionList,
|
||||
regionSummary: apiData.summary || prev.regionSummary,
|
||||
}));
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// 4. 执业全过程管控 + 项目流程
|
||||
fetchProcessOverview()
|
||||
.then((apiData) => {
|
||||
if (apiData.nodes && apiData.nodes.length > 0) {
|
||||
setData(prev => ({ ...prev, processNodes: apiData.nodes }));
|
||||
setApiFlags(prev => ({ ...prev, process: true }));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// 5. 评价类型趋势
|
||||
fetchEvalTypeTrend('month', currentYear)
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
trendBuckets: apiData.buckets || prev.trendBuckets,
|
||||
trendSeries: apiData.series || prev.trendSeries,
|
||||
}));
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// 6. 复盘评估改进提效
|
||||
fetchReviewSummary(currentYear)
|
||||
.then((apiData) => {
|
||||
setData(prev => ({
|
||||
...prev,
|
||||
reviewStats: apiData.stats || prev.reviewStats,
|
||||
reviewPieItems: apiData.pie || prev.reviewPieItems,
|
||||
}));
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// 7. 项目实时监控(当前暂无对应面板,仅预取)
|
||||
fetchProjectMonitor(currentMonth)
|
||||
.catch(() => {});
|
||||
}, [currentYear, currentMonth]);
|
||||
|
||||
// 项目流程面板数据:API 有值时用 processNodes,否则用 mock processMockNodes
|
||||
const processPanelNodes = useMemo(() => {
|
||||
if (apiFlags.process && data.processNodes && data.processNodes.length > 0) {
|
||||
return data.processNodes;
|
||||
}
|
||||
return data.processMockNodes || data.processNodes || [];
|
||||
}, [apiFlags.process, data.processNodes, data.processMockNodes]);
|
||||
|
||||
return (
|
||||
<div className="cockpit-page">
|
||||
<div className="cockpit-stage">
|
||||
<ScreenHeader data={data} />
|
||||
<main className="cockpit-layout">
|
||||
<aside className="cockpit-left"><LifecyclePanel data={data} /><RiskPanel data={data} /></aside>
|
||||
<section className="cockpit-center"><KpiCards data={data} /><MapSection data={data} /><ProcessPanel data={data} /></section>
|
||||
<aside className="cockpit-right"><TodoPanel data={data} /><ReviewLinePanel data={data} /><ReviewProgressPanel data={data} /></aside>
|
||||
<aside className="cockpit-left">
|
||||
<LifecyclePanel data={data} />
|
||||
<RiskPanel data={data} />
|
||||
</aside>
|
||||
<section className="cockpit-center">
|
||||
<KpiCards data={data} />
|
||||
<MapSection data={data} />
|
||||
<ProcessPanel data={{ processNodes: processPanelNodes }} />
|
||||
</section>
|
||||
<aside className="cockpit-right">
|
||||
<TodoPanel data={data} />
|
||||
<ReviewLinePanel data={data} />
|
||||
<ReviewProgressPanel data={data} />
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -96,3 +96,32 @@ export function resolveDriverAuthParams() {
|
|||
export function getDriverToken() {
|
||||
return resolveDriverAuthParams().token || window.sessionStorage.getItem("token");
|
||||
}
|
||||
|
||||
const PAGE_MODE_KEY = "driverPageMode";
|
||||
|
||||
/**
|
||||
* 页面开关变量:
|
||||
* 1 — 请求 checkTerminalType,由接口决定展示哪个页面(默认行为)
|
||||
* 2 — 跳过接口请求,直接展示监管端驾驶舱
|
||||
* 3 — 跳过接口请求,直接展示机构首页
|
||||
*
|
||||
* 优先级:URL 参数 pageMode > sessionStorage > 默认值 1
|
||||
* 通过 URL 参数设置时会自动写入 sessionStorage 持久化。
|
||||
*/
|
||||
export function getPageMode() {
|
||||
try {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const urlMode = Number(searchParams.get("pageMode"));
|
||||
if (urlMode === 1 || urlMode === 2 || urlMode === 3) {
|
||||
window.sessionStorage.setItem(PAGE_MODE_KEY, String(urlMode));
|
||||
return urlMode;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const storedMode = Number(window.sessionStorage.getItem(PAGE_MODE_KEY));
|
||||
if (storedMode === 1 || storedMode === 2 || storedMode === 3) {
|
||||
return storedMode;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue