|
|
@@ -4,18 +4,29 @@
|
|
|
|
|
|
<div class="cockpit-region-rank__body">
|
|
|
<!-- 健康排名 -->
|
|
|
- <div v-show="activeTab === 'health'" class="cockpit-region-rank__pane">
|
|
|
- <div v-if="loading" class="cockpit-region-rank__loading" v-loading="true" />
|
|
|
+ <div
|
|
|
+ v-show="activeTab === 'health'"
|
|
|
+ v-hasPermi="HEALTH_TAB_PERMI"
|
|
|
+ class="cockpit-region-rank__pane"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-if="loading"
|
|
|
+ class="cockpit-region-rank__loading"
|
|
|
+ v-loading="true"
|
|
|
+ />
|
|
|
<template v-else-if="healthRanks.length">
|
|
|
<div
|
|
|
v-for="(item, index) in healthRanks"
|
|
|
:key="item.codeNumber"
|
|
|
- class="cockpit-region-rank__row"
|
|
|
+ class="cockpit-region-rank__row is-clickable"
|
|
|
+ :class="{ 'is-active': isActiveRegion(item) }"
|
|
|
+ @click="handleSelectRegion(item)"
|
|
|
>
|
|
|
<span
|
|
|
class="cockpit-region-rank__badge"
|
|
|
:class="getBadgeClass(index)"
|
|
|
- >{{ index + 1 }}</span>
|
|
|
+ >{{ index + 1 }}</span
|
|
|
+ >
|
|
|
<span class="cockpit-region-rank__name" :title="item.regionName">
|
|
|
{{ item.regionName }}
|
|
|
</span>
|
|
|
@@ -23,12 +34,11 @@
|
|
|
<span
|
|
|
v-for="lv in levelBars"
|
|
|
:key="lv.key"
|
|
|
- class="cockpit-region-rank__dot"
|
|
|
- :style="{ background: lv.color }"
|
|
|
+ class="cockpit-region-rank__level"
|
|
|
+ :style="{ color: lv.color }"
|
|
|
:title="`${lv.label}: ${item[lv.key]}台`"
|
|
|
- />
|
|
|
- <span class="cockpit-region-rank__count">
|
|
|
- 优{{ item.excellent }}
|
|
|
+ >
|
|
|
+ {{ lv.label }}{{ item[lv.key] }}
|
|
|
</span>
|
|
|
</span>
|
|
|
</div>
|
|
|
@@ -37,23 +47,36 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- 异常排名 -->
|
|
|
- <div v-show="activeTab === 'abnormal'" class="cockpit-region-rank__pane">
|
|
|
- <div v-if="loading" class="cockpit-region-rank__loading" v-loading="true" />
|
|
|
+ <div
|
|
|
+ v-show="activeTab === 'abnormal'"
|
|
|
+ v-hasPermi="ABNORMAL_TAB_PERMI"
|
|
|
+ class="cockpit-region-rank__pane"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-if="loading"
|
|
|
+ class="cockpit-region-rank__loading"
|
|
|
+ v-loading="true"
|
|
|
+ />
|
|
|
<template v-else-if="abnormalRanks.length">
|
|
|
<div
|
|
|
v-for="(item, index) in abnormalRanks"
|
|
|
:key="item.codeNumber"
|
|
|
- class="cockpit-region-rank__row"
|
|
|
+ class="cockpit-region-rank__row is-clickable"
|
|
|
+ :class="{ 'is-active': isActiveRegion(item) }"
|
|
|
+ @click="handleSelectRegion(item)"
|
|
|
>
|
|
|
<span
|
|
|
class="cockpit-region-rank__badge"
|
|
|
:class="getBadgeClass(index)"
|
|
|
- >{{ index + 1 }}</span>
|
|
|
+ >{{ index + 1 }}</span
|
|
|
+ >
|
|
|
<span class="cockpit-region-rank__name" :title="item.regionName">
|
|
|
{{ item.regionName }}
|
|
|
</span>
|
|
|
<span class="cockpit-region-rank__abnormal-count">
|
|
|
- <i class="el-icon-warning-outline cockpit-region-rank__warn-icon" />
|
|
|
+ <i
|
|
|
+ class="el-icon-warning-outline cockpit-region-rank__warn-icon"
|
|
|
+ />
|
|
|
{{ item.errCount }} 台
|
|
|
</span>
|
|
|
</div>
|
|
|
@@ -65,37 +88,47 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
+import { store } from "@/store";
|
|
|
import CockpitTechTabs from "./CockpitTechTabs.vue";
|
|
|
-import { getHealthscoresWindList } from "@/api/healthAnalyse";
|
|
|
-import { getAnomalyOverview } from "@/api/anomalyAnalyse";
|
|
|
+import { getHealthscoresRegionList } from "@/api/healthAnalyse";
|
|
|
+import { getAnomalyRegionList } from "@/api/anomalyAnalyse";
|
|
|
import {
|
|
|
- collectFieldCodes,
|
|
|
- buildFieldNameMap,
|
|
|
- buildFieldCodeMap,
|
|
|
- mapHealthscoresWindList,
|
|
|
- aggregateRegionHealth,
|
|
|
- aggregateRegionAbnormal,
|
|
|
+ mapHealthscoresRegionList,
|
|
|
+ mapAnomalyRegionList,
|
|
|
} from "./cockpitHealthMapper";
|
|
|
-import {
|
|
|
- normalizeAnomalyOverviewList,
|
|
|
- mapCockpitAbnormalOverview,
|
|
|
-} from "./cockpitAbnormalMapper";
|
|
|
+
|
|
|
+const HEALTH_TAB_PERMI = ["home:health:btn"];
|
|
|
+const ABNORMAL_TAB_PERMI = ["home:anomalyDetection:btn"];
|
|
|
+
|
|
|
+function hasTabPermi(permi) {
|
|
|
+ const flags = Array.isArray(permi) ? permi : [];
|
|
|
+ if (!flags.length) return true;
|
|
|
+ const allPermission = "*:*:*";
|
|
|
+ const permissions = store?.getters?.permissions || [];
|
|
|
+ return permissions.some(
|
|
|
+ (permission) => permission === allPermission || flags.includes(permission),
|
|
|
+ );
|
|
|
+}
|
|
|
|
|
|
export default {
|
|
|
name: "CockpitRegionRankPanel",
|
|
|
components: { CockpitTechTabs },
|
|
|
props: {
|
|
|
- /** 当前选中节点(含 children 的根/集团节点) */
|
|
|
+ /** 排名数据所属根节点(集团),不随区域点击切换 */
|
|
|
rootNode: { type: Object, default: () => ({}) },
|
|
|
+ /** 当前选中的区域公司 code,用于高亮 */
|
|
|
+ activeCode: { type: [String, Number], default: "" },
|
|
|
parentOpt: { type: Array, default: () => [] },
|
|
|
datatime: { type: String, default: "" },
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
+ HEALTH_TAB_PERMI,
|
|
|
+ ABNORMAL_TAB_PERMI,
|
|
|
activeTab: "health",
|
|
|
rankTabs: [
|
|
|
- { label: "健康排名", value: "health" },
|
|
|
- { label: "异常排名", value: "abnormal" },
|
|
|
+ { label: "健康排名", value: "health", permi: HEALTH_TAB_PERMI },
|
|
|
+ { label: "异常排名", value: "abnormal", permi: ABNORMAL_TAB_PERMI },
|
|
|
],
|
|
|
loading: false,
|
|
|
healthRanks: [],
|
|
|
@@ -123,62 +156,127 @@ export default {
|
|
|
this.fetchRankData();
|
|
|
}
|
|
|
},
|
|
|
+ rankTabs: {
|
|
|
+ deep: true,
|
|
|
+ handler() {
|
|
|
+ this.syncActiveTabByPermi();
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.syncActiveTabByPermi();
|
|
|
},
|
|
|
methods: {
|
|
|
+ syncActiveTabByPermi() {
|
|
|
+ const visibleTabs = this.rankTabs.filter((tab) => hasTabPermi(tab.permi));
|
|
|
+ if (!visibleTabs.length) return;
|
|
|
+ if (!visibleTabs.some((tab) => tab.value === this.activeTab)) {
|
|
|
+ this.activeTab = visibleTabs[0].value;
|
|
|
+ }
|
|
|
+ },
|
|
|
getBadgeClass(index) {
|
|
|
if (index === 0) return "is-gold";
|
|
|
if (index === 1) return "is-silver";
|
|
|
if (index === 2) return "is-bronze";
|
|
|
return "";
|
|
|
},
|
|
|
+ isActiveRegion(item) {
|
|
|
+ if (!this.activeCode || !item?.codeNumber) return false;
|
|
|
+ return String(this.activeCode) === String(item.codeNumber);
|
|
|
+ },
|
|
|
+ buildRequestParams() {
|
|
|
+ const params = {};
|
|
|
+ const companyCode = this.rootNode?.codeNumber;
|
|
|
+ if (companyCode) params.companyCode = companyCode;
|
|
|
+ if (this.datatime) params.datatime = this.datatime;
|
|
|
+ return params;
|
|
|
+ },
|
|
|
+ findNodeByCode(nodes, codeNumber) {
|
|
|
+ const target = String(codeNumber || "");
|
|
|
+ if (!target) return null;
|
|
|
+ const list = Array.isArray(nodes) ? nodes : [];
|
|
|
+ for (let i = 0; i < list.length; i += 1) {
|
|
|
+ const node = list[i];
|
|
|
+ if (String(node.codeNumber) === target) return node;
|
|
|
+ const found = this.findNodeByCode(node.children, target);
|
|
|
+ if (found) return found;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ },
|
|
|
+ findRegionNode(item) {
|
|
|
+ const byCode = this.findNodeByCode(
|
|
|
+ this.rootNode?.children || this.parentOpt,
|
|
|
+ item?.codeNumber,
|
|
|
+ );
|
|
|
+ if (byCode) return byCode;
|
|
|
+ const name = item?.regionName;
|
|
|
+ if (!name) return null;
|
|
|
+ const children = this.rootNode?.children || [];
|
|
|
+ return (
|
|
|
+ children.find(
|
|
|
+ (node) =>
|
|
|
+ node.companyName === name ||
|
|
|
+ node.fieldName === name ||
|
|
|
+ node.companyName === item.regionName,
|
|
|
+ ) || null
|
|
|
+ );
|
|
|
+ },
|
|
|
+ handleSelectRegion(item) {
|
|
|
+ const node = this.findRegionNode(item);
|
|
|
+ if (!node) return;
|
|
|
+ this.$emit("select-region", node);
|
|
|
+ },
|
|
|
async fetchRankData() {
|
|
|
const node = this.rootNode;
|
|
|
- if (!node || !node.children || !node.children.length) return;
|
|
|
+ if (!node || !node.codeNumber) return;
|
|
|
+
|
|
|
+ const canHealth = hasTabPermi(HEALTH_TAB_PERMI);
|
|
|
+ const canAbnormal = hasTabPermi(ABNORMAL_TAB_PERMI);
|
|
|
+ if (!canHealth && !canAbnormal) {
|
|
|
+ this.healthRanks = [];
|
|
|
+ this.abnormalRanks = [];
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
this.loading = true;
|
|
|
+ const params = this.buildRequestParams();
|
|
|
try {
|
|
|
- // 收集根节点下所有 field 的 fieldCode
|
|
|
- const fieldCodes = collectFieldCodes(node);
|
|
|
- if (!fieldCodes.length) return;
|
|
|
-
|
|
|
- // 并发请求所有场站的健康和异常数据
|
|
|
- const healthRequests = fieldCodes.map((fc) => {
|
|
|
- const p = { fieldCode: fc };
|
|
|
- if (this.datatime) p.datatime = this.datatime;
|
|
|
- return getHealthscoresWindList(p)
|
|
|
- .then((res) => (Array.isArray(res?.data) ? res.data : []))
|
|
|
- .catch(() => []);
|
|
|
- });
|
|
|
- const abnormalRequests = fieldCodes.map((fc) => {
|
|
|
- const payload = { fieldCode: fc };
|
|
|
- if (this.datatime) payload.datatime = this.datatime;
|
|
|
- return getAnomalyOverview(payload)
|
|
|
- .then((res) => {
|
|
|
- const list = normalizeAnomalyOverviewList(res?.data);
|
|
|
- const fieldNameMap = buildFieldNameMap(this.parentOpt);
|
|
|
- const fieldCodeMap = buildFieldCodeMap(this.parentOpt);
|
|
|
- return mapCockpitAbnormalOverview(list, fieldNameMap, fieldCodeMap).tableData;
|
|
|
- })
|
|
|
- .catch(() => []);
|
|
|
- });
|
|
|
-
|
|
|
- const [healthResults, abnormalResults] = await Promise.all([
|
|
|
- Promise.all(healthRequests),
|
|
|
- Promise.all(abnormalRequests),
|
|
|
+ const [healthRes, abnormalRes] = await Promise.all([
|
|
|
+ canHealth
|
|
|
+ ? getHealthscoresRegionList(params).catch((error) => {
|
|
|
+ console.error("加载区域健康排名失败:", error);
|
|
|
+ return null;
|
|
|
+ })
|
|
|
+ : Promise.resolve(null),
|
|
|
+ canAbnormal
|
|
|
+ ? getAnomalyRegionList(params).catch((error) => {
|
|
|
+ console.error("加载区域异常排名失败:", error);
|
|
|
+ return null;
|
|
|
+ })
|
|
|
+ : Promise.resolve(null),
|
|
|
]);
|
|
|
|
|
|
- // 拼装成带 fieldCode 的行
|
|
|
- const fieldNameMap = buildFieldNameMap(this.parentOpt);
|
|
|
- const allHealthRows = healthResults.flat().map((item) => ({
|
|
|
- ...mapHealthscoresWindList([item], fieldNameMap)[0],
|
|
|
- fieldCode: String(item.fieldCode || item.codeNumber || ""),
|
|
|
- }));
|
|
|
- const allAbnormalRows = abnormalResults.flat();
|
|
|
+ if (canHealth) {
|
|
|
+ this.healthRanks =
|
|
|
+ healthRes?.code === 200
|
|
|
+ ? mapHealthscoresRegionList(healthRes.data)
|
|
|
+ : [];
|
|
|
+ } else {
|
|
|
+ this.healthRanks = [];
|
|
|
+ }
|
|
|
|
|
|
- this.healthRanks = aggregateRegionHealth(node, allHealthRows);
|
|
|
- this.abnormalRanks = aggregateRegionAbnormal(node, allAbnormalRows);
|
|
|
+ if (canAbnormal) {
|
|
|
+ this.abnormalRanks =
|
|
|
+ abnormalRes?.code === 200
|
|
|
+ ? mapAnomalyRegionList(abnormalRes.data)
|
|
|
+ : [];
|
|
|
+ } else {
|
|
|
+ this.abnormalRanks = [];
|
|
|
+ }
|
|
|
} catch (e) {
|
|
|
console.error("加载区域排名失败:", e);
|
|
|
+ this.healthRanks = [];
|
|
|
+ this.abnormalRanks = [];
|
|
|
} finally {
|
|
|
this.loading = false;
|
|
|
}
|
|
|
@@ -223,6 +321,21 @@ export default {
|
|
|
font-size: 13px;
|
|
|
line-height: 28px;
|
|
|
color: var(--cockpit-panel-text, #303133);
|
|
|
+
|
|
|
+ &.is-clickable {
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 0 4px;
|
|
|
+ margin: 0 -4px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: rgba(0, 210, 255, 0.12);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-active {
|
|
|
+ background: rgba(0, 210, 255, 0.2);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.cockpit-region-rank__badge {
|
|
|
@@ -262,23 +375,14 @@ export default {
|
|
|
flex-shrink: 0;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 3px;
|
|
|
-}
|
|
|
-
|
|
|
-.cockpit-region-rank__dot {
|
|
|
- width: 8px;
|
|
|
- height: 8px;
|
|
|
- border-radius: 2px;
|
|
|
- flex-shrink: 0;
|
|
|
+ gap: 6px;
|
|
|
}
|
|
|
|
|
|
-.cockpit-region-rank__count {
|
|
|
- font-size: 12px;
|
|
|
+.cockpit-region-rank__level {
|
|
|
+ font-size: 11px;
|
|
|
font-weight: 600;
|
|
|
- color: #7ecf51;
|
|
|
- margin-left: 2px;
|
|
|
- min-width: 32px;
|
|
|
- text-align: right;
|
|
|
+ white-space: nowrap;
|
|
|
+ line-height: 1;
|
|
|
}
|
|
|
|
|
|
.cockpit-region-rank__abnormal-count {
|