Files
DataGovernanceDashboard/res/js/indexV2.group.js
T
2026-07-12 00:53:38 +08:00

477 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function kpiCard(label, value, unit, desc, trend, widthPercent, tone = "up") {
/**
* 集团级页面渲染模块。
* 负责集团总览、集团上线率卡片、企业分析、指标分析和集团上线统计弹窗。
*/
return `
<article class="kpi-card">
${trend ? `<div class="kpi-trend ${tone === "down" ? "down" : ""}">${trend}</div>` : ""}
<div class="kpi-label">${label}</div>
<div class="kpi-value">${value}<span class="kpi-unit">${unit}</span></div>
<div class="risk-band"><span style="width:${clamp(widthPercent, 0, 100)}%"></span></div>
<div class="kpi-desc">${desc}</div>
</article>
`;
}
function clickableKpiCard(label, value, unit, desc, action, widthPercent, trend = "") {
return `
<article class="kpi-card chart-click" data-action="${action}">
${trend ? `<div class="kpi-trend">${trend}</div>` : ""}
<div class="kpi-label">${label}</div>
<div class="kpi-value">${value}<span class="kpi-unit">${unit}</span></div>
<div class="risk-band"><span style="width:${clamp(widthPercent, 0, 100)}%"></span></div>
<div class="kpi-desc">${desc}</div>
</article>
`;
}
/**
* 集团项目上线率卡片。
* 数值只来自集团监控接口,接口失败时显示错误状态。
*/
function onlineKpiCard(summary) {
const trendText = projectMasterDataState.loading ? "加载中" : `${toSafeNumber(summary.onlineRate).toFixed(2)}%`;
const statusText = projectMasterDataState.error
? `接口请求失败:${projectMasterDataState.error}`
: "上线项目/申请不用/应使用项目/项目总数";
return `
<article class="kpi-card chart-click" data-action="group-online-stats">
<div class="kpi-trend">${trendText}</div>
<div class="kpi-label">集团项目上线率</div>
<div class="kpi-value">${summary.proInfoText || `${summary.onlineActual}/${summary.noNeedProjectCount}/${summary.onlineExpected}`}</div>
<div class="risk-band"><span style="width:${clamp(summary.onlineRate, 0, 100)}%"></span></div>
<div class="kpi-desc">${statusText}</div>
</article>
`;
}
/**
* 面包屑渲染。
* 根据 state.view 组合集团、企业、项目、指标穿透层级,并附带当前维度和系统,方便用户确认筛选上下文。
*/
function renderBreadcrumbs() {
const parts = [`<span class="crumb clickable" data-action="group">集团总览</span>`];
if (state.view === "enterprise" || state.view === "project") {
parts.push(`<span>›</span><span class="crumb clickable" data-action="enterprise" data-enterprise="${state.enterpriseName}">${state.enterpriseName}</span>`);
}
if (state.view === "project") {
const projectName = projectMonitoringState.data?.MetricProjectInfo?.ProjectShortName
|| projectMonitoringState.data?.MetricProjectInfo?.ProjectName
|| "项目明细";
parts.push(`<span>›</span><span class="crumb">${projectName}</span>`);
}
if (state.view === "indicatorTrend" || state.view === "indicatorEnterprise" || state.view === "indicatorProject") {
const indicator = getSelectedIndicator();
parts.push(`<span>›</span><span class="crumb clickable" data-action="indicator-trend">${indicator.name}</span>`);
}
if (state.view === "indicatorProject") {
parts.push(`<span>›</span><span class="crumb">${state.enterpriseName}</span>`);
}
parts.push(`<span class="crumb">当前维度:${dimLabel()}</span>`);
parts.push(`<span class="crumb">当前系统:${systemLabel()}</span>`);
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();
updateDataDateDisplay();
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 analysisIndicators = projectMasterDataState.metricDatas.length ? projectMasterDataState.metricDatas : indicators;
const activeEnterprises = getGroupEnterpriseModels();
app.innerHTML = `
<section class="kpi-grid">
${onlineKpiCard(summary)}
${clickableKpiCard("预立项项目数", fmtNumber(summary.preProjectTotal), "个", "点击查看集团项目上线统计。", "group-online-stats", clamp(summary.preProjectTotal / 2, 0, 100))}
${clickableKpiCard("待建项目数", fmtNumber(summary.pendingTotal), "个", "点击查看集团项目上线统计。", "group-online-stats", clamp(summary.pendingTotal, 0, 100))}
${clickableKpiCard("未达标项目数", fmtNumber(summary.belowStandardProjects), "个", "指标使用率低于 80% 的项目。", "below-standard-projects", clamp(100 - summary.belowStandardProjects, 0, 100))}
${clickableKpiCard("项目指标使用率", fmtPercent(summary.usageRate), "", "点击查看单位项目指标使用情况。", "group-unit-metric-usages", summary.usageRate)}
${kpiCard("项目指标准确率", fmtPercent(summary.accuracyRate), "", "", "", summary.accuracyRate)}
</section>
<section class="indicator-strip">
${renderFocusIndicators(indicators.slice(0, 8), 0, { highlightHigh: false })}
</section>
<article class="panel trend-panel group-trend-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">集团三率趋势曲线</h2>
<div class="panel-note">近 7 日集团上线率、使用率、准确率变化。</div>
</div>
<div class="legend">
<span class="legend-item"><span class="dot" style="background:${COLORS.online}"></span>上线</span>
<span class="legend-item"><span class="dot" style="background:${COLORS.usage}"></span>使用</span>
<span class="legend-item"><span class="dot" style="background:${COLORS.accuracy}"></span>准确</span>
</div>
</div>
<div class="chart-box">${renderGroupTrendChart(activeEnterprises)}</div>
</article>
<section class="group-bottom-grid">
<div class="group-left-stack">
<article class="panel">
<div class="panel-header">
<div>
<h2 class="panel-title">企业分析</h2>
<div class="panel-note">按${dimLabel()}展示${systemLabel()}系统的企业上线率、使用率、准确率。点击企业柱子下钻到企业看板。</div>
</div>
<div class="legend">
<span class="legend-item"><span class="dot" style="background:${COLORS.online}"></span>上线率</span>
<span class="legend-item"><span class="dot" style="background:${COLORS.usage}"></span>使用率</span>
<span class="legend-item"><span class="dot" style="background:${COLORS.accuracy}"></span>准确率</span>
</div>
</div>
<div class="chart-box">${activeEnterprises.length ? renderGroupedRateChart(activeEnterprises, "enterprise", { height: 132 }) : '<div class="empty-state compact-empty">暂无企业分析数据</div>'}</div>
</article>
<article class="panel usage-report-panel chart-click" data-action="group-online-stats">
<div class="panel-header">
<div>
<h2 class="panel-title">集团在建项目使用分析报告</h2>
</div>
<div class="panel-tools">
<button type="button" class="more-btn" data-action="group-online-stats">查看</button>
</div>
</div>
</article>
</div>
<article class="panel risk-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">指标分析</h2>
</div>
<div class="panel-tools">
<button type="button" class="more-btn" data-action="more-indicators">更多</button>
</div>
</div>
${renderIndicatorAnalysis(analysisIndicators)}
</article>
</section>
`;
}
/**
* 顶部重点指标条。
* 每张指标卡都带 data-indicator,事件代理会据此打开指标趋势或企业内项目穿透弹窗。
*/
function renderFocusIndicators(items, offset, options = {}) {
const highlightHigh = options.highlightHigh !== false;
return items.map((item, index) => `
<article class="warning-card chart-click ${highlightHigh && offset + index < 2 ? "high" : ""}" title="${item.name}" data-indicator="${escapeHtmlAttr(item.name)}" data-indicator-field="${escapeHtmlAttr(resolveMetricField(item.name, item.field))}">
<div class="warning-name">${item.name}</div>
<div class="warning-lines">
<span>指标使用率:<strong>${item.usageRate}%</strong></span>
<span>使用项目数:<strong>${item.projects}</strong></span>
</div>
</article>
`).join("");
}
/**
* 指标分析中的迷你进度条。
* value 控制条宽,displayValue 控制右侧展示文案,便于同一组件同时展示百分比和项目数量。
*/
function miniRate(label, value, color, displayValue = `${Math.round(value)}%`) {
return `
<div class="mini-bar">
<span>${label}</span>
<span class="mini-track"><span style="width:${value}%;background:${color}"></span></span>
<strong>${displayValue}</strong>
</div>
`;
}
/**
* 集团指标分析列表。
* 以项目覆盖量、使用率、准确率三条迷你进度条展示指标风险,用于快速挑出需要穿透的指标。
*/
function renderIndicatorAnalysis(indicators) {
const maxProjects = Math.max(...indicators.map((item) => item.projects), 1);
return `
<div class="risk-summary">
${indicators.slice(0, 8).map((item) => {
return `
<div class="risk-item" data-indicator="${escapeHtmlAttr(item.name)}" data-indicator-field="${escapeHtmlAttr(resolveMetricField(item.name, item.field))}">
<div class="risk-name">${item.name}</div>
<div class="risk-bars">
${miniRate("项目", Math.round((item.projects / maxProjects) * 100), COLORS.online, item.projects)}
${miniRate("使用", item.usageRate, COLORS.usage)}
${miniRate("准确", indicatorAccuracy(item), COLORS.accuracy)}
</div>
</div>
`;
}).join("")}
</div>
`;
}
/**
* 企业指标分析列表。
* 与集团指标列表共用展示结构,但风险文案更偏整改和跟踪,用于企业页面的责任短板展示。
*/
function renderEnterpriseIndicatorAnalysis(items) {
const maxProjects = Math.max(...items.map((item) => item.projects), 1);
return `
<div class="risk-summary">
${items.map((item) => {
return `
<div class="risk-item" data-indicator="${escapeHtmlAttr(item.name)}" data-indicator-field="${escapeHtmlAttr(resolveMetricField(item.name, item.field))}">
<div class="risk-name">${item.name}</div>
<div class="risk-bars">
${miniRate("项目", Math.round((item.projects / maxProjects) * 100), COLORS.online, item.projects)}
${miniRate("使用", item.usageRate, COLORS.usage)}
${miniRate("准确", item.accuracyRate, COLORS.accuracy)}
</div>
</div>
`;
}).join("")}
</div>
`;
}
/**
* 集团上线统计弹窗表格。
* 表格行统一从 getProjectMasterRecords 读取接口数据。
* 国际、生态单位在这里加 green-row,高亮规则集中在表格渲染层。
*/
function renderUsageReportTable(list) {
const rows = getProjectMasterRecords();
const summary = getProjectMasterSummary();
const statusText = projectMasterDataState.loading
? `<div class="table-status">正在获取接口数据...</div>`
: projectMasterDataState.error
? `<div class="table-status error">接口请求失败:${projectMasterDataState.error}</div>`
: "";
if (!rows.length) {
return `${statusText}<div class="empty-state">暂无集团项目上线统计数据</div>`;
}
const bodyRows = rows.map((row) => ({
unit: row.name,
cells: [
row.onlineAutoTotal,
row.onlineReportedTotal,
fmtFixedPercent(row.onlineRate || projectMasterOnlineRate(row), 2),
row.total,
row.linkedTotal,
row.preProjectTotal,
row.pendingTotal,
Math.max(row.total - row.linkedTotal - row.preProjectTotal - row.pendingTotal, 0),
row.noNeedTotal,
row.noNeedCompletedTotal,
row.noNeedStoppedTotal,
row.noNeedRestrictedTotal,
row.masterProjectCount
],
webUrl: resolveProjectMasterWebUrl(row)
}));
return `
${statusText}
<div class="table-wrap">
<table class="project-master-table">
<thead>
<tr>
<th rowspan="2">企业简称</th>
<th colspan="2">上线项目数</th>
<th rowspan="2">上线率</th>
<th colspan="5">应使用项目数</th>
<th colspan="4">申请不用</th>
<th rowspan="2">主数据项目数</th>
</tr>
<tr>
<th>自动取值</th>
<th>定期填报</th>
<th>总数</th>
<th>已关联项目</th>
<th>预立项项目</th>
<th>待建项目</th>
<th>条件受限</th>
<th>总数</th>
<th>已完工</th>
<th>停缓建</th>
<th>条件受限</th>
</tr>
</thead>
<tbody>
${bodyRows.map((row) => {
const unit = row.unit || "";
const rowClass = [
unit.includes("国际") || unit.includes("生态") ? "green-row" : ""
].filter(Boolean).join(" ");
const unitCell = /^https?:\/\//i.test(row.webUrl)
? `<a class="table-link" href="${escapeHtmlAttr(row.webUrl)}" target="_blank" rel="noopener noreferrer">${unit}</a>`
: unit;
return `
<tr class="${rowClass}">
<td>${unitCell}</td>
${row.cells.map((cell) => `<td class="num">${cell}</td>`).join("")}
</tr>
`;
}).join("")}
<tr class="summary-row">
<td>合计</td>
${["onlineAutoTotal", "onlineReportedTotal"].map((key) => `<td class="num">${rows.reduce((sum, row) => sum + toSafeNumber(row[key]), 0)}</td>`).join("")}
<td class="num">${fmtFixedPercent(summary.onlineRate, 2)}</td>
<td class="num">${rows.reduce((sum, row) => sum + toSafeNumber(row.total), 0)}</td>
<td class="num">${rows.reduce((sum, row) => sum + toSafeNumber(row.linkedTotal), 0)}</td>
<td class="num">${summary.preProjectTotal}</td>
<td class="num">${summary.pendingTotal}</td>
<td class="num">${rows.reduce((sum, row) => sum + Math.max(row.total - row.linkedTotal - row.preProjectTotal - row.pendingTotal, 0), 0)}</td>
<td class="num">${summary.noNeedProjectCount}</td>
<td class="num">${rows.reduce((sum, row) => sum + toSafeNumber(row.noNeedCompletedTotal), 0)}</td>
<td class="num">${rows.reduce((sum, row) => sum + toSafeNumber(row.noNeedStoppedTotal), 0)}</td>
<td class="num">${rows.reduce((sum, row) => sum + toSafeNumber(row.noNeedRestrictedTotal), 0)}</td>
<td class="num">${rows.reduce((sum, row) => sum + toSafeNumber(row.masterProjectCount), 0)}</td>
</tr>
</tbody>
</table>
</div>
`;
}
function renderBelowStandardProjectListModal() {
const enterprise = state.enterpriseName ? getEnterprise(state.enterpriseName) : null;
const rows = getBelowStandardProjects(enterprise);
const title = enterprise ? `${enterprise.name} · 未达标项目清单` : "集团未达标项目清单";
if (!rows.length) {
return `
<section class="panel drill-panel">
<div class="panel-header"><div><h2 class="panel-title">${title}</h2></div></div>
<div class="empty-state">当前暂无指标使用率低于 80% 的项目。</div>
</section>
`;
}
return `
<section class="panel drill-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">${title}</h2>
<div class="panel-note">点击项目行进入项目级看板。</div>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>单位</th>
<th>项目名称</th>
<th>指标使用率</th>
<th>是否上线</th>
<th>最近更新</th>
<th>责任人</th>
</tr>
</thead>
<tbody>
${rows.map(({ enterprise: item, project }) => `
<tr class="clickable-row" data-project="${project.id}" data-project-unit-id="${escapeHtmlAttr(project.unitId || "")}" data-project-enterprise="${item.name}">
<td>${item.name}</td>
<td>${project.name}</td>
<td class="num">${fmtPercent(project.usageRate)}</td>
<td>${project.created ? '<span class="status-pill ok">已上线</span>' : '<span class="status-pill bad">未上线</span>'}</td>
<td>${project.lastUpdate}</td>
<td>${project.owner}</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
</section>
`;
}
function renderUnitMetricUsageModal() {
const rows = getUnitMetricUsageRecords();
if (!rows.length) {
return `
<section class="panel drill-panel">
<div class="panel-header"><div><h2 class="panel-title">项目指标使用率</h2></div></div>
<div class="empty-state">当前暂无单位项目指标使用数据。</div>
</section>
`;
}
return `
<section class="panel drill-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">项目指标使用率</h2>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>单位简称</th>
<th>QHSE在建项目数</th>
<th>活跃项目数</th>
<th>使用率</th>
<th>日期</th>
</tr>
</thead>
<tbody>
${rows.map((row) => {
const unitName = row.abbreviation || row.unitName || "";
const webUrl = resolveUnitMetricUsageWebUrl(row);
const unitCell = /^https?:\/\//i.test(webUrl)
? `<a class="table-link" href="${escapeHtmlAttr(webUrl)}" target="_blank" rel="noopener noreferrer">${unitName}</a>`
: unitName;
return `
<tr>
<td>${unitCell}</td>
<td class="num">${row.qhseTotal}</td>
<td class="num">${row.activeNum}</td>
<td class="num">${fmtPercent(row.usageRate)}</td>
<td>${formatApiDate(row.date)}</td>
</tr>
`;
}).join("")}
</tbody>
</table>
</div>
</section>
`;
}
/**
* “集团项目上线统计”弹窗内容。
* 弹窗标题由 core 模块 renderDrillModal 设置,这里只负责面板主体和表格。
*/
function renderGroupOnlineStatsModal() {
return `
<section class="panel drill-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">集团项目上线统计</h2>
</div>
</div>
${renderUsageReportTable()}
</section>
`;
}