Explorar o código

添加选择范围插入元素

pipipi-pikachu %!s(int64=5) %!d(string=hai) anos
pai
achega
9fdbf35fa1

+ 2 - 2
src/components/FileInput.vue

@@ -2,7 +2,7 @@
   <div class="file-input" @click="handleClick()">
     <slot></slot>
     <input 
-      class="file-input"
+      class="input"
       type="file" 
       name="upload" 
       ref="inputRef" 
@@ -46,7 +46,7 @@ export default defineComponent({
 </script>
 
 <style lang="scss" scoped>
-.file-input {
+.input {
   display: none;
 }
 </style>

+ 0 - 8
src/configs/element.ts

@@ -1,13 +1,6 @@
 const DEFAULT_COLOR = '#41464b'
 
 export const DEFAULT_TEXT = {
-  left: 0,
-  top: 0,
-  width: 300,
-  height: 0,
-  opacity: 1,
-  lineHeight: 1.5,
-  segmentSpacing: 5,
   content: '请输入内容',
 }
 
@@ -24,7 +17,6 @@ export const DEFAULT_SHAPE = {
 
 export const DEFAULT_LINE = {
   style: 'solid',
-  points: ['', ''],
   width: 4,
   color: DEFAULT_COLOR,
 }

+ 9 - 8
src/configs/lines.ts

@@ -1,12 +1,13 @@
-export const LINES = [
+export interface LinePoolItem {
+  path: string;
+  style: string;
+  points: [string, string];
+}
+
+export const LINE_LIST = [
   { 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: 'solid', points: ['', 'arrow'] },
   { path: 'M0,0 L20,20', style: 'dashed', points: ['', 'arrow'] },
-  { path: 'M0,0 L20,20', style: 'dashed', points: ['arrow', 'arrow'] },
+  { path: 'M0,0 L20,20', style: 'solid', points: ['', 'dot'] },
 ]

+ 127 - 0
src/configs/shapes.ts

@@ -0,0 +1,127 @@
+export interface ShapePoolItem {
+  viewBox: number;
+  path: string;
+}
+
+export const SHAPE_LIST = [
+  {
+    viewBox: 200,
+    path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 0 200 L 0 0 L 150 0 L 200 50 L 200 200 L 0 200'
+  },
+  {
+    viewBox: 200,
+    path: 'M 0 150 L 0 0 L 150 0 L 200 50 L 200 200 L 50 200 L 0 150'
+  },
+  {
+    viewBox: 200,
+    path: 'M 20 0 L 180 0 Q 200 0 200 20 L 200 180 Q 200 200 180 200 L 20 200 Q 0 200 0 180 L 0 20 Q 0 0 20 0 '
+  },
+  {
+    viewBox: 200,
+    path: 'M 0 0 L 140 0 Q 200 0 200 60 L 200 200 L 60 200 Q 0 200 0 140 L 0 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 0 0 L 140 0 Q 200 0 200 60 L 200 200 L 0 200 L 0 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 0 A 50 50 0 1 1 100 200 A 50 50 0 1 1 100 0 Z'
+  }, 
+  {
+    viewBox: 200,
+    path: 'M 200 0 Q 0 0 0 200 L 200 200 L 200 0'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 0 A 100 100 0 1 1 0 100 L 100 100 L 100 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 0 L 0 200 L 200 200 L 100 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 0 0 L 0 200 L 200 200 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 50 0 L 200 0 L 150 200 L 0 200 L 50 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 0 L 0 100 L 100 200 L 200 100 L 100 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 50 0 L 150 0 L 200 200 L 0 200 L 50 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 0 L 0 90 L 50 200 L 150 200 L 200 90 L 100 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 0 L 0 60 L 0 140 L 100 200 L 200 140 L 200 60 L 100 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 60 0 L 140 0 L 200 60 L 200 140 L 140 200 L 60 200 L 0 140 L 0 60 L 60 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 0 A 100 100 0 1 1 0 100 L 0 0 L 100 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 0 A 50 50 0 1 0 200 120 A 100 100 0 1 1 100 0'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 0 L 122 70 L 196 70 L 136 114 L 158 182 L 100 140 L 42 182 L 64 114 L 4 70 L 78 70 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 0 L 0 100 L 50 100 L 50 200 L 150 200 L 150 100 L 200 100 L 100 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 100 200 L 200 100 L 150 100 L 150 0 L 50 0 L 50 100 L 0 100 L 100 200 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 0 100 L 100 0 L 100 50 L 200 50 L 200 150 L 100 150 L 100 200 L 0 100 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 200 100 L 100 0 L 100 50 L 0 50 L 0 150 L 100 150 L 100 200 L 200 100 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 0 0 L 120 0 L 200 100 L 120 200 L 0 200 L 80 100 L 0 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 80 0 L 200 0 L 120 100 L 200 200 L 80 200 L 0 100 L 80 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 0 0 L 140 0 L 200 100 L 140 200 L 0 200 L 0 100 L 0 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 60 0 L 200 0 L 200 100 L 200 200 L 60 200 L 0 100 L 60 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 70 0 L 70 70 L 0 70 L 0 130 L 70 130 L 70 200 L 130 200 L 130 130 L 200 130 L 200 70 L 130 70 L 130 0 L 70 0 Z'
+  },
+  {
+    viewBox: 200,
+    path: 'M 40 0 L 0 40 L 60 100 L 0 160 L 40 200 L 100 140 L 160 200 L 200 160 L 140 100 L 200 40 L 160 0 L 100 60 L 40 0 Z'
+  },
+]

+ 9 - 5
src/hooks/useCreateElement.ts

@@ -4,6 +4,8 @@ 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 { ShapePoolItem } from '@/configs/shapes'
+import { LinePoolItem } from '@/configs/lines'
 import {
   DEFAULT_IMAGE,
   DEFAULT_TEXT,
@@ -33,6 +35,8 @@ export default () => {
 
   const { addHistorySnapshot } = useHistorySnapshot()
 
+  
+
   const createElement = (element: PPTElement) => {
     store.commit(MutationTypes.ADD_ELEMENT, element)
     store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [element.id])
@@ -109,7 +113,7 @@ export default () => {
     })
   }
   
-  const createShapeElement = (position: CommonElementPosition, path: string, viewBox: number) => {
+  const createShapeElement = (position: CommonElementPosition, data: ShapePoolItem) => {
     const { left, top, width, height } = position
     createElement({
       ...DEFAULT_SHAPE,
@@ -119,12 +123,12 @@ export default () => {
       top, 
       width, 
       height,
-      viewBox,
-      path,
+      viewBox: data.viewBox,
+      path: data.path,
     })
   }
   
-  const createLineElement = (position: LineElementPosition, points: [string, string]) => {
+  const createLineElement = (position: LineElementPosition, data: LinePoolItem) => {
     const { left, top, start, end } = position
     createElement({
       ...DEFAULT_LINE,
@@ -134,7 +138,7 @@ export default () => {
       top, 
       start,
       end,
-      points,
+      points: data.points,
     })
   }
 

+ 1 - 1
src/store/constants.ts

@@ -9,7 +9,7 @@ export enum MutationTypes {
   SET_EDITORAREA_FOCUS = 'setEditorAreaFocus',
   SET_DISABLE_HOTKEYS_STATE = 'setDisableHotkeysState',
   SET_GRID_LINES_STATE = 'setGridLinesState',
-  SET_CREATING_ELEMENT_TYPE = 'setCreatingElementType',
+  SET_CREATING_ELEMENT = 'setCreatingElement',
   SET_AVAILABLE_FONTS = 'setAvailableFonts',
   SET_TOOLBAR_STATE = 'setToolbarState',
 

+ 3 - 2
src/store/index.ts

@@ -5,6 +5,7 @@ import { mutations } from './mutations'
 import { MutationTypes, ActionTypes } from './constants'
 
 import { Slide } from '@/types/slides'
+import { CreatingElement } from '@/types/edit'
 import { ToolbarState } from '@/types/toolbar'
 import { slides } from '@/mocks/index'
 import { FontName } from '@/configs/fontName'
@@ -20,7 +21,7 @@ export interface State {
   editorAreaFocus: boolean;
   disableHotkeys: boolean;
   showGridLines: boolean;
-  creatingElementType: string;
+  creatingElement: CreatingElement | null;
   availableFonts: FontName[];
   toolbarState: ToolbarState;
   slides: Slide[];
@@ -41,7 +42,7 @@ const state: State = {
   editorAreaFocus: false,
   disableHotkeys: false,
   showGridLines: false,
-  creatingElementType: '',
+  creatingElement: null,
   availableFonts: [],
   toolbarState: 'slideStyle',
   slides: slides,

+ 3 - 2
src/store/mutations.ts

@@ -2,6 +2,7 @@ import { MutationTree } from 'vuex'
 import { MutationTypes } from './constants'
 import { State } from './index'
 import { Slide, PPTElement } from '@/types/slides'
+import { CreatingElement } from '@/types/edit'
 import { FONT_NAMES } from '@/configs/fontName'
 import { isSupportFontFamily } from '@/utils/fontFamily'
 
@@ -49,8 +50,8 @@ export const mutations: MutationTree<State> = {
     state.showGridLines = show
   },
 
-  [MutationTypes.SET_CREATING_ELEMENT_TYPE](state, type: string) {
-    state.creatingElementType = type
+  [MutationTypes.SET_CREATING_ELEMENT](state, element: CreatingElement | null) {
+    state.creatingElement = element
   },
 
   [MutationTypes.SET_AVAILABLE_FONTS](state) {

+ 17 - 1
src/types/edit.ts

@@ -1,3 +1,6 @@
+import { ShapePoolItem } from '@/configs/shapes'
+import { LinePoolItem } from '@/configs/lines'
+
 export type ElementOrderCommand = 'up' | 'down' | 'top' | 'bottom'
 
 export enum ElementOrderCommands {
@@ -85,4 +88,17 @@ export interface ImageClipedEmitData {
 export interface CreateElementSelectionData {
   start: [number, number];
   end: [number, number];
-}
+}
+
+export interface CreatingTextElement {
+  type: 'text';
+}
+export interface CreatingShapeElement {
+  type: 'shape';
+  data: ShapePoolItem;
+}
+export interface CreatingLineElement {
+  type: 'line';
+  data: LinePoolItem;
+}
+export type CreatingElement = CreatingTextElement | CreatingShapeElement | CreatingLineElement

+ 2 - 0
src/types/slides.ts

@@ -33,6 +33,8 @@ export interface PPTTextElement {
   rotate?: number;
   outline?: PPTElementOutline;
   fill?: string;
+  lineHeight?: number;
+  wordSpace?: number;
   opacity?: number;
   shadow?: PPTElementShadow;
 }

+ 10 - 9
src/views/Editor/Canvas/ElementCreateSelection.vue

@@ -4,11 +4,11 @@
     ref="selectionRef"
     @mousedown.stop="$event => createSelection($event)"
   >
-    <div :class="['selection', elementType]" v-if="start && end" :style="position">
+    <div :class="['selection', creatingElement.type]" v-if="start && end" :style="position">
 
       <!-- 绘制线条专用 -->
       <SvgWrapper
-        v-if="elementType === 'line' && lineData"
+        v-if="creatingElement.type === 'line' && lineData"
         overflow="visible" 
         :width="lineData.svgWidth"
         :height="lineData.svgHeight"
@@ -42,7 +42,7 @@ export default defineComponent({
   setup(props, { emit }) {
     const store = useStore<State>()
     const ctrlOrShiftKeyActive: Ref<boolean> = computed(() => store.getters.ctrlOrShiftKeyActive)
-    const elementType = computed(() => store.state.creatingElementType)
+    const creatingElement = computed(() => store.state.creatingElement)
 
     const start = ref<[number, number] | null>(null)
     const end = ref<[number, number] | null>(null)
@@ -67,7 +67,7 @@ export default defineComponent({
       start.value = [startPageX, startPageY]
 
       document.onmousemove = e => {
-        if(!isMouseDown) return
+        if(!creatingElement.value || !isMouseDown) return
 
         let currentPageX = e.pageX
         let currentPageY = e.pageY
@@ -79,7 +79,7 @@ export default defineComponent({
           const absX = Math.abs(moveX)
           const absY = Math.abs(moveY)
 
-          if(elementType.value === 'shape') {
+          if(creatingElement.value.type === 'shape') {
             // moveX和moveY一正一负
             const isOpposite = (moveY > 0 && moveX < 0) || (moveY < 0 && moveX > 0)
 
@@ -91,7 +91,7 @@ export default defineComponent({
             }
           }
 
-          else if(elementType.value === 'line') {
+          else if(creatingElement.value.type === 'line') {
             if(absX > absY) currentPageY = startPageY
             else currentPageX = startPageX
           }
@@ -115,13 +115,14 @@ export default defineComponent({
             start: start.value,
             end: end.value,
           })
-          store.commit(MutationTypes.SET_CREATING_ELEMENT_TYPE, '')
         }
+        else store.commit(MutationTypes.SET_CREATING_ELEMENT, null)
       }
     }
 
     const lineData = computed(() => {
-      if(!start.value || !end.value || elementType.value !== 'line') return null
+      if(!start.value || !end.value) return null
+      if(!creatingElement.value || creatingElement.value.type !== 'line') return null
 
       const [_startX, _startY] = start.value
       const [_endX, _endY] = end.value
@@ -176,7 +177,7 @@ export default defineComponent({
       selectionRef,
       start,
       end,
-      elementType,
+      creatingElement,
       createSelection,
       lineData,
       position,

+ 92 - 0
src/views/Editor/Canvas/hooks/useInsertFromCreateSelection.ts

@@ -0,0 +1,92 @@
+import { computed, Ref } from 'vue'
+import { useStore } from 'vuex'
+import { MutationTypes, State } from '@/store'
+import { CreateElementSelectionData, CreatingLineElement, CreatingShapeElement } from '@/types/edit'
+import useCreateElement from '@/hooks/useCreateElement'
+
+export default (viewportRef: Ref<HTMLElement | null>) => {
+  const store = useStore<State>()
+  const canvasScale = computed(() => store.state.canvasScale)
+  const creatingElement = computed(() => store.state.creatingElement)
+
+  const formatCreateSelection = (selectionData: CreateElementSelectionData) => {
+    const { start, end } = selectionData
+
+    if(!viewportRef.value) return
+    const viewportRect = viewportRef.value.getBoundingClientRect()
+
+    const [startX, startY] = start
+    const [endX, endY] = end
+    const minX = Math.min(startX, endX)
+    const maxX = Math.max(startX, endX)
+    const minY = Math.min(startY, endY)
+    const maxY = Math.max(startY, endY)
+
+    const left = (minX - viewportRect.x) / canvasScale.value
+    const top = (minY - viewportRect.y) / canvasScale.value
+    const width = (maxX - minX) / canvasScale.value
+    const height = (maxY - minY) / canvasScale.value
+
+    return { left, top, width, height }
+  }
+
+  const formatCreateSelectionForLine = (selectionData: CreateElementSelectionData) => {
+    const { start, end } = selectionData
+
+    if(!viewportRef.value) return
+    const viewportRect = viewportRef.value.getBoundingClientRect()
+
+    const [startX, startY] = start
+    const [endX, endY] = end
+    const minX = Math.min(startX, endX)
+    const maxX = Math.max(startX, endX)
+    const minY = Math.min(startY, endY)
+    const maxY = Math.max(startY, endY)
+
+    const left = (minX - viewportRect.x) / canvasScale.value
+    const top = (minY - viewportRect.y) / canvasScale.value
+    const width = (maxX - minX) / canvasScale.value
+    const height = (maxY - minY) / canvasScale.value
+
+    const _start: [number, number] = [
+      startX === minX ? 0 : width,
+      startY === minY ? 0 : height,
+    ]
+    const _end: [number, number] = [
+      endX === minX ? 0 : width,
+      endY === minY ? 0 : height,
+    ]
+
+    return {
+      left,
+      top,
+      start: _start,
+      end: _end,
+    }
+  }
+
+  const { createTextElement, createShapeElement, createLineElement } = useCreateElement()
+
+  const insertElementFromCreateSelection = (selectionData: CreateElementSelectionData) => {
+    if(!creatingElement.value) return
+
+    const type = creatingElement.value.type
+    if(type === 'text') {
+      const position = formatCreateSelection(selectionData)
+      position && createTextElement(position)
+    }
+    else if(type === 'shape') {
+      const position = formatCreateSelection(selectionData)
+      position && createShapeElement(position, (creatingElement.value as CreatingShapeElement).data)
+    }
+    else if(type === 'line') {
+      const position = formatCreateSelectionForLine(selectionData)
+      position && createLineElement(position, (creatingElement.value as CreatingLineElement).data)
+    }
+    store.commit(MutationTypes.SET_CREATING_ELEMENT, null)
+  }
+
+  return {
+    insertElementFromCreateSelection,
+  }
+}

+ 8 - 9
src/views/Editor/Canvas/index.vue

@@ -8,8 +8,8 @@
     v-click-outside="removeEditorAreaFocus"
   >
     <ElementCreateSelection
-      v-if="creatingElementType"
-      @created="data => createElement(data)"
+      v-if="creatingElement"
+      @created="data => insertElementFromCreateSelection(data)"
     />
     <div 
       class="viewport-wrapper"
@@ -81,7 +81,7 @@ import throttle from 'lodash/throttle'
 import { State, MutationTypes } from '@/store'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
 import { PPTElement, Slide } from '@/types/slides'
-import { AlignmentLineProps, CreateElementSelectionData } from '@/types/edit'
+import { AlignmentLineProps } from '@/types/edit'
 import { removeAllRanges } from '@/utils/selection'
 
 import useViewportSize from './hooks/useViewportSize'
@@ -92,6 +92,7 @@ import useScaleElement from './hooks/useScaleElement'
 import useSelectElement from './hooks/useSelectElement'
 import useDragElement from './hooks/useDragElement'
 import useDragLineElement from './hooks/useDragLineElement'
+import useInsertFromCreateSelection from './hooks/useInsertFromCreateSelection'
 
 import useDeleteElement from '@/hooks/useDeleteElement'
 import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
@@ -185,10 +186,8 @@ export default defineComponent({
       store.commit(MutationTypes.SET_GRID_LINES_STATE, !showGridLines.value)
     }
 
-    const creatingElementType = computed(() => store.state.creatingElementType)
-    const createElement = (data: CreateElementSelectionData) => {
-      console.log(data)
-    }
+    const creatingElement = computed(() => store.state.creatingElement)
+    const { insertElementFromCreateSelection } = useInsertFromCreateSelection(viewportRef)
 
     const contextmenus = (): ContextmenuItem[] => {
       return [
@@ -230,8 +229,8 @@ export default defineComponent({
       removeEditorAreaFocus,
       currentSlide,
       isShowGridLines,
-      creatingElementType,
-      createElement,
+      creatingElement,
+      insertElementFromCreateSelection,
       alignmentLines,
       selectElement,
       rotateElement,

+ 106 - 0
src/views/Editor/CanvasTool/LinePool.vue

@@ -0,0 +1,106 @@
+<template>
+  <div class="line-pool">
+    <div class="line-item" v-for="(line, index) in lineList" :key="index">
+      <div class="line-content" @click="selectLine(line)">
+        <SvgWrapper
+          overflow="visible" 
+          width="20"
+          height="20"
+        >
+          <defs>
+            <LinePointMarker
+              v-if="line.points[0]"
+              :id="`preset-line-${index}`"
+              position="start"
+              :type="line.points[0]"
+              color="#aaa"
+              :baseSize="2"
+            />
+            <LinePointMarker
+              v-if="line.points[1]"
+              :id="`preset-line-${index}`"
+              position="end"
+              :type="line.points[1]"
+              color="#999"
+              :baseSize="2"
+            />
+          </defs>
+          <path
+            :d="line.path" 
+            stroke="#aaa" 
+            fill="none" 
+            stroke-width="2" 
+            :stroke-dasharray="line.style === 'solid' ? '0, 0' : '4, 1'"
+            stroke-linecap 
+            stroke-linejoin 
+            stroke-miterlimit 
+            :marker-start="line.points[0] ? `url(#${`preset-line-${index}`}-${line.points[0]}-start)` : ''"
+            :marker-end="line.points[1] ? `url(#${`preset-line-${index}`}-${line.points[1]}-end)` : ''"
+          ></path>
+        </SvgWrapper>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import { LINE_LIST, LinePoolItem } from '@/configs/lines'
+
+import SvgWrapper from '@/components/SvgWrapper.vue'
+import LinePointMarker from '@/views/components/element/LineElement/LinePointMarker.vue'
+
+export default defineComponent({
+  name: 'line-pool',
+  components: {
+    SvgWrapper,
+    LinePointMarker,
+  },
+  setup(props, { emit }) {
+    const lineList = LINE_LIST
+
+    const selectLine = (line: LinePoolItem) => {
+      emit('select', line)
+    }
+
+    return {
+      lineList,
+      selectLine,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.line-pool {
+  width: 200px;
+  margin-bottom: -5px;
+
+  @include grid-layout-wrapper();
+}
+.line-item {
+  @include grid-layout-item(5, 19%);
+
+  height: 0;
+  padding-bottom: 19%;
+  flex-shrink: 0;
+  position: relative;
+  display: flex;
+  justify-content: center;
+  cursor: pointer;
+}
+.line-content {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  svg:not(:root) {
+    overflow: visible;
+  }
+}
+</style>

+ 88 - 0
src/views/Editor/CanvasTool/ShapePool.vue

@@ -0,0 +1,88 @@
+<template>
+  <ul class="shape-pool">
+    <li class="shape-item" v-for="(shape, index) in shapeList" :key="index">
+      <div class="shape-content" @click="selectShape(shape)">
+        <SvgWrapper 
+          overflow="visible" 
+          width="20"
+          height="20"
+        >
+          <g 
+            :transform="`scale(${20 / shape.viewBox}, ${20 / shape.viewBox}) translate(0,0) matrix(1,0,0,1,0,0)`"
+          >
+            <path 
+              vector-effect="non-scaling-stroke" 
+              stroke-linecap="butt" 
+              stroke-miterlimit="8"
+              stroke-linejoin
+              fill="transparent"
+              stroke="#999"
+              stroke-width="2" 
+              :d="shape.path"
+            ></path>
+          </g>
+        </SvgWrapper>
+      </div>
+    </li>
+  </ul>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import { SHAPE_LIST, ShapePoolItem } from '@/configs/shapes'
+
+import SvgWrapper from '@/components/SvgWrapper.vue'
+
+export default defineComponent({
+  name: 'shape-pool',
+  components: {
+    SvgWrapper,
+  },
+  setup(props, { emit }) {
+    const shapeList = SHAPE_LIST
+
+    const selectShape = (shape: ShapePoolItem) => {
+      emit('select', shape)
+    }
+
+    return {
+      shapeList,
+      selectShape,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.shape-pool {
+  width: 400px;
+  max-height: 400px;
+  overflow: auto;
+  margin-bottom: -5px;
+
+  @include grid-layout-wrapper();
+}
+.shape-item {
+  @include grid-layout-item(10, 9%);
+
+  height: 0;
+  padding-bottom: 9%;
+  flex-shrink: 0;
+  position: relative;
+  cursor: pointer;
+}
+.shape-content {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  svg:not(:root) {
+    overflow: visible;
+  }
+}
+</style>

+ 43 - 6
src/views/Editor/CanvasTool/index.vue

@@ -6,12 +6,22 @@
     </div>
 
     <div class="add-element-handler">
-      <FontSizeOutlined class="handler-item" @click="createElement('text')" />
+      <FontSizeOutlined class="handler-item" @click="drawText()" />
       <FileInput @change="files => insertImageElement(files)">
         <PictureOutlined class="handler-item" />
       </FileInput>
-      <StarOutlined class="handler-item" @click="createElement('shape')" />
-      <LineOutlined class="handler-item" @click="createElement('line')" />
+      <Popover trigger="click">
+        <template v-slot:content>
+          <ShapePool @select="shape => drawShape(shape)" />
+        </template>
+        <StarOutlined class="handler-item" />
+      </Popover>
+      <Popover trigger="click">
+        <template v-slot:content>
+          <LinePool @select="line => drawLine(line)" />
+        </template>
+        <LineOutlined class="handler-item" />
+      </Popover>
       <TableOutlined class="handler-item" />
       <PieChartOutlined class="handler-item" />
     </div>
@@ -29,11 +39,16 @@ import { defineComponent, computed } from 'vue'
 import { useStore } from 'vuex'
 import { MutationTypes, State } from '@/store'
 import { getImageDataURL } from '@/utils/image'
+import { ShapePoolItem } from '@/configs/shapes'
+import { LinePoolItem } from '@/configs/lines'
 import useScaleCanvas from '@/hooks/useScaleCanvas'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 import useCreateElement from '@/hooks/useCreateElement'
 
+import ShapePool from './ShapePool.vue'
+import LinePool from './LinePool.vue'
 import FileInput from '@/components/FileInput.vue'
+import { Popover } from 'ant-design-vue'
 import {
   UndoOutlined,
   RedoOutlined,
@@ -50,6 +65,8 @@ import {
 export default defineComponent({
   name: 'canvas-tool',
   components: {
+    ShapePool,
+    LinePool,
     FileInput,
     UndoOutlined,
     RedoOutlined,
@@ -61,6 +78,7 @@ export default defineComponent({
     PieChartOutlined,
     MinusOutlined,
     PlusOutlined,
+    Popover,
   },
   setup() {
     const store = useStore<State>()
@@ -81,8 +99,25 @@ export default defineComponent({
       getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
     }
 
-    const createElement = (type: string) => {
-      store.commit(MutationTypes.SET_CREATING_ELEMENT_TYPE, type)
+    const drawText = () => {
+      store.commit(MutationTypes.SET_CREATING_ELEMENT, {
+        type: 'text',
+        data: null,
+      })
+    }
+
+    const drawShape = (shape: ShapePoolItem) => {
+      store.commit(MutationTypes.SET_CREATING_ELEMENT, {
+        type: 'shape',
+        data: shape,
+      })
+    }
+
+    const drawLine = (line: LinePoolItem) => {
+      store.commit(MutationTypes.SET_CREATING_ELEMENT, {
+        type: 'line',
+        data: line,
+      })
     }
 
     return {
@@ -93,7 +128,9 @@ export default defineComponent({
       redo,
       undo,
       insertImageElement,
-      createElement,
+      drawText,
+      drawShape,
+      drawLine,
     }
   },
 })