PeriodModal.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. <script setup lang="ts">
  2. import * as echarts from 'echarts'
  3. import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
  4. import type { PeriodDetail } from '../types'
  5. const props = defineProps<{
  6. detail: PeriodDetail | null
  7. visible: boolean
  8. loading?: boolean
  9. }>()
  10. const emit = defineEmits<{
  11. close: []
  12. }>()
  13. const chartElement = ref<HTMLDivElement | null>(null)
  14. const chartMode = ref<'angle' | 'volume'>('volume')
  15. let chart: echarts.ECharts | undefined
  16. function render() {
  17. if (!chart || !props.detail) return
  18. const detail = props.detail
  19. const volumeMode = chartMode.value === 'volume' && detail.volume
  20. const series: echarts.SeriesOption[] = detail.phases.map((phase, phaseIndex) => {
  21. const isLast = phaseIndex === detail.phases.length - 1
  22. const data: Array<{ value: [number, number]; idx: number }> = []
  23. detail.angles360.forEach((angle360, index) => {
  24. const x = volumeMode ? detail.volume?.[index] : detail.angles[index]
  25. if (x == null || !Number.isFinite(x)) return
  26. const inPhase = isLast ? angle360 >= phase.start : angle360 >= phase.start && angle360 < phase.end
  27. if (inPhase) data.push({ value: [x, detail.pressure[index]], idx: index })
  28. })
  29. return {
  30. name: phase.name,
  31. type: 'line' as const,
  32. showSymbol: false,
  33. lineStyle: { color: phase.color, width: 1.6 },
  34. itemStyle: { color: phase.color },
  35. data,
  36. }
  37. })
  38. chart.setOption({
  39. animation: false,
  40. grid: { left: 64, right: 28, top: 38, bottom: 52 },
  41. tooltip: {
  42. trigger: 'axis',
  43. backgroundColor: '#162b3c',
  44. borderWidth: 0,
  45. textStyle: { color: '#fff', fontSize: 11 },
  46. formatter: (params: any[]) => {
  47. const item = params?.[0]
  48. if (!item) return ''
  49. const index = item.data?.idx ?? 0
  50. const angle = detail.angles[index]
  51. const pressure = detail.pressure[index]
  52. const volume = detail.volume?.[index]
  53. const phase = detail.phases.find((p) => detail.angles360[index] >= p.start && detail.angles360[index] < p.end)
  54. return [
  55. `曲轴角度:${angle?.toFixed(1)}°${volumeMode ? `(0-360:${detail.angles360[index]?.toFixed(1)}°)` : ''}`,
  56. `压力原始值:${pressure?.toPrecision(8)}`,
  57. volumeMode ? `体积:${volume?.toFixed(4)} L` : '',
  58. phase ? `<span style="color:${phase.color}">●</span> ${phase.name}` : '',
  59. ].filter(Boolean).join('<br/>')
  60. },
  61. },
  62. legend: { top: 0, left: 70, itemWidth: 16, itemHeight: 7, textStyle: { color: '#60717b', fontSize: 11 } },
  63. xAxis: {
  64. type: 'value',
  65. name: volumeMode ? '气缸体积 (L)' : '曲轴角度 (°)',
  66. nameLocation: 'middle',
  67. nameGap: 30,
  68. min: volumeMode ? undefined : 0,
  69. max: volumeMode ? undefined : 180,
  70. axisLine: { lineStyle: { color: '#9eabb1' } },
  71. axisLabel: { color: '#71808a' },
  72. },
  73. yAxis: {
  74. type: 'value',
  75. name: '压力原始值',
  76. nameLocation: 'middle',
  77. nameGap: 45,
  78. nameTextStyle: { color: '#e4572e' },
  79. axisLine: { show: true, lineStyle: { color: '#e4572e' } },
  80. axisLabel: { color: '#71808a' },
  81. splitLine: { lineStyle: { color: '#edf1f2' } },
  82. },
  83. series,
  84. }, true)
  85. chart.resize()
  86. }
  87. async function ensureChart() {
  88. if (!props.visible) {
  89. chart?.dispose()
  90. chart = undefined
  91. return
  92. }
  93. await nextTick()
  94. if (!chartElement.value) return
  95. if (!chart) chart = echarts.init(chartElement.value, undefined, { renderer: 'canvas' })
  96. render()
  97. }
  98. watch(() => [props.detail, props.visible, chartMode.value], () => void ensureChart(), { deep: true })
  99. onMounted(() => {
  100. void ensureChart()
  101. })
  102. onBeforeUnmount(() => chart?.dispose())
  103. </script>
  104. <template>
  105. <Teleport to="body">
  106. <div v-if="visible" class="modal-backdrop" @click.self="emit('close')">
  107. <section class="period-modal" role="dialog" aria-modal="true">
  108. <header class="period-modal-head">
  109. <div>
  110. <h2>周期功图</h2>
  111. <p>{{ detail?.waveFile.pointName }} · wave_file #{{ detail?.waveFile.id }} · {{ detail?.waveFile.sampleTime }}</p>
  112. </div>
  113. <button class="icon-button" type="button" aria-label="关闭" @click="emit('close')">×</button>
  114. </header>
  115. <div v-if="loading" class="modal-loading">正在生成角度与体积数据…</div>
  116. <template v-else-if="detail">
  117. <div class="period-stat-row">
  118. <div><span>采样范围</span><strong>{{ detail.period.startSampleIndex }} — {{ detail.period.endSampleIndex }}</strong></div>
  119. <div><span>周期采样数</span><strong>{{ detail.period.sampleCount.toLocaleString() }}</strong></div>
  120. <div><span>键相点</span><strong>{{ detail.period.triggerSampleIndices.length }} 个</strong></div>
  121. <div v-if="detail.volumeInfo"><span>缸径 / 余隙</span><strong>{{ detail.volumeInfo.boreMm }} mm / {{ detail.volumeInfo.clearanceVolumeL }} L</strong></div>
  122. </div>
  123. <div class="modal-tabs">
  124. <button :class="{ active: chartMode === 'volume' }" type="button" :disabled="!detail.volume" @click="chartMode = 'volume'">压力-体积功图</button>
  125. <button :class="{ active: chartMode === 'angle' }" type="button" @click="chartMode = 'angle'">压力-角度图</button>
  126. </div>
  127. <div ref="chartElement" class="period-chart"></div>
  128. </template>
  129. </section>
  130. </div>
  131. </Teleport>
  132. </template>