diff --git a/indexV2.html b/indexV2.html index d8919f5..1d2e345 100644 --- a/indexV2.html +++ b/indexV2.html @@ -1,15 +1,16 @@ - + + 集团数据治理项目监控大屏 V2 - - - - - - + + + + + +
@@ -25,8 +26,7 @@
数据溯源表
- 数据时间 2026-04-27 - 对比日期 2026-04-26 + 数据时间 2026-07-02
@@ -58,20 +58,19 @@
指标穿透分析
-
- - - - - - - - + + + + + + + + diff --git a/res/css/indexV2.cards.css b/res/css/indexV2.cards.css index 9bcd8e3..57014b0 100644 --- a/res/css/indexV2.cards.css +++ b/res/css/indexV2.cards.css @@ -1,6 +1,6 @@ .kpi-grid { display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; } @@ -210,7 +210,7 @@ .indicator-strip { display: grid; - grid-template-columns: repeat(8, minmax(0, 1fr)) 84px; + grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; align-items: stretch; diff --git a/res/css/indexV2.detail.css b/res/css/indexV2.detail.css index 2d00a34..4371142 100644 --- a/res/css/indexV2.detail.css +++ b/res/css/indexV2.detail.css @@ -1,10 +1,14 @@ .metric-cards { display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin-bottom: 12px; } + .project-metric-cards { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + .mini-panel { padding: 14px; min-height: 116px; diff --git a/res/css/indexV2.modal-table.css b/res/css/indexV2.modal-table.css index b30057a..958fe93 100644 --- a/res/css/indexV2.modal-table.css +++ b/res/css/indexV2.modal-table.css @@ -5,6 +5,10 @@ margin-bottom: 12px; } + .drill-hero.group-indicator-hero { + grid-template-columns: minmax(0, 1fr) repeat(4, 170px); + } + .drill-title-card { position: relative; min-height: 118px; @@ -280,6 +284,27 @@ white-space: nowrap; } + .project-master-table { + min-width: 1480px; + } + + .project-master-table thead tr:first-child th { + text-align: center; + } + + .project-master-table thead th { + border: 1px solid rgba(186, 232, 255, 0.62); + } + + .project-master-table thead tr:nth-child(2) th { + top: 39px; + text-align: center; + } + + .indicator-enterprise-chart-panel { + min-height: 640px; + } + td { padding: 10px; border-bottom: 1px solid rgba(147, 221, 255, 0.12); @@ -297,6 +322,14 @@ background: rgba(73, 214, 255, 0.14); } + .project-master-table tr.summary-row td { + position: sticky; + bottom: 0; + z-index: 3; + background: #164f70; + box-shadow: 0 -1px 0 rgba(147, 221, 255, 0.38); + } + tr.green-row td { color: #d9fff1; background: rgba(94, 225, 181, 0.18); @@ -306,6 +339,22 @@ cursor: pointer; } + .table-link { + border: 0; + padding: 0; + color: #9feaff; + background: transparent; + cursor: pointer; + font: inherit; + font-weight: 900; + text-decoration: underline; + text-underline-offset: 3px; + } + + .table-link:hover { + color: #ffffff; + } + .num { font-family: var(--num); color: white; diff --git a/res/css/indexV2.panels.css b/res/css/indexV2.panels.css index 381e082..454f7bb 100644 --- a/res/css/indexV2.panels.css +++ b/res/css/indexV2.panels.css @@ -26,7 +26,7 @@ } .dashboard-grid.enterprise-overview .risk-item { - grid-template-columns: 64px 1fr auto; + grid-template-columns: 64px 1fr; gap: 9px; padding: 7px 9px; } @@ -197,7 +197,7 @@ .risk-item { display: grid; - grid-template-columns: 72px 1fr auto; + grid-template-columns: 72px 1fr; gap: 12px; align-items: center; padding: 10px 11px; @@ -225,7 +225,7 @@ } .risk-panel .risk-item { - grid-template-columns: 64px 1fr auto; + grid-template-columns: 64px 1fr; gap: 10px; padding: 8px 10px; } diff --git a/res/js/indexV2.charts.js b/res/js/indexV2.charts.js index 9b992c8..42a4730 100644 --- a/res/js/indexV2.charts.js +++ b/res/js/indexV2.charts.js @@ -79,7 +79,7 @@ const x = center - barWidth * 1.05 + seriesIndex * barWidth * 2.1; const y = margin.top + chartHeight - barHeight; return ` - + ${Math.round(value)} `; }).join(""); @@ -88,9 +88,9 @@ const statusText = project.created ? "已上线" : "未上线"; return ` ${rects} - - ${statusText} - ${shortName} + + ${statusText} + ${shortName} `; }).join(""); @@ -103,33 +103,21 @@ `; } - /** - * 集团趋势加权平均。 - * 按企业项目数加权汇总各企业七日趋势,避免项目少的企业对集团曲线影响过大。 - */ - function averageTrend(list, key) { - const active = list.filter((enterprise) => enterprise.stats[state.dimension].projectCount > 0); - return Array.from({ length: 7 }, (_, index) => { - if (!active.length) return 0; - const weighted = active.reduce((sum, enterprise) => { - const projects = enterprise.stats[state.dimension].projectCount; - return sum + enterprise.trend[key][index] * projects; - }, 0); - const total = active.reduce((sum, enterprise) => sum + enterprise.stats[state.dimension].projectCount, 0); - return weighted / total; - }); - } - /** * 集团三率趋势图。 * 先把企业趋势压成集团趋势,再复用通用折线图渲染方法。 */ function renderGroupTrendChart(list) { - return renderTrendChart({ - online: averageTrend(list, "online"), - usage: averageTrend(list, "usage"), - accuracy: averageTrend(list, "accuracy") - }, 1840, 112); + const trendCurve = projectMasterDataState.trendCurve; + if (trendCurve && Array.isArray(trendCurve.Date) && trendCurve.Date.length) { + return renderTrendChart({ + date: trendCurve.Date, + online: trendCurve.OnlineRate || [], + usage: trendCurve.UsageRate || [], + accuracy: trendCurve.AccuracyRate || [] + }, 1840, 112); + } + return '
暂无趋势数据
'; } /** @@ -140,7 +128,7 @@ const margin = { top: 14, right: 28, bottom: 28, left: 46 }; const chartWidth = width - margin.left - margin.right; const chartHeight = height - margin.top - margin.bottom; - const dates = ["04-21", "04-22", "04-23", "04-24", "04-25", "04-26", "04-27"]; + const dates = data.date || ["06-26", "06-27", "06-28", "06-29", "06-30", "07-01", "07-02"]; const series = [ { key: "online", label: "上线", color: COLORS.online }, { key: "usage", label: "使用", color: COLORS.usage }, diff --git a/res/js/indexV2.config.js b/res/js/indexV2.config.js index 766c383..111b8c8 100644 --- a/res/js/indexV2.config.js +++ b/res/js/indexV2.config.js @@ -1,15 +1,16 @@ /** * 看板全局配置模块。 - * 这里集中维护日期、外链、接口注册、兜底数据和运行时状态,其他 JS 模块只读取这些常量。 + * 这里集中维护日期、外链、接口注册和运行时状态,其他 JS 模块只读取这些常量。 * 新增业务接口时优先注册到 apiRegistry,再通过 core 模块的统一请求方法调用,避免在视图层散落接口地址和 token。 */ - const TODAY = "2026-04-27"; - const PREVIOUS_DAY = "2026-04-26"; + const TODAY = "2026-07-02"; + const PREVIOUS_DAY = "2026-07-01"; const INDICATOR_MORE_URL = "https://psecc.cncecoa.com/webroot/decision/v10/entry/access/e6251c34-31ea-4a35-8269-b83074bd9c64?preview=true"; const ENTERPRISE_INDICATOR_MORE_URL = "https://psecc.cncecoa.com/webroot/decision/view/report?viewlet=%252F%25E6%2595%25B0%25E7%25A7%2591%25E5%2585%25AC%25E5%258F%25B8%252F%25E5%25AE%2589%25E8%25B4%25A8%25E9%2583%25A8%25E6%2595%25B0%25E6%258D%25AE%25E6%25BA%25AF%25E6%25BA%2590%252F%25E5%25AE%2589%25E5%2585%25A8%25E6%2595%25B0%25E6%258D%25AE%25E5%25B1%2595%25E7%25A4%25BA_%25E4%25BC%2581%25E4%25B8%259A%25E7%25BA%25A7.cpt&op=view&__parameters__=%257B%2522__pi__%2522%253Atrue%252C%2522fine_hyperlink%2522%253A%2522ec19fc0a-9bfe-44f8-ba96-8402ba9b7bf2%2522%257D&_=1777453619793"; const GROUP_USAGE_REPORT_URL = "https://lygcgs.com.cn:8078/QHSEDataExChange/res/report/%E6%95%B0%E6%8D%AE%E6%B2%BB%E7%90%86%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%E5%8F%8A%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A.pdf"; const OFFICE_PREVIEW_BASE_URL = "https://view.officeapps.live.com/op/embed.aspx?src="; const REPORT_FILE_BASE_URL = "https://sggl.sedin.com.cn/res/report/"; + const DATA_GOVERNANCE_API_BASE = "/qhse_webapi/api/DataGovernance"; /** * 接口注册表。 * 当前集团项目上线率卡片和“集团项目上线统计”弹窗依赖 projectMasterDataAnalysis。 @@ -18,33 +19,83 @@ const apiRegistry = { projectMasterDataAnalysis: { method: "GET", - url: "https://qhse.cncecoa.com/qhse_webapi/api/Common/GetProjectMasterDataAnalysis?unitId=", + url: `${DATA_GOVERNANCE_API_BASE}/GetMetricMonitoringSituation`, + headers: { + token: "AF17168B-87BD-4GLY-1111-F0A0A1158F9B", + Accept: "*/*" + } + }, + groupMetricAnalysis: { + method: "GET", + url: `${DATA_GOVERNANCE_API_BASE}/GetGroupMetricAnalysis`, + headers: { + token: "AF17168B-87BD-4GLY-1111-F0A0A1158F9B", + Accept: "*/*" + } + }, + unitMetricMonitoring: { + method: "GET", + url: `${DATA_GOVERNANCE_API_BASE}/GetUnitMetricMonitoringSituation`, + headers: { + token: "AF17168B-87BD-4GLY-1111-F0A0A1158F9B", + Accept: "*/*" + } + }, + unitMetricAnalysis: { + method: "GET", + url: `${DATA_GOVERNANCE_API_BASE}/GetUnitMetricAnalysis`, + headers: { + token: "AF17168B-87BD-4GLY-1111-F0A0A1158F9B", + Accept: "*/*" + } + }, + projectMonitoring: { + method: "GET", + url: `${DATA_GOVERNANCE_API_BASE}/GetProjectMonitoringSituation`, headers: { token: "AF17168B-87BD-4GLY-1111-F0A0A1158F9B", Accept: "*/*" } } }; - /** - * 本地兜底表头和行数据。 - * 接口加载失败或跨域被拦截时仍展示旧 Excel 口径,避免主卡片和弹窗空白。 - */ - const usageReportHeaders = ["\u5e8f\u53f7", "\u5355\u4f4d", "\u4e3b\u6570\u636e\u9879\u76ee\u603b\u6570", "\u7533\u8bf7QHSE\u672a\u5efa\u9879\u76ee\u6570", "QHSE\u9879\u76ee\u603b\u6570\u6570", "QHSE\u4e0e\u4e3b\u6570\u636e\u5173\u8054\u9879\u76ee\u6570", "QHSE\u9879\u76ee\u5173\u8054\u4e0d\u5230\u4e3b\u6570\u636e\u9879\u76ee\u6570"]; - const projectMasterDataHeaders = ["序号", "单位", "主数据项目数", "申请不用项目数", "QHSE使用项目数", "已关联", "待建", "预立项", "上报日期"]; - const usageReportRows = [["1","\u5929\u8fb0","67","34","42","33","9"],["2","\u8d5b\u9f0e","33","3","52","30","22"],["3","\u4e1c\u534e","40","11","33","29","4"],["4","\u4e94\u73af","51","22","29","29","0"],["5","\u534e\u9646","38","4","34","34","0"],["6","\u6210\u8fbe","24","6","19","18","1"],["7","\u4e8c\u5316\u5efa","234","78","166","156","10"],["8","\u4e09\u5316\u5efa","167","36","151","131","20"],["9","\u56db\u5316\u5efa","117","46","84","71","13"],["10","\u516d\u5316\u5efa","263","90","199","173","26"],["11","\u4e03\u5316\u5efa","24","4","22","20","2"],["12","\u4e5d\u5316\u5efa","36","19","19","17","2"],["13","\u5341\u4e00\u5316\u5efa","69","17","57","52","5"],["14","\u5341\u4e09\u5316\u5efa","90","27","70","63","7"],["15","\u5341\u56db\u5316\u5efa","74","1","78","73","5"],["16","\u5341\u516d\u5316\u5efa","177","78","110","99","11"],["17","\u571f\u6728","42","11","33","31","2"],["18","\u88c5\u5907","14","5","9","9","0"],["19","\u534e\u8c0a","9","2","9","7","2"],["20","\u56fd\u9645","1","0","1","1","0"],["21","\u91cd\u673a","51","10","44","41","3"],["22","\u5317\u5efa\u6295","18","5","16","13","3"],["23","\u5357\u5efa\u6295","83","24","60","59","1"],["24","\u57ce\u6295","12","4","12","8","4"],["25","\u4ea4\u5efa","21","5","27","16","11"],["26","\u751f\u6001","31","0","31","31","0"],["27","\u897f\u5357","9","2","18","7","11"],["\u5408\u8ba1","","1795","544","1425","1251","174"]]; /** * 集团主数据接口运行态。 * rows 直接供弹窗表格读取,summary 供集团上线率卡片读取;二者必须成对更新。 - * loaded/error/loading 用于视图层展示“接口数据/兜底数据/加载中”的状态差异。 + * loaded/error/loading 用于视图层展示“接口数据/错误/加载中”的状态差异。 */ const projectMasterDataState = { loading: false, loaded: false, error: "", - rows: usageReportRows, + records: [], summary: null, + keyMetricDatas: [], + metricDatas: [], + metricFieldMap: {}, + trendCurve: null, + unitAnalysises: [], + projectAnalysises: [], + unitMetricUsages: [], + failProjects: [], + metricAnalysis: null, + metricAnalysisLoading: false, + unitId: "", updatedAt: "" }; + const unitMetricAnalysisState = { + loading: false, + error: "", + field: "", + unitId: "", + data: null + }; + const projectMonitoringState = { + loading: false, + error: "", + unitId: "", + projectId: "", + data: null + }; /** * 单位一览表文件名映射。 * 弹窗“预览”按钮只通过单位简称查表,不直接拼猜文件名,避免单位顺序或文件命名变化时打开错误附件。 @@ -64,176 +115,6 @@ muted: "#91b8d0" }; - /** - * 企业级模拟基础数据。 - * 企业/项目/指标多层页面都会从这里派生统计值;真实接口只接入集团主数据时,仍保留该数据支撑其他模块演示。 - */ - const enterprises = [ - { - name: "天辰公司", - owner: "张海", - stats: { - all: { projectCount: 40, onlineRate: 92, usageRate: 84, accuracyRate: 88, warningProjects: 4, warningItems: 39 }, - new: { projectCount: 7, onlineRate: 86, usageRate: 78, accuracyRate: 82, warningProjects: 2, warningItems: 8 } - }, - trend: { online: [88, 89, 90, 90, 91, 92, 92], usage: [78, 79, 80, 81, 82, 83, 84], accuracy: [84, 84, 85, 86, 86, 87, 88] } - }, - { - name: "赛鼎公司", - owner: "刘明", - stats: { - all: { projectCount: 28, onlineRate: 83, usageRate: 69, accuracyRate: 70, warningProjects: 6, warningItems: 39 }, - new: { projectCount: 6, onlineRate: 72, usageRate: 58, accuracyRate: 63, warningProjects: 4, warningItems: 12 } - }, - trend: { online: [80, 80, 81, 82, 82, 83, 83], usage: [64, 64, 65, 66, 67, 68, 69], accuracy: [73, 72, 72, 71, 70, 70, 70] } - }, - { - name: "东华公司", - owner: "王斌", - stats: { - all: { projectCount: 32, onlineRate: 88, usageRate: 76, accuracyRate: 79, warningProjects: 5, warningItems: 39 }, - new: { projectCount: 3, onlineRate: 85, usageRate: 67, accuracyRate: 72, warningProjects: 2, warningItems: 7 } - }, - trend: { online: [85, 85, 86, 86, 87, 88, 88], usage: [72, 72, 73, 74, 75, 76, 76], accuracy: [78, 78, 78, 79, 79, 79, 79] } - }, - { - name: "五环公司", - owner: "赵宁", - stats: { - all: { projectCount: 24, onlineRate: 86, usageRate: 72, accuracyRate: 75, warningProjects: 4, warningItems: 32 }, - new: { projectCount: 0, onlineRate: 0, usageRate: 0, accuracyRate: 0, warningProjects: 0, warningItems: 0 } - }, - trend: { online: [82, 83, 84, 85, 85, 86, 86], usage: [68, 69, 70, 70, 71, 72, 72], accuracy: [72, 73, 74, 74, 74, 75, 75] } - }, - { - name: "华陆公司", - owner: "陈哲", - stats: { - all: { projectCount: 25, onlineRate: 91, usageRate: 81, accuracyRate: 86, warningProjects: 3, warningItems: 28 }, - new: { projectCount: 2, onlineRate: 90, usageRate: 74, accuracyRate: 80, warningProjects: 1, warningItems: 4 } - }, - trend: { online: [89, 89, 90, 90, 91, 91, 91], usage: [76, 77, 78, 79, 80, 80, 81], accuracy: [83, 84, 84, 85, 85, 86, 86] } - }, - { - name: "成达公司", - owner: "李强", - stats: { - all: { projectCount: 13, onlineRate: 95, usageRate: 88, accuracyRate: 90, warningProjects: 1, warningItems: 16 }, - new: { projectCount: 0, onlineRate: 0, usageRate: 0, accuracyRate: 0, warningProjects: 0, warningItems: 0 } - }, - trend: { online: [93, 93, 94, 94, 95, 95, 95], usage: [84, 85, 86, 86, 87, 88, 88], accuracy: [88, 88, 89, 89, 90, 90, 90] } - }, - { - name: "二建", - owner: "周亮", - stats: { - all: { projectCount: 112, onlineRate: 84, usageRate: 73, accuracyRate: 80, warningProjects: 9, warningItems: 42 }, - new: { projectCount: 13, onlineRate: 79, usageRate: 66, accuracyRate: 70, warningProjects: 5, warningItems: 10 } - }, - trend: { online: [82, 82, 83, 83, 84, 84, 84], usage: [68, 69, 70, 71, 72, 73, 73], accuracy: [78, 78, 79, 79, 80, 80, 80] } - }, - { - name: "三建", - owner: "孙越", - stats: { - all: { projectCount: 130, onlineRate: 78, usageRate: 64, accuracyRate: 66, warningProjects: 14, warningItems: 48 }, - new: { projectCount: 12, onlineRate: 75, usageRate: 58, accuracyRate: 61, warningProjects: 7, warningItems: 13 } - }, - trend: { online: [75, 75, 76, 76, 77, 78, 78], usage: [61, 61, 62, 62, 63, 64, 64], accuracy: [68, 67, 67, 66, 66, 66, 66] } - }, - { - name: "四建", - owner: "马骏", - stats: { - all: { projectCount: 71, onlineRate: 87, usageRate: 78, accuracyRate: 82, warningProjects: 6, warningItems: 24 }, - new: { projectCount: 5, onlineRate: 82, usageRate: 74, accuracyRate: 76, warningProjects: 2, warningItems: 6 } - }, - trend: { online: [85, 85, 86, 86, 87, 87, 87], usage: [74, 75, 76, 76, 77, 78, 78], accuracy: [79, 80, 80, 81, 81, 82, 82] } - }, - { - name: "六建", - owner: "韩伟", - stats: { - all: { projectCount: 192, onlineRate: 84, usageRate: 61, accuracyRate: 60, warningProjects: 22, warningItems: 55 }, - new: { projectCount: 22, onlineRate: 78, usageRate: 52, accuracyRate: 56, warningProjects: 10, warningItems: 18 } - }, - trend: { online: [81, 82, 82, 83, 83, 84, 84], usage: [58, 58, 59, 60, 60, 61, 61], accuracy: [63, 62, 62, 61, 61, 60, 60] } - }, - { - name: "十一建", - owner: "郑一", - stats: { - all: { projectCount: 62, onlineRate: 89, usageRate: 77, accuracyRate: 78, warningProjects: 5, warningItems: 21 }, - new: { projectCount: 5, onlineRate: 84, usageRate: 70, accuracyRate: 70, warningProjects: 2, warningItems: 5 } - }, - trend: { online: [86, 87, 87, 88, 88, 89, 89], usage: [73, 74, 75, 76, 76, 77, 77], accuracy: [76, 76, 77, 77, 78, 78, 78] } - }, - { - name: "十四建", - owner: "沈涛", - stats: { - all: { projectCount: 64, onlineRate: 82, usageRate: 68, accuracyRate: 69, warningProjects: 8, warningItems: 35 }, - new: { projectCount: 9, onlineRate: 76, usageRate: 57, accuracyRate: 62, warningProjects: 5, warningItems: 9 } - }, - trend: { online: [80, 80, 81, 81, 82, 82, 82], usage: [65, 65, 66, 67, 67, 68, 68], accuracy: [72, 71, 70, 70, 69, 69, 69] } - }, - { - name: "十六建", - owner: "蒋峰", - stats: { - all: { projectCount: 81, onlineRate: 85, usageRate: 71, accuracyRate: 73, warningProjects: 7, warningItems: 31 }, - new: { projectCount: 6, onlineRate: 80, usageRate: 65, accuracyRate: 67, warningProjects: 3, warningItems: 6 } - }, - trend: { online: [82, 83, 83, 84, 84, 85, 85], usage: [67, 68, 69, 69, 70, 71, 71], accuracy: [75, 74, 74, 73, 73, 73, 73] } - }, - { - name: "装备科技", - owner: "何平", - stats: { - all: { projectCount: 1, onlineRate: 100, usageRate: 100, accuracyRate: 100, warningProjects: 0, warningItems: 2 }, - new: { projectCount: 0, onlineRate: 0, usageRate: 0, accuracyRate: 0, warningProjects: 0, warningItems: 0 } - }, - trend: { online: [100, 100, 100, 100, 100, 100, 100], usage: [100, 100, 100, 100, 100, 100, 100], accuracy: [100, 100, 100, 100, 100, 100, 100] } - }, - { - name: "南投", - owner: "梁旭", - stats: { - all: { projectCount: 29, onlineRate: 74, usageRate: 55, accuracyRate: 66, warningProjects: 6, warningItems: 18 }, - new: { projectCount: 2, onlineRate: 70, usageRate: 50, accuracyRate: 55, warningProjects: 1, warningItems: 3 } - }, - trend: { online: [72, 72, 73, 73, 74, 74, 74], usage: [53, 53, 54, 54, 55, 55, 55], accuracy: [66, 66, 66, 66, 66, 66, 66] } - }, - { - name: "交建", - owner: "秦毅", - stats: { - all: { projectCount: 26, onlineRate: 80, usageRate: 69, accuracyRate: 72, warningProjects: 4, warningItems: 17 }, - new: { projectCount: 2, onlineRate: 75, usageRate: 60, accuracyRate: 62, warningProjects: 1, warningItems: 2 } - }, - trend: { online: [78, 78, 79, 79, 80, 80, 80], usage: [66, 67, 67, 68, 68, 69, 69], accuracy: [71, 71, 72, 72, 72, 72, 72] } - }, - { - name: "生态", - owner: "郭晨", - stats: { - all: { projectCount: 17, onlineRate: 76, usageRate: 62, accuracyRate: 64, warningProjects: 5, warningItems: 22 }, - new: { projectCount: 1, onlineRate: 70, usageRate: 55, accuracyRate: 55, warningProjects: 1, warningItems: 3 } - }, - trend: { online: [73, 74, 74, 75, 75, 76, 76], usage: [59, 60, 60, 61, 61, 62, 62], accuracy: [66, 65, 65, 64, 64, 64, 64] } - } - ]; - - /** - * 当前准确率口径尚未接入正式数据,统一置 0,避免不同层级出现不一致的模拟准确率。 - */ - enterprises.forEach((enterprise) => { - ["all", "new"].forEach((dimension) => { - enterprise.stats[dimension].accuracyRate = 0; - }); - enterprise.trend.accuracy = [0, 0, 0, 0, 0, 0, 0]; - }); - /** * 系统页签配置。 * URL 参数 type=1/2/3 会映射到 safety/quality/emergency,并影响可见页签与指标库。 @@ -267,97 +148,6 @@ const allowedSystems = getAllowedSystemsFromUrl(); - /** - * 各业务系统的重点指标池。 - * 集团页指标卡、指标分析面板、指标穿透弹窗都从当前系统对应数组取数。 - */ - const warningIndicatorMap = { - safety: [ - { name: "专职安全管理人员(含分包)总数", projects: 150, enterprises: 18, usageRate: 72 }, - { name: "特种作业人员数量", projects: 138, enterprises: 16, usageRate: 74 }, - { name: "危大工程进行中个数", projects: 121, enterprises: 15, usageRate: 69 }, - { name: "重大风险", projects: 116, enterprises: 14, usageRate: 76 }, - { name: "较大风险", projects: 104, enterprises: 13, usageRate: 68 }, - { name: "高风险作业许可进行中数量", projects: 98, enterprises: 12, usageRate: 71 }, - { name: "专项培训", projects: 92, enterprises: 12, usageRate: 66 }, - { name: "三级安全教育培训人次", projects: 86, enterprises: 11, usageRate: 70 } - ], - quality: [ - { name: "项目质量管理人数", projects: 136, enterprises: 17, usageRate: 70 }, - { name: "人员报验数", projects: 128, enterprises: 16, usageRate: 68 }, - { name: "培训人次", projects: 112, enterprises: 15, usageRate: 73 }, - { name: "技术交底人次", projects: 105, enterprises: 14, usageRate: 71 }, - { name: "企业及分支机构检查数", projects: 96, enterprises: 13, usageRate: 65 }, - { name: "项目质量检查数", projects: 91, enterprises: 12, usageRate: 69 }, - { name: "施工方案企业审批总数", projects: 83, enterprises: 11, usageRate: 63 }, - { name: "施工方案项目审批总数", projects: 80, enterprises: 11, usageRate: 66 } - ], - emergency: [ - { name: "应急预案备案", projects: 118, enterprises: 16, usageRate: 69 }, - { name: "应急演练记录", projects: 111, enterprises: 15, usageRate: 64 }, - { name: "应急物资台账", projects: 103, enterprises: 14, usageRate: 67 }, - { name: "应急队伍配置", projects: 96, enterprises: 13, usageRate: 70 }, - { name: "重大风险告警", projects: 88, enterprises: 12, usageRate: 62 }, - { name: "专项应急培训", projects: 81, enterprises: 11, usageRate: 66 }, - { name: "应急联络人员", projects: 76, enterprises: 10, usageRate: 71 }, - { name: "现场处置方案", projects: 72, enterprises: 10, usageRate: 63 }, - { name: "应急响应记录", projects: 66, enterprises: 9, usageRate: 60 }, - { name: "复盘整改闭环", projects: 61, enterprises: 8, usageRate: 58 } - ] - }; - - /** - * 项目明细页字段模板。 - * buildIndicatorRows 会结合项目使用率和滞后天数生成“是否启用、三日值、是否预警”等派生字段。 - */ - const indicatorTemplates = [ - { name: "预警项数", field: "warningCount", base: 48 }, - { name: "参建人数", field: "participants", base: 215 }, - { name: "在施危大工程数", field: "dangerWorks", base: 3 }, - { name: "安全技术交底数", field: "safetyDisclosure", base: 69 }, - { name: "三级安全教育培训数", field: "safetyTraining", base: 555 }, - { name: "专项培训数", field: "specialTraining", base: 25 }, - { name: "特种作业培训数", field: "specialWorkTraining", base: 3 }, - { name: "项目总监人数", field: "directorCount", base: 1 }, - { name: "项目专职人数", field: "fulltimeCount", base: 5 }, - { name: "项目安全监护人数", field: "guardianCount", base: 2 }, - { name: "在岗执业注册师", field: "registeredEngineer", base: 4 }, - { name: "A证人员", field: "certA", base: 1 }, - { name: "B证人员", field: "certB", base: 2 }, - { name: "C证人员", field: "certC", base: 4 }, - { name: "特种作业人员", field: "specialWorkers", base: 57 }, - { name: "项目安全领导小组会议数", field: "safetyMeeting", base: 2 }, - { name: "项目安全例会数", field: "regularMeeting", base: 21 }, - { name: "项目负责人带班检查次数", field: "leaderInspection", base: 3 }, - { name: "项目专项检查次数", field: "specialInspection", base: 46 }, - { name: "项目专业检查次数", field: "professionInspection", base: 132 } - ]; - - /** - * 项目名称池。 - * generateProjects 用企业简称加这些后缀生成稳定项目名,保证同一企业在不同渲染中项目顺序一致。 - */ - const projectNamePool = [ - "精细化工材料项目", - "煤炭深加工基地项目", - "全场系统工程", - "新材料园区工程", - "装置检修项目", - "安全提升改造工程", - "数字化施工项目", - "生产运行保障项目", - "罐区扩能工程", - "绿色低碳改造项目", - "配套管廊工程", - "污水处理优化项目", - "储运系统升级项目", - "消防能力提升项目", - "基础设施完善项目", - "智能工地试点项目", - "综合楼建设项目", - "设备更新项目" - ]; - /** * 单页看板运行时状态。 * view 控制主区域层级,drillOpen/drillView 控制弹窗层级;事件模块只改 state,再统一触发 render。 diff --git a/res/js/indexV2.core.js b/res/js/indexV2.core.js index 2f4c93b..10dc414 100644 --- a/res/js/indexV2.core.js +++ b/res/js/indexV2.core.js @@ -8,6 +8,7 @@ const drillModal = document.getElementById("drillModal"); const drillModalBody = document.getElementById("drillModalBody"); const drillModalTitle = document.getElementById("drillModalTitle"); + const dataDateText = document.getElementById("dataDateText"); /** * 根据 URL 参数过滤系统页签。 @@ -50,11 +51,31 @@ return Number(value).toLocaleString("zh-CN"); } + function fmtFixedPercent(value, digits = 2) { + return `${Number(value).toFixed(digits)}%`; + } + function toSafeNumber(value) { const number = Number(value); return Number.isFinite(number) ? number : 0; } + function escapeHtmlAttr(value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(//g, ">"); + } + + function getObjectValueIgnoreCase(item, keys) { + if (!item || typeof item !== "object") return ""; + const entries = Object.entries(item); + const normalizedKeys = keys.map((key) => key.toLowerCase()); + const matched = entries.find(([key]) => normalizedKeys.includes(key.toLowerCase())); + return matched ? matched[1] : ""; + } + function formatApiDate(value) { if (!value) return ""; const date = new Date(value); @@ -78,24 +99,40 @@ reportDate: item.ReportDate || item.reportDate || "", dataDate: item.DataDate || item.dataDate || "", total: toSafeNumber(item.Total ?? item.total), - masterTotal: toSafeNumber(item.MasterTotal ?? item.masterTotal), - qhseTotal: toSafeNumber(item.QHSETotal ?? item.qhseTotal), + onlineAutoTotal: toSafeNumber(item.YiShangXian ?? item.yiShangXian), + onlineReportedTotal: toSafeNumber(item.TianBao ?? item.tianBao), + masterProjectCount: toSafeNumber(item.MasterTotal ?? item.masterTotal ?? item.masterProjectCount), + qhseProjectCount: toSafeNumber(item.QHSETotal ?? item.qhseTotal ?? item.qhseProjectCount), linkedTotal: toSafeNumber(item.ZhengChang ?? item.zhengChang), pendingTotal: toSafeNumber(item.DaiJian ?? item.daiJian), noNeedTotal: toSafeNumber(item.BuYong ?? item.buYong), - preProjectTotal: toSafeNumber(item.YuLiXiang ?? item.yuLiXiang) + noNeedCompletedTotal: toSafeNumber(item.BuYongYiWanGong ?? item.buYongYiWanGong), + noNeedStoppedTotal: toSafeNumber(item.BuYongTingHuanJian ?? item.buYongTingHuanJian), + noNeedRestrictedTotal: toSafeNumber(item.BuYongTJShouXian ?? item.buYongTJShouXian), + preProjectTotal: toSafeNumber(item.YuLiXiang ?? item.yuLiXiang), + onlineRate: toSafeNumber(item.OnlinRate ?? item.OnlineRate ?? item.onlinRate ?? item.onlineRate), + webUrl: getObjectValueIgnoreCase(item, ["WebUrl", "WebURL", "webUrl", "webURL", "web_url", "Url", "URL"]) }; } + function sameProjectMasterUnit(left, right) { + const normalize = (value) => String(value || "") + .replace(/公司|工程|化建/g, "") + .trim(); + const a = normalize(left); + const b = normalize(right); + return a && b && (a === b || a.includes(b) || b.includes(a)); + } + /** * 汇总接口返回的单位行。 * 结果同时服务集团上线率卡片和弹窗合计行,所以这里保留主数据、QHSE、申请不用、已关联等完整口径。 */ function summarizeProjectMasterData(rows) { return rows.reduce((summary, row) => { - summary.masterProjectCount += row.masterTotal; + summary.masterProjectCount += row.masterProjectCount; summary.noNeedProjectCount += row.noNeedTotal; - summary.qhseProjectCount += row.qhseTotal; + summary.qhseProjectCount += row.qhseProjectCount; summary.linkedTotal += row.linkedTotal; summary.pendingTotal += row.pendingTotal; summary.preProjectTotal += row.preProjectTotal; @@ -113,61 +150,272 @@ } /** - * 把接口对象转换为弹窗表格二维数组。 - * 视图层按数组渲染表格,最后追加“合计”行,保持接口数据和本地兜底数据结构一致。 + * 上线率口径:已关联 + 预立项 / 主数据 + 预立项 - 申请不用。 + * 分母为 0 时兜底使用 Total,避免接口单行主数据异常时显示 NaN。 */ - function buildProjectMasterRows(rows) { - const sortedRows = [...rows].sort((a, b) => a.sortIndex - b.sortIndex); - const bodyRows = sortedRows.map((row, index) => [ - String(index + 1), - row.name, - String(row.masterTotal), - String(row.noNeedTotal), - String(row.qhseTotal), - String(row.linkedTotal), - String(row.pendingTotal), - String(row.preProjectTotal), - formatApiDate(row.reportDate) - ]); - const summary = summarizeProjectMasterData(sortedRows); - bodyRows.push([ - "合计", - "", - String(summary.masterProjectCount), - String(summary.noNeedProjectCount), - String(summary.qhseProjectCount), - String(summary.linkedTotal), - String(summary.pendingTotal), - String(summary.preProjectTotal), - "" - ]); - return bodyRows; + function projectMasterOnlineRate(row) { + const numerator = toSafeNumber(row.linkedTotal) + toSafeNumber(row.preProjectTotal); + const denominator = toSafeNumber(row.masterProjectCount) + toSafeNumber(row.preProjectTotal) - toSafeNumber(row.noNeedTotal); + const fallbackDenominator = toSafeNumber(row.total); + const base = denominator || fallbackDenominator; + return base ? numerator / base * 100 : 0; + } + + function parseProjectMasterProInfo(value) { + const parts = String(value || "") + .split("/") + .map((item) => toSafeNumber(item)); + if (parts.length < 3) return {}; + return { + proInfoText: String(value || ""), + onlineActual: parts[0], + noNeedProjectCount: parts[1], + onlineExpected: parts[2], + projectCount: parts[3] || parts[2], + masterProjectCount: parts[2], + qhseProjectCount: parts[0] + }; + } + + function buildProjectMasterSummary(summary, apiData = {}) { + const proInfo = parseProjectMasterProInfo(apiData.ProInfo ?? apiData.proInfo); + const onlineExpected = proInfo.onlineExpected || summary.total || summary.masterProjectCount; + const onlineActual = proInfo.onlineActual || summary.linkedTotal + summary.preProjectTotal || summary.qhseProjectCount; + return { + masterProjectCount: proInfo.masterProjectCount || onlineExpected, + noNeedProjectCount: proInfo.noNeedProjectCount || summary.noNeedProjectCount, + qhseProjectCount: proInfo.qhseProjectCount || onlineActual, + onlineActual, + onlineExpected, + onlineRate: toSafeNumber(apiData.OnlineRate ?? apiData.onlineRate) || (onlineExpected ? onlineActual / onlineExpected * 100 : 0), + usageRate: toSafeNumber(apiData.UsageRate ?? apiData.usageRate), + accuracyRate: toSafeNumber(apiData.AccuracyRate ?? apiData.accuracyRate), + preProjectTotal: toSafeNumber(apiData.YuLiXiang ?? apiData.yuLiXiang) || summary.preProjectTotal, + pendingTotal: toSafeNumber(apiData.DaiJian ?? apiData.daiJian) || summary.pendingTotal, + failProjectNum: toSafeNumber(apiData.FailProjectNum ?? apiData.failProjectNum), + projectCount: proInfo.projectCount || summary.total, + proInfoText: proInfo.proInfoText || `${onlineActual}/${proInfo.noNeedProjectCount || summary.noNeedProjectCount}/${onlineExpected}`, + dataDate: apiData.DataDate || apiData.dataDate || "", + total: summary.total + }; + } + + function updateDataDateDisplay() { + if (dataDateText) { + dataDateText.textContent = getProjectMasterSummary().dataDate || "--"; + } + } + + function apiMetricType() { + if (state.system === "quality") return 1; + return 0; + } + + function apiRange() { + return state.dimension === "new" ? 1 : 0; + } + + function normalizeMetricData(item, index = 0) { + return { + name: item.Name || item.name || "", + field: item.Field || item.field || item.Filed || item.filed || "", + projects: toSafeNumber(item.ProjectNum ?? item.projectNum), + projectUsageNum: toSafeNumber(item.ProjectUsageNum ?? item.projectUsageNum), + usageRate: toSafeNumber(item.UsageRate ?? item.usageRate), + frequency: toSafeNumber(item.Frequency ?? item.frequency), + accuracyRate: toSafeNumber(item.ExactRate ?? item.exactRate ?? item.AccuracyRate ?? item.accuracyRate), + sortIndex: toSafeNumber(item.SortIndex ?? item.sortIndex ?? index + 1) + }; + } + + function normalizeMetricName(value) { + return String(value || "").trim(); + } + + function buildMetricFieldMap(...metricGroups) { + return metricGroups.flat().reduce((map, item) => { + const name = normalizeMetricName(item?.name); + const field = item?.field || ""; + if (name && field && !map[name]) { + map[name] = field; + } + return map; + }, {}); + } + + function getAllMonitoringMetrics() { + return [ + ...(projectMasterDataState.keyMetricDatas || []), + ...(projectMasterDataState.metricDatas || []) + ]; + } + + function getMetricByName(name) { + const key = normalizeMetricName(name); + if (!key) return null; + return getAllMonitoringMetrics().find((item) => normalizeMetricName(item.name) === key) || null; + } + + function resolveMetricField(name, fallbackField = "") { + const key = normalizeMetricName(name); + if (!key) return fallbackField || ""; + return fallbackField + || projectMasterDataState.metricFieldMap[key] + || getMetricByName(key)?.field + || ""; + } + + function normalizeFailProject(item, index = 0) { + const usageRate = toSafeNumber(item.UsageRate ?? item.usageRate); + return { + unitId: item.UnitId || item.unitId || "", + projectId: item.ProjectId || item.projectId || `fail-${index}`, + unitName: item.UnitName || item.unitName || "", + abbreviation: item.Abbreviation || item.abbreviation || "", + sortIndex: toSafeNumber(item.SortIndex ?? item.sortIndex ?? index + 1), + projectName: item.ProjectName || item.projectName || "", + shortName: item.ShortName || item.shortName || "", + usage: item.Usage || item.usage || "", + usageRate + }; + } + + function normalizeUnitMetricUsage(item, index = 0) { + return { + unitId: item.UnitId || item.unitId || "", + unitName: item.UnitName || item.unitName || "", + abbreviation: item.Abbreviation || item.abbreviation || "", + qhseTotal: toSafeNumber(item.QHSETotal ?? item.qhseTotal), + activeNum: toSafeNumber(item.ActiveNum ?? item.activeNum), + usageRate: toSafeNumber(item.UsageRate ?? item.usageRate), + date: item.Date || item.date || "", + sortIndex: toSafeNumber(item.SortIndex ?? item.sortIndex ?? index + 1) + }; + } + + function normalizeUnitAnalysis(item, index = 0) { + return { + unitId: item.UnitId || item.unitId || "", + name: item.Name || item.name || "", + onlineRate: toSafeNumber(item.OnlineRate ?? item.onlineRate), + usageRate: toSafeNumber(item.UsageRate ?? item.usageRate), + accuracyRate: toSafeNumber(item.AccuracyRate ?? item.accuracyRate), + sortIndex: toSafeNumber(item.SortIndex ?? item.sortIndex ?? index + 1) + }; + } + + function normalizeProjectAnalysis(item, index = 0) { + return { + unitId: item.UnitId || item.unitId || "", + projectId: item.ProjectId || item.projectId || `project-analysis-${index}`, + name: item.Name || item.name || "", + isOnline: Boolean(item.IsOnline ?? item.isOnline), + usageRate: toSafeNumber(item.UsageRate ?? item.usageRate), + accuracyRate: toSafeNumber(item.AccuracyRate ?? item.accuracyRate), + sortIndex: toSafeNumber(item.SortIndex ?? item.sortIndex ?? index + 1) + }; } /** * 读取弹窗表格行。 - * 接口尚未返回或加载失败时直接回退本地 usageReportRows,保证弹窗始终有可展示数据。 + * 只读取接口返回的集团上线统计数据。 */ - function getProjectMasterRows() { - return projectMasterDataState.rows && projectMasterDataState.rows.length - ? projectMasterDataState.rows - : usageReportRows; + function getProjectMasterRecords() { + return projectMasterDataState.records || []; + } + + function resolveProjectMasterWebUrl(row) { + return row && /^https?:\/\//i.test(row.webUrl || "") ? row.webUrl : ""; + } + + function getUnitMetricUsageRecords() { + return (projectMasterDataState.unitMetricUsages || []) + .slice() + .sort((a, b) => a.sortIndex - b.sortIndex); + } + + function resolveUnitMetricUsageWebUrl(row) { + const masterRecord = getProjectMasterRecords().find((item) => { + const sameUnitId = row.unitId && item.unitId && String(item.unitId).toLowerCase() === String(row.unitId).toLowerCase(); + return sameUnitId || sameProjectMasterUnit(item.name, row.abbreviation || row.unitName); + }); + return resolveProjectMasterWebUrl(masterRecord || { name: row.abbreviation || row.unitName }); } /** * 读取集团上线率卡片所需的三个核心数。 - * 接口成功时优先用 summary;失败时从兜底表的“合计”行读取,兜底表缺失时再用固定默认值。 + * 接口未返回时使用全零空状态,不提供业务数据兜底。 */ function getProjectMasterSummary() { - if (projectMasterDataState.summary) return projectMasterDataState.summary; - const totalRow = getProjectMasterRows().find((row) => row[0] === "合计") || []; - return { - masterProjectCount: toSafeNumber(totalRow[2]) || 1795, - noNeedProjectCount: toSafeNumber(totalRow[3]) || 544, - qhseProjectCount: toSafeNumber(totalRow[4]) || 1425 + return projectMasterDataState.summary || { + masterProjectCount: 0, + noNeedProjectCount: 0, + qhseProjectCount: 0, + onlineActual: 0, + onlineExpected: 0, + onlineRate: 0, + usageRate: 0, + accuracyRate: 0, + preProjectTotal: 0, + pendingTotal: 0, + failProjectNum: 0, + projectCount: 0, + proInfoText: "-/-/-/-", + dataDate: "", + total: 0 }; } + function getMonitoringKeyMetrics() { + return projectMasterDataState.keyMetricDatas && projectMasterDataState.keyMetricDatas.length + ? projectMasterDataState.keyMetricDatas + : null; + } + + function getEnterpriseMasterRecord(enterpriseName) { + const aliases = { + 天辰公司: "天辰", + 赛鼎公司: "赛鼎", + 东华公司: "东华", + 五环公司: "五环", + 华陆公司: "华陆", + 成达公司: "成达" + }; + const key = aliases[enterpriseName] || enterpriseName; + return getProjectMasterRecords().find((row) => sameProjectMasterUnit(row.name, key)) || null; + } + + function getBelowStandardProjects(enterprise = null) { + if (projectMasterDataState.failProjects && projectMasterDataState.failProjects.length) { + const enterpriseKey = enterprise ? enterprise.name : ""; + return projectMasterDataState.failProjects + .filter((item) => !enterprise || sameProjectMasterUnit(item.abbreviation || item.unitName, enterpriseKey)) + .sort((a, b) => a.sortIndex - b.sortIndex) + .map((item) => ({ + enterprise: getEnterpriseByUnitName(item.abbreviation || item.unitName), + project: { + id: item.projectId, + unitId: item.unitId, + unit: item.abbreviation || item.unitName, + name: item.projectName || item.shortName, + usageRate: item.usageRate, + accuracyRate: 0, + created: true, + lastUpdate: TODAY, + owner: item.abbreviation || item.unitName, + usage: item.usage, + systemStatus: "已上线" + } + })); + } + return []; + } + + function getEnterpriseByUnitName(name) { + const analysis = (projectMasterDataState.unitAnalysises || []).find((item) => sameProjectMasterUnit(item.name, name)); + return analysis ? { name: analysis.name, unitId: analysis.unitId } : { name: name || "未知单位", unitId: "" }; + } + /** * 从接口注册表生成可请求配置。 * 所有查询参数统一在这里拼接,调用方只传业务参数,避免重复处理空值和 URL 编码。 @@ -175,9 +423,10 @@ function getRegisteredApiConfig(name, params = {}) { const api = apiRegistry[name]; if (!api) throw new Error(`未注册接口:${name}`); - const url = new URL(api.url); + const url = new URL(api.url, window.location.origin); Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null && value !== "") { + const shouldKeepEmptyValue = name === "projectMasterDataAnalysis" && key === "unitId"; + if (value !== undefined && value !== null && (value !== "" || shouldKeepEmptyValue)) { url.searchParams.set(key, value); } }); @@ -190,7 +439,7 @@ /** * 统一接口请求入口。 - * 使用 fetch 直接请求已注册接口;如果浏览器跨域或后端异常,会抛给上层进入兜底流程。 + * 使用同源路径请求已注册接口;本地开发由 server.js 代理到正式接口。 */ async function requestRegisteredApi(name, params = {}) { const config = getRegisteredApiConfig(name, params); @@ -207,31 +456,54 @@ /** * 加载集团主数据在建项目分析接口。 - * 成功后更新 projectMasterDataState.rows/summary 并触发整页重渲染; - * 失败时记录错误并恢复本地兜底数据,让主卡片和统计弹窗仍可用。 + * 成功后更新 projectMasterDataState.rows/summary 并触发整页重渲染;失败时清空接口状态并展示错误。 */ async function loadProjectMasterDataAnalysis(unitId = "") { projectMasterDataState.loading = true; projectMasterDataState.error = ""; + if (unitId) { + projectMasterDataState.projectAnalysises = []; + projectMasterDataState.failProjects = []; + projectMasterDataState.keyMetricDatas = []; + projectMasterDataState.metricDatas = []; + projectMasterDataState.metricFieldMap = {}; + } + render(); try { - const result = await requestRegisteredApi("projectMasterDataAnalysis", { unitId }); - if (toSafeNumber(result.code) !== 1 || !Array.isArray(result.data)) { + const apiName = unitId ? "unitMetricMonitoring" : "projectMasterDataAnalysis"; + const result = await requestRegisteredApi(apiName, { type: apiMetricType(), range: apiRange(), unitId }); + const apiData = result.data || {}; + const apiRows = Array.isArray(apiData) ? apiData : apiData.MasterDataAnalysiss; + if (toSafeNumber(result.code) !== 1 || !Array.isArray(apiRows)) { throw new Error(result.message || "接口返回数据格式异常"); } - const normalizedRows = result.data.map(normalizeProjectMasterData).filter((row) => row.name); + const normalizedRows = apiRows.map(normalizeProjectMasterData).filter((row) => row.name); const summary = summarizeProjectMasterData(normalizedRows); - projectMasterDataState.rows = buildProjectMasterRows(normalizedRows); - projectMasterDataState.summary = { - masterProjectCount: summary.masterProjectCount, - noNeedProjectCount: summary.noNeedProjectCount, - qhseProjectCount: summary.qhseProjectCount - }; + projectMasterDataState.records = normalizedRows; + projectMasterDataState.summary = buildProjectMasterSummary(summary, Array.isArray(apiData) ? {} : apiData); + projectMasterDataState.keyMetricDatas = (apiData.KeyMetricDatas || []).map(normalizeMetricData).filter((item) => item.name); + projectMasterDataState.metricDatas = (apiData.MetricDatas || []).map(normalizeMetricData).filter((item) => item.name); + projectMasterDataState.metricFieldMap = buildMetricFieldMap(projectMasterDataState.keyMetricDatas, projectMasterDataState.metricDatas); + projectMasterDataState.trendCurve = apiData.TrendCurve || null; + projectMasterDataState.unitAnalysises = (apiData.UnitAnalysises || []).map(normalizeUnitAnalysis).filter((item) => item.name); + projectMasterDataState.projectAnalysises = (apiData.ProjectAnalysises || []).map(normalizeProjectAnalysis).filter((item) => item.name); + projectMasterDataState.unitMetricUsages = (apiData.UnitMetricUsages || []).map(normalizeUnitMetricUsage).filter((item) => item.abbreviation || item.unitName); + projectMasterDataState.failProjects = (apiData.FailProjects || []).map(normalizeFailProject).filter((item) => item.projectName || item.shortName); + projectMasterDataState.unitId = unitId || ""; projectMasterDataState.loaded = true; projectMasterDataState.updatedAt = new Date().toISOString(); } catch (error) { projectMasterDataState.error = error.message || "接口请求失败"; projectMasterDataState.summary = null; - projectMasterDataState.rows = usageReportRows; + projectMasterDataState.records = []; + projectMasterDataState.keyMetricDatas = []; + projectMasterDataState.metricDatas = []; + projectMasterDataState.metricFieldMap = {}; + projectMasterDataState.unitMetricUsages = []; + projectMasterDataState.trendCurve = null; + projectMasterDataState.unitAnalysises = []; + projectMasterDataState.projectAnalysises = []; + projectMasterDataState.failProjects = []; console.error("集团项目上线统计接口请求失败", error); } finally { projectMasterDataState.loading = false; @@ -239,6 +511,78 @@ render(); } + async function loadGroupMetricAnalysis(field) { + if (!field) return; + projectMasterDataState.metricAnalysisLoading = true; + try { + const result = await requestRegisteredApi("groupMetricAnalysis", { type: apiMetricType(), range: apiRange(), filed: field }); + if (toSafeNumber(result.code) !== 1 || !result.data) { + throw new Error(result.message || "指标穿透接口返回数据格式异常"); + } + projectMasterDataState.metricAnalysis = { field, data: result.data }; + } catch (error) { + console.error("集团指标穿透接口请求失败", error); + } finally { + projectMasterDataState.metricAnalysisLoading = false; + } + renderDrillModal(); + } + + async function loadUnitMetricAnalysis(field, unitId) { + if (!field || !unitId) return; + unitMetricAnalysisState.loading = true; + unitMetricAnalysisState.error = ""; + unitMetricAnalysisState.field = field; + unitMetricAnalysisState.unitId = unitId; + unitMetricAnalysisState.data = null; + renderDrillModal(); + try { + const result = await requestRegisteredApi("unitMetricAnalysis", { + type: apiMetricType(), + range: apiRange(), + filed: field, + unitId + }); + if (toSafeNumber(result.code) !== 1 || !result.data) { + throw new Error(result.message || "企业指标穿透接口返回数据格式异常"); + } + unitMetricAnalysisState.data = result.data; + } catch (error) { + unitMetricAnalysisState.error = error.message || "接口请求失败"; + console.error("企业指标穿透接口请求失败", error); + } finally { + unitMetricAnalysisState.loading = false; + } + renderDrillModal(); + } + + async function loadProjectMonitoring(unitId, projectId) { + if (!unitId || !projectId) return; + projectMonitoringState.loading = true; + projectMonitoringState.error = ""; + projectMonitoringState.unitId = unitId; + projectMonitoringState.projectId = projectId; + projectMonitoringState.data = null; + render(); + try { + const result = await requestRegisteredApi("projectMonitoring", { + type: apiMetricType(), + unitId, + projectId + }); + if (toSafeNumber(result.code) !== 1 || !result.data) { + throw new Error(result.message || "项目监控接口返回数据格式异常"); + } + projectMonitoringState.data = result.data; + } catch (error) { + projectMonitoringState.error = error.message || "接口请求失败"; + console.error("项目监控接口请求失败", error); + } finally { + projectMonitoringState.loading = false; + } + render(); + } + function dimLabel() { return state.dimension === "all" ? "所有项目" : "新开工项目"; } @@ -248,39 +592,65 @@ } function getWarningIndicators() { - return warningIndicatorMap[state.system] || warningIndicatorMap.safety; + const keyMetrics = getMonitoringKeyMetrics(); + return keyMetrics || []; } function getSelectedIndicator() { const indicators = getWarningIndicators(); - return indicators.find((item) => item.name === state.selectedIndicator) || indicators[0]; - } - - function indicatorAccuracy(item) { - return item.accuracyRate ?? 0; + return getMetricByName(state.selectedIndicator) || indicators[0] || { name: "指标", field: "", projects: 0, usageRate: 0, accuracyRate: 0 }; } /** - * 生成“指标-企业”维度的穿透数据。 - * 当前没有企业级指标接口时,用企业三率和指标基础使用率加权生成稳定样本,供排行和预警数量展示。 + * 统一弹窗渲染入口。 + * state.drillView 决定展示集团上线统计、企业项目清单、指标项目穿透或指标趋势; + * 关闭弹窗时清空 body,避免旧内容残留到下一次打开。 + */ + function renderDrillModal() { + if (!state.drillOpen) { + drillModal.hidden = true; + drillModalBody.innerHTML = ""; + return; + } + const indicator = getSelectedIndicator(); + drillModal.hidden = false; + if (state.drillView === "groupOnlineStats") { + drillModalTitle.textContent = "集团项目上线统计"; + drillModalBody.innerHTML = renderGroupOnlineStatsModal(); + return; + } + if (state.drillView === "enterpriseProjectList") { + drillModalTitle.textContent = `${state.enterpriseName} · 项目清单`; + drillModalBody.innerHTML = renderEnterpriseProjectListModal(state.drillListType); + return; + } + if (state.drillView === "belowStandardProjects") { + drillModalTitle.textContent = "未达标项目清单"; + drillModalBody.innerHTML = renderBelowStandardProjectListModal(); + return; + } + if (state.drillView === "groupUnitMetricUsages") { + drillModalTitle.textContent = "项目指标使用率"; + drillModalBody.innerHTML = renderUnitMetricUsageModal(); + return; + } + drillModalTitle.textContent = `${indicator.name} · 指标穿透分析`; + if (state.drillView === "project") { + drillModalBody.innerHTML = renderIndicatorProjectView(); + return; + } + drillModalBody.innerHTML = renderIndicatorTrendView(); + } + + function indicatorAccuracy(item) { + return item.accuracyRate ?? item.exactRate ?? 0; + } + + /** + * 集团指标接口未返回企业明细时保持空数组,不生成模拟企业数据。 */ function buildIndicatorEnterpriseMetrics(indicator) { - return enterprises - .filter((enterprise) => enterprise.stats[state.dimension].projectCount > 0) - .map((enterprise, index) => { - const stats = enterprise.stats[state.dimension]; - const usageRate = clamp(Math.round(indicator.usageRate * 0.52 + stats.usageRate * 0.48 - (index % 4) * 2 + (index % 3)), 0, 100); - const accuracyRate = 0; - const projectCount = stats.projectCount; - const warningProjects = Math.max(0, Math.round(projectCount * (100 - usageRate) / 100 * 0.24) + (index % 3)); - return { - enterprise, - projectCount, - warningProjects, - usageRate, - accuracyRate - }; - }); + return []; } /** @@ -288,46 +658,15 @@ * 三日值是否不变会影响 warning 字段,指标弹窗据此判断项目是否进入预警列表。 */ function buildIndicatorProjectMetrics(indicator, enterprise) { - return generateProjects(enterprise, state.dimension).map((project, index) => { - const usageRate = clamp(Math.round(project.usageRate * 0.58 + indicator.usageRate * 0.42 - (index % 4) * 2), 0, 100); - const enabled = usageRate >= 58 && project.created; - const stable = enabled && (project.lagDays >= 3 || index % 4 === 0); - const todayValue = enabled ? Math.max(0, Math.round(indicator.projects * (usageRate / 100) * 0.18 + index * 2)) : "-"; - const yesterdayValue = enabled ? (stable ? todayValue : Math.max(0, todayValue - 1 - (index % 2))) : "-"; - const beforeValue = enabled ? (stable ? todayValue : Math.max(0, todayValue - 2 - (index % 3))) : "-"; - return { - project, - enabled, - usageRate, - todayValue, - yesterdayValue, - beforeValue, - lastUpdate: project.lastUpdate, - warning: stable - }; - }); + return []; } function getEnterprise(name) { - return enterprises.find((item) => item.name === name) || enterprises[0]; - } - - function getStats(enterprise) { - return enterprise.stats[state.dimension]; - } - - /** - * 按项目数加权计算集团平均率。 - * 企业项目量差异较大,不能直接对企业百分比做算术平均。 - */ - function weightedRate(key, list = enterprises) { - const totalProjects = list.reduce((sum, enterprise) => sum + enterprise.stats[state.dimension].projectCount, 0); - if (!totalProjects) return 0; - const weighted = list.reduce((sum, enterprise) => { - const stats = enterprise.stats[state.dimension]; - return sum + stats.projectCount * stats[key]; - }, 0); - return weighted / totalProjects; + const masterRecord = getEnterpriseMasterRecord(name); + return { + name: name || masterRecord?.name || "未知单位", + unitId: masterRecord?.unitId || projectMasterDataState.unitId || "" + }; } /** @@ -335,127 +674,68 @@ * 上线率三个数来自主数据接口状态;使用率、准确率、预警项仍来自企业基础数据的加权派生。 */ function groupSummary() { - const active = enterprises.filter((enterprise) => enterprise.stats[state.dimension].projectCount > 0); const projectMasterSummary = getProjectMasterSummary(); - const masterProjectCount = projectMasterSummary.masterProjectCount; + const masterProjectCount = projectMasterSummary.onlineExpected || projectMasterSummary.masterProjectCount; const noNeedProjectCount = projectMasterSummary.noNeedProjectCount; - const qhseProjectCount = projectMasterSummary.qhseProjectCount; + const qhseProjectCount = projectMasterSummary.onlineActual || projectMasterSummary.qhseProjectCount; return { projectCount: masterProjectCount, onlineExpected: masterProjectCount, onlineActual: qhseProjectCount, noNeedProjectCount, - onlineRate: masterProjectCount ? qhseProjectCount / masterProjectCount * 100 : 0, - usageRate: weightedRate("usageRate", active), - accuracyRate: weightedRate("accuracyRate", active), - warningProjects: active.reduce((sum, enterprise) => sum + enterprise.stats[state.dimension].warningProjects, 0), - warningItems: active.reduce((sum, enterprise) => sum + enterprise.stats[state.dimension].warningItems, 0), - riskEnterprises: active.filter((enterprise) => { - const stats = enterprise.stats[state.dimension]; - return Math.min(stats.onlineRate, stats.usageRate, stats.accuracyRate) < 70 || stats.warningProjects >= 8; - }).length + onlineRate: projectMasterSummary.onlineRate ?? (masterProjectCount ? qhseProjectCount / masterProjectCount * 100 : 0), + usageRate: projectMasterSummary.usageRate, + accuracyRate: projectMasterSummary.accuracyRate, + preProjectTotal: projectMasterSummary.preProjectTotal || 0, + pendingTotal: projectMasterSummary.pendingTotal || 0, + belowStandardProjects: projectMasterSummary.failProjectNum, + proInfoText: projectMasterSummary.proInfoText || "", + dataDate: projectMasterSummary.dataDate || "", + warningProjects: 0, + warningItems: 0, + riskEnterprises: 0 }; } - /** - * 风险排序分值。 - * 使用率和准确率权重较高,同时叠加预警项目数和预警项数,让列表优先暴露“用了但数据质量差”的单位。 - */ - function riskScore(stats) { - const weakness = (100 - stats.onlineRate) * 0.25 + (100 - stats.usageRate) * 0.38 + (100 - stats.accuracyRate) * 0.32; - return Math.round(weakness + stats.warningProjects * 1.4 + stats.warningItems * 0.12); - } - - function riskLevel(score) { - if (score >= 38) return { text: "重点督办", cls: "red" }; - if (score >= 26) return { text: "持续跟踪", cls: "orange" }; - return { text: "状态稳定", cls: "green" }; - } - - /** - * 按企业和维度生成稳定的项目样本。 - * 该函数是企业页、项目页、指标穿透的共同数据源,项目 id 必须稳定,否则点击下钻会找不到项目。 - */ - function generateProjects(enterprise, dimension = state.dimension) { - const stats = enterprise.stats[dimension]; - if (stats.projectCount === 0) return []; - const visibleCount = Math.min(Math.max(stats.projectCount, 6), 18); - const projectCount = Math.min(visibleCount, dimension === "new" ? Math.max(stats.projectCount, 3) : visibleCount); - const projects = []; - - for (let index = 0; index < projectCount; index += 1) { - const shift = ((index % 7) - 3) * 3; - const riskBias = index % 5 === 0 ? -12 : index % 4 === 0 ? -7 : index % 3 === 0 ? -3 : 4; - const onlineRate = clamp(stats.onlineRate + shift + (index % 2 ? 2 : -1), 45, 100); - const usageRate = clamp(stats.usageRate + shift + riskBias, 35, 100); - const accuracyRate = 0; - const warningCount = Math.max(0, Math.round((100 - Math.min(onlineRate, usageRate, accuracyRate)) / 7) + (index % 4)); - const lagDays = accuracyRate < 65 ? 5 + (index % 3) : accuracyRate < 78 ? 3 + (index % 2) : index % 3; - const created = onlineRate >= 72 || index % 6 !== 0; - const id = `${enterprise.name}-${dimension}-${index}`; - const name = `${enterprise.name}${projectNamePool[index % projectNamePool.length]}`; - projects.push({ - id, - unit: enterprise.name, - name, - isNew: dimension === "new" || index < enterprise.stats.new.projectCount, - onlineRate, - usageRate, - accuracyRate, - warningCount, - lagDays, - created, - owner: ["王工", "李工", "赵工", "陈工", "刘工", "孙工"][index % 6], - systemStatus: created ? "已上线" : "未建档", - startDate: dateMinus(dimension === "new" ? 30 + index * 4 : 180 + index * 9), - lastUpdate: dateMinus(lagDays), - risk: warningCount >= 7 || accuracyRate < 60 ? "高" : warningCount >= 4 || usageRate < 70 ? "中" : "低" + function getGroupEnterpriseModels() { + return (projectMasterDataState.unitAnalysises || []).map((analysis) => { + const masterRecord = getProjectMasterRecords().find((row) => { + const sameUnitId = analysis.unitId && row.unitId && String(analysis.unitId).toLowerCase() === String(row.unitId).toLowerCase(); + return sameUnitId || sameProjectMasterUnit(row.name, analysis.name); }); - } - return projects; - } - - function dateMinus(days) { - const base = new Date(`${TODAY}T00:00:00+08:00`); - base.setDate(base.getDate() - days); - const y = base.getFullYear(); - const m = String(base.getMonth() + 1).padStart(2, "0"); - const d = String(base.getDate()).padStart(2, "0"); - return `${y}-${m}-${d}`; - } - - function getProjectById(projectId) { - const enterprise = getEnterprise(state.enterpriseName); - return generateProjects(enterprise, state.dimension).find((project) => project.id === projectId) || generateProjects(enterprise, state.dimension)[0]; - } - - /** - * 生成项目明细页指标行。 - * 根据项目使用率推导指标是否启用,再用最近三日值是否停滞判断是否预警。 - */ - function buildIndicatorRows(project) { - return indicatorTemplates.map((indicator, index) => { - const enabledThreshold = project.usageRate - (index % 4) * 5 + (index % 2) * 3; - const enabled = enabledThreshold >= 58; - const lagDays = enabled ? clamp(project.lagDays + (index % 5) - 2, 0, 8) : 0; - const currentValue = enabled ? Math.max(0, Math.round(indicator.base * (project.usageRate / 82) + index * 2 + (project.warningCount % 4))) : "-"; - const isWarning = enabled && lagDays >= 3; - const todayValue = currentValue; - const yesterdayValue = enabled ? (isWarning ? currentValue : Math.max(0, Number(currentValue) - 1 - (index % 2))) : "-"; - const beforeValue = enabled ? (isWarning ? currentValue : Math.max(0, Number(currentValue) - 2 - (index % 3))) : "-"; + const stats = { + projectCount: masterRecord?.total || 0, + onlineRate: analysis.onlineRate, + usageRate: analysis.usageRate, + accuracyRate: analysis.accuracyRate, + warningProjects: 0, + warningItems: 0 + }; return { - name: indicator.name, - enabled, - todayValue, - yesterdayValue, - beforeValue, - lastUpdate: enabled ? dateMinus(lagDays) : "-", - lagDays: enabled ? lagDays : "-", - isWarning + name: analysis.name, + unitId: analysis.unitId, + stats: { all: stats, new: stats }, + trend: { online: [], usage: [], accuracy: [] } }; }); } + /** + * 只将企业接口返回的项目分析转换为页面项目模型。 + */ + function generateProjects(enterprise, dimension = state.dimension) { + return (projectMasterDataState.projectAnalysises || []).map((item) => ({ + id: item.projectId, + unitId: item.unitId, + unit: enterprise?.name || state.enterpriseName, + name: item.name, + usageRate: item.usageRate, + accuracyRate: item.accuracyRate, + created: item.isOnline, + systemStatus: item.isOnline ? "已上线" : "未上线" + })); + } + function statusClassByRate(rate) { if (rate >= 80) return "ok"; if (rate >= 65) return "warn"; diff --git a/res/js/indexV2.enterprise.js b/res/js/indexV2.enterprise.js index 469b998..2b8c0d7 100644 --- a/res/js/indexV2.enterprise.js +++ b/res/js/indexV2.enterprise.js @@ -1,24 +1,43 @@ /** * 企业级页面渲染模块。 - * 负责企业总览、企业指标分析、项目图表和项目清单弹窗,数据来源主要是 core 模块按企业派生的项目样本。 + * 负责企业总览、企业指标分析、项目图表和项目清单弹窗,数据来自企业监控接口。 */ function renderEnterpriseView() { const enterprise = getEnterprise(state.enterpriseName); - const stats = getStats(enterprise); - const projects = generateProjects(enterprise, state.dimension); - const indicators = buildEnterpriseIndicatorItems(getWarningIndicators(), projects, stats); + const summary = getProjectMasterSummary(); + const indicators = getWarningIndicators(); + const analysisIndicators = projectMasterDataState.metricDatas.length ? projectMasterDataState.metricDatas : indicators; + const projects = (projectMasterDataState.projectAnalysises || []).map((item) => ({ + id: item.projectId, + unitId: item.unitId, + unit: enterprise.name, + name: item.name, + created: item.isOnline, + usageRate: item.usageRate, + accuracyRate: item.accuracyRate, + warningCount: item.usageRate < 80 ? 1 : 0, + systemStatus: item.isOnline ? "已上线" : "未上线", + lastUpdate: summary.dataDate || "", + owner: "-", + risk: item.usageRate < 65 ? "高" : item.usageRate < 80 ? "中" : "低" + })); + const enterpriseOnlineRate = summary.onlineRate; + const belowStandardProjects = summary.failProjectNum; - // 新开工维度下部分企业没有项目,单独展示空态,避免图表按空数组渲染出异常坐标。 - if (!stats.projectCount) { + if (projectMasterDataState.loading) { + app.innerHTML = '
正在获取企业监控数据...
'; + return; + } + if (!projects.length && projectMasterDataState.error) { app.innerHTML = `

${enterprise.name} · ${dimLabel()}看板

-
当前维度暂无项目数据,可切换到“所有项目”查看企业总体情况。
+
${projectMasterDataState.error}
-
当前企业在「${dimLabel()}」维度暂无数据。
+
企业监控接口数据加载失败。
`; return; @@ -26,17 +45,16 @@ app.innerHTML = `
- ${miniMetric("项目数量", stats.projectCount, "个", `${enterprise.name} 当前${dimLabel()}项目数量`, "enterprise-project-list")} - ${miniMetric("上线率", fmtPercent(stats.onlineRate), "", "项目已建档并进入系统", "enterprise-offline-list")} - ${miniMetric("使用率", fmtPercent(stats.usageRate), "", "连续三天无更新视未使用")} - ${miniMetric("准确率", fmtPercent(stats.accuracyRate), "", "数据准确情况")} + ${miniMetric("项目上线率", summary.proInfoText, "", `上线率 ${fmtFixedPercent(enterpriseOnlineRate, 2)}`, "group-online-stats")} + ${miniMetric("预立项项目数", summary.preProjectTotal, "个", "点击查看项目上线统计", "group-online-stats")} + ${miniMetric("待建项目数", summary.pendingTotal, "个", "点击查看项目上线统计", "group-online-stats")} + ${miniMetric("未达标项目数", belowStandardProjects, "个", "指标使用率低于 80% 的项目", "below-standard-projects")} + ${miniMetric("项目指标使用率", fmtPercent(summary.usageRate), "", "指标使用率低于 80% 的项目计入未达标项目")} + ${miniMetric("项目指标准确率", fmtPercent(summary.accuracyRate), "", "")}
${renderFocusIndicators(indicators.slice(0, 8), 0, { highlightHigh: false })} -
- -
@@ -52,9 +70,10 @@
${renderTrendChart({ - online: enterprise.trend.online, - usage: enterprise.trend.usage, - accuracy: enterprise.trend.accuracy + date: projectMasterDataState.trendCurve?.Date || [], + online: projectMasterDataState.trendCurve?.OnlineRate || [], + usage: projectMasterDataState.trendCurve?.UsageRate || [], + accuracy: projectMasterDataState.trendCurve?.AccuracyRate || [] }, 1840, 112)}
@@ -85,7 +104,7 @@ - ${renderEnterpriseIndicatorAnalysis(indicators)} + ${renderEnterpriseIndicatorAnalysis(analysisIndicators)} `; @@ -148,7 +167,18 @@ */ function renderEnterpriseProjectListModal(type) { const enterprise = getEnterprise(state.enterpriseName); - const projects = generateProjects(enterprise, state.dimension); + const projects = (projectMasterDataState.projectAnalysises || []).map((item) => ({ + id: item.projectId, + unitId: item.unitId, + name: item.name, + created: item.isOnline, + usageRate: item.usageRate, + accuracyRate: item.accuracyRate, + warningCount: item.usageRate < 80 ? 1 : 0, + lastUpdate: getProjectMasterSummary().dataDate || "-", + owner: "-", + risk: item.usageRate < 65 ? "高" : item.usageRate < 80 ? "中" : "低" + })); const rows = type === "offline" ? projects.filter((project) => !project.created) : projects; @@ -190,7 +220,7 @@ ${rows.map((project) => ` - + ${project.name} ${project.created ? '已上线' : '未上线'} ${fmtPercent(project.usageRate)} diff --git a/res/js/indexV2.events.js b/res/js/indexV2.events.js index 8db60c4..cf9ea27 100644 --- a/res/js/indexV2.events.js +++ b/res/js/indexV2.events.js @@ -15,6 +15,7 @@ state.drillView = "trend"; } render(); + loadProjectMasterDataAnalysis(state.view === "enterprise" ? (getEnterpriseMasterRecord(state.enterpriseName)?.unitId || "") : ""); return; } @@ -27,6 +28,7 @@ state.selectedIndicator = getWarningIndicators()[0]?.name || ""; } render(); + loadProjectMasterDataAnalysis(state.view === "enterprise" ? (getEnterpriseMasterRecord(state.enterpriseName)?.unitId || "") : ""); return; } @@ -41,13 +43,26 @@ window.open(ENTERPRISE_INDICATOR_MORE_URL, "_blank", "noopener,noreferrer"); return; } - if (action.dataset.action === "group-usage-report-preview") { - window.open(GROUP_USAGE_REPORT_URL, "_blank", "noopener,noreferrer"); - return; - } if (action.dataset.action === "preview-unit-report") { const unit = action.dataset.unit || ""; - const fileName = usageReportFiles[unit]; + const unitAliases = { + 二建: "二化建", + 三建: "三化建", + 四建: "四化建", + 六建: "六化建", + 七建: "七化建", + 九建: "九化建", + 十一建: "十一化建", + 十三建: "十三化建", + 十四建: "十四化建", + 十六建: "十六化建", + 装备科技: "装备", + 华谊工程: "华谊", + 国际工程公司: "国际", + 建投: "北建投", + 南投: "南建投" + }; + const fileName = usageReportFiles[unit] || usageReportFiles[unitAliases[unit]]; if (!fileName) { window.alert(`${unit || "该单位"}未上传一览表`); return; @@ -56,6 +71,15 @@ window.open(`${OFFICE_PREVIEW_BASE_URL}${encodeURIComponent(fileUrl)}`, "_blank", "noopener,noreferrer"); return; } + if (action.dataset.action === "master-web-url") { + const url = action.dataset.url || ""; + if (!/^https?:\/\//i.test(url)) { + window.alert("该单位未配置有效穿透链接"); + return; + } + window.open(url, "_blank", "noopener,noreferrer"); + return; + } if (action.dataset.action === "group-online-stats") { state.drillOpen = true; state.drillView = "groupOnlineStats"; @@ -65,6 +89,23 @@ renderDrillModal(); return; } + if (action.dataset.action === "below-standard-projects") { + state.drillOpen = true; + state.drillView = "belowStandardProjects"; + state.drillListType = "below-standard"; + state.projectId = ""; + renderDrillModal(); + return; + } + if (action.dataset.action === "group-unit-metric-usages") { + state.drillOpen = true; + state.drillView = "groupUnitMetricUsages"; + state.drillEnterpriseName = ""; + state.drillListType = ""; + state.projectId = ""; + renderDrillModal(); + return; + } if (action.dataset.action === "close-drill") { state.drillOpen = false; state.drillView = "trend"; @@ -102,11 +143,13 @@ state.view = "group"; state.enterpriseName = ""; state.projectId = ""; + loadProjectMasterDataAnalysis(""); } if (action.dataset.action === "enterprise") { state.view = "enterprise"; state.enterpriseName = action.dataset.enterprise || state.enterpriseName; state.projectId = ""; + loadProjectMasterDataAnalysis(getEnterpriseMasterRecord(state.enterpriseName)?.unitId || ""); } render(); return; @@ -116,6 +159,7 @@ const indicatorTarget = event.target.closest("[data-indicator]"); if (indicatorTarget) { state.selectedIndicator = indicatorTarget.dataset.indicator; + const selectedIndicatorField = resolveMetricField(state.selectedIndicator, indicatorTarget.dataset.indicatorField); state.drillOpen = true; if (state.view === "enterprise") { state.drillView = "project"; @@ -126,6 +170,11 @@ } state.projectId = ""; renderDrillModal(); + if (state.drillView === "trend") { + loadGroupMetricAnalysis(selectedIndicatorField); + } else { + loadUnitMetricAnalysis(selectedIndicatorField, getEnterpriseMasterRecord(state.enterpriseName)?.unitId || projectMasterDataState.unitId); + } return; } @@ -147,6 +196,7 @@ state.enterpriseName = enterpriseTarget.dataset.enterprise; state.projectId = ""; render(); + loadProjectMasterDataAnalysis(getEnterpriseMasterRecord(state.enterpriseName)?.unitId || ""); return; } @@ -154,17 +204,23 @@ const projectTarget = event.target.closest("[data-project]"); if (projectTarget) { state.view = "project"; + if (projectTarget.dataset.projectEnterprise) { + state.enterpriseName = projectTarget.dataset.projectEnterprise; + } state.projectId = projectTarget.dataset.project; state.drillOpen = false; state.drillView = "trend"; state.drillListType = ""; - render(); + const unitId = projectTarget.dataset.projectUnitId + || getEnterpriseMasterRecord(state.enterpriseName)?.unitId + || projectMasterDataState.unitId; + loadProjectMonitoring(unitId, state.projectId); } }); /** * 初始化顺序: - * 先绑定缩放并渲染兜底数据,再异步加载集团主数据接口; + * 先绑定缩放并渲染加载状态,再异步加载集团主数据接口; * 接口返回后 loadProjectMasterDataAnalysis 会再次 render,刷新集团上线率卡片和统计弹窗。 */ window.addEventListener("resize", scaleScreen); diff --git a/res/js/indexV2.group.js b/res/js/indexV2.group.js index cafce6e..efd8ba6 100644 --- a/res/js/indexV2.group.js +++ b/res/js/indexV2.group.js @@ -5,7 +5,19 @@ */ return `
-
${trend}
+ ${trend ? `
${trend}
` : ""} +
${label}
+
${value}${unit}
+
+
${desc}
+
+ `; + } + + function clickableKpiCard(label, value, unit, desc, action, widthPercent, trend = "") { + return ` +
+ ${trend ? `
${trend}
` : ""}
${label}
${value}${unit}
@@ -16,17 +28,18 @@ /** * 集团项目上线率卡片。 - * 数值来自 groupSummary,而 groupSummary 会优先读取 projectMasterDataState.summary; - * 接口失败时 desc 展示错误原因,同时数值回退到本地兜底汇总。 + * 数值只来自集团监控接口,接口失败时显示错误状态。 */ function onlineKpiCard(summary) { - const trendText = projectMasterDataState.loading ? "加载中" : `${summary.onlineRate.toFixed(2)}%`; - const statusText = projectMasterDataState.error ? `接口失败,已使用本地兜底数据:${projectMasterDataState.error}` : "QHSE使用项目数 / 申请不用项目数 / 主数据项目数"; + const trendText = projectMasterDataState.loading ? "加载中" : `${toSafeNumber(summary.onlineRate).toFixed(2)}%`; + const statusText = projectMasterDataState.error + ? `接口请求失败:${projectMasterDataState.error}` + : "上线项目/申请不用/应使用项目/项目总数"; return `
${trendText}
集团项目上线率
-
${summary.onlineActual}/${summary.noNeedProjectCount}/${summary.onlineExpected}
+
${summary.proInfoText || `${summary.onlineActual}/${summary.noNeedProjectCount}/${summary.onlineExpected}`}
${statusText}
@@ -43,8 +56,10 @@ parts.push(`›${state.enterpriseName}`); } if (state.view === "project") { - const project = getProjectById(state.projectId); - parts.push(`›${project ? project.name : "项目明细"}`); + const projectName = projectMonitoringState.data?.MetricProjectInfo?.ProjectShortName + || projectMonitoringState.data?.MetricProjectInfo?.ProjectName + || "项目明细"; + parts.push(`›${projectName}`); } if (state.view === "indicatorTrend" || state.view === "indicatorEnterprise" || state.view === "indicatorProject") { const indicator = getSelectedIndicator(); @@ -71,6 +86,7 @@ button.classList.toggle("active", button.dataset.system === state.system); }); renderBreadcrumbs(); + updateDataDateDisplay(); if (state.view === "group") renderGroupView(); if (state.view === "enterprise") renderEnterpriseView(); if (state.view === "project") renderProjectView(); @@ -86,25 +102,21 @@ function renderGroupView() { const summary = groupSummary(); const indicators = getWarningIndicators(); - const activeEnterprises = enterprises.filter((enterprise) => enterprise.stats[state.dimension].projectCount > 0); - const riskList = activeEnterprises - .map((enterprise) => ({ enterprise, stats: enterprise.stats[state.dimension], score: riskScore(enterprise.stats[state.dimension]) })) - .sort((a, b) => b.score - a.score); + const analysisIndicators = projectMasterDataState.metricDatas.length ? projectMasterDataState.metricDatas : indicators; + const activeEnterprises = getGroupEnterpriseModels(); app.innerHTML = `
${onlineKpiCard(summary)} - ${kpiCard("项目指标使用率", fmtPercent(summary.usageRate), "", "连续三天无更新视为未使用,用于发现用了但数据不动。", "+0.8%", summary.usageRate)} - ${kpiCard("指标数据准确率", fmtPercent(summary.accuracyRate), "", "用于发现数据准不准。", "-0.6%", summary.accuracyRate, "down")} - ${kpiCard("预警项目数", fmtNumber(summary.warningProjects), "个", `当前触发上线、使用、准确性风险的项目数,预警项 ${fmtNumber(summary.warningItems)} 条。`, "", clamp(100 - summary.warningProjects, 0, 100), "down")} - ${kpiCard("预警企业", fmtNumber(summary.riskEnterprises), "家", "上线率、使用率、准确率任一维度触发预警的企业。", "督办", clamp(100 - summary.riskEnterprises * 10, 0, 100), "down")} + ${clickableKpiCard("预立项项目数", fmtNumber(summary.preProjectTotal), "个", "点击查看集团项目上线统计。", "group-online-stats", clamp(summary.preProjectTotal / 2, 0, 100))} + ${clickableKpiCard("待建项目数", fmtNumber(summary.pendingTotal), "个", "点击查看集团项目上线统计。", "group-online-stats", clamp(summary.pendingTotal, 0, 100))} + ${clickableKpiCard("未达标项目数", fmtNumber(summary.belowStandardProjects), "个", "指标使用率低于 80% 的项目。", "below-standard-projects", clamp(100 - summary.belowStandardProjects, 0, 100))} + ${clickableKpiCard("项目指标使用率", fmtPercent(summary.usageRate), "", "点击查看单位项目指标使用情况。", "group-unit-metric-usages", summary.usageRate)} + ${kpiCard("项目指标准确率", fmtPercent(summary.accuracyRate), "", "", "", summary.accuracyRate)}
${renderFocusIndicators(indicators.slice(0, 8), 0, { highlightHigh: false })} -
- -
@@ -136,16 +148,16 @@ 准确率 -
${renderGroupedRateChart(activeEnterprises, "enterprise", { height: 132 })}
+
${activeEnterprises.length ? renderGroupedRateChart(activeEnterprises, "enterprise", { height: 132 }) : '
暂无企业分析数据
'}
-
+

集团在建项目使用分析报告

- +
@@ -159,7 +171,7 @@ - ${renderIndicatorAnalysis(indicators)} + ${renderIndicatorAnalysis(analysisIndicators)}
`; @@ -172,11 +184,11 @@ function renderFocusIndicators(items, offset, options = {}) { const highlightHigh = options.highlightHigh !== false; return items.map((item, index) => ` -
+
${item.name}
指标使用率:${item.usageRate}% - 指标准确率:${indicatorAccuracy(item)}% + 使用项目数:${item.projects}
`).join(""); @@ -205,20 +217,14 @@ return `
${indicators.slice(0, 8).map((item) => { - const level = item.usageRate < 65 - ? { text: "重点督办", cls: "red" } - : item.usageRate < 72 - ? { text: "持续跟踪", cls: "orange" } - : { text: "保持关注", cls: "green" }; return ` -
+
${item.name}
${miniRate("项目", Math.round((item.projects / maxProjects) * 100), COLORS.online, item.projects)} ${miniRate("使用", item.usageRate, COLORS.usage)} ${miniRate("准确", indicatorAccuracy(item), COLORS.accuracy)}
- ${level.text}
`; }).join("")} @@ -226,28 +232,6 @@ `; } - /** - * 企业页指标分析数据派生。 - * 在集团指标基础上叠加当前企业使用率和项目数量,生成更贴近企业维度的指标预警项。 - */ - function buildEnterpriseIndicatorItems(indicators, projects, stats) { - const projectBase = Math.max(projects.length, 1); - return indicators.slice(0, 8).map((item, index) => { - const usageRate = clamp(Math.round(item.usageRate * 0.55 + stats.usageRate * 0.45 - (index % 3) * 2), 0, 100); - const projectsCount = clamp(Math.round(projectBase * (0.55 + (index % 4) * 0.08)), 1, projectBase); - const warningProjects = clamp(Math.round(projectsCount * (100 - usageRate) / 100 + (index % 2)), 0, projectsCount); - return { - ...item, - projects: projectsCount, - warningProjects, - usageRate, - accuracyRate: 0, - missingUpdate: warningProjects + (index % 3), - owner: ["安全管理部", "质量管理部", "项目管理部", "数据治理专员"][index % 4] - }; - }); - } - /** * 企业指标分析列表。 * 与集团指标列表共用展示结构,但风险文案更偏整改和跟踪,用于企业页面的责任短板展示。 @@ -257,20 +241,14 @@ return `
${items.map((item) => { - const level = item.warningProjects >= 6 || item.usageRate < 65 - ? { text: "重点整改", cls: "red" } - : item.warningProjects >= 3 || item.usageRate < 72 - ? { text: "持续跟踪", cls: "orange" } - : { text: "保持关注", cls: "green" }; return ` -
+
${item.name}
${miniRate("项目", Math.round((item.projects / maxProjects) * 100), COLORS.online, item.projects)} ${miniRate("使用", item.usageRate, COLORS.usage)} ${miniRate("准确", item.accuracyRate, COLORS.accuracy)}
- ${level.text}
`; }).join("")} @@ -278,107 +256,212 @@ `; } - function renderEnterpriseTable(riskList) { + /** + * 集团上线统计弹窗表格。 + * 表格行统一从 getProjectMasterRecords 读取接口数据。 + * 国际、生态单位在这里加 green-row,高亮规则集中在表格渲染层。 + */ + function renderUsageReportTable(list) { + const rows = getProjectMasterRecords(); + const summary = getProjectMasterSummary(); + const statusText = projectMasterDataState.loading + ? `
正在获取接口数据...
` + : projectMasterDataState.error + ? `
接口请求失败:${projectMasterDataState.error}
` + : ""; + if (!rows.length) { + return `${statusText}
暂无集团项目上线统计数据
`; + } + const bodyRows = rows.map((row) => ({ + unit: row.name, + cells: [ + row.onlineAutoTotal, + row.onlineReportedTotal, + fmtFixedPercent(row.onlineRate || projectMasterOnlineRate(row), 2), + row.total, + row.linkedTotal, + row.preProjectTotal, + row.pendingTotal, + Math.max(row.total - row.linkedTotal - row.preProjectTotal - row.pendingTotal, 0), + row.noNeedTotal, + row.noNeedCompletedTotal, + row.noNeedStoppedTotal, + row.noNeedRestrictedTotal, + row.masterProjectCount + ], + webUrl: resolveProjectMasterWebUrl(row) + })); return ` + ${statusText}
- +
- - - - - - - - + + + + + + + + + + + + + + + + + + + - ${riskList.map(({ enterprise, stats, score }) => { - const level = riskLevel(score); + ${bodyRows.map((row) => { + const unit = row.unit || ""; + const rowClass = [ + unit.includes("国际") || unit.includes("生态") ? "green-row" : "" + ].filter(Boolean).join(" "); + const unitCell = /^https?:\/\//i.test(row.webUrl) + ? `${unit}` + : unit; return ` - - - - - - - - - - - `; + + + ${row.cells.map((cell) => ``).join("")} + + `; }).join("")} + + + ${["onlineAutoTotal", "onlineReportedTotal"].map((key) => ``).join("")} + + + + + + + + + + + +
企业项目数上线率使用率准确率预警项目责任人状态企业简称上线项目数上线率应使用项目数申请不用主数据项目数
自动取值定期填报总数已关联项目预立项项目待建项目条件受限总数已完工停缓建条件受限
${enterprise.name}${stats.projectCount}${fmtPercent(stats.onlineRate)}${fmtPercent(stats.usageRate)}${fmtPercent(stats.accuracyRate)}${stats.warningProjects}${enterprise.owner}${level.text}
${unitCell}${cell}
合计${rows.reduce((sum, row) => sum + toSafeNumber(row[key]), 0)}${fmtFixedPercent(summary.onlineRate, 2)}${rows.reduce((sum, row) => sum + toSafeNumber(row.total), 0)}${rows.reduce((sum, row) => sum + toSafeNumber(row.linkedTotal), 0)}${summary.preProjectTotal}${summary.pendingTotal}${rows.reduce((sum, row) => sum + Math.max(row.total - row.linkedTotal - row.preProjectTotal - row.pendingTotal, 0), 0)}${summary.noNeedProjectCount}${rows.reduce((sum, row) => sum + toSafeNumber(row.noNeedCompletedTotal), 0)}${rows.reduce((sum, row) => sum + toSafeNumber(row.noNeedStoppedTotal), 0)}${rows.reduce((sum, row) => sum + toSafeNumber(row.noNeedRestrictedTotal), 0)}${rows.reduce((sum, row) => sum + toSafeNumber(row.masterProjectCount), 0)}
`; } - function buildUsageReportRows(list) { - return list.map((enterprise) => { - const stats = enterprise.stats[state.dimension]; - const usedProjects = Math.round(stats.projectCount * stats.usageRate / 100); - return { - name: enterprise.name, - expected: stats.projectCount, - used: usedProjects, - usageRate: stats.projectCount ? Math.round(usedProjects / stats.projectCount * 100) : 0, - trend: enterprise.trend.usage - }; - }); + function renderBelowStandardProjectListModal() { + const enterprise = state.enterpriseName ? getEnterprise(state.enterpriseName) : null; + const rows = getBelowStandardProjects(enterprise); + const title = enterprise ? `${enterprise.name} · 未达标项目清单` : "集团未达标项目清单"; + if (!rows.length) { + return ` +
+

${title}

+
当前暂无指标使用率低于 80% 的项目。
+
+ `; + } + return ` +
+
+
+

${title}

+
点击项目行进入项目级看板。
+
+
+
+ + + + + + + + + + + + + ${rows.map(({ enterprise: item, project }) => ` + + + + + + + + + `).join("")} + +
单位项目名称指标使用率是否上线最近更新责任人
${item.name}${project.name}${fmtPercent(project.usageRate)}${project.created ? '已上线' : '未上线'}${project.lastUpdate}${project.owner}
+
+
+ `; } - /** - * 集团上线统计弹窗表格。 - * 表格行统一从 getProjectMasterRows 读取:接口成功展示接口行,接口失败展示本地兜底行。 - * 国际、生态单位在这里加 green-row,高亮规则集中在表格渲染层。 - */ - function renderUsageReportTable(list) { - const rows = getProjectMasterRows(); - const headers = projectMasterDataState.loaded ? projectMasterDataHeaders : usageReportHeaders; - const statusText = projectMasterDataState.loading - ? `
正在获取接口数据...
` - : projectMasterDataState.error - ? `
接口请求失败,当前展示本地兜底数据:${projectMasterDataState.error}
` - : ""; + function renderUnitMetricUsageModal() { + const rows = getUnitMetricUsageRecords(); + if (!rows.length) { + return ` +
+

项目指标使用率

+
当前暂无单位项目指标使用数据。
+
+ `; + } return ` - ${statusText} -
- - - - ${headers.map((header) => ``).join("")} - - - - - ${rows.map((row) => { - const unit = row[1] || ""; - const rowClass = [ - row[0] === "合计" ? "summary-row" : "", - ["国际", "生态"].includes(unit) ? "green-row" : "" - ].filter(Boolean).join(" "); - return ` - - ${row.map((cell, index) => ``).join("")} - +
+
+
+

项目指标使用率

+
+
+
+
${header}操作
${cell}${unit && rowClass.indexOf("summary-row") === -1 ? `` : ""}
+ + + + + + + - `; - }).join("")} - -
单位简称QHSE在建项目数活跃项目数使用率日期
-
+ + + ${rows.map((row) => { + const unitName = row.abbreviation || row.unitName || ""; + const webUrl = resolveUnitMetricUsageWebUrl(row); + const unitCell = /^https?:\/\//i.test(webUrl) + ? `${unitName}` + : unitName; + return ` + + ${unitCell} + ${row.qhseTotal} + ${row.activeNum} + ${fmtPercent(row.usageRate)} + ${formatApiDate(row.date)} + + `; + }).join("")} + + +
+ `; } /** * “集团项目上线统计”弹窗内容。 - * 弹窗标题由 project 模块 renderDrillModal 设置,这里只负责面板主体和表格。 + * 弹窗标题由 core 模块 renderDrillModal 设置,这里只负责面板主体和表格。 */ function renderGroupOnlineStatsModal() { - const activeEnterprises = enterprises.filter((enterprise) => enterprise.stats[state.dimension].projectCount > 0); return `
@@ -386,7 +469,7 @@

集团项目上线统计

- ${renderUsageReportTable(activeEnterprises)} + ${renderUsageReportTable()} `; } diff --git a/res/js/indexV2.indicators.js b/res/js/indexV2.indicators.js index 7247e74..9ee6ddf 100644 --- a/res/js/indexV2.indicators.js +++ b/res/js/indexV2.indicators.js @@ -4,53 +4,61 @@ */ function renderIndicatorTrendView() { const indicator = getSelectedIndicator(); - const enterpriseMetrics = buildIndicatorEnterpriseMetrics(indicator); + const analysis = projectMasterDataState.metricAnalysis && projectMasterDataState.metricAnalysis.field === indicator.field + ? projectMasterDataState.metricAnalysis.data + : null; + const metricData = analysis && analysis.MetricData ? normalizeMetricData(analysis.MetricData) : indicator; + const enterpriseMetrics = analysis && Array.isArray(analysis.UnitAnalysises) + ? analysis.UnitAnalysises.map((item, index) => ({ + enterprise: getEnterpriseByUnitName(item.Name || item.name), + projectCount: 0, + warningProjects: 0, + usageRate: toSafeNumber(item.UsageRate ?? item.usageRate), + accuracyRate: toSafeNumber(item.AccuracyRate ?? item.accuracyRate), + sortIndex: toSafeNumber(item.SortIndex ?? item.sortIndex ?? index + 1) + })).sort((a, b) => b.usageRate - a.usageRate) + : buildIndicatorEnterpriseMetrics(indicator); const avgUsage = enterpriseMetrics.length ? Math.round(enterpriseMetrics.reduce((sum, item) => sum + item.usageRate, 0) / enterpriseMetrics.length) : 0; - const warningEnterprises = enterpriseMetrics.filter((item) => item.usageRate < 70 || item.accuracyRate === 0).length; - const positive = [...enterpriseMetrics].sort((a, b) => b.usageRate - a.usageRate).slice(0, 10); + const warningEnterprises = toSafeNumber(analysis?.AbnormalNum ?? analysis?.abnormalNum) + || enterpriseMetrics.filter((item) => item.usageRate < 70 || item.accuracyRate === 0).length; return ` -
+
-

${indicator.name}

+

${metricData.name}

-
指标使用率
-
${avgUsage}%
+
使用项目/项目总数
+
${metricData.projectUsageNum}/${metricData.projects}
-
指标准确率
-
0%
+
使用率
+
${fmtPercent(metricData.usageRate || avgUsage)}
-
预警企业
+
准确率
+
${fmtPercent(indicatorAccuracy(metricData))}
+
+
+
预警企业数
${warningEnterprises}家
-
-
+
+
-

指标变化曲线

-
X轴为日期,Y轴为该指标值,用于发现指标波动、停滞和异常断点。
+

该指标企业使用情况

+
+
+ 使用率 + 准确率
-
${renderIndicatorValueTrend(indicator)}
-
- -
-
-
-

指标企业排行

-
展示该指标使用较好的企业,点击企业进入项目级指标明细。
-
-
-
- ${positive.map((item, index) => renderEnterpriseRankRow(item, index)).join("")} -
+
${renderIndicatorEnterpriseUsageChart(enterpriseMetrics)}
`; @@ -63,24 +71,28 @@ function renderIndicatorProjectView() { const indicator = getSelectedIndicator(); const enterprise = getEnterprise(state.drillEnterpriseName || state.enterpriseName); - const projectMetrics = buildIndicatorProjectMetrics(indicator, enterprise); - const avgUsage = projectMetrics.length - ? Math.round(projectMetrics.reduce((sum, item) => sum + item.usageRate, 0) / projectMetrics.length) - : 0; - const warningCount = projectMetrics.filter((item) => item.warning).length; + const analysis = unitMetricAnalysisState.field === resolveMetricField(indicator.name, indicator.field) + ? unitMetricAnalysisState.data + : null; + const metricData = analysis?.MetricData ? normalizeMetricData(analysis.MetricData) : indicator; + const projectMetrics = Array.isArray(analysis?.ProjectMetrics) ? analysis.ProjectMetrics : []; + const warningCount = toSafeNumber(analysis?.AbnormalNum); + + if (unitMetricAnalysisState.loading) return '
正在获取企业指标穿透数据...
'; + if (unitMetricAnalysisState.error) return `
企业指标穿透数据加载失败:${unitMetricAnalysisState.error}
`; return `
-

${enterprise.name} · ${indicator.name}

+

${metricData.name}

-
项目数量
-
${projectMetrics.length}个
+
使用项目/项目总数
+
${metricData.projectUsageNum}/${metricData.projects}
-
平均使用率
-
${avgUsage}%
+
使用率
+
${fmtFixedPercent(metricData.usageRate, 2)}
预警项目
@@ -92,7 +104,7 @@

项目指标分析

-
三日数值未变化的项目指标判定为预警。
+
${analysis?.CompareStr || ""}
${renderIndicatorProjectTable(projectMetrics)} @@ -100,19 +112,50 @@ `; } + function renderIndicatorEnterpriseUsageChart(items) { + const width = Math.max(1500, items.length * 92 + 120); + const height = 560; + const margin = { top: 34, right: 30, bottom: 100, left: 58 }; + const chartHeight = height - margin.top - margin.bottom; + const chartWidth = width - margin.left - margin.right; + const groupWidth = chartWidth / Math.max(items.length, 1); + const barWidth = Math.min(18, groupWidth / 3.2); + const grid = [0, 25, 50, 75, 100].map((tick) => { + const y = margin.top + chartHeight - tick / 100 * chartHeight; + return `${tick}%`; + }).join(""); + const bars = items.map((item, index) => { + const center = margin.left + groupWidth * index + groupWidth / 2; + return [ + { value: item.usageRate, color: COLORS.usage, offset: -barWidth - 2 }, + { value: item.accuracyRate, color: COLORS.accuracy, offset: 2 } + ].map((series) => { + const value = clamp(toSafeNumber(series.value), 0, 100); + const barHeight = value / 100 * chartHeight; + const x = center + series.offset; + const y = margin.top + chartHeight - barHeight; + return `${fmtPercent(value)}`; + }).join("") + `${item.enterprise?.name || "未知单位"}`; + }).join(""); + return `${grid}${bars}`; + } + /** * 指标七日变化 SVG。 * 这里用当前指标覆盖项目数生成稳定趋势线,主要表达波动、停滞和异常断点的展示结构。 */ - function renderIndicatorValueTrend(indicator) { + function renderIndicatorValueTrend(indicator, trendCurves = null) { const width = 1180; const height = 560; const margin = { top: 28, right: 40, bottom: 62, left: 72 }; const chartWidth = width - margin.left - margin.right; const chartHeight = height - margin.top - margin.bottom; - const dates = ["04-21", "04-22", "04-23", "04-24", "04-25", "04-26", "04-27"]; - const base = indicator.projects; - const values = dates.map((_, index) => Math.max(0, Math.round(base * (0.82 + index * 0.025 + ((index % 3) - 1) * 0.035)))); + const trendRows = Array.isArray(trendCurves) && trendCurves.length ? trendCurves : null; + const dates = trendRows ? trendRows.map((item) => item.Date || item.date) : ["06-26", "06-27", "06-28", "06-29", "06-30", "07-01", "07-02"]; + const base = indicator.projects || indicator.ProjectNum || 1; + const values = trendRows + ? trendRows.map((item) => toSafeNumber(item.Value ?? item.value)) + : dates.map((_, index) => Math.max(0, Math.round(base * (0.82 + index * 0.025 + ((index % 3) - 1) * 0.035)))); const maxValue = Math.max(...values, 1); const tickValues = [0, 0.25, 0.5, 0.75, 1].map((ratio) => Math.round(maxValue * ratio)); const points = values.map((value, index) => { @@ -167,34 +210,35 @@ /** * 指标项目明细表。 - * 今日、昨日、前日三列用于判断指标是否连续不变,warning 字段决定“是否预警”标签。 + * 展示企业指标穿透接口返回的项目异常状态和六个历史时点数据。 */ function renderIndicatorProjectTable(items) { - const beforeDay = dateMinus(2); return `
- - - - - - + + + + + + + ${items.map((item) => ` - - - - - - - + + + + + + + + `).join("")} @@ -231,31 +275,34 @@ * 与指标项目明细表结构相近,但数据来源是当前项目的 buildIndicatorRows 结果。 */ function renderIndicatorTable(rows) { - const beforeDay = dateMinus(2); return `
项目名称是否启用今日(${TODAY})昨日(${PREVIOUS_DAY})前日(${beforeDay})最后更新是否预警是否异常今日昨日三天前一周前一个月前三个月前
${item.project.name}${item.enabled ? '已启用' : '未启用'}${item.todayValue}${item.yesterdayValue}${item.beforeValue}${item.lastUpdate}${item.warning ? '是' : '否'}${item.ProjectName || item.projectName || ""}${item.IsAbnormal ?? item.isAbnormal ? '异常' : '正常'}${item.NowData ?? item.nowData ?? "-"}${item.YesterdayData ?? item.yesterdayData ?? "-"}${item.ThreeDaysAgoData ?? item.threeDaysAgoData ?? "-"}${item.WeekAgoData ?? item.weekAgoData ?? "-"}${item.MonthAgoData ?? item.monthAgoData ?? "-"}${item.ThreeMonthsAgoData ?? item.threeMonthsAgoData ?? "-"}
- - - - - - + + + + + + + + ${rows.map((row) => ` - - - - - - - + + + + + + + + + `).join("")} diff --git a/res/js/indexV2.project.js b/res/js/indexV2.project.js index 941cbf9..66a6bd0 100644 --- a/res/js/indexV2.project.js +++ b/res/js/indexV2.project.js @@ -1,21 +1,26 @@ /** * 项目级页面渲染模块。 - * 展示单个项目的上线状态、指标启用数量、准确数量和预警指标,并列出项目下所有治理指标。 + * 展示单个项目的达标状态、指标使用数量、准确数量和预警指标,并列出项目下所有治理指标。 */ function renderProjectView() { - const enterprise = getEnterprise(state.enterpriseName); - const project = getProjectById(state.projectId); - const rows = buildIndicatorRows(project); - const enabledCount = rows.filter((row) => row.enabled).length; - const accurateCount = rows.filter((row) => row.enabled && !row.isWarning).length; - const warningIndicatorCount = rows.filter((row) => row.isWarning).length; + if (projectMonitoringState.loading) { + app.innerHTML = '
正在获取项目监控数据...
'; + return; + } + if (projectMonitoringState.error || !projectMonitoringState.data) { + app.innerHTML = `
项目监控数据加载失败:${projectMonitoringState.error || "暂无数据"}
`; + return; + } + const data = projectMonitoringState.data; + const project = data.MetricProjectInfo || {}; + const rows = Array.isArray(data.MetricDataAnalysises) ? data.MetricDataAnalysises : []; app.innerHTML = ` -
- ${miniMetric("项目上线状态", project.systemStatus, "", project.created ? "已创建项目档案并进入系统" : "未创建项目档案,需先完成上线")} - ${miniMetric("指标使用数量", `${enabledCount}/${rows.length}`, "", "项目内治理指标动没动")} - ${miniMetric("数据准确数量", `${accurateCount}/${rows.length}`, "", "数据准不准")} - ${miniMetric("预警指标", warningIndicatorCount, "项", "")} +
+ ${miniMetric("达标状态", data.UsageState || "-", "", "")} + ${miniMetric("指标使用数量", data.MetricUsageInfo || "-", "", "")} + ${miniMetric("准确数量", data.MetricAccuracyInfo || "-", "", "")} + ${miniMetric("预警指标", toSafeNumber(data.AbnormalNum), "项", "")}
@@ -26,11 +31,13 @@
- ${fieldRow("所属企业", enterprise.name)} - ${fieldRow("项目名称", project.name)} - ${fieldRow("系统状态", project.systemStatus)} - ${fieldRow("开工日期", project.startDate)} - ${fieldRow("最近更新", project.lastUpdate)} + ${fieldRow("所属企业", project.UnitName || state.enterpriseName)} + ${fieldRow("项目名称", project.ProjectName || project.ProjectShortName || "-")} + ${fieldRow("项目编号", project.ProjectCode || "-")} + ${fieldRow("项目地址", project.ProjectAddress || "-")} + ${fieldRow("项目状态", project.ProjectState || data.OnlineState || "-")} + ${fieldRow("开工日期", formatApiDate(project.StartDate) || "-")} + ${fieldRow("数据日期", formatApiDate(data.DataDate) || "-")}
@@ -45,34 +52,3 @@
`; } - - /** - * 统一弹窗渲染入口。 - * state.drillView 决定展示集团上线统计、企业项目清单、指标项目穿透或指标趋势; - * 关闭弹窗时清空 body,避免旧内容残留到下一次打开。 - */ - function renderDrillModal() { - if (!state.drillOpen) { - drillModal.hidden = true; - drillModalBody.innerHTML = ""; - return; - } - const indicator = getSelectedIndicator(); - drillModal.hidden = false; - if (state.drillView === "groupOnlineStats") { - drillModalTitle.textContent = "集团项目上线统计"; - drillModalBody.innerHTML = renderGroupOnlineStatsModal(); - return; - } - if (state.drillView === "enterpriseProjectList") { - drillModalTitle.textContent = `${state.enterpriseName} · 项目清单`; - drillModalBody.innerHTML = renderEnterpriseProjectListModal(state.drillListType); - return; - } - drillModalTitle.textContent = `${indicator.name} · 指标穿透分析`; - if (state.drillView === "project") { - drillModalBody.innerHTML = renderIndicatorProjectView(); - return; - } - drillModalBody.innerHTML = renderIndicatorTrendView(); - } diff --git a/server.js b/server.js new file mode 100644 index 0000000..a37ccca --- /dev/null +++ b/server.js @@ -0,0 +1,71 @@ +const http = require("http"); +const https = require("https"); +const fs = require("fs"); +const path = require("path"); + +const port = Number(process.env.PORT) || 18080; +const root = __dirname; +const apiOrigin = "https://qhse.cncecoa.com"; +const mimeTypes = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".pdf": "application/pdf", + ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" +}; + +function proxyApi(request, response) { + const target = new URL(request.url, apiOrigin); + const headers = { ...request.headers, host: target.host }; + delete headers.origin; + delete headers.referer; + + const proxyRequest = https.request(target, { + method: request.method, + headers + }, (proxyResponse) => { + response.writeHead(proxyResponse.statusCode || 502, proxyResponse.headers); + proxyResponse.pipe(response); + }); + + proxyRequest.on("error", (error) => { + response.writeHead(502, { "Content-Type": "application/json; charset=utf-8" }); + response.end(JSON.stringify({ code: 0, message: `接口代理失败:${error.message}`, data: null })); + }); + request.pipe(proxyRequest); +} + +function serveStatic(request, response) { + const requestPath = decodeURIComponent(new URL(request.url, "http://localhost").pathname); + const relativePath = requestPath === "/" ? "indexV2.html" : requestPath.replace(/^\/+/, ""); + const filePath = path.resolve(root, relativePath); + if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) { + response.writeHead(403); + response.end("Forbidden"); + return; + } + + fs.stat(filePath, (statError, stat) => { + if (statError || !stat.isFile()) { + response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Not Found"); + return; + } + response.writeHead(200, { + "Content-Type": mimeTypes[path.extname(filePath).toLowerCase()] || "application/octet-stream", + "Cache-Control": "no-cache" + }); + fs.createReadStream(filePath).pipe(response); + }); +} + +http.createServer((request, response) => { + if (request.url.startsWith("/qhse_webapi/")) { + proxyApi(request, response); + return; + } + serveStatic(request, response); +}).listen(port, "127.0.0.1", () => { + console.log(`数据治理看板:http://127.0.0.1:${port}/indexV2.html`); +});
指标名称是否启用今日(${TODAY})昨日(${PREVIOUS_DAY})前日(${beforeDay})最后更新是否预警是否异常对比今日昨日三天前一周前一个月前三个月前
${row.name}${row.enabled ? '已启用' : '未启用'}${row.todayValue}${row.yesterdayValue}${row.beforeValue}${row.lastUpdate}${row.isWarning ? '是' : '否'}${row.Name || row.name || ""}${row.IsAbnormal ?? row.isAbnormal ? '异常' : '正常'}${row.Compare || row.compare || "-"}${row.NowData ?? row.nowData ?? "-"}${row.YesterdayData ?? row.yesterdayData ?? "-"}${row.ThreeDaysAgoData ?? row.threeDaysAgoData ?? "-"}${row.WeekAgoData ?? row.weekAgoData ?? "-"}${row.MonthAgoData ?? row.monthAgoData ?? "-"}${row.ThreeMonthsAgoData ?? row.threeMonthsAgoData ?? "-"}