diff --git a/README.md b/README.md index 99fa100..aa52768 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ const data = ref({ | `frameNumber` | `string \| number` | 未设置 | 图框水印内容 | | `zeroLine` | `WaveformZeroLineOptions` | `{ visible: false }` | 零值参考线显隐与样式 | | `cleanView` | `boolean` | `false` | 仅保留波形的净图模式 | +| `presentationMode` | `boolean` | `false` | 禁用绘图区交互的展示模式 | | `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 | | `annotationsVisible` | `boolean` | `true` | 标注图层显隐 | | `interactionMode` | `'zoom' \| 'annotation'` | `'zoom'` | 左键交互模式 | @@ -471,6 +472,14 @@ tooltip 仍然可用,切换回普通模式后原有配置和标注不会丢失 `interactionMode` 可选 `zoom` 或 `annotation`,默认使用缩放模式。右键绘图区可直接打开 标注编辑器,无需切换交互模式。`zoomable`、`pannable` 和 `showTooltip` 可分别控制缩放、 空格拖拽平移和 tooltip;平移默认关闭。 + +展示场景可启用 `presentationMode`,统一禁用绘图区的 tooltip、缩放、平移、双击复位和 +标注交互。该模式不会隐藏任何图形内容,也不会禁用图例切换或分页;关闭后恢复原交互配置。 + +```vue + +``` + 空数据或过滤后没有有效点时,组件会保留图框布局并显示“暂无有效波形数据”。 ## 大数据渲染 diff --git a/package.json b/package.json index c4c497b..a1a371c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "waveform-analysis", - "version": "0.1.22", + "version": "0.1.23", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/types/index.d.ts", @@ -14,6 +14,7 @@ }, "files": [ "dist", + "src", "README.md" ], "sideEffects": [ diff --git a/src/App.test.ts b/src/App.test.ts index d06b867..0b45151 100644 --- a/src/App.test.ts +++ b/src/App.test.ts @@ -92,19 +92,22 @@ describe('App workspace layout', { timeout: 20_000 }, () => { wrapper.unmount() }) - it('passes clean view and zero-line controls to the chart', async () => { + it('passes clean view, presentation mode, and zero-line controls to the chart', async () => { const wrapper = mount(App) await flushPromises() const chart = wrapper.getComponent(WaveformChart) expect(chart.props('cleanView')).toBe(false) + expect(chart.props('presentationMode')).toBe(false) expect(chart.props('zeroLine')).toMatchObject({ visible: false, color: '#98a2b3', width: 1 }) await wrapper.get('[aria-label="净图模式"]').trigger('click') + await wrapper.get('[aria-label="展示模式"]').trigger('click') await wrapper.get('[aria-label="显示零值参考线"]').trigger('click') await flushPromises() expect(chart.props('cleanView')).toBe(true) + expect(chart.props('presentationMode')).toBe(true) expect(chart.props('zeroLine')).toMatchObject({ visible: true, color: '#98a2b3', width: 1 }) wrapper.unmount() }) diff --git a/src/App.vue b/src/App.vue index f408e3d..735f938 100644 --- a/src/App.vue +++ b/src/App.vue @@ -43,6 +43,7 @@ const yAxisLineVisible = ref(false) const annotations = ref([]) const annotationsVisible = ref(true) const cleanView = ref(false) +const presentationMode = ref(false) const showTooltip = ref(true) const zeroLineVisible = ref(false) const zeroLineColor = ref('#98a2b3') @@ -289,6 +290,7 @@ const controlPanelModel = reactive({ overlayMode, showTooltip, cleanView, + presentationMode, selectedSeriesId, selectedLineStyle, lineStyleOptions, @@ -350,6 +352,7 @@ const chartModel = reactive({ frameStyle, axes, cleanView, + presentationMode, showTooltip, zeroLine, frameWatermarkVisible, diff --git a/src/components/WaveformChart.css b/src/components/WaveformChart.css index 48eaaf2..9ea5596 100644 --- a/src/components/WaveformChart.css +++ b/src/components/WaveformChart.css @@ -104,6 +104,11 @@ cursor: crosshair; } +.waveform-chart--presentation .waveform-chart__overlay { + cursor: default; + pointer-events: none; +} + .waveform-chart__label, .waveform-chart__empty { fill: #595959; diff --git a/src/components/WaveformChart.vue b/src/components/WaveformChart.vue index 4d99bdc..3ab8475 100644 --- a/src/components/WaveformChart.vue +++ b/src/components/WaveformChart.vue @@ -26,6 +26,7 @@ const props = withDefaults(defineProps(), { legend: () => ({ position: 'top-right', orientation: 'auto' }), defaultHiddenSeriesIds: () => [], cleanView: false, + presentationMode: false, zeroLine: () => ({ visible: false }), }) const emit = defineEmits() diff --git a/src/components/WaveformChartView.vue b/src/components/WaveformChartView.vue index cfbcaf6..6423482 100644 --- a/src/components/WaveformChartView.vue +++ b/src/components/WaveformChartView.vue @@ -13,6 +13,7 @@ const { displayMode, activeInteractionMode, isCleanView, + isPresentationMode, selection, containerStyle, overlayMode, @@ -45,7 +46,7 @@ const { beginSharedViewportDrag, finishViewportDrag, cancelViewportDrag, - clearHover, + handlePointerLeave, handleAnnotationClick, handleAnnotationContextMenu, resolveFrameNumber, @@ -107,12 +108,14 @@ const { `waveform-chart--interaction-${activeInteractionMode}`, { 'waveform-chart--clean': isCleanView, + 'waveform-chart--presentation': isPresentationMode, 'waveform-chart--panning': selection?.mode === 'pan', }, ]" :style="containerStyle" :data-display-mode="displayMode" :data-interaction-mode="activeInteractionMode" + :data-presentation-mode="isPresentationMode" :data-overlay-mode="overlayMode" :data-chart-left-margin="resolvedChartLeftMargin" :data-title-area-height="titleAreaHeight" @@ -192,8 +195,8 @@ const { :ref="setSharedOverlayElement" class="waveform-chart__overlay waveform-chart__overlay--shared" :class="{ - 'is-zoomable': zoomable && isZoomMode, - 'is-annotating': activeInteractionMode === 'annotation', + 'is-zoomable': !isPresentationMode && zoomable && isZoomMode, + 'is-annotating': !isPresentationMode && activeInteractionMode === 'annotation', }" :width="innerWidth" :height="innerHeight" @@ -201,7 +204,7 @@ const { @pointerdown="beginSharedViewportDrag" @pointerup="finishViewportDrag" @pointercancel="cancelViewportDrag" - @pointerleave="clearHover" + @pointerleave="handlePointerLeave" @click="handleAnnotationClick" @contextmenu="handleAnnotationContextMenu" /> @@ -214,6 +217,7 @@ const { :clip-path-id="clipPathId" :inner-width="innerWidth" :zoomable="zoomable" + :interactive="!isPresentationMode" :display-mode="displayMode" :interaction-mode="activeInteractionMode" :frame-number="resolveFrameNumber(track.index)" @@ -227,7 +231,7 @@ const { @pointer-down="beginViewportDrag($event, track.index, true)" @pointer-up="finishViewportDrag" @pointer-cancel="cancelViewportDrag" - @pointer-leave="clearHover" + @pointer-leave="handlePointerLeave" @click="handleAnnotationClick($event, track.index)" @contextmenu="handleAnnotationContextMenu($event, track.index)" /> @@ -236,7 +240,7 @@ const { :state="hoverState" :tracks="trackLayouts" :clip-path-id="clipPathId" - :visible="showTooltip" + :visible="showTooltip && !isPresentationMode" /> () +const props = withDefaults(defineProps(), { + interactive: true, +}) const emit = defineEmits<{ (event: 'contextmenu', annotationId: string, mouseEvent: MouseEvent): void (event: 'drag-start'): void @@ -79,7 +82,7 @@ function scheduleDragFrame() { } function handlePointerDown(rendered: RenderedAnnotation, event: PointerEvent) { - if (event.button !== 0 || dragState) return + if (!props.interactive || event.button !== 0 || dragState) return event.preventDefault() event.stopPropagation() const target = event.currentTarget as SVGGElement | null @@ -108,6 +111,7 @@ function handlePointerDown(rendered: RenderedAnnotation, event: PointerEvent) { } function handlePointerMove(event: PointerEvent) { + if (!props.interactive) return event.stopPropagation() if (!dragState || event.pointerId !== dragState.pointerId) return const deltaX = event.clientX - dragState.startX @@ -119,6 +123,7 @@ function handlePointerMove(event: PointerEvent) { } function finishPointerDrag(event: PointerEvent) { + if (!props.interactive) return event.preventDefault() event.stopPropagation() if (!dragState || event.pointerId !== dragState.pointerId) return @@ -181,6 +186,11 @@ function handlePointerCancel(event: PointerEvent) { event.preventDefault() event.stopPropagation() if (!dragState || event.pointerId !== dragState.pointerId) return + cancelActiveDrag() +} + +function cancelActiveDrag() { + if (!dragState) return const state = dragState dragState = null dragOffsets.value = new Map(dragOffsets.value).set(state.annotationId, { x: 0, y: 0 }) @@ -188,10 +198,21 @@ function handlePointerCancel(event: PointerEvent) { cancelAnimationFrame(dragFrame) dragFrame = null } + if (state.target.hasPointerCapture(state.pointerId)) { + state.target.releasePointerCapture(state.pointerId) + } emit('drag-end', true) } +watch( + () => props.interactive, + (interactive) => { + if (!interactive) cancelActiveDrag() + }, +) + function handleContextMenu(annotationId: string, event: MouseEvent) { + if (!props.interactive) return event.preventDefault() event.stopPropagation() // 使用时间戳比较:如果 contextmenu 在拖动结束后 100ms 内触发,则抑制 @@ -206,7 +227,11 @@ function markerId(annotationId: string) {