更新文档及优化行为树编辑器
This commit is contained in:
@@ -56,7 +56,7 @@ export function useAppState() {
|
||||
|
||||
// UI状态
|
||||
const showExportModal = ref(false);
|
||||
const exportFormat = ref('json');
|
||||
const exportFormat = ref('json'); // 默认JSON格式,TypeScript暂时禁用
|
||||
|
||||
// 工具函数
|
||||
const getNodeByIdLocal = (id: string): TreeNode | undefined => {
|
||||
|
||||
@@ -159,23 +159,51 @@ export function useBehaviorTreeEditor() {
|
||||
|
||||
try {
|
||||
const blackboardData = event.dataTransfer?.getData('application/blackboard-variable');
|
||||
if (!blackboardData) return;
|
||||
|
||||
if (!blackboardData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variable = JSON.parse(blackboardData);
|
||||
const activeNode = computedProps.activeNode.value;
|
||||
|
||||
if (!activeNode || !activeNode.properties) return;
|
||||
|
||||
const property = activeNode.properties[propertyKey];
|
||||
if (!property) return;
|
||||
|
||||
// 设置Blackboard引用
|
||||
const referenceValue = `{{${variable.name}}}`;
|
||||
nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, referenceValue);
|
||||
|
||||
// 移除拖拽样式
|
||||
const element = event.currentTarget as HTMLElement;
|
||||
element.classList.remove('drag-over');
|
||||
// 检查当前是否在编辑条件节点
|
||||
if (appState.selectedConditionNodeId.value) {
|
||||
// 条件节点:直接更新装饰器的属性
|
||||
const decoratorNode = appState.getNodeByIdLocal(appState.selectedConditionNodeId.value);
|
||||
if (decoratorNode) {
|
||||
const referenceValue = `{{${variable.name}}}`;
|
||||
|
||||
if (!decoratorNode.properties) {
|
||||
decoratorNode.properties = {};
|
||||
}
|
||||
decoratorNode.properties[propertyKey] = referenceValue;
|
||||
|
||||
// 强制触发响应式更新
|
||||
const nodeIndex = appState.treeNodes.value.findIndex(n => n.id === decoratorNode.id);
|
||||
if (nodeIndex > -1) {
|
||||
const newNodes = [...appState.treeNodes.value];
|
||||
newNodes[nodeIndex] = { ...decoratorNode };
|
||||
appState.treeNodes.value = newNodes;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 普通节点:使用原来的逻辑
|
||||
const activeNode = computedProps.activeNode.value;
|
||||
|
||||
if (!activeNode || !activeNode.properties) {
|
||||
return;
|
||||
}
|
||||
|
||||
const property = activeNode.properties[propertyKey];
|
||||
|
||||
if (!property) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置Blackboard引用
|
||||
const referenceValue = `{{${variable.name}}}`;
|
||||
nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, referenceValue);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理Blackboard拖拽失败:', error);
|
||||
@@ -187,6 +215,7 @@ export function useBehaviorTreeEditor() {
|
||||
event.stopPropagation();
|
||||
|
||||
const hasBlackboardData = event.dataTransfer?.types.includes('application/blackboard-variable');
|
||||
|
||||
if (hasBlackboardData) {
|
||||
event.dataTransfer!.dropEffect = 'copy';
|
||||
const element = event.currentTarget as HTMLElement;
|
||||
@@ -200,7 +229,25 @@ export function useBehaviorTreeEditor() {
|
||||
};
|
||||
|
||||
const clearBlackboardReference = (propertyKey: string) => {
|
||||
nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, '');
|
||||
// 检查当前是否在编辑条件节点
|
||||
if (appState.selectedConditionNodeId.value) {
|
||||
// 条件节点:直接清除装饰器的属性
|
||||
const decoratorNode = appState.getNodeByIdLocal(appState.selectedConditionNodeId.value);
|
||||
if (decoratorNode && decoratorNode.properties) {
|
||||
decoratorNode.properties[propertyKey] = '';
|
||||
|
||||
// 强制触发响应式更新
|
||||
const nodeIndex = appState.treeNodes.value.findIndex(n => n.id === decoratorNode.id);
|
||||
if (nodeIndex > -1) {
|
||||
const newNodes = [...appState.treeNodes.value];
|
||||
newNodes[nodeIndex] = { ...decoratorNode };
|
||||
appState.treeNodes.value = newNodes;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 普通节点:使用原来的逻辑
|
||||
nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, '');
|
||||
}
|
||||
};
|
||||
|
||||
const startNodeDrag = (event: MouseEvent, node: any) => {
|
||||
@@ -461,62 +508,128 @@ export function useBehaviorTreeEditor() {
|
||||
}
|
||||
};
|
||||
|
||||
// 保存到文件
|
||||
const saveToFile = () => {
|
||||
const code = computedProps.exportedCode();
|
||||
const format = appState.exportFormat.value;
|
||||
const extension = format === 'json' ? '.json' : '.ts';
|
||||
const mimeType = format === 'json' ? 'application/json' : 'text/typescript';
|
||||
|
||||
// 创建文件并下载
|
||||
const blob = new Blob([code], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `behavior_tree_config${extension}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// 显示成功消息
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10001;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.3s ease;
|
||||
`;
|
||||
toast.textContent = `文件已保存: behavior_tree_config${extension}`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '1';
|
||||
toast.style.transform = 'translateX(0)';
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
// 保存到文件 - 使用Cocos Creator扩展API提供保存路径选择
|
||||
const saveToFile = async () => {
|
||||
try {
|
||||
const code = computedProps.exportedCode();
|
||||
const format = appState.exportFormat.value;
|
||||
const extension = format === 'json' ? '.json' : '.ts';
|
||||
const fileType = format === 'json' ? 'JSON配置文件' : 'TypeScript文件';
|
||||
|
||||
// 使用Cocos Creator的文件保存对话框
|
||||
const result = await Editor.Dialog.save({
|
||||
title: `保存${fileType}`,
|
||||
filters: [
|
||||
{
|
||||
name: fileType,
|
||||
extensions: extension === '.json' ? ['json'] : ['ts']
|
||||
},
|
||||
{
|
||||
name: '所有文件',
|
||||
extensions: ['*']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
return; // 用户取消了保存
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
const fs = require('fs-extra');
|
||||
await fs.writeFile(result.filePath, code, 'utf8');
|
||||
|
||||
// 显示成功消息
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10001;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.3s ease;
|
||||
max-width: 400px;
|
||||
word-wrap: break-word;
|
||||
`;
|
||||
|
||||
const path = require('path');
|
||||
const fileName = path.basename(result.filePath);
|
||||
toast.innerHTML = `
|
||||
<div style="font-weight: bold; margin-bottom: 4px;">✅ 文件保存成功</div>
|
||||
<div style="font-size: 12px; opacity: 0.9;">文件名: ${fileName}</div>
|
||||
<div style="font-size: 11px; opacity: 0.7; margin-top: 2px;">路径: ${result.filePath}</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
document.body.removeChild(toast);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
toast.style.opacity = '1';
|
||||
toast.style.transform = 'translateX(0)';
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
document.body.removeChild(toast);
|
||||
}
|
||||
}, 300);
|
||||
}, 4000);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('保存文件失败:', error);
|
||||
|
||||
// 显示错误消息
|
||||
const errorToast = document.createElement('div');
|
||||
errorToast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
background: #f56565;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10001;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.3s ease;
|
||||
max-width: 400px;
|
||||
word-wrap: break-word;
|
||||
`;
|
||||
errorToast.innerHTML = `
|
||||
<div style="font-weight: bold; margin-bottom: 4px;">❌ 保存失败</div>
|
||||
<div style="font-size: 12px;">${error?.message || error}</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(errorToast);
|
||||
|
||||
setTimeout(() => {
|
||||
errorToast.style.opacity = '1';
|
||||
errorToast.style.transform = 'translateX(0)';
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
errorToast.style.opacity = '0';
|
||||
errorToast.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(errorToast)) {
|
||||
document.body.removeChild(errorToast);
|
||||
}
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// 自动检查安装状态
|
||||
installation.checkInstallStatus();
|
||||
@@ -634,23 +747,31 @@ export function useBehaviorTreeEditor() {
|
||||
},
|
||||
updateNodeProperty: (path: string, value: any) => {
|
||||
if (appState.selectedConditionNodeId.value) {
|
||||
// 条件节点的属性更新 - 需要同步到装饰器
|
||||
const decoratorNode = appState.getNodeByIdLocal(appState.selectedConditionNodeId.value);
|
||||
if (decoratorNode) {
|
||||
const keys = path.split('.');
|
||||
let current: any = decoratorNode;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
||||
current[key] = {};
|
||||
// 解析路径,例如 "properties.variableName.value" -> "variableName"
|
||||
const pathParts = path.split('.');
|
||||
if (pathParts[0] === 'properties' && pathParts[2] === 'value') {
|
||||
const propertyName = pathParts[1];
|
||||
|
||||
// 直接更新装饰器的属性
|
||||
if (!decoratorNode.properties) {
|
||||
decoratorNode.properties = {};
|
||||
}
|
||||
decoratorNode.properties[propertyName] = value;
|
||||
|
||||
// 强制触发响应式更新
|
||||
const nodeIndex = appState.treeNodes.value.findIndex(n => n.id === decoratorNode.id);
|
||||
if (nodeIndex > -1) {
|
||||
const newNodes = [...appState.treeNodes.value];
|
||||
newNodes[nodeIndex] = { ...decoratorNode };
|
||||
appState.treeNodes.value = newNodes;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
const finalKey = keys[keys.length - 1];
|
||||
current[finalKey] = value;
|
||||
}
|
||||
} else {
|
||||
// 普通节点属性更新
|
||||
nodeOps.updateNodeProperty(path, value);
|
||||
}
|
||||
},
|
||||
@@ -660,9 +781,11 @@ export function useBehaviorTreeEditor() {
|
||||
handleDecoratorDragLeave: conditionAttachment.handleDecoratorDragLeave,
|
||||
attachConditionToDecorator: conditionAttachment.attachConditionToDecorator,
|
||||
getConditionDisplayText: conditionAttachment.getConditionDisplayText,
|
||||
getConditionProperties: conditionAttachment.getConditionProperties,
|
||||
removeConditionFromDecorator: conditionAttachment.removeConditionFromDecorator,
|
||||
canAcceptCondition: conditionAttachment.canAcceptCondition,
|
||||
resetDragState: conditionAttachment.resetDragState,
|
||||
toggleConditionExpanded: conditionAttachment.toggleConditionExpanded,
|
||||
|
||||
handleCanvasDrop: (event: DragEvent) => {
|
||||
if (conditionAttachment.handleCanvasDrop(event)) {
|
||||
|
||||
@@ -346,15 +346,27 @@ export function useBlackboard() {
|
||||
};
|
||||
|
||||
const onVariableDragStart = (event: DragEvent, variable: BlackboardVariable) => {
|
||||
if (!event.dataTransfer) return;
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.dataTransfer.setData('application/blackboard-variable', JSON.stringify({
|
||||
const dragData = {
|
||||
name: variable.name,
|
||||
type: variable.type,
|
||||
value: variable.value
|
||||
}));
|
||||
};
|
||||
|
||||
event.dataTransfer.setData('application/blackboard-variable', JSON.stringify(dragData));
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
|
||||
// 添加视觉反馈
|
||||
const dragElement = event.currentTarget as HTMLElement;
|
||||
if (dragElement) {
|
||||
dragElement.style.opacity = '0.8';
|
||||
setTimeout(() => {
|
||||
dragElement.style.opacity = '1';
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const editVariable = (variable: BlackboardVariable) => {
|
||||
|
||||
@@ -86,18 +86,216 @@ export function useComputedProperties(
|
||||
const decoratorNode = treeNodes.value.find(n => n.id === selectedConditionNodeId.value);
|
||||
if (!decoratorNode || !decoratorNode.attachedCondition) return null;
|
||||
|
||||
// 根据条件类型重新构建属性结构
|
||||
const conditionProperties = reconstructConditionProperties(
|
||||
decoratorNode.attachedCondition.type,
|
||||
decoratorNode.properties || {}
|
||||
);
|
||||
|
||||
// 创建一个虚拟的条件节点对象,用于属性编辑
|
||||
return {
|
||||
id: decoratorNode.id + '_condition',
|
||||
name: decoratorNode.attachedCondition.name + '(条件)',
|
||||
type: decoratorNode.attachedCondition.type,
|
||||
icon: decoratorNode.attachedCondition.icon,
|
||||
properties: decoratorNode.properties || {},
|
||||
properties: conditionProperties,
|
||||
isConditionNode: true,
|
||||
parentDecorator: decoratorNode
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据条件类型重新构建属性结构
|
||||
* 将装饰器的扁平属性转换回条件模板的属性结构
|
||||
*/
|
||||
const reconstructConditionProperties = (conditionType: string, decoratorProperties: Record<string, any>) => {
|
||||
switch (conditionType) {
|
||||
case 'condition-random':
|
||||
return {
|
||||
successProbability: {
|
||||
type: 'number',
|
||||
name: '成功概率',
|
||||
value: decoratorProperties.successProbability || 0.5,
|
||||
description: '条件成功的概率 (0.0 - 1.0)'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-component':
|
||||
return {
|
||||
componentType: {
|
||||
type: 'string',
|
||||
name: '组件类型',
|
||||
value: decoratorProperties.componentType || '',
|
||||
description: '要检查的组件类型名称'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-tag':
|
||||
return {
|
||||
tagValue: {
|
||||
type: 'number',
|
||||
name: '标签值',
|
||||
value: decoratorProperties.tagValue || 0,
|
||||
description: '要检查的标签值'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-active':
|
||||
return {
|
||||
checkHierarchy: {
|
||||
type: 'boolean',
|
||||
name: '检查层级激活',
|
||||
value: decoratorProperties.checkHierarchy || false,
|
||||
description: '是否检查整个层级的激活状态'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-numeric':
|
||||
return {
|
||||
propertyPath: {
|
||||
type: 'string',
|
||||
name: '属性路径',
|
||||
value: decoratorProperties.propertyPath || 'context.someValue',
|
||||
description: '要比较的数值属性路径'
|
||||
},
|
||||
compareOperator: {
|
||||
type: 'select',
|
||||
name: '比较操作符',
|
||||
value: decoratorProperties.compareOperator || 'greater',
|
||||
options: ['greater', 'less', 'equal', 'greaterEqual', 'lessEqual', 'notEqual'],
|
||||
description: '数值比较的操作符'
|
||||
},
|
||||
compareValue: {
|
||||
type: 'number',
|
||||
name: '比较值',
|
||||
value: decoratorProperties.compareValue || 0,
|
||||
description: '用于比较的目标值'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-property':
|
||||
return {
|
||||
propertyPath: {
|
||||
type: 'string',
|
||||
name: '属性路径',
|
||||
value: decoratorProperties.propertyPath || 'context.someProperty',
|
||||
description: '要检查的属性路径'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-custom':
|
||||
return {
|
||||
conditionCode: {
|
||||
type: 'code',
|
||||
name: '条件代码',
|
||||
value: decoratorProperties.conditionCode || '(context) => true',
|
||||
description: '自定义条件判断函数'
|
||||
}
|
||||
};
|
||||
|
||||
// Blackboard相关条件(使用实际的模板类型名)
|
||||
case 'blackboard-variable-exists':
|
||||
return {
|
||||
variableName: {
|
||||
type: 'string',
|
||||
name: '变量名',
|
||||
value: decoratorProperties.variableName || '',
|
||||
description: '要检查的黑板变量名'
|
||||
},
|
||||
invert: {
|
||||
type: 'boolean',
|
||||
name: '反转结果',
|
||||
value: decoratorProperties.invert || false,
|
||||
description: '是否反转检查结果'
|
||||
}
|
||||
};
|
||||
|
||||
case 'blackboard-value-comparison':
|
||||
return {
|
||||
variableName: {
|
||||
type: 'string',
|
||||
name: '变量名',
|
||||
value: decoratorProperties.variableName || '',
|
||||
description: '要比较的黑板变量名'
|
||||
},
|
||||
operator: {
|
||||
type: 'select',
|
||||
name: '比较操作符',
|
||||
value: decoratorProperties.operator || 'equal',
|
||||
options: ['equal', 'notEqual', 'greater', 'greaterOrEqual', 'less', 'lessOrEqual', 'contains', 'notContains'],
|
||||
description: '比较操作类型'
|
||||
},
|
||||
compareValue: {
|
||||
type: 'string',
|
||||
name: '比较值',
|
||||
value: decoratorProperties.compareValue || '',
|
||||
description: '用于比较的值(留空则使用比较变量)'
|
||||
},
|
||||
compareVariable: {
|
||||
type: 'string',
|
||||
name: '比较变量名',
|
||||
value: decoratorProperties.compareVariable || '',
|
||||
description: '用于比较的另一个黑板变量名'
|
||||
}
|
||||
};
|
||||
|
||||
case 'blackboard-variable-type-check':
|
||||
return {
|
||||
variableName: {
|
||||
type: 'string',
|
||||
name: '变量名',
|
||||
value: decoratorProperties.variableName || '',
|
||||
description: '要检查的黑板变量名'
|
||||
},
|
||||
expectedType: {
|
||||
type: 'select',
|
||||
name: '期望类型',
|
||||
value: decoratorProperties.expectedType || 'string',
|
||||
options: ['string', 'number', 'boolean', 'vector2', 'vector3', 'object', 'array'],
|
||||
description: '期望的变量类型'
|
||||
}
|
||||
};
|
||||
|
||||
case 'blackboard-variable-range-check':
|
||||
return {
|
||||
variableName: {
|
||||
type: 'string',
|
||||
name: '变量名',
|
||||
value: decoratorProperties.variableName || '',
|
||||
description: '要检查的数值型黑板变量名'
|
||||
},
|
||||
minValue: {
|
||||
type: 'number',
|
||||
name: '最小值',
|
||||
value: decoratorProperties.minValue || 0,
|
||||
description: '范围的最小值(包含)'
|
||||
},
|
||||
maxValue: {
|
||||
type: 'number',
|
||||
name: '最大值',
|
||||
value: decoratorProperties.maxValue || 100,
|
||||
description: '范围的最大值(包含)'
|
||||
}
|
||||
};
|
||||
|
||||
default:
|
||||
// 对于未知的条件类型,尝试从装饰器属性中推断
|
||||
const reconstructed: Record<string, any> = {};
|
||||
Object.keys(decoratorProperties).forEach(key => {
|
||||
if (key !== 'conditionType') {
|
||||
reconstructed[key] = {
|
||||
type: typeof decoratorProperties[key] === 'number' ? 'number' :
|
||||
typeof decoratorProperties[key] === 'boolean' ? 'boolean' : 'string',
|
||||
name: key,
|
||||
value: decoratorProperties[key],
|
||||
description: `${key}参数`
|
||||
};
|
||||
}
|
||||
});
|
||||
return reconstructed;
|
||||
}
|
||||
};
|
||||
|
||||
// 当前显示在属性面板的节点(普通节点或条件节点)
|
||||
const activeNode = computed(() => selectedConditionNode.value || selectedNode.value);
|
||||
|
||||
|
||||
@@ -161,6 +161,38 @@ export function useConditionAttachment(
|
||||
conditionCode: conditionTemplate.properties?.conditionCode?.value || '(context) => true'
|
||||
};
|
||||
|
||||
// Blackboard相关条件支持
|
||||
case 'blackboard-variable-exists':
|
||||
return {
|
||||
...baseConfig,
|
||||
variableName: conditionTemplate.properties?.variableName?.value || '',
|
||||
invert: conditionTemplate.properties?.invert?.value || false
|
||||
};
|
||||
|
||||
case 'blackboard-value-comparison':
|
||||
return {
|
||||
...baseConfig,
|
||||
variableName: conditionTemplate.properties?.variableName?.value || '',
|
||||
operator: conditionTemplate.properties?.operator?.value || 'equal',
|
||||
compareValue: conditionTemplate.properties?.compareValue?.value || '',
|
||||
compareVariable: conditionTemplate.properties?.compareVariable?.value || ''
|
||||
};
|
||||
|
||||
case 'blackboard-variable-type-check':
|
||||
return {
|
||||
...baseConfig,
|
||||
variableName: conditionTemplate.properties?.variableName?.value || '',
|
||||
expectedType: conditionTemplate.properties?.expectedType?.value || 'string'
|
||||
};
|
||||
|
||||
case 'blackboard-variable-range-check':
|
||||
return {
|
||||
...baseConfig,
|
||||
variableName: conditionTemplate.properties?.variableName?.value || '',
|
||||
minValue: conditionTemplate.properties?.minValue?.value || 0,
|
||||
maxValue: conditionTemplate.properties?.maxValue?.value || 100
|
||||
};
|
||||
|
||||
default:
|
||||
return baseConfig;
|
||||
}
|
||||
@@ -177,7 +209,12 @@ export function useConditionAttachment(
|
||||
'condition-active': 'isActive',
|
||||
'condition-numeric': 'numericCompare',
|
||||
'condition-property': 'propertyExists',
|
||||
'condition-custom': 'custom'
|
||||
'condition-custom': 'custom',
|
||||
// Blackboard相关条件
|
||||
'blackboard-variable-exists': 'blackboardExists',
|
||||
'blackboard-value-comparison': 'blackboardCompare',
|
||||
'blackboard-variable-type-check': 'blackboardTypeCheck',
|
||||
'blackboard-variable-range-check': 'blackboardRangeCheck'
|
||||
};
|
||||
|
||||
return typeMap[template.type] || 'custom';
|
||||
@@ -190,27 +227,19 @@ export function useConditionAttachment(
|
||||
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) {
|
||||
@@ -225,8 +254,11 @@ export function useConditionAttachment(
|
||||
name: dragState.conditionTemplate.name,
|
||||
icon: dragState.conditionTemplate.icon
|
||||
};
|
||||
|
||||
console.log('✅ 条件吸附成功!', decoratorNode.attachedCondition);
|
||||
|
||||
// 初始化为收缩状态
|
||||
if (decoratorNode.conditionExpanded === undefined) {
|
||||
decoratorNode.conditionExpanded = false;
|
||||
}
|
||||
|
||||
// 重置拖拽状态
|
||||
resetDragState();
|
||||
@@ -260,7 +292,6 @@ export function useConditionAttachment(
|
||||
* 重置拖拽状态
|
||||
*/
|
||||
const resetDragState = () => {
|
||||
console.log('🔄 重置拖拽状态');
|
||||
dragState.isDraggingCondition = false;
|
||||
dragState.conditionTemplate = null;
|
||||
dragState.mousePosition = null;
|
||||
@@ -268,45 +299,126 @@ export function useConditionAttachment(
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取条件显示文本
|
||||
* 获取条件显示文本(简化版始终显示条件名称)
|
||||
*/
|
||||
const getConditionDisplayText = (decoratorNode: TreeNode): string => {
|
||||
if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
|
||||
const getConditionDisplayText = (decoratorNode: TreeNode, expanded: boolean = false): string => {
|
||||
if (!decoratorNode.attachedCondition) {
|
||||
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;
|
||||
// 始终返回条件名称,不管是否展开
|
||||
return decoratorNode.attachedCondition.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取条件的可见属性(用于展开时显示)
|
||||
*/
|
||||
const getConditionProperties = (decoratorNode: TreeNode): Record<string, any> => {
|
||||
if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const conditionType = decoratorNode.attachedCondition.type;
|
||||
const visibleProps: Record<string, any> = {};
|
||||
|
||||
// 根据条件类型筛选相关属性
|
||||
switch (conditionType) {
|
||||
case 'condition-random':
|
||||
if ('successProbability' in decoratorNode.properties) {
|
||||
visibleProps['成功概率'] = `${(decoratorNode.properties.successProbability * 100).toFixed(1)}%`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition-component':
|
||||
if ('componentType' in decoratorNode.properties) {
|
||||
visibleProps['组件类型'] = decoratorNode.properties.componentType;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition-tag':
|
||||
if ('tagValue' in decoratorNode.properties) {
|
||||
visibleProps['标签值'] = decoratorNode.properties.tagValue;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition-active':
|
||||
if ('checkHierarchy' in decoratorNode.properties) {
|
||||
visibleProps['检查层级'] = decoratorNode.properties.checkHierarchy ? '是' : '否';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition-numeric':
|
||||
if ('propertyPath' in decoratorNode.properties) {
|
||||
visibleProps['属性路径'] = decoratorNode.properties.propertyPath;
|
||||
}
|
||||
if ('compareOperator' in decoratorNode.properties) {
|
||||
visibleProps['比较操作'] = decoratorNode.properties.compareOperator;
|
||||
}
|
||||
if ('compareValue' in decoratorNode.properties) {
|
||||
visibleProps['比较值'] = decoratorNode.properties.compareValue;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition-property':
|
||||
if ('propertyPath' in decoratorNode.properties) {
|
||||
visibleProps['属性路径'] = decoratorNode.properties.propertyPath;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'blackboard-variable-exists':
|
||||
if ('variableName' in decoratorNode.properties) {
|
||||
visibleProps['变量名'] = decoratorNode.properties.variableName;
|
||||
}
|
||||
if ('invert' in decoratorNode.properties) {
|
||||
visibleProps['反转结果'] = decoratorNode.properties.invert ? '是' : '否';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'blackboard-value-comparison':
|
||||
if ('variableName' in decoratorNode.properties) {
|
||||
visibleProps['变量名'] = decoratorNode.properties.variableName;
|
||||
}
|
||||
if ('operator' in decoratorNode.properties) {
|
||||
visibleProps['操作符'] = decoratorNode.properties.operator;
|
||||
}
|
||||
if ('compareValue' in decoratorNode.properties) {
|
||||
visibleProps['比较值'] = decoratorNode.properties.compareValue;
|
||||
}
|
||||
if ('compareVariable' in decoratorNode.properties) {
|
||||
visibleProps['比较变量'] = decoratorNode.properties.compareVariable;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'blackboard-variable-type-check':
|
||||
if ('variableName' in decoratorNode.properties) {
|
||||
visibleProps['变量名'] = decoratorNode.properties.variableName;
|
||||
}
|
||||
if ('expectedType' in decoratorNode.properties) {
|
||||
visibleProps['期望类型'] = decoratorNode.properties.expectedType;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'blackboard-variable-range-check':
|
||||
if ('variableName' in decoratorNode.properties) {
|
||||
visibleProps['变量名'] = decoratorNode.properties.variableName;
|
||||
}
|
||||
if ('minValue' in decoratorNode.properties) {
|
||||
visibleProps['最小值'] = decoratorNode.properties.minValue;
|
||||
}
|
||||
if ('maxValue' in decoratorNode.properties) {
|
||||
visibleProps['最大值'] = decoratorNode.properties.maxValue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return visibleProps;
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换条件展开状态
|
||||
*/
|
||||
const toggleConditionExpanded = (decoratorNode: TreeNode) => {
|
||||
decoratorNode.conditionExpanded = !decoratorNode.conditionExpanded;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -314,10 +426,34 @@ export function useConditionAttachment(
|
||||
*/
|
||||
const removeConditionFromDecorator = (decoratorNode: TreeNode) => {
|
||||
if (decoratorNode.attachedCondition) {
|
||||
// 删除附加的条件信息
|
||||
delete decoratorNode.attachedCondition;
|
||||
|
||||
// 完全清空所有属性,回到初始空白状态
|
||||
decoratorNode.properties = {};
|
||||
// 重置展开状态
|
||||
decoratorNode.conditionExpanded = false;
|
||||
|
||||
// 保留装饰器的基础属性,只删除条件相关的属性
|
||||
const preservedProperties: Record<string, any> = {};
|
||||
|
||||
// 条件装饰器的基础属性
|
||||
const baseDecoratorProperties = [
|
||||
'executeWhenTrue',
|
||||
'executeWhenFalse',
|
||||
'checkInterval',
|
||||
'abortType'
|
||||
];
|
||||
|
||||
// 保留基础属性
|
||||
if (decoratorNode.properties) {
|
||||
baseDecoratorProperties.forEach(key => {
|
||||
if (key in decoratorNode.properties!) {
|
||||
preservedProperties[key] = decoratorNode.properties![key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 重置为只包含基础属性的对象
|
||||
decoratorNode.properties = preservedProperties;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -339,6 +475,8 @@ export function useConditionAttachment(
|
||||
getConditionDisplayText,
|
||||
removeConditionFromDecorator,
|
||||
canAcceptCondition,
|
||||
isConditionalDecorator
|
||||
isConditionalDecorator,
|
||||
toggleConditionExpanded,
|
||||
getConditionProperties
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -135,18 +135,47 @@ export function useNodeOperations(
|
||||
|
||||
// 节点属性更新
|
||||
const updateNodeProperty = (path: string, value: any) => {
|
||||
const node = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
|
||||
if (!node) return;
|
||||
const selectedNode = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
|
||||
if (!selectedNode) return;
|
||||
|
||||
// 使用通用方法更新属性
|
||||
setNestedProperty(node, path, value);
|
||||
|
||||
// 强制触发响应式更新
|
||||
const nodeIndex = treeNodes.value.findIndex(n => n.id === node.id);
|
||||
if (nodeIndex > -1) {
|
||||
const newNodes = [...treeNodes.value];
|
||||
newNodes[nodeIndex] = { ...node };
|
||||
treeNodes.value = newNodes;
|
||||
// 检查是否是条件节点的属性更新
|
||||
if (selectedNode.isConditionNode && selectedNode.parentDecorator) {
|
||||
// 条件节点的属性更新需要同步到装饰器
|
||||
updateConditionNodeProperty(selectedNode.parentDecorator, path, value);
|
||||
} else {
|
||||
// 普通节点的属性更新
|
||||
setNestedProperty(selectedNode, path, value);
|
||||
|
||||
// 强制触发响应式更新
|
||||
const nodeIndex = treeNodes.value.findIndex(n => n.id === selectedNode.id);
|
||||
if (nodeIndex > -1) {
|
||||
const newNodes = [...treeNodes.value];
|
||||
newNodes[nodeIndex] = { ...selectedNode };
|
||||
treeNodes.value = newNodes;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 更新条件节点属性到装饰器
|
||||
const updateConditionNodeProperty = (decoratorNode: TreeNode, path: string, value: any) => {
|
||||
// 解析属性路径,例如 "properties.variableName.value" -> "variableName"
|
||||
const pathParts = path.split('.');
|
||||
if (pathParts[0] === 'properties' && pathParts[2] === 'value') {
|
||||
const propertyName = pathParts[1];
|
||||
|
||||
// 直接更新装饰器的属性
|
||||
if (!decoratorNode.properties) {
|
||||
decoratorNode.properties = {};
|
||||
}
|
||||
decoratorNode.properties[propertyName] = value;
|
||||
|
||||
// 强制触发响应式更新
|
||||
const nodeIndex = treeNodes.value.findIndex(n => n.id === decoratorNode.id);
|
||||
if (nodeIndex > -1) {
|
||||
const newNodes = [...treeNodes.value];
|
||||
newNodes[nodeIndex] = { ...decoratorNode };
|
||||
treeNodes.value = newNodes;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -292,7 +292,45 @@ export const nodeTemplates: NodeTemplate[] = [
|
||||
minChildren: 1,
|
||||
className: 'ConditionalDecorator',
|
||||
namespace: 'behaviourTree/decorators',
|
||||
properties: {}
|
||||
properties: {
|
||||
conditionType: {
|
||||
name: '条件类型',
|
||||
type: 'select',
|
||||
value: 'custom',
|
||||
options: ['custom', 'random', 'hasComponent', 'hasTag', 'isActive', 'numericCompare', 'propertyExists'],
|
||||
description: '装饰器使用的条件类型',
|
||||
required: false
|
||||
},
|
||||
executeWhenTrue: {
|
||||
name: '条件为真时执行',
|
||||
type: 'boolean',
|
||||
value: true,
|
||||
description: '条件为真时是否执行子节点',
|
||||
required: false
|
||||
},
|
||||
executeWhenFalse: {
|
||||
name: '条件为假时执行',
|
||||
type: 'boolean',
|
||||
value: false,
|
||||
description: '条件为假时是否执行子节点',
|
||||
required: false
|
||||
},
|
||||
checkInterval: {
|
||||
name: '检查间隔',
|
||||
type: 'number',
|
||||
value: 0,
|
||||
description: '条件检查间隔时间(秒),0表示每帧检查',
|
||||
required: false
|
||||
},
|
||||
abortType: {
|
||||
name: '中止类型',
|
||||
type: 'select',
|
||||
value: 'None',
|
||||
options: ['None', 'LowerPriority', 'Self', 'Both'],
|
||||
description: '决定节点在何种情况下会被中止',
|
||||
required: false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 动作节点 (Actions) - 叶子节点,不能有子节点
|
||||
|
||||
@@ -22,6 +22,11 @@ export interface TreeNode {
|
||||
name: string;
|
||||
icon: string;
|
||||
};
|
||||
// 条件节点相关(用于虚拟条件节点)
|
||||
isConditionNode?: boolean;
|
||||
parentDecorator?: TreeNode;
|
||||
// 条件显示状态
|
||||
conditionExpanded?: boolean; // 条件是否展开显示详细信息
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
|
||||
@@ -28,6 +28,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 条件装饰器 - 基础状态 */
|
||||
.conditional-decorator {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
max-width: 350px; /* 增加最大宽度以容纳长的条件名称 */
|
||||
min-height: 80px;
|
||||
transition: all 0.3s ease;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 条件装饰器 - 有附加条件状态 */
|
||||
.conditional-decorator.has-attached-condition {
|
||||
width: auto; /* 自动调整宽度 */
|
||||
min-width: 280px; /* 进一步增加最小宽度,确保较长的条件名称能完整显示 */
|
||||
max-width: 400px; /* 增加最大宽度 */
|
||||
min-height: 110px; /* 增加基础高度 */
|
||||
}
|
||||
|
||||
/* 条件装饰器 - 展开状态 */
|
||||
.conditional-decorator.has-attached-condition .condition-properties {
|
||||
min-height: 30px; /* 为展开内容预留空间 */
|
||||
}
|
||||
|
||||
/* 条件装饰器接受状态 */
|
||||
.tree-node.can-accept-condition {
|
||||
border: 2px dashed #ffd700;
|
||||
@@ -51,7 +74,7 @@
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* 条件附加区域 */
|
||||
/* 条件吸附区域 */
|
||||
.condition-attachment-area {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
@@ -61,84 +84,214 @@
|
||||
|
||||
.condition-placeholder {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
padding: 6px 8px;
|
||||
border: 2px dashed #4a5568;
|
||||
border-radius: 4px;
|
||||
color: #a0aec0;
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tree-node.can-accept-condition .condition-placeholder {
|
||||
border-color: #ffd700;
|
||||
color: #ffd700;
|
||||
background: rgba(255, 215, 0, 0.05);
|
||||
animation: pulse-placeholder 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-placeholder {
|
||||
0%, 100% {
|
||||
background: rgba(255, 215, 0, 0.05);
|
||||
}
|
||||
50% {
|
||||
background: rgba(255, 215, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 附加的条件 */
|
||||
.attached-condition {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
border: 1px solid #ffd700;
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
/* 条件信息区域 */
|
||||
.condition-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
padding: 4px 6px 4px 6px;
|
||||
padding-right: 40px; /* 为右侧两个按钮预留空间 */
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1.3;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.condition-info:hover {
|
||||
background: rgba(255, 215, 0, 0.2);
|
||||
transform: scale(1.02);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.condition-info.condition-selected {
|
||||
background: rgba(255, 215, 0, 0.3);
|
||||
border: 1px solid #ffd700;
|
||||
box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.25);
|
||||
box-shadow: 0 0 0 1px rgba(255, 215, 0, 0.25);
|
||||
}
|
||||
|
||||
.condition-icon {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.condition-text {
|
||||
flex: 1;
|
||||
color: #ffd700;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
line-height: 1.3;
|
||||
min-width: 0;
|
||||
max-width: calc(100% - 50px); /* 为图标、编辑提示和按钮预留空间 */
|
||||
white-space: nowrap; /* 尽量保持一行显示 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; /* 如果实在太长则显示省略号 */
|
||||
}
|
||||
|
||||
.edit-hint {
|
||||
font-size: 10px;
|
||||
font-size: 9px;
|
||||
color: #a0aec0;
|
||||
margin-left: auto;
|
||||
transition: color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.condition-info:hover .edit-hint {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.remove-condition-btn {
|
||||
background: none;
|
||||
/* 展开/收缩按钮 */
|
||||
.toggle-condition-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 20px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: none;
|
||||
color: #e53e3e;
|
||||
background: #4a5568;
|
||||
color: #a0aec0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
font-size: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.toggle-condition-btn:hover {
|
||||
background: #2d3748;
|
||||
color: #ffd700;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 移除条件按钮 */
|
||||
.remove-condition-btn {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
background: #e53e3e;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.8;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.remove-condition-btn:hover {
|
||||
background: rgba(229, 62, 62, 0.2);
|
||||
background: #c53030;
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 条件属性展开区域 */
|
||||
.condition-properties {
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-right: 6px; /* 为按钮预留一点空间 */
|
||||
}
|
||||
|
||||
/* 属性分隔线 */
|
||||
.properties-divider {
|
||||
width: calc(100% - 6px);
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent 0%, #ffd700 20%, #ffd700 80%, transparent 100%);
|
||||
margin-bottom: 6px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 条件属性项 */
|
||||
.condition-property-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 2px 4px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.condition-property-label {
|
||||
color: #a0aec0;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.condition-property-value {
|
||||
color: #ffd700;
|
||||
font-weight: 400;
|
||||
text-align: right;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
white-space: normal; /* 允许值换行 */
|
||||
}
|
||||
|
||||
/* 画布状态 */
|
||||
@@ -151,9 +304,33 @@
|
||||
}
|
||||
|
||||
/* 条件装饰器节点的特殊样式 */
|
||||
.tree-node.node-conditional-decorator {
|
||||
/* 基础高度和宽度 */
|
||||
min-height: 80px;
|
||||
width: 200px; /* 增加基础宽度 */
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 附加了条件的装饰器节点需要更大的高度 */
|
||||
.tree-node.node-conditional-decorator.has-attached-condition {
|
||||
min-height: 130px; /* 增加高度 */
|
||||
width: 220px; /* 进一步增加宽度以容纳更多内容 */
|
||||
}
|
||||
|
||||
.tree-node.node-conditional-decorator .condition-attachment-area {
|
||||
border: 1px solid #9f7aea;
|
||||
background: rgba(159, 122, 234, 0.05);
|
||||
margin-top: 4px;
|
||||
min-height: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 当有条件附加时,增加条件区域的高度 */
|
||||
.tree-node.node-conditional-decorator.has-attached-condition .condition-attachment-area {
|
||||
min-height: 45px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 215, 0, 0.08);
|
||||
border-color: #ffd700;
|
||||
}
|
||||
|
||||
.tree-node.node-conditional-decorator.node-selected .condition-attachment-area {
|
||||
|
||||
@@ -56,8 +56,10 @@
|
||||
background: #2d3748;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6);
|
||||
max-width: 90vw;
|
||||
max-width: 95vw;
|
||||
max-height: 90vh;
|
||||
min-width: 800px;
|
||||
width: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #4a5568;
|
||||
@@ -138,18 +140,119 @@
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
background: #2d3748;
|
||||
min-width: 750px;
|
||||
}
|
||||
|
||||
/* 导出选项样式增强 */
|
||||
.export-options {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.export-options label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #e2e8f0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.export-options label:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: #667eea;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.export-options label:has(input[type="radio"]:checked) {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.export-options input[type="radio"] {
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #4a5568;
|
||||
border-radius: 50%;
|
||||
background: #1a202c;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.export-options input[type="radio"]:checked {
|
||||
border-color: #667eea;
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
.export-options input[type="radio"]:checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.export-options label span {
|
||||
color: #e2e8f0;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.export-options label small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #a0aec0;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 禁用选项样式 */
|
||||
.export-options label.disabled-option {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.export-options label.disabled-option:hover {
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
border-color: #374151;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.export-options label.disabled-option span {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.export-options label.disabled-option input[type="radio"] {
|
||||
border-color: #374151;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.export-options label.disabled-option input[type="radio"]:disabled {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.code-output {
|
||||
@@ -217,22 +320,38 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.modal-footer .save-btn {
|
||||
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
||||
.modal-footer .copy-btn {
|
||||
background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
|
||||
border: 1px solid #3182ce;
|
||||
}
|
||||
|
||||
.modal-footer .save-btn:hover {
|
||||
.modal-footer .copy-btn:hover {
|
||||
background: linear-gradient(135deg, #2c5282 0%, #2a4365 100%);
|
||||
border-color: #2c5282;
|
||||
}
|
||||
|
||||
.modal-footer .save-file-btn {
|
||||
background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
|
||||
border: 1px solid #38a169;
|
||||
}
|
||||
|
||||
.modal-footer .cancel-btn {
|
||||
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||||
.modal-footer .save-file-btn:hover {
|
||||
background: linear-gradient(135deg, #2f855a 0%, #276749 100%);
|
||||
border-color: #2f855a;
|
||||
}
|
||||
|
||||
.modal-footer .cancel-btn:hover {
|
||||
background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
|
||||
.modal-footer .close-btn {
|
||||
background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
|
||||
border: 1px solid #718096;
|
||||
}
|
||||
|
||||
.modal-footer .close-btn:hover {
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Blackboard模态框特定样式 */
|
||||
.blackboard-modal {
|
||||
width: 520px;
|
||||
@@ -427,5 +546,11 @@
|
||||
font-size: 11px;
|
||||
color: #e2e8f0;
|
||||
resize: none;
|
||||
min-height: 300px;
|
||||
min-height: 400px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.4;
|
||||
overflow-y: auto;
|
||||
white-space: pre;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@@ -83,6 +83,15 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 条件装饰器节点的node-body需要更大的宽度 */
|
||||
.tree-node.node-conditional-decorator .node-body {
|
||||
max-width: 176px; /* 基础状态 */
|
||||
}
|
||||
|
||||
.tree-node.node-conditional-decorator.has-attached-condition .node-body {
|
||||
max-width: 196px; /* 附加条件后更宽 */
|
||||
}
|
||||
|
||||
.node-description {
|
||||
margin-bottom: 6px;
|
||||
color: #cbd5e0;
|
||||
|
||||
@@ -564,61 +564,73 @@
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-item {
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
cursor: grab;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
background: #4a5568;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-left-width: 0;
|
||||
min-height: 44px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
overflow: hidden;
|
||||
border-left: 3px solid transparent;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1), 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-item:hover {
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
border-color: #667eea;
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(102, 126, 234, 0.3);
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
border-radius: 2px 0 0 2px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-item:hover::before {
|
||||
transform: translateY(-50%) scale(1.2);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2), 0 0 12px currentColor, 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-item:active {
|
||||
cursor: grabbing;
|
||||
transform: scale(0.98);
|
||||
background: #ffd700;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-drag-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
cursor: grab;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-drag-area:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-drag-area:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-drag-area .drag-hint {
|
||||
opacity: 0;
|
||||
font-size: 10px;
|
||||
color: #ffd700;
|
||||
transition: opacity 0.2s ease;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-drag-area:hover .drag-hint {
|
||||
opacity: 1;
|
||||
animation: pulse-hint 2s infinite;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-info {
|
||||
@@ -920,4 +932,170 @@
|
||||
font-size: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Blackboard 拖拽目标区域样式 */
|
||||
.blackboard-drop-zone {
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border: 2px dashed #4a5568;
|
||||
border-radius: 6px;
|
||||
background: rgba(74, 85, 104, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blackboard-drop-zone:hover {
|
||||
border-color: #667eea;
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
.blackboard-drop-zone.drag-over {
|
||||
border-color: #667eea;
|
||||
background: rgba(102, 126, 234, 0.15);
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.blackboard-drop-zone.has-reference {
|
||||
border-color: #48bb78;
|
||||
background: rgba(72, 187, 120, 0.1);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.blackboard-drop-zone.has-reference:hover {
|
||||
background: rgba(72, 187, 120, 0.15);
|
||||
}
|
||||
|
||||
.drop-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #a0aec0;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.drop-icon {
|
||||
font-size: 16px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.drop-text {
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.blackboard-drop-zone .drop-placeholder {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.blackboard-drop-zone:hover .drop-placeholder {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.blackboard-drop-zone:hover .drop-icon {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.blackboard-drop-zone.drag-over .drop-placeholder {
|
||||
color: #667eea;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.blackboard-drop-zone.drag-over .drop-icon {
|
||||
animation: bounce-drop 0.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce-drop {
|
||||
0%, 100% {
|
||||
transform: scale(1.1) translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2) translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 输入框的Blackboard集成样式 */
|
||||
.property-item input.with-blackboard {
|
||||
border-top: 1px solid #4a5568;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.property-item input.with-blackboard:focus {
|
||||
border-top-color: #667eea;
|
||||
}
|
||||
|
||||
@keyframes pulse-hint {
|
||||
0%, 100% {
|
||||
opacity: 0.7;
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce-drop {
|
||||
0%, 100% {
|
||||
transform: scale(1.1) translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2) translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.blackboard-usage-hint {
|
||||
margin: 8px 12px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border: 1px solid rgba(102, 126, 234, 0.2);
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.hint-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.hint-icon {
|
||||
font-size: 12px;
|
||||
animation: pulse-hint 3s infinite;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* 调试信息样式 */
|
||||
.property-item.debug-info {
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border-left: 3px solid #ffc107;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.debug-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
color: #e83e8c;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -287,7 +287,8 @@
|
||||
'node-error': node.hasError,
|
||||
'dragging': dragState.dragNode && dragState.dragNode.id === node.id,
|
||||
'condition-hover': conditionDragState.hoveredDecoratorId === node.id,
|
||||
'can-accept-condition': canAcceptCondition(node) && conditionDragState.isDraggingCondition
|
||||
'can-accept-condition': canAcceptCondition(node) && conditionDragState.isDraggingCondition,
|
||||
'has-attached-condition': node.type === 'conditional-decorator' && node.attachedCondition
|
||||
}
|
||||
]"
|
||||
:style="{
|
||||
@@ -321,6 +322,25 @@
|
||||
<span class="condition-text">{{ getConditionDisplayText(node) }}</span>
|
||||
<span class="edit-hint">📝</span>
|
||||
</div>
|
||||
|
||||
<!-- 展开时显示条件属性 -->
|
||||
<div v-if="node.conditionExpanded" class="condition-properties">
|
||||
<div class="properties-divider"></div>
|
||||
<div
|
||||
v-for="(value, key) in getConditionProperties(node)"
|
||||
:key="key"
|
||||
class="condition-property-item"
|
||||
>
|
||||
<span class="condition-property-label">{{ key }}:</span>
|
||||
<span class="condition-property-value">{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="toggle-condition-btn"
|
||||
@click.stop="toggleConditionExpanded(node)"
|
||||
:title="node.conditionExpanded ? '收缩详情' : '展开详情'"
|
||||
>{{ node.conditionExpanded ? '▲' : '▼' }}</button>
|
||||
<button
|
||||
class="remove-condition-btn"
|
||||
@click.stop="removeConditionFromDecorator(node)"
|
||||
@@ -440,13 +460,35 @@
|
||||
>
|
||||
<label>{{ prop.name }}:</label>
|
||||
<div class="property-input-container">
|
||||
<!-- Blackboard 拖拽目标区域 -->
|
||||
<div
|
||||
v-if="isBlackboardDroppable(prop)"
|
||||
class="blackboard-drop-zone"
|
||||
:class="{ 'has-reference': isBlackboardReference(prop.value) }"
|
||||
@drop="handleBlackboardDrop($event, key)"
|
||||
@dragover="handleBlackboardDragOver"
|
||||
@dragleave="handleBlackboardDragLeave"
|
||||
>
|
||||
<div v-if="!isBlackboardReference(prop.value)" class="drop-placeholder">
|
||||
<span class="drop-icon">📋</span>
|
||||
<span class="drop-text">拖拽Blackboard变量到此处</span>
|
||||
</div>
|
||||
<div v-else class="blackboard-reference">
|
||||
<span class="ref-icon">🔗</span>
|
||||
<span class="ref-text">{{ prop.value }}</span>
|
||||
<button class="clear-ref" @click="clearBlackboardReference(key)" title="清除引用">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入控件 -->
|
||||
<input
|
||||
v-if="prop.type === 'string'"
|
||||
type="text"
|
||||
:value="prop.value"
|
||||
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
||||
:key="activeNode.id + '_' + key + '_string'"
|
||||
:placeholder="'拖拽Blackboard变量或直接输入'"
|
||||
:placeholder="isBlackboardDroppable(prop) ? '或直接输入值' : '请输入值'"
|
||||
:class="{ 'with-blackboard': isBlackboardDroppable(prop) }"
|
||||
>
|
||||
<input
|
||||
v-else-if="prop.type === 'number'"
|
||||
@@ -454,7 +496,8 @@
|
||||
:value="prop.value"
|
||||
@input="updateNodeProperty('properties.' + key + '.value', parseFloat($event.target.value) || 0)"
|
||||
:key="activeNode.id + '_' + key + '_number'"
|
||||
:placeholder="'拖拽Blackboard变量或直接输入'"
|
||||
:placeholder="isBlackboardDroppable(prop) ? '或直接输入值' : '请输入值'"
|
||||
:class="{ 'with-blackboard': isBlackboardDroppable(prop) }"
|
||||
>
|
||||
<input
|
||||
v-else-if="prop.type === 'boolean'"
|
||||
@@ -487,12 +530,6 @@
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div v-if="isBlackboardReference(prop.value)" class="blackboard-reference">
|
||||
<span class="ref-icon">🔗</span>
|
||||
<span class="ref-text">{{ prop.value }}</span>
|
||||
<button class="clear-ref" @click="clearBlackboardReference(key)" title="清除引用">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="prop.description" class="property-help">{{ prop.description }}</p>
|
||||
</div>
|
||||
@@ -688,6 +725,14 @@
|
||||
<button @click="clearBlackboard" v-if="blackboardVariables.length > 0" class="clear-btn">清空</button>
|
||||
</div>
|
||||
|
||||
<!-- 使用提示 -->
|
||||
<div v-if="blackboardVariables.length > 0" class="blackboard-usage-hint">
|
||||
<div class="hint-content">
|
||||
<span class="hint-icon">💡</span>
|
||||
<span class="hint-text">拖拽变量名使用</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blackboard-scroll-area">
|
||||
<div v-if="blackboardVariables.length > 0" class="blackboard-groups">
|
||||
<div
|
||||
@@ -702,16 +747,22 @@
|
||||
:key="variable.name"
|
||||
class="variable-item"
|
||||
:class="variable.type"
|
||||
:draggable="true"
|
||||
@dragstart="onBlackboardDragStart($event, variable)"
|
||||
:title="variable.description"
|
||||
>
|
||||
<div class="variable-header">
|
||||
<span class="variable-name">{{ variable.name }}</span>
|
||||
<span class="variable-type">{{ getTypeDisplayName(variable.type) }}</span>
|
||||
<div
|
||||
class="variable-drag-area"
|
||||
:draggable="true"
|
||||
@dragstart="onBlackboardDragStart($event, variable)"
|
||||
@click.stop
|
||||
>
|
||||
<span class="variable-name">{{ variable.name }}</span>
|
||||
<span class="variable-type">{{ getTypeDisplayName(variable.type) }}</span>
|
||||
<span class="drag-hint">🎯</span>
|
||||
</div>
|
||||
<div class="variable-actions">
|
||||
<button @click="editBlackboardVariable(variable)" class="edit-btn">✏️</button>
|
||||
<button @click="removeBlackboardVariable(variable.name)" class="remove-btn">🗑️</button>
|
||||
<button @click.stop="editBlackboardVariable(variable)" class="edit-btn">✏️</button>
|
||||
<button @click.stop="removeBlackboardVariable(variable.name)" class="remove-btn">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -720,6 +771,7 @@
|
||||
v-if="variable.constraints && variable.constraints.allowedValues"
|
||||
v-model="variable.value"
|
||||
@change="onBlackboardValueChange(variable)"
|
||||
@click.stop
|
||||
:disabled="variable.readOnly"
|
||||
>
|
||||
<option
|
||||
@@ -755,28 +807,51 @@
|
||||
<div v-if="showExportModal" class="modal-overlay" @click="showExportModal = false">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>导出配置</h3>
|
||||
<h3>⚡ 导出行为树配置</h3>
|
||||
<button @click="showExportModal = false">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="export-options">
|
||||
<h4 style="margin: 0 0 12px 0; color: #e2e8f0; font-size: 14px;">📄 选择导出格式:</h4>
|
||||
<label>
|
||||
<input type="radio" v-model="exportFormat" value="json"> JSON配置
|
||||
<input type="radio" v-model="exportFormat" value="json">
|
||||
<span>📄 JSON配置文件</span>
|
||||
<small style="display: block; margin-left: 24px; color: #a0aec0; font-size: 11px;">适用于运行时动态加载行为树</small>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" v-model="exportFormat" value="typescript"> TypeScript代码
|
||||
<label class="disabled-option">
|
||||
<input type="radio" v-model="exportFormat" value="typescript" disabled>
|
||||
<span>📝 TypeScript代码 (暂不可用)</span>
|
||||
<small style="display: block; margin-left: 24px; color: #6b7280; font-size: 11px;">此功能正在开发中,敬请期待</small>
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
class="export-code"
|
||||
:value="exportedCode()"
|
||||
readonly
|
||||
></textarea>
|
||||
<div class="preview-section">
|
||||
<h4 style="margin: 0 0 8px 0; color: #e2e8f0; font-size: 13px;">📋 代码预览:</h4>
|
||||
<textarea
|
||||
class="export-code"
|
||||
:value="exportedCode()"
|
||||
readonly
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="usage-hint" style="margin-top: 16px; padding: 12px; background: rgba(102, 126, 234, 0.08); border: 1px solid rgba(102, 126, 234, 0.2); border-radius: 6px;">
|
||||
<div style="color: #a0aec0; font-size: 12px; line-height: 1.4;">
|
||||
<strong style="color: #e2e8f0;">💡 使用提示:</strong><br/>
|
||||
<span v-if="exportFormat === 'json'">
|
||||
• JSON配置可用于运行时动态加载行为树<br/>
|
||||
• 使用 BehaviorTreeBuilder.fromConfig() 方法构建行为树<br/>
|
||||
• 可以保存为 .json 文件在项目中使用
|
||||
</span>
|
||||
<span v-else>
|
||||
• 当前仅支持JSON格式导出<br/>
|
||||
• TypeScript代码生成功能正在开发中
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="copyToClipboard">复制到剪贴板</button>
|
||||
<button @click="saveToFile">保存到文件</button>
|
||||
<button @click="showExportModal = false">关闭</button>
|
||||
<button @click="copyToClipboard" class="copy-btn">📋 复制到剪贴板</button>
|
||||
<button @click="saveToFile" class="save-file-btn">💾 保存到文件</button>
|
||||
<button @click="showExportModal = false" class="close-btn">❌ 关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user