pipipi-pikachu 5 yıl önce
ebeveyn
işleme
b715809a34
43 değiştirilmiş dosya ile 583 ekleme ve 497 silme
  1. 8 16
      src/configs/element.ts
  2. 10 10
      src/configs/lines.ts
  3. 1 1
      src/hooks/useAlignElementToCanvas.ts
  4. 5 5
      src/hooks/useCombineElement.ts
  5. 12 12
      src/hooks/useCreateElement.ts
  6. 1 1
      src/hooks/useDeleteElement.ts
  7. 4 4
      src/hooks/useLockElement.ts
  8. 1 1
      src/hooks/useMoveElement.ts
  9. 6 6
      src/hooks/useOrderElement.ts
  10. 4 4
      src/hooks/usePasteTextClipboardData.ts
  11. 2 2
      src/hooks/useSelectAllElement.ts
  12. 3 2
      src/hooks/useSlideBackgroundStyle.ts
  13. 33 30
      src/mocks/index.ts
  14. 3 3
      src/store/getters.ts
  15. 4 4
      src/store/mutations.ts
  16. 55 35
      src/types/slides.ts
  17. 2 2
      src/utils/element.ts
  18. 2 2
      src/utils/image.ts
  19. 1 1
      src/views/Editor/Canvas/MultiSelectOperate.vue
  20. 2 1
      src/views/Editor/Canvas/SlideBackground.vue
  21. 11 11
      src/views/Editor/Canvas/hooks/useDragElement.ts
  22. 4 4
      src/views/Editor/Canvas/hooks/useMouseSelection.ts
  23. 1 1
      src/views/Editor/Canvas/hooks/useRotateElement.ts
  24. 29 29
      src/views/Editor/Canvas/hooks/useScaleElement.ts
  25. 14 14
      src/views/Editor/Canvas/hooks/useSelectElement.ts
  26. 4 4
      src/views/Editor/Canvas/index.vue
  27. 1 1
      src/views/_common/ThumbnailSlide.vue
  28. 2 2
      src/views/_common/_element/EditableElement.vue
  29. 0 59
      src/views/_common/_element/ElementBorder.vue
  30. 69 0
      src/views/_common/_element/ElementOutline.vue
  31. 23 23
      src/views/_common/_element/ImageElement/BaseImageElement.vue
  32. 3 3
      src/views/_common/_element/ImageElement/ImageClipHandler.vue
  33. 0 66
      src/views/_common/_element/ImageElement/ImageEllipseBorder.vue
  34. 73 0
      src/views/_common/_element/ImageElement/ImageEllipseOutline.vue
  35. 0 67
      src/views/_common/_element/ImageElement/ImagePolygonBorder.vue
  36. 74 0
      src/views/_common/_element/ImageElement/ImagePolygonOutline.vue
  37. 25 18
      src/views/_common/_element/ImageElement/ImageRectBorder.vue
  38. 29 29
      src/views/_common/_element/ImageElement/index.vue
  39. 17 10
      src/views/_common/_element/TextElement/BaseTextElement.vue
  40. 17 14
      src/views/_common/_element/TextElement/index.vue
  41. 0 0
      src/views/_common/_element/hooks/useCommonOperate.ts
  42. 14 0
      src/views/_common/_element/hooks/useElementOutline.ts
  43. 14 0
      src/views/_common/_element/hooks/useElementShadow.ts

+ 8 - 16
src/configs/element.ts

@@ -1,14 +1,5 @@
 const DEFAULT_COLOR = '#41464b'
 
-export enum ElementTypes {
-  TEXT = '文本',
-  IMAGE = '图片',
-  SHAPE = '形状',
-  LINE = '线条',
-  CHART = '图表',
-  TABLE = '表格',
-}
-
 export const DEFAULT_TEXT = {
   left: 0,
   top: 0,
@@ -23,17 +14,17 @@ export const DEFAULT_TEXT = {
 export const DEFAULT_IMAGE = {
   left: 0,
   top: 0,
-  lockRatio: true,
+  fixedRatio: true,
 }
 
 export const DEFAULT_SHAPE = {
   fill: DEFAULT_COLOR,
-  lockRatio: false,
+  fixedRatio: false,
 }
 
 export const DEFAULT_LINE = {
   style: 'solid',
-  marker: ['', ''],
+  points: ['', ''],
   width: 4,
   color: DEFAULT_COLOR,
 }
@@ -48,8 +39,9 @@ export const DEFAULT_CHART = {
 export const DEFAULT_TABLE = {
   left: 0,
   top: 0,
-  isLock: false,
-  borderStyle: 'solid',
-  borderWidth: 2,
-  borderColor: DEFAULT_COLOR,
+  outline: {
+    width: 2,
+    style: 'solid',
+    color: DEFAULT_COLOR
+  },
 }

+ 10 - 10
src/configs/lines.ts

@@ -1,12 +1,12 @@
 export const LINES = [
-  { path: 'M0,0 L20,20', style: 'solid', marker: ['', ''] },
-  { path: 'M0,0 L20,20', style: 'solid', marker: ['', 'arrow'] },
-  { path: 'M0,0 L20,20', style: 'solid', marker: ['arrow', 'arrow'] },
-  { path: 'M0,0 L20,20', style: 'solid', marker: ['', 'cusp'] },
-  { path: 'M0,0 L20,20', style: 'solid', marker: ['cusp', 'cusp'] },
-  { path: 'M0,0 L20,20', style: 'solid', marker: ['', 'dot'] },
-  { path: 'M0,0 L20,20', style: 'solid', marker: ['dot', 'dot'] },
-  { path: 'M0,0 L20,20', style: 'dashed', marker: ['', ''] },
-  { path: 'M0,0 L20,20', style: 'dashed', marker: ['', 'arrow'] },
-  { path: 'M0,0 L20,20', style: 'dashed', marker: ['arrow', 'arrow'] },
+  { path: 'M0,0 L20,20', style: 'solid', points: ['', ''] },
+  { path: 'M0,0 L20,20', style: 'solid', points: ['', 'arrow'] },
+  { path: 'M0,0 L20,20', style: 'solid', points: ['arrow', 'arrow'] },
+  { path: 'M0,0 L20,20', style: 'solid', points: ['', 'cusp'] },
+  { path: 'M0,0 L20,20', style: 'solid', points: ['cusp', 'cusp'] },
+  { path: 'M0,0 L20,20', style: 'solid', points: ['', 'dot'] },
+  { path: 'M0,0 L20,20', style: 'solid', points: ['dot', 'dot'] },
+  { path: 'M0,0 L20,20', style: 'dashed', points: ['', ''] },
+  { path: 'M0,0 L20,20', style: 'dashed', points: ['', 'arrow'] },
+  { path: 'M0,0 L20,20', style: 'dashed', points: ['arrow', 'arrow'] },
 ]

+ 1 - 1
src/hooks/useAlignElementToCanvas.ts

@@ -20,7 +20,7 @@ export default () => {
   
     const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
     for(const element of newElementList) {
-      if(!activeElementIdList.value.includes(element.elId)) continue
+      if(!activeElementIdList.value.includes(element.id)) continue
       
       if(command === ElementAlignCommands.TOP) {
         const offsetY = minY - 0

+ 5 - 5
src/hooks/useCombineElement.ts

@@ -22,16 +22,16 @@ export default () => {
 
     const combineElementList: PPTElement[] = []
     for(const element of newElementList) {
-      if(activeElementIdList.value.includes(element.elId)) {
+      if(activeElementIdList.value.includes(element.id)) {
         element.groupId = groupId
         combineElementList.push(element)
       }
     }
 
     // 注意,组合元素的层级应该是连续的,所以需要获取该组元素中最顶层的元素,将组内其他成员从原位置移动到最顶层的元素的下面
-    const combineElementMaxIndex = newElementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
-    const combineElementIdList = combineElementList.map(_element => _element.elId)
-    newElementList = newElementList.filter(_element => !combineElementIdList.includes(_element.elId))
+    const combineElementMaxIndex = newElementList.findIndex(_element => _element.id === combineElementList[combineElementList.length - 1].id)
+    const combineElementIdList = combineElementList.map(_element => _element.id)
+    newElementList = newElementList.filter(_element => !combineElementIdList.includes(_element.id))
 
     const insertIndex = combineElementMaxIndex - combineElementList.length + 1
     newElementList.splice(insertIndex, 0, ...combineElementList)
@@ -48,7 +48,7 @@ export default () => {
     
     const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
     for(const element of newElementList) {
-      if(activeElementIdList.value.includes(element.elId) && element.groupId) delete element.groupId
+      if(activeElementIdList.value.includes(element.id) && element.groupId) delete element.groupId
     }
     store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
     addHistorySnapshot()

+ 12 - 12
src/hooks/useCreateElement.ts

@@ -35,12 +35,12 @@ export default () => {
 
   const createElement = (element: PPTElement) => {
     store.commit(MutationTypes.ADD_ELEMENT, element)
-    store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [element.elId])
+    store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [element.id])
     addHistorySnapshot()
   }
 
-  const createImageElement = (imgUrl: string) => {
-    getImageSize(imgUrl).then(({ width, height }) => {
+  const createImageElement = (src: string) => {
+    getImageSize(src).then(({ width, height }) => {
       const scale = width / height
   
       if(scale < VIEWPORT_ASPECT_RATIO && width > VIEWPORT_SIZE) {
@@ -55,8 +55,8 @@ export default () => {
       createElement({
         ...DEFAULT_IMAGE,
         type: 'image',
-        elId: createRandomCode(),
-        imgUrl,
+        id: createRandomCode(),
+        src,
         width,
         height,
       })
@@ -67,7 +67,7 @@ export default () => {
     createElement({
       ...DEFAULT_CHART,
       type: 'chart',
-      elId: createRandomCode(),
+      id: createRandomCode(),
       chartType,
       data,
     })
@@ -87,7 +87,7 @@ export default () => {
     createElement({
       ...DEFAULT_TABLE,
       type: 'table',
-      elId: createRandomCode(),
+      id: createRandomCode(),
       width: colCount * DEFAULT_CELL_WIDTH + DEFAULT_BORDER_WIDTH,
       height: rowCount * DEFAULT_CELL_HEIGHT + DEFAULT_BORDER_WIDTH,
       colSizes,
@@ -101,7 +101,7 @@ export default () => {
     createElement({
       ...DEFAULT_TEXT,
       type: 'text',
-      elId: createRandomCode(),
+      id: createRandomCode(),
       left, 
       top, 
       width, 
@@ -114,7 +114,7 @@ export default () => {
     createElement({
       ...DEFAULT_SHAPE,
       type: 'shape',
-      elId: createRandomCode(),
+      id: createRandomCode(),
       left, 
       top, 
       width, 
@@ -123,17 +123,17 @@ export default () => {
     })
   }
   
-  const createLineElement = (position: LineElementPosition, marker: [string, string], lineType: string) => {
+  const createLineElement = (position: LineElementPosition, points: [string, string], lineType: string) => {
     const { left, top, start, end } = position
     createElement({
       ...DEFAULT_LINE,
       type: 'line',
-      elId: createRandomCode(),
+      id: createRandomCode(),
       left, 
       top, 
       start,
       end,
-      marker,
+      points,
       lineType,
     })
   }

+ 1 - 1
src/hooks/useDeleteElement.ts

@@ -13,7 +13,7 @@ export default () => {
 
   const deleteElement = () => {
     if(!activeElementIdList.value.length) return
-    const newElementList = currentSlide.value.elements.filter(el => !activeElementIdList.value.includes(el.elId))
+    const newElementList = currentSlide.value.elements.filter(el => !activeElementIdList.value.includes(el.id))
     store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
     store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
     addHistorySnapshot()

+ 4 - 4
src/hooks/useLockElement.ts

@@ -15,7 +15,7 @@ export default () => {
     const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
   
     for(const element of newElementList) {
-      if(activeElementIdList.value.includes(element.elId)) element.isLock = true
+      if(activeElementIdList.value.includes(element.id)) element.lock = true
     }
     store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
     addHistorySnapshot()
@@ -26,14 +26,14 @@ export default () => {
 
     if(handleElement.groupId) {
       for(const element of newElementList) {
-        if(element.groupId === handleElement.groupId) element.isLock = false
+        if(element.groupId === handleElement.groupId) element.lock = false
       }
       return newElementList
     }
     
     for(const element of newElementList) {
-      if(element.elId === handleElement.elId) {
-        element.isLock = false
+      if(element.id === handleElement.id) {
+        element.lock = false
         break
       }
     }

+ 1 - 1
src/hooks/useMoveElement.ts

@@ -14,7 +14,7 @@ export default () => {
 
   const moveElement = (command: string) => {
     const newElementList = currentSlide.value.elements.map(el => {
-      if(activeElementIdList.value.includes(el.elId)) {
+      if(activeElementIdList.value.includes(el.id)) {
         let { left, top } = el
         switch(command) {
           case KEYS.LEFT: 

+ 6 - 6
src/hooks/useOrderElement.ts

@@ -13,8 +13,8 @@ export default () => {
 
   // 获取组合元素层级范围(组合成员中的最大层级和最小层级)
   const getCombineElementIndexRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => {
-    const minIndex = elementList.findIndex(_element => _element.elId === combineElementList[0].elId)
-    const maxIndex = elementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
+    const minIndex = elementList.findIndex(_element => _element.id === combineElementList[0].id)
+    const maxIndex = elementList.findIndex(_element => _element.id === combineElementList[combineElementList.length - 1].id)
     return { minIndex, maxIndex }
   }
 
@@ -53,7 +53,7 @@ export default () => {
     else {
 
       // 元素在元素列表中的层级
-      const elementIndex = elementList.findIndex(item => item.elId === element.elId)
+      const elementIndex = elementList.findIndex(item => item.id === element.id)
 
       // 无法移动(已经处在顶层)
       if(elementIndex === elementList.length - 1) return null
@@ -96,7 +96,7 @@ export default () => {
     }
 
     else {
-      const elementIndex = elementList.findIndex(item => item.elId === element.elId)
+      const elementIndex = elementList.findIndex(item => item.id === element.id)
       if(elementIndex === 0) return null
       const prevElement = copyOfElementList[elementIndex - 1]
       const movedElement = copyOfElementList.splice(elementIndex, 1)[0]
@@ -133,7 +133,7 @@ export default () => {
     else {
 
       // 元素在元素列表中的层级
-      const elementIndex = elementList.findIndex(item => item.elId === element.elId)
+      const elementIndex = elementList.findIndex(item => item.id === element.id)
 
       // 无法移动(已经处在顶层)
       if(elementIndex === elementList.length - 1) return null
@@ -159,7 +159,7 @@ export default () => {
     }
 
     else {
-      const elementIndex = elementList.findIndex(item => item.elId === element.elId)
+      const elementIndex = elementList.findIndex(item => item.id === element.id)
       if(elementIndex === 0) return null
       copyOfElementList.splice(elementIndex, 1)
       copyOfElementList.unshift(element)

+ 4 - 4
src/hooks/usePasteTextClipboardData.ts

@@ -25,14 +25,14 @@ export default () => {
       if(groupId && !groupIdMap[groupId]) {
         groupIdMap[groupId] = createRandomCode()
       }
-      elIdMap[element.elId] = createRandomCode()
+      elIdMap[element.id] = createRandomCode()
     }
-    const currentSlideElementIdList = currentSlide.value.elements.map(el => el.elId)
+    const currentSlideElementIdList = currentSlide.value.elements.map(el => el.id)
     
     for(const element of elements) {
-      const inCurrentSlide = currentSlideElementIdList.includes(element.elId)
+      const inCurrentSlide = currentSlideElementIdList.includes(element.id)
       
-      element.elId = elIdMap[element.elId]
+      element.id = elIdMap[element.id]
 
       if(inCurrentSlide) {
         element.left = element.left + 10

+ 2 - 2
src/hooks/useSelectAllElement.ts

@@ -8,8 +8,8 @@ export default () => {
   const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
 
   const selectAllElement = () => {
-    const unlockedElements = currentSlide.value.elements.filter(el => !el.isLock)
-    const newActiveElementIdList = unlockedElements.map(el => el.elId)
+    const unlockedElements = currentSlide.value.elements.filter(el => !el.lock)
+    const newActiveElementIdList = unlockedElements.map(el => el.id)
     store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveElementIdList)
   }
 

+ 3 - 2
src/hooks/useSlideBackgroundStyle.ts

@@ -1,10 +1,11 @@
 import { Ref, computed } from 'vue'
+import { SlideBackground } from '@/types/slides'
 
-export default (background: Ref<[string, string] | undefined>) => {
+export default (background: Ref<SlideBackground | undefined>) => {
   const backgroundStyle = computed(() => {
     if(!background.value) return { backgroundColor: '#fff' }
 
-    const [type, value] = background.value
+    const { type, value } = background.value
     if(type === 'solid') return { backgroundColor: value }
     else if(type === 'image') return { backgroundImage: `url(${value}` }
 

+ 33 - 30
src/mocks/index.ts

@@ -3,66 +3,71 @@ import { Slide } from '@/types/slides'
 export const slides: Slide[] = [
   {
     id: 'xxx1',
-    background: ['solid', '#fff'],
+    background: {
+      type: 'solid',
+      value: '#fff',
+    },
     elements: [
       {
-        elId: 'xxx1',
+        id: 'xxx1',
         type: 'text',
         left: 190,
         top: 50,
         width: 320,
         height: 104,
         rotate: 0,
-        borderStyle: 'solid',
-        borderWidth: 4,
-        borderColor: '#5b7d89',
-        fill: 'rgba(220,220,220,0.8)',
-        shadow: '1px 1px 3px rgba(10,10,10,.5)',
+        fill: 'rgba(220, 220, 220, 0.8)',
+        shadow: {
+          h: 1,
+          v: 1,
+          blur: 3,
+          color: 'rgba(10, 10, 10, .5)'
+        },
         opacity: 1,
-        lineHeight: 1.5,
-        segmentSpacing: 10,
-        isLock: false,
+        lock: false,
         content: '<div style=\'text-align: center;\'><span style=\'font-size: 28px;\'><span style=\'color: rgb(232, 107, 153); font-weight: bold;\'>一段测试文字</span>,字号固定为<span style=\'font-weight: bold; font-style: italic; text-decoration-line: underline;\'>28px</span></span></div>',
       },
       {
-        elId: 'xxx3',
+        id: 'xxx3',
         type: 'image',
         left: 80,
         top: 250,
         width: 180,
         height: 180,
         rotate: 0,
-        borderStyle: 'solid',
-        borderWidth: 4,
-        borderColor: 'rgba(10,10,10,1)',
-        filter: '',
+        outline: {
+          width: 4,
+          style: 'solid',
+          color: '#333'
+        },
         clip: {
           range: [[30, 0], [100, 70]],
           shape: 'ellipse'
         },
-        lockRatio: false,
-        isLock: false,
-        imgUrl: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/1573622467064v2-7aa3ce420052983d91c6d01b47a7441d_hd.jpg',
+        fixedRatio: false,
+        lock: false,
+        src: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/1573622467064v2-7aa3ce420052983d91c6d01b47a7441d_hd.jpg',
       },
       {
-        elId: 'xxx2',
+        id: 'xxx2',
         type: 'image',
         left: 750,
         top: 320,
         width: 150,
         height: 150,
         rotate: 0,
-        borderStyle: 'solid',
-        borderWidth: 6,
-        borderColor: 'rgba(10,10,10,1)',
-        filter: '',
+        outline: {
+          width: 6,
+          style: 'solid',
+          color: '#333'
+        },
         clip: {
           range: [[0, 0], [100, 100]],
           shape: 'roundRect'
         },
-        lockRatio: true,
-        isLock: false,
-        imgUrl: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/62d9adb3-e7a6-4dc4-a352-095cffb49f08/b1be1a2f-f893-47d3-a8a3-eac7d04d395f/1596159381259v2-b2c69096d25ae16bf6ca09e30add3e65_hd.jpg',
+        fixedRatio: true,
+        lock: false,
+        src: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/62d9adb3-e7a6-4dc4-a352-095cffb49f08/b1be1a2f-f893-47d3-a8a3-eac7d04d395f/1596159381259v2-b2c69096d25ae16bf6ca09e30add3e65_hd.jpg',
       },
     ],
   },
@@ -70,7 +75,7 @@ export const slides: Slide[] = [
     id: 'sajd172',
     elements: [
       {
-        elId: 'yyx1',
+        id: 'yyx1',
         type: 'text',
         left: 590,
         top: 90,
@@ -78,9 +83,7 @@ export const slides: Slide[] = [
         height: 188,
         rotate: 0,
         opacity: 1,
-        lineHeight: 1.5,
-        segmentSpacing: 10,
-        isLock: false,
+        lock: false,
         content: '<div>😀 😐 😶 😜 🔔 ⭐ ⚡ 🔥 👍 💡 🔰 🎀 🎁 🥇 🏅 🏆 🎈 🎉 💎 🚧 ⛔ 📢 ⌛ ⏰ 🕒 🧩 🎵 📎 🔒 🔑 ⛳ 📌 📍 💬 📅 📈 📋 📜 📁 📱 💻 💾 🌏 🚚 🚡 🚢💧 🌐 🧭 💰 💳 🛒</div>',
       },
     ],

+ 3 - 3
src/store/getters.ts

@@ -13,20 +13,20 @@ export const getters: GetterTree<State, State> = {
     if(!animations) return null
 
     const els = currentSlide.elements
-    const elIds = els.map(el => el.elId)
+    const elIds = els.map(el => el.id)
     return animations.filter(animation => elIds.includes(animation.elId))
   },
 
   activeElementList(state) {
     const currentSlide = state.slides[state.slideIndex]
     if(!currentSlide || !currentSlide.elements) return []
-    return currentSlide.elements.filter(element => state.activeElementIdList.includes(element.elId))
+    return currentSlide.elements.filter(element => state.activeElementIdList.includes(element.id))
   },
 
   handleElement(state) {
     const currentSlide = state.slides[state.slideIndex]
     if(!currentSlide || !currentSlide.elements) return null
-    return currentSlide.elements.find(element => state.handleElementId === element.elId) || null
+    return currentSlide.elements.find(element => state.handleElementId === element.id) || null
   },
 
   canUndo(state) {

+ 4 - 4
src/store/mutations.ts

@@ -6,7 +6,7 @@ import { FONT_NAMES } from '@/configs/fontName'
 import { isSupportFontFamily } from '@/utils/fontFamily'
 
 interface UpdateElementData {
-  elId: string | string[];
+  id: string | string[];
   props: Partial<PPTElement>;
 }
 
@@ -91,13 +91,13 @@ export const mutations: MutationTree<State> = {
   },
 
   [MutationTypes.UPDATE_ELEMENT](state, data: UpdateElementData) {
-    const { elId, props } = data
-    const elIdList = typeof elId === 'string' ? [elId] : elId
+    const { id, props } = data
+    const elIdList = typeof id === 'string' ? [id] : id
 
     const slideIndex = state.slideIndex
     const slide = state.slides[slideIndex]
     const elements = slide.elements.map(el => {
-      return elIdList.includes(el.elId) ? { ...el, ...props } : el
+      return elIdList.includes(el.id) ? { ...el, ...props } : el
     })
     state.slides[slideIndex].elements = (elements as PPTElement[])
   },

+ 55 - 35
src/types/slides.ts

@@ -1,4 +1,9 @@
-export type ElementType = 'text' | 'image' | 'shape' | 'line' | 'chart' | 'table'
+export interface PPTElementShadow {
+  h: number;
+  v: number;
+  blur: number;
+  color: string;
+}
 
 export enum ElementTypes {
   TEXT = 'text',
@@ -10,58 +15,68 @@ export enum ElementTypes {
 }
 
 export interface PPTElementBaseProps {
-  elId: string;
+  id: string;
   left: number;
   top: number;
-  isLock?: boolean;
+  lock?: boolean;
   groupId?: string;
 }
 
-export interface PPTElementSizeProps {
-  width: number;
-  height: number;
+export interface PPTElementOutline {
+  style?: 'dashed' | 'solid';
+  width?: number;
+  color?: string;
 }
 
-export interface PPTElementBorderProps {
-  borderStyle?: string;
-  borderWidth?: number;
-  borderColor?: string;
-}
-
-export interface PPTTextElement extends PPTElementBaseProps, PPTElementSizeProps, PPTElementBorderProps {
+export interface PPTTextElement extends PPTElementBaseProps {
   type: 'text';
+  width: number;
+  height: number;
   content: string;
   rotate?: number;
+  outline?: PPTElementOutline;
   fill?: string;
   opacity?: number;
-  lineHeight?: number;
-  segmentSpacing?: number;
-  letterSpacing?: number;
-  shadow?: string;
+  shadow?: PPTElementShadow;
 }
 
-export interface PPTImageElement extends PPTElementBaseProps, PPTElementSizeProps, PPTElementBorderProps {
+export interface ImageElementFilters {
+  'blur': string;
+  'brightness': string;
+  'contrast': string;
+  'grayscale': string;
+  'saturate': string;
+  'hue-rotate': string;
+  'opacity': string;
+}
+export interface PPTImageElement extends PPTElementBaseProps {
   type: 'image';
-  lockRatio: boolean;
-  imgUrl: string;
+  width: number;
+  height: number;
+  fixedRatio: boolean;
+  src: string;
   rotate?: number;
-  filter?: string;
+  outline?: PPTElementOutline;
+  filters?: ImageElementFilters;
   clip?: {
     range: [[number, number], [number, number]];
     shape: 'rect' | 'roundRect' | 'ellipse' | 'triangle' | 'pentagon' | 'rhombus' | 'star';
   };
   flip?: { x?: number; y?: number };
-  shadow?: string;
+  shadow?: PPTElementShadow;
 }
 
-export interface PPTShapeElement extends PPTElementBaseProps, PPTElementSizeProps, PPTElementBorderProps {
+export interface PPTShapeElement extends PPTElementBaseProps {
   type: 'shape';
+  width: number;
+  height: number;
   svgCode: string;
-  lockRatio: boolean;
+  fixedRatio: boolean;
   fill: string;
   rotate?: number;
+  outline?: PPTElementOutline;
   opacity?: number;
-  shadow?: string;
+  shadow?: PPTElementShadow;
 }
 
 export interface PPTLineElement extends PPTElementBaseProps {
@@ -71,14 +86,17 @@ export interface PPTLineElement extends PPTElementBaseProps {
   width: number;
   style: string;
   color: string;
-  marker: [string, string];
+  points: [string, string];
   lineType: string;
 }
 
-export interface PPTChartElement extends PPTElementBaseProps, PPTElementSizeProps, PPTElementBorderProps {
+export interface PPTChartElement extends PPTElementBaseProps {
   type: 'chart';
+  width: number;
+  height: number;
   chartType: string;
   data: string;
+  outline?: PPTElementOutline;
   theme?: string;
 }
 
@@ -88,8 +106,10 @@ export interface TableElementCell {
   content: string;
   bgColor: string;
 }
-export interface PPTTableElement extends PPTElementBaseProps, PPTElementSizeProps {
+export interface PPTTableElement extends PPTElementBaseProps {
   type: 'table';
+  width: number;
+  height: number;
   borderTheme?: string;
   theme?: string;
   rowSizes: number[];
@@ -97,12 +117,7 @@ export interface PPTTableElement extends PPTElementBaseProps, PPTElementSizeProp
   data: TableElementCell[][];
 }
 
-export type PPTElement = PPTTextElement | 
-                         PPTImageElement | 
-                         PPTShapeElement | 
-                         PPTLineElement | 
-                         PPTChartElement |
-                         PPTTableElement
+export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PPTLineElement | PPTChartElement | PPTTableElement
 
 export interface PPTAnimation {
   elId: string;
@@ -110,9 +125,14 @@ export interface PPTAnimation {
   duration: number;
 }
 
+export interface SlideBackground {
+  type: 'solid' | 'image';
+  value: string;
+}
+
 export interface Slide {
   id: string;
   elements: PPTElement[];
-  background?: [string, string];
+  background?: SlideBackground;
   animations?: PPTAnimation[];
 }

+ 2 - 2
src/utils/element.ts

@@ -1,4 +1,4 @@
-import { PPTElement } from '@/types/slides'
+import { ElementTypes, PPTElement } from '@/types/slides'
 
 // 获取矩形旋转后在画布中的位置范围
 interface RotatedElementData {
@@ -46,7 +46,7 @@ export const getRectRotatedRange = (element: RotatedElementData) => {
 export const getElementRange = (element: PPTElement) => {
   let minX, maxX, minY, maxY
 
-  if(element.type === 'line') {
+  if(element.type === ElementTypes.LINE) {
     minX = element.left
     maxX = element.left + Math.max(element.start[0], element.end[0])
     minY = element.top

+ 2 - 2
src/utils/image.ts

@@ -4,10 +4,10 @@ interface ImageSize {
 }
 
 // 获取图片的原始宽高
-export const getImageSize = (imgUrl: string): Promise<ImageSize> => {
+export const getImageSize = (src: string): Promise<ImageSize> => {
   return new Promise(resolve => {
     const img = document.createElement('img')
-    img.src = imgUrl
+    img.src = src
     img.style.opacity = '0'
     document.body.appendChild(img)
 

+ 1 - 1
src/views/Editor/Canvas/MultiSelectOperate.vue

@@ -52,7 +52,7 @@ export default defineComponent({
     const store = useStore<State>()
     const activeElementIdList = computed(() => store.state.activeElementIdList)
     const canvasScale = computed(() => store.state.canvasScale)
-    const localActiveElementList = computed(() => props.elementList.filter(el => activeElementIdList.value.includes(el.elId)))
+    const localActiveElementList = computed(() => props.elementList.filter(el => activeElementIdList.value.includes(el.id)))
 
     const range = reactive({
       minX: 0,

+ 2 - 1
src/views/Editor/Canvas/SlideBackground.vue

@@ -14,6 +14,7 @@
 import { Ref, computed, defineComponent } from 'vue'
 import { useStore } from 'vuex'
 import { State } from '@/store'
+import { SlideBackground } from '@/types/slides'
 import GridLines from './GridLines.vue'
 import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
 
@@ -25,7 +26,7 @@ export default defineComponent({
   setup() {
     const store = useStore<State>()
     const showGridLines = computed(() => store.state.showGridLines)
-    const background: Ref<[string, string] | undefined> = computed(() => store.getters.currentSlide.background)
+    const background: Ref<SlideBackground | undefined> = computed(() => store.getters.currentSlide.background)
 
     const { backgroundStyle } = useSlideBackgroundStyle(background)
 

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

@@ -19,7 +19,7 @@ export default (
   const { addHistorySnapshot } = useHistorySnapshot()
 
   const dragElement = (e: MouseEvent, element: PPTElement) => {
-    if(!activeElementIdList.value.includes(element.elId)) return
+    if(!activeElementIdList.value.includes(element.id)) return
     let isMouseDown = true
 
     // 可视范围宽高,用于边缘对齐吸附
@@ -27,7 +27,7 @@ export default (
     const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
 
     const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
-    const originActiveElementList = originElementList.filter(el => activeElementIdList.value.includes(el.elId))
+    const originActiveElementList = originElementList.filter(el => activeElementIdList.value.includes(el.id))
 
     const sorptionRange = 3
     const elOriginLeft = element.left
@@ -40,7 +40,7 @@ export default (
 
     let isMisoperation: boolean | null = null
 
-    const isActiveGroupElement = element.elId === activeGroupElementId.value
+    const isActiveGroupElement = element.id === activeGroupElementId.value
 
     // 收集对齐参考线
     // 包括页面内出被操作元素以外的所有元素在页面内水平和垂直方向的范围和中心位置、页面边界和水平和垂直的中心位置
@@ -50,8 +50,8 @@ export default (
     // 元素在页面内水平和垂直方向的范围和中心位置(需要特殊计算线条和被旋转的元素)
     for(const el of elementList.value) {
       if(el.type === ElementTypes.LINE) continue
-      if(isActiveGroupElement && el.elId === element.elId) continue
-      if(!isActiveGroupElement && activeElementIdList.value.includes(el.elId)) continue
+      if(isActiveGroupElement && el.id === element.id) continue
+      if(!isActiveGroupElement && activeElementIdList.value.includes(el.id)) continue
 
       let left, top, width, height
       if('rotate' in el && el.rotate) {
@@ -145,7 +145,7 @@ export default (
           targetMinY = yRange[0]
           targetMaxY = yRange[1]
         }
-        else if(element.type === 'line') {
+        else if(element.type === ElementTypes.LINE) {
           targetMinX = targetLeft
           targetMaxX = targetLeft + Math.max(element.start[0], element.end[0])
           targetMinY = targetTop
@@ -179,7 +179,7 @@ export default (
             rightValues.push(xRange[1])
             bottomValues.push(yRange[1])
           }
-          else if(element.type === 'line') {
+          else if(element.type === ElementTypes.LINE) {
             leftValues.push(left)
             topValues.push(top)
             rightValues.push(left + Math.max(element.start[0], element.end[0]))
@@ -265,19 +265,19 @@ export default (
       // 非多选,或者当前操作的元素时激活的组合元素
       if(activeElementIdList.value.length === 1 || isActiveGroupElement) {
         elementList.value = elementList.value.map(el => {
-          return el.elId === element.elId ? { ...el, left: targetLeft, top: targetTop } : el
+          return el.id === element.id ? { ...el, left: targetLeft, top: targetTop } : el
         })
       }
 
       // 修改元素位置,如果需要修改位置的元素不是被操作的元素(例如多选下的操作)
       // 那么其他非操作元素要移动的位置通过操作元素的移动偏移量计算
       else {
-        const handleElement = elementList.value.find(el => el.elId === element.elId)
+        const handleElement = elementList.value.find(el => el.id === element.id)
         if(!handleElement) return
 
         elementList.value = elementList.value.map(el => {
-          if(activeElementIdList.value.includes(el.elId)) {
-            if(el.elId === element.elId) {
+          if(activeElementIdList.value.includes(el.id)) {
+            if(el.id === element.id) {
               return {
                 ...el,
                 left: targetLeft,

+ 4 - 4
src/views/Editor/Canvas/hooks/useMouseSelection.ts

@@ -110,19 +110,19 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | n
         }
 
         // 被锁定的元素除外
-        if(isInclude && !element.isLock) inRangeElementList.push(element)
+        if(isInclude && !element.lock) inRangeElementList.push(element)
       }
 
       // 对于组合元素成员,必须所有成员都在选择范围中才算被选中
       inRangeElementList = inRangeElementList.filter(inRangeElement => {
         if(inRangeElement.groupId) {
-          const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
+          const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.id)
           const groupElementList = elementList.value.filter(element => element.groupId === inRangeElement.groupId)
-          return groupElementList.every(groupElement => inRangeElementIdList.includes(groupElement.elId))
+          return groupElementList.every(groupElement => inRangeElementIdList.includes(groupElement.id))
         }
         return true
       })
-      const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
+      const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.id)
       if(inRangeElementIdList.length) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, inRangeElementIdList)
 
       mouseSelectionState.isShow = false

+ 1 - 1
src/views/Editor/Canvas/hooks/useRotateElement.ts

@@ -59,7 +59,7 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | n
       else if( angle < 0 && Math.abs(angle + 180) <= sorptionRange ) angle -= (angle + 180)
 
       // 修改元素角度
-      elementList.value = elementList.value.map(el => element.elId === el.elId ? { ...el, rotate: angle } : el)
+      elementList.value = elementList.value.map(el => element.id === el.id ? { ...el, rotate: angle } : el)
     }
 
     document.onmouseup = () => {

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

@@ -102,8 +102,8 @@ export default (
     const elOriginWidth = element.width
     const elOriginHeight = element.height
 
-    const isLockRatio = ctrlOrShiftKeyActive.value || ('lockRatio' in element && element.lockRatio)
-    const lockRatio = elOriginWidth / elOriginHeight
+    const fixedRatio = ctrlOrShiftKeyActive.value || ('fixedRatio' in element && element.fixedRatio)
+    const aspectRatio = elOriginWidth / elOriginHeight
     
     const elRotate = ('rotate' in element && element.rotate) ? element.rotate : 0
     const rotateRadian = Math.PI * elRotate / 180
@@ -133,13 +133,13 @@ export default (
     else {
       const edgeWidth = VIEWPORT_SIZE
       const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
-      const isActiveGroupElement = element.elId === activeGroupElementId.value
+      const isActiveGroupElement = element.id === activeGroupElementId.value
       
       for(const el of elementList.value) {
         if('rotate' in el && el.rotate) continue
         if(el.type === ElementTypes.LINE) continue
-        if(isActiveGroupElement && el.elId === element.elId) continue
-        if(!isActiveGroupElement && activeElementIdList.value.includes(el.elId)) continue
+        if(isActiveGroupElement && el.id === element.id) continue
+        if(!isActiveGroupElement && activeElementIdList.value.includes(el.id)) continue
 
         const left = el.left
         const top = el.top
@@ -236,9 +236,9 @@ export default (
         let revisedY = (Math.cos(rotateRadian) * y - Math.sin(rotateRadian) * x) / canvasScale.value
 
         // 锁定宽高比例
-        if(isLockRatio) {
-          if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) revisedY = revisedX / lockRatio
-          if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) revisedY = -revisedX / lockRatio
+        if(fixedRatio) {
+          if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) revisedY = revisedX / aspectRatio
+          if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) revisedY = -revisedX / aspectRatio
         }
 
         // 根据不同的操作点分别计算元素缩放后的大小和位置
@@ -297,18 +297,18 @@ export default (
         let moveX = x / canvasScale.value
         let moveY = y / canvasScale.value
 
-        if(isLockRatio) {
-          if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) moveY = moveX / lockRatio
-          if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) moveY = -moveX / lockRatio
+        if(fixedRatio) {
+          if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) moveY = moveX / aspectRatio
+          if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) moveY = -moveX / aspectRatio
         }
 
         if(command === OperatePoints.RIGHT_BOTTOM) {
           const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, elOriginTop + elOriginHeight + moveY)
           moveX = moveX - offsetX
           moveY = moveY - offsetY
-          if(isLockRatio) {
-            if(offsetY) moveX = moveY * lockRatio
-            else moveY = moveX / lockRatio
+          if(fixedRatio) {
+            if(offsetY) moveX = moveY * aspectRatio
+            else moveY = moveX / aspectRatio
           }
           width = getSizeWithinRange(elOriginWidth + moveX)
           height = getSizeWithinRange(elOriginHeight + moveY)
@@ -317,9 +317,9 @@ export default (
           const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + moveX, elOriginTop + elOriginHeight + moveY)
           moveX = moveX - offsetX
           moveY = moveY - offsetY
-          if(isLockRatio) {
-            if(offsetY) moveX = -moveY * lockRatio
-            else moveY = -moveX / lockRatio
+          if(fixedRatio) {
+            if(offsetY) moveX = -moveY * aspectRatio
+            else moveY = -moveX / aspectRatio
           }
           width = getSizeWithinRange(elOriginWidth - moveX)
           height = getSizeWithinRange(elOriginHeight + moveY)
@@ -329,9 +329,9 @@ export default (
           const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + moveX, elOriginTop + moveY)
           moveX = moveX - offsetX
           moveY = moveY - offsetY
-          if(isLockRatio) {
-            if(offsetY) moveX = moveY * lockRatio
-            else moveY = moveX / lockRatio
+          if(fixedRatio) {
+            if(offsetY) moveX = moveY * aspectRatio
+            else moveY = moveX / aspectRatio
           }
           width = getSizeWithinRange(elOriginWidth - moveX)
           height = getSizeWithinRange(elOriginHeight - moveY)
@@ -342,9 +342,9 @@ export default (
           const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, elOriginTop + moveY)
           moveX = moveX - offsetX
           moveY = moveY - offsetY
-          if(isLockRatio) {
-            if(offsetY) moveX = -moveY * lockRatio
-            else moveY = -moveX / lockRatio
+          if(fixedRatio) {
+            if(offsetY) moveX = -moveY * aspectRatio
+            else moveY = -moveX / aspectRatio
           }
           width = getSizeWithinRange(elOriginWidth + moveX)
           height = getSizeWithinRange(elOriginHeight - moveY)
@@ -374,7 +374,7 @@ export default (
         }
       }
       
-      elementList.value = elementList.value.map(el => element.elId === el.elId ? { ...el, left, top, width, height } : el)
+      elementList.value = elementList.value.map(el => element.id === el.id ? { ...el, left, top, width, height } : el)
     }
 
     document.onmouseup = e => {
@@ -396,7 +396,7 @@ export default (
     const { minX, maxX, minY, maxY } = range
     const operateWidth = maxX - minX
     const operateHeight = maxY - minY
-    const lockRatio = operateWidth / operateHeight
+    const aspectRatio = operateWidth / operateHeight
 
     const startPageX = e.pageX
     const startPageY = e.pageY
@@ -415,8 +415,8 @@ export default (
 
       // 锁定宽高比例
       if(ctrlOrShiftKeyActive.value) {
-        if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) y = x / lockRatio
-        if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) y = -x / lockRatio
+        if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) y = x / aspectRatio
+        if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) y = -x / aspectRatio
       }
 
       // 获取鼠标缩放时当前所有激活元素的范围
@@ -468,8 +468,8 @@ export default (
       // 根据上面计算的比例,修改所有被激活元素的位置大小
       // 宽高通过乘以对应的比例得到,位置通过将被操作元素在所有元素整体中的相对位置乘以对应比例获得
       elementList.value = elementList.value.map(el => {
-        if((el.type === ElementTypes.IMAGE || el.type === ElementTypes.SHAPE) && activeElementIdList.value.includes(el.elId)) {
-          const originElement = originElementList.find(originEl => originEl.elId === el.elId) as PPTImageElement | PPTShapeElement
+        if((el.type === ElementTypes.IMAGE || el.type === ElementTypes.SHAPE) && activeElementIdList.value.includes(el.id)) {
+          const originElement = originElementList.find(originEl => originEl.id === el.id) as PPTImageElement | PPTShapeElement
           return {
             ...el,
             width: originElement.width * widthScale,

+ 14 - 14
src/views/Editor/Canvas/hooks/useSelectElement.ts

@@ -19,25 +19,25 @@ export default (
     if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
 
     // 如果被点击的元素处于未激活状态,则将他设置为激活元素(单选),或者加入到激活元素中(多选)
-    if(!activeElementIdList.value.includes(element.elId)) {
+    if(!activeElementIdList.value.includes(element.id)) {
       let newActiveIdList: string[] = []
 
       if(ctrlOrShiftKeyActive.value) {
-        newActiveIdList = [...activeElementIdList.value, element.elId]
+        newActiveIdList = [...activeElementIdList.value, element.id]
       }
-      else newActiveIdList = [element.elId]
+      else newActiveIdList = [element.id]
       
       // 同时如果该元素是分组成员,需要将和他同组的元素一起激活
       if(element.groupId) {
         const groupMembersId: string[] = []
         elementList.value.forEach((el: PPTElement) => {
-          if(el.groupId === element.groupId) groupMembersId.push(el.elId)
+          if(el.groupId === element.groupId) groupMembersId.push(el.id)
         })
         newActiveIdList = [...newActiveIdList, ...groupMembersId]
       }
 
       store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, uniq(newActiveIdList))
-      store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.elId)
+      store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.id)
     }
 
     // 如果被点击的元素已激活,且按下了多选按钮,则取消其激活状态(除非该元素或分组是最后的一个激活元素)
@@ -48,12 +48,12 @@ export default (
       if(element.groupId) {
         const groupMembersId: string[] = []
         elementList.value.forEach((el: PPTElement) => {
-          if(el.groupId === element.groupId) groupMembersId.push(el.elId)
+          if(el.groupId === element.groupId) groupMembersId.push(el.id)
         })
-        newActiveIdList = activeElementIdList.value.filter(elId => !groupMembersId.includes(elId))
+        newActiveIdList = activeElementIdList.value.filter(id => !groupMembersId.includes(id))
       }
       else {
-        newActiveIdList = activeElementIdList.value.filter(elId => elId !== element.elId)
+        newActiveIdList = activeElementIdList.value.filter(id => id !== element.id)
       }
 
       if(newActiveIdList.length > 0) {
@@ -62,11 +62,11 @@ export default (
     }
 
     // 如果被点击的元素已激活,且没有按下多选按钮,且该元素不是当前操作元素,则将其设置为当前操作元素
-    else if(handleElementId.value !== element.elId) {
-      store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.elId)
+    else if(handleElementId.value !== element.id) {
+      store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.id)
     }
 
-    else if(activeGroupElementId.value !== element.elId && element.groupId) {
+    else if(activeGroupElementId.value !== element.id && element.groupId) {
       const startPageX = e.pageX
       const startPageY = e.pageY
 
@@ -75,7 +75,7 @@ export default (
         const currentPageY = e.pageY
 
         if(startPageX === currentPageX && startPageY === currentPageY) {
-          activeGroupElementId.value = element.elId
+          activeGroupElementId.value = element.id
           ;(e.target as HTMLElement).onmouseup = null
         }
       }
@@ -85,8 +85,8 @@ export default (
   }
 
   const selectAllElement = () => {
-    const unlockedElements = elementList.value.filter(el => !el.isLock)
-    const newActiveElementIdList = unlockedElements.map(el => el.elId)
+    const unlockedElements = elementList.value.filter(el => !el.lock)
+    const newActiveElementIdList = unlockedElements.map(el => el.id)
     store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveElementIdList)
   }
 

+ 4 - 4
src/views/Editor/Canvas/index.vue

@@ -42,12 +42,12 @@
 
       <EditableElement 
         v-for="(element, index) in elementList" 
-        :key="element.elId"
+        :key="element.id"
         :elementInfo="element"
         :elementIndex="index + 1"
-        :isActive="activeElementIdList.includes(element.elId)"
-        :isHandleEl="element.elId === handleElementId"
-        :isActiveGroupElement="activeGroupElementId === element.elId"
+        :isActive="activeElementIdList.includes(element.id)"
+        :isHandleEl="element.id === handleElementId"
+        :isActiveGroupElement="activeGroupElementId === element.id"
         :isMultiSelect="activeElementIdList.length > 1"
         :selectElement="selectElement"
         :rotateElement="rotateElement"

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

@@ -16,7 +16,7 @@
       <div class="background" :style="{ ...backgroundStyle }"></div>
       <BaseElement
         v-for="(element, index) in slide.elements"
-        :key="element.elId"
+        :key="element.id"
         :elementInfo="element"
         :elementIndex="index + 1"
       />

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

@@ -2,7 +2,7 @@
   <div 
     class="editable-element"
     ref="elementRef"
-    :id="'editable-element-' + elementInfo.elId"
+    :id="'editable-element-' + elementInfo.id"
     :style="{ zIndex: elementIndex }"
   >
     <component
@@ -105,7 +105,7 @@ export default defineComponent({
     const { copyElement, cutElement } = useCopyAndPasteElement()
 
     const contextmenus = (): ContextmenuItem[] => {
-      if(props.elementInfo.isLock) {
+      if(props.elementInfo.lock) {
         return [{
           text: '解锁', 
           icon: 'icon-unlock',

+ 0 - 59
src/views/_common/_element/ElementBorder.vue

@@ -1,59 +0,0 @@
-<template>
-  <SvgWrapper 
-    class="element-border"
-    overflow="visible" 
-    :width="width"
-    :height="height"
-  >
-    <path 
-      vector-effect="non-scaling-stroke" 
-      stroke-linecap="butt" 
-      stroke-miterlimit="8"
-      stroke-linejoin
-      fill="transparent"
-      :d="`M0,0 L${width},0 L${width},${height} L0,${height} Z`" 
-      :stroke="borderColor"
-      :stroke-width="borderWidth" 
-      :stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'" 
-    ></path>
-	</SvgWrapper>
-</template>
-
-<script lang="ts">
-import SvgWrapper from '@/components/SvgWrapper.vue'
-
-export default {
-  name: 'element-border', 
-  components: {
-    SvgWrapper,
-  },
-  props: {
-    width: {
-      type: Number,
-      required: true,
-    },
-    height: {
-      type: Number,
-      required: true,
-    },
-    borderColor: {
-      type: String,
-    },
-    borderWidth: {
-      type: Number,
-    },
-    borderStyle: {
-      type: String,
-    },
-  },
-}
-</script>
-
-<style lang="scss" scoped>
-svg {
-  overflow: visible;
-  position: absolute;
-  top: 0;
-  left: 0;
-}
-</style>

+ 69 - 0
src/views/_common/_element/ElementOutline.vue

@@ -0,0 +1,69 @@
+<template>
+  <SvgWrapper 
+    class="element-outline"
+    overflow="visible" 
+    :width="width"
+    :height="height"
+  >
+    <path 
+      vector-effect="non-scaling-stroke" 
+      stroke-linecap="butt" 
+      stroke-miterlimit="8"
+      stroke-linejoin
+      fill="transparent"
+      :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'" 
+    ></path>
+	</SvgWrapper>
+</template>
+
+<script lang="ts">
+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'
+
+export default defineComponent({
+  name: 'element-outline', 
+  components: {
+    SvgWrapper,
+  },
+  props: {
+    width: {
+      type: Number,
+      required: true,
+    },
+    height: {
+      type: Number,
+      required: true,
+    },
+    outline: {
+      type: Object as PropType<PPTElementOutline>
+    },
+  },
+  setup(props) {
+    const {
+      outlineWidth,
+      outlineStyle,
+      outlineColor,
+    } = useElementOutline(toRef(props, 'outline'))
+
+    return {
+      outlineWidth,
+      outlineStyle,
+      outlineColor,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+svg {
+  overflow: visible;
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+</style>

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

@@ -12,40 +12,34 @@
     <div 
       class="element-content"
       :style="{
-        filter: elementInfo.shadow ? `drop-shadow(${elementInfo.shadow})` : '',
+        filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
         transform: flip,
       }"
     >
-      <ImageRectBorder
+      <ImageRectOutline
         v-if="clipShape.type === 'rect'"
         :width="elementInfo.width"
         :height="elementInfo.height"
         :radius="clipShape.radius"
-        :borderColor="elementInfo.borderColor"
-        :borderWidth="elementInfo.borderWidth"
-        :borderStyle="elementInfo.borderStyle"
+        :outline="elementInfo.outline"
       />
-      <ImageEllipseBorder
+      <ImageEllipseOutline
         v-else-if="clipShape.type === 'ellipse'"
         :width="elementInfo.width"
         :height="elementInfo.height"
-        :borderColor="elementInfo.borderColor"
-        :borderWidth="elementInfo.borderWidth"
-        :borderStyle="elementInfo.borderStyle"
+        :outline="elementInfo.outline"
       />
-      <ImagePolygonBorder
+      <ImagePolygonOutline
         v-else-if="clipShape.type === 'polygon'"
         :width="elementInfo.width"
         :height="elementInfo.height"
         :createPath="clipShape.createPath"
-        :borderColor="elementInfo.borderColor"
-        :borderWidth="elementInfo.borderWidth"
-        :borderStyle="elementInfo.borderStyle"
+        :outline="elementInfo.outline"
       />
 
       <div class="img-wrapper" :style="{ clipPath: clipShape.style }">
         <img 
-          :src="elementInfo.imgUrl" 
+          :src="elementInfo.src" 
           :draggable="false" 
           :style="{
             top: imgPosition.top,
@@ -67,16 +61,18 @@ import { computed, defineComponent, PropType } from 'vue'
 import { PPTImageElement } from '@/types/slides'
 import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
 
-import ImageRectBorder from './ImageRectBorder.vue'
-import ImageEllipseBorder from './ImageEllipseBorder.vue'
-import ImagePolygonBorder from './ImagePolygonBorder.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: 'base-element-image',
   components: {
-    ImageRectBorder,
-    ImageEllipseBorder,
-    ImagePolygonBorder,
+    ImageRectOutline,
+    ImageEllipseOutline,
+    ImagePolygonOutline,
   },
   props: {
     elementInfo: {
@@ -118,10 +114,10 @@ export default defineComponent({
     })
 
     const filter = computed(() => {
-      if(!props.elementInfo.filter) return ''
+      if(!props.elementInfo.filters) return ''
       let filter = ''
-      for(const key of Object.keys(props.elementInfo.filter)) {
-        filter += `${key}(${props.elementInfo.filter[key]}) `
+      for(const key of Object.keys(props.elementInfo.filters)) {
+        filter += `${key}(${props.elementInfo.filters[key]}) `
       }
       return filter
     })
@@ -135,11 +131,15 @@ export default defineComponent({
       return ''
     })
 
+    const shadow = computed(() => props.elementInfo.shadow)
+    const { shadowStyle } = useElementShadow(shadow)
+
     return {
       imgPosition,
       clipShape,
       filter,
       flip,
+      shadowStyle,
     }
   },
 })

+ 3 - 3
src/views/_common/_element/ImageElement/ImageClipHandler.vue

@@ -6,7 +6,7 @@
   >
     <img 
       class="bottom-img" 
-      :src="imgUrl" 
+      :src="src" 
       :draggable="false" 
       alt="" 
       :style="bottomImgPositionStyle" 
@@ -21,7 +21,7 @@
     >
       <img 
         class="top-img" 
-        :src="imgUrl" 
+        :src="src" 
         :draggable="false" 
         alt="" 
         :style="topImgPositionStyle" 
@@ -78,7 +78,7 @@ export default defineComponent({
     SvgWrapper,
   },
   props: {
-    imgUrl: {
+    src: {
       type: String,
       required: true,
     },

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

@@ -1,66 +0,0 @@
-<template>
-  <SvgWrapper 
-    class="image-ellipse-border" 
-    overflow="visible" 
-    :width="width"
-    :height="height"
-  >
-    <ellipse 
-      vector-effect="non-scaling-stroke" 
-      stroke-linecap="butt" 
-      stroke-miterlimit="8"
-      stroke-linejoin
-      fill="transparent"
-      :cx="width / 2" 
-      :cy="height / 2"
-      :rx="width / 2" 
-      :ry="height / 2"
-      :stroke="borderColor"
-      :stroke-width="borderWidth" 
-      :stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'" 
-    ></ellipse>
-	</SvgWrapper>
-</template>
-
-<script lang="ts">
-import SvgWrapper from '@/components/SvgWrapper.vue'
-
-export default {
-  name: 'image-ellipse-border',
-  components: {
-    SvgWrapper,
-  },
-  props: {
-    width: {
-      type: Number,
-      required: true,
-    },
-    height: {
-      type: Number,
-      required: true,
-    },
-    borderColor: {
-      type: String,
-      default: '',
-    },
-    borderWidth: {
-      type: Number,
-      default: 0,
-    },
-    borderStyle: {
-      type: String,
-      default: '',
-    },
-  },
-}
-</script>
-
-<style lang="scss" scoped>
-svg {
-  overflow: visible;
-  position: absolute;
-  z-index: 2;
-  top: 0;
-  left: 0;
-}
-</style>

+ 73 - 0
src/views/_common/_element/ImageElement/ImageEllipseOutline.vue

@@ -0,0 +1,73 @@
+<template>
+  <SvgWrapper 
+    class="image-ellipse-outline" 
+    overflow="visible" 
+    :width="width"
+    :height="height"
+  >
+    <ellipse 
+      vector-effect="non-scaling-stroke" 
+      stroke-linecap="butt" 
+      stroke-miterlimit="8"
+      stroke-linejoin
+      fill="transparent"
+      :cx="width / 2" 
+      :cy="height / 2"
+      :rx="width / 2" 
+      :ry="height / 2"
+      :stroke="outlineColor"
+      :stroke-width="outlineWidth" 
+      :stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'" 
+    ></ellipse>
+	</SvgWrapper>
+</template>
+
+<script lang="ts">
+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'
+
+export default defineComponent({
+  name: 'image-ellipse-outline',
+  components: {
+    SvgWrapper,
+  },
+  props: {
+    width: {
+      type: Number,
+      required: true,
+    },
+    height: {
+      type: Number,
+      required: true,
+    },
+    outline: {
+      type: Object as PropType<PPTElementOutline>
+    },
+  },
+  setup(props) {
+    const {
+      outlineWidth,
+      outlineStyle,
+      outlineColor,
+    } = useElementOutline(toRef(props, 'outline'))
+
+    return {
+      outlineWidth,
+      outlineStyle,
+      outlineColor,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+svg {
+  overflow: visible;
+  position: absolute;
+  z-index: 2;
+  top: 0;
+  left: 0;
+}
+</style>

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

@@ -1,67 +0,0 @@
-<template>
-  <SvgWrapper 
-    class="image-polygon-border" 
-    overflow="visible" 
-    :width="width"
-    :height="height"
-  >
-    <path 
-      vector-effect="non-scaling-stroke" 
-      stroke-linecap="butt" 
-      stroke-miterlimit="8"
-      stroke-linejoin
-      fill="transparent"
-      :d="createPath(width, height)"
-      :stroke="borderColor"
-      :stroke-width="borderWidth" 
-      :stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'" 
-    ></path>
-	</SvgWrapper>
-</template>
-
-<script lang="ts">
-import SvgWrapper from '@/components/SvgWrapper.vue'
-
-export default {
-  name: 'image-polygon-border',
-  components: {
-    SvgWrapper,
-  },
-  props: {
-    width: {
-      type: Number,
-      required: true,
-    },
-    height: {
-      type: Number,
-      required: true,
-    },
-    borderColor: {
-      type: String,
-      default: '',
-    },
-    borderWidth: {
-      type: Number,
-      default: 0,
-    },
-    borderStyle: {
-      type: String,
-      default: '',
-    },
-    createPath: {
-      type: Function,
-      required: true,
-    },
-  },
-}
-</script>
-
-<style lang="scss" scoped>
-svg {
-  overflow: visible;
-  position: absolute;
-  z-index: 2;
-  top: 0;
-  left: 0;
-}
-</style>

+ 74 - 0
src/views/_common/_element/ImageElement/ImagePolygonOutline.vue

@@ -0,0 +1,74 @@
+<template>
+  <SvgWrapper 
+    class="image-polygon-outline" 
+    overflow="visible" 
+    :width="width"
+    :height="height"
+  >
+    <path 
+      vector-effect="non-scaling-stroke" 
+      stroke-linecap="butt" 
+      stroke-miterlimit="8"
+      stroke-linejoin
+      fill="transparent"
+      :d="createPath(width, height)"
+      :stroke="outlineColor"
+      :stroke-width="outlineWidth" 
+      :stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'" 
+    ></path>
+	</SvgWrapper>
+</template>
+
+<script lang="ts">
+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'
+
+export default defineComponent({
+  name: 'image-polygon-outline',
+  components: {
+    SvgWrapper,
+  },
+  props: {
+    width: {
+      type: Number,
+      required: true,
+    },
+    height: {
+      type: Number,
+      required: true,
+    },
+    outline: {
+      type: Object as PropType<PPTElementOutline>
+    },
+    createPath: {
+      type: Function,
+      required: true,
+    },
+  },
+  setup(props) {
+    const {
+      outlineWidth,
+      outlineStyle,
+      outlineColor,
+    } = useElementOutline(toRef(props, 'outline'))
+
+    return {
+      outlineWidth,
+      outlineStyle,
+      outlineColor,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+svg {
+  overflow: visible;
+  position: absolute;
+  z-index: 2;
+  top: 0;
+  left: 0;
+}
+</style>

+ 25 - 18
src/views/_common/_element/ImageElement/ImageRectBorder.vue

@@ -1,6 +1,6 @@
 <template>
   <SvgWrapper 
-    class="image-rect-border" 
+    class="image-rect-outline" 
     overflow="visible" 
     :width="width"
     :height="height"
@@ -15,18 +15,21 @@
       :ry="radius"
       :width="width"
       :height="height"
-      :stroke="borderColor"
-      :stroke-width="borderWidth" 
-      :stroke-dasharray="borderStyle === 'dashed' ? '12 9' : '0 0'" 
+      :stroke="outlineColor"
+      :stroke-width="outlineWidth" 
+      :stroke-dasharray="outlineStyle === 'dashed' ? '12 9' : '0 0'" 
     ></rect>
 	</SvgWrapper>
 </template>
 
 <script lang="ts">
+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'
 
-export default {
-  name: 'image-rect-border',
+export default defineComponent({
+  name: 'image-rect-outline',
   components: {
     SvgWrapper,
   },
@@ -39,24 +42,28 @@ export default {
       type: Number,
       required: true,
     },
-    borderColor: {
-      type: String,
-      default: '',
-    },
-    borderWidth: {
-      type: Number,
-      default: 0,
-    },
-    borderStyle: {
-      type: String,
-      default: '',
+    outline: {
+      type: Object as PropType<PPTElementOutline>
     },
     radius: {
       type: String,
       default: '0',
     },
   },
-}
+  setup(props) {
+    const {
+      outlineWidth,
+      outlineStyle,
+      outlineColor,
+    } = useElementOutline(toRef(props, 'outline'))
+
+    return {
+      outlineWidth,
+      outlineStyle,
+      outlineColor,
+    }
+  },
+})
 </script>
 
 <style lang="scss" scoped>

+ 29 - 29
src/views/_common/_element/ImageElement/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div 
     class="editable-element image"
-    :class="{ 'lock': elementInfo.isLock }"
+    :class="{ 'lock': elementInfo.lock }"
     :style="{
       top: elementInfo.top + 'px',
       left: elementInfo.left + 'px',
@@ -13,7 +13,7 @@
   >
     <ImageClip
       v-if="isCliping"
-      :imgUrl="elementInfo.imgUrl"
+      :src="elementInfo.src"
       :clipData="elementInfo.clip"
       :canvasScale="canvasScale"
       :width="elementInfo.width"
@@ -29,40 +29,34 @@
       v-if="!isCliping"
       v-contextmenu="contextmenus"
       :style="{
-        filter: elementInfo.shadow ? `drop-shadow(${elementInfo.shadow})` : '',
+        filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
         transform: flip,
       }"
     >
-      <ImageRectBorder
+      <ImageRectOutline
         v-if="clipShape.type === 'rect'"
         :width="elementInfo.width"
         :height="elementInfo.height"
         :radius="clipShape.radius"
-        :borderColor="elementInfo.borderColor"
-        :borderWidth="elementInfo.borderWidth"
-        :borderStyle="elementInfo.borderStyle"
+        :outline="elementInfo.outline"
       />
-      <ImageEllipseBorder
+      <ImageEllipseOutline
         v-else-if="clipShape.type === 'ellipse'"
         :width="elementInfo.width"
         :height="elementInfo.height"
-        :borderColor="elementInfo.borderColor"
-        :borderWidth="elementInfo.borderWidth"
-        :borderStyle="elementInfo.borderStyle"
+        :outline="elementInfo.outline"
       />
-      <ImagePolygonBorder
+      <ImagePolygonOutline
         v-else-if="clipShape.type === 'polygon'"
         :width="elementInfo.width"
         :height="elementInfo.height"
+        :outline="elementInfo.outline"
         :createPath="clipShape.createPath"
-        :borderColor="elementInfo.borderColor"
-        :borderWidth="elementInfo.borderWidth"
-        :borderStyle="elementInfo.borderStyle"
       />
 
       <div class="img-wrapper" :style="{clipPath: clipShape.style}">
         <img 
-          :src="elementInfo.imgUrl" 
+          :src="elementInfo.src" 
           :draggable="false" 
           :style="{
             top: imgPosition.top,
@@ -93,7 +87,7 @@
         :type="line.type" 
         :style="line.style" 
       />
-      <template v-if="!elementInfo.isLock && (isActiveGroupElement || !isMultiSelect)">
+      <template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
         <ResizablePoint 
           class="el-resizable-point" 
           v-for="point in resizablePoints"
@@ -119,7 +113,7 @@ import { computed, defineComponent, ref, PropType } from 'vue'
 
 import { PPTImageElement } from '@/types/slides'
 import { ElementScaleHandler } from '@/types/edit'
-import useCommonOperate from '@/views/_common/_element/useCommonOperate'
+import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
 
 import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
 
@@ -129,9 +123,11 @@ import BorderLine from '@/views/_common/_operate/BorderLine.vue'
 import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
 
 import ImageClip, { ClipedEmitData } from './ImageClipHandler.vue'
-import ImageRectBorder from './ImageRectBorder.vue'
-import ImageEllipseBorder from './ImageEllipseBorder.vue'
-import ImagePolygonBorder from './ImagePolygonBorder.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',
@@ -141,9 +137,9 @@ export default defineComponent({
     BorderLine,
     AnimationIndex,
     ImageClip,
-    ImageRectBorder,
-    ImageEllipseBorder,
-    ImagePolygonBorder,
+    ImageRectOutline,
+    ImageEllipseOutline,
+    ImagePolygonOutline,
   },
   props: {
     elementInfo: {
@@ -198,7 +194,7 @@ export default defineComponent({
 
     const { resizablePoints, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
 
-    const isCliping = computed(() => clipingImageElId.value === props.elementInfo.elId)
+    const isCliping = computed(() => clipingImageElId.value === props.elementInfo.id)
 
     const imgPosition = computed(() => {
       if(!props.elementInfo || !props.elementInfo.clip) {
@@ -233,10 +229,10 @@ export default defineComponent({
     })
 
     const filter = computed(() => {
-      if(!props.elementInfo.filter) return ''
+      if(!props.elementInfo.filters) return ''
       let filter = ''
-      for(const key of Object.keys(props.elementInfo.filter)) {
-        filter += `${key}(${props.elementInfo.filter[key]}) `
+      for(const key of Object.keys(props.elementInfo.filters)) {
+        filter += `${key}(${props.elementInfo.filters[key]}) `
       }
       return filter
     })
@@ -250,8 +246,11 @@ export default defineComponent({
       return ''
     })
 
+    const shadow = computed(() => props.elementInfo.shadow)
+    const { shadowStyle } = useElementShadow(shadow)
+
     const handleSelectElement = (e: MouseEvent) => {
-      if(isCliping.value || props.elementInfo.isLock) return
+      if(isCliping.value || props.elementInfo.lock) return
       e.stopPropagation()
       props.selectElement(e, props.elementInfo)
     }
@@ -283,6 +282,7 @@ export default defineComponent({
       borderLines,
       filter,
       flip,
+      shadowStyle,
       handleSelectElement,
       clip,
     }

+ 17 - 10
src/views/_common/_element/TextElement/BaseTextElement.vue

@@ -12,17 +12,13 @@
       :style="{
         backgroundColor: elementInfo.fill,
         opacity: elementInfo.opacity,
-        textShadow: elementInfo.shadow,
-        lineHeight: elementInfo.lineHeight,
-        letterSpacing: (elementInfo.letterSpacing || 0) + 'px',
+        textShadow: shadowStyle,
       }"
     >
-      <ElementBorder
+      <ElementOutline
         :width="elementInfo.width"
         :height="elementInfo.height"
-        :borderColor="elementInfo.borderColor"
-        :borderWidth="elementInfo.borderWidth"
-        :borderStyle="elementInfo.borderStyle"
+        :outline="elementInfo.outline"
       />
       <div class="text-content"
         v-html="elementInfo.content" 
@@ -32,14 +28,16 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, PropType } from 'vue'
+import { defineComponent, PropType, computed } from 'vue'
 import { PPTTextElement } from '@/types/slides'
-import ElementBorder from '@/views/_common/_element/ElementBorder.vue'
+import ElementOutline from '@/views/_common/_element/ElementOutline.vue'
+
+import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
 
 export default defineComponent({
   name: 'base-element-text',
   components: {
-    ElementBorder,
+    ElementOutline,
   },
   props: {
     elementInfo: {
@@ -47,6 +45,14 @@ export default defineComponent({
       required: true,
     },
   },
+  setup(props) {
+    const shadow = computed(() => props.elementInfo.shadow)
+    const { shadowStyle } = useElementShadow(shadow)
+
+    return {
+      shadowStyle,
+    }
+  },
 })
 </script>
 
@@ -58,6 +64,7 @@ export default defineComponent({
 .element-content {
   position: relative;
   padding: 10px;
+  line-height: 1.5;
 
   .text-content {
     position: relative;

+ 17 - 14
src/views/_common/_element/TextElement/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div 
     class="editable-element text" 
-    :class="{ 'lock': elementInfo.isLock }"
+    :class="{ 'lock': elementInfo.lock }"
     :style="{
       top: elementInfo.top + 'px',
       left: elementInfo.left + 'px',
@@ -14,22 +14,18 @@
       :style="{
         backgroundColor: elementInfo.fill,
         opacity: elementInfo.opacity,
-        textShadow: elementInfo.shadow,
-        lineHeight: elementInfo.lineHeight,
-        letterSpacing: (elementInfo.letterSpacing || 0) + 'px',
+        textShadow: shadowStyle,
       }"
       v-contextmenu="contextmenus"
     >
-      <ElementBorder
+      <ElementOutline
         :width="elementInfo.width"
         :height="elementInfo.height"
-        :borderColor="elementInfo.borderColor"
-        :borderWidth="elementInfo.borderWidth"
-        :borderStyle="elementInfo.borderStyle"
+        :outline="elementInfo.outline"
       />
       <div class="text-content"
         v-html="elementInfo.content" 
-        :contenteditable="isActive && !elementInfo.isLock"
+        :contenteditable="isActive && !elementInfo.lock"
       ></div>
     </div>
 
@@ -52,7 +48,7 @@
         :isWide="true"
         @mousedown="handleSelectElement($event)"
       />
-      <template v-if="!elementInfo.isLock && (isActiveGroupElement || !isMultiSelect)">
+      <template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
         <ResizablePoint class="el-resizable-point" 
           v-for="point in resizablePoints"
           :key="point.type"
@@ -77,18 +73,20 @@ import { computed, defineComponent, PropType } from 'vue'
 
 import { PPTTextElement } from '@/types/slides'
 import { ElementScaleHandler } from '@/types/edit'
-import useCommonOperate from '@/views/_common/_element/useCommonOperate'
+import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
 
-import ElementBorder from '@/views/_common/_element/ElementBorder.vue'
+import ElementOutline from '@/views/_common/_element/ElementOutline.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'
 
+import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
+
 export default defineComponent({
   name: 'editable-element-text',
   components: {
-    ElementBorder,
+    ElementOutline,
     RotateHandler,
     ResizablePoint,
     BorderLine,
@@ -146,17 +144,21 @@ export default defineComponent({
     const { resizablePoints, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
 
     const handleSelectElement = (e: MouseEvent, canMove = true) => {
-      if(props.elementInfo.isLock) return
+      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,
       resizablePoints,
       borderLines,
       handleSelectElement,
+      shadowStyle,
     }
   },
 })
@@ -182,6 +184,7 @@ export default defineComponent({
 .element-content {
   position: relative;
   padding: 10px;
+  line-height: 1.5;
 
   .text-content {
     position: relative;

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


+ 14 - 0
src/views/_common/_element/hooks/useElementOutline.ts

@@ -0,0 +1,14 @@
+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 : '#41464b')
+
+  return {
+    outlineWidth,
+    outlineStyle,
+    outlineColor,
+  }
+}

+ 14 - 0
src/views/_common/_element/hooks/useElementShadow.ts

@@ -0,0 +1,14 @@
+import { Ref } from 'vue'
+import { PPTElementShadow } from '@/types/slides'
+
+export default (shadow: Ref<PPTElementShadow | undefined>) => {
+  let shadowStyle = ''
+  if(shadow.value) {
+    const { h, v, blur, color } = shadow.value
+    shadowStyle = `${h} ${v} ${blur} ${color}`
+  }
+
+  return {
+    shadowStyle,
+  }
+}