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,
},
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: [
{

View File

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

View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -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('浏览器不支持或禁止访问剪贴板')
})
}

View File

@ -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: {

View File

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

View File

@ -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>) => {

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@ -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'

View File

@ -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'