组件属性编辑器
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Core, Scene } from '@esengine/ecs-framework';
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService } from '@esengine/editor-core';
|
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, PropertyMetadataService } from '@esengine/editor-core';
|
||||||
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
|
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
|
||||||
import { SceneHierarchy } from './components/SceneHierarchy';
|
import { SceneHierarchy } from './components/SceneHierarchy';
|
||||||
import { EntityInspector } from './components/EntityInspector';
|
import { EntityInspector } from './components/EntityInspector';
|
||||||
@@ -13,13 +13,43 @@ import { en, zh } from './locales';
|
|||||||
import './styles/App.css';
|
import './styles/App.css';
|
||||||
|
|
||||||
// 在 App 组件外部初始化 Core 和基础服务
|
// 在 App 组件外部初始化 Core 和基础服务
|
||||||
Core.create({ debug: true });
|
const coreInstance = Core.create({ debug: true });
|
||||||
|
|
||||||
const localeService = new LocaleService();
|
const localeService = new LocaleService();
|
||||||
localeService.registerTranslations('en', en);
|
localeService.registerTranslations('en', en);
|
||||||
localeService.registerTranslations('zh', zh);
|
localeService.registerTranslations('zh', zh);
|
||||||
Core.services.registerInstance(LocaleService, localeService);
|
Core.services.registerInstance(LocaleService, localeService);
|
||||||
|
|
||||||
|
const propertyMetadata = new PropertyMetadataService();
|
||||||
|
Core.services.registerInstance(PropertyMetadataService, propertyMetadata);
|
||||||
|
|
||||||
|
propertyMetadata.register(TransformComponent, {
|
||||||
|
properties: {
|
||||||
|
x: { type: 'number', label: 'X Position' },
|
||||||
|
y: { type: 'number', label: 'Y Position' },
|
||||||
|
rotation: { type: 'number', label: 'Rotation', min: 0, max: 360 },
|
||||||
|
scaleX: { type: 'number', label: 'Scale X', min: 0, step: 0.1 },
|
||||||
|
scaleY: { type: 'number', label: 'Scale Y', min: 0, step: 0.1 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
propertyMetadata.register(SpriteComponent, {
|
||||||
|
properties: {
|
||||||
|
texturePath: { type: 'string', label: 'Texture Path' },
|
||||||
|
color: { type: 'color', label: 'Tint Color' },
|
||||||
|
visible: { type: 'boolean', label: 'Visible' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
propertyMetadata.register(RigidBodyComponent, {
|
||||||
|
properties: {
|
||||||
|
mass: { type: 'number', label: 'Mass', min: 0, step: 0.1 },
|
||||||
|
friction: { type: 'number', label: 'Friction', min: 0, max: 1, step: 0.01 },
|
||||||
|
restitution: { type: 'number', label: 'Restitution', min: 0, max: 1, step: 0.01 },
|
||||||
|
isDynamic: { type: 'boolean', label: 'Dynamic' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [initialized, setInitialized] = useState(false);
|
const [initialized, setInitialized] = useState(false);
|
||||||
const [pluginManager, setPluginManager] = useState<EditorPluginManager | null>(null);
|
const [pluginManager, setPluginManager] = useState<EditorPluginManager | null>(null);
|
||||||
@@ -68,7 +98,7 @@ function App() {
|
|||||||
Core.services.registerInstance(ComponentRegistry, componentRegistry);
|
Core.services.registerInstance(ComponentRegistry, componentRegistry);
|
||||||
|
|
||||||
const pluginMgr = new EditorPluginManager();
|
const pluginMgr = new EditorPluginManager();
|
||||||
pluginMgr.initialize(Core, Core.services);
|
pluginMgr.initialize(coreInstance, Core.services);
|
||||||
|
|
||||||
await pluginMgr.installEditor(new SceneInspectorPlugin());
|
await pluginMgr.installEditor(new SceneInspectorPlugin());
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Entity, Core } from '@esengine/ecs-framework';
|
import { Entity, Core } from '@esengine/ecs-framework';
|
||||||
import { EntityStoreService, MessageHub, ComponentRegistry } from '@esengine/editor-core';
|
import { EntityStoreService, MessageHub, ComponentRegistry } from '@esengine/editor-core';
|
||||||
import { AddComponent } from './AddComponent';
|
import { AddComponent } from './AddComponent';
|
||||||
|
import { PropertyInspector } from './PropertyInspector';
|
||||||
import '../styles/EntityInspector.css';
|
import '../styles/EntityInspector.css';
|
||||||
|
|
||||||
interface EntityInspectorProps {
|
interface EntityInspectorProps {
|
||||||
@@ -12,6 +13,7 @@ interface EntityInspectorProps {
|
|||||||
export function EntityInspector({ entityStore, messageHub }: EntityInspectorProps) {
|
export function EntityInspector({ entityStore, messageHub }: EntityInspectorProps) {
|
||||||
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
|
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
|
||||||
const [showAddComponent, setShowAddComponent] = useState(false);
|
const [showAddComponent, setShowAddComponent] = useState(false);
|
||||||
|
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleSelection = (data: { entity: Entity | null }) => {
|
const handleSelection = (data: { entity: Entity | null }) => {
|
||||||
@@ -52,6 +54,28 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleComponentExpanded = (index: number) => {
|
||||||
|
setExpandedComponents(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(index)) {
|
||||||
|
newSet.delete(index);
|
||||||
|
} else {
|
||||||
|
newSet.add(index);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePropertyChange = (component: any, propertyName: string, value: any) => {
|
||||||
|
if (!selectedEntity) return;
|
||||||
|
messageHub.publish('component:property:changed', {
|
||||||
|
entity: selectedEntity,
|
||||||
|
component,
|
||||||
|
propertyName,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (!selectedEntity) {
|
if (!selectedEntity) {
|
||||||
return (
|
return (
|
||||||
<div className="entity-inspector">
|
<div className="entity-inspector">
|
||||||
@@ -107,8 +131,18 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
|
|||||||
<div className="empty-state">No components</div>
|
<div className="empty-state">No components</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="component-list">
|
<ul className="component-list">
|
||||||
{components.map((component, index) => (
|
{components.map((component, index) => {
|
||||||
|
const isExpanded = expandedComponents.has(index);
|
||||||
|
return (
|
||||||
<li key={index} className="component-item">
|
<li key={index} className="component-item">
|
||||||
|
<div className="component-header">
|
||||||
|
<button
|
||||||
|
className="component-expand-btn"
|
||||||
|
onClick={() => toggleComponentExpanded(index)}
|
||||||
|
title={isExpanded ? 'Collapse' : 'Expand'}
|
||||||
|
>
|
||||||
|
{isExpanded ? '▼' : '▶'}
|
||||||
|
</button>
|
||||||
<span className="component-icon">🔧</span>
|
<span className="component-icon">🔧</span>
|
||||||
<span className="component-name">{component.constructor.name}</span>
|
<span className="component-name">{component.constructor.name}</span>
|
||||||
<button
|
<button
|
||||||
@@ -118,8 +152,18 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
|
|||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="component-properties">
|
||||||
|
<PropertyInspector
|
||||||
|
component={component}
|
||||||
|
onChange={(propertyName, value) => handlePropertyChange(component, propertyName, value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
156
packages/editor-app/src/components/PropertyInspector.tsx
Normal file
156
packages/editor-app/src/components/PropertyInspector.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Component, Core } from '@esengine/ecs-framework';
|
||||||
|
import { PropertyMetadataService, PropertyMetadata } from '@esengine/editor-core';
|
||||||
|
import '../styles/PropertyInspector.css';
|
||||||
|
|
||||||
|
interface PropertyInspectorProps {
|
||||||
|
component: Component;
|
||||||
|
onChange?: (propertyName: string, value: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PropertyInspector({ component, onChange }: PropertyInspectorProps) {
|
||||||
|
const [properties, setProperties] = useState<Record<string, PropertyMetadata>>({});
|
||||||
|
const [values, setValues] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const propertyMetadataService = Core.services.resolve(PropertyMetadataService);
|
||||||
|
if (!propertyMetadataService) return;
|
||||||
|
|
||||||
|
const metadata = propertyMetadataService.getEditableProperties(component);
|
||||||
|
setProperties(metadata);
|
||||||
|
|
||||||
|
const componentAsAny = component as any;
|
||||||
|
const currentValues: Record<string, any> = {};
|
||||||
|
for (const key in metadata) {
|
||||||
|
currentValues[key] = componentAsAny[key];
|
||||||
|
}
|
||||||
|
setValues(currentValues);
|
||||||
|
}, [component]);
|
||||||
|
|
||||||
|
const handleChange = (propertyName: string, value: any) => {
|
||||||
|
const componentAsAny = component as any;
|
||||||
|
componentAsAny[propertyName] = value;
|
||||||
|
|
||||||
|
setValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[propertyName]: value
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (onChange) {
|
||||||
|
onChange(propertyName, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderProperty = (propertyName: string, metadata: PropertyMetadata) => {
|
||||||
|
const value = values[propertyName];
|
||||||
|
const label = metadata.label || propertyName;
|
||||||
|
|
||||||
|
switch (metadata.type) {
|
||||||
|
case 'number':
|
||||||
|
return (
|
||||||
|
<div key={propertyName} className="property-field">
|
||||||
|
<label className="property-label">{label}</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input"
|
||||||
|
value={value ?? 0}
|
||||||
|
min={metadata.min}
|
||||||
|
max={metadata.max}
|
||||||
|
step={metadata.step ?? 1}
|
||||||
|
disabled={metadata.readOnly}
|
||||||
|
onChange={(e) => handleChange(propertyName, parseFloat(e.target.value) || 0)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'string':
|
||||||
|
return (
|
||||||
|
<div key={propertyName} className="property-field">
|
||||||
|
<label className="property-label">{label}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="property-input"
|
||||||
|
value={value ?? ''}
|
||||||
|
disabled={metadata.readOnly}
|
||||||
|
onChange={(e) => handleChange(propertyName, e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
return (
|
||||||
|
<div key={propertyName} className="property-field property-field-checkbox">
|
||||||
|
<label className="property-label">{label}</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="property-checkbox"
|
||||||
|
checked={value ?? false}
|
||||||
|
disabled={metadata.readOnly}
|
||||||
|
onChange={(e) => handleChange(propertyName, e.target.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'color':
|
||||||
|
return (
|
||||||
|
<div key={propertyName} className="property-field">
|
||||||
|
<label className="property-label">{label}</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="property-input property-color"
|
||||||
|
value={value ?? '#ffffff'}
|
||||||
|
disabled={metadata.readOnly}
|
||||||
|
onChange={(e) => handleChange(propertyName, e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'vector2':
|
||||||
|
case 'vector3':
|
||||||
|
return (
|
||||||
|
<div key={propertyName} className="property-field">
|
||||||
|
<label className="property-label">{label}</label>
|
||||||
|
<div className="property-vector">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-vector-input"
|
||||||
|
value={value?.x ?? 0}
|
||||||
|
disabled={metadata.readOnly}
|
||||||
|
placeholder="X"
|
||||||
|
onChange={(e) => handleChange(propertyName, { ...value, x: parseFloat(e.target.value) || 0 })}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-vector-input"
|
||||||
|
value={value?.y ?? 0}
|
||||||
|
disabled={metadata.readOnly}
|
||||||
|
placeholder="Y"
|
||||||
|
onChange={(e) => handleChange(propertyName, { ...value, y: parseFloat(e.target.value) || 0 })}
|
||||||
|
/>
|
||||||
|
{metadata.type === 'vector3' && (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-vector-input"
|
||||||
|
value={value?.z ?? 0}
|
||||||
|
disabled={metadata.readOnly}
|
||||||
|
placeholder="Z"
|
||||||
|
onChange={(e) => handleChange(propertyName, { ...value, z: parseFloat(e.target.value) || 0 })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="property-inspector">
|
||||||
|
{Object.entries(properties).map(([propertyName, metadata]) =>
|
||||||
|
renderProperty(propertyName, metadata)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -90,22 +90,48 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.component-item {
|
.component-item {
|
||||||
display: flex;
|
margin-bottom: 8px;
|
||||||
align-items: center;
|
|
||||||
padding: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
background-color: #252526;
|
background-color: #252526;
|
||||||
border: 1px solid #3c3c3c;
|
border: 1px solid #3c3c3c;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
position: relative;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-item:hover {
|
.component-item:hover {
|
||||||
background-color: #2a2d2e;
|
|
||||||
border-color: #505050;
|
border-color: #505050;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.component-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-header:hover {
|
||||||
|
background-color: #2a2d2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-expand-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #858585;
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-expand-btn:hover {
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
.component-icon {
|
.component-icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -132,6 +158,12 @@
|
|||||||
color: #ff5555;
|
color: #ff5555;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.component-properties {
|
||||||
|
border-top: 1px solid #3c3c3c;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
80
packages/editor-app/src/styles/PropertyInspector.css
Normal file
80
packages/editor-app/src/styles/PropertyInspector.css
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
.property-inspector {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-field-checkbox {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-field-checkbox .property-label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-input {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-input:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-checkbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-checkbox:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-color {
|
||||||
|
height: 36px;
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-vector {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-vector-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
87
packages/editor-core/src/Services/PropertyMetadata.ts
Normal file
87
packages/editor-core/src/Services/PropertyMetadata.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import type { IService } from '@esengine/ecs-framework';
|
||||||
|
import { Injectable, Component } from '@esengine/ecs-framework';
|
||||||
|
import { createLogger } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
const logger = createLogger('PropertyMetadata');
|
||||||
|
|
||||||
|
export type PropertyType = 'number' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3';
|
||||||
|
|
||||||
|
export interface PropertyMetadata {
|
||||||
|
type: PropertyType;
|
||||||
|
label?: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
options?: Array<{ label: string; value: any }>;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComponentMetadata {
|
||||||
|
properties: Record<string, PropertyMetadata>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件属性元数据服务
|
||||||
|
*
|
||||||
|
* 管理组件属性的元数据信息,用于动态生成属性编辑器
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class PropertyMetadataService implements IService {
|
||||||
|
private metadata: Map<new (...args: any[]) => Component, ComponentMetadata> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册组件元数据
|
||||||
|
*/
|
||||||
|
public register(componentType: new (...args: any[]) => Component, metadata: ComponentMetadata): void {
|
||||||
|
this.metadata.set(componentType, metadata);
|
||||||
|
logger.debug(`Registered metadata for component: ${componentType.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取组件元数据
|
||||||
|
*/
|
||||||
|
public getMetadata(componentType: new (...args: any[]) => Component): ComponentMetadata | undefined {
|
||||||
|
return this.metadata.get(componentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取组件的所有可编辑属性
|
||||||
|
*/
|
||||||
|
public getEditableProperties(component: Component): Record<string, PropertyMetadata> {
|
||||||
|
const metadata = this.metadata.get(component.constructor as new (...args: any[]) => Component);
|
||||||
|
if (!metadata) {
|
||||||
|
return this.inferProperties(component);
|
||||||
|
}
|
||||||
|
return metadata.properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推断组件属性(当没有明确元数据时)
|
||||||
|
*/
|
||||||
|
private inferProperties(component: Component): Record<string, PropertyMetadata> {
|
||||||
|
const properties: Record<string, PropertyMetadata> = {};
|
||||||
|
const componentAsAny = component as any;
|
||||||
|
|
||||||
|
for (const key in component) {
|
||||||
|
if (component.hasOwnProperty(key)) {
|
||||||
|
const value = componentAsAny[key];
|
||||||
|
const type = typeof value;
|
||||||
|
|
||||||
|
if (type === 'number') {
|
||||||
|
properties[key] = { type: 'number' };
|
||||||
|
} else if (type === 'string') {
|
||||||
|
properties[key] = { type: 'string' };
|
||||||
|
} else if (type === 'boolean') {
|
||||||
|
properties[key] = { type: 'boolean' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.metadata.clear();
|
||||||
|
logger.info('PropertyMetadataService disposed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,5 +13,6 @@ export * from './Services/SerializerRegistry';
|
|||||||
export * from './Services/EntityStoreService';
|
export * from './Services/EntityStoreService';
|
||||||
export * from './Services/ComponentRegistry';
|
export * from './Services/ComponentRegistry';
|
||||||
export * from './Services/LocaleService';
|
export * from './Services/LocaleService';
|
||||||
|
export * from './Services/PropertyMetadata';
|
||||||
|
|
||||||
export * from './Types/UITypes';
|
export * from './Types/UITypes';
|
||||||
|
|||||||
Reference in New Issue
Block a user