机构端、监管端首页修改

dev
huwei 2026-07-31 15:17:21 +08:00
parent 3e682fd0aa
commit 53d7617b94
15 changed files with 2510 additions and 548 deletions

View File

@ -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 分组
- 名称取自 EvalTypeEnumPRE/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

View File

@ -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: 打卡异常数

View File

@ -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 });
}

View File

@ -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 });
}

View File

@ -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 (

View File

@ -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} />;
}

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>
);
}

View File

@ -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',
},
];

View File

@ -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>
);
}

View File

@ -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;
}