Files
DataGovernanceDashboard/res/js/indexV2.group.js
T
2026-06-22 10:48:35 +08:00

394 lines
18 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">
<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>
`;
}
/**
* 集团项目上线率卡片。
* 数值来自 groupSummary,而 groupSummary 会优先读取 projectMasterDataState.summary;
* 接口失败时 desc 展示错误原因,同时数值回退到本地兜底汇总。
*/
function onlineKpiCard(summary) {
const trendText = projectMasterDataState.loading ? "加载中" : `${summary.onlineRate.toFixed(2)}%`;
const statusText = projectMasterDataState.error ? `接口失败,已使用本地兜底数据:${projectMasterDataState.error}` : "QHSE使用项目数 / 申请不用项目数 / 主数据项目数";
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.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 project = getProjectById(state.projectId);
parts.push(`<span>›</span><span class="crumb">${project ? project.name : "项目明细"}</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();
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 = `
<section class="kpi-grid">
${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")}
</section>
<section class="indicator-strip">
${renderFocusIndicators(indicators.slice(0, 8), 0, { highlightHigh: false })}
<div class="indicator-more-cell">
<button type="button" class="more-btn" data-action="more-indicators">更多</button>
</div>
</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">${renderGroupedRateChart(activeEnterprises, "enterprise", { height: 132 })}</div>
</article>
<article class="panel usage-report-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">集团在建项目使用分析报告</h2>
</div>
<div class="panel-tools">
<button type="button" class="more-btn" data-action="group-usage-report-preview">预览</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(indicators)}
</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="${item.name}">
<div class="warning-name">${item.name}</div>
<div class="warning-lines">
<span>指标使用率:<strong>${item.usageRate}%</strong></span>
<span>指标准确率:<strong>${indicatorAccuracy(item)}%</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) => {
const level = item.usageRate < 65
? { text: "重点督办", cls: "red" }
: item.usageRate < 72
? { text: "持续跟踪", cls: "orange" }
: { text: "保持关注", cls: "green" };
return `
<div class="risk-item" data-indicator="${item.name}">
<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>
<span class="risk-badge ${level.cls}">${level.text}</span>
</div>
`;
}).join("")}
</div>
`;
}
/**
* 企业页指标分析数据派生。
* 在集团指标基础上叠加当前企业使用率和项目数量,生成更贴近企业维度的指标预警项。
*/
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 `
<div class="risk-summary">
${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 `
<div class="risk-item" data-indicator="${item.name}">
<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>
<span class="risk-badge ${level.cls}">${level.text}</span>
</div>
`;
}).join("")}
</div>
`;
}
function renderEnterpriseTable(riskList) {
return `
<div class="table-wrap">
<table>
<thead>
<tr>
<th>企业</th>
<th>项目数</th>
<th>上线率</th>
<th>使用率</th>
<th>准确率</th>
<th>预警项目</th>
<th>责任人</th>
<th>状态</th>
</tr>
</thead>
<tbody>
${riskList.map(({ enterprise, stats, score }) => {
const level = riskLevel(score);
return `
<tr class="clickable-row" data-enterprise="${enterprise.name}">
<td>${enterprise.name}</td>
<td class="num">${stats.projectCount}</td>
<td class="num">${fmtPercent(stats.onlineRate)}</td>
<td class="num">${fmtPercent(stats.usageRate)}</td>
<td class="num">${fmtPercent(stats.accuracyRate)}</td>
<td class="num">${stats.warningProjects}</td>
<td>${enterprise.owner}</td>
<td><span class="risk-badge ${level.cls}">${level.text}</span></td>
</tr>
`;
}).join("")}
</tbody>
</table>
</div>
`;
}
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
? `<div class="table-status">正在获取接口数据...</div>`
: projectMasterDataState.error
? `<div class="table-status error">接口请求失败,当前展示本地兜底数据:${projectMasterDataState.error}</div>`
: "";
return `
${statusText}
<div class="table-wrap">
<table>
<thead>
<tr>
${headers.map((header) => `<th>${header}</th>`).join("")}
<th>操作</th>
</tr>
</thead>
<tbody>
${rows.map((row) => {
const unit = row[1] || "";
const rowClass = [
row[0] === "合计" ? "summary-row" : "",
["国际", "生态"].includes(unit) ? "green-row" : ""
].filter(Boolean).join(" ");
return `
<tr class="${rowClass}">
${row.map((cell, index) => `<td class="${index >= 2 ? "num" : ""}">${cell}</td>`).join("")}
<td>${unit && rowClass.indexOf("summary-row") === -1 ? `<button type="button" class="more-btn" data-action="preview-unit-report" data-unit="${unit}">预览</button>` : ""}</td>
</tr>
`;
}).join("")}
</tbody>
</table>
</div>
`;
}
/**
* “集团项目上线统计”弹窗内容。
* 弹窗标题由 project 模块 renderDrillModal 设置,这里只负责面板主体和表格。
*/
function renderGroupOnlineStatsModal() {
const activeEnterprises = enterprises.filter((enterprise) => enterprise.stats[state.dimension].projectCount > 0);
return `
<section class="panel drill-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">集团项目上线统计</h2>
</div>
</div>
${renderUsageReportTable(activeEnterprises)}
</section>
`;
}