This commit is contained in:
pipipi-pikachu 2020-12-19 16:10:48 +08:00
parent ce9069d941
commit 585ecf008f
26 changed files with 818 additions and 352 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -59,14 +59,3 @@ export const DEFAULT_TABLE = {
borderWidth: 2, borderWidth: 2,
borderColor: DEFAULT_COLOR, 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,
}

View File

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

View File

@ -1,17 +1,7 @@
import { PPTElement, Slide, PPTAnimation } from '@/types/slides' import { GetterTree } from 'vuex'
import { State } from './state' import { State } from './index'
export type Getters = { export const getters: GetterTree<State, State> = {
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 = {
currentSlide(state) { currentSlide(state) {
return state.slides[state.slideIndex] || null return state.slides[state.slideIndex] || null
}, },

View File

@ -1,7 +1,47 @@
import { createStore } from 'vuex' import { createStore } from 'vuex'
import { state } from './state'
import { mutations } from './mutations' import { mutations } from './mutations'
import { getters } from './getters' 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({ export default createStore({
state, state,

View File

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

View File

@ -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,
}

View File

@ -20,13 +20,6 @@ export enum ElementAlignCommands {
export type ElementScaleHandler = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 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 type OperateBorderLineType = 't' | 'b' | 'l' | 'r'
export enum OperateBorderLineTypes { export enum OperateBorderLineTypes {
@ -49,3 +42,14 @@ export enum OperateResizablePointTypes {
BR = 'b-r', BR = 'b-r',
ANY = 'any', 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,
}

View File

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

View File

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

View File

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

View File

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

View File

@ -38,8 +38,7 @@
<MultiSelectOperate <MultiSelectOperate
v-if="activeElementIdList.length > 1" v-if="activeElementIdList.length > 1"
:activeElementList="activeElementList" :elementList="elementList"
:canvasScale="canvasScale"
:scaleMultiElement="scaleMultiElement" :scaleMultiElement="scaleMultiElement"
/> />
@ -52,16 +51,16 @@
:isHandleEl="element.elId === handleElementId" :isHandleEl="element.elId === handleElementId"
:isActiveGroupElement="activeGroupElementId === element.elId" :isActiveGroupElement="activeGroupElementId === element.elId"
:isMultiSelect="activeElementIdList.length > 1" :isMultiSelect="activeElementIdList.length > 1"
:canvasScale="canvasScale"
:selectElement="selectElement" :selectElement="selectElement"
:rotateElement="rotateElement" :rotateElement="rotateElement"
:scaleElement="scaleElement" :scaleElement="scaleElement"
:orderElement="orderElement" :orderElement="orderElement"
:combineElements="combineElements" :combineElements="combineElements"
:uncombineElements="uncombineElements" :uncombineElements="uncombineElements"
:alignElement="alignElement" :alignElementToCanvas="alignElementToCanvas"
:deleteElement="deleteElement" :deleteElement="deleteElement"
:lockElement="lockElement" :lockElement="lockElement"
:unlockElement="unlockElement"
:copyElement="copyElement" :copyElement="copyElement"
:cutElement="cutElement" :cutElement="cutElement"
/> />
@ -70,17 +69,27 @@
</template> </template>
<script lang="ts"> <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 { useStore } from 'vuex'
import uniq from 'lodash/uniq' import uniq from 'lodash/uniq'
import { State } from '@/store/state' import { message } from 'ant-design-vue'
import { MutationTypes } from '@/store/constants' import { State, MutationTypes } from '@/store'
import { ContextmenuItem } from '@/components/Contextmenu/types' 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 { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import { getImageDataURL } from '@/utils/image' 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 useDropImage from '@/hooks/useDropImage'
import useSetViewportSize from './hooks/useSetViewportSize' import useSetViewportSize from './hooks/useSetViewportSize'
@ -88,11 +97,11 @@ import useSetViewportSize from './hooks/useSetViewportSize'
import EditableElement from '@/views/_common/_element/EditableElement.vue' import EditableElement from '@/views/_common/_element/EditableElement.vue'
import MouseSelection from './MouseSelection.vue' import MouseSelection from './MouseSelection.vue'
import SlideBackground from './SlideBackground.vue' import SlideBackground from './SlideBackground.vue'
import MultiSelectOperate from './MultiSelectOperate.vue' import MultiSelectOperate, { MultiSelectRange } from './MultiSelectOperate.vue'
import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue' import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue'
export default defineComponent({ export default defineComponent({
name: 'v-canvas', name: 'editor-canvas',
components: { components: {
EditableElement, EditableElement,
MouseSelection, MouseSelection,
@ -118,8 +127,7 @@ export default defineComponent({
const setLocalElementList = () => { const setLocalElementList = () => {
elementList.value = currentSlide.value ? JSON.parse(JSON.stringify(currentSlide.value.elements)) : [] elementList.value = currentSlide.value ? JSON.parse(JSON.stringify(currentSlide.value.elements)) : []
} }
onMounted(setLocalElementList) watchEffect(setLocalElementList)
watch(currentSlide, setLocalElementList)
const dropImageFile = useDropImage(viewportRef) const dropImageFile = useDropImage(viewportRef)
watch(dropImageFile, () => { watch(dropImageFile, () => {
@ -253,12 +261,7 @@ export default defineComponent({
return true return true
}) })
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId) const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
if(inRangeElementIdList.length) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, inRangeElementIdList)
//
// ->
if(activeElementIdList.value.length > 0 || inRangeElementIdList.length) {
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, inRangeElementIdList)
}
mouseSelectionState.isShow = false mouseSelectionState.isShow = false
} }
@ -267,6 +270,7 @@ export default defineComponent({
const editorAreaFocus = computed(() => store.state.editorAreaFocus) const editorAreaFocus = computed(() => store.state.editorAreaFocus)
const handleClickBlankArea = (e: MouseEvent) => { const handleClickBlankArea = (e: MouseEvent) => {
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
if(!ctrlOrShiftKeyActive.value) updateMouseSelection(e) if(!ctrlOrShiftKeyActive.value) updateMouseSelection(e)
if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true) if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
} }
@ -278,6 +282,7 @@ export default defineComponent({
const moveElement = (e: MouseEvent, element: PPTElement) => { const moveElement = (e: MouseEvent, element: PPTElement) => {
console.log(e, element) console.log(e, element)
} }
const selectElement = (e: MouseEvent, element: PPTElement, canMove = true) => { const selectElement = (e: MouseEvent, element: PPTElement, canMove = true) => {
if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true) if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
@ -346,38 +351,538 @@ export default defineComponent({
if(canMove) moveElement(e, element) 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 = () => { const combineElements = () => {
console.log('combineElements') const newElementList = _combineElements(elementList.value, activeElementList.value, activeElementIdList.value)
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
} }
const uncombineElements = () => { 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 = () => { 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 = () => { 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 = () => { 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[] => { const contextmenus = (): ContextmenuItem[] => {
@ -385,13 +890,16 @@ export default defineComponent({
{ {
text: '全选', text: '全选',
subText: 'Ctrl + A', subText: 'Ctrl + A',
handler: selectAllElement,
}, },
{ {
text: '粘贴', text: '粘贴',
subText: 'Ctrl + V', subText: 'Ctrl + V',
handler: pasteElement,
}, },
{ {
text: '清空页面', text: '清空本页',
handler: deleteAllElements,
}, },
] ]
} }
@ -419,9 +927,10 @@ export default defineComponent({
orderElement, orderElement,
combineElements, combineElements,
uncombineElements, uncombineElements,
alignElement, alignElementToCanvas,
deleteElement, deleteElement,
lockElement, lockElement,
unlockElement,
copyElement, copyElement,
cutElement, cutElement,
contextmenus, contextmenus,
@ -438,7 +947,6 @@ export default defineComponent({
background-color: #f9f9f9; background-color: #f9f9f9;
position: relative; position: relative;
} }
.viewport { .viewport {
position: absolute; position: absolute;
transform-origin: 0 0; transform-origin: 0 0;

View File

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

View File

@ -4,7 +4,7 @@ import { getElementListRange } from './elementRange'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas' import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
// 将元素对齐到屏幕 // 将元素对齐到屏幕
export const alignElement = (elementList: PPTElement[], activeElementList: PPTElement[], activeElementIdList: string[], command: ElementAlignCommand) => { export const alignElementToCanvas = (elementList: PPTElement[], activeElementList: PPTElement[], activeElementIdList: string[], command: ElementAlignCommand) => {
const viewportWidth = VIEWPORT_SIZE const viewportWidth = VIEWPORT_SIZE
const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
const { minX, maxX, minY, maxY } = getElementListRange(activeElementList) const { minX, maxX, minY, maxY } = getElementListRange(activeElementList)

View File

@ -1,14 +1,17 @@
import { PPTElement } from '@/types/slides' 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) { for(const element of copyOfElementList) {
if(activeElementIdList.includes(handleElement.elId)) element.isLock = true if(activeElementIdList.includes(handleElement.elId)) element.isLock = true
} }
return copyOfElementList 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) { if(handleElement.groupId) {
for(const element of copyOfElementList) { for(const element of copyOfElementList) {
if(element.groupId === handleElement.groupId) element.isLock = false if(element.groupId === handleElement.groupId) element.isLock = false
@ -24,11 +27,3 @@ const unlock = (copyOfElementList: PPTElement[], handleElement: PPTElement) => {
} }
return copyOfElementList 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)
}

View File

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

View File

@ -1,5 +1,4 @@
import { PPTTextElement, PPTImageElement, PPTShapeElement } from '@/types/slides' import { OPERATE_KEYS } from '@/types/edit'
import { OPERATE_KEYS } from '@/configs/element'
// 给定一个坐标,计算该坐标到(0, 0)点连线的弧度值 // 给定一个坐标,计算该坐标到(0, 0)点连线的弧度值
// 注意Math.atan2的一般用法是Math.atan2(y, x)返回的是原点(0,0)到(x,y)点的线段与X轴正方向之间的弧度值 // 注意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 { left, top, width, height } = element
const radius = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ) / 2 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 = { const oppositeMap = {
[OPERATE_KEYS.RIGHT_BOTTOM]: points.leftTopPoint, [OPERATE_KEYS.RIGHT_BOTTOM]: points.leftTopPoint,
[OPERATE_KEYS.LEFT_BOTTOM]: points.rightTopPoint, [OPERATE_KEYS.LEFT_BOTTOM]: points.rightTopPoint,

View File

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

View File

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

View File

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

View File

@ -118,10 +118,9 @@
import { computed, defineComponent, ref, PropType } from 'vue' import { computed, defineComponent, ref, PropType } from 'vue'
import { PPTImageElement } from '@/types/slides' 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 { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
import { OPERATE_KEYS } from '@/configs/element'
import RotateHandler from '@/views/_common/_operate/RotateHandler.vue' import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue' import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'

View File

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