update
This commit is contained in:
parent
7246f89bc0
commit
d57cdafa5d
|
@ -101,7 +101,7 @@ $subMenuWidth: 120px;
|
|||
}
|
||||
.contextmenu-item {
|
||||
padding: 0 20px;
|
||||
color: #666;
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -4,6 +4,16 @@ export enum ClipPathTypes {
|
|||
POLYGON = 'polygon',
|
||||
}
|
||||
|
||||
export enum ClipPaths {
|
||||
RECT = 'rect',
|
||||
ROUNDRECT = 'roundRect',
|
||||
ELLIPSE = 'ellipse',
|
||||
TRIANGLE = 'triangle',
|
||||
PENTAGON = 'pentagon',
|
||||
RHOMBUS = 'rhombus',
|
||||
STAR = 'star',
|
||||
}
|
||||
|
||||
export const CLIPPATHS = {
|
||||
rect: {
|
||||
name: '矩形',
|
||||
|
|
|
@ -39,7 +39,7 @@ export interface PPTImageElement extends PPTElementBaseProps, PPTElementSizeProp
|
|||
filter?: string;
|
||||
clip?: {
|
||||
range: [[number, number], [number, number]];
|
||||
shape: 'rect' | 'ellipse' | 'polygon';
|
||||
shape: 'rect' | 'roundRect' | 'ellipse' | 'triangle' | 'pentagon' | 'rhombus' | 'star';
|
||||
};
|
||||
flip?: { x?: number, y?: number };
|
||||
shadow?: string;
|
||||
|
|
|
@ -35,12 +35,35 @@
|
|||
v-for="(line, index) in alignmentLines" :key="index"
|
||||
:type="line.type" :axis="line.axis" :length="line.length"
|
||||
/>
|
||||
|
||||
<EditableElement
|
||||
v-for="(element, index) in elementList"
|
||||
:key="element.elId"
|
||||
:elementInfo="element"
|
||||
:elementIndex="index + 1"
|
||||
:isActive="activeElementIdList.includes(element.elId)"
|
||||
: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"
|
||||
:deleteElement="deleteElement"
|
||||
:lockElement="lockElement"
|
||||
:copyElement="copyElement"
|
||||
:cutElement="cutElement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, defineComponent, reactive, ref, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State } from '@/store/state'
|
||||
import { MutationTypes } from '@/store/constants'
|
||||
|
@ -51,6 +74,7 @@ import { getImageDataURL } from '@/utils/image'
|
|||
import useDropImage from '@/hooks/useDropImage'
|
||||
import useSetViewportSize from './hooks/useSetViewportSize'
|
||||
|
||||
import EditableElement from '@/views/_common/_element/EditableElement.vue'
|
||||
import MouseSelection from './MouseSelection.vue'
|
||||
import SlideBackground from './SlideBackground.vue'
|
||||
import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue'
|
||||
|
@ -58,12 +82,21 @@ import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue'
|
|||
export default defineComponent({
|
||||
name: 'v-canvas',
|
||||
components: {
|
||||
EditableElement,
|
||||
MouseSelection,
|
||||
SlideBackground,
|
||||
AlignmentLine,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore<State>()
|
||||
const elementList = computed(() => {
|
||||
const currentSlide = store.getters.currentSlide
|
||||
return currentSlide ? JSON.parse(JSON.stringify(currentSlide.elements)) : []
|
||||
})
|
||||
const activeElementIdList = computed(() => store.state.activeElementIdList)
|
||||
const handleElementId = computed(() => store.state.handleElementId)
|
||||
const activeGroupElementId = ref('')
|
||||
|
||||
const viewportRef = ref<HTMLElement | null>(null)
|
||||
const isShowGridLines = ref(false)
|
||||
const alignmentLines = ref<AlignmentLineProps[]>([])
|
||||
|
@ -162,6 +195,40 @@ export default defineComponent({
|
|||
if(editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, false)
|
||||
}
|
||||
|
||||
const selectElement = () => {
|
||||
console.log('selectElement')
|
||||
}
|
||||
const rotateElement = () => {
|
||||
console.log('rotateElement')
|
||||
}
|
||||
const scaleElement = () => {
|
||||
console.log('scaleElement')
|
||||
}
|
||||
const orderElement = () => {
|
||||
console.log('orderElement')
|
||||
}
|
||||
const combineElements = () => {
|
||||
console.log('combineElements')
|
||||
}
|
||||
const uncombineElements = () => {
|
||||
console.log('uncombineElements')
|
||||
}
|
||||
const alignElement = () => {
|
||||
console.log('alignElement')
|
||||
}
|
||||
const deleteElement = () => {
|
||||
console.log('deleteElement')
|
||||
}
|
||||
const lockElement = () => {
|
||||
console.log('lockElement')
|
||||
}
|
||||
const copyElement = () => {
|
||||
console.log('copyElement')
|
||||
}
|
||||
const cutElement = () => {
|
||||
console.log('cutElement')
|
||||
}
|
||||
|
||||
const contextmenus = (): ContextmenuItem[] => {
|
||||
return [
|
||||
{
|
||||
|
@ -179,6 +246,10 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
return {
|
||||
elementList,
|
||||
activeElementIdList,
|
||||
handleElementId,
|
||||
activeGroupElementId,
|
||||
canvasRef,
|
||||
viewportRef,
|
||||
viewportStyles,
|
||||
|
@ -189,6 +260,17 @@ export default defineComponent({
|
|||
currentSlide,
|
||||
isShowGridLines,
|
||||
alignmentLines,
|
||||
selectElement,
|
||||
rotateElement,
|
||||
scaleElement,
|
||||
orderElement,
|
||||
combineElements,
|
||||
uncombineElements,
|
||||
alignElement,
|
||||
deleteElement,
|
||||
lockElement,
|
||||
copyElement,
|
||||
cutElement,
|
||||
contextmenus,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
:isHandleEl="isHandleEl"
|
||||
:isActiveGroupElement="isActiveGroupElement"
|
||||
:isMultiSelect="isMultiSelect"
|
||||
:animationIndex="-1"
|
||||
:animationIndex="animationIndex"
|
||||
:selectElement="selectElement"
|
||||
:rotateElement="rotateElement"
|
||||
:scaleElement="scaleElement"
|
||||
|
@ -36,8 +36,8 @@ import {
|
|||
ElementLockCommands,
|
||||
} from '@/types/edit'
|
||||
|
||||
import ImageElement from './ImageElement.index.vue'
|
||||
import TextElement from './TextElement.index.vue'
|
||||
import ImageElement from './ImageElement/index.vue'
|
||||
import TextElement from './TextElement/index.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'editable-element',
|
||||
|
@ -70,6 +70,10 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
animationIndex: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
selectElement: {
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove?: boolean) => void>,
|
||||
required: true,
|
||||
|
|
|
@ -20,8 +20,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||
|
||||
export default {
|
||||
name: 'element-border',
|
||||
components: {
|
||||
SvgWrapper,
|
||||
},
|
||||
props: {
|
||||
width: {
|
||||
type: Number,
|
||||
|
|
|
@ -51,6 +51,8 @@
|
|||
import { computed, defineComponent, onMounted, onUnmounted, PropType, reactive, ref } from 'vue'
|
||||
import { KEYCODE } from '@/configs/keyCode'
|
||||
|
||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||
|
||||
type ClipDataRange = [[number, number], [number, number]]
|
||||
|
||||
interface ClipData {
|
||||
|
@ -72,6 +74,9 @@ type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
|
|||
|
||||
export default defineComponent({
|
||||
name: 'image-clip-handler',
|
||||
components: {
|
||||
SvgWrapper,
|
||||
},
|
||||
props: {
|
||||
imgUrl: {
|
||||
type: String,
|
||||
|
|
|
@ -23,8 +23,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||
|
||||
export default {
|
||||
name: 'image-ellipse-border',
|
||||
components: {
|
||||
SvgWrapper,
|
||||
},
|
||||
props: {
|
||||
width: {
|
||||
type: Number,
|
||||
|
|
|
@ -20,8 +20,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||
|
||||
export default {
|
||||
name: 'image-polygon-border',
|
||||
components: {
|
||||
SvgWrapper,
|
||||
},
|
||||
props: {
|
||||
width: {
|
||||
type: Number,
|
||||
|
|
|
@ -23,8 +23,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||
|
||||
export default {
|
||||
name: 'image-rect-border',
|
||||
components: {
|
||||
SvgWrapper,
|
||||
},
|
||||
props: {
|
||||
width: {
|
||||
type: Number,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="editable-element image"
|
||||
:class="{'lock': elementInfo.isLock}"
|
||||
:class="{ 'lock': elementInfo.isLock }"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
left: elementInfo.left + 'px',
|
||||
|
@ -33,7 +33,7 @@
|
|||
transform: flip,
|
||||
}"
|
||||
>
|
||||
<RectBorder
|
||||
<ImageRectBorder
|
||||
v-if="clipShape.type === 'rect'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
|
@ -42,7 +42,7 @@
|
|||
:borderWidth="elementInfo.borderWidth"
|
||||
:borderStyle="elementInfo.borderStyle"
|
||||
/>
|
||||
<EllipseBorder
|
||||
<ImageEllipseBorder
|
||||
v-else-if="clipShape.type === 'ellipse'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
|
@ -50,7 +50,7 @@
|
|||
:borderWidth="elementInfo.borderWidth"
|
||||
:borderStyle="elementInfo.borderStyle"
|
||||
/>
|
||||
<PolygonBorder
|
||||
<ImagePolygonBorder
|
||||
v-else-if="clipShape.type === 'polygon'"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
|
@ -83,7 +83,7 @@
|
|||
'multi-select': isMultiSelect && isActive,
|
||||
'selected': isHandleEl,
|
||||
}"
|
||||
:style="{transform: `scale(${1 / canvasScale})`}"
|
||||
:style="{ transform: `scale(${1 / canvasScale})` }"
|
||||
v-if="!isCliping"
|
||||
>
|
||||
<BorderLine
|
||||
|
@ -102,7 +102,7 @@
|
|||
:style="point.style"
|
||||
@mousedown.stop="scaleElement($event, elementInfo, point.direction)"
|
||||
/>
|
||||
<RotateHandle
|
||||
<RotateHandler
|
||||
class="el-rotate-handle"
|
||||
:style="{left: scaleWidth / 2 + 'px'}"
|
||||
@mousedown.stop="rotateElement(elementInfo)"
|
||||
|
@ -186,6 +186,9 @@ export default defineComponent({
|
|||
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: ElementScaleHandler) => void>,
|
||||
required: true,
|
||||
},
|
||||
contextmenus: {
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const clipingImageElId = ref('')
|
||||
|
@ -291,6 +294,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
return {
|
||||
scaleWidth,
|
||||
isCliping,
|
||||
imgPosition,
|
||||
clipShape,
|
||||
|
|
|
@ -0,0 +1,266 @@
|
|||
<template>
|
||||
<div
|
||||
class="editable-element text"
|
||||
:class="{ 'lock': elementInfo.isLock }"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
left: elementInfo.left + 'px',
|
||||
width: elementInfo.width + 'px',
|
||||
transform: `rotate(${elementInfo.rotate}deg)`,
|
||||
}"
|
||||
@mousedown="handleSelectElement($event, false)"
|
||||
>
|
||||
<div class="element-content"
|
||||
:style="{
|
||||
backgroundColor: elementInfo.fill,
|
||||
opacity: elementInfo.opacity,
|
||||
textShadow: elementInfo.shadow,
|
||||
lineHeight: elementInfo.lineHeight,
|
||||
letterSpacing: (elementInfo.letterSpacing || 0) + 'px',
|
||||
}"
|
||||
v-contextmenu="contextmenus"
|
||||
>
|
||||
<ElementBorder
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:borderColor="elementInfo.borderColor"
|
||||
:borderWidth="elementInfo.borderWidth"
|
||||
:borderStyle="elementInfo.borderStyle"
|
||||
/>
|
||||
<div class="text-content"
|
||||
v-html="elementInfo.content"
|
||||
:contenteditable="isActive && !elementInfo.isLock"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="operate"
|
||||
:class="{
|
||||
'show': isActive,
|
||||
'multi-select': isMultiSelect && isActive,
|
||||
'selected': isHandleEl
|
||||
}"
|
||||
:style="{ transform: `scale(${1 / canvasScale})` }"
|
||||
v-contextmenu="contextmenus"
|
||||
>
|
||||
<BorderLine
|
||||
class="el-border-line"
|
||||
v-for="line in borderLines"
|
||||
:key="line.type"
|
||||
:type="line.type"
|
||||
:style="line.style"
|
||||
:isWide="true"
|
||||
@mousedown="handleSelectElement($event)"
|
||||
/>
|
||||
<template v-if="!elementInfo.isLock && (isActiveGroupElement || !isMultiSelect)">
|
||||
<ResizablePoint class="el-resizable-point"
|
||||
v-for="point in resizablePoints"
|
||||
:key="point.type"
|
||||
:type="point.type"
|
||||
:style="point.style"
|
||||
@mousedown.stop="scaleElement($event, elementInfo, point.direction)"
|
||||
/>
|
||||
<RotateHandler
|
||||
class="el-rotate-handle"
|
||||
:style="{ left: scaleWidth / 2 + 'px' }"
|
||||
@mousedown.stop="rotateElement(elementInfo)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, PropType } from 'vue'
|
||||
|
||||
import { PPTTextElement } from '@/types/slides'
|
||||
import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
|
||||
|
||||
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
|
||||
import { OPERATE_KEYS } from '@/configs/element'
|
||||
|
||||
import ElementBorder from '@/views/_common/_element/ElementBorder.vue'
|
||||
import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
|
||||
import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'
|
||||
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
|
||||
import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'slide-element-text',
|
||||
components: {
|
||||
ElementBorder,
|
||||
RotateHandler,
|
||||
ResizablePoint,
|
||||
BorderLine,
|
||||
AnimationIndex,
|
||||
},
|
||||
props: {
|
||||
elementInfo: {
|
||||
type: Object as PropType<PPTTextElement>,
|
||||
required: true,
|
||||
},
|
||||
canvasScale: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isHandleEl: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isActiveGroupElement: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isMultiSelect: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
animationIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
selectElement: {
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTTextElement, canMove?: boolean) => void>,
|
||||
required: true,
|
||||
},
|
||||
rotateElement: {
|
||||
type: Function as PropType<(element: PPTTextElement) => void>,
|
||||
required: true,
|
||||
},
|
||||
scaleElement: {
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTTextElement, command: ElementScaleHandler) => void>,
|
||||
required: true,
|
||||
},
|
||||
contextmenus: {
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const scaleWidth = computed(() => props.elementInfo ? props.elementInfo.width * props.canvasScale : 0)
|
||||
const scaleHeight = computed(() => props.elementInfo ? props.elementInfo.height * props.canvasScale : 0)
|
||||
|
||||
const resizablePoints = computed(() => {
|
||||
return [
|
||||
{ type: OperateResizablePointTypes.TL, direction: OPERATE_KEYS.LEFT_TOP, style: {} },
|
||||
{ type: OperateResizablePointTypes.TC, direction: OPERATE_KEYS.TOP, style: {left: scaleWidth.value / 2 + 'px'} },
|
||||
{ type: OperateResizablePointTypes.TR, direction: OPERATE_KEYS.RIGHT_TOP, style: {left: scaleWidth.value + 'px'} },
|
||||
{ type: OperateResizablePointTypes.ML, direction: OPERATE_KEYS.LEFT, style: {top: scaleHeight.value / 2 + 'px'} },
|
||||
{ type: OperateResizablePointTypes.MR, direction: OPERATE_KEYS.RIGHT, style: {left: scaleWidth.value + 'px', top: scaleHeight.value / 2 + 'px'} },
|
||||
{ type: OperateResizablePointTypes.BL, direction: OPERATE_KEYS.LEFT_BOTTOM, style: {top: scaleHeight.value + 'px'} },
|
||||
{ type: OperateResizablePointTypes.BC, direction: OPERATE_KEYS.BOTTOM, style: {left: scaleWidth.value / 2 + 'px', top: scaleHeight.value + 'px'} },
|
||||
{ type: OperateResizablePointTypes.BR, direction: OPERATE_KEYS.RIGHT_BOTTOM, style: {left: scaleWidth.value + 'px', top: scaleHeight.value + 'px'} },
|
||||
]
|
||||
})
|
||||
|
||||
const borderLines = computed(() => {
|
||||
return [
|
||||
{ type: OperateBorderLineTypes.T, style: {width: scaleWidth.value + 'px'} },
|
||||
{ type: OperateBorderLineTypes.B, style: {top: scaleHeight.value + 'px', width: scaleWidth.value + 'px'} },
|
||||
{ type: OperateBorderLineTypes.L, style: {height: scaleHeight.value + 'px'} },
|
||||
{ type: OperateBorderLineTypes.R, style: {left: scaleWidth.value + 'px', height: scaleHeight.value + 'px'} },
|
||||
]
|
||||
})
|
||||
|
||||
const handleSelectElement = (e: MouseEvent, canMove = true) => {
|
||||
if(props.elementInfo.isLock) return
|
||||
e.stopPropagation()
|
||||
|
||||
props.selectElement(e, props.elementInfo, canMove)
|
||||
}
|
||||
|
||||
return {
|
||||
scaleWidth,
|
||||
resizablePoints,
|
||||
borderLines,
|
||||
handleSelectElement,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editable-element {
|
||||
position: absolute;
|
||||
|
||||
&.lock .el-border-line {
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
&:hover .el-border-line {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.lock .element-content {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.element-content {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
|
||||
.text-content {
|
||||
position: relative;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .text-content {
|
||||
word-break: break-word;
|
||||
font-family: '微软雅黑';
|
||||
outline: 0;
|
||||
|
||||
::selection {
|
||||
background-color: rgba(27, 110, 232, 0.3);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
padding-inline-start: 30px;
|
||||
li {
|
||||
list-style-type: disc;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
padding-inline-start: 30px;
|
||||
li {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operate {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
|
||||
&.active {
|
||||
.el-border-line,
|
||||
.el-resizable-point,
|
||||
.el-rotate-handle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.multi-select:not(.selected) .el-border-line {
|
||||
border-color: rgba($color: $themeColor, $alpha: .5);
|
||||
}
|
||||
|
||||
.el-border-line,
|
||||
.el-resizable-point,
|
||||
.el-rotate-handle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue