增加数据治理“用不用/动不动”分析报告
This commit is contained in:
@@ -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 渲染器。
|
||||
|
||||
@@ -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" }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user