فهرست منبع

perf: 代码优化&注释

pipipi-pikachu 5 سال پیش
والد
کامیت
d4a8d41394
23فایلهای تغییر یافته به همراه308 افزوده شده و 227 حذف شده
  1. 5 4
      src/types/slides.ts
  2. 1 1
      src/views/components/ThumbnailSlide/index.vue
  3. 10 5
      src/views/components/element/ChartElement/Chart.vue
  4. 1 1
      src/views/components/element/ElementOutline.vue
  5. 13 72
      src/views/components/element/ImageElement/BaseImageElement.vue
  6. 23 12
      src/views/components/element/ImageElement/ImageClipHandler.vue
  7. 1 1
      src/views/components/element/ImageElement/ImageEllipseOutline.vue
  8. 1 1
      src/views/components/element/ImageElement/ImagePolygonOutline.vue
  9. 1 1
      src/views/components/element/ImageElement/ImageRectOutline.vue
  10. 57 0
      src/views/components/element/ImageElement/ImageOutline/index.vue
  11. 17 73
      src/views/components/element/ImageElement/index.vue
  12. 42 0
      src/views/components/element/ImageElement/useClipImage.ts
  13. 17 0
      src/views/components/element/ImageElement/useFilter.ts
  14. 1 0
      src/views/components/element/LineElement/BaseLineElement.vue
  15. 2 1
      src/views/components/element/LineElement/index.vue
  16. 1 1
      src/views/components/element/ShapeElement/index.vue
  17. 5 0
      src/views/components/element/TableElement/CustomTextarea.vue
  18. 49 14
      src/views/components/element/TableElement/EditableTable.vue
  19. 15 7
      src/views/components/element/TableElement/index.vue
  20. 26 15
      src/views/components/element/TextElement/index.vue
  21. 11 9
      src/views/components/element/hooks/useElementFlip.ts
  22. 4 3
      src/views/components/element/hooks/useElementOutline.ts
  23. 5 6
      src/views/components/element/hooks/useElementShadow.ts

+ 5 - 4
src/types/slides.ts

@@ -54,6 +54,10 @@ export interface ImageElementFilters {
   'hue-rotate'?: string;
   'opacity'?: string;
 }
+export interface ImageElementClip {
+  range: [[number, number], [number, number]];
+  shape: string;
+}
 export interface PPTImageElement {
   type: 'image';
   id: string;
@@ -68,10 +72,7 @@ export interface PPTImageElement {
   rotate?: number;
   outline?: PPTElementOutline;
   filters?: ImageElementFilters;
-  clip?: {
-    range: [[number, number], [number, number]];
-    shape: 'rect' | 'roundRect' | 'ellipse' | 'triangle' | 'pentagon' | 'rhombus' | 'star';
-  };
+  clip?: ImageElementClip;
   flip?: ImageOrShapeFlip;
   shadow?: PPTElementShadow;
 }

+ 1 - 1
src/views/components/ThumbnailSlide/index.vue

@@ -13,7 +13,7 @@
         transform: `scale(${scale})`,
       }"
     >
-      <div class="background" :style="{ ...backgroundStyle }"></div>
+      <div class="background" :style="backgroundStyle"></div>
       <ThumbnailElement
         v-for="(element, index) in slide.elements"
         :key="element.id"

+ 10 - 5
src/views/components/element/ChartElement/Chart.vue

@@ -101,6 +101,7 @@ export default defineComponent({
 
     onMounted(renderChart)
 
+    // 更新主题配色:通过主题色的色相旋转,计算出一系列的颜色作为主题配色
     const updateTheme = () => {
       if (!chartRef.value) return
 
@@ -116,15 +117,19 @@ export default defineComponent({
         }
         chartRef.value.style.setProperty(`--theme-color-${i + 1}`, tinycolor(_hsla).toRgbString())
       }
+    }
+
+    watch(() => props.themeColor, updateTheme)
+    onMounted(updateTheme)
 
+    // 更新网格颜色,包括坐标的文字部分
+    const updateGridColor = () => {
+      if (!chartRef.value) return
       if (props.gridColor) chartRef.value.style.setProperty(`--grid-color`, props.gridColor)
     }
 
-    watch([
-      () => props.themeColor,
-      () => props.gridColor,
-    ], updateTheme)
-    onMounted(updateTheme)
+    watch(() => props.gridColor, updateGridColor)
+    onMounted(updateGridColor)
 
     return {
       slideScale,

+ 1 - 1
src/views/components/element/ElementOutline.vue

@@ -15,7 +15,7 @@
       :d="`M0,0 L${width},0 L${width},${height} L0,${height} Z`" 
       :stroke="outlineColor"
       :stroke-width="outlineWidth" 
-      :stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'" 
+      :stroke-dasharray="outlineStyle === 'dashed' ? '10 6' : '0 0'" 
     ></path>
 	</SvgWrapper>
 </template>

+ 13 - 72
src/views/components/element/ImageElement/BaseImageElement.vue

@@ -16,26 +16,7 @@
         transform: flipStyle,
       }"
     >
-      <ImageRectOutline
-        v-if="clipShape.type === 'rect'"
-        :width="elementInfo.width"
-        :height="elementInfo.height"
-        :radius="clipShape.radius"
-        :outline="elementInfo.outline"
-      />
-      <ImageEllipseOutline
-        v-else-if="clipShape.type === 'ellipse'"
-        :width="elementInfo.width"
-        :height="elementInfo.height"
-        :outline="elementInfo.outline"
-      />
-      <ImagePolygonOutline
-        v-else-if="clipShape.type === 'polygon'"
-        :width="elementInfo.width"
-        :height="elementInfo.height"
-        :createPath="clipShape.createPath"
-        :outline="elementInfo.outline"
-      />
+      <ImageOutline :elementInfo="elementInfo" />
 
       <div class="image-content" :style="{ clipPath: clipShape.style }">
         <img 
@@ -57,23 +38,18 @@
 
 <script lang="ts">
 import { computed, defineComponent, PropType } from 'vue'
-
 import { PPTImageElement } from '@/types/slides'
-import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
-
-import ImageRectOutline from './ImageRectOutline.vue'
-import ImageEllipseOutline from './ImageEllipseOutline.vue'
-import ImagePolygonOutline from './ImagePolygonOutline.vue'
-
 import useElementShadow from '@/views/components/element/hooks/useElementShadow'
 import useElementFlip from '@/views/components/element/hooks/useElementFlip'
+import useClipImage from './useClipImage'
+import useFilter from './useFilter'
+
+import ImageOutline from './ImageOutline/index.vue'
 
 export default defineComponent({
   name: 'base-element-image',
   components: {
-    ImageRectOutline,
-    ImageEllipseOutline,
-    ImagePolygonOutline,
+    ImageOutline,
   },
   props: {
     elementInfo: {
@@ -82,59 +58,24 @@ export default defineComponent({
     },
   },
   setup(props) {
-    const imgPosition = computed(() => {
-      if (!props.elementInfo || !props.elementInfo.clip) {
-        return {
-          top: '0',
-          left: '0',
-          width: '100%',
-          height: '100%',
-        }
-      }
-
-      const [start, end] = props.elementInfo.clip.range
-
-      const widthScale = (end[0] - start[0]) / 100
-      const heightScale = (end[1] - start[1]) / 100
-      const left = start[0] / widthScale
-      const top = start[1] / heightScale
-
-      return {
-        left: -left + '%',
-        top: -top + '%',
-        width: 100 / widthScale + '%',
-        height: 100 / heightScale + '%',
-      }
-    })
-
-    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 = ''
-      for (const key of Object.keys(props.elementInfo.filters)) {
-        filter += `${key}(${props.elementInfo.filters[key]}) `
-      }
-      return filter
-    })
-
     const shadow = computed(() => props.elementInfo.shadow)
     const { shadowStyle } = useElementShadow(shadow)
 
     const flip = computed(() => props.elementInfo.flip)
     const { flipStyle } = useElementFlip(flip)
+    
+    const clip = computed(() => props.elementInfo.clip)
+    const { clipShape, imgPosition } = useClipImage(clip)
+
+    const filters = computed(() => props.elementInfo.filters)
+    const { filter } = useFilter(filters)
 
     return {
       imgPosition,
-      clipShape,
       filter,
       flipStyle,
       shadowStyle,
+      clipShape,
     }
   },
 })

+ 23 - 12
src/views/components/element/ImageElement/ImageClipHandler.vue

@@ -95,12 +95,6 @@ export default defineComponent({
     const canvasScale = computed(() => store.state.canvasScale)
     const ctrlOrShiftKeyActive = computed<boolean>(() => store.getters.ctrlOrShiftKeyActive)
 
-    const topImgWrapperPosition = reactive({
-      top: 0,
-      left: 0,
-      width: 0,
-      height: 0,
-    })
     const clipWrapperPositionStyle = reactive({
       top: '0',
       left: '0',
@@ -108,6 +102,7 @@ export default defineComponent({
     const isSettingClipRange = ref(false)
     const currentRange = ref<ImageClipDataRange | null>(null)
 
+    // 获取裁剪区域信息(裁剪区域占原图的宽高比例,处在原图中的位置)
     const getClipDataTransformInfo = () => {
       const [start, end] = props.clipData ? props.clipData.range : [[0, 0], [100, 100]]
 
@@ -118,7 +113,8 @@ export default defineComponent({
 
       return { widthScale, heightScale, left, top }
     }
-
+    
+    // 底层图片位置大小(遮罩区域图片)
     const imgPosition = computed(() => {
       const { widthScale, heightScale, left, top } = getClipDataTransformInfo()
       return {
@@ -129,6 +125,7 @@ export default defineComponent({
       }
     })
 
+    // 底层图片位置大小样式(遮罩区域图片)
     const bottomImgPositionStyle = computed(() => {
       return {
         top: imgPosition.value.top + '%',
@@ -138,6 +135,15 @@ export default defineComponent({
       }
     })
 
+    // 顶层图片容器位置大小(裁剪高亮区域)
+    const topImgWrapperPosition = reactive({
+      top: 0,
+      left: 0,
+      width: 0,
+      height: 0,
+    })
+
+    // 顶层图片容器位置大小样式(裁剪高亮区域)
     const topImgWrapperPositionStyle = computed(() => {
       return {
         top: topImgWrapperPosition.top + '%',
@@ -147,6 +153,7 @@ export default defineComponent({
       }
     })
 
+    // 顶层图片位置大小样式(裁剪区域图片)
     const topImgPositionStyle = computed(() => {
       const bottomWidth = imgPosition.value.width
       const bottomHeight = imgPosition.value.height
@@ -164,6 +171,7 @@ export default defineComponent({
       }
     })
 
+    // 初始化裁剪位置信息
     const initClipPosition = () => {
       const { left, top } = getClipDataTransformInfo()
       topImgWrapperPosition.left = left
@@ -175,6 +183,7 @@ export default defineComponent({
       clipWrapperPositionStyle.left = -left + '%'
     }
 
+    // 执行裁剪:计算裁剪后的图片位置大小和裁剪信息,并将数据同步出去
     const handleClip = () => {
       if (isSettingClipRange.value) return
 
@@ -199,6 +208,7 @@ export default defineComponent({
       emit('clip', clipedEmitData)
     }
 
+    // 快捷键监听:回车确认裁剪
     const keyboardListener = (e: KeyboardEvent) => {
       const key = e.key.toUpperCase()
       if (key === KEYS.ENTER) handleClip()
@@ -212,7 +222,8 @@ export default defineComponent({
       document.removeEventListener('keydown', keyboardListener)
     })
 
-    const getRange = () => {
+    // 计算并更新裁剪区域范围数据
+    const updateRange = () => {
       const retPosition = {
         left: parseInt(topImgPositionStyle.value.left),
         top: parseInt(topImgPositionStyle.value.top),
@@ -235,6 +246,7 @@ export default defineComponent({
       currentRange.value = [start, end]
     }
 
+    // 移动裁剪区域
     const moveClipRange = (e: MouseEvent) => {
       isSettingClipRange.value = true
       let isMouseDown = true
@@ -261,7 +273,6 @@ export default defineComponent({
         let targetLeft = originPositopn.left + moveX
         let targetTop = originPositopn.top + moveY
 
-        // 范围限制
         if (targetLeft < 0) targetLeft = 0
         else if (targetLeft + originPositopn.width > bottomPosition.width) {
           targetLeft = bottomPosition.width - originPositopn.width
@@ -280,7 +291,7 @@ export default defineComponent({
         document.onmousemove = null
         document.onmouseup = null
 
-        getRange()
+        updateRange()
 
         setTimeout(() => {
           isSettingClipRange.value = false
@@ -288,6 +299,7 @@ export default defineComponent({
       }
     }
 
+    // 缩放裁剪区域
     const scaleClipRange = (e: MouseEvent, type: ScaleClipRangeType) => {
       isSettingClipRange.value = true
       let isMouseDown = true
@@ -323,7 +335,6 @@ export default defineComponent({
 
         let targetLeft, targetTop, targetWidth, targetHeight
 
-        // 根据不同缩放点,计算目标大小和位置,同时做大小和范围的限制
         if (type === 't-l') {
           if (originPositopn.left + moveX < 0) {
             moveX = -originPositopn.left
@@ -408,7 +419,7 @@ export default defineComponent({
         document.onmousemove = null
         document.onmouseup = null
 
-        getRange()
+        updateRange()
 
         setTimeout(() => isSettingClipRange.value = false, 0)
       }

+ 1 - 1
src/views/components/element/ImageElement/ImageEllipseOutline.vue

@@ -18,7 +18,7 @@
       :ry="height / 2"
       :stroke="outlineColor"
       :stroke-width="outlineWidth" 
-      :stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'" 
+      :stroke-dasharray="outlineStyle === 'dashed' ? '10 6' : '0 0'" 
     ></ellipse>
 	</SvgWrapper>
 </template>

+ 1 - 1
src/views/components/element/ImageElement/ImagePolygonOutline.vue

@@ -15,7 +15,7 @@
       :d="createPath(width, height)"
       :stroke="outlineColor"
       :stroke-width="outlineWidth" 
-      :stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'" 
+      :stroke-dasharray="outlineStyle === 'dashed' ? '10 6' : '0 0'" 
     ></path>
 	</SvgWrapper>
 </template>

+ 1 - 1
src/views/components/element/ImageElement/ImageRectOutline.vue

@@ -18,7 +18,7 @@
       :height="height"
       :stroke="outlineColor"
       :stroke-width="outlineWidth" 
-      :stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'" 
+      :stroke-dasharray="outlineStyle === 'dashed' ? '10 6' : '0 0'" 
     ></rect>
 	</SvgWrapper>
 </template>

+ 57 - 0
src/views/components/element/ImageElement/ImageOutline/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="image-outline">
+    <ImageRectOutline
+      v-if="clipShape.type === 'rect'"
+      :width="elementInfo.width"
+      :height="elementInfo.height"
+      :radius="clipShape.radius"
+      :outline="elementInfo.outline"
+    />
+    <ImageEllipseOutline
+      v-else-if="clipShape.type === 'ellipse'"
+      :width="elementInfo.width"
+      :height="elementInfo.height"
+      :outline="elementInfo.outline"
+    />
+    <ImagePolygonOutline
+      v-else-if="clipShape.type === 'polygon'"
+      :width="elementInfo.width"
+      :height="elementInfo.height"
+      :outline="elementInfo.outline"
+      :createPath="clipShape.createPath"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue'
+import { PPTImageElement } from '@/types/slides'
+import useClipImage from '../useClipImage'
+
+import ImageRectOutline from './ImageRectOutline.vue'
+import ImageEllipseOutline from './ImageEllipseOutline.vue'
+import ImagePolygonOutline from './ImagePolygonOutline.vue'
+
+export default defineComponent({
+  name: 'image-outline',
+  components: {
+    ImageRectOutline,
+    ImageEllipseOutline,
+    ImagePolygonOutline,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTImageElement>,
+      required: true,
+    },
+  },
+  setup(props) {
+    const clip = computed(() => props.elementInfo.clip)
+    const { clipShape } = useClipImage(clip)
+
+    return {
+      clipShape,
+    }
+  },
+})
+</script>

+ 17 - 73
src/views/components/element/ImageElement/index.vue

@@ -20,7 +20,7 @@
       :top="elementInfo.top"
       :left="elementInfo.left"
       :clipPath="clipShape.style"
-      @clip="range => clip(range)"
+      @clip="range => handleClip(range)"
     />
     <div 
       class="element-content"
@@ -31,28 +31,9 @@
         transform: flipStyle,
       }"
     >
-      <ImageRectOutline
-        v-if="clipShape.type === 'rect'"
-        :width="elementInfo.width"
-        :height="elementInfo.height"
-        :radius="clipShape.radius"
-        :outline="elementInfo.outline"
-      />
-      <ImageEllipseOutline
-        v-else-if="clipShape.type === 'ellipse'"
-        :width="elementInfo.width"
-        :height="elementInfo.height"
-        :outline="elementInfo.outline"
-      />
-      <ImagePolygonOutline
-        v-else-if="clipShape.type === 'polygon'"
-        :width="elementInfo.width"
-        :height="elementInfo.height"
-        :outline="elementInfo.outline"
-        :createPath="clipShape.createPath"
-      />
-
-      <div class="image-content" :style="{clipPath: clipShape.style}">
+      <ImageOutline :elementInfo="elementInfo" />
+
+      <div class="image-content" :style="{ clipPath: clipShape.style }">
         <img 
           :src="elementInfo.src" 
           :draggable="false" 
@@ -74,23 +55,20 @@
 import { computed, defineComponent, PropType } from 'vue'
 import { MutationTypes, useStore } from '@/store'
 import { PPTImageElement } from '@/types/slides'
+import { ImageClipedEmitData } from '@/types/edit'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
-import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
 import useElementShadow from '@/views/components/element/hooks/useElementShadow'
 import useElementFlip from '@/views/components/element/hooks/useElementFlip'
+import useClipImage from './useClipImage'
+import useFilter from './useFilter'
 
-import ImageRectOutline from './ImageRectOutline.vue'
-import ImageEllipseOutline from './ImageEllipseOutline.vue'
-import ImagePolygonOutline from './ImagePolygonOutline.vue'
+import ImageOutline from './ImageOutline/index.vue'
 import ImageClipHandler from './ImageClipHandler.vue'
-import { ImageClipedEmitData } from '@/types/edit'
 
 export default defineComponent({
   name: 'editable-element-image',
   components: {
-    ImageRectOutline,
-    ImageEllipseOutline,
-    ImagePolygonOutline,
+    ImageOutline,
     ImageClipHandler,
   },
   props: {
@@ -117,53 +95,19 @@ export default defineComponent({
     const flip = computed(() => props.elementInfo.flip)
     const { flipStyle } = useElementFlip(flip)
 
+    const clip = computed(() => props.elementInfo.clip)
+    const { clipShape, imgPosition } = useClipImage(clip)
+
+    const filters = computed(() => props.elementInfo.filters)
+    const { filter } = useFilter(filters)
+
     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
-
-      return CLIPPATHS[shape]
-    })
-
-    const imgPosition = computed(() => {
-      if (!props.elementInfo || !props.elementInfo.clip) {
-        return {
-          top: '0',
-          left: '0',
-          width: '100%',
-          height: '100%',
-        }
-      }
-
-      const [start, end] = props.elementInfo.clip.range
-
-      const widthScale = (end[0] - start[0]) / 100
-      const heightScale = (end[1] - start[1]) / 100
-      const left = start[0] / widthScale
-      const top = start[1] / heightScale
-
-      return {
-        left: -left + '%',
-        top: -top + '%',
-        width: 100 / widthScale + '%',
-        height: 100 / heightScale + '%',
-      }
-    })
-
-    const filter = computed(() => {
-      if (!props.elementInfo.filters) return ''
-      let filter = ''
-      for (const key of Object.keys(props.elementInfo.filters)) {
-        filter += `${key}(${props.elementInfo.filters[key]}) `
-      }
-      return filter
-    })
 
-    const clip = (data: ImageClipedEmitData) => {
+    const handleClip = (data: ImageClipedEmitData) => {
       store.commit(MutationTypes.SET_CLIPING_IMAGE_ELEMENT_ID, '')
       
       if (!data) return
@@ -183,7 +127,7 @@ export default defineComponent({
 
     return {
       isCliping,
-      clip,
+      handleClip,
       clipingImageElementId,
       shadowStyle,
       handleSelectElement,

+ 42 - 0
src/views/components/element/ImageElement/useClipImage.ts

@@ -0,0 +1,42 @@
+import { computed, Ref } from 'vue'
+import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
+import { ImageElementClip } from '@/types/slides'
+
+export default (clip: Ref<ImageElementClip | undefined>) => {
+  const clipShape = computed(() => {
+    if (!clip.value) return CLIPPATHS.rect
+    const shape = clip.value.shape || ClipPathTypes.RECT
+
+    return CLIPPATHS[shape]
+  })
+
+  const imgPosition = computed(() => {
+    if (!clip.value) {
+      return {
+        top: '0',
+        left: '0',
+        width: '100%',
+        height: '100%',
+      }
+    }
+
+    const [start, end] = clip.value.range
+
+    const widthScale = (end[0] - start[0]) / 100
+    const heightScale = (end[1] - start[1]) / 100
+    const left = start[0] / widthScale
+    const top = start[1] / heightScale
+
+    return {
+      left: -left + '%',
+      top: -top + '%',
+      width: 100 / widthScale + '%',
+      height: 100 / heightScale + '%',
+    }
+  })
+
+  return {
+    clipShape,
+    imgPosition,
+  }
+}

+ 17 - 0
src/views/components/element/ImageElement/useFilter.ts

@@ -0,0 +1,17 @@
+import { computed, Ref } from 'vue'
+import { ImageElementFilters } from '@/types/slides'
+
+export default (filters: Ref<ImageElementFilters | undefined>) => {
+  const filter = computed(() => {
+    if (!filters.value) return ''
+    let filter = ''
+    for (const key of Object.keys(filters.value)) {
+      filter += `${key}(${filters.value[key]}) `
+    }
+    return filter
+  })
+
+  return {
+    filter,
+  }
+}

+ 1 - 0
src/views/components/element/LineElement/BaseLineElement.vue

@@ -82,6 +82,7 @@ export default defineComponent({
     })
 
     const lineDashArray = computed(() => props.elementInfo.style === 'dashed' ? '10, 5' : '0, 0')
+
     const path = computed(() => {
       const start = props.elementInfo.start.join(',')
       const end = props.elementInfo.end.join(',')

+ 2 - 1
src/views/components/element/LineElement/index.vue

@@ -106,7 +106,8 @@ export default defineComponent({
       return height < 24 ? 24 : height
     })
 
-    const lineDashArray = computed(() => props.elementInfo.style === 'dashed' ? '10, 5' : '0, 0')
+    const lineDashArray = computed(() => props.elementInfo.style === 'dashed' ? '10 6' : '0 0')
+
     const path = computed(() => {
       const start = props.elementInfo.start.join(',')
       const end = props.elementInfo.end.join(',')

+ 1 - 1
src/views/components/element/ShapeElement/index.vue

@@ -46,7 +46,7 @@
             :fill="elementInfo.gradient ? `url(#editabel-gradient-${elementInfo.id})` : elementInfo.fill"
             :stroke="outlineColor"
             :stroke-width="outlineWidth" 
-            :stroke-dasharray="outlineStyle === 'dashed' ? '10 5' : '0 0'" 
+            :stroke-dasharray="outlineStyle === 'dashed' ? '10 6' : '0 0'" 
           ></path>
         </g>
 			</SvgWrapper>

+ 5 - 0
src/views/components/element/TableElement/CustomTextarea.vue

@@ -30,6 +30,8 @@ export default defineComponent({
     const text = ref('')
     const isFocus = ref(false)
 
+    // 自定义v-modal,同步数据
+    // 当文本框聚焦时,不执行数据同步
     watch(() => props.modelValue, () => {
       if (isFocus.value) return
       text.value = props.modelValue
@@ -42,6 +44,7 @@ export default defineComponent({
       emit('update:modelValue', text)
     }
 
+    // 聚焦时更新焦点标记,并监听粘贴事件
     const handleFocus = () => {
       isFocus.value = true
 
@@ -58,11 +61,13 @@ export default defineComponent({
       }
     }
 
+    // 失焦时更新焦点标记,清除粘贴事件监听
     const handleBlur = () => {
       isFocus.value = false
       if (textareaRef.value) textareaRef.value.onpaste = null
     }
 
+    // 清除粘贴事件监听
     onUnmounted(() => {
       if (textareaRef.value) textareaRef.value.onpaste = null
     })

+ 49 - 14
src/views/components/element/TableElement/EditableTable.vue

@@ -113,7 +113,21 @@ export default defineComponent({
   setup(props, { emit }) {
     const store = useStore()
     const canvasScale = computed(() => store.state.canvasScale)
+    
+    const isStartSelect = ref(false)
+    const startCell = ref<number[]>([])
+    const endCell = ref<number[]>([])
+
+    const tableCells = computed<TableCell[][]>({
+      get() {
+        return props.data
+      },
+      set(newData) {
+        emit('change', newData)
+      },
+    })
 
+    // 通过表格的主题色计算辅助颜色
     const subThemeColor = ref(['', ''])
     watch(() => props.theme, () => {
       if (props.theme) {
@@ -127,15 +141,7 @@ export default defineComponent({
       }
     }, { immediate: true })
 
-    const tableCells = computed<TableCell[][]>({
-      get() {
-        return props.data
-      },
-      set(newData) {
-        emit('change', newData)
-      },
-    })
-
+    // 计算表格每一列的列宽和总宽度
     const colSizeList = ref<number[]>([])
     const totalWidth = computed(() => colSizeList.value.reduce((a, b) => a + b))
     watch([
@@ -145,10 +151,8 @@ export default defineComponent({
       colSizeList.value = props.colWidths.map(item => item * props.width)
     }, { immediate: true })
     
-    const isStartSelect = ref(false)
-    const startCell = ref<number[]>([])
-    const endCell = ref<number[]>([])
-    
+    // 清除全部单元格的选中状态
+    // 表格处于不可编辑状态时也需要清除
     const removeSelectedCells = () => {
       startCell.value = []
       endCell.value = []
@@ -158,6 +162,7 @@ export default defineComponent({
       if (!props.editable) removeSelectedCells()
     })
 
+    // 用于拖拽列宽的操作节点位置
     const dragLinePosition = computed(() => {
       const dragLinePosition: number[] = []
       for (let i = 1; i < colSizeList.value.length + 1; i++) {
@@ -167,6 +172,7 @@ export default defineComponent({
       return dragLinePosition
     })
 
+    // 无效的单元格位置(被合并的单元格位置)集合
     const hideCells = computed(() => {
       const hideCells = []
       
@@ -188,6 +194,7 @@ export default defineComponent({
       return hideCells
     })
 
+    // 当前选中的单元格集合
     const selectedCells = computed(() => {
       if (!startCell.value.length) return []
       const [startX, startY] = startCell.value
@@ -217,11 +224,13 @@ export default defineComponent({
       emit('changeSelectedCells', selectedCells.value)
     })
 
+    // 当前激活的单元格:当且仅当只有一个选中单元格时,该单元格为激活的单元格
     const activedCell = computed(() => {
       if (selectedCells.value.length > 1) return null
       return selectedCells.value[0]
     })
 
+    // 当前选中的单元格位置范围
     const selectedRange = computed(() => {
       if (!startCell.value.length) return null
       const [startX, startY] = startCell.value
@@ -242,6 +251,7 @@ export default defineComponent({
       }
     })
 
+    // 设置选中单元格状态(鼠标点击或拖选)
     const handleMouseup = () => isStartSelect.value = false
 
     const handleCellMousedown = (e: MouseEvent, rowIndex: number, colIndex: number) => {
@@ -264,20 +274,24 @@ export default defineComponent({
       document.removeEventListener('mouseup', handleMouseup)
     })
 
+    // 判断某位置是否为无效单元格(被合并掉的位置)
     const isHideCell = (rowIndex: number, colIndex: number) => hideCells.value.includes(`${rowIndex}_${colIndex}`)
 
+    // 选中指定的列
     const selectCol = (index: number) => {
       const maxRow = tableCells.value.length - 1
       startCell.value = [0, index]
       endCell.value = [maxRow, index]
     }
 
+    // 选中指定的行
     const selectRow = (index: number) => {
       const maxCol = tableCells.value[index].length - 1
       startCell.value = [index, 0]
       endCell.value = [index, maxCol]
     }
 
+    // 选中全部单元格
     const selectAll = () => {
       const maxRow = tableCells.value.length - 1
       const maxCol = tableCells.value[maxRow].length - 1
@@ -285,6 +299,7 @@ export default defineComponent({
       endCell.value = [maxRow, maxCol]
     }
 
+    // 删除一行
     const deleteRow = (rowIndex: number) => {
       const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
 
@@ -307,6 +322,7 @@ export default defineComponent({
       tableCells.value = _tableCells
     }
 
+    // 删除一列
     const deleteCol = (colIndex: number) => {
       const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
 
@@ -332,6 +348,7 @@ export default defineComponent({
       emit('changeColWidths', colSizeList.value)
     }
     
+    // 插入一行
     const insertRow = (rowIndex: number) => {
       const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
 
@@ -349,6 +366,7 @@ export default defineComponent({
       tableCells.value = _tableCells
     }
 
+    // 插入一列
     const insertCol = (colIndex: number) => {
       tableCells.value = tableCells.value.map(item => {
         const cell = {
@@ -364,6 +382,7 @@ export default defineComponent({
       emit('changeColWidths', colSizeList.value)
     }
     
+    // 合并单元格
     const mergeCells = () => {
       const [startX, startY] = startCell.value
       const [endX, endY] = endCell.value
@@ -382,6 +401,7 @@ export default defineComponent({
       removeSelectedCells()
     }
 
+    // 拆分单元格
     const splitCells = (rowIndex: number, colIndex: number) => {
       const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
       _tableCells[rowIndex][colIndex].rowspan = 1
@@ -391,6 +411,7 @@ export default defineComponent({
       removeSelectedCells()
     }
 
+    // 鼠标拖拽调整列宽
     const handleMousedownColHandler = (e: MouseEvent, colIndex: number) => {
       removeSelectedCells()
       let isMouseDown = true
@@ -417,6 +438,7 @@ export default defineComponent({
       }
     }
 
+    // 清空选中单元格内的文字
     const clearSelectedCellText = () => {
       const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
 
@@ -430,6 +452,10 @@ export default defineComponent({
       tableCells.value = _tableCells
     }
 
+    // 将焦点移动到下一个单元格
+    // 当前行右边有单元格时,焦点右移
+    // 当前行右边无单元格(已处在行末),且存在下一行时,焦点移动下下一行行首
+    // 当前行右边无单元格(已处在行末),且不存在下一行(已处在最后一行)时,新建一行并将焦点移动下下一行行首
     const tabActiveCell = () => {
       const getNextCell = (i: number, j: number): [number, number] | null => {
         if (!tableCells.value[i]) return null
@@ -450,12 +476,14 @@ export default defineComponent({
       }
       else startCell.value = nextCell
 
+      // 移动焦点后自动聚焦文本
       nextTick(() => {
         const textRef = document.querySelector('.cell-text.active') as HTMLInputElement
         if (textRef) textRef.focus()
       })
     }
 
+    // 表格快捷键监听
     const keydownListener = (e: KeyboardEvent) => {
       if (!props.editable || !selectedCells.value.length) return
 
@@ -498,6 +526,7 @@ export default defineComponent({
       document.removeEventListener('keydown', keydownListener)
     })
 
+    // 计算单元格文本样式
     const getTextStyle = (style?: TableCellStyle) => {
       if (!style) return {}
       const {
@@ -524,10 +553,12 @@ export default defineComponent({
       }
     }
 
+    // 单元格文字输入时更新表格数据
     const handleInput = debounce(function() {
       emit('change', tableCells.value)
     }, 300, { trailing: true })
 
+    // 获取有效的单元格(排除掉被合并的单元格)
     const getEffectiveTableCells = () => {
       const effectiveTableCells = []
 
@@ -543,6 +574,7 @@ export default defineComponent({
       return effectiveTableCells
     }
 
+    // 检查是否可以删除行和列:有效的行/列数大于1
     const checkCanDeleteRowOrCol = () => {
       const effectiveTableCells = getEffectiveTableCells()
       const canDeleteRow = effectiveTableCells.length > 1
@@ -551,6 +583,9 @@ export default defineComponent({
       return { canDeleteRow, canDeleteCol }
     }
 
+    // 检查是否可以合并或拆分
+    // 必须多选才可以合并
+    // 必须单选且所选单元格为合并单元格才可以拆分
     const checkCanMergeOrSplit = (rowIndex: number, colIndex: number) => {
       const isMultiSelected = selectedCells.value.length > 1
       const targetCell = tableCells.value[rowIndex][colIndex]
@@ -717,7 +752,7 @@ table {
       position: absolute;
       top: 0;
       left: 0;
-      background-color: rgba($color: $themeColor, $alpha: .3);
+      background-color: rgba($color: #666, $alpha: .4);
     }
   }
 

+ 15 - 7
src/views/components/element/TableElement/index.vue

@@ -69,6 +69,9 @@ export default defineComponent({
   setup(props) {
     const store = useStore()
     const canvasScale = computed(() => store.state.canvasScale)
+    const handleElementId = computed(() => store.state.handleElementId)
+    
+    const elementRef = ref<HTMLElement>()
 
     const { addHistorySnapshot } = useHistorySnapshot()
 
@@ -78,8 +81,9 @@ export default defineComponent({
 
       props.selectElement(e, props.elementInfo)
     }
+
+    // 更新表格的可编辑状态,表格处于编辑状态时需要禁用全局快捷键
     const editable = ref(false)
-    const handleElementId = computed(() => store.state.handleElementId)
 
     watch(handleElementId, () => {
       if (handleElementId.value !== props.elementInfo.id) editable.value = false
@@ -88,9 +92,13 @@ export default defineComponent({
     watch(editable, () => {
       store.commit(MutationTypes.SET_DISABLE_HOTKEYS_STATE, editable.value)
     })
-    
-    const elementRef = ref<HTMLElement>()
 
+    const startEdit = () => {
+      if (!props.elementInfo.lock) editable.value = true
+    }
+
+    // 监听表格元素的尺寸变化,当高度变化时,更新高度到vuex
+    // 如果高度变化时正处在缩放操作中,则等待缩放操作结束后再更新
     const isScaling = ref(false)
     const realHeightCache = ref(-1)
 
@@ -139,6 +147,7 @@ export default defineComponent({
       if (elementRef.value) resizeObserver.unobserve(elementRef.value)
     })
 
+    // 更新表格内容数据
     const updateTableCells = (data: TableCell[][]) => {
       store.commit(MutationTypes.UPDATE_ELEMENT, {
         id: props.elementInfo.id, 
@@ -146,6 +155,8 @@ export default defineComponent({
       })
       addHistorySnapshot()
     }
+
+    // 更新表格的列宽数据
     const updateColWidths = (widths: number[]) => {
       const width = widths.reduce((a, b) => a + b)
       const colWidths = widths.map(item => item / width)
@@ -157,14 +168,11 @@ export default defineComponent({
       addHistorySnapshot()
     }
 
+    // 更新表格当前选中的单元格
     const updateSelectedCells = (cells: string[]) => {
       nextTick(() => emitter.emit(EmitterEvents.UPDATE_TABLE_SELECTED_CELL, cells))
     }
 
-    const startEdit = () => {
-      if (!props.elementInfo.lock) editable.value = true
-    }
-
     return {
       elementRef,
       canvasScale,

+ 26 - 15
src/views/components/element/TextElement/index.vue

@@ -86,6 +86,23 @@ export default defineComponent({
     const isScaling = ref(false)
     const realHeightCache = ref(-1)
 
+    const editorViewRef = ref<HTMLElement>()
+    let editorView: EditorView
+
+    const shadow = computed(() => props.elementInfo.shadow)
+    const { shadowStyle } = useElementShadow(shadow)
+
+    const handleElementId = computed(() => store.state.handleElementId)
+
+    const handleSelectElement = (e: MouseEvent, canMove = true) => {
+      if (props.elementInfo.lock) return
+      e.stopPropagation()
+
+      props.selectElement(e, props.elementInfo, canMove)
+    }
+
+    // 监听文本元素的尺寸变化,当高度变化时,更新高度到vuex
+    // 如果高度变化时正处在缩放操作中,则等待缩放操作结束后再更新
     const scaleElementStateListener = (state: boolean) => {
       isScaling.value = state
 
@@ -127,10 +144,11 @@ export default defineComponent({
     onUnmounted(() => {
       if (elementRef.value) resizeObserver.unobserve(elementRef.value)
     })
-    
-    const editorViewRef = ref<HTMLElement>()
-    let editorView: EditorView
 
+    // 富文本的各种交互事件监听:
+    // 聚焦时取消全局快捷键事件
+    // 输入文字时同步数据到vuex
+    // 点击鼠标和键盘时同步富文本状态到工具栏
     const handleFocus = () => {
       store.commit(MutationTypes.SET_DISABLE_HOTKEYS_STATE, true)
     }
@@ -155,6 +173,7 @@ export default defineComponent({
       handleClick()
     }
 
+    // 将富文本内容同步到DOM
     const textContent = computed(() => props.elementInfo.content)
     watch(textContent, () => {
       if (!editorView) return
@@ -162,11 +181,13 @@ export default defineComponent({
       editorView.dom.innerHTML = textContent.value
     })
 
+    // 打开/关闭编辑器的编辑模式
     const editable = computed(() => !props.elementInfo.lock)
     watch(editable, () => {
       editorView.setProps({ editable: () => editable.value })
     })
 
+    // Prosemirror编辑器的初始化和卸载
     onMounted(() => {
       editorView = initProsemirrorEditor((editorViewRef.value as Element), textContent.value, {
         handleDOMEvents: {
@@ -181,19 +202,9 @@ export default defineComponent({
     onUnmounted(() => {
       editorView && editorView.destroy()
     })
-
-    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)
-
-    const handleElementId = computed(() => store.state.handleElementId)
     
+    // 执行富文本命令(可以是一个或多个)
+    // 部分命令在执行前先判断当前选区是否为空,如果选区为空先进行全选操作
     const execCommand = (payload: CommandPayload | CommandPayload[]) => {
       if (handleElementId.value !== props.elementInfo.id) return
 

+ 11 - 9
src/views/components/element/hooks/useElementFlip.ts

@@ -1,18 +1,20 @@
-import { ref, Ref, watchEffect } from 'vue'
+import { computed, Ref } from 'vue'
 import { ImageOrShapeFlip } from '@/types/slides'
 
+// 计算元素的翻转样式
 export default (flip: Ref<ImageOrShapeFlip | undefined>) => {
-  const flipStyle = ref('')
-
-  watchEffect(() => {
+  const flipStyle = computed(() => {
     if (flip.value) {
+      let style = ''
+      
       const { x, y } = flip.value
-      if (x && y) flipStyle.value = `rotateX(${x}deg) rotateY(${y}deg)`
-      else if (x) flipStyle.value = `rotateX(${x}deg)`
-      else if (y) flipStyle.value = `rotateY(${y}deg)`
-      else flipStyle.value = ''
+      if (x && y) style = `rotateX(${x}deg) rotateY(${y}deg)`
+      else if (x) style = `rotateX(${x}deg)`
+      else if (y) style = `rotateY(${y}deg)`
+
+      return style
     }
-    else flipStyle.value = ''
+    return ''
   })
 
   return {

+ 4 - 3
src/views/components/element/hooks/useElementOutline.ts

@@ -1,10 +1,11 @@
 import { computed, Ref } from 'vue'
 import { PPTElementOutline } from '@/types/slides'
 
+// 计算边框相关属性值,主要是对默认值的处理
 export default (outline: Ref<PPTElementOutline | undefined>) => {
-  const outlineWidth = computed(() => (outline.value && outline.value.width !== undefined) ? outline.value.width : 0)
-  const outlineStyle = computed(() => (outline.value && outline.value.style !== undefined) ? outline.value.style : 'solid')
-  const outlineColor = computed(() => (outline.value && outline.value.color !== undefined) ? outline.value.color : '#d14424')
+  const outlineWidth = computed(() => outline.value?.width ?? 0)
+  const outlineStyle = computed(() => outline.value?.style || 'solid')
+  const outlineColor = computed(() => outline.value?.color || '#d14424')
 
   return {
     outlineWidth,

+ 5 - 6
src/views/components/element/hooks/useElementShadow.ts

@@ -1,15 +1,14 @@
-import { ref, Ref, watchEffect } from 'vue'
+import { computed, Ref } from 'vue'
 import { PPTElementShadow } from '@/types/slides'
 
+// 计算元素的阴影样式
 export default (shadow: Ref<PPTElementShadow | undefined>) => {
-  const shadowStyle = ref('')
-
-  watchEffect(() => {
+  const shadowStyle = computed(() => {
     if (shadow.value) {
       const { h, v, blur, color } = shadow.value
-      shadowStyle.value = `${h}px ${v}px ${blur}px ${color}`
+      return `${h}px ${v}px ${blur}px ${color}`
     }
-    else shadowStyle.value = ''
+    return ''
   })
 
   return {