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
+9
View File
@@ -11,6 +11,15 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
+57
View File
@@ -0,0 +1,57 @@
import { setValue, getValue } from '@/utils'
const undoStack: any[] = shallowReactive([])
const redoStack: any[] = shallowReactive([])
export function useUndoRedo() {
// 是否可以撤销
const canUndo = computed(() => undoStack.length > 0)
// 是否可以重做
const canRedo = computed(() => redoStack.length > 0)
// 应用更改
function applyChange(target: any, key: string, newValue: any) {
const oldValue = getValue(target, key)
if (oldValue === newValue) return
const record = {
target,
key,
oldValue,
newValue,
}
undoStack.push(record)
setValue(target, key, newValue)
// 清空重做栈
redoStack.length = 0
}
// 撤销
function undo() {
const record = undoStack.pop()
if (!record) return
const { target, key, oldValue } = record
setValue(target, key, oldValue)
redoStack.push(record)
}
// 重做
function redo() {
const record = redoStack.pop()
if (!record) return
const { target, key, newValue } = record
setValue(target, key, newValue)
undoStack.push(record)
}
return {
canUndo,
canRedo,
applyChange,
undo,
redo,
}
}
@@ -0,0 +1,86 @@
import { useEditorStore } from '@/stores/editor'
import { storeToRefs } from 'pinia'
type MoveableLike = {
updateRect?: () => void
}
/**
* 标尺视口量测、工作区冻结、缩放后同步 Moveable 选框
*/
export const useCanvasRuler = (
moveableRef: { value: MoveableLike | null },
canvasRootRef: Ref<HTMLElement | null>,
) => {
const editorStore = useEditorStore()
const { canvas } = storeToRefs(editorStore)
// 标尺厚度
const RULER_THICK = 20
// 缩放比例
const scale = ref(1)
// 标尺线条
const lines = ref({ h: [], v: [] })
/**
* 标尺工作区只取初始设计稿尺寸,之后不再跟着改。
* vue3-sketch-ruler 会 watch canvasWidth/Height 并 initPanzoom(销毁重建 + 重新居中),跟手改会抖。
*/
const rulerCanvasSize = {
width: canvas.value.width,
height: canvas.value.height,
}
// 外层视口(含标尺),只给 SketchRuler 用
const viewportSize = reactive({ width: 0, height: 0 })
// 标尺颜色主题
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',
}
let resizeObserver: ResizeObserver | null = null
/** 捕获父级容器实际宽高,作为标尺视口 */
const updateCanvasSize = () => {
const el = canvasRootRef.value
if (!el) return
const { width, height } = el.getBoundingClientRect()
viewportSize.width = Math.floor(width)
viewportSize.height = Math.floor(height)
}
/** SketchRuler 缩放变化时同步 Moveable 选框,防止错位 */
const onZoomChange = () => {
moveableRef.value?.updateRect?.()
}
onMounted(() => {
updateCanvasSize()
if (!canvasRootRef.value) return
resizeObserver = new ResizeObserver(updateCanvasSize)
resizeObserver.observe(canvasRootRef.value)
})
onBeforeUnmount(() => {
resizeObserver?.disconnect()
resizeObserver = null
})
return {
RULER_THICK,
scale,
lines,
rulerCanvasSize,
viewportSize,
palette,
onZoomChange,
}
}
@@ -0,0 +1,57 @@
import { useEditorStore } from '@/stores/editor'
import type { OnDrag, OnResize } from 'vue3-moveable'
/**
* Moveable 单选 / 组拖拽与缩放
*/
export const useMoveable = () => {
const editorStore = useEditorStore()
/** 从节点 DOM 反查 store 节点 */
const getNodeByTarget = (target: HTMLElement) => {
const nodeId = target.getAttribute('data-node-id')
if (!nodeId) return null
return editorStore.findNodeById(nodeId)
}
/** 拖拽节点 */
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 onDragGroup = (e: { events: OnDrag[] }) => {
e.events.forEach(onDrag)
}
/** 缩放节点组 */
const onResizeGroup = (e: { events: OnResize[] }) => {
e.events.forEach(onResize)
}
return {
onDrag,
onResize,
onDragGroup,
onResizeGroup,
}
}
@@ -0,0 +1,65 @@
import type { MaterialSchema } from '@/schema/materials'
import { useEditorStore } from '@/stores/editor'
import { storeToRefs } from 'pinia'
type MoveableLike = {
updateTarget?: () => void
}
/**
* 节点点选 / 框选,并同步 Moveable target
*/
export const useSelection = (
stageRef: { value: HTMLElement | null },
moveableRef: { value: MoveableLike | null },
) => {
const editorStore = useEditorStore()
const { selectedNodeIds } = storeToRefs(editorStore)
// 当前选中的节点dom元素
const selectedTarget = shallowRef<HTMLElement[] | null>(null)
watch(
selectedNodeIds,
(newVal) => {
console.log('选中的节点id变化', newVal)
selectedTarget.value = newVal
.map((id) =>
stageRef.value?.querySelector<HTMLElement>(
`[data-node-id="${id}"]:not([data-node-locked="true"])`,
),
)
.filter((target): target is HTMLElement => target != null)
},
{ deep: true, flush: 'post' },
)
/** 点选节点 */
const onSelect = (node: MaterialSchema) => {
editorStore.selectNode(node.id)
// 处理第一次选中节点时,moveable组件没有正确获取到target导致无法移动问题
nextTick(() => {
moveableRef.value?.updateTarget?.()
})
}
/** 清空选中节点 */
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)
}
return {
selectedTarget,
onSelect,
onClearSelect,
onSelectEnd,
}
}
+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"
+3 -2
View File
@@ -5,6 +5,7 @@ import ToolbarRight from '@/editor/toolbar/ToolbarRight.vue'
import MaterialPanel from '@/editor/panels/material/index.vue'
import LayerPanel from '@/editor/panels/layer/index.vue'
import CanvasRoot from '@/editor/canvas/index.vue'
import PropertyPanel from '@/editor/panels/property/index.vue'
import { useEditorStore } from '@/stores/editor'
import { storeToRefs } from 'pinia'
@@ -43,12 +44,12 @@ const layerWidth = computed(() => (panelVisible.value.layer ? '260px' : '0'))
<!-- 画布 -->
<CanvasRoot class="canvas flex-1">画布</CanvasRoot>
<!-- 属性 -->
<aside
<PropertyPanel
class="w-[256px] border-l border-white/20 overflow-hidden transition-all"
:style="{ width: layerWidth }"
>
属性
</aside>
</PropertyPanel>
</main>
</div>
</template>
@@ -0,0 +1,27 @@
<script setup lang="ts">
import { useEditorStore } from '@/stores/editor'
import { storeToRefs } from 'pinia'
defineOptions({
name: 'CanvasProperty',
})
const editorStore = useEditorStore()
const { canvas } = storeToRefs(editorStore)
</script>
<template>
<div>
<el-form class="p-5" size="small" label-width="60px">
<el-form-item label="宽度">
<el-input-number v-model="canvas.width"></el-input-number>
</el-form-item>
<el-form-item label="高度">
<el-input-number v-model="canvas.height"></el-input-number>
</el-form-item>
<el-form-item label="背景色">
<el-color-picker v-model="canvas.backgroundColor"></el-color-picker>
</el-form-item>
</el-form>
</div>
</template>
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { getValue } from '@/utils'
import { ElInput, ElInputNumber, ElColorPicker } from 'element-plus'
import { useUndoRedo } from '@/composables/useUndoRedo'
defineProps(['setters', 'formData'])
const { applyChange } = useUndoRedo()
const componentMap: Record<string, any> = {
input: ElInput,
number: (props: Record<string, any>) => h(ElInputNumber, { precision: 0, ...props }),
color: ElColorPicker,
}
</script>
<template>
<div>
<el-form class="p-5" size="small" label-width="60px">
<el-form-item v-for="setter in setters" :key="setter.key" :label="setter.label">
<component
:is="componentMap[setter.type]"
:model-value="getValue(formData, setter.key)"
@update:model-value="(value: any) => applyChange(formData, setter.key, value)"
/>
</el-form-item>
</el-form>
</div>
</template>
<style scoped></style>
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { getMaterialSetters } from '@/materials'
import { useEditorStore } from '@/stores/editor'
import { storeToRefs } from 'pinia'
import FormCreate from './FormCreate.vue'
defineOptions({
name: 'NodeProperty',
})
const editorStore = useEditorStore()
const { selectedNode } = storeToRefs(editorStore)
const setters = getMaterialSetters(selectedNode.value?.type || '') ?? []
// 布局属性
const layoutSetters = [
{
label: '宽度',
key: 'layout.width',
type: 'number',
},
{
label: '高度',
key: 'layout.height',
type: 'number',
},
{
label: 'X',
key: 'layout.x',
type: 'number',
},
{
label: 'Y',
key: 'layout.y',
type: 'number',
},
]
</script>
<template>
<div class="px-2">
<el-collapse>
<el-collapse-item title="布局属性">
<FormCreate :setters="layoutSetters" :form-data="selectedNode" />
</el-collapse-item>
<el-collapse-item title="组件属性">
<FormCreate :setters="setters" :form-data="selectedNode" />
</el-collapse-item>
</el-collapse>
</div>
</template>
+22
View File
@@ -0,0 +1,22 @@
<script setup lang="ts">
import CanvasProperty from './components/CanvasProperty.vue'
import NodeProperty from './components/NodeProperty.vue'
import { useEditorStore } from '@/stores/editor'
import { storeToRefs } from 'pinia'
defineOptions({
name: 'PropertyPanel',
})
const editorStore = useEditorStore()
const { selectedNode } = storeToRefs(editorStore)
</script>
<template>
<div>
<NodeProperty v-if="selectedNode" />
<CanvasProperty v-else />
</div>
</template>
<style scoped></style>
+19 -5
View File
@@ -3,15 +3,17 @@
import { Icon } from '@iconify/vue'
import { useEditorStore } from '@/stores/editor'
import { storeToRefs } from 'pinia'
import { useUndoRedo } from '@/composables/useUndoRedo'
const editorStore = useEditorStore()
const { panelVisible } = storeToRefs(editorStore)
const { undo, redo, canUndo, canRedo } = useUndoRedo()
</script>
<template>
<div class="flex items-center gap-5">
<span
class="border p-1 rounded-lg"
class="border p-1 rounded-lg cursor-pointer"
:class="{ 'bg-white/20': panelVisible.material }"
@click="panelVisible.material = !panelVisible.material"
>
@@ -31,9 +33,21 @@ const { panelVisible } = storeToRefs(editorStore)
>
<Icon icon="mynaui:layers-three-solid"></Icon>
</span>
<span class="border p-1 rounded-lg"><Icon icon="mynaui:undo-solid"></Icon></span>
<span class="border p-1 rounded-lg"><Icon icon="mynaui:redo-solid"></Icon></span>
<!-- 撤销 -->
<span
class="border p-1 rounded-lg cursor-pointer"
@click="undo"
:class="{ 'opacity-50 cursor-not-allowed': !canUndo }"
>
<Icon icon="mynaui:undo-solid"> </Icon>
</span>
<!-- 重做 -->
<span
class="border p-1 rounded-lg cursor-pointer"
@click="redo"
:class="{ 'opacity-50 cursor-not-allowed': !canRedo }"
>
<Icon icon="mynaui:redo-solid"></Icon>
</span>
</div>
</template>
<style scoped></style>
+1
View File
@@ -4,6 +4,7 @@ export const chartsMaterial: MaterialDefinition = {
name: '柱状图',
icon: '',
group: 'charts',
setters: [],
schema: {
type: 'chart',
name: '柱状图',
+9 -2
View File
@@ -1,12 +1,15 @@
import type { MaterialDefinition, MaterialSchema } from '@/schema/materials'
import type { MaterialDefinition, MaterialSchema, SetterSchema } from '@/schema/materials'
// 物料列表
const materials: MaterialDefinition[] = []
// 已注册的物料列表
const componentMap = new Map<string, Component>()
const settersMap = new Map<string, SetterSchema[]>()
export function register(material: MaterialDefinition, component: Component) {
materials.push(material)
componentMap.set(material.schema.type, component)
settersMap.set(material.schema.type, material.setters)
}
const materialModules = import.meta.glob('./**/index.ts', { eager: true })
@@ -47,6 +50,10 @@ export function getMaterialComponent(type: string): Component | undefined {
return componentMap.get(type)
}
export function getMaterialSetters(type: string): SetterSchema[] | undefined {
return settersMap.get(type)
}
// 创建一个新的节点对象,生成唯一的 id
export function createNode(node: MaterialSchema) {
return {
+13 -1
View File
@@ -1,10 +1,22 @@
import type { MaterialDefinition } from '../types'
import type { MaterialDefinition } from '@/schema/materials.ts'
import TextMaterial from './component.vue'
const textMaterial: MaterialDefinition = {
name: '文本',
icon: '',
group: 'info',
setters: [
{
type: 'input',
label: '内容',
key: 'props.content',
},
{
type: 'color',
label: '颜色',
key: 'style.color',
},
],
schema: {
type: 'text',
name: '文本',
+9
View File
@@ -10,15 +10,24 @@ export interface MaterialSchema {
type: string
name: string
id: string
locked?: boolean
layout: Layout
style?: Record<string, any>
props: Record<string, any>
}
export interface SetterSchema {
type: string
label: string
key: string
[key: string]: any
}
// 物料定义结构
export interface MaterialDefinition {
name: string
icon: string
group: string
setters: SetterSchema[]
schema: Omit<MaterialSchema, 'id'>
}
+41
View File
@@ -59,6 +59,42 @@ export const useEditorStore = defineStore('editor', () => {
selectedNodeIds.value = []
}
/* 复制节点 */
function copyNode(node: MaterialSchema) {
const newNode = JSON.parse(JSON.stringify(node)) as MaterialSchema
newNode.id = crypto.randomUUID()
// 偏移
newNode.layout.x += 20
newNode.layout.y += 20
addNode(newNode)
selectNode(newNode.id)
}
// 删除节点
function removeNode(node: MaterialSchema) {
nodes.value = nodes.value.filter((n) => n.id !== node.id)
selectedNodeIds.value = selectedNodeIds.value.filter((id) => id !== node.id)
}
// 置顶节点
function moveTop(node: MaterialSchema) {
const index = nodes.value.findIndex((n) => n.id === node.id)
nodes.value.splice(index, 1)
nodes.value.unshift(node)
}
// 置顶节点
function moveBottom(node: MaterialSchema) {
const index = nodes.value.findIndex((n) => n.id === node.id)
nodes.value.splice(index, 1)
nodes.value.push(node)
}
// 锁定节点
function toggleLock(node: MaterialSchema) {
node.locked = !node.locked
}
return {
panelVisible,
page,
@@ -72,5 +108,10 @@ export const useEditorStore = defineStore('editor', () => {
clearSelect,
selectNodes,
findNodeById,
copyNode,
removeNode,
moveTop,
moveBottom,
toggleLock,
}
})
+17
View File
@@ -0,0 +1,17 @@
// 获取对象中指定路径的值
export function getValue(target: any, key: string) {
// 将 'props.content' 转换为 ['props', 'content']
const keys = key.split('.')
while (keys.length) {
target = target[keys.shift()!]
}
return target
}
// 设置对象中指定路径的值
export function setValue(target: any, key: string, value: any) {
const keys = key.split('.')
const lastKey = keys.pop()!
const _target = getValue(target, keys.join('.'))
_target[lastKey] = value
}
+2 -1
View File
@@ -4,7 +4,8 @@
"env.d.ts",
"src/**/*",
"src/**/*.vue",
"auto-imports.d.ts"
"auto-imports.d.ts",
"components.d.ts"
],
"exclude": [
"src/**/__tests__/*"