feat: 属性面板以及简单撤销重做功能
This commit is contained in:
+60
-145
@@ -4,56 +4,40 @@ import { createNode, getMaterialComponent } from '@/materials'
|
||||
import type { MaterialSchema } from '@/schema/materials'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Moveable, { type OnDrag, type OnResize } from 'vue3-moveable'
|
||||
import Moveable from 'vue3-moveable'
|
||||
import { VueSelecto } from 'vue3-selecto'
|
||||
import SketchRuler from 'vue3-sketch-ruler'
|
||||
import 'vue3-sketch-ruler/lib/style.css'
|
||||
import { useCanvasRuler } from './composables/useCanvasRuler'
|
||||
import { useMoveable } from './composables/useMoveable'
|
||||
import { useSelection } from './composables/useSelection'
|
||||
|
||||
defineOptions({
|
||||
name: 'CanvasRoot',
|
||||
})
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const { nodes, selectedNodeIds, canvas } = storeToRefs(editorStore)
|
||||
const { nodes, canvas } = storeToRefs(editorStore)
|
||||
|
||||
const moveableRef = useTemplateRef('moveableRef')
|
||||
const stageRef = useTemplateRef('stageRef')
|
||||
// 标尺厚度
|
||||
const RULER_THICK = 20
|
||||
// 缩放比例
|
||||
const scale = ref(1)
|
||||
// 标尺线条
|
||||
const lines = ref({ h: [], v: [] })
|
||||
const canvasWidth = toRef(canvas.value, 'width')
|
||||
const canvasHeight = toRef(canvas.value, 'height')
|
||||
/** 画布样式*/
|
||||
// canvas容器的ref
|
||||
const canvasRootRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const { RULER_THICK, scale, lines, rulerCanvasSize, viewportSize, palette, onZoomChange } =
|
||||
useCanvasRuler(moveableRef, canvasRootRef)
|
||||
const { onDrag, onResize, onDragGroup, onResizeGroup } = useMoveable()
|
||||
const { selectedTarget, onSelect, onClearSelect, onSelectEnd } = useSelection(stageRef, moveableRef)
|
||||
|
||||
/** 设计稿白底:宽高和背景色立刻跟 store */
|
||||
const canvasStyle = computed(() => {
|
||||
return {
|
||||
width: canvasWidth.value,
|
||||
height: canvasHeight.value,
|
||||
width: canvas.value.width,
|
||||
height: canvas.value.height,
|
||||
backgroundColor: canvas.value.backgroundColor,
|
||||
}
|
||||
})
|
||||
|
||||
// canvas容器的ref
|
||||
const wrapRef = ref<HTMLElement | null>(null)
|
||||
// 外层视口(含标尺),只给 SketchRuler 用
|
||||
const viewportSize = reactive({ width: 0, height: 0 })
|
||||
|
||||
// 当前选中的节点dom元素
|
||||
const selectNodeTarget = shallowRef<HTMLElement[] | null>(null)
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
/** 捕获父级容器实际宽高,作为标尺视口 */
|
||||
const updateCanvasSize = () => {
|
||||
const el = wrapRef.value
|
||||
if (!el) return
|
||||
const { width, height } = el.getBoundingClientRect()
|
||||
viewportSize.width = Math.floor(width)
|
||||
viewportSize.height = Math.floor(height)
|
||||
}
|
||||
|
||||
// 获取节点的样式
|
||||
const getNodeStyle = (node: MaterialSchema, zIndex: number) => {
|
||||
return {
|
||||
@@ -65,32 +49,6 @@ const getNodeStyle = (node: MaterialSchema, zIndex: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 标尺颜色主题
|
||||
const palette = {
|
||||
bgColor: '#1f2937',
|
||||
longfgColor: '#6b7280',
|
||||
fontColor: '#9ca3af',
|
||||
fontShadowColor: '#0e8da7',
|
||||
shadowColor: 'rgba(14, 141, 167, 0.14)',
|
||||
lineColor: '#22c55e',
|
||||
lineType: 'solid',
|
||||
lockLineColor: '#4b5563',
|
||||
borderColor: '#374151',
|
||||
hoverBg: '#111827',
|
||||
hoverColor: '#ffffff',
|
||||
}
|
||||
|
||||
watch(
|
||||
selectedNodeIds,
|
||||
(newVal) => {
|
||||
console.log('选中的节点id变化', newVal)
|
||||
selectNodeTarget.value = newVal
|
||||
.map((id) => stageRef.value?.querySelector<HTMLElement>(`[data-node-id="${id}"]`))
|
||||
.filter((target): target is HTMLElement => target != null)
|
||||
},
|
||||
{ deep: true, flush: 'post' },
|
||||
)
|
||||
|
||||
// 拖拽放置节点
|
||||
const onDrop = (e: DragEvent) => {
|
||||
const data = e.dataTransfer?.getData('schema')
|
||||
@@ -104,97 +62,36 @@ const onDrop = (e: DragEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 选中节点
|
||||
const onSelect = (node: MaterialSchema) => {
|
||||
editorStore.selectNode(node.id)
|
||||
|
||||
// 处理第一次选中节点时,moveable组件没有正确获取到target导致无法移动问题
|
||||
nextTick(() => {
|
||||
moveableRef.value?.updateTarget()
|
||||
})
|
||||
// 右键菜单操作映射
|
||||
const commendMap = {
|
||||
copy: () => editorStore.selectedNode && editorStore.copyNode(editorStore.selectedNode),
|
||||
remove: () => editorStore.selectedNode && editorStore.removeNode(editorStore.selectedNode),
|
||||
moveBottom: () => editorStore.selectedNode && editorStore.moveTop(editorStore.selectedNode), // 列表渲染时反转了,这里也反转操作
|
||||
moveTop: () => editorStore.selectedNode && editorStore.moveBottom(editorStore.selectedNode),
|
||||
toggleLock: () => {
|
||||
if (editorStore.selectedNode) {
|
||||
editorStore.toggleLock(editorStore.selectedNode)
|
||||
}
|
||||
selectedTarget.value = []
|
||||
},
|
||||
}
|
||||
|
||||
const getNodeByTarget = (target: HTMLElement) => {
|
||||
const nodeId = target.getAttribute('data-node-id')
|
||||
if (!nodeId) return null
|
||||
return editorStore.findNodeById(nodeId)
|
||||
// 右键菜单操作
|
||||
const onCommand = (command: keyof typeof commendMap) => {
|
||||
commendMap[command]?.()
|
||||
}
|
||||
|
||||
// 拖拽节点
|
||||
const onDrag = (e: OnDrag) => {
|
||||
console.log('拖拽事件', e)
|
||||
e.target.style.setProperty('left', `${e.left}px`)
|
||||
e.target.style.setProperty('top', `${e.top}px`)
|
||||
const node = getNodeByTarget(e.target as HTMLElement)
|
||||
console.log('拖拽节点', node)
|
||||
if (!node) return
|
||||
node.layout.x = e.left
|
||||
node.layout.y = e.top
|
||||
}
|
||||
|
||||
// 缩放节点
|
||||
const onResize = (e: OnResize) => {
|
||||
e.target.style.setProperty('width', `${e.width}px`)
|
||||
e.target.style.setProperty('height', `${e.height}px`)
|
||||
const node = getNodeByTarget(e.target as HTMLElement)
|
||||
if (!node) return
|
||||
node.layout.width = e.width
|
||||
node.layout.height = e.height
|
||||
// 处理左侧圆点的拖拽,改变节点位置
|
||||
onDrag(e.drag)
|
||||
}
|
||||
|
||||
// 清空选中节点
|
||||
const onClearSelect = () => {
|
||||
editorStore.clearSelect()
|
||||
}
|
||||
|
||||
// 选中区域结束
|
||||
const onSelectEnd = (e: any) => {
|
||||
const ids = e.selected.map((el: HTMLElement) => el.getAttribute('data-node-id'))
|
||||
editorStore.selectNodes(ids)
|
||||
console.log('选中节点id', ids)
|
||||
}
|
||||
|
||||
// 拖拽节点组
|
||||
const onDragGroup = (e: any) => {
|
||||
e.events.forEach(onDrag)
|
||||
}
|
||||
|
||||
// 拖动节点组
|
||||
const onResizeGroup = (e: any) => {
|
||||
e.events.forEach(onResize)
|
||||
}
|
||||
|
||||
// SketchRuler缩放变化
|
||||
const onZoomChange = () => {
|
||||
// 更新moveableRef的选框,防止缩放时选框位置错位
|
||||
moveableRef.value?.updateRect()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateCanvasSize()
|
||||
if (!wrapRef.value) return
|
||||
resizeObserver = new ResizeObserver(updateCanvasSize)
|
||||
resizeObserver.observe(wrapRef.value)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect()
|
||||
resizeObserver = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="wrapRef" class="h-full w-full min-h-0 min-w-0 overflow-hidden">
|
||||
<div ref="canvasRootRef" class="h-full w-full min-h-0 min-w-0 overflow-hidden">
|
||||
<SketchRuler
|
||||
v-if="viewportSize.width && viewportSize.height"
|
||||
:palette="palette"
|
||||
:thick="RULER_THICK"
|
||||
:width="viewportSize.width"
|
||||
:height="viewportSize.height"
|
||||
:canvas-width="canvasStyle.width"
|
||||
:canvas-height="canvasStyle.height"
|
||||
:canvas-width="rulerCanvasSize.width"
|
||||
:canvas-height="rulerCanvasSize.height"
|
||||
:scale="scale"
|
||||
:lines="lines"
|
||||
@zoomchange="onZoomChange"
|
||||
@@ -211,16 +108,34 @@ onBeforeUnmount(() => {
|
||||
@drop="onDrop"
|
||||
@mousedown.self="onClearSelect"
|
||||
>
|
||||
<div
|
||||
<el-dropdown
|
||||
v-for="(node, index) in nodes"
|
||||
:key="node.id"
|
||||
:data-node-id="node.id"
|
||||
class="canvas-node absolute"
|
||||
:style="getNodeStyle(node, index)"
|
||||
@mousedown="onSelect(node)"
|
||||
trigger="contextmenu"
|
||||
@command="onCommand"
|
||||
>
|
||||
<component :is="getMaterialComponent(node.type)" :schema="node" />
|
||||
</div>
|
||||
<div
|
||||
:data-node-id="node.id"
|
||||
:data-node-locked="node.locked"
|
||||
class="canvas-node absolute"
|
||||
:style="getNodeStyle(node, index)"
|
||||
@mousedown="onSelect(node)"
|
||||
>
|
||||
<component :is="getMaterialComponent(node.type)" :schema="node" />
|
||||
</div>
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="copy">复制</el-dropdown-item>
|
||||
<el-dropdown-item command="remove">移除</el-dropdown-item>
|
||||
<el-dropdown-item command="moveTop">置顶</el-dropdown-item>
|
||||
<el-dropdown-item command="moveBottom">置底</el-dropdown-item>
|
||||
<el-dropdown-item command="toggleLock">
|
||||
{{ node.locked ? '解锁' : '锁定' }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</SketchRuler>
|
||||
|
||||
@@ -235,7 +150,7 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
<Moveable
|
||||
ref="moveableRef"
|
||||
:target="selectNodeTarget"
|
||||
:target="selectedTarget"
|
||||
:draggable="true"
|
||||
:resizable="true"
|
||||
:origin="false"
|
||||
|
||||
Reference in New Issue
Block a user