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

+ 34 - 34
.eslintrc.js

@@ -12,57 +12,57 @@ module.exports = {
     ecmaVersion: 2020,
   },
   rules: {
-    'curly': ['error', 'multi-line'], // if、while等仅允许在单行中省略大括号
-    'quotes': ['error', 'single', { // 字符串使用单引号(允许含有单引号的字符串使用双引号,允许模板字符串)
+    'curly': ['error', 'multi-line'],
+    'quotes': ['error', 'single', {
       'avoidEscape': true,
       'allowTemplateLiterals': true,
     }],
-    'key-spacing': ['error', { // 强制在对象字面量的键和值之间使用一致的空格
+    'key-spacing': ['error', {
       'beforeColon': false,
       'afterColon': true,
       'mode': 'strict',
     }],
-    'no-empty': 'error', // 禁止空白块
-    'no-else-return': 'error', // 禁止 if 语句中 return 语句之后有 else 块
-    'no-multi-spaces': 'error', // 禁止出现多个空格
-    'require-await': 'error', // 禁止使用不带 await 表达式的 async 函数
-    'brace-style': ['error', 'stroustrup'], // 大括号风格要求
-    'spaced-comment': ['error', 'always'], // 要求在注释前有空白
-    'arrow-spacing': 'error', // 要求箭头函数的箭头之前或之后有空格
-    'no-duplicate-imports': 'error', // 禁止重复导入
-    'semi': ['error', 'never'], // 禁止行末分号
-    'comma-spacing': ['error', { 'before': false, 'after': true }], // 强制在逗号周围使用空格
-    'indent': ['error', 2, {'SwitchCase': 1}], // 两个空格的缩进
-    'eqeqeq': ['error', 'always', {'null': 'ignore'}], // 必须使用全等判断(null的判断除外)
-    'default-case': 'error', // switch块必须有default结尾
-    'no-eval': 'error', // 禁止eval 
-    'no-var': 'error', // 禁止var
-    'no-with': 'error', // 禁止with
-    'max-depth': ['error', 5], // 代码最大嵌套5层
-    'consistent-this': ['error', 'self'], // 只能使用self代替this
-    'max-lines': ['error', 1200], // 单文件最大1200行
-    'no-multi-str': 'error', // 禁止多行字符串
-    'space-infix-ops': 'error', // 中缀操作符周围有空格
-    'space-before-blocks': ['error', 'always'], // 函数大括号前有空格
-    'space-before-function-paren': ['error', { // 函数小括号前无空格(匿名异步函数前有)
+    'no-empty': 'error',
+    'no-else-return': 'error',
+    'no-multi-spaces': 'error',
+    'require-await': 'error',
+    'brace-style': ['error', 'stroustrup'],
+    'spaced-comment': ['error', 'always'],
+    'arrow-spacing': 'error',
+    'no-duplicate-imports': 'error',
+    'semi': ['error', 'never'],
+    'comma-spacing': ['error', { 'before': false, 'after': true }],
+    'indent': ['error', 2, {'SwitchCase': 1}],
+    'eqeqeq': ['error', 'always', {'null': 'ignore'}],
+    'default-case': 'error',
+    'no-eval': 'error',
+    'no-var': 'error',
+    'no-with': 'error',
+    'max-depth': ['error', 5],
+    'consistent-this': ['error', 'self'],
+    'max-lines': ['error', 1200],
+    'no-multi-str': 'error',
+    'space-infix-ops': 'error',
+    'space-before-blocks': ['error', 'always'],
+    'space-before-function-paren': ['error', {
       'anonymous': 'never',
       'named': 'never',
       'asyncArrow': 'always',
     }],
-    'keyword-spacing': ['error', { 'overrides': { // 强制关键字周围空格的一致性
+    'keyword-spacing': ['error', { 'overrides': {
       'if': { 'after': false },
       'for': { 'after': false },
       'while': { 'after': false },
       'function': { 'after': false },
       'switch': { 'after': false },
     }}],
-    'prefer-const': 'error', // 必须优先使用const
-    'no-useless-return': 'error', // 禁止多余的return
-    'array-bracket-spacing': 'error', // 强制数组方括号中使用一致的空格
-    'no-useless-escape': 'off', // 关闭禁用不必要的转义
-    'no-alert': process.env.NODE_ENV === 'production' ? 'warn' : 'off', // 禁止alert
-    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', // 禁止console
-    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', // 禁止debugger
+    'prefer-const': 'error',
+    'no-useless-return': 'error',
+    'array-bracket-spacing': 'error',
+    'no-useless-escape': 'off',
+    'no-alert': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
   },
   overrides: [
     {

+ 1 - 1
src/App.vue

@@ -5,7 +5,7 @@
 <script>
 import { defineComponent, onMounted } from 'vue'
 import { useStore } from 'vuex'
-import { MutationTypes } from '@/store/constants'
+import { MutationTypes } from '@/store'
 
 export default defineComponent({
   name: 'app',

+ 18 - 25
src/components/Contextmenu/index.vue

@@ -1,13 +1,17 @@
 <template>
-  <div class="contextmenu" 
-    ref="contextmenuRef"
-    v-show="visible" 
+  <div 
+    class="mask"
+    @contextmenu.prevent="removeContextMenu()"
+    @mousedown="removeContextMenu()"
+  ></div>
+
+  <div 
+    class="contextmenu"
     :style="{
       left: style.left,
       top: style.top,
     }"
     @contextmenu.prevent
-    v-click-outside="removeContextMenu"
   >
     <ContextmenuContent 
       :menus="menus" 
@@ -19,11 +23,10 @@
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, nextTick, onMounted, onUnmounted, ref, PropType } from 'vue'
+import { computed, defineComponent, PropType } from 'vue'
 import { ContextmenuItem, Axis } from './types'
 
 import ContextmenuContent from './ContextmenuContent.vue'
-import clickOutside from '@/plugins/clickOutside'
 
 const MENU_WIDTH = 160
 const MENU_HEIGHT = 32
@@ -35,9 +38,6 @@ export default defineComponent({
   components: {
     ContextmenuContent,
   },
-  directives: {
-    'click-outside': clickOutside.directive,
-  },
   props: {
     axis: {
       type: Object as PropType<Axis>,
@@ -61,9 +61,6 @@ export default defineComponent({
     },
   },
   setup(props) {
-    const contextmenuRef = ref<Element | null>(null)
-    const visible = ref(false)
-
     const style = computed(() => {
       const { x, y } = props.axis
       const normalMenuCount = props.menus.filter(menu => !menu.divider && !menu.hide).length
@@ -91,24 +88,12 @@ export default defineComponent({
 
     const handleClickMenuItem = (item: ContextmenuItem) => {
       if(item.disable || item.children) return
-
-      visible.value = false
-      item.action && item.action(props.el)
-
+      if(item.handler) item.handler(props.el)
       props.removeContextMenu()
     }
 
-    onMounted(() => {
-      nextTick(() => visible.value = true)
-    })
-    onUnmounted(() => {
-      if(contextmenuRef.value) document.body.removeChild(contextmenuRef.value)
-    })
-
     return {
-      visible,
       style,
-      contextmenuRef,
       handleClickMenuItem,
     }
   },
@@ -116,6 +101,14 @@ export default defineComponent({
 </script>
 
 <style lang="scss">
+.mask {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 9998;
+}
 .contextmenu {
   position: fixed;
   z-index: 9999;

+ 1 - 1
src/components/Contextmenu/types.ts

@@ -7,7 +7,7 @@ export interface ContextmenuItem {
   hide?: boolean;
   iconPlacehoder?: boolean;
   children?: ContextmenuItem[];
-  action?: (el: HTMLElement) => void;
+  handler?: (el: HTMLElement) => void;
 }
 
 export interface Axis {

+ 0 - 11
src/configs/element.ts

@@ -58,15 +58,4 @@ export const DEFAULT_TABLE = {
   borderStyle: 'solid',
   borderWidth: 2,
   borderColor: DEFAULT_COLOR,
-}
-
-export enum OPERATE_KEYS {
-  LEFT_TOP = 1,
-  TOP = 2,
-  RIGHT_TOP = 3,
-  LEFT = 4,
-  RIGHT = 5,
-  LEFT_BOTTOM = 6,
-  BOTTOM = 7,
-  RIGHT_BOTTOM = 8,
 }

+ 1 - 1
src/plugins/clickOutside.ts

@@ -19,7 +19,7 @@ const ClickOutsideDirective: Directive = {
   },
   
   unmounted(el: HTMLElement) {
-    if(el && el[CTX_CLICK_OUTSIDE_HANDLER]) {
+    if(el[CTX_CLICK_OUTSIDE_HANDLER]) {
       document.removeEventListener('mousedown', el[CTX_CLICK_OUTSIDE_HANDLER])
       delete el[CTX_CLICK_OUTSIDE_HANDLER]
     }

+ 4 - 14
src/store/getters.ts

@@ -1,17 +1,7 @@
-import { PPTElement, Slide, PPTAnimation } from '@/types/slides'
-import { State } from './state'
-
-export type Getters = {
-  currentSlide(state: State): Slide | null;
-  currentSlideAnimations(state: State): PPTAnimation[] | null;
-  activeElementList(state: State): PPTElement[];
-  handleElement(state: State): PPTElement | null;
-  canUndo(state: State): boolean;
-  canRedo(state: State): boolean;
-  ctrlOrShiftKeyActive(state: State): boolean;
-}
-
-export const getters: Getters = {
+import { GetterTree } from 'vuex'
+import { State } from './index'
+
+export const getters: GetterTree<State, State> = {
   currentSlide(state) {
     return state.slides[state.slideIndex] || null
   },

+ 41 - 1
src/store/index.ts

@@ -1,7 +1,47 @@
 import { createStore } from 'vuex'
-import { state } from './state'
 import { mutations } from './mutations'
 import { getters } from './getters'
+import { MutationTypes } from './constants'
+
+import { Slide } from '@/types/slides'
+import { slides } from '@/mocks/index'
+import { FontName } from '@/configs/fontName'
+
+export { MutationTypes }
+
+export interface State {
+  activeElementIdList: string[];
+  handleElementId: string;
+  editorAreaShowScale: number;
+  canvasScale: number;
+  thumbnailsFocus: boolean;
+  editorAreaFocus: boolean;
+  disableHotkeys: boolean;
+  availableFonts: FontName[];
+  slides: Slide[];
+  slideIndex: number;
+  cursor: number;
+  historyRecordLength: number;
+  ctrlKeyState: boolean;
+  shiftKeyState: boolean;
+}
+
+const state: State = {
+  activeElementIdList: [],
+  handleElementId: '',
+  editorAreaShowScale: 85,
+  canvasScale: 1,
+  thumbnailsFocus: false,
+  editorAreaFocus: false,
+  disableHotkeys: false,
+  availableFonts: [],
+  slides: slides,
+  slideIndex: 0,
+  cursor: -1,
+  historyRecordLength: 0,
+  ctrlKeyState: false,
+  shiftKeyState: false,
+}
 
 export default createStore({
   state,

+ 23 - 47
src/store/mutations.ts

@@ -1,5 +1,6 @@
+import { MutationTree } from 'vuex'
 import { MutationTypes } from './constants'
-import { State } from './state'
+import { State } from './index'
 import { Slide, PPTElement } from '@/types/slides'
 import { FONT_NAMES } from '@/configs/fontName'
 import { isSupportFontFamily } from '@/utils/fontFamily'
@@ -14,65 +15,38 @@ interface UpdateElementData {
   props: Partial<PPTElement>;
 }
 
-export type Mutations = {
-  [MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST](state: State, activeElementIdList: string[]): void;
-  [MutationTypes.SET_HANDLE_ELEMENT_ID](state: State, handleElementId: string): void;
-  [MutationTypes.SET_EDITOR_AREA_SHOW_SCALE](state: State, scale: number): void;
-  [MutationTypes.SET_CANVAS_SCALE](state: State, scale: number): void;
-  [MutationTypes.SET_THUMBNAILS_FOCUS](state: State, isFocus: boolean): void;
-  [MutationTypes.SET_EDITORAREA_FOCUS](state: State, isFocus: boolean): void;
-  [MutationTypes.SET_DISABLE_HOTKEYS_STATE](state: State, disable: boolean): void;
-  [MutationTypes.SET_AVAILABLE_FONTS](state: State): void;
-
-  [MutationTypes.SET_SLIDES](state: State, slides: Slide[]): void;
-  [MutationTypes.ADD_SLIDE](state: State, data: AddSlideData): void;
-  [MutationTypes.UPDATE_SLIDE](state: State, data: Partial<Slide>): void;
-  [MutationTypes.DELETE_SLIDE](state: State, slideId: string): void;
-  [MutationTypes.UPDATE_SLIDE_INDEX](state: State, index: number): void;
-  [MutationTypes.ADD_ELEMENT](state: State, element: PPTElement | PPTElement[]): void;
-  [MutationTypes.UPDATE_ELEMENT](state: State, data: UpdateElementData): void;
-  
-  [MutationTypes.SET_CURSOR](state: State, cursor: number): void;
-  [MutationTypes.UNDO](state: State): void;
-  [MutationTypes.REDO](state: State): void;
-  [MutationTypes.SET_HISTORY_RECORD_LENGTH](state: State, length: number): void;
-
-  [MutationTypes.SET_CTRL_KEY_STATE](state: State, isActive: boolean): void;
-  [MutationTypes.SET_SHIFT_KEY_STATE](state: State, isActive: boolean): void;
-}
-
-export const mutations: Mutations = {
+export const mutations: MutationTree<State> = {
 
   // editor
 
-  [MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST](state, activeElementIdList) {
+  [MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST](state, activeElementIdList: string[]) {
     if(activeElementIdList.length === 1) state.handleElementId = activeElementIdList[0]
     else state.handleElementId = ''
     
     state.activeElementIdList = activeElementIdList
   },
   
-  [MutationTypes.SET_HANDLE_ELEMENT_ID](state, handleElementId) {
+  [MutationTypes.SET_HANDLE_ELEMENT_ID](state, handleElementId: string) {
     state.handleElementId = handleElementId
   },
 
-  [MutationTypes.SET_EDITOR_AREA_SHOW_SCALE](state, scale) {
+  [MutationTypes.SET_EDITOR_AREA_SHOW_SCALE](state, scale: number) {
     state.editorAreaShowScale = scale
   },
 
-  [MutationTypes.SET_CANVAS_SCALE](state, scale) {
+  [MutationTypes.SET_CANVAS_SCALE](state, scale: number) {
     state.canvasScale = scale
   },
 
-  [MutationTypes.SET_THUMBNAILS_FOCUS](state, isFocus) {
+  [MutationTypes.SET_THUMBNAILS_FOCUS](state, isFocus: boolean) {
     state.thumbnailsFocus = isFocus
   },
 
-  [MutationTypes.SET_EDITORAREA_FOCUS](state, isFocus) {
+  [MutationTypes.SET_EDITORAREA_FOCUS](state, isFocus: boolean) {
     state.editorAreaFocus = isFocus
   },
 
-  [MutationTypes.SET_DISABLE_HOTKEYS_STATE](state, disable) {
+  [MutationTypes.SET_DISABLE_HOTKEYS_STATE](state, disable: boolean) {
     state.disableHotkeys = disable
   },
 
@@ -82,23 +56,24 @@ export const mutations: Mutations = {
 
   // slides
 
-  [MutationTypes.SET_SLIDES](state, slides) {
+  [MutationTypes.SET_SLIDES](state, slides: Slide[]) {
     state.slides = slides
   },
 
-  [MutationTypes.ADD_SLIDE](state, { index, slide }) {
+  [MutationTypes.ADD_SLIDE](state, data: AddSlideData) {
+    const { index, slide } = data
     const slides = Array.isArray(slide) ? slide : [slide]
     const addIndex = index !== undefined ? index : (state.slideIndex + 1)
     state.slides.splice(addIndex, 0, ...slides)
     state.slideIndex = addIndex
   },
 
-  [MutationTypes.UPDATE_SLIDE](state, props) {
+  [MutationTypes.UPDATE_SLIDE](state, props: Partial<Slide>) {
     const slideIndex = state.slideIndex
     state.slides[slideIndex] = { ...state.slides[slideIndex], ...props }
   },
 
-  [MutationTypes.DELETE_SLIDE](state, slideId) {
+  [MutationTypes.DELETE_SLIDE](state, slideId: string) {
     const deleteIndex = state.slides.findIndex(item => item.id === slideId)
 
     if(deleteIndex === state.slides.length - 1) {
@@ -107,18 +82,19 @@ export const mutations: Mutations = {
     state.slides.splice(deleteIndex, 1)
   },
 
-  [MutationTypes.UPDATE_SLIDE_INDEX](state, index) {
+  [MutationTypes.UPDATE_SLIDE_INDEX](state, index: number) {
     state.slideIndex = index
   },
 
-  [MutationTypes.ADD_ELEMENT](state, element) {
+  [MutationTypes.ADD_ELEMENT](state, element: PPTElement | PPTElement[]) {
     const elements = Array.isArray(element) ? element : [element]
     const currentSlideEls = state.slides[state.slideIndex].elements
     const newEls = [...currentSlideEls, ...elements]
     state.slides[state.slideIndex].elements = newEls
   },
 
-  [MutationTypes.UPDATE_ELEMENT](state, { elId, props }) {
+  [MutationTypes.UPDATE_ELEMENT](state, data: UpdateElementData) {
+    const { elId, props } = data
     const elIdList = typeof elId === 'string' ? [elId] : elId
 
     const slideIndex = state.slideIndex
@@ -131,7 +107,7 @@ export const mutations: Mutations = {
 
   // history
 
-  [MutationTypes.SET_CURSOR](state, cursor) {
+  [MutationTypes.SET_CURSOR](state, cursor: number) {
     state.cursor = cursor
   },
 
@@ -143,16 +119,16 @@ export const mutations: Mutations = {
     state.cursor += 1
   },
 
-  [MutationTypes.SET_HISTORY_RECORD_LENGTH](state, length) {
+  [MutationTypes.SET_HISTORY_RECORD_LENGTH](state, length: number) {
     state.historyRecordLength = length
   },
 
   // keyBoard
 
-  [MutationTypes.SET_CTRL_KEY_STATE](state, isActive) {
+  [MutationTypes.SET_CTRL_KEY_STATE](state, isActive: boolean) {
     state.ctrlKeyState = isActive
   },
-  [MutationTypes.SET_SHIFT_KEY_STATE](state, isActive) {
+  [MutationTypes.SET_SHIFT_KEY_STATE](state, isActive: boolean) {
     state.shiftKeyState = isActive
   },
 }

+ 0 - 37
src/store/state.ts

@@ -1,37 +0,0 @@
-import { Slide } from '@/types/slides'
-import { slides } from '@/mocks/index'
-import { FontName } from '@/configs/fontName'
-
-export type State = {
-  activeElementIdList: string[];
-  handleElementId: string;
-  editorAreaShowScale: number;
-  canvasScale: number;
-  thumbnailsFocus: boolean;
-  editorAreaFocus: boolean;
-  disableHotkeys: boolean;
-  availableFonts: FontName[];
-  slides: Slide[];
-  slideIndex: number;
-  cursor: number;
-  historyRecordLength: number;
-  ctrlKeyState: boolean;
-  shiftKeyState: boolean;
-}
-
-export const state: State = {
-  activeElementIdList: [],
-  handleElementId: '',
-  editorAreaShowScale: 85,
-  canvasScale: 1,
-  thumbnailsFocus: false,
-  editorAreaFocus: false,
-  disableHotkeys: false,
-  availableFonts: [],
-  slides: slides,
-  slideIndex: 0,
-  cursor: -1,
-  historyRecordLength: 0,
-  ctrlKeyState: false,
-  shiftKeyState: false,
-}

+ 11 - 7
src/types/edit.ts

@@ -20,13 +20,6 @@ export enum ElementAlignCommands {
 
 export type ElementScaleHandler = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
 
-export type ElementLockCommand = 'lock' | 'unlock'
-
-export enum ElementLockCommands {
-  LOCK = 'lock',
-  UNLOCK = 'unlock',
-}
-
 export type OperateBorderLineType = 't' | 'b' | 'l' | 'r'
 
 export enum OperateBorderLineTypes {
@@ -48,4 +41,15 @@ export enum OperateResizablePointTypes {
   BC = 'b-c',
   BR = 'b-r',
   ANY = 'any',
+}
+
+export enum OPERATE_KEYS {
+  LEFT_TOP = 1,
+  TOP = 2,
+  RIGHT_TOP = 3,
+  LEFT = 4,
+  RIGHT = 5,
+  LEFT_BOTTOM = 6,
+  BOTTOM = 7,
+  RIGHT_BOTTOM = 8,
 }

+ 10 - 8
src/utils/clipboard.ts

@@ -24,12 +24,14 @@ export const copyText = (text: string) => {
 }
 
 // 读取剪贴板
-export const readClipboard = () => {
-  if(navigator.clipboard) {
-    navigator.clipboard.readText().then(text => {
-      if(!text) return { err: '剪贴板为空或者不包含文本' }
-      return { text }
-    })
-  }
-  return { err: '浏览器不支持或禁止访问剪贴板' }
+export const readClipboard = (): Promise<string> => {
+  return new Promise((resolve, reject) => {
+    if(navigator.clipboard) {
+      navigator.clipboard.readText().then(text => {
+        if(!text) reject('剪贴板为空或者不包含文本')
+        return resolve(text)
+      })
+    }
+    else reject('浏览器不支持或禁止访问剪贴板')
+  })
 }

+ 3 - 5
src/views/Editor/Canvas/AlignmentLine.vue

@@ -16,15 +16,13 @@
 <script lang="ts">
 import { PropType } from 'vue'
 
-type AlignmentLineType = 'vertical' | 'horizontal'
-
 interface Axis {
-  x: number;
+  x: number; 
   y: number;
 }
 
 export interface AlignmentLineProps {
-  type: AlignmentLineType;
+  type: 'vertical' | 'horizontal';
   axis: Axis;
   length: number;
 }
@@ -33,7 +31,7 @@ export default {
   name: 'alignment-line',
   props: {
     type: {
-      type: String as PropType<AlignmentLineType>,
+      type: String as PropType<'vertical' | 'horizontal'>,
       required: true,
     },
     axis: {

+ 18 - 16
src/views/Editor/Canvas/MultiSelectOperate.vue

@@ -22,16 +22,17 @@
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, reactive, PropType, watch, toRefs, onMounted } from 'vue'
-import { OPERATE_KEYS } from '@/configs/element'
+import { computed, defineComponent, reactive, PropType, watchEffect, toRefs } from 'vue'
+import { useStore } from 'vuex'
+import { State } from '@/store'
 import { PPTElement, ElementTypes } from '@/types/slides'
 import { getElementListRange } from './utils/elementRange'
-import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
+import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes, OPERATE_KEYS } from '@/types/edit'
 
 import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'
 import BorderLine from '@/views/_common/_operate/BorderLine.vue'
 
-interface Range {
+export interface MultiSelectRange {
   minX: number;
   maxX: number;
   minY: number;
@@ -45,20 +46,21 @@ export default defineComponent({
     BorderLine,
   },
   props: {
-    canvasScale: {
-      type: Number,
-      required: true,
-    },
-    activeElementList: {
+    elementList: {
       type: Array as PropType<PPTElement[]>,
       required: true,
     },
     scaleMultiElement: {
-      type: Function as PropType<(e: MouseEvent, range: Range, command: ElementScaleHandler) => void>,
+      type: Function as PropType<(e: MouseEvent, range: MultiSelectRange, command: ElementScaleHandler) => void>,
       required: true,
     },
   },
   setup(props) {
+    const store = useStore<State>()
+    const canvasScale = computed(() => store.state.canvasScale)
+    const activeElementIdList = computed(() => store.state.activeElementIdList)
+    const localActiveElementList = computed(() => props.elementList.filter(el => activeElementIdList.value.includes(el.elId)))
+
     const range = reactive({
       minX: 0,
       maxX: 0,
@@ -66,8 +68,8 @@ export default defineComponent({
       maxY: 0,
     })
 
-    const width = computed(() => (range.maxX - range.minX) * props.canvasScale)
-    const height = computed(() => (range.maxY - range.minY) * props.canvasScale)
+    const width = computed(() => (range.maxX - range.minX) * canvasScale.value)
+    const height = computed(() => (range.maxY - range.minY) * canvasScale.value)
 
     const resizablePoints = computed(() => {
       return [
@@ -92,7 +94,7 @@ export default defineComponent({
     })
 
     const disableResizablePoint = computed(() => {
-      return props.activeElementList.some(item => {
+      return localActiveElementList.value.some(item => {
         if(
           (item.type === ElementTypes.IMAGE || item.type === ElementTypes.SHAPE) && 
           !item.rotate
@@ -102,18 +104,18 @@ export default defineComponent({
     })
 
     const setRange = () => {
-      const { minX, maxX, minY, maxY } = getElementListRange(props.activeElementList)
+      const { minX, maxX, minY, maxY } = getElementListRange(localActiveElementList.value)
       range.minX = minX
       range.maxX = maxX
       range.minY = minY
       range.maxY = maxY
     }
 
-    onMounted(setRange)
-    watch(props.activeElementList, setRange)
+    watchEffect(setRange)
 
     return {
       ...toRefs(range),
+      canvasScale,
       borderLines,
       disableResizablePoint,
       resizablePoints,

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

@@ -1,6 +1,6 @@
 import { ref, computed, onMounted, onUnmounted, Ref } from 'vue'
 import { useStore } from 'vuex'
-import { State } from '@/store/state'
+import { State } from '@/store'
 import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
 
 export default (canvasRef: Ref<HTMLElement | null>) => {

+ 547 - 39
src/views/Editor/Canvas/index.vue

@@ -38,8 +38,7 @@
 
       <MultiSelectOperate 
         v-if="activeElementIdList.length > 1"
-        :activeElementList="activeElementList"
-        :canvasScale="canvasScale"
+        :elementList="elementList"
         :scaleMultiElement="scaleMultiElement"
       />
 
@@ -52,16 +51,16 @@
         :isHandleEl="element.elId === handleElementId"
         :isActiveGroupElement="activeGroupElementId === element.elId"
         :isMultiSelect="activeElementIdList.length > 1"
-        :canvasScale="canvasScale"
         :selectElement="selectElement"
         :rotateElement="rotateElement"
         :scaleElement="scaleElement"
         :orderElement="orderElement"
         :combineElements="combineElements"
         :uncombineElements="uncombineElements"
-        :alignElement="alignElement"
+        :alignElementToCanvas="alignElementToCanvas"
         :deleteElement="deleteElement"
         :lockElement="lockElement"
+        :unlockElement="unlockElement"
         :copyElement="copyElement"
         :cutElement="cutElement"
       />
@@ -70,17 +69,27 @@
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, onMounted, reactive, ref, watch } from 'vue'
+import { computed, defineComponent, reactive, ref, watch, watchEffect } from 'vue'
 import { useStore } from 'vuex'
 import uniq from 'lodash/uniq'
-import { State } from '@/store/state'
-import { MutationTypes } from '@/store/constants'
+import { message } from 'ant-design-vue'
+import { State, MutationTypes } from '@/store'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
+import { ElementTypes, PPTElement, PPTLineElement, PPTTextElement, PPTImageElement, PPTShapeElement } from '@/types/slides'
+import { OPERATE_KEYS, ElementOrderCommand, ElementAlignCommand, ElementScaleHandler } from '@/types/edit'
+
 import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
 import { getImageDataURL } from '@/utils/image'
-import { getElementRange } from './utils/elementRange'
+import { copyText, readClipboard } from '@/utils/clipboard'
+import { encrypt, decrypt } from '@/utils/crypto'
 
-import { PPTElement } from '@/types/slides'
+import { getElementRange } from './utils/elementRange'
+import { getAngleFromCoordinate, getRotateElementPoints, getOppositePoint } from './utils/elementRotate'
+import { lockElement as _lockElement, unlockElement as _unlockElement } from './utils/elementLock'
+import { combineElements as _combineElements, uncombineElements as _uncombineElements } from './utils/elementCombine'
+import { orderElement as _orderElement } from './utils/elementOrder'
+import { alignElementToCanvas as _alignElementToCanvas } from './utils/elementAlignToCanvas'
+import { AlignLine, uniqAlignLines } from './utils/alignLines'
 
 import useDropImage from '@/hooks/useDropImage'
 import useSetViewportSize from './hooks/useSetViewportSize'
@@ -88,11 +97,11 @@ import useSetViewportSize from './hooks/useSetViewportSize'
 import EditableElement from '@/views/_common/_element/EditableElement.vue'
 import MouseSelection from './MouseSelection.vue'
 import SlideBackground from './SlideBackground.vue'
-import MultiSelectOperate from './MultiSelectOperate.vue'
+import MultiSelectOperate, { MultiSelectRange } from './MultiSelectOperate.vue'
 import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue'
 
 export default defineComponent({
-  name: 'v-canvas',
+  name: 'editor-canvas',
   components: {
     EditableElement,
     MouseSelection,
@@ -118,8 +127,7 @@ export default defineComponent({
     const setLocalElementList = () => {
       elementList.value = currentSlide.value ? JSON.parse(JSON.stringify(currentSlide.value.elements)) : []
     }
-    onMounted(setLocalElementList)
-    watch(currentSlide, setLocalElementList)
+    watchEffect(setLocalElementList)
 
     const dropImageFile = useDropImage(viewportRef)
     watch(dropImageFile, () => {
@@ -253,12 +261,7 @@ export default defineComponent({
           return true
         })
         const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
-
-        // 原本就存在激活元素(可能需要清空),或者本次选择了至少一个元素(可能需要选择),才会具体更新激活元素状态
-        // 否则不做多余的激活元素状态更新(原本就没有激活元素,本次也没有选择任何元素,只是点击了一下空白区域,状态为:空 -> 空)
-        if(activeElementIdList.value.length > 0 || inRangeElementIdList.length) {
-          store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, inRangeElementIdList)
-        }
+        if(inRangeElementIdList.length) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, inRangeElementIdList)
 
         mouseSelectionState.isShow = false
       }
@@ -267,6 +270,7 @@ export default defineComponent({
     const editorAreaFocus = computed(() => store.state.editorAreaFocus)
 
     const handleClickBlankArea = (e: MouseEvent) => {
+      store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
       if(!ctrlOrShiftKeyActive.value) updateMouseSelection(e)
       if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
     }
@@ -278,6 +282,7 @@ export default defineComponent({
     const moveElement = (e: MouseEvent, element: PPTElement) => {
       console.log(e, element)
     }
+
     const selectElement = (e: MouseEvent, element: PPTElement, canMove = true) => {
       if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
 
@@ -346,38 +351,538 @@ export default defineComponent({
 
       if(canMove) moveElement(e, element)
     }
-    const rotateElement = () => {
-      console.log('rotateElement')
+
+    const rotateElement = (element: PPTTextElement | PPTImageElement | PPTShapeElement) => {
+      let isMouseDown = true
+      let angle = 0
+      const elOriginRotate = element.rotate || 0
+
+      // 计算元素中心(旋转的中心,坐标原点)
+      const elLeft = element.left
+      const elTop = element.top
+      const elWidth = element.width
+      const elHeight = element.height
+      const centerX = elLeft + elWidth / 2
+      const centerY = elTop + elHeight / 2
+
+      if(!viewportRef.value) return
+      const viewportRect = viewportRef.value.getBoundingClientRect()
+
+      document.onmousemove = e => {
+        if(!isMouseDown) return
+        
+        // 计算鼠标基于旋转中心的坐标
+        const mouseX = (e.pageX - viewportRect.left) / canvasScale.value
+        const mouseY = (e.pageY - viewportRect.top) / canvasScale.value
+        const x = mouseX - centerX
+        const y = centerY - mouseY
+
+        angle = getAngleFromCoordinate(x, y)
+
+        // 45°的倍数位置有吸附效果
+        const sorptionRange = 5
+        if( Math.abs(angle) <= sorptionRange ) angle = 0
+        else if( angle > 0 && Math.abs(angle - 45) <= sorptionRange ) angle -= (angle - 45)
+        else if( angle < 0 && Math.abs(angle + 45) <= sorptionRange ) angle -= (angle + 45)
+        else if( angle > 0 && Math.abs(angle - 90) <= sorptionRange ) angle -= (angle - 90)
+        else if( angle < 0 && Math.abs(angle + 90) <= sorptionRange ) angle -= (angle + 90)
+        else if( angle > 0 && Math.abs(angle - 135) <= sorptionRange ) angle -= (angle - 135)
+        else if( angle < 0 && Math.abs(angle + 135) <= sorptionRange ) angle -= (angle + 135)
+        else if( angle > 0 && Math.abs(angle - 180) <= sorptionRange ) angle -= (angle - 180)
+        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)
+      }
+
+      document.onmouseup = () => {
+        isMouseDown = false
+        document.onmousemove = null
+        document.onmouseup = null
+
+        if(elOriginRotate === angle) return
+
+        store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
+      }
     }
-    const scaleElement = () => {
-      console.log('scaleElement')
+
+    const scaleElement = (e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: ElementScaleHandler) => {
+      let isMouseDown = true
+
+      const elOriginLeft = element.left
+      const elOriginTop = element.top
+      const elOriginWidth = element.width
+      const elOriginHeight = element.height
+
+      const isLockRatio = ctrlOrShiftKeyActive.value || ('lockRatio' in element && element.lockRatio)
+      const lockRatio = elOriginWidth / elOriginHeight
+      
+      const elRotate = ('rotate' in element && element.rotate) ? element.rotate : 0
+      const rotateRadian = Math.PI * elRotate / 180
+
+      const startPageX = e.pageX
+      const startPageY = e.pageY
+
+      const minSize = 15
+      const getSizeWithinRange = (size: number) => size < minSize ? minSize : size
+
+      let points: ReturnType<typeof getRotateElementPoints>
+      let baseLeft = 0
+      let baseTop = 0
+      let horizontalLines: AlignLine[] = []
+      let verticalLines: AlignLine[] = []
+
+      if('rotate' in element && element.rotate) {
+        // 元素旋转后的各点坐标以及对角坐标
+        const { left, top, width, height } = element
+        points = getRotateElementPoints({ left, top, width, height }, elRotate)
+        const oppositePoint = getOppositePoint(command, points)
+  
+        // 基点坐标(以操作点相对的点为基点,例如拖动右下角,实际上是保持左上角不变的前提下修改其他信息)
+        baseLeft = oppositePoint.left
+        baseTop = oppositePoint.top
+      }
+      else {
+        const edgeWidth = VIEWPORT_SIZE
+        const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
+        const isActiveGroupElement = element.elId === 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
+
+          const left = el.left
+          const top = el.top
+          const width = el.width
+          const height = el.height
+          const right = left + width
+          const bottom = top + height
+
+          const topLine: AlignLine = { value: top, range: [left, right] }
+          const bottomLine: AlignLine = { value: bottom, range: [left, right] }
+          const leftLine: AlignLine = { value: left, range: [top, bottom] }
+          const rightLine: AlignLine = { value: right, range: [top, bottom] }
+
+          horizontalLines.push(topLine, bottomLine)
+          verticalLines.push(leftLine, rightLine)
+        }
+
+        // 页面边界、水平和垂直的中心位置
+        const edgeTopLine: AlignLine = { value: 0, range: [0, edgeWidth] }
+        const edgeBottomLine: AlignLine = { value: edgeHeight, range: [0, edgeWidth] }
+        const edgeHorizontalCenterLine: AlignLine = { value: edgeHeight / 2, range: [0, edgeWidth] }
+        const edgeLeftLine: AlignLine = { value: 0, range: [0, edgeHeight] }
+        const edgeRightLine: AlignLine = { value: edgeWidth, range: [0, edgeHeight] }
+        const edgeVerticalCenterLine: AlignLine = { value: edgeWidth / 2, range: [0, edgeHeight] }
+
+        horizontalLines.push(edgeTopLine, edgeBottomLine, edgeHorizontalCenterLine)
+        verticalLines.push(edgeLeftLine, edgeRightLine, edgeVerticalCenterLine)
+        
+        horizontalLines = uniqAlignLines(horizontalLines)
+        verticalLines = uniqAlignLines(verticalLines)
+      }
+      
+      // 对齐吸附方法
+      const alignedAdsorption = (currentX: number | null, currentY: number | null) => {
+        const sorptionRange = 3
+
+        const _alignmentLines: AlignmentLineProps[] = []
+        let isVerticalAdsorbed = false
+        let isHorizontalAdsorbed = false
+        const correctionVal = { offsetX: 0, offsetY: 0 }
+        
+        if(currentY || currentY === 0) {
+          for(let i = 0; i < horizontalLines.length; i++) {
+            const { value, range } = horizontalLines[i]
+            const min = Math.min(...range, currentX || 0)
+            const max = Math.max(...range, currentX || 0)
+            
+            if(Math.abs(currentY - value) < sorptionRange) {
+              if(!isHorizontalAdsorbed) {
+                correctionVal.offsetY = currentY - value
+                isHorizontalAdsorbed = true
+              }
+              _alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
+            }
+          }
+        }
+        if(currentX || currentX === 0) {
+          for(let i = 0; i < verticalLines.length; i++) {
+            const { value, range } = verticalLines[i]
+            const min = Math.min(...range, (currentY || 0))
+            const max = Math.max(...range, (currentY || 0))
+  
+            if(Math.abs(currentX - value) < sorptionRange) {
+              if(!isVerticalAdsorbed) {
+                correctionVal.offsetX = currentX - value
+                isVerticalAdsorbed = true
+              }
+              _alignmentLines.push({ type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40 })
+            }
+          }
+        }
+        alignmentLines.value = _alignmentLines
+        return correctionVal
+      }
+
+      document.onmousemove = e => {
+        if(!isMouseDown) return
+
+        const currentPageX = e.pageX
+        const currentPageY = e.pageY
+
+        const x = currentPageX - startPageX
+        const y = currentPageY - startPageY
+
+        let width = elOriginWidth
+        let height = elOriginHeight
+        let left = elOriginLeft
+        let top = elOriginTop
+        
+        // 元素被旋转的情况下
+        if(elRotate) {
+          // 根据元素旋转的角度,修正鼠标按下后移动的距离(因为拖动的方向发生了改变)
+          const revisedX = (Math.cos(rotateRadian) * x + Math.sin(rotateRadian) * y) / canvasScale.value
+          let revisedY = (Math.cos(rotateRadian) * y - Math.sin(rotateRadian) * x) / canvasScale.value
+  
+          // 锁定宽高比例
+          if(isLockRatio) {
+            if(command === OPERATE_KEYS.RIGHT_BOTTOM || command === OPERATE_KEYS.LEFT_TOP) revisedY = revisedX / lockRatio
+            if(command === OPERATE_KEYS.LEFT_BOTTOM || command === OPERATE_KEYS.RIGHT_TOP) revisedY = -revisedX / lockRatio
+          }
+  
+          // 根据不同的操作点分别计算元素缩放后的大小和位置
+          // 这里计算的位置是错误的,因为旋转后缩放实际上也改变了元素的位置,需要在后面进行矫正
+          // 这里计算的大小是正确的,因为上面修正鼠标按下后移动的距离时其实已经进行过了矫正
+          if(command === OPERATE_KEYS.RIGHT_BOTTOM) {
+            width = getSizeWithinRange(elOriginWidth + revisedX)
+            height = getSizeWithinRange(elOriginHeight + revisedY)
+          }
+          else if(command === OPERATE_KEYS.LEFT_BOTTOM) {
+            width = getSizeWithinRange(elOriginWidth - revisedX)
+            height = getSizeWithinRange(elOriginHeight + revisedY)
+            left = elOriginLeft - (width - elOriginWidth)
+          }
+          else if(command === OPERATE_KEYS.LEFT_TOP) {
+            width = getSizeWithinRange(elOriginWidth - revisedX)
+            height = getSizeWithinRange(elOriginHeight - revisedY)
+            left = elOriginLeft - (width - elOriginWidth)
+            top = elOriginTop - (height - elOriginHeight)
+          }
+          else if(command === OPERATE_KEYS.RIGHT_TOP) {
+            width = getSizeWithinRange(elOriginWidth + revisedX)
+            height = getSizeWithinRange(elOriginHeight - revisedY)
+            top = elOriginTop - (height - elOriginHeight)
+          }
+          else if(command === OPERATE_KEYS.TOP) {
+            height = getSizeWithinRange(elOriginHeight - revisedY)
+            top = elOriginTop - (height - elOriginHeight)
+          }
+          else if(command === OPERATE_KEYS.BOTTOM) {
+            height = getSizeWithinRange(elOriginHeight + revisedY)
+          }
+          else if(command === OPERATE_KEYS.LEFT) {
+            width = getSizeWithinRange(elOriginWidth - revisedX)
+            left = elOriginLeft - (width - elOriginWidth)
+          }
+          else if(command === OPERATE_KEYS.RIGHT) {
+            width = getSizeWithinRange(elOriginWidth + revisedX)
+          }
+  
+          // 获取当前元素基点坐标,与初始状态的基点坐标进行对比并矫正差值
+          const currentPoints = getRotateElementPoints({ width, height, left, top }, elRotate)
+          const currentOppositePoint = getOppositePoint(command, currentPoints)
+          const currentBaseLeft = currentOppositePoint.left
+          const currentBaseTop = currentOppositePoint.top
+  
+          const offsetX = currentBaseLeft - baseLeft
+          const offsetY = currentBaseTop - baseTop
+  
+          left = left - offsetX
+          top = top - offsetY
+        }
+
+        // 元素未被旋转的情况下,根据所操纵点的位置添加对齐吸附
+        else {
+          let moveX = x / canvasScale.value
+          let moveY = y / canvasScale.value
+
+          if(isLockRatio) {
+            if(command === OPERATE_KEYS.RIGHT_BOTTOM || command === OPERATE_KEYS.LEFT_TOP) moveY = moveX / lockRatio
+            if(command === OPERATE_KEYS.LEFT_BOTTOM || command === OPERATE_KEYS.RIGHT_TOP) moveY = -moveX / lockRatio
+          }
+
+          if(command === OPERATE_KEYS.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
+            }
+            width = getSizeWithinRange(elOriginWidth + moveX)
+            height = getSizeWithinRange(elOriginHeight + moveY)
+          }
+          else if(command === OPERATE_KEYS.LEFT_BOTTOM) {
+            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
+            }
+            width = getSizeWithinRange(elOriginWidth - moveX)
+            height = getSizeWithinRange(elOriginHeight + moveY)
+            left = elOriginLeft - (width - elOriginWidth)
+          }
+          else if(command === OPERATE_KEYS.LEFT_TOP) {
+            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
+            }
+            width = getSizeWithinRange(elOriginWidth - moveX)
+            height = getSizeWithinRange(elOriginHeight - moveY)
+            left = elOriginLeft - (width - elOriginWidth)
+            top = elOriginTop - (height - elOriginHeight)
+          }
+          else if(command === OPERATE_KEYS.RIGHT_TOP) {
+            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
+            }
+            width = getSizeWithinRange(elOriginWidth + moveX)
+            height = getSizeWithinRange(elOriginHeight - moveY)
+            top = elOriginTop - (height - elOriginHeight)
+          }
+          else if(command === OPERATE_KEYS.LEFT) {
+            const { offsetX } = alignedAdsorption(elOriginLeft + moveX, null)
+            moveX = moveX - offsetX
+            width = getSizeWithinRange(elOriginWidth - moveX)
+            left = elOriginLeft - (width - elOriginWidth)
+          }
+          else if(command === OPERATE_KEYS.RIGHT) {
+            const { offsetX } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, null)
+            moveX = moveX - offsetX
+            width = getSizeWithinRange(elOriginWidth + moveX)
+          }
+          else if(command === OPERATE_KEYS.TOP) {
+            const { offsetY } = alignedAdsorption(null, elOriginTop + moveY)
+            moveY = moveY - offsetY
+            height = getSizeWithinRange(elOriginHeight - moveY)
+            top = elOriginTop - (height - elOriginHeight)
+          }
+          else if(command === OPERATE_KEYS.BOTTOM) {
+            const { offsetY } = alignedAdsorption(null, elOriginTop + elOriginHeight + moveY)
+            moveY = moveY - offsetY
+            height = getSizeWithinRange(elOriginHeight + moveY)
+          }
+        }
+        
+        elementList.value = elementList.value.map(el => element.elId === el.elId ? { ...el, left, top, width, height } : el)
+      }
+
+      document.onmouseup = e => {
+        isMouseDown = false
+        document.onmousemove = null
+        document.onmouseup = null
+        alignmentLines.value = []
+
+        if(startPageX === e.pageX && startPageY === e.pageY) return
+
+        store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
+      }
     }
-    const scaleMultiElement = () => {
-      console.log('scaleMultiElement')
+
+    const scaleMultiElement = (e: MouseEvent, range: MultiSelectRange, command: ElementScaleHandler) => {
+      let isMouseDown = true
+      
+      const { minX, maxX, minY, maxY } = range
+      const operateWidth = maxX - minX
+      const operateHeight = maxY - minY
+      const lockRatio = operateWidth / operateHeight
+
+      const startPageX = e.pageX
+      const startPageY = e.pageY
+  
+      const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
+  
+      document.onmousemove = e => {
+        if(!isMouseDown) return
+        
+        const currentPageX = e.pageX
+        const currentPageY = e.pageY
+  
+        // 鼠标按下后移动的距离
+        const x = (currentPageX - startPageX) / canvasScale.value
+        let y = (currentPageY - startPageY) / canvasScale.value
+
+        // 锁定宽高比例
+        if(ctrlOrShiftKeyActive.value) {
+          if(command === OPERATE_KEYS.RIGHT_BOTTOM || command === OPERATE_KEYS.LEFT_TOP) y = x / lockRatio
+          if(command === OPERATE_KEYS.LEFT_BOTTOM || command === OPERATE_KEYS.RIGHT_TOP) y = -x / lockRatio
+        }
+
+        // 获取鼠标缩放时当前所有激活元素的范围
+        let currentMinX = minX
+        let currentMaxX = maxX
+        let currentMinY = minY
+        let currentMaxY = maxY
+
+        if(command === OPERATE_KEYS.RIGHT_BOTTOM) {
+          currentMaxX = maxX + x
+          currentMaxY = maxY + y
+        }
+        else if(command === OPERATE_KEYS.LEFT_BOTTOM) {
+          currentMinX = minX + x
+          currentMaxY = maxY + y
+        }
+        else if(command === OPERATE_KEYS.LEFT_TOP) {
+          currentMinX = minX + x
+          currentMinY = minY + y
+        }
+        else if(command === OPERATE_KEYS.RIGHT_TOP) {
+          currentMaxX = maxX + x
+          currentMinY = minY + y
+        }
+        else if(command === OPERATE_KEYS.TOP) {
+          currentMinY = minY + y
+        }
+        else if(command === OPERATE_KEYS.BOTTOM) {
+          currentMaxY = maxY + y
+        }
+        else if(command === OPERATE_KEYS.LEFT) {
+          currentMinX = minX + x
+        }
+        else if(command === OPERATE_KEYS.RIGHT) {
+          currentMaxX = maxX + x
+        }
+
+        // 多选下所有元素整体宽高
+        const currentOppositeWidth = currentMaxX - currentMinX
+        const currentOppositeHeight = currentMaxY - currentMinY
+
+        // 所有元素的整体宽高与被操作元素宽高的比例
+        let widthScale = currentOppositeWidth / operateWidth
+        let heightScale = currentOppositeHeight / operateHeight
+
+        if(widthScale <= 0) widthScale = 0
+        if(heightScale <= 0) heightScale = 0
+        
+        // 根据上面计算的比例,修改所有被激活元素的位置大小
+        // 宽高通过乘以对应的比例得到,位置通过将被操作元素在所有元素整体中的相对位置乘以对应比例获得
+        elementList.value = elementList.value.map(el => {
+          const newEl = el
+          if((newEl.type === ElementTypes.IMAGE || newEl.type === ElementTypes.SHAPE) && activeElementIdList.value.includes(newEl.elId)) {
+            const originElement = originElementList.find(originEl => originEl.elId === el.elId)
+            if(originElement && (originElement.type === ElementTypes.IMAGE || originElement.type === ElementTypes.SHAPE)) {
+              newEl.width = originElement.width * widthScale
+              newEl.height = originElement.height * heightScale
+              newEl.left = currentMinX + (originElement.left - minX) * widthScale
+              newEl.top = currentMinY + (originElement.top - minY) * heightScale
+            }
+          }
+          return newEl
+        })
+      }
+
+      document.onmouseup = e => {
+        isMouseDown = false
+        document.onmousemove = null
+        document.onmouseup = null
+
+        if(startPageX === e.pageX && startPageY === e.pageY) return
+
+        store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
+      }
     }
-    const orderElement = () => {
-      console.log('orderElement')
+
+    const orderElement = (element: PPTElement, command: ElementOrderCommand) => {
+      const newElementList = _orderElement(elementList.value, element, command)
+      store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
     }
+    
     const combineElements = () => {
-      console.log('combineElements')
+      const newElementList = _combineElements(elementList.value, activeElementList.value, activeElementIdList.value)
+      store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
     }
+
     const uncombineElements = () => {
-      console.log('uncombineElements')
+      const newElementList = _uncombineElements(elementList.value, activeElementList.value, activeElementIdList.value)
+      store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
     }
-    const alignElement = () => {
-      console.log('alignElement')
+
+    const alignElementToCanvas = (command: ElementAlignCommand) => {
+      const newElementList = _alignElementToCanvas(elementList.value, activeElementList.value, activeElementIdList.value, command)
+      store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
+    }
+
+    const selectAllElement = () => {
+      const unlockedElements = elementList.value.filter(el => !el.isLock)
+      const newActiveElementIdList = unlockedElements.map(el => el.elId)
+      store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveElementIdList)
     }
+
     const deleteElement = () => {
-      console.log('deleteElement')
+      if(!activeElementIdList.value.length) return
+      const newElementList = elementList.value.filter(el => !activeElementIdList.value.includes(el.elId))
+      store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
+      store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
     }
-    const lockElement = () => {
-      console.log('lockElement')
+
+    const deleteAllElements = () => {
+      if(!elementList.value.length) return
+      store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
+      store.commit(MutationTypes.UPDATE_SLIDE, { elements: [] })
+    }
+
+    const lockElement = (element: PPTElement) => {
+      const newElementList = _lockElement(elementList.value, element, activeElementIdList.value)
+      store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
+    }
+
+    const unlockElement = (element: PPTElement) => {
+      const newElementList = _unlockElement(elementList.value, element)
+      store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
     }
+
     const copyElement = () => {
-      console.log('copyElement')
+      if(!activeElementIdList.value.length) return
+
+      const text = encrypt(JSON.stringify({
+        type: 'elements',
+        data: activeElementList.value,
+      }))
+
+      copyText(text).then(() => {
+        store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
+        message.success('元素已复制到剪贴板', 0.8)
+      })
     }
+
     const cutElement = () => {
-      console.log('cutElement')
+      copyElement()
+      deleteElement()
+    }
+
+    const pasteElement = () => {
+      readClipboard().then(text => {
+        let clipboardData
+        try {
+          clipboardData = JSON.parse(decrypt(text))
+        }
+        catch {
+          clipboardData = text
+        }
+        console.log(clipboardData)
+      }).catch(err => message.warning(err))
     }
 
     const contextmenus = (): ContextmenuItem[] => {
@@ -385,13 +890,16 @@ export default defineComponent({
         {
           text: '全选',
           subText: 'Ctrl + A',
+          handler: selectAllElement,
         },
         {
           text: '粘贴',
           subText: 'Ctrl + V',
+          handler: pasteElement,
         },
         {
-          text: '清空页面',
+          text: '清空本页',
+          handler: deleteAllElements,
         },
       ]
     }
@@ -419,9 +927,10 @@ export default defineComponent({
       orderElement,
       combineElements,
       uncombineElements,
-      alignElement,
+      alignElementToCanvas,
       deleteElement,
       lockElement,
+      unlockElement,
       copyElement,
       cutElement,
       contextmenus,
@@ -438,7 +947,6 @@ export default defineComponent({
   background-color: #f9f9f9;
   position: relative;
 }
-
 .viewport {
   position: absolute;
   transform-origin: 0 0;

+ 1 - 1
src/views/Editor/Canvas/utils/alignLines.ts

@@ -1,4 +1,4 @@
-interface AlignLine {
+export interface AlignLine {
   value: number;
   range: [number, number];
 }

+ 43 - 43
src/views/Editor/Canvas/utils/alignToCanvas.ts

@@ -1,44 +1,44 @@
-import { PPTElement } from '@/types/slides'
-import { ElementAlignCommand, ElementAlignCommands } from '@/types/edit'
-import { getElementListRange } from './elementRange'
-import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
-
-// 将元素对齐到屏幕
-export const alignElement = (elementList: PPTElement[], activeElementList: PPTElement[], activeElementIdList: string[], command: ElementAlignCommand) => {
-  const viewportWidth = VIEWPORT_SIZE
-  const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
-  const { minX, maxX, minY, maxY } = getElementListRange(activeElementList)
-
-  const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
-  for(const element of copyOfElementList) {
-    if(!activeElementIdList.includes(element.elId)) continue
-    
-    if(command === ElementAlignCommands.TOP) {
-      const offsetY = minY - 0
-      element.top = element.top - offsetY            
-    }
-    else if(command === ElementAlignCommands.VERTICAL) {
-      const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2
-      element.top = element.top - offsetY            
-    }
-    else if(command === ElementAlignCommands.BOTTOM) {
-      const offsetY = maxY - viewportHeight
-      element.top = element.top - offsetY       
-    }
-    
-    else if(command === ElementAlignCommands.LEFT) {
-      const offsetX = minX - 0
-      element.left = element.left - offsetX            
-    }
-    else if(command === ElementAlignCommands.HORIZONTAL) {
-      const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2
-      element.left = element.left - offsetX            
-    }
-    else if(command === ElementAlignCommands.RIGHT) {
-      const offsetX = maxX - viewportWidth
-      element.left = element.left - offsetX            
-    }
-  }
-  
-  return copyOfElementList
+import { PPTElement } from '@/types/slides'
+import { ElementAlignCommand, ElementAlignCommands } from '@/types/edit'
+import { getElementListRange } from './elementRange'
+import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
+
+// 将元素对齐到屏幕
+export const alignElementToCanvas = (elementList: PPTElement[], activeElementList: PPTElement[], activeElementIdList: string[], command: ElementAlignCommand) => {
+  const viewportWidth = VIEWPORT_SIZE
+  const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
+  const { minX, maxX, minY, maxY } = getElementListRange(activeElementList)
+
+  const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
+  for(const element of copyOfElementList) {
+    if(!activeElementIdList.includes(element.elId)) continue
+    
+    if(command === ElementAlignCommands.TOP) {
+      const offsetY = minY - 0
+      element.top = element.top - offsetY            
+    }
+    else if(command === ElementAlignCommands.VERTICAL) {
+      const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2
+      element.top = element.top - offsetY            
+    }
+    else if(command === ElementAlignCommands.BOTTOM) {
+      const offsetY = maxY - viewportHeight
+      element.top = element.top - offsetY       
+    }
+    
+    else if(command === ElementAlignCommands.LEFT) {
+      const offsetX = minX - 0
+      element.left = element.left - offsetX            
+    }
+    else if(command === ElementAlignCommands.HORIZONTAL) {
+      const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2
+      element.left = element.left - offsetX            
+    }
+    else if(command === ElementAlignCommands.RIGHT) {
+      const offsetX = maxX - viewportWidth
+      element.left = element.left - offsetX            
+    }
+  }
+  
+  return copyOfElementList
 }

+ 6 - 11
src/views/Editor/Canvas/utils/elementLock.ts

@@ -1,14 +1,17 @@
 import { PPTElement } from '@/types/slides'
-import { ElementLockCommand, ElementLockCommands } from '@/types/edit'
 
-const lock = (copyOfElementList: PPTElement[], handleElement: PPTElement, activeElementIdList: string[]) => {
+export const lockElement = (elementList: PPTElement[], handleElement: PPTElement, activeElementIdList: string[]) => {
+  const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
+
   for(const element of copyOfElementList) {
     if(activeElementIdList.includes(handleElement.elId)) element.isLock = true
   }
   return copyOfElementList
 }
 
-const unlock = (copyOfElementList: PPTElement[], handleElement: PPTElement) => {
+export const unlockElement = (elementList: PPTElement[], handleElement: PPTElement) => {
+  const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
+
   if(handleElement.groupId) {
     for(const element of copyOfElementList) {
       if(element.groupId === handleElement.groupId) element.isLock = false
@@ -23,12 +26,4 @@ const unlock = (copyOfElementList: PPTElement[], handleElement: PPTElement) => {
     }
   }
   return copyOfElementList
-}
-
-// 锁定&解锁 元素
-export const lockElement = (elementList: PPTElement[], handleElement: PPTElement, activeElementIdList: string[], command: ElementLockCommand) => {
-  const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
-
-  if(command === ElementLockCommands.LOCK) return lock(copyOfElementList, handleElement, activeElementIdList)
-  return unlock(copyOfElementList, handleElement)
 }

+ 1 - 1
src/views/Editor/Canvas/utils/elementOrder.ts

@@ -158,7 +158,7 @@ const moveBottomElement = (elementList: PPTElement[], element: PPTElement) => {
   return copyOfElementList
 }
 
-export const setElementOrder = (elementList: PPTElement[], element: PPTElement, command: ElementOrderCommand) => {
+export const orderElement = (elementList: PPTElement[], element: PPTElement, command: ElementOrderCommand) => {
   let newElementList = null
   
   if(command === ElementOrderCommands.UP) newElementList = moveUpElement(elementList, element)

+ 9 - 4
src/views/Editor/Canvas/utils/elementRotate.ts

@@ -1,5 +1,4 @@
-import { PPTTextElement, PPTImageElement, PPTShapeElement } from '@/types/slides'
-import { OPERATE_KEYS } from '@/configs/element'
+import { OPERATE_KEYS } from '@/types/edit'
 
 // 给定一个坐标,计算该坐标到(0, 0)点连线的弧度值
 // 注意,Math.atan2的一般用法是Math.atan2(y, x)返回的是原点(0,0)到(x,y)点的线段与X轴正方向之间的弧度值
@@ -11,7 +10,13 @@ export const getAngleFromCoordinate = (x: number, y: number) => {
 }
 
 // 计算元素被旋转一定角度后,八个操作点的新坐标
-export const getRotateElementPoints = (element: PPTTextElement | PPTImageElement | PPTShapeElement, angle: number) => {
+interface RotateElementData {
+  left: number;
+  top: number;
+  width: number;
+  height: number;
+}
+export const getRotateElementPoints = (element: RotateElementData, angle: number) => {
   const { left, top, width, height } = element
 
   const radius = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ) / 2
@@ -65,7 +70,7 @@ export const getRotateElementPoints = (element: PPTTextElement | PPTImageElement
 }
 
 // 获取元素某个操作点对角线上另一端的操作点坐标(例如:左上 <-> 右下)
-export const getOppositePoint = (direction: number, points: ReturnType<typeof getRotateElementPoints>) => {
+export const getOppositePoint = (direction: number, points: ReturnType<typeof getRotateElementPoints>): { left: number; top: number } => {
   const oppositeMap = {
     [OPERATE_KEYS.RIGHT_BOTTOM]: points.leftTopPoint,
     [OPERATE_KEYS.LEFT_BOTTOM]: points.rightTopPoint,

+ 9 - 9
src/views/Editor/Thumbnails/index.vue

@@ -32,9 +32,9 @@
 import { computed, defineComponent } from 'vue'
 import draggable from 'vuedraggable'
 import { useStore } from 'vuex'
-import { State } from '@/store/state'
-import { MutationTypes } from '@/store/constants'
+import { State, MutationTypes } from '@/store'
 import { fillDigit } from '@/utils/common'
+import { ContextmenuItem } from '@/components/Contextmenu/types'
 
 export default defineComponent({
   name: 'thumbnails',
@@ -91,43 +91,43 @@ export default defineComponent({
       console.log('deleteSlide')
     }
 
-    const contextmenus = () => {
+    const contextmenus = (): ContextmenuItem[] => {
       return [
         {
           text: '剪切',
           subText: 'Ctrl + X',
           icon: 'icon-scissor',
-          action: cutSlide,
+          handler: cutSlide,
         },
         {
           text: '复制',
           subText: 'Ctrl + C',
           icon: 'icon-copy',
-          action: copySlide,
+          handler: copySlide,
         },
         {
           text: '粘贴',
           subText: 'Ctrl + V',
           icon: 'icon-paste',
-          action: pasteSlide,
+          handler: pasteSlide,
         },
         { divider: true },
         {
           text: '新建页面',
           subText: 'Enter',
           icon: 'icon-add-page',
-          action: createSlide,
+          handler: createSlide,
         },
         {
           text: '复制页面',
           icon: 'icon-copy',
-          action: copyAndPasteSlide,
+          handler: copyAndPasteSlide,
         },
         {
           text: '删除页面',
           subText: 'Delete',
           icon: 'icon-delete',
-          action: deleteSlide,
+          handler: deleteSlide,
         },
       ]
     }

+ 1 - 2
src/views/Editor/index.vue

@@ -15,7 +15,7 @@
 <script lang="ts">
 import { computed, defineComponent, onMounted, onUnmounted } from 'vue'
 import { useStore } from 'vuex'
-import { State } from '@/store/state'
+import { State, MutationTypes } from '@/store'
 import { KEYCODE } from '@/configs/keyCode'
 import { decrypt } from '@/utils/crypto'
 import { getImageDataURL } from '@/utils/image'
@@ -27,7 +27,6 @@ import Canvas from './Canvas/index.vue'
 import CanvasTool from './CanvasTool/index.vue'
 import Thumbnails from './Thumbnails/index.vue'
 import Toolbar from './Toolbar/index.vue'
-import { MutationTypes } from '@/store/constants'
 
 export default defineComponent({
   name: 'editor',

+ 34 - 29
src/views/_common/_element/EditableElement.vue

@@ -24,7 +24,10 @@
 
 <script lang="ts">
 import { computed, defineComponent, PropType } from 'vue'
-import { PPTElement } from '@/types/slides'
+import { useStore } from 'vuex'
+import { State } from '@/store'
+import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement, PPTLineElement } from '@/types/slides'
+import { ContextmenuItem } from '@/components/Contextmenu/types'
 
 import {
   ElementOrderCommand,
@@ -32,8 +35,6 @@ import {
   ElementAlignCommand,
   ElementAlignCommands,
   ElementScaleHandler,
-  ElementLockCommand,
-  ElementLockCommands,
 } from '@/types/edit'
 
 import ImageElement from './ImageElement/index.vue'
@@ -50,10 +51,6 @@ export default defineComponent({
       type: Number,
       required: true,
     },
-    canvasScale: {
-      type: Number,
-      required: true,
-    },
     isActive: {
       type: Boolean,
       required: true,
@@ -79,11 +76,11 @@ export default defineComponent({
       required: true,
     },
     rotateElement: {
-      type: Function as PropType<(element: PPTElement) => void>,
+      type: Function as PropType<(element: PPTTextElement | PPTImageElement | PPTShapeElement) => void>,
       required: true,
     },
     scaleElement: {
-      type: Function as PropType<(e: MouseEvent, element: PPTElement, command: ElementScaleHandler) => void>,
+      type: Function as PropType<(e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: ElementScaleHandler) => void>,
       required: true,
     },
     orderElement: {
@@ -98,7 +95,7 @@ export default defineComponent({
       type: Function as PropType<() => void>,
       required: true,
     },
-    alignElement: {
+    alignElementToCanvas: {
       type: Function as PropType<(command: ElementAlignCommand) => void>,
       required: true,
     },
@@ -107,7 +104,11 @@ export default defineComponent({
       required: true,
     },
     lockElement: {
-      type: Function as PropType<(element: PPTElement, command: ElementLockCommand) => void>,
+      type: Function as PropType<(element: PPTElement) => void>,
+      required: true,
+    },
+    unlockElement: {
+      type: Function as PropType<(element: PPTElement) => void>,
       required: true,
     },
     copyElement: {
@@ -120,6 +121,9 @@ export default defineComponent({
     },
   },
   setup(props) {
+    const store = useStore<State>()
+    const canvasScale = computed(() => store.state.canvasScale)
+
     const currentElementComponent = computed(() => {
       const elementTypeMap = {
         'image': ImageElement,
@@ -128,12 +132,12 @@ export default defineComponent({
       return elementTypeMap[props.elementInfo.type] || null
     })
 
-    const contextmenus = () => {
+    const contextmenus = (): ContextmenuItem[] => {
       if(props.elementInfo.isLock) {
         return [{
           text: '解锁', 
           icon: 'icon-unlock',
-          action: () => props.lockElement(props.elementInfo, ElementLockCommands.UNLOCK),
+          handler: () => props.unlockElement(props.elementInfo),
         }]
       }
 
@@ -142,43 +146,43 @@ export default defineComponent({
           text: '剪切',
           subText: 'Ctrl + X',
           icon: 'icon-scissor',
-          action: props.cutElement,
+          handler: props.cutElement,
         },
         {
           text: '复制',
           subText: 'Ctrl + C',
           icon: 'icon-copy',
-          action: props.copyElement,
+          handler: props.copyElement,
         },
         { divider: true },
         {
-          text: '层级',
+          text: '层级排序',
           icon: 'icon-top-layer',
           disable: props.isMultiSelect && !props.elementInfo.groupId,
           children: [
-            { text: '置顶层', action: () => props.orderElement(props.elementInfo, ElementOrderCommands.TOP) },
-            { text: '置底层', action: () => props.orderElement(props.elementInfo, ElementOrderCommands.BOTTOM) },
+            { text: '置顶层', handler: () => props.orderElement(props.elementInfo, ElementOrderCommands.TOP) },
+            { text: '置底层', handler: () => props.orderElement(props.elementInfo, ElementOrderCommands.BOTTOM) },
             { divider: true },
-            { text: '上移一层', action: () => props.orderElement(props.elementInfo, ElementOrderCommands.UP) },
-            { text: '下移一层', action: () => props.orderElement(props.elementInfo, ElementOrderCommands.DOWN) },
+            { text: '上移一层', handler: () => props.orderElement(props.elementInfo, ElementOrderCommands.UP) },
+            { text: '下移一层', handler: () => props.orderElement(props.elementInfo, ElementOrderCommands.DOWN) },
           ],
         },
         {
           text: '水平对齐',
           icon: 'icon-align-left',
           children: [
-            { text: '水平居中', action: () => props.alignElement(ElementAlignCommands.HORIZONTAL) },
-            { text: '左对齐', action: () => props.alignElement(ElementAlignCommands.LEFT) },
-            { text: '右对齐', action: () => props.alignElement(ElementAlignCommands.RIGHT) },
+            { text: '水平居中', handler: () => props.alignElementToCanvas(ElementAlignCommands.HORIZONTAL) },
+            { text: '左对齐', handler: () => props.alignElementToCanvas(ElementAlignCommands.LEFT) },
+            { text: '右对齐', handler: () => props.alignElementToCanvas(ElementAlignCommands.RIGHT) },
           ],
         },
         {
           text: '垂直对齐',
           icon: 'icon-align-bottom',
           children: [
-            { text: '垂直居中', action: () => props.alignElement(ElementAlignCommands.VERTICAL) },
-            { text: '上对齐', action: () => props.alignElement(ElementAlignCommands.TOP) },
-            { text: '下对齐', action: () => props.alignElement(ElementAlignCommands.BOTTOM) },
+            { text: '垂直居中', handler: () => props.alignElementToCanvas(ElementAlignCommands.VERTICAL) },
+            { text: '上对齐', handler: () => props.alignElementToCanvas(ElementAlignCommands.TOP) },
+            { text: '下对齐', handler: () => props.alignElementToCanvas(ElementAlignCommands.BOTTOM) },
           ],
         },
         { divider: true },
@@ -186,25 +190,26 @@ export default defineComponent({
           text: props.elementInfo.groupId ? '取消组合' : '组合',
           subText: 'Ctrl + G',
           icon: 'icon-block',
-          action: props.elementInfo.groupId ? props.uncombineElements : props.combineElements,
+          handler: props.elementInfo.groupId ? props.uncombineElements : props.combineElements,
           hide: !props.isMultiSelect,
         },
         {
           text: '锁定',
           subText: 'Ctrl + L',
           icon: 'icon-lock',
-          action: () => props.lockElement(props.elementInfo, ElementLockCommands.LOCK),
+          handler: () => props.lockElement(props.elementInfo),
         },
         {
           text: '删除',
           subText: 'Delete',
           icon: 'icon-delete',
-          action: () => props.deleteElement(),
+          handler: () => props.deleteElement(),
         },
       ]
     }
 
     return {
+      canvasScale,
       currentElementComponent,
       contextmenus,
     }

+ 1 - 2
src/views/_common/_element/ImageElement/index.vue

@@ -118,10 +118,9 @@
 import { computed, defineComponent, ref, PropType } from 'vue'
 
 import { PPTImageElement } from '@/types/slides'
-import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
+import { OPERATE_KEYS, ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
 
 import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
-import { OPERATE_KEYS } from '@/configs/element'
 
 import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
 import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'

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

@@ -76,9 +76,7 @@
 import { computed, defineComponent, PropType } from 'vue'
 
 import { PPTTextElement } from '@/types/slides'
-import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
-
-import { OPERATE_KEYS } from '@/configs/element'
+import { OPERATE_KEYS, ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
 
 import ElementBorder from '@/views/_common/_element/ElementBorder.vue'
 import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'