CockpitHealthPanel.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <template>
  2. <div class="cockpit-health-panel">
  3. <CockpitTechTabs v-model="activeTab" :tabs="healthTabs" variant="panel" />
  4. <div class="cockpit-health-panel__tab-body">
  5. <div
  6. v-show="activeTab === 'healthy'"
  7. v-hasPermi="['home:health:btn']"
  8. class="cockpit-health-panel__pane cockpit-health-panel__pane--healthy"
  9. >
  10. <div class="cockpit-health-panel__subtitle">综合健康评分</div>
  11. <div class="cockpit-health-panel__body">
  12. <ul class="cockpit-health-panel__legend">
  13. <li
  14. v-for="item in legendItems"
  15. :key="item.name"
  16. class="cockpit-health-panel__legend-item"
  17. >
  18. <i :style="{ background: item.color }"></i>
  19. <span>{{ item.name }}</span>
  20. </li>
  21. </ul>
  22. <div
  23. ref="chartRef"
  24. v-loading="loading"
  25. class="cockpit-health-panel__chart"
  26. element-loading-background="rgba(5, 14, 35, 0.6)"
  27. ></div>
  28. </div>
  29. <CockpitHealthTable
  30. class="cockpit-health-panel__table"
  31. :table-data="tableData"
  32. :loading="loading"
  33. :datatime="datatime"
  34. :sort-config="sortConfig"
  35. @sort-change="handleSortChange"
  36. />
  37. </div>
  38. <div
  39. v-show="activeTab === 'abnormal'"
  40. v-hasPermi="['home:anomalyDetection:btn']"
  41. class="cockpit-health-panel__pane cockpit-health-panel__pane--abnormal"
  42. >
  43. <CockpitAbnormalPanel
  44. :maplistArr="maplistArr"
  45. :defaultdata="defaultdata"
  46. :datatime="datatime"
  47. :parent-opt="parentOpt"
  48. />
  49. </div>
  50. </div>
  51. </div>
  52. </template>
  53. <script>
  54. import * as echarts from "echarts";
  55. import { store } from "@/store";
  56. import { getHealthscoresWindList } from "@/api/healthAnalyse";
  57. import CockpitTechTabs from "./CockpitTechTabs.vue";
  58. import CockpitHealthTable from "./CockpitHealthTable.vue";
  59. import CockpitAbnormalPanel from "./CockpitAbnormalPanel.vue";
  60. import {
  61. aggregateHealthScoreDistribution,
  62. buildFieldCodeMap,
  63. buildFieldNameMap,
  64. mapHealthscoresWindList,
  65. filterEvaluatedRows,
  66. filterRowsByNodeFields,
  67. sortHealthTableData,
  68. } from "./cockpitHealthMapper";
  69. const HEALTH_TAB_PERMI = ["home:health:btn"];
  70. const ABNORMAL_TAB_PERMI = ["home:anomalyDetection:btn"];
  71. function hasTabPermi(permi) {
  72. const flags = Array.isArray(permi) ? permi : [];
  73. if (!flags.length) return true;
  74. const allPermission = "*:*:*";
  75. const permissions = store?.getters?.permissions || [];
  76. return permissions.some(
  77. (permission) => permission === allPermission || flags.includes(permission),
  78. );
  79. }
  80. export default {
  81. name: "CockpitHealthPanel",
  82. components: { CockpitTechTabs, CockpitHealthTable, CockpitAbnormalPanel },
  83. props: {
  84. maplistArr: { type: Object, default: () => ({}) },
  85. defaultdata: { type: Object, default: () => ({}) },
  86. parentOpt: { type: Array, default: () => [] },
  87. datatime: { type: String, default: "" },
  88. },
  89. data() {
  90. return {
  91. activeTab: "healthy",
  92. healthTabs: [
  93. { label: "健康", value: "healthy", permi: HEALTH_TAB_PERMI },
  94. { label: "异常", value: "abnormal", permi: ABNORMAL_TAB_PERMI },
  95. ],
  96. chart: null,
  97. loading: false,
  98. tableData: [],
  99. scoreDistribution: [],
  100. lastHealthList: [],
  101. sortConfig: { prop: "", order: "descending" },
  102. legendItems: [
  103. { name: "优", color: "#7ECF51" },
  104. { name: "良", color: "#2979ff" },
  105. { name: "中", color: "#EECB5F" },
  106. { name: "差", color: "#E16757" },
  107. ],
  108. };
  109. },
  110. watch: {
  111. maplistArr: {
  112. handler() {
  113. this.fetchHealthData();
  114. },
  115. deep: true,
  116. immediate: true,
  117. },
  118. datatime() {
  119. this.fetchHealthData();
  120. },
  121. parentOpt: {
  122. handler() {
  123. if (this.tableData.length) {
  124. this.applyHealthList(this.lastHealthList);
  125. }
  126. },
  127. deep: true,
  128. },
  129. activeTab(val) {
  130. if (val === "healthy") {
  131. this.$nextTick(() => {
  132. if (this.chart) {
  133. this.chart.resize();
  134. } else {
  135. this.initChart();
  136. }
  137. });
  138. }
  139. },
  140. healthTabs: {
  141. deep: true,
  142. handler() {
  143. this.syncActiveTabByPermi();
  144. },
  145. },
  146. scoreDistribution: {
  147. deep: true,
  148. handler() {
  149. this.renderChart();
  150. },
  151. },
  152. },
  153. mounted() {
  154. this.syncActiveTabByPermi();
  155. this.$nextTick(() => {
  156. this.initChart();
  157. });
  158. window.addEventListener("resize", this.handleResize);
  159. },
  160. beforeDestroy() {
  161. window.removeEventListener("resize", this.handleResize);
  162. if (this.chart) {
  163. this.chart.dispose();
  164. this.chart = null;
  165. }
  166. },
  167. methods: {
  168. syncActiveTabByPermi() {
  169. const visibleTabs = this.healthTabs.filter((tab) =>
  170. hasTabPermi(tab.permi),
  171. );
  172. if (!visibleTabs.length) return;
  173. if (!visibleTabs.some((tab) => tab.value === this.activeTab)) {
  174. this.activeTab = visibleTabs[0].value;
  175. }
  176. },
  177. getChartTheme() {
  178. const host =
  179. this.$refs.chartRef?.closest(".cockpit-dashboard-page") ||
  180. document.documentElement;
  181. const read = (name, fallback) =>
  182. getComputedStyle(host).getPropertyValue(name).trim() || fallback;
  183. return {
  184. labelColor: read("--cockpit-chart-label-text", "#ffffff"),
  185. labelLineColor: read(
  186. "--cockpit-chart-label-line",
  187. "rgba(255, 255, 255, 0.55)",
  188. ),
  189. };
  190. },
  191. initChart() {
  192. if (!this.$refs.chartRef) return;
  193. if (this.chart) {
  194. this.chart.dispose();
  195. }
  196. this.chart = echarts.init(this.$refs.chartRef);
  197. this.renderChart();
  198. },
  199. renderChart() {
  200. if (!this.chart) return;
  201. const { labelColor, labelLineColor } = this.getChartTheme();
  202. const chartData = this.scoreDistribution.length
  203. ? this.scoreDistribution
  204. : aggregateHealthScoreDistribution([]);
  205. this.chart.setOption(
  206. {
  207. tooltip: {
  208. trigger: "item",
  209. formatter: "{b}: {c}台 ({d}%)",
  210. },
  211. series: [
  212. {
  213. type: "pie",
  214. radius: ["42%", "72%"],
  215. center: ["55%", "50%"],
  216. avoidLabelOverlap: true,
  217. label: {
  218. show: true,
  219. overflow: "none",
  220. formatter: (params) => {
  221. if (!params.value) return "";
  222. return `{name|${params.name}}\n{value|${params.value}台}`;
  223. },
  224. rich: {
  225. name: {
  226. fontSize: 11,
  227. color: labelColor,
  228. lineHeight: 16,
  229. },
  230. value: {
  231. fontSize: 11,
  232. color: labelColor,
  233. lineHeight: 16,
  234. width: 72,
  235. overflow: "none",
  236. },
  237. },
  238. },
  239. emphasis: {
  240. label: {
  241. color: labelColor,
  242. overflow: "none",
  243. },
  244. },
  245. labelLine: {
  246. show: true,
  247. length: 6,
  248. length2: 12,
  249. smooth: true,
  250. lineStyle: { color: labelLineColor },
  251. },
  252. data: chartData,
  253. },
  254. ],
  255. },
  256. true,
  257. );
  258. },
  259. handleSortChange(sort) {
  260. this.sortConfig = sort;
  261. this.applyHealthList(this.lastHealthList);
  262. },
  263. applyHealthList(list) {
  264. const fieldNameMap = buildFieldNameMap(this.parentOpt);
  265. const fieldCodeMap = buildFieldCodeMap(this.parentOpt);
  266. const rows = mapHealthscoresWindList(list, fieldNameMap).map(
  267. (row, index) => {
  268. const raw = list[index] || {};
  269. const fieldCode =
  270. raw.fieldCode ||
  271. raw.codeNumber ||
  272. row.fieldCode ||
  273. fieldCodeMap[row.fieldName] ||
  274. "";
  275. return {
  276. ...row,
  277. fieldCode: fieldCode ? String(fieldCode) : "",
  278. };
  279. },
  280. );
  281. const scoped = filterRowsByNodeFields(rows, this.maplistArr);
  282. // 只展示已评估(有任意一台风机评分数据)的场站
  283. const evaluated = filterEvaluatedRows(scoped);
  284. // 按排序配置排序
  285. const sorted = sortHealthTableData(evaluated, this.sortConfig);
  286. this.tableData = sorted;
  287. this.scoreDistribution = aggregateHealthScoreDistribution(sorted);
  288. },
  289. buildHealthRequestParams() {
  290. const fieldCode =
  291. this.maplistArr?.codeNumber || this.maplistArr?.fieldCode;
  292. if (!fieldCode) {
  293. return null;
  294. }
  295. const params = { fieldCode };
  296. if (this.datatime) {
  297. params.datatime = this.datatime;
  298. }
  299. return params;
  300. },
  301. async fetchHealthData() {
  302. const selectedNode = this.maplistArr;
  303. if (!selectedNode || !Object.keys(selectedNode).length) return;
  304. this.loading = true;
  305. try {
  306. let list = [];
  307. // fieldCode 可传公司/区域 codeNumber,接口一次返回下属全部风场
  308. const params = this.buildHealthRequestParams();
  309. if (params) {
  310. const res = await getHealthscoresWindList(params);
  311. list = Array.isArray(res?.data) ? res.data : [];
  312. }
  313. this.lastHealthList = list;
  314. this.applyHealthList(list);
  315. } catch (error) {
  316. console.error("加载健康风场统计失败:", error);
  317. this.lastHealthList = [];
  318. this.tableData = [];
  319. this.scoreDistribution = aggregateHealthScoreDistribution([]);
  320. } finally {
  321. this.loading = false;
  322. this.$nextTick(() => {
  323. if (!this.chart) {
  324. this.initChart();
  325. } else {
  326. this.renderChart();
  327. this.chart.resize();
  328. }
  329. });
  330. }
  331. },
  332. handleResize() {
  333. if (this.chart) this.chart.resize();
  334. },
  335. },
  336. };
  337. </script>
  338. <style lang="scss" scoped>
  339. .cockpit-health-panel__subtitle {
  340. margin: 4px 0 10px;
  341. font-size: 13px;
  342. font-weight: 600;
  343. color: var(--cockpit-panel-title, #303133);
  344. }
  345. .cockpit-health-panel__tab-body {
  346. position: relative;
  347. height: var(--cockpit-health-pane-h);
  348. min-height: var(--cockpit-health-pane-h);
  349. overflow: hidden;
  350. }
  351. .cockpit-health-panel__pane {
  352. position: absolute;
  353. inset: 0;
  354. overflow: hidden;
  355. display: flex;
  356. flex-direction: column;
  357. justify-content: flex-start;
  358. }
  359. .cockpit-health-panel__body {
  360. display: flex;
  361. align-items: center;
  362. gap: 8px;
  363. flex-shrink: 0;
  364. height: var(--cockpit-health-chart-h, 150px);
  365. min-height: var(--cockpit-health-chart-h, 150px);
  366. }
  367. .cockpit-health-panel__legend {
  368. flex: 0 0 52px;
  369. margin: 0;
  370. padding: 0;
  371. list-style: none;
  372. }
  373. .cockpit-health-panel__legend-item {
  374. display: flex;
  375. align-items: center;
  376. gap: 6px;
  377. margin-bottom: 10px;
  378. font-size: 12px;
  379. color: var(--cockpit-panel-muted, #909399);
  380. i {
  381. flex-shrink: 0;
  382. width: 10px;
  383. height: 10px;
  384. border-radius: 1px;
  385. }
  386. }
  387. .cockpit-health-panel__chart {
  388. flex: 1;
  389. min-width: 0;
  390. height: var(--cockpit-health-chart-h, 150px);
  391. }
  392. .cockpit-health-panel__table {
  393. flex: 0 0 auto;
  394. margin-top: 8px;
  395. }
  396. </style>