Feature/ecs behavior tree (#188)
* feat(behavior-tree): 完全 ECS 化的行为树系统 * feat(editor-app): 添加行为树可视化编辑器 * chore: 移除 Cocos Creator 扩展目录 * feat(editor-app): 行为树编辑器功能增强 * fix(editor-app): 修复 TypeScript 类型错误 * feat(editor-app): 使用 FlexLayout 重构面板系统并优化资产浏览器 * feat(editor-app): 改进编辑器UI样式并修复行为树执行顺序 * feat(behavior-tree,editor-app): 添加装饰器系统并优化编辑器性能 * feat(behavior-tree,editor-app): 添加属性绑定系统 * feat(editor-app,behavior-tree): 优化编辑器UI并改进行为树功能 * feat(editor-app,behavior-tree): 添加全局黑板系统并增强资产浏览器功能 * feat(behavior-tree,editor-app): 添加运行时资产导出系统 * feat(behavior-tree,editor-app): 添加SubTree系统和资产选择器 * feat(behavior-tree,editor-app): 优化系统架构并改进编辑器文件管理 * fix(behavior-tree,editor-app): 修复SubTree节点错误显示空节点警告 * fix(editor-app): 修复局部黑板类型定义文件扩展名错误
This commit is contained in:
831
packages/editor-app/src/components/BehaviorTreeBlackboard.tsx
Normal file
831
packages/editor-app/src/components/BehaviorTreeBlackboard.tsx
Normal file
@@ -0,0 +1,831 @@
|
||||
import { useState } from 'react';
|
||||
import { Clipboard, Edit2, Trash2, ChevronDown, ChevronRight, Globe, Save, Folder, FileCode } from 'lucide-react';
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import type { BlackboardValueType } from '@esengine/behavior-tree';
|
||||
import { GlobalBlackboardService } from '@esengine/behavior-tree';
|
||||
import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('BehaviorTreeBlackboard');
|
||||
|
||||
type SimpleBlackboardType = 'number' | 'string' | 'boolean' | 'object';
|
||||
|
||||
interface BlackboardVariable {
|
||||
key: string;
|
||||
value: any;
|
||||
type: SimpleBlackboardType;
|
||||
}
|
||||
|
||||
interface BehaviorTreeBlackboardProps {
|
||||
variables: Record<string, any>;
|
||||
initialVariables?: Record<string, any>;
|
||||
globalVariables?: Record<string, any>;
|
||||
onVariableChange: (key: string, value: any) => void;
|
||||
onVariableAdd: (key: string, value: any, type: SimpleBlackboardType) => void;
|
||||
onVariableDelete: (key: string) => void;
|
||||
onVariableRename?: (oldKey: string, newKey: string) => void;
|
||||
onGlobalVariableChange?: (key: string, value: any) => void;
|
||||
onGlobalVariableAdd?: (key: string, value: any, type: BlackboardValueType) => void;
|
||||
onGlobalVariableDelete?: (key: string) => void;
|
||||
projectPath?: string;
|
||||
hasUnsavedGlobalChanges?: boolean;
|
||||
onSaveGlobal?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树黑板变量面板
|
||||
*
|
||||
* 用于管理和调试行为树运行时的黑板变量
|
||||
*/
|
||||
export const BehaviorTreeBlackboard: React.FC<BehaviorTreeBlackboardProps> = ({
|
||||
variables,
|
||||
initialVariables,
|
||||
globalVariables,
|
||||
onVariableChange,
|
||||
onVariableAdd,
|
||||
onVariableDelete,
|
||||
onVariableRename,
|
||||
onGlobalVariableChange,
|
||||
onGlobalVariableAdd,
|
||||
onGlobalVariableDelete,
|
||||
projectPath,
|
||||
hasUnsavedGlobalChanges,
|
||||
onSaveGlobal
|
||||
}) => {
|
||||
const [viewMode, setViewMode] = useState<'local' | 'global'>('local');
|
||||
|
||||
const isModified = (key: string): boolean => {
|
||||
if (!initialVariables) return false;
|
||||
return JSON.stringify(variables[key]) !== JSON.stringify(initialVariables[key]);
|
||||
};
|
||||
|
||||
const handleExportTypeScript = async () => {
|
||||
try {
|
||||
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
|
||||
const config = globalBlackboard.exportConfig();
|
||||
|
||||
const tsCode = GlobalBlackboardTypeGenerator.generate(config);
|
||||
|
||||
const outputPath = await save({
|
||||
filters: [{
|
||||
name: 'TypeScript',
|
||||
extensions: ['ts']
|
||||
}],
|
||||
defaultPath: 'GlobalBlackboard.ts'
|
||||
});
|
||||
|
||||
if (outputPath) {
|
||||
await invoke('write_file_content', {
|
||||
path: outputPath,
|
||||
content: tsCode
|
||||
});
|
||||
logger.info('TypeScript 类型定义已导出', outputPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('导出 TypeScript 失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [newKey, setNewKey] = useState('');
|
||||
const [newValue, setNewValue] = useState('');
|
||||
const [newType, setNewType] = useState<BlackboardVariable['type']>('string');
|
||||
const [editingKey, setEditingKey] = useState<string | null>(null);
|
||||
const [editingNewKey, setEditingNewKey] = useState('');
|
||||
const [editValue, setEditValue] = useState('');
|
||||
const [editType, setEditType] = useState<BlackboardVariable['type']>('string');
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
const handleAddVariable = () => {
|
||||
if (!newKey.trim()) return;
|
||||
|
||||
let parsedValue: any = newValue;
|
||||
if (newType === 'number') {
|
||||
parsedValue = parseFloat(newValue) || 0;
|
||||
} else if (newType === 'boolean') {
|
||||
parsedValue = newValue === 'true';
|
||||
} else if (newType === 'object') {
|
||||
try {
|
||||
parsedValue = JSON.parse(newValue);
|
||||
} catch {
|
||||
parsedValue = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (viewMode === 'global' && onGlobalVariableAdd) {
|
||||
const globalType = newType as BlackboardValueType;
|
||||
onGlobalVariableAdd(newKey, parsedValue, globalType);
|
||||
} else {
|
||||
onVariableAdd(newKey, parsedValue, newType);
|
||||
}
|
||||
|
||||
setNewKey('');
|
||||
setNewValue('');
|
||||
setIsAdding(false);
|
||||
};
|
||||
|
||||
const handleStartEdit = (key: string, value: any) => {
|
||||
setEditingKey(key);
|
||||
setEditingNewKey(key);
|
||||
const currentType = getVariableType(value);
|
||||
setEditType(currentType);
|
||||
setEditValue(typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value));
|
||||
};
|
||||
|
||||
const handleSaveEdit = (key: string) => {
|
||||
const newKey = editingNewKey.trim();
|
||||
if (!newKey) return;
|
||||
|
||||
let parsedValue: any = editValue;
|
||||
if (editType === 'number') {
|
||||
parsedValue = parseFloat(editValue) || 0;
|
||||
} else if (editType === 'boolean') {
|
||||
parsedValue = editValue === 'true' || editValue === '1';
|
||||
} else if (editType === 'object') {
|
||||
try {
|
||||
parsedValue = JSON.parse(editValue);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (viewMode === 'global' && onGlobalVariableChange) {
|
||||
if (newKey !== key && onGlobalVariableDelete) {
|
||||
onGlobalVariableDelete(key);
|
||||
}
|
||||
onGlobalVariableChange(newKey, parsedValue);
|
||||
} else {
|
||||
if (newKey !== key && onVariableRename) {
|
||||
onVariableRename(key, newKey);
|
||||
}
|
||||
onVariableChange(newKey, parsedValue);
|
||||
}
|
||||
|
||||
setEditingKey(null);
|
||||
};
|
||||
|
||||
const toggleGroup = (groupName: string) => {
|
||||
setCollapsedGroups(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(groupName)) {
|
||||
newSet.delete(groupName);
|
||||
} else {
|
||||
newSet.add(groupName);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const getVariableType = (value: any): BlackboardVariable['type'] => {
|
||||
if (typeof value === 'number') return 'number';
|
||||
if (typeof value === 'boolean') return 'boolean';
|
||||
if (typeof value === 'object') return 'object';
|
||||
return 'string';
|
||||
};
|
||||
|
||||
const currentVariables = viewMode === 'global' ? (globalVariables || {}) : variables;
|
||||
const variableEntries = Object.entries(currentVariables);
|
||||
|
||||
const currentOnDelete = viewMode === 'global' ? onGlobalVariableDelete : onVariableDelete;
|
||||
|
||||
const groupedVariables: Record<string, Array<{ fullKey: string; varName: string; value: any }>> = variableEntries.reduce((groups, [key, value]) => {
|
||||
const parts = key.split('.');
|
||||
const groupName = (parts.length > 1 && parts[0]) ? parts[0] : 'default';
|
||||
const varName = parts.length > 1 ? parts.slice(1).join('.') : key;
|
||||
|
||||
if (!groups[groupName]) {
|
||||
groups[groupName] = [];
|
||||
}
|
||||
const group = groups[groupName];
|
||||
if (group) {
|
||||
group.push({ fullKey: key, varName, value });
|
||||
}
|
||||
return groups;
|
||||
}, {} as Record<string, Array<{ fullKey: string; varName: string; value: any }>>);
|
||||
|
||||
const groupNames = Object.keys(groupedVariables).sort((a, b) => {
|
||||
if (a === 'default') return 1;
|
||||
if (b === 'default') return -1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#1e1e1e',
|
||||
color: '#cccccc'
|
||||
}}>
|
||||
<style>{`
|
||||
.blackboard-list::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
.blackboard-list::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
.blackboard-list::-webkit-scrollbar-thumb {
|
||||
background: #3c3c3c;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.blackboard-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #4c4c4c;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* 标题栏 */}
|
||||
<div style={{
|
||||
backgroundColor: '#2d2d2d',
|
||||
borderBottom: '1px solid #333'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '10px 12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
color: '#ccc'
|
||||
}}>
|
||||
<Clipboard size={14} />
|
||||
<span>Blackboard</span>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
backgroundColor: '#1e1e1e',
|
||||
borderRadius: '3px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setViewMode('local')}
|
||||
style={{
|
||||
padding: '3px 8px',
|
||||
backgroundColor: viewMode === 'local' ? '#0e639c' : 'transparent',
|
||||
border: 'none',
|
||||
color: viewMode === 'local' ? '#fff' : '#888',
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px'
|
||||
}}
|
||||
>
|
||||
<Clipboard size={11} />
|
||||
Local
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('global')}
|
||||
style={{
|
||||
padding: '3px 8px',
|
||||
backgroundColor: viewMode === 'global' ? '#0e639c' : 'transparent',
|
||||
border: 'none',
|
||||
color: viewMode === 'global' ? '#fff' : '#888',
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px'
|
||||
}}
|
||||
>
|
||||
<Globe size={11} />
|
||||
Global
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 工具栏 */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#252525',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{
|
||||
flex: 1,
|
||||
fontSize: '10px',
|
||||
color: '#888',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
minWidth: 0,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{viewMode === 'global' && projectPath ? (
|
||||
<>
|
||||
<Folder size={10} style={{ flexShrink: 0 }} />
|
||||
<span style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>.ecs/global-blackboard.json</span>
|
||||
</>
|
||||
) : (
|
||||
<span>
|
||||
{viewMode === 'local' ? '当前行为树的本地变量' : '所有行为树共享的全局变量'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{viewMode === 'global' && onSaveGlobal && (
|
||||
<>
|
||||
<button
|
||||
onClick={hasUnsavedGlobalChanges ? onSaveGlobal : undefined}
|
||||
disabled={!hasUnsavedGlobalChanges}
|
||||
style={{
|
||||
padding: '4px 6px',
|
||||
backgroundColor: hasUnsavedGlobalChanges ? '#ff9800' : '#4caf50',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
color: '#fff',
|
||||
cursor: hasUnsavedGlobalChanges ? 'pointer' : 'not-allowed',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
opacity: hasUnsavedGlobalChanges ? 1 : 0.7
|
||||
}}
|
||||
title={hasUnsavedGlobalChanges ? '点击保存全局配置' : '全局配置已保存'}
|
||||
>
|
||||
<Save size={12} />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportTypeScript}
|
||||
style={{
|
||||
padding: '4px 6px',
|
||||
backgroundColor: '#9c27b0',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
title="导出为 TypeScript 类型定义"
|
||||
>
|
||||
<FileCode size={12} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsAdding(true)}
|
||||
style={{
|
||||
padding: '4px 6px',
|
||||
backgroundColor: '#0e639c',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
title="添加变量"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 变量列表 */}
|
||||
<div className="blackboard-list" style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
padding: '10px'
|
||||
}}>
|
||||
{variableEntries.length === 0 && !isAdding && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
fontSize: '12px',
|
||||
padding: '20px'
|
||||
}}>
|
||||
No variables yet. Click "Add" to create one.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupNames.map(groupName => {
|
||||
const isCollapsed = collapsedGroups.has(groupName);
|
||||
const groupVars = groupedVariables[groupName];
|
||||
|
||||
if (!groupVars) return null;
|
||||
|
||||
return (
|
||||
<div key={groupName} style={{ marginBottom: '8px' }}>
|
||||
{groupName !== 'default' && (
|
||||
<div
|
||||
onClick={() => toggleGroup(groupName)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
padding: '4px 6px',
|
||||
backgroundColor: '#252525',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '4px',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
{isCollapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
|
||||
<span style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold',
|
||||
color: '#888'
|
||||
}}>
|
||||
{groupName} ({groupVars.length})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCollapsed && groupVars.map(({ fullKey: key, varName, value }) => {
|
||||
const type = getVariableType(value);
|
||||
const isEditing = editingKey === key;
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
const variableData = {
|
||||
variableName: key,
|
||||
variableValue: value,
|
||||
variableType: type
|
||||
};
|
||||
e.dataTransfer.setData('application/blackboard-variable', JSON.stringify(variableData));
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
};
|
||||
|
||||
const typeColor =
|
||||
type === 'number' ? '#4ec9b0' :
|
||||
type === 'boolean' ? '#569cd6' :
|
||||
type === 'object' ? '#ce9178' : '#d4d4d4';
|
||||
|
||||
const displayValue = type === 'object' ?
|
||||
JSON.stringify(value) :
|
||||
String(value);
|
||||
|
||||
const truncatedValue = displayValue.length > 30 ?
|
||||
displayValue.substring(0, 30) + '...' :
|
||||
displayValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
draggable={!isEditing}
|
||||
onDragStart={handleDragStart}
|
||||
style={{
|
||||
marginBottom: '6px',
|
||||
padding: '6px 8px',
|
||||
backgroundColor: '#2d2d2d',
|
||||
borderRadius: '3px',
|
||||
borderLeft: `3px solid ${typeColor}`,
|
||||
cursor: isEditing ? 'default' : 'grab'
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '10px',
|
||||
color: '#666',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
Name
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={editingNewKey}
|
||||
onChange={(e) => setEditingNewKey(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px',
|
||||
marginBottom: '4px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '2px',
|
||||
color: '#9cdcfe',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
placeholder="Variable name (e.g., player.health)"
|
||||
/>
|
||||
<div style={{
|
||||
fontSize: '10px',
|
||||
color: '#666',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
Type
|
||||
</div>
|
||||
<select
|
||||
value={editType}
|
||||
onChange={(e) => setEditType(e.target.value as BlackboardVariable['type'])}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px',
|
||||
marginBottom: '4px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '2px',
|
||||
color: '#cccccc',
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="object">Object (JSON)</option>
|
||||
</select>
|
||||
<div style={{
|
||||
fontSize: '10px',
|
||||
color: '#666',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
Value
|
||||
</div>
|
||||
<textarea
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: editType === 'object' ? '60px' : '24px',
|
||||
padding: '4px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #0e639c',
|
||||
borderRadius: '2px',
|
||||
color: '#cccccc',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'monospace',
|
||||
resize: 'vertical',
|
||||
marginBottom: '4px'
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<button
|
||||
onClick={() => handleSaveEdit(key)}
|
||||
style={{
|
||||
padding: '3px 8px',
|
||||
backgroundColor: '#0e639c',
|
||||
border: 'none',
|
||||
borderRadius: '2px',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingKey(null)}
|
||||
style={{
|
||||
padding: '3px 8px',
|
||||
backgroundColor: '#3c3c3c',
|
||||
border: 'none',
|
||||
borderRadius: '2px',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#9cdcfe',
|
||||
fontWeight: 'bold',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
{varName} <span style={{
|
||||
color: '#666',
|
||||
fontWeight: 'normal',
|
||||
fontSize: '10px'
|
||||
}}>({type})</span>
|
||||
{viewMode === 'local' && isModified(key) && (
|
||||
<span style={{
|
||||
fontSize: '9px',
|
||||
color: '#ffbb00',
|
||||
backgroundColor: 'rgba(255, 187, 0, 0.15)',
|
||||
padding: '1px 4px',
|
||||
borderRadius: '2px'
|
||||
}} title="运行时修改的值,停止后会恢复">
|
||||
运行时
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '10px',
|
||||
fontFamily: 'monospace',
|
||||
color: typeColor,
|
||||
marginTop: '2px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
backgroundColor: (viewMode === 'local' && isModified(key)) ? 'rgba(255, 187, 0, 0.1)' : 'transparent',
|
||||
padding: '1px 3px',
|
||||
borderRadius: '2px'
|
||||
}} title={(viewMode === 'local' && isModified(key)) ? `初始值: ${JSON.stringify(initialVariables?.[key])}\n当前值: ${displayValue}` : displayValue}>
|
||||
{truncatedValue}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '2px',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<button
|
||||
onClick={() => handleStartEdit(key, value)}
|
||||
style={{
|
||||
padding: '2px',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="Edit"
|
||||
>
|
||||
<Edit2 size={12} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => currentOnDelete && currentOnDelete(key)}
|
||||
style={{
|
||||
padding: '2px',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#f44336',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 添加新变量表单 */}
|
||||
{isAdding && (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
backgroundColor: '#2d2d2d',
|
||||
borderRadius: '4px',
|
||||
borderLeft: '3px solid #0e639c'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '10px',
|
||||
color: '#9cdcfe'
|
||||
}}>
|
||||
New Variable
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Variable name"
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '3px',
|
||||
color: '#cccccc',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
/>
|
||||
|
||||
<select
|
||||
value={newType}
|
||||
onChange={(e) => setNewType(e.target.value as BlackboardVariable['type'])}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '3px',
|
||||
color: '#cccccc',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="object">Object (JSON)</option>
|
||||
</select>
|
||||
|
||||
<textarea
|
||||
placeholder={
|
||||
newType === 'object' ? '{"key": "value"}' :
|
||||
newType === 'boolean' ? 'true or false' :
|
||||
newType === 'number' ? '0' : 'value'
|
||||
}
|
||||
value={newValue}
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: newType === 'object' ? '80px' : '30px',
|
||||
padding: '6px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '3px',
|
||||
color: '#cccccc',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', gap: '5px' }}>
|
||||
<button
|
||||
onClick={handleAddVariable}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
backgroundColor: '#0e639c',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAdding(false);
|
||||
setNewKey('');
|
||||
setNewValue('');
|
||||
}}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
backgroundColor: '#3c3c3c',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div style={{
|
||||
padding: '8px 15px',
|
||||
borderTop: '1px solid #333',
|
||||
fontSize: '11px',
|
||||
color: '#666',
|
||||
backgroundColor: '#2d2d2d',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<span>
|
||||
{viewMode === 'local' ? 'Local' : 'Global'}: {variableEntries.length} variable{variableEntries.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user