2026-06-22 10:48:35 +08:00
|
|
|
|
function kpiCard(label, value, unit, desc, trend, widthPercent, tone = "up") {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 集团级页面渲染模块。
|
|
|
|
|
|
* 负责集团总览、集团上线率卡片、企业分析、指标分析和集团上线统计弹窗。
|
|
|
|
|
|
*/
|
|
|
|
|
|
return `
|
|
|
|
|
|
<article class="kpi-card">
|
2026-07-12 00:53:38 +08:00
|
|
|
|
${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>` : ""}
|
2026-06-22 10:48:35 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 集团项目上线率卡片。
|
2026-07-12 00:53:38 +08:00
|
|
|
|
* 数值只来自集团监控接口,接口失败时显示错误状态。
|
2026-06-22 10:48:35 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function onlineKpiCard(summary) {
|
2026-07-12 00:53:38 +08:00
|
|
|
|
const trendText = projectMasterDataState.loading ? "加载中" : `${toSafeNumber(summary.onlineRate).toFixed(2)}%`;
|
|
|
|
|
|
const statusText = projectMasterDataState.error
|
|
|
|
|
|
? `接口请求失败:${projectMasterDataState.error}`
|
|
|
|
|
|
: "上线项目/申请不用/应使用项目/项目总数";
|
2026-06-22 10:48:35 +08:00
|
|
|
|
return `
|
|
|
|
|
|
<article class="kpi-card chart-click" data-action="group-online-stats">
|
|
|
|
|
|
<div class="kpi-trend">${trendText}</div>
|
|
|
|
|
|
<div class="kpi-label">集团项目上线率</div>
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<div class="kpi-value">${summary.proInfoText || `${summary.onlineActual}/${summary.noNeedProjectCount}/${summary.onlineExpected}`}</div>
|
2026-06-22 10:48:35 +08:00
|
|
|
|
<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") {
|
2026-07-12 00:53:38 +08:00
|
|
|
|
const projectName = projectMonitoringState.data?.MetricProjectInfo?.ProjectShortName
|
|
|
|
|
|
|| projectMonitoringState.data?.MetricProjectInfo?.ProjectName
|
|
|
|
|
|
|| "项目明细";
|
|
|
|
|
|
parts.push(`<span>›</span><span class="crumb">${projectName}</span>`);
|
2026-06-22 10:48:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
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();
|
2026-07-12 00:53:38 +08:00
|
|
|
|
updateDataDateDisplay();
|
2026-06-22 10:48:35 +08:00
|
|
|
|
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();
|
2026-07-12 00:53:38 +08:00
|
|
|
|
const analysisIndicators = projectMasterDataState.metricDatas.length ? projectMasterDataState.metricDatas : indicators;
|
|
|
|
|
|
const activeEnterprises = getGroupEnterpriseModels();
|
2026-06-22 10:48:35 +08:00
|
|
|
|
|
|
|
|
|
|
app.innerHTML = `
|
|
|
|
|
|
<section class="kpi-grid">
|
|
|
|
|
|
${onlineKpiCard(summary)}
|
2026-07-12 00:53:38 +08:00
|
|
|
|
${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)}
|
2026-06-22 10:48:35 +08:00
|
|
|
|
</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>
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<div class="chart-box">${activeEnterprises.length ? renderGroupedRateChart(activeEnterprises, "enterprise", { height: 132 }) : '<div class="empty-state compact-empty">暂无企业分析数据</div>'}</div>
|
2026-06-22 10:48:35 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<article class="panel usage-report-panel chart-click" data-action="group-online-stats">
|
2026-06-22 10:48:35 +08:00
|
|
|
|
<div class="panel-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 class="panel-title">集团在建项目使用分析报告</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="panel-tools">
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<button type="button" class="more-btn" data-action="group-online-stats">查看</button>
|
2026-06-22 10:48:35 +08:00
|
|
|
|
</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>
|
2026-07-12 00:53:38 +08:00
|
|
|
|
${renderIndicatorAnalysis(analysisIndicators)}
|
2026-06-22 10:48:35 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 顶部重点指标条。
|
|
|
|
|
|
* 每张指标卡都带 data-indicator,事件代理会据此打开指标趋势或企业内项目穿透弹窗。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function renderFocusIndicators(items, offset, options = {}) {
|
|
|
|
|
|
const highlightHigh = options.highlightHigh !== false;
|
|
|
|
|
|
return items.map((item, index) => `
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<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))}">
|
2026-06-22 10:48:35 +08:00
|
|
|
|
<div class="warning-name">${item.name}</div>
|
|
|
|
|
|
<div class="warning-lines">
|
|
|
|
|
|
<span>指标使用率:<strong>${item.usageRate}%</strong></span>
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<span>使用项目数:<strong>${item.projects}</strong></span>
|
2026-06-22 10:48:35 +08:00
|
|
|
|
</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 `
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<div class="risk-item" data-indicator="${escapeHtmlAttr(item.name)}" data-indicator-field="${escapeHtmlAttr(resolveMetricField(item.name, item.field))}">
|
2026-06-22 10:48:35 +08:00
|
|
|
|
<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 `
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<div class="risk-item" data-indicator="${escapeHtmlAttr(item.name)}" data-indicator-field="${escapeHtmlAttr(resolveMetricField(item.name, item.field))}">
|
2026-06-22 10:48:35 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 集团上线统计弹窗表格。
|
2026-07-12 00:53:38 +08:00
|
|
|
|
* 表格行统一从 getProjectMasterRecords 读取接口数据。
|
2026-06-22 10:48:35 +08:00
|
|
|
|
* 国际、生态单位在这里加 green-row,高亮规则集中在表格渲染层。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function renderUsageReportTable(list) {
|
2026-07-12 00:53:38 +08:00
|
|
|
|
const rows = getProjectMasterRecords();
|
|
|
|
|
|
const summary = getProjectMasterSummary();
|
2026-06-22 10:48:35 +08:00
|
|
|
|
const statusText = projectMasterDataState.loading
|
|
|
|
|
|
? `<div class="table-status">正在获取接口数据...</div>`
|
|
|
|
|
|
: projectMasterDataState.error
|
2026-07-12 00:53:38 +08:00
|
|
|
|
? `<div class="table-status error">接口请求失败:${projectMasterDataState.error}</div>`
|
2026-06-22 10:48:35 +08:00
|
|
|
|
: "";
|
2026-07-12 00:53:38 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}));
|
2026-06-22 10:48:35 +08:00
|
|
|
|
return `
|
|
|
|
|
|
${statusText}
|
|
|
|
|
|
<div class="table-wrap">
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<table class="project-master-table">
|
2026-06-22 10:48:35 +08:00
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<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>
|
2026-06-22 10:48:35 +08:00
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
2026-07-12 00:53:38 +08:00
|
|
|
|
${bodyRows.map((row) => {
|
|
|
|
|
|
const unit = row.unit || "";
|
2026-06-22 10:48:35 +08:00
|
|
|
|
const rowClass = [
|
2026-07-12 00:53:38 +08:00
|
|
|
|
unit.includes("国际") || unit.includes("生态") ? "green-row" : ""
|
2026-06-22 10:48:35 +08:00
|
|
|
|
].filter(Boolean).join(" ");
|
2026-07-12 00:53:38 +08:00
|
|
|
|
const unitCell = /^https?:\/\//i.test(row.webUrl)
|
|
|
|
|
|
? `<a class="table-link" href="${escapeHtmlAttr(row.webUrl)}" target="_blank" rel="noopener noreferrer">${unit}</a>`
|
|
|
|
|
|
: unit;
|
2026-06-22 10:48:35 +08:00
|
|
|
|
return `
|
|
|
|
|
|
<tr class="${rowClass}">
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<td>${unitCell}</td>
|
|
|
|
|
|
${row.cells.map((cell) => `<td class="num">${cell}</td>`).join("")}
|
2026-06-22 10:48:35 +08:00
|
|
|
|
</tr>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join("")}
|
2026-07-12 00:53:38 +08:00
|
|
|
|
<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>
|
2026-06-22 10:48:35 +08:00
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-12 00:53:38 +08:00
|
|
|
|
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>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 10:48:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* “集团项目上线统计”弹窗内容。
|
2026-07-12 00:53:38 +08:00
|
|
|
|
* 弹窗标题由 core 模块 renderDrillModal 设置,这里只负责面板主体和表格。
|
2026-06-22 10:48:35 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function renderGroupOnlineStatsModal() {
|
|
|
|
|
|
return `
|
|
|
|
|
|
<section class="panel drill-panel">
|
|
|
|
|
|
<div class="panel-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 class="panel-title">集团项目上线统计</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-07-12 00:53:38 +08:00
|
|
|
|
${renderUsageReportTable()}
|
2026-06-22 10:48:35 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|