mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
update
This commit is contained in:
parent
ded04bf837
commit
bfb24e5055
@ -63,4 +63,21 @@ export interface MultiSelectRange {
|
|||||||
maxX: number;
|
maxX: number;
|
||||||
minY: number;
|
minY: number;
|
||||||
maxY: number;
|
maxY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ImageClipDataRange = [[number, number], [number, number]]
|
||||||
|
|
||||||
|
export interface ImageClipData {
|
||||||
|
range: ImageClipDataRange;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageClipedEmitData {
|
||||||
|
range: ImageClipDataRange;
|
||||||
|
position: {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
}
|
}
|
@ -25,21 +25,13 @@ export default defineComponent({
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
offsetX: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
offsetY: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const store = useStore<State>()
|
const store = useStore<State>()
|
||||||
const canvasScale = computed(() => store.state.canvasScale)
|
const canvasScale = computed(() => store.state.canvasScale)
|
||||||
|
|
||||||
const left = computed(() => props.axis.x * canvasScale.value + props.offsetX + 'px')
|
const left = computed(() => props.axis.x * canvasScale.value + 'px')
|
||||||
const top = computed(() => props.axis.y * canvasScale.value + props.offsetY + 'px')
|
const top = computed(() => props.axis.y * canvasScale.value + 'px')
|
||||||
|
|
||||||
const sizeStyle = computed(() => {
|
const sizeStyle = computed(() => {
|
||||||
if(props.type === 'vertical') return { height: props.length * canvasScale.value + 'px' }
|
if(props.type === 'vertical') return { height: props.length * canvasScale.value + 'px' }
|
||||||
@ -66,11 +58,11 @@ export default defineComponent({
|
|||||||
border: 0 dashed $themeColor;
|
border: 0 dashed $themeColor;
|
||||||
|
|
||||||
&.vertical {
|
&.vertical {
|
||||||
margin-left: -0.5px;
|
transform: translateY(-0.5px);
|
||||||
border-left-width: 1px;
|
border-left-width: 1px;
|
||||||
}
|
}
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
margin-top: -0.5px;
|
transform: translateX(-0.5px);
|
||||||
border-top-width: 1px;
|
border-top-width: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,26 +50,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, onMounted, onUnmounted, PropType, reactive, ref } from 'vue'
|
import { computed, defineComponent, onMounted, onUnmounted, PropType, reactive, ref } from 'vue'
|
||||||
import { KEYS } from '@/configs/hotkey'
|
import { KEYS } from '@/configs/hotkey'
|
||||||
|
import { ImageClipData, ImageClipDataRange, ImageClipedEmitData } from '@/types/edit'
|
||||||
|
|
||||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||||
|
|
||||||
type ClipDataRange = [[number, number], [number, number]]
|
|
||||||
|
|
||||||
interface ClipData {
|
|
||||||
range: ClipDataRange;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClipedEmitData {
|
|
||||||
range: ClipDataRange;
|
|
||||||
position: {
|
|
||||||
left: number;
|
|
||||||
top: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
|
type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -83,7 +67,7 @@ export default defineComponent({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
clipData: {
|
clipData: {
|
||||||
type: Object as PropType<ClipData>,
|
type: Object as PropType<ImageClipData>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
clipPath: {
|
clipPath: {
|
||||||
@ -123,7 +107,7 @@ export default defineComponent({
|
|||||||
left: '0',
|
left: '0',
|
||||||
})
|
})
|
||||||
const isSettingClipRange = ref(false)
|
const isSettingClipRange = ref(false)
|
||||||
const currentRange = ref<ClipDataRange | null>(null)
|
const currentRange = ref<ImageClipDataRange | null>(null)
|
||||||
|
|
||||||
const getClipDataTransformInfo = () => {
|
const getClipDataTransformInfo = () => {
|
||||||
const [start, end] = props.clipData ? props.clipData.range : [[0, 0], [100, 100]]
|
const [start, end] = props.clipData ? props.clipData.range : [[0, 0], [100, 100]]
|
||||||
@ -209,7 +193,7 @@ export default defineComponent({
|
|||||||
height: (topImgWrapperPosition.height - 100) / 100 * props.height,
|
height: (topImgWrapperPosition.height - 100) / 100 * props.height,
|
||||||
}
|
}
|
||||||
|
|
||||||
const clipedEmitData: ClipedEmitData = {
|
const clipedEmitData: ImageClipedEmitData = {
|
||||||
range: currentRange.value,
|
range: currentRange.value,
|
||||||
position,
|
position,
|
||||||
}
|
}
|
169
src/views/Editor/Canvas/Operate/ImageElementOperate.vue
Normal file
169
src/views/Editor/Canvas/Operate/ImageElementOperate.vue
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<ImageClipHandler
|
||||||
|
v-if="isCliping"
|
||||||
|
:src="elementInfo.src"
|
||||||
|
:clipData="elementInfo.clip"
|
||||||
|
:canvasScale="canvasScale"
|
||||||
|
:width="elementInfo.width"
|
||||||
|
:height="elementInfo.height"
|
||||||
|
:top="elementInfo.top"
|
||||||
|
:left="elementInfo.left"
|
||||||
|
:clipPath="clipShape.style"
|
||||||
|
@clip="range => clip(range)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="image-element-operate"
|
||||||
|
v-else
|
||||||
|
:class="{
|
||||||
|
'selected': isSelected,
|
||||||
|
'multi-select': isMultiSelect && isSelected,
|
||||||
|
'active': isActive,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<BorderLine
|
||||||
|
class="operate-border-line"
|
||||||
|
v-for="line in borderLines"
|
||||||
|
:key="line.type"
|
||||||
|
:type="line.type"
|
||||||
|
:style="line.style"
|
||||||
|
/>
|
||||||
|
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
|
||||||
|
<ResizeHandler
|
||||||
|
class="operate-resize-handler"
|
||||||
|
v-for="point in resizeHandlers"
|
||||||
|
:key="point.direction"
|
||||||
|
:type="point.direction"
|
||||||
|
:style="point.style"
|
||||||
|
@mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
|
||||||
|
/>
|
||||||
|
<RotateHandler
|
||||||
|
class="operate-rotate-handler"
|
||||||
|
:style="{ left: scaleWidth / 2 + 'px' }"
|
||||||
|
@mousedown.stop="rotateElement(elementInfo)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType, ref } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { State } from '@/store'
|
||||||
|
|
||||||
|
import { PPTImageElement } from '@/types/slides'
|
||||||
|
import { OperateResizeHandler, ImageClipedEmitData } from '@/types/edit'
|
||||||
|
import useCommonOperate from '../hooks/useCommonOperate'
|
||||||
|
|
||||||
|
import RotateHandler from './RotateHandler.vue'
|
||||||
|
import ResizeHandler from './ResizeHandler.vue'
|
||||||
|
import BorderLine from './BorderLine.vue'
|
||||||
|
import AnimationIndex from './AnimationIndex.vue'
|
||||||
|
import ImageClipHandler from './ImageClipHandler.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'image-element-operate',
|
||||||
|
components: {
|
||||||
|
RotateHandler,
|
||||||
|
ResizeHandler,
|
||||||
|
BorderLine,
|
||||||
|
AnimationIndex,
|
||||||
|
ImageClipHandler,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
elementInfo: {
|
||||||
|
type: Object as PropType<PPTImageElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isSelected: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isActiveGroupElement: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isMultiSelect: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
animationIndex: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
rotateElement: {
|
||||||
|
type: Function as PropType<(element: PPTImageElement) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
scaleElement: {
|
||||||
|
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: OperateResizeHandler) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore<State>()
|
||||||
|
const canvasScale = computed(() => store.state.canvasScale)
|
||||||
|
|
||||||
|
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
|
||||||
|
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
|
||||||
|
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
|
||||||
|
|
||||||
|
const clipingImageElId = ref('')
|
||||||
|
|
||||||
|
const isCliping = computed(() => clipingImageElId.value === props.elementInfo.id)
|
||||||
|
|
||||||
|
const clip = (data: ImageClipedEmitData) => {
|
||||||
|
clipingImageElId.value = ''
|
||||||
|
|
||||||
|
if(!data) return
|
||||||
|
|
||||||
|
const { range, position } = data
|
||||||
|
const originClip = props.elementInfo.clip || {}
|
||||||
|
|
||||||
|
const _props = {
|
||||||
|
clip: { ...originClip, range },
|
||||||
|
left: props.elementInfo.left + position.left,
|
||||||
|
top: props.elementInfo.top + position.top,
|
||||||
|
width: props.elementInfo.width + position.width,
|
||||||
|
height: props.elementInfo.height + position.height,
|
||||||
|
}
|
||||||
|
console.log(_props)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scaleWidth,
|
||||||
|
resizeHandlers,
|
||||||
|
borderLines,
|
||||||
|
isCliping,
|
||||||
|
clip,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.image-element-operate {
|
||||||
|
&.selected {
|
||||||
|
.operate-border-line,
|
||||||
|
.operate-resize-handler,
|
||||||
|
.operate-rotate-handler {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multi-select:not(.selected) .operate-border-line {
|
||||||
|
border-color: rgba($color: $themeColor, $alpha: .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operate-border-line,
|
||||||
|
.operate-resize-handler,
|
||||||
|
.operate-rotate-handler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -2,9 +2,8 @@
|
|||||||
<div
|
<div
|
||||||
class="multi-select-operate"
|
class="multi-select-operate"
|
||||||
:style="{
|
:style="{
|
||||||
left: minX + 'px',
|
left: minX * canvasScale + 'px',
|
||||||
top: minY + 'px',
|
top: minY * canvasScale + 'px',
|
||||||
transform: `scale(${1 / canvasScale})`,
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<BorderLine v-for="line in borderLines" :key="line.type" :type="line.type" :style="line.style" />
|
<BorderLine v-for="line in borderLines" :key="line.type" :type="line.type" :style="line.style" />
|
||||||
@ -28,10 +27,10 @@ import { State } from '@/store'
|
|||||||
import { PPTElement, ElementTypes } from '@/types/slides'
|
import { PPTElement, ElementTypes } from '@/types/slides'
|
||||||
import { getElementListRange } from '@/utils/element'
|
import { getElementListRange } from '@/utils/element'
|
||||||
import { OperateResizeHandler, MultiSelectRange } from '@/types/edit'
|
import { OperateResizeHandler, MultiSelectRange } from '@/types/edit'
|
||||||
import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
|
import useCommonOperate from '../hooks/useCommonOperate'
|
||||||
|
|
||||||
import ResizeHandler from '@/views/_common/_operate/ResizeHandler.vue'
|
import ResizeHandler from './ResizeHandler.vue'
|
||||||
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
|
import BorderLine from './BorderLine.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'multi-select-operate',
|
name: 'multi-select-operate',
|
||||||
@ -102,6 +101,6 @@ export default defineComponent({
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 100;
|
z-index: 101;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
22
src/views/Editor/Canvas/Operate/RotateHandler.vue
Normal file
22
src/views/Editor/Canvas/Operate/RotateHandler.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rotate-handler"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'rotate-handler',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.rotate-handler {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
top: -25px;
|
||||||
|
margin-left: -5px;
|
||||||
|
border: 1px solid $themeColor;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
131
src/views/Editor/Canvas/Operate/TextElementOperate.vue
Normal file
131
src/views/Editor/Canvas/Operate/TextElementOperate.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="text-element-operate"
|
||||||
|
:class="{
|
||||||
|
'selected': isSelected,
|
||||||
|
'multi-select': isMultiSelect && isSelected,
|
||||||
|
'active': isActive,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<BorderLine
|
||||||
|
class="operate-border-line"
|
||||||
|
v-for="line in borderLines"
|
||||||
|
:key="line.type"
|
||||||
|
:type="line.type"
|
||||||
|
:style="line.style"
|
||||||
|
/>
|
||||||
|
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
|
||||||
|
<ResizeHandler
|
||||||
|
class="operate-resize-handler"
|
||||||
|
v-for="point in textElementResizeHandlers"
|
||||||
|
:key="point.direction"
|
||||||
|
:type="point.direction"
|
||||||
|
:style="point.style"
|
||||||
|
@mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
|
||||||
|
/>
|
||||||
|
<RotateHandler
|
||||||
|
class="operate-rotate-handler"
|
||||||
|
:style="{ left: scaleWidth / 2 + 'px' }"
|
||||||
|
@mousedown.stop="rotateElement(elementInfo)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { State } from '@/store'
|
||||||
|
|
||||||
|
import { PPTTextElement } from '@/types/slides'
|
||||||
|
import { OperateResizeHandler } from '@/types/edit'
|
||||||
|
import useCommonOperate from '../hooks/useCommonOperate'
|
||||||
|
|
||||||
|
import RotateHandler from './RotateHandler.vue'
|
||||||
|
import ResizeHandler from './ResizeHandler.vue'
|
||||||
|
import BorderLine from './BorderLine.vue'
|
||||||
|
import AnimationIndex from './AnimationIndex.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'text-element-operate',
|
||||||
|
components: {
|
||||||
|
RotateHandler,
|
||||||
|
ResizeHandler,
|
||||||
|
BorderLine,
|
||||||
|
AnimationIndex,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
elementInfo: {
|
||||||
|
type: Object as PropType<PPTTextElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isSelected: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isActiveGroupElement: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isMultiSelect: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
animationIndex: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
rotateElement: {
|
||||||
|
type: Function as PropType<(element: PPTTextElement) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
scaleElement: {
|
||||||
|
type: Function as PropType<(e: MouseEvent, element: PPTTextElement, command: OperateResizeHandler) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore<State>()
|
||||||
|
const canvasScale = computed(() => store.state.canvasScale)
|
||||||
|
|
||||||
|
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
|
||||||
|
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
|
||||||
|
|
||||||
|
const { textElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
|
||||||
|
|
||||||
|
return {
|
||||||
|
scaleWidth,
|
||||||
|
textElementResizeHandlers,
|
||||||
|
borderLines,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.text-element-operate {
|
||||||
|
&.selected {
|
||||||
|
.operate-border-line,
|
||||||
|
.operate-resize-handler,
|
||||||
|
.operate-rotate-handler {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multi-select:not(.selected) .operate-border-line {
|
||||||
|
border-color: rgba($color: $themeColor, $alpha: .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operate-border-line,
|
||||||
|
.operate-resize-handler,
|
||||||
|
.operate-rotate-handler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
97
src/views/Editor/Canvas/Operate/index.vue
Normal file
97
src/views/Editor/Canvas/Operate/index.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="operate"
|
||||||
|
:style="{
|
||||||
|
top: elementInfo.top * canvasScale + 'px',
|
||||||
|
left: elementInfo.left * canvasScale + 'px',
|
||||||
|
transform: `rotate(${elementInfo.rotate}deg)`,
|
||||||
|
'transform-origin': `${elementInfo.width * canvasScale / 2}px ${elementInfo.height * canvasScale / 2}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="currentOperateComponent"
|
||||||
|
:elementInfo="elementInfo"
|
||||||
|
:isSelected="isSelected"
|
||||||
|
:isActive="isActive"
|
||||||
|
:isActiveGroupElement="isActiveGroupElement"
|
||||||
|
:isMultiSelect="isMultiSelect"
|
||||||
|
:animationIndex="animationIndex"
|
||||||
|
:rotateElement="rotateElement"
|
||||||
|
:scaleElement="scaleElement"
|
||||||
|
></component>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType, computed } from 'vue'
|
||||||
|
import { PPTElement } from '@/types/slides'
|
||||||
|
import { OperateResizeHandler } from '@/types/edit'
|
||||||
|
|
||||||
|
import ImageElementOperate from './ImageElementOperate.vue'
|
||||||
|
import TextElementOperate from './TextElementOperate.vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { State } from '@/store'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'operate',
|
||||||
|
props: {
|
||||||
|
elementInfo: {
|
||||||
|
type: Object as PropType<PPTElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isSelected: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isActiveGroupElement: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isMultiSelect: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
animationIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
rotateElement: {
|
||||||
|
type: Function as PropType<(element: PPTElement) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
scaleElement: {
|
||||||
|
type: Function as PropType<(e: MouseEvent, element: PPTElement, command: OperateResizeHandler) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore<State>()
|
||||||
|
const canvasScale = computed(() => store.state.canvasScale)
|
||||||
|
|
||||||
|
const currentOperateComponent = computed(() => {
|
||||||
|
const elementTypeMap = {
|
||||||
|
'image': ImageElementOperate,
|
||||||
|
'text': TextElementOperate,
|
||||||
|
}
|
||||||
|
return elementTypeMap[props.elementInfo.type] || null
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentOperateComponent,
|
||||||
|
canvasScale,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.operate {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
@ -13,6 +13,13 @@ export default (width: Ref<number>, height: Ref<number>) => {
|
|||||||
{ direction: OperateResizeHandlers.BOTTOM, style: {left: width.value / 2 + 'px', top: height.value + 'px'} },
|
{ direction: OperateResizeHandlers.BOTTOM, style: {left: width.value / 2 + 'px', top: height.value + 'px'} },
|
||||||
{ direction: OperateResizeHandlers.RIGHT_BOTTOM, style: {left: width.value + 'px', top: height.value + 'px'} },
|
{ direction: OperateResizeHandlers.RIGHT_BOTTOM, style: {left: width.value + 'px', top: height.value + 'px'} },
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const textElementResizeHandlers = computed(() => {
|
||||||
|
return [
|
||||||
|
{ direction: OperateResizeHandlers.LEFT, style: {top: height.value / 2 + 'px'} },
|
||||||
|
{ direction: OperateResizeHandlers.RIGHT, style: {left: width.value + 'px', top: height.value / 2 + 'px'} },
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const borderLines = computed(() => {
|
const borderLines = computed(() => {
|
||||||
@ -26,6 +33,7 @@ export default (width: Ref<number>, height: Ref<number>) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
resizeHandlers,
|
resizeHandlers,
|
||||||
|
textElementResizeHandlers,
|
||||||
borderLines,
|
borderLines,
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -216,21 +216,21 @@ export default (
|
|||||||
targetTop = targetTop - (targetMinY - value)
|
targetTop = targetTop - (targetMinY - value)
|
||||||
isHorizontalAdsorbed = true
|
isHorizontalAdsorbed = true
|
||||||
}
|
}
|
||||||
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
|
_alignmentLines.push({type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100})
|
||||||
}
|
}
|
||||||
if(Math.abs(targetMaxY - value) < sorptionRange) {
|
if(Math.abs(targetMaxY - value) < sorptionRange) {
|
||||||
if(!isHorizontalAdsorbed) {
|
if(!isHorizontalAdsorbed) {
|
||||||
targetTop = targetTop - (targetMaxY - value)
|
targetTop = targetTop - (targetMaxY - value)
|
||||||
isHorizontalAdsorbed = true
|
isHorizontalAdsorbed = true
|
||||||
}
|
}
|
||||||
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
|
_alignmentLines.push({type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100})
|
||||||
}
|
}
|
||||||
if(Math.abs(targetCenterY - value) < sorptionRange) {
|
if(Math.abs(targetCenterY - value) < sorptionRange) {
|
||||||
if(!isHorizontalAdsorbed) {
|
if(!isHorizontalAdsorbed) {
|
||||||
targetTop = targetTop - (targetCenterY - value)
|
targetTop = targetTop - (targetCenterY - value)
|
||||||
isHorizontalAdsorbed = true
|
isHorizontalAdsorbed = true
|
||||||
}
|
}
|
||||||
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
|
_alignmentLines.push({type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(let i = 0; i < verticalLines.length; i++) {
|
for(let i = 0; i < verticalLines.length; i++) {
|
||||||
@ -243,21 +243,21 @@ export default (
|
|||||||
targetLeft = targetLeft - (targetMinX - value)
|
targetLeft = targetLeft - (targetMinX - value)
|
||||||
isVerticalAdsorbed = true
|
isVerticalAdsorbed = true
|
||||||
}
|
}
|
||||||
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
|
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100})
|
||||||
}
|
}
|
||||||
if(Math.abs(targetMaxX - value) < sorptionRange) {
|
if(Math.abs(targetMaxX - value) < sorptionRange) {
|
||||||
if(!isVerticalAdsorbed) {
|
if(!isVerticalAdsorbed) {
|
||||||
targetLeft = targetLeft - (targetMaxX - value)
|
targetLeft = targetLeft - (targetMaxX - value)
|
||||||
isVerticalAdsorbed = true
|
isVerticalAdsorbed = true
|
||||||
}
|
}
|
||||||
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
|
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100})
|
||||||
}
|
}
|
||||||
if(Math.abs(targetCenterX - value) < sorptionRange) {
|
if(Math.abs(targetCenterX - value) < sorptionRange) {
|
||||||
if(!isVerticalAdsorbed) {
|
if(!isVerticalAdsorbed) {
|
||||||
targetLeft = targetLeft - (targetCenterX - value)
|
targetLeft = targetLeft - (targetCenterX - value)
|
||||||
isVerticalAdsorbed = true
|
isVerticalAdsorbed = true
|
||||||
}
|
}
|
||||||
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
|
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
alignmentLines.value = _alignmentLines
|
alignmentLines.value = _alignmentLines
|
||||||
|
@ -192,7 +192,7 @@ export default (
|
|||||||
correctionVal.offsetY = currentY - value
|
correctionVal.offsetY = currentY - value
|
||||||
isHorizontalAdsorbed = true
|
isHorizontalAdsorbed = true
|
||||||
}
|
}
|
||||||
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
|
_alignmentLines.push({ type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,7 +207,7 @@ export default (
|
|||||||
correctionVal.offsetX = currentX - value
|
correctionVal.offsetX = currentX - value
|
||||||
isVerticalAdsorbed = true
|
isVerticalAdsorbed = true
|
||||||
}
|
}
|
||||||
_alignmentLines.push({ type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40 })
|
_alignmentLines.push({ type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,15 +7,37 @@
|
|||||||
v-contextmenu="contextmenus"
|
v-contextmenu="contextmenus"
|
||||||
v-click-outside="removeEditorAreaFocus"
|
v-click-outside="removeEditorAreaFocus"
|
||||||
>
|
>
|
||||||
<AlignmentLine
|
<div
|
||||||
v-for="(line, index) in alignmentLines"
|
class="operates"
|
||||||
:key="index"
|
:style="{
|
||||||
:type="line.type"
|
left: viewportStyles.left + 'px',
|
||||||
:axis="line.axis"
|
top: viewportStyles.top + 'px',
|
||||||
:length="line.length"
|
}"
|
||||||
:offsetX="viewportStyles.left"
|
>
|
||||||
:offsetY="viewportStyles.top"
|
<AlignmentLine
|
||||||
/>
|
v-for="(line, index) in alignmentLines"
|
||||||
|
:key="index"
|
||||||
|
:type="line.type"
|
||||||
|
:axis="line.axis"
|
||||||
|
:length="line.length"
|
||||||
|
/>
|
||||||
|
<MultiSelectOperate
|
||||||
|
v-if="activeElementIdList.length > 1"
|
||||||
|
:elementList="elementList"
|
||||||
|
:scaleMultiElement="scaleMultiElement"
|
||||||
|
/>
|
||||||
|
<Operate
|
||||||
|
v-for="element in elementList"
|
||||||
|
:key="element.id"
|
||||||
|
:elementInfo="element"
|
||||||
|
:isSelected="activeElementIdList.includes(element.id)"
|
||||||
|
:isActiveGroupElement="activeGroupElementId === element.id"
|
||||||
|
:isMultiSelect="activeElementIdList.length > 1"
|
||||||
|
:rotateElement="rotateElement"
|
||||||
|
:scaleElement="scaleElement"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="viewport"
|
class="viewport"
|
||||||
ref="viewportRef"
|
ref="viewportRef"
|
||||||
@ -35,27 +57,14 @@
|
|||||||
:height="mouseSelectionState.height"
|
:height="mouseSelectionState.height"
|
||||||
:quadrant="mouseSelectionState.quadrant"
|
:quadrant="mouseSelectionState.quadrant"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MultiSelectOperate
|
|
||||||
v-if="activeElementIdList.length > 1"
|
|
||||||
:elementList="elementList"
|
|
||||||
:scaleMultiElement="scaleMultiElement"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SlideBackground />
|
<SlideBackground />
|
||||||
|
|
||||||
<EditableElement
|
<EditableElement
|
||||||
v-for="(element, index) in elementList"
|
v-for="(element, index) in elementList"
|
||||||
:key="element.id"
|
:key="element.id"
|
||||||
:elementInfo="element"
|
:elementInfo="element"
|
||||||
:elementIndex="index + 1"
|
:elementIndex="index + 1"
|
||||||
:isSelected="activeElementIdList.includes(element.id)"
|
|
||||||
:isActive="element.id === handleElementId"
|
|
||||||
:isActiveGroupElement="activeGroupElementId === element.id"
|
|
||||||
:isMultiSelect="activeElementIdList.length > 1"
|
:isMultiSelect="activeElementIdList.length > 1"
|
||||||
:selectElement="selectElement"
|
:selectElement="selectElement"
|
||||||
:rotateElement="rotateElement"
|
|
||||||
:scaleElement="scaleElement"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -83,11 +92,12 @@ import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
|
|||||||
import useSelectAllElement from '@/hooks/useSelectAllElement'
|
import useSelectAllElement from '@/hooks/useSelectAllElement'
|
||||||
import useScaleCanvas from '@/hooks/useScaleCanvas'
|
import useScaleCanvas from '@/hooks/useScaleCanvas'
|
||||||
|
|
||||||
import EditableElement from '@/views/_common/_element/EditableElement.vue'
|
import EditableElement from '@/views/_element/EditableElement.vue'
|
||||||
import MouseSelection from './MouseSelection.vue'
|
import MouseSelection from './MouseSelection.vue'
|
||||||
import SlideBackground from './SlideBackground.vue'
|
import SlideBackground from './SlideBackground.vue'
|
||||||
import MultiSelectOperate from './MultiSelectOperate.vue'
|
|
||||||
import AlignmentLine from './AlignmentLine.vue'
|
import AlignmentLine from './AlignmentLine.vue'
|
||||||
|
import MultiSelectOperate from './Operate/MultiSelectOperate.vue'
|
||||||
|
import Operate from './Operate/index.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'editor-canvas',
|
name: 'editor-canvas',
|
||||||
@ -95,8 +105,9 @@ export default defineComponent({
|
|||||||
EditableElement,
|
EditableElement,
|
||||||
MouseSelection,
|
MouseSelection,
|
||||||
SlideBackground,
|
SlideBackground,
|
||||||
MultiSelectOperate,
|
|
||||||
AlignmentLine,
|
AlignmentLine,
|
||||||
|
MultiSelectOperate,
|
||||||
|
Operate,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore<State>()
|
const store = useStore<State>()
|
||||||
@ -218,4 +229,7 @@ export default defineComponent({
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, .1);
|
box-shadow: 0 0 20px 0 rgba(0, 0, 0, .1);
|
||||||
}
|
}
|
||||||
|
.operates {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -42,7 +42,7 @@ import { fillDigit } from '@/utils/common'
|
|||||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
import useSlideHandler from '@/hooks/useSlideHandler'
|
import useSlideHandler from '@/hooks/useSlideHandler'
|
||||||
|
|
||||||
import ThumbnailSlide from '@/views/_common/ThumbnailSlide.vue'
|
import ThumbnailSlide from '@/views/ThumbnailSlide.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'thumbnails',
|
name: 'thumbnails',
|
||||||
|
@ -23,7 +23,7 @@ import { Slide } from '@/types/slides'
|
|||||||
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
|
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
|
||||||
import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
|
import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
|
||||||
|
|
||||||
import BaseElement from '@/views/_common/_element/BaseElement.vue'
|
import BaseElement from '@/views/_element/BaseElement.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'screen-slide',
|
name: 'screen-slide',
|
||||||
|
@ -172,10 +172,10 @@ export default defineComponent({
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
&.prev {
|
&.prev {
|
||||||
transform: translateX(-100%);
|
transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
&.next {
|
&.next {
|
||||||
transform: translateX(100%);
|
transform: translateY(100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.slide-content {
|
.slide-content {
|
||||||
|
@ -30,7 +30,7 @@ import { Slide } from '@/types/slides'
|
|||||||
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
|
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
|
||||||
import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
|
import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
|
||||||
|
|
||||||
import BaseElement from '@/views/_common/_element/BaseElement.vue'
|
import BaseElement from '@/views/_element/BaseElement.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'thumbnail-slide',
|
name: 'thumbnail-slide',
|
@ -1,223 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="editable-element-text"
|
|
||||||
:class="{ 'lock': elementInfo.lock }"
|
|
||||||
:style="{
|
|
||||||
top: elementInfo.top + 'px',
|
|
||||||
left: elementInfo.left + 'px',
|
|
||||||
width: elementInfo.width + 'px',
|
|
||||||
transform: `rotate(${elementInfo.rotate}deg)`,
|
|
||||||
}"
|
|
||||||
@mousedown="$event => handleSelectElement($event)"
|
|
||||||
>
|
|
||||||
<div class="element-content"
|
|
||||||
:style="{
|
|
||||||
backgroundColor: elementInfo.fill,
|
|
||||||
opacity: elementInfo.opacity,
|
|
||||||
textShadow: shadowStyle,
|
|
||||||
}"
|
|
||||||
v-contextmenu="contextmenus"
|
|
||||||
>
|
|
||||||
<ElementOutline
|
|
||||||
:width="elementInfo.width"
|
|
||||||
:height="elementInfo.height"
|
|
||||||
:outline="elementInfo.outline"
|
|
||||||
/>
|
|
||||||
<div class="text"
|
|
||||||
v-html="elementInfo.content"
|
|
||||||
:contenteditable="isActive && !elementInfo.lock"
|
|
||||||
@mousedown="$event => handleSelectElement($event, false)"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="operate"
|
|
||||||
:class="{
|
|
||||||
'selected': isSelected,
|
|
||||||
'multi-select': isMultiSelect && isSelected,
|
|
||||||
'active': isActive,
|
|
||||||
}"
|
|
||||||
:style="{ transform: `scale(${1 / canvasScale})` }"
|
|
||||||
>
|
|
||||||
<BorderLine
|
|
||||||
class="operate-border-line"
|
|
||||||
v-for="line in borderLines"
|
|
||||||
:key="line.type"
|
|
||||||
:type="line.type"
|
|
||||||
:style="line.style"
|
|
||||||
/>
|
|
||||||
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
|
|
||||||
<ResizeHandler
|
|
||||||
class="operate-resize-handler"
|
|
||||||
v-for="point in resizeHandlers"
|
|
||||||
:key="point.direction"
|
|
||||||
:type="point.direction"
|
|
||||||
:style="point.style"
|
|
||||||
@mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
|
|
||||||
/>
|
|
||||||
<RotateHandler
|
|
||||||
class="operate-rotate-handler"
|
|
||||||
: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, PropType } from 'vue'
|
|
||||||
|
|
||||||
import { PPTTextElement } from '@/types/slides'
|
|
||||||
import { OperateResizeHandler } from '@/types/edit'
|
|
||||||
import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
|
|
||||||
|
|
||||||
import ElementOutline from '@/views/_common/_element/ElementOutline.vue'
|
|
||||||
import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
|
|
||||||
import ResizeHandler from '@/views/_common/_operate/ResizeHandler.vue'
|
|
||||||
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
|
|
||||||
import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
|
|
||||||
|
|
||||||
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'editable-element-text',
|
|
||||||
components: {
|
|
||||||
ElementOutline,
|
|
||||||
RotateHandler,
|
|
||||||
ResizeHandler,
|
|
||||||
BorderLine,
|
|
||||||
AnimationIndex,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
elementInfo: {
|
|
||||||
type: Object as PropType<PPTTextElement>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
canvasScale: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isSelected: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
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: OperateResizeHandler) => void>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
contextmenus: {
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const scaleWidth = computed(() => props.elementInfo.width * props.canvasScale)
|
|
||||||
const scaleHeight = computed(() => props.elementInfo.height * props.canvasScale)
|
|
||||||
|
|
||||||
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
|
|
||||||
|
|
||||||
const handleSelectElement = (e: MouseEvent, canMove = true) => {
|
|
||||||
if(props.elementInfo.lock) return
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
props.selectElement(e, props.elementInfo, canMove)
|
|
||||||
}
|
|
||||||
|
|
||||||
const shadow = computed(() => props.elementInfo.shadow)
|
|
||||||
const { shadowStyle } = useElementShadow(shadow)
|
|
||||||
|
|
||||||
return {
|
|
||||||
scaleWidth,
|
|
||||||
resizeHandlers,
|
|
||||||
borderLines,
|
|
||||||
handleSelectElement,
|
|
||||||
shadowStyle,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.editable-element-text {
|
|
||||||
position: absolute;
|
|
||||||
cursor: move;
|
|
||||||
|
|
||||||
&.lock .element-content {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.element-content {
|
|
||||||
position: relative;
|
|
||||||
padding: 10px;
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
.text {
|
|
||||||
position: relative;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(.text) {
|
|
||||||
word-break: break-word;
|
|
||||||
font-family: '微软雅黑';
|
|
||||||
outline: 0;
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background-color: rgba(27, 110, 232, 0.3);
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.operate {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 100;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
.operate-border-line,
|
|
||||||
.operate-resize-handler,
|
|
||||||
.operate-rotate-handler {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.multi-select:not(.selected) .operate-border-line {
|
|
||||||
border-color: rgba($color: $themeColor, $alpha: .3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.operate-border-line,
|
|
||||||
.operate-resize-handler,
|
|
||||||
.operate-rotate-handler {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,34 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="rotate-handler">
|
|
||||||
<div class="rotate-icon"><IconFont type="icon-rotate" /></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'rotate-handler',
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.rotate-handler {
|
|
||||||
position: absolute;
|
|
||||||
top: -24px;
|
|
||||||
margin: -10px 0 0 -10px;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #fff;
|
|
||||||
box-shadow: 1px 1px 2px #888;
|
|
||||||
|
|
||||||
.rotate-icon {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -7,16 +7,8 @@
|
|||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="currentElementComponent"
|
:is="currentElementComponent"
|
||||||
:canvasScale="canvasScale"
|
|
||||||
:elementInfo="elementInfo"
|
:elementInfo="elementInfo"
|
||||||
:isSelected="isSelected"
|
|
||||||
:isActive="isActive"
|
|
||||||
:isActiveGroupElement="isActiveGroupElement"
|
|
||||||
:isMultiSelect="isMultiSelect"
|
|
||||||
:animationIndex="animationIndex"
|
|
||||||
:selectElement="selectElement"
|
:selectElement="selectElement"
|
||||||
:rotateElement="rotateElement"
|
|
||||||
:scaleElement="scaleElement"
|
|
||||||
:contextmenus="contextmenus"
|
:contextmenus="contextmenus"
|
||||||
></component>
|
></component>
|
||||||
</div>
|
</div>
|
||||||
@ -24,9 +16,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, PropType } from 'vue'
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { PPTElement } from '@/types/slides'
|
||||||
import { State } from '@/store'
|
|
||||||
import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement, PPTLineElement } from '@/types/slides'
|
|
||||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
|
|
||||||
import useLockElement from '@/hooks/useLockElement'
|
import useLockElement from '@/hooks/useLockElement'
|
||||||
@ -36,7 +26,7 @@ import useOrderElement from '@/hooks/useOrderElement'
|
|||||||
import useAlignElementToCanvas from '@/hooks/useAlignElementToCanvas'
|
import useAlignElementToCanvas from '@/hooks/useAlignElementToCanvas'
|
||||||
import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
|
import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
|
||||||
|
|
||||||
import { ElementOrderCommands, ElementAlignCommands, OperateResizeHandler } from '@/types/edit'
|
import { ElementOrderCommands, ElementAlignCommands } from '@/types/edit'
|
||||||
|
|
||||||
import ImageElement from './ImageElement/index.vue'
|
import ImageElement from './ImageElement/index.vue'
|
||||||
import TextElement from './TextElement/index.vue'
|
import TextElement from './TextElement/index.vue'
|
||||||
@ -52,43 +42,16 @@ export default defineComponent({
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
isSelected: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isActiveGroupElement: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isMultiSelect: {
|
isMultiSelect: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
animationIndex: {
|
|
||||||
type: Number,
|
|
||||||
default: -1,
|
|
||||||
},
|
|
||||||
selectElement: {
|
selectElement: {
|
||||||
type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove?: boolean) => void>,
|
type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove?: boolean) => void>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
rotateElement: {
|
|
||||||
type: Function as PropType<(element: PPTTextElement | PPTImageElement | PPTShapeElement) => void>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
scaleElement: {
|
|
||||||
type: Function as PropType<(e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandler) => void>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const store = useStore<State>()
|
|
||||||
const canvasScale = computed(() => store.state.canvasScale)
|
|
||||||
|
|
||||||
const currentElementComponent = computed(() => {
|
const currentElementComponent = computed(() => {
|
||||||
const elementTypeMap = {
|
const elementTypeMap = {
|
||||||
'image': ImageElement,
|
'image': ImageElement,
|
||||||
@ -172,7 +135,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canvasScale,
|
|
||||||
currentElementComponent,
|
currentElementComponent,
|
||||||
contextmenus,
|
contextmenus,
|
||||||
}
|
}
|
@ -24,7 +24,7 @@
|
|||||||
import { PropType, defineComponent, toRef } from 'vue'
|
import { PropType, defineComponent, toRef } from 'vue'
|
||||||
import { PPTElementOutline } from '@/types/slides'
|
import { PPTElementOutline } from '@/types/slides'
|
||||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||||
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
|
import useElementOutline from '@/views/_element/hooks/useElementOutline'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'element-outline',
|
name: 'element-outline',
|
@ -65,7 +65,7 @@ import ImageRectOutline from './ImageRectOutline.vue'
|
|||||||
import ImageEllipseOutline from './ImageEllipseOutline.vue'
|
import ImageEllipseOutline from './ImageEllipseOutline.vue'
|
||||||
import ImagePolygonOutline from './ImagePolygonOutline.vue'
|
import ImagePolygonOutline from './ImagePolygonOutline.vue'
|
||||||
|
|
||||||
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
|
import useElementShadow from '@/views/_element/hooks/useElementShadow'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'base-element-image',
|
name: 'base-element-image',
|
@ -27,7 +27,7 @@
|
|||||||
import { PropType, defineComponent, toRef } from 'vue'
|
import { PropType, defineComponent, toRef } from 'vue'
|
||||||
import { PPTElementOutline } from '@/types/slides'
|
import { PPTElementOutline } from '@/types/slides'
|
||||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||||
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
|
import useElementOutline from '@/views/_element/hooks/useElementOutline'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'image-ellipse-outline',
|
name: 'image-ellipse-outline',
|
@ -24,7 +24,7 @@
|
|||||||
import { PropType, defineComponent, toRef } from 'vue'
|
import { PropType, defineComponent, toRef } from 'vue'
|
||||||
import { PPTElementOutline } from '@/types/slides'
|
import { PPTElementOutline } from '@/types/slides'
|
||||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||||
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
|
import useElementOutline from '@/views/_element/hooks/useElementOutline'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'image-polygon-outline',
|
name: 'image-polygon-outline',
|
@ -27,7 +27,7 @@
|
|||||||
import { PropType, defineComponent, toRef } from 'vue'
|
import { PropType, defineComponent, toRef } from 'vue'
|
||||||
import { PPTElementOutline } from '@/types/slides'
|
import { PPTElementOutline } from '@/types/slides'
|
||||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||||
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
|
import useElementOutline from '@/views/_element/hooks/useElementOutline'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'image-rect-outline',
|
name: 'image-rect-outline',
|
@ -11,22 +11,8 @@
|
|||||||
}"
|
}"
|
||||||
@mousedown="$event => handleSelectElement($event)"
|
@mousedown="$event => handleSelectElement($event)"
|
||||||
>
|
>
|
||||||
<ImageClip
|
|
||||||
v-if="isCliping"
|
|
||||||
:src="elementInfo.src"
|
|
||||||
:clipData="elementInfo.clip"
|
|
||||||
:canvasScale="canvasScale"
|
|
||||||
:width="elementInfo.width"
|
|
||||||
:height="elementInfo.height"
|
|
||||||
:top="elementInfo.top"
|
|
||||||
:left="elementInfo.left"
|
|
||||||
:clipPath="clipShape.style"
|
|
||||||
@clip="range => clip(range)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="element-content"
|
class="element-content"
|
||||||
v-if="!isCliping"
|
|
||||||
v-contextmenu="contextmenus"
|
v-contextmenu="contextmenus"
|
||||||
:style="{
|
:style="{
|
||||||
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
|
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
|
||||||
@ -69,74 +55,25 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
class="operate"
|
|
||||||
:class="{
|
|
||||||
'selected': isSelected,
|
|
||||||
'multi-select': isMultiSelect && isSelected,
|
|
||||||
'active': isActive,
|
|
||||||
}"
|
|
||||||
:style="{ transform: `scale(${1 / canvasScale})` }"
|
|
||||||
v-if="!isCliping"
|
|
||||||
>
|
|
||||||
<BorderLine
|
|
||||||
class="operate-border-line"
|
|
||||||
v-for="line in borderLines"
|
|
||||||
:key="line.type"
|
|
||||||
:type="line.type"
|
|
||||||
:style="line.style"
|
|
||||||
/>
|
|
||||||
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
|
|
||||||
<ResizeHandler
|
|
||||||
class="operate-resize-handler"
|
|
||||||
v-for="point in resizeHandlers"
|
|
||||||
:key="point.direction"
|
|
||||||
:type="point.direction"
|
|
||||||
:style="point.style"
|
|
||||||
@mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
|
|
||||||
/>
|
|
||||||
<RotateHandler
|
|
||||||
class="operate-rotate-handler"
|
|
||||||
:style="{left: scaleWidth / 2 + 'px'}"
|
|
||||||
@mousedown.stop="rotateElement(elementInfo)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, ref, PropType } from 'vue'
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
|
|
||||||
import { PPTImageElement } from '@/types/slides'
|
import { PPTImageElement } from '@/types/slides'
|
||||||
import { OperateResizeHandler } from '@/types/edit'
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
|
|
||||||
|
|
||||||
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
|
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
|
||||||
|
import useElementShadow from '@/views/_element/hooks/useElementShadow'
|
||||||
|
|
||||||
import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
|
|
||||||
import ResizeHandler from '@/views/_common/_operate/ResizeHandler.vue'
|
|
||||||
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
|
|
||||||
import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
|
|
||||||
|
|
||||||
import ImageClip, { ClipedEmitData } from './ImageClipHandler.vue'
|
|
||||||
import ImageRectOutline from './ImageRectOutline.vue'
|
import ImageRectOutline from './ImageRectOutline.vue'
|
||||||
import ImageEllipseOutline from './ImageEllipseOutline.vue'
|
import ImageEllipseOutline from './ImageEllipseOutline.vue'
|
||||||
import ImagePolygonOutline from './ImagePolygonOutline.vue'
|
import ImagePolygonOutline from './ImagePolygonOutline.vue'
|
||||||
|
|
||||||
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'editable-element-image',
|
name: 'editable-element-image',
|
||||||
components: {
|
components: {
|
||||||
RotateHandler,
|
|
||||||
ResizeHandler,
|
|
||||||
BorderLine,
|
|
||||||
AnimationIndex,
|
|
||||||
ImageClip,
|
|
||||||
ImageRectOutline,
|
ImageRectOutline,
|
||||||
ImageEllipseOutline,
|
ImageEllipseOutline,
|
||||||
ImagePolygonOutline,
|
ImagePolygonOutline,
|
||||||
@ -146,55 +83,29 @@ export default defineComponent({
|
|||||||
type: Object as PropType<PPTImageElement>,
|
type: Object as PropType<PPTImageElement>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
canvasScale: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isSelected: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isActiveGroupElement: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isMultiSelect: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
animationIndex: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
selectElement: {
|
selectElement: {
|
||||||
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, canMove?: boolean) => void>,
|
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, canMove?: boolean) => void>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
rotateElement: {
|
|
||||||
type: Function as PropType<(element: PPTImageElement) => void>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
scaleElement: {
|
|
||||||
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: OperateResizeHandler) => void>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
contextmenus: {
|
contextmenus: {
|
||||||
type: Function,
|
type: Function as PropType<() => ContextmenuItem[]>,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const clipingImageElId = ref('')
|
const shadow = computed(() => props.elementInfo.shadow)
|
||||||
|
const { shadowStyle } = useElementShadow(shadow)
|
||||||
|
|
||||||
const scaleWidth = computed(() => props.elementInfo.width * props.canvasScale)
|
const handleSelectElement = (e: MouseEvent) => {
|
||||||
const scaleHeight = computed(() => props.elementInfo.height * props.canvasScale)
|
if(props.elementInfo.lock) return
|
||||||
|
e.stopPropagation()
|
||||||
|
props.selectElement(e, props.elementInfo)
|
||||||
|
}
|
||||||
|
const clipShape = computed(() => {
|
||||||
|
if(!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
|
||||||
|
const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
|
||||||
|
|
||||||
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
|
return CLIPPATHS[shape]
|
||||||
|
})
|
||||||
const isCliping = computed(() => clipingImageElId.value === props.elementInfo.id)
|
|
||||||
|
|
||||||
const imgPosition = computed(() => {
|
const imgPosition = computed(() => {
|
||||||
if(!props.elementInfo || !props.elementInfo.clip) {
|
if(!props.elementInfo || !props.elementInfo.clip) {
|
||||||
@ -221,13 +132,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const clipShape = computed(() => {
|
|
||||||
if(!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
|
|
||||||
const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
|
|
||||||
|
|
||||||
return CLIPPATHS[shape]
|
|
||||||
})
|
|
||||||
|
|
||||||
const filter = computed(() => {
|
const filter = computed(() => {
|
||||||
if(!props.elementInfo.filters) return ''
|
if(!props.elementInfo.filters) return ''
|
||||||
let filter = ''
|
let filter = ''
|
||||||
@ -246,45 +150,13 @@ export default defineComponent({
|
|||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const shadow = computed(() => props.elementInfo.shadow)
|
|
||||||
const { shadowStyle } = useElementShadow(shadow)
|
|
||||||
|
|
||||||
const handleSelectElement = (e: MouseEvent) => {
|
|
||||||
if(isCliping.value || props.elementInfo.lock) return
|
|
||||||
e.stopPropagation()
|
|
||||||
props.selectElement(e, props.elementInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clip = (data: ClipedEmitData) => {
|
|
||||||
clipingImageElId.value = ''
|
|
||||||
|
|
||||||
if(!data) return
|
|
||||||
|
|
||||||
const { range, position } = data
|
|
||||||
const originClip = props.elementInfo.clip || {}
|
|
||||||
|
|
||||||
const _props = {
|
|
||||||
clip: { ...originClip, range },
|
|
||||||
left: props.elementInfo.left + position.left,
|
|
||||||
top: props.elementInfo.top + position.top,
|
|
||||||
width: props.elementInfo.width + position.width,
|
|
||||||
height: props.elementInfo.height + position.height,
|
|
||||||
}
|
|
||||||
console.log(_props)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scaleWidth,
|
|
||||||
isCliping,
|
|
||||||
imgPosition,
|
|
||||||
clipShape,
|
|
||||||
resizeHandlers,
|
|
||||||
borderLines,
|
|
||||||
filter,
|
|
||||||
flip,
|
|
||||||
shadowStyle,
|
shadowStyle,
|
||||||
handleSelectElement,
|
handleSelectElement,
|
||||||
clip,
|
clipShape,
|
||||||
|
imgPosition,
|
||||||
|
filter,
|
||||||
|
flip,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -311,35 +183,8 @@ export default defineComponent({
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.operate {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 100;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
.operate-border-line,
|
|
||||||
.operate-resize-handler,
|
|
||||||
.operate-rotate-handler {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.multi-select:not(.selected) .operate-border-line {
|
|
||||||
border-color: rgba($color: $themeColor, $alpha: .3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.operate-border-line,
|
|
||||||
.operate-resize-handler,
|
|
||||||
.operate-rotate-handler {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
@ -28,9 +28,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType, computed } from 'vue'
|
import { defineComponent, PropType, computed } from 'vue'
|
||||||
import { PPTTextElement } from '@/types/slides'
|
import { PPTTextElement } from '@/types/slides'
|
||||||
import ElementOutline from '@/views/_common/_element/ElementOutline.vue'
|
import ElementOutline from '@/views/_element/ElementOutline.vue'
|
||||||
|
|
||||||
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
|
import useElementShadow from '@/views/_element/hooks/useElementShadow'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'base-element-text',
|
name: 'base-element-text',
|
111
src/views/_element/TextElement/index.vue
Normal file
111
src/views/_element/TextElement/index.vue
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="editable-element-text"
|
||||||
|
:class="{ 'lock': elementInfo.lock }"
|
||||||
|
:style="{
|
||||||
|
top: elementInfo.top + 'px',
|
||||||
|
left: elementInfo.left + 'px',
|
||||||
|
width: elementInfo.width + 'px',
|
||||||
|
transform: `rotate(${elementInfo.rotate}deg)`,
|
||||||
|
}"
|
||||||
|
@mousedown="$event => handleSelectElement($event)"
|
||||||
|
>
|
||||||
|
<div class="element-content"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: elementInfo.fill,
|
||||||
|
opacity: elementInfo.opacity,
|
||||||
|
textShadow: shadowStyle,
|
||||||
|
}"
|
||||||
|
v-contextmenu="contextmenus"
|
||||||
|
>
|
||||||
|
<ElementOutline
|
||||||
|
:width="elementInfo.width"
|
||||||
|
:height="elementInfo.height"
|
||||||
|
:outline="elementInfo.outline"
|
||||||
|
/>
|
||||||
|
<div class="text"
|
||||||
|
v-html="elementInfo.content"
|
||||||
|
:contenteditable="!elementInfo.lock"
|
||||||
|
@mousedown="$event => handleSelectElement($event, false)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
|
import { PPTTextElement } from '@/types/slides'
|
||||||
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
|
import useElementShadow from '@/views/_element/hooks/useElementShadow'
|
||||||
|
|
||||||
|
import ElementOutline from '@/views/_element/ElementOutline.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'editable-element-text',
|
||||||
|
components: {
|
||||||
|
ElementOutline,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
elementInfo: {
|
||||||
|
type: Object as PropType<PPTTextElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selectElement: {
|
||||||
|
type: Function as PropType<(e: MouseEvent, element: PPTTextElement, canMove?: boolean) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
contextmenus: {
|
||||||
|
type: Function as PropType<() => ContextmenuItem[]>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const handleSelectElement = (e: MouseEvent, canMove = true) => {
|
||||||
|
if(props.elementInfo.lock) return
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
props.selectElement(e, props.elementInfo, canMove)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shadow = computed(() => props.elementInfo.shadow)
|
||||||
|
const { shadowStyle } = useElementShadow(shadow)
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleSelectElement,
|
||||||
|
shadowStyle,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.editable-element-text {
|
||||||
|
position: absolute;
|
||||||
|
cursor: move;
|
||||||
|
|
||||||
|
&.lock .element-content {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-content {
|
||||||
|
position: relative;
|
||||||
|
padding: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
position: relative;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.text) {
|
||||||
|
word-break: break-word;
|
||||||
|
font-family: '微软雅黑';
|
||||||
|
outline: 0;
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: rgba(27, 110, 232, 0.3);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
x
Reference in New Issue
Block a user