123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- <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,
- isPPTWheelPage
- } = 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)
- /* 这个功能和 enjoyPlayer 冲突,所以禁用掉 */
- //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) => {
- // 按住Ctrl键时:缩放画布
- if (ctrlKeyState.value) {
- e.preventDefault()
- if (e.deltaY > 0) throttleScaleCanvas("-")
- else if (e.deltaY < 0) throttleScaleCanvas("+")
- }
- // 上下翻页
- else {
- // 控制能不能翻页
- if (!isPPTWheelPage.value) return
- e.preventDefault()
- 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>
|