From af5211d46d488495564b6360e76f2d17c77ef530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 25 Aug 2026 12:30:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E7=94=BB=E5=B8=83?= =?UTF-8?q?=E6=89=B9=E6=B3=A8=E5=B1=82=20AnnotationLayer,=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E8=BF=9E=E7=BA=BF+=E5=8F=AF=E6=8B=96=E5=8A=A8?= =?UTF-8?q?=E6=B0=94=E6=B3=A1,SVG+DOM=20=E5=88=86=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/editor/AnnotationLayer.vue | 439 ++++++++++++++++++++++ src/components/editor/Canvas.vue | 46 ++- tests/shape-render.test.ts | 57 +++ 3 files changed, 536 insertions(+), 6 deletions(-) create mode 100644 src/components/editor/AnnotationLayer.vue create mode 100644 tests/shape-render.test.ts diff --git a/src/components/editor/AnnotationLayer.vue b/src/components/editor/AnnotationLayer.vue new file mode 100644 index 0000000..452e46b --- /dev/null +++ b/src/components/editor/AnnotationLayer.vue @@ -0,0 +1,439 @@ + + + + + + diff --git a/src/components/editor/Canvas.vue b/src/components/editor/Canvas.vue index 8f15084..b98ed4a 100644 --- a/src/components/editor/Canvas.vue +++ b/src/components/editor/Canvas.vue @@ -7,6 +7,7 @@ import { store, resolveBg } from '../../core/store' import { CANVAS_W } from '../../core/sample' import { useEditor } from '../../composables/useEditor' import ElementView from './ElementView.vue' +import AnnotationLayer from './AnnotationLayer.vue' const canvasRef = ref() const canvasFrameRef = ref() // 注意:绑定 .canvas-frame(不含 stage 的 padding),scale 以它为基准 @@ -20,6 +21,7 @@ const slide = computed(() => store.currentSlide.value) const selectedId = computed(() => store.selectedId.value) const selectedIds = computed(() => store.selectedIds.value || []) const HANDLES = ['tl', 'tm', 'tr', 'lm', 'rm', 'bl', 'bm', 'br'] +const noteOpen = ref(false) // 演讲者备注栏展开态 /** 框选矩形样式(% → CSS) */ const marqueeBox = computed(() => { @@ -45,11 +47,16 @@ function onElementBlur(id: string, field: string, value: string) { editing.value = false if (field === 'label') { store.updateElement(id, { style: { label: value } }) - } else if (store.findElement(id)?.type === 'list') { - store.updateElement(id, { content: value }) - } else { - store.updateElement(id, { content: value }) + return } + // 形状输入文字:装饰细条(h 过小)自动撑高到能容纳一行字,避免文字被裁 + const el = store.findElement(id) + if (el?.type === 'shape' && value.trim() && el.h < 6) { + const grow = 6 - el.h + store.updateElement(id, { content: value, h: 6, y: Math.max(0, el.y - grow / 2) }) + return + } + store.updateElement(id, { content: value }) } /** 拖拽中元素的实时位置(覆盖 state) */ @@ -76,6 +83,10 @@ function liveMultiBox(elId: string) { return null } +function onNoteInput(e: Event) { + store.setSlideNote((e.target as HTMLTextAreaElement).value) +} + const onResize = () => fitCanvas() /** 监听 frame 尺寸:v-show 隐藏→显示、窗口缩放、布局变化都会触发(隐藏时 rect=0,onMounted 的 rAF 会空跑) */ @@ -134,10 +145,33 @@ onUnmounted(() => {
+ + -
- {{ store.currentIndex.value + 1 }} / {{ store.count.value }} + +
+
diff --git a/tests/shape-render.test.ts b/tests/shape-render.test.ts new file mode 100644 index 0000000..cac28df --- /dev/null +++ b/tests/shape-render.test.ts @@ -0,0 +1,57 @@ +/* shape-render.test.ts — 形状元素渲染回归(细条/装饰形状不被文字层污染) */ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import ElementView from '../src/components/editor/ElementView.vue' +import { store } from '../src/core/store' +import type { SlideElement } from '../src/core/types' + +/** 目录页 t2:标题下方的细横条(rect 128×11.5px) */ +const t2: SlideElement = { + id: 't2', type: 'shape', x: 8, y: 23, w: 10, h: 1.6, content: '', + style: { shapeType: 'rect', fill: 'accent', radius: 4, anim: 'fade-up' } +} + +/** 步骤页 st4:步骤间的小三角 */ +const st4: SlideElement = { + id: 'st4', type: 'shape', x: 27.5, y: 40, w: 5, h: 0.8, content: '', + style: { shapeType: 'triangle', fill: 'accent', anim: 'fade' } +} + +describe('装饰形状渲染', () => { + it('细横条 rect:编辑态渲染图形 div + 空 contenteditable 文字层,无占位文字', () => { + const w = mount(ElementView, { props: { el: t2, bg: 'bg', edit: true }, attachTo: document.body }) + const shape = w.find('.el-shape') + expect(shape.exists()).toBe(true) + expect(shape.attributes('style')).toContain('border-radius: 4px') + const text = w.find('.el-shape-text') + expect(text.exists()).toBe(true) // 编辑态保留可点击输入 + expect(text.text()).toBe('') // 无占位文字 + // 根节点暴露 data-shape 供 CSS 区分满幅/多边形安全区 + expect(w.find('.el').attributes('data-shape')).toBe('rect') + w.unmount() + }) + + it('小三角 triangle:走 clip-path 分支,非正比框渲染', () => { + const w = mount(ElementView, { props: { el: st4, bg: 'bg', edit: true }, attachTo: document.body }) + const shape = w.find('.el-shape') + expect(shape.attributes('style') || '').toContain('clip-path') + w.unmount() + }) + + it('非编辑态(模板预览):空 content 不渲染文字层', () => { + const w = mount(ElementView, { props: { el: t2, bg: 'bg' }, attachTo: document.body }) + expect(w.find('.el-shape-text').exists()).toBe(false) + w.unmount() + }) + + it('有文字的形状:非编辑态渲染 md 文字层且默认白字', () => { + void store + const el = { ...t2, h: 10, content: '**加粗**文字' } + const w = mount(ElementView, { props: { el, bg: 'bg' }, attachTo: document.body }) + const text = w.find('.el-shape-text') + expect(text.exists()).toBe(true) + expect(text.html()).toContain('加粗') + expect(text.attributes('style') || '').toContain('rgb(255, 255, 255)') + w.unmount() + }) +})