CatPtain's picture
Upload 339 files
89ce340 verified
raw
history blame
12.1 kB
<template>
<div
class="canvas"
ref="canvasRef"
@wheel="$event => handleMousewheelCanvas($event)"
@mousedown="$event => handleClickBlankArea($event)"
@dblclick="$event => handleDblClick($event)"
v-contextmenu="contextmenus"
v-click-outside="removeEditorAreaFocus"
>
<ElementCreateSelection
v-if="creatingElement"
@created="data => insertElementFromCreateSelection(data)"
/>
<ShapeCreateCanvas
v-if="creatingCustomShape"
@created="data => insertCustomShape(data)"
/>
<div
class="viewport-wrapper"
:style="{
width: viewportStyles.width * canvasScale + 'px',
height: viewportStyles.height * canvasScale + 'px',
left: viewportStyles.left + 'px',
top: viewportStyles.top + 'px',
}"
>
<div class="operates">
<AlignmentLine
v-for="(line, index) in alignmentLines"
:key="index"
:type="line.type"
:axis="line.axis"
:length="line.length"
:canvasScale="canvasScale"
/>
<MultiSelectOperate
v-if="activeElementIdList.length > 1"
:elementList="elementList"
:scaleMultiElement="scaleMultiElement"
/>
<Operate
v-for="element in elementList"
:key="element.id"
:elementInfo="element"
:isSelected="activeElementIdList.includes(element.id)"
:isActive="handleElementId === element.id"
:isActiveGroupElement="activeGroupElementId === element.id"
:isMultiSelect="activeElementIdList.length > 1"
:rotateElement="rotateElement"
:scaleElement="scaleElement"
:openLinkDialog="openLinkDialog"
:dragLineElement="dragLineElement"
:moveShapeKeypoint="moveShapeKeypoint"
v-show="!hiddenElementIdList.includes(element.id)"
/>
<ViewportBackground />
</div>
<div
class="viewport"
ref="viewportRef"
:style="{ transform: `scale(${canvasScale})` }"
>
<MouseSelection
v-if="mouseSelectionVisible"
:top="mouseSelection.top"
:left="mouseSelection.left"
:width="mouseSelection.width"
:height="mouseSelection.height"
:quadrant="mouseSelectionQuadrant"
/>
<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)"
/>
</div>
</div>
<div class="drag-mask" v-if="spaceKeyState"></div>
<Ruler :viewportStyles="viewportStyles" :elementList="elementList" v-if="showRuler" />
<Modal
v-model:visible="linkDialogVisible"
:width="540"
>
<LinkDialog @close="linkDialogVisible = false" />
</Modal>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, provide, ref, watch, watchEffect } from 'vue'
import { throttle } from 'lodash'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
import type { ContextmenuItem } from '@/components/Contextmenu/types'
import type { PPTElement, PPTShapeElement } from '@/types/slides'
import type { AlignmentLineProps, CreateCustomShapeData } from '@/types/edit'
import { injectKeySlideScale } from '@/types/injectKey'
import { removeAllRanges } from '@/utils/selection'
import { KEYS } from '@/configs/hotkey'
import useViewportSize from './hooks/useViewportSize'
import useMouseSelection from './hooks/useMouseSelection'
import useDropImageOrText from './hooks/useDropImageOrText'
import useRotateElement from './hooks/useRotateElement'
import useScaleElement from './hooks/useScaleElement'
import useSelectAndMoveElement from './hooks/useSelectElement'
import useDragElement from './hooks/useDragElement'
import useDragLineElement from './hooks/useDragLineElement'
import useMoveShapeKeypoint from './hooks/useMoveShapeKeypoint'
import useInsertFromCreateSelection from './hooks/useInsertFromCreateSelection'
import useDeleteElement from '@/hooks/useDeleteElement'
import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
import useSelectElement from '@/hooks/useSelectElement'
import useScaleCanvas from '@/hooks/useScaleCanvas'
import useScreening from '@/hooks/useScreening'
import useSlideHandler from '@/hooks/useSlideHandler'
import useCreateElement from '@/hooks/useCreateElement'
import EditableElement from './EditableElement.vue'
import MouseSelection from './MouseSelection.vue'
import ViewportBackground from './ViewportBackground.vue'
import AlignmentLine from './AlignmentLine.vue'
import Ruler from './Ruler.vue'
import ElementCreateSelection from './ElementCreateSelection.vue'
import ShapeCreateCanvas from './ShapeCreateCanvas.vue'
import MultiSelectOperate from './Operate/MultiSelectOperate.vue'
import Operate from './Operate/index.vue'
import LinkDialog from './LinkDialog.vue'
import Modal from '@/components/Modal.vue'
const mainStore = useMainStore()
const {
activeElementIdList,
activeGroupElementId,
handleElementId,
hiddenElementIdList,
editorAreaFocus,
gridLineSize,
showRuler,
creatingElement,
creatingCustomShape,
canvasScale,
textFormatPainter,
} = storeToRefs(mainStore)
const { currentSlide } = storeToRefs(useSlidesStore())
const { ctrlKeyState, spaceKeyState } = storeToRefs(useKeyboardStore())
const viewportRef = ref<HTMLElement>()
const alignmentLines = ref<AlignmentLineProps[]>([])
const linkDialogVisible = ref(false)
const openLinkDialog = () => linkDialogVisible.value = true
watch(handleElementId, () => {
mainStore.setActiveGroupElementId('')
})
const elementList = ref<PPTElement[]>([])
const setLocalElementList = () => {
elementList.value = currentSlide.value ? JSON.parse(JSON.stringify(currentSlide.value.elements)) : []
}
watchEffect(setLocalElementList)
const canvasRef = ref<HTMLElement>()
const { dragViewport, viewportStyles } = useViewportSize(canvasRef)
useDropImageOrText(canvasRef)
const { mouseSelection, mouseSelectionVisible, mouseSelectionQuadrant, updateMouseSelection } = useMouseSelection(elementList, viewportRef)
const { dragElement } = useDragElement(elementList, alignmentLines, canvasScale)
const { dragLineElement } = useDragLineElement(elementList)
const { selectElement } = useSelectAndMoveElement(elementList, dragElement)
const { scaleElement, scaleMultiElement } = useScaleElement(elementList, alignmentLines, canvasScale)
const { rotateElement } = useRotateElement(elementList, viewportRef, canvasScale)
const { moveShapeKeypoint } = useMoveShapeKeypoint(elementList, canvasScale)
const { selectAllElements } = useSelectElement()
const { deleteAllElements } = useDeleteElement()
const { pasteElement } = useCopyAndPasteElement()
const { enterScreeningFromStart } = useScreening()
const { updateSlideIndex } = useSlideHandler()
const { createTextElement, createShapeElement } = useCreateElement()
// 组件渲染时,如果存在元素焦点,需要清除
// 这种情况存在于:有焦点元素的情况下进入了放映模式,再退出时,需要清除原先的焦点(因为可能已经切换了页面)
onMounted(() => {
if (activeElementIdList.value.length) {
nextTick(() => mainStore.setActiveElementIdList([]))
}
})
// 点击画布的空白区域:清空焦点元素、设置画布焦点、清除文字选区、清空格式刷状态
const handleClickBlankArea = (e: MouseEvent) => {
if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
if (!spaceKeyState.value) updateMouseSelection(e)
else dragViewport(e)
if (!editorAreaFocus.value) mainStore.setEditorareaFocus(true)
if (textFormatPainter.value) mainStore.setTextFormatPainter(null)
removeAllRanges()
}
// 双击空白处插入文本
const handleDblClick = (e: MouseEvent) => {
if (activeElementIdList.value.length || creatingElement.value || creatingCustomShape.value) return
if (!viewportRef.value) return
const viewportRect = viewportRef.value.getBoundingClientRect()
const left = (e.pageX - viewportRect.x) / canvasScale.value
const top = (e.pageY - viewportRect.y) / canvasScale.value
createTextElement({
left,
top,
width: 200 / canvasScale.value, // 除以 canvasScale 是为了与点击选区创建的形式保持相同的宽度
height: 0,
})
}
// 画布注销时清空格式刷状态
onUnmounted(() => {
if (textFormatPainter.value) mainStore.setTextFormatPainter(null)
})
// 移除画布编辑区域焦点
const removeEditorAreaFocus = () => {
if (editorAreaFocus.value) mainStore.setEditorareaFocus(false)
}
// 滚动鼠标
const { scaleCanvas } = useScaleCanvas()
const throttleScaleCanvas = throttle(scaleCanvas, 100, { leading: true, trailing: false })
const throttleUpdateSlideIndex = throttle(updateSlideIndex, 300, { leading: true, trailing: false })
const handleMousewheelCanvas = (e: WheelEvent) => {
e.preventDefault()
// 按住Ctrl键时:缩放画布
if (ctrlKeyState.value) {
if (e.deltaY > 0) throttleScaleCanvas('-')
else if (e.deltaY < 0) throttleScaleCanvas('+')
}
// 上下翻页
else {
if (e.deltaY > 0) throttleUpdateSlideIndex(KEYS.DOWN)
else if (e.deltaY < 0) throttleUpdateSlideIndex(KEYS.UP)
}
}
// 开关标尺
const toggleRuler = () => {
mainStore.setRulerState(!showRuler.value)
}
// 在鼠标绘制的范围插入元素
const { insertElementFromCreateSelection, formatCreateSelection } = useInsertFromCreateSelection(viewportRef)
// 插入自定义任意多边形
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)
}
mainStore.setCreatingCustomShapeState(false)
}
const contextmenus = (): ContextmenuItem[] => {
return [
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteElement,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllElements,
},
{
text: '标尺',
subText: showRuler.value ? '√' : '',
handler: toggleRuler,
},
{
text: '网格线',
handler: () => mainStore.setGridLineSize(gridLineSize.value ? 0 : 50),
children: [
{
text: '无',
subText: gridLineSize.value === 0 ? '√' : '',
handler: () => mainStore.setGridLineSize(0),
},
{
text: '小',
subText: gridLineSize.value === 25 ? '√' : '',
handler: () => mainStore.setGridLineSize(25),
},
{
text: '中',
subText: gridLineSize.value === 50 ? '√' : '',
handler: () => mainStore.setGridLineSize(50),
},
{
text: '大',
subText: gridLineSize.value === 100 ? '√' : '',
handler: () => mainStore.setGridLineSize(100),
},
],
},
{
text: '重置当前页',
handler: deleteAllElements,
},
{ divider: true },
{
text: '幻灯片放映',
subText: 'F5',
handler: enterScreeningFromStart,
},
]
}
provide(injectKeySlideScale, canvasScale)
</script>
<style lang="scss" scoped>
.canvas {
height: 100%;
user-select: none;
overflow: hidden;
background-color: $lightGray;
position: relative;
}
.drag-mask {
cursor: grab;
@include absolute-0();
}
.viewport-wrapper {
position: absolute;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.01), 0 0 12px 0 rgba(0, 0, 0, 0.1);
}
.viewport {
position: absolute;
top: 0;
left: 0;
transform-origin: 0 0;
}
</style>