2026-07-29 21:26:48 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="slide-canvas-wrapper" :style="wrapperStyle">
|
|
|
|
|
<canvas
|
|
|
|
|
ref="canvasRef"
|
|
|
|
|
:width="canvasWidth"
|
|
|
|
|
:height="canvasHeight"
|
|
|
|
|
class="slide-canvas"
|
2026-07-29 22:14:02 +08:00
|
|
|
role="img"
|
|
|
|
|
:aria-label="`幻灯片 ${(slide?.index ?? 0) + 1}`"
|
2026-07-29 21:26:48 +08:00
|
|
|
@click="handleClick"
|
2026-07-31 15:41:58 +08:00
|
|
|
@dblclick="handleDoubleClick"
|
2026-07-29 21:26:48 +08:00
|
|
|
@mousemove="handleMouseMove"
|
|
|
|
|
/>
|
|
|
|
|
<!-- 行内文字编辑器 -->
|
|
|
|
|
<div
|
2026-07-31 15:41:58 +08:00
|
|
|
v-if="editingShape || editingCell"
|
2026-07-29 21:26:48 +08:00
|
|
|
class="text-editor-overlay"
|
|
|
|
|
:style="editorStyle"
|
|
|
|
|
>
|
|
|
|
|
<textarea
|
|
|
|
|
ref="editorRef"
|
|
|
|
|
v-model="editingText"
|
|
|
|
|
class="text-editor"
|
|
|
|
|
:style="editorTextStyle"
|
|
|
|
|
@blur="commitEdit"
|
|
|
|
|
@keydown.escape="cancelEdit"
|
|
|
|
|
@keydown.ctrl.enter="commitEdit"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
|
|
|
|
import type { SlideData, SlideShape, SlideParagraph } from '@/utils/ppt-api'
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
slide: SlideData | null
|
|
|
|
|
slideWidth?: number
|
|
|
|
|
slideHeight?: number
|
|
|
|
|
scale?: number
|
|
|
|
|
editable?: boolean
|
2026-07-31 15:41:58 +08:00
|
|
|
selectedShapeId?: string | null
|
2026-07-29 21:26:48 +08:00
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
'update:shapes': [shapes: SlideShape[]]
|
2026-07-31 15:41:58 +08:00
|
|
|
'shape-select': [shape: SlideShape | null, index: number]
|
2026-07-29 21:26:48 +08:00
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
|
|
|
const editorRef = ref<HTMLTextAreaElement | null>(null)
|
|
|
|
|
|
|
|
|
|
// 编辑状态
|
|
|
|
|
const editingShape = ref<SlideShape | null>(null)
|
|
|
|
|
const editingText = ref('')
|
|
|
|
|
const editingShapeIndex = ref(-1)
|
2026-07-31 15:41:58 +08:00
|
|
|
const editingCell = ref<{ shape: SlideShape; shapeIndex: number; row: number; col: number } | null>(null)
|
2026-07-29 21:26:48 +08:00
|
|
|
|
|
|
|
|
// 画布尺寸
|
2026-07-31 15:20:37 +08:00
|
|
|
const baseWidth = computed(() => props.slideWidth || 960)
|
2026-07-29 21:26:48 +08:00
|
|
|
const baseHeight = computed(() => props.slideHeight || 540)
|
|
|
|
|
const scale = computed(() => props.scale ?? 1)
|
|
|
|
|
const canvasWidth = computed(() => Math.round(baseWidth.value * scale.value))
|
|
|
|
|
const canvasHeight = computed(() => Math.round(baseHeight.value * scale.value))
|
|
|
|
|
|
|
|
|
|
const wrapperStyle = computed(() => ({
|
|
|
|
|
width: `${canvasWidth.value}px`,
|
|
|
|
|
height: `${canvasHeight.value}px`,
|
|
|
|
|
position: 'relative' as const,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// 编辑器定位
|
|
|
|
|
const editorStyle = computed(() => {
|
2026-07-31 15:41:58 +08:00
|
|
|
if (editingCell.value) {
|
|
|
|
|
const { shape, row, col } = editingCell.value
|
|
|
|
|
const rows = shape.rows || []
|
|
|
|
|
const cellH = Math.max(20, shape.h / Math.max(rows.length, 1))
|
|
|
|
|
const cellW = shape.w / Math.max(rows[0]?.length || 1, 1)
|
|
|
|
|
return {
|
|
|
|
|
left: `${(shape.x + col * cellW) * scale.value}px`,
|
|
|
|
|
top: `${(shape.y + row * cellH) * scale.value}px`,
|
|
|
|
|
width: `${cellW * scale.value}px`,
|
|
|
|
|
minHeight: `${cellH * scale.value}px`,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-29 21:26:48 +08:00
|
|
|
if (!editingShape.value) return {}
|
|
|
|
|
const s = editingShape.value
|
|
|
|
|
return {
|
|
|
|
|
left: `${s.x * scale.value}px`,
|
|
|
|
|
top: `${s.y * scale.value}px`,
|
|
|
|
|
width: `${s.w * scale.value}px`,
|
|
|
|
|
minHeight: `${s.h * scale.value}px`,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const editorTextStyle = computed(() => {
|
2026-07-31 15:41:58 +08:00
|
|
|
if (editingCell.value) {
|
|
|
|
|
const cell = editingCell.value.shape.rows?.[editingCell.value.row]?.[editingCell.value.col]
|
|
|
|
|
return {
|
|
|
|
|
fontSize: `${(cell?.fontSize || 12) * scale.value}px`,
|
|
|
|
|
fontWeight: cell?.bold ? 'bold' : 'normal',
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-29 21:26:48 +08:00
|
|
|
if (!editingShape.value?.paragraphs?.length) return {}
|
|
|
|
|
const p = editingShape.value.paragraphs[0]
|
|
|
|
|
return {
|
|
|
|
|
fontSize: `${(p.fontSize || 14) * scale.value}px`,
|
|
|
|
|
fontWeight: p.bold ? 'bold' : 'normal',
|
|
|
|
|
color: p.color || '#333',
|
|
|
|
|
textAlign: p.align || 'left',
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 渲染
|
|
|
|
|
function render() {
|
|
|
|
|
const canvas = canvasRef.value
|
|
|
|
|
if (!canvas || !props.slide) return
|
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
|
|
|
if (!ctx) return
|
|
|
|
|
|
|
|
|
|
const s = scale.value
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
|
|
|
ctx.save()
|
|
|
|
|
ctx.scale(s, s)
|
|
|
|
|
|
|
|
|
|
// 背景
|
|
|
|
|
const bg = props.slide.background || '#ffffff'
|
|
|
|
|
ctx.fillStyle = bg
|
|
|
|
|
ctx.fillRect(0, 0, baseWidth.value, baseHeight.value)
|
|
|
|
|
|
|
|
|
|
// 形状
|
|
|
|
|
for (const shape of props.slide.shapes) {
|
|
|
|
|
renderShape(ctx, shape)
|
|
|
|
|
}
|
2026-07-31 15:41:58 +08:00
|
|
|
const selected = props.slide.shapes.find(shape => shape.id === props.selectedShapeId)
|
|
|
|
|
if (selected) {
|
|
|
|
|
ctx.strokeStyle = '#3B7A57'
|
|
|
|
|
ctx.lineWidth = 2 / s
|
|
|
|
|
ctx.setLineDash([6 / s, 4 / s])
|
|
|
|
|
ctx.strokeRect(selected.x, selected.y, selected.w, selected.h)
|
|
|
|
|
ctx.setLineDash([])
|
|
|
|
|
}
|
2026-07-29 21:26:48 +08:00
|
|
|
|
|
|
|
|
ctx.restore()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderShape(ctx: CanvasRenderingContext2D, shape: SlideShape) {
|
|
|
|
|
if (shape.type === 'group' && shape.children) {
|
|
|
|
|
for (const child of shape.children) {
|
|
|
|
|
renderShape(ctx, child)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 填充背景
|
|
|
|
|
if (shape.fill) {
|
|
|
|
|
ctx.fillStyle = shape.fill
|
|
|
|
|
ctx.fillRect(shape.x, shape.y, shape.w, shape.h)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 边框
|
|
|
|
|
if (shape.lineColor) {
|
|
|
|
|
ctx.strokeStyle = shape.lineColor
|
|
|
|
|
ctx.lineWidth = shape.lineWidth || 1
|
|
|
|
|
ctx.strokeRect(shape.x, shape.y, shape.w, shape.h)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 文本
|
|
|
|
|
if (shape.type === 'textbox' && shape.paragraphs) {
|
|
|
|
|
renderText(ctx, shape)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表格
|
|
|
|
|
if (shape.type === 'table' && shape.rows) {
|
|
|
|
|
renderTable(ctx, shape)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 图片占位
|
|
|
|
|
if (shape.type === 'image') {
|
|
|
|
|
renderImagePlaceholder(ctx, shape)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderText(ctx: CanvasRenderingContext2D, shape: SlideShape) {
|
|
|
|
|
const paragraphs = shape.paragraphs || []
|
|
|
|
|
let y = shape.y + 4 // 顶部内边距
|
|
|
|
|
const maxWidth = shape.w - 8
|
|
|
|
|
|
|
|
|
|
for (const para of paragraphs) {
|
|
|
|
|
if (!para.text) {
|
|
|
|
|
y += (para.fontSize || 14) * 1.4
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fontSize = para.fontSize || 14
|
|
|
|
|
const fontWeight = para.bold ? 'bold' : 'normal'
|
|
|
|
|
const color = para.color || '#333333'
|
|
|
|
|
const align = para.align || 'left'
|
|
|
|
|
|
|
|
|
|
ctx.font = `${fontWeight} ${fontSize}px -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif`
|
|
|
|
|
ctx.fillStyle = color
|
|
|
|
|
ctx.textBaseline = 'top'
|
|
|
|
|
|
|
|
|
|
// 自动换行
|
|
|
|
|
const lineHeight = fontSize * 1.4
|
|
|
|
|
const words = para.text.split('')
|
|
|
|
|
let line = ''
|
|
|
|
|
let lineX = shape.x + 4
|
|
|
|
|
|
|
|
|
|
for (const char of words) {
|
|
|
|
|
const testLine = line + char
|
|
|
|
|
const metrics = ctx.measureText(testLine)
|
|
|
|
|
if (metrics.width > maxWidth && line) {
|
|
|
|
|
// 绘制当前行
|
|
|
|
|
let drawX = lineX
|
|
|
|
|
if (align === 'center') {
|
|
|
|
|
drawX = shape.x + (shape.w - ctx.measureText(line).width) / 2
|
|
|
|
|
} else if (align === 'right') {
|
|
|
|
|
drawX = shape.x + shape.w - 4 - ctx.measureText(line).width
|
|
|
|
|
}
|
|
|
|
|
ctx.fillText(line, drawX, y)
|
|
|
|
|
line = char
|
|
|
|
|
y += lineHeight
|
|
|
|
|
} else {
|
|
|
|
|
line = testLine
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (line) {
|
|
|
|
|
let drawX = lineX
|
|
|
|
|
if (align === 'center') {
|
|
|
|
|
drawX = shape.x + (shape.w - ctx.measureText(line).width) / 2
|
|
|
|
|
} else if (align === 'right') {
|
|
|
|
|
drawX = shape.x + shape.w - 4 - ctx.measureText(line).width
|
|
|
|
|
}
|
|
|
|
|
ctx.fillText(line, drawX, y)
|
|
|
|
|
y += lineHeight
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderTable(ctx: CanvasRenderingContext2D, shape: SlideShape) {
|
|
|
|
|
const rows = shape.rows || []
|
|
|
|
|
if (rows.length === 0) return
|
|
|
|
|
|
|
|
|
|
const cellH = Math.max(20, shape.h / rows.length)
|
|
|
|
|
const cellW = shape.w / (rows[0]?.length || 1)
|
|
|
|
|
|
|
|
|
|
for (let r = 0; r < rows.length; r++) {
|
|
|
|
|
for (let c = 0; c < rows[r].length; c++) {
|
|
|
|
|
const cell = rows[r][c]
|
|
|
|
|
const cx = shape.x + c * cellW
|
|
|
|
|
const cy = shape.y + r * cellH
|
|
|
|
|
|
|
|
|
|
// 单元格背景(表头行深色)
|
|
|
|
|
ctx.fillStyle = r === 0 ? '#f0f4f8' : '#ffffff'
|
|
|
|
|
ctx.fillRect(cx, cy, cellW, cellH)
|
|
|
|
|
ctx.strokeStyle = '#e0e0e0'
|
|
|
|
|
ctx.lineWidth = 0.5
|
|
|
|
|
ctx.strokeRect(cx, cy, cellW, cellH)
|
|
|
|
|
|
|
|
|
|
// 文本
|
|
|
|
|
const fontSize = cell.fontSize || 12
|
|
|
|
|
ctx.font = `${cell.bold ? 'bold' : 'normal'} ${fontSize}px -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif`
|
|
|
|
|
ctx.fillStyle = '#333'
|
|
|
|
|
ctx.textBaseline = 'middle'
|
|
|
|
|
ctx.fillText(cell.text || '', cx + 4, cy + cellH / 2)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderImagePlaceholder(ctx: CanvasRenderingContext2D, shape: SlideShape) {
|
|
|
|
|
ctx.fillStyle = '#e8edf2'
|
|
|
|
|
ctx.fillRect(shape.x, shape.y, shape.w, shape.h)
|
|
|
|
|
ctx.strokeStyle = '#c0c8d4'
|
|
|
|
|
ctx.lineWidth = 1
|
|
|
|
|
ctx.strokeRect(shape.x, shape.y, shape.w, shape.h)
|
|
|
|
|
|
|
|
|
|
// 图片图标
|
|
|
|
|
const cx = shape.x + shape.w / 2
|
|
|
|
|
const cy = shape.y + shape.h / 2
|
|
|
|
|
const iconSize = Math.min(shape.w, shape.h) * 0.3
|
|
|
|
|
ctx.fillStyle = '#a0aec0'
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
ctx.moveTo(cx - iconSize * 0.5, cy + iconSize * 0.3)
|
|
|
|
|
ctx.lineTo(cx - iconSize * 0.2, cy - iconSize * 0.1)
|
|
|
|
|
ctx.lineTo(cx + iconSize * 0.1, cy + iconSize * 0.2)
|
|
|
|
|
ctx.lineTo(cx + iconSize * 0.3, cy - iconSize * 0.2)
|
|
|
|
|
ctx.lineTo(cx + iconSize * 0.5, cy + iconSize * 0.3)
|
|
|
|
|
ctx.closePath()
|
|
|
|
|
ctx.fill()
|
|
|
|
|
|
|
|
|
|
ctx.font = '11px sans-serif'
|
|
|
|
|
ctx.fillStyle = '#8899aa'
|
|
|
|
|
ctx.textAlign = 'center'
|
|
|
|
|
ctx.textBaseline = 'top'
|
|
|
|
|
ctx.fillText('图片', cx, cy + iconSize * 0.5)
|
|
|
|
|
ctx.textAlign = 'left'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 点击处理
|
|
|
|
|
function handleClick(e: MouseEvent) {
|
|
|
|
|
if (!props.editable || !props.slide) return
|
|
|
|
|
|
|
|
|
|
const rect = canvasRef.value!.getBoundingClientRect()
|
|
|
|
|
const x = (e.clientX - rect.left) / scale.value
|
|
|
|
|
const y = (e.clientY - rect.top) / scale.value
|
|
|
|
|
|
2026-07-31 15:41:58 +08:00
|
|
|
const hit = findShapeAt(x, y)
|
|
|
|
|
emit('shape-select', hit?.shape || null, hit?.index ?? -1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleDoubleClick(e: MouseEvent) {
|
|
|
|
|
if (!props.editable || !props.slide) return
|
|
|
|
|
const rect = canvasRef.value!.getBoundingClientRect()
|
|
|
|
|
const x = (e.clientX - rect.left) / scale.value
|
|
|
|
|
const y = (e.clientY - rect.top) / scale.value
|
|
|
|
|
const hit = findShapeAt(x, y)
|
|
|
|
|
if (!hit) return
|
|
|
|
|
emit('shape-select', hit.shape, hit.index)
|
|
|
|
|
if (hit.shape.type === 'textbox') {
|
|
|
|
|
startEdit(hit.shape, hit.index)
|
|
|
|
|
} else if (hit.shape.type === 'table') {
|
|
|
|
|
startCellEdit(hit.shape, hit.index, x, y)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findShapeAt(x: number, y: number) {
|
|
|
|
|
const shapes = props.slide?.shapes || []
|
2026-07-29 21:26:48 +08:00
|
|
|
for (let i = shapes.length - 1; i >= 0; i--) {
|
|
|
|
|
const shape = shapes[i]
|
|
|
|
|
if (x >= shape.x && x <= shape.x + shape.w &&
|
|
|
|
|
y >= shape.y && y <= shape.y + shape.h) {
|
2026-07-31 15:41:58 +08:00
|
|
|
return { shape, index: i }
|
2026-07-29 21:26:48 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-31 15:41:58 +08:00
|
|
|
return null
|
2026-07-29 21:26:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startEdit(shape: SlideShape, index: number) {
|
2026-07-31 15:41:58 +08:00
|
|
|
editingCell.value = null
|
|
|
|
|
editingShape.value = JSON.parse(JSON.stringify(shape))
|
2026-07-29 21:26:48 +08:00
|
|
|
editingShapeIndex.value = index
|
|
|
|
|
editingText.value = (shape.paragraphs || []).map(p => p.text).join('\n')
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
editorRef.value?.focus()
|
|
|
|
|
editorRef.value?.select()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-31 15:41:58 +08:00
|
|
|
function startCellEdit(shape: SlideShape, shapeIndex: number, x: number, y: number) {
|
|
|
|
|
const cloned: SlideShape = JSON.parse(JSON.stringify(shape))
|
|
|
|
|
const rows = cloned.rows || []
|
|
|
|
|
if (!rows.length) return
|
|
|
|
|
const row = Math.min(rows.length - 1, Math.floor((y - cloned.y) / Math.max(20, cloned.h / rows.length)))
|
|
|
|
|
const colCount = rows[row]?.length || 1
|
|
|
|
|
const col = Math.min(colCount - 1, Math.floor((x - cloned.x) / (cloned.w / colCount)))
|
|
|
|
|
editingShape.value = null
|
|
|
|
|
editingCell.value = { shape: cloned, shapeIndex, row, col }
|
|
|
|
|
editingText.value = rows[row]?.[col]?.text || ''
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
editorRef.value?.focus()
|
|
|
|
|
editorRef.value?.select()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-29 21:26:48 +08:00
|
|
|
function commitEdit() {
|
2026-07-31 15:41:58 +08:00
|
|
|
if (editingCell.value && props.slide) {
|
|
|
|
|
const { shape, shapeIndex, row, col } = editingCell.value
|
|
|
|
|
if (shape.rows?.[row]?.[col]) {
|
|
|
|
|
shape.rows[row][col] = { ...shape.rows[row][col], text: editingText.value }
|
|
|
|
|
const updatedShapes = [...props.slide.shapes]
|
|
|
|
|
updatedShapes[shapeIndex] = shape
|
|
|
|
|
emit('update:shapes', updatedShapes)
|
|
|
|
|
}
|
|
|
|
|
editingCell.value = null
|
|
|
|
|
render()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-29 21:26:48 +08:00
|
|
|
if (!editingShape.value || !props.slide) return
|
|
|
|
|
|
|
|
|
|
const text = editingText.value
|
|
|
|
|
const lines = text.split('\n')
|
|
|
|
|
const originalParas = editingShape.value.paragraphs || []
|
|
|
|
|
|
|
|
|
|
// 更新段落文本,保留原格式
|
|
|
|
|
const updatedParas = lines.map((line, i) => {
|
|
|
|
|
const base = originalParas[i] || originalParas[originalParas.length - 1] || {
|
|
|
|
|
fontSize: 14, bold: false, color: '#333333', align: 'left' as const,
|
|
|
|
|
}
|
|
|
|
|
return { ...base, text: line }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
editingShape.value.paragraphs = updatedParas
|
|
|
|
|
|
|
|
|
|
// 通知父组件
|
|
|
|
|
const updatedShapes = [...props.slide!.shapes]
|
|
|
|
|
updatedShapes[editingShapeIndex.value] = { ...editingShape.value }
|
|
|
|
|
emit('update:shapes', updatedShapes)
|
|
|
|
|
|
|
|
|
|
editingShape.value = null
|
|
|
|
|
editingShapeIndex.value = -1
|
|
|
|
|
render()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cancelEdit() {
|
|
|
|
|
editingShape.value = null
|
2026-07-31 15:41:58 +08:00
|
|
|
editingCell.value = null
|
2026-07-29 21:26:48 +08:00
|
|
|
editingShapeIndex.value = -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// hover 光标
|
|
|
|
|
function handleMouseMove(e: MouseEvent) {
|
|
|
|
|
if (!props.editable || !canvasRef.value) return
|
|
|
|
|
const rect = canvasRef.value.getBoundingClientRect()
|
|
|
|
|
const x = (e.clientX - rect.left) / scale.value
|
|
|
|
|
const y = (e.clientY - rect.top) / scale.value
|
|
|
|
|
|
2026-07-31 15:41:58 +08:00
|
|
|
let isEditable = false
|
2026-07-29 21:26:48 +08:00
|
|
|
if (props.slide) {
|
|
|
|
|
for (const shape of props.slide.shapes) {
|
|
|
|
|
if (x >= shape.x && x <= shape.x + shape.w &&
|
|
|
|
|
y >= shape.y && y <= shape.y + shape.h) {
|
2026-07-31 15:41:58 +08:00
|
|
|
isEditable = ['textbox', 'table', 'image'].includes(shape.type)
|
2026-07-29 21:26:48 +08:00
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-31 15:41:58 +08:00
|
|
|
canvasRef.value.style.cursor = isEditable ? 'pointer' : 'default'
|
2026-07-29 21:26:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 监听数据变化重新渲染
|
|
|
|
|
watch(() => props.slide, () => {
|
|
|
|
|
nextTick(render)
|
|
|
|
|
}, { deep: true })
|
|
|
|
|
|
|
|
|
|
watch(scale, () => {
|
|
|
|
|
nextTick(render)
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-31 15:41:58 +08:00
|
|
|
watch(() => props.selectedShapeId, () => nextTick(render))
|
|
|
|
|
|
2026-07-29 21:26:48 +08:00
|
|
|
onMounted(() => {
|
|
|
|
|
render()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
defineExpose({ render })
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.slide-canvas-wrapper {
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slide-canvas {
|
|
|
|
|
display: block;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.text-editor-overlay {
|
|
|
|
|
position: absolute;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.text-editor {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 100%;
|
2026-07-29 21:41:28 +08:00
|
|
|
border: 2px solid #3B7A57;
|
2026-07-29 21:26:48 +08:00
|
|
|
border-radius: 2px;
|
|
|
|
|
background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
padding: 4px;
|
|
|
|
|
outline: none;
|
|
|
|
|
resize: vertical;
|
|
|
|
|
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
}
|
|
|
|
|
</style>
|