LineChart.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. <!-- <template>
  2. <div :class="className" :style="{height:height,width:width}" />
  3. </template>
  4. <script>
  5. import echarts from 'echarts'
  6. require('echarts/theme/macarons') // echarts theme
  7. import resize from './mixins/resize'
  8. export default {
  9. mixins: [resize],
  10. props: {
  11. className: {
  12. type: String,
  13. default: 'chart'
  14. },
  15. width: {
  16. type: String,
  17. default: '100%'
  18. },
  19. height: {
  20. type: String,
  21. default: '350px'
  22. },
  23. autoResize: {
  24. type: Boolean,
  25. default: true
  26. },
  27. chartData: {
  28. type: Object,
  29. required: true
  30. }
  31. },
  32. data() {
  33. return {
  34. chart: null
  35. }
  36. },
  37. watch: {
  38. chartData: {
  39. deep: true,
  40. handler(val) {
  41. this.setOptions(val)
  42. }
  43. }
  44. },
  45. mounted() {
  46. this.$nextTick(() => {
  47. this.initChart()
  48. })
  49. },
  50. beforeDestroy() {
  51. if (!this.chart) {
  52. return
  53. }
  54. this.chart.dispose()
  55. this.chart = null
  56. },
  57. methods: {
  58. initChart() {
  59. this.chart = echarts.init(this.$el, 'macarons')
  60. this.setOptions(this.chartData)
  61. },
  62. setOptions({ expectedData, actualData } = {}) {
  63. this.chart.setOption({
  64. xAxis: {
  65. data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
  66. boundaryGap: false,
  67. axisTick: {
  68. show: false
  69. }
  70. },
  71. grid: {
  72. left: 10,
  73. right: 10,
  74. bottom: 20,
  75. top: 30,
  76. containLabel: true
  77. },
  78. tooltip: {
  79. trigger: 'axis',
  80. axisPointer: {
  81. type: 'cross'
  82. },
  83. padding: [5, 10]
  84. },
  85. yAxis: {
  86. axisTick: {
  87. show: false
  88. }
  89. },
  90. legend: {
  91. data: ['expected', 'actual']
  92. },
  93. series: [{
  94. name: 'expected', itemStyle: {
  95. normal: {
  96. color: '#FF005A',
  97. lineStyle: {
  98. color: '#FF005A',
  99. width: 2
  100. }
  101. }
  102. },
  103. smooth: true,
  104. type: 'line',
  105. data: expectedData,
  106. animationDuration: 2800,
  107. animationEasing: 'cubicInOut'
  108. },
  109. {
  110. name: 'actual',
  111. smooth: true,
  112. type: 'line',
  113. itemStyle: {
  114. normal: {
  115. color: '#3888fa',
  116. lineStyle: {
  117. color: '#3888fa',
  118. width: 2
  119. },
  120. areaStyle: {
  121. color: '#f3f8ff'
  122. }
  123. }
  124. },
  125. data: actualData,
  126. animationDuration: 2800,
  127. animationEasing: 'quadraticOut'
  128. }]
  129. })
  130. }
  131. }
  132. }
  133. </script>
  134. -->
  135. <template>
  136. <div ref="chartDiv" style="width: 100%; height: 350px"></div>
  137. </template>
  138. <script>
  139. import { gpu_info } from "@/api/knowledge";
  140. import * as am5 from "@amcharts/amcharts5";
  141. import * as am5xy from "@amcharts/amcharts5/xy";
  142. import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
  143. export default {
  144. name: "AmAreaChart",
  145. props: {
  146. className: {
  147. type: String,
  148. default: "chart",
  149. },
  150. width: {
  151. type: String,
  152. default: "100%",
  153. },
  154. height: {
  155. type: String,
  156. default: "350px",
  157. },
  158. autoResize: {
  159. type: Boolean,
  160. default: true,
  161. },
  162. data: {
  163. type: Object,
  164. default: {},
  165. },
  166. },
  167. data() {
  168. return {
  169. chart: null,
  170. root: null,
  171. series1: null,
  172. series2: null,
  173. xAxis: null,
  174. data1: [],
  175. data2: [],
  176. timer: null,
  177. isDestroyed: false,
  178. isOnDashboard: false,
  179. };
  180. },
  181. watch: {
  182. $route: {
  183. immediate: true,
  184. handler(newRoute) {
  185. this.isOnDashboard = newRoute.path === "/dashboard";
  186. if (this.isOnDashboard) {
  187. // 清理旧的资源
  188. this.cleanupResources();
  189. // 重新初始化
  190. this.$nextTick(() => {
  191. this.initChart();
  192. /* this.startLiveData(); */
  193. });
  194. } else {
  195. this.stopLiveData();
  196. }
  197. },
  198. },
  199. data: {
  200. immediate: true,
  201. handler(newValue) {
  202. if (!newValue || !this.series1 || !this.series2) return;
  203. const currentTime = Date.now();
  204. // 更新数据数组
  205. this.data1.push({
  206. date: currentTime,
  207. value: newValue.data.system.cpu.total_usage || 0, // 假设传入的data中有cpu字段
  208. });
  209. this.data2.push({
  210. date: currentTime,
  211. value:newValue.data.system.gpu.devices[0]?.power || 0/* newValue.data.system.gpu.devices|| */ , // 假设传入的data中有gpu字段
  212. });
  213. // 限制数据点数量
  214. if (this.data1.length > 15) {
  215. this.data1 = this.data1.slice(-15);
  216. this.data2 = this.data2.slice(-15);
  217. }
  218. // 更新图表
  219. this.series1.data.setAll(this.data1);
  220. this.series2.data.setAll(this.data2);
  221. // 更新X轴范围
  222. if (this.data1.length > 1) {
  223. this.xAxis.zoomToDates(
  224. new Date(this.data1[0].date),
  225. new Date(this.data1[this.data1.length - 1].date)
  226. );
  227. }
  228. },
  229. },
  230. },
  231. mounted() {
  232. /* this.initChart(); */
  233. /* if (this.isOnDashboard) {
  234. this.startLiveData();
  235. } */
  236. },
  237. beforeDestroy() {
  238. this.cleanupResources();
  239. },
  240. methods: {
  241. /* async cleanupResources() {
  242. this.isDestroyed = true;
  243. this.stopLiveData();
  244. if (this.timer) {
  245. clearTimeout(this.timer);
  246. this.timer = null;
  247. }
  248. }, */
  249. initChart() {
  250. // 确保DOM元素存在
  251. if (!this.$refs.chartDiv) {
  252. return;
  253. }
  254. try {
  255. this.isDestroyed = false; // 重置销毁标志
  256. const root = (this.root = am5.Root.new(this.$refs.chartDiv));
  257. root.setThemes([am5themes_Animated.new(root)]);
  258. // 创建图表
  259. this.chart = root.container.children.push(
  260. am5xy.XYChart.new(root, {
  261. panX: true,
  262. panY: true,
  263. wheelX: "panX",
  264. wheelY: "zoomX",
  265. pinchZoomX: true,
  266. })
  267. );
  268. // 创建Y轴
  269. let yAxis = this.chart.yAxes.push(
  270. am5xy.ValueAxis.new(root, {
  271. renderer: am5xy.AxisRendererY.new(root, {
  272. labelFormat: "{value}%",
  273. }),
  274. min: 0,
  275. max: 100,
  276. })
  277. );
  278. // 创建X轴
  279. this.xAxis = this.chart.xAxes.push(
  280. am5xy.DateAxis.new(root, {
  281. baseInterval: { timeUnit: "second", count: 1 },
  282. renderer: am5xy.AxisRendererX.new(root, {
  283. minGridDistance: 200, // 增加标签之间的最小距离
  284. cellStartLocation: 0.2,
  285. cellEndLocation: 0.8,
  286. dateFormats: {
  287. second: "HH:mm",
  288. minute: "HH:mm",
  289. hour: "HH:mm",
  290. day: "HH:mm",
  291. },
  292. periodChangeDateFormats: {
  293. second: "HH:mm",
  294. minute: "HH:mm",
  295. hour: "HH:mm",
  296. day: "HH:mm",
  297. },
  298. // 减少标签数量
  299. maxDeviation: 0.5,
  300. // 只显示首尾和少量中间的标签
  301. minLabelGap: 50,
  302. }),
  303. // 控制要显示的标签数量
  304. gridIntervals: [
  305. { timeUnit: "second", count: 5 }, // 每5秒显示一个标签
  306. ],
  307. // 确保时间轴平滑滚动
  308. interpolationDuration: 500,
  309. interpolationEasing: am5.ease.cubic,
  310. })
  311. );
  312. // 创建第一条线(expected)
  313. this.series1 = this.chart.series.push(
  314. am5xy.SmoothedXLineSeries.new(root, {
  315. name: "CPU",
  316. xAxis: this.xAxis,
  317. yAxis: yAxis,
  318. valueYField: "value",
  319. valueXField: "date",
  320. tension: 0.8,
  321. stroke: am5.color("#FF005A"),
  322. strokeWidth: 3,
  323. tooltip: am5.Tooltip.new(root, {
  324. labelText: "{name}: {valueY}%",
  325. }),
  326. })
  327. );
  328. // 创建第二条线(actual)
  329. this.series2 = this.chart.series.push(
  330. am5xy.SmoothedXLineSeries.new(root, {
  331. name: "GPU",
  332. xAxis: this.xAxis,
  333. yAxis: yAxis,
  334. valueYField: "value",
  335. valueXField: "date",
  336. tension: 0.8,
  337. stroke: am5.color("#3888fa"),
  338. strokeWidth: 3,
  339. fill: am5.color("#f3f8ff"),
  340. tooltip: am5.Tooltip.new(root, {
  341. labelText: "{name}: {valueY}%",
  342. }),
  343. })
  344. );
  345. // 添加图例
  346. const legend = this.chart.children.push(
  347. am5.Legend.new(root, {
  348. centerX: am5.percent(50),
  349. x: am5.percent(50),
  350. layout: root.horizontalLayout,
  351. })
  352. );
  353. legend.data.setAll(this.chart.series.values);
  354. // 启用动画
  355. this.series1.appear(1000);
  356. this.series2.appear(1000);
  357. this.chart.appear(1000, 100);
  358. // 为 series1 添加圆点
  359. this.series1.bullets.push((root) => {
  360. const container = am5.Container.new(root, {});
  361. // 创建光晕效果(外圈)
  362. const halo = am5.Circle.new(root, {
  363. radius: 12,
  364. fill: am5.color("#FF005A"),
  365. fillOpacity: 0.3,
  366. stroke: am5.color("#FF005A"),
  367. strokeOpacity: 0.3,
  368. strokeWidth: 2,
  369. });
  370. // 创建主圆点(内圈)
  371. const circle = am5.Circle.new(root, {
  372. radius: 6,
  373. fill: am5.color("#FF005A"),
  374. fillOpacity: 0.8,
  375. stroke: am5.color("#fff"),
  376. strokeWidth: 2,
  377. });
  378. // 将两个圆形添加到容器
  379. container.children.push(halo);
  380. container.children.push(circle);
  381. // 创建光晕的呼吸动画
  382. halo.animate({
  383. key: "radius",
  384. from: 8,
  385. to: 16,
  386. duration: 1000,
  387. loops: Infinity,
  388. easing: am5.ease.cubic,
  389. direction: "alternate",
  390. });
  391. halo.animate({
  392. key: "fillOpacity",
  393. from: 0.3,
  394. to: 0.1,
  395. duration: 1000,
  396. loops: Infinity,
  397. easing: am5.ease.cubic,
  398. direction: "alternate",
  399. });
  400. return am5.Bullet.new(root, {
  401. sprite: container,
  402. });
  403. });
  404. // 为 series2 添加圆点
  405. this.series2.bullets.push((root) => {
  406. const container = am5.Container.new(root, {});
  407. // 创建光晕效果(外圈)
  408. const halo = am5.Circle.new(root, {
  409. radius: 12,
  410. fill: am5.color("#3888fa"),
  411. fillOpacity: 0.3,
  412. stroke: am5.color("#3888fa"),
  413. strokeOpacity: 0.3,
  414. strokeWidth: 2,
  415. });
  416. // 创建主圆点(内圈)
  417. const circle = am5.Circle.new(root, {
  418. radius: 6,
  419. fill: am5.color("#3888fa"),
  420. fillOpacity: 0.8,
  421. stroke: am5.color("#fff"),
  422. strokeWidth: 2,
  423. });
  424. // 将两个圆形添加到容器
  425. container.children.push(halo);
  426. container.children.push(circle);
  427. // 创建光晕的呼吸动画
  428. halo.animate({
  429. key: "radius",
  430. from: 8,
  431. to: 16,
  432. duration: 1000,
  433. loops: Infinity,
  434. easing: am5.ease.cubic,
  435. direction: "alternate",
  436. });
  437. halo.animate({
  438. key: "fillOpacity",
  439. from: 0.3,
  440. to: 0.1,
  441. duration: 1000,
  442. loops: Infinity,
  443. easing: am5.ease.cubic,
  444. direction: "alternate",
  445. });
  446. return am5.Bullet.new(root, {
  447. sprite: container,
  448. });
  449. });
  450. } catch (error) {
  451. console.error("初始化图表时出错:", error);
  452. }
  453. },
  454. async startLiveData() {
  455. // 如果已经不在dashboard页面,则停止请求
  456. if (!this.isOnDashboard) {
  457. return;
  458. }
  459. try {
  460. const response = await gpu_info();
  461. const currentTime = Date.now();
  462. // 更新数据
  463. this.data1.push({
  464. date: currentTime,
  465. value: Math.random() * 30 + 50,
  466. });
  467. this.data2.push({
  468. date: currentTime,
  469. value: response.data.data.cpu.total_usage,
  470. });
  471. // 限制只保留最新的6个数据点
  472. if (this.data1.length > 6) {
  473. this.data1 = this.data1.slice(-6);
  474. this.data2 = this.data2.slice(-6);
  475. }
  476. // 更新图表
  477. this.series1.data.setAll(this.data1);
  478. this.series2.data.setAll(this.data2);
  479. // 更新X轴范围
  480. if (this.data1.length > 1) {
  481. this.xAxis.zoomToDates(
  482. new Date(this.data1[0].date),
  483. new Date(this.data1[this.data1.length - 1].date)
  484. );
  485. }
  486. if (response.status !== 200) return;
  487. // 设置下一次请求的定时器
  488. this.timer = setTimeout(() => {
  489. this.startLiveData();
  490. }, 2000);
  491. } catch (error) {
  492. console.error("Failed to fetch data:", error);
  493. // 发生错误时,等待一段时间后重试
  494. this.timer = setTimeout(() => {
  495. this.startLiveData();
  496. }, 5000); // 错误后等待5秒重试
  497. }
  498. },
  499. stopLiveData() {
  500. if (this.timer) {
  501. clearTimeout(this.timer); // 改用clearTimeout
  502. this.timer = null;
  503. }
  504. },
  505. async cleanupResources() {
  506. this.isDestroyed = true;
  507. this.stopLiveData();
  508. if (this.timer) {
  509. clearInterval(this.timer);
  510. this.timer = null;
  511. }
  512. // 清空数据数组
  513. this.data1 = [];
  514. this.data2 = [];
  515. // 销毁图表实例
  516. if (this.root) {
  517. this.root.dispose();
  518. this.root = null;
  519. }
  520. this.chart = null;
  521. this.series1 = null;
  522. this.series2 = null;
  523. this.xAxis = null;
  524. },
  525. /*
  526. websocket请求
  527. websocket: null, // 新增 WebSocket 连接对象
  528. if (this.isOnDashboard) {
  529. this.initWebSocket(); // 替换原来的 startLiveData
  530. }
  531. initWebSocket() {
  532. // 初始化 WebSocket 连接
  533. this.websocket = new WebSocket('ws://192.168.1.199:8084/ws/gpu-info/');
  534. // WebSocket 连接成功回调
  535. this.websocket.onopen = () => {
  536. console.log('WebSocket 连接已建立');
  537. };
  538. // WebSocket 接收消息回调
  539. this.websocket.onmessage = (event) => {
  540. if (!this.isOnDashboard) {
  541. this.closeWebSocket();
  542. return;
  543. }
  544. try {
  545. const response = JSON.parse(event.data);
  546. const currentTime = Date.now();
  547. // 更新数据
  548. this.data1.push({
  549. date: currentTime,
  550. value: Math.random() * 30 + 50 // 模拟数据保持不变
  551. });
  552. this.data2.push({
  553. date: currentTime,
  554. value: response.data.cpu.total_usage
  555. });
  556. // 保持固定数量的数据点
  557. if (this.data1.length > 15) {
  558. this.data1.shift();
  559. this.data2.shift();
  560. }
  561. // 更新图表
  562. this.series1.data.setAll(this.data1);
  563. this.series2.data.setAll(this.data2);
  564. // 更新X轴范围
  565. if (this.data1.length > 1) {
  566. this.xAxis.zoomToDates(
  567. new Date(this.data1[0].date),
  568. new Date(this.data1[this.data1.length - 1].date)
  569. );
  570. }
  571. } catch (error) {
  572. console.error("处理 WebSocket 数据时出错:", error);
  573. }
  574. };
  575. // WebSocket 错误处理
  576. this.websocket.onerror = (error) => {
  577. console.error("WebSocket 错误:", error);
  578. };
  579. // WebSocket 连接关闭处理
  580. this.websocket.onclose = () => {
  581. console.log("WebSocket 连接已关闭");
  582. // 可以在这里添加重连逻辑
  583. if (this.isOnDashboard && !this.isDestroyed) {
  584. setTimeout(() => this.initWebSocket(), 3000);
  585. }
  586. };
  587. },
  588. closeWebSocket() {
  589. if (this.websocket) {
  590. this.websocket.close();
  591. this.websocket = null;
  592. }
  593. },
  594. cleanupResources() {
  595. this.isDestroyed = true;
  596. this.closeWebSocket(); // 替换原来的 stopLiveData
  597. if (this.root) {
  598. this.root.dispose();
  599. }
  600. } */
  601. },
  602. };
  603. </script>