Procházet zdrojové kódy

添加创建元素区域

pipipi-pikachu před 5 roky
rodič
revize
0988cbae90

+ 1 - 0
src/store/constants.ts

@@ -9,6 +9,7 @@ export enum MutationTypes {
   SET_EDITORAREA_FOCUS = 'setEditorAreaFocus',
   SET_DISABLE_HOTKEYS_STATE = 'setDisableHotkeysState',
   SET_GRID_LINES_STATE = 'setGridLinesState',
+  SET_CREATING_ELEMENT_TYPE = 'setCreatingElementType',
   SET_AVAILABLE_FONTS = 'setAvailableFonts',
   SET_TOOLBAR_STATE = 'setToolbarState',
 

+ 2 - 0
src/store/index.ts

@@ -20,6 +20,7 @@ export interface State {
   editorAreaFocus: boolean;
   disableHotkeys: boolean;
   showGridLines: boolean;
+  creatingElementType: string;
   availableFonts: FontName[];
   toolbarState: ToolbarState;
   slides: Slide[];
@@ -40,6 +41,7 @@ const state: State = {
   editorAreaFocus: false,
   disableHotkeys: false,
   showGridLines: false,
+  creatingElementType: '',
   availableFonts: [],
   toolbarState: 'slideStyle',
   slides: slides,

+ 4 - 0
src/store/mutations.ts

@@ -49,6 +49,10 @@ export const mutations: MutationTree<State> = {
     state.showGridLines = show
   },
 
+  [MutationTypes.SET_CREATING_ELEMENT_TYPE](state, type: string) {
+    state.creatingElementType = type
+  },
+
   [MutationTypes.SET_AVAILABLE_FONTS](state) {
     state.availableFonts = FONT_NAMES.filter(font => isSupportFontFamily(font.en))
   },

+ 5 - 0
src/types/edit.ts

@@ -80,4 +80,9 @@ export interface ImageClipedEmitData {
     width: number;
     height: number;
   };
+}
+
+export interface CreateElementSelectionData {
+  start: [number, number];
+  end: [number, number];
 }

+ 205 - 0
src/views/Editor/Canvas/ElementCreateSelection.vue

@@ -0,0 +1,205 @@
+<template>
+  <div 
+    class="element-create-selection"
+    ref="selectionRef"
+    @mousedown.stop="$event => createSelection($event)"
+  >
+    <div :class="['selection', elementType]" v-if="start && end" :style="position">
+
+      <!-- 绘制线条专用 -->
+      <SvgWrapper
+        v-if="elementType === 'line' && lineData"
+        overflow="visible" 
+        :width="lineData.svgWidth"
+        :height="lineData.svgHeight"
+      >
+				<path
+          :d="lineData.path" 
+          stroke="#888" 
+          fill="none" 
+          stroke-width="1" 
+          stroke-linecap 
+          stroke-linejoin 
+          stroke-miterlimit 
+        ></path>
+			</SvgWrapper>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, onMounted, reactive, Ref, ref } from 'vue'
+import { useStore } from 'vuex'
+import { MutationTypes, State } from '@/store'
+
+import SvgWrapper from '@/components/SvgWrapper.vue'
+
+export default defineComponent({
+  name: 'element-create-selection',
+  components: {
+    SvgWrapper,
+  },
+  setup(props, { emit }) {
+    const store = useStore<State>()
+    const ctrlOrShiftKeyActive: Ref<boolean> = computed(() => store.getters.ctrlOrShiftKeyActive)
+    const elementType = computed(() => store.state.creatingElementType)
+
+    const start = ref<[number, number] | null>(null)
+    const end = ref<[number, number] | null>(null)
+
+    const selectionRef = ref<HTMLElement | null>(null)
+    const offset = reactive({
+      x: 0,
+      y: 0,
+    })
+    onMounted(() => {
+      if(!selectionRef.value) return
+      const { x, y } = selectionRef.value.getBoundingClientRect()
+      offset.x = x
+      offset.y = y
+    })
+
+    const createSelection = (e: MouseEvent) => {
+      let isMouseDown = true
+
+      const startPageX = e.pageX
+      const startPageY = e.pageY
+      start.value = [startPageX, startPageY]
+
+      document.onmousemove = e => {
+        if(!isMouseDown) return
+
+        let currentPageX = e.pageX
+        let currentPageY = e.pageY
+
+        if(ctrlOrShiftKeyActive.value) {
+          const moveX = currentPageX - startPageX
+          const moveY = currentPageY - startPageY
+
+          const absX = Math.abs(moveX)
+          const absY = Math.abs(moveY)
+
+          if(elementType.value === 'shape') {
+            // moveX和moveY一正一负
+            const isOpposite = (moveY > 0 && moveX < 0) || (moveY < 0 && moveX > 0)
+
+            if(absX > absY) {
+              currentPageY = isOpposite ? startPageY - moveX : startPageY + moveX
+            }
+            else {
+              currentPageX = isOpposite ? startPageX - moveY : startPageX + moveY
+            }
+          }
+
+          else if(elementType.value === 'line') {
+            if(absX > absY) currentPageY = startPageY
+            else currentPageX = startPageX
+          }
+        }
+
+        end.value = [currentPageX, currentPageY]
+      }
+
+      document.onmouseup = e => {
+        document.onmousemove = null
+        document.onmouseup = null
+        isMouseDown = false
+
+        const endPageX = e.pageX
+        const endPageY = e.pageY
+
+        const minSize = 30
+
+        if(Math.abs(endPageX - startPageX) >= minSize || Math.abs(endPageY - startPageY) >= minSize) {
+          emit('created', {
+            start: start.value,
+            end: end.value,
+          })
+          store.commit(MutationTypes.SET_CREATING_ELEMENT_TYPE, '')
+        }
+      }
+    }
+
+    const lineData = computed(() => {
+      if(!start.value || !end.value || elementType.value !== 'line') return null
+
+      const [_startX, _startY] = start.value
+      const [_endX, _endY] = end.value
+      const minX = Math.min(_startX, _endX)
+      const maxX = Math.max(_startX, _endX)
+      const minY = Math.min(_startY, _endY)
+      const maxY = Math.max(_startY, _endY)
+
+      const svgWidth = maxX - minX >= 24 ? maxX - minX : 24
+      const svgHeight = maxY - minY >= 24 ? maxY - minY : 24
+
+      const startX = _startX === minX ? 0 : maxX - minX
+      const startY = _startY === minY ? 0 : maxY - minY
+      const endX = _endX === minX ? 0 : maxX - minX
+      const endY = _endY === minY ? 0 : maxY - minY
+
+      const path = `M${startX}, ${startY} L${endX}, ${endY}`
+
+      return {
+        svgWidth,
+        svgHeight,
+        startX,
+        startY,
+        endX,
+        endY,
+        path,
+      }
+    })
+
+    const position = computed(() => {
+      if(!start.value || !end.value) return {}
+
+      const [startX, startY] = start.value
+      const [endX, endY] = end.value
+      const minX = Math.min(startX, endX)
+      const maxX = Math.max(startX, endX)
+      const minY = Math.min(startY, endY)
+      const maxY = Math.max(startY, endY)
+
+      const width = maxX - minX
+      const height = maxY - minY
+
+      return {
+        left: minX - offset.x + 'px',
+        top: minY - offset.y + 'px',
+        width: width + 'px',
+        height: height + 'px',
+      }
+    })
+
+    return {
+      selectionRef,
+      start,
+      end,
+      elementType,
+      createSelection,
+      lineData,
+      position,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.element-create-selection {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 2;
+  cursor: crosshair;
+}
+.selection {
+  position: absolute;
+
+  &:not(.line) {
+    border: 1px solid #888;
+  }
+}
+</style>

+ 14 - 1
src/views/Editor/Canvas/index.vue

@@ -7,6 +7,10 @@
     v-contextmenu="contextmenus"
     v-click-outside="removeEditorAreaFocus"
   >
+    <ElementCreateSelection
+      v-if="creatingElementType"
+      @created="data => createElement(data)"
+    />
     <div 
       class="viewport-wrapper"
       :style="{
@@ -77,7 +81,7 @@ import throttle from 'lodash/throttle'
 import { State, MutationTypes } from '@/store'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
 import { PPTElement, Slide } from '@/types/slides'
-import { AlignmentLineProps } from '@/types/edit'
+import { AlignmentLineProps, CreateElementSelectionData } from '@/types/edit'
 
 import useViewportSize from './hooks/useViewportSize'
 import useMouseSelection from './hooks/useMouseSelection'
@@ -97,6 +101,7 @@ import EditableElement from './EditableElement.vue'
 import MouseSelection from './MouseSelection.vue'
 import SlideBackground from './SlideBackground.vue'
 import AlignmentLine from './AlignmentLine.vue'
+import ElementCreateSelection from './ElementCreateSelection.vue'
 import MultiSelectOperate from './Operate/MultiSelectOperate.vue'
 import Operate from './Operate/index.vue'
 
@@ -107,6 +112,7 @@ export default defineComponent({
     MouseSelection,
     SlideBackground,
     AlignmentLine,
+    ElementCreateSelection,
     MultiSelectOperate,
     Operate,
   },
@@ -177,6 +183,11 @@ export default defineComponent({
       store.commit(MutationTypes.SET_GRID_LINES_STATE, !showGridLines.value)
     }
 
+    const creatingElementType = computed(() => store.state.creatingElementType)
+    const createElement = (data: CreateElementSelectionData) => {
+      console.log(data)
+    }
+
     const contextmenus = (): ContextmenuItem[] => {
       return [
         {
@@ -217,6 +228,8 @@ export default defineComponent({
       removeEditorAreaFocus,
       currentSlide,
       isShowGridLines,
+      creatingElementType,
+      createElement,
       alignmentLines,
       selectElement,
       rotateElement,

+ 9 - 4
src/views/Editor/CanvasTool/index.vue

@@ -6,12 +6,12 @@
     </div>
 
     <div class="add-element-handler">
-      <IconFont class="handler-item" type="icon-font-size" />
+      <IconFont class="handler-item" type="icon-font-size" @click="createElement('text')" />
       <UploadInput @change="files => insertImageElement(files)">
         <IconFont class="handler-item" type="icon-image" />
       </UploadInput>
-      <IconFont class="handler-item" type="icon-star" />
-      <IconFont class="handler-item" type="icon-line" />
+      <IconFont class="handler-item" type="icon-star" @click="createElement('shape')" />
+      <IconFont class="handler-item" type="icon-line" @click="createElement('line')" />
       <IconFont class="handler-item" type="icon-table" />
       <IconFont class="handler-item" type="icon-piechart" />
     </div>
@@ -27,7 +27,7 @@
 <script lang="ts">
 import { defineComponent, computed } from 'vue'
 import { useStore } from 'vuex'
-import { State } from '@/store'
+import { MutationTypes, State } from '@/store'
 import { getImageDataURL } from '@/utils/image'
 import useScaleCanvas from '@/hooks/useScaleCanvas'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
@@ -59,6 +59,10 @@ export default defineComponent({
       getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
     }
 
+    const createElement = (type: string) => {
+      store.commit(MutationTypes.SET_CREATING_ELEMENT_TYPE, type)
+    }
+
     return {
       scaleCanvas,
       canvasScalePercentage,
@@ -67,6 +71,7 @@ export default defineComponent({
       redo,
       undo,
       insertImageElement,
+      createElement,
     }
   },
 })

+ 4 - 3
src/views/Editor/useHotkey.ts

@@ -97,13 +97,14 @@ export default () => {
     const { ctrlKey, shiftKey } = e
     const key = e.key.toUpperCase()
 
+    if(ctrlKey && !ctrlKeyActive.value) store.commit(MutationTypes.SET_CTRL_KEY_STATE, true)
+    if(shiftKey && !shiftKeyActive.value) store.commit(MutationTypes.SET_SHIFT_KEY_STATE, true)
+
     if(ctrlKey && key === KEYS.F) {
       e.preventDefault()
       enterScreening()
+      store.commit(MutationTypes.SET_CTRL_KEY_STATE, false)
     }
-
-    if(ctrlKey && !ctrlKeyActive.value) store.commit(MutationTypes.SET_CTRL_KEY_STATE, true)
-    if(shiftKey && !shiftKeyActive.value) store.commit(MutationTypes.SET_SHIFT_KEY_STATE, true)
     
     if(!editorAreaFocus.value && !thumbnailsFocus.value) return