新增Blackboard
This commit is contained in:
@@ -9,6 +9,7 @@ import { useConnectionManager } from './useConnectionManager';
|
|||||||
import { useCanvasManager } from './useCanvasManager';
|
import { useCanvasManager } from './useCanvasManager';
|
||||||
import { useNodeDisplay } from './useNodeDisplay';
|
import { useNodeDisplay } from './useNodeDisplay';
|
||||||
import { useConditionAttachment } from './useConditionAttachment';
|
import { useConditionAttachment } from './useConditionAttachment';
|
||||||
|
import { useBlackboard } from './useBlackboard';
|
||||||
import { validateTree as validateTreeStructure } from '../utils/nodeUtils';
|
import { validateTree as validateTreeStructure } from '../utils/nodeUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +78,15 @@ export function useBehaviorTreeEditor() {
|
|||||||
appState.isInstalling
|
appState.isInstalling
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Blackboard功能
|
||||||
|
const blackboard = useBlackboard();
|
||||||
|
|
||||||
|
// Blackboard常驻侧边面板状态
|
||||||
|
const blackboardSidebarState = reactive({
|
||||||
|
collapsed: false,
|
||||||
|
transparent: true
|
||||||
|
});
|
||||||
|
|
||||||
const connectionState = reactive({
|
const connectionState = reactive({
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
startNodeId: null as string | null,
|
startNodeId: null as string | null,
|
||||||
@@ -134,6 +144,65 @@ export function useBehaviorTreeEditor() {
|
|||||||
updateCounter: 0
|
updateCounter: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Blackboard拖拽相关功能
|
||||||
|
const isBlackboardDroppable = (prop: any): boolean => {
|
||||||
|
return prop && (prop.type === 'string' || prop.type === 'number' || prop.type === 'boolean');
|
||||||
|
};
|
||||||
|
|
||||||
|
const isBlackboardReference = (value: string): boolean => {
|
||||||
|
return typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlackboardDrop = (event: DragEvent, propertyKey: string) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blackboardData = event.dataTransfer?.getData('application/blackboard-variable');
|
||||||
|
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');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理Blackboard拖拽失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlackboardDragOver = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const hasBlackboardData = event.dataTransfer?.types.includes('application/blackboard-variable');
|
||||||
|
if (hasBlackboardData) {
|
||||||
|
event.dataTransfer!.dropEffect = 'copy';
|
||||||
|
const element = event.currentTarget as HTMLElement;
|
||||||
|
element.classList.add('drag-over');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlackboardDragLeave = (event: DragEvent) => {
|
||||||
|
const element = event.currentTarget as HTMLElement;
|
||||||
|
element.classList.remove('drag-over');
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearBlackboardReference = (propertyKey: string) => {
|
||||||
|
nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, '');
|
||||||
|
};
|
||||||
|
|
||||||
const startNodeDrag = (event: MouseEvent, node: any) => {
|
const startNodeDrag = (event: MouseEvent, node: any) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -448,8 +517,6 @@ export function useBehaviorTreeEditor() {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 自动检查安装状态
|
// 自动检查安装状态
|
||||||
installation.checkInstallStatus();
|
installation.checkInstallStatus();
|
||||||
@@ -551,6 +618,7 @@ export function useBehaviorTreeEditor() {
|
|||||||
...fileOps,
|
...fileOps,
|
||||||
...codeGen,
|
...codeGen,
|
||||||
...installation,
|
...installation,
|
||||||
|
...blackboard,
|
||||||
handleInstall,
|
handleInstall,
|
||||||
connectionState,
|
connectionState,
|
||||||
...connectionManager,
|
...connectionManager,
|
||||||
@@ -656,6 +724,23 @@ export function useBehaviorTreeEditor() {
|
|||||||
if (node.type === 'conditional-decorator') {
|
if (node.type === 'conditional-decorator') {
|
||||||
conditionAttachment.handleDecoratorDragLeave(node);
|
conditionAttachment.handleDecoratorDragLeave(node);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
// Blackboard拖拽相关功能
|
||||||
|
isBlackboardDroppable,
|
||||||
|
isBlackboardReference,
|
||||||
|
handleBlackboardDrop,
|
||||||
|
handleBlackboardDragOver,
|
||||||
|
handleBlackboardDragLeave,
|
||||||
|
clearBlackboardReference,
|
||||||
|
|
||||||
|
// Blackboard常驻侧边面板功能
|
||||||
|
blackboardCollapsed: computed({
|
||||||
|
get: () => blackboardSidebarState.collapsed,
|
||||||
|
set: (value: boolean) => blackboardSidebarState.collapsed = value
|
||||||
|
}),
|
||||||
|
blackboardTransparent: computed({
|
||||||
|
get: () => blackboardSidebarState.transparent,
|
||||||
|
set: (value: boolean) => blackboardSidebarState.transparent = value
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,575 @@
|
|||||||
|
import { ref, computed, reactive } from 'vue';
|
||||||
|
|
||||||
|
export interface BlackboardVariable {
|
||||||
|
name: string;
|
||||||
|
type: 'string' | 'number' | 'boolean' | 'vector2' | 'vector3' | 'object' | 'array';
|
||||||
|
value: any;
|
||||||
|
defaultValue: any;
|
||||||
|
description?: string;
|
||||||
|
group?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
constraints?: {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
allowedValues?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlackboardModalData {
|
||||||
|
name: string;
|
||||||
|
type: 'string' | 'number' | 'boolean' | 'vector2' | 'vector3' | 'object' | 'array';
|
||||||
|
defaultValue: any;
|
||||||
|
description: string;
|
||||||
|
group: string;
|
||||||
|
readOnly: boolean;
|
||||||
|
constraints: {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
};
|
||||||
|
useAllowedValues: boolean;
|
||||||
|
allowedValuesText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBlackboard() {
|
||||||
|
const blackboardVariables = ref<Map<string, BlackboardVariable>>(new Map());
|
||||||
|
const expandedGroups = ref<Set<string>>(new Set(['未分组']));
|
||||||
|
const selectedVariable = ref<BlackboardVariable | null>(null);
|
||||||
|
const showBlackboardModal = ref(false);
|
||||||
|
const editingBlackboardVariable = ref<BlackboardVariable | null>(null);
|
||||||
|
|
||||||
|
const blackboardModalData = reactive<BlackboardModalData>({
|
||||||
|
name: '',
|
||||||
|
type: 'string',
|
||||||
|
defaultValue: '',
|
||||||
|
description: '',
|
||||||
|
group: '',
|
||||||
|
readOnly: false,
|
||||||
|
constraints: {},
|
||||||
|
useAllowedValues: false,
|
||||||
|
allowedValuesText: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const showAddVariableDialog = ref(false);
|
||||||
|
const editingVariable = ref<BlackboardVariable | null>(null);
|
||||||
|
const newVariable = reactive({
|
||||||
|
name: '',
|
||||||
|
type: 'string' as any,
|
||||||
|
defaultValue: '' as any,
|
||||||
|
defaultValueText: '',
|
||||||
|
description: '',
|
||||||
|
group: '',
|
||||||
|
readonly: false,
|
||||||
|
min: undefined as number | undefined,
|
||||||
|
max: undefined as number | undefined,
|
||||||
|
optionsText: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const showImportExportDialog = ref(false);
|
||||||
|
const activeTab = ref('export');
|
||||||
|
const exportData = computed(() => {
|
||||||
|
const data = Array.from(blackboardVariables.value.values());
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
});
|
||||||
|
const importData = ref('');
|
||||||
|
const clearBeforeImport = ref(false);
|
||||||
|
|
||||||
|
const blackboardCollapsed = ref(false);
|
||||||
|
const blackboardTransparent = ref(true);
|
||||||
|
|
||||||
|
const blackboardVariablesArray = computed(() => {
|
||||||
|
return Array.from(blackboardVariables.value.values());
|
||||||
|
});
|
||||||
|
|
||||||
|
const blackboardVariableGroups = computed(() => {
|
||||||
|
const groups: Record<string, BlackboardVariable[]> = {};
|
||||||
|
|
||||||
|
blackboardVariables.value.forEach(variable => {
|
||||||
|
const groupName = variable.group || '未分组';
|
||||||
|
if (!groups[groupName]) {
|
||||||
|
groups[groupName] = [];
|
||||||
|
}
|
||||||
|
groups[groupName].push(variable);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedGroups: Record<string, BlackboardVariable[]> = {};
|
||||||
|
const groupNames = Object.keys(groups).sort((a, b) => {
|
||||||
|
if (a === '未分组') return -1;
|
||||||
|
if (b === '未分组') return 1;
|
||||||
|
return a.localeCompare(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
groupNames.forEach(groupName => {
|
||||||
|
groups[groupName].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
sortedGroups[groupName] = groups[groupName];
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedGroups;
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups = computed(() => {
|
||||||
|
const groupSet = new Set<string>();
|
||||||
|
blackboardVariables.value.forEach(variable => {
|
||||||
|
groupSet.add(variable.group || '未分组');
|
||||||
|
});
|
||||||
|
return Array.from(groupSet);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValidVariable = computed(() => {
|
||||||
|
return newVariable.name.trim().length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupedBlackboardVariables = () => {
|
||||||
|
return Object.entries(blackboardVariableGroups.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGroupExpanded = (groupName: string): boolean => {
|
||||||
|
return expandedGroups.value.has(groupName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleGroup = (groupName: string) => {
|
||||||
|
if (expandedGroups.value.has(groupName)) {
|
||||||
|
expandedGroups.value.delete(groupName);
|
||||||
|
} else {
|
||||||
|
expandedGroups.value.add(groupName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVariableTypeIcon = (type: string): string => {
|
||||||
|
const iconMap: Record<string, string> = {
|
||||||
|
string: '📝',
|
||||||
|
number: '🔢',
|
||||||
|
boolean: '☑️',
|
||||||
|
vector2: '📐',
|
||||||
|
vector3: '🧊',
|
||||||
|
object: '📦',
|
||||||
|
array: '📋'
|
||||||
|
};
|
||||||
|
return iconMap[type] || '❓';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatBlackboardValue = (variable: BlackboardVariable): string => {
|
||||||
|
if (variable.value === null || variable.value === undefined) {
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (variable.type) {
|
||||||
|
case 'boolean':
|
||||||
|
return variable.value ? 'true' : 'false';
|
||||||
|
case 'string':
|
||||||
|
return `"${variable.value}"`;
|
||||||
|
case 'number':
|
||||||
|
return variable.value.toString();
|
||||||
|
default:
|
||||||
|
return String(variable.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasVisibleConstraints = (variable: BlackboardVariable): boolean => {
|
||||||
|
if (!variable.constraints) return false;
|
||||||
|
|
||||||
|
return !!(
|
||||||
|
variable.constraints.min !== undefined ||
|
||||||
|
variable.constraints.max !== undefined ||
|
||||||
|
variable.constraints.allowedValues?.length
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatConstraints = (constraints: BlackboardVariable['constraints']): string => {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (constraints?.min !== undefined) {
|
||||||
|
parts.push(`最小: ${constraints.min}`);
|
||||||
|
}
|
||||||
|
if (constraints?.max !== undefined) {
|
||||||
|
parts.push(`最大: ${constraints.max}`);
|
||||||
|
}
|
||||||
|
if (constraints?.allowedValues?.length) {
|
||||||
|
parts.push(`可选: ${constraints.allowedValues.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(', ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeDisplayName = (type: string): string => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
string: 'STR',
|
||||||
|
number: 'NUM',
|
||||||
|
boolean: 'BOOL',
|
||||||
|
vector2: 'VEC2',
|
||||||
|
vector3: 'VEC3',
|
||||||
|
object: 'OBJ',
|
||||||
|
array: 'ARR'
|
||||||
|
};
|
||||||
|
return typeMap[type] || type.toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayValue = (variable: BlackboardVariable): string => {
|
||||||
|
if (variable.value === null || variable.value === undefined) {
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (variable.type) {
|
||||||
|
case 'string':
|
||||||
|
return String(variable.value);
|
||||||
|
case 'number':
|
||||||
|
return variable.value.toString();
|
||||||
|
case 'boolean':
|
||||||
|
return variable.value ? 'true' : 'false';
|
||||||
|
case 'vector2':
|
||||||
|
if (typeof variable.value === 'object' && variable.value.x !== undefined && variable.value.y !== undefined) {
|
||||||
|
return `(${variable.value.x}, ${variable.value.y})`;
|
||||||
|
}
|
||||||
|
return String(variable.value);
|
||||||
|
case 'vector3':
|
||||||
|
if (typeof variable.value === 'object' && variable.value.x !== undefined && variable.value.y !== undefined && variable.value.z !== undefined) {
|
||||||
|
return `(${variable.value.x}, ${variable.value.y}, ${variable.value.z})`;
|
||||||
|
}
|
||||||
|
return String(variable.value);
|
||||||
|
case 'object':
|
||||||
|
case 'array':
|
||||||
|
try {
|
||||||
|
const jsonStr = JSON.stringify(variable.value);
|
||||||
|
return jsonStr.length > 20 ? jsonStr.substring(0, 17) + '...' : jsonStr;
|
||||||
|
} catch {
|
||||||
|
return String(variable.value);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return String(variable.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTypeChange = () => {
|
||||||
|
switch (newVariable.type) {
|
||||||
|
case 'string':
|
||||||
|
newVariable.defaultValue = '';
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
newVariable.defaultValue = 0;
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
newVariable.defaultValue = false;
|
||||||
|
break;
|
||||||
|
case 'vector2':
|
||||||
|
newVariable.defaultValue = { x: 0, y: 0 };
|
||||||
|
break;
|
||||||
|
case 'vector3':
|
||||||
|
newVariable.defaultValue = { x: 0, y: 0, z: 0 };
|
||||||
|
break;
|
||||||
|
case 'object':
|
||||||
|
case 'array':
|
||||||
|
newVariable.defaultValue = '';
|
||||||
|
newVariable.defaultValueText = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveBlackboardVariable = () => {
|
||||||
|
if (!blackboardModalData.name.trim()) {
|
||||||
|
alert('请输入变量名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalValue = blackboardModalData.defaultValue;
|
||||||
|
|
||||||
|
// 处理对象和数组类型的JSON格式
|
||||||
|
if (blackboardModalData.type === 'object' || blackboardModalData.type === 'array') {
|
||||||
|
try {
|
||||||
|
if (typeof blackboardModalData.defaultValue === 'string') {
|
||||||
|
finalValue = blackboardModalData.defaultValue ? JSON.parse(blackboardModalData.defaultValue) : (blackboardModalData.type === 'array' ? [] : {});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('JSON格式错误,请检查输入');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const constraints: BlackboardVariable['constraints'] = {};
|
||||||
|
if (blackboardModalData.constraints.min !== undefined) constraints.min = blackboardModalData.constraints.min;
|
||||||
|
if (blackboardModalData.constraints.max !== undefined) constraints.max = blackboardModalData.constraints.max;
|
||||||
|
if (blackboardModalData.constraints.step !== undefined) constraints.step = blackboardModalData.constraints.step;
|
||||||
|
|
||||||
|
// 处理字符串的可选值列表
|
||||||
|
if (blackboardModalData.useAllowedValues && blackboardModalData.allowedValuesText.trim()) {
|
||||||
|
constraints.allowedValues = blackboardModalData.allowedValuesText
|
||||||
|
.split('\n')
|
||||||
|
.map(val => val.trim())
|
||||||
|
.filter(val => val.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const variable: BlackboardVariable = {
|
||||||
|
name: blackboardModalData.name,
|
||||||
|
type: blackboardModalData.type,
|
||||||
|
value: finalValue,
|
||||||
|
defaultValue: finalValue,
|
||||||
|
description: blackboardModalData.description,
|
||||||
|
group: blackboardModalData.group || undefined,
|
||||||
|
readOnly: blackboardModalData.readOnly,
|
||||||
|
constraints: Object.keys(constraints).length > 0 ? constraints : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
blackboardVariables.value.set(variable.name, variable);
|
||||||
|
|
||||||
|
const groupName = variable.group || '未分组';
|
||||||
|
expandedGroups.value.add(groupName);
|
||||||
|
|
||||||
|
showBlackboardModal.value = false;
|
||||||
|
editingBlackboardVariable.value = null;
|
||||||
|
|
||||||
|
// 重置模态框数据
|
||||||
|
Object.assign(blackboardModalData, {
|
||||||
|
name: '',
|
||||||
|
type: 'string',
|
||||||
|
defaultValue: '',
|
||||||
|
description: '',
|
||||||
|
group: '',
|
||||||
|
readOnly: false,
|
||||||
|
constraints: {},
|
||||||
|
useAllowedValues: false,
|
||||||
|
allowedValuesText: ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBlackboardVariable = (variableName: string) => {
|
||||||
|
if (confirm(`确定要删除变量 "${variableName}" 吗?`)) {
|
||||||
|
blackboardVariables.value.delete(variableName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBlackboardVariable = (variableName: string, newValue: any) => {
|
||||||
|
const variable = blackboardVariables.value.get(variableName);
|
||||||
|
if (!variable) return;
|
||||||
|
|
||||||
|
if (variable.readOnly) {
|
||||||
|
alert('该变量为只读,无法修改');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedVariable = { ...variable, value: newValue };
|
||||||
|
blackboardVariables.value.set(variableName, updatedVariable);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectVariable = (variable: BlackboardVariable) => {
|
||||||
|
selectedVariable.value = variable;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearBlackboard = () => {
|
||||||
|
if (confirm('确定要清空所有变量吗?此操作不可恢复。')) {
|
||||||
|
blackboardVariables.value.clear();
|
||||||
|
selectedVariable.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportBlackboard = () => {
|
||||||
|
const data = Array.from(blackboardVariables.value.values());
|
||||||
|
const json = JSON.stringify(data, null, 2);
|
||||||
|
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(json);
|
||||||
|
alert('Blackboard配置已复制到剪贴板');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const importBlackboard = () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
|
||||||
|
input.onchange = (event) => {
|
||||||
|
const file = (event.target as HTMLInputElement).files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target?.result as string);
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('格式错误:期望数组格式');
|
||||||
|
}
|
||||||
|
|
||||||
|
let importCount = 0;
|
||||||
|
data.forEach(varData => {
|
||||||
|
if (varData.name && varData.type) {
|
||||||
|
blackboardVariables.value.set(varData.name, varData);
|
||||||
|
importCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
alert(`成功导入 ${importCount} 个变量`);
|
||||||
|
} catch (error) {
|
||||||
|
alert('导入失败:' + (error as Error).message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVariableDragStart = (event: DragEvent, variable: BlackboardVariable) => {
|
||||||
|
if (!event.dataTransfer) return;
|
||||||
|
|
||||||
|
event.dataTransfer.setData('application/blackboard-variable', JSON.stringify({
|
||||||
|
name: variable.name,
|
||||||
|
type: variable.type,
|
||||||
|
value: variable.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBlackboardVariable = (variableName: string) => {
|
||||||
|
deleteBlackboardVariable(variableName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editVariable = (variable: BlackboardVariable) => {
|
||||||
|
editingBlackboardVariable.value = variable;
|
||||||
|
|
||||||
|
Object.assign(blackboardModalData, {
|
||||||
|
name: variable.name,
|
||||||
|
type: variable.type,
|
||||||
|
defaultValue: (variable.type === 'object' || variable.type === 'array') ? JSON.stringify(variable.value, null, 2) : variable.value,
|
||||||
|
description: variable.description || '',
|
||||||
|
group: variable.group || '',
|
||||||
|
readOnly: variable.readOnly || false,
|
||||||
|
constraints: {
|
||||||
|
min: variable.constraints?.min,
|
||||||
|
max: variable.constraints?.max,
|
||||||
|
step: variable.constraints?.step
|
||||||
|
},
|
||||||
|
useAllowedValues: !!(variable.constraints?.allowedValues?.length),
|
||||||
|
allowedValuesText: variable.constraints?.allowedValues?.join('\n') || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
showBlackboardModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlackboardDragStart = (event: DragEvent, variable: BlackboardVariable) => {
|
||||||
|
onVariableDragStart(event, variable);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeAddVariableDialog = () => {
|
||||||
|
showAddVariableDialog.value = false;
|
||||||
|
editingVariable.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveVariable = () => {
|
||||||
|
saveBlackboardVariable();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeImportExportDialog = () => {
|
||||||
|
showImportExportDialog.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyExportData = () => {
|
||||||
|
navigator.clipboard.writeText(exportData.value);
|
||||||
|
alert('已复制到剪贴板');
|
||||||
|
};
|
||||||
|
|
||||||
|
const importVariables = () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(importData.value);
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('格式错误:期望数组格式');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearBeforeImport.value) {
|
||||||
|
blackboardVariables.value.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let importCount = 0;
|
||||||
|
data.forEach((varData: any) => {
|
||||||
|
if (varData.name && varData.type) {
|
||||||
|
blackboardVariables.value.set(varData.name, varData);
|
||||||
|
importCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
alert(`成功导入 ${importCount} 个变量`);
|
||||||
|
showImportExportDialog.value = false;
|
||||||
|
importData.value = '';
|
||||||
|
} catch (error) {
|
||||||
|
alert('导入失败:' + (error as Error).message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addBlackboardVariable = () => {
|
||||||
|
Object.assign(newVariable, {
|
||||||
|
name: '',
|
||||||
|
type: 'string',
|
||||||
|
defaultValue: '',
|
||||||
|
defaultValueText: '',
|
||||||
|
description: '',
|
||||||
|
group: '',
|
||||||
|
readonly: false,
|
||||||
|
min: undefined,
|
||||||
|
max: undefined,
|
||||||
|
optionsText: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
editingVariable.value = null;
|
||||||
|
showAddVariableDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
blackboardVariables: blackboardVariablesArray,
|
||||||
|
selectedVariable,
|
||||||
|
showBlackboardModal,
|
||||||
|
editingBlackboardVariable,
|
||||||
|
blackboardModalData,
|
||||||
|
expandedGroups,
|
||||||
|
blackboardVariableGroups,
|
||||||
|
|
||||||
|
showAddVariableDialog,
|
||||||
|
editingVariable,
|
||||||
|
newVariable,
|
||||||
|
groups,
|
||||||
|
showImportExportDialog,
|
||||||
|
activeTab,
|
||||||
|
exportData,
|
||||||
|
importData,
|
||||||
|
clearBeforeImport,
|
||||||
|
isValidVariable,
|
||||||
|
blackboardCollapsed,
|
||||||
|
blackboardTransparent,
|
||||||
|
|
||||||
|
groupedBlackboardVariables,
|
||||||
|
isGroupExpanded,
|
||||||
|
toggleGroup,
|
||||||
|
getVariableTypeIcon,
|
||||||
|
formatBlackboardValue,
|
||||||
|
hasVisibleConstraints,
|
||||||
|
formatConstraints,
|
||||||
|
getTypeDisplayName,
|
||||||
|
getDisplayValue,
|
||||||
|
|
||||||
|
addBlackboardVariable,
|
||||||
|
saveBlackboardVariable,
|
||||||
|
deleteBlackboardVariable,
|
||||||
|
removeBlackboardVariable,
|
||||||
|
updateBlackboardVariable,
|
||||||
|
editVariable,
|
||||||
|
selectVariable,
|
||||||
|
clearBlackboard,
|
||||||
|
|
||||||
|
closeAddVariableDialog,
|
||||||
|
saveVariable,
|
||||||
|
onTypeChange,
|
||||||
|
closeImportExportDialog,
|
||||||
|
copyExportData,
|
||||||
|
importVariables,
|
||||||
|
|
||||||
|
exportBlackboard,
|
||||||
|
importBlackboard,
|
||||||
|
|
||||||
|
onBlackboardDragStart,
|
||||||
|
|
||||||
|
editBlackboardVariable: editVariable,
|
||||||
|
onBlackboardValueChange: (variable: BlackboardVariable) => {
|
||||||
|
updateBlackboardVariable(variable.name, variable.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -702,5 +702,370 @@ export const nodeTemplates: NodeTemplate[] = [
|
|||||||
maxChildren: 0,
|
maxChildren: 0,
|
||||||
className: 'DestroyEntityAction',
|
className: 'DestroyEntityAction',
|
||||||
namespace: 'ecs-integration/behaviors'
|
namespace: 'ecs-integration/behaviors'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 黑板相关节点 - 动作节点
|
||||||
|
{
|
||||||
|
type: 'set-blackboard-value',
|
||||||
|
name: '设置黑板变量',
|
||||||
|
icon: '📝',
|
||||||
|
category: 'action',
|
||||||
|
description: '设置黑板变量的值',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: true,
|
||||||
|
maxChildren: 0,
|
||||||
|
className: 'SetBlackboardValue',
|
||||||
|
namespace: 'behaviourTree/actions',
|
||||||
|
properties: {
|
||||||
|
variableName: {
|
||||||
|
name: '变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '黑板变量名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
name: '设置值',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '要设置的值(留空则使用源变量)',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
sourceVariable: {
|
||||||
|
name: '源变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '从另一个黑板变量复制值',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
force: {
|
||||||
|
name: '强制设置',
|
||||||
|
type: 'boolean',
|
||||||
|
value: false,
|
||||||
|
description: '是否忽略只读限制',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'add-blackboard-value',
|
||||||
|
name: '增加数值变量',
|
||||||
|
icon: '➕',
|
||||||
|
category: 'action',
|
||||||
|
description: '增加数值型黑板变量的值',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: true,
|
||||||
|
maxChildren: 0,
|
||||||
|
className: 'AddToBlackboardValue',
|
||||||
|
namespace: 'behaviourTree/actions',
|
||||||
|
properties: {
|
||||||
|
variableName: {
|
||||||
|
name: '变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '数值型黑板变量名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
increment: {
|
||||||
|
name: '增量',
|
||||||
|
type: 'number',
|
||||||
|
value: 1,
|
||||||
|
description: '增加的数值',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
incrementVariable: {
|
||||||
|
name: '增量变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '从另一个变量获取增量值',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'toggle-blackboard-bool',
|
||||||
|
name: '切换布尔变量',
|
||||||
|
icon: '🔄',
|
||||||
|
category: 'action',
|
||||||
|
description: '切换布尔型黑板变量的值',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: true,
|
||||||
|
maxChildren: 0,
|
||||||
|
className: 'ToggleBlackboardBool',
|
||||||
|
namespace: 'behaviourTree/actions',
|
||||||
|
properties: {
|
||||||
|
variableName: {
|
||||||
|
name: '变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '布尔型黑板变量名',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'reset-blackboard-variable',
|
||||||
|
name: '重置变量',
|
||||||
|
icon: '🔄',
|
||||||
|
category: 'action',
|
||||||
|
description: '重置黑板变量到默认值',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: true,
|
||||||
|
maxChildren: 0,
|
||||||
|
className: 'ResetBlackboardVariable',
|
||||||
|
namespace: 'behaviourTree/actions',
|
||||||
|
properties: {
|
||||||
|
variableName: {
|
||||||
|
name: '变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '要重置的黑板变量名',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'wait-blackboard-condition',
|
||||||
|
name: '等待黑板条件',
|
||||||
|
icon: '⏳',
|
||||||
|
category: 'action',
|
||||||
|
description: '等待黑板变量满足指定条件',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: true,
|
||||||
|
maxChildren: 0,
|
||||||
|
className: 'WaitForBlackboardCondition',
|
||||||
|
namespace: 'behaviourTree/actions',
|
||||||
|
properties: {
|
||||||
|
variableName: {
|
||||||
|
name: '变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '要监听的黑板变量名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
expectedValue: {
|
||||||
|
name: '期望值',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '期望的变量值',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'log-blackboard-value',
|
||||||
|
name: '记录黑板变量',
|
||||||
|
icon: '📊',
|
||||||
|
category: 'action',
|
||||||
|
description: '将黑板变量值记录到控制台',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: true,
|
||||||
|
maxChildren: 0,
|
||||||
|
className: 'LogBlackboardValue',
|
||||||
|
namespace: 'behaviourTree/actions',
|
||||||
|
properties: {
|
||||||
|
variableName: {
|
||||||
|
name: '变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '要记录的黑板变量名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
prefix: {
|
||||||
|
name: '日志前缀',
|
||||||
|
type: 'string',
|
||||||
|
value: '[Blackboard]',
|
||||||
|
description: '日志消息的前缀',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'math-blackboard-operation',
|
||||||
|
name: '数学运算',
|
||||||
|
icon: '🧮',
|
||||||
|
category: 'action',
|
||||||
|
description: '对黑板变量执行数学运算',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: true,
|
||||||
|
maxChildren: 0,
|
||||||
|
className: 'MathBlackboardOperation',
|
||||||
|
namespace: 'behaviourTree/actions',
|
||||||
|
properties: {
|
||||||
|
targetVariable: {
|
||||||
|
name: '目标变量',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '存储结果的变量名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
operand1Variable: {
|
||||||
|
name: '操作数1变量',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '第一个操作数的变量名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
operand2: {
|
||||||
|
name: '操作数2',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '第二个操作数(数值或变量名)',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
name: '运算类型',
|
||||||
|
type: 'select',
|
||||||
|
value: 'add',
|
||||||
|
options: ['add', 'subtract', 'multiply', 'divide', 'modulo', 'power', 'min', 'max'],
|
||||||
|
description: '要执行的数学运算',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 黑板相关节点 - 条件节点
|
||||||
|
{
|
||||||
|
type: 'blackboard-value-comparison',
|
||||||
|
name: '黑板值比较',
|
||||||
|
icon: '⚖️',
|
||||||
|
category: 'condition',
|
||||||
|
description: '比较黑板变量与指定值或另一个变量 (拖拽到条件装饰器上使用)',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: false,
|
||||||
|
maxChildren: 0,
|
||||||
|
isDraggableCondition: true,
|
||||||
|
attachableToDecorator: true,
|
||||||
|
className: 'BlackboardValueComparison',
|
||||||
|
namespace: 'behaviourTree/conditionals',
|
||||||
|
properties: {
|
||||||
|
variableName: {
|
||||||
|
name: '变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '要比较的黑板变量名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
operator: {
|
||||||
|
name: '比较操作符',
|
||||||
|
type: 'select',
|
||||||
|
value: 'equal',
|
||||||
|
options: ['equal', 'notEqual', 'greater', 'greaterOrEqual', 'less', 'lessOrEqual', 'contains', 'notContains'],
|
||||||
|
description: '比较操作类型',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
compareValue: {
|
||||||
|
name: '比较值',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '用于比较的值(留空则使用比较变量)',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
compareVariable: {
|
||||||
|
name: '比较变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '用于比较的另一个黑板变量名',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'blackboard-variable-exists',
|
||||||
|
name: '黑板变量存在',
|
||||||
|
icon: '✅',
|
||||||
|
category: 'condition',
|
||||||
|
description: '检查黑板变量是否存在且有效 (拖拽到条件装饰器上使用)',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: false,
|
||||||
|
maxChildren: 0,
|
||||||
|
isDraggableCondition: true,
|
||||||
|
attachableToDecorator: true,
|
||||||
|
className: 'BlackboardVariableExists',
|
||||||
|
namespace: 'behaviourTree/conditionals',
|
||||||
|
properties: {
|
||||||
|
variableName: {
|
||||||
|
name: '变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '要检查的黑板变量名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
invert: {
|
||||||
|
name: '反转结果',
|
||||||
|
type: 'boolean',
|
||||||
|
value: false,
|
||||||
|
description: '是否反转检查结果',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'blackboard-variable-type-check',
|
||||||
|
name: '黑板变量类型检查',
|
||||||
|
icon: '🔍',
|
||||||
|
category: 'condition',
|
||||||
|
description: '检查黑板变量是否为指定类型 (拖拽到条件装饰器上使用)',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: false,
|
||||||
|
maxChildren: 0,
|
||||||
|
isDraggableCondition: true,
|
||||||
|
attachableToDecorator: true,
|
||||||
|
className: 'BlackboardVariableTypeCheck',
|
||||||
|
namespace: 'behaviourTree/conditionals',
|
||||||
|
properties: {
|
||||||
|
variableName: {
|
||||||
|
name: '变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '要检查的黑板变量名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
expectedType: {
|
||||||
|
name: '期望类型',
|
||||||
|
type: 'select',
|
||||||
|
value: 'string',
|
||||||
|
options: ['string', 'number', 'boolean', 'vector2', 'vector3', 'object', 'array'],
|
||||||
|
description: '期望的变量类型',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'blackboard-variable-range-check',
|
||||||
|
name: '黑板变量范围检查',
|
||||||
|
icon: '📏',
|
||||||
|
category: 'condition',
|
||||||
|
description: '检查数值型黑板变量是否在指定范围内 (拖拽到条件装饰器上使用)',
|
||||||
|
canHaveChildren: false,
|
||||||
|
canHaveParent: false,
|
||||||
|
maxChildren: 0,
|
||||||
|
isDraggableCondition: true,
|
||||||
|
attachableToDecorator: true,
|
||||||
|
className: 'BlackboardVariableRangeCheck',
|
||||||
|
namespace: 'behaviourTree/conditionals',
|
||||||
|
properties: {
|
||||||
|
variableName: {
|
||||||
|
name: '变量名',
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
description: '要检查的数值型黑板变量名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
minValue: {
|
||||||
|
name: '最小值',
|
||||||
|
type: 'number',
|
||||||
|
value: 0,
|
||||||
|
description: '范围的最小值(包含)',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
maxValue: {
|
||||||
|
name: '最大值',
|
||||||
|
type: 'number',
|
||||||
|
value: 100,
|
||||||
|
description: '范围的最大值(包含)',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -45,16 +45,50 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 画布容器 */
|
||||||
|
.canvas-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #2d3748;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
.properties-panel-container {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar {
|
||||||
|
width: 260px;
|
||||||
|
right: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar.collapsed {
|
||||||
|
right: 232px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.nodes-panel {
|
.nodes-panel {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.properties-panel {
|
.properties-panel-container {
|
||||||
width: 280px;
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar {
|
||||||
|
width: 240px;
|
||||||
|
right: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar.collapsed {
|
||||||
|
right: 212px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,14 +97,33 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodes-panel, .properties-panel {
|
.nodes-panel {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.properties-panel-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar {
|
||||||
|
width: 250px;
|
||||||
|
max-height: 300px;
|
||||||
|
top: 380px;
|
||||||
|
right: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar.collapsed {
|
||||||
|
right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-container {
|
.canvas-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 400px;
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,13 +252,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.overlay-note {
|
.overlay-note {
|
||||||
padding: 12px;
|
margin-top: 16px;
|
||||||
background: rgba(59, 130, 246, 0.1);
|
|
||||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-note small {
|
.overlay-note small {
|
||||||
font-size: 14px;
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
@@ -32,52 +32,112 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
|
animation: modalFadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
backdrop-filter: blur(0px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: #2d3748;
|
background: #2d3748;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6);
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 1px solid #4a5568;
|
border: 1px solid #4a5568;
|
||||||
|
animation: modalSlideIn 0.3s ease-out;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 20px;
|
padding: 20px 24px;
|
||||||
border-bottom: 1px solid #4a5568;
|
border-bottom: 1px solid #4a5568;
|
||||||
background: #4a5568;
|
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||||
border-radius: 8px 8px 0 0;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header h3 {
|
.modal-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #e2e8f0;
|
color: #ffffff;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3::before {
|
||||||
|
content: '✨';
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header button {
|
.modal-header button {
|
||||||
background: none;
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border: none;
|
border: none;
|
||||||
color: #a0aec0;
|
color: #cbd5e0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
|
background: #2d3748;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-options {
|
.export-options {
|
||||||
@@ -112,22 +172,260 @@
|
|||||||
.modal-footer {
|
.modal-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px 20px;
|
padding: 20px 24px;
|
||||||
border-top: 1px solid #4a5568;
|
border-top: 1px solid #4a5568;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
background: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer button {
|
.modal-footer button {
|
||||||
padding: 8px 16px;
|
padding: 12px 20px;
|
||||||
background: #667eea;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
transition: background 0.3s ease;
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||||
|
transition: left 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer button:hover::before {
|
||||||
|
left: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer button:hover {
|
.modal-footer button:hover {
|
||||||
background: #5a67d8;
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .save-btn {
|
||||||
|
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .save-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .cancel-btn {
|
||||||
|
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .cancel-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blackboard模态框特定样式 */
|
||||||
|
.blackboard-modal {
|
||||||
|
width: 520px;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label::before {
|
||||||
|
content: '';
|
||||||
|
width: 3px;
|
||||||
|
height: 14px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-select,
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #1a202c;
|
||||||
|
border: 2px solid #4a5568;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus,
|
||||||
|
.form-select:focus,
|
||||||
|
.form-textarea:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:hover,
|
||||||
|
.form-select:hover,
|
||||||
|
.form-textarea:hover {
|
||||||
|
border-color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder,
|
||||||
|
.form-textarea::placeholder {
|
||||||
|
color: #718096;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint-inputs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint-row label {
|
||||||
|
min-width: 60px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a0aec0;
|
||||||
|
margin: 0;
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint-row label::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义复选框样式 */
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 16px 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid #4a5568;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #1a202c;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"]:checked {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"]:checked::after {
|
||||||
|
content: '✓';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
margin: 0;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint-row label {
|
||||||
|
min-width: 60px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.constraint-row input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allowed-values textarea {
|
||||||
|
margin-top: 6px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-code {
|
||||||
|
background: #1a202c;
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
resize: none;
|
||||||
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
@@ -11,15 +11,53 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
padding: 16px;
|
padding: 12px 16px;
|
||||||
background: #4a5568;
|
background: #4a5568;
|
||||||
border-bottom: 1px solid #718096;
|
border-bottom: 1px solid #718096;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header h3 {
|
.panel-header h3 {
|
||||||
margin: 0 0 12px 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
|
flex: 1;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-btn.small {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #1a202c;
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-btn.small:hover {
|
||||||
|
background: #2d3748;
|
||||||
|
border-color: #667eea;
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
@@ -49,10 +87,36 @@
|
|||||||
.category-title {
|
.category-title {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
background: #4a5568;
|
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
color: #e2e8f0;
|
font-weight: 500;
|
||||||
|
color: #cbd5e0;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
border-left: 3px solid #667eea;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||||
|
transition: left 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title:hover {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #4a5568 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
transform: translateX(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-list {
|
.node-list {
|
||||||
@@ -105,8 +169,8 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 右侧属性面板 */
|
/* 右侧属性面板容器 */
|
||||||
.properties-panel {
|
.properties-panel-container {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
background: #2d3748;
|
background: #2d3748;
|
||||||
border-left: 1px solid #4a5568;
|
border-left: 1px solid #4a5568;
|
||||||
@@ -115,6 +179,19 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 属性面板 */
|
||||||
|
.properties-panel {
|
||||||
|
flex: 1;
|
||||||
|
background: #2d3748;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.node-properties {
|
.node-properties {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -126,13 +203,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.property-section h4 {
|
.property-section h4 {
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 10px 0;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid #4a5568;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-item {
|
.property-item {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-item label {
|
.property-item label {
|
||||||
@@ -143,6 +226,10 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.property-input-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.property-item input,
|
.property-item input,
|
||||||
.property-item textarea,
|
.property-item textarea,
|
||||||
.property-item select {
|
.property-item select {
|
||||||
@@ -154,6 +241,7 @@
|
|||||||
color: white;
|
color: white;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-item input:focus,
|
.property-item input:focus,
|
||||||
@@ -178,7 +266,52 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-preview {
|
.blackboard-droppable {
|
||||||
|
border: 2px dashed transparent;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-droppable.drag-over {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-reference {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #4a5568;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ref-icon {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ref-text {
|
||||||
|
color: #e2e8f0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-ref {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #ef4444;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-ref:hover {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-preview {
|
||||||
background: #1a202c;
|
background: #1a202c;
|
||||||
border: 1px solid #4a5568;
|
border: 1px solid #4a5568;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -248,4 +381,543 @@
|
|||||||
.tree-node-type {
|
.tree-node-type {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: #718096;
|
color: #718096;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Blackboard悬浮窗口样式 */
|
||||||
|
.blackboard-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 160px;
|
||||||
|
right: 320px;
|
||||||
|
width: 280px;
|
||||||
|
max-height: calc(100vh - 180px);
|
||||||
|
background: #2d3748;
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar.collapsed {
|
||||||
|
width: 48px;
|
||||||
|
right: 272px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar.transparent {
|
||||||
|
opacity: 0.7;
|
||||||
|
background: rgba(45, 55, 72, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar.transparent:not(.collapsed) {
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-toggle {
|
||||||
|
position: absolute;
|
||||||
|
left: -24px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 24px;
|
||||||
|
height: 48px;
|
||||||
|
background: #667eea;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 1001;
|
||||||
|
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-toggle:hover {
|
||||||
|
background: #5a6acf;
|
||||||
|
transform: translateY(-50%) translateX(-2px);
|
||||||
|
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-toggle span {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar.collapsed .blackboard-toggle span {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-header {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
position: relative;
|
||||||
|
min-height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-scroll-area {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 12px 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .blackboard-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .blackboard-actions button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .add-var-btn {
|
||||||
|
background: #48bb78;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .add-var-btn:hover {
|
||||||
|
background: #38a169;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .clear-btn {
|
||||||
|
background: #f56565;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .clear-btn:hover {
|
||||||
|
background: #e53e3e;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .blackboard-groups {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-group {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .group-title {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #1a202c;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-bottom: 1px solid #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-list {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f7fafc;
|
||||||
|
font-size: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .value-separator {
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-type {
|
||||||
|
font-size: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
line-height: 1;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.string::before {
|
||||||
|
background: #48bb78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.string .variable-type {
|
||||||
|
background: #48bb78;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.number::before {
|
||||||
|
background: #3182ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.number .variable-type {
|
||||||
|
background: #3182ce;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.boolean::before {
|
||||||
|
background: #d69e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.boolean .variable-type {
|
||||||
|
background: #d69e2e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.vector2::before,
|
||||||
|
.blackboard-sidebar .variable-item.vector3::before {
|
||||||
|
background: #9f7aea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.vector2 .variable-type,
|
||||||
|
.blackboard-sidebar .variable-item.vector3 .variable-type {
|
||||||
|
background: #9f7aea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.object::before,
|
||||||
|
.blackboard-sidebar .variable-item.array::before {
|
||||||
|
background: #ed8936;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.object .variable-type,
|
||||||
|
.blackboard-sidebar .variable-item.array .variable-type {
|
||||||
|
background: #ed8936;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transform: translateX(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item:hover .variable-actions {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-actions button {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #cbd5e0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .edit-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-color: #667eea;
|
||||||
|
transform: translateY(-1px) scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .remove-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
|
||||||
|
border-color: #f56565;
|
||||||
|
transform: translateY(-1px) scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .value-display {
|
||||||
|
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #10b981;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 不同类型的值颜色 */
|
||||||
|
.blackboard-sidebar .variable-item.string .value-display {
|
||||||
|
color: #f59e0b; /* 琥珀色 - 字符串 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.number .value-display {
|
||||||
|
color: #3b82f6; /* 蓝色 - 数字 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.boolean .value-display {
|
||||||
|
color: #8b5cf6; /* 紫色 - 布尔值 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.vector2 .value-display,
|
||||||
|
.blackboard-sidebar .variable-item.vector3 .value-display {
|
||||||
|
color: #ec4899; /* 粉色 - 向量 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.object .value-display,
|
||||||
|
.blackboard-sidebar .variable-item.array .value-display {
|
||||||
|
color: #10b981; /* 绿色 - 对象/数组 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-constraints {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #718096;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .empty-blackboard {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .empty-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .empty-blackboard p {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .add-first-var {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .add-first-var:hover {
|
||||||
|
background: #5a67d8;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 只读变量样式 */
|
||||||
|
.blackboard-sidebar .variable-item.readonly {
|
||||||
|
opacity: 0.8;
|
||||||
|
background: linear-gradient(135deg, #374151 0%, #1f2937 100%) !important;
|
||||||
|
border-color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.readonly::before {
|
||||||
|
background: #6b7280 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.readonly .variable-name {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item.readonly .variable-type {
|
||||||
|
background: #6b7280 !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽状态样式 */
|
||||||
|
.blackboard-sidebar .variable-item:active {
|
||||||
|
cursor: grabbing !important;
|
||||||
|
transform: scale(0.95) rotate(2deg);
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式断点调整 */
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
.blackboard-sidebar .variable-item {
|
||||||
|
padding: 10px 14px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-name {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .value-display {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-type {
|
||||||
|
font-size: 7px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-actions button {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.blackboard-sidebar .variable-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-header {
|
||||||
|
margin-left: 16px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-name {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .value-display {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-item::before {
|
||||||
|
left: 10px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-actions button {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
font-size: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,15 +17,16 @@
|
|||||||
|
|
||||||
.toolbar-left h2 {
|
.toolbar-left h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 20px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-file {
|
.current-file {
|
||||||
color: #a0aec0;
|
color: #a0aec0;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +65,12 @@
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-btn.active {
|
||||||
|
background: rgba(102, 126, 234, 0.3);
|
||||||
|
border-color: #667eea;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.tool-btn.has-changes {
|
.tool-btn.has-changes {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<button class="tool-btn" @click="exportConfig" title="导出配置">
|
<button class="tool-btn" @click="exportConfig" title="导出配置">
|
||||||
<span>⚡</span> 导出配置
|
<span>⚡</span> 导出配置
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
@@ -395,104 +396,123 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧属性面板 -->
|
<!-- 右侧属性面板 -->
|
||||||
<div class="properties-panel">
|
<div class="properties-panel-container">
|
||||||
<div class="panel-header">
|
<!-- 属性面板 -->
|
||||||
<h3>⚙️ 属性面板</h3>
|
<div class="properties-panel">
|
||||||
</div>
|
<div class="panel-header">
|
||||||
|
<h3>⚙️ 属性面板</h3>
|
||||||
<div v-if="activeNode" class="node-properties">
|
|
||||||
<div class="property-section">
|
|
||||||
<h4>基本信息</h4>
|
|
||||||
<div class="property-item">
|
|
||||||
<label>节点名称:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
:value="activeNode.name"
|
|
||||||
@input="updateNodeProperty('name', $event.target.value)"
|
|
||||||
:key="activeNode.id + '_name'"
|
|
||||||
:disabled="activeNode.isConditionNode"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="property-item">
|
|
||||||
<label>描述:</label>
|
|
||||||
<textarea
|
|
||||||
:value="activeNode.description"
|
|
||||||
@input="updateNodeProperty('description', $event.target.value)"
|
|
||||||
:key="activeNode.id + '_description'"
|
|
||||||
:disabled="activeNode.isConditionNode"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="property-section" v-if="activeNode.properties">
|
<div v-if="activeNode" class="node-properties">
|
||||||
<h4>节点属性</h4>
|
<div class="property-section">
|
||||||
<div
|
<h4>基本信息</h4>
|
||||||
v-for="(prop, key) in activeNode.properties"
|
<div class="property-item">
|
||||||
:key="key"
|
<label>节点名称:</label>
|
||||||
class="property-item"
|
<input
|
||||||
>
|
type="text"
|
||||||
<label>{{ prop.name }}:</label>
|
:value="activeNode.name"
|
||||||
<input
|
@input="updateNodeProperty('name', $event.target.value)"
|
||||||
v-if="prop.type === 'string'"
|
:key="activeNode.id + '_name'"
|
||||||
type="text"
|
:disabled="activeNode.isConditionNode"
|
||||||
:value="prop.value"
|
|
||||||
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
|
||||||
:key="activeNode.id + '_' + key + '_string'"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-else-if="prop.type === 'number'"
|
|
||||||
type="number"
|
|
||||||
:value="prop.value"
|
|
||||||
@input="updateNodeProperty('properties.' + key + '.value', parseFloat($event.target.value) || 0)"
|
|
||||||
:key="activeNode.id + '_' + key + '_number'"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-else-if="prop.type === 'boolean'"
|
|
||||||
type="checkbox"
|
|
||||||
:checked="prop.value"
|
|
||||||
@change="updateNodeProperty('properties.' + key + '.value', $event.target.checked)"
|
|
||||||
:key="activeNode.id + '_' + key + '_boolean'"
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
v-else-if="prop.type === 'code'"
|
|
||||||
:value="prop.value"
|
|
||||||
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
|
||||||
rows="6"
|
|
||||||
class="code-input"
|
|
||||||
placeholder="请输入代码..."
|
|
||||||
:key="activeNode.id + '_' + key + '_code'"
|
|
||||||
></textarea>
|
|
||||||
<select
|
|
||||||
v-else-if="prop.type === 'select'"
|
|
||||||
:value="prop.value"
|
|
||||||
@change="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
|
||||||
:key="activeNode.id + '_' + key + '_select_' + prop.value"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="option in prop.options"
|
|
||||||
:key="option"
|
|
||||||
:value="option"
|
|
||||||
:selected="option === prop.value"
|
|
||||||
>
|
>
|
||||||
{{ option }}
|
</div>
|
||||||
</option>
|
<div class="property-item">
|
||||||
</select>
|
<label>描述:</label>
|
||||||
<p v-if="prop.description" class="property-help">{{ prop.description }}</p>
|
<textarea
|
||||||
|
:value="activeNode.description"
|
||||||
|
@input="updateNodeProperty('description', $event.target.value)"
|
||||||
|
:key="activeNode.id + '_description'"
|
||||||
|
:disabled="activeNode.isConditionNode"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="property-section" v-if="activeNode.properties">
|
||||||
|
<h4>节点属性</h4>
|
||||||
|
<div
|
||||||
|
v-for="(prop, key) in activeNode.properties"
|
||||||
|
:key="key"
|
||||||
|
class="property-item"
|
||||||
|
:class="{ 'blackboard-droppable': isBlackboardDroppable(prop) }"
|
||||||
|
@drop="handleBlackboardDrop($event, key)"
|
||||||
|
@dragover="handleBlackboardDragOver"
|
||||||
|
@dragleave="handleBlackboardDragLeave"
|
||||||
|
>
|
||||||
|
<label>{{ prop.name }}:</label>
|
||||||
|
<div class="property-input-container">
|
||||||
|
<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变量或直接输入'"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-else-if="prop.type === 'number'"
|
||||||
|
type="number"
|
||||||
|
:value="prop.value"
|
||||||
|
@input="updateNodeProperty('properties.' + key + '.value', parseFloat($event.target.value) || 0)"
|
||||||
|
:key="activeNode.id + '_' + key + '_number'"
|
||||||
|
:placeholder="'拖拽Blackboard变量或直接输入'"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-else-if="prop.type === 'boolean'"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="prop.value"
|
||||||
|
@change="updateNodeProperty('properties.' + key + '.value', $event.target.checked)"
|
||||||
|
:key="activeNode.id + '_' + key + '_boolean'"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
v-else-if="prop.type === 'code'"
|
||||||
|
:value="prop.value"
|
||||||
|
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
||||||
|
rows="6"
|
||||||
|
class="code-input"
|
||||||
|
placeholder="请输入代码..."
|
||||||
|
:key="activeNode.id + '_' + key + '_code'"
|
||||||
|
></textarea>
|
||||||
|
<select
|
||||||
|
v-else-if="prop.type === 'select'"
|
||||||
|
:value="prop.value"
|
||||||
|
@change="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
||||||
|
:key="activeNode.id + '_' + key + '_select_' + prop.value"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="option in prop.options"
|
||||||
|
:key="option"
|
||||||
|
:value="option"
|
||||||
|
:selected="option === prop.value"
|
||||||
|
>
|
||||||
|
{{ option }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Blackboard引用指示器 -->
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="property-section">
|
||||||
|
<h4>节点配置</h4>
|
||||||
|
<pre class="config-preview">{{ activeNode ? JSON.stringify(activeNode, null, 2) : '{}' }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="property-section">
|
<div v-else class="no-selection">
|
||||||
<h4>节点配置</h4>
|
<p>请选择一个节点查看属性</p>
|
||||||
<pre class="config-preview">{{ activeNode ? JSON.stringify(activeNode, null, 2) : '{}' }}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="no-selection">
|
|
||||||
<p>请选择一个节点查看属性</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- 行为树结构面板 -->
|
<!-- 行为树结构面板 -->
|
||||||
<div class="tree-structure-panel" v-if="rootNode()">
|
<div class="tree-structure-panel" v-if="rootNode()">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -514,6 +534,233 @@
|
|||||||
<span>{{ validationResult().message }}</span>
|
<span>{{ validationResult().message }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Blackboard变量编辑模态框 -->
|
||||||
|
<div v-if="showBlackboardModal" class="modal-overlay" @click="showBlackboardModal = false">
|
||||||
|
<div class="modal-content blackboard-modal" @click.stop>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>{{ editingBlackboardVariable ? '编辑变量' : '添加变量' }}</h3>
|
||||||
|
<button @click="showBlackboardModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>变量名称:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="blackboardModalData.name"
|
||||||
|
placeholder="例如:playerHealth、enemyCount..."
|
||||||
|
:disabled="editingBlackboardVariable"
|
||||||
|
class="form-input"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>变量类型:</label>
|
||||||
|
<select v-model="blackboardModalData.type" class="form-select">
|
||||||
|
<option value="string">📝 字符串 (string)</option>
|
||||||
|
<option value="number">🔢 数字 (number)</option>
|
||||||
|
<option value="boolean">☑️ 布尔值 (boolean)</option>
|
||||||
|
<option value="vector2">📐 二维向量 (vector2)</option>
|
||||||
|
<option value="vector3">📏 三维向量 (vector3)</option>
|
||||||
|
<option value="object">📦 对象 (object)</option>
|
||||||
|
<option value="array">📋 数组 (array)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>默认值:</label>
|
||||||
|
<input
|
||||||
|
v-if="blackboardModalData.type === 'string'"
|
||||||
|
type="text"
|
||||||
|
v-model="blackboardModalData.defaultValue"
|
||||||
|
placeholder="输入默认字符串值..."
|
||||||
|
class="form-input"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-else-if="blackboardModalData.type === 'number'"
|
||||||
|
type="number"
|
||||||
|
v-model="blackboardModalData.defaultValue"
|
||||||
|
placeholder="输入默认数值..."
|
||||||
|
class="form-input"
|
||||||
|
>
|
||||||
|
<div v-else-if="blackboardModalData.type === 'boolean'" class="checkbox-group">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="blackboardModalData.defaultValue"
|
||||||
|
id="defaultBoolValue"
|
||||||
|
>
|
||||||
|
<label for="defaultBoolValue">默认为 True</label>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-else
|
||||||
|
v-model="blackboardModalData.defaultValue"
|
||||||
|
placeholder="请输入JSON格式的默认值,例如:{"x": 0, "y": 0}"
|
||||||
|
rows="3"
|
||||||
|
class="form-textarea"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>描述:</label>
|
||||||
|
<textarea
|
||||||
|
v-model="blackboardModalData.description"
|
||||||
|
placeholder="描述这个变量的用途和作用..."
|
||||||
|
rows="2"
|
||||||
|
class="form-textarea"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>分组:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="blackboardModalData.group"
|
||||||
|
placeholder="例如:Player、AI、Environment、Game..."
|
||||||
|
class="form-input"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 约束设置 -->
|
||||||
|
<div v-if="blackboardModalData.type === 'number'" class="form-group">
|
||||||
|
<label>数值约束:</label>
|
||||||
|
<div class="constraint-inputs">
|
||||||
|
<div class="constraint-row">
|
||||||
|
<label>最小值:</label>
|
||||||
|
<input type="number" v-model="blackboardModalData.constraints.min" placeholder="不限制" class="form-input">
|
||||||
|
</div>
|
||||||
|
<div class="constraint-row">
|
||||||
|
<label>最大值:</label>
|
||||||
|
<input type="number" v-model="blackboardModalData.constraints.max" placeholder="不限制" class="form-input">
|
||||||
|
</div>
|
||||||
|
<div class="constraint-row">
|
||||||
|
<label>步长:</label>
|
||||||
|
<input type="number" v-model="blackboardModalData.constraints.step" placeholder="1" class="form-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="blackboardModalData.type === 'string'" class="form-group">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" v-model="blackboardModalData.useAllowedValues" id="useAllowedValues">
|
||||||
|
<label for="useAllowedValues">限制可选值</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="blackboardModalData.useAllowedValues" class="allowed-values">
|
||||||
|
<textarea
|
||||||
|
v-model="blackboardModalData.allowedValuesText"
|
||||||
|
placeholder="每行输入一个可选值 例如: idle running jumping attacking"
|
||||||
|
rows="4"
|
||||||
|
class="form-textarea"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" v-model="blackboardModalData.readOnly" id="readOnlyVar">
|
||||||
|
<label for="readOnlyVar">只读变量</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button @click="saveBlackboardVariable" class="save-btn">保存</button>
|
||||||
|
<button @click="showBlackboardModal = false" class="cancel-btn">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blackboard常驻侧边面板 -->
|
||||||
|
<div class="blackboard-sidebar"
|
||||||
|
:class="{
|
||||||
|
'collapsed': blackboardCollapsed,
|
||||||
|
'transparent': blackboardTransparent
|
||||||
|
}"
|
||||||
|
@mouseenter="blackboardTransparent = false"
|
||||||
|
@mouseleave="blackboardTransparent = true">
|
||||||
|
|
||||||
|
<!-- 收缩/展开按钮 -->
|
||||||
|
<button class="blackboard-toggle"
|
||||||
|
@click="blackboardCollapsed = !blackboardCollapsed"
|
||||||
|
:title="blackboardCollapsed ? '展开 Blackboard' : '收缩 Blackboard'">
|
||||||
|
<span v-if="blackboardCollapsed">📋</span>
|
||||||
|
<span v-else>◀</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 面板内容 -->
|
||||||
|
<div class="blackboard-content" v-show="!blackboardCollapsed">
|
||||||
|
<div class="blackboard-header">
|
||||||
|
<h3>📋 Blackboard</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blackboard操作按钮 -->
|
||||||
|
<div class="blackboard-actions">
|
||||||
|
<button @click="showBlackboardModal = true" class="add-var-btn">+ 添加变量</button>
|
||||||
|
<button @click="clearBlackboard" v-if="blackboardVariables.length > 0" class="clear-btn">清空</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blackboard变量列表 -->
|
||||||
|
<div class="blackboard-scroll-area">
|
||||||
|
<div v-if="blackboardVariables.length > 0" class="blackboard-groups">
|
||||||
|
<div
|
||||||
|
v-for="[groupName, variables] in groupedBlackboardVariables()"
|
||||||
|
:key="groupName"
|
||||||
|
class="variable-group"
|
||||||
|
>
|
||||||
|
<h4 class="group-title">{{ groupName }}</h4>
|
||||||
|
<div class="variable-list">
|
||||||
|
<div
|
||||||
|
v-for="variable in variables"
|
||||||
|
: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-actions">
|
||||||
|
<button @click="editBlackboardVariable(variable)" class="edit-btn">✏️</button>
|
||||||
|
<button @click="removeBlackboardVariable(variable.name)" class="remove-btn">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="variable-value">
|
||||||
|
<select
|
||||||
|
v-if="variable.constraints && variable.constraints.allowedValues"
|
||||||
|
v-model="variable.value"
|
||||||
|
@change="onBlackboardValueChange(variable)"
|
||||||
|
:disabled="variable.readOnly"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="option in variable.constraints.allowedValues"
|
||||||
|
:key="option"
|
||||||
|
:value="option"
|
||||||
|
>
|
||||||
|
{{ option }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<span v-else class="value-display">{{ getDisplayValue(variable) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 约束显示 -->
|
||||||
|
<div v-if="variable.constraints && hasVisibleConstraints(variable)" class="variable-constraints">
|
||||||
|
<small>{{ formatConstraints(variable.constraints) }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else class="empty-blackboard">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<p>还没有定义任何变量</p>
|
||||||
|
<button class="add-first-var" @click="showBlackboardModal = true">添加第一个变量</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 导出模态框 -->
|
<!-- 导出模态框 -->
|
||||||
<div v-if="showExportModal" class="modal-overlay" @click="showExportModal = false">
|
<div v-if="showExportModal" class="modal-overlay" @click="showExportModal = false">
|
||||||
<div class="modal-content" @click.stop>
|
<div class="modal-content" @click.stop>
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
<div class="blackboard-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h3>🎯 黑板变量</h3>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="btn-add-variable" @click="showAddVariableDialog = true" title="添加新变量">
|
||||||
|
➕ 添加变量
|
||||||
|
</button>
|
||||||
|
<button class="btn-import-export" @click="showImportExportDialog = true" title="导入/导出变量">
|
||||||
|
📤 导入/导出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="variables-container">
|
||||||
|
<!-- 按分组显示变量 -->
|
||||||
|
<div v-for="group in groups" :key="group" class="variable-group">
|
||||||
|
<div class="group-header" @click="toggleGroup(group)">
|
||||||
|
<span class="group-icon">{{ expandedGroups.has(group) ? '📂' : '📁' }}</span>
|
||||||
|
<span class="group-name">{{ group }}</span>
|
||||||
|
<span class="group-count">({{ groupedBlackboardVariables()[group]?.length || 0 }})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="expandedGroups.has(group)" class="group-variables">
|
||||||
|
<div v-for="variable in groupedBlackboardVariables()[group]" :key="variable.name" class="variable-item"
|
||||||
|
:class="[variable.type, { 'readonly': variable.readonly }]" :draggable="true"
|
||||||
|
@dragstart="onBlackboardDragStart($event, variable)">
|
||||||
|
|
||||||
|
<div class="variable-header">
|
||||||
|
<div class="variable-info">
|
||||||
|
<span class="variable-name">{{ variable.name }}</span>
|
||||||
|
<span class="value-separator">:</span>
|
||||||
|
<span class="value-display">{{ getDisplayValue(variable) }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="variable-type">{{ getTypeDisplayName(variable.type) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="variable-actions">
|
||||||
|
<button @click="editVariable(variable)" title="编辑" class="edit-btn">✏</button>
|
||||||
|
<button @click="removeBlackboardVariable(variable.name)" title="删除"
|
||||||
|
class="remove-btn">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="groups.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<div class="empty-text">还没有黑板变量</div>
|
||||||
|
<div class="empty-hint">点击"添加变量"来创建第一个变量</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加变量对话框 -->
|
||||||
|
<div v-if="showAddVariableDialog" class="dialog-overlay" @click="closeAddVariableDialog">
|
||||||
|
<div class="dialog" @click.stop>
|
||||||
|
<div class="dialog-header">
|
||||||
|
<h4>{{ editingVariable ? '编辑变量' : '添加变量' }}</h4>
|
||||||
|
<button @click="closeAddVariableDialog" class="dialog-close">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-content">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>变量名</label>
|
||||||
|
<input v-model="newVariable.name" :disabled="editingVariable" type="text" placeholder="请输入变量名"
|
||||||
|
class="form-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>变量类型</label>
|
||||||
|
<select v-model="newVariable.type" class="form-select" @change="onTypeChange">
|
||||||
|
<option value="string">字符串</option>
|
||||||
|
<option value="number">数字</option>
|
||||||
|
<option value="boolean">布尔</option>
|
||||||
|
<option value="vector2">Vector2</option>
|
||||||
|
<option value="vector3">Vector3</option>
|
||||||
|
<option value="object">对象</option>
|
||||||
|
<option value="array">数组</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>默认值</label>
|
||||||
|
<!-- 根据类型显示不同的输入控件 -->
|
||||||
|
<input v-if="newVariable.type === 'string'" v-model="newVariable.defaultValue" type="text"
|
||||||
|
class="form-input">
|
||||||
|
<input v-else-if="newVariable.type === 'number'" v-model.number="newVariable.defaultValue" type="number"
|
||||||
|
class="form-input">
|
||||||
|
<label v-else-if="newVariable.type === 'boolean'" class="checkbox-label">
|
||||||
|
<input type="checkbox" v-model="newVariable.defaultValue">
|
||||||
|
<span>{{ newVariable.defaultValue ? '真' : '假' }}</span>
|
||||||
|
</label>
|
||||||
|
<div v-else-if="newVariable.type === 'vector2'" class="vector-input">
|
||||||
|
<input v-model.number="newVariable.defaultValue.x" type="number" placeholder="X">
|
||||||
|
<input v-model.number="newVariable.defaultValue.y" type="number" placeholder="Y">
|
||||||
|
</div>
|
||||||
|
<div v-else-if="newVariable.type === 'vector3'" class="vector-input">
|
||||||
|
<input v-model.number="newVariable.defaultValue.x" type="number" placeholder="X">
|
||||||
|
<input v-model.number="newVariable.defaultValue.y" type="number" placeholder="Y">
|
||||||
|
<input v-model.number="newVariable.defaultValue.z" type="number" placeholder="Z">
|
||||||
|
</div>
|
||||||
|
<textarea v-else-if="newVariable.type === 'object' || newVariable.type === 'array'"
|
||||||
|
v-model="newVariable.defaultValueText" class="form-textarea" rows="3"
|
||||||
|
placeholder="JSON格式"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>描述</label>
|
||||||
|
<input v-model="newVariable.description" type="text" placeholder="变量的用途描述" class="form-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>分组</label>
|
||||||
|
<input v-model="newVariable.group" type="text" placeholder="变量分组(如:Player、Enemy等)" class="form-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" v-model="newVariable.readonly">
|
||||||
|
<span>只读</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数字类型的约束 -->
|
||||||
|
<div v-if="newVariable.type === 'number'" class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>最小值</label>
|
||||||
|
<input v-model.number="newVariable.min" type="number" class="form-input">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>最大值</label>
|
||||||
|
<input v-model.number="newVariable.max" type="number" class="form-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 可选值列表 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label>可选值列表(用逗号分隔)</label>
|
||||||
|
<input v-model="newVariable.optionsText" type="text" placeholder="例如: small,medium,large"
|
||||||
|
class="form-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button @click="closeAddVariableDialog" class="btn-cancel">取消</button>
|
||||||
|
<button @click="saveVariable" class="btn-save" :disabled="!isValidVariable">
|
||||||
|
{{ editingVariable ? '保存' : '添加' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 导入/导出对话框 -->
|
||||||
|
<div v-if="showImportExportDialog" class="dialog-overlay" @click="closeImportExportDialog">
|
||||||
|
<div class="dialog large" @click.stop>
|
||||||
|
<div class="dialog-header">
|
||||||
|
<h4>导入/导出黑板变量</h4>
|
||||||
|
<button @click="closeImportExportDialog" class="dialog-close">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-content">
|
||||||
|
<div class="tab-container">
|
||||||
|
<div class="tabs">
|
||||||
|
<button :class="{ active: activeTab === 'export' }" @click="activeTab = 'export'">导出</button>
|
||||||
|
<button :class="{ active: activeTab === 'import' }" @click="activeTab = 'import'">导入</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'export'" class="tab-content">
|
||||||
|
<p>以下是当前黑板变量的JSON数据,可以复制保存:</p>
|
||||||
|
<textarea v-model="exportData" readonly class="export-textarea" rows="15"></textarea>
|
||||||
|
<button @click="copyExportData" class="btn-copy">📋 复制到剪贴板</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'import'" class="tab-content">
|
||||||
|
<p>粘贴黑板变量的JSON数据进行导入:</p>
|
||||||
|
<textarea v-model="importData" class="import-textarea" rows="15"
|
||||||
|
placeholder="粘贴JSON数据..."></textarea>
|
||||||
|
<div class="import-options">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" v-model="clearBeforeImport">
|
||||||
|
<span>导入前清空现有变量</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button @click="importVariables" class="btn-import" :disabled="!importData.trim()">
|
||||||
|
📥 导入变量
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user