条件装饰节点和条件装饰器结合

This commit is contained in:
YHH
2025-06-18 18:21:55 +08:00
parent 343f5a44f2
commit 5a06f5420b
17 changed files with 1916 additions and 1134 deletions

View File

@@ -18,6 +18,17 @@ export function useAppState() {
const selectedNodeId = ref<string | null>(null);
const nodeSearchText = ref('');
// 调试:检查条件节点模板
console.log('🔍 条件节点模板检查:');
nodeTemplates.filter(t => t.category === 'condition').forEach(template => {
console.log(` ${template.name}: isDraggableCondition=${template.isDraggableCondition}`);
});
console.log('🎭 装饰器节点模板检查:');
nodeTemplates.filter(t => t.category === 'decorator').forEach(template => {
console.log(` ${template.name}: type=${template.type}`);
});
// 画布状态
const canvasWidth = ref(800);
const canvasHeight = ref(600);

View File

@@ -8,6 +8,7 @@ import { useFileOperations } from './useFileOperations';
import { useConnectionManager } from './useConnectionManager';
import { useCanvasManager } from './useCanvasManager';
import { useNodeDisplay } from './useNodeDisplay';
import { useConditionAttachment } from './useConditionAttachment';
import { validateTree as validateTreeStructure } from '../utils/nodeUtils';
/**
@@ -118,6 +119,11 @@ export function useBehaviorTreeEditor() {
const nodeDisplay = useNodeDisplay();
const conditionAttachment = useConditionAttachment(
appState.treeNodes,
appState.getNodeByIdLocal
);
const dragState = reactive({
isDragging: false,
dragNode: null as any,
@@ -340,6 +346,8 @@ export function useBehaviorTreeEditor() {
}
};
onMounted(() => {
const appContainer = document.querySelector('#behavior-tree-app');
if (appContainer) {
@@ -370,16 +378,51 @@ export function useBehaviorTreeEditor() {
event.preventDefault();
connectionManager.cancelConnection();
}
// Escape键取消条件拖拽
if (event.key === 'Escape' && conditionAttachment.dragState.isDraggingCondition) {
event.preventDefault();
conditionAttachment.resetDragState();
}
};
// 全局拖拽结束处理
const handleGlobalDragEnd = (event: DragEvent) => {
console.log('🔚 全局拖拽结束,是否正在拖拽条件:', conditionAttachment.dragState.isDraggingCondition);
if (conditionAttachment.dragState.isDraggingCondition) {
setTimeout(() => {
console.log('⏰ 延迟重置拖拽状态');
conditionAttachment.resetDragState();
}, 100); // 延迟重置确保drop事件先执行
}
};
// 全局拖拽监听器用于调试
const handleGlobalDragOver = (event: DragEvent) => {
if (conditionAttachment.dragState.isDraggingCondition) {
console.log('🌐 全局dragover鼠标位置:', event.clientX, event.clientY, '目标:', event.target);
}
};
const handleGlobalDrop = (event: DragEvent) => {
if (conditionAttachment.dragState.isDraggingCondition) {
console.log('🌐 全局drop事件目标:', event.target, '位置:', event.clientX, event.clientY);
}
};
document.addEventListener('load-behavior-tree-file', handleLoadBehaviorTreeFile as EventListener);
document.addEventListener('file-load-error', handleFileLoadError as EventListener);
document.addEventListener('keydown', handleKeydown);
document.addEventListener('dragend', handleGlobalDragEnd);
document.addEventListener('dragover', handleGlobalDragOver);
document.addEventListener('drop', handleGlobalDrop);
onUnmounted(() => {
document.removeEventListener('load-behavior-tree-file', handleLoadBehaviorTreeFile as EventListener);
document.removeEventListener('file-load-error', handleFileLoadError as EventListener);
document.removeEventListener('keydown', handleKeydown);
document.removeEventListener('dragend', handleGlobalDragEnd);
document.removeEventListener('dragover', handleGlobalDragOver);
document.removeEventListener('drop', handleGlobalDrop);
// 清理暴露的方法
if (appContainer) {
@@ -412,6 +455,57 @@ export function useBehaviorTreeEditor() {
dragState,
autoLayout,
validateTree,
clearAllConnections
clearAllConnections,
// 条件吸附功能
conditionDragState: conditionAttachment.dragState,
startConditionDrag: conditionAttachment.startConditionDrag,
handleDecoratorDragOver: conditionAttachment.handleDecoratorDragOver,
handleDecoratorDragLeave: conditionAttachment.handleDecoratorDragLeave,
attachConditionToDecorator: conditionAttachment.attachConditionToDecorator,
getConditionDisplayText: conditionAttachment.getConditionDisplayText,
removeConditionFromDecorator: conditionAttachment.removeConditionFromDecorator,
canAcceptCondition: conditionAttachment.canAcceptCondition,
resetDragState: conditionAttachment.resetDragState,
// 合并的画布拖拽处理
handleCanvasDrop: (event: DragEvent) => {
// 先尝试条件拖拽处理
if (conditionAttachment.handleCanvasDrop(event)) {
return; // 如果是条件拖拽,直接返回
}
// 否则使用正常的节点拖拽处理
nodeOps.onCanvasDrop(event);
},
// 条件节点拖拽处理
handleConditionNodeDragStart: (event: DragEvent, template: any) => {
console.log('🎯 条件节点拖拽事件:', template.name, template.isDraggableCondition);
if (template.isDraggableCondition) {
conditionAttachment.startConditionDrag(event, template);
} else {
nodeOps.onNodeDragStart(event, template);
}
},
// 节点拖拽事件处理
handleNodeDrop: (event: DragEvent, node: any) => {
console.log('📦 节点拖拽放置:', node.name, node.type, 'isDraggingCondition:', conditionAttachment.dragState.isDraggingCondition);
if (node.type === 'conditional-decorator') {
event.preventDefault();
event.stopPropagation();
return conditionAttachment.attachConditionToDecorator(event, node);
}
},
handleNodeDragOver: (event: DragEvent, node: any) => {
console.log('🔄 节点拖拽悬停:', node.name, node.type, 'isDraggingCondition:', conditionAttachment.dragState.isDraggingCondition);
if (node.type === 'conditional-decorator') {
event.preventDefault();
event.stopPropagation();
return conditionAttachment.handleDecoratorDragOver(event, node);
}
},
handleNodeDragLeave: (event: DragEvent, node: any) => {
console.log('🔙 节点拖拽离开:', node.name, node.type);
if (node.type === 'conditional-decorator') {
conditionAttachment.handleDecoratorDragLeave(node);
}
}
};
}

View File

@@ -0,0 +1,344 @@
import { ref, reactive, Ref } from 'vue';
import { TreeNode } from '../types';
import { NodeTemplate } from '../data/nodeTemplates';
/**
* 拖拽状态
*/
interface DragState {
isDraggingCondition: boolean;
conditionTemplate: NodeTemplate | null;
mousePosition: { x: number, y: number } | null;
hoveredDecoratorId: string | null;
}
/**
* 条件节点吸附功能
*/
export function useConditionAttachment(
treeNodes: Ref<TreeNode[]>,
getNodeByIdLocal: (id: string) => TreeNode | undefined
) {
const dragState = reactive<DragState>({
isDraggingCondition: false,
conditionTemplate: null,
mousePosition: null,
hoveredDecoratorId: null
});
/**
* 检查节点是否为条件装饰器
*/
const isConditionalDecorator = (node: TreeNode): boolean => {
return node.type === 'conditional-decorator';
};
/**
* 开始拖拽条件节点
*/
const startConditionDrag = (event: DragEvent, template: NodeTemplate) => {
console.log('🎯 开始条件拖拽:', template.name, template.isDraggableCondition);
if (!template.isDraggableCondition) {
console.warn('节点不是可拖拽条件:', template.name);
return;
}
dragState.isDraggingCondition = true;
dragState.conditionTemplate = template;
if (event.dataTransfer) {
event.dataTransfer.setData('application/json', JSON.stringify({
...template,
isConditionDrag: true
}));
event.dataTransfer.effectAllowed = 'copy';
}
console.log('✅ 条件拖拽状态已设置:', dragState);
};
/**
* 处理拖拽悬停在装饰器上
*/
const handleDecoratorDragOver = (event: DragEvent, decoratorNode: TreeNode) => {
console.log('🔀 装饰器拖拽悬停:', decoratorNode.name, decoratorNode.type, 'isDragging:', dragState.isDraggingCondition);
// 检查传输数据
const transferData = event.dataTransfer?.getData('application/json');
if (transferData) {
try {
const data = JSON.parse(transferData);
console.log('📦 传输数据:', data.isConditionDrag, data.isDraggableCondition, data.name);
} catch (e) {
console.log('❌ 传输数据解析失败:', transferData);
}
}
if (!dragState.isDraggingCondition || !isConditionalDecorator(decoratorNode)) {
console.log('❌ 不符合条件:', {
isDragging: dragState.isDraggingCondition,
isDecorator: isConditionalDecorator(decoratorNode),
nodeType: decoratorNode.type
});
return false;
}
event.preventDefault();
event.stopPropagation();
dragState.hoveredDecoratorId = decoratorNode.id;
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
console.log('✅ 装饰器可接受拖拽:', decoratorNode.name);
return true;
};
/**
* 处理拖拽离开装饰器
*/
const handleDecoratorDragLeave = (decoratorNode: TreeNode) => {
if (dragState.hoveredDecoratorId === decoratorNode.id) {
dragState.hoveredDecoratorId = null;
}
};
/**
* 条件到装饰器属性的映射
*/
const mapConditionToDecoratorProperties = (conditionTemplate: NodeTemplate): Record<string, any> => {
const baseConfig = {
conditionType: getConditionTypeFromTemplate(conditionTemplate),
shouldReevaluate: true
};
switch (conditionTemplate.type) {
case 'condition-random':
return {
...baseConfig,
successProbability: conditionTemplate.properties?.successProbability?.value || 0.5
};
case 'condition-component':
return {
...baseConfig,
componentType: conditionTemplate.properties?.componentType?.value || 'Component'
};
case 'condition-tag':
return {
...baseConfig,
tagValue: conditionTemplate.properties?.tagValue?.value || 0
};
case 'condition-active':
return {
...baseConfig,
checkHierarchy: conditionTemplate.properties?.checkHierarchy?.value || true
};
case 'condition-numeric':
return {
...baseConfig,
propertyPath: conditionTemplate.properties?.propertyPath?.value || 'context.someValue',
compareOperator: conditionTemplate.properties?.compareOperator?.value || 'greater',
compareValue: conditionTemplate.properties?.compareValue?.value || 0
};
case 'condition-property':
return {
...baseConfig,
propertyPath: conditionTemplate.properties?.propertyPath?.value || 'context.someProperty'
};
case 'condition-custom':
return {
...baseConfig,
conditionCode: conditionTemplate.properties?.conditionCode?.value || '(context) => true'
};
default:
return baseConfig;
}
};
/**
* 获取条件类型字符串
*/
const getConditionTypeFromTemplate = (template: NodeTemplate): string => {
const typeMap: Record<string, string> = {
'condition-random': 'random',
'condition-component': 'hasComponent',
'condition-tag': 'hasTag',
'condition-active': 'isActive',
'condition-numeric': 'numericCompare',
'condition-property': 'propertyExists',
'condition-custom': 'custom'
};
return typeMap[template.type] || 'custom';
};
/**
* 执行条件吸附到装饰器
*/
const attachConditionToDecorator = (
event: DragEvent,
decoratorNode: TreeNode
): boolean => {
console.log('🎯 执行条件吸附:', decoratorNode.name, dragState.conditionTemplate?.name);
event.preventDefault();
event.stopPropagation();
if (!dragState.isDraggingCondition || !dragState.conditionTemplate) {
console.log('❌ 拖拽状态无效:', {
isDragging: dragState.isDraggingCondition,
hasTemplate: !!dragState.conditionTemplate
});
return false;
}
if (!isConditionalDecorator(decoratorNode)) {
console.log('❌ 不是条件装饰器:', decoratorNode.type);
return false;
}
// 获取条件配置
const conditionConfig = mapConditionToDecoratorProperties(dragState.conditionTemplate);
console.log('📝 条件配置:', conditionConfig);
// 更新装饰器属性
if (!decoratorNode.properties) {
decoratorNode.properties = {};
}
Object.assign(decoratorNode.properties, conditionConfig);
// 标记装饰器已附加条件
decoratorNode.attachedCondition = {
type: dragState.conditionTemplate.type,
name: dragState.conditionTemplate.name,
icon: dragState.conditionTemplate.icon
};
console.log('✅ 条件吸附成功!', decoratorNode.attachedCondition);
// 重置拖拽状态
resetDragState();
return true;
};
/**
* 处理画布拖拽事件(阻止条件节点创建为独立节点)
*/
const handleCanvasDrop = (event: DragEvent): boolean => {
const templateData = event.dataTransfer?.getData('application/json');
if (!templateData) return false;
try {
const data = JSON.parse(templateData);
// 如果是条件拖拽,阻止创建独立节点
if (data.isConditionDrag || data.isDraggableCondition) {
event.preventDefault();
resetDragState();
return true;
}
} catch (error) {
// 忽略解析错误
}
return false;
};
/**
* 重置拖拽状态
*/
const resetDragState = () => {
console.log('🔄 重置拖拽状态');
dragState.isDraggingCondition = false;
dragState.conditionTemplate = null;
dragState.mousePosition = null;
dragState.hoveredDecoratorId = null;
};
/**
* 获取条件显示文本
*/
const getConditionDisplayText = (decoratorNode: TreeNode): string => {
if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
return '';
}
const conditionType = decoratorNode.properties.conditionType;
switch (conditionType) {
case 'random':
const probability = decoratorNode.properties.successProbability || 0.5;
return `${(probability * 100).toFixed(0)}%概率`;
case 'hasComponent':
return `${decoratorNode.properties.componentType || 'Component'}`;
case 'hasTag':
return `标签=${decoratorNode.properties.tagValue || 0}`;
case 'isActive':
const checkHierarchy = decoratorNode.properties.checkHierarchy;
return checkHierarchy ? '激活(含层级)' : '激活';
case 'numericCompare':
const path = decoratorNode.properties.propertyPath || 'value';
const operator = decoratorNode.properties.compareOperator || '>';
const value = decoratorNode.properties.compareValue || 0;
return `${path} ${operator} ${value}`;
case 'propertyExists':
return `存在${decoratorNode.properties.propertyPath || 'property'}`;
case 'custom':
return '自定义条件';
default:
return decoratorNode.attachedCondition.name;
}
};
/**
* 移除装饰器的条件
*/
const removeConditionFromDecorator = (decoratorNode: TreeNode) => {
if (decoratorNode.attachedCondition) {
delete decoratorNode.attachedCondition;
// 完全清空所有属性,回到初始空白状态
decoratorNode.properties = {};
}
};
/**
* 检查装饰器是否可以接受条件吸附
*/
const canAcceptCondition = (decoratorNode: TreeNode): boolean => {
return isConditionalDecorator(decoratorNode);
};
return {
dragState,
startConditionDrag,
handleDecoratorDragOver,
handleDecoratorDragLeave,
attachConditionToDecorator,
handleCanvasDrop,
resetDragState,
getConditionDisplayText,
removeConditionFromDecorator,
canAcceptCondition,
isConditionalDecorator
};
}

View File

@@ -26,7 +26,12 @@ export function useNodeOperations(
// 拖拽事件处理
const onNodeDragStart = (event: DragEvent, template: NodeTemplate) => {
if (event.dataTransfer) {
event.dataTransfer.setData('application/json', JSON.stringify(template));
// 检查是否为条件节点,如果是则标记为条件拖拽
const dragData = {
...template,
isConditionDrag: template.isDraggableCondition || false
};
event.dataTransfer.setData('application/json', JSON.stringify(dragData));
event.dataTransfer.effectAllowed = 'copy';
}
};
@@ -45,7 +50,14 @@ export function useNodeOperations(
if (!templateData) return;
try {
const template: NodeTemplate = JSON.parse(templateData);
const dragData = JSON.parse(templateData);
// 如果是条件节点拖拽,阻止创建独立节点
if (dragData.isConditionDrag || dragData.isDraggableCondition) {
return; // 条件节点不能作为独立节点创建
}
const template: NodeTemplate = dragData;
const canvasElement = event.currentTarget as HTMLElement;
const { x, y } = getCanvasCoords(event, canvasElement);

View File

@@ -26,6 +26,9 @@ export interface NodeTemplate {
properties?: Record<string, PropertyDefinition>;
className?: string; // 对应的实际类名
namespace?: string; // 命名空间
// 条件节点相关
isDraggableCondition?: boolean; // 是否为可拖拽的条件节点
attachableToDecorator?: boolean; // 是否可以吸附到条件装饰器
}
/**
@@ -126,7 +129,24 @@ export const nodeTemplates: NodeTemplate[] = [
canHaveParent: true,
minChildren: 2,
className: 'RandomSelector',
namespace: 'behaviourTree/composites'
namespace: 'behaviourTree/composites',
properties: {
reshuffleOnRestart: {
name: '重启时重新洗牌',
type: 'boolean',
value: true,
description: '是否在每次重新开始时都重新洗牌子节点顺序',
required: false
},
abortType: {
name: '中止类型',
type: 'select',
value: 'None',
options: ['None', 'LowerPriority', 'Self', 'Both'],
description: '决定节点在何种情况下会被中止',
required: false
}
}
},
{
type: 'random-sequence',
@@ -138,7 +158,24 @@ export const nodeTemplates: NodeTemplate[] = [
canHaveParent: true,
minChildren: 2,
className: 'RandomSequence',
namespace: 'behaviourTree/composites'
namespace: 'behaviourTree/composites',
properties: {
reshuffleOnRestart: {
name: '重启时重新洗牌',
type: 'boolean',
value: true,
description: '是否在每次重新开始时都重新洗牌子节点顺序',
required: false
},
abortType: {
name: '中止类型',
type: 'select',
value: 'None',
options: ['None', 'LowerPriority', 'Self', 'Both'],
description: '决定节点在何种情况下会被中止',
required: false
}
}
},
// 装饰器节点 (Decorators) - 只能有一个子节点
@@ -155,18 +192,25 @@ export const nodeTemplates: NodeTemplate[] = [
className: 'Repeater',
namespace: 'behaviourTree/decorators',
properties: {
repeatCount: {
count: {
name: '重复次数',
type: 'number',
value: -1,
description: '重复执行次数,-1表示无限重复',
description: '重复执行次数,-1表示无限重复,必须是正整数',
required: true
},
repeatForever: {
name: '无限重复',
endOnFailure: {
name: '失败时停止',
type: 'boolean',
value: true,
description: '是否无限重复执行',
value: false,
description: '子节点失败时是否停止重复',
required: false
},
endOnSuccess: {
name: '成功时停止',
type: 'boolean',
value: false,
description: '子节点成功时是否停止重复',
required: false
}
}
@@ -241,22 +285,14 @@ export const nodeTemplates: NodeTemplate[] = [
name: '条件装饰器',
icon: '🔀',
category: 'decorator',
description: '基于条件执行子节点',
description: '基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)',
canHaveChildren: true,
canHaveParent: true,
maxChildren: 1,
minChildren: 1,
className: 'ConditionalDecorator',
namespace: 'behaviourTree/decorators',
properties: {
conditionCode: {
name: '条件代码',
type: 'code',
value: '(context) => true',
description: '条件判断函数代码',
required: true
}
}
properties: {}
},
// 动作节点 (Actions) - 叶子节点,不能有子节点
@@ -304,14 +340,14 @@ export const nodeTemplates: NodeTemplate[] = [
name: '等待时间',
type: 'number',
value: 1.0,
description: '等待时间(秒)',
description: '等待时间(秒)必须大于0',
required: true
},
randomVariance: {
name: '随机变化',
type: 'number',
value: 0.0,
description: '时间的随机变化量',
useExternalTime: {
name: '使用外部时间',
type: 'boolean',
value: false,
description: '是否使用上下文提供的deltaTime否则使用内部时间计算',
required: false
}
}
@@ -348,7 +384,7 @@ export const nodeTemplates: NodeTemplate[] = [
{
type: 'behavior-tree-reference',
name: '行为树引用',
icon: '🌳',
icon: '🔗',
category: 'action',
description: '运行另一个行为树',
canHaveChildren: false,
@@ -367,39 +403,44 @@ export const nodeTemplates: NodeTemplate[] = [
}
},
// 条件节点 (基础条件) - 叶子节点,不能有子节点
// 条件节点 (可拖拽到条件装饰器上吸附)
{
type: 'execute-conditional',
name: '执行条件',
icon: '',
type: 'condition-random',
name: '随机概率',
icon: '🎲',
category: 'condition',
description: '执行自定义条件判断',
description: '基于概率的随机条件 (拖拽到条件装饰器上使用)',
canHaveChildren: false,
canHaveParent: true,
canHaveParent: false, // 不能作为常规子节点
maxChildren: 0,
className: 'ExecuteActionConditional',
isDraggableCondition: true, // 标记为可拖拽的条件
attachableToDecorator: true, // 可以吸附到装饰器
className: 'RandomProbability',
namespace: 'behaviourTree/conditionals',
properties: {
conditionCode: {
name: '条件代码',
type: 'code',
value: '(context) => {\n // 在这里编写条件判断逻辑\n return TaskStatus.Success; // 或 TaskStatus.Failure\n}',
description: '条件判断函数代码',
successProbability: {
name: '成功概率',
type: 'number',
value: 0.5,
description: '返回成功的概率 (0.0 - 1.0)',
required: true
}
}
},
// ECS专用节点 - 都是叶子节点
{
type: 'has-component',
name: '检查组件',
icon: '🔍',
category: 'ecs',
description: '检查实体是否包含指定组件',
type: 'condition-component',
name: '组件检查',
icon: '🔍📦',
category: 'condition',
description: '检查实体是否指定组件 (拖拽到条件装饰器上使用)',
canHaveChildren: false,
canHaveParent: true,
canHaveParent: false,
maxChildren: 0,
isDraggableCondition: true,
attachableToDecorator: true,
className: 'HasComponentCondition',
namespace: 'ecs-integration/behaviors',
properties: {
@@ -412,6 +453,145 @@ export const nodeTemplates: NodeTemplate[] = [
}
}
},
{
type: 'condition-tag',
name: '标签检查',
icon: '🏷️',
category: 'condition',
description: '检查实体标签 (拖拽到条件装饰器上使用)',
canHaveChildren: false,
canHaveParent: false,
maxChildren: 0,
isDraggableCondition: true,
attachableToDecorator: true,
className: 'HasTagCondition',
namespace: 'ecs-integration/behaviors',
properties: {
tagValue: {
name: '标签值',
type: 'number',
value: 0,
description: '要检查的标签值',
required: true
}
}
},
{
type: 'condition-active',
name: '激活状态',
icon: '👁️',
category: 'condition',
description: '检查实体激活状态 (拖拽到条件装饰器上使用)',
canHaveChildren: false,
canHaveParent: false,
maxChildren: 0,
isDraggableCondition: true,
attachableToDecorator: true,
className: 'IsActiveCondition',
namespace: 'ecs-integration/behaviors',
properties: {
checkHierarchy: {
name: '检查层级',
type: 'boolean',
value: true,
description: '是否检查层级激活状态',
required: false
}
}
},
{
type: 'condition-numeric',
name: '数值比较',
icon: '🔢',
category: 'condition',
description: '数值比较条件 (拖拽到条件装饰器上使用)',
canHaveChildren: false,
canHaveParent: false,
maxChildren: 0,
isDraggableCondition: true,
attachableToDecorator: true,
className: 'NumericComparison',
namespace: 'behaviourTree/conditionals',
properties: {
propertyPath: {
name: '属性路径',
type: 'string',
value: 'context.someValue',
description: '要比较的属性路径',
required: true
},
compareOperator: {
name: '比较操作符',
type: 'select',
value: 'greater',
options: ['greater', 'less', 'equal', 'greaterEqual', 'lessEqual', 'notEqual'],
description: '数值比较操作符',
required: true
},
compareValue: {
name: '比较值',
type: 'number',
value: 0,
description: '用于比较的数值',
required: true
}
}
},
{
type: 'condition-property',
name: '属性存在',
icon: '📋',
category: 'condition',
description: '检查属性是否存在 (拖拽到条件装饰器上使用)',
canHaveChildren: false,
canHaveParent: false,
maxChildren: 0,
isDraggableCondition: true,
attachableToDecorator: true,
className: 'PropertyExists',
namespace: 'behaviourTree/conditionals',
properties: {
propertyPath: {
name: '属性路径',
type: 'string',
value: 'context.someProperty',
description: '要检查的属性路径',
required: true
}
}
},
{
type: 'condition-custom',
name: '自定义条件',
icon: '⚙️',
category: 'condition',
description: '自定义代码条件 (拖拽到条件装饰器上使用)',
canHaveChildren: false,
canHaveParent: false,
maxChildren: 0,
isDraggableCondition: true,
attachableToDecorator: true,
className: 'ExecuteActionConditional',
namespace: 'behaviourTree/conditionals',
properties: {
conditionCode: {
name: '条件代码',
type: 'code',
value: '(context) => {\n // 条件判断逻辑\n return true; // 返回 true/false\n}',
description: '条件判断函数代码',
required: true
},
conditionName: {
name: '条件名称',
type: 'string',
value: '',
description: '用于调试的条件名称',
required: false
}
}
},
// ECS专用节点 - 动作节点
{
type: 'add-component',
name: '添加组件',
@@ -489,48 +669,7 @@ export const nodeTemplates: NodeTemplate[] = [
}
}
},
{
type: 'has-tag',
name: '检查标签',
icon: '🏷️',
category: 'ecs',
description: '检查实体是否具有指定标签',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'HasTagCondition',
namespace: 'ecs-integration/behaviors',
properties: {
tag: {
name: '标签值',
type: 'number',
value: 0,
description: '要检查的标签值',
required: true
}
}
},
{
type: 'is-active',
name: '检查激活状态',
icon: '🔋',
category: 'ecs',
description: '检查实体是否处于激活状态',
canHaveChildren: false,
canHaveParent: true,
maxChildren: 0,
className: 'IsActiveCondition',
namespace: 'ecs-integration/behaviors',
properties: {
checkHierarchy: {
name: '检查层级',
type: 'boolean',
value: true,
description: '是否检查层级激活状态',
required: false
}
}
},
{
type: 'wait-time',
name: 'ECS等待',

View File

@@ -18,30 +18,23 @@ let currentPanelInstance: any = null;
* 面板定义
*/
const panelDefinition = {
/**
* 面板模板
*/
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/index.html'), 'utf-8'),
/**
* 面板样式
*/
style: readFileSync(join(__dirname, '../../../static/style/behavior-tree/index.css'), 'utf-8'),
style: [
readFileSync(join(__dirname, '../../../static/style/behavior-tree/base.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/toolbar.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/panels.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/canvas.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/nodes.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/conditions.css'), 'utf-8'),
readFileSync(join(__dirname, '../../../static/style/behavior-tree/modals.css'), 'utf-8')
].join('\n'),
/**
* 选择器
*/
$: {
app: '#behavior-tree-app',
},
/**
* 面板方法 - 用于处理来自扩展主进程的消息
*/
methods: {
/**
* 加载行为树文件
*/
async loadBehaviorTreeFile(assetInfo: any) {
try {
const filePath = assetInfo?.file || assetInfo?.path;
@@ -113,9 +106,6 @@ const panelDefinition = {
},
},
/**
* 面板准备完成时调用
*/
ready() {
currentPanelInstance = this;

View File

@@ -10,12 +10,18 @@ export interface TreeNode {
y: number;
children: string[];
parent?: string;
properties?: Record<string, PropertyDefinition>;
properties?: Record<string, any>; // 改为any以支持动态属性值
canHaveChildren: boolean;
canHaveParent: boolean;
maxChildren?: number; // 最大子节点数量限制
minChildren?: number; // 最小子节点数量要求
hasError?: boolean;
// 条件装饰器相关
attachedCondition?: {
type: string;
name: string;
icon: string;
};
}
export interface Connection {

View File

@@ -31,7 +31,7 @@ export function createNodeFromTemplate(template: NodeTemplate, x: number = 100,
const node: TreeNode = {
id: nodeId,
type: template.className || template.type,
type: template.type,
name: template.name,
icon: template.icon,
description: template.description,

View File

@@ -0,0 +1,75 @@
/* 基础样式和布局 */
/* 全局重置和通用样式 */
* {
box-sizing: border-box;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #1a202c;
}
::-webkit-scrollbar-thumb {
background: #4a5568;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #667eea;
}
/* 选择文本样式 */
::selection {
background: rgba(102, 126, 234, 0.3);
color: #ffffff;
}
#behavior-tree-app {
display: flex;
flex-direction: column;
height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1e1e1e;
color: #ffffff;
overflow: hidden;
}
/* 编辑器容器 */
.editor-container {
display: flex;
flex: 1;
overflow: hidden;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.nodes-panel {
width: 240px;
}
.properties-panel {
width: 280px;
}
}
@media (max-width: 900px) {
.editor-container {
flex-direction: column;
}
.nodes-panel, .properties-panel {
width: 100%;
height: 200px;
}
.canvas-container {
flex: 1;
min-height: 400px;
}
}

View File

@@ -0,0 +1,149 @@
/* 画布区域样式 */
.canvas-container {
flex: 1;
display: flex;
flex-direction: column;
background: #1a202c;
overflow: hidden;
}
.canvas-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #2d3748;
border-bottom: 1px solid #4a5568;
}
.zoom-controls, .canvas-actions {
display: flex;
gap: 8px;
align-items: center;
}
.zoom-controls button, .canvas-actions button {
padding: 4px 8px;
background: #4a5568;
border: 1px solid #718096;
border-radius: 4px;
color: white;
cursor: pointer;
font-size: 11px;
transition: background 0.3s ease;
}
.zoom-controls button:hover, .canvas-actions button:hover {
background: #667eea;
}
.canvas-area {
flex: 1;
position: relative;
overflow: hidden;
background: #1a202c;
background-image:
radial-gradient(circle, #2d3748 1px, transparent 1px);
background-size: 20px 20px;
background-position: 0 0;
cursor: grab;
}
.behavior-tree-canvas {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
/* 连接线样式 */
.connection-layer {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
overflow: visible;
}
.connection-line {
fill: none;
stroke: #667eea;
stroke-width: 2;
pointer-events: all;
cursor: pointer;
transition: all 0.3s ease;
}
.connection-line:hover {
stroke: #f6ad55;
stroke-width: 3;
filter: drop-shadow(0 0 6px rgba(246, 173, 85, 0.6));
}
.connection-active {
stroke: #48bb78;
stroke-dasharray: 8, 4;
animation: flow 2s linear infinite;
}
@keyframes flow {
0% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: 12; }
}
.connection-temp {
fill: none;
stroke: #f6ad55;
stroke-width: 2;
stroke-dasharray: 5, 5;
animation: dash 1s linear infinite;
opacity: 0.8;
pointer-events: none;
}
@keyframes dash {
0% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: 10; }
}
/* 节点层 */
.nodes-layer {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
transform-origin: 0 0;
width: 100%;
height: 100%;
}
/* 网格背景 */
.grid-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0.3;
}
/* 画布状态 */
.canvas-area.connecting {
cursor: crosshair;
}
.canvas-area.connecting .tree-node {
pointer-events: none;
}
.canvas-area.connecting .port {
pointer-events: all;
cursor: pointer;
}
.drag-over {
background: rgba(102, 126, 234, 0.1);
border: 2px dashed #667eea;
}

View File

@@ -0,0 +1,156 @@
/* 条件拖拽功能样式 */
/* 可拖拽条件节点 */
.node-item.draggable-condition {
border-left: 3px solid #ffd700;
position: relative;
}
.node-item.draggable-condition .drag-hint {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 10px;
color: #ffd700;
animation: bounce-hint 2s infinite;
}
@keyframes bounce-hint {
0%, 20%, 50%, 80%, 100% {
transform: translateY(-50%);
}
40% {
transform: translateY(-60%);
}
60% {
transform: translateY(-40%);
}
}
/* 条件装饰器接受状态 */
.tree-node.can-accept-condition {
border: 2px dashed #ffd700;
animation: pulse-accept 1.5s infinite;
}
@keyframes pulse-accept {
0%, 100% {
border-color: #ffd700;
box-shadow: 0 0 5px rgba(255, 215, 0, 0.3);
}
50% {
border-color: #ffed4e;
box-shadow: 0 0 15px rgba(255, 215, 0, 0.6);
}
}
.tree-node.condition-hover {
border-color: #ffd700;
background: rgba(255, 215, 0, 0.1);
transform: scale(1.02);
}
/* 条件附加区域 */
.condition-attachment-area {
margin-top: 8px;
padding: 8px;
border-radius: 4px;
min-height: 32px;
}
.condition-placeholder {
text-align: center;
padding: 12px;
border: 2px dashed #4a5568;
border-radius: 4px;
color: #a0aec0;
font-size: 11px;
transition: all 0.3s ease;
}
.tree-node.can-accept-condition .condition-placeholder {
border-color: #ffd700;
color: #ffd700;
background: rgba(255, 215, 0, 0.05);
}
.attached-condition {
background: rgba(255, 215, 0, 0.1);
border: 1px solid #ffd700;
border-radius: 4px;
padding: 6px;
}
.condition-info {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
}
.condition-icon {
font-size: 12px;
}
.condition-text {
flex: 1;
color: #ffd700;
font-weight: 500;
}
.remove-condition-btn {
background: none;
border: none;
color: #e53e3e;
cursor: pointer;
font-size: 14px;
padding: 2px 4px;
border-radius: 2px;
transition: all 0.2s ease;
}
.remove-condition-btn:hover {
background: rgba(229, 62, 62, 0.2);
transform: scale(1.1);
}
/* 画布状态 */
.canvas-area.condition-dragging {
background: rgba(255, 215, 0, 0.02);
}
.canvas-area.condition-dragging .tree-node:not(.can-accept-condition) {
opacity: 0.6;
}
/* 条件装饰器节点的特殊样式 */
.tree-node.node-conditional-decorator .condition-attachment-area {
border: 1px solid #9f7aea;
background: rgba(159, 122, 234, 0.05);
}
.tree-node.node-conditional-decorator.node-selected .condition-attachment-area {
border-color: #ffd700;
background: rgba(255, 215, 0, 0.1);
}
/* 条件附加成功的动画效果 */
@keyframes condition-attached {
0% {
transform: scale(1);
background: rgba(255, 215, 0, 0.3);
}
50% {
transform: scale(1.05);
background: rgba(255, 215, 0, 0.5);
}
100% {
transform: scale(1);
background: rgba(255, 215, 0, 0.1);
}
}
.condition-just-attached {
animation: condition-attached 0.6s ease-out;
}

View File

@@ -0,0 +1,133 @@
/* 模态框和状态栏样式 */
/* 状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #2d3748;
border-top: 1px solid #4a5568;
font-size: 12px;
color: #a0aec0;
}
.status-left, .status-right {
display: flex;
gap: 16px;
}
.status-valid {
color: #48bb78;
}
.status-invalid {
color: #e53e3e;
}
/* 模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
}
.modal-content {
background: #2d3748;
border-radius: 8px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
max-width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
border: 1px solid #4a5568;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #4a5568;
background: #4a5568;
border-radius: 8px 8px 0 0;
}
.modal-header h3 {
margin: 0;
color: #e2e8f0;
font-size: 16px;
}
.modal-header button {
background: none;
border: none;
color: #a0aec0;
cursor: pointer;
font-size: 18px;
}
.modal-body {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.export-options {
margin-bottom: 20px;
}
.export-options label {
display: block;
margin-bottom: 8px;
color: #e2e8f0;
font-size: 14px;
font-weight: 500;
}
.code-output {
background: #1a202c;
border: 1px solid #4a5568;
border-radius: 4px;
padding: 16px;
height: 400px;
overflow: auto;
}
.code-output pre {
margin: 0;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
color: #e2e8f0;
white-space: pre-wrap;
}
.modal-footer {
display: flex;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid #4a5568;
justify-content: flex-end;
}
.modal-footer button {
padding: 8px 16px;
background: #667eea;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
font-size: 12px;
transition: background 0.3s ease;
}
.modal-footer button:hover {
background: #5a67d8;
}

View File

@@ -0,0 +1,322 @@
/* 节点样式 */
.tree-node {
position: absolute;
width: 160px;
min-height: 80px;
background: #2d3748;
border: 2px solid #4a5568;
border-radius: 8px;
pointer-events: all;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 1;
animation: nodeAppear 0.3s ease-out;
}
.tree-node:hover {
border-color: #667eea;
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
transform: translateY(-2px);
}
.tree-node.node-selected {
border-color: #f6ad55;
box-shadow: 0 0 20px rgba(246, 173, 85, 0.4);
}
.tree-node.node-error {
border-color: #e53e3e;
background: rgba(229, 62, 62, 0.1);
}
.node-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #4a5568;
border-radius: 6px 6px 0 0;
border-bottom: 1px solid #718096;
}
.node-header .node-icon {
font-size: 16px;
flex-shrink: 0;
}
.node-title {
flex: 1;
font-weight: 600;
font-size: 12px;
color: #e2e8f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-delete {
background: none;
border: none;
color: #e53e3e;
cursor: pointer;
font-size: 16px;
font-weight: bold;
padding: 2px 4px;
border-radius: 3px;
transition: all 0.2s ease;
flex-shrink: 0;
}
.node-delete:hover {
background: rgba(229, 62, 62, 0.2);
transform: scale(1.1);
}
.node-body {
padding: 8px 12px;
color: #a0aec0;
font-size: 11px;
line-height: 1.4;
max-width: 136px;
overflow: hidden;
}
.node-description {
margin-bottom: 6px;
color: #cbd5e0;
font-style: italic;
}
/* 端口样式 */
.port {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
cursor: pointer;
transform: translateX(-50%);
transition: all 0.3s ease;
z-index: 10;
opacity: 0.8;
}
.port::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border-radius: 50%;
background: transparent;
transition: all 0.3s ease;
}
.port:hover {
transform: translateX(-50%) scale(1.3);
opacity: 1;
}
.port-input {
top: -6px;
left: 50%;
background: #4299e1;
border: 2px solid #2b6cb0;
}
.port-input:hover {
background: #63b3ed;
box-shadow: 0 0 12px rgba(66, 153, 225, 0.6);
}
.port-output {
bottom: -6px;
left: 50%;
background: #48bb78;
border: 2px solid #2f855a;
}
.port-output:hover {
background: #68d391;
box-shadow: 0 0 12px rgba(72, 187, 120, 0.6);
}
.port.connecting {
animation: pulse-port 1s ease-in-out infinite;
transform: translateX(-50%) scale(1.4);
}
@keyframes pulse-port {
0%, 100% {
box-shadow: 0 0 8px rgba(246, 173, 85, 0.4);
}
50% {
box-shadow: 0 0 20px rgba(246, 173, 85, 0.8);
transform: translateX(-50%) scale(1.6);
}
}
.port.drag-target {
animation: pulse-target 0.8s ease-in-out infinite;
background: #f6ad55 !important;
border-color: #dd6b20 !important;
transform: translateX(-50%) scale(1.5);
}
@keyframes pulse-target {
0%, 100% {
box-shadow: 0 0 12px rgba(246, 173, 85, 0.6);
}
50% {
box-shadow: 0 0 24px rgba(246, 173, 85, 1);
transform: translateX(-50%) scale(1.7);
}
}
.port-inner {
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
}
.port:hover .port-inner {
background: rgba(255, 255, 255, 0.6);
}
.port.connecting .port-inner {
background: rgba(255, 255, 255, 0.8);
}
.port.drag-target .port-inner {
background: rgba(255, 255, 255, 0.9);
}
.children-indicator {
position: absolute;
bottom: 2px;
right: 6px;
background: #667eea;
color: white;
border-radius: 50%;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
font-weight: bold;
z-index: 5;
}
/* 节点属性预览 */
.node-properties-preview {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
font-size: 10px;
}
.property-preview-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
padding: 1px 0;
}
.property-label {
color: #a0aec0;
font-weight: 500;
flex-shrink: 0;
margin-right: 4px;
font-size: 9px;
}
.property-value {
color: #e2e8f0;
font-weight: 600;
text-align: right;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 9px;
}
/* 不同类型属性的颜色 */
.property-value.property-boolean {
color: #68d391;
}
.property-value.property-number {
color: #63b3ed;
}
.property-value.property-select {
color: #f6ad55;
}
.property-value.property-string {
color: #cbd5e0;
}
.property-value.property-code {
color: #d69e2e;
font-family: 'Consolas', 'Monaco', monospace;
}
/* 当节点有属性时稍微增加高度空间 */
.tree-node:has(.node-properties-preview) {
min-height: 100px;
}
/* 节点悬停时的端口显示优化 */
.tree-node:hover .port {
opacity: 1;
transform: translateX(-50%) scale(1.1);
}
.tree-node .port {
opacity: 0.8;
transition: all 0.2s ease;
}
/* 拖动状态样式 - 优化硬件加速 */
.tree-node.dragging {
opacity: 0.9;
transform: translateZ(0) scale(1.02) rotate(1deg);
box-shadow: 0 12px 30px rgba(0,0,0,0.4);
z-index: 1000;
cursor: grabbing;
will-change: transform, opacity;
backface-visibility: hidden;
transform-style: preserve-3d;
transition: none !important;
border-color: #67b7dc;
transform-origin: center center;
perspective: 1000px;
}
.dragging {
opacity: 0.7;
transform: rotate(5deg);
}
/* 动画效果 */
@keyframes nodeAppear {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}

View File

@@ -0,0 +1,251 @@
/* 侧边面板样式 */
/* 左侧节点面板 */
.nodes-panel {
width: 280px;
background: #2d3748;
border-right: 1px solid #4a5568;
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
padding: 16px;
background: #4a5568;
border-bottom: 1px solid #718096;
}
.panel-header h3 {
margin: 0 0 12px 0;
font-size: 16px;
color: #e2e8f0;
}
.search-input {
width: 100%;
padding: 8px 12px;
background: #1a202c;
border: 1px solid #4a5568;
border-radius: 4px;
color: white;
font-size: 12px;
}
.search-input::placeholder {
color: #a0aec0;
}
.node-categories {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.category {
margin-bottom: 16px;
}
.category-title {
margin: 0 0 8px 0;
padding: 8px 12px;
background: #4a5568;
border-radius: 4px;
font-size: 14px;
color: #e2e8f0;
}
.node-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.node-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #1a202c;
border: 1px solid #4a5568;
border-radius: 4px;
cursor: grab;
transition: all 0.3s ease;
font-size: 12px;
position: relative;
}
.node-item:hover {
background: #2d3748;
border-color: #667eea;
transform: translateX(4px);
}
.node-item:active {
cursor: grabbing;
}
.node-item.root { border-left: 3px solid #f6ad55; }
.node-item.composite { border-left: 3px solid #667eea; }
.node-item.decorator { border-left: 3px solid #9f7aea; }
.node-item.action { border-left: 3px solid #48bb78; }
.node-item.condition { border-left: 3px solid #ed8936; }
.node-item.ecs { border-left: 3px solid #38b2ac; }
.node-icon {
font-size: 14px;
flex-shrink: 0;
}
.node-name {
font-weight: 500;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 右侧属性面板 */
.properties-panel {
width: 320px;
background: #2d3748;
border-left: 1px solid #4a5568;
display: flex;
flex-direction: column;
overflow: hidden;
}
.node-properties {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.property-section {
margin-bottom: 20px;
}
.property-section h4 {
margin: 0 0 12px 0;
color: #e2e8f0;
font-size: 14px;
}
.property-item {
margin-bottom: 12px;
}
.property-item label {
display: block;
margin-bottom: 4px;
color: #a0aec0;
font-size: 12px;
font-weight: 500;
}
.property-item input,
.property-item textarea,
.property-item select {
width: 100%;
padding: 8px 12px;
background: #1a202c;
border: 1px solid #4a5568;
border-radius: 4px;
color: white;
font-size: 12px;
font-family: inherit;
}
.property-item input:focus,
.property-item textarea:focus,
.property-item select:focus {
border-color: #667eea;
outline: none;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.property-item .code-input {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
resize: vertical;
min-height: 80px;
}
.property-help {
margin-top: 4px;
font-size: 10px;
color: #718096;
line-height: 1.4;
}
.code-preview {
background: #1a202c;
border: 1px solid #4a5568;
border-radius: 4px;
padding: 12px;
margin-top: 8px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
color: #e2e8f0;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
}
.no-selection {
padding: 40px 20px;
text-align: center;
color: #718096;
font-style: italic;
}
.tree-structure {
margin-top: 20px;
}
.structure-tree {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
}
.empty-tree {
padding: 20px;
text-align: center;
color: #718096;
font-style: italic;
border: 2px dashed #4a5568;
border-radius: 4px;
}
.tree-node-item {
cursor: pointer;
}
.tree-node-line {
padding: 2px 4px;
border-radius: 2px;
margin: 1px 0;
display: flex;
align-items: center;
gap: 4px;
transition: background 0.2s ease;
}
.tree-node-line:hover {
background: rgba(102, 126, 234, 0.1);
}
.tree-node-icon {
font-size: 10px;
width: 12px;
}
.tree-node-name {
flex: 1;
color: #e2e8f0;
}
.tree-node-type {
font-size: 9px;
color: #718096;
}

View File

@@ -0,0 +1,89 @@
/* 头部工具栏样式 */
.header-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-bottom: 2px solid #4a5568;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 20px;
}
.toolbar-left h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.current-file {
color: #a0aec0;
font-weight: 400;
font-size: 16px;
margin-left: 8px;
}
.unsaved-indicator {
color: #ff6b6b;
animation: pulse-unsaved 2s infinite;
margin-left: 8px;
}
@keyframes pulse-unsaved {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.toolbar-buttons {
display: flex;
gap: 8px;
}
.tool-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 6px;
color: white;
cursor: pointer;
transition: all 0.3s ease;
font-size: 12px;
}
.tool-btn:hover {
background: rgba(255,255,255,0.2);
transform: translateY(-1px);
}
.tool-btn.has-changes {
background: rgba(255, 107, 107, 0.2);
border-color: #ff6b6b;
animation: glow-save 2s infinite;
}
@keyframes glow-save {
0%, 100% {
box-shadow: 0 0 5px rgba(255, 107, 107, 0.5);
}
50% {
box-shadow: 0 0 15px rgba(255, 107, 107, 0.8);
}
}
.toolbar-right .install-status {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
background: rgba(255,255,255,0.1);
border-radius: 8px;
}

View File

@@ -127,12 +127,14 @@
v-for="node in filteredConditionNodes()"
:key="node.type"
class="node-item condition"
:class="{ 'draggable-condition': node.isDraggableCondition }"
:draggable="true"
@dragstart="onNodeDragStart($event, node)"
:title="node.description"
@dragstart="handleConditionNodeDragStart($event, node)"
:title="node.description + (node.isDraggableCondition ? ' (可拖拽到条件装饰器)' : '')"
>
<span class="node-icon">{{ node.icon }}</span>
<span class="node-name">{{ node.name }}</span>
<span v-if="node.isDraggableCondition" class="drag-hint">🎯</span>
</div>
</div>
</div>
@@ -177,8 +179,11 @@
<div
ref="canvasAreaRef"
class="canvas-area"
:class="{ 'connecting': dragState.isConnecting }"
@drop="onCanvasDrop"
:class="{
'connecting': dragState.isConnecting,
'condition-dragging': conditionDragState.isDraggingCondition
}"
@drop="handleCanvasDrop"
@dragover="onCanvasDragOver"
@wheel="onCanvasWheel"
@mousedown="onCanvasMouseDown"
@@ -232,7 +237,9 @@
{
'node-selected': selectedNodeId === node.id,
'node-error': node.hasError,
'dragging': dragState.dragNode && dragState.dragNode.id === node.id
'dragging': dragState.dragNode && dragState.dragNode.id === node.id,
'condition-hover': conditionDragState.hoveredDecoratorId === node.id,
'can-accept-condition': canAcceptCondition(node) && conditionDragState.isDraggingCondition
}
]"
:style="{
@@ -241,6 +248,9 @@
}"
@click="selectNode(node.id)"
@mousedown="startNodeDrag($event, node)"
@drop="handleNodeDrop($event, node)"
@dragover="handleNodeDragOver($event, node)"
@dragleave="handleNodeDragLeave($event, node)"
>
<div class="node-header">
<span class="node-icon">{{ node.icon }}</span>
@@ -249,6 +259,25 @@
</div>
<div class="node-body">
<div v-if="node.description" class="node-description">{{ node.description }}</div>
<!-- 条件装饰器的条件显示 -->
<div v-if="node.type === 'conditional-decorator'" class="condition-attachment-area">
<div v-if="node.attachedCondition" class="attached-condition">
<div class="condition-info">
<span class="condition-icon">{{ node.attachedCondition.icon }}</span>
<span class="condition-text">{{ getConditionDisplayText(node) }}</span>
<button
class="remove-condition-btn"
@click.stop="removeConditionFromDecorator(node)"
title="移除条件"
>×</button>
</div>
</div>
<div v-else class="condition-placeholder">
<span class="placeholder-text">🎯 拖拽条件到此处</span>
</div>
</div>
<!-- 节点属性预览 -->
<div v-if="hasVisibleProperties(node)" class="node-properties-preview">
<div