一、插入形状
插入形状有两种情况,一种是插入固定的形状,
一种是插入自定义的形状。
插入固定的形状时,跟上一篇文章 绘制文本框 是一样一样的,都是调用的 mainStore.setCreatingElement()
方法,只不多传的类型不一样。还有插入线条,也是类似的。
mainStore.setCreatingElement({
type: 'shape',
data: shape,
})
所以咱们那接下来主要看插入自定义形状时的代码执行流程
1、点击
<Popover trigger="click" v-model:value="shapeMenuVisible" style="height: 100%;" :offset="10">
<template #content>
<PopoverMenuItem center @click="() => { drawCustomShape(); shapeMenuVisible = false }">自由绘制</PopoverMenuItem>
</template>
<IconDown class="arrow" />
</Popover>
src/views/Editor/CanvasTool/index.vue
// 绘制自定义任意多边形
const drawCustomShape = () => {
mainStore.setCreatingCustomShapeState(true)
shapePoolVisible.value = false
}
src/store/main.ts
setCreatingCustomShapeState(state: boolean) {
this.creatingCustomShape = state
},
有了 creatingCustomShape
,下面的组件就会显示
<ShapeCreateCanvas
v-if="creatingCustomShape"
@created="data => insertCustomShape(data)"
/>
2、mousedown
src/views/Editor/Canvas/ShapeCreateCanvas.vue
触发 created
方法
const addPoint = (e: MouseEvent) => {
const { pageX, pageY } = getPoint(e)
isMouseDown.value = true
if (closed.value) emit('created', getCreateData())
else points.value.push([pageX, pageY])
document.onmouseup = () => {
isMouseDown.value = false
}
}
3、created
src/views/Editor/Canvas/index.vue
插入任意多边形
// 插入自定义任意多边形
const insertCustomShape = (data: CreateCustomShapeData) => {
const {
start,
end,
path,
viewBox,
} = data
const position = formatCreateSelection({ start, end })
if (position) {
const supplement: Partial<PPTShapeElement> = {}
if (data.fill) supplement.fill = data.fill
if (data.outline) supplement.outline = data.outline
// 创建形状元素
createShapeElement(position, { path, viewBox }, supplement)
}
// 清除 creatingCustomShape
mainStore.setCreatingCustomShapeState(false)
}
4、mousemove
src/views/Editor/Canvas/ShapeCreateCanvas.vue
如果鼠标按下,添加 points
,就会形成折线的效果。
可以看到只要起点和终点比较近就算闭合了,防止对不上
const updateMousePosition = (e: MouseEvent) => {
// 如果鼠标按下,则添加点
if (isMouseDown.value) {
const { pageX, pageY } = getPoint(e, true)
points.value.push([pageX, pageY])
mousePosition.value = null
return
}
// 更新鼠标位置
const { pageX, pageY } = getPoint(e)
mousePosition.value = [pageX, pageY]
// 判断是否闭合
if (points.value.length >= 2) {
const [firstPointX, firstPointY] = points.value[0]
if (Math.abs(firstPointX - pageX) < 5 && Math.abs(firstPointY - pageY) < 5) {
closed.value = true
}
else closed.value = false
}
else closed.value = false
}
根据鼠标位置 mousePosition
计算 path
const path = computed(() => {
let d = ''
for (let i = 0; i < points.value.length; i++) {
const point = points.value[i]
if (i === 0) d += `M ${point[0]} ${point[1]} `
else d += `L ${point[0]} ${point[1]} `
}
if (points.value.length && mousePosition.value) {
d += `L ${mousePosition.value[0]} ${mousePosition.value[1]}`
}
return d
})
模版中的 path
元素随之更新
<svg overflow="visible">
<path
:d="path"
stroke="#d14424"
:fill="closed ? 'rgba(226, 83, 77, 0.15)' : 'none'"
stroke-width="2"
></path>
</svg>
5、取消绘制的按键绑定
const keydownListener = (e: KeyboardEvent) => {
const key = e.key.toUpperCase()
if (key === KEYS.ESC) close()
if (key === KEYS.ENTER) create()
}
onMounted(() => {
message.success('点击绘制任意形状,首尾闭合完成绘制,按 ESC 键或鼠标右键取消,按 ENTER 键提前完成', {
duration: 0,
})
document.addEventListener('keydown', keydownListener)
})
以及鼠标右键也会取消绘制
@contextmenu.stop.prevent="close()"
const close = () => {
mainStore.setCreatingCustomShapeState(false)
}
二、插入图片
src/views/Editor/CanvasTool/index.vue
插入图片也是一个自定义组件
<FileInput @change="files => insertImageElement(files)">
<IconPicture class="handler-item" v-tooltip="'插入图片'" />
</FileInput>
这个组件里面实现上传功能的是 input
标签
<input
class="input"
type="file"
name="upload"
ref="inputRef"
:accept="accept"
@change="$event => handleChange($event)"
>
上传之后插入图片元素
const insertImageElement = (files: FileList) => {
const imageFile = files[0]
if (!imageFile) return
getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
}
src/utils/image.ts
获取图片宽高的方法,相比大家都挺熟悉的
/**
* 获取图片的原始宽高
* @param src 图片地址
*/
export const getImageSize = (src: string): Promise<ImageSize> => {
return new Promise(resolve => {
const img = document.createElement('img')
img.src = src
img.style.opacity = '0'
document.body.appendChild(img)
img.onload = () => {
const imgWidth = img.clientWidth
const imgHeight = img.clientHeight
img.onload = null
img.onerror = null
document.body.removeChild(img)
resolve({ width: imgWidth, height: imgHeight })
}
img.onerror = () => {
img.onload = null
img.onerror = null
}
})
}
获取图片宽高之后,创建图片元素,通过 left
和 top
将图片水平垂直居中
src/hooks/useCreateElement.ts
/**
* 创建图片元素
* @param src 图片地址
*/
const createImageElement = (src: string) => {
getImageSize(src).then(({ width, height }) => {
const scale = height / width
if (scale < viewportRatio.value && width > VIEWPORT_SIZE) {
width = VIEWPORT_SIZE
height = width * scale
}
else if (height > VIEWPORT_SIZE * viewportRatio.value) {
height = VIEWPORT_SIZE * viewportRatio.value
width = height / scale
}
createElement({
type: 'image',
id: nanoid(10),
src,
width,
height,
left: (VIEWPORT_SIZE - width) / 2,
top: (VIEWPORT_SIZE * viewportRatio.value - height) / 2,
fixedRatio: true,
rotate: 0,
})
})
}
复习一下创建元素的方法,会把元素放到当前幻灯片的元素列表中
// 创建(插入)一个元素并将其设置为被选中元素
const createElement = (element: PPTElement, callback?: () => void) => {
// 添加元素到元素列表
slidesStore.addElement(element)
// 设置被选中元素列表
mainStore.setActiveElementIdList([element.id])
if (creatingElement.value) mainStore.setCreatingElement(null)
setTimeout(() => {
// 设置编辑器区域为聚焦状态
mainStore.setEditorareaFocus(true)
}, 0)
if (callback) callback()
// 添加历史快照
addHistorySnapshot()
}
三、插入图表
插入图表的方法,其实也差不多,就是往当前的幻灯片里添加一个图表对象。不过这里就不讲前面怎么添加元素了,讲讲后面怎么展示元素吧。先来看一下图表元素的数据:
const newElement: PPTChartElement = {
type: 'chart',
id: nanoid(10),
chartType: CHART_TYPES[type],
left: 300,
top: 81.25,
width: 400,
height: 400,
rotate: 0,
themeColor: [theme.value.themeColor],
gridColor: theme.value.fontColor,
data: {
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
legends: ['系列1'],
series: [
[12, 19, 5, 2, 18],
],
},
}
是这个元素对元素列表进行循环的
src/views/Editor/Canvas/index.vue
<EditableElement
v-for="(element, index) in elementList"
:key="element.id"
:elementInfo="element"
:elementIndex="index + 1"
:isMultiSelect="activeElementIdList.length > 1"
:selectElement="selectElement"
:openLinkDialog="openLinkDialog"
v-show="!hiddenElementIdList.includes(element.id)"
/>
src/views/Editor/Canvas/EditableElement.vue
这个组件中通过动态组件的方式控制显示哪个元素
<component
:is="currentElementComponent"
:elementInfo="elementInfo"
:selectElement="selectElement"
:contextmenus="contextmenus"
></component>
const currentElementComponent = computed<unknown>(() => {
const elementTypeMap = {
[ElementTypes.IMAGE]: ImageElement,
[ElementTypes.TEXT]: TextElement,
[ElementTypes.SHAPE]: ShapeElement,
[ElementTypes.LINE]: LineElement,
[ElementTypes.CHART]: ChartElement,
[ElementTypes.TABLE]: TableElement,
[ElementTypes.LATEX]: LatexElement,
[ElementTypes.VIDEO]: VideoElement,
[ElementTypes.AUDIO]: AudioElement,
}
return elementTypeMap[props.elementInfo.type] || null
})
我们的目标就是 ChartElement
:src/views/components/element/ChartElement/index.vue
然后图表那一小块是这个:src/views/components/element/ChartElement/Chart.vue,图表是通过 chartist
库实现的
import { BarChart, LineChart, PieChart } from 'chartist'