mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
添加线条元素
This commit is contained in:
parent
8fe7b266f3
commit
8fb6e53342
@ -124,7 +124,7 @@ export default () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const createLineElement = (position: LineElementPosition, points: [string, string], lineType: string) => {
|
const createLineElement = (position: LineElementPosition, points: [string, string]) => {
|
||||||
const { left, top, start, end } = position
|
const { left, top, start, end } = position
|
||||||
createElement({
|
createElement({
|
||||||
...DEFAULT_LINE,
|
...DEFAULT_LINE,
|
||||||
@ -135,7 +135,6 @@ export default () => {
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
points,
|
points,
|
||||||
lineType,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +88,18 @@ export const slides: Slide[] = [
|
|||||||
lock: false,
|
lock: false,
|
||||||
content: '<div>😀 😐 😶 😜 🔔 ⭐ ⚡ 🔥 👍 💡 🔰 🎀 🎁 🥇 🏅 🏆 🎈 🎉 💎 🚧 ⛔ 📢 ⌛ ⏰ 🕒 🧩 🎵 📎 🔒 🔑 ⛳ 📌 📍 💬 📅 📈 📋 📜 📁 📱 💻 💾 🌏 🚚 🚡 🚢💧 🌐 🧭 💰 💳 🛒</div>',
|
content: '<div>😀 😐 😶 😜 🔔 ⭐ ⚡ 🔥 👍 💡 🔰 🎀 🎁 🥇 🏅 🏆 🎈 🎉 💎 🚧 ⛔ 📢 ⌛ ⏰ 🕒 🧩 🎵 📎 🔒 🔑 ⛳ 📌 📍 💬 📅 📈 📋 📜 📁 📱 💻 💾 🌏 🚚 🚡 🚢💧 🌐 🧭 💰 💳 🛒</div>',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'xsfdas',
|
||||||
|
type: 'line',
|
||||||
|
width: 2,
|
||||||
|
left: 100,
|
||||||
|
top: 400,
|
||||||
|
end: [0, 0],
|
||||||
|
start: [300, 120],
|
||||||
|
style: 'solid',
|
||||||
|
color: '#888',
|
||||||
|
points: ['', 'arrow'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'xxx7',
|
id: 'xxx7',
|
||||||
type: 'shape',
|
type: 'shape',
|
||||||
|
@ -27,7 +27,7 @@ export enum OperateBorderLines {
|
|||||||
R = 'right',
|
R = 'right',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OperateResizeHandler = 'left-top' | 'top' | 'right-top' | 'left' | 'right' | 'left-bottom' | 'bottom' | 'right-bottom'
|
export type OperateResizeHandler = '' | 'left-top' | 'top' | 'right-top' | 'left' | 'right' | 'left-bottom' | 'bottom' | 'right-bottom'
|
||||||
|
|
||||||
export enum OperateResizeHandlers {
|
export enum OperateResizeHandlers {
|
||||||
LEFT_TOP = 'left-top',
|
LEFT_TOP = 'left-top',
|
||||||
@ -40,6 +40,13 @@ export enum OperateResizeHandlers {
|
|||||||
RIGHT_BOTTOM = 'right-bottom',
|
RIGHT_BOTTOM = 'right-bottom',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OperateLineHandler = 'start' | 'end'
|
||||||
|
|
||||||
|
export enum OperateLineHandlers {
|
||||||
|
START = 'start',
|
||||||
|
END = 'end,'
|
||||||
|
}
|
||||||
|
|
||||||
export interface AlignmentLineAxis {
|
export interface AlignmentLineAxis {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
@ -14,22 +14,19 @@ export enum ElementTypes {
|
|||||||
TABLE = 'table',
|
TABLE = 'table',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PPTElementBaseProps {
|
|
||||||
id: string;
|
|
||||||
left: number;
|
|
||||||
top: number;
|
|
||||||
lock?: boolean;
|
|
||||||
groupId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PPTElementOutline {
|
export interface PPTElementOutline {
|
||||||
style?: 'dashed' | 'solid';
|
style?: 'dashed' | 'solid';
|
||||||
width?: number;
|
width?: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PPTTextElement extends PPTElementBaseProps {
|
export interface PPTTextElement {
|
||||||
type: 'text';
|
type: 'text';
|
||||||
|
id: string;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
lock?: boolean;
|
||||||
|
groupId?: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
content: string;
|
content: string;
|
||||||
@ -49,8 +46,13 @@ export interface ImageElementFilters {
|
|||||||
'hue-rotate': string;
|
'hue-rotate': string;
|
||||||
'opacity': string;
|
'opacity': string;
|
||||||
}
|
}
|
||||||
export interface PPTImageElement extends PPTElementBaseProps {
|
export interface PPTImageElement {
|
||||||
type: 'image';
|
type: 'image';
|
||||||
|
id: string;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
lock?: boolean;
|
||||||
|
groupId?: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
fixedRatio: boolean;
|
fixedRatio: boolean;
|
||||||
@ -66,8 +68,13 @@ export interface PPTImageElement extends PPTElementBaseProps {
|
|||||||
shadow?: PPTElementShadow;
|
shadow?: PPTElementShadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PPTShapeElement extends PPTElementBaseProps {
|
export interface PPTShapeElement {
|
||||||
type: 'shape';
|
type: 'shape';
|
||||||
|
id: string;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
lock?: boolean;
|
||||||
|
groupId?: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
viewBox: number;
|
viewBox: number;
|
||||||
@ -80,19 +87,29 @@ export interface PPTShapeElement extends PPTElementBaseProps {
|
|||||||
shadow?: PPTElementShadow;
|
shadow?: PPTElementShadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PPTLineElement extends PPTElementBaseProps {
|
export interface PPTLineElement {
|
||||||
type: 'line';
|
type: 'line';
|
||||||
|
id: string;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
lock?: boolean;
|
||||||
|
groupId?: string;
|
||||||
start: [number, number];
|
start: [number, number];
|
||||||
end: [number, number];
|
end: [number, number];
|
||||||
width: number;
|
width: number;
|
||||||
style: string;
|
style: string;
|
||||||
color: string;
|
color: string;
|
||||||
points: [string, string];
|
points: [string, string];
|
||||||
lineType: string;
|
shadow?: PPTElementShadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PPTChartElement extends PPTElementBaseProps {
|
export interface PPTChartElement {
|
||||||
type: 'chart';
|
type: 'chart';
|
||||||
|
id: string;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
lock?: boolean;
|
||||||
|
groupId?: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
chartType: string;
|
chartType: string;
|
||||||
@ -107,8 +124,13 @@ export interface TableElementCell {
|
|||||||
content: string;
|
content: string;
|
||||||
bgColor: string;
|
bgColor: string;
|
||||||
}
|
}
|
||||||
export interface PPTTableElement extends PPTElementBaseProps {
|
export interface PPTTableElement {
|
||||||
type: 'table';
|
type: 'table';
|
||||||
|
id: string;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
lock?: boolean;
|
||||||
|
groupId?: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
borderTheme?: string;
|
borderTheme?: string;
|
||||||
|
@ -31,6 +31,7 @@ import { ElementOrderCommands, ElementAlignCommands } from '@/types/edit'
|
|||||||
import ImageElement from '@/views/components/element/ImageElement/index.vue'
|
import ImageElement from '@/views/components/element/ImageElement/index.vue'
|
||||||
import TextElement from '@/views/components/element/TextElement/index.vue'
|
import TextElement from '@/views/components/element/TextElement/index.vue'
|
||||||
import ShapeElement from '@/views/components/element/ShapeElement/index.vue'
|
import ShapeElement from '@/views/components/element/ShapeElement/index.vue'
|
||||||
|
import LineElement from '@/views/components/element/LineElement/index.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'editable-element',
|
name: 'editable-element',
|
||||||
@ -58,6 +59,7 @@ export default defineComponent({
|
|||||||
'image': ImageElement,
|
'image': ImageElement,
|
||||||
'text': TextElement,
|
'text': TextElement,
|
||||||
'shape': ShapeElement,
|
'shape': ShapeElement,
|
||||||
|
'line': LineElement,
|
||||||
}
|
}
|
||||||
return elementTypeMap[props.elementInfo.type] || null
|
return elementTypeMap[props.elementInfo.type] || null
|
||||||
})
|
})
|
||||||
|
77
src/views/Editor/Canvas/Operate/LineElementOperate.vue
Normal file
77
src/views/Editor/Canvas/Operate/LineElementOperate.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-element-operate">
|
||||||
|
<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 => dragLineElement($event, elementInfo, point.handler)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { State } from '@/store'
|
||||||
|
|
||||||
|
import { PPTLineElement } from '@/types/slides'
|
||||||
|
import { OperateLineHandler, OperateLineHandlers } from '@/types/edit'
|
||||||
|
|
||||||
|
import ResizeHandler from './ResizeHandler.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'text-element-operate',
|
||||||
|
components: {
|
||||||
|
ResizeHandler,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
elementInfo: {
|
||||||
|
type: Object as PropType<PPTLineElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isActiveGroupElement: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isMultiSelect: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
dragLineElement: {
|
||||||
|
type: Function as PropType<(e: MouseEvent, element: PPTLineElement, command: OperateLineHandler) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore<State>()
|
||||||
|
const canvasScale = computed(() => store.state.canvasScale)
|
||||||
|
|
||||||
|
const resizeHandlers = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
handler: OperateLineHandlers.START,
|
||||||
|
style: {
|
||||||
|
left: props.elementInfo.start[0] * canvasScale.value + 'px',
|
||||||
|
top: props.elementInfo.start[1] * canvasScale.value + 'px',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handler: OperateLineHandlers.END,
|
||||||
|
style: {
|
||||||
|
left: props.elementInfo.end[0] * canvasScale.value + 'px',
|
||||||
|
top: props.elementInfo.end[1] * canvasScale.value + 'px',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
resizeHandlers,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
@ -11,7 +11,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
type: {
|
type: {
|
||||||
type: String as PropType<OperateResizeHandler>,
|
type: String as PropType<OperateResizeHandler>,
|
||||||
required: true,
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
:isMultiSelect="isMultiSelect"
|
:isMultiSelect="isMultiSelect"
|
||||||
:rotateElement="rotateElement"
|
:rotateElement="rotateElement"
|
||||||
:scaleElement="scaleElement"
|
:scaleElement="scaleElement"
|
||||||
|
:dragLineElement="dragLineElement"
|
||||||
></component>
|
></component>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -33,11 +34,12 @@ import { defineComponent, PropType, computed, Ref } from 'vue'
|
|||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { State } from '@/store'
|
import { State } from '@/store'
|
||||||
import { PPTElement, Slide } from '@/types/slides'
|
import { PPTElement, Slide } from '@/types/slides'
|
||||||
import { OperateResizeHandler } from '@/types/edit'
|
import { OperateLineHandler, OperateResizeHandler } from '@/types/edit'
|
||||||
|
|
||||||
import ImageElementOperate from './ImageElementOperate.vue'
|
import ImageElementOperate from './ImageElementOperate.vue'
|
||||||
import TextElementOperate from './TextElementOperate.vue'
|
import TextElementOperate from './TextElementOperate.vue'
|
||||||
import ShapeElementOperate from './ShapeElementOperate.vue'
|
import ShapeElementOperate from './ShapeElementOperate.vue'
|
||||||
|
import LineElementOperate from './LineElementOperate.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'operate',
|
name: 'operate',
|
||||||
@ -70,6 +72,10 @@ export default defineComponent({
|
|||||||
type: Function as PropType<(e: MouseEvent, element: PPTElement, command: OperateResizeHandler) => void>,
|
type: Function as PropType<(e: MouseEvent, element: PPTElement, command: OperateResizeHandler) => void>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
dragLineElement: {
|
||||||
|
type: Function as PropType<(e: MouseEvent, element: PPTElement, command: OperateLineHandler) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const store = useStore<State>()
|
const store = useStore<State>()
|
||||||
@ -82,6 +88,7 @@ export default defineComponent({
|
|||||||
'image': ImageElementOperate,
|
'image': ImageElementOperate,
|
||||||
'text': TextElementOperate,
|
'text': TextElementOperate,
|
||||||
'shape': ShapeElementOperate,
|
'shape': ShapeElementOperate,
|
||||||
|
'line': LineElementOperate,
|
||||||
}
|
}
|
||||||
return elementTypeMap[props.elementInfo.type] || null
|
return elementTypeMap[props.elementInfo.type] || null
|
||||||
})
|
})
|
||||||
|
169
src/views/Editor/Canvas/hooks/useDragLineElement.ts
Normal file
169
src/views/Editor/Canvas/hooks/useDragLineElement.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { Ref, computed } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { State, MutationTypes } from '@/store'
|
||||||
|
import { PPTElement, PPTLineElement } from '@/types/slides'
|
||||||
|
import { OperateLineHandler, OperateLineHandlers } from '@/types/edit'
|
||||||
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
|
|
||||||
|
interface AdsorptionPoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (elementList: Ref<PPTElement[]>) => {
|
||||||
|
const store = useStore<State>()
|
||||||
|
const canvasScale = computed(() => store.state.canvasScale)
|
||||||
|
|
||||||
|
const { addHistorySnapshot } = useHistorySnapshot()
|
||||||
|
|
||||||
|
const dragLineElement = (e: MouseEvent, element: PPTLineElement, command: OperateLineHandler) => {
|
||||||
|
let isMouseDown = true
|
||||||
|
|
||||||
|
const sorptionRange = 10
|
||||||
|
|
||||||
|
const startPageX = e.pageX
|
||||||
|
const startPageY = e.pageY
|
||||||
|
|
||||||
|
const adsorptionPoints: AdsorptionPoint[] = []
|
||||||
|
|
||||||
|
// 获取全部非线条且未旋转元素的8个点作为吸附点
|
||||||
|
for(let i = 0; i < elementList.value.length; i++) {
|
||||||
|
const _element = elementList.value[i]
|
||||||
|
if(_element.type === 'line' || ('rotate' in _element && _element.rotate)) continue
|
||||||
|
|
||||||
|
const left = _element.left
|
||||||
|
const top = _element.top
|
||||||
|
const width = _element.width
|
||||||
|
const height = _element.height
|
||||||
|
|
||||||
|
const right = left + width
|
||||||
|
const bottom = top + height
|
||||||
|
const centerX = top + height / 2
|
||||||
|
const centerY = left + width / 2
|
||||||
|
|
||||||
|
const topPoint = { x: centerY, y: top }
|
||||||
|
const bottomPoint = { x: centerY, y: bottom }
|
||||||
|
const leftPoint = { x: left, y: centerX }
|
||||||
|
const rightPoint = { x: right, y: centerX }
|
||||||
|
|
||||||
|
const leftTopPoint = { x: left, y: top }
|
||||||
|
const rightTopPoint = { x: right, y: top }
|
||||||
|
const leftBottomPoint = { x: left, y: bottom }
|
||||||
|
const rightBottomPoint = { x: right, y: bottom }
|
||||||
|
|
||||||
|
adsorptionPoints.push(
|
||||||
|
topPoint,
|
||||||
|
bottomPoint,
|
||||||
|
leftPoint,
|
||||||
|
rightPoint,
|
||||||
|
leftTopPoint,
|
||||||
|
rightTopPoint,
|
||||||
|
leftBottomPoint,
|
||||||
|
rightBottomPoint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.onmousemove = e => {
|
||||||
|
if(!isMouseDown) return
|
||||||
|
|
||||||
|
const currentPageX = e.pageX
|
||||||
|
const currentPageY = e.pageY
|
||||||
|
|
||||||
|
// 鼠标按下后移动的距离
|
||||||
|
const moveX = (currentPageX - startPageX) / canvasScale.value
|
||||||
|
const moveY = (currentPageY - startPageY) / canvasScale.value
|
||||||
|
|
||||||
|
// 线条两个端点(起点和终点)基于编辑区域的位置
|
||||||
|
let startX = element.left + element.start[0]
|
||||||
|
let startY = element.top + element.start[1]
|
||||||
|
let endX = element.left + element.end[0]
|
||||||
|
let endY = element.top + element.end[1]
|
||||||
|
|
||||||
|
// 根据拖拽的点,选择修改起点或终点的位置
|
||||||
|
// 两点在水平和垂直方向上有对齐吸附
|
||||||
|
// 靠近其他元素的吸附点有对齐吸附
|
||||||
|
if(command === OperateLineHandlers.START) {
|
||||||
|
startX = startX + moveX
|
||||||
|
startY = startY + moveY
|
||||||
|
|
||||||
|
if(Math.abs(startX - endX) < sorptionRange) startX = endX
|
||||||
|
if(Math.abs(startY - endY) < sorptionRange) startY = endY
|
||||||
|
|
||||||
|
for(const adsorptionPoint of adsorptionPoints) {
|
||||||
|
const { x, y } = adsorptionPoint
|
||||||
|
if(Math.abs(x - startX) < sorptionRange && Math.abs(y - startY) < sorptionRange) {
|
||||||
|
startX = x
|
||||||
|
startY = y
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
endX = endX + moveX
|
||||||
|
endY = endY + moveY
|
||||||
|
|
||||||
|
if(Math.abs(startX - endX) < sorptionRange) endX = startX
|
||||||
|
if(Math.abs(startY - endY) < sorptionRange) endY = startY
|
||||||
|
|
||||||
|
for(const adsorptionPoint of adsorptionPoints) {
|
||||||
|
const { x, y } = adsorptionPoint
|
||||||
|
if(Math.abs(x - endX) < sorptionRange && Math.abs(y - endY) < sorptionRange) {
|
||||||
|
endX = x
|
||||||
|
endY = y
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算两个端点基于自身元素位置的坐标
|
||||||
|
const minX = Math.min(startX, endX)
|
||||||
|
const minY = Math.min(startY, endY)
|
||||||
|
const maxX = Math.max(startX, endX)
|
||||||
|
const maxY = Math.max(startY, endY)
|
||||||
|
|
||||||
|
const start: [number, number] = [0, 0]
|
||||||
|
const end: [number, number] = [maxX - minX, maxY - minY]
|
||||||
|
if(startX > endX) {
|
||||||
|
start[0] = maxX - minX
|
||||||
|
end[0] = 0
|
||||||
|
}
|
||||||
|
if(startY > endY) {
|
||||||
|
start[1] = maxY - minY
|
||||||
|
end[1] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改线条的位置和两点的坐标
|
||||||
|
elementList.value = elementList.value.map(el => {
|
||||||
|
if(el.id === element.id) {
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
left: minX,
|
||||||
|
top: minY,
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return el
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
document.onmouseup = e => {
|
||||||
|
isMouseDown = false
|
||||||
|
document.onmousemove = null
|
||||||
|
document.onmouseup = null
|
||||||
|
|
||||||
|
const currentPageX = e.pageX
|
||||||
|
const currentPageY = e.pageY
|
||||||
|
|
||||||
|
// 对比原始鼠标位置,没有实际的位移不更新数据
|
||||||
|
if(startPageX === currentPageX && startPageY === currentPageY) return
|
||||||
|
|
||||||
|
store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
|
||||||
|
addHistorySnapshot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dragLineElement,
|
||||||
|
}
|
||||||
|
}
|
@ -39,6 +39,7 @@
|
|||||||
:isMultiSelect="activeElementIdList.length > 1"
|
:isMultiSelect="activeElementIdList.length > 1"
|
||||||
:rotateElement="rotateElement"
|
:rotateElement="rotateElement"
|
||||||
:scaleElement="scaleElement"
|
:scaleElement="scaleElement"
|
||||||
|
:dragLineElement="dragLineElement"
|
||||||
/>
|
/>
|
||||||
<SlideBackground />
|
<SlideBackground />
|
||||||
</div>
|
</div>
|
||||||
@ -85,6 +86,7 @@ import useRotateElement from './hooks/useRotateElement'
|
|||||||
import useScaleElement from './hooks/useScaleElement'
|
import useScaleElement from './hooks/useScaleElement'
|
||||||
import useSelectElement from './hooks/useSelectElement'
|
import useSelectElement from './hooks/useSelectElement'
|
||||||
import useDragElement from './hooks/useDragElement'
|
import useDragElement from './hooks/useDragElement'
|
||||||
|
import useDragLineElement from './hooks/useDragLineElement'
|
||||||
|
|
||||||
import useDeleteElement from '@/hooks/useDeleteElement'
|
import useDeleteElement from '@/hooks/useDeleteElement'
|
||||||
import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
|
import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
|
||||||
@ -140,6 +142,7 @@ export default defineComponent({
|
|||||||
const { mouseSelectionState, updateMouseSelection } = useMouseSelection(elementList, viewportRef)
|
const { mouseSelectionState, updateMouseSelection } = useMouseSelection(elementList, viewportRef)
|
||||||
|
|
||||||
const { dragElement } = useDragElement(elementList, activeGroupElementId, alignmentLines)
|
const { dragElement } = useDragElement(elementList, activeGroupElementId, alignmentLines)
|
||||||
|
const { dragLineElement } = useDragLineElement(elementList)
|
||||||
const { selectElement } = useSelectElement(elementList, activeGroupElementId, dragElement)
|
const { selectElement } = useSelectElement(elementList, activeGroupElementId, dragElement)
|
||||||
const { scaleElement, scaleMultiElement } = useScaleElement(elementList, activeGroupElementId, alignmentLines)
|
const { scaleElement, scaleMultiElement } = useScaleElement(elementList, activeGroupElementId, alignmentLines)
|
||||||
const { rotateElement } = useRotateElement(elementList, viewportRef)
|
const { rotateElement } = useRotateElement(elementList, viewportRef)
|
||||||
@ -218,6 +221,7 @@ export default defineComponent({
|
|||||||
selectElement,
|
selectElement,
|
||||||
rotateElement,
|
rotateElement,
|
||||||
scaleElement,
|
scaleElement,
|
||||||
|
dragLineElement,
|
||||||
scaleMultiElement,
|
scaleMultiElement,
|
||||||
mousewheelScaleCanvas,
|
mousewheelScaleCanvas,
|
||||||
contextmenus,
|
contextmenus,
|
||||||
|
@ -21,6 +21,8 @@ import { PPTElement, Slide } from '@/types/slides'
|
|||||||
|
|
||||||
import BaseImageElement from '@/views/components/element/ImageElement/BaseImageElement.vue'
|
import BaseImageElement from '@/views/components/element/ImageElement/BaseImageElement.vue'
|
||||||
import BaseTextElement from '@/views/components/element/TextElement/BaseTextElement.vue'
|
import BaseTextElement from '@/views/components/element/TextElement/BaseTextElement.vue'
|
||||||
|
import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeElement.vue'
|
||||||
|
import BaseLineElement from '@/views/components/element/LineElement/BaseLineElement.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'screen-element',
|
name: 'screen-element',
|
||||||
@ -43,6 +45,8 @@ export default defineComponent({
|
|||||||
const elementTypeMap = {
|
const elementTypeMap = {
|
||||||
'image': BaseImageElement,
|
'image': BaseImageElement,
|
||||||
'text': BaseTextElement,
|
'text': BaseTextElement,
|
||||||
|
'shape': BaseShapeElement,
|
||||||
|
'line': BaseLineElement,
|
||||||
}
|
}
|
||||||
return elementTypeMap[props.elementInfo.type] || null
|
return elementTypeMap[props.elementInfo.type] || null
|
||||||
})
|
})
|
||||||
|
@ -17,6 +17,7 @@ import { PPTElement } from '@/types/slides'
|
|||||||
import BaseImageElement from '@/views/components/element/ImageElement/BaseImageElement.vue'
|
import BaseImageElement from '@/views/components/element/ImageElement/BaseImageElement.vue'
|
||||||
import BaseTextElement from '@/views/components/element/TextElement/BaseTextElement.vue'
|
import BaseTextElement from '@/views/components/element/TextElement/BaseTextElement.vue'
|
||||||
import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeElement.vue'
|
import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeElement.vue'
|
||||||
|
import BaseLineElement from '@/views/components/element/LineElement/BaseLineElement.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'base-element',
|
name: 'base-element',
|
||||||
@ -36,6 +37,7 @@ export default defineComponent({
|
|||||||
'image': BaseImageElement,
|
'image': BaseImageElement,
|
||||||
'text': BaseTextElement,
|
'text': BaseTextElement,
|
||||||
'shape': BaseShapeElement,
|
'shape': BaseShapeElement,
|
||||||
|
'line': BaseLineElement,
|
||||||
}
|
}
|
||||||
return elementTypeMap[props.elementInfo.type] || null
|
return elementTypeMap[props.elementInfo.type] || null
|
||||||
})
|
})
|
||||||
|
118
src/views/components/element/LineElement/BaseLineElement.vue
Normal file
118
src/views/components/element/LineElement/BaseLineElement.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="editable-element-shape"
|
||||||
|
:style="{
|
||||||
|
top: elementInfo.top + 'px',
|
||||||
|
left: elementInfo.left + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="element-content"
|
||||||
|
:style="{ filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '' }"
|
||||||
|
>
|
||||||
|
<SvgWrapper
|
||||||
|
overflow="visible"
|
||||||
|
:width="svgWidth"
|
||||||
|
:height="svgHeight"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<LinePointMarker
|
||||||
|
v-if="elementInfo.points[0]"
|
||||||
|
:id="elementInfo.id"
|
||||||
|
position="start"
|
||||||
|
:type="elementInfo.points[0]"
|
||||||
|
:color="elementInfo.color"
|
||||||
|
:baseSize="elementInfo.width"
|
||||||
|
/>
|
||||||
|
<LinePointMarker
|
||||||
|
v-if="elementInfo.points[1]"
|
||||||
|
:id="elementInfo.id"
|
||||||
|
position="end"
|
||||||
|
:type="elementInfo.points[1]"
|
||||||
|
:color="elementInfo.color"
|
||||||
|
:baseSize="elementInfo.width"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
:d="path"
|
||||||
|
:stroke="elementInfo.color"
|
||||||
|
:stroke-width="elementInfo.width"
|
||||||
|
:stroke-dasharray="lineDashArray"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap
|
||||||
|
stroke-linejoin
|
||||||
|
stroke-miterlimit
|
||||||
|
:marker-start="elementInfo.points[0] ? `url(#${elementInfo.id}-${elementInfo.points[0]}-start)` : ''"
|
||||||
|
:marker-end="elementInfo.points[1] ? `url(#${elementInfo.id}-${elementInfo.points[1]}-end)` : ''"
|
||||||
|
></path>
|
||||||
|
</SvgWrapper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
|
import { PPTLineElement } from '@/types/slides'
|
||||||
|
import useElementShadow from '@/views/components/element/hooks/useElementShadow'
|
||||||
|
|
||||||
|
import LinePointMarker from './LinePointMarker.vue'
|
||||||
|
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'editable-element-shape',
|
||||||
|
components: {
|
||||||
|
LinePointMarker,
|
||||||
|
SvgWrapper,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
elementInfo: {
|
||||||
|
type: Object as PropType<PPTLineElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const shadow = computed(() => props.elementInfo.shadow)
|
||||||
|
const { shadowStyle } = useElementShadow(shadow)
|
||||||
|
|
||||||
|
const svgWidth = computed(() => {
|
||||||
|
const width = Math.abs(props.elementInfo.start[0] - props.elementInfo.end[0])
|
||||||
|
return width < 24 ? 24 : width
|
||||||
|
})
|
||||||
|
const svgHeight = computed(() => {
|
||||||
|
const height = Math.abs(props.elementInfo.start[1] - props.elementInfo.end[1])
|
||||||
|
return height < 24 ? 24 : height
|
||||||
|
})
|
||||||
|
|
||||||
|
const lineDashArray = computed(() => props.elementInfo.style === 'dashed' ? '10, 5' : '0, 0')
|
||||||
|
const path = computed(() => {
|
||||||
|
const start = props.elementInfo.start.join(',')
|
||||||
|
const end = props.elementInfo.end.join(',')
|
||||||
|
return `M${start} L${end}`
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
shadowStyle,
|
||||||
|
svgWidth,
|
||||||
|
svgHeight,
|
||||||
|
lineDashArray,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.editable-element-shape {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
transform-origin: 0 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
66
src/views/components/element/LineElement/LinePointMarker.vue
Normal file
66
src/views/components/element/LineElement/LinePointMarker.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<marker
|
||||||
|
:id="`${id}-${type}-${position}`"
|
||||||
|
markerUnits="userSpaceOnUse"
|
||||||
|
orient="auto"
|
||||||
|
:markerWidth="size * 3"
|
||||||
|
:markerHeight="size * 3"
|
||||||
|
:refX="size * 1.5"
|
||||||
|
:refY="size * 1.5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
:d="path"
|
||||||
|
:fill="color"
|
||||||
|
:transform="`scale(${size * 0.3}, ${size * 0.3}) rotate(${rotate}, 5, 5)`"
|
||||||
|
></path>
|
||||||
|
</marker>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
|
|
||||||
|
const pathMap = {
|
||||||
|
dot: 'm0 5a5 5 0 1 0 10 0a5 5 0 1 0 -10 0z',
|
||||||
|
arrow: 'M0,0 L10,5 0,10 Z',
|
||||||
|
}
|
||||||
|
const rotateMap = {
|
||||||
|
'arrow-start': 180,
|
||||||
|
'arrow-end': 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'line-point-marker',
|
||||||
|
props: {
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: String as PropType<'start' | 'end'>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String as PropType<'dot' | 'arrow'>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
baseSize: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const path = computed(() => pathMap[props.type])
|
||||||
|
const rotate = computed(() => rotateMap[`${props.type}-${props.position}`] || 0)
|
||||||
|
const size = computed(() => props.baseSize < 2 ? 2 : props.baseSize)
|
||||||
|
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
rotate,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
142
src/views/components/element/LineElement/index.vue
Normal file
142
src/views/components/element/LineElement/index.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<div class="editable-element-shape"
|
||||||
|
:class="{ 'lock': elementInfo.lock }"
|
||||||
|
:style="{
|
||||||
|
top: elementInfo.top + 'px',
|
||||||
|
left: elementInfo.left + 'px',
|
||||||
|
}"
|
||||||
|
@mousedown="$event => handleSelectElement($event)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="element-content"
|
||||||
|
v-contextmenu="contextmenus"
|
||||||
|
:style="{ filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '' }"
|
||||||
|
>
|
||||||
|
<SvgWrapper
|
||||||
|
overflow="visible"
|
||||||
|
:width="svgWidth"
|
||||||
|
:height="svgHeight"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<LinePointMarker
|
||||||
|
v-if="elementInfo.points[0]"
|
||||||
|
:id="elementInfo.id"
|
||||||
|
position="start"
|
||||||
|
:type="elementInfo.points[0]"
|
||||||
|
:color="elementInfo.color"
|
||||||
|
:baseSize="elementInfo.width"
|
||||||
|
/>
|
||||||
|
<LinePointMarker
|
||||||
|
v-if="elementInfo.points[1]"
|
||||||
|
:id="elementInfo.id"
|
||||||
|
position="end"
|
||||||
|
:type="elementInfo.points[1]"
|
||||||
|
:color="elementInfo.color"
|
||||||
|
:baseSize="elementInfo.width"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
:d="path"
|
||||||
|
:stroke="elementInfo.color"
|
||||||
|
:stroke-width="elementInfo.width"
|
||||||
|
:stroke-dasharray="lineDashArray"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap
|
||||||
|
stroke-linejoin
|
||||||
|
stroke-miterlimit
|
||||||
|
:marker-start="elementInfo.points[0] ? `url(#${elementInfo.id}-${elementInfo.points[0]}-start)` : ''"
|
||||||
|
:marker-end="elementInfo.points[1] ? `url(#${elementInfo.id}-${elementInfo.points[1]}-end)` : ''"
|
||||||
|
></path>
|
||||||
|
</SvgWrapper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from 'vue'
|
||||||
|
import { PPTLineElement } from '@/types/slides'
|
||||||
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
|
import useElementShadow from '@/views/components/element/hooks/useElementShadow'
|
||||||
|
|
||||||
|
import LinePointMarker from './LinePointMarker.vue'
|
||||||
|
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'editable-element-shape',
|
||||||
|
components: {
|
||||||
|
LinePointMarker,
|
||||||
|
SvgWrapper,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
elementInfo: {
|
||||||
|
type: Object as PropType<PPTLineElement>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selectElement: {
|
||||||
|
type: Function as PropType<(e: MouseEvent, element: PPTLineElement, canMove?: boolean) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
contextmenus: {
|
||||||
|
type: Function as PropType<() => ContextmenuItem[]>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const handleSelectElement = (e: MouseEvent) => {
|
||||||
|
if(props.elementInfo.lock) return
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
props.selectElement(e, props.elementInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shadow = computed(() => props.elementInfo.shadow)
|
||||||
|
const { shadowStyle } = useElementShadow(shadow)
|
||||||
|
|
||||||
|
const svgWidth = computed(() => {
|
||||||
|
const width = Math.abs(props.elementInfo.start[0] - props.elementInfo.end[0])
|
||||||
|
return width < 24 ? 24 : width
|
||||||
|
})
|
||||||
|
const svgHeight = computed(() => {
|
||||||
|
const height = Math.abs(props.elementInfo.start[1] - props.elementInfo.end[1])
|
||||||
|
return height < 24 ? 24 : height
|
||||||
|
})
|
||||||
|
|
||||||
|
const lineDashArray = computed(() => props.elementInfo.style === 'dashed' ? '10, 5' : '0, 0')
|
||||||
|
const path = computed(() => {
|
||||||
|
const start = props.elementInfo.start.join(',')
|
||||||
|
const end = props.elementInfo.end.join(',')
|
||||||
|
return `M${start} L${end}`
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleSelectElement,
|
||||||
|
shadowStyle,
|
||||||
|
svgWidth,
|
||||||
|
svgHeight,
|
||||||
|
lineDashArray,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.editable-element-shape {
|
||||||
|
position: absolute;
|
||||||
|
cursor: move;
|
||||||
|
|
||||||
|
&.lock .element-content {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
transform-origin: 0 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
x
Reference in New Issue
Block a user