深度画布框架

This commit is contained in:
lzp
2026-03-06 15:50:05 +08:00
parent a3938662c9
commit d0d8c0ab9b
47 changed files with 3827 additions and 88 deletions

View File

@@ -0,0 +1,84 @@
import { TOOLS } from "./ToolManager"
export class EventManager {
stateManager: any
vueFlow: any
zoom: any
constructor(options) {
this.stateManager = options.stateManager;
this.vueFlow = options.vueFlow
this.zoom = this.stateManager.zoom
this.registerEvents()
}
/** 处理视口变化 */
handleViewportChange(e: any) {
const { zoom } = e
this.zoom.value = zoom
}
/** 处理节点拖动停止 */
handleNodeDragStop(e: any) {
const { node } = e
const { id, position } = node
this.stateManager.nodes.value.forEach((item) => {
if (item.id === id) {
item.position.x = position.x
item.position.y = position.y
}
})
this.stateManager.recordState()
}
/** 处理点击 */
handleClick(event: any) {
this.stateManager.setActiveNodeID("")
const tool = this.stateManager.tool.value
if (tool === TOOLS.TEXT) {
const { x, y, zoom } = this.vueFlow.value.viewport
const position = {
x: (event.offsetX - x) / zoom,
y: (event.offsetY - y) / zoom
}
this.stateManager.nodeManager.createTextNode({ position })
this.stateManager.toolManager.setTool(TOOLS.SELECT)
}
}
/** 处理复制 */
handleCopy(event: any, activeNodeID: string) {
event.preventDefault()
if (!activeNodeID) return console.warn('没有选中节点')
this.stateManager.nodeManager.copyNodeById(activeNodeID)
}
/** 处理删除 */
handleDelete(event: any, activeNodeID: string) {
event.preventDefault()
if (!activeNodeID) return console.warn('没有选中节点')
this.stateManager.deleteNode(activeNodeID, { isElMessageBox: true })
}
/** 处理键盘事件 */
handleKeyDown(event: any) {
const activeNodeID = this.stateManager.activeNodeID.value;
// const shiftKey
const ctrl = event.ctrlKey ? 'ctrl-' : "";
const shift = event.shiftKey ? 'shift-' : "";
const key = event.key;
const reg = new RegExp(`^${ctrl}${shift}${key}$`, 'i')
const list = [
{ key: "ctrl-c", handler: () => this.handleCopy(event, activeNodeID) },
{ key: "delete", handler: () => this.handleDelete(event, activeNodeID) },
{ key: "ctrl-z", handler: () => this.stateManager.undoState() },
{ key: "ctrl-shift-z", handler: () => this.stateManager.redoState() },
]
list.forEach((v: any) => {
if (reg.test(v.key)) v.handler(event)
})
}
/** 注册事件 */
registerEvents() {
// document.addEventListener('copy', this.handleCopy.bind(this))
document.addEventListener('keydown', this.handleKeyDown.bind(this))
}
/** 删除事件 */
removeEvents() {
// document.removeEventListener('copy', this.handleCopy.bind(this))
document.removeEventListener('keydown', this.handleKeyDown.bind(this))
}
}

View File

@@ -0,0 +1,26 @@
export class FlowManager {
stateManager: any
vueFlow: any
constructor(options) {
this.stateManager = options.stateManager;
this.vueFlow = options.vueFlow
}
setZoom(zoom: number) {
this.stateManager.zoom.value = zoom
this.vueFlow.value.zoomTo(zoom)
}
getNodeById(id: string) {
return this.vueFlow.value.getNode(id)
}
getLastNode() {
const lastNode = this.stateManager.getLastNode()
if (lastNode?.id) {
return this.vueFlow.value.getNode(lastNode.id)
}
return null;
}
getSubordNodeByID(id: string) {
return this.vueFlow.value.getNodes?.find((v) => v.data.superiorID === id)
}
}

View File

@@ -0,0 +1,133 @@
import { createId } from '../../tools/tools'
import { NODE_DATATYPE, NODE_COMPONENT, NODE_DATATIER } from '../tools/index.d'
interface NodeData {
type?: string
component?: any// 节点组件
data?: object// 节点数据
tier?: string// 节点层级
isHeader?: boolean// 是否显示头
superiorID?: string// 上级节点ID
disableDelete?: boolean// 是否禁用删除
disableCopy?: boolean// 是否禁用复制
}
interface NodeOptions {
id?: string
position?: { x: number, y: number }
positionX?: number
positionY?: number
component?: any
data?: NodeData
}// 不可传入type class (内部使用)
export class NodeManager {
stateManager: any
vueFlow: any
nodesep = 100 // 节点间距
ranksep = 100 // 层级间距
constructor(options) {
this.stateManager = options.stateManager;
this.vueFlow = options.vueFlow
}
/** 删除节点 */
deleteNode(id: string) {
this.stateManager.deleteNode(id)
}
/** 添加节点 */
addNode(node: any) {
this.stateManager.addNode(node)
}
/** 创建节点 */
createNode(options: NodeOptions) {
const superiorID = options?.data?.superiorID
const snode = superiorID ? this.stateManager.flowManager.getNodeById(superiorID) : this.stateManager.flowManager.getLastNode();
const id = options.id || createId()
const positionX = options.positionX || 0
const positionY = options.positionY || 0
const position = options.position ||
(!snode ?
{ x: positionX, y: positionY } :
{
x: snode.position.x + snode.dimensions.width + this.nodesep + positionX,
y: snode.position.y + positionY
})
const data = options?.data || {}
data['component'] = options.component
const options_ = {
id,
position,
data
}
this.addNode(options_)
return options_;
}
/** 创建结果节点 */
createResultNode(options?: NodeOptions) {
const options_ = {
...(options ? options : {}),
component: NODE_COMPONENT.RESULT_IMAGE,
data: {
tier: NODE_DATATIER.RESULT_IMAGE,
type: NODE_DATATYPE.RESULT_IMAGE,
isHeader: true,
...(options?.data || {}),
},
}
return this.createNode(options_)
}
/** 创建卡片选择节点 */
createCardsSelect(options?: NodeOptions) {
const options_ = {
...(options ? options : {}),
component: NODE_COMPONENT.CARD,
positionY: 50,
data: {
tier: NODE_DATATIER.CARDS_SELECT,
type: NODE_DATATYPE.CARDS_SELECT,
...(options?.data || {}),
},
}
return this.createNode(options_)
}
/** 创建卡片节点 */
createCardNode(options?: NodeOptions) {
const options_ = {
...(options ? options : {}),
component: NODE_COMPONENT.CARD,
data: {
...(options?.data || {}),
}
}
return this.createNode(options_)
}
/** 创建文本节点 */
createTextNode(options?: NodeOptions) {
const options_ = {
...(options ? options : {}),
component: NODE_COMPONENT.TEXT,
data: {
...(options?.data || {}),
}
}
return this.createNode(options_)
}
copyNodeById(id: string) {
const node = this.stateManager.getNodeById(id)
const flowNode = this.stateManager.flowManager.getNodeById(id)
if (!node) return console.warn(`${id}找不到对应节点`)
if (node.data?.disableCopy) return console.warn(`${id}节点已禁用复制`)
const node_ = {
...JSON.parse(JSON.stringify(node)),
id: createId(),
position: {
x: node.position.x,
y: node.position.y + (flowNode?.dimensions?.height || 0) + this.ranksep,
}
}
delete node_.data?.superiorID
delete node_.data?.disableDelete
this.stateManager.addNode(node_)
}
}

View File

@@ -0,0 +1,186 @@
import { ref, computed } from "vue";
import { NODE_TYPE } from '../tools/index.d'
import { ElMessageBox } from 'element-plus'
import i18n from '@/lang'
const t = i18n.global.t
export interface NodesItem {
id: string
type: string
class: string
position: { x: number, y: number }
data: { component: any, type: string, superiorID?: string }
}
export class StateManager {
vueFlow: any
activeNodeID: any
nodes: any
nodes_: any
edges: any
zoom: any
tool: any
cursor: any
// 节点是否可拖动
nodesDraggable: any
// 拖动时是否可以平移画布
panOnDrag: any
// 历史记录-撤回/重做
mxHistory: any
historyList: any
historyIndex: any
// 管理器
eventManager: any
flowManager: any
nodeManager: any
toolManager: any
// 设置管理器
setManager(options) {
options.eventManager && (this.eventManager = options.eventManager)
options.flowManager && (this.flowManager = options.flowManager)
options.nodeManager && (this.nodeManager = options.nodeManager)
options.toolManager && (this.toolManager = options.toolManager)
}
constructor(options) {
this.vueFlow = options.vueFlow
this.zoom = ref(1)
this.tool = ref("")
this.cursor = ref("")
this.nodesDraggable = ref(false)
this.panOnDrag = ref(false)
this.mxHistory = ref(50)
this.historyList = ref([])
this.historyIndex = ref(0)
this.activeNodeID = ref("")
this.nodes = ref<NodesItem[]>([]);
this.nodes_ = computed(() => {
return this.nodes.value.map((node, index) => {
const obj = node;
const superiorID = node.data.superiorID;
const isSuperior = this.nodes.value.some((v) => v.id === superiorID)
const isSubord = this.nodes.value.some((v) => v.data.superiorID === node.id)
if (!isSuperior && isSubord) {// 没有上级 有下级
obj.type = NODE_TYPE.INPUT;
} else if (isSuperior && isSubord) {// 有上级 有下级
obj.type = NODE_TYPE.SECONDARY;
} else if (isSuperior && !isSubord) {// 有上级 没有下级
obj.type = NODE_TYPE.OUTPUT;
} else {// 其他情况-没有上级 没有下级
obj.type = NODE_TYPE.ALONE;
}
return obj
})
})
this.edges = computed(() => {
const arr = []
this.nodes.value.forEach((node, index) => {
const superiorID = node.data.superiorID;
const isSuperior = this.nodes.value.some((v) => v.id === superiorID)
if (superiorID && isSuperior) {
const source = node.data.superiorID
const target = node.id
arr.push({
id: `el-${source}-${target}`,
source: source,
target: target,
selectable: false,
type: 'default'
})
}
})
return arr
})
}
/** 设置激活节点 */
setActiveNodeID(id: string) { this.activeNodeID.value = id }
/** 添加节点 */
addNode(node: NodesItem) {
this.nodes.value.push(node);
this.recordState()
}
/** 删除节点 */
async deleteNode(id: string, { isElMessageBox } = { isElMessageBox: false }) {
const node = this.getNodeById(id)
if (!node) return console.warn(`没有找到指定id:${id}`)
if (node.data.disableDelete) return console.warn('该节点禁用删除')
let deletePromise: any = true
if (isElMessageBox) {
deletePromise = await new Promise<void>((resolve, reject) => {
ElMessageBox.confirm(
t('flowCanvas.deleteCardConfirm'),
'',
{
confirmButtonText: t('flowCanvas.confirm'),
cancelButtonText: t('flowCanvas.cancel'),
}
).then(() => {
resolve(true)
}).catch(() => {
resolve(false)
})
})
}
if (!deletePromise) return console.log('删除操作被取消')
this.nodes.value = this.nodes.value.filter((node: NodesItem) => node.id !== id)
this.recordState()
}
/** 获取节点 */
getNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.id === id) }
/** 获取下级节点 */
getSubordNodeByID(id: string) { return this.nodes.value.find((node: NodesItem) => node.data.superiorID === id) }
getLastNode() { return this.nodes.value[this.nodes.value.length - 1] }
/** 设置工具 */
setTool(tool: string) { this.tool.value = tool }
/** 设置光标 */
setCursor(v: string) { this.cursor.value = v }
/** 设置节点是否可拖动 */
setNodesDraggable(v: boolean) { this.nodesDraggable.value = v }
/** 设置是否可以平移画布 */
setPanOnDrag(v: boolean) { this.panOnDrag.value = v }
/** 设置节点层级至最顶部 */
bringToFont(id) {
const fromIndex = this.nodes.value.findIndex(item => item.id === id)
if (fromIndex === -1) return console.warn(`没有找到指定id:${id}`)
this.nodes.value.splice(this.nodes.value.length - 1, 0, ...this.nodes.value.splice(fromIndex, 1))
}
/** 设置节点层级至最低部 */
sendToBack(id) {
const fromIndex = this.nodes.value.findIndex(item => item.id === id)
if (fromIndex === -1) return console.warn(`没有找到指定id:${id}`)
this.nodes.value.splice(0, 0, ...this.nodes.value.splice(fromIndex, 1))
}
/** 记录状态 */
recordState() {
if (this.historyIndex.value < this.historyList.value.length - 1) {
this.historyList.value.splice(this.historyIndex.value + 1)
}
const state = {
nodes: JSON.stringify(this.nodes.value)
}
this.historyList.value.push(state)
const size = this.historyList.value.length - this.mxHistory.value
if (size > 0) this.historyList.value.splice(0, size)
this.historyIndex.value = this.historyList.value.length - 1
}
/** 撤回状态 */
undoState() {
var index = this.historyIndex.value - 1
const state = this.historyList.value[index]
if (!state) return
this.historyIndex.value = index
this.nodes.value = JSON.parse(state.nodes)
}
/** 重做状态 */
redoState() {
var index = this.historyIndex.value + 1
const state = this.historyList.value[index]
if (!state) return
this.historyIndex.value = index
this.nodes.value = JSON.parse(state.nodes)
}
}

View File

@@ -0,0 +1,52 @@
export const TOOLS = {
SELECT: "SELECT",
MOVE: "MOVE",
BRUSH: "BRUSH",
ERASER: "ERASER",
IMAGE: "IMAGE",
SELECTBOX: "SELECTBOX",
RECTANGLE: "RECTANGLE",
TEXT: "TEXT",
UNDO: "UNDO",
REDO: "REDO",
}
export const tools = [
/** 选择工具 */
{
name: TOOLS.SELECT,
nodesDraggable: true,
panOnDrag: false,
},
/** 移动工具 */
{
name: TOOLS.MOVE,
nodesDraggable: false,
panOnDrag: true,
},
/** 文本工具 */
{
name: TOOLS.TEXT,
cursor: "text",
nodesDraggable: false,
panOnDrag: false,
},
]
export class ToolManager {
stateManager: any
vueFlow: any
constructor(options) {
this.stateManager = options.stateManager;
this.vueFlow = options.vueFlow
this.setTool(TOOLS.SELECT)
}
setTool(value: string) {
const tool = tools.find((t) => t.name === value)
if (!tool) return console.warn(`工具${tool}不存在`)
this.stateManager.tool.value = tool.name
this.stateManager.setNodesDraggable(!!tool.nodesDraggable)
this.stateManager.setPanOnDrag(!!tool.panOnDrag)
this.stateManager.setCursor(tool.cursor || "")
}
}