IndicatorChart.vue 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. <script setup lang="ts">
  2. import * as echarts from 'echarts'
  3. import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
  4. const props = withDefaults(
  5. defineProps<{
  6. pressure: number[]
  7. volume: number[]
  8. angle: number[]
  9. height?: number
  10. }>(),
  11. { height: 170 },
  12. )
  13. // 曲柄角度 4 阶段(配色为根目录 frontend/src/components/WaveChart.vue 的加亮版)。
  14. const PHASES = [
  15. { start: 0, end: 120, color: '#ab47bc', name: '进气' },
  16. { start: 120, end: 195, color: '#ef5350', name: '膨胀' },
  17. { start: 195, end: 270, color: '#42a5f5', name: '压缩' },
  18. { start: 270, end: 360, color: '#66bb6a', name: '排气' },
  19. ]
  20. const el = ref<HTMLElement | null>(null)
  21. let chart: echarts.ECharts | null = null
  22. let observer: ResizeObserver | null = null
  23. function buildOption(): Record<string, unknown> {
  24. // 按曲柄角顺时针排序,保证同一阶段为连续折线(避免跨圈连成一条直线)。
  25. const order = props.volume
  26. .map((_, index) => index)
  27. .sort((a, b) => (props.angle[a] ?? 0) - (props.angle[b] ?? 0))
  28. const series = PHASES.map((phase) => {
  29. const data: [number, number][] = []
  30. for (const index of order) {
  31. const angle = props.angle[index]
  32. if (angle === undefined || angle < phase.start || angle >= phase.end) continue
  33. data.push([props.volume[index], props.pressure[index]])
  34. }
  35. return {
  36. type: 'line',
  37. name: phase.name,
  38. data,
  39. showSymbol: false,
  40. symbol: 'none',
  41. animation: false,
  42. sampling: 'none',
  43. lineStyle: { width: 1.6, color: phase.color },
  44. itemStyle: { color: phase.color },
  45. }
  46. }).filter((item) => item.data.length >= 2)
  47. return {
  48. animation: false,
  49. backgroundColor: 'transparent',
  50. textStyle: {
  51. fontFamily: '-apple-system, "PingFang SC", "Microsoft YaHei", sans-serif',
  52. fontWeight: 'normal',
  53. },
  54. legend: {
  55. top: 0,
  56. left: 'center',
  57. itemWidth: 12,
  58. itemHeight: 8,
  59. itemGap: 10,
  60. textStyle: { fontSize: 9, color: '#aaaaaa' },
  61. data: series.map((item) => item.name),
  62. },
  63. grid: { left: 46, right: 12, top: 24, bottom: 26, containLabel: false },
  64. xAxis: {
  65. type: 'value',
  66. name: 'V/L',
  67. nameTextStyle: { color: '#aaaaaa', fontSize: 9 },
  68. axisLine: { lineStyle: { color: '#555555' } },
  69. axisTick: { show: false },
  70. axisLabel: { color: '#aaaaaa', fontSize: 9 },
  71. splitLine: { show: false },
  72. },
  73. yAxis: {
  74. type: 'value',
  75. scale: true,
  76. axisLine: { show: false },
  77. axisTick: { show: false },
  78. axisLabel: {
  79. color: '#aaaaaa',
  80. fontSize: 9,
  81. formatter: (value: number) => Number(value).toFixed(2),
  82. },
  83. splitLine: { show: false },
  84. },
  85. series,
  86. tooltip: {
  87. trigger: 'item',
  88. backgroundColor: 'rgba(0,0,0,0.8)',
  89. textStyle: { fontSize: 11, color: '#ffffff' },
  90. formatter: (params: unknown) => {
  91. const item = params as { value?: [number, number]; seriesName?: string } | undefined
  92. const value = item?.value
  93. if (!value) return ''
  94. return `${item?.seriesName ?? ''}<br/>V ${Number(value[0]).toFixed(3)} L<br/>P ${Number(value[1]).toFixed(3)} MPa`
  95. },
  96. },
  97. }
  98. }
  99. function render(): void {
  100. if (!chart) return
  101. chart.setOption(buildOption() as unknown as echarts.EChartsOption, { notMerge: true })
  102. }
  103. onMounted(() => {
  104. if (!el.value) return
  105. chart = echarts.init(el.value)
  106. render()
  107. observer = new ResizeObserver(() => {
  108. chart?.resize()
  109. render()
  110. })
  111. observer.observe(el.value)
  112. })
  113. onBeforeUnmount(() => {
  114. observer?.disconnect()
  115. observer = null
  116. chart?.dispose()
  117. chart = null
  118. })
  119. watch(() => [props.pressure, props.volume, props.angle], render, { deep: true })
  120. </script>
  121. <template>
  122. <div ref="el" class="indicator-chart" :style="{ height: `${height}px` }" />
  123. </template>
  124. <style scoped>
  125. .indicator-chart {
  126. width: 100%;
  127. }
  128. </style>