画布节点创建删除

This commit is contained in:
2026-02-27 11:43:27 +08:00
parent a8a898d2df
commit 10d58fb819
9 changed files with 160 additions and 121 deletions

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="canvas-test"> <div class="canvas-test">
<div class="canvas-main" ref="canvasMain" @mousedown="onMouseDown" @wheel="onMouseWheel"> <!-- <div class="canvas-main" ref="canvasMain" @mousedown="onMouseDown" @wheel="onMouseWheel">
<div <div
class="canvas-content" class="canvas-content"
:style="{ :style="{
@@ -23,11 +23,13 @@
<card type="to-3view" /> <card type="to-3view" />
</div> </div>
</div> </div> -->
<flow-canvas />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import flowCanvas from './FlowCanvas/flow-canvas.vue'
import card from './FlowCanvas/components/cards/index.vue' import card from './FlowCanvas/components/cards/index.vue'
import { computed, ref, markRaw, onMounted, reactive, nextTick } from 'vue' import { computed, ref, markRaw, onMounted, reactive, nextTick } from 'vue'
const data = reactive({ const data = reactive({

View File

@@ -11,26 +11,31 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, inject } from 'vue'
import { NODE_DATATYPE } from '../../tools/index.d'
const nodeManager = inject('nodeManager') as any
const props = defineProps({
node: { required: true, type: Object }
})
const list = ref([ const list = ref([
{ {
type: 'to-real-style', type: NODE_DATATYPE.TO_REAL_STYLE,
title: 'To Real Style' title: 'To Real Style'
}, },
{ {
type: 'surface-edit', type: NODE_DATATYPE.SURFACE_EDIT,
title: 'Surface Edit' title: 'Surface Edit'
}, },
{ {
type: 'scene-composition', type: NODE_DATATYPE.SCENE_COMPOSITION,
title: 'Scene Composition' title: 'Scene Composition'
}, },
{ {
type: 'color-palette', type: NODE_DATATYPE.COLOR_PALETTE,
title: 'Color Palette' title: 'Color Palette'
}, },
{ {
type: 'to-3view', type: NODE_DATATYPE.TO_3VIEW,
title: 'To 3-View' title: 'To 3-View'
}, },
{ {
@@ -39,7 +44,10 @@
} }
]) ])
const onClickItem = (v) => { const onClickItem = (v) => {
console.log(v.type) const id = props.node.id
if (!id) return
nodeManager.deleteNode(id)
nodeManager.createCardNode({ data: { type: v.type } })
} }
defineExpose({}) defineExpose({})
</script> </script>

View File

@@ -11,7 +11,7 @@
<span>{{ currentComponent?.title }}</span> <span>{{ currentComponent?.title }}</span>
</div> </div>
<div class="body" @mousedown.stop> <div class="body" @mousedown.stop>
<component :is="currentComponent?.component" ref="componentRef" /> <component :is="currentComponent?.component" ref="componentRef" v-bind="attrs" />
</div> </div>
<div class="footer" @mousedown.stop v-if="!currentComponent?.hideFooter"> <div class="footer" @mousedown.stop v-if="!currentComponent?.hideFooter">
<button @click="onGenerateClick"> <button @click="onGenerateClick">
@@ -23,7 +23,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, markRaw, onMounted } from 'vue' import { computed, ref, useAttrs } from 'vue'
import CardsSelect from './cards-select.vue' import CardsSelect from './cards-select.vue'
import ToRealStyle from './to-real-style.vue' import ToRealStyle from './to-real-style.vue'
import SurfaceEdit from './surface-edit.vue' import SurfaceEdit from './surface-edit.vue'
@@ -103,6 +103,7 @@
default: 'to-real-style' default: 'to-real-style'
} }
}) })
const attrs = useAttrs()
const currentComponent = computed(() => { const currentComponent = computed(() => {
return components.find((item) => item.type === props.type) return components.find((item) => item.type === props.type)
}) })

View File

@@ -1,12 +1,31 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core' import { Handle, Position } from '@vue-flow/core'
import { ref } from 'vue' import { NODE_DATATYPE } from '../tools/index.d'
import { computed } from 'vue'
const props = defineProps({ const props = defineProps({
type: { type: {
type: String as () => 'InputNode' | 'SecondaryNode', type: String as () => 'InputNode' | 'SecondaryNode',
default: 'InputNode' default: 'InputNode'
},
node: {
type: Object,
required: true
},
stateManager: {
type: Object,
required: true
} }
}) })
const nodes = computed(() => props.stateManager.nodes.value)
const firstNode = computed(() => nodes.value[0])
const lastNode = computed(() => nodes.value[nodes.value.length - 1])
const notAddNodeTypes = [NODE_DATATYPE.CARDS_SELECT]
const isAdd = computed(
() => props.node.id === lastNode.value.id && !notAddNodeTypes.includes(props.node.data.type)
)
const onAdd = () => {
props.stateManager.nodeManager.createCardsSelect()
}
</script> </script>
<template> <template>
@@ -21,7 +40,7 @@
<div class="item"> <div class="item">
<slot></slot> <slot></slot>
</div> </div>
<div class="add" @mousedown.stop> <div class="add" @mousedown.stop v-if="isAdd" @click="onAdd">
<svg-icon name="add" size="14" size-unit="px" /> <svg-icon name="add" size="14" size-unit="px" />
</div> </div>
</div> </div>

View File

@@ -12,13 +12,21 @@
@viewport-change="(e) => eventManager.handleViewportChange(e)" @viewport-change="(e) => eventManager.handleViewportChange(e)"
> >
<template #node-InputNode="nodeProps"> <template #node-InputNode="nodeProps">
<node type="InputNode" :isAdd="lastNode.id === nodeProps.id"> <node type="InputNode" :stateManager="stateManager" :node="nodeProps">
<component :is="nodeProps.data.component" v-bind="nodeProps.data" /> <component
:is="nodeProps.data.component"
:node="nodeProps"
v-bind="nodeProps.data"
/>
</node> </node>
</template> </template>
<template #node-SecondaryNode="nodeProps"> <template #node-SecondaryNode="nodeProps">
<node type="SecondaryNode" :isAdd="lastNode.id === nodeProps.id"> <node type="SecondaryNode" :stateManager="stateManager" :node="nodeProps">
<component :is="nodeProps.data.component" v-bind="nodeProps.data" /> <component
:is="nodeProps.data.component"
:node="nodeProps"
v-bind="nodeProps.data"
/>
</node> </node>
</template> </template>
</VueFlow> </VueFlow>
@@ -39,9 +47,7 @@
import headerTools from './components/header-tools.vue' import headerTools from './components/header-tools.vue'
import zoom from '../components/zoom.vue' import zoom from '../components/zoom.vue'
import node from './components/node.vue' import node from './components/node.vue'
import card from './components/cards/index.vue' import { computed, ref, markRaw, onMounted, reactive, nextTick, provide } from 'vue'
import resultImage from './components/result/result-image.vue'
import { computed, ref, markRaw, onMounted, reactive, nextTick } from 'vue'
// 管理器 // 管理器
import { StateManager } from './manager/StateManager' import { StateManager } from './manager/StateManager'
@@ -53,9 +59,8 @@
// 状态管理器 // 状态管理器
const stateManager = new StateManager({ vueFlow }) const stateManager = new StateManager({ vueFlow })
const nodes = computed(() => stateManager.nodes.value) const nodes = computed(() => stateManager.nodes_.value)
const edges = computed(() => stateManager.edges.value) const edges = computed(() => stateManager.edges.value)
const lastNode = computed(() => nodes.value[nodes.value.length - 1])
// 事件管理器 // 事件管理器
const eventManager = new EventManager({ stateManager, vueFlow }) const eventManager = new EventManager({ stateManager, vueFlow })
// 流程管理器 // 流程管理器
@@ -68,13 +73,11 @@
nodeManager nodeManager
}) })
nodeManager.createNode({ provide('stateManager', stateManager)
type: 'InputNode', provide('eventManager', eventManager)
class: 'custom-node start', provide('flowManager', flowManager)
component: resultImage, provide('nodeManager', nodeManager)
data: {} provide('nodeManager', nodeManager)
})
const { fitView } = useVueFlow() const { fitView } = useVueFlow()
const { layout } = useLayout() const { layout } = useLayout()
const index = ref(0) const index = ref(0)
@@ -88,7 +91,7 @@
direction direction
) )
nextTick(() => { nextTick(() => {
fitView() fitView({ maxZoom: 1 })
}) })
}, 0) }, 0)
} }
@@ -96,22 +99,9 @@
onMounted(() => { onMounted(() => {
window.vueFlow = vueFlow window.vueFlow = vueFlow
window.nodes = nodes window.nodes = nodes
// window.addaaaaa = () => { nodeManager.createResultNode()
// const lastNode = vueFlow.value.getNode(nodes.value[nodes.value.length - 1].id) // nodeManager.createCardsSelect()
// const width = lastNode.dimensions.width // nodeManager.createResultNode()
// const x = lastNode.position.x
// const y = lastNode.position.y
// nodes.value.push({
// id: nodes.value.length + 1 + '',
// type: 'SecondaryNode',
// class: 'custom-node',
// data: { component: card, type_: 'to-3d-model' },
// position: {
// x: width + x + 50,
// y: y
// }
// })
// }
}) })
</script> </script>
<style lang="less"> <style lang="less">

View File

@@ -9,14 +9,12 @@ export class FlowManager {
this.stateManager.zoom.value = zoom this.stateManager.zoom.value = zoom
this.vueFlow.value.zoomTo(zoom) this.vueFlow.value.zoomTo(zoom)
} }
handleNodeDragStop(e: any) { getLastNode() {
const { node } = e const lastNode = this.stateManager.getLastNode()
const { id, position } = node if (lastNode?.id) {
this.stateManager.nodes.value.forEach((item) => { return this.vueFlow.value.getNode(lastNode.id)
if (item.id === id) { }
item.position.x = position.x return null;
item.position.y = position.y
}
})
} }
} }

View File

@@ -1,12 +1,16 @@
import { createId } from '../../tools/tools' import { createId } from '../../tools/tools'
import card from '../components/cards/index.vue'
import resultImage from '../components/result/result-image.vue'
import { NODE_DATATYPE } from '../tools/index.d'
interface NodeOptions { interface NodeOptions {
id?: string id?: string
type: "InputNode" | "SecondaryNode"
class?: string
position?: { x: number, y: number } position?: { x: number, y: number }
component: any positionX?: number
positionY?: number
component?: any
data?: object data?: object
} }// 不可传入type class (内部使用)
export class NodeManager { export class NodeManager {
stateManager: any stateManager: any
vueFlow: any vueFlow: any
@@ -14,23 +18,60 @@ export class NodeManager {
this.stateManager = options.stateManager; this.stateManager = options.stateManager;
this.vueFlow = options.vueFlow this.vueFlow = options.vueFlow
} }
nodes: [
] /** 删除节点 */
deleteNode(id: string) {
this.stateManager.nodes.value = this.stateManager.nodes.value.filter((node: any) => node.id !== id)
}
/** 创建节点 */
createNode(options: NodeOptions) { createNode(options: NodeOptions) {
const lastNode = this.stateManager.flowManager.getLastNode();
const id = options.id || createId() const id = options.id || createId()
const type = options.type || 'InputNode' const positionX = options.positionX || 0
const class_ = options.class || 'custom-node' const positionY = options.positionY || 0
const position = options.position || { x: 0, y: 0 } const position = options.position ||
!lastNode ? { x: positionX, y: positionY } :
{ x: lastNode.position.x + lastNode.dimensions.width + 50 + positionX, y: lastNode.position.y + positionY }
const data = options.data || {} const data = options.data || {}
data['component'] = options.component data['component'] = options.component
this.stateManager.nodes.value.push({ const options_ = {
id, id,
type,
class: class_,
position, position,
data data
}) }
console.log(this.stateManager.nodes.value) this.stateManager.nodes.value.push(options_)
return options_;
}
/** 创建结果节点 */
createResultNode(options?: NodeOptions) {
const options_ = {
component: resultImage,
data: {
type: NODE_DATATYPE.RESULT_IMAGE,
},
...(options ? options : {}),
}
return this.createNode(options_)
}
/** 创建卡片选择节点 */
createCardsSelect(options?: NodeOptions) {
const options_ = {
component: card,
positionY: 50,
data: {
type: NODE_DATATYPE.CARDS_SELECT,
},
...(options ? options : {}),
}
return this.createNode(options_)
}
/** 创建卡片节点 */
createCardNode(options?: NodeOptions) {
const options_ = {
component: card,
...(options ? options : {}),
}
return this.createNode(options_)
} }
} }

View File

@@ -1,9 +1,9 @@
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import card from '../components/cards/index.vue'
export class StateManager { export class StateManager {
vueFlow: any vueFlow: any
nodes: any nodes: any
nodes_: any
edges: any edges: any
zoom: any zoom: any
// 管理器 // 管理器
@@ -19,55 +19,6 @@ export class StateManager {
constructor(options) { constructor(options) {
this.vueFlow = options.vueFlow this.vueFlow = options.vueFlow
this.nodes = ref<any[]>([ this.nodes = ref<any[]>([
// {
// id: '1',
// type: 'InputNode',
// class: 'custom-node start',
// position: { x: 0, y: 0 },
// data: { component: card, type: 'to-real-style' }
// },
// {
// id: '2',
// type: 'SecondaryNode',
// class: 'custom-node',
// position: { x: 0, y: 0 },
// data: { component: card, type: 'scene-composition' }
// },
// {
// id: '3',
// type: 'SecondaryNode',
// class: 'custom-node',
// position: { x: 0, y: 0 },
// data: { component: card, type: 'color-palette' }
// },
// {
// id: '4',
// type: 'SecondaryNode',
// class: 'custom-node',
// position: { x: 0, y: 0 },
// data: { component: card, type: 'to-video' }
// },
// {
// id: '5',
// type: 'SecondaryNode',
// class: 'custom-node',
// position: { x: 0, y: 0 },
// data: { component: card, type: 'to-3d-model' }
// },
// {
// id: '6',
// type: 'SecondaryNode',
// class: 'custom-node',
// position: { x: 0, y: 0 },
// data: { component: card, type: 'to-cad' }
// },
// {
// id: '7',
// type: 'SecondaryNode',
// class: 'custom-node',
// position: { x: 0, y: 0 },
// data: { component: card, type: 'add-print' }
// },
// { // {
// id: '8', // id: '8',
// type: 'SecondaryNode', // type: 'SecondaryNode',
@@ -76,6 +27,21 @@ export class StateManager {
// data: { component: card, type: 'edit-material' } // data: { component: card, type: 'edit-material' }
// } // }
]); ]);
this.nodes_ = computed(() => {
return this.nodes.value.map((node, index) => {
const obj = {
...node,
}
if (index === 0) {
obj.class = 'custom-node start';
obj.type = 'InputNode';
} else {
obj.class = 'custom-node';
obj.type = 'SecondaryNode';
}
return obj
})
})
this.edges = computed(() => { this.edges = computed(() => {
const arr = [] const arr = []
@@ -96,6 +62,7 @@ export class StateManager {
this.zoom = ref(1) this.zoom = ref(1)
} }
getLastNode() {
return this.nodes.value[this.nodes.value.length - 1]
}
} }

View File

@@ -0,0 +1,13 @@
/**
* 节点数据类型
*/
export const NODE_DATATYPE = {
RESULT_IMAGE: 'result-image',
CARDS_SELECT: 'cards-select',
TO_REAL_STYLE: 'to-real-style',
SURFACE_EDIT: 'surface-edit',
SCENE_COMPOSITION: 'scene-composition',
COLOR_PALETTE: 'color-palette',
TO_3VIEW: 'to-3view',
TO_3D_MODEL: 'to-3d-model',
}