|
|
@@ -0,0 +1,219 @@
|
|
|
+<template>
|
|
|
+ <Moveable
|
|
|
+ :target="elements"
|
|
|
+ :draggable="true"
|
|
|
+ :resizable="true"
|
|
|
+ :rotatable="false"
|
|
|
+ :padding="2"
|
|
|
+ :container="rootContainer"
|
|
|
+ :useMutationObserver="true"
|
|
|
+ :useResizeObserver="true"
|
|
|
+ :throttleDrag="1"
|
|
|
+ :throttleResize="1"
|
|
|
+ :throttleRotate="1"
|
|
|
+ :snappable="true"
|
|
|
+ :snapDirections="{
|
|
|
+ top: true,
|
|
|
+ left: true,
|
|
|
+ bottom: true,
|
|
|
+ right: true,
|
|
|
+ center: true,
|
|
|
+ middle: true
|
|
|
+ }"
|
|
|
+ :elementSnapDirections="{
|
|
|
+ top: true,
|
|
|
+ left: true,
|
|
|
+ bottom: true,
|
|
|
+ right: true,
|
|
|
+ center: true,
|
|
|
+ middle: true
|
|
|
+ }"
|
|
|
+ :maxSnapElementGuidelineDistance="100"
|
|
|
+ :elementGuidelines="getElementGridelines"
|
|
|
+ :verticalGuidelines="verticalGuidelines"
|
|
|
+ :horizontalGuidelines="horizontalGuidelines"
|
|
|
+ :controlPadding="4"
|
|
|
+ @render="onRender"
|
|
|
+ @drag="onDrag"
|
|
|
+ @resize="onResize"
|
|
|
+ @resizeEnd="onResizeEnd"
|
|
|
+ @dragStart="onDragStart"
|
|
|
+ @dragEnd="onDragEnd"
|
|
|
+ @dragGroup="onDragGroup"
|
|
|
+ @dragGroupEnd="onDragGroupEnd"
|
|
|
+ @resizeGroup="onResizeGroup"
|
|
|
+ @resizeGroupEnd="onResizeGroupEnd"
|
|
|
+ />
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, ref, inject, watch } from 'vue'
|
|
|
+import Moveable from 'vue3-moveable'
|
|
|
+import { useProjectStore } from '@/store/modules/project'
|
|
|
+import { useMutationObserver } from '@vueuse/core'
|
|
|
+
|
|
|
+import type { StageState } from './type'
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ // 父级容器 拖拽缩放设置
|
|
|
+ rootContainer: HTMLElement
|
|
|
+}>()
|
|
|
+
|
|
|
+const pageState = inject<StageState>('state')
|
|
|
+const pageEl = inject<HTMLElement>('pageEl')
|
|
|
+const projectStore = useProjectStore()
|
|
|
+const elements = ref<HTMLElement[]>([])
|
|
|
+
|
|
|
+// 吸附附近元素
|
|
|
+const elementGridelines = ref<Element[]>([])
|
|
|
+// 获取吸附附近元素
|
|
|
+const getElementGridelines = computed(() => {
|
|
|
+ const ids = projectStore.activeWidgets.map((item) => item.id)
|
|
|
+ return elementGridelines.value.filter((el) => {
|
|
|
+ const id = el.attributes.getNamedItem('widget-id')?.value
|
|
|
+ return id && !ids.includes(id)
|
|
|
+ })
|
|
|
+})
|
|
|
+// 垂直辅助线
|
|
|
+const verticalGuidelines = computed(() => {
|
|
|
+ return (
|
|
|
+ (pageState?.showReferenceLine &&
|
|
|
+ projectStore.activePage?.referenceLine
|
|
|
+ .filter((item) => item.type === 'horizontal')
|
|
|
+ .map((item) => item.value)) ||
|
|
|
+ []
|
|
|
+ )
|
|
|
+})
|
|
|
+// 水平辅助线
|
|
|
+const horizontalGuidelines = computed(() => {
|
|
|
+ return (
|
|
|
+ (pageState?.showReferenceLine &&
|
|
|
+ projectStore.activePage?.referenceLine
|
|
|
+ .filter((item) => item.type === 'vertical')
|
|
|
+ .map((item) => item.value)) ||
|
|
|
+ []
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+// 监听选中元素
|
|
|
+watch(
|
|
|
+ () => projectStore.activeWidgets,
|
|
|
+ () => {
|
|
|
+ elements.value = []
|
|
|
+ const ids = projectStore.activeWidgets.map((item) => item.id)
|
|
|
+ document.querySelectorAll('[widget-id]').forEach((el) => {
|
|
|
+ const id = el.attributes.getNamedItem('widget-id')?.value
|
|
|
+ if (id && ids.includes(id)) {
|
|
|
+ elements.value.push(el as HTMLElement)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ {
|
|
|
+ deep: true
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+// 监听页面节点变化 设置元素辅助线
|
|
|
+useMutationObserver(
|
|
|
+ pageEl,
|
|
|
+ (mutations) => {
|
|
|
+ mutations.forEach(() => {
|
|
|
+ const els = document.querySelectorAll('.widget-node')
|
|
|
+ elementGridelines.value = []
|
|
|
+ els.forEach((el) => {
|
|
|
+ elementGridelines.value.push(el)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ {
|
|
|
+ childList: true,
|
|
|
+ subtree: true
|
|
|
+ }
|
|
|
+)
|
|
|
+// 临时层级
|
|
|
+const zIndex = ref()
|
|
|
+
|
|
|
+// 拖拽开始
|
|
|
+const onDragStart = (e) => {
|
|
|
+ zIndex.value = e.target.style.zIndex
|
|
|
+ e.target.style.zIndex = 999
|
|
|
+}
|
|
|
+// 渲染节点拖拽
|
|
|
+const onDrag = (e) => {
|
|
|
+ // 当前选中节点整体移动
|
|
|
+ e.target.style.transform = e.transform
|
|
|
+}
|
|
|
+// 拖拽结束
|
|
|
+const onDragEnd = (e) => {
|
|
|
+ e.target.style.zIndex = zIndex.value
|
|
|
+ const id = e.target.attributes['widget-id']?.value
|
|
|
+ if (id && projectStore.activeWidgetMap[id] && e?.lastEvent?.translate) {
|
|
|
+ projectStore.activeWidgetMap[id].props.x = Math.round(e.lastEvent.translate[0])
|
|
|
+ projectStore.activeWidgetMap[id].props.y = Math.round(e.lastEvent.translate[1])
|
|
|
+ }
|
|
|
+}
|
|
|
+// 渲染节点缩放
|
|
|
+const onResize = (e) => {
|
|
|
+ e.target.style.width = `${e.width}px`
|
|
|
+ e.target.style.height = `${e.height}px`
|
|
|
+ e.target.style.transform = e.drag.transform
|
|
|
+}
|
|
|
+// 节点缩放完成
|
|
|
+const onResizeEnd = (e) => {
|
|
|
+ const id = e.target.attributes['widget-id']?.value
|
|
|
+ if (e.lastEvent && id && projectStore.activeWidgetMap[id]) {
|
|
|
+ projectStore.activeWidgetMap[id].props.width = Math.round(e.lastEvent.width)
|
|
|
+ projectStore.activeWidgetMap[id].props.height = Math.round(e.lastEvent.height)
|
|
|
+ // 设置位置
|
|
|
+ if (e.lastEvent.drag?.translate) {
|
|
|
+ projectStore.activeWidgetMap[id].props.x = Math.round(e.lastEvent.drag.translate[0])
|
|
|
+ projectStore.activeWidgetMap[id].props.y = Math.round(e.lastEvent.drag.translate[1])
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+// 渲染节点事件
|
|
|
+const onRender = (e) => {
|
|
|
+ e.target.style.cssText += e.cssText
|
|
|
+}
|
|
|
+// 节点组拖拽
|
|
|
+const onDragGroup = ({ events }) => {
|
|
|
+ events.forEach((ev) => {
|
|
|
+ ev.target.style.transform = ev.transform
|
|
|
+ })
|
|
|
+}
|
|
|
+// 节点组缩放
|
|
|
+const onResizeGroup = ({ events }) => {
|
|
|
+ events.forEach((ev) => {
|
|
|
+ ev.target.style.width = `${ev.width}px`
|
|
|
+ ev.target.style.height = `${ev.height}px`
|
|
|
+ ev.target.style.transform = ev.drag.transform
|
|
|
+ })
|
|
|
+}
|
|
|
+// 节点组拖拽结束
|
|
|
+const onDragGroupEnd = ({ events }) => {
|
|
|
+ events.forEach((ev) => {
|
|
|
+ const id = ev.target.attributes['widget-id']?.value
|
|
|
+ if (id && projectStore.activeWidgetMap[id] && ev?.lastEvent?.translate) {
|
|
|
+ projectStore.activeWidgetMap[id].props.x = Math.round(ev.lastEvent.translate[0])
|
|
|
+ projectStore.activeWidgetMap[id].props.y = Math.round(ev.lastEvent.translate[1])
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+// 节点组缩放结束
|
|
|
+const onResizeGroupEnd = ({ events }) => {
|
|
|
+ events.forEach((ev) => {
|
|
|
+ const id = ev.target.attributes['widget-id']?.value
|
|
|
+ if (ev.lastEvent && id && projectStore.activeWidgetMap[id]) {
|
|
|
+ projectStore.activeWidgetMap[id].props.width = Math.round(ev.lastEvent.width)
|
|
|
+ projectStore.activeWidgetMap[id].props.height = Math.round(ev.lastEvent.height)
|
|
|
+ // 设置位置
|
|
|
+ if (ev.lastEvent.drag?.translate) {
|
|
|
+ projectStore.activeWidgetMap[id].props.x = Math.round(ev.lastEvent.drag.translate[0])
|
|
|
+ projectStore.activeWidgetMap[id].props.y = Math.round(ev.lastEvent.drag.translate[1])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped></style>
|