-
-
-
-
-
-
-
-
diff --git a/src/components/annotation/components.test.ts b/src/components/annotation/components.test.ts
index 72d9986..3a1d8ee 100644
--- a/src/components/annotation/components.test.ts
+++ b/src/components/annotation/components.test.ts
@@ -5,7 +5,6 @@ import { ColorPicker } from 'vue3-colorpicker'
import WaveformAnnotationContextMenu from './WaveformAnnotationContextMenu.vue'
import WaveformAnnotationEditor from './WaveformAnnotationEditor.vue'
import WaveformAnnotationLayer from './WaveformAnnotationLayer.vue'
-import WaveformAnnotationToolbar from './WaveformAnnotationToolbar.vue'
describe('waveform annotation controls', () => {
it('keeps dialog title ids unique across editor instances', () => {
@@ -76,18 +75,6 @@ describe('waveform annotation controls', () => {
expect(wrapper.get('.waveform-annotation-editor__series').text()).toContain('通道 A')
})
- it('emits controlled toolbar changes', async () => {
- const wrapper = mount(WaveformAnnotationToolbar, {
- props: { interactionMode: 'zoom', annotationsVisible: true },
- })
-
- await wrapper.get('button[aria-label="添加标注"]').trigger('click')
- await wrapper.get('button[aria-label="隐藏标注"]').trigger('click')
-
- expect(wrapper.emitted('update:interaction-mode')).toEqual([['annotation']])
- expect(wrapper.emitted('update:annotations-visible')).toEqual([[false]])
- })
-
it('validates text and emits an immutable edited annotation with style defaults', async () => {
const annotation = { id: 'note', seriesId: 'a', x: 1, y: 2, text: '' }
const wrapper = mount(WaveformAnnotationEditor, {
diff --git a/src/components/annotation/index.ts b/src/components/annotation/index.ts
index ca6e31a..b564393 100644
--- a/src/components/annotation/index.ts
+++ b/src/components/annotation/index.ts
@@ -1,6 +1,6 @@
export { default as WaveformAnnotationLayer } from './WaveformAnnotationLayer.vue'
-export { default as WaveformAnnotationToolbar } from './WaveformAnnotationToolbar.vue'
export { default as WaveformAnnotationContextMenu } from './WaveformAnnotationContextMenu.vue'
export * from './markup'
+export * from './serialization'
export * from './types'
export * from './useWaveformAnnotationInteraction'
diff --git a/src/components/annotation/serialization.test.ts b/src/components/annotation/serialization.test.ts
new file mode 100644
index 0000000..191fa9b
--- /dev/null
+++ b/src/components/annotation/serialization.test.ts
@@ -0,0 +1,114 @@
+import { describe, expect, it } from 'vitest'
+
+import type { WaveformAnnotation } from '../../types'
+import { parseWaveformAnnotations, serializeWaveformAnnotations } from './serialization'
+
+describe('waveform annotation serialization', () => {
+ it('round-trips every annotation field through a versioned document', () => {
+ const source: WaveformAnnotation[] = [
+ {
+ id: 'note-1',
+ seriesId: 'channel-a',
+ x: 1.25,
+ y: -3.5,
+ text: '峰值',
+ labelOffsetX: 12,
+ labelOffsetY: -8,
+ createdAt: '2026-07-21T12:00:00.000Z',
+ style: {
+ borderColor: '#1677ff',
+ textColor: '#333333',
+ backgroundColor: 'rgba(255, 255, 255, 0.92)',
+ },
+ },
+ ]
+ const sourceSnapshot = JSON.parse(JSON.stringify(source))
+
+ const parsed = parseWaveformAnnotations(serializeWaveformAnnotations(source))
+
+ expect(JSON.parse(serializeWaveformAnnotations(source))).toMatchObject({ version: 1 })
+ expect(source).toEqual(sourceSnapshot)
+ expect(parsed).toEqual(source)
+ expect(parsed).not.toBe(source)
+ expect(parsed[0]).not.toBe(source[0])
+ expect(parsed[0].style).not.toBe(source[0].style)
+ })
+
+ it('allows annotations for series that are not currently loaded', () => {
+ expect(
+ parseWaveformAnnotations(
+ JSON.stringify({
+ version: 1,
+ annotations: [{ id: 'future', seriesId: 'missing', x: 1, y: 2, text: '稍后显示' }],
+ }),
+ ),
+ ).toEqual([{ id: 'future', seriesId: 'missing', x: 1, y: 2, text: '稍后显示' }])
+ })
+
+ it.each([
+ ['invalid JSON', '{'],
+ ['non-object root', '[]'],
+ ['unsupported version', JSON.stringify({ version: 2, annotations: [] })],
+ ['missing annotation array', JSON.stringify({ version: 1 })],
+ [
+ 'invalid annotation entry',
+ JSON.stringify({ version: 1, annotations: [{ id: 'a', seriesId: 's', x: 1 }] }),
+ ],
+ [
+ 'non-finite coordinate',
+ '{"version":1,"annotations":[{"id":"a","seriesId":"s","x":1e400,"y":2,"text":"a"}]}',
+ ],
+ [
+ 'overlong text',
+ JSON.stringify({
+ version: 1,
+ annotations: [{ id: 'a', seriesId: 's', x: 1, y: 2, text: 'a'.repeat(41) }],
+ }),
+ ],
+ [
+ 'duplicate IDs',
+ JSON.stringify({
+ version: 1,
+ annotations: [
+ { id: 'a', seriesId: 's', x: 1, y: 2, text: 'one' },
+ { id: 'a', seriesId: 's', x: 2, y: 3, text: 'two' },
+ ],
+ }),
+ ],
+ ])('rejects %s without returning partial data', (_label, json) => {
+ expect(() => parseWaveformAnnotations(json)).toThrow('Invalid waveform annotation file')
+ })
+
+ it('rejects invalid optional fields and serialization input', () => {
+ expect(() =>
+ parseWaveformAnnotations(
+ JSON.stringify({
+ version: 1,
+ annotations: [
+ {
+ id: 'a',
+ seriesId: 's',
+ x: 1,
+ y: 2,
+ text: 'a',
+ labelOffsetX: '12',
+ },
+ ],
+ }),
+ ),
+ ).toThrow('labelOffsetX')
+
+ expect(() =>
+ parseWaveformAnnotations(
+ JSON.stringify({
+ version: 1,
+ annotations: [{ id: 'a', seriesId: 's', x: 1, y: 2, text: 'a', style: [] }],
+ }),
+ ),
+ ).toThrow('style must be an object')
+
+ expect(() =>
+ serializeWaveformAnnotations([{ id: 'a', seriesId: 's', x: Number.NaN, y: 2, text: 'a' }]),
+ ).toThrow('x must be a finite number')
+ })
+})
diff --git a/src/components/annotation/serialization.ts b/src/components/annotation/serialization.ts
new file mode 100644
index 0000000..6e0a7a4
--- /dev/null
+++ b/src/components/annotation/serialization.ts
@@ -0,0 +1,127 @@
+import type { WaveformAnnotation, WaveformAnnotationStyle } from '../../types'
+import { ANNOTATION_MAX_TEXT_LENGTH } from './markup'
+
+const ANNOTATION_FILE_VERSION = 1
+
+type JsonRecord = Record