新增: 批注/备注/对齐分布/形状扩充/video 元素——编辑器能力增强
This commit is contained in:
@@ -53,6 +53,8 @@ const props = defineProps<{
|
||||
edit?: boolean
|
||||
/** 是否显示八向缩放手柄 */
|
||||
showHandles?: boolean
|
||||
/** 是否为缩略图端(视频等重元素降级为静态渲染) */
|
||||
thumb?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -108,8 +110,33 @@ const shapeBg = computed(() => {
|
||||
return fill
|
||||
})
|
||||
|
||||
const isCircle = computed(() => s.value.shapeType === 'circle')
|
||||
const isTriangle = computed(() => s.value.shapeType === 'triangle')
|
||||
/** 各形状 clip-path(rect/circle 走 CSS 圆角,不走 clip) */
|
||||
const SHAPE_CLIPS: Partial<Record<string, string>> = {
|
||||
triangle: 'polygon(50% 0, 100% 100%, 0 100%)',
|
||||
diamond: 'polygon(50% 0, 100% 50%, 50% 100%, 0 50%)',
|
||||
pentagon: 'polygon(50% 0, 100% 38%, 82% 100%, 18% 100%, 0 38%)',
|
||||
hexagon: 'polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%)',
|
||||
star: 'polygon(50% 0, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
|
||||
arrow: 'polygon(0 20%, 75% 20%, 75% 0, 100% 50%, 75% 100%, 75% 80%, 0 80%)',
|
||||
chevron: 'polygon(0 0, 75% 0, 100% 50%, 75% 100%, 0 100%, 25% 50%)',
|
||||
bubble: 'polygon(0 0, 100% 0, 100% 75%, 25% 75%, 10% 100%, 15% 75%, 0 75%)'
|
||||
}
|
||||
const shapeClip = computed(() => SHAPE_CLIPS[s.value.shapeType || 'rect'] || null)
|
||||
|
||||
/** 形状文字(md 渲染;segments 存在时优先) */
|
||||
const renderedShapeText = computed(() => {
|
||||
if (props.el.segments && props.el.segments.length && hasFormatting(props.el.segments)) {
|
||||
return segmentsToHtml(props.el.segments)
|
||||
}
|
||||
return segmentsToHtml(markdownToSegments(props.el.content || ''))
|
||||
})
|
||||
|
||||
/** 形状文字层样式:对齐 + 未设色时默认白(填充色底) */
|
||||
const shapeTextStyle = computed(() => {
|
||||
const css: Record<string, string> = { textAlign: s.value.align || 'center' }
|
||||
if (!s.value.color) css.color = '#fff'
|
||||
return css
|
||||
})
|
||||
|
||||
const cardAccentColor = computed(() => resolveColor(s.value.accent, false))
|
||||
|
||||
@@ -119,6 +146,19 @@ const cardParts = computed(() => {
|
||||
return { title: lines[0] || '', body: lines.slice(1).join('\n') }
|
||||
})
|
||||
|
||||
/** 卡片标题/正文失焦:从 DOM 读两块文字拼回 content(标题在前的兄弟节点)。
|
||||
* 标题空但正文非空时保留空行占位,避免正文首行漂移成标题。 */
|
||||
function onBlurCard(e: Event) {
|
||||
const node = e.target as HTMLElement
|
||||
const card = node.closest('.el-card')
|
||||
if (!card) return
|
||||
const title = (card.querySelector('.card-title')?.textContent || '').trim()
|
||||
const body = (card.querySelector('.card-body')?.textContent || '').trim()
|
||||
if (!title && !body) { emit('blur', props.el.id, 'content', ''); return }
|
||||
if (body) emit('blur', props.el.id, 'content', title + '\n' + body)
|
||||
else emit('blur', props.el.id, 'content', title)
|
||||
}
|
||||
|
||||
/* ---------- Rich text(segments 结构化富文本)---------- */
|
||||
/** 是否有 segments(结构化富文本),优先于 content */
|
||||
const hasSegments = computed(() => {
|
||||
@@ -161,6 +201,7 @@ function onBlur(e: Event, field: string) {
|
||||
:data-id="el.id"
|
||||
:data-type="el.type"
|
||||
:data-anim="s.anim"
|
||||
:data-shape="el.type === 'shape' ? (s.shapeType || 'rect') : undefined"
|
||||
:style="{
|
||||
left: el.x + '%',
|
||||
top: el.y + '%',
|
||||
@@ -234,15 +275,54 @@ function onBlur(e: Event, field: string) {
|
||||
<img v-else class="el-image" :src="el.content" draggable="false" />
|
||||
</template>
|
||||
|
||||
<!-- 视频(缩略图端降级静态;preload=metadata 控制加载开销) -->
|
||||
<template v-else-if="el.type === 'video'">
|
||||
<div v-if="!el.content" class="el-image-empty">
|
||||
<span class="el-image-empty-icon">🎬</span>
|
||||
<span class="el-image-empty-text">属性面板选择本地视频或填入 URL</span>
|
||||
</div>
|
||||
<!-- 缩略图:poster 静态图(无 poster 黑底▶),不加载视频 -->
|
||||
<div v-else-if="thumb" class="el-video-thumb">
|
||||
<img v-if="s.poster" :src="s.poster" draggable="false" />
|
||||
<span v-else class="el-video-thumb-play">▶</span>
|
||||
</div>
|
||||
<!-- 编辑/演示:可播放(点选拖动交给画布 mousedown 冒泡,不阻断) -->
|
||||
<video
|
||||
v-else
|
||||
class="el-video"
|
||||
:src="el.content"
|
||||
:poster="s.poster || undefined"
|
||||
controls
|
||||
preload="metadata"
|
||||
:autoplay="!!s.autoplay"
|
||||
:loop="!!s.loop"
|
||||
:muted="!!s.muted"
|
||||
:style="{ objectFit: s.fit || 'contain' }"
|
||||
draggable="false"
|
||||
@click.stop
|
||||
></video>
|
||||
</template>
|
||||
|
||||
<!-- 形状 -->
|
||||
<template v-else-if="el.type === 'shape'">
|
||||
<div v-if="isCircle" class="el-shape" :class="{ gradient: s.gradient }" :style="{ borderRadius: '50%', background: shapeBg }"></div>
|
||||
<div v-else-if="isTriangle" class="el-shape">
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="width:100%;height:100%;display:block">
|
||||
<polygon points="50,5 95,95 5,95" :fill="shapeBg" />
|
||||
</svg>
|
||||
</div>
|
||||
<div v-if="s.shapeType === 'circle' || s.shapeType === 'ellipse'" class="el-shape" :class="{ gradient: s.gradient }" :style="{ borderRadius: '50%', background: shapeBg }"></div>
|
||||
<div v-else-if="shapeClip" class="el-shape" :class="{ gradient: s.gradient }" :style="{ background: shapeBg, clipPath: shapeClip }"></div>
|
||||
<div v-else class="el-shape" :class="{ gradient: s.gradient }" :style="{ background: shapeBg, borderRadius: (s.radius != null ? s.radius : 12) + 'px' }"></div>
|
||||
<!-- 形状文字层:编辑态 contenteditable,非编辑态渲染 md;未设色时默认白(填充色底) -->
|
||||
<div
|
||||
v-if="edit"
|
||||
class="el-shape-text"
|
||||
:style="!s.color ? { color: '#fff' } : {}"
|
||||
contenteditable="true"
|
||||
data-edit="content"
|
||||
@blur="onBlur($event, 'content')"
|
||||
>{{ el.content }}</div>
|
||||
<div
|
||||
v-else-if="el.content"
|
||||
class="el-shape-text"
|
||||
:style="shapeTextStyle"
|
||||
v-html="renderedShapeText"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- 图表 -->
|
||||
@@ -255,8 +335,18 @@ function onBlur(e: Event, field: string) {
|
||||
<div class="card-bar" :style="{ background: cardAccentColor }"></div>
|
||||
<div class="el-card">
|
||||
<div v-if="s.icon" class="card-icon">{{ s.icon }}</div>
|
||||
<div class="card-title">{{ cardParts.title }}</div>
|
||||
<div class="card-body">{{ cardParts.body }}</div>
|
||||
<div
|
||||
class="card-title"
|
||||
:contenteditable="edit"
|
||||
data-edit="content"
|
||||
@blur="edit && onBlurCard($event)"
|
||||
>{{ cardParts.title }}</div>
|
||||
<div
|
||||
class="card-body"
|
||||
:contenteditable="edit"
|
||||
data-edit="content"
|
||||
@blur="edit && onBlurCard($event)"
|
||||
>{{ cardParts.body }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user