pipipi-pikachu 5 年之前
父節點
當前提交
d040a92b18

+ 17 - 0
src/hooks/useSlideBackgroundStyle.ts

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

+ 3 - 11
src/views/Editor/Canvas/SlideBackground.vue

@@ -14,8 +14,8 @@
 import { Ref, computed, defineComponent } from 'vue'
 import { useStore } from 'vuex'
 import { State } from '@/store'
-import { Slide } from '@/types/slides'
 import GridLines from './GridLines.vue'
+import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
 
 export default defineComponent({
   name: 'slide-background',
@@ -25,17 +25,9 @@ export default defineComponent({
   setup() {
     const store = useStore<State>()
     const showGridLines = computed(() => store.state.showGridLines)
-    const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
+    const background: Ref<[string, string] | undefined> = computed(() => store.getters.currentSlide.background)
 
-    const backgroundStyle = computed(() => {
-      if(!currentSlide.value.background) return { backgroundColor: '#fff' }
-
-      const [type, value] = currentSlide.value.background
-      if(type === 'solid') return { backgroundColor: value }
-      else if(type === 'image') return { backgroundImage: `url(${value}` }
-
-      return { backgroundColor: '#fff' }
-    })
+    const { backgroundStyle } = useSlideBackgroundStyle(background)
 
     return {
       showGridLines,

+ 7 - 2
src/views/Editor/Thumbnails/index.vue

@@ -16,7 +16,7 @@
       @end="handleDragEnd"
       itemKey="id"
     >
-      <template #item="{ index }">
+      <template #item="{ element, index }">
         <div
           class="thumbnail-wrapper"
           :class="{ 'active': slideIndex === index }"
@@ -24,7 +24,9 @@
           v-contextmenu="contextmenus"
         >
           <div class="slide-index">{{ fillDigit(index + 1, 2) }}</div>
-          <div class="thumbnail"></div>
+          <div class="thumbnail">
+            <ThumbnailSlide :slide="element" :size="120" />
+          </div>
         </div>
       </template>
     </draggable>
@@ -40,10 +42,13 @@ import { fillDigit } from '@/utils/common'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
 import useSlideHandler from '@/hooks/useSlideHandler'
 
+import ThumbnailSlide from '@/views/_common/ThumbnailSlide.vue'
+
 export default defineComponent({
   name: 'thumbnails',
   components: {
     draggable,
+    ThumbnailSlide,
   },
   setup() {
     const store = useStore<State>()

+ 77 - 0
src/views/_common/ThumbnailSlide.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="thumbnail-slide"
+    :style="{
+      width: size + 'px',
+      height: size * VIEWPORT_ASPECT_RATIO + 'px',
+    }"
+  >
+    <div 
+      class="elements-wrapper"
+      :style="{
+        width: VIEWPORT_SIZE + 'px',
+        height: VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO + 'px',
+        transform: `scale(${size / VIEWPORT_SIZE})`,
+      }"
+    >
+      <div class="background" :style="{ ...backgroundStyle }"></div>
+
+      <template v-for="(element, index) in slide.elements" :key="element.elId">
+        <BaseElement
+          :elementInfo="element"
+          :elementIndex="index + 1"
+        />
+      </template>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, PropType, defineComponent } from 'vue'
+import { Slide } from '@/types/slides'
+import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
+import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
+
+import BaseElement from '@/views/_common/_element/BaseElement.vue'
+
+export default defineComponent({
+  name: 'thumbnail-slide',
+  components: {
+    BaseElement,
+  },
+  props: {
+    slide: {
+      type: Object as PropType<Slide>,
+      required: true,
+    },
+    size: {
+      type: Number,
+      required: true,
+    },
+  },
+  setup(props) {
+    const background = computed(() => props.slide.background)
+    const { backgroundStyle } = useSlideBackgroundStyle(background)
+
+    return {
+      backgroundStyle,
+      VIEWPORT_SIZE,
+      VIEWPORT_ASPECT_RATIO,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.thumbnail-slide {
+  background-color: #fff;
+  overflow: hidden;
+}
+.elements-wrapper {
+  transform-origin: 0 0;
+}
+.background {
+  background-position: center;
+  background-size: cover;
+  position: absolute;
+}
+</style>

+ 46 - 0
src/views/_common/_element/BaseElement.vue

@@ -0,0 +1,46 @@
+<template>
+  <div 
+    class="base-element"
+    :style="{ zIndex: elementIndex }"
+  >
+    <component
+      :is="currentElementComponent"
+      :elementInfo="elementInfo"
+    ></component>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue'
+import { PPTElement } from '@/types/slides'
+
+import BaseImageElement from './ImageElement/BaseImageElement.vue'
+import BaseTextElement from './TextElement/BaseTextElement.vue'
+
+export default defineComponent({
+  name: 'editable-element',
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTElement>,
+      required: true,
+    },
+    elementIndex: {
+      type: Number,
+      required: true,
+    },
+  },
+  setup(props) {
+    const currentElementComponent = computed(() => {
+      const elementTypeMap = {
+        'image': BaseImageElement,
+        'text': BaseTextElement,
+      }
+      return elementTypeMap[props.elementInfo.type] || null
+    })
+
+    return {
+      currentElementComponent,
+    }
+  },
+})
+</script>

+ 169 - 0
src/views/_common/_element/ImageElement/BaseImageElement.vue

@@ -0,0 +1,169 @@
+<template>
+  <div 
+    class="base-element image"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px',
+      transform: `rotate(${elementInfo.rotate}deg)`,
+    }"
+  >
+    <div 
+      class="element-content"
+      :style="{
+        filter: elementInfo.shadow ? `drop-shadow(${elementInfo.shadow})` : '',
+        transform: flip,
+      }"
+    >
+      <ImageRectBorder
+        v-if="clipShape.type === 'rect'"
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :radius="clipShape.radius"
+        :borderColor="elementInfo.borderColor"
+        :borderWidth="elementInfo.borderWidth"
+        :borderStyle="elementInfo.borderStyle"
+      />
+      <ImageEllipseBorder
+        v-else-if="clipShape.type === 'ellipse'"
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :borderColor="elementInfo.borderColor"
+        :borderWidth="elementInfo.borderWidth"
+        :borderStyle="elementInfo.borderStyle"
+      />
+      <ImagePolygonBorder
+        v-else-if="clipShape.type === 'polygon'"
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :createPath="clipShape.createPath"
+        :borderColor="elementInfo.borderColor"
+        :borderWidth="elementInfo.borderWidth"
+        :borderStyle="elementInfo.borderStyle"
+      />
+
+      <div class="img-wrapper" :style="{ clipPath: clipShape.style }">
+        <img 
+          :src="elementInfo.imgUrl" 
+          :draggable="false" 
+          :style="{
+            top: imgPosition.top,
+            left: imgPosition.left,
+            width: imgPosition.width,
+            height: imgPosition.height,
+            filter: filter,
+          }" 
+          alt=""
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+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'
+
+export default defineComponent({
+  name: 'base-element-image',
+  components: {
+    ImageRectBorder,
+    ImageEllipseBorder,
+    ImagePolygonBorder,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTImageElement>,
+      required: true,
+    },
+  },
+  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.filter) return ''
+      let filter = ''
+      for(const key of Object.keys(props.elementInfo.filter)) {
+        filter += `${key}(${props.elementInfo.filter[key]}) `
+      }
+      return filter
+    })
+
+    const flip = computed(() => {
+      if(!props.elementInfo.flip) return ''
+      const { x, y } = props.elementInfo.flip
+      if(x && y) return `rotateX(${x}deg) rotateY(${y}deg)`
+      else if(x) return `rotateX(${x}deg)`
+      else if(y) return `rotateY(${y}deg)`
+      return ''
+    })
+
+    return {
+      imgPosition,
+      clipShape,
+      filter,
+      flip,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.base-element {
+  position: absolute;
+}
+
+.element-content {
+  width: 100%;
+  height: 100%;
+  position: relative;
+
+  .img-wrapper {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    position: relative;
+  }
+
+  img {
+    position: absolute;
+  }
+}
+</style>

+ 93 - 0
src/views/_common/_element/TextElement/BaseTextElement.vue

@@ -0,0 +1,93 @@
+<template>
+  <div 
+    class="base-element text"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      transform: `rotate(${elementInfo.rotate}deg)`,
+    }"
+  >
+    <div class="element-content"
+      :style="{
+        backgroundColor: elementInfo.fill,
+        opacity: elementInfo.opacity,
+        textShadow: elementInfo.shadow,
+        lineHeight: elementInfo.lineHeight,
+        letterSpacing: (elementInfo.letterSpacing || 0) + 'px',
+      }"
+    >
+      <ElementBorder
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :borderColor="elementInfo.borderColor"
+        :borderWidth="elementInfo.borderWidth"
+        :borderStyle="elementInfo.borderStyle"
+      />
+      <div class="text-content"
+        v-html="elementInfo.content" 
+      ></div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue'
+import { PPTTextElement } from '@/types/slides'
+import ElementBorder from '@/views/_common/_element/ElementBorder.vue'
+
+export default defineComponent({
+  name: 'base-element-text',
+  components: {
+    ElementBorder,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTTextElement>,
+      required: true,
+    },
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.base-element {
+  position: absolute;
+}
+
+.element-content {
+  position: relative;
+  padding: 10px;
+
+  .text-content {
+    position: relative;
+  }
+}
+
+::v-deep(.text-content) {
+  word-break: break-word;
+  font-family: '微软雅黑';
+  outline: 0;
+
+  ::selection {
+    background-color: rgba(27, 110, 232, 0.3);
+    color: inherit;
+  }
+
+  ul {
+    list-style-type: disc;
+    padding-inline-start: 30px;
+    li {
+      list-style-type: disc;
+    }
+  }
+
+  ol {
+    list-style-type: decimal;
+    padding-inline-start: 30px;
+    li {
+      list-style-type: decimal;
+    }
+  }
+}
+</style>

+ 1 - 1
src/views/_common/_element/TextElement/index.vue

@@ -86,7 +86,7 @@ import BorderLine from '@/views/_common/_operate/BorderLine.vue'
 import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
 
 export default defineComponent({
-  name: 'slide-element-text',
+  name: 'editable-element-text',
   components: {
     ElementBorder,
     RotateHandler,