pipipi-pikachu il y a 5 ans
Parent
commit
bfb24e5055
33 fichiers modifiés avec 649 ajouts et 555 suppressions
  1. 17 0
      src/types/edit.ts
  2. 4 12
      src/views/Editor/Canvas/AlignmentLine.vue
  3. 0 0
      src/views/Editor/Canvas/Operate/AnimationIndex.vue
  4. 0 0
      src/views/Editor/Canvas/Operate/BorderLine.vue
  5. 4 20
      src/views/_common/_element/ImageElement/ImageClipHandler.vue
  6. 169 0
      src/views/Editor/Canvas/Operate/ImageElementOperate.vue
  7. 6 7
      src/views/Editor/Canvas/MultiSelectOperate.vue
  8. 0 0
      src/views/Editor/Canvas/Operate/ResizeHandler.vue
  9. 22 0
      src/views/Editor/Canvas/Operate/RotateHandler.vue
  10. 131 0
      src/views/Editor/Canvas/Operate/TextElementOperate.vue
  11. 97 0
      src/views/Editor/Canvas/Operate/index.vue
  12. 8 0
      src/views/_common/_element/hooks/useCommonOperate.ts
  13. 6 6
      src/views/Editor/Canvas/hooks/useDragElement.ts
  14. 2 2
      src/views/Editor/Canvas/hooks/useScaleElement.ts
  15. 39 25
      src/views/Editor/Canvas/index.vue
  16. 1 1
      src/views/Editor/Thumbnails/index.vue
  17. 1 1
      src/views/Screen/ScreenSlide.vue
  18. 2 2
      src/views/Screen/index.vue
  19. 1 1
      src/views/_common/ThumbnailSlide.vue
  20. 0 223
      src/views/_common/_element/TextElement/index.vue
  21. 0 34
      src/views/_common/_operate/RotateHandler.vue
  22. 0 0
      src/views/_element/BaseElement.vue
  23. 2 40
      src/views/_common/_element/EditableElement.vue
  24. 1 1
      src/views/_common/_element/ElementOutline.vue
  25. 1 1
      src/views/_common/_element/ImageElement/BaseImageElement.vue
  26. 1 1
      src/views/_common/_element/ImageElement/ImageEllipseOutline.vue
  27. 1 1
      src/views/_common/_element/ImageElement/ImagePolygonOutline.vue
  28. 1 1
      src/views/_common/_element/ImageElement/ImageRectOutline.vue
  29. 19 174
      src/views/_common/_element/ImageElement/index.vue
  30. 2 2
      src/views/_common/_element/TextElement/BaseTextElement.vue
  31. 111 0
      src/views/_element/TextElement/index.vue
  32. 0 0
      src/views/_element/hooks/useElementOutline.ts
  33. 0 0
      src/views/_element/hooks/useElementShadow.ts

+ 17 - 0
src/types/edit.ts

@@ -63,4 +63,21 @@ export interface MultiSelectRange {
   maxX: number;
   minY: number;
   maxY: number;
+}
+
+export type ImageClipDataRange = [[number, number], [number, number]]
+
+export interface ImageClipData {
+  range: ImageClipDataRange;
+  path: string;
+}
+
+export interface ImageClipedEmitData {
+  range: ImageClipDataRange;
+  position: {
+    left: number;
+    top: number;
+    width: number;
+    height: number;
+  };
 }

+ 4 - 12
src/views/Editor/Canvas/AlignmentLine.vue

@@ -25,21 +25,13 @@ export default defineComponent({
       type: Number,
       required: true,
     },
-    offsetX: {
-      type: Number,
-      required: true,
-    },
-    offsetY: {
-      type: Number,
-      required: true,
-    },
   },
   setup(props) {
     const store = useStore<State>()
     const canvasScale = computed(() => store.state.canvasScale)
 
-    const left = computed(() => props.axis.x * canvasScale.value + props.offsetX + 'px')
-    const top = computed(() => props.axis.y * canvasScale.value + props.offsetY + 'px')
+    const left = computed(() => props.axis.x * canvasScale.value + 'px')
+    const top = computed(() => props.axis.y * canvasScale.value + 'px')
 
     const sizeStyle = computed(() => {
       if(props.type === 'vertical') return { height: props.length * canvasScale.value + 'px' }
@@ -66,11 +58,11 @@ export default defineComponent({
     border: 0 dashed $themeColor;
 
     &.vertical {
-      margin-left: -0.5px;
+      transform: translateY(-0.5px);
       border-left-width: 1px;
     }
     &.horizontal {
-      margin-top: -0.5px;
+      transform: translateX(-0.5px);
       border-top-width: 1px;
     }
   }

src/views/_common/_operate/AnimationIndex.vue → src/views/Editor/Canvas/Operate/AnimationIndex.vue


src/views/_common/_operate/BorderLine.vue → src/views/Editor/Canvas/Operate/BorderLine.vue


+ 4 - 20
src/views/_common/_element/ImageElement/ImageClipHandler.vue

@@ -50,26 +50,10 @@
 <script lang="ts">
 import { computed, defineComponent, onMounted, onUnmounted, PropType, reactive, ref } from 'vue'
 import { KEYS } from '@/configs/hotkey'
+import { ImageClipData, ImageClipDataRange, ImageClipedEmitData } from '@/types/edit'
 
 import SvgWrapper from '@/components/SvgWrapper.vue'
 
-type ClipDataRange = [[number, number], [number, number]]
-
-interface ClipData {
-  range: ClipDataRange;
-  path: string;
-}
-
-export interface ClipedEmitData {
-  range: ClipDataRange;
-  position: {
-    left: number;
-    top: number;
-    width: number;
-    height: number;
-  };
-}
-
 type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
 
 export default defineComponent({
@@ -83,7 +67,7 @@ export default defineComponent({
       required: true,
     },
     clipData: {
-      type: Object as PropType<ClipData>,
+      type: Object as PropType<ImageClipData>,
       required: true,
     },
     clipPath: {
@@ -123,7 +107,7 @@ export default defineComponent({
       left: '0',
     })
     const isSettingClipRange = ref(false)
-    const currentRange = ref<ClipDataRange | null>(null)
+    const currentRange = ref<ImageClipDataRange | null>(null)
 
     const getClipDataTransformInfo = () => {
       const [start, end] = props.clipData ? props.clipData.range : [[0, 0], [100, 100]]
@@ -209,7 +193,7 @@ export default defineComponent({
         height: (topImgWrapperPosition.height - 100) / 100 * props.height,
       }
 
-      const clipedEmitData: ClipedEmitData = {
+      const clipedEmitData: ImageClipedEmitData = {
         range: currentRange.value,
         position,
       }

+ 169 - 0
src/views/Editor/Canvas/Operate/ImageElementOperate.vue

@@ -0,0 +1,169 @@
+<template>
+  <ImageClipHandler
+    v-if="isCliping"
+    :src="elementInfo.src"
+    :clipData="elementInfo.clip"
+    :canvasScale="canvasScale"
+    :width="elementInfo.width"
+    :height="elementInfo.height"
+    :top="elementInfo.top"
+    :left="elementInfo.left"
+    :clipPath="clipShape.style"
+    @clip="range => clip(range)"
+  />
+  <div 
+    class="image-element-operate" 
+    v-else
+    :class="{
+      'selected': isSelected,
+      'multi-select': isMultiSelect && isSelected,
+      'active': isActive,
+    }"
+  >
+    <BorderLine 
+      class="operate-border-line"
+      v-for="line in borderLines" 
+      :key="line.type" 
+      :type="line.type" 
+      :style="line.style"
+    />
+    <template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
+      <ResizeHandler
+        class="operate-resize-handler" 
+        v-for="point in resizeHandlers"
+        :key="point.direction"
+        :type="point.direction"
+        :style="point.style"
+        @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
+      />
+      <RotateHandler
+        class="operate-rotate-handler" 
+        :style="{ left: scaleWidth / 2 + 'px' }"
+        @mousedown.stop="rotateElement(elementInfo)"
+      />
+    </template>
+
+    <AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType, ref } from 'vue'
+import { useStore } from 'vuex'
+import { State } from '@/store'
+
+import { PPTImageElement } from '@/types/slides'
+import { OperateResizeHandler, ImageClipedEmitData } from '@/types/edit'
+import useCommonOperate from '../hooks/useCommonOperate'
+
+import RotateHandler from './RotateHandler.vue'
+import ResizeHandler from './ResizeHandler.vue'
+import BorderLine from './BorderLine.vue'
+import AnimationIndex from './AnimationIndex.vue'
+import ImageClipHandler from './ImageClipHandler.vue'
+
+export default defineComponent({
+  name: 'image-element-operate',
+  components: {
+    RotateHandler,
+    ResizeHandler,
+    BorderLine,
+    AnimationIndex,
+    ImageClipHandler,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTImageElement>,
+      required: true,
+    },
+    isSelected: {
+      type: Boolean,
+      required: true,
+    },
+    isActive: {
+      type: Boolean,
+      required: true,
+    },
+    isActiveGroupElement: {
+      type: Boolean,
+      required: true,
+    },
+    isMultiSelect: {
+      type: Boolean,
+      required: true,
+    },
+    animationIndex: {
+      type: Number,
+      required: true,
+    },
+    rotateElement: {
+      type: Function as PropType<(element: PPTImageElement) => void>,
+      required: true,
+    },
+    scaleElement: {
+      type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: OperateResizeHandler) => void>,
+      required: true,
+    },
+  },
+  setup(props) {
+    const store = useStore<State>()
+    const canvasScale = computed(() => store.state.canvasScale)
+
+    const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
+    const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
+    const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
+
+    const clipingImageElId = ref('')
+
+    const isCliping = computed(() => clipingImageElId.value === props.elementInfo.id)
+
+    const clip = (data: ImageClipedEmitData) => {
+      clipingImageElId.value = ''
+      
+      if(!data) return
+
+      const { range, position } = data
+      const originClip = props.elementInfo.clip || {}
+      
+      const _props = {
+        clip: { ...originClip, range },
+        left: props.elementInfo.left + position.left,
+        top: props.elementInfo.top + position.top,
+        width: props.elementInfo.width + position.width,
+        height: props.elementInfo.height + position.height,
+      }
+      console.log(_props)
+    }
+
+    return {
+      scaleWidth,
+      resizeHandlers,
+      borderLines,
+      isCliping,
+      clip,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.image-element-operate {
+  &.selected {
+    .operate-border-line,
+    .operate-resize-handler,
+    .operate-rotate-handler {
+      display: block;
+    }
+  }
+
+  &.multi-select:not(.selected) .operate-border-line {
+    border-color: rgba($color: $themeColor, $alpha: .3);
+  }
+
+  .operate-border-line,
+  .operate-resize-handler,
+  .operate-rotate-handler {
+    display: none;
+  }
+}
+</style>

+ 6 - 7
src/views/Editor/Canvas/MultiSelectOperate.vue

@@ -2,9 +2,8 @@
   <div 
     class="multi-select-operate"
     :style="{
-      left: minX + 'px',
-      top: minY + 'px',
-      transform: `scale(${1 / canvasScale})`,
+      left: minX * canvasScale + 'px',
+      top: minY * canvasScale + 'px',
     }"
   >
     <BorderLine v-for="line in borderLines" :key="line.type" :type="line.type" :style="line.style" />
@@ -28,10 +27,10 @@ import { State } from '@/store'
 import { PPTElement, ElementTypes } from '@/types/slides'
 import { getElementListRange } from '@/utils/element'
 import { OperateResizeHandler, MultiSelectRange } from '@/types/edit'
-import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
+import useCommonOperate from '../hooks/useCommonOperate'
 
-import ResizeHandler from '@/views/_common/_operate/ResizeHandler.vue'
-import BorderLine from '@/views/_common/_operate/BorderLine.vue'
+import ResizeHandler from './ResizeHandler.vue'
+import BorderLine from './BorderLine.vue'
 
 export default defineComponent({
   name: 'multi-select-operate',
@@ -102,6 +101,6 @@ export default defineComponent({
   position: absolute;
   top: 0;
   left: 0;
-  z-index: 100;
+  z-index: 101;
 }
 </style>

src/views/_common/_operate/ResizeHandler.vue → src/views/Editor/Canvas/Operate/ResizeHandler.vue


+ 22 - 0
src/views/Editor/Canvas/Operate/RotateHandler.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="rotate-handler"></div>
+</template>
+
+<script>
+export default {
+  name: 'rotate-handler',
+}
+</script>
+
+<style lang="scss" scoped>
+.rotate-handler {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  top: -25px;
+  margin-left: -5px;
+  border: 1px solid $themeColor;
+  background-color: #fff;
+  border-radius: 1px;
+}
+</style>

+ 131 - 0
src/views/Editor/Canvas/Operate/TextElementOperate.vue

@@ -0,0 +1,131 @@
+<template>
+  <div 
+    class="text-element-operate" 
+    :class="{
+      'selected': isSelected,
+      'multi-select': isMultiSelect && isSelected,
+      'active': isActive,
+    }"
+  >
+    <BorderLine 
+      class="operate-border-line"
+      v-for="line in borderLines" 
+      :key="line.type" 
+      :type="line.type" 
+      :style="line.style"
+    />
+    <template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
+      <ResizeHandler
+        class="operate-resize-handler" 
+        v-for="point in textElementResizeHandlers"
+        :key="point.direction"
+        :type="point.direction"
+        :style="point.style"
+        @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
+      />
+      <RotateHandler
+        class="operate-rotate-handler" 
+        :style="{ left: scaleWidth / 2 + 'px' }"
+        @mousedown.stop="rotateElement(elementInfo)"
+      />
+    </template>
+
+    <AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue'
+import { useStore } from 'vuex'
+import { State } from '@/store'
+
+import { PPTTextElement } from '@/types/slides'
+import { OperateResizeHandler } from '@/types/edit'
+import useCommonOperate from '../hooks/useCommonOperate'
+
+import RotateHandler from './RotateHandler.vue'
+import ResizeHandler from './ResizeHandler.vue'
+import BorderLine from './BorderLine.vue'
+import AnimationIndex from './AnimationIndex.vue'
+
+export default defineComponent({
+  name: 'text-element-operate',
+  components: {
+    RotateHandler,
+    ResizeHandler,
+    BorderLine,
+    AnimationIndex,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTTextElement>,
+      required: true,
+    },
+    isSelected: {
+      type: Boolean,
+      required: true,
+    },
+    isActive: {
+      type: Boolean,
+      required: true,
+    },
+    isActiveGroupElement: {
+      type: Boolean,
+      required: true,
+    },
+    isMultiSelect: {
+      type: Boolean,
+      required: true,
+    },
+    animationIndex: {
+      type: Number,
+      required: true,
+    },
+    rotateElement: {
+      type: Function as PropType<(element: PPTTextElement) => void>,
+      required: true,
+    },
+    scaleElement: {
+      type: Function as PropType<(e: MouseEvent, element: PPTTextElement, command: OperateResizeHandler) => void>,
+      required: true,
+    },
+  },
+  setup(props) {
+    const store = useStore<State>()
+    const canvasScale = computed(() => store.state.canvasScale)
+
+    const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
+    const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
+
+    const { textElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
+
+    return {
+      scaleWidth,
+      textElementResizeHandlers,
+      borderLines,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.text-element-operate {
+  &.selected {
+    .operate-border-line,
+    .operate-resize-handler,
+    .operate-rotate-handler {
+      display: block;
+    }
+  }
+
+  &.multi-select:not(.selected) .operate-border-line {
+    border-color: rgba($color: $themeColor, $alpha: .3);
+  }
+
+  .operate-border-line,
+  .operate-resize-handler,
+  .operate-rotate-handler {
+    display: none;
+  }
+}
+</style>

+ 97 - 0
src/views/Editor/Canvas/Operate/index.vue

@@ -0,0 +1,97 @@
+<template>
+  <div
+    class="operate"
+    :style="{
+      top: elementInfo.top * canvasScale + 'px',
+      left: elementInfo.left * canvasScale + 'px',
+      transform: `rotate(${elementInfo.rotate}deg)`,
+      'transform-origin': `${elementInfo.width * canvasScale / 2}px ${elementInfo.height * canvasScale / 2}px`,
+    }"
+  >
+    <component
+      :is="currentOperateComponent"
+      :elementInfo="elementInfo"
+      :isSelected="isSelected"
+      :isActive="isActive"
+      :isActiveGroupElement="isActiveGroupElement"
+      :isMultiSelect="isMultiSelect"
+      :animationIndex="animationIndex"
+      :rotateElement="rotateElement"
+      :scaleElement="scaleElement"
+    ></component>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType, computed } from 'vue'
+import { PPTElement } from '@/types/slides'
+import { OperateResizeHandler } from '@/types/edit'
+
+import ImageElementOperate from './ImageElementOperate.vue'
+import TextElementOperate from './TextElementOperate.vue'
+import { useStore } from 'vuex'
+import { State } from '@/store'
+
+export default defineComponent({
+  name: 'operate',
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTElement>,
+      required: true,
+    },
+    isSelected: {
+      type: Boolean,
+      required: true,
+    },
+    isActive: {
+      type: Boolean,
+      required: true,
+    },
+    isActiveGroupElement: {
+      type: Boolean,
+      required: true,
+    },
+    isMultiSelect: {
+      type: Boolean,
+      required: true,
+    },
+    animationIndex: {
+      type: Number,
+      default: -1,
+    },
+    rotateElement: {
+      type: Function as PropType<(element: PPTElement) => void>,
+      required: true,
+    },
+    scaleElement: {
+      type: Function as PropType<(e: MouseEvent, element: PPTElement, command: OperateResizeHandler) => void>,
+      required: true,
+    },
+  },
+  setup(props) {
+    const store = useStore<State>()
+    const canvasScale = computed(() => store.state.canvasScale)
+
+    const currentOperateComponent = computed(() => {
+      const elementTypeMap = {
+        'image': ImageElementOperate,
+        'text': TextElementOperate,
+      }
+      return elementTypeMap[props.elementInfo.type] || null
+    })
+
+    return {
+      currentOperateComponent,
+      canvasScale,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.operate {
+  position: absolute;
+  z-index: 100;
+  user-select: none;
+}
+</style>

+ 8 - 0
src/views/_common/_element/hooks/useCommonOperate.ts

@@ -13,6 +13,13 @@ export default (width: Ref<number>, height: Ref<number>) => {
       { direction: OperateResizeHandlers.BOTTOM, style: {left: width.value / 2 + 'px', top: height.value + 'px'} },
       { direction: OperateResizeHandlers.RIGHT_BOTTOM, style: {left: width.value + 'px', top: height.value + 'px'} },
     ]
+  }
+  )
+  const textElementResizeHandlers = computed(() => {
+    return [
+      { direction: OperateResizeHandlers.LEFT, style: {top: height.value / 2 + 'px'} },
+      { direction: OperateResizeHandlers.RIGHT, style: {left: width.value + 'px', top: height.value / 2 + 'px'} },
+    ]
   })
 
   const borderLines = computed(() => {
@@ -26,6 +33,7 @@ export default (width: Ref<number>, height: Ref<number>) => {
 
   return {
     resizeHandlers,
+    textElementResizeHandlers,
     borderLines,
   }
 }

+ 6 - 6
src/views/Editor/Canvas/hooks/useDragElement.ts

@@ -216,21 +216,21 @@ export default (
             targetTop = targetTop - (targetMinY - value)
             isHorizontalAdsorbed = true
           }
-          _alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
+          _alignmentLines.push({type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100})
         }
         if(Math.abs(targetMaxY - value) < sorptionRange) {
           if(!isHorizontalAdsorbed) {
             targetTop = targetTop - (targetMaxY - value)
             isHorizontalAdsorbed = true
           }
-          _alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
+          _alignmentLines.push({type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100})
         }
         if(Math.abs(targetCenterY - value) < sorptionRange) {
           if(!isHorizontalAdsorbed) {
             targetTop = targetTop - (targetCenterY - value)
             isHorizontalAdsorbed = true
           }
-          _alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
+          _alignmentLines.push({type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100})
         }
       }
       for(let i = 0; i < verticalLines.length; i++) {
@@ -243,21 +243,21 @@ export default (
             targetLeft = targetLeft - (targetMinX - value)
             isVerticalAdsorbed = true
           }
-          _alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
+          _alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100})
         }
         if(Math.abs(targetMaxX - value) < sorptionRange) {
           if(!isVerticalAdsorbed) {
             targetLeft = targetLeft - (targetMaxX - value)
             isVerticalAdsorbed = true
           }
-          _alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
+          _alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100})
         }
         if(Math.abs(targetCenterX - value) < sorptionRange) {
           if(!isVerticalAdsorbed) {
             targetLeft = targetLeft - (targetCenterX - value)
             isVerticalAdsorbed = true
           }
-          _alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
+          _alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100})
         }
       }
       alignmentLines.value = _alignmentLines

+ 2 - 2
src/views/Editor/Canvas/hooks/useScaleElement.ts

@@ -192,7 +192,7 @@ export default (
               correctionVal.offsetY = currentY - value
               isHorizontalAdsorbed = true
             }
-            _alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
+            _alignmentLines.push({ type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100 })
           }
         }
       }
@@ -207,7 +207,7 @@ export default (
               correctionVal.offsetX = currentX - value
               isVerticalAdsorbed = true
             }
-            _alignmentLines.push({ type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40 })
+            _alignmentLines.push({ type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100 })
           }
         }
       }

+ 39 - 25
src/views/Editor/Canvas/index.vue

@@ -7,15 +7,37 @@
     v-contextmenu="contextmenus"
     v-click-outside="removeEditorAreaFocus"
   >
-    <AlignmentLine 
-      v-for="(line, index) in alignmentLines" 
-      :key="index" 
-      :type="line.type" 
-      :axis="line.axis" 
-      :length="line.length"
-      :offsetX="viewportStyles.left"
-      :offsetY="viewportStyles.top"
-    />
+    <div 
+      class="operates"
+      :style="{
+        left: viewportStyles.left + 'px',
+        top: viewportStyles.top + 'px',
+      }"
+    >
+      <AlignmentLine 
+        v-for="(line, index) in alignmentLines" 
+        :key="index" 
+        :type="line.type" 
+        :axis="line.axis" 
+        :length="line.length"
+      />
+      <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)"
+        :isActiveGroupElement="activeGroupElementId === element.id"
+        :isMultiSelect="activeElementIdList.length > 1"
+        :rotateElement="rotateElement"
+        :scaleElement="scaleElement"
+      />
+    </div>
+
     <div 
       class="viewport" 
       ref="viewportRef"
@@ -35,27 +57,14 @@
         :height="mouseSelectionState.height" 
         :quadrant="mouseSelectionState.quadrant"
       />
-
-      <MultiSelectOperate 
-        v-if="activeElementIdList.length > 1"
-        :elementList="elementList"
-        :scaleMultiElement="scaleMultiElement"
-      />
-
       <SlideBackground />
-
       <EditableElement 
         v-for="(element, index) in elementList" 
         :key="element.id"
         :elementInfo="element"
         :elementIndex="index + 1"
-        :isSelected="activeElementIdList.includes(element.id)"
-        :isActive="element.id === handleElementId"
-        :isActiveGroupElement="activeGroupElementId === element.id"
         :isMultiSelect="activeElementIdList.length > 1"
         :selectElement="selectElement"
-        :rotateElement="rotateElement"
-        :scaleElement="scaleElement"
       />
     </div>
   </div>
@@ -83,11 +92,12 @@ import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
 import useSelectAllElement from '@/hooks/useSelectAllElement'
 import useScaleCanvas from '@/hooks/useScaleCanvas'
 
-import EditableElement from '@/views/_common/_element/EditableElement.vue'
+import EditableElement from '@/views/_element/EditableElement.vue'
 import MouseSelection from './MouseSelection.vue'
 import SlideBackground from './SlideBackground.vue'
-import MultiSelectOperate from './MultiSelectOperate.vue'
 import AlignmentLine from './AlignmentLine.vue'
+import MultiSelectOperate from './Operate/MultiSelectOperate.vue'
+import Operate from './Operate/index.vue'
 
 export default defineComponent({
   name: 'editor-canvas',
@@ -95,8 +105,9 @@ export default defineComponent({
     EditableElement,
     MouseSelection,
     SlideBackground,
-    MultiSelectOperate,
     AlignmentLine,
+    MultiSelectOperate,
+    Operate,
   },
   setup() {
     const store = useStore<State>()
@@ -218,4 +229,7 @@ export default defineComponent({
   background-color: #fff;
   box-shadow: 0 0 20px 0 rgba(0, 0, 0, .1);
 }
+.operates {
+  position: absolute;
+}
 </style>

+ 1 - 1
src/views/Editor/Thumbnails/index.vue

@@ -42,7 +42,7 @@ import { fillDigit } from '@/utils/common'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
 import useSlideHandler from '@/hooks/useSlideHandler'
 
-import ThumbnailSlide from '@/views/_common/ThumbnailSlide.vue'
+import ThumbnailSlide from '@/views/ThumbnailSlide.vue'
 
 export default defineComponent({
   name: 'thumbnails',

+ 1 - 1
src/views/Screen/ScreenSlide.vue

@@ -23,7 +23,7 @@ import { Slide } from '@/types/slides'
 import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
 import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
 
-import BaseElement from '@/views/_common/_element/BaseElement.vue'
+import BaseElement from '@/views/_element/BaseElement.vue'
 
 export default defineComponent({
   name: 'screen-slide',

+ 2 - 2
src/views/Screen/index.vue

@@ -172,10 +172,10 @@ export default defineComponent({
     z-index: 2;
   }
   &.prev {
-    transform: translateX(-100%);
+    transform: translateY(-100%);
   }
   &.next {
-    transform: translateX(100%);
+    transform: translateY(100%);
   }
 }
 .slide-content {

+ 1 - 1
src/views/_common/ThumbnailSlide.vue

@@ -30,7 +30,7 @@ import { Slide } from '@/types/slides'
 import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
 import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
 
-import BaseElement from '@/views/_common/_element/BaseElement.vue'
+import BaseElement from '@/views/_element/BaseElement.vue'
 
 export default defineComponent({
   name: 'thumbnail-slide',

+ 0 - 223
src/views/_common/_element/TextElement/index.vue

@@ -1,223 +0,0 @@
-<template>
-  <div 
-    class="editable-element-text" 
-    :class="{ 'lock': elementInfo.lock }"
-    :style="{
-      top: elementInfo.top + 'px',
-      left: elementInfo.left + 'px',
-      width: elementInfo.width + 'px',
-      transform: `rotate(${elementInfo.rotate}deg)`,
-    }"
-    @mousedown="$event => handleSelectElement($event)"
-  >
-    <div class="element-content"
-      :style="{
-        backgroundColor: elementInfo.fill,
-        opacity: elementInfo.opacity,
-        textShadow: shadowStyle,
-      }"
-      v-contextmenu="contextmenus"
-    >
-      <ElementOutline
-        :width="elementInfo.width"
-        :height="elementInfo.height"
-        :outline="elementInfo.outline"
-      />
-      <div class="text"
-        v-html="elementInfo.content" 
-        :contenteditable="isActive && !elementInfo.lock"
-        @mousedown="$event => handleSelectElement($event, false)"
-      ></div>
-    </div>
-
-    <div 
-      class="operate" 
-      :class="{
-        'selected': isSelected,
-        'multi-select': isMultiSelect && isSelected,
-        'active': isActive,
-      }" 
-      :style="{ transform: `scale(${1 / canvasScale})` }"
-    >
-      <BorderLine 
-        class="operate-border-line"
-        v-for="line in borderLines" 
-        :key="line.type" 
-        :type="line.type" 
-        :style="line.style"
-      />
-      <template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
-        <ResizeHandler
-          class="operate-resize-handler" 
-          v-for="point in resizeHandlers"
-          :key="point.direction"
-          :type="point.direction"
-          :style="point.style"
-          @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
-        />
-        <RotateHandler
-          class="operate-rotate-handler" 
-          :style="{ left: scaleWidth / 2 + 'px' }"
-          @mousedown.stop="rotateElement(elementInfo)"
-        />
-      </template>
-
-      <AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
-    </div>
-  </div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue'
-
-import { PPTTextElement } from '@/types/slides'
-import { OperateResizeHandler } from '@/types/edit'
-import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
-
-import ElementOutline from '@/views/_common/_element/ElementOutline.vue'
-import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
-import ResizeHandler from '@/views/_common/_operate/ResizeHandler.vue'
-import BorderLine from '@/views/_common/_operate/BorderLine.vue'
-import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
-
-import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
-
-export default defineComponent({
-  name: 'editable-element-text',
-  components: {
-    ElementOutline,
-    RotateHandler,
-    ResizeHandler,
-    BorderLine,
-    AnimationIndex,
-  },
-  props: {
-    elementInfo: {
-      type: Object as PropType<PPTTextElement>,
-      required: true,
-    },
-    canvasScale: {
-      type: Number,
-      required: true,
-    },
-    isSelected: {
-      type: Boolean,
-      required: true,
-    },
-    isActive: {
-      type: Boolean,
-      required: true,
-    },
-    isActiveGroupElement: {
-      type: Boolean,
-      required: true,
-    },
-    isMultiSelect: {
-      type: Boolean,
-      required: true,
-    },
-    animationIndex: {
-      type: Number,
-      required: true,
-    },
-    selectElement: {
-      type: Function as PropType<(e: MouseEvent, element: PPTTextElement, canMove?: boolean) => void>,
-      required: true,
-    },
-    rotateElement: {
-      type: Function as PropType<(element: PPTTextElement) => void>,
-      required: true,
-    },
-    scaleElement: {
-      type: Function as PropType<(e: MouseEvent, element: PPTTextElement, command: OperateResizeHandler) => void>,
-      required: true,
-    },
-    contextmenus: {
-      type: Function,
-    },
-  },
-  setup(props) {
-    const scaleWidth = computed(() => props.elementInfo.width * props.canvasScale)
-    const scaleHeight = computed(() => props.elementInfo.height * props.canvasScale)
-
-    const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
-
-    const handleSelectElement = (e: MouseEvent, canMove = true) => {
-      if(props.elementInfo.lock) return
-      e.stopPropagation()
-
-      props.selectElement(e, props.elementInfo, canMove)
-    }
-    
-    const shadow = computed(() => props.elementInfo.shadow)
-    const { shadowStyle } = useElementShadow(shadow)
-
-    return {
-      scaleWidth,
-      resizeHandlers,
-      borderLines,
-      handleSelectElement,
-      shadowStyle,
-    }
-  },
-})
-</script>
-
-<style lang="scss" scoped>
-.editable-element-text {
-  position: absolute;
-  cursor: move;
-
-  &.lock .element-content {
-    cursor: default;
-  }
-}
-
-.element-content {
-  position: relative;
-  padding: 10px;
-  line-height: 1.5;
-
-  .text {
-    position: relative;
-    cursor: text;
-  }
-}
-
-::v-deep(.text) {
-  word-break: break-word;
-  font-family: '微软雅黑';
-  outline: 0;
-
-  ::selection {
-    background-color: rgba(27, 110, 232, 0.3);
-    color: inherit;
-  }
-}
-
-.operate {
-  position: absolute;
-  top: 0;
-  left: 0;
-  z-index: 100;
-  user-select: none;
-
-  &.selected {
-    .operate-border-line,
-    .operate-resize-handler,
-    .operate-rotate-handler {
-      display: block;
-    }
-  }
-
-  &.multi-select:not(.selected) .operate-border-line {
-    border-color: rgba($color: $themeColor, $alpha: .3);
-  }
-
-  .operate-border-line,
-  .operate-resize-handler,
-  .operate-rotate-handler {
-    display: none;
-  }
-}
-</style>

+ 0 - 34
src/views/_common/_operate/RotateHandler.vue

@@ -1,34 +0,0 @@
-<template>
-  <div class="rotate-handler">
-    <div class="rotate-icon"><IconFont type="icon-rotate" /></div>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'rotate-handler',
-}
-</script>
-
-<style lang="scss" scoped>
-.rotate-handler {
-  position: absolute;
-  top: -24px;
-  margin: -10px 0 0 -10px;
-  width: 20px;
-  height: 20px;
-  border-radius: 50%;
-  background-color: #fff;
-  box-shadow: 1px 1px 2px #888;
-
-  .rotate-icon {
-    width: 100%;
-    height: 100%;
-    font-size: 12px;
-    color: #666;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-  }
-}
-</style>

src/views/_common/_element/BaseElement.vue → src/views/_element/BaseElement.vue


+ 2 - 40
src/views/_common/_element/EditableElement.vue

@@ -7,16 +7,8 @@
   >
     <component
       :is="currentElementComponent"
-      :canvasScale="canvasScale"
       :elementInfo="elementInfo"
-      :isSelected="isSelected"
-      :isActive="isActive"
-      :isActiveGroupElement="isActiveGroupElement"
-      :isMultiSelect="isMultiSelect"
-      :animationIndex="animationIndex"
       :selectElement="selectElement"
-      :rotateElement="rotateElement"
-      :scaleElement="scaleElement"
       :contextmenus="contextmenus"
     ></component>
   </div>
@@ -24,9 +16,7 @@
 
 <script lang="ts">
 import { computed, defineComponent, PropType } from 'vue'
-import { useStore } from 'vuex'
-import { State } from '@/store'
-import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement, PPTLineElement } from '@/types/slides'
+import { PPTElement } from '@/types/slides'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
 
 import useLockElement from '@/hooks/useLockElement'
@@ -36,7 +26,7 @@ import useOrderElement from '@/hooks/useOrderElement'
 import useAlignElementToCanvas from '@/hooks/useAlignElementToCanvas'
 import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
 
-import { ElementOrderCommands, ElementAlignCommands, OperateResizeHandler } from '@/types/edit'
+import { ElementOrderCommands, ElementAlignCommands } from '@/types/edit'
 
 import ImageElement from './ImageElement/index.vue'
 import TextElement from './TextElement/index.vue'
@@ -52,43 +42,16 @@ export default defineComponent({
       type: Number,
       required: true,
     },
-    isSelected: {
-      type: Boolean,
-      required: true,
-    },
-    isActive: {
-      type: Boolean,
-      required: true,
-    },
-    isActiveGroupElement: {
-      type: Boolean,
-      required: true,
-    },
     isMultiSelect: {
       type: Boolean,
       required: true,
     },
-    animationIndex: {
-      type: Number,
-      default: -1,
-    },
     selectElement: {
       type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove?: boolean) => void>,
       required: true,
     },
-    rotateElement: {
-      type: Function as PropType<(element: PPTTextElement | PPTImageElement | PPTShapeElement) => void>,
-      required: true,
-    },
-    scaleElement: {
-      type: Function as PropType<(e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandler) => void>,
-      required: true,
-    },
   },
   setup(props) {
-    const store = useStore<State>()
-    const canvasScale = computed(() => store.state.canvasScale)
-
     const currentElementComponent = computed(() => {
       const elementTypeMap = {
         'image': ImageElement,
@@ -172,7 +135,6 @@ export default defineComponent({
     }
 
     return {
-      canvasScale,
       currentElementComponent,
       contextmenus,
     }

+ 1 - 1
src/views/_common/_element/ElementOutline.vue

@@ -24,7 +24,7 @@
 import { PropType, defineComponent, toRef } from 'vue'
 import { PPTElementOutline } from '@/types/slides'
 import SvgWrapper from '@/components/SvgWrapper.vue'
-import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
+import useElementOutline from '@/views/_element/hooks/useElementOutline'
 
 export default defineComponent({
   name: 'element-outline', 

+ 1 - 1
src/views/_common/_element/ImageElement/BaseImageElement.vue

@@ -65,7 +65,7 @@ import ImageRectOutline from './ImageRectOutline.vue'
 import ImageEllipseOutline from './ImageEllipseOutline.vue'
 import ImagePolygonOutline from './ImagePolygonOutline.vue'
 
-import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
+import useElementShadow from '@/views/_element/hooks/useElementShadow'
 
 export default defineComponent({
   name: 'base-element-image',

+ 1 - 1
src/views/_common/_element/ImageElement/ImageEllipseOutline.vue

@@ -27,7 +27,7 @@
 import { PropType, defineComponent, toRef } from 'vue'
 import { PPTElementOutline } from '@/types/slides'
 import SvgWrapper from '@/components/SvgWrapper.vue'
-import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
+import useElementOutline from '@/views/_element/hooks/useElementOutline'
 
 export default defineComponent({
   name: 'image-ellipse-outline',

+ 1 - 1
src/views/_common/_element/ImageElement/ImagePolygonOutline.vue

@@ -24,7 +24,7 @@
 import { PropType, defineComponent, toRef } from 'vue'
 import { PPTElementOutline } from '@/types/slides'
 import SvgWrapper from '@/components/SvgWrapper.vue'
-import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
+import useElementOutline from '@/views/_element/hooks/useElementOutline'
 
 export default defineComponent({
   name: 'image-polygon-outline',

+ 1 - 1
src/views/_common/_element/ImageElement/ImageRectOutline.vue

@@ -27,7 +27,7 @@
 import { PropType, defineComponent, toRef } from 'vue'
 import { PPTElementOutline } from '@/types/slides'
 import SvgWrapper from '@/components/SvgWrapper.vue'
-import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
+import useElementOutline from '@/views/_element/hooks/useElementOutline'
 
 export default defineComponent({
   name: 'image-rect-outline',

+ 19 - 174
src/views/_common/_element/ImageElement/index.vue

@@ -11,22 +11,8 @@
     }"
     @mousedown="$event => handleSelectElement($event)" 
   >
-    <ImageClip
-      v-if="isCliping"
-      :src="elementInfo.src"
-      :clipData="elementInfo.clip"
-      :canvasScale="canvasScale"
-      :width="elementInfo.width"
-      :height="elementInfo.height"
-      :top="elementInfo.top"
-      :left="elementInfo.left"
-      :clipPath="clipShape.style"
-      @clip="range => clip(range)"
-    />
-
     <div 
       class="element-content"
-      v-if="!isCliping"
       v-contextmenu="contextmenus"
       :style="{
         filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
@@ -69,74 +55,25 @@
         />
       </div>
     </div>
-
-    <div 
-      class="operate"
-      :class="{
-        'selected': isSelected,
-        'multi-select': isMultiSelect && isSelected,
-        'active': isActive,
-      }" 
-      :style="{ transform: `scale(${1 / canvasScale})` }"
-      v-if="!isCliping"
-    >
-      <BorderLine 
-        class="operate-border-line" 
-        v-for="line in borderLines" 
-        :key="line.type" 
-        :type="line.type" 
-        :style="line.style" 
-      />
-      <template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
-        <ResizeHandler 
-          class="operate-resize-handler" 
-          v-for="point in resizeHandlers"
-          :key="point.direction"
-          :type="point.direction"
-          :style="point.style"
-          @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
-        />
-        <RotateHandler
-          class="operate-rotate-handler" 
-          :style="{left: scaleWidth / 2 + 'px'}"
-          @mousedown.stop="rotateElement(elementInfo)"
-        />
-      </template>
-
-      <AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
-    </div>
   </div>
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, ref, PropType } from 'vue'
+import { computed, defineComponent, PropType } from 'vue'
 
 import { PPTImageElement } from '@/types/slides'
-import { OperateResizeHandler } from '@/types/edit'
-import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
-
+import { ContextmenuItem } from '@/components/Contextmenu/types'
 import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
+import useElementShadow from '@/views/_element/hooks/useElementShadow'
 
-import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
-import ResizeHandler from '@/views/_common/_operate/ResizeHandler.vue'
-import BorderLine from '@/views/_common/_operate/BorderLine.vue'
-import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
-
-import ImageClip, { ClipedEmitData } from './ImageClipHandler.vue'
 import ImageRectOutline from './ImageRectOutline.vue'
 import ImageEllipseOutline from './ImageEllipseOutline.vue'
 import ImagePolygonOutline from './ImagePolygonOutline.vue'
 
-import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
 
 export default defineComponent({
   name: 'editable-element-image',
   components: {
-    RotateHandler,
-    ResizeHandler,
-    BorderLine,
-    AnimationIndex,
-    ImageClip,
     ImageRectOutline,
     ImageEllipseOutline,
     ImagePolygonOutline,
@@ -146,55 +83,29 @@ export default defineComponent({
       type: Object as PropType<PPTImageElement>,
       required: true,
     },
-    canvasScale: {
-      type: Number,
-      required: true,
-    },
-    isSelected: {
-      type: Boolean,
-      required: true,
-    },
-    isActive: {
-      type: Boolean,
-      required: true,
-    },
-    isActiveGroupElement: {
-      type: Boolean,
-      required: true,
-    },
-    isMultiSelect: {
-      type: Boolean,
-      required: true,
-    },
-    animationIndex: {
-      type: Number,
-      required: true,
-    },
     selectElement: {
       type: Function as PropType<(e: MouseEvent, element: PPTImageElement, canMove?: boolean) => void>,
       required: true,
     },
-    rotateElement: {
-      type: Function as PropType<(element: PPTImageElement) => void>,
-      required: true,
-    },
-    scaleElement: {
-      type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: OperateResizeHandler) => void>,
-      required: true,
-    },
     contextmenus: {
-      type: Function,
+      type: Function as PropType<() => ContextmenuItem[]>,
     },
   },
   setup(props) {
-    const clipingImageElId = ref('')
-
-    const scaleWidth = computed(() => props.elementInfo.width * props.canvasScale)
-    const scaleHeight = computed(() => props.elementInfo.height * props.canvasScale)
+    const shadow = computed(() => props.elementInfo.shadow)
+    const { shadowStyle } = useElementShadow(shadow)
 
-    const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
+    const handleSelectElement = (e: MouseEvent) => {
+      if(props.elementInfo.lock) return
+      e.stopPropagation()
+      props.selectElement(e, props.elementInfo)
+    }
+    const clipShape = computed(() => {
+      if(!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
+      const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
 
-    const isCliping = computed(() => clipingImageElId.value === props.elementInfo.id)
+      return CLIPPATHS[shape]
+    })
 
     const imgPosition = computed(() => {
       if(!props.elementInfo || !props.elementInfo.clip) {
@@ -221,13 +132,6 @@ export default defineComponent({
       }
     })
 
-    const clipShape = computed(() => {
-      if(!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
-      const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
-
-      return CLIPPATHS[shape]
-    })
-
     const filter = computed(() => {
       if(!props.elementInfo.filters) return ''
       let filter = ''
@@ -246,45 +150,13 @@ export default defineComponent({
       return ''
     })
 
-    const shadow = computed(() => props.elementInfo.shadow)
-    const { shadowStyle } = useElementShadow(shadow)
-
-    const handleSelectElement = (e: MouseEvent) => {
-      if(isCliping.value || props.elementInfo.lock) return
-      e.stopPropagation()
-      props.selectElement(e, props.elementInfo)
-    }
-
-    const clip = (data: ClipedEmitData) => {
-      clipingImageElId.value = ''
-      
-      if(!data) return
-
-      const { range, position } = data
-      const originClip = props.elementInfo.clip || {}
-      
-      const _props = {
-        clip: { ...originClip, range },
-        left: props.elementInfo.left + position.left,
-        top: props.elementInfo.top + position.top,
-        width: props.elementInfo.width + position.width,
-        height: props.elementInfo.height + position.height,
-      }
-      console.log(_props)
-    }
-
     return {
-      scaleWidth,
-      isCliping,
-      imgPosition,
+      shadowStyle,
+      handleSelectElement,
       clipShape,
-      resizeHandlers,
-      borderLines,
+      imgPosition,
       filter,
       flip,
-      shadowStyle,
-      handleSelectElement,
-      clip,
     }
   },
 })
@@ -311,35 +183,8 @@ export default defineComponent({
     overflow: hidden;
     position: relative;
   }
-
   img {
     position: absolute;
   }
 }
-
-.operate {
-  position: absolute;
-  top: 0;
-  left: 0;
-  z-index: 100;
-  user-select: none;
-
-  &.selected {
-    .operate-border-line,
-    .operate-resize-handler,
-    .operate-rotate-handler {
-      display: block;
-    }
-  }
-
-  &.multi-select:not(.selected) .operate-border-line {
-    border-color: rgba($color: $themeColor, $alpha: .3);
-  }
-
-  .operate-border-line,
-  .operate-resize-handler,
-  .operate-rotate-handler {
-    display: none;
-  }
-}
 </style>

+ 2 - 2
src/views/_common/_element/TextElement/BaseTextElement.vue

@@ -28,9 +28,9 @@
 <script lang="ts">
 import { defineComponent, PropType, computed } from 'vue'
 import { PPTTextElement } from '@/types/slides'
-import ElementOutline from '@/views/_common/_element/ElementOutline.vue'
+import ElementOutline from '@/views/_element/ElementOutline.vue'
 
-import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
+import useElementShadow from '@/views/_element/hooks/useElementShadow'
 
 export default defineComponent({
   name: 'base-element-text',

+ 111 - 0
src/views/_element/TextElement/index.vue

@@ -0,0 +1,111 @@
+<template>
+  <div 
+    class="editable-element-text" 
+    :class="{ 'lock': elementInfo.lock }"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      transform: `rotate(${elementInfo.rotate}deg)`,
+    }"
+    @mousedown="$event => handleSelectElement($event)"
+  >
+    <div class="element-content"
+      :style="{
+        backgroundColor: elementInfo.fill,
+        opacity: elementInfo.opacity,
+        textShadow: shadowStyle,
+      }"
+      v-contextmenu="contextmenus"
+    >
+      <ElementOutline
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :outline="elementInfo.outline"
+      />
+      <div class="text"
+        v-html="elementInfo.content" 
+        :contenteditable="!elementInfo.lock"
+        @mousedown="$event => handleSelectElement($event, false)"
+      ></div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue'
+import { PPTTextElement } from '@/types/slides'
+import { ContextmenuItem } from '@/components/Contextmenu/types'
+import useElementShadow from '@/views/_element/hooks/useElementShadow'
+
+import ElementOutline from '@/views/_element/ElementOutline.vue'
+
+export default defineComponent({
+  name: 'editable-element-text',
+  components: {
+    ElementOutline,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTTextElement>,
+      required: true,
+    },
+    selectElement: {
+      type: Function as PropType<(e: MouseEvent, element: PPTTextElement, canMove?: boolean) => void>,
+      required: true,
+    },
+    contextmenus: {
+      type: Function as PropType<() => ContextmenuItem[]>,
+    },
+  },
+  setup(props) {
+    const handleSelectElement = (e: MouseEvent, canMove = true) => {
+      if(props.elementInfo.lock) return
+      e.stopPropagation()
+
+      props.selectElement(e, props.elementInfo, canMove)
+    }
+    
+    const shadow = computed(() => props.elementInfo.shadow)
+    const { shadowStyle } = useElementShadow(shadow)
+
+    return {
+      handleSelectElement,
+      shadowStyle,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.editable-element-text {
+  position: absolute;
+  cursor: move;
+
+  &.lock .element-content {
+    cursor: default;
+  }
+}
+
+.element-content {
+  position: relative;
+  padding: 10px;
+  line-height: 1.5;
+
+  .text {
+    position: relative;
+    cursor: text;
+  }
+}
+
+::v-deep(.text) {
+  word-break: break-word;
+  font-family: '微软雅黑';
+  outline: 0;
+
+  ::selection {
+    background-color: rgba(27, 110, 232, 0.3);
+    color: inherit;
+  }
+}
+</style>

src/views/_common/_element/hooks/useElementOutline.ts → src/views/_element/hooks/useElementOutline.ts


src/views/_common/_element/hooks/useElementShadow.ts → src/views/_element/hooks/useElementShadow.ts