2026-07-27 15:05:13 +08:00
|
|
|
<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,
|
2026-07-27 15:05:13 +08:00
|
|
|
selection,
|
|
|
|
|
containerStyle,
|
|
|
|
|
overlayMode,
|
|
|
|
|
resolvedChartLeftMargin,
|
|
|
|
|
titleAreaHeight,
|
2026-08-05 15:16:52 +08:00
|
|
|
resolvedPlotMargin,
|
2026-07-27 15:05:13 +08:00
|
|
|
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,
|
2026-07-27 15:05:13 +08:00
|
|
|
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,
|
2026-08-07 15:59:26 +08:00
|
|
|
'waveform-chart--panning': selection?.kind === 'pan',
|
2026-07-27 15:05:13 +08:00
|
|
|
},
|
|
|
|
|
]"
|
|
|
|
|
:style="containerStyle"
|
|
|
|
|
:data-display-mode="displayMode"
|
|
|
|
|
:data-interaction-mode="activeInteractionMode"
|
2026-07-29 12:12:03 +08:00
|
|
|
:data-presentation-mode="isPresentationMode"
|
2026-07-27 15:05:13 +08:00
|
|
|
:data-overlay-mode="overlayMode"
|
|
|
|
|
:data-chart-left-margin="resolvedChartLeftMargin"
|
2026-08-05 15:16:52 +08:00
|
|
|
:data-plot-margin-top="resolvedPlotMargin.top"
|
|
|
|
|
:data-plot-margin-bottom="resolvedPlotMargin.bottom"
|
2026-07-27 15:05:13 +08:00
|
|
|
: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',
|
2026-07-27 15:05:13 +08:00
|
|
|
}"
|
|
|
|
|
:width="innerWidth"
|
|
|
|
|
:height="innerHeight"
|
|
|
|
|
@pointermove="handleSharedPointerMove"
|
|
|
|
|
@pointerdown="beginSharedViewportDrag"
|
|
|
|
|
@pointerup="finishViewportDrag"
|
|
|
|
|
@pointercancel="cancelViewportDrag"
|
2026-07-29 12:12:03 +08:00
|
|
|
@pointerleave="handlePointerLeave"
|
2026-07-27 15:05:13 +08:00
|
|
|
@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"
|
2026-07-27 15:05:13 +08:00
|
|
|
: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"
|
2026-07-27 15:05:13 +08:00
|
|
|
@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"
|
2026-07-27 15:05:13 +08:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<rect
|
2026-08-07 15:59:26 +08:00
|
|
|
v-if="selectionBox && selection?.kind === 'box'"
|
2026-07-27 15:05:13 +08:00
|
|
|
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"
|
2026-07-27 15:05:13 +08:00
|
|
|
@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>
|
|
|
|
|
|
2026-08-05 15:16:52 +08:00
|
|
|
<text
|
|
|
|
|
v-if="resolvedXLabel && !isCleanView"
|
|
|
|
|
class="waveform-chart__label waveform-chart__x-label"
|
|
|
|
|
:x="resolvedChartLeftMargin + innerWidth / 2"
|
|
|
|
|
:y="xAxisTitleY"
|
|
|
|
|
text-anchor="middle"
|
|
|
|
|
>
|
|
|
|
|
{{ resolvedXLabel }}
|
|
|
|
|
</text>
|
|
|
|
|
|
2026-07-27 15:05:13 +08:00
|
|
|
<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"
|
2026-07-27 15:05:13 +08:00
|
|
|
: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"
|
2026-07-27 15:05:13 +08:00
|
|
|
: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"
|
2026-07-27 15:05:13 +08:00
|
|
|
:time-unit="timeUnit"
|
|
|
|
|
:container-width="chartWidth"
|
|
|
|
|
:container-height="chartHeight"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|