|
|
@@ -0,0 +1,297 @@
|
|
|
+<template>
|
|
|
+ <div class="cockpit-region-rank">
|
|
|
+ <CockpitTechTabs v-model="activeTab" :tabs="rankTabs" variant="panel" />
|
|
|
+
|
|
|
+ <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" />
|
|
|
+ <template v-else-if="healthRanks.length">
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in healthRanks"
|
|
|
+ :key="item.codeNumber"
|
|
|
+ class="cockpit-region-rank__row"
|
|
|
+ >
|
|
|
+ <span
|
|
|
+ class="cockpit-region-rank__badge"
|
|
|
+ :class="getBadgeClass(index)"
|
|
|
+ >{{ index + 1 }}</span>
|
|
|
+ <span class="cockpit-region-rank__name" :title="item.regionName">
|
|
|
+ {{ item.regionName }}
|
|
|
+ </span>
|
|
|
+ <span class="cockpit-region-rank__bars">
|
|
|
+ <span
|
|
|
+ v-for="lv in levelBars"
|
|
|
+ :key="lv.key"
|
|
|
+ class="cockpit-region-rank__dot"
|
|
|
+ :style="{ background: lv.color }"
|
|
|
+ :title="`${lv.label}: ${item[lv.key]}台`"
|
|
|
+ />
|
|
|
+ <span class="cockpit-region-rank__count">
|
|
|
+ 优{{ item.excellent }}
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <p v-else class="cockpit-region-rank__empty">暂无评估数据</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 异常排名 -->
|
|
|
+ <div v-show="activeTab === 'abnormal'" 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"
|
|
|
+ >
|
|
|
+ <span
|
|
|
+ class="cockpit-region-rank__badge"
|
|
|
+ :class="getBadgeClass(index)"
|
|
|
+ >{{ 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" />
|
|
|
+ {{ item.errCount }} 台
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <p v-else class="cockpit-region-rank__empty">暂无异常数据</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import CockpitTechTabs from "./CockpitTechTabs.vue";
|
|
|
+import { getHealthscoresWindList } from "@/api/healthAnalyse";
|
|
|
+import { getAnomalyOverview } from "@/api/anomalyAnalyse";
|
|
|
+import {
|
|
|
+ collectFieldCodes,
|
|
|
+ buildFieldNameMap,
|
|
|
+ buildFieldCodeMap,
|
|
|
+ mapHealthscoresWindList,
|
|
|
+ aggregateRegionHealth,
|
|
|
+ aggregateRegionAbnormal,
|
|
|
+} from "./cockpitHealthMapper";
|
|
|
+import {
|
|
|
+ normalizeAnomalyOverviewList,
|
|
|
+ mapCockpitAbnormalOverview,
|
|
|
+} from "./cockpitAbnormalMapper";
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "CockpitRegionRankPanel",
|
|
|
+ components: { CockpitTechTabs },
|
|
|
+ props: {
|
|
|
+ /** 当前选中节点(含 children 的根/集团节点) */
|
|
|
+ rootNode: { type: Object, default: () => ({}) },
|
|
|
+ parentOpt: { type: Array, default: () => [] },
|
|
|
+ datatime: { type: String, default: "" },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ activeTab: "health",
|
|
|
+ rankTabs: [
|
|
|
+ { label: "健康排名", value: "health" },
|
|
|
+ { label: "异常排名", value: "abnormal" },
|
|
|
+ ],
|
|
|
+ loading: false,
|
|
|
+ healthRanks: [],
|
|
|
+ abnormalRanks: [],
|
|
|
+ levelBars: [
|
|
|
+ { key: "excellent", label: "优", color: "#7ECF51" },
|
|
|
+ { key: "good", label: "良", color: "#2979ff" },
|
|
|
+ { key: "medium", label: "中", color: "#EECB5F" },
|
|
|
+ { key: "poor", label: "差", color: "#E16757" },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ rootNode: {
|
|
|
+ handler(val) {
|
|
|
+ if (val && val.codeNumber) {
|
|
|
+ this.fetchRankData();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ immediate: true,
|
|
|
+ deep: false,
|
|
|
+ },
|
|
|
+ datatime() {
|
|
|
+ if (this.rootNode && this.rootNode.codeNumber) {
|
|
|
+ this.fetchRankData();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ getBadgeClass(index) {
|
|
|
+ if (index === 0) return "is-gold";
|
|
|
+ if (index === 1) return "is-silver";
|
|
|
+ if (index === 2) return "is-bronze";
|
|
|
+ return "";
|
|
|
+ },
|
|
|
+ async fetchRankData() {
|
|
|
+ const node = this.rootNode;
|
|
|
+ if (!node || !node.children || !node.children.length) return;
|
|
|
+
|
|
|
+ this.loading = true;
|
|
|
+ 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),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 拼装成带 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();
|
|
|
+
|
|
|
+ this.healthRanks = aggregateRegionHealth(node, allHealthRows);
|
|
|
+ this.abnormalRanks = aggregateRegionAbnormal(node, allAbnormalRows);
|
|
|
+ } catch (e) {
|
|
|
+ console.error("加载区域排名失败:", e);
|
|
|
+ } finally {
|
|
|
+ this.loading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.cockpit-region-rank {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__body {
|
|
|
+ margin-top: 4px;
|
|
|
+ min-height: 80px;
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__pane {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+ padding: 4px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__loading {
|
|
|
+ height: 80px;
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__empty {
|
|
|
+ margin: 0;
|
|
|
+ padding: 16px 0;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--cockpit-panel-muted, #909399);
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 28px;
|
|
|
+ color: var(--cockpit-panel-text, #303133);
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__badge {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ border-radius: 4px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 700;
|
|
|
+ background: var(--cockpit-panel-muted, #c0c4cc);
|
|
|
+ color: #fff;
|
|
|
+
|
|
|
+ &.is-gold {
|
|
|
+ background: #f5a623;
|
|
|
+ }
|
|
|
+ &.is-silver {
|
|
|
+ background: #8c9eb5;
|
|
|
+ }
|
|
|
+ &.is-bronze {
|
|
|
+ background: #b87333;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__name {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__bars {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 2px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__count {
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #7ecf51;
|
|
|
+ margin-left: 2px;
|
|
|
+ min-width: 32px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__abnormal-count {
|
|
|
+ flex-shrink: 0;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--cockpit-status-danger, #e16757);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.cockpit-region-rank__warn-icon {
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+</style>
|