支持先加入实体后加入系统以让matcher进行实体匹配/优化行为树节点效果及逻辑
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ref } from 'vue';
|
||||
import { TreeNode, DragState, Connection } from '../types';
|
||||
import { nodeTemplates } from '../data/nodeTemplates';
|
||||
import { allNodeTemplates as nodeTemplates } from '../data/nodeTemplates';
|
||||
|
||||
/**
|
||||
* 应用状态管理
|
||||
|
||||
@@ -36,7 +36,11 @@ export function useBehaviorTreeEditor() {
|
||||
appState.treeNodes,
|
||||
appState.nodeTemplates,
|
||||
appState.getNodeByIdLocal,
|
||||
getRootNode
|
||||
getRootNode,
|
||||
computed(() => blackboard.blackboardVariables.value.reduce((map, variable) => {
|
||||
map.set(variable.name, variable);
|
||||
return map;
|
||||
}, new Map()))
|
||||
);
|
||||
|
||||
const computedProps = useComputedProperties(
|
||||
@@ -115,7 +119,14 @@ export function useBehaviorTreeEditor() {
|
||||
tempConnection: appState.tempConnection,
|
||||
showExportModal: appState.showExportModal,
|
||||
codeGeneration: codeGen,
|
||||
updateConnections: connectionManager.updateConnections
|
||||
updateConnections: connectionManager.updateConnections,
|
||||
blackboardOperations: {
|
||||
getBlackboardVariables: () => blackboard.blackboardVariables.value,
|
||||
loadBlackboardVariables: (variables: any[]) => {
|
||||
blackboard.loadBlackboardFromArray(variables);
|
||||
},
|
||||
clearBlackboard: blackboard.clearBlackboard
|
||||
}
|
||||
});
|
||||
|
||||
const canvasManager = useCanvasManager(
|
||||
@@ -250,6 +261,51 @@ export function useBehaviorTreeEditor() {
|
||||
}
|
||||
};
|
||||
|
||||
// 节点类型识别相关方法
|
||||
const getOriginalNodeName = (nodeType: string): string => {
|
||||
const template = appState.nodeTemplates.value.find(t => t.type === nodeType);
|
||||
return template?.name || nodeType;
|
||||
};
|
||||
|
||||
const getNodeTemplate = (nodeType: string) => {
|
||||
return appState.nodeTemplates.value.find(t => t.type === nodeType);
|
||||
};
|
||||
|
||||
const getNodeCategory = (nodeType: string): string => {
|
||||
const template = getNodeTemplate(nodeType);
|
||||
if (!template) return 'unknown';
|
||||
|
||||
const category = template.category || 'unknown';
|
||||
const categoryMap: Record<string, string> = {
|
||||
'root': '根节点',
|
||||
'composite': '组合',
|
||||
'decorator': '装饰器',
|
||||
'action': '动作',
|
||||
'condition': '条件',
|
||||
'ecs': 'ECS'
|
||||
};
|
||||
|
||||
return categoryMap[category] || category;
|
||||
};
|
||||
|
||||
const isNodeNameCustomized = (node: any): boolean => {
|
||||
if (!node) return false;
|
||||
const originalName = getOriginalNodeName(node.type);
|
||||
return node.name !== originalName;
|
||||
};
|
||||
|
||||
const resetNodeToOriginalName = () => {
|
||||
if (!appState.selectedNodeId.value) return;
|
||||
|
||||
const selectedNode = appState.getNodeByIdLocal(appState.selectedNodeId.value);
|
||||
if (!selectedNode) return;
|
||||
|
||||
const originalName = getOriginalNodeName(selectedNode.type);
|
||||
nodeOps.updateNodeProperty('name', originalName);
|
||||
|
||||
console.log(`节点名称已重置为原始名称: ${originalName}`);
|
||||
};
|
||||
|
||||
const startNodeDrag = (event: MouseEvent, node: any) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -306,12 +362,13 @@ export function useBehaviorTreeEditor() {
|
||||
installation.handleInstall();
|
||||
};
|
||||
|
||||
// 自动布局功能
|
||||
// 紧凑子树布局算法 - 体现行为树的层次结构
|
||||
const autoLayout = () => {
|
||||
if (appState.treeNodes.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到根节点
|
||||
const rootNode = appState.treeNodes.value.find(node =>
|
||||
!appState.treeNodes.value.some(otherNode =>
|
||||
otherNode.children?.includes(node.id)
|
||||
@@ -319,57 +376,225 @@ export function useBehaviorTreeEditor() {
|
||||
);
|
||||
|
||||
if (!rootNode) {
|
||||
console.warn('未找到根节点,无法进行自动布局');
|
||||
return;
|
||||
}
|
||||
|
||||
const levelNodes: { [level: number]: any[] } = {};
|
||||
const visited = new Set<string>();
|
||||
|
||||
const queue = [{ node: rootNode, level: 0 }];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { node, level } = queue.shift()!;
|
||||
// 计算节点尺寸
|
||||
const getNodeSize = (node: any) => {
|
||||
let width = 180;
|
||||
let height = 100;
|
||||
|
||||
if (visited.has(node.id)) continue;
|
||||
visited.add(node.id);
|
||||
|
||||
if (!levelNodes[level]) {
|
||||
levelNodes[level] = [];
|
||||
// 根据节点类型调整基础尺寸
|
||||
switch (node.category || node.type) {
|
||||
case 'root':
|
||||
width = 200; height = 70;
|
||||
break;
|
||||
case 'composite':
|
||||
width = 160; height = 90;
|
||||
break;
|
||||
case 'decorator':
|
||||
width = 140; height = 80;
|
||||
break;
|
||||
case 'action':
|
||||
width = 180; height = 100;
|
||||
break;
|
||||
case 'condition':
|
||||
width = 150; height = 85;
|
||||
break;
|
||||
}
|
||||
levelNodes[level].push(node);
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
// 根据属性数量动态调整
|
||||
if (node.properties) {
|
||||
const propertyCount = Object.keys(node.properties).length;
|
||||
height += propertyCount * 20;
|
||||
}
|
||||
|
||||
// 根据名称长度调整宽度
|
||||
if (node.name) {
|
||||
const nameWidth = node.name.length * 8 + 40;
|
||||
width = Math.max(width, nameWidth);
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
// 紧凑子树布局核心算法
|
||||
const layoutSubtree = (node: any, parentX = 0, parentY = 0, depth = 0): { width: number, height: number } => {
|
||||
const nodeSize = getNodeSize(node);
|
||||
|
||||
// 如果是叶子节点,直接返回自身尺寸
|
||||
if (!node.children || node.children.length === 0) {
|
||||
node.x = parentX;
|
||||
node.y = parentY;
|
||||
return { width: nodeSize.width, height: nodeSize.height };
|
||||
}
|
||||
|
||||
// 递归布局所有子节点,收集子树信息
|
||||
const childSubtrees: Array<{ node: any, width: number, height: number }> = [];
|
||||
let totalChildrenWidth = 0;
|
||||
let maxChildHeight = 0;
|
||||
|
||||
const childY = parentY + nodeSize.height + 60; // 子节点距离父节点的垂直间距
|
||||
const siblingSpacing = 40; // 同级子节点间的水平间距
|
||||
|
||||
// 先计算每个子树的尺寸
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
const subtreeInfo = layoutSubtree(childNode, 0, childY, depth + 1);
|
||||
childSubtrees.push({ node: childNode, ...subtreeInfo });
|
||||
totalChildrenWidth += subtreeInfo.width;
|
||||
maxChildHeight = Math.max(maxChildHeight, subtreeInfo.height);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加子节点间的间距
|
||||
if (childSubtrees.length > 1) {
|
||||
totalChildrenWidth += (childSubtrees.length - 1) * siblingSpacing;
|
||||
}
|
||||
|
||||
// 计算父节点的最终位置(在子节点的中心上方)
|
||||
const subtreeWidth = Math.max(nodeSize.width, totalChildrenWidth);
|
||||
node.x = parentX + subtreeWidth / 2 - nodeSize.width / 2;
|
||||
node.y = parentY;
|
||||
|
||||
// 布局子节点(以父节点为中心分布)
|
||||
let currentX = parentX + subtreeWidth / 2 - totalChildrenWidth / 2;
|
||||
|
||||
childSubtrees.forEach(({ node: childNode, width: childWidth }) => {
|
||||
// 将子节点定位到其子树的中心
|
||||
const childCenterOffset = childWidth / 2;
|
||||
childNode.x = currentX + childCenterOffset - getNodeSize(childNode).width / 2;
|
||||
|
||||
// 递归调整子树中所有节点的位置
|
||||
adjustSubtreePosition(childNode, currentX, childY);
|
||||
|
||||
currentX += childWidth + siblingSpacing;
|
||||
});
|
||||
|
||||
// 返回整个子树的尺寸
|
||||
const subtreeHeight = nodeSize.height + 60 + maxChildHeight;
|
||||
return { width: subtreeWidth, height: subtreeHeight };
|
||||
};
|
||||
|
||||
// 递归调整子树位置
|
||||
const adjustSubtreePosition = (node: any, baseX: number, baseY: number) => {
|
||||
const nodeSize = getNodeSize(node);
|
||||
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算子节点的总宽度
|
||||
let totalChildrenWidth = 0;
|
||||
const siblingSpacing = 40;
|
||||
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
const childSubtreeWidth = calculateSubtreeWidth(childNode);
|
||||
totalChildrenWidth += childSubtreeWidth;
|
||||
}
|
||||
});
|
||||
|
||||
if (node.children.length > 1) {
|
||||
totalChildrenWidth += (node.children.length - 1) * siblingSpacing;
|
||||
}
|
||||
|
||||
// 重新定位子节点
|
||||
let currentX = baseX + Math.max(nodeSize.width, totalChildrenWidth) / 2 - totalChildrenWidth / 2;
|
||||
const childY = baseY + nodeSize.height + 60;
|
||||
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
const childSubtreeWidth = calculateSubtreeWidth(childNode);
|
||||
const childCenterOffset = childSubtreeWidth / 2;
|
||||
childNode.x = currentX + childCenterOffset - getNodeSize(childNode).width / 2;
|
||||
childNode.y = childY;
|
||||
|
||||
adjustSubtreePosition(childNode, currentX, childY);
|
||||
currentX += childSubtreeWidth + siblingSpacing;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 计算子树宽度
|
||||
const calculateSubtreeWidth = (node: any): number => {
|
||||
const nodeSize = getNodeSize(node);
|
||||
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return nodeSize.width;
|
||||
}
|
||||
|
||||
let totalChildrenWidth = 0;
|
||||
const siblingSpacing = 40;
|
||||
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
totalChildrenWidth += calculateSubtreeWidth(childNode);
|
||||
}
|
||||
});
|
||||
|
||||
if (node.children.length > 1) {
|
||||
totalChildrenWidth += (node.children.length - 1) * siblingSpacing;
|
||||
}
|
||||
|
||||
return Math.max(nodeSize.width, totalChildrenWidth);
|
||||
};
|
||||
|
||||
// 开始布局 - 从根节点开始
|
||||
const startX = 400; // 画布中心X
|
||||
const startY = 50; // 顶部留白
|
||||
|
||||
const treeInfo = layoutSubtree(rootNode, startX, startY);
|
||||
|
||||
// 处理孤立节点
|
||||
const connectedNodeIds = new Set<string>();
|
||||
const collectConnectedNodes = (node: any) => {
|
||||
connectedNodeIds.add(node.id);
|
||||
if (node.children) {
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode && !visited.has(childId)) {
|
||||
queue.push({ node: childNode, level: level + 1 });
|
||||
if (childNode) {
|
||||
collectConnectedNodes(childNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
collectConnectedNodes(rootNode);
|
||||
|
||||
const orphanNodes = appState.treeNodes.value.filter(node => !connectedNodeIds.has(node.id));
|
||||
if (orphanNodes.length > 0) {
|
||||
const orphanY = startY + treeInfo.height + 100;
|
||||
orphanNodes.forEach((node, index) => {
|
||||
node.x = startX + (index - orphanNodes.length / 2) * 200;
|
||||
node.y = orphanY + Math.floor(index / 5) * 120;
|
||||
});
|
||||
}
|
||||
|
||||
const nodeWidth = 200;
|
||||
const nodeHeight = 150;
|
||||
const startX = 400;
|
||||
const startY = 100;
|
||||
|
||||
Object.keys(levelNodes).forEach(levelStr => {
|
||||
const level = parseInt(levelStr);
|
||||
const nodes = levelNodes[level];
|
||||
const totalWidth = (nodes.length - 1) * nodeWidth;
|
||||
const offsetX = -totalWidth / 2;
|
||||
|
||||
nodes.forEach((node, index) => {
|
||||
node.x = startX + offsetX + index * nodeWidth;
|
||||
node.y = startY + level * nodeHeight;
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// 强制更新连接线
|
||||
const forceUpdateConnections = () => {
|
||||
connectionManager.updateConnections();
|
||||
}, 100);
|
||||
|
||||
nextTick(() => {
|
||||
connectionManager.updateConnections();
|
||||
|
||||
setTimeout(() => {
|
||||
connectionManager.updateConnections();
|
||||
}, 150);
|
||||
});
|
||||
};
|
||||
|
||||
forceUpdateConnections();
|
||||
|
||||
console.log(`紧凑子树布局完成:${appState.treeNodes.value.length} 个节点已重新排列`);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 验证树结构
|
||||
const validateTree = () => {
|
||||
// 使用改进的验证函数
|
||||
@@ -830,6 +1055,13 @@ export function useBehaviorTreeEditor() {
|
||||
handleBlackboardDragLeave,
|
||||
clearBlackboardReference,
|
||||
|
||||
// 节点类型识别方法
|
||||
getOriginalNodeName,
|
||||
getNodeTemplate,
|
||||
getNodeCategory,
|
||||
isNodeNameCustomized,
|
||||
resetNodeToOriginalName,
|
||||
|
||||
blackboardCollapsed: computed({
|
||||
get: () => blackboardSidebarState.collapsed,
|
||||
set: (value: boolean) => blackboardSidebarState.collapsed = value
|
||||
|
||||
@@ -297,6 +297,17 @@ export function useBlackboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const resetBlackboardToDefaults = () => {
|
||||
if (confirm('确定要重置所有变量到默认值吗?')) {
|
||||
blackboardVariables.value.forEach((variable, name) => {
|
||||
if (variable.defaultValue !== undefined) {
|
||||
variable.value = variable.defaultValue;
|
||||
blackboardVariables.value.set(name, { ...variable });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const exportBlackboard = () => {
|
||||
const data = Array.from(blackboardVariables.value.values());
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
@@ -309,6 +320,19 @@ export function useBlackboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadBlackboardFromArray = (variables: BlackboardVariable[]) => {
|
||||
blackboardVariables.value.clear();
|
||||
variables.forEach(variable => {
|
||||
if (variable.name && variable.type) {
|
||||
blackboardVariables.value.set(variable.name, variable);
|
||||
|
||||
// 展开变量所在的组
|
||||
const groupName = variable.group || '未分组';
|
||||
expandedGroups.value.add(groupName);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importBlackboard = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
@@ -436,6 +460,8 @@ export function useBlackboard() {
|
||||
editVariable,
|
||||
selectVariable,
|
||||
clearBlackboard,
|
||||
resetBlackboardToDefaults,
|
||||
loadBlackboardFromArray,
|
||||
exportBlackboard,
|
||||
importBlackboard,
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ export function useCodeGeneration(
|
||||
treeNodes: Ref<TreeNode[]>,
|
||||
nodeTemplates: Ref<NodeTemplate[]>,
|
||||
getNodeByIdLocal: (id: string) => TreeNode | undefined,
|
||||
rootNode: () => TreeNode | null
|
||||
rootNode: () => TreeNode | null,
|
||||
blackboardVariables?: Ref<Map<string, any>>
|
||||
) {
|
||||
|
||||
// 生成行为树配置JSON
|
||||
@@ -20,7 +21,7 @@ export function useCodeGeneration(
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
const config: any = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
metadata: {
|
||||
@@ -30,6 +31,13 @@ export function useCodeGeneration(
|
||||
},
|
||||
tree: generateNodeConfig(root)
|
||||
};
|
||||
|
||||
// 包含黑板数据
|
||||
if (blackboardVariables && blackboardVariables.value.size > 0) {
|
||||
config.blackboard = Array.from(blackboardVariables.value.values());
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// 生成可读的配置JSON字符串
|
||||
|
||||
@@ -462,25 +462,49 @@ export function useConnectionManager(
|
||||
updateConnections();
|
||||
};
|
||||
|
||||
// 更新连接线
|
||||
// 改进的连接线更新方法
|
||||
const updateConnections = () => {
|
||||
// 立即清空现有连接
|
||||
connections.value.length = 0;
|
||||
|
||||
// 添加一个小延迟,确保DOM已经更新
|
||||
setTimeout(() => {
|
||||
treeNodes.value.forEach(node => {
|
||||
if (node.children) {
|
||||
// 创建新的连接数据
|
||||
const newConnections: Connection[] = [];
|
||||
|
||||
// 遍历所有节点建立连接
|
||||
treeNodes.value.forEach(node => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(childId => {
|
||||
const childNode = treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
// 尝试获取端口位置
|
||||
const parentPos = getPortPosition(node.id, 'output');
|
||||
const childPos = getPortPosition(childId, 'input');
|
||||
|
||||
if (parentPos && childPos) {
|
||||
const controlOffset = Math.abs(childPos.y - parentPos.y) * 0.5;
|
||||
// 计算贝塞尔曲线路径
|
||||
const deltaY = Math.abs(childPos.y - parentPos.y);
|
||||
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
|
||||
|
||||
const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`;
|
||||
|
||||
connections.value.push({
|
||||
newConnections.push({
|
||||
id: `${node.id}-${childId}`,
|
||||
sourceId: node.id,
|
||||
targetId: childId,
|
||||
path: path,
|
||||
active: false
|
||||
});
|
||||
} else {
|
||||
// 如果无法获取实际位置,使用计算位置作为后备
|
||||
const fallbackParentPos = getCalculatedPortPosition(node, 'output');
|
||||
const fallbackChildPos = getCalculatedPortPosition(childNode, 'input');
|
||||
|
||||
const deltaY = Math.abs(fallbackChildPos.y - fallbackParentPos.y);
|
||||
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
|
||||
|
||||
const path = `M ${fallbackParentPos.x} ${fallbackParentPos.y} C ${fallbackParentPos.x} ${fallbackParentPos.y + controlOffset} ${fallbackChildPos.x} ${fallbackChildPos.y - controlOffset} ${fallbackChildPos.x} ${fallbackChildPos.y}`;
|
||||
|
||||
newConnections.push({
|
||||
id: `${node.id}-${childId}`,
|
||||
sourceId: node.id,
|
||||
targetId: childId,
|
||||
@@ -492,7 +516,50 @@ export function useConnectionManager(
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 50); // 50ms延迟,确保DOM渲染完成
|
||||
|
||||
// 批量更新连接
|
||||
connections.value.push(...newConnections);
|
||||
|
||||
// 如果有DOM元素,进行二次精确更新
|
||||
if (canvasAreaRef.value) {
|
||||
setTimeout(() => {
|
||||
// 二次更新,使用实际DOM位置
|
||||
const updatedConnections: Connection[] = [];
|
||||
|
||||
treeNodes.value.forEach(node => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(childId => {
|
||||
const childNode = treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
const parentPos = getPortPosition(node.id, 'output');
|
||||
const childPos = getPortPosition(childId, 'input');
|
||||
|
||||
if (parentPos && childPos) {
|
||||
const deltaY = Math.abs(childPos.y - parentPos.y);
|
||||
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
|
||||
|
||||
const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`;
|
||||
|
||||
updatedConnections.push({
|
||||
id: `${node.id}-${childId}`,
|
||||
sourceId: node.id,
|
||||
targetId: childId,
|
||||
path: path,
|
||||
active: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 如果二次更新得到了有效结果,替换连接数据
|
||||
if (updatedConnections.length > 0) {
|
||||
connections.value.length = 0;
|
||||
connections.value.push(...updatedConnections);
|
||||
}
|
||||
}, 100); // 100ms延迟,确保DOM完全渲染
|
||||
}
|
||||
};
|
||||
|
||||
// 删除连接线
|
||||
|
||||
@@ -11,11 +11,17 @@ interface FileOperationOptions {
|
||||
createTreeFromConfig: (config: any) => TreeNode[];
|
||||
};
|
||||
updateConnections?: () => void;
|
||||
blackboardOperations?: {
|
||||
getBlackboardVariables: () => any[];
|
||||
loadBlackboardVariables: (variables: any[]) => void;
|
||||
clearBlackboard: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface FileData {
|
||||
nodes: TreeNode[];
|
||||
connections: Connection[];
|
||||
blackboard?: any[];
|
||||
metadata: {
|
||||
name: string;
|
||||
created: string;
|
||||
@@ -31,7 +37,8 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
tempConnection,
|
||||
showExportModal,
|
||||
codeGeneration,
|
||||
updateConnections
|
||||
updateConnections,
|
||||
blackboardOperations
|
||||
} = options;
|
||||
|
||||
const hasUnsavedChanges = ref(false);
|
||||
@@ -70,7 +77,7 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
};
|
||||
|
||||
const exportBehaviorTreeData = (): FileData => {
|
||||
return {
|
||||
const data: FileData = {
|
||||
nodes: treeNodes.value,
|
||||
connections: connections.value,
|
||||
metadata: {
|
||||
@@ -79,6 +86,16 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
|
||||
// 包含黑板数据
|
||||
if (blackboardOperations) {
|
||||
const blackboardVariables = blackboardOperations.getBlackboardVariables();
|
||||
if (blackboardVariables.length > 0) {
|
||||
data.blackboard = blackboardVariables;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const showMessage = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
@@ -165,6 +182,12 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
selectedNodeId.value = null;
|
||||
connections.value = [];
|
||||
tempConnection.value.path = '';
|
||||
|
||||
// 清空黑板
|
||||
if (blackboardOperations) {
|
||||
blackboardOperations.clearBlackboard();
|
||||
}
|
||||
|
||||
clearCurrentFile();
|
||||
markAsSaved();
|
||||
}
|
||||
@@ -409,6 +432,11 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
setCurrentFile('untitled', filePath);
|
||||
}
|
||||
|
||||
// 加载黑板数据
|
||||
if (blackboardOperations && parsedData.blackboard && Array.isArray(parsedData.blackboard)) {
|
||||
blackboardOperations.loadBlackboardVariables(parsedData.blackboard);
|
||||
}
|
||||
|
||||
selectedNodeId.value = null;
|
||||
tempConnection.value.path = '';
|
||||
|
||||
@@ -463,6 +491,11 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
|
||||
tempConnection.value.path = '';
|
||||
|
||||
// 加载黑板数据
|
||||
if (blackboardOperations && config.blackboard && Array.isArray(config.blackboard)) {
|
||||
blackboardOperations.loadBlackboardVariables(config.blackboard);
|
||||
}
|
||||
|
||||
const fileName = file.name.replace(/\.(json|bt)$/, '');
|
||||
setCurrentFile(fileName, '');
|
||||
|
||||
|
||||
@@ -1106,4 +1106,10 @@ export const nodeTemplates: NodeTemplate[] = [
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
// 导出所有节点模板
|
||||
export const allNodeTemplates: NodeTemplate[] = nodeTemplates;
|
||||
|
||||
// 为了保持向后兼容,保留原来的导出
|
||||
export { nodeTemplates as default };
|
||||
@@ -389,14 +389,15 @@
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: #1a202c;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
border: 2px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
@@ -404,14 +405,35 @@
|
||||
.form-textarea:focus {
|
||||
border-color: #667eea;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.form-input:hover,
|
||||
.form-select:hover,
|
||||
.form-textarea:hover {
|
||||
border-color: #718096;
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 美化表单下拉选择框 */
|
||||
.form-select {
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"><path fill="%23718096" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 16px center;
|
||||
background-size: 12px;
|
||||
padding-right: 48px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-select option {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.form-input::placeholder,
|
||||
@@ -522,8 +544,40 @@
|
||||
}
|
||||
|
||||
.allowed-values textarea {
|
||||
margin-top: 6px;
|
||||
margin-top: 8px;
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
||||
border: 2px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
color: #e2e8f0;
|
||||
padding: 12px 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.allowed-values textarea:focus {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.allowed-values textarea:hover {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.allowed-values textarea::placeholder {
|
||||
color: #718096;
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
|
||||
@@ -328,4 +328,66 @@
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 节点类型提示 */
|
||||
.tree-node[data-original-name]:not([data-original-name=""]):hover::after {
|
||||
content: "原始: " attr(data-original-name);
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #f7fafc;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
animation: tooltipFadeIn 0.3s ease forwards;
|
||||
border: 1px solid #4a5568;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tree-node.node-selected[data-original-name]:not([data-original-name=""]) .node-header::before {
|
||||
content: "📝";
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 8px;
|
||||
border: 2px solid #2d3748;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
@keyframes tooltipFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 节点标题自定义指示器 */
|
||||
.node-title.customized {
|
||||
color: #f6ad55;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.node-title.customized::after {
|
||||
content: " ✏️";
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -235,13 +235,24 @@
|
||||
.property-item select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: #1a202c;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
color: #e2e8f0;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.property-item input:hover,
|
||||
.property-item textarea:hover,
|
||||
.property-item select:hover {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.property-item input:focus,
|
||||
@@ -249,7 +260,26 @@
|
||||
.property-item select:focus {
|
||||
border-color: #667eea;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 美化下拉选择框箭头 */
|
||||
.property-item select {
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"><path fill="%23718096" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 10px;
|
||||
padding-right: 36px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.property-item select option {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.property-item .code-input {
|
||||
@@ -521,6 +551,16 @@
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.blackboard-sidebar .reset-btn {
|
||||
background: #ed8936;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .reset-btn:hover {
|
||||
background: #dd6b20;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.blackboard-sidebar .clear-btn {
|
||||
background: #f56565;
|
||||
color: white;
|
||||
@@ -812,6 +852,112 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 黑板变量值区域样式 */
|
||||
.blackboard-sidebar .variable-value {
|
||||
margin-top: 6px;
|
||||
min-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 美化约束值下拉选择框 */
|
||||
.blackboard-sidebar .variable-value select {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 6px;
|
||||
color: #e2e8f0;
|
||||
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
outline: none;
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"><path fill="%23718096" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 8px;
|
||||
padding-right: 28px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* 下拉框 hover 状态 */
|
||||
.blackboard-sidebar .variable-value select:hover {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 下拉框 focus 状态 */
|
||||
.blackboard-sidebar .variable-value select:focus {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 下拉框选项样式 */
|
||||
.blackboard-sidebar .variable-value select option {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 不同类型变量的下拉框颜色 */
|
||||
.blackboard-sidebar .variable-item.string .variable-value select {
|
||||
border-left: 3px solid #f59e0b;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-item.string .variable-value select:focus {
|
||||
border-left-color: #f59e0b;
|
||||
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2), 0 4px 12px rgba(245, 158, 11, 0.15);
|
||||
}
|
||||
|
||||
/* 约束值下拉框的特殊标识 */
|
||||
.blackboard-sidebar .variable-value select::before {
|
||||
content: '🔒';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 8px;
|
||||
transform: translateY(-50%);
|
||||
font-size: 8px;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 添加约束值提示 */
|
||||
.blackboard-sidebar .variable-item:has(.variable-value select) .variable-constraints {
|
||||
color: #f59e0b;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-item:has(.variable-value select) .variable-constraints::before {
|
||||
content: '🔐 ';
|
||||
font-size: 10px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* 禁用状态样式 */
|
||||
.blackboard-sidebar .variable-value select:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: #1a202c;
|
||||
border-color: #2d3748;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-value select:disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .empty-blackboard {
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
@@ -1098,4 +1244,181 @@
|
||||
font-size: 11px;
|
||||
color: #e83e8c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 节点类型信息区域样式 */
|
||||
.node-type-info {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.node-type-info::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.node-type-info h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
padding: 12px 16px 8px 16px;
|
||||
color: #e2e8f0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.node-type-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.reset-name-btn {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.reset-name-btn:hover {
|
||||
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(217, 119, 6, 0.3);
|
||||
}
|
||||
|
||||
.node-type-details {
|
||||
padding: 12px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.type-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.type-info-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
min-width: 80px;
|
||||
color: #a0aec0;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
color: #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.original-type {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.node-id {
|
||||
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
|
||||
background: rgba(45, 55, 72, 0.8);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
border: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
.original-description {
|
||||
font-style: italic;
|
||||
color: #cbd5e0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.custom-name {
|
||||
font-weight: 600;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
font-size: 9px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.badge-根节点 {
|
||||
background: linear-gradient(135deg, #f6ad55 0%, #ed8936 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-组合 {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-装饰器 {
|
||||
background: linear-gradient(135deg, #9f7aea 0%, #805ad5 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-动作 {
|
||||
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-条件 {
|
||||
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-ecs {
|
||||
background: linear-gradient(135deg, #38b2ac 0%, #319795 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-unknown {
|
||||
background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 名称输入容器样式 */
|
||||
.name-input-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-indicator {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #f59e0b;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -71,6 +71,42 @@
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* 布局组样式 */
|
||||
.layout-group {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.layout-group button {
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255,255,255,0.8);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.layout-group button:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.layout-group button:active {
|
||||
transform: translateY(0);
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tool-btn.has-changes {
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
</div>
|
||||
<div class="canvas-actions">
|
||||
<button @click="centerView">居中</button>
|
||||
<button @click="autoLayout">自动布局</button>
|
||||
<button @click="autoLayout" title="紧凑子树布局 - 子节点紧贴父节点">自动布局</button>
|
||||
<button @click="validateTree">验证</button>
|
||||
<button @click="clearAllConnections" title="清除所有连接线" v-if="connections.length > 0">清除连线</button>
|
||||
</div>
|
||||
@@ -279,6 +279,7 @@
|
||||
v-for="node in treeNodes"
|
||||
:key="node.id"
|
||||
:data-node-id="node.id"
|
||||
:data-original-name="isNodeNameCustomized(node) ? getOriginalNodeName(node.type) : ''"
|
||||
class="tree-node"
|
||||
:class="[
|
||||
'node-' + node.type,
|
||||
@@ -303,7 +304,7 @@
|
||||
>
|
||||
<div class="node-header">
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-title">{{ node.name }}</span>
|
||||
<span class="node-title" :class="{ 'customized': isNodeNameCustomized(node) }">{{ node.name }}</span>
|
||||
<button class="node-delete" @click.stop="deleteNode(node.id)">×</button>
|
||||
</div>
|
||||
<div class="node-body">
|
||||
@@ -424,17 +425,60 @@
|
||||
</div>
|
||||
|
||||
<div v-if="activeNode" class="node-properties">
|
||||
<!-- 节点类型信息区域 -->
|
||||
<div class="property-section node-type-info">
|
||||
<h4>
|
||||
<span class="node-type-icon">{{ activeNode.icon }}</span>
|
||||
节点类型信息
|
||||
<button
|
||||
class="reset-name-btn"
|
||||
@click="resetNodeToOriginalName"
|
||||
:title="'重置为原始名称: ' + getOriginalNodeName(activeNode.type)"
|
||||
v-if="isNodeNameCustomized(activeNode)"
|
||||
>
|
||||
🔄 恢复原名
|
||||
</button>
|
||||
</h4>
|
||||
<div class="node-type-details">
|
||||
<div class="type-info-row">
|
||||
<span class="info-label">原始类型:</span>
|
||||
<span class="info-value original-type">
|
||||
{{ getOriginalNodeName(activeNode.type) }}
|
||||
<span class="type-badge" :class="'badge-' + getNodeCategory(activeNode.type)">
|
||||
{{ getNodeCategory(activeNode.type) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="type-info-row">
|
||||
<span class="info-label">节点ID:</span>
|
||||
<span class="info-value node-id">{{ activeNode.type }}</span>
|
||||
</div>
|
||||
<div class="type-info-row" v-if="getNodeTemplate(activeNode.type)">
|
||||
<span class="info-label">原始描述:</span>
|
||||
<span class="info-value original-description">{{ getNodeTemplate(activeNode.type).description }}</span>
|
||||
</div>
|
||||
<div class="type-info-row" v-if="isNodeNameCustomized(activeNode)">
|
||||
<span class="info-label">⚠️ 自定义名称:</span>
|
||||
<span class="info-value custom-name">{{ activeNode.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-section">
|
||||
<h4>基本信息</h4>
|
||||
<div class="property-item">
|
||||
<label>节点名称:</label>
|
||||
<input
|
||||
type="text"
|
||||
:value="activeNode.name"
|
||||
@input="updateNodeProperty('name', $event.target.value)"
|
||||
:key="activeNode.id + '_name'"
|
||||
:disabled="activeNode.isConditionNode"
|
||||
>
|
||||
<div class="name-input-container">
|
||||
<input
|
||||
type="text"
|
||||
:value="activeNode.name"
|
||||
@input="updateNodeProperty('name', $event.target.value)"
|
||||
:key="activeNode.id + '_name'"
|
||||
:disabled="activeNode.isConditionNode"
|
||||
:placeholder="getOriginalNodeName(activeNode.type)"
|
||||
>
|
||||
<span v-if="isNodeNameCustomized(activeNode)" class="custom-indicator" title="已自定义名称">✏️</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="property-item">
|
||||
<label>描述:</label>
|
||||
@@ -443,6 +487,7 @@
|
||||
@input="updateNodeProperty('description', $event.target.value)"
|
||||
:key="activeNode.id + '_description'"
|
||||
:disabled="activeNode.isConditionNode"
|
||||
:placeholder="getNodeTemplate(activeNode.type)?.description || '请输入节点描述...'"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -722,6 +767,7 @@
|
||||
|
||||
<div class="blackboard-actions">
|
||||
<button @click="showBlackboardModal = true" class="add-var-btn">+ 添加变量</button>
|
||||
<button @click="resetBlackboardToDefaults" v-if="blackboardVariables.length > 0" class="reset-btn" title="重置所有变量到默认值">🔄 重置</button>
|
||||
<button @click="clearBlackboard" v-if="blackboardVariables.length > 0" class="clear-btn">清空</button>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user