index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774
  1. <template>
  2. <div class="global-variable">
  3. <div class="searchbox">
  4. <p>
  5. 单位:
  6. <selecttree
  7. size="small"
  8. style="width: 180px"
  9. placeholder="请选择所属公司"
  10. :list="parentOpt"
  11. type="1"
  12. v-model="companyCode"
  13. :defaultParentProps="{
  14. children: 'children',
  15. label: 'companyName',
  16. value: 'codeNumber',
  17. }"
  18. />
  19. </p>
  20. <p>
  21. 时间:
  22. <el-date-picker
  23. v-model="timevalue"
  24. size="small"
  25. type="month"
  26. placeholder="选择月"
  27. :picker-options="monthPickerOptions"
  28. />
  29. </p>
  30. <el-button type="primary" size="small" @click="conditions">
  31. 查询
  32. </el-button>
  33. </div>
  34. <div class="kuang">
  35. <!-- 加载中动画 -->
  36. <div v-if="loading" class="loader-wrapper">
  37. <div class="loader"></div>
  38. </div>
  39. <!-- 空状态 -->
  40. <el-empty
  41. class="Anempty"
  42. v-if="!loading && ListData.length === 0"
  43. :image-size="200"
  44. />
  45. <!-- 主体内容 -->
  46. <div class="main-body" v-if="!loading && ListData.length > 0">
  47. <div
  48. class="basics"
  49. v-for="(item, index) in ListData"
  50. :key="item + index"
  51. >
  52. <div class="title">
  53. <span>
  54. <SvgIcons
  55. name="WindPower3"
  56. class="WindPower3"
  57. width="26"
  58. height="26"
  59. />
  60. </span>
  61. <h4>风机:{{ item.wind_turbine_name }}</h4>
  62. <span class="mill">{{ millTypeMap[item.mill_type] || "-" }}</span>
  63. </div>
  64. <div class="content">
  65. <div class="summary" style="color: #28a745">
  66. <span :style="{ color: getHealthColor(item.total_health_score) }">
  67. {{
  68. item.total_health_score < 0
  69. ? "-"
  70. : Math.floor(item.total_health_score)
  71. }}
  72. </span>
  73. <span :style="{ color: getHealthColor(item.total_health_score) }">
  74. {{ getHealthLabel(item.total_health_score) }}
  75. </span>
  76. </div>
  77. <ul class="health">
  78. <li>
  79. <span>发电机组</span>
  80. <span :class="'health-' + item.val">
  81. {{
  82. item.subsystems.generator.health_score >= 0
  83. ? String(
  84. Math.floor(item.subsystems.generator.health_score),
  85. ).slice(0, 3)
  86. : "-"
  87. }}
  88. </span>
  89. </li>
  90. <li v-if="item.subsystems.drive_train">
  91. <span>传动系统</span>
  92. <span :class="'health-' + item.val">
  93. {{
  94. item.subsystems.drive_train.health_score >= 0
  95. ? String(
  96. Math.floor(item.subsystems.drive_train.health_score),
  97. ).slice(0, 3)
  98. : "-"
  99. }}
  100. </span>
  101. </li>
  102. <li>
  103. <span>机舱系统</span>
  104. <span :class="'health-' + item.val">
  105. {{
  106. item.subsystems.nacelle.health_score >= 0
  107. ? String(
  108. Math.floor(item.subsystems.nacelle.health_score),
  109. ).slice(0, 3)
  110. : "-"
  111. }}
  112. </span>
  113. </li>
  114. <li>
  115. <span>电网环境</span>
  116. <span :class="'health-' + item.val">
  117. {{
  118. item.subsystems.grid.health_score >= 0
  119. ? String(
  120. Math.floor(item.subsystems.grid.health_score),
  121. ).slice(0, 3)
  122. : "-"
  123. }}
  124. </span>
  125. </li>
  126. </ul>
  127. </div>
  128. <!-- 健康评估建议(通用展示) -->
  129. <div class="advice-box">
  130. <div class="advice-head">
  131. <span class="advice-title">健康评估建议</span>
  132. <span
  133. class="advice-badge"
  134. :class="`is-${getHealthLevelKey(item.total_health_score)}`"
  135. >
  136. {{ getHealthLabel(item.total_health_score) }}
  137. </span>
  138. </div>
  139. <p class="advice-text">
  140. {{ getHealthAdvice(item.total_health_score) }}
  141. </p>
  142. </div>
  143. </div>
  144. </div>
  145. </div>
  146. <!-- 健康度说明(卡片版) -->
  147. <div class="health-legend">
  148. <div class="legend-head">
  149. <span class="legend-title">健康度说明</span>
  150. </div>
  151. <div class="legend-body">
  152. <div class="legend-item is-good">
  153. <div class="legend-icon" aria-hidden="true">
  154. <svg viewBox="0 0 24 24" class="legend-icon-svg">
  155. <path
  156. class="shield"
  157. d="M12 2.5 19 6.2v6.4c0 5.2-3.6 9.8-7 10.9-3.4-1.1-7-5.7-7-10.9V6.2L12 2.5Z"
  158. />
  159. <path class="mark" d="M8.2 12.2l2.3 2.3 5.3-5.3" />
  160. </svg>
  161. </div>
  162. <div class="legend-content">
  163. <p class="legend-range">100–85分(健康)</p>
  164. <p class="legend-advice">
  165. 建议:按计划进行常规巡检与维护,保持当前运行状态,无需额外检修。
  166. </p>
  167. </div>
  168. </div>
  169. <div class="legend-item is-subhealthy">
  170. <div class="legend-icon" aria-hidden="true">
  171. <svg viewBox="0 0 24 24" class="legend-icon-svg">
  172. <path
  173. class="shield"
  174. d="M12 2.5 19 6.2v6.4c0 5.2-3.6 9.8-7 10.9-3.4-1.1-7-5.7-7-10.9V6.2L12 2.5Z"
  175. />
  176. <path class="mark" d="M12 7.6v6.2" />
  177. <path class="mark" d="M12 16.9h.01" />
  178. </svg>
  179. </div>
  180. <div class="legend-content">
  181. <p class="legend-range">84–70分(亚健康)</p>
  182. <p class="legend-advice">
  183. 建议:加强状态监测,缩短巡检周期,排查潜在隐患,可安排预防性维护。
  184. </p>
  185. </div>
  186. </div>
  187. <div class="legend-item is-normal">
  188. <div class="legend-icon" aria-hidden="true">
  189. <svg viewBox="0 0 24 24" class="legend-icon-svg">
  190. <path
  191. class="shield"
  192. d="M12 2.5 19 6.2v6.4c0 5.2-3.6 9.8-7 10.9-3.4-1.1-7-5.7-7-10.9V6.2L12 2.5Z"
  193. />
  194. <path class="mark" d="M12 7.6v6.2" />
  195. <path class="mark" d="M12 16.9h.01" />
  196. </svg>
  197. </div>
  198. <div class="legend-content">
  199. <p class="legend-range">69–55分(一般)</p>
  200. <p class="legend-advice">
  201. 建议:开展专项检测与故障诊断,制定针对性检修计划,及时处理发现的问题,避免状态进一步恶化。
  202. </p>
  203. </div>
  204. </div>
  205. <div class="legend-item is-fault">
  206. <div class="legend-icon" aria-hidden="true">
  207. <svg viewBox="0 0 24 24" class="legend-icon-svg">
  208. <path
  209. class="shield"
  210. d="M12 2.5 19 6.2v6.4c0 5.2-3.6 9.8-7 10.9-3.4-1.1-7-5.7-7-10.9V6.2L12 2.5Z"
  211. />
  212. <path class="mark" d="M9 9l6 6" />
  213. <path class="mark" d="M15 9l-6 6" />
  214. </svg>
  215. </div>
  216. <div class="legend-content">
  217. <p class="legend-range">55分以下(故障)</p>
  218. <p class="legend-advice">
  219. 建议:立即停机或安排紧急检修,进行全面故障排查与修复,必要时更换关键部件,确保设备恢复正常运行能力。
  220. </p>
  221. </div>
  222. </div>
  223. </div>
  224. </div>
  225. </div>
  226. </template>
  227. <script>
  228. import { getSysOrganizationAuthTreeByRoleId } from "@/api/ledger.js";
  229. import selecttree from "../../components/selecttree.vue";
  230. import axios from "axios";
  231. export default {
  232. components: {
  233. selecttree,
  234. },
  235. data() {
  236. return {
  237. parentOpt: [],
  238. companyCode: "",
  239. ListData: [],
  240. loading: false, // ✅ 加载状态.
  241. // demo演示
  242. timevalue: new Date(2024, 6), // 7 月是 6,因为月份从 0 开始
  243. };
  244. },
  245. computed: {
  246. millTypeMap() {
  247. return {
  248. dfig: "双馈",
  249. direct: "直驱",
  250. semi: "半直驱",
  251. };
  252. },
  253. monthPickerOptions() {
  254. return {
  255. disabledDate: (time) => {
  256. const code = this.companyCode;
  257. const date = time;
  258. if (code === "WOF046400029") {
  259. const start = new Date("2023-10-01");
  260. const end = new Date("2024-10-30");
  261. return date < start || date > end;
  262. } else if (code === "WOF091200030") {
  263. const start = new Date("2023-01-01");
  264. const end = new Date("2024-11-30");
  265. return date < start || date > end;
  266. } else if (code === "WOF093400005") {
  267. const start = new Date("2023-12-01");
  268. const end = new Date("2024-05-30");
  269. return date < start || date > end;
  270. } else {
  271. // 其他公司不限制时间选择
  272. return false;
  273. }
  274. },
  275. };
  276. },
  277. },
  278. created() {
  279. this.GETtree();
  280. },
  281. methods: {
  282. async GETtree() {
  283. const res = await getSysOrganizationAuthTreeByRoleId();
  284. const treedata = res.data;
  285. const processedData = this.processTreeData(treedata);
  286. this.parentOpt = processedData;
  287. this.defaultdata = res.data[0];
  288. // demo演示
  289. this.$nextTick(() => {
  290. this.companyCode = "WOF046400029";
  291. });
  292. },
  293. processTreeData(treeData) {
  294. const processedData = [];
  295. function processNode(node) {
  296. if (node.codeType === "field") {
  297. node.companyName = node.fieldName;
  298. }
  299. if (node.children && node.children.length > 0) {
  300. node.children.forEach((child) => {
  301. processNode(child);
  302. });
  303. }
  304. }
  305. treeData.forEach((root) => {
  306. processNode(root);
  307. processedData.push(root);
  308. });
  309. return processedData;
  310. },
  311. getHealthLabel(val) {
  312. if (!Number.isFinite(val) || val < 0) return "无数据";
  313. if (val >= 85) return "健康";
  314. if (val >= 70) return "亚健康";
  315. if (val >= 55) return "一般";
  316. return "故障";
  317. },
  318. getHealthLevelKey(val) {
  319. if (!Number.isFinite(val) || val < 0) return "nodata";
  320. if (val >= 85) return "good";
  321. if (val >= 70) return "subhealthy";
  322. if (val >= 55) return "normal";
  323. return "fault";
  324. },
  325. getHealthAdvice(val) {
  326. if (!Number.isFinite(val) || val < 0) return "暂无健康评估数据。";
  327. if (val >= 85) {
  328. return "按计划进行常规巡检与维护,保持当前运行状态,无需额外检修。";
  329. }
  330. if (val >= 70) {
  331. return "加强状态监测,缩短巡检周期,排查潜在隐患,可安排预防性维护。";
  332. }
  333. if (val >= 55) {
  334. return "开展专项检测与故障诊断,制定针对性检修计划,及时处理发现的问题,避免状态进一步恶化。";
  335. }
  336. return "立即停机或安排紧急检修,进行全面故障排查与修复,必要时更换关键部件,确保设备恢复正常运行能力。";
  337. },
  338. getHealthColor(val) {
  339. if (!Number.isFinite(val) || val < 0) return "#6c757d"; // 灰色
  340. if (val >= 85) return "#28a745"; // 绿色
  341. if (val >= 70) return "#ffc107"; // 黄色
  342. if (val >= 55) return "#fd7e14"; // 橙色
  343. return "#dc3545"; // 红色
  344. },
  345. conditions() {
  346. const fullStr = this.$formatDateTWO(this.timevalue);
  347. const datePart = typeof fullStr === "string" ? fullStr.slice(0, 7) : "";
  348. const parms = {
  349. windcode: this.companyCode,
  350. month: datePart,
  351. };
  352. this.loading = true; // ✅ 显示 loading
  353. const start = performance.now(); // ⏱️ 请求开始时间
  354. axios
  355. .post(`/AnalysisMulti/health_assess`, parms)
  356. .then((res) => {
  357. this.ListData = res.data;
  358. const end = performance.now(); // ⏱️ 请求结束时间
  359. const duration = ((end - start) / 1000).toFixed(2);
  360. console.log(`接口请求耗时:${duration} 秒`);
  361. })
  362. .catch((error) => {
  363. console.error("Error fetching data:", error);
  364. const end = performance.now(); // ⏱️ 请求结束时间
  365. const duration = ((end - start) / 1000).toFixed(2);
  366. console.log(`接口请求耗时:${duration} 秒`);
  367. })
  368. .finally(() => {
  369. this.loading = false; // ✅ 隐藏 loading
  370. });
  371. },
  372. },
  373. };
  374. </script>
  375. <style lang="scss" scoped>
  376. .searchbox {
  377. display: flex;
  378. margin: 0 0 20px 0;
  379. p {
  380. margin-right: 10px;
  381. }
  382. .el-select {
  383. width: 180px;
  384. }
  385. }
  386. .el-input__inner {
  387. width: 340px;
  388. }
  389. .main-body {
  390. margin: 0 auto;
  391. display: grid;
  392. grid-template-columns: repeat(auto-fill, 260px); /* 严格保持300px宽度 */
  393. justify-content: space-evenly; /* 均匀分布 */
  394. gap: 10px;
  395. // height: min-content;
  396. // max-height: 700px;
  397. overflow: hidden;
  398. overflow-y: auto;
  399. .basics {
  400. width: 260px; /* 固定宽度 */
  401. display: flex;
  402. flex-direction: column;
  403. margin-bottom: 20px;
  404. border: 1px solid var(--header-bg);
  405. .title {
  406. padding: 5px 0;
  407. width: 100%;
  408. border-bottom: 1px solid var(--header-bg);
  409. text-align: left;
  410. display: flex;
  411. align-items: center; // 垂直居中
  412. // justify-content: center; // 水平居中
  413. span {
  414. display: inline-block;
  415. margin-right: 6px;
  416. svg {
  417. display: block;
  418. }
  419. }
  420. h4 {
  421. width: 175px;
  422. color: var(--header-bg);
  423. font-size: 1.1em;
  424. font-weight: 600;
  425. margin: 0;
  426. }
  427. .mill {
  428. font-size: 12px;
  429. color: rgb(43, 42, 42);
  430. }
  431. }
  432. .content {
  433. display: flex;
  434. width: 100%;
  435. gap: 28px;
  436. .summary {
  437. width: 70px;
  438. display: flex;
  439. flex-direction: column; // 垂直排列
  440. align-items: center; // 居中对齐
  441. justify-content: center;
  442. font-weight: bold;
  443. padding-left: 26px;
  444. span {
  445. display: block;
  446. text-align: center;
  447. line-height: 1.3;
  448. &:first-child {
  449. font-size: 1.6em; // 更大的字体
  450. margin-bottom: 4px;
  451. }
  452. &:last-child {
  453. font-size: 0.8em; // 稍小一点
  454. }
  455. }
  456. }
  457. .health {
  458. flex: 1;
  459. display: flex;
  460. flex-wrap: wrap;
  461. gap: 6px;
  462. list-style: none;
  463. padding: 0;
  464. margin: 0;
  465. li {
  466. display: flex;
  467. flex-direction: column;
  468. align-items: center;
  469. justify-content: center;
  470. padding: 6px 6px;
  471. border-radius: 4px;
  472. min-width: 60px;
  473. font-size: 0.8em;
  474. span {
  475. display: block;
  476. text-align: center;
  477. line-height: 1.3;
  478. &:first-child {
  479. font-size: 0.9em;
  480. font-weight: 600;
  481. margin-bottom: 2px;
  482. }
  483. }
  484. }
  485. li span {
  486. &:last-child {
  487. color: var(--header-bg);
  488. }
  489. }
  490. }
  491. }
  492. .advice-box {
  493. margin-top: 10px;
  494. padding: 10px 10px;
  495. border-top: 1px solid var(--header-bg);
  496. background: #fff;
  497. .advice-head {
  498. display: flex;
  499. align-items: center;
  500. justify-content: space-between;
  501. gap: 8px;
  502. margin-bottom: 6px;
  503. }
  504. .advice-title {
  505. font-size: 12px;
  506. font-weight: 700;
  507. color: #303133;
  508. }
  509. .advice-badge {
  510. font-size: 12px;
  511. font-weight: 700;
  512. padding: 2px 8px;
  513. border-radius: 999px;
  514. &.is-good {
  515. color: #1f7a3a;
  516. background: #e7f6ee;
  517. }
  518. &.is-subhealthy {
  519. color: #8a6d00;
  520. background: #fff6e5;
  521. }
  522. &.is-normal {
  523. color: #9a4d00;
  524. background: #fff0e6;
  525. }
  526. &.is-fault {
  527. color: #b42318;
  528. background: #ffebee;
  529. }
  530. &.is-nodata {
  531. color: #606266;
  532. background: #f4f4f5;
  533. }
  534. }
  535. .advice-text {
  536. margin: 0;
  537. font-size: 12px;
  538. line-height: 1.45;
  539. color: #606266;
  540. word-break: break-all;
  541. }
  542. }
  543. }
  544. }
  545. .jia {
  546. color: var(--header-bg);
  547. }
  548. .kuang {
  549. min-height: 79vh;
  550. height: 680px;
  551. position: relative;
  552. overflow: hidden;
  553. overflow-y: auto;
  554. .Anempty {
  555. position: absolute;
  556. top: 50%;
  557. left: 50%;
  558. transform: translate(-50%, -50%);
  559. }
  560. }
  561. .health-legend {
  562. margin-top: 14px;
  563. border-radius: 14px;
  564. background: #fff;
  565. box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.08);
  566. border: 1px solid rgba(228, 231, 237, 0.8);
  567. overflow: hidden;
  568. .legend-head {
  569. padding: 12px 14px 0;
  570. }
  571. .legend-title {
  572. font-size: 14px;
  573. font-weight: 800;
  574. color: #303133;
  575. }
  576. .legend-body {
  577. display: grid;
  578. grid-template-columns: repeat(4, minmax(0, 1fr));
  579. gap: 0;
  580. padding: 10px 10px 12px;
  581. }
  582. .legend-item {
  583. display: flex;
  584. align-items: flex-start;
  585. gap: 10px;
  586. padding: 10px 12px;
  587. position: relative;
  588. min-height: 88px;
  589. }
  590. .legend-item + .legend-item::before {
  591. content: "";
  592. position: absolute;
  593. left: 0;
  594. top: 16px;
  595. bottom: 16px;
  596. width: 1px;
  597. background: rgba(220, 223, 230, 0.7);
  598. }
  599. .legend-icon {
  600. width: 34px;
  601. height: 34px;
  602. flex: 0 0 auto;
  603. display: flex;
  604. align-items: center;
  605. justify-content: center;
  606. color: currentColor;
  607. }
  608. .legend-icon-svg {
  609. width: 22px;
  610. height: 22px;
  611. }
  612. .legend-icon-svg .shield {
  613. fill: none;
  614. stroke: currentColor;
  615. stroke-width: 1.8;
  616. stroke-linejoin: round;
  617. }
  618. .legend-icon-svg .mark {
  619. fill: none;
  620. stroke: currentColor;
  621. stroke-width: 2.2;
  622. stroke-linecap: round;
  623. stroke-linejoin: round;
  624. }
  625. .legend-content {
  626. flex: 1;
  627. min-width: 0;
  628. }
  629. .legend-range {
  630. margin: 0 0 6px;
  631. font-size: 13px;
  632. font-weight: 800;
  633. line-height: 1.2;
  634. }
  635. .legend-advice {
  636. margin: 0;
  637. font-size: 12px;
  638. line-height: 1.45;
  639. color: #606266;
  640. word-break: break-all;
  641. }
  642. .legend-item.is-good {
  643. color: #28a745;
  644. .legend-range {
  645. color: inherit;
  646. }
  647. }
  648. .legend-item.is-subhealthy {
  649. color: #ffc107;
  650. .legend-range {
  651. color: inherit;
  652. }
  653. }
  654. .legend-item.is-normal {
  655. color: #fd7e14;
  656. .legend-range {
  657. color: inherit;
  658. }
  659. }
  660. .legend-item.is-fault {
  661. color: #dc3545;
  662. .legend-range {
  663. color: inherit;
  664. }
  665. }
  666. }
  667. @media (max-width: 768px) {
  668. .health-legend {
  669. .legend-body {
  670. grid-template-columns: 1fr;
  671. }
  672. .legend-item + .legend-item::before {
  673. display: none;
  674. }
  675. .legend-item {
  676. border-top: 1px solid rgba(220, 223, 230, 0.7);
  677. }
  678. .legend-item:first-child {
  679. border-top: 0;
  680. }
  681. }
  682. }
  683. // 动画
  684. /* ✅ loading 包裹容器 */
  685. .loader-wrapper {
  686. display: flex;
  687. justify-content: center;
  688. align-items: center;
  689. height: 100%; /* 可根据页面需要调整高度 */
  690. }
  691. /* ✅ loading 样式动画 */
  692. .loader {
  693. width: 50px;
  694. --b: 8px;
  695. aspect-ratio: 1;
  696. border-radius: 50%;
  697. padding: 1px;
  698. background: conic-gradient(#0000 10%, var(--header-bg)) content-box;
  699. -webkit-mask: repeating-conic-gradient(
  700. #0000 0deg,
  701. #000 1deg 20deg,
  702. #0000 21deg 36deg
  703. ),
  704. radial-gradient(
  705. farthest-side,
  706. #0000 calc(100% - var(--b) - 1px),
  707. #000 calc(100% - var(--b))
  708. );
  709. mask: repeating-conic-gradient(#0000 0deg, #000 1deg 20deg, #0000 21deg 36deg),
  710. radial-gradient(
  711. farthest-side,
  712. #0000 calc(100% - var(--b) - 1px),
  713. #000 calc(100% - var(--b))
  714. );
  715. -webkit-mask-composite: destination-in;
  716. mask-composite: intersect;
  717. animation: l4 1s infinite steps(10);
  718. }
  719. @keyframes l4 {
  720. to {
  721. transform: rotate(1turn);
  722. }
  723. }
  724. </style>