Files
waveform-analysis/src/components/WaveformChartView.vue

360 lines
11 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { Pagination } from 'ant-design-vue'
import { toRefs } from 'vue'
import { WaveformAnnotationContextMenu, WaveformAnnotationLayer } from './annotation'
import WaveformAnnotationEditor from './annotation/WaveformAnnotationEditor.vue'
import type { WaveformChartController } from './core/useWaveformChartController'
import { WaveformHoverHost } from './interaction'
import { WaveformHoverLayer, WaveformLegend, WaveformTrack } from './rendering'
const props = defineProps<{ controller: WaveformChartController }>()
const {
displayMode,
activeInteractionMode,
isCleanView,
2026-07-29 12:12:03 +08:00
isPresentationMode,
selection,
containerStyle,
overlayMode,
resolvedChartLeftMargin,
titleAreaHeight,
resolvedPlotMargin,
pointerInsideChart,
handleNativeContextMenu,
titleAreaReserved,
titleAreaStyle,
titleVisible,
titleMeasureStyle,
resolvedTitleText,
titleVisualStyle,
titleTextStyle,
titleLayout,
chartWidth,
drawingHeight,
hasWaveformData,
requestViewportReset,
trackLayouts,
clipPathId,
chartTopMargin,
gridCells,
hasVisibleWaveformData,
zoomable,
isZoomMode,
innerWidth,
innerHeight,
handleSharedPointerMove,
beginSharedViewportDrag,
finishViewportDrag,
cancelViewportDrag,
2026-07-29 12:12:03 +08:00
handlePointerLeave,
handleAnnotationClick,
handleAnnotationContextMenu,
resolveFrameNumber,
frameStyle,
axes,
resolvedZeroLine,
timeUnit,
yLabel,
handleIndependentPointerMove,
beginViewportDrag,
hoverState,
showTooltip,
selectionBox,
renderedAnnotations,
annotationsVisible,
handleExistingAnnotationContextMenu,
beginAnnotationDrag,
handleAnnotationMove,
endAnnotationDrag,
resolveLegendPosition,
resolveLegendOrientation,
legendBackgroundColor,
legendInteractive,
resolvedHiddenSeriesIds,
toggleSeriesVisibility,
resolvedXLabel,
xAxisTitleY,
hasChartArea,
gridOptions,
pageCount,
currentPage,
getPageSize,
chartTracks,
goToPage,
annotationInteraction,
editorDraft,
contextMenu,
editorSeries,
editorSeriesOptions,
confirmAnnotation,
cancelAnnotation,
changeDraftSeries,
editContextAnnotation,
deleteContextAnnotation,
chartHeight,
setContainer,
setSvgElement,
setTitleMeasureElement,
setSharedOverlayElement,
} = toRefs(props.controller)
</script>
<template>
<div
:ref="setContainer"
class="waveform-chart"
:class="[
`waveform-chart--${displayMode}`,
`waveform-chart--interaction-${activeInteractionMode}`,
{
'waveform-chart--clean': isCleanView,
2026-07-29 12:12:03 +08:00
'waveform-chart--presentation': isPresentationMode,
'waveform-chart--panning': selection?.kind === 'pan',
},
]"
:style="containerStyle"
:data-display-mode="displayMode"
:data-interaction-mode="activeInteractionMode"
2026-07-29 12:12:03 +08:00
:data-presentation-mode="isPresentationMode"
:data-overlay-mode="overlayMode"
:data-chart-left-margin="resolvedChartLeftMargin"
:data-plot-margin-top="resolvedPlotMargin.top"
:data-plot-margin-bottom="resolvedPlotMargin.bottom"
:data-title-area-height="titleAreaHeight"
@pointerenter="pointerInsideChart = true"
@pointerleave="pointerInsideChart = false"
@contextmenu.capture="handleNativeContextMenu"
>
<div
v-if="titleAreaReserved"
class="waveform-chart__title-area"
:style="titleAreaStyle"
:role="titleVisible ? 'heading' : undefined"
:aria-level="titleVisible ? 2 : undefined"
:aria-hidden="isCleanView || undefined"
>
<span
:ref="setTitleMeasureElement"
class="waveform-chart__title-measure"
:style="titleMeasureStyle"
aria-hidden="true"
>
{{ resolvedTitleText }}
</span>
<span v-if="titleVisible" class="waveform-chart__title-visual" :style="titleVisualStyle">
<span
class="waveform-chart__title-text"
:style="titleTextStyle"
:data-title-scale="titleLayout.scale"
:data-title-wrapped="titleLayout.wrapped || undefined"
>
{{ resolvedTitleText }}
</span>
</span>
</div>
<svg
:ref="setSvgElement"
class="waveform-chart__svg"
:width="chartWidth"
:height="drawingHeight"
role="img"
:aria-label="hasWaveformData ? '波形折线图' : '暂无波形数据'"
@dblclick="requestViewportReset"
>
<defs>
<clipPath
v-for="track in trackLayouts"
:id="`${clipPathId}-${track.index}`"
:key="`${clipPathId}-${track.index}`"
clipPathUnits="userSpaceOnUse"
>
<rect :width="track.width" :height="track.height" />
</clipPath>
</defs>
<g :transform="`translate(${resolvedChartLeftMargin}, ${chartTopMargin})`">
<g
v-if="displayMode !== 'compact' && !isCleanView"
class="waveform-chart__grid-slots"
aria-hidden="true"
>
<g
v-for="cell in gridCells"
:key="`grid-slot-${cell.slotIndex}`"
:transform="`translate(${cell.left}, ${cell.top})`"
>
<rect
v-if="!cell.series"
class="waveform-chart__grid-slot-placeholder"
:width="cell.width"
:height="cell.cellHeight"
/>
</g>
</g>
<rect
v-if="displayMode !== 'independent' && trackLayouts.length && hasVisibleWaveformData"
:ref="setSharedOverlayElement"
class="waveform-chart__overlay waveform-chart__overlay--shared"
:class="{
2026-07-29 12:12:03 +08:00
'is-zoomable': !isPresentationMode && zoomable && isZoomMode,
'is-annotating': !isPresentationMode && activeInteractionMode === 'annotation',
}"
:width="innerWidth"
:height="innerHeight"
@pointermove="handleSharedPointerMove"
@pointerdown="beginSharedViewportDrag"
@pointerup="finishViewportDrag"
@pointercancel="cancelViewportDrag"
2026-07-29 12:12:03 +08:00
@pointerleave="handlePointerLeave"
@click="handleAnnotationClick"
@contextmenu="handleAnnotationContextMenu"
/>
<!-- 轨道渲染 -->
<WaveformTrack
v-for="track in trackLayouts"
:key="`${track.index}-${track.series.name}`"
:track="track"
:clip-path-id="clipPathId"
:inner-width="innerWidth"
:zoomable="zoomable"
2026-07-29 12:12:03 +08:00
:interactive="!isPresentationMode"
:display-mode="displayMode"
:interaction-mode="activeInteractionMode"
:frame-number="resolveFrameNumber(track.index)"
:frame-style="frameStyle"
:axes="axes"
:clean-view="isCleanView"
:zero-line="resolvedZeroLine"
:time-unit="timeUnit"
:y-label="yLabel"
@pointer-move="handleIndependentPointerMove($event, track.index)"
@pointer-down="beginViewportDrag($event, track.index, true)"
@pointer-up="finishViewportDrag"
@pointer-cancel="cancelViewportDrag"
2026-07-29 12:12:03 +08:00
@pointer-leave="handlePointerLeave"
@click="handleAnnotationClick($event, track.index)"
@contextmenu="handleAnnotationContextMenu($event, track.index)"
/>
<WaveformHoverLayer
:state="hoverState"
:tracks="trackLayouts"
:clip-path-id="clipPathId"
2026-07-29 12:12:03 +08:00
:visible="showTooltip && !isPresentationMode"
/>
<rect
v-if="selectionBox && selection?.kind === 'box'"
class="waveform-chart__zoom-selection"
:x="selectionBox.x"
:y="selectionBox.y"
:width="selectionBox.width"
:height="selectionBox.height"
aria-hidden="true"
/>
<WaveformAnnotationLayer
v-if="!isCleanView"
:annotations="renderedAnnotations"
:visible="annotationsVisible"
2026-07-29 12:12:03 +08:00
:interactive="!isPresentationMode"
@contextmenu="handleExistingAnnotationContextMenu"
@drag-start="beginAnnotationDrag"
@move="handleAnnotationMove"
@drag-end="endAnnotationDrag"
/>
<g v-if="!isCleanView" class="waveform-chart__legend-layer">
<g
v-for="track in trackLayouts"
:key="`legend-${track.index}-${track.series.name}`"
class="waveform-chart__legend-track"
:data-legend-track-index="track.index"
:data-legend-track-id="track.id"
:transform="`translate(${track.left}, ${track.top})`"
>
<WaveformLegend
v-if="!track.isEmpty && track.legendSeries.length > 1"
:series="track.legendSeries"
:position="resolveLegendPosition(track.id)"
:orientation="resolveLegendOrientation(resolveLegendPosition(track.id))"
:background-color="legendBackgroundColor"
:interactive="legendInteractive"
:hidden-series-ids="resolvedHiddenSeriesIds"
:width="track.width ?? innerWidth"
:height="track.height"
@toggle="toggleSeriesVisibility"
/>
</g>
</g>
</g>
<text
v-if="resolvedXLabel && !isCleanView"
class="waveform-chart__label waveform-chart__x-label"
:x="resolvedChartLeftMargin + innerWidth / 2"
:y="xAxisTitleY"
text-anchor="middle"
>
{{ resolvedXLabel }}
</text>
<text
v-if="hasChartArea && !hasWaveformData"
class="waveform-chart__empty"
:x="chartWidth / 2"
:y="drawingHeight / 2"
text-anchor="middle"
>
暂无有效波形数据
</text>
</svg>
<Pagination
v-if="gridOptions.showPagination && pageCount > 1 && !isCleanView"
class="waveform-chart__pagination"
aria-label="波形分页"
:current="currentPage"
:page-size="getPageSize(gridOptions)"
:total="chartTracks.length"
:show-size-changer="false"
:show-quick-jumper="false"
@change="goToPage"
/>
<WaveformAnnotationEditor
2026-07-29 12:12:03 +08:00
v-if="editorDraft && !isCleanView && !isPresentationMode"
:annotation="editorDraft.annotation"
:mode="editorDraft.mode"
:series="editorSeries"
:series-options="editorSeriesOptions"
:time-unit="timeUnit"
@confirm="confirmAnnotation"
@cancel="cancelAnnotation"
@series-change="changeDraftSeries"
/>
<WaveformAnnotationContextMenu
2026-07-29 12:12:03 +08:00
v-if="!isCleanView && !isPresentationMode"
:visible="contextMenu !== null"
:x="contextMenu?.x || 0"
:y="contextMenu?.y || 0"
:can-edit="Boolean(contextMenu?.annotationId)"
@edit="editContextAnnotation"
@delete="deleteContextAnnotation"
@close="annotationInteraction.closeContextMenu"
/>
<WaveformHoverHost
:state="hoverState"
2026-07-29 12:12:03 +08:00
:visible="showTooltip && !isPresentationMode"
:time-unit="timeUnit"
:container-width="chartWidth"
:container-height="chartHeight"
/>
</div>
</template>