增加数据治理“用不用/动不动”分析报告

This commit is contained in:
2026-07-21 23:25:40 +08:00
parent 7fac3c7f97
commit 8fd4cca652
9 changed files with 377 additions and 68 deletions
+5
View File
@@ -71,6 +71,7 @@ next/
- 监控状态:集团或当前公司的 `monitoring`、`loading`、`error`。
- 项目状态:`projectData`、`projectLoading`、`projectError`。
- 穿透状态:`dialog`、`selectedMetric`、`metricAnalysis`、`metricLoading`。
- 动不动报告状态:`moveReport`、`moveReportLoading`、`moveReportError`。
集团和公司监控共用 `loadMonitoring`。新请求开始时会取消旧请求,并通过递增请求序号阻止旧响应覆盖最新筛选结果。项目详情和指标穿透分别使用独立 Action,避免页面组件自行维护请求状态。
@@ -81,6 +82,8 @@ next/
- `GetMetricMonitoringSituation`:集团监控。
- `GetUnitMetricMonitoringSituation`:公司监控。
- `GetGroupMetricAnalysis`:集团指标穿透。
- `GetGroupMoveReportMetricData`:动不动报告动态指标列,`type=0/1` 分别对应安全/质量。
- `GetGroupMetricAnalysisMoveReport`:动不动报告企业行,`range=0/1` 分别对应所有项目/新开工项目。
- `GetUnitMetricAnalysis`:公司指标穿透。
- `GetProjectMonitoringSituation`:项目详情。
@@ -94,6 +97,8 @@ next/
组件只能读取适配后的 ViewModel。新增或调整接口字段时,应先修改 `types/` 和 `adapters/`,不要在 Vue 模板中增加字段兼容分支。
动不动报告由两个接口共同组成:指标定义接口决定动态列顺序和中文名称,企业行接口通过 `MetricAnalysis[].Field` 绑定每列使用率。若指标定义接口临时返回空字段,适配层会使用当前监控接口的指标名称兜底,并对重复 `Field` 去重,避免生成空表头或重复列。
### 图表与大屏布局
`components/EChart.vue` 统一负责 ECharts 初始化、响应式更新、`ResizeObserver` 监听和 `dispose`。当前只注册柱状图、折线图、网格、图例、提示框和 Canvas 渲染器。
+45 -1
View File
@@ -1,6 +1,6 @@
/** 数据适配层回归测试:锁定接口兼容规则和核心统计口径。 */
import { describe, expect, it } from "vitest";
import { normalizeMonitoring } from "./dashboard";
import { normalizeMonitoring, normalizeMoveReport } from "./dashboard";
describe("normalizeMonitoring", () => {
it("按业务口径归一化集团汇总、单位和指标数据", () => {
@@ -33,4 +33,48 @@ describe("normalizeMonitoring", () => {
expect(result.summary.onlineRate).toBe(0);
expect(result.trend.dates).toEqual([]);
});
it("按指标定义顺序生成动不动报告动态列并绑定使用率", () => {
const result = normalizeMoveReport(
[
{ Name: "安全检查", Field: "CheckNum" },
{ Name: "培训人次", Field: "TrainPersonNum" },
{ Name: "重复列", Field: "CheckNum" },
],
[{
UnitName: "测试企业",
PassProjectNum: 8,
NotPassProjectNum: 2,
UsageRate: "80%",
MetricAnalysis: [
{ Field: "CheckNum", UsageRate: "75%" },
{ Field: "TrainPersonNum", UsageRate: 60 },
],
}],
);
expect(result.metrics).toEqual([
{ name: "安全检查", field: "CheckNum" },
{ name: "培训人次", field: "TrainPersonNum" },
]);
expect(result.rows[0]).toMatchObject({
unitName: "测试企业",
passProjectNum: 8,
usageRate: "80%",
metricRates: { CheckNum: "75%", TrainPersonNum: "60%" },
});
});
it("指标定义接口为空时使用监控指标名称兜底", () => {
const result = normalizeMoveReport(
[{ Name: "", Field: "" }],
[{ UnitName: "测试企业", MetricAnalysis: [{ Field: "SafeTrainNum", UsageRate: "10%" }] }],
[{
name: "安全教育培训人次", field: "SafeTrainNum", projects: 0, projectUsageNum: 0,
usageRate: 0, accuracyRate: 0, frequency: 0, sortIndex: 1,
}],
);
expect(result.metrics).toEqual([{ name: "安全教育培训人次", field: "SafeTrainNum" }]);
});
});
+80
View File
@@ -7,6 +7,7 @@ import type {
MasterRecordVM,
MetricVM,
MonitoringVM,
MoveReportVM,
ProjectAnalysisVM,
SummaryVM,
UnitAnalysisVM,
@@ -51,6 +52,8 @@ function normalizeRecord(item: Raw, index: number): MasterRecordVM {
total: numberValue(value(item, "Total", "total")),
onlineAutoTotal: numberValue(value(item, "YiShangXian", "yiShangXian")),
onlineReportedTotal: numberValue(value(item, "TianBao", "tianBao")),
offlineTotal: numberValue(value(item, "WeiShangXian", "weiShangXian")),
closedRequestTotal: numberValue(value(item, "ShenQingGuanBi", "shenQingGuanBi")),
masterProjectCount: numberValue(value(item, "MasterTotal", "masterTotal", "masterProjectCount")),
qhseProjectCount: numberValue(value(item, "QHSETotal", "qhseTotal", "qhseProjectCount")),
linkedTotal: numberValue(value(item, "ZhengChang", "zhengChang")),
@@ -58,6 +61,7 @@ function normalizeRecord(item: Raw, index: number): MasterRecordVM {
noNeedTotal: numberValue(value(item, "BuYong", "buYong")),
noNeedCompletedTotal: numberValue(value(item, "BuYongYiWanGong", "buYongYiWanGong")),
noNeedStoppedTotal: numberValue(value(item, "BuYongTingHuanJian", "buYongTingHuanJian")),
noNeedInapplicableTotal: numberValue(value(item, "BuYongBuShiYong", "buYongBuShiYong")),
noNeedRestrictedTotal: numberValue(value(item, "BuYongTJShouXian", "buYongTJShouXian")),
preProjectTotal: numberValue(value(item, "YuLiXiang", "yuLiXiang")),
onlineRate: numberValue(value(item, "OnlineRate", "OnlinRate", "onlineRate", "onlinRate")),
@@ -156,3 +160,79 @@ export function normalizeMonitoring(input: unknown): MonitoringVM {
}
};
}
const percentText = (input: unknown): string => {
const text = textValue(input).trim();
if (!text) return "-";
return text.endsWith("%") ? text : `${text}%`;
};
/**
* 组合“动不动”报告的指标定义和企业行。
* 指标定义接口是动态表头的首选来源;若接口字段暂时为空,则使用监控接口指标名称补齐,最后才回退到字段代码。
*/
export function normalizeMoveReport(
definitionInput: unknown,
rowInput: unknown,
fallbackMetrics: MetricVM[] = [],
): MoveReportVM {
const definitions = Array.isArray(definitionInput) ? definitionInput as Raw[] : [];
const rawRows = Array.isArray(rowInput) ? rowInput as Raw[] : [];
const fallbackNames = new Map<string, string>();
fallbackMetrics.forEach((metric) => {
if (metric.field && metric.name && !fallbackNames.has(metric.field)) {
fallbackNames.set(metric.field, metric.name);
}
});
// 行接口决定实际需要展示的字段集合,避免定义接口异常时渲染整排空列。
const rowFields: string[] = [];
const rowFieldSet = new Set<string>();
rawRows.forEach((row) => {
const analyses = value(row, "MetricAnalysis", "metricAnalysis");
if (!Array.isArray(analyses)) return;
(analyses as Raw[]).forEach((item) => {
const field = textValue(value(item, "Field", "field")).trim();
if (field && !rowFieldSet.has(field)) {
rowFieldSet.add(field);
rowFields.push(field);
}
});
});
const metrics: MoveReportVM["metrics"] = [];
const metricFields = new Set<string>();
definitions.forEach((item) => {
const field = textValue(value(item, "Field", "field")).trim();
const name = textValue(value(item, "Name", "name")).trim();
if (!field || !name || metricFields.has(field)) return;
if (rowFieldSet.size && !rowFieldSet.has(field)) return;
metricFields.add(field);
metrics.push({ field, name });
});
rowFields.forEach((field) => {
if (metricFields.has(field)) return;
metricFields.add(field);
metrics.push({ field, name: fallbackNames.get(field) || field });
});
const rows = rawRows.map((item) => {
const metricRates: Record<string, string> = {};
const analyses = value(item, "MetricAnalysis", "metricAnalysis");
if (Array.isArray(analyses)) {
(analyses as Raw[]).forEach((analysis) => {
const field = textValue(value(analysis, "Field", "field")).trim();
if (field) metricRates[field] = percentText(value(analysis, "UsageRate", "usageRate"));
});
}
return {
unitName: textValue(value(item, "UnitName", "unitName")),
passProjectNum: numberValue(value(item, "PassProjectNum", "passProjectNum")),
notPassProjectNum: numberValue(value(item, "NotPassProjectNum", "notPassProjectNum")),
usageRate: percentText(value(item, "UsageRate", "usageRate")),
metricRates,
};
}).filter((item) => item.unitName);
return { metrics, rows };
}
+124 -51
View File
@@ -66,6 +66,7 @@ const unitChart = computed<EChartsOption>(() => ({
const title = computed(() => {
const labels = {
online: "集团项目上线统计",
moveReport: "数据治理“动不动”分析报告",
belowStandard: "未达标项目清单",
unitUsage: "项目指标使用率(使用率=100*活跃项目/QHSE在建项目;项目中18个指标有一个指标有异动则视为活跃项目)",
groupMetric: "指标穿透分析",
@@ -74,20 +75,29 @@ const title = computed(() => {
return store.dialog ? labels[store.dialog] : "";
});
const dialogLoading = computed(() =>
store.dialog === "moveReport" ? store.moveReportLoading : store.metricLoading,
);
const dialogError = computed(() =>
store.dialog === "moveReport" ? store.moveReportError : store.metricError,
);
const loadingText = computed(() =>
store.dialog === "moveReport" ? "正在获取动不动分析数据..." : "正在获取指标穿透数据...",
);
const onlineRateTotal = computed(() => {
const total = sum("total");
return total ? sum("onlineAutoTotal") / total * 100 : 0;
});
const moveTableMinWidth = computed(() =>
Math.max(980, 420 + store.moveReport.metrics.length * 138),
);
const sum = (field: keyof MasterRecordVM) =>
store.monitoring.records.reduce(
(total, row) => total + numberValue(row[field]),
0,
);
// “应使用项目数-条件受限”沿用原版表格口径,不能与“申请不用-条件受限”混用。
const restricted = (row: MasterRecordVM) =>
Math.max(
row.total - row.linkedTotal - row.preProjectTotal - row.pendingTotal,
0,
);
const restrictedTotal = computed(() =>
store.monitoring.records.reduce((total, row) => total + restricted(row), 0),
);
const dateText = (value: string) => value ? value.split("T")[0] : "-";
function previewUnitReport(unit: string) {
// 单位简称存在历史别名,先归一化再匹配固定文件名,避免按名称模糊拼接到错误文件。
@@ -132,17 +142,17 @@ function previewUnitReport(unit: string) {
type="button"
class="modal-close"
aria-label="关闭"
@click="store.dialog = null"
@click="store.closeDialog()"
>
×
</button>
</div>
<div class="drill-modal-body">
<div v-if="store.metricLoading" class="empty-state">
正在获取指标穿透数据...
<div v-if="dialogLoading" class="empty-state">
{{ loadingText }}
</div>
<div v-else-if="store.metricError" class="empty-state">
{{ store.metricError }}
<div v-else-if="dialogError" class="empty-state">
{{ dialogError }}
</div>
<section
@@ -153,25 +163,27 @@ function previewUnitReport(unit: string) {
<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>
<th rowspan="2">单位名称</th>
<th rowspan="2">项目上线率(%)</th>
<th rowspan="2">应使用项目</th>
<th colspan="3">已上线项目</th>
<th colspan="4">未上线项目</th>
<th colspan="3">申请关闭</th>
<th rowspan="2">主数据项目</th>
<th rowspan="2">上报日期</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>
<th>待上线</th>
<th>停缓建</th>
<th>条件受限</th>
<th>总数</th>
<th>已完工/撤场</th>
<th>不适用</th>
</tr>
</thead>
<tbody>
@@ -183,6 +195,21 @@ function previewUnitReport(unit: string) {
row.name.includes('国际') || row.name.includes('生态'),
}"
>
<td>{{ row.name }}</td>
<td class="num">{{ row.onlineRate.toFixed(2) }}%</td>
<td class="num">{{ row.total }}</td>
<td class="num">{{ row.onlineAutoTotal }}</td>
<td class="num">{{ row.linkedTotal }}</td>
<td class="num">{{ row.preProjectTotal }}</td>
<td class="num">{{ row.offlineTotal }}</td>
<td class="num">{{ row.pendingTotal }}</td>
<td class="num">{{ row.noNeedStoppedTotal }}</td>
<td class="num">{{ row.noNeedRestrictedTotal }}</td>
<td class="num">{{ row.closedRequestTotal }}</td>
<td class="num">{{ row.noNeedCompletedTotal }}</td>
<td class="num">{{ row.noNeedInapplicableTotal }}</td>
<td class="num">{{ row.masterProjectCount }}</td>
<td>{{ dateText(row.reportDate) }}</td>
<td>
<a
v-if="/^https?:\/\//i.test(row.webUrl)"
@@ -190,40 +217,86 @@ function previewUnitReport(unit: string) {
:href="row.webUrl"
target="_blank"
rel="noopener noreferrer"
>{{ row.name }}</a
><template v-else>{{ row.name }}</template>
>查看</a>
<span v-else>-</span>
</td>
<td class="num">{{ row.onlineAutoTotal }}</td>
<td class="num">{{ row.onlineReportedTotal }}</td>
<td class="num">{{ row.onlineRate.toFixed(2) }}%</td>
<td class="num">{{ row.total }}</td>
<td class="num">{{ row.linkedTotal }}</td>
<td class="num">{{ row.preProjectTotal }}</td>
<td class="num">{{ row.pendingTotal }}</td>
<td class="num">{{ restricted(row) }}</td>
<td class="num">{{ row.noNeedTotal }}</td>
<td class="num">{{ row.noNeedCompletedTotal }}</td>
<td class="num">{{ row.noNeedStoppedTotal }}</td>
<td class="num">{{ row.noNeedRestrictedTotal }}</td>
<td class="num">{{ row.masterProjectCount }}</td>
</tr>
<tr class="summary-row">
<td>合计</td>
<td class="num">{{ sum("onlineAutoTotal") }}</td>
<td class="num">{{ sum("onlineReportedTotal") }}</td>
<td class="num">
{{ store.monitoring.summary.onlineRate.toFixed(2) }}%
</td>
<td class="num">{{ onlineRateTotal.toFixed(2) }}%</td>
<td class="num">{{ sum("total") }}</td>
<td class="num">{{ sum("onlineAutoTotal") }}</td>
<td class="num">{{ sum("linkedTotal") }}</td>
<td class="num">{{ sum("preProjectTotal") }}</td>
<td class="num">{{ sum("offlineTotal") }}</td>
<td class="num">{{ sum("pendingTotal") }}</td>
<td class="num">{{ restrictedTotal }}</td>
<td class="num">{{ sum("noNeedTotal") }}</td>
<td class="num">{{ sum("noNeedCompletedTotal") }}</td>
<td class="num">{{ sum("noNeedStoppedTotal") }}</td>
<td class="num">{{ sum("noNeedRestrictedTotal") }}</td>
<td class="num">{{ sum("masterProjectCount") }}</td>
<td class="num">{{ sum("closedRequestTotal") }}</td>
<td class="num">{{ sum("noNeedCompletedTotal") }}</td>
<td class="num">{{ sum("noNeedInapplicableTotal") }}</td>
<td class="num">{{ sum("masterProjectCount") }}</td>
<td>-</td>
<td>-</td>
</tr>
</tbody>
</table>
</div>
</section>
<section
v-else-if="store.dialog === 'moveReport'"
class="panel drill-panel move-report-panel"
>
<div v-if="!store.moveReport.rows.length" class="empty-state">
当前暂无动不动分析数据。
</div>
<div v-else class="table-wrap">
<table
class="move-report-table"
:style="{ minWidth: `${moveTableMinWidth}px` }"
>
<thead>
<tr>
<th rowspan="2">企业简称</th>
<th colspan="2">上线项目数</th>
<th rowspan="2">使用率</th>
<th
v-if="store.moveReport.metrics.length"
class="move-metric-group"
:colspan="store.moveReport.metrics.length"
>
<span>核心指标使用率</span>
</th>
</tr>
<tr>
<th>达标项目数</th>
<th>未达标项目</th>
<th
v-for="metricItem in store.moveReport.metrics"
:key="metricItem.field"
:title="metricItem.field"
>
{{ metricItem.name }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in store.moveReport.rows"
:key="row.unitName"
>
<td>{{ row.unitName }}</td>
<td class="num">{{ row.passProjectNum }}</td>
<td class="num">{{ row.notPassProjectNum }}</td>
<td class="num">{{ row.usageRate }}</td>
<td
v-for="metricItem in store.moveReport.metrics"
:key="metricItem.field"
class="num"
>
{{ row.metricRates[metricItem.field] || "-" }}
</td>
</tr>
</tbody>
</table>
+2
View File
@@ -11,6 +11,8 @@ export const API_ENDPOINTS = {
monitoring: `${API_BASE}/GetMetricMonitoringSituation`,
unitMonitoring: `${API_BASE}/GetUnitMetricMonitoringSituation`,
groupMetric: `${API_BASE}/GetGroupMetricAnalysis`,
groupMoveReportMetrics: `${API_BASE}/GetGroupMoveReportMetricData`,
groupMoveReport: `${API_BASE}/GetGroupMetricAnalysisMoveReport`,
unitMetric: `${API_BASE}/GetUnitMetricAnalysis`,
projectMonitoring: `${API_BASE}/GetProjectMonitoringSituation`
} as const;
+48 -3
View File
@@ -5,11 +5,12 @@
import { computed, ref } from "vue";
import { defineStore } from "pinia";
import { API_ENDPOINTS, SYSTEMS, type DimensionKey, type SystemKey } from "../config/dashboard";
import { normalizeMonitoring } from "../adapters/dashboard";
import { normalizeMonitoring, normalizeMoveReport } from "../adapters/dashboard";
import { getJson } from "../services/http";
import type { DialogKind, MasterRecordVM, MonitoringVM } from "../types/dashboard";
import type { DialogKind, MasterRecordVM, MonitoringVM, MoveReportVM } from "../types/dashboard";
const emptyMonitoring = (): MonitoringVM => normalizeMonitoring({});
const emptyMoveReport = (): MoveReportVM => ({ metrics: [], rows: [] });
function allowedSystemsFromUrl(): SystemKey[] {
// 兼容外部系统沿用的 ?type=1,2 参数;非法值回退为全部系统。
@@ -36,8 +37,13 @@ export const useDashboardStore = defineStore("dashboard", () => {
const metricAnalysis = ref<Record<string, unknown> | null>(null);
const metricLoading = ref(false);
const metricError = ref("");
const moveReport = ref<MoveReportVM>(emptyMoveReport());
const moveReportLoading = ref(false);
const moveReportError = ref("");
let monitoringRequest = 0;
let monitoringController: AbortController | null = null;
let moveReportRequest = 0;
let moveReportController: AbortController | null = null;
const apiType = computed(() => SYSTEMS.find((item) => item.key === system.value)?.type ?? 0);
const apiRange = computed(() => dimension.value === "new" ? 1 : 0);
@@ -103,7 +109,45 @@ export const useDashboardStore = defineStore("dashboard", () => {
}
}
async function openMoveReport() {
// 动态表头和企业数据必须使用同一组 type/range 快照,防止切换筛选时新旧结果混用。
const request = ++moveReportRequest;
moveReportController?.abort();
moveReportController = new AbortController();
dialog.value = "moveReport";
moveReportLoading.value = true;
moveReportError.value = "";
moveReport.value = emptyMoveReport();
try {
const [definitions, rows] = await Promise.all([
getJson<unknown>(API_ENDPOINTS.groupMoveReportMetrics, {
type: apiType.value,
}, { signal: moveReportController.signal }),
getJson<unknown>(API_ENDPOINTS.groupMoveReport, {
type: apiType.value,
range: apiRange.value,
}, { signal: moveReportController.signal }),
]);
if (request !== moveReportRequest) return;
moveReport.value = normalizeMoveReport(definitions.data, rows.data, allMetrics.value);
} catch (reason) {
if (request !== moveReportRequest) return;
moveReportError.value = reason instanceof Error ? reason.message : "动不动分析报告加载失败";
} finally {
if (request === moveReportRequest) moveReportLoading.value = false;
}
}
function closeDialog() {
if (dialog.value === "moveReport") {
moveReportRequest += 1;
moveReportController?.abort();
}
dialog.value = null;
}
function setFilters(nextSystem: SystemKey, nextDimension: DimensionKey) {
closeDialog();
system.value = nextSystem;
dimension.value = nextDimension;
}
@@ -115,6 +159,7 @@ export const useDashboardStore = defineStore("dashboard", () => {
return {
allowedSystems, system, dimension, loading, error, monitoring, currentUnitId, currentUnitName,
projectData, projectLoading, projectError, dialog, selectedMetric, selectedMetricVM, metricAnalysis,
metricLoading, metricError, allMetrics, loadMonitoring, loadProject, openMetric, setFilters, findRecord
metricLoading, metricError, moveReport, moveReportLoading, moveReportError, allMetrics,
loadMonitoring, loadProject, openMetric, openMoveReport, closeDialog, setFilters, findRecord
};
});
+31
View File
@@ -15,7 +15,19 @@
.usage-dot { background: #5ee1b5; }
.accuracy-dot { background: #ffc857; }
.trend-panel .chart-box, .enterprise-overview .chart-box { width: 100%; min-width: 0; }
.group-left-stack { grid-template-rows: minmax(0, 1fr) repeat(2, auto); }
.group-left-stack .usage-report-panel { width: 100%; margin: 0; }
.group-left-stack .report-entry-panel {
min-height: 62px;
padding: 12px 16px;
border: 1px solid rgba(91, 180, 255, 0.28);
color: inherit;
background: rgba(4, 35, 64, 0.72);
font: inherit;
text-align: left;
}
.report-entry-panel .panel-header { align-items: center; margin: 0; }
.report-entry-panel:hover { border-color: rgba(91, 214, 255, 0.58); background: rgba(10, 57, 91, 0.86); }
/* 全局穿透弹窗和表格状态。 */
.drill-modal { display: grid; }
.drill-modal[hidden] { display: none; }
@@ -24,6 +36,25 @@
.table-link { color: var(--cyan); }
.chart-click { cursor: pointer; }
/* “动不动”报告按接口指标数量横向扩展,固定列和动态列保持稳定宽度。 */
.move-report-panel .table-wrap { max-height: 610px; }
.move-report-table { width: 100%; table-layout: fixed; }
.move-report-table th,
.move-report-table td { min-width: 118px; text-align: center; }
.move-report-table th {
white-space: normal;
line-height: 1.35;
border: 1px solid rgba(186, 232, 255, 0.62);
background: linear-gradient(180deg, #1c99d9, #054876);
}
.move-report-table thead tr:first-child th { top: 0; z-index: 4; }
.move-report-table thead tr:nth-child(2) th { top: 40px; z-index: 3; }
.move-report-table .move-metric-group { text-align: center; }
.move-metric-group span { position: static; display: inline-block; }
.move-report-table th:first-child,
.move-report-table td:first-child { width: 112px; min-width: 112px; }
.move-report-table tbody tr:first-child td { color: #ffffff; font-weight: 900; background: rgba(73, 214, 255, 0.14); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; }
}
+24 -1
View File
@@ -36,6 +36,8 @@ export interface MasterRecordVM {
total: number;
onlineAutoTotal: number;
onlineReportedTotal: number;
offlineTotal: number;
closedRequestTotal: number;
masterProjectCount: number;
qhseProjectCount: number;
linkedTotal: number;
@@ -43,6 +45,7 @@ export interface MasterRecordVM {
noNeedTotal: number;
noNeedCompletedTotal: number;
noNeedStoppedTotal: number;
noNeedInapplicableTotal: number;
noNeedRestrictedTotal: number;
preProjectTotal: number;
onlineRate: number;
@@ -51,6 +54,26 @@ export interface MasterRecordVM {
sortIndex: number;
}
/** “动不动”报告的动态指标列定义。 */
export interface MoveReportMetricVM {
name: string;
field: string;
}
/** “动不动”报告的企业行,动态指标按字段名存入 metricRates。 */
export interface MoveReportRowVM {
unitName: string;
passProjectNum: number;
notPassProjectNum: number;
usageRate: string;
metricRates: Record<string, string>;
}
export interface MoveReportVM {
metrics: MoveReportMetricVM[];
rows: MoveReportRowVM[];
}
/** 集团或公司级顶部汇总模型。 */
export interface SummaryVM {
onlineActual: number;
@@ -136,4 +159,4 @@ export interface AppFilters {
dimension: DimensionKey;
}
export type DialogKind = "online" | "belowStandard" | "unitUsage" | "groupMetric" | "unitMetric" | null;
export type DialogKind = "online" | "moveReport" | "belowStandard" | "unitUsage" | "groupMetric" | "unitMetric" | null;
+18 -12
View File
@@ -220,21 +220,27 @@ onMounted(() => store.loadMonitoring());
<div v-else class="empty-state compact-empty">暂无企业分析数据</div>
</div>
</article>
<article
class="panel usage-report-panel chart-click"
<button
type="button"
class="panel usage-report-panel report-entry-panel chart-click"
@click="store.dialog = 'online'"
>
<div class="panel-header">
<h2 class="panel-title">集团在建项目使用分析报告</h2>
<button
type="button"
class="more-btn"
@click.stop="store.dialog = 'online'"
>
查看
</button>
<h2 class="panel-title">数据治理“用不用”分析报告</h2>
<span class="more-btn">查看</span>
</div>
</article>
</button>
<button
v-if="store.system !== 'emergency'"
type="button"
class="panel usage-report-panel report-entry-panel chart-click"
@click="store.openMoveReport()"
>
<div class="panel-header">
<h2 class="panel-title">数据治理“动不动”分析报告</h2>
<span class="more-btn">查看</span>
</div>
</button>
</div>
<article class="panel risk-panel">
<div class="panel-header">
@@ -249,7 +255,7 @@ onMounted(() => store.loadMonitoring());
</div>
<IndicatorAnalysis
:metrics="store.allMetrics"
:limit="8"
:limit="0"
@select="store.openMetric"
/>
</article>