组件注册与添加

This commit is contained in:
YHH
2025-10-14 23:42:06 +08:00
parent 1cf5641c4c
commit 3a5e73266e
10 changed files with 481 additions and 4 deletions

View File

@@ -0,0 +1,175 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.add-component-dialog {
background-color: #2d2d2d;
border-radius: 8px;
width: 500px;
max-width: 90%;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #404040;
}
.dialog-header h3 {
margin: 0;
font-size: 16px;
color: #fff;
}
.close-btn {
background: none;
border: none;
color: #aaa;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
line-height: 20px;
transition: color 0.2s;
}
.close-btn:hover {
color: #fff;
}
.dialog-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 16px 20px;
}
.component-filter {
width: 100%;
padding: 8px 12px;
background-color: #1e1e1e;
border: 1px solid #404040;
border-radius: 4px;
color: #fff;
font-size: 14px;
margin-bottom: 12px;
}
.component-filter:focus {
outline: none;
border-color: #007acc;
}
.component-list {
flex: 1;
overflow-y: auto;
min-height: 200px;
max-height: 400px;
}
.component-category {
margin-bottom: 16px;
}
.category-header {
font-size: 12px;
font-weight: 600;
color: #888;
text-transform: uppercase;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid #404040;
}
.component-option {
padding: 10px 12px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 4px;
transition: background-color 0.2s;
}
.component-option:hover {
background-color: #3a3a3a;
}
.component-option.selected {
background-color: #094771;
border: 1px solid #007acc;
}
.component-name {
font-size: 14px;
color: #fff;
font-weight: 500;
margin-bottom: 2px;
}
.component-description {
font-size: 12px;
color: #aaa;
}
.empty-message {
color: #888;
text-align: center;
padding: 40px 20px;
font-size: 14px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 20px;
border-top: 1px solid #404040;
}
.btn {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-cancel {
background-color: #3a3a3a;
color: #fff;
}
.btn-cancel:hover:not(:disabled) {
background-color: #4a4a4a;
}
.btn-primary {
background-color: #007acc;
color: #fff;
}
.btn-primary:hover:not(:disabled) {
background-color: #0098ff;
}

View File

@@ -0,0 +1,108 @@
import { useState, useEffect } from 'react';
import { Entity } from '@esengine/ecs-framework';
import { ComponentRegistry, ComponentTypeInfo } from '@esengine/editor-core';
import './AddComponent.css';
interface AddComponentProps {
entity: Entity;
componentRegistry: ComponentRegistry;
onAdd: (componentName: string) => void;
onCancel: () => void;
}
export function AddComponent({ entity, componentRegistry, onAdd, onCancel }: AddComponentProps) {
const [components, setComponents] = useState<ComponentTypeInfo[]>([]);
const [selectedComponent, setSelectedComponent] = useState<string | null>(null);
const [filter, setFilter] = useState('');
useEffect(() => {
const allComponents = componentRegistry.getAllComponents();
const existingComponentNames = entity.components.map(c => c.constructor.name);
const availableComponents = allComponents.filter(
comp => !existingComponentNames.includes(comp.name)
);
setComponents(availableComponents);
}, [entity, componentRegistry]);
const filteredComponents = components.filter(comp =>
comp.name.toLowerCase().includes(filter.toLowerCase()) ||
comp.category?.toLowerCase().includes(filter.toLowerCase())
);
const handleAdd = () => {
if (selectedComponent) {
onAdd(selectedComponent);
}
};
const groupedComponents = filteredComponents.reduce((groups, comp) => {
const category = comp.category || 'Other';
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(comp);
return groups;
}, {} as Record<string, ComponentTypeInfo[]>);
return (
<div className="modal-overlay" onClick={onCancel}>
<div className="add-component-dialog" onClick={(e) => e.stopPropagation()}>
<div className="dialog-header">
<h3>Add Component</h3>
<button className="close-btn" onClick={onCancel}>&times;</button>
</div>
<div className="dialog-content">
<input
type="text"
className="component-filter"
placeholder="Search components..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
autoFocus
/>
<div className="component-list">
{Object.keys(groupedComponents).length === 0 ? (
<div className="empty-message">No available components</div>
) : (
Object.entries(groupedComponents).map(([category, comps]) => (
<div key={category} className="component-category">
<div className="category-header">{category}</div>
{comps.map(comp => (
<div
key={comp.name}
className={`component-option ${selectedComponent === comp.name ? 'selected' : ''}`}
onClick={() => setSelectedComponent(comp.name)}
onDoubleClick={handleAdd}
>
<div className="component-name">{comp.name}</div>
{comp.description && (
<div className="component-description">{comp.description}</div>
)}
</div>
))}
</div>
))
)}
</div>
</div>
<div className="dialog-footer">
<button className="btn btn-cancel" onClick={onCancel}>
Cancel
</button>
<button
className="btn btn-primary"
onClick={handleAdd}
disabled={!selectedComponent}
>
Add Component
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { Entity } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
import { Entity, Core } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub, ComponentRegistry } from '@esengine/editor-core';
import { AddComponent } from './AddComponent';
import '../styles/EntityInspector.css';
interface EntityInspectorProps {
@@ -10,10 +11,12 @@ interface EntityInspectorProps {
export function EntityInspector({ entityStore, messageHub }: EntityInspectorProps) {
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
const [showAddComponent, setShowAddComponent] = useState(false);
useEffect(() => {
const handleSelection = (data: { entity: Entity | null }) => {
setSelectedEntity(data.entity);
setShowAddComponent(false);
};
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
@@ -23,6 +26,32 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
};
}, [messageHub]);
const handleAddComponent = (componentName: string) => {
if (!selectedEntity) return;
const componentRegistry = Core.services.resolve(ComponentRegistry);
if (!componentRegistry) {
console.error('ComponentRegistry not found');
return;
}
const component = componentRegistry.createInstance(componentName);
if (component) {
selectedEntity.addComponent(component);
messageHub.publish('component:added', { entity: selectedEntity, component });
setShowAddComponent(false);
}
};
const handleRemoveComponent = (index: number) => {
if (!selectedEntity) return;
const component = selectedEntity.components[index];
if (component) {
selectedEntity.removeComponent(component);
messageHub.publish('component:removed', { entity: selectedEntity, component });
}
};
if (!selectedEntity) {
return (
<div className="entity-inspector">
@@ -63,7 +92,16 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
</div>
<div className="inspector-section">
<div className="section-header">Components ({components.length})</div>
<div className="section-header">
<span>Components ({components.length})</span>
<button
className="add-component-btn"
onClick={() => setShowAddComponent(true)}
title="Add Component"
>
+
</button>
</div>
<div className="section-content">
{components.length === 0 ? (
<div className="empty-state">No components</div>
@@ -73,6 +111,13 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
<li key={index} className="component-item">
<span className="component-icon">🔧</span>
<span className="component-name">{component.constructor.name}</span>
<button
className="remove-component-btn"
onClick={() => handleRemoveComponent(index)}
title="Remove Component"
>
×
</button>
</li>
))}
</ul>
@@ -80,6 +125,15 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
</div>
</div>
</div>
{showAddComponent && selectedEntity && (
<AddComponent
entity={selectedEntity}
componentRegistry={Core.services.resolve(ComponentRegistry)}
onAdd={handleAddComponent}
onCancel={() => setShowAddComponent(false)}
/>
)}
</div>
);
}