pipipi-pikachu 5 jaren geleden
bovenliggende
commit
d57cdafa5d

+ 1 - 1
src/components/Contextmenu/ContextmenuContent.vue

@@ -101,7 +101,7 @@ $subMenuWidth: 120px;
 }
 .contextmenu-item {
   padding: 0 20px;
-  color: #666;
+  color: #555;
   font-size: 12px;
   transition: all 0.3s;
   white-space: nowrap;

+ 10 - 0
src/configs/imageClip.ts

@@ -4,6 +4,16 @@ export enum ClipPathTypes {
   POLYGON = 'polygon',
 }
 
+export enum ClipPaths {
+  RECT = 'rect',
+  ROUNDRECT = 'roundRect',
+  ELLIPSE = 'ellipse',
+  TRIANGLE = 'triangle',
+  PENTAGON = 'pentagon',
+  RHOMBUS = 'rhombus',
+  STAR = 'star',
+}
+
 export const CLIPPATHS = {
   rect: {
     name: '矩形',

+ 1 - 1
src/types/slides.ts

@@ -39,7 +39,7 @@ export interface PPTImageElement extends PPTElementBaseProps, PPTElementSizeProp
   filter?: string;
   clip?: {
     range: [[number, number], [number, number]];
-    shape: 'rect' | 'ellipse' | 'polygon';
+    shape: 'rect' | 'roundRect' | 'ellipse' | 'triangle' | 'pentagon' | 'rhombus' | 'star';
   };
   flip?: { x?: number, y?: number };
   shadow?: string;

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

@@ -35,12 +35,35 @@
         v-for="(line, index) in alignmentLines" :key="index" 
         :type="line.type" :axis="line.axis" :length="line.length"
       />
+
+      <EditableElement 
+        v-for="(element, index) in elementList" 
+        :key="element.elId"
+        :elementInfo="element"
+        :elementIndex="index + 1"
+        :isActive="activeElementIdList.includes(element.elId)"
+        :isHandleEl="element.elId === handleElementId"
+        :isActiveGroupElement="activeGroupElementId === element.elId"
+        :isMultiSelect="activeElementIdList.length > 1"
+        :canvasScale="canvasScale"
+        :selectElement="selectElement"
+        :rotateElement="rotateElement"
+        :scaleElement="scaleElement"
+        :orderElement="orderElement"
+        :combineElements="combineElements"
+        :uncombineElements="uncombineElements"
+        :alignElement="alignElement"
+        :deleteElement="deleteElement"
+        :lockElement="lockElement"
+        :copyElement="copyElement"
+        :cutElement="cutElement"
+      />
     </div>
   </div>
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
+import { computed, defineComponent, reactive, ref, watch } from 'vue'
 import { useStore } from 'vuex'
 import { State } from '@/store/state'
 import { MutationTypes } from '@/store/constants'
@@ -51,6 +74,7 @@ import { getImageDataURL } from '@/utils/image'
 import useDropImage from '@/hooks/useDropImage'
 import useSetViewportSize from './hooks/useSetViewportSize'
 
+import EditableElement from '@/views/_common/_element/EditableElement.vue'
 import MouseSelection from './MouseSelection.vue'
 import SlideBackground from './SlideBackground.vue'
 import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue'
@@ -58,12 +82,21 @@ import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue'
 export default defineComponent({
   name: 'v-canvas',
   components: {
+    EditableElement,
     MouseSelection,
     SlideBackground,
     AlignmentLine,
   },
   setup() {
     const store = useStore<State>()
+    const elementList = computed(() => {
+      const currentSlide = store.getters.currentSlide
+      return currentSlide ? JSON.parse(JSON.stringify(currentSlide.elements)) : []
+    })
+    const activeElementIdList = computed(() => store.state.activeElementIdList)
+    const handleElementId = computed(() => store.state.handleElementId)
+    const activeGroupElementId = ref('')
+
     const viewportRef = ref<HTMLElement | null>(null)
     const isShowGridLines = ref(false)
     const alignmentLines = ref<AlignmentLineProps[]>([])
@@ -162,6 +195,40 @@ export default defineComponent({
       if(editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, false)
     }
 
+    const selectElement = () => {
+      console.log('selectElement')
+    }
+    const rotateElement = () => {
+      console.log('rotateElement')
+    }
+    const scaleElement = () => {
+      console.log('scaleElement')
+    }
+    const orderElement = () => {
+      console.log('orderElement')
+    }
+    const combineElements = () => {
+      console.log('combineElements')
+    }
+    const uncombineElements = () => {
+      console.log('uncombineElements')
+    }
+    const alignElement = () => {
+      console.log('alignElement')
+    }
+    const deleteElement = () => {
+      console.log('deleteElement')
+    }
+    const lockElement = () => {
+      console.log('lockElement')
+    }
+    const copyElement = () => {
+      console.log('copyElement')
+    }
+    const cutElement = () => {
+      console.log('cutElement')
+    }
+
     const contextmenus = (): ContextmenuItem[] => {
       return [
         {
@@ -179,6 +246,10 @@ export default defineComponent({
     }
 
     return {
+      elementList,
+      activeElementIdList,
+      handleElementId,
+      activeGroupElementId,
       canvasRef,
       viewportRef,
       viewportStyles,
@@ -189,6 +260,17 @@ export default defineComponent({
       currentSlide,
       isShowGridLines,
       alignmentLines,
+      selectElement,
+      rotateElement,
+      scaleElement,
+      orderElement,
+      combineElements,
+      uncombineElements,
+      alignElement,
+      deleteElement,
+      lockElement,
+      copyElement,
+      cutElement,
       contextmenus,
     }
   },

+ 7 - 3
src/views/_common/_element/EditableElement.vue

@@ -13,7 +13,7 @@
       :isHandleEl="isHandleEl"
       :isActiveGroupElement="isActiveGroupElement"
       :isMultiSelect="isMultiSelect"
-      :animationIndex="-1"
+      :animationIndex="animationIndex"
       :selectElement="selectElement"
       :rotateElement="rotateElement"
       :scaleElement="scaleElement"
@@ -36,8 +36,8 @@ import {
   ElementLockCommands,
 } from '@/types/edit'
 
-import ImageElement from './ImageElement.index.vue'
-import TextElement from './TextElement.index.vue'
+import ImageElement from './ImageElement/index.vue'
+import TextElement from './TextElement/index.vue'
 
 export default defineComponent({
   name: 'editable-element',
@@ -70,6 +70,10 @@ export default defineComponent({
       type: Boolean,
       required: true,
     },
+    animationIndex: {
+      type: Number,
+      default: -1,
+    },
     selectElement: {
       type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove?: boolean) => void>,
       required: true,

+ 6 - 1
src/views/_common/_element/ElementBorder.vue

@@ -20,8 +20,13 @@
 </template>
 
 <script lang="ts">
+import SvgWrapper from '@/components/SvgWrapper.vue'
+
 export default {
-  name: 'element-border',
+  name: 'element-border', 
+  components: {
+    SvgWrapper,
+  },
   props: {
     width: {
       type: Number,

+ 5 - 0
src/views/_common/_element/ImageElement/ImageClipHandler.vue

@@ -51,6 +51,8 @@
 import { computed, defineComponent, onMounted, onUnmounted, PropType, reactive, ref } from 'vue'
 import { KEYCODE } from '@/configs/keyCode'
 
+import SvgWrapper from '@/components/SvgWrapper.vue'
+
 type ClipDataRange = [[number, number], [number, number]]
 
 interface ClipData {
@@ -72,6 +74,9 @@ type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
 
 export default defineComponent({
   name: 'image-clip-handler',
+  components: {
+    SvgWrapper,
+  },
   props: {
     imgUrl: {
       type: String,

+ 5 - 0
src/views/_common/_element/ImageElement/ImageEllipseBorder.vue

@@ -23,8 +23,13 @@
 </template>
 
 <script lang="ts">
+import SvgWrapper from '@/components/SvgWrapper.vue'
+
 export default {
   name: 'image-ellipse-border',
+  components: {
+    SvgWrapper,
+  },
   props: {
     width: {
       type: Number,

+ 5 - 0
src/views/_common/_element/ImageElement/ImagePolygonBorder.vue

@@ -20,8 +20,13 @@
 </template>
 
 <script lang="ts">
+import SvgWrapper from '@/components/SvgWrapper.vue'
+
 export default {
   name: 'image-polygon-border',
+  components: {
+    SvgWrapper,
+  },
   props: {
     width: {
       type: Number,

+ 5 - 0
src/views/_common/_element/ImageElement/ImageRectBorder.vue

@@ -23,8 +23,13 @@
 </template>
 
 <script lang="ts">
+import SvgWrapper from '@/components/SvgWrapper.vue'
+
 export default {
   name: 'image-rect-border',
+  components: {
+    SvgWrapper,
+  },
   props: {
     width: {
       type: Number,

+ 10 - 6
src/views/_common/_element/ImageElement/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div 
     class="editable-element image"
-    :class="{'lock': elementInfo.isLock}"
+    :class="{ 'lock': elementInfo.isLock }"
     :style="{
       top: elementInfo.top + 'px',
       left: elementInfo.left + 'px',
@@ -33,7 +33,7 @@
         transform: flip,
       }"
     >
-      <RectBorder
+      <ImageRectBorder
         v-if="clipShape.type === 'rect'"
         :width="elementInfo.width"
         :height="elementInfo.height"
@@ -42,7 +42,7 @@
         :borderWidth="elementInfo.borderWidth"
         :borderStyle="elementInfo.borderStyle"
       />
-      <EllipseBorder
+      <ImageEllipseBorder
         v-else-if="clipShape.type === 'ellipse'"
         :width="elementInfo.width"
         :height="elementInfo.height"
@@ -50,7 +50,7 @@
         :borderWidth="elementInfo.borderWidth"
         :borderStyle="elementInfo.borderStyle"
       />
-      <PolygonBorder
+      <ImagePolygonBorder
         v-else-if="clipShape.type === 'polygon'"
         :width="elementInfo.width"
         :height="elementInfo.height"
@@ -83,7 +83,7 @@
         'multi-select': isMultiSelect && isActive,
         'selected': isHandleEl,
       }" 
-      :style="{transform: `scale(${1 / canvasScale})`}"
+      :style="{ transform: `scale(${1 / canvasScale})` }"
       v-if="!isCliping"
     >
       <BorderLine 
@@ -102,7 +102,7 @@
           :style="point.style"
           @mousedown.stop="scaleElement($event, elementInfo, point.direction)"
         />
-        <RotateHandle 
+        <RotateHandler
           class="el-rotate-handle" 
           :style="{left: scaleWidth / 2 + 'px'}"
           @mousedown.stop="rotateElement(elementInfo)"
@@ -186,6 +186,9 @@ export default defineComponent({
       type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: ElementScaleHandler) => void>,
       required: true,
     },
+    contextmenus: {
+      type: Function,
+    },
   },
   setup(props) {
     const clipingImageElId = ref('')
@@ -291,6 +294,7 @@ export default defineComponent({
     }
 
     return {
+      scaleWidth,
       isCliping,
       imgPosition,
       clipShape,

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

@@ -0,0 +1,266 @@
+<template>
+  <div 
+    class="editable-element text" 
+    :class="{ 'lock': elementInfo.isLock }"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      transform: `rotate(${elementInfo.rotate}deg)`,
+    }"
+    @mousedown="handleSelectElement($event, false)" 
+  >
+    <div class="element-content"
+      :style="{
+        backgroundColor: elementInfo.fill,
+        opacity: elementInfo.opacity,
+        textShadow: elementInfo.shadow,
+        lineHeight: elementInfo.lineHeight,
+        letterSpacing: (elementInfo.letterSpacing || 0) + 'px',
+      }"
+      v-contextmenu="contextmenus"
+    >
+      <ElementBorder
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :borderColor="elementInfo.borderColor"
+        :borderWidth="elementInfo.borderWidth"
+        :borderStyle="elementInfo.borderStyle"
+      />
+      <div class="text-content"
+        v-html="elementInfo.content" 
+        :contenteditable="isActive && !elementInfo.isLock"
+      ></div>
+    </div>
+
+    <div 
+      class="operate" 
+      :class="{
+        'show': isActive,
+        'multi-select': isMultiSelect && isActive,
+        'selected': isHandleEl
+      }" 
+      :style="{ transform: `scale(${1 / canvasScale})` }"
+      v-contextmenu="contextmenus"
+    >
+      <BorderLine 
+        class="el-border-line"
+        v-for="line in borderLines" 
+        :key="line.type" 
+        :type="line.type" 
+        :style="line.style"
+        :isWide="true"
+        @mousedown="handleSelectElement($event)"
+      />
+      <template v-if="!elementInfo.isLock && (isActiveGroupElement || !isMultiSelect)">
+        <ResizablePoint class="el-resizable-point" 
+          v-for="point in resizablePoints"
+          :key="point.type"
+          :type="point.type"
+          :style="point.style"
+          @mousedown.stop="scaleElement($event, elementInfo, point.direction)"
+        />
+        <RotateHandler
+          class="el-rotate-handle" 
+          :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 { PPTTextElement } from '@/types/slides'
+import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
+
+import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
+import { OPERATE_KEYS } from '@/configs/element'
+
+import ElementBorder from '@/views/_common/_element/ElementBorder.vue'
+import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
+import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'
+import BorderLine from '@/views/_common/_operate/BorderLine.vue'
+import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
+
+export default defineComponent({
+  name: 'slide-element-text',
+  components: {
+    ElementBorder,
+    RotateHandler,
+    ResizablePoint,
+    BorderLine,
+    AnimationIndex,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTTextElement>,
+      required: true,
+    },
+    canvasScale: {
+      type: Number,
+      required: true,
+    },
+    isActive: {
+      type: Boolean,
+      required: true,
+    },
+    isHandleEl: {
+      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: ElementScaleHandler) => void>,
+      required: true,
+    },
+    contextmenus: {
+      type: Function,
+    },
+  },
+  setup(props) {
+    const scaleWidth = computed(() => props.elementInfo ? props.elementInfo.width * props.canvasScale : 0)
+    const scaleHeight = computed(() => props.elementInfo ? props.elementInfo.height * props.canvasScale : 0)
+
+    const resizablePoints = computed(() => {
+      return [
+        { type: OperateResizablePointTypes.TL, direction: OPERATE_KEYS.LEFT_TOP, style: {} },
+        { type: OperateResizablePointTypes.TC, direction: OPERATE_KEYS.TOP, style: {left: scaleWidth.value / 2 + 'px'} },
+        { type: OperateResizablePointTypes.TR, direction: OPERATE_KEYS.RIGHT_TOP, style: {left: scaleWidth.value + 'px'} },
+        { type: OperateResizablePointTypes.ML, direction: OPERATE_KEYS.LEFT, style: {top: scaleHeight.value / 2 + 'px'} },
+        { type: OperateResizablePointTypes.MR, direction: OPERATE_KEYS.RIGHT, style: {left: scaleWidth.value + 'px', top: scaleHeight.value / 2 + 'px'} },
+        { type: OperateResizablePointTypes.BL, direction: OPERATE_KEYS.LEFT_BOTTOM, style: {top: scaleHeight.value + 'px'} },
+        { type: OperateResizablePointTypes.BC, direction: OPERATE_KEYS.BOTTOM, style: {left: scaleWidth.value / 2 + 'px', top: scaleHeight.value + 'px'} },
+        { type: OperateResizablePointTypes.BR, direction: OPERATE_KEYS.RIGHT_BOTTOM, style: {left: scaleWidth.value + 'px', top: scaleHeight.value + 'px'} },
+      ]
+    })
+
+    const borderLines = computed(() => {
+      return [
+        { type: OperateBorderLineTypes.T, style: {width: scaleWidth.value + 'px'} },
+        { type: OperateBorderLineTypes.B, style: {top: scaleHeight.value + 'px', width: scaleWidth.value + 'px'} },
+        { type: OperateBorderLineTypes.L, style: {height: scaleHeight.value + 'px'} },
+        { type: OperateBorderLineTypes.R, style: {left: scaleWidth.value + 'px', height: scaleHeight.value + 'px'} },
+      ]
+    })
+
+    const handleSelectElement = (e: MouseEvent, canMove = true) => {
+      if(props.elementInfo.isLock) return
+      e.stopPropagation()
+
+      props.selectElement(e, props.elementInfo, canMove)
+    }
+
+    return {
+      scaleWidth,
+      resizablePoints,
+      borderLines,
+      handleSelectElement,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.editable-element {
+  position: absolute;
+
+  &.lock .el-border-line {
+    border-color: #888;
+  }
+
+  &:hover .el-border-line {
+    display: block;
+  }
+
+  &.lock .element-content {
+    cursor: default;
+  }
+}
+
+.element-content {
+  position: relative;
+  padding: 10px;
+
+  .text-content {
+    position: relative;
+    cursor: text;
+  }
+}
+
+::v-deep .text-content {
+  word-break: break-word;
+  font-family: '微软雅黑';
+  outline: 0;
+
+  ::selection {
+    background-color: rgba(27, 110, 232, 0.3);
+    color: inherit;
+  }
+
+  ul {
+    list-style-type: disc;
+    padding-inline-start: 30px;
+    li {
+      list-style-type: disc;
+    }
+  }
+
+  ol {
+    list-style-type: decimal;
+    padding-inline-start: 30px;
+    li {
+      list-style-type: decimal;
+    }
+  }
+}
+
+.operate {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  user-select: none;
+
+  &.active {
+    .el-border-line,
+    .el-resizable-point,
+    .el-rotate-handle {
+      display: block;
+    }
+  }
+
+  &.multi-select:not(.selected) .el-border-line {
+    border-color: rgba($color: $themeColor, $alpha: .5);
+  }
+
+  .el-border-line,
+  .el-resizable-point,
+  .el-rotate-handle {
+    display: none;
+  }
+}
+</style>