index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. <template>
  2. <div
  3. draggable="true"
  4. @dragstart="startDrag"
  5. @dragend="stopDrag"
  6. :style="{ left: x + 'px', top: y + 'px' }"
  7. style="cursor: pointer; position: absolute; z-index: 999"
  8. >
  9. <el-badge
  10. :value="warnLength"
  11. :class="{ active: warnLength && !displayAlarms ? false : true }"
  12. >
  13. <div class="alarmDeligo" @click="displayAlarm" v-if="warnLength">
  14. <img src="@assets/imgs/ygj.png" class="trans" />
  15. <img src="@assets/imgs/ygj1.png" />
  16. </div>
  17. <div class="alarmDeligo" @dblclick="setting" v-if="!warnLength">
  18. <img src="@assets/imgs/wgj.png" class="trans" />
  19. <img src="@assets/imgs/wgj1.png" />
  20. </div>
  21. </el-badge>
  22. <deligo-alarm-list
  23. :dialogList="warnList.slice(0, 12)"
  24. @setConfig="displaySetting = true"
  25. @confirmed="handleConfirm"
  26. ref="deligoChild"
  27. @close="displayClose"
  28. v-if="displayAlarms == true"
  29. ></deligo-alarm-list>
  30. <alarmSetting
  31. v-if="displaySetting"
  32. @saveConfig="getAlarmConfig"
  33. @cancleConfig="displaySetting = false"
  34. :alarmConfigArray="alarmConfigArray"
  35. />
  36. </div>
  37. </template>
  38. <script>
  39. import deligoAlarmList from "@/components/deligoAlarmList";
  40. import alarmSetting from "@/components/alarm-badge/alarm-setting.vue";
  41. import { ElNotification } from "element-plus";
  42. import { GetDeviceTableData } from "@/api/zhbj/index.js";
  43. import dayjs from "dayjs";
  44. export default {
  45. name: "alarmBadge",
  46. components: {
  47. deligoAlarmList,
  48. alarmSetting,
  49. },
  50. data() {
  51. return {
  52. //实时报警弹窗ws
  53. socketObj1: "", // websocket实例对象
  54. //心跳检测
  55. heartCheck1: {
  56. vueThis1: this, // vue实例
  57. timeout1: 30000, // 超时时间
  58. timeoutObj1: null, // 计时器对象——向后端发送心跳检测
  59. serverTimeoutObj1: null, // 计时器对象——等待后端心跳检测的回复
  60. // 心跳检测重置
  61. reset: function () {
  62. clearTimeout(this.timeoutObj1);
  63. clearTimeout(this.serverTimeoutObj1);
  64. return this;
  65. },
  66. // 心跳检测启动
  67. start: function () {
  68. this.timeoutObj1 && clearTimeout(this.timeoutObj1);
  69. this.serverTimeoutObj1 && clearTimeout(this.serverTimeoutObj1);
  70. this.timeoutObj1 = setTimeout(() => {
  71. // 这里向后端发送一个心跳检测,后端收到后,会返回一个心跳回复
  72. this.vueThis1.socketObj1.send("HeartBeat");
  73. console.log("发送心跳检测1");
  74. this.serverTimeoutObj1 = setTimeout(() => {
  75. // 如果超过一定时间还没重置计时器,说明websocket与后端断开了
  76. console.log("未收到心跳检测回复1");
  77. // 关闭WebSocket
  78. this.vueThis1.socketObj1.close();
  79. }, this.timeout1);
  80. }, this.timeout1);
  81. },
  82. },
  83. socketReconnectTimer1: null, // 计时器对象——重连
  84. socketReconnectLock1: false, // WebSocket重连的锁
  85. socketLeaveFlag1: false, // 离开标记(解决 退出登录再登录 时出现的 多次相同推送 问题,出现的本质是多次建立了WebSocket连接)
  86. alarmList: [],
  87. dialogList: [],
  88. //实时报警图标相关
  89. x: null,
  90. y: null,
  91. currentX: 0,
  92. currentY: 0,
  93. displayAlarms: false,
  94. indexNum: 0,
  95. data1: 0,
  96. dragging: false,
  97. initialX: 0,
  98. initialY: 0,
  99. timer: null,
  100. firstTime: null,
  101. lastTime: null,
  102. key: false,
  103. //请求参数
  104. requestAlarmHistoryParams: [
  105. {
  106. alarmType: "booststation",
  107. deviceType: "",
  108. },
  109. {
  110. alarmType: "inverter",
  111. deviceType: "",
  112. },
  113. {
  114. alarmType: "windturbine",
  115. deviceType: "",
  116. },
  117. ],
  118. alarmConfigArray: [],
  119. displaySetting: false,
  120. seriousWarning: false,
  121. audioElement: null,
  122. };
  123. },
  124. computed: {
  125. warnLength() {
  126. return this.$store.state.warnList.length;
  127. },
  128. warnList() {
  129. return this.$store.state.warnList;
  130. },
  131. isLogined() {
  132. return this.$store.state.user?.loginState;
  133. },
  134. },
  135. created() {
  136. this.getAlarmConfig();
  137. this.x = window.innerWidth - 82;
  138. this.y = window.innerHeight - 32;
  139. let requestResult = [];
  140. this.requestAlarmHistoryParams.forEach(({ alarmType }) => {
  141. requestResult.push(this.getAlarmHistory(alarmType));
  142. });
  143. Promise.all(requestResult)
  144. .then((promiseResult) => {
  145. promiseResult.forEach(({ data }) => {
  146. data?.records?.forEach((ele) => {
  147. this.pushALarmItem(ele);
  148. });
  149. });
  150. this.dialogList.sort((a, b) => {
  151. return b.lv - a.lv;
  152. });
  153. this.$store.commit("changeAlarmlist", this.alarmList);
  154. this.$store.commit("setWarning", this.dialogList);
  155. if (!this.socketLeaveFlag) {
  156. // 没有离开——重连
  157. // websocket重连
  158. this.socketReconnect1();
  159. }
  160. })
  161. .catch(() => {
  162. requestResult.forEach((ele, index) => {
  163. ele
  164. .then(({ data }) => {
  165. data?.ls?.forEach((ele) => {
  166. this.pushALarmItem(ele);
  167. });
  168. })
  169. .catch((error) => {
  170. ElNotification({
  171. type: "error",
  172. title: "查询历史未处理报警请求出错!",
  173. dangerouslyUseHTMLString: true,
  174. message: `<div class="currentRequestErrorNotification">
  175. <p><span>主要参数:</p>
  176. <p style="color:var(--el-color-primary)"><span class="errorTitle">alarmType:</span><span class="errorDesc">"${this.requestAlarmHistoryParams[index].alarmType}"</span></p>
  177. <p style="color:var(--el-color-primary)"><span class="errorTitle">deviceType:</span><span class="errorDesc">"${this.requestAlarmHistoryParams[index].deviceType}"</span></p>
  178. <p style="color:var(--el-color-danger)"><span class="errorTitle">错误正文:</span><span class="errorDesc">${error}</span></p>
  179. </div>`,
  180. });
  181. });
  182. });
  183. });
  184. },
  185. mounted() {},
  186. unmounted() {
  187. console.log("离开标记", this.socketLeaveFlag);
  188. },
  189. methods: {
  190. setting() {
  191. this.displaySetting = true;
  192. },
  193. handleConfirm(flag) {
  194. this.playAudioEffect();
  195. if (flag) {
  196. this.dialogList = [];
  197. this.$store.commit("setWarning", this.dialogList);
  198. setTimeout(() => {
  199. this.$refs.deligoChild && this.$refs.deligoChild.init(this.warnList);
  200. }, 0);
  201. }
  202. },
  203. playAudioEffect() {
  204. const lv1Config = this.getConfigItem(1);
  205. let lv1Play = false;
  206. if (lv1Config.isAlarmSound) {
  207. lv1Play = this.dialogList.some((ele) => {
  208. return ele.lv === 1 && !ele.confirm;
  209. });
  210. }
  211. const lv2Config = this.getConfigItem(2);
  212. let lv2Play = false;
  213. if (lv2Config.isAlarmSound) {
  214. lv2Play = this.dialogList.some((ele) => {
  215. return ele.lv === 2 && !ele.confirm;
  216. });
  217. }
  218. const lv3Config = this.getConfigItem(3);
  219. let lv3Play = false;
  220. if (lv3Config.isAlarmSound) {
  221. lv3Play = this.dialogList.some((ele) => {
  222. return ele.lv === 3 && !ele.confirm;
  223. });
  224. }
  225. const lv4Config = this.getConfigItem(4);
  226. let lv4Play = false;
  227. if (lv4Config.isAlarmSound) {
  228. lv4Play = this.dialogList.some((ele) => {
  229. return ele.lv === 4 && !ele.confirm;
  230. });
  231. }
  232. const lv5Config = this.getConfigItem(5);
  233. let lv5Play = false;
  234. if (lv5Config.isAlarmSound) {
  235. lv5Play = this.dialogList.some((ele) => {
  236. return ele.lv === 5 && !ele.confirm;
  237. });
  238. }
  239. // console.log(lv1Play, lv2Play, lv3Play, lv4Play, lv5Play);
  240. if (lv5Play && !this.seriousWarning) {
  241. this.seriousWarning = true;
  242. this.audioElement = new Audio();
  243. this.audioElement.src = "./static/sound/lv5.mp3";
  244. this.audioElement.loop = true;
  245. this.audioElement?.play();
  246. } else if (
  247. (lv1Play || lv2Play || lv3Play || lv4Play) &&
  248. !this.seriousWarning
  249. ) {
  250. this.audioElement = new Audio();
  251. this.audioElement.src = "./static/sound/lv4.mp3";
  252. this.audioElement.addEventListener("ended", () => {
  253. this.audioElement?.removeEventListener(
  254. "ended",
  255. this.stopPlayAudioEffect
  256. );
  257. });
  258. this.audioElement?.play();
  259. } else {
  260. if (!this.seriousWarning) {
  261. this.stopPlayAudioEffect();
  262. }
  263. }
  264. },
  265. stopPlayAudioEffect() {
  266. this.seriousWarning = false;
  267. if (this.audioElement) {
  268. this.audioElement.pause();
  269. this.audioElement.currentTime = 0;
  270. this.audioElement.loop = false;
  271. }
  272. this.audioElement = null;
  273. },
  274. //获取报警配置
  275. getAlarmConfig() {
  276. if (localStorage.getItem("alarmConfigArray")) {
  277. this.alarmConfigArray = JSON.parse(
  278. localStorage.getItem("alarmConfigArray")
  279. );
  280. } else {
  281. this.alarmConfigArray = [
  282. {
  283. id: "1",
  284. alarmLevel: 1,
  285. isAlart: false,
  286. isAlarmSound: false,
  287. isContinuousAlarm: false,
  288. },
  289. {
  290. id: "2",
  291. alarmLevel: 2,
  292. isAlart: false,
  293. isAlarmSound: false,
  294. isContinuousAlarm: false,
  295. },
  296. {
  297. id: "3",
  298. alarmLevel: 3,
  299. isAlart: true,
  300. isAlarmSound: false,
  301. isContinuousAlarm: false,
  302. },
  303. {
  304. id: "4",
  305. alarmLevel: 4,
  306. isAlart: true,
  307. isAlarmSound: true,
  308. isContinuousAlarm: false,
  309. },
  310. {
  311. id: "5",
  312. alarmLevel: 5,
  313. isAlart: true,
  314. isAlarmSound: true,
  315. isContinuousAlarm: true,
  316. },
  317. ];
  318. localStorage.setItem(
  319. "alarmConfigArray",
  320. JSON.stringify(this.alarmConfigArray)
  321. );
  322. }
  323. },
  324. getAlarmName(alarmItem) {
  325. let alarmName = "";
  326. if (alarmItem.deviceType === "booststation") {
  327. alarmName = "升压站报警";
  328. } else if (alarmItem.deviceType === "inverter") {
  329. alarmName = "光伏报警";
  330. } else if (alarmItem.deviceType === "windturbine") {
  331. alarmName = "设备报警";
  332. } else if (alarmItem.deviceType === "station") {
  333. alarmName = "场站";
  334. }
  335. if (alarmItem.alarmType === "custom") {
  336. alarmName = "自定义报警";
  337. }
  338. return alarmName;
  339. },
  340. getLvName(alarmItem) {
  341. if (alarmItem.rank === 1) {
  342. return "低级";
  343. } else if (alarmItem.rank === 2) {
  344. return "低中级";
  345. } else if (alarmItem.rank === 3) {
  346. return "中级";
  347. } else if (alarmItem.rank === 4) {
  348. return "中高级";
  349. } else if (alarmItem.rank === 5) {
  350. return "高级";
  351. }
  352. },
  353. //查历史报警
  354. getAlarmHistory(alarmType) {
  355. let params = {
  356. pageNum: 1,
  357. pageSize: 5000,
  358. alarmId: "",
  359. alarmType,
  360. stationid: "",
  361. deviceid: "",
  362. modelId: "",
  363. components: "",
  364. description: "",
  365. isclose: false,
  366. begin: "",
  367. end: "",
  368. };
  369. if (params.alarmType == "windturbine") {
  370. params.stationid = "SXJ_KGDL_DJY_FDC_STA";
  371. } else if (params.alarmType == "inverter") {
  372. params.stationid = "SXJ_KGDL_JR_GDC_STA";
  373. }
  374. return GetDeviceTableData(params, 12000);
  375. },
  376. pushALarmItem(alarmItem, type) {
  377. const configItem = this.getConfigItem(alarmItem.rank);
  378. const alarmOption = {
  379. id: alarmItem.id ? alarmItem.id : alarmItem.tbname,
  380. lv: alarmItem.rank,
  381. lvName: this.getLvName(alarmItem),
  382. rank: alarmItem.rank,
  383. class: `animate__bounceInRight lv${alarmItem.rank}`,
  384. deviceId: alarmItem.deviceId,
  385. faultCause: alarmItem.faultCause,
  386. resolvent: alarmItem.resolvent,
  387. characteristic: alarmItem.characteristic,
  388. code: alarmItem.code,
  389. wpName: alarmItem.stationName
  390. ? alarmItem.stationName
  391. : alarmItem.wpName,
  392. stationId: alarmItem.stationId ? alarmItem.stationId : alarmItem.wpId,
  393. isClose: alarmItem.closeTime ? true : alarmItem.endts ? true : false,
  394. isCloseName: alarmItem.closeTime
  395. ? "已解除"
  396. : alarmItem.endts
  397. ? "已解除"
  398. : "未解除",
  399. alarmId: alarmItem.alarmId,
  400. alarmType: alarmItem.alarmType,
  401. alarmName: this.getAlarmName(alarmItem),
  402. description: alarmItem.description,
  403. deviceType: alarmItem.deviceType,
  404. oval: alarmItem.oval,
  405. triggerType: alarmItem.triggerType,
  406. ts: alarmItem.ts
  407. ? dayjs(alarmItem.ts).valueOf()
  408. : dayjs(alarmItem.updateTime).valueOf(),
  409. endts: alarmItem.endts
  410. ? dayjs(alarmItem.endts).format("YYYY-MM-DD HH:mm:ss")
  411. : null,
  412. closeTime: alarmItem.closeTime
  413. ? dayjs(alarmItem.closeTime).format("YYYY-MM-DD HH:mm:ss")
  414. : null,
  415. deviceName: alarmItem.deviceName
  416. ? alarmItem.deviceName
  417. : alarmItem.code,
  418. tsName: alarmItem.ts
  419. ? new Date(alarmItem.ts).formatDate("MM-dd hh:mm:ss")
  420. : new Date(alarmItem.updateTime).formatDate("MM-dd hh:mm:ss"),
  421. fullTsName: alarmItem.ts
  422. ? new Date(alarmItem.ts).formatDate("yyyy-MM-dd hh:mm:ss")
  423. : new Date(alarmItem.updateTime).formatDate("yyyy-MM-dd hh:mm:ss"),
  424. };
  425. if (
  426. alarmItem.alarmType == "booststation" &&
  427. alarmItem.deviceType != "custom"
  428. ) {
  429. if (
  430. configItem.isAlarmSound ||
  431. configItem.isAlart ||
  432. configItem.isContinuousAlarm
  433. ) {
  434. let a = {};
  435. a[`${alarmItem.stationId}`] = alarmItem.closeTime ? false : true;
  436. this.alarmList.push(a);
  437. this.alarmList = [
  438. ...new Set(this.alarmList.map((t) => JSON.stringify(t))),
  439. ].map((s) => JSON.parse(s));
  440. }
  441. }
  442. if (
  443. (configItem.isAlarmSound ||
  444. configItem.isAlart ||
  445. configItem.isContinuousAlarm) &&
  446. alarmItem.deviceType != "custom"
  447. ) {
  448. if (type && type == "ws") {
  449. this.dialogList.unshift(alarmOption);
  450. } else {
  451. this.dialogList.push(alarmOption);
  452. }
  453. }
  454. this.playAudioEffect();
  455. },
  456. getConfigItem(lv) {
  457. return (
  458. this.alarmConfigArray.find((ele) => {
  459. return ele.alarmLevel === lv;
  460. }) || {}
  461. );
  462. },
  463. // 拖拽相关
  464. startDrag(event) {
  465. this.currentX = event.clientX;
  466. this.currentY = event.clientY;
  467. },
  468. stopDrag(event) {
  469. let x = event.clientX - this.currentX;
  470. let y = event.clientY - this.currentY;
  471. this.x += x;
  472. if (this.x < 60) {
  473. this.x = 60;
  474. } else if (this.x > window.innerWidth - 20) {
  475. this.x = window.innerWidth - 82;
  476. }
  477. this.y += y;
  478. if (this.y > window.innerHeight - 20) {
  479. this.y = window.innerHeight - 32;
  480. } else if (this.y < 0) {
  481. this.y = 0;
  482. }
  483. },
  484. //开关列表
  485. displayAlarm(val) {
  486. this.displayAlarms = !this.displayAlarms;
  487. if (this.displayAlarms == true) {
  488. setTimeout(() => {
  489. this.$refs.deligoChild && this.$refs.deligoChild.init(this.warnList);
  490. }, 0);
  491. }
  492. },
  493. displayClose() {
  494. this.displayAlarms = false;
  495. },
  496. // websocket启动
  497. createWebSocket1() {
  498. // let webSocketLink = `ws://192.168.1.102:6014/websocket/${this.$store.state.user.userId}_${this.$store.state.user.authToken}`;
  499. let webSocketLink1 = `ws://10.81.3.154:6014/websocket/${this.$store.state.user.userId}_${this.$store.state.user.authToken}`; // webSocket地址
  500. try {
  501. if ("WebSocket" in window) {
  502. this.socketObj1 = new WebSocket(webSocketLink1);
  503. }
  504. // websocket事件绑定
  505. this.socketEventBind1();
  506. } catch (e) {
  507. console.log("catch" + e);
  508. // websocket重连
  509. this.socketReconnect1();
  510. }
  511. },
  512. // websocket事件绑定
  513. socketEventBind1() {
  514. // 连接成功建立的回调
  515. this.socketObj1.onopen = this.onopenCallback1;
  516. // 连接发生错误的回调
  517. this.socketObj1.onerror = this.onerrorCallback1;
  518. // 连接关闭的回调
  519. this.socketObj1.onclose = this.oncloseCallback1;
  520. // 向后端发送数据的回调
  521. this.socketObj1.onsend = this.onsendCallback1;
  522. // 接收到消息的回调
  523. this.socketObj1.onmessage = this.getMessageCallback1;
  524. //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
  525. window.onbeforeunload = () => {
  526. this.socketObj1.close();
  527. };
  528. },
  529. // websocket重连
  530. socketReconnect1() {
  531. if (this.socketReconnectLock1) {
  532. return;
  533. }
  534. this.socketReconnectLock1 = true;
  535. this.socketReconnectTimer1 && clearTimeout(this.socketReconnectTimer1);
  536. this.socketReconnectTimer1 = setTimeout(() => {
  537. console.log("WebSocket1:重连中...");
  538. this.socketReconnectLock1 = false;
  539. // websocket启动
  540. this.createWebSocket1();
  541. }, 4000);
  542. },
  543. // 连接成功建立的回调
  544. onopenCallback1: function (event) {
  545. console.log("WebSocket1:已连接");
  546. // 心跳检测重置
  547. this.heartCheck1.reset().start();
  548. },
  549. // 连接发生错误的回调
  550. onerrorCallback1: function (event) {
  551. console.log("WebSocket1:发生错误");
  552. // websocket重连
  553. this.socketReconnect1();
  554. },
  555. // 连接关闭的回调
  556. oncloseCallback1: function (event) {
  557. console.log("WebSocket1:已关闭");
  558. // 心跳检测重置
  559. this.heartCheck1.reset();
  560. if (!this.socketLeaveFlag1) {
  561. // 没有离开——重连
  562. // websocket重连
  563. this.socketReconnect1();
  564. }
  565. },
  566. // 向后端发送数据的回调
  567. onsendCallback1: function () {
  568. console.log("WebSocket1:发送信息给后端");
  569. },
  570. // 接收到消息的回调
  571. getMessageCallback1: function (msg) {
  572. // console.log(msg);
  573. if (Object.keys(msg) && msg.data == "ok") {
  574. // 心跳回复——心跳检测重置
  575. // 收到心跳检测回复就说明连接正常
  576. console.log("收到心跳检测回复1");
  577. // 心跳检测重置
  578. this.heartCheck1.reset().start();
  579. } else {
  580. // 普通推送——正常处理
  581. console.log("收到推送消息1");
  582. let data = JSON.parse(msg.data);
  583. // 相关处理
  584. if (data) {
  585. this.pushALarmItem(data, "ws");
  586. // this.dialogList.sort((a, b) => {
  587. // return b.lv - a.lv;
  588. // });
  589. this.$store.commit("changeAlarmlist", this.alarmList);
  590. this.$store.commit("setWarning", this.dialogList);
  591. }
  592. }
  593. },
  594. },
  595. watch: {
  596. isLogined: {
  597. handler(res) {
  598. if (!res && this.socketObj1) {
  599. // 离开标记
  600. this.socketLeaveFlag1 = true;
  601. // 关闭WebSocket
  602. this.socketObj1.close();
  603. }
  604. },
  605. immediate: true,
  606. },
  607. },
  608. };
  609. </script>
  610. <style lang="less" scoped>
  611. .alarmDeligo {
  612. z-index: 2003;
  613. position: relative;
  614. img {
  615. width: 43px;
  616. max-width: 43px;
  617. height: 43px;
  618. z-index: 3;
  619. }
  620. img:first-child {
  621. width: 53px;
  622. height: 57px;
  623. max-width: 53px;
  624. top: -7px;
  625. left: -5px;
  626. position: absolute;
  627. z-index: 3;
  628. }
  629. }
  630. .el-badge {
  631. &.active {
  632. .el-badge__content--danger {
  633. opacity: 0 !important;
  634. }
  635. }
  636. }
  637. .el-badge__content--danger {
  638. background: #ff4e00 !important;
  639. border: none !important;
  640. }
  641. .el-badge__content.is-fixed {
  642. top: 7px !important;
  643. right: 14px !important;
  644. z-index: 2003 !important;
  645. }
  646. .trans {
  647. animation: circleProgess 3s linear infinite;
  648. }
  649. @keyframes circleProgess {
  650. 0% {
  651. transform: rotateZ(360deg);
  652. }
  653. 25% {
  654. transform: rotateZ(270deg);
  655. }
  656. 50% {
  657. transform: rotateZ(180deg);
  658. }
  659. 75% {
  660. transform: rotateZ(90deg);
  661. }
  662. 100% {
  663. transform: rotateZ(0deg);
  664. }
  665. }
  666. </style>