Bläddra i källkod

添加图表元素

pipipi-pikachu 5 år sedan
förälder
incheckning
c4c87b1b9c

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 587 - 537
package-lock.json


+ 130 - 131
src/components/Chart.vue

@@ -1,131 +1,130 @@
-<template>
-  <div 
-    class="chart"
-    :style="{
-      width: width + 'px',
-      height: height + 'px',
-    }"
-  >
-    <canvas ref="canvasRef"></canvas>
-  </div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, onMounted, onUnmounted, PropType, ref } from 'vue'
-import Chart from 'chart.js'
-
-interface ChartData {
-  labels: string[];
-  values: number[][];
-}
-
-// const data = {
-//   labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
-//   values: [
-//     [12, 19, 3, 5, 2, 3],
-//     [22, 9, 13, 25, 12, 5],
-//   ]
-// }
-
-// bar
-// horizontalBar
-// line
-// radar
-// pie
-// doughnut
-// polarArea
-
-export default defineComponent({
-  name: 'chart',
-  props: {
-    type: {
-      type: String,
-      required: true,
-    },
-    width: {
-      type: Number,
-      required: true,
-    },
-    height: {
-      type: Number,
-      required: true,
-    },
-    data: {
-      type: Object as PropType<ChartData>,
-      required: true,
-    },
-  },
-  setup(props) {
-    const canvasRef = ref<HTMLCanvasElement | null>(null)
-    let chart: Chart
-
-    const data = computed(() => ({
-      labels: props.data.labels,
-      datasets: props.data.values.map(item => {
-        return {
-          data: item,
-          backgroundColor: [
-            'rgba(255, 99, 132, 0.2)',
-            'rgba(54, 162, 235, 0.2)',
-            'rgba(255, 206, 86, 0.2)',
-            'rgba(75, 192, 192, 0.2)',
-            'rgba(153, 102, 255, 0.2)',
-            'rgba(255, 159, 64, 0.2)',
-          ],
-          borderColor: [
-            'rgba(255, 99, 132, 0.5)',
-            'rgba(54, 162, 235, 0.5)',
-            'rgba(255, 206, 86, 0.5)',
-            'rgba(75, 192, 192, 0.5)',
-            'rgba(153, 102, 255, 0.5)',
-            'rgba(255, 159, 64, 0.5)',
-          ],
-          borderWidth: 1,
-        }
-      }),
-    }))
-
-    onMounted(() => {
-      if(!canvasRef.value) return
-      const ctx = canvasRef.value.getContext('2d') as CanvasRenderingContext2D
-      chart = new Chart(ctx, {
-        type: props.type,
-        data: data.value,
-        options: {
-          maintainAspectRatio: false,
-          animation: {
-            duration: 0,
-          },
-          hover: {
-            animationDuration: 0,
-          },
-          responsiveAnimationDuration: 0,
-          layout: {
-            padding: {
-              left: 8,
-              right: 8,
-              top: 8,
-              bottom: 8
-            },
-          },
-          legend: {
-            display: false,
-          },
-          elements: {
-            line: {
-              tension: 0,
-              fill: false,
-            },
-          },
-        },
-      })
-    })
-
-    onUnmounted(() => chart.destroy())
-
-    return {
-      canvasRef,
-    }
-  },
-})
-</script>
+<template>
+  <div 
+    class="chart"
+    :style="{
+      width: width * scale + 'px',
+      height: height * scale + 'px',
+      transform: `scale(${1 / scale})`,
+    }"
+  >
+    <canvas ref="canvasRef"></canvas>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, watch } from 'vue'
+import Chart from 'chart.js'
+import { ChartData, ChartType } from '@/types/slides'
+
+const commonConfigs = {
+  backgroundColor: 'rgba(209, 68, 36, 0.3)',
+  borderColor: 'rgba(209, 68, 36)',
+  borderWidth: 2,
+}
+
+const defaultOptions: Chart.ChartOptions = {
+  maintainAspectRatio: false,
+  animation: {
+    duration: 0,
+  },
+  hover: {
+    animationDuration: 0,
+  },
+  responsiveAnimationDuration: 0,
+  layout: {
+    padding: {
+      left: 5,
+      right: 5,
+      top: 5,
+      bottom: 5,
+    },
+  },
+  legend: {
+    display: false,
+  },
+  elements: {
+    line: {
+      tension: 0,
+      fill: false,
+      ...commonConfigs,
+    },
+    rectangle: {
+      ...commonConfigs,
+    },
+    arc: {
+      ...commonConfigs,
+    },
+  },
+}
+
+export default defineComponent({
+  name: 'chart',
+  props: {
+    type: {
+      type: String as PropType<ChartType>,
+      required: true,
+    },
+    width: {
+      type: Number,
+      required: true,
+    },
+    height: {
+      type: Number,
+      required: true,
+    },
+    data: {
+      type: Object as PropType<ChartData>,
+      required: true,
+    },
+    options: {
+      type: Object as PropType<Chart.ChartOptions>,
+    },
+    scale: {
+      type: Number,
+      default: 1,
+    },
+  },
+  setup(props) {
+    const canvasRef = ref<HTMLCanvasElement | null>(null)
+    let chart: Chart
+
+    const data = computed(() => ({
+      labels: props.data.labels,
+      datasets: props.data.series.map(item => ({ data: item })),
+    }))
+
+    const options = computed(() => {
+      const options = props.options || {}
+      return { ...defaultOptions, ...options }
+    })
+
+    watch(data, () => {
+      if(!chart) return
+      chart.data = data.value
+      chart.update()
+    })
+
+    onMounted(() => {
+      if(!canvasRef.value) return
+      const ctx = canvasRef.value.getContext('2d') as CanvasRenderingContext2D
+      chart = new Chart(ctx, {
+        type: props.type,
+        data: data.value,
+        options: options.value,
+      })
+    })
+
+    onUnmounted(() => chart.destroy())
+
+    return {
+      canvasRef,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.chart {
+  transform-origin: 0 0;
+}
+</style>

+ 1 - 0
src/components/IconFont.vue

@@ -23,5 +23,6 @@ export default {
   vertical-align: -0.15em;
   fill: currentColor;
   overflow: hidden;
+  outline: 0;
 }
 </style>

+ 6 - 0
src/configs/element.ts

@@ -35,6 +35,12 @@ export const DEFAULT_CHART = {
   top: 0,
   width: 500,
   height: 500,
+  data: {
+    labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
+    series: [
+      [12, 19, 3, 5, 2, 18],
+    ]
+  },
 }
 
 export const DEFAULT_TABLE = {

+ 2 - 3
src/hooks/useCreateElement.ts

@@ -3,7 +3,7 @@ import { MutationTypes } from '@/store'
 import { createRandomCode } from '@/utils/common'
 import { getImageSize } from '@/utils/image'
 import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
-import { PPTElement, TableElementCell } from '@/types/slides'
+import { ChartType, PPTElement, TableElementCell } from '@/types/slides'
 import { ShapePoolItem } from '@/configs/shapes'
 import { LinePoolItem } from '@/configs/lines'
 import {
@@ -67,13 +67,12 @@ export default () => {
     })
   }
   
-  const createChartElement = (chartType: string, data: string) => {
+  const createChartElement = (chartType: ChartType) => {
     createElement({
       ...DEFAULT_CHART,
       type: 'chart',
       id: createRandomCode(),
       chartType,
-      data,
     })
   }
   

+ 20 - 0
src/mocks/index.ts

@@ -74,6 +74,26 @@ export const slides: Slide[] = [
     ],
   },
   {
+    id: 'sahduyi',
+    elements: [
+      {
+        id: 'sdasdax',
+        type: 'chart',
+        left: 0,
+        top: 0,
+        width: 400,
+        height: 400,
+        chartType: 'line',
+        data: {
+          labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
+          series: [
+            [12, 19, 3, 5, 4, 18],
+          ]
+        },
+      },
+    ],
+  },
+  {
     id: 'sajd172',
     elements: [
       {

+ 7 - 2
src/types/slides.ts

@@ -105,6 +105,11 @@ export interface PPTLineElement {
   shadow?: PPTElementShadow;
 }
 
+export type ChartType = 'bar' | 'horizontalBar' | 'line' | 'pie' | 'doughnut' | 'polarArea' | 'radar'
+export interface ChartData {
+  labels: string[];
+  series: number[][];
+}
 export interface PPTChartElement {
   type: 'chart';
   id: string;
@@ -114,8 +119,8 @@ export interface PPTChartElement {
   groupId?: string;
   width: number;
   height: number;
-  chartType: string;
-  data: string;
+  chartType: ChartType;
+  data: ChartData;
   outline?: PPTElementOutline;
   theme?: string;
 }

+ 7 - 5
src/views/Editor/Canvas/EditableElement.vue

@@ -17,7 +17,7 @@
 
 <script lang="ts">
 import { computed, defineComponent, PropType } from 'vue'
-import { PPTElement } from '@/types/slides'
+import { ElementTypes, PPTElement } from '@/types/slides'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
 
 import useLockElement from '@/hooks/useLockElement'
@@ -33,6 +33,7 @@ import ImageElement from '@/views/components/element/ImageElement/index.vue'
 import TextElement from '@/views/components/element/TextElement/index.vue'
 import ShapeElement from '@/views/components/element/ShapeElement/index.vue'
 import LineElement from '@/views/components/element/LineElement/index.vue'
+import ChartElement from '@/views/components/element/ChartElement/index.vue'
 
 export default defineComponent({
   name: 'editable-element',
@@ -57,10 +58,11 @@ export default defineComponent({
   setup(props) {
     const currentElementComponent = computed(() => {
       const elementTypeMap = {
-        'image': ImageElement,
-        'text': TextElement,
-        'shape': ShapeElement,
-        'line': LineElement,
+        [ElementTypes.IMAGE]: ImageElement,
+        [ElementTypes.TEXT]: TextElement,
+        [ElementTypes.SHAPE]: ShapeElement,
+        [ElementTypes.LINE]: LineElement,
+        [ElementTypes.CHART]: ChartElement,
       }
       return elementTypeMap[props.elementInfo.type] || null
     })

+ 75 - 0
src/views/Editor/Canvas/Operate/ChartElementOperate.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="chart-element-operate">
+    <BorderLine 
+      class="operate-border-line"
+      v-for="line in borderLines" 
+      :key="line.type" 
+      :type="line.type" 
+      :style="line.style"
+    />
+    <template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
+      <ResizeHandler
+        class="operate-resize-handler" 
+        v-for="point in resizeHandlers"
+        :key="point.direction"
+        :type="point.direction"
+        :style="point.style"
+        @mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
+      />
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue'
+import { useStore } from 'vuex'
+import { State } from '@/store'
+
+import { PPTShapeElement } from '@/types/slides'
+import { OperateResizeHandler } from '@/types/edit'
+import useCommonOperate from '../hooks/useCommonOperate'
+
+import ResizeHandler from './ResizeHandler.vue'
+import BorderLine from './BorderLine.vue'
+
+export default defineComponent({
+  name: 'chart-element-operate',
+  inheritAttrs: false,
+  components: {
+    ResizeHandler,
+    BorderLine,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTShapeElement>,
+      required: true,
+    },
+    isActiveGroupElement: {
+      type: Boolean,
+      required: true,
+    },
+    isMultiSelect: {
+      type: Boolean,
+      required: true,
+    },
+    scaleElement: {
+      type: Function as PropType<(e: MouseEvent, element: PPTShapeElement, command: OperateResizeHandler) => void>,
+      required: true,
+    },
+  },
+  setup(props) {
+    const store = useStore<State>()
+    const canvasScale = computed(() => store.state.canvasScale)
+
+    const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
+    const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
+    const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
+
+    return {
+      scaleWidth,
+      resizeHandlers,
+      borderLines,
+    }
+  },
+})
+</script>

+ 2 - 2
src/views/Editor/Canvas/Operate/ShapeElementOperate.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="text-element-operate">
+  <div class="shape-element-operate">
     <BorderLine 
       class="operate-border-line"
       v-for="line in borderLines" 
@@ -39,7 +39,7 @@ import ResizeHandler from './ResizeHandler.vue'
 import BorderLine from './BorderLine.vue'
 
 export default defineComponent({
-  name: 'text-element-operate',
+  name: 'shape-element-operate',
   inheritAttrs: false,
   components: {
     RotateHandler,

+ 7 - 5
src/views/Editor/Canvas/Operate/index.vue

@@ -33,13 +33,14 @@
 import { defineComponent, PropType, computed, Ref } from 'vue'
 import { useStore } from 'vuex'
 import { State } from '@/store'
-import { PPTElement, Slide } from '@/types/slides'
+import { ElementTypes, PPTElement, Slide } from '@/types/slides'
 import { OperateLineHandler, OperateResizeHandler } from '@/types/edit'
 
 import ImageElementOperate from './ImageElementOperate.vue'
 import TextElementOperate from './TextElementOperate.vue'
 import ShapeElementOperate from './ShapeElementOperate.vue'
 import LineElementOperate from './LineElementOperate.vue'
+import ChartElementOperate from './ChartElementOperate.vue'
 
 export default defineComponent({
   name: 'operate',
@@ -85,10 +86,11 @@ export default defineComponent({
 
     const currentOperateComponent = computed(() => {
       const elementTypeMap = {
-        'image': ImageElementOperate,
-        'text': TextElementOperate,
-        'shape': ShapeElementOperate,
-        'line': LineElementOperate,
+        [ElementTypes.IMAGE]: ImageElementOperate,
+        [ElementTypes.TEXT]: TextElementOperate,
+        [ElementTypes.SHAPE]: ShapeElementOperate,
+        [ElementTypes.LINE]: LineElementOperate,
+        [ElementTypes.CHART]: ChartElementOperate,
       }
       return elementTypeMap[props.elementInfo.type] || null
     })

+ 13 - 0
src/views/Editor/Toolbar/ElementStylePanel/ChartStylePanel.vue

@@ -0,0 +1,13 @@
+<template>
+  <div class="chart-style-panel">
+    chart-style-panel
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  name: 'chart-style-panel',
+})
+</script>

+ 2 - 0
src/views/Editor/Toolbar/ElementStylePanel/index.vue

@@ -17,6 +17,7 @@ import TextStylePanel from './TextStylePanel.vue'
 import ImageStylePanel from './ImageStylePanel.vue'
 import ShapeStylePanel from './ShapeStylePanel.vue'
 import LineStylePanel from './LineStylePanel.vue'
+import ChartStylePanel from './ChartStylePanel.vue'
 
 export default defineComponent({
   name: 'element-style-panel',
@@ -32,6 +33,7 @@ export default defineComponent({
         [ElementTypes.IMAGE]: ImageStylePanel,
         [ElementTypes.SHAPE]: ShapeStylePanel,
         [ElementTypes.LINE]: LineStylePanel,
+        [ElementTypes.CHART]: ChartStylePanel,
       }
       return panelMap[handleElement.value.type] || null
     })

+ 1 - 1
src/views/Editor/Toolbar/common/ColorButton.vue

@@ -29,7 +29,7 @@ export default {
   margin-left: 8px;
   flex: 1;
 }
-.color-btn-icon {
+svg.color-btn-icon {
   width: 30px;
   font-size: 12px;
   margin-top: 2px;

+ 8 - 5
src/views/Screen/ScreenElement.vue

@@ -9,6 +9,7 @@
     <component
       :is="currentElementComponent"
       :elementInfo="elementInfo"
+      target="screen"
     ></component>
   </div>
 </template>
@@ -17,12 +18,13 @@
 import { computed, defineComponent, PropType, Ref } from 'vue'
 import { useStore } from 'vuex'
 import { State } from '@/store'
-import { PPTElement, Slide } from '@/types/slides'
+import { ElementTypes, PPTElement, Slide } from '@/types/slides'
 
 import BaseImageElement from '@/views/components/element/ImageElement/BaseImageElement.vue'
 import BaseTextElement from '@/views/components/element/TextElement/BaseTextElement.vue'
 import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeElement.vue'
 import BaseLineElement from '@/views/components/element/LineElement/BaseLineElement.vue'
+import BaseChartElement from '@/views/components/element/ChartElement/BaseChartElement.vue'
 
 export default defineComponent({
   name: 'screen-element',
@@ -43,10 +45,11 @@ export default defineComponent({
   setup(props) {
     const currentElementComponent = computed(() => {
       const elementTypeMap = {
-        'image': BaseImageElement,
-        'text': BaseTextElement,
-        'shape': BaseShapeElement,
-        'line': BaseLineElement,
+        [ElementTypes.IMAGE]: BaseImageElement,
+        [ElementTypes.TEXT]: BaseTextElement,
+        [ElementTypes.SHAPE]: BaseShapeElement,
+        [ElementTypes.LINE]: BaseLineElement,
+        [ElementTypes.CHART]: BaseChartElement,
       }
       return elementTypeMap[props.elementInfo.type] || null
     })

+ 4 - 5
src/views/Screen/ScreenSlide.vue

@@ -20,7 +20,7 @@
 </template>
 
 <script lang="ts">
-import { computed, PropType, defineComponent } from 'vue'
+import { computed, PropType, defineComponent, inject } from 'vue'
 import { Slide } from '@/types/slides'
 import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
 import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
@@ -37,10 +37,6 @@ export default defineComponent({
       type: Object as PropType<Slide>,
       required: true,
     },
-    scale: {
-      type: Number,
-      required: true,
-    },
     animationIndex: {
       type: Number,
       default: -1,
@@ -50,7 +46,10 @@ export default defineComponent({
     const background = computed(() => props.slide.background)
     const { backgroundStyle } = useSlideBackgroundStyle(background)
 
+    const scale = inject('scale')
+
     return {
+      scale,
       backgroundStyle,
       VIEWPORT_SIZE,
       VIEWPORT_ASPECT_RATIO,

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

@@ -26,7 +26,6 @@
           }"
         >
           <ScreenSlide 
-            :scale="scale" 
             :slide="slide" 
             :animationIndex="animationIndex"
           />
@@ -59,7 +58,7 @@
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, onMounted, onUnmounted, Ref, ref } from 'vue'
+import { computed, defineComponent, onMounted, onUnmounted, provide, Ref, ref } from 'vue'
 import { useStore } from 'vuex'
 import throttle from 'lodash/throttle'
 import { MutationTypes, State } from '@/store'
@@ -88,7 +87,9 @@ export default defineComponent({
 
     const slideWidth = ref(0)
     const slideHeight = ref(0)
+
     const scale = computed(() => slideWidth.value / VIEWPORT_SIZE)
+    provide('scale', scale)
 
     const slideThumbnailModelVisible = ref(false)
 

+ 8 - 5
src/views/components/ThumbnailSlide/ThumbnailElement.vue

@@ -6,18 +6,20 @@
     <component
       :is="currentElementComponent"
       :elementInfo="elementInfo"
+      target="thumbnail"
     ></component>
   </div>
 </template>
 
 <script lang="ts">
 import { computed, defineComponent, PropType } from 'vue'
-import { PPTElement } from '@/types/slides'
+import { ElementTypes, PPTElement } from '@/types/slides'
 
 import BaseImageElement from '@/views/components/element/ImageElement/BaseImageElement.vue'
 import BaseTextElement from '@/views/components/element/TextElement/BaseTextElement.vue'
 import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeElement.vue'
 import BaseLineElement from '@/views/components/element/LineElement/BaseLineElement.vue'
+import BaseChartElement from '@/views/components/element/ChartElement/BaseChartElement.vue'
 
 export default defineComponent({
   name: 'base-element',
@@ -34,10 +36,11 @@ export default defineComponent({
   setup(props) {
     const currentElementComponent = computed(() => {
       const elementTypeMap = {
-        'image': BaseImageElement,
-        'text': BaseTextElement,
-        'shape': BaseShapeElement,
-        'line': BaseLineElement,
+        [ElementTypes.IMAGE]: BaseImageElement,
+        [ElementTypes.TEXT]: BaseTextElement,
+        [ElementTypes.SHAPE]: BaseShapeElement,
+        [ElementTypes.LINE]: BaseLineElement,
+        [ElementTypes.CHART]: BaseChartElement,
       }
       return elementTypeMap[props.elementInfo.type] || null
     })

+ 6 - 2
src/views/components/ThumbnailSlide/index.vue

@@ -10,7 +10,7 @@
       :style="{
         width: VIEWPORT_SIZE + 'px',
         height: VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO + 'px',
-        transform: `scale(${size / VIEWPORT_SIZE})`,
+        transform: `scale(${scale})`,
       }"
     >
       <div class="background" :style="{ ...backgroundStyle }"></div>
@@ -25,7 +25,7 @@
 </template>
 
 <script lang="ts">
-import { computed, PropType, defineComponent } from 'vue'
+import { computed, PropType, defineComponent, provide } from 'vue'
 import { Slide } from '@/types/slides'
 import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
 import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
@@ -51,7 +51,11 @@ export default defineComponent({
     const background = computed(() => props.slide.background)
     const { backgroundStyle } = useSlideBackgroundStyle(background)
 
+    const scale = computed(() => props.size / VIEWPORT_SIZE)
+    provide('scale', 1)
+
     return {
+      scale,
       backgroundStyle,
       VIEWPORT_SIZE,
       VIEWPORT_ASPECT_RATIO,

+ 70 - 0
src/views/components/element/ChartElement/BaseChartElement.vue

@@ -0,0 +1,70 @@
+<template>
+  <div class="base-element-chart"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px',
+    }"
+  >
+    <div class="element-content">
+      <ElementOutline
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :outline="elementInfo.outline"
+      />
+      <Chart
+        :type="elementInfo.chartType"
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :data="elementInfo.data"
+        :scale="scale"
+        :options="target === 'thumbnail' ? { tooltips: { enabled: false } } : {}"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, inject, PropType } from 'vue'
+import { PPTChartElement } from '@/types/slides'
+
+import ElementOutline from '@/views/components/element/ElementOutline.vue'
+import Chart from '@/components/Chart.vue'
+
+export default defineComponent({
+  name: 'base-element-chart',
+  components: {
+    ElementOutline,
+    Chart,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTChartElement>,
+      required: true,
+    },
+    target: {
+      type: String as PropType<'thumbnail' | 'screen'>,
+      required: true,
+    },
+  },
+  setup() {
+    const scale = inject('scale') || 1
+
+    return {
+      scale,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.base-element-chart {
+  position: absolute;
+}
+
+.element-content {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 94 - 0
src/views/components/element/ChartElement/index.vue

@@ -0,0 +1,94 @@
+<template>
+  <div class="editable-element-chart"
+    :class="{ 'lock': elementInfo.lock }"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px',
+    }"
+    @mousedown="$event => handleSelectElement($event)"
+  >
+    <div 
+      class="element-content" 
+      v-contextmenu="contextmenus"
+    >
+      <ElementOutline
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :outline="elementInfo.outline"
+      />
+      <Chart
+        :type="elementInfo.chartType"
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :data="elementInfo.data"
+        :scale="canvasScale"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue'
+import { useStore } from 'vuex'
+import { State } from '@/store'
+import { PPTChartElement } from '@/types/slides'
+import { ContextmenuItem } from '@/components/Contextmenu/types'
+
+import ElementOutline from '@/views/components/element/ElementOutline.vue'
+import Chart from '@/components/Chart.vue'
+
+export default defineComponent({
+  name: 'editable-element-chart',
+  components: {
+    ElementOutline,
+    Chart,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTChartElement>,
+      required: true,
+    },
+    selectElement: {
+      type: Function as PropType<(e: MouseEvent, element: PPTChartElement, canMove?: boolean) => void>,
+      required: true,
+    },
+    contextmenus: {
+      type: Function as PropType<() => ContextmenuItem[]>,
+    },
+  },
+  setup(props) {
+    const handleSelectElement = (e: MouseEvent) => {
+      if(props.elementInfo.lock) return
+      e.stopPropagation()
+
+      props.selectElement(e, props.elementInfo)
+    }
+
+    const store = useStore<State>()
+    const canvasScale = computed(() => store.state.canvasScale)
+
+    return {
+      handleSelectElement,
+      canvasScale,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.editable-element-chart {
+  position: absolute;
+  cursor: move;
+
+  &.lock .element-content {
+    cursor: default;
+  }
+}
+
+.element-content {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 1 - 0
src/views/components/element/ShapeElement/BaseShapeElement.vue

@@ -4,6 +4,7 @@
       top: elementInfo.top + 'px',
       left: elementInfo.left + 'px',
       width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px',
       transform: `rotate(${elementInfo.rotate}deg)`,
     }"
   >

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

@@ -5,6 +5,7 @@
       top: elementInfo.top + 'px',
       left: elementInfo.left + 'px',
       width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px',
       transform: `rotate(${elementInfo.rotate}deg)`,
     }"
     @mousedown="$event => handleSelectElement($event)"