commit d2543779513c6c9b297f03fbdbc93f90e5e5f392 Author: fei550 <1420031550@qq.com> Date: Mon Jun 22 10:48:35 2026 +0800 初始提交 diff --git a/indexV2.html b/indexV2.html new file mode 100644 index 0000000..d8919f5 --- /dev/null +++ b/indexV2.html @@ -0,0 +1,77 @@ + + + + + + 集团数据治理项目监控大屏 V2 + + + + + + + + +
+
+
+ +
+
DATA GOVERNANCE COMMAND CENTER
+

集团数据治理项目监控大屏

+
+
+
+
+ 数据溯源表 +
+ 数据时间 2026-04-27 + 对比日期 2026-04-26 +
+
+
+
+ + +
+
+ + + +
+
+
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + + + + + + diff --git a/res/css/indexV2.base.css b/res/css/indexV2.base.css new file mode 100644 index 0000000..933e914 --- /dev/null +++ b/res/css/indexV2.base.css @@ -0,0 +1,320 @@ + :root { + --bg: #031b34; + --bg-deep: #001327; + --panel: rgba(9, 55, 91, 0.86); + --panel-strong: rgba(13, 74, 115, 0.92); + --panel-soft: rgba(18, 91, 132, 0.42); + --line: rgba(93, 191, 255, 0.32); + --line-strong: rgba(99, 213, 255, 0.64); + --text: #e8f7ff; + --muted: #92b9d1; + --blue: #5d93ff; + --cyan: #49d6ff; + --green: #5ee1b5; + --yellow: #ffc857; + --orange: #ff8b3d; + --red: #ff5c66; + --shadow: 0 18px 50px rgba(0, 8, 20, 0.42); + --radius: 18px; + --font: "Microsoft YaHei", "Source Han Sans SC", "PingFang SC", sans-serif; + --num: "DIN Alternate", "Bahnschrift", "Microsoft YaHei", sans-serif; + } + + * { + box-sizing: border-box; + } + + html, + body { + width: 100vw; + height: 100vh; + margin: 0; + background: + radial-gradient(circle at 14% 8%, rgba(29, 126, 177, 0.38), transparent 30%), + radial-gradient(circle at 86% 12%, rgba(31, 95, 155, 0.28), transparent 34%), + linear-gradient(180deg, #02152b 0%, #062b4a 48%, #031b34 100%); + color: var(--text); + font-family: var(--font); + overflow: hidden; + } + + button, + input, + select { + font-family: inherit; + } + + .screen { + position: absolute; + left: 0; + top: 0; + width: 1920px; + height: 1080px; + min-height: 1080px; + margin: 0; + padding: 14px 18px 18px; + overflow: hidden; + isolation: isolate; + transform-origin: left top; + } + + .screen::before { + content: ""; + position: absolute; + inset: 0; + z-index: -2; + background-image: + linear-gradient(rgba(89, 185, 255, 0.06) 1px, transparent 1px), + linear-gradient(90deg, rgba(89, 185, 255, 0.05) 1px, transparent 1px); + background-size: 100% 28px, 28px 100%; + opacity: 0.8; + pointer-events: none; + } + + .screen::after { + content: ""; + position: absolute; + inset: auto 0 0; + height: 38vh; + z-index: -1; + background: linear-gradient(180deg, transparent, rgba(0, 11, 23, 0.62)); + pointer-events: none; + } + + .topbar { + display: grid; + grid-template-columns: 1fr auto; + gap: 14px; + align-items: start; + margin-bottom: 8px; + } + + .title-zone { + display: flex; + align-items: center; + gap: 16px; + min-width: 0; + } + + .brand-mark { + position: relative; + width: 52px; + height: 52px; + border-radius: 16px; + background: + linear-gradient(135deg, rgba(85, 214, 255, 0.96), rgba(45, 110, 255, 0.72)), + linear-gradient(180deg, rgba(255, 255, 255, 0.36), transparent); + box-shadow: 0 0 30px rgba(73, 214, 255, 0.34), inset 0 0 18px rgba(255, 255, 255, 0.18); + overflow: hidden; + flex: 0 0 auto; + } + + .brand-mark::before, + .brand-mark::after { + content: ""; + position: absolute; + border-radius: 999px; + background: rgba(255, 255, 255, 0.86); + } + + .brand-mark::before { + width: 30px; + height: 8px; + left: 14px; + top: 16px; + box-shadow: 0 14px 0 rgba(255, 255, 255, 0.76), 0 28px 0 rgba(255, 255, 255, 0.56); + } + + .brand-mark::after { + width: 8px; + height: 8px; + right: 13px; + top: 16px; + box-shadow: 0 14px 0 rgba(255, 255, 255, 0.76), 0 28px 0 rgba(255, 255, 255, 0.56); + } + + .title-text { + min-width: 0; + } + + .eyebrow { + color: var(--cyan); + font-size: 13px; + letter-spacing: 0.18em; + text-transform: uppercase; + margin-bottom: 4px; + } + + h1 { + margin: 0; + font-size: clamp(26px, 2.6vw, 42px); + line-height: 1.1; + letter-spacing: 0.04em; + text-shadow: 0 0 22px rgba(83, 202, 255, 0.44); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .purpose { + margin-top: 6px; + color: var(--muted); + font-size: 13px; + line-height: 1.5; + } + + .header-actions { + display: grid; + gap: 8px; + justify-items: end; + } + + .top-meta-row { + display: flex; + align-items: center; + gap: 10px; + justify-content: flex-end; + } + + .source-download { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 36px; + padding: 8px 14px; + border: 1px solid rgba(83, 191, 255, 0.42); + border-radius: 14px; + color: #e7f8ff; + background: linear-gradient(180deg, rgba(27, 140, 208, 0.86), rgba(10, 90, 145, 0.86)); + box-shadow: 0 0 18px rgba(73, 214, 255, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.14); + font-size: 13px; + font-weight: 900; + text-decoration: none; + white-space: nowrap; + } + + .source-download:hover { + filter: brightness(1.12); + } + + .time-card { + display: flex; + align-items: center; + gap: 14px; + padding: 8px 12px; + border: 1px solid var(--line); + border-radius: 14px; + background: rgba(4, 28, 52, 0.72); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); + color: var(--muted); + font-size: 13px; + } + + .time-card strong { + color: var(--text); + font-family: var(--num); + letter-spacing: 0.04em; + } + + .toggle-row { + display: flex; + gap: 10px; + justify-content: flex-end; + align-items: center; + } + + .dimension-toggle, + .system-toggle, + .view-switch { + display: inline-flex; + padding: 4px; + border: 1px solid rgba(80, 165, 235, 0.28); + border-radius: 14px; + background: rgba(3, 20, 39, 0.74); + box-shadow: inset 0 1px 12px rgba(0, 0, 0, 0.18); + } + + .dimension-toggle button, + .system-toggle button, + .view-switch button { + border: 0; + border-radius: 10px; + padding: 8px 14px; + color: #a9cbe0; + background: transparent; + cursor: pointer; + font-size: 14px; + font-weight: 700; + transition: 0.18s ease; + } + + .dimension-toggle button.active, + .system-toggle button.active, + .view-switch button.active { + color: white; + background: linear-gradient(180deg, #1b8cd0, #0a5a91); + box-shadow: 0 0 18px rgba(73, 214, 255, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.18); + } + + .dimension-toggle button:hover, + .system-toggle button:hover, + .view-switch button:hover { + color: white; + } + + .crumb-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + margin: 8px 0 10px; + } + + .breadcrumbs { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + color: var(--muted); + font-size: 13px; + } + + .crumb { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 11px; + border: 1px solid rgba(87, 184, 255, 0.2); + border-radius: 999px; + background: rgba(6, 35, 62, 0.64); + } + + .crumb.clickable { + cursor: pointer; + color: #bfeaff; + } + + .crumb.clickable:hover { + border-color: var(--line-strong); + color: white; + } + + .quick-actions { + display: flex; + gap: 10px; + align-items: center; + } + + .ghost-btn { + border: 1px solid rgba(83, 191, 255, 0.34); + color: #c7e9ff; + background: rgba(8, 43, 76, 0.62); + border-radius: 12px; + padding: 9px 13px; + cursor: pointer; + font-weight: 700; + } + + .ghost-btn:hover { + background: rgba(18, 93, 142, 0.72); + } diff --git a/res/css/indexV2.cards.css b/res/css/indexV2.cards.css new file mode 100644 index 0000000..9bcd8e3 --- /dev/null +++ b/res/css/indexV2.cards.css @@ -0,0 +1,306 @@ + .kpi-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 10px; + } + + .kpi-card, + .warning-card, + .panel, + .mini-panel { + position: relative; + border: 1px solid rgba(91, 180, 255, 0.26); + border-radius: var(--radius); + background: + linear-gradient(180deg, rgba(24, 91, 132, 0.88), rgba(4, 33, 63, 0.9)), + repeating-linear-gradient(0deg, rgba(108, 205, 255, 0.04), rgba(108, 205, 255, 0.04) 1px, transparent 1px, transparent 9px); + box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.08); + overflow: hidden; + } + + .kpi-card::before, + .warning-card::before, + .panel::before, + .mini-panel::before { + content: ""; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 20% 0%, rgba(87, 214, 255, 0.18), transparent 32%), + radial-gradient(circle at 90% 20%, rgba(78, 127, 255, 0.14), transparent 28%); + pointer-events: none; + } + + .kpi-card { + min-height: 104px; + padding: 13px 14px 11px; + } + + .kpi-label, + .warning-label { + position: relative; + color: #c4e7ff; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.02em; + margin-bottom: 7px; + } + + .kpi-value { + position: relative; + display: flex; + align-items: baseline; + gap: 6px; + font-family: var(--num); + font-size: 30px; + font-weight: 900; + line-height: 1; + color: white; + text-shadow: 0 0 20px rgba(88, 210, 255, 0.34); + } + + .kpi-unit { + color: var(--muted); + font-size: 14px; + font-weight: 700; + } + + .kpi-desc { + position: relative; + margin-top: 8px; + color: #9fc6db; + font-size: 12px; + line-height: 1.45; + } + + .kpi-trend { + position: absolute; + right: 14px; + top: 15px; + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 8px; + border-radius: 999px; + background: rgba(0, 20, 39, 0.45); + color: #b8f4df; + font-size: 12px; + font-family: var(--num); + } + + .kpi-trend.down { + color: #ffb9aa; + } + + .risk-band { + position: relative; + margin-top: 8px; + height: 7px; + border-radius: 999px; + background: rgba(121, 185, 220, 0.18); + overflow: hidden; + } + + .risk-band span { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--green), var(--cyan)); + } + + .warning-strip { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 10px; + } + + .focus-row { + display: grid; + grid-template-columns: 430px minmax(0, 1fr) 430px; + gap: 10px; + margin-bottom: 10px; + align-items: stretch; + } + + .focus-indicators { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + } + + .warning-card { + min-height: 82px; + padding: 10px 12px; + border-color: rgba(255, 139, 61, 0.32); + } + + .warning-card.high { + border-color: rgba(255, 92, 102, 0.48); + } + + .warning-card.high::after { + content: ""; + position: absolute; + inset: 0 0 auto; + height: 3px; + background: linear-gradient(90deg, var(--red), var(--orange)); + } + + .warning-name { + position: relative; + min-height: 24px; + color: #ffb46f; + font-size: 15px; + font-weight: 900; + line-height: 1.25; + } + + .warning-lines { + position: relative; + display: grid; + gap: 2px; + margin-top: 5px; + color: #c7e7f8; + font-size: 12px; + line-height: 1.36; + } + + .warning-lines strong { + color: white; + font-family: var(--num); + font-size: 14px; + } + + .focus-row .warning-card { + min-height: 94px; + padding: 9px 10px; + } + + .focus-row .warning-name { + min-height: 22px; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .focus-row .warning-lines { + gap: 1px; + font-size: 11px; + } + + .focus-row .warning-lines strong { + font-size: 13px; + } + + .enterprise-trend-panel { + min-height: 150px; + margin-bottom: 10px; + } + + .enterprise-trend-panel .panel-header { + margin-bottom: 2px; + } + + .enterprise-trend-panel .chart-box { + overflow-x: hidden; + } + + .indicator-strip { + display: grid; + grid-template-columns: repeat(8, minmax(0, 1fr)) 84px; + gap: 10px; + margin-bottom: 10px; + align-items: stretch; + } + + .indicator-strip .warning-card { + min-height: 90px; + padding: 9px 10px; + } + + .indicator-strip .warning-name { + min-height: 34px; + font-size: 13px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .indicator-strip .warning-lines { + gap: 1px; + font-size: 11px; + line-height: 1.3; + } + + .indicator-strip .warning-lines strong { + font-size: 12px; + } + + .indicator-more-cell { + position: relative; + z-index: 1; + display: grid; + place-items: center; + min-height: 90px; + border: 1px solid rgba(91, 214, 255, 0.36); + border-radius: var(--radius); + background: rgba(4, 35, 64, 0.72); + box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.08); + } + + .trend-panel.group-trend-panel { + min-height: 150px; + margin-bottom: 10px; + } + + .trend-panel.group-trend-panel .panel-header { + margin-bottom: 2px; + } + + .trend-panel.group-trend-panel .chart-box { + overflow-x: hidden; + } + + .group-bottom-grid { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.65fr); + gap: 12px; + align-items: stretch; + } + + .group-left-stack { + display: grid; + grid-template-rows: 1fr 1fr; + gap: 10px; + min-height: 388px; + } + + .group-left-stack .panel { + min-height: 189px; + } + + .group-left-stack .chart-box { + overflow-y: hidden; + } + + .usage-report-panel .table-wrap { + max-height: 136px; + } + + .usage-report-panel table { + min-width: 620px; + font-size: 12px; + } + + .usage-report-panel th, + .usage-report-panel td { + padding: 7px 9px; + } + + .usage-report-panel .panel-header { + margin-bottom: 6px; + } diff --git a/res/css/indexV2.detail.css b/res/css/indexV2.detail.css new file mode 100644 index 0000000..2d00a34 --- /dev/null +++ b/res/css/indexV2.detail.css @@ -0,0 +1,101 @@ + .metric-cards { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 12px; + } + + .mini-panel { + padding: 14px; + min-height: 116px; + } + + .mini-panel[data-action] { + cursor: pointer; + } + + .mini-panel[data-action]:hover { + border-color: rgba(91, 214, 255, 0.52); + filter: brightness(1.08); + } + + .mini-label { + position: relative; + color: #a8cbe1; + font-size: 13px; + margin-bottom: 8px; + } + + .mini-value { + position: relative; + font-family: var(--num); + font-size: 30px; + line-height: 1; + color: white; + font-weight: 900; + } + + .mini-desc { + position: relative; + margin-top: 10px; + color: #94bad1; + font-size: 12px; + } + + .project-layout { + display: grid; + grid-template-columns: minmax(320px, 0.72fr) minmax(0, 1.28fr); + gap: 12px; + align-items: start; + } + + .field-list { + position: relative; + z-index: 1; + display: grid; + gap: 9px; + } + + .field-row { + display: grid; + grid-template-columns: 112px 1fr; + gap: 12px; + padding: 11px 12px; + border-radius: 13px; + background: rgba(0, 18, 37, 0.36); + border: 1px solid rgba(91, 180, 255, 0.14); + } + + .field-name { + color: #91bdd5; + font-size: 13px; + } + + .field-value { + color: white; + font-weight: 800; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .empty-state { + position: relative; + z-index: 1; + min-height: 220px; + display: grid; + place-items: center; + color: #9ec2da; + border: 1px dashed rgba(115, 198, 255, 0.25); + border-radius: 14px; + background: rgba(0, 16, 31, 0.24); + } + + .explain { + position: relative; + z-index: 1; + margin-top: 10px; + color: #9fc2d9; + font-size: 12px; + line-height: 1.5; + } diff --git a/res/css/indexV2.modal-table.css b/res/css/indexV2.modal-table.css new file mode 100644 index 0000000..b30057a --- /dev/null +++ b/res/css/indexV2.modal-table.css @@ -0,0 +1,351 @@ + .drill-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) repeat(3, 190px); + gap: 12px; + margin-bottom: 12px; + } + + .drill-title-card { + position: relative; + min-height: 118px; + padding: 18px; + border: 1px solid rgba(91, 180, 255, 0.28); + border-radius: var(--radius); + background: + linear-gradient(135deg, rgba(13, 80, 126, 0.95), rgba(3, 26, 53, 0.92)), + repeating-linear-gradient(0deg, rgba(108, 205, 255, 0.04), rgba(108, 205, 255, 0.04) 1px, transparent 1px, transparent 9px); + box-shadow: var(--shadow); + overflow: hidden; + } + + .drill-title-card h2 { + position: relative; + margin: 0; + font-size: 28px; + line-height: 1.2; + } + + .drill-title-card p { + position: relative; + margin: 10px 0 0; + color: #a9cfe5; + font-size: 13px; + line-height: 1.5; + } + + .drill-card { + position: relative; + min-height: 118px; + padding: 14px; + border: 1px solid rgba(91, 180, 255, 0.24); + border-radius: var(--radius); + background: linear-gradient(180deg, rgba(19, 84, 126, 0.88), rgba(3, 32, 61, 0.92)); + box-shadow: var(--shadow); + } + + .drill-card-label { + color: #a8cbe1; + font-size: 13px; + } + + .drill-card-value { + margin-top: 10px; + font-family: var(--num); + font-size: 32px; + font-weight: 900; + color: white; + } + + .drill-grid { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(420px, 0.8fr); + gap: 12px; + align-items: stretch; + } + + .drill-grid.equal { + grid-template-columns: 1fr 1fr; + } + + .drill-panel { + min-height: 690px; + } + + .rank-list { + position: relative; + z-index: 1; + display: grid; + gap: 9px; + max-height: 600px; + overflow-y: auto; + padding-right: 4px; + } + + .rank-row { + display: grid; + grid-template-columns: 42px 1fr 94px; + gap: 12px; + align-items: center; + padding: 10px 12px; + border: 1px solid rgba(91, 180, 255, 0.16); + border-radius: 13px; + background: rgba(1, 21, 42, 0.42); + cursor: pointer; + } + + .rank-row:hover { + border-color: rgba(91, 214, 255, 0.46); + background: rgba(12, 62, 98, 0.62); + } + + .rank-row:not([data-indicator-enterprise]) { + cursor: default; + } + + .rank-index { + width: 30px; + height: 30px; + display: inline-grid; + place-items: center; + border-radius: 10px; + background: rgba(93, 147, 255, 0.2); + color: #dff7ff; + font-family: var(--num); + font-weight: 900; + } + + .rank-name { + color: #f0fbff; + font-weight: 900; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .rank-bars { + display: grid; + gap: 6px; + margin-top: 7px; + } + + .rank-value { + text-align: right; + font-family: var(--num); + color: white; + font-size: 18px; + font-weight: 900; + } + + .drill-modal[hidden] { + display: none; + } + + .drill-modal { + position: fixed; + inset: 0; + z-index: 100; + display: grid; + place-items: center; + padding: 34px; + background: rgba(0, 10, 24, 0.72); + backdrop-filter: blur(8px); + } + + .drill-modal-panel { + width: min(1780px, calc(100vw - 68px)); + height: min(960px, calc(100vh - 68px)); + border: 1px solid rgba(91, 214, 255, 0.44); + border-radius: 24px; + background: + linear-gradient(180deg, rgba(8, 49, 83, 0.98), rgba(1, 18, 36, 0.98)), + repeating-linear-gradient(0deg, rgba(108, 205, 255, 0.04), rgba(108, 205, 255, 0.04) 1px, transparent 1px, transparent 9px); + box-shadow: 0 28px 80px rgba(0, 0, 0, 0.52), inset 0 1px 0 rgba(255, 255, 255, 0.1); + overflow: hidden; + display: grid; + grid-template-rows: auto 1fr; + } + + .drill-modal-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 14px 18px; + border-bottom: 1px solid rgba(91, 180, 255, 0.22); + background: linear-gradient(180deg, rgba(22, 102, 151, 0.72), rgba(4, 38, 72, 0.72)); + } + + .drill-modal-title { + color: #f2fbff; + font-size: 20px; + font-weight: 900; + letter-spacing: 0.02em; + } + + .drill-modal-actions { + display: flex; + align-items: center; + gap: 10px; + } + + .modal-close { + width: 34px; + height: 34px; + border: 1px solid rgba(147, 221, 255, 0.38); + border-radius: 999px; + color: white; + background: rgba(5, 41, 76, 0.84); + cursor: pointer; + font-size: 20px; + line-height: 1; + } + + .modal-close:hover { + background: rgba(24, 117, 166, 0.9); + } + + .drill-modal-body { + padding: 16px; + overflow: auto; + } + + .drill-modal-body .drill-panel { + min-height: 600px; + } + + .drill-modal-body .rank-list { + max-height: 510px; + } + + .panel-tools { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 10px; + } + + .more-btn { + border: 1px solid rgba(91, 214, 255, 0.42); + border-radius: 999px; + padding: 7px 13px; + color: #e3f8ff; + background: linear-gradient(180deg, rgba(28, 153, 217, 0.82), rgba(5, 72, 118, 0.86)); + cursor: pointer; + font-size: 12px; + font-weight: 900; + box-shadow: 0 0 16px rgba(73, 214, 255, 0.18); + } + + .more-btn:hover { + filter: brightness(1.12); + } + + .table-status { + position: relative; + z-index: 1; + margin-bottom: 10px; + padding: 8px 12px; + border: 1px solid rgba(91, 214, 255, 0.28); + border-radius: 8px; + color: #dff7ff; + background: rgba(4, 35, 64, 0.72); + font-size: 13px; + font-weight: 700; + } + + .table-status.error { + border-color: rgba(255, 92, 102, 0.4); + color: #ffd6d9; + background: rgba(99, 24, 36, 0.42); + } + + table { + width: 100%; + border-collapse: collapse; + min-width: 760px; + color: #dcefff; + font-size: 13px; + } + + th { + position: sticky; + top: 0; + z-index: 2; + padding: 11px 10px; + background: linear-gradient(180deg, rgba(28, 153, 217, 0.96), rgba(5, 72, 118, 0.96)); + color: white; + text-align: left; + border-bottom: 1px solid rgba(147, 221, 255, 0.38); + white-space: nowrap; + } + + td { + padding: 10px; + border-bottom: 1px solid rgba(147, 221, 255, 0.12); + color: #cfe7f6; + white-space: nowrap; + } + + tr:hover td { + background: rgba(39, 126, 178, 0.16); + } + + tr.summary-row td { + color: #ffffff; + font-weight: 900; + background: rgba(73, 214, 255, 0.14); + } + + tr.green-row td { + color: #d9fff1; + background: rgba(94, 225, 181, 0.18); + } + + tr.clickable-row { + cursor: pointer; + } + + .num { + font-family: var(--num); + color: white; + font-size: 15px; + font-weight: 800; + } + + .delta-up { + color: #7df5c5; + } + + .delta-down { + color: #ff9d94; + } + + .status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 58px; + padding: 4px 8px; + border-radius: 999px; + font-size: 12px; + font-weight: 800; + } + + .status-pill.ok { + color: #bdf9e4; + background: rgba(94, 225, 181, 0.14); + border: 1px solid rgba(94, 225, 181, 0.36); + } + + .status-pill.warn { + color: #ffe2ad; + background: rgba(255, 200, 87, 0.14); + border: 1px solid rgba(255, 200, 87, 0.36); + } + + .status-pill.bad { + color: #ffc2c7; + background: rgba(255, 92, 102, 0.14); + border: 1px solid rgba(255, 92, 102, 0.36); + } diff --git a/res/css/indexV2.panels.css b/res/css/indexV2.panels.css new file mode 100644 index 0000000..381e082 --- /dev/null +++ b/res/css/indexV2.panels.css @@ -0,0 +1,318 @@ + .dashboard-grid { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.65fr); + gap: 12px; + align-items: stretch; + } + + .dashboard-grid.group-overview { + align-items: stretch; + } + + .dashboard-grid.enterprise-overview { + grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.65fr); + gap: 12px; + } + + .dashboard-grid.enterprise-overview .panel { + min-height: 290px; + } + + .dashboard-grid.enterprise-overview .risk-summary { + gap: 7px; + max-height: 226px; + overflow-y: auto; + padding-right: 4px; + } + + .dashboard-grid.enterprise-overview .risk-item { + grid-template-columns: 64px 1fr auto; + gap: 9px; + padding: 7px 9px; + } + + .dashboard-grid.enterprise-overview .mini-bar { + grid-template-columns: 42px 1fr 38px; + gap: 6px; + font-size: 11px; + } + + .dashboard-grid.enterprise-overview .mini-track { + height: 5px; + } + + .dashboard-grid.equal { + grid-template-columns: 1fr 1fr; + } + + .subgrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-top: 12px; + } + + .panel { + min-height: 360px; + padding: 16px; + } + + .panel.compact { + min-height: 270px; + } + + .panel.tall { + min-height: 450px; + } + + .trend-panel { + min-height: 198px; + padding: 12px 14px; + } + + .trend-panel .panel-header { + margin-bottom: 4px; + } + + .panel-header { + position: relative; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + margin-bottom: 10px; + z-index: 1; + } + + .panel-title { + margin: 0; + color: #f1fbff; + font-size: 20px; + font-weight: 900; + letter-spacing: 0.02em; + } + + .panel-note { + margin-top: 6px; + color: #9fc2d9; + font-size: 12px; + line-height: 1.45; + } + + .legend { + display: flex; + flex-wrap: wrap; + gap: 12px; + justify-content: flex-end; + color: #a9cde2; + font-size: 12px; + white-space: nowrap; + } + + .legend-item { + display: inline-flex; + align-items: center; + gap: 6px; + } + + .dot { + width: 10px; + height: 10px; + border-radius: 3px; + display: inline-block; + } + + .chart-box { + position: relative; + z-index: 1; + overflow-x: auto; + overflow-y: hidden; + scrollbar-color: rgba(86, 151, 239, 0.72) rgba(30, 89, 130, 0.42); + scrollbar-width: thin; + padding-bottom: 2px; + } + + .chart-box::-webkit-scrollbar, + .table-wrap::-webkit-scrollbar { + height: 9px; + width: 9px; + } + + .chart-box::-webkit-scrollbar-thumb, + .table-wrap::-webkit-scrollbar-thumb { + background: rgba(78, 145, 226, 0.74); + border-radius: 999px; + } + + .chart-box::-webkit-scrollbar-track, + .table-wrap::-webkit-scrollbar-track { + background: rgba(22, 69, 105, 0.42); + border-radius: 999px; + } + + svg { + display: block; + max-width: none; + font-family: var(--font); + } + + .axis { + stroke: rgba(155, 211, 247, 0.28); + stroke-width: 1; + } + + .grid-line { + stroke: rgba(155, 211, 247, 0.16); + stroke-width: 1; + } + + .axis-label { + fill: #a8c7db; + font-size: 12px; + } + + .bar-label, + .point-label { + fill: #dff7ff; + font-size: 12px; + font-family: var(--num); + font-weight: 700; + } + + .chart-click { + cursor: pointer; + transition: 0.16s ease; + } + + .chart-click:hover { + filter: brightness(1.2) drop-shadow(0 0 8px rgba(91, 217, 255, 0.46)); + } + + .risk-summary { + position: relative; + z-index: 1; + display: grid; + gap: 10px; + } + + .risk-item { + display: grid; + grid-template-columns: 72px 1fr auto; + gap: 12px; + align-items: center; + padding: 10px 11px; + border: 1px solid rgba(91, 180, 255, 0.14); + border-radius: 13px; + background: rgba(1, 21, 42, 0.42); + cursor: pointer; + } + + .risk-item:hover { + border-color: rgba(91, 214, 255, 0.46); + background: rgba(12, 62, 98, 0.62); + } + + .risk-panel { + min-height: 450px; + height: 100%; + } + + .risk-panel .risk-summary { + gap: 8px; + max-height: 366px; + overflow-y: auto; + padding-right: 4px; + } + + .risk-panel .risk-item { + grid-template-columns: 64px 1fr auto; + gap: 10px; + padding: 8px 10px; + } + + .risk-panel .mini-bar { + grid-template-columns: 42px 1fr 40px; + gap: 6px; + font-size: 11px; + } + + .risk-panel .mini-track { + height: 5px; + } + + .risk-panel .risk-badge { + padding: 4px 8px; + font-size: 11px; + } + + .risk-name { + color: #e8f8ff; + font-weight: 800; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .risk-bars { + display: grid; + gap: 6px; + } + + .mini-bar { + display: grid; + grid-template-columns: 52px 1fr 34px; + gap: 8px; + align-items: center; + color: #9fc4dc; + font-size: 12px; + } + + .mini-track { + height: 7px; + border-radius: 999px; + background: rgba(126, 184, 219, 0.16); + overflow: hidden; + } + + .mini-track span { + display: block; + height: 100%; + border-radius: inherit; + } + + .risk-badge { + padding: 5px 9px; + border-radius: 999px; + color: white; + font-size: 12px; + font-weight: 800; + white-space: nowrap; + } + + .risk-badge.red { + background: rgba(255, 92, 102, 0.22); + border: 1px solid rgba(255, 92, 102, 0.42); + color: #ffc2c7; + } + + .risk-badge.orange { + background: rgba(255, 139, 61, 0.2); + border: 1px solid rgba(255, 139, 61, 0.44); + color: #ffd1ad; + } + + .risk-badge.green { + background: rgba(94, 225, 181, 0.16); + border: 1px solid rgba(94, 225, 181, 0.42); + color: #bdf9e4; + } + + .table-wrap { + position: relative; + z-index: 1; + overflow: auto; + max-height: 545px; + border: 1px solid rgba(91, 180, 255, 0.18); + border-radius: 14px; + background: rgba(1, 18, 36, 0.34); + } diff --git a/res/css/indexV2.responsive.css b/res/css/indexV2.responsive.css new file mode 100644 index 0000000..6b17be1 --- /dev/null +++ b/res/css/indexV2.responsive.css @@ -0,0 +1,42 @@ + @media (max-width: 1px) { + .kpi-grid, + .warning-strip { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .dashboard-grid, + .dashboard-grid.equal, + .project-layout { + grid-template-columns: 1fr; + } + } + + @media (max-width: 1px) { + .screen { + padding: 14px; + } + + .topbar { + grid-template-columns: 1fr; + } + + .header-actions { + justify-items: start; + } + + h1 { + white-space: normal; + } + + .kpi-grid, + .warning-strip, + .metric-cards, + .subgrid { + grid-template-columns: 1fr; + } + + .crumb-row { + align-items: flex-start; + flex-direction: column; + } + } diff --git a/res/js/indexV2.charts.js b/res/js/indexV2.charts.js new file mode 100644 index 0000000..9b992c8 --- /dev/null +++ b/res/js/indexV2.charts.js @@ -0,0 +1,180 @@ + /** + * SVG 图表渲染模块。 + * 所有图表都返回字符串,由视图层直接写入 chart-box;交互点通过 data-enterprise/data-project 交给事件代理处理。 + */ + function renderGroupedRateChart(list, mode, options = {}) { + const width = Math.max(1180, list.length * 88 + 120); + const height = options.height || 350; + const margin = { top: 20, right: 30, bottom: height < 240 ? 58 : 98, left: 54 }; + const chartHeight = height - margin.top - margin.bottom; + const chartWidth = width - margin.left - margin.right; + const groupWidth = chartWidth / Math.max(list.length, 1); + const barWidth = Math.min(height < 240 ? 12 : 16, groupWidth / 5); + const series = [ + { key: "onlineRate", label: "上线", color: COLORS.online }, + { key: "usageRate", label: "使用", color: COLORS.usage }, + { key: "accuracyRate", label: "准确", color: COLORS.accuracy } + ]; + const grid = [0, 25, 50, 75, 100].map((tick) => { + const y = margin.top + chartHeight - (tick / 100) * chartHeight; + return `${tick}%`; + }).join(""); + + const bars = list.map((enterprise, index) => { + const stats = enterprise.stats[state.dimension]; + const center = margin.left + groupWidth * index + groupWidth / 2; + const label = enterprise.name; + const rects = series.map((item, seriesIndex) => { + const value = stats[item.key]; + const barHeight = (value / 100) * chartHeight; + const x = center - barWidth * 1.8 + seriesIndex * barWidth * 1.8; + const y = margin.top + chartHeight - barHeight; + return ` + + ${Math.round(value)} + `; + }).join(""); + return ` + ${rects} + ${label} + `; + }).join(""); + + return ` + + ${grid} + + ${bars} + + `; + } + + /** + * 项目使用率/准确率柱状图。 + * 柱子用于进入项目明细,底部圆点表达项目是否已上线,便于在企业层同时查看使用和上线状态。 + */ + function renderProjectRateChart(projects, options = {}) { + const width = Math.max(1100, projects.length * 126 + 120); + const height = options.height || 350; + const margin = { top: 20, right: 30, bottom: height < 260 ? 66 : 98, left: 54 }; + const chartHeight = height - margin.top - margin.bottom; + const chartWidth = width - margin.left - margin.right; + const groupWidth = chartWidth / Math.max(projects.length, 1); + const barWidth = Math.min(20, groupWidth / 4); + const series = [ + { key: "usageRate", color: COLORS.usage }, + { key: "accuracyRate", color: COLORS.accuracy } + ]; + const grid = [0, 25, 50, 75, 100].map((tick) => { + const y = margin.top + chartHeight - (tick / 100) * chartHeight; + return `${tick}%`; + }).join(""); + + const bars = projects.map((project, index) => { + const center = margin.left + groupWidth * index + groupWidth / 2; + const shortName = project.name.replace(project.unit, "").replace("项目", ""); + const rects = series.map((item, seriesIndex) => { + const value = project[item.key]; + const barHeight = (value / 100) * chartHeight; + const x = center - barWidth * 1.05 + seriesIndex * barWidth * 2.1; + const y = margin.top + chartHeight - barHeight; + return ` + + ${Math.round(value)} + `; + }).join(""); + const statusY = height - (height < 260 ? 48 : 78); + const statusColor = project.created ? COLORS.cyan : COLORS.warning; + const statusText = project.created ? "已上线" : "未上线"; + return ` + ${rects} + + ${statusText} + ${shortName} + `; + }).join(""); + + return ` + + ${grid} + + ${bars} + + `; + } + + /** + * 集团趋势加权平均。 + * 按企业项目数加权汇总各企业七日趋势,避免项目少的企业对集团曲线影响过大。 + */ + 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); + } + + /** + * 通用三率折线图。 + * data 需要包含 online/usage/accuracy 三组七日数组,适用于集团趋势和企业趋势。 + */ + function renderTrendChart(data, width = 720, height = 250) { + 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 series = [ + { key: "online", label: "上线", color: COLORS.online }, + { key: "usage", label: "使用", color: COLORS.usage }, + { key: "accuracy", label: "准确", color: COLORS.accuracy } + ]; + const grid = [0, 25, 50, 75, 100].map((tick) => { + const y = margin.top + chartHeight - (tick / 100) * chartHeight; + return `${tick}%`; + }).join(""); + const lines = series.map((item) => { + const values = data[item.key] || []; + const points = values.map((value, index) => { + const x = margin.left + (chartWidth / (values.length - 1)) * index; + const y = margin.top + chartHeight - (value / 100) * chartHeight; + return `${x},${y}`; + }).join(" "); + const circles = values.map((value, index) => { + const x = margin.left + (chartWidth / (values.length - 1)) * index; + const y = margin.top + chartHeight - (value / 100) * chartHeight; + return ``; + }).join(""); + return `${circles}`; + }).join(""); + const labels = dates.map((date, index) => { + const x = margin.left + (chartWidth / (dates.length - 1)) * index; + return `${date}`; + }).join(""); + + return ` + + ${grid} + + ${lines} + ${labels} + + `; + } diff --git a/res/js/indexV2.config.js b/res/js/indexV2.config.js new file mode 100644 index 0000000..766c383 --- /dev/null +++ b/res/js/indexV2.config.js @@ -0,0 +1,376 @@ + /** + * 看板全局配置模块。 + * 这里集中维护日期、外链、接口注册、兜底数据和运行时状态,其他 JS 模块只读取这些常量。 + * 新增业务接口时优先注册到 apiRegistry,再通过 core 模块的统一请求方法调用,避免在视图层散落接口地址和 token。 + */ + const TODAY = "2026-04-27"; + const PREVIOUS_DAY = "2026-04-26"; + 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/"; + /** + * 接口注册表。 + * 当前集团项目上线率卡片和“集团项目上线统计”弹窗依赖 projectMasterDataAnalysis。 + * Header 中仅保留后端要求的 token 和 Accept,浏览器侧不主动写 Host/Cookie/Connection 等受限头。 + */ + const apiRegistry = { + projectMasterDataAnalysis: { + method: "GET", + url: "https://qhse.cncecoa.com/qhse_webapi/api/Common/GetProjectMasterDataAnalysis?unitId=", + 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 用于视图层展示“接口数据/兜底数据/加载中”的状态差异。 + */ + const projectMasterDataState = { + loading: false, + loaded: false, + error: "", + rows: usageReportRows, + summary: null, + updatedAt: "" + }; + /** + * 单位一览表文件名映射。 + * 弹窗“预览”按钮只通过单位简称查表,不直接拼猜文件名,避免单位顺序或文件命名变化时打开错误附件。 + */ + const usageReportFiles = {"\u516d\u5316\u5efa":"10\u3001\u516d\u5316\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u4e03\u5316\u5efa":"11\u3001\u4e03\u5316\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u4e5d\u5316\u5efa":"12\u3001\u4e5d\u5316\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u5341\u4e00\u5316\u5efa":"13\u3001\u5341\u4e00\u5316\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u5341\u4e09\u5316\u5efa":"14\u3001\u5341\u4e09\u5316\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u5341\u56db\u5316\u5efa":"15\u3001\u5341\u56db\u5316\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u5341\u516d\u5316\u5efa":"16\u3001\u5341\u516d\u5316\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u571f\u6728":"17\u3001\u571f\u6728~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u88c5\u5907":"18\u3001\u88c5\u5907~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u534e\u8c0a":"19\u3001\u534e\u8c0a~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u5929\u8fb0":"1\u3001\u5929\u8fb0~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u91cd\u673a":"21\u3001\u91cd\u673a~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u5317\u5efa\u6295":"22\u3001\u5317\u5efa\u6295~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u5357\u5efa\u6295":"23\u3001\u5357\u5efa\u6295~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u57ce\u6295":"24\u3001\u57ce\u6295~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u4ea4\u5efa":"25\u3001\u4ea4\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u897f\u5357":"27\u3001\u897f\u5357~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u8d5b\u9f0e":"2\u3001\u8d5b\u9f0e~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u4e1c\u534e":"3\u3001\u4e1c\u534e~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u4e94\u73af":"4\u3001\u4e94\u73af~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u534e\u9646":"5\u3001\u534e\u9646~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u6210\u8fbe":"6\u3001\u6210\u8fbe~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u4e8c\u5316\u5efa":"7\u3001\u4e8c\u5316\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u4e09\u5316\u5efa":"8\u3001\u4e09\u5316\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx","\u56db\u5316\u5efa":"9\u3001\u56db\u5316\u5efa~\u4e3b\u6570\u636e\u9879\u76ee\u4e0eQHSE\u9879\u76ee\u4e00\u89c8\u88682026.6.3.xlsx"}; + + /** + * 图表和状态标签共用色板。 + * 图表模块只引用这里的语义色,不在 SVG 字符串中硬编码业务颜色。 + */ + const COLORS = { + online: "#5d93ff", + usage: "#5ee1b5", + accuracy: "#ffc857", + warning: "#ff5c66", + cyan: "#49d6ff", + 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,并影响可见页签与指标库。 + */ + const systemLabels = { + safety: "安全", + quality: "质量", + emergency: "应急" + }; + + const systemTypeMap = { + 1: "safety", + 2: "quality", + 3: "emergency" + }; + + /** + * 从 URL 解析外部系统允许展示的页签。 + * 支持 type、types、system、systems,也兼容纯查询串写法;非法值会回退为全部页签。 + */ + function getAllowedSystemsFromUrl() { + const params = new URLSearchParams(window.location.search); + const rawValue = params.get("type") || params.get("types") || params.get("system") || params.get("systems") || window.location.search.replace(/^\?/, ""); + if (!rawValue) return Object.values(systemTypeMap); + const allowed = rawValue + .split(/[,\s,]+/) + .map((item) => systemTypeMap[item.trim()]) + .filter(Boolean); + return allowed.length ? [...new Set(allowed)] : Object.values(systemTypeMap); + } + + 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。 + */ + const state = { + view: "group", + dimension: "all", + system: "safety", + enterpriseName: "", + projectId: "", + selectedIndicator: "", + drillOpen: false, + drillView: "trend", + drillEnterpriseName: "", + drillListType: "" + }; diff --git a/res/js/indexV2.core.js b/res/js/indexV2.core.js new file mode 100644 index 0000000..2f4c93b --- /dev/null +++ b/res/js/indexV2.core.js @@ -0,0 +1,469 @@ + const app = document.getElementById("app"); + /** + * 核心工具与数据加工模块。 + * 这里不直接拼整页 HTML,主要负责 DOM 引用、通用格式化、接口请求、集团汇总和项目/指标派生数据。 + */ + const breadcrumbs = document.getElementById("breadcrumbs"); + const screen = document.querySelector(".screen"); + const drillModal = document.getElementById("drillModal"); + const drillModalBody = document.getElementById("drillModalBody"); + const drillModalTitle = document.getElementById("drillModalTitle"); + + /** + * 根据 URL 参数过滤系统页签。 + * 如果当前 state.system 被过滤掉,会自动切到允许列表第一个系统,保证后续取指标池时有有效 key。 + */ + function applySystemVisibility() { + document.querySelectorAll("[data-system]").forEach((button) => { + const visible = allowedSystems.includes(button.dataset.system); + button.hidden = !visible; + }); + if (!allowedSystems.includes(state.system)) { + state.system = allowedSystems[0] || "safety"; + } + } + + /** + * 按 1920x1080 设计稿等比缩放整屏。 + * 缩放只改外层 transform,内部图表和布局仍按固定设计尺寸计算,避免大屏和小屏下比例漂移。 + */ + function scaleScreen() { + const designWidth = 1920; + const designHeight = 1080; + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const scale = Math.min(viewportWidth / designWidth, viewportHeight / designHeight); + const left = Math.max((viewportWidth - designWidth * scale) / 2, 0); + const top = Math.max((viewportHeight - designHeight * scale) / 2, 0); + screen.style.transform = `translate(${left}px, ${top}px) scale(${scale})`; + } + + function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); + } + + function fmtPercent(value) { + return `${Number(value).toFixed(1).replace(".0", "")}%`; + } + + function fmtNumber(value) { + return Number(value).toLocaleString("zh-CN"); + } + + function toSafeNumber(value) { + const number = Number(value); + return Number.isFinite(number) ? number : 0; + } + + function formatApiDate(value) { + if (!value) return ""; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return String(value).split("T")[0] || ""; + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; + } + + /** + * 归一化集团主数据接口行。 + * 后端字段目前是 PascalCase,这里兼容 camelCase,避免接口层大小写调整导致前端表格直接断掉。 + */ + function normalizeProjectMasterData(item, index) { + return { + sortIndex: toSafeNumber(item.SortIndex ?? item.sortIndex ?? index + 1), + name: item.Abbreviation || item.abbreviation || "", + id: item.Id || item.id || "", + unitId: item.UnitId || item.unitId || "", + 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), + linkedTotal: toSafeNumber(item.ZhengChang ?? item.zhengChang), + pendingTotal: toSafeNumber(item.DaiJian ?? item.daiJian), + noNeedTotal: toSafeNumber(item.BuYong ?? item.buYong), + preProjectTotal: toSafeNumber(item.YuLiXiang ?? item.yuLiXiang) + }; + } + + /** + * 汇总接口返回的单位行。 + * 结果同时服务集团上线率卡片和弹窗合计行,所以这里保留主数据、QHSE、申请不用、已关联等完整口径。 + */ + function summarizeProjectMasterData(rows) { + return rows.reduce((summary, row) => { + summary.masterProjectCount += row.masterTotal; + summary.noNeedProjectCount += row.noNeedTotal; + summary.qhseProjectCount += row.qhseTotal; + summary.linkedTotal += row.linkedTotal; + summary.pendingTotal += row.pendingTotal; + summary.preProjectTotal += row.preProjectTotal; + summary.total += row.total; + return summary; + }, { + masterProjectCount: 0, + noNeedProjectCount: 0, + qhseProjectCount: 0, + linkedTotal: 0, + pendingTotal: 0, + preProjectTotal: 0, + total: 0 + }); + } + + /** + * 把接口对象转换为弹窗表格二维数组。 + * 视图层按数组渲染表格,最后追加“合计”行,保持接口数据和本地兜底数据结构一致。 + */ + 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; + } + + /** + * 读取弹窗表格行。 + * 接口尚未返回或加载失败时直接回退本地 usageReportRows,保证弹窗始终有可展示数据。 + */ + function getProjectMasterRows() { + return projectMasterDataState.rows && projectMasterDataState.rows.length + ? projectMasterDataState.rows + : usageReportRows; + } + + /** + * 读取集团上线率卡片所需的三个核心数。 + * 接口成功时优先用 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 + }; + } + + /** + * 从接口注册表生成可请求配置。 + * 所有查询参数统一在这里拼接,调用方只传业务参数,避免重复处理空值和 URL 编码。 + */ + function getRegisteredApiConfig(name, params = {}) { + const api = apiRegistry[name]; + if (!api) throw new Error(`未注册接口:${name}`); + const url = new URL(api.url); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== "") { + url.searchParams.set(key, value); + } + }); + return { + method: api.method || "GET", + url: url.toString(), + headers: api.headers || {} + }; + } + + /** + * 统一接口请求入口。 + * 使用 fetch 直接请求已注册接口;如果浏览器跨域或后端异常,会抛给上层进入兜底流程。 + */ + async function requestRegisteredApi(name, params = {}) { + const config = getRegisteredApiConfig(name, params); + const response = await fetch(config.url, { + method: config.method, + headers: config.headers, + cache: "no-store" + }); + if (!response.ok) { + throw new Error(`接口请求失败:${response.status}`); + } + return response.json(); + } + + /** + * 加载集团主数据在建项目分析接口。 + * 成功后更新 projectMasterDataState.rows/summary 并触发整页重渲染; + * 失败时记录错误并恢复本地兜底数据,让主卡片和统计弹窗仍可用。 + */ + async function loadProjectMasterDataAnalysis(unitId = "") { + projectMasterDataState.loading = true; + projectMasterDataState.error = ""; + try { + const result = await requestRegisteredApi("projectMasterDataAnalysis", { unitId }); + if (toSafeNumber(result.code) !== 1 || !Array.isArray(result.data)) { + throw new Error(result.message || "接口返回数据格式异常"); + } + const normalizedRows = result.data.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.loaded = true; + projectMasterDataState.updatedAt = new Date().toISOString(); + } catch (error) { + projectMasterDataState.error = error.message || "接口请求失败"; + projectMasterDataState.summary = null; + projectMasterDataState.rows = usageReportRows; + console.error("集团项目上线统计接口请求失败", error); + } finally { + projectMasterDataState.loading = false; + } + render(); + } + + function dimLabel() { + return state.dimension === "all" ? "所有项目" : "新开工项目"; + } + + function systemLabel() { + return systemLabels[state.system] || "安全"; + } + + function getWarningIndicators() { + return warningIndicatorMap[state.system] || warningIndicatorMap.safety; + } + + function getSelectedIndicator() { + const indicators = getWarningIndicators(); + return indicators.find((item) => item.name === state.selectedIndicator) || indicators[0]; + } + + function indicatorAccuracy(item) { + return item.accuracyRate ?? 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 + }; + }); + } + + /** + * 生成“指标-项目”维度的穿透数据。 + * 三日值是否不变会影响 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 + }; + }); + } + + 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; + } + + /** + * 集团总览汇总口径。 + * 上线率三个数来自主数据接口状态;使用率、准确率、预警项仍来自企业基础数据的加权派生。 + */ + function groupSummary() { + const active = enterprises.filter((enterprise) => enterprise.stats[state.dimension].projectCount > 0); + const projectMasterSummary = getProjectMasterSummary(); + const masterProjectCount = projectMasterSummary.masterProjectCount; + const noNeedProjectCount = projectMasterSummary.noNeedProjectCount; + const qhseProjectCount = 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 + }; + } + + /** + * 风险排序分值。 + * 使用率和准确率权重较高,同时叠加预警项目数和预警项数,让列表优先暴露“用了但数据质量差”的单位。 + */ + 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 ? "中" : "低" + }); + } + 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))) : "-"; + return { + name: indicator.name, + enabled, + todayValue, + yesterdayValue, + beforeValue, + lastUpdate: enabled ? dateMinus(lagDays) : "-", + lagDays: enabled ? lagDays : "-", + isWarning + }; + }); + } + + function statusClassByRate(rate) { + if (rate >= 80) return "ok"; + if (rate >= 65) return "warn"; + return "bad"; + } + + function statusTextByRate(rate) { + if (rate >= 80) return "正常"; + if (rate >= 65) return "关注"; + return "异常"; + } diff --git a/res/js/indexV2.enterprise.js b/res/js/indexV2.enterprise.js new file mode 100644 index 0000000..469b998 --- /dev/null +++ b/res/js/indexV2.enterprise.js @@ -0,0 +1,209 @@ + /** + * 企业级页面渲染模块。 + * 负责企业总览、企业指标分析、项目图表和项目清单弹窗,数据来源主要是 core 模块按企业派生的项目样本。 + */ + function renderEnterpriseView() { + const enterprise = getEnterprise(state.enterpriseName); + const stats = getStats(enterprise); + const projects = generateProjects(enterprise, state.dimension); + const indicators = buildEnterpriseIndicatorItems(getWarningIndicators(), projects, stats); + + // 新开工维度下部分企业没有项目,单独展示空态,避免图表按空数组渲染出异常坐标。 + if (!stats.projectCount) { + app.innerHTML = ` +
+
+
+

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

+
当前维度暂无项目数据,可切换到“所有项目”查看企业总体情况。
+
+
+
当前企业在「${dimLabel()}」维度暂无数据。
+
+ `; + return; + } + + 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), "", "数据准确情况")} +
+ +
+ ${renderFocusIndicators(indicators.slice(0, 8), 0, { highlightHigh: false })} +
+ +
+
+ +
+
+
+

企业三率趋势曲线

+
近 7 日${enterprise.name}上线率、使用率、准确率变化。
+
+
+ 上线 + 使用 + 准确 +
+
+
${renderTrendChart({ + online: enterprise.trend.online, + usage: enterprise.trend.usage, + accuracy: enterprise.trend.accuracy + }, 1840, 112)}
+
+ +
+
+
+
+

${enterprise.name} · 项目分析

+
项目级不统计上线率,底部圆点表示是否上线;点击项目下钻到字段追踪。
+
+
+ 使用率 + 准确率 + 已上线 +
+
+
${renderProjectRateChart(projects, { height: 210 })}
+
企业层用于回答:本单位哪些项目未上线、哪些项目上线后未使用指标、哪些项目数据连续未更新。
+
+ +
+
+
+

指标分析

+
按${enterprise.name}指标预警项目数、使用率和数据状态展示责任短板。
+
+
+ +
+
+ ${renderEnterpriseIndicatorAnalysis(indicators)} +
+
+ `; + } + + /** + * 企业页顶部小指标卡。 + * action 不为空时会写入 data-action,事件代理据此打开项目清单或未上线项目清单弹窗。 + */ + function miniMetric(label, value, unit, desc, action = "") { + return ` +
+
${label}
+
${value}${unit}
+
${desc}
+
+ `; + } + + /** + * 企业项目风险表。 + * 每行带 data-project,点击后切换到项目明细页;表格主要用于从企业层快速定位异常项目。 + */ + function renderProjectTable(projectRiskList) { + return ` +
+ + + + + + + + + + + + + + ${projectRiskList.map(({ project }) => ` + + + + + + + + + + `).join("")} + +
项目名称是否上线使用率准确率预警项最近更新风险
${project.name}${project.created ? '已上线' : '未上线'}${fmtPercent(project.usageRate)}${fmtPercent(project.accuracyRate)}${project.warningCount}${project.lastUpdate}${riskPill(project.risk)}
+
+ `; + } + + /** + * 企业项目清单弹窗。 + * type=offline 时只展示未建档/未上线项目;其他情况展示当前维度下所有生成项目。 + */ + function renderEnterpriseProjectListModal(type) { + const enterprise = getEnterprise(state.enterpriseName); + const projects = generateProjects(enterprise, state.dimension); + const rows = type === "offline" + ? projects.filter((project) => !project.created) + : projects; + const title = type === "offline" ? "未上线项目清单" : "所有项目清单"; + const emptyText = type === "offline" ? "当前企业暂无未上线项目。" : "当前企业暂无项目。"; + if (!rows.length) { + return ` +
+
+
+

${enterprise.name} · ${title}

+
+
+
${emptyText}
+
+ `; + } + return ` +
+
+
+

${enterprise.name} · ${title}

+
点击项目行进入项目级看板。
+
+
+
+ + + + + + + + + + + + + + + ${rows.map((project) => ` + + + + + + + + + + + `).join("")} + +
项目名称是否上线使用率准确率预警项最近更新责任人风险
${project.name}${project.created ? '已上线' : '未上线'}${fmtPercent(project.usageRate)}${fmtPercent(project.accuracyRate)}${project.warningCount}${project.lastUpdate}${project.owner}${riskPill(project.risk)}
+
+
+ `; + } diff --git a/res/js/indexV2.events.js b/res/js/indexV2.events.js new file mode 100644 index 0000000..8db60c4 --- /dev/null +++ b/res/js/indexV2.events.js @@ -0,0 +1,177 @@ + /** + * 全局事件模块。 + * 页面大部分内容由模板字符串动态生成,因此统一使用事件代理监听 click,再按 data-* 属性更新 state 或打开外链。 + */ + document.addEventListener("click", (event) => { + // 维度切换会影响企业、项目、指标所有派生数据;项目页切换维度后回到企业页,防止旧项目 id 失效。 + const dimButton = event.target.closest("[data-dim]"); + if (dimButton) { + state.dimension = dimButton.dataset.dim; + if (state.view === "project") { + state.view = "enterprise"; + state.projectId = ""; + } + if (state.drillOpen && state.drillView === "project") { + state.drillView = "trend"; + } + render(); + return; + } + + // 系统切换后需要校验当前选中指标是否仍属于新系统,不属于则切换到该系统第一个指标。 + const systemButton = event.target.closest("[data-system]"); + if (systemButton) { + if (!allowedSystems.includes(systemButton.dataset.system)) return; + state.system = systemButton.dataset.system; + if (!getWarningIndicators().some((item) => item.name === state.selectedIndicator)) { + state.selectedIndicator = getWarningIndicators()[0]?.name || ""; + } + render(); + return; + } + + // 显式操作按钮:外链预览、弹窗开关、面包屑返回都通过 data-action 分发。 + const action = event.target.closest("[data-action]"); + if (action) { + if (action.dataset.action === "more-indicators") { + window.open(INDICATOR_MORE_URL, "_blank", "noopener,noreferrer"); + return; + } + if (action.dataset.action === "enterprise-more-indicators") { + 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]; + if (!fileName) { + window.alert(`${unit || "该单位"}未上传一览表`); + return; + } + const fileUrl = `${REPORT_FILE_BASE_URL}${encodeURIComponent(fileName)}`; + window.open(`${OFFICE_PREVIEW_BASE_URL}${encodeURIComponent(fileUrl)}`, "_blank", "noopener,noreferrer"); + return; + } + if (action.dataset.action === "group-online-stats") { + state.drillOpen = true; + state.drillView = "groupOnlineStats"; + state.drillEnterpriseName = ""; + state.drillListType = ""; + state.projectId = ""; + renderDrillModal(); + return; + } + if (action.dataset.action === "close-drill") { + state.drillOpen = false; + state.drillView = "trend"; + state.drillEnterpriseName = ""; + state.drillListType = ""; + renderDrillModal(); + return; + } + if (action.dataset.action === "enterprise-project-list") { + state.drillOpen = true; + state.drillView = "enterpriseProjectList"; + state.drillListType = "all"; + state.projectId = ""; + renderDrillModal(); + return; + } + if (action.dataset.action === "enterprise-offline-list") { + state.drillOpen = true; + state.drillView = "enterpriseProjectList"; + state.drillListType = "offline"; + state.projectId = ""; + renderDrillModal(); + return; + } + if (action.dataset.action === "indicator-trend") { + state.drillOpen = true; + state.drillView = "trend"; + state.drillEnterpriseName = ""; + state.projectId = ""; + state.drillListType = ""; + renderDrillModal(); + return; + } + if (action.dataset.action === "group") { + state.view = "group"; + state.enterpriseName = ""; + state.projectId = ""; + } + if (action.dataset.action === "enterprise") { + state.view = "enterprise"; + state.enterpriseName = action.dataset.enterprise || state.enterpriseName; + state.projectId = ""; + } + render(); + return; + } + + // 指标卡点击:集团页打开指标趋势,企业页打开该企业下的项目指标穿透。 + const indicatorTarget = event.target.closest("[data-indicator]"); + if (indicatorTarget) { + state.selectedIndicator = indicatorTarget.dataset.indicator; + state.drillOpen = true; + if (state.view === "enterprise") { + state.drillView = "project"; + state.drillEnterpriseName = state.enterpriseName; + } else { + state.drillView = "trend"; + state.drillEnterpriseName = ""; + } + state.projectId = ""; + renderDrillModal(); + return; + } + + // 指标排行中的企业点击:保留当前指标,切换穿透企业,弹窗内容更新为项目维度明细。 + const indicatorEnterpriseTarget = event.target.closest("[data-indicator-enterprise]"); + if (indicatorEnterpriseTarget) { + state.drillEnterpriseName = indicatorEnterpriseTarget.dataset.indicatorEnterprise; + state.drillOpen = true; + state.drillView = "project"; + state.projectId = ""; + renderDrillModal(); + return; + } + + // 企业点击:从集团图表、表格或面包屑进入企业看板。 + const enterpriseTarget = event.target.closest("[data-enterprise]"); + if (enterpriseTarget) { + state.view = "enterprise"; + state.enterpriseName = enterpriseTarget.dataset.enterprise; + state.projectId = ""; + render(); + return; + } + + // 项目点击:进入项目级看板,并关闭当前弹窗,避免弹窗遮挡项目详情。 + const projectTarget = event.target.closest("[data-project]"); + if (projectTarget) { + state.view = "project"; + state.projectId = projectTarget.dataset.project; + state.drillOpen = false; + state.drillView = "trend"; + state.drillListType = ""; + render(); + } + }); + + /** + * 初始化顺序: + * 先绑定缩放并渲染兜底数据,再异步加载集团主数据接口; + * 接口返回后 loadProjectMasterDataAnalysis 会再次 render,刷新集团上线率卡片和统计弹窗。 + */ + window.addEventListener("resize", scaleScreen); + if (window.visualViewport) { + window.visualViewport.addEventListener("resize", scaleScreen); + } + + scaleScreen(); + render(); + loadProjectMasterDataAnalysis(); diff --git a/res/js/indexV2.group.js b/res/js/indexV2.group.js new file mode 100644 index 0000000..cafce6e --- /dev/null +++ b/res/js/indexV2.group.js @@ -0,0 +1,393 @@ + function kpiCard(label, value, unit, desc, trend, widthPercent, tone = "up") { + /** + * 集团级页面渲染模块。 + * 负责集团总览、集团上线率卡片、企业分析、指标分析和集团上线统计弹窗。 + */ + return ` +
+
${trend}
+
${label}
+
${value}${unit}
+
+
${desc}
+
+ `; + } + + /** + * 集团项目上线率卡片。 + * 数值来自 groupSummary,而 groupSummary 会优先读取 projectMasterDataState.summary; + * 接口失败时 desc 展示错误原因,同时数值回退到本地兜底汇总。 + */ + function onlineKpiCard(summary) { + const trendText = projectMasterDataState.loading ? "加载中" : `${summary.onlineRate.toFixed(2)}%`; + const statusText = projectMasterDataState.error ? `接口失败,已使用本地兜底数据:${projectMasterDataState.error}` : "QHSE使用项目数 / 申请不用项目数 / 主数据项目数"; + return ` +
+
${trendText}
+
集团项目上线率
+
${summary.onlineActual}/${summary.noNeedProjectCount}/${summary.onlineExpected}
+
+
${statusText}
+
+ `; + } + + /** + * 面包屑渲染。 + * 根据 state.view 组合集团、企业、项目、指标穿透层级,并附带当前维度和系统,方便用户确认筛选上下文。 + */ + function renderBreadcrumbs() { + const parts = [`集团总览`]; + if (state.view === "enterprise" || state.view === "project") { + parts.push(`›${state.enterpriseName}`); + } + if (state.view === "project") { + const project = getProjectById(state.projectId); + parts.push(`›${project ? project.name : "项目明细"}`); + } + if (state.view === "indicatorTrend" || state.view === "indicatorEnterprise" || state.view === "indicatorProject") { + const indicator = getSelectedIndicator(); + parts.push(`›${indicator.name}`); + } + if (state.view === "indicatorProject") { + parts.push(`›${state.enterpriseName}`); + } + parts.push(`当前维度:${dimLabel()}`); + parts.push(`当前系统:${systemLabel()}`); + breadcrumbs.innerHTML = parts.join(""); + } + + /** + * 全局主渲染入口。 + * 事件模块只改 state,然后调用 render;这里集中决定当前应该渲染哪个视图,避免分散刷新局部 DOM。 + */ + function render() { + applySystemVisibility(); + document.querySelectorAll("[data-dim]").forEach((button) => { + button.classList.toggle("active", button.dataset.dim === state.dimension); + }); + document.querySelectorAll("[data-system]").forEach((button) => { + button.classList.toggle("active", button.dataset.system === state.system); + }); + renderBreadcrumbs(); + if (state.view === "group") renderGroupView(); + if (state.view === "enterprise") renderEnterpriseView(); + if (state.view === "project") renderProjectView(); + if (state.view === "indicatorTrend") app.innerHTML = renderIndicatorTrendView(); + if (state.view === "indicatorProject") app.innerHTML = renderIndicatorProjectView(); + renderDrillModal(); + } + + /** + * 集团总览主视图。 + * 顶部卡片展示集团汇总,中部图表支持企业下钻,底部指标区支持指标穿透和集团报告预览。 + */ + 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); + + 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")} +
+ +
+ ${renderFocusIndicators(indicators.slice(0, 8), 0, { highlightHigh: false })} +
+ +
+
+ +
+
+
+

集团三率趋势曲线

+
近 7 日集团上线率、使用率、准确率变化。
+
+
+ 上线 + 使用 + 准确 +
+
+
${renderGroupTrendChart(activeEnterprises)}
+
+ +
+
+
+
+
+

企业分析

+
按${dimLabel()}展示${systemLabel()}系统的企业上线率、使用率、准确率。点击企业柱子下钻到企业看板。
+
+
+ 上线率 + 使用率 + 准确率 +
+
+
${renderGroupedRateChart(activeEnterprises, "enterprise", { height: 132 })}
+
+ +
+
+
+

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

+
+
+ +
+
+
+
+
+
+
+

指标分析

+
+
+ +
+
+ ${renderIndicatorAnalysis(indicators)} +
+
+ `; + } + + /** + * 顶部重点指标条。 + * 每张指标卡都带 data-indicator,事件代理会据此打开指标趋势或企业内项目穿透弹窗。 + */ + function renderFocusIndicators(items, offset, options = {}) { + const highlightHigh = options.highlightHigh !== false; + return items.map((item, index) => ` +
+
${item.name}
+
+ 指标使用率:${item.usageRate}% + 指标准确率:${indicatorAccuracy(item)}% +
+
+ `).join(""); + } + + /** + * 指标分析中的迷你进度条。 + * value 控制条宽,displayValue 控制右侧展示文案,便于同一组件同时展示百分比和项目数量。 + */ + function miniRate(label, value, color, displayValue = `${Math.round(value)}%`) { + return ` +
+ ${label} + + ${displayValue} +
+ `; + } + + /** + * 集团指标分析列表。 + * 以项目覆盖量、使用率、准确率三条迷你进度条展示指标风险,用于快速挑出需要穿透的指标。 + */ + function renderIndicatorAnalysis(indicators) { + const maxProjects = Math.max(...indicators.map((item) => item.projects), 1); + 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("")} +
+ `; + } + + /** + * 企业页指标分析数据派生。 + * 在集团指标基础上叠加当前企业使用率和项目数量,生成更贴近企业维度的指标预警项。 + */ + 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] + }; + }); + } + + /** + * 企业指标分析列表。 + * 与集团指标列表共用展示结构,但风险文案更偏整改和跟踪,用于企业页面的责任短板展示。 + */ + function renderEnterpriseIndicatorAnalysis(items) { + const maxProjects = Math.max(...items.map((item) => item.projects), 1); + 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("")} +
+ `; + } + + function renderEnterpriseTable(riskList) { + return ` +
+ + + + + + + + + + + + + + + ${riskList.map(({ enterprise, stats, score }) => { + const level = riskLevel(score); + return ` + + + + + + + + + + + `; + }).join("")} + +
企业项目数上线率使用率准确率预警项目责任人状态
${enterprise.name}${stats.projectCount}${fmtPercent(stats.onlineRate)}${fmtPercent(stats.usageRate)}${fmtPercent(stats.accuracyRate)}${stats.warningProjects}${enterprise.owner}${level.text}
+
+ `; + } + + 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 + }; + }); + } + + /** + * 集团上线统计弹窗表格。 + * 表格行统一从 getProjectMasterRows 读取:接口成功展示接口行,接口失败展示本地兜底行。 + * 国际、生态单位在这里加 green-row,高亮规则集中在表格渲染层。 + */ + function renderUsageReportTable(list) { + const rows = getProjectMasterRows(); + const headers = projectMasterDataState.loaded ? projectMasterDataHeaders : usageReportHeaders; + const statusText = projectMasterDataState.loading + ? `
正在获取接口数据...
` + : projectMasterDataState.error + ? `
接口请求失败,当前展示本地兜底数据:${projectMasterDataState.error}
` + : ""; + 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("")} + + + `; + }).join("")} + +
${header}操作
${cell}${unit && rowClass.indexOf("summary-row") === -1 ? `` : ""}
+
+ `; + } + + /** + * “集团项目上线统计”弹窗内容。 + * 弹窗标题由 project 模块 renderDrillModal 设置,这里只负责面板主体和表格。 + */ + function renderGroupOnlineStatsModal() { + const activeEnterprises = enterprises.filter((enterprise) => enterprise.stats[state.dimension].projectCount > 0); + return ` +
+
+
+

集团项目上线统计

+
+
+ ${renderUsageReportTable(activeEnterprises)} +
+ `; + } + diff --git a/res/js/indexV2.indicators.js b/res/js/indexV2.indicators.js new file mode 100644 index 0000000..7247e74 --- /dev/null +++ b/res/js/indexV2.indicators.js @@ -0,0 +1,265 @@ + /** + * 指标穿透页面渲染模块。 + * 包含集团维度的指标趋势、企业排行,以及企业维度的项目指标明细弹窗。 + */ + function renderIndicatorTrendView() { + const indicator = getSelectedIndicator(); + const enterpriseMetrics = 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); + + return ` +
+
+

${indicator.name}

+
+
+
指标使用率
+
${avgUsage}%
+
+
+
指标准确率
+
0%
+
+
+
预警企业
+
${warningEnterprises}家
+
+
+ +
+
+
+
+

指标变化曲线

+
X轴为日期,Y轴为该指标值,用于发现指标波动、停滞和异常断点。
+
+
+
${renderIndicatorValueTrend(indicator)}
+
+ +
+
+
+

指标企业排行

+
展示该指标使用较好的企业,点击企业进入项目级指标明细。
+
+
+
+ ${positive.map((item, index) => renderEnterpriseRankRow(item, index)).join("")} +
+
+
+ `; + } + + /** + * 企业下的指标项目穿透视图。 + * 根据当前选中指标和 drillEnterpriseName 生成项目明细,用于定位哪些项目该指标停滞或未启用。 + */ + 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; + + return ` +
+
+

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

+
+
+
项目数量
+
${projectMetrics.length}个
+
+
+
平均使用率
+
${avgUsage}%
+
+
+
预警项目
+
${warningCount}个
+
+
+ +
+
+
+

项目指标分析

+
三日数值未变化的项目指标判定为预警。
+
+
+ ${renderIndicatorProjectTable(projectMetrics)} +
+ `; + } + + /** + * 指标七日变化 SVG。 + * 这里用当前指标覆盖项目数生成稳定趋势线,主要表达波动、停滞和异常断点的展示结构。 + */ + function renderIndicatorValueTrend(indicator) { + 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 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) => { + const x = margin.left + (chartWidth / (values.length - 1)) * index; + const y = margin.top + chartHeight - (value / maxValue) * chartHeight; + return `${x},${y}`; + }).join(" "); + const grid = tickValues.map((tick) => { + const y = margin.top + chartHeight - (tick / maxValue) * chartHeight; + return `${tick}`; + }).join(""); + const circles = values.map((value, index) => { + const x = margin.left + (chartWidth / (values.length - 1)) * index; + const y = margin.top + chartHeight - (value / maxValue) * chartHeight; + return `${value}`; + }).join(""); + const labels = dates.map((date, index) => { + const x = margin.left + (chartWidth / (dates.length - 1)) * index; + return `${date}`; + }).join(""); + + return ` + + ${grid} + + + ${circles} + ${labels} + + `; + } + + /** + * 指标企业排行行。 + * clickable 为 true 时写入 data-indicator-enterprise,点击后进入该企业的项目维度指标明细。 + */ + function renderEnterpriseRankRow(item, index, clickable = true) { + return ` +
+
${index + 1}
+
+
${item.enterprise.name}
+
+ ${miniRate("使用", item.usageRate, COLORS.usage)} + ${miniRate("准确", item.accuracyRate, COLORS.accuracy)} +
+
+
${item.usageRate}%
+
+ `; + } + + /** + * 指标项目明细表。 + * 今日、昨日、前日三列用于判断指标是否连续不变,warning 字段决定“是否预警”标签。 + */ + function renderIndicatorProjectTable(items) { + const beforeDay = dateMinus(2); + return ` +
+ + + + + + + + + + + + + + ${items.map((item) => ` + + + + + + + + + + `).join("")} + +
项目名称是否启用今日(${TODAY})昨日(${PREVIOUS_DAY})前日(${beforeDay})最后更新是否预警
${item.project.name}${item.enabled ? '已启用' : '未启用'}${item.todayValue}${item.yesterdayValue}${item.beforeValue}${item.lastUpdate}${item.warning ? '是' : '否'}
+
+ `; + } + + /** + * 项目信息字段行。 + * title 保留完整值,防止长项目名在窄列中被截断后无法查看全文。 + */ + function fieldRow(label, value) { + return ` +
+
${label}
+
${value}
+
+ `; + } + + /** + * 风险标签。 + * 将高/中/低或未启用状态映射成统一的颜色标签,供项目表格和项目清单复用。 + */ + function riskPill(risk) { + if (risk === "高" || risk === "未启用") return `${risk}`; + if (risk === "中") return `${risk}`; + return `${risk}`; + } + + /** + * 项目级指标表。 + * 与指标项目明细表结构相近,但数据来源是当前项目的 buildIndicatorRows 结果。 + */ + function renderIndicatorTable(rows) { + const beforeDay = dateMinus(2); + return ` +
+ + + + + + + + + + + + + + ${rows.map((row) => ` + + + + + + + + + + `).join("")} + +
指标名称是否启用今日(${TODAY})昨日(${PREVIOUS_DAY})前日(${beforeDay})最后更新是否预警
${row.name}${row.enabled ? '已启用' : '未启用'}${row.todayValue}${row.yesterdayValue}${row.beforeValue}${row.lastUpdate}${row.isWarning ? '是' : '否'}
+
+ `; + } diff --git a/res/js/indexV2.project.js b/res/js/indexV2.project.js new file mode 100644 index 0000000..941cbf9 --- /dev/null +++ b/res/js/indexV2.project.js @@ -0,0 +1,78 @@ + /** + * 项目级页面渲染模块。 + * 展示单个项目的上线状态、指标启用数量、准确数量和预警指标,并列出项目下所有治理指标。 + */ + 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; + + app.innerHTML = ` +
+ ${miniMetric("项目上线状态", project.systemStatus, "", project.created ? "已创建项目档案并进入系统" : "未创建项目档案,需先完成上线")} + ${miniMetric("指标使用数量", `${enabledCount}/${rows.length}`, "", "项目内治理指标动没动")} + ${miniMetric("数据准确数量", `${accurateCount}/${rows.length}`, "", "数据准不准")} + ${miniMetric("预警指标", warningIndicatorCount, "项", "")} +
+ +
+
+
+
+

项目信息

+
+
+
+ ${fieldRow("所属企业", enterprise.name)} + ${fieldRow("项目名称", project.name)} + ${fieldRow("系统状态", project.systemStatus)} + ${fieldRow("开工日期", project.startDate)} + ${fieldRow("最近更新", project.lastUpdate)} +
+
+ +
+
+
+

指标分析

+
+
+ ${renderIndicatorTable(rows)} +
+
+ `; + } + + /** + * 统一弹窗渲染入口。 + * 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/res/report/10、六化建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/10、六化建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..0248558 Binary files /dev/null and b/res/report/10、六化建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/11、七化建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/11、七化建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..7ceaa30 Binary files /dev/null and b/res/report/11、七化建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/12、九化建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/12、九化建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..d44bbc7 Binary files /dev/null and b/res/report/12、九化建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/13、十一化建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/13、十一化建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..c864a25 Binary files /dev/null and b/res/report/13、十一化建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/14、十三化建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/14、十三化建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..9d40a0b Binary files /dev/null and b/res/report/14、十三化建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/15、十四化建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/15、十四化建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..c067130 Binary files /dev/null and b/res/report/15、十四化建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/16、十六化建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/16、十六化建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..e26d17b Binary files /dev/null and b/res/report/16、十六化建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/17、土木~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/17、土木~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..9f993f5 Binary files /dev/null and b/res/report/17、土木~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/18、装备~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/18、装备~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..3e04f24 Binary files /dev/null and b/res/report/18、装备~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/19、华谊~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/19、华谊~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..f815997 Binary files /dev/null and b/res/report/19、华谊~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/1、天辰~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/1、天辰~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..4da55f8 Binary files /dev/null and b/res/report/1、天辰~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/21、重机~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/21、重机~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..e7f51eb Binary files /dev/null and b/res/report/21、重机~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/22、北建投~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/22、北建投~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..692ef13 Binary files /dev/null and b/res/report/22、北建投~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/23、南建投~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/23、南建投~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..e406cf4 Binary files /dev/null and b/res/report/23、南建投~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/24、城投~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/24、城投~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..bc3635d Binary files /dev/null and b/res/report/24、城投~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/25、交建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/25、交建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..8e4c790 Binary files /dev/null and b/res/report/25、交建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/27、西南~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/27、西南~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..af8d53c Binary files /dev/null and b/res/report/27、西南~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/2、赛鼎~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/2、赛鼎~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..bd314fd Binary files /dev/null and b/res/report/2、赛鼎~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/3、东华~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/3、东华~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..c66fb53 Binary files /dev/null and b/res/report/3、东华~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/4、五环~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/4、五环~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..1735adc Binary files /dev/null and b/res/report/4、五环~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/5、华陆~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/5、华陆~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..772baa5 Binary files /dev/null and b/res/report/5、华陆~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/6、成达~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/6、成达~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..9d8a80a Binary files /dev/null and b/res/report/6、成达~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/7、二化建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/7、二化建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..f49bebc Binary files /dev/null and b/res/report/7、二化建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/8、三化建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/8、三化建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..861aba7 Binary files /dev/null and b/res/report/8、三化建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/9、四化建~主数据项目与QHSE项目一览表2026.6.3.xlsx b/res/report/9、四化建~主数据项目与QHSE项目一览表2026.6.3.xlsx new file mode 100644 index 0000000..c0bc5bb Binary files /dev/null and b/res/report/9、四化建~主数据项目与QHSE项目一览表2026.6.3.xlsx differ diff --git a/res/report/数据治理解决方案及数据分析报告.docx b/res/report/数据治理解决方案及数据分析报告.docx new file mode 100644 index 0000000..691513b Binary files /dev/null and b/res/report/数据治理解决方案及数据分析报告.docx differ diff --git a/res/report/数据治理解决方案及数据分析报告.pdf b/res/report/数据治理解决方案及数据分析报告.pdf new file mode 100644 index 0000000..72fbc8c Binary files /dev/null and b/res/report/数据治理解决方案及数据分析报告.pdf differ