条件装饰节点和条件装饰器结合
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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等待',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user