mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
添加创建元素区域
This commit is contained in:
parent
8fb6e53342
commit
0988cbae90
@ -9,6 +9,7 @@ export enum MutationTypes {
|
|||||||
SET_EDITORAREA_FOCUS = 'setEditorAreaFocus',
|
SET_EDITORAREA_FOCUS = 'setEditorAreaFocus',
|
||||||
SET_DISABLE_HOTKEYS_STATE = 'setDisableHotkeysState',
|
SET_DISABLE_HOTKEYS_STATE = 'setDisableHotkeysState',
|
||||||
SET_GRID_LINES_STATE = 'setGridLinesState',
|
SET_GRID_LINES_STATE = 'setGridLinesState',
|
||||||
|
SET_CREATING_ELEMENT_TYPE = 'setCreatingElementType',
|
||||||
SET_AVAILABLE_FONTS = 'setAvailableFonts',
|
SET_AVAILABLE_FONTS = 'setAvailableFonts',
|
||||||
SET_TOOLBAR_STATE = 'setToolbarState',
|
SET_TOOLBAR_STATE = 'setToolbarState',
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ export interface State {
|
|||||||
editorAreaFocus: boolean;
|
editorAreaFocus: boolean;
|
||||||
disableHotkeys: boolean;
|
disableHotkeys: boolean;
|
||||||
showGridLines: boolean;
|
showGridLines: boolean;
|
||||||
|
creatingElementType: string;
|
||||||
availableFonts: FontName[];
|
availableFonts: FontName[];
|
||||||
toolbarState: ToolbarState;
|
toolbarState: ToolbarState;
|
||||||
slides: Slide[];
|
slides: Slide[];
|
||||||
@ -40,6 +41,7 @@ const state: State = {
|
|||||||
editorAreaFocus: false,
|
editorAreaFocus: false,
|
||||||
disableHotkeys: false,
|
disableHotkeys: false,
|
||||||
showGridLines: false,
|
showGridLines: false,
|
||||||
|
creatingElementType: '',
|
||||||
availableFonts: [],
|
availableFonts: [],
|
||||||
toolbarState: 'slideStyle',
|
toolbarState: 'slideStyle',
|
||||||
slides: slides,
|
slides: slides,
|
||||||
|
@ -49,6 +49,10 @@ export const mutations: MutationTree<State> = {
|
|||||||
state.showGridLines = show
|
state.showGridLines = show
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[MutationTypes.SET_CREATING_ELEMENT_TYPE](state, type: string) {
|
||||||
|
state.creatingElementType = type
|
||||||
|
},
|
||||||
|
|
||||||
[MutationTypes.SET_AVAILABLE_FONTS](state) {
|
[MutationTypes.SET_AVAILABLE_FONTS](state) {
|
||||||
state.availableFonts = FONT_NAMES.filter(font => isSupportFontFamily(font.en))
|
state.availableFonts = FONT_NAMES.filter(font => isSupportFontFamily(font.en))
|
||||||
},
|
},
|
||||||
|
@ -80,4 +80,9 @@ export interface ImageClipedEmitData {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateElementSelectionData {
|
||||||
|
start: [number, number];
|
||||||
|
end: [number, number];
|
||||||
}
|
}
|
205
src/views/Editor/Canvas/ElementCreateSelection.vue
Normal file
205
src/views/Editor/Canvas/ElementCreateSelection.vue
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="element-create-selection"
|
||||||
|
ref="selectionRef"
|
||||||
|
@mousedown.stop="$event => createSelection($event)"
|
||||||
|
>
|
||||||
|
<div :class="['selection', elementType]" v-if="start && end" :style="position">
|
||||||
|
|
||||||
|
<!-- 绘制线条专用 -->
|
||||||
|
<SvgWrapper
|
||||||
|
v-if="elementType === 'line' && lineData"
|
||||||
|
overflow="visible"
|
||||||
|
:width="lineData.svgWidth"
|
||||||
|
:height="lineData.svgHeight"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
:d="lineData.path"
|
||||||
|
stroke="#888"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1"
|
||||||
|
stroke-linecap
|
||||||
|
stroke-linejoin
|
||||||
|
stroke-miterlimit
|
||||||
|
></path>
|
||||||
|
</SvgWrapper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, onMounted, reactive, Ref, ref } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { MutationTypes, State } from '@/store'
|
||||||
|
|
||||||
|
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'element-create-selection',
|
||||||
|
components: {
|
||||||
|
SvgWrapper,
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const store = useStore<State>()
|
||||||
|
const ctrlOrShiftKeyActive: Ref<boolean> = computed(() => store.getters.ctrlOrShiftKeyActive)
|
||||||
|
const elementType = computed(() => store.state.creatingElementType)
|
||||||
|
|
||||||
|
const start = ref<[number, number] | null>(null)
|
||||||
|
const end = ref<[number, number] | null>(null)
|
||||||
|
|
||||||
|
const selectionRef = ref<HTMLElement | null>(null)
|
||||||
|
const offset = reactive({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
})
|
||||||
|
onMounted(() => {
|
||||||
|
if(!selectionRef.value) return
|
||||||
|
const { x, y } = selectionRef.value.getBoundingClientRect()
|
||||||
|
offset.x = x
|
||||||
|
offset.y = y
|
||||||
|
})
|
||||||
|
|
||||||
|
const createSelection = (e: MouseEvent) => {
|
||||||
|
let isMouseDown = true
|
||||||
|
|
||||||
|
const startPageX = e.pageX
|
||||||
|
const startPageY = e.pageY
|
||||||
|
start.value = [startPageX, startPageY]
|
||||||
|
|
||||||
|
document.onmousemove = e => {
|
||||||
|
if(!isMouseDown) return
|
||||||
|
|
||||||
|
let currentPageX = e.pageX
|
||||||
|
let currentPageY = e.pageY
|
||||||
|
|
||||||
|
if(ctrlOrShiftKeyActive.value) {
|
||||||
|
const moveX = currentPageX - startPageX
|
||||||
|
const moveY = currentPageY - startPageY
|
||||||
|
|
||||||
|
const absX = Math.abs(moveX)
|
||||||
|
const absY = Math.abs(moveY)
|
||||||
|
|
||||||
|
if(elementType.value === 'shape') {
|
||||||
|
// moveX和moveY一正一负
|
||||||
|
const isOpposite = (moveY > 0 && moveX < 0) || (moveY < 0 && moveX > 0)
|
||||||
|
|
||||||
|
if(absX > absY) {
|
||||||
|
currentPageY = isOpposite ? startPageY - moveX : startPageY + moveX
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
currentPageX = isOpposite ? startPageX - moveY : startPageX + moveY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if(elementType.value === 'line') {
|
||||||
|
if(absX > absY) currentPageY = startPageY
|
||||||
|
else currentPageX = startPageX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
end.value = [currentPageX, currentPageY]
|
||||||
|
}
|
||||||
|
|
||||||
|
document.onmouseup = e => {
|
||||||
|
document.onmousemove = null
|
||||||
|
document.onmouseup = null
|
||||||
|
isMouseDown = false
|
||||||
|
|
||||||
|
const endPageX = e.pageX
|
||||||
|
const endPageY = e.pageY
|
||||||
|
|
||||||
|
const minSize = 30
|
||||||
|
|
||||||
|
if(Math.abs(endPageX - startPageX) >= minSize || Math.abs(endPageY - startPageY) >= minSize) {
|
||||||
|
emit('created', {
|
||||||
|
start: start.value,
|
||||||
|
end: end.value,
|
||||||
|
})
|
||||||
|
store.commit(MutationTypes.SET_CREATING_ELEMENT_TYPE, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineData = computed(() => {
|
||||||
|
if(!start.value || !end.value || elementType.value !== 'line') return null
|
||||||
|
|
||||||
|
const [_startX, _startY] = start.value
|
||||||
|
const [_endX, _endY] = end.value
|
||||||
|
const minX = Math.min(_startX, _endX)
|
||||||
|
const maxX = Math.max(_startX, _endX)
|
||||||
|
const minY = Math.min(_startY, _endY)
|
||||||
|
const maxY = Math.max(_startY, _endY)
|
||||||
|
|
||||||
|
const svgWidth = maxX - minX >= 24 ? maxX - minX : 24
|
||||||
|
const svgHeight = maxY - minY >= 24 ? maxY - minY : 24
|
||||||
|
|
||||||
|
const startX = _startX === minX ? 0 : maxX - minX
|
||||||
|
const startY = _startY === minY ? 0 : maxY - minY
|
||||||
|
const endX = _endX === minX ? 0 : maxX - minX
|
||||||
|
const endY = _endY === minY ? 0 : maxY - minY
|
||||||
|
|
||||||
|
const path = `M${startX}, ${startY} L${endX}, ${endY}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
svgWidth,
|
||||||
|
svgHeight,
|
||||||
|
startX,
|
||||||
|
startY,
|
||||||
|
endX,
|
||||||
|
endY,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const position = computed(() => {
|
||||||
|
if(!start.value || !end.value) return {}
|
||||||
|
|
||||||
|
const [startX, startY] = start.value
|
||||||
|
const [endX, endY] = end.value
|
||||||
|
const minX = Math.min(startX, endX)
|
||||||
|
const maxX = Math.max(startX, endX)
|
||||||
|
const minY = Math.min(startY, endY)
|
||||||
|
const maxY = Math.max(startY, endY)
|
||||||
|
|
||||||
|
const width = maxX - minX
|
||||||
|
const height = maxY - minY
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: minX - offset.x + 'px',
|
||||||
|
top: minY - offset.y + 'px',
|
||||||
|
width: width + 'px',
|
||||||
|
height: height + 'px',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectionRef,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
elementType,
|
||||||
|
createSelection,
|
||||||
|
lineData,
|
||||||
|
position,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.element-create-selection {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.selection {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
&:not(.line) {
|
||||||
|
border: 1px solid #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -7,6 +7,10 @@
|
|||||||
v-contextmenu="contextmenus"
|
v-contextmenu="contextmenus"
|
||||||
v-click-outside="removeEditorAreaFocus"
|
v-click-outside="removeEditorAreaFocus"
|
||||||
>
|
>
|
||||||
|
<ElementCreateSelection
|
||||||
|
v-if="creatingElementType"
|
||||||
|
@created="data => createElement(data)"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
class="viewport-wrapper"
|
class="viewport-wrapper"
|
||||||
:style="{
|
:style="{
|
||||||
@ -77,7 +81,7 @@ import throttle from 'lodash/throttle'
|
|||||||
import { State, MutationTypes } from '@/store'
|
import { State, MutationTypes } from '@/store'
|
||||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
import { PPTElement, Slide } from '@/types/slides'
|
import { PPTElement, Slide } from '@/types/slides'
|
||||||
import { AlignmentLineProps } from '@/types/edit'
|
import { AlignmentLineProps, CreateElementSelectionData } from '@/types/edit'
|
||||||
|
|
||||||
import useViewportSize from './hooks/useViewportSize'
|
import useViewportSize from './hooks/useViewportSize'
|
||||||
import useMouseSelection from './hooks/useMouseSelection'
|
import useMouseSelection from './hooks/useMouseSelection'
|
||||||
@ -97,6 +101,7 @@ import EditableElement from './EditableElement.vue'
|
|||||||
import MouseSelection from './MouseSelection.vue'
|
import MouseSelection from './MouseSelection.vue'
|
||||||
import SlideBackground from './SlideBackground.vue'
|
import SlideBackground from './SlideBackground.vue'
|
||||||
import AlignmentLine from './AlignmentLine.vue'
|
import AlignmentLine from './AlignmentLine.vue'
|
||||||
|
import ElementCreateSelection from './ElementCreateSelection.vue'
|
||||||
import MultiSelectOperate from './Operate/MultiSelectOperate.vue'
|
import MultiSelectOperate from './Operate/MultiSelectOperate.vue'
|
||||||
import Operate from './Operate/index.vue'
|
import Operate from './Operate/index.vue'
|
||||||
|
|
||||||
@ -107,6 +112,7 @@ export default defineComponent({
|
|||||||
MouseSelection,
|
MouseSelection,
|
||||||
SlideBackground,
|
SlideBackground,
|
||||||
AlignmentLine,
|
AlignmentLine,
|
||||||
|
ElementCreateSelection,
|
||||||
MultiSelectOperate,
|
MultiSelectOperate,
|
||||||
Operate,
|
Operate,
|
||||||
},
|
},
|
||||||
@ -177,6 +183,11 @@ export default defineComponent({
|
|||||||
store.commit(MutationTypes.SET_GRID_LINES_STATE, !showGridLines.value)
|
store.commit(MutationTypes.SET_GRID_LINES_STATE, !showGridLines.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const creatingElementType = computed(() => store.state.creatingElementType)
|
||||||
|
const createElement = (data: CreateElementSelectionData) => {
|
||||||
|
console.log(data)
|
||||||
|
}
|
||||||
|
|
||||||
const contextmenus = (): ContextmenuItem[] => {
|
const contextmenus = (): ContextmenuItem[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -217,6 +228,8 @@ export default defineComponent({
|
|||||||
removeEditorAreaFocus,
|
removeEditorAreaFocus,
|
||||||
currentSlide,
|
currentSlide,
|
||||||
isShowGridLines,
|
isShowGridLines,
|
||||||
|
creatingElementType,
|
||||||
|
createElement,
|
||||||
alignmentLines,
|
alignmentLines,
|
||||||
selectElement,
|
selectElement,
|
||||||
rotateElement,
|
rotateElement,
|
||||||
|
@ -6,12 +6,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="add-element-handler">
|
<div class="add-element-handler">
|
||||||
<IconFont class="handler-item" type="icon-font-size" />
|
<IconFont class="handler-item" type="icon-font-size" @click="createElement('text')" />
|
||||||
<UploadInput @change="files => insertImageElement(files)">
|
<UploadInput @change="files => insertImageElement(files)">
|
||||||
<IconFont class="handler-item" type="icon-image" />
|
<IconFont class="handler-item" type="icon-image" />
|
||||||
</UploadInput>
|
</UploadInput>
|
||||||
<IconFont class="handler-item" type="icon-star" />
|
<IconFont class="handler-item" type="icon-star" @click="createElement('shape')" />
|
||||||
<IconFont class="handler-item" type="icon-line" />
|
<IconFont class="handler-item" type="icon-line" @click="createElement('line')" />
|
||||||
<IconFont class="handler-item" type="icon-table" />
|
<IconFont class="handler-item" type="icon-table" />
|
||||||
<IconFont class="handler-item" type="icon-piechart" />
|
<IconFont class="handler-item" type="icon-piechart" />
|
||||||
</div>
|
</div>
|
||||||
@ -27,7 +27,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed } from 'vue'
|
import { defineComponent, computed } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { State } from '@/store'
|
import { MutationTypes, State } from '@/store'
|
||||||
import { getImageDataURL } from '@/utils/image'
|
import { getImageDataURL } from '@/utils/image'
|
||||||
import useScaleCanvas from '@/hooks/useScaleCanvas'
|
import useScaleCanvas from '@/hooks/useScaleCanvas'
|
||||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
@ -59,6 +59,10 @@ export default defineComponent({
|
|||||||
getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
|
getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createElement = (type: string) => {
|
||||||
|
store.commit(MutationTypes.SET_CREATING_ELEMENT_TYPE, type)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scaleCanvas,
|
scaleCanvas,
|
||||||
canvasScalePercentage,
|
canvasScalePercentage,
|
||||||
@ -67,6 +71,7 @@ export default defineComponent({
|
|||||||
redo,
|
redo,
|
||||||
undo,
|
undo,
|
||||||
insertImageElement,
|
insertImageElement,
|
||||||
|
createElement,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -97,13 +97,14 @@ export default () => {
|
|||||||
const { ctrlKey, shiftKey } = e
|
const { ctrlKey, shiftKey } = e
|
||||||
const key = e.key.toUpperCase()
|
const key = e.key.toUpperCase()
|
||||||
|
|
||||||
|
if(ctrlKey && !ctrlKeyActive.value) store.commit(MutationTypes.SET_CTRL_KEY_STATE, true)
|
||||||
|
if(shiftKey && !shiftKeyActive.value) store.commit(MutationTypes.SET_SHIFT_KEY_STATE, true)
|
||||||
|
|
||||||
if(ctrlKey && key === KEYS.F) {
|
if(ctrlKey && key === KEYS.F) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
enterScreening()
|
enterScreening()
|
||||||
|
store.commit(MutationTypes.SET_CTRL_KEY_STATE, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ctrlKey && !ctrlKeyActive.value) store.commit(MutationTypes.SET_CTRL_KEY_STATE, true)
|
|
||||||
if(shiftKey && !shiftKeyActive.value) store.commit(MutationTypes.SET_SHIFT_KEY_STATE, true)
|
|
||||||
|
|
||||||
if(!editorAreaFocus.value && !thumbnailsFocus.value) return
|
if(!editorAreaFocus.value && !thumbnailsFocus.value) return
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user