feat: 属性面板以及简单撤销重做功能

This commit is contained in:
2026-08-15 21:32:02 +08:00
parent a043fd9d8e
commit f99324adb4
19 changed files with 580 additions and 156 deletions
+60 -145
View File
@@ -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"