玛尔挡水温监测系统
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

TemperatureChart.vue 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <template>
  2. <div class="temperature-chart">
  3. <div class="chart-container">
  4. <!-- 趋势图 -->
  5. <div ref="trendChartRef" class="trend-chart" style="width: 60%; height: 600px;"></div>
  6. <!-- 时刻温度对比图 -->
  7. <div ref="momentChartRef" class="moment-chart" style="width: 40%; height: 600px;"></div>
  8. </div>
  9. </div>
  10. </template>
  11. <script setup>
  12. import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
  13. import * as echarts from 'echarts'
  14. // Props
  15. const props = defineProps({
  16. chartData: {
  17. type: Array,
  18. default: () => []
  19. },
  20. sensorColumns: {
  21. type: Array,
  22. default: () => []
  23. }
  24. })
  25. // 响应式数据
  26. const trendChartRef = ref(null)
  27. const momentChartRef = ref(null)
  28. let trendChartInstance = null
  29. let momentChartInstance = null
  30. // 当前选中的时刻数据
  31. const selectedMoment = ref(null)
  32. // 暴露方法给父组件调用
  33. const resizeChart = () => {
  34. if (trendChartInstance) {
  35. trendChartInstance.resize()
  36. }
  37. if (momentChartInstance) {
  38. momentChartInstance.resize()
  39. }
  40. }
  41. // 暴露方法
  42. defineExpose({
  43. resizeChart
  44. })
  45. // 初始化图表
  46. const initChart = () => {
  47. if (trendChartRef.value && momentChartRef.value) {
  48. trendChartInstance = echarts.init(trendChartRef.value)
  49. momentChartInstance = echarts.init(momentChartRef.value)
  50. updateCharts()
  51. }
  52. }
  53. // 更新图表数据
  54. const updateCharts = () => {
  55. if (!trendChartInstance || !momentChartInstance || !props.chartData || props.chartData.length === 0) {
  56. return
  57. }
  58. // 准备图表数据 - 按时间排序
  59. const sortedData = [...props.chartData].sort((a, b) => new Date(a.datetime) - new Date(b.datetime))
  60. const series = []
  61. // 为每个传感器创建数据系列
  62. props.sensorColumns.forEach(sensor => {
  63. // 过滤出有效的数据点
  64. const validDataPoints = sortedData
  65. .map(item => {
  66. const value = item[sensor.sensorName]
  67. return {
  68. datetime: item.datetime,
  69. value: value !== null && value !== undefined && !isNaN(value) ? parseFloat(value) : null
  70. }
  71. })
  72. .filter(point => point.value !== null)
  73. // 只有当传感器有有效数据时才添加到系列中
  74. if (validDataPoints.length > 0) {
  75. series.push({
  76. name: sensor.sensorName,
  77. type: 'line',
  78. data: validDataPoints.map(point => [point.datetime, point.value]),
  79. showSymbol: false,
  80. smooth: true,
  81. symbol: 'circle',
  82. symbolSize: 6,
  83. lineStyle: {
  84. width: 1
  85. },
  86. itemStyle: {
  87. borderWidth: 1
  88. }
  89. })
  90. }
  91. })
  92. // 趋势图配置
  93. const trendOption = {
  94. title: {
  95. text: '温度监控趋势图',
  96. left: 'center',
  97. textStyle: {
  98. fontSize: 16,
  99. fontWeight: 'bold',
  100. color: '#303133'
  101. }
  102. },
  103. tooltip: {
  104. trigger: 'axis',
  105. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  106. borderColor: '#e4e7ed',
  107. borderWidth: 1,
  108. textStyle: {
  109. color: '#303133'
  110. },
  111. axisPointer: {
  112. type: 'cross',
  113. label: {
  114. backgroundColor: '#6a7985'
  115. }
  116. },
  117. formatter: function (params) {
  118. // 格式化时间显示
  119. const formatTime = (timeStr) => {
  120. const date = new Date(timeStr)
  121. return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
  122. }
  123. let result = `<div style="font-weight: bold; margin-bottom: 8px; color: #303133;">${formatTime(params[0].axisValue)}</div>`
  124. params.forEach(param => {
  125. if (param.value !== null && param.value !== undefined) {
  126. // 数据格式为 [datetime, value],所以取第二个元素作为温度值
  127. const temperature = Array.isArray(param.value) ? param.value[1] : param.value
  128. result += `<div style="margin: 5px 0; display: flex; align-items: center;">
  129. <span style="display: inline-block; width: 12px; height: 12px; background: ${param.color}; margin-right: 8px; border-radius: 2px;"></span>
  130. <span style="color: #606266;">${param.seriesName}: </span>
  131. <span style="font-weight: bold; color: #303133; margin-left: 4px;">${temperature.toFixed(2)}°C</span>
  132. </div>`
  133. }
  134. })
  135. return result
  136. }
  137. },
  138. legend: {
  139. data: series.map(s => s.name),
  140. top: 30,
  141. type: 'scroll',
  142. textStyle: {
  143. color: '#606266'
  144. }
  145. },
  146. grid: {
  147. left: '3%',
  148. right: '4%',
  149. bottom: '15%',
  150. top: '15%',
  151. containLabel: true
  152. },
  153. xAxis: {
  154. type: 'time',
  155. boundaryGap: false,
  156. axisLine: {
  157. lineStyle: {
  158. color: '#e4e7ed'
  159. }
  160. },
  161. axisLabel: {
  162. color: '#606266',
  163. formatter: function (value) {
  164. // 格式化时间显示
  165. const date = new Date(value)
  166. return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
  167. },
  168. rotate: 45
  169. },
  170. axisTick: {
  171. lineStyle: {
  172. color: '#e4e7ed'
  173. }
  174. }
  175. },
  176. yAxis: {
  177. type: 'value',
  178. name: '温度 (°C)',
  179. nameLocation: 'middle',
  180. nameGap: 40,
  181. nameTextStyle: {
  182. color: '#606266'
  183. },
  184. axisLine: {
  185. lineStyle: {
  186. color: '#e4e7ed'
  187. }
  188. },
  189. axisLabel: {
  190. color: '#606266',
  191. formatter: '{value} °C'
  192. },
  193. axisTick: {
  194. lineStyle: {
  195. color: '#e4e7ed'
  196. }
  197. },
  198. splitLine: {
  199. lineStyle: {
  200. color: '#f0f0f0',
  201. type: 'dashed'
  202. }
  203. },
  204. // 根据实际数据范围设置坐标轴,最小温度刻度比实际最小温度小2度,最大温度刻度比实际最大温度高2度
  205. min: function(value) {
  206. const allTemps = []
  207. series.forEach(s => {
  208. s.data.forEach(point => {
  209. if (Array.isArray(point) && point[1] !== null && point[1] !== undefined) {
  210. allTemps.push(point[1])
  211. }
  212. })
  213. })
  214. if (allTemps.length === 0) return 0
  215. const minTemp = Math.min(...allTemps)
  216. return parseInt(minTemp - 2)
  217. },
  218. max: function(value) {
  219. const allTemps = []
  220. series.forEach(s => {
  221. s.data.forEach(point => {
  222. if (Array.isArray(point) && point[1] !== null && point[1] !== undefined) {
  223. allTemps.push(point[1])
  224. }
  225. })
  226. })
  227. if (allTemps.length === 0) return 100
  228. const maxTemp = Math.max(...allTemps)
  229. return parseInt(maxTemp + 2)
  230. }
  231. },
  232. series: series,
  233. dataZoom: [
  234. {
  235. type: 'inside',
  236. start: 0,
  237. end: 100
  238. },
  239. {
  240. type: 'slider',
  241. start: 0,
  242. end: 100,
  243. bottom: 10,
  244. height: 20,
  245. borderColor: '#e4e7ed',
  246. fillerColor: 'rgba(64, 158, 255, 0.1)',
  247. handleStyle: {
  248. color: '#409eff'
  249. }
  250. }
  251. ]
  252. }
  253. // 设置趋势图点击事件
  254. trendChartInstance.off('click')
  255. trendChartInstance.on('click', (params) => {
  256. // 处理点击事件,获取点击的时间点
  257. let clickedTime = null
  258. if (params.componentType === 'series') {
  259. // 点击在数据系列上
  260. clickedTime = params.value[0]
  261. } else if (params.componentType === 'xAxis') {
  262. // 点击在X轴上
  263. clickedTime = params.value
  264. } else if (params.componentType === 'grid') {
  265. // 点击在图表网格上,需要根据鼠标位置计算时间
  266. const pointInPixel = [params.event.offsetX, params.event.offsetY]
  267. const pointInGrid = trendChartInstance.convertFromPixel({seriesIndex: 0}, pointInPixel)
  268. if (pointInGrid && pointInGrid.length > 0) {
  269. clickedTime = pointInGrid[0]
  270. }
  271. }
  272. if (clickedTime) {
  273. // 找到最接近的时间点
  274. const closestDataPoint = sortedData.reduce((closest, current) => {
  275. const currentDiff = Math.abs(new Date(current.datetime) - new Date(clickedTime))
  276. const closestDiff = Math.abs(new Date(closest.datetime) - new Date(clickedTime))
  277. return currentDiff < closestDiff ? current : closest
  278. })
  279. updateMomentChart(closestDataPoint.datetime)
  280. }
  281. })
  282. trendChartInstance.setOption(trendOption, true)
  283. // 初始化时刻图表,默认显示第一个时刻的数据
  284. if (sortedData.length > 0) {
  285. const firstTime = sortedData[0].datetime
  286. updateMomentChart(firstTime)
  287. }
  288. }
  289. // 更新时刻温度对比图
  290. const updateMomentChart = (selectedTime) => {
  291. if (!momentChartInstance || !props.chartData || props.chartData.length === 0) {
  292. return
  293. }
  294. // 找到对应时刻的数据
  295. const momentData = props.chartData.find(item => item.datetime === selectedTime)
  296. if (!momentData) {
  297. return
  298. }
  299. selectedMoment.value = selectedTime
  300. // 准备时刻数据
  301. const momentSeries = []
  302. const colors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4']
  303. props.sensorColumns.forEach((sensor, index) => {
  304. const value = momentData[sensor.sensorName]
  305. if (value !== null && value !== undefined && !isNaN(value)) {
  306. momentSeries.push({
  307. name: sensor.sensorName,
  308. value: parseFloat(value),
  309. itemStyle: {
  310. color: colors[index % colors.length]
  311. }
  312. })
  313. }
  314. })
  315. // 格式化时间显示
  316. const formatTime = (timeStr) => {
  317. const date = new Date(timeStr)
  318. return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
  319. }
  320. // 如果没有有效数据,显示空状态
  321. if (momentSeries.length === 0) {
  322. const emptyOption = {
  323. title: {
  324. text: `时刻温度对比 - ${formatTime(selectedTime)}`,
  325. left: 'center',
  326. textStyle: {
  327. fontSize: 14,
  328. fontWeight: 'bold',
  329. color: '#303133'
  330. }
  331. },
  332. graphic: {
  333. type: 'text',
  334. left: 'center',
  335. top: 'middle',
  336. style: {
  337. text: '该时刻暂无温度数据',
  338. fontSize: 16,
  339. fill: '#909399'
  340. }
  341. }
  342. }
  343. momentChartInstance.setOption(emptyOption, true)
  344. return
  345. }
  346. // 时刻图表配置 - 折线图,y轴为legend,x轴为温度
  347. const momentOption = {
  348. title: {
  349. text: `时刻温度对比 - ${formatTime(selectedTime)}`,
  350. left: 'center',
  351. textStyle: {
  352. fontSize: 14,
  353. fontWeight: 'bold',
  354. color: '#303133'
  355. }
  356. },
  357. tooltip: {
  358. trigger: 'axis',
  359. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  360. borderColor: '#e4e7ed',
  361. borderWidth: 1,
  362. textStyle: {
  363. color: '#303133'
  364. },
  365. formatter: function (params) {
  366. return `<div style="font-weight: bold; margin-bottom: 8px; color: #303133;">${params[0].name}</div>
  367. <div style="margin: 5px 0; display: flex; align-items: center;">
  368. <span style="display: inline-block; width: 12px; height: 12px; background: ${params[0].color}; margin-right: 8px; border-radius: 2px;"></span>
  369. <span style="color: #606266;">温度: </span>
  370. <span style="font-weight: bold; color: #303133; margin-left: 4px;">${params[0].value.toFixed(2)}°C</span>
  371. </div>`
  372. }
  373. },
  374. grid: {
  375. left: '15%',
  376. right: '15%',
  377. bottom: '5%',
  378. top: '8%',
  379. containLabel: true
  380. },
  381. xAxis: {
  382. type: 'value',
  383. name: '温度 (°C)',
  384. nameLocation: 'middle',
  385. nameGap: 30,
  386. nameTextStyle: {
  387. color: '#606266'
  388. },
  389. axisLine: {
  390. lineStyle: {
  391. color: '#e4e7ed'
  392. }
  393. },
  394. axisLabel: {
  395. color: '#606266',
  396. formatter: '{value} °C'
  397. },
  398. axisTick: {
  399. lineStyle: {
  400. color: '#e4e7ed'
  401. }
  402. },
  403. splitLine: {
  404. lineStyle: {
  405. color: '#f0f0f0',
  406. type: 'dashed'
  407. }
  408. },
  409. // 根据实际数据范围设置坐标轴,最小温度刻度比实际最小温度小2度,最大温度刻度比实际最大温度高2度
  410. min: function(value) {
  411. const minTemp = Math.min(...momentSeries.map(s => s.value))
  412. return parseInt(minTemp - 2)
  413. },
  414. max: function(value) {
  415. const maxTemp = Math.max(...momentSeries.map(s => s.value))
  416. return parseInt(maxTemp + 2)
  417. }
  418. },
  419. yAxis: {
  420. type: 'category',
  421. data: momentSeries.map(s => s.name),
  422. axisLine: {
  423. lineStyle: {
  424. color: '#e4e7ed'
  425. }
  426. },
  427. axisLabel: {
  428. color: '#606266',
  429. show: true
  430. },
  431. axisTick: {
  432. lineStyle: {
  433. color: '#e4e7ed'
  434. }
  435. }
  436. },
  437. series: [
  438. {
  439. name: '温度',
  440. type: 'line',
  441. data: momentSeries.map(s => s.value),
  442. symbol: 'circle',
  443. symbolSize: 8,
  444. lineStyle: {
  445. width: 3
  446. },
  447. itemStyle: {
  448. color: '#409eff'
  449. },
  450. label: {
  451. show: true,
  452. position: 'right',
  453. formatter: '{c}°C',
  454. color: '#606266'
  455. }
  456. }
  457. ]
  458. }
  459. momentChartInstance.setOption(momentOption, true)
  460. }
  461. // 监听数据变化
  462. watch(
  463. () => [props.chartData, props.sensorColumns],
  464. () => {
  465. nextTick(() => {
  466. updateCharts()
  467. // 延迟重新调整大小,确保DOM已更新
  468. setTimeout(() => {
  469. if (trendChartInstance) {
  470. trendChartInstance.resize()
  471. }
  472. if (momentChartInstance) {
  473. momentChartInstance.resize()
  474. }
  475. }, 100)
  476. })
  477. },
  478. { deep: true }
  479. )
  480. // 监听窗口大小变化
  481. const handleResize = () => {
  482. if (trendChartInstance) {
  483. trendChartInstance.resize()
  484. }
  485. if (momentChartInstance) {
  486. momentChartInstance.resize()
  487. }
  488. }
  489. // 生命周期
  490. onMounted(() => {
  491. initChart()
  492. window.addEventListener('resize', handleResize)
  493. })
  494. onUnmounted(() => {
  495. if (trendChartInstance) {
  496. trendChartInstance.dispose()
  497. trendChartInstance = null
  498. }
  499. if (momentChartInstance) {
  500. momentChartInstance.dispose()
  501. momentChartInstance = null
  502. }
  503. window.removeEventListener('resize', handleResize)
  504. })
  505. </script>
  506. <style lang="scss" scoped>
  507. .temperature-chart {
  508. width: 100%;
  509. height: 100%;
  510. display: flex;
  511. flex-direction: column;
  512. .chart-container {
  513. width: 100%;
  514. height: 100%;
  515. min-height: 600px;
  516. flex: 1;
  517. display: flex;
  518. flex-direction: row;
  519. gap: 20px;
  520. .trend-chart {
  521. width: 60% !important;
  522. height: 600px !important;
  523. border: 1px solid #e4e7ed;
  524. border-radius: 8px;
  525. background: #ffffff;
  526. }
  527. .moment-chart {
  528. width: 40% !important;
  529. height: 600px !important;
  530. border: 1px solid #e4e7ed;
  531. border-radius: 8px;
  532. background: #ffffff;
  533. }
  534. }
  535. }
  536. </style>