diff --git a/docs/机构端-首页驾驶舱.openapi.yaml b/docs/机构端-首页驾驶舱.openapi.yaml new file mode 100644 index 0000000..22f1ba8 --- /dev/null +++ b/docs/机构端-首页驾驶舱.openapi.yaml @@ -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) diff --git a/docs/监管端-驾驶舱大屏.openapi.yaml b/docs/监管端-驾驶舱大屏.openapi.yaml new file mode 100644 index 0000000..9b0c09e --- /dev/null +++ b/docs/监管端-驾驶舱大屏.openapi.yaml @@ -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: 打卡异常数 diff --git a/src/api/institution/index.js b/src/api/institution/index.js new file mode 100644 index 0000000..2d96bd4 --- /dev/null +++ b/src/api/institution/index.js @@ -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 }); +} diff --git a/src/api/supervision/index.js b/src/api/supervision/index.js new file mode 100644 index 0000000..06666ec --- /dev/null +++ b/src/api/supervision/index.js @@ -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 }); +} diff --git a/src/pages/Container/Driver/index.js b/src/pages/Container/Driver/index.js index 13cde9d..6802368 100644 --- a/src/pages/Container/Driver/index.js +++ b/src/pages/Container/Driver/index.js @@ -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 ( diff --git a/src/pages/Container/Institution/components/EnterpriseTypeChart.jsx b/src/pages/Container/Institution/components/EnterpriseTypeChart.jsx index 41c931e..1d43a17 100644 --- a/src/pages/Container/Institution/components/EnterpriseTypeChart.jsx +++ b/src/pages/Container/Institution/components/EnterpriseTypeChart.jsx @@ -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
; -} - -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 ( - 服务企业类型统计} - size="small" - className="institution-panel-card institution-enterprise-card" - styles={{ body: { padding: '20px 26px 15px' } }} - extra={ -
- 项目数 - 金额 -
- } - > - -
- ); + useEffect(() => { + if (instanceRef.current) instanceRef.current.setOption(option, true); + }, [option]); + + return
; } diff --git a/src/pages/Container/Institution/components/InfoAlerts.jsx b/src/pages/Container/Institution/components/InfoAlerts.jsx index a520dad..b1e7b46 100644 --- a/src/pages/Container/Institution/components/InfoAlerts.jsx +++ b/src/pages/Container/Institution/components/InfoAlerts.jsx @@ -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 ( -
-
- -
-
-
{item.title}
-
{item.value}
-
-
- ); -} export default function InfoAlerts({ data }) { return ( -
- {data.map((item) => ( - +
+ {data.map(item => ( +
+ {item.char} +
+ {item.title} + {item.value} +
+
))}
); diff --git a/src/pages/Container/Institution/components/ProjectCompletionTable.jsx b/src/pages/Container/Institution/components/ProjectCompletionTable.jsx index 0e7c826..7396233 100644 --- a/src/pages/Container/Institution/components/ProjectCompletionTable.jsx +++ b/src/pages/Container/Institution/components/ProjectCompletionTable.jsx @@ -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: () => 查看详情 } -]; +export default function ProjectCompletionTable({ data, onView }) { + const list = data && data.list ? data.list : []; -export default function ProjectCompletionTable({ data }) { return ( - 项目完成情况统计} - size="small" - className="institution-panel-card institution-table-card" - styles={{ body: { padding: '12px 14px 14px' } }} - > - - +
+
+

项目执行情况

+ +
+
+
+ + + + + + + + + + + + + + {list.map((record, index) => ( + + + + + + + + + + + ))} + +
序号项目编号项目名称被评价企业评价类别项目负责人项目结束日期操作
{index + 1}{record.projectNo}{record.projectName}{record.customerName}{record.evalTypeName}{record.projectLeaderName}{record.planEndDate} + +
+
+ ); } diff --git a/src/pages/Container/Institution/components/ProjectTypeChart.jsx b/src/pages/Container/Institution/components/ProjectTypeChart.jsx index f89bccf..a74c04f 100644 --- a/src/pages/Container/Institution/components/ProjectTypeChart.jsx +++ b/src/pages/Container/Institution/components/ProjectTypeChart.jsx @@ -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
; } -function Legend({ data }) { - return ( -
- {data.map((item) => ( - - - {item.name} - - ))} -
- ); -} +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}
项目数:{c}
占比:{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 ( - 项目类型占比} - size="small" - className="institution-panel-card institution-project-card" - styles={{ body: { padding: '24px 22px 18px' } }} - > + <> - - +
+ {items.map((item, i) => ( + + + {item.evalTypeName} {Number(item.count) || 0} + + ))} +
+ ); } diff --git a/src/pages/Container/Institution/components/StatisticCards.jsx b/src/pages/Container/Institution/components/StatisticCards.jsx index 36d6b94..a1b102b 100644 --- a/src/pages/Container/Institution/components/StatisticCards.jsx +++ b/src/pages/Container/Institution/components/StatisticCards.jsx @@ -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 (
-
- +
+ {item.char}
{item.title}
diff --git a/src/pages/Container/Institution/index.css b/src/pages/Container/Institution/index.css index 5ff5ec5..59d3fef 100644 --- a/src/pages/Container/Institution/index.css +++ b/src/pages/Container/Institution/index.css @@ -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; } } - diff --git a/src/pages/Container/Institution/index.js b/src/pages/Container/Institution/index.js index 4a99e34..8f6d1ec 100644 --- a/src/pages/Container/Institution/index.js +++ b/src/pages/Container/Institution/index.js @@ -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 ( +
+ {item.char} +
+ {item.title} + {item.value} +
+
+ ); +} + +function ReminderCard({ item }) { + return ( +
+ {item.char} +
+ {item.title} + {item.value} +
+
+ ); +} + +// ==================== 主组件 ==================== 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 (
-
- 当前评价状态状态统计} - size="small" - className="institution-dashboard__status-card institution-panel-card" - extra={ - - - 项目总数: {PROJECT_SUMMARY.total} - - - 超期数: {PROJECT_SUMMARY.overdue} - - - } - styles={{ body: { padding: '16px 34px 17px' } }} - > - - - 信息提醒} - size="small" - className="institution-dashboard__alerts-card institution-panel-card" - styles={{ body: { padding: '20px 24px' } }} - > - - +
+ {/* ===== 第一行:项目节点统计 + 通知提醒 ===== */} +
+
+
+

当前项目节点统计

+
+ 项目总数:{projectSummary.total} + 法定项目:{projectSummary.statutory} +
+
+
+ {statusCards.map(item => ( + + ))} +
+
+ +
+
+

通知提醒

+
+
+ {alertItems.map(item => ( + + ))} +
+
+
+ + {/* ===== 第二行:服务行业项目统计 + 评价类别占比 ===== */} +
+
+
+

服务行业项目统计

+
+ 项目数 + 法定项目 +
+
+ +
+ +
+
+

评价类别占比

+
+ +
+
+ + {/* ===== 第三行:项目执行情况 ===== */} + {}} + />
- -
- - -
- -
); } diff --git a/src/pages/Container/Institution/mockData.js b/src/pages/Container/Institution/mockData.js index c311eb8..a127346 100644 --- a/src/pages/Container/Institution/mockData.js +++ b/src/pages/Container/Institution/mockData.js @@ -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', + }, ]; diff --git a/src/pages/Container/Supervision/Cockpit/index.js b/src/pages/Container/Supervision/Cockpit/index.js index cb5abbc..71246e3 100644 --- a/src/pages/Container/Supervision/Cockpit/index.js +++ b/src/pages/Container/Supervision/Cockpit/index.js @@ -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 (
- {data.lifecycleStats.map((item, index) => ( + {statItems.map((item) => (

{item.label}

{item.value}
@@ -131,7 +236,7 @@ function LifecyclePanel({ data }) { ))}
- {data.progress.map(item => ( + {progressItems.map(item => (
{item.label}
@@ -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
{data.kpis.map(item =>

{item.title}

{item.value}同比 ↗ {item.change}
)}
; + const { kpiItems } = data; + return ( +
+ {kpiItems.map(item => ( +
+

{item.name}

+ {item.value}% + 同比 ↗ {item.yoy}% +
+ ))} +
+ ); } +// ==================== 地图区域分布 ==================== 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 [ `${params.name}`, - `评价项目数:${item.projectCount || item.value || 0}`, - `备案机构数:${item.institutionCount || 0}`, - `隐患数量:${item.hiddenDanger || 0}`, - `整改率:${item.rectificationRate || '-'}`, + `评价项目数:${item.evalProjectCount || 0}`, + `备案机构数:${item.filingOrgCount || 0}`, ].join('
'); }, }, @@ -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 (
- {data.mapScatterPoints.map((item, index) =>
{item.name}正在开展评价项目数:{item.value}
)} + {mapScatterPoints.map((item, index) =>
{item.name}正在开展评价项目数:{item.value}
)}
- {selectedArea && selectedArea.name} - 项目数:{selectedArea && (selectedArea.projectCount || selectedArea.value)} - 隐患:{selectedArea && selectedArea.hiddenDanger} - 整改率:{selectedArea && selectedArea.rectificationRate} + {selectedArea.name} + 项目数:{selectedArea.evalProjectCount} + 备案机构数:{selectedArea.filingOrgCount}
  • 0-10
  • 11-30
  • 31-50
  • 51-100
); } + +// ==================== 执业全过程管控 ==================== function TodoPanel({ data }) { + const { processNodes } = data; return (
- {data.todo.map((item, index) => { + {processNodes.map((item, index) => { const Icon = iconMap[index] || FileDoneOutlined; - return

{item.label}

{item.value}
; + return

{item.nodeName}

{item.pendingCount}
; })}
); } +// ==================== 评价类型趋势 ==================== 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
; + 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 ( + +
+ +
+ ); } +// ==================== 复盘评估改进提效 ==================== 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 ( -
{data.reviewProgress.map(item =>

{item.label}:{item.value}

)}
+
+ {statItems.map(item =>

{item.label}:{item.value}

)} +
); } +// ==================== 项目流程 ==================== function ProcessPanel({ data }) { + const { processNodes } = data; return ( -
{data.process.map((item, index) =>
{item.value}{item.label}{index < data.process.length - 1 && }
)}
+
+ {processNodes.map((item, index) => ( +
+ {item.totalCount} + {item.nodeName} + {index < processNodes.length - 1 && } +
+ ))} +
); } +// ==================== 主组件 ==================== 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 (
- -
- + +
+ + + +
+
); } - - diff --git a/src/utils/resolveDriverAuthParams.js b/src/utils/resolveDriverAuthParams.js index 2de7e43..48add2b 100644 --- a/src/utils/resolveDriverAuthParams.js +++ b/src/utils/resolveDriverAuthParams.js @@ -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; +}