refactor(editor-app): 改进架构和类型安全 (#226)
* refactor(editor-app): 改进架构和类型安全 * refactor(editor-app): 开始拆分 Inspector.tsx - 创建基础架构 * refactor(editor-app): 完成 Inspector.tsx 拆分 * refactor(editor-app): 优化 Inspector 类型定义,消除所有 any 使用 * refactor(editor): 实现可扩展的属性渲染器系统 * Potential fix for code scanning alert no. 231: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix(ci): 防止 Codecov 服务故障阻塞 CI 流程 --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
74
packages/editor-app/src/components/ErrorBoundary.tsx
Normal file
74
packages/editor-app/src/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { DomainError } from '../domain/errors';
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: (error: Error) => ReactNode;
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
console.error('ErrorBoundary caught error:', error, errorInfo);
|
||||
this.props.onError?.(error, errorInfo);
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError && this.state.error) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback(this.state.error);
|
||||
}
|
||||
|
||||
return <DefaultErrorFallback error={this.state.error} />;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function DefaultErrorFallback({ error }: { error: Error }): JSX.Element {
|
||||
const message = error instanceof DomainError ? error.getUserMessage() : error.message;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#fee',
|
||||
border: '1px solid #fcc',
|
||||
borderRadius: '4px',
|
||||
margin: '20px'
|
||||
}}
|
||||
>
|
||||
<h2 style={{ color: '#c00', marginTop: 0 }}>出错了</h2>
|
||||
<p>{message}</p>
|
||||
<details style={{ marginTop: '10px' }}>
|
||||
<summary style={{ cursor: 'pointer' }}>技术详情</summary>
|
||||
<pre
|
||||
style={{
|
||||
marginTop: '10px',
|
||||
padding: '10px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
{error.stack}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
packages/editor-app/src/components/PluginErrorBoundary.tsx
Normal file
48
packages/editor-app/src/components/PluginErrorBoundary.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { PluginError } from '../domain/errors';
|
||||
|
||||
interface PluginErrorBoundaryProps {
|
||||
pluginId: string;
|
||||
pluginName: string;
|
||||
children: React.ReactNode;
|
||||
onPluginError?: (pluginId: string, error: Error) => void;
|
||||
}
|
||||
|
||||
export function PluginErrorBoundary({
|
||||
pluginId,
|
||||
pluginName,
|
||||
children,
|
||||
onPluginError
|
||||
}: PluginErrorBoundaryProps): JSX.Element {
|
||||
const handleError = (error: Error) => {
|
||||
onPluginError?.(pluginId, error);
|
||||
};
|
||||
|
||||
const renderFallback = (error: Error) => {
|
||||
const pluginError =
|
||||
error instanceof PluginError ? error : new PluginError(error.message, pluginId, pluginName, 'execute', error);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
backgroundColor: '#fff3cd',
|
||||
border: '1px solid #ffc107',
|
||||
borderRadius: '4px',
|
||||
margin: '10px'
|
||||
}}
|
||||
>
|
||||
<h3 style={{ margin: '0 0 8px 0', color: '#856404' }}>插件错误</h3>
|
||||
<p style={{ margin: '0 0 8px 0' }}>{pluginError.getUserMessage()}</p>
|
||||
<small style={{ color: '#666' }}>插件ID: {pluginId}</small>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={renderFallback} onError={handleError}>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
203
packages/editor-app/src/components/inspectors/Inspector.tsx
Normal file
203
packages/editor-app/src/components/inspectors/Inspector.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Entity } from '@esengine/ecs-framework';
|
||||
import { TauriAPI } from '../../api/tauri';
|
||||
import { SettingsService } from '../../services/SettingsService';
|
||||
import { InspectorProps, InspectorTarget, AssetFileInfo, RemoteEntity } from './types';
|
||||
import { getProfilerService } from './utils';
|
||||
import {
|
||||
EmptyInspector,
|
||||
ExtensionInspector,
|
||||
AssetFileInspector,
|
||||
RemoteEntityInspector,
|
||||
EntityInspector
|
||||
} from './views';
|
||||
|
||||
export function Inspector({ entityStore: _entityStore, messageHub, inspectorRegistry, projectPath }: InspectorProps) {
|
||||
const [target, setTarget] = useState<InspectorTarget>(null);
|
||||
const [componentVersion, setComponentVersion] = useState(0);
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
const [decimalPlaces, setDecimalPlaces] = useState(() => {
|
||||
const settings = SettingsService.getInstance();
|
||||
return settings.get<number>('inspector.decimalPlaces', 4);
|
||||
});
|
||||
const targetRef = useRef<InspectorTarget>(null);
|
||||
|
||||
useEffect(() => {
|
||||
targetRef.current = target;
|
||||
}, [target]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSettingsChanged = (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
const changedSettings = customEvent.detail;
|
||||
if ('inspector.decimalPlaces' in changedSettings) {
|
||||
setDecimalPlaces(changedSettings['inspector.decimalPlaces']);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('settings:changed', handleSettingsChanged);
|
||||
return () => {
|
||||
window.removeEventListener('settings:changed', handleSettingsChanged);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEntitySelection = (data: { entity: Entity | null }) => {
|
||||
if (data.entity) {
|
||||
setTarget({ type: 'entity', data: data.entity });
|
||||
} else {
|
||||
setTarget(null);
|
||||
}
|
||||
setComponentVersion(0);
|
||||
};
|
||||
|
||||
const handleRemoteEntitySelection = (data: { entity: RemoteEntity }) => {
|
||||
setTarget({ type: 'remote-entity', data: data.entity });
|
||||
const profilerService = getProfilerService();
|
||||
if (profilerService && data.entity?.id !== undefined) {
|
||||
profilerService.requestEntityDetails(data.entity.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEntityDetails = (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
const details = customEvent.detail;
|
||||
const currentTarget = targetRef.current;
|
||||
if (currentTarget?.type === 'remote-entity' && details?.id === currentTarget.data.id) {
|
||||
setTarget({ ...currentTarget, details });
|
||||
}
|
||||
};
|
||||
|
||||
const handleExtensionSelection = (data: { data: unknown }) => {
|
||||
setTarget({ type: 'extension', data: data.data as Record<string, any> });
|
||||
};
|
||||
|
||||
const handleAssetFileSelection = async (data: { fileInfo: AssetFileInfo }) => {
|
||||
const fileInfo = data.fileInfo;
|
||||
|
||||
if (fileInfo.isDirectory) {
|
||||
setTarget({ type: 'asset-file', data: fileInfo });
|
||||
return;
|
||||
}
|
||||
|
||||
const textExtensions = [
|
||||
'txt',
|
||||
'json',
|
||||
'md',
|
||||
'ts',
|
||||
'tsx',
|
||||
'js',
|
||||
'jsx',
|
||||
'css',
|
||||
'html',
|
||||
'xml',
|
||||
'yaml',
|
||||
'yml',
|
||||
'toml',
|
||||
'ini',
|
||||
'cfg',
|
||||
'conf',
|
||||
'log',
|
||||
'btree',
|
||||
'ecs'
|
||||
];
|
||||
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'ico', 'tiff', 'tif'];
|
||||
const isTextFile = fileInfo.extension && textExtensions.includes(fileInfo.extension.toLowerCase());
|
||||
const isImageFile = fileInfo.extension && imageExtensions.includes(fileInfo.extension.toLowerCase());
|
||||
|
||||
if (isTextFile) {
|
||||
try {
|
||||
const content = await TauriAPI.readFileContent(fileInfo.path);
|
||||
setTarget({ type: 'asset-file', data: fileInfo, content });
|
||||
} catch (error) {
|
||||
console.error('Failed to read file content:', error);
|
||||
setTarget({ type: 'asset-file', data: fileInfo });
|
||||
}
|
||||
} else if (isImageFile) {
|
||||
setTarget({ type: 'asset-file', data: fileInfo, isImage: true });
|
||||
} else {
|
||||
setTarget({ type: 'asset-file', data: fileInfo });
|
||||
}
|
||||
};
|
||||
|
||||
const handleComponentChange = () => {
|
||||
setComponentVersion((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const unsubEntitySelect = messageHub.subscribe('entity:selected', handleEntitySelection);
|
||||
const unsubRemoteSelect = messageHub.subscribe('remote-entity:selected', handleRemoteEntitySelection);
|
||||
const unsubNodeSelect = messageHub.subscribe('behavior-tree:node-selected', handleExtensionSelection);
|
||||
const unsubAssetFileSelect = messageHub.subscribe('asset-file:selected', handleAssetFileSelection);
|
||||
const unsubComponentAdded = messageHub.subscribe('component:added', handleComponentChange);
|
||||
const unsubComponentRemoved = messageHub.subscribe('component:removed', handleComponentChange);
|
||||
|
||||
window.addEventListener('profiler:entity-details', handleEntityDetails);
|
||||
|
||||
return () => {
|
||||
unsubEntitySelect();
|
||||
unsubRemoteSelect();
|
||||
unsubNodeSelect();
|
||||
unsubAssetFileSelect();
|
||||
unsubComponentAdded();
|
||||
unsubComponentRemoved();
|
||||
window.removeEventListener('profiler:entity-details', handleEntityDetails);
|
||||
};
|
||||
}, [messageHub]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh || target?.type !== 'remote-entity') {
|
||||
return;
|
||||
}
|
||||
|
||||
const profilerService = getProfilerService();
|
||||
if (!profilerService) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleProfilerData = () => {
|
||||
const currentTarget = targetRef.current;
|
||||
if (currentTarget?.type === 'remote-entity' && currentTarget.data?.id !== undefined) {
|
||||
profilerService.requestEntityDetails(currentTarget.data.id);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = profilerService.subscribe(handleProfilerData);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [autoRefresh, target?.type]);
|
||||
|
||||
if (!target) {
|
||||
return <EmptyInspector />;
|
||||
}
|
||||
|
||||
if (target.type === 'extension') {
|
||||
return <ExtensionInspector data={target.data} inspectorRegistry={inspectorRegistry} projectPath={projectPath} />;
|
||||
}
|
||||
|
||||
if (target.type === 'asset-file') {
|
||||
return <AssetFileInspector fileInfo={target.data} content={target.content} isImage={target.isImage} />;
|
||||
}
|
||||
|
||||
if (target.type === 'remote-entity') {
|
||||
const entity = target.data;
|
||||
const details = target.details;
|
||||
|
||||
return (
|
||||
<RemoteEntityInspector
|
||||
entity={entity}
|
||||
details={details}
|
||||
autoRefresh={autoRefresh}
|
||||
onAutoRefreshChange={setAutoRefresh}
|
||||
decimalPlaces={decimalPlaces}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (target.type === 'entity') {
|
||||
return <EntityInspector entity={target.data} messageHub={messageHub} componentVersion={componentVersion} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { PropertyContext, PropertyRendererRegistry } from '@esengine/editor-core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
interface ComponentData {
|
||||
typeName: string;
|
||||
properties: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ComponentItemProps {
|
||||
component: ComponentData;
|
||||
decimalPlaces?: number;
|
||||
}
|
||||
|
||||
export function ComponentItem({ component, decimalPlaces = 4 }: ComponentItemProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '2px',
|
||||
backgroundColor: '#2a2a2a',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '6px 8px',
|
||||
backgroundColor: '#3a3a3a',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
borderBottom: isExpanded ? '1px solid #4a4a4a' : 'none'
|
||||
}}
|
||||
>
|
||||
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
color: '#e0e0e0'
|
||||
}}
|
||||
>
|
||||
{component.typeName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div style={{ padding: '6px 8px' }}>
|
||||
{Object.entries(component.properties).map(([propName, propValue]) => {
|
||||
const registry = Core.services.resolve(PropertyRendererRegistry);
|
||||
const context: PropertyContext = {
|
||||
name: propName,
|
||||
decimalPlaces,
|
||||
readonly: true,
|
||||
depth: 1
|
||||
};
|
||||
const rendered = registry.render(propValue, context);
|
||||
return rendered ? <div key={propName}>{rendered}</div> : null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
export interface ImagePreviewProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
}
|
||||
|
||||
export function ImagePreview({ src, alt }: ImagePreviewProps) {
|
||||
const [scale, setScale] = useState(1);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleWheel = (e: React.WheelEvent) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
setScale((prev) => Math.min(Math.max(prev * delta, 0.1), 10));
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (e.button === 0) {
|
||||
setIsDragging(true);
|
||||
setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (isDragging) {
|
||||
setPosition({
|
||||
x: e.clientX - dragStart.x,
|
||||
y: e.clientY - dragStart.y
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setScale(1);
|
||||
setPosition({ x: 0, y: 0 });
|
||||
};
|
||||
|
||||
if (imageError) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
color: '#f87171',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
图片加载失败
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
onWheel={handleWheel}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: '4px',
|
||||
minHeight: '200px',
|
||||
maxHeight: '400px',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
backgroundImage: `
|
||||
linear-gradient(45deg, #404040 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #404040 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #404040 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #404040 75%)
|
||||
`,
|
||||
backgroundSize: '16px 16px',
|
||||
backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0px',
|
||||
backgroundColor: '#2a2a2a'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
draggable={false}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: `translate(calc(-50% + ${position.x}px), calc(-50% + ${position.y}px)) scale(${scale})`,
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: '6px',
|
||||
fontSize: '10px',
|
||||
color: '#888'
|
||||
}}
|
||||
>
|
||||
<span>缩放: {(scale * 100).toFixed(0)}%</span>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
style={{
|
||||
background: '#3a3a3a',
|
||||
border: 'none',
|
||||
color: '#ccc',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { PropertyContext, PropertyRendererRegistry } from '@esengine/editor-core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
interface PropertyFieldProps {
|
||||
name: string;
|
||||
value: any;
|
||||
readonly?: boolean;
|
||||
decimalPlaces?: number;
|
||||
path?: string[];
|
||||
onChange?: (value: any) => void;
|
||||
}
|
||||
|
||||
export function PropertyField({
|
||||
name,
|
||||
value,
|
||||
readonly = false,
|
||||
decimalPlaces = 4,
|
||||
path = [],
|
||||
onChange
|
||||
}: PropertyFieldProps) {
|
||||
const registry = Core.services.resolve(PropertyRendererRegistry);
|
||||
|
||||
const context: PropertyContext = {
|
||||
name,
|
||||
path,
|
||||
readonly,
|
||||
decimalPlaces,
|
||||
depth: 0,
|
||||
expandByDefault: false
|
||||
};
|
||||
|
||||
const rendered = registry.render(value, context);
|
||||
|
||||
if (rendered) {
|
||||
return <>{rendered}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="property-field">
|
||||
<label className="property-label">{name}</label>
|
||||
<span
|
||||
className="property-value-text"
|
||||
style={{
|
||||
color: '#666',
|
||||
fontStyle: 'italic',
|
||||
fontSize: '0.9em'
|
||||
}}
|
||||
>
|
||||
No renderer available
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { ComponentItem } from './ComponentItem';
|
||||
export { ImagePreview } from './ImagePreview';
|
||||
export { PropertyField } from './PropertyField';
|
||||
export type { ComponentItemProps } from './ComponentItem';
|
||||
export type { ImagePreviewProps } from './ImagePreview';
|
||||
2
packages/editor-app/src/components/inspectors/index.ts
Normal file
2
packages/editor-app/src/components/inspectors/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Inspector } from './Inspector';
|
||||
export type { InspectorProps, InspectorTarget, AssetFileInfo } from './types';
|
||||
54
packages/editor-app/src/components/inspectors/types.ts
Normal file
54
packages/editor-app/src/components/inspectors/types.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Entity } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub, InspectorRegistry } from '@esengine/editor-core';
|
||||
|
||||
export interface InspectorProps {
|
||||
entityStore: EntityStoreService;
|
||||
messageHub: MessageHub;
|
||||
inspectorRegistry: InspectorRegistry;
|
||||
projectPath?: string | null;
|
||||
}
|
||||
|
||||
export interface AssetFileInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
extension?: string;
|
||||
size?: number;
|
||||
modified?: number;
|
||||
isDirectory: boolean;
|
||||
}
|
||||
|
||||
type ExtensionData = Record<string, any>;
|
||||
|
||||
export type InspectorTarget =
|
||||
| { type: 'entity'; data: Entity }
|
||||
| { type: 'remote-entity'; data: RemoteEntity; details?: EntityDetails }
|
||||
| { type: 'asset-file'; data: AssetFileInfo; content?: string; isImage?: boolean }
|
||||
| { type: 'extension'; data: ExtensionData }
|
||||
| null;
|
||||
|
||||
export interface RemoteEntity {
|
||||
id: number;
|
||||
destroyed?: boolean;
|
||||
componentTypes?: string[];
|
||||
name?: string;
|
||||
enabled?: boolean;
|
||||
tag?: number;
|
||||
depth?: number;
|
||||
updateOrder?: number;
|
||||
parentId?: number | null;
|
||||
childCount?: number;
|
||||
activeInHierarchy?: boolean;
|
||||
componentMask?: string;
|
||||
}
|
||||
|
||||
export interface ComponentData {
|
||||
typeName: string;
|
||||
properties: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface EntityDetails {
|
||||
id: number;
|
||||
components?: ComponentData[];
|
||||
componentTypes?: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
32
packages/editor-app/src/components/inspectors/utils.ts
Normal file
32
packages/editor-app/src/components/inspectors/utils.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ComponentData } from './types';
|
||||
|
||||
export function formatNumber(value: number, decimalPlaces: number): string {
|
||||
if (decimalPlaces < 0) {
|
||||
return String(value);
|
||||
}
|
||||
if (Number.isInteger(value)) {
|
||||
return String(value);
|
||||
}
|
||||
return value.toFixed(decimalPlaces);
|
||||
}
|
||||
|
||||
export interface ProfilerService {
|
||||
requestEntityDetails(entityId: number): void;
|
||||
subscribe(callback: () => void): () => void;
|
||||
}
|
||||
|
||||
export function getProfilerService(): ProfilerService | undefined {
|
||||
return (window as any).__PROFILER_SERVICE__;
|
||||
}
|
||||
|
||||
export function isComponentData(value: unknown): value is ComponentData {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
'typeName' in value &&
|
||||
typeof (value as Record<string, unknown>).typeName === 'string' &&
|
||||
'properties' in value &&
|
||||
typeof (value as Record<string, unknown>).properties === 'object'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Folder, File as FileIcon, Image as ImageIcon, Clock, HardDrive } from 'lucide-react';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { AssetFileInfo } from '../types';
|
||||
import { ImagePreview } from '../common';
|
||||
import '../../../styles/EntityInspector.css';
|
||||
|
||||
interface AssetFileInspectorProps {
|
||||
fileInfo: AssetFileInfo;
|
||||
content?: string;
|
||||
isImage?: boolean;
|
||||
}
|
||||
|
||||
function formatFileSize(bytes?: number): string {
|
||||
if (!bytes) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatDate(timestamp?: number): string {
|
||||
if (!timestamp) return '未知';
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInspectorProps) {
|
||||
const IconComponent = fileInfo.isDirectory ? Folder : isImage ? ImageIcon : FileIcon;
|
||||
const iconColor = fileInfo.isDirectory ? '#dcb67a' : isImage ? '#a78bfa' : '#90caf9';
|
||||
|
||||
return (
|
||||
<div className="entity-inspector">
|
||||
<div className="inspector-header">
|
||||
<IconComponent size={16} style={{ color: iconColor }} />
|
||||
<span className="entity-name">{fileInfo.name}</span>
|
||||
</div>
|
||||
|
||||
<div className="inspector-content">
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">文件信息</div>
|
||||
<div className="property-field">
|
||||
<label className="property-label">类型</label>
|
||||
<span className="property-value-text">
|
||||
{fileInfo.isDirectory
|
||||
? '文件夹'
|
||||
: fileInfo.extension
|
||||
? `.${fileInfo.extension}`
|
||||
: '文件'}
|
||||
</span>
|
||||
</div>
|
||||
{fileInfo.size !== undefined && !fileInfo.isDirectory && (
|
||||
<div className="property-field">
|
||||
<label className="property-label">
|
||||
<HardDrive size={14} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
|
||||
大小
|
||||
</label>
|
||||
<span className="property-value-text">{formatFileSize(fileInfo.size)}</span>
|
||||
</div>
|
||||
)}
|
||||
{fileInfo.modified !== undefined && (
|
||||
<div className="property-field">
|
||||
<label className="property-label">
|
||||
<Clock size={14} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
|
||||
修改时间
|
||||
</label>
|
||||
<span className="property-value-text">{formatDate(fileInfo.modified)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="property-field">
|
||||
<label className="property-label">路径</label>
|
||||
<span
|
||||
className="property-value-text"
|
||||
style={{
|
||||
fontFamily: 'Consolas, Monaco, monospace',
|
||||
fontSize: '11px',
|
||||
color: '#666',
|
||||
wordBreak: 'break-all'
|
||||
}}
|
||||
>
|
||||
{fileInfo.path}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isImage && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">图片预览</div>
|
||||
<ImagePreview src={convertFileSrc(fileInfo.path)} alt={fileInfo.name} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">文件预览</div>
|
||||
<div className="file-preview-content">{content}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!content && !isImage && !fileInfo.isDirectory && (
|
||||
<div className="inspector-section">
|
||||
<div
|
||||
style={{
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
fontSize: '13px'
|
||||
}}
|
||||
>
|
||||
此文件类型不支持预览
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FileSearch } from 'lucide-react';
|
||||
|
||||
interface EmptyInspectorProps {
|
||||
message?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function EmptyInspector({
|
||||
message = '未选择对象',
|
||||
description = '选择实体或节点以查看详细信息'
|
||||
}: EmptyInspectorProps) {
|
||||
return (
|
||||
<div className="entity-inspector">
|
||||
<div className="empty-inspector">
|
||||
<FileSearch size={48} style={{ color: '#555', marginBottom: '16px' }} />
|
||||
<div style={{ color: '#999', fontSize: '14px' }}>{message}</div>
|
||||
<div style={{ color: '#666', fontSize: '12px', marginTop: '8px' }}>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useState } from 'react';
|
||||
import { Settings, ChevronDown, ChevronRight, X } from 'lucide-react';
|
||||
import { Entity, Component } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import { PropertyInspector } from '../../PropertyInspector';
|
||||
import '../../../styles/EntityInspector.css';
|
||||
|
||||
interface EntityInspectorProps {
|
||||
entity: Entity;
|
||||
messageHub: MessageHub;
|
||||
componentVersion: number;
|
||||
}
|
||||
|
||||
export function EntityInspector({ entity, messageHub, componentVersion }: EntityInspectorProps) {
|
||||
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
|
||||
|
||||
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 handleRemoveComponent = (index: number) => {
|
||||
const component = entity.components[index];
|
||||
if (component) {
|
||||
entity.removeComponent(component);
|
||||
messageHub.publish('component:removed', { entity, component });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePropertyChange = (component: Component, propertyName: string, value: unknown) => {
|
||||
messageHub.publish('component:property:changed', {
|
||||
entity,
|
||||
component,
|
||||
propertyName,
|
||||
value
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="entity-inspector">
|
||||
<div className="inspector-header">
|
||||
<Settings size={16} />
|
||||
<span className="entity-name">{entity.name || `Entity #${entity.id}`}</span>
|
||||
</div>
|
||||
|
||||
<div className="inspector-content">
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">基本信息</div>
|
||||
<div className="property-field">
|
||||
<label className="property-label">Entity ID</label>
|
||||
<span className="property-value-text">{entity.id}</span>
|
||||
</div>
|
||||
<div className="property-field">
|
||||
<label className="property-label">Enabled</label>
|
||||
<span className="property-value-text">{entity.enabled ? 'true' : 'false'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{entity.components.length > 0 && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">组件</div>
|
||||
{entity.components.map((component: Component, index: number) => {
|
||||
const isExpanded = expandedComponents.has(index);
|
||||
const componentName = component.constructor?.name || 'Component';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${componentName}-${index}-${componentVersion}`}
|
||||
style={{
|
||||
marginBottom: '2px',
|
||||
backgroundColor: '#2a2a2a',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={() => toggleComponentExpanded(index)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '6px 8px',
|
||||
backgroundColor: '#3a3a3a',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
borderBottom: isExpanded ? '1px solid #4a4a4a' : 'none'
|
||||
}}
|
||||
>
|
||||
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '6px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
color: '#e0e0e0',
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{componentName}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveComponent(index);
|
||||
}}
|
||||
title="移除组件"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#888',
|
||||
cursor: 'pointer',
|
||||
padding: '2px',
|
||||
borderRadius: '3px',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.color = '#dc2626')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.color = '#888')}
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div style={{ padding: '6px 8px' }}>
|
||||
<PropertyInspector
|
||||
component={component}
|
||||
onChange={(propName: string, value: unknown) =>
|
||||
handlePropertyChange(component, propName, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { InspectorRegistry, InspectorContext } from '@esengine/editor-core';
|
||||
import { EmptyInspector } from './EmptyInspector';
|
||||
|
||||
interface ExtensionInspectorProps {
|
||||
data: unknown;
|
||||
inspectorRegistry: InspectorRegistry;
|
||||
projectPath?: string | null;
|
||||
}
|
||||
|
||||
export function ExtensionInspector({ data, inspectorRegistry, projectPath }: ExtensionInspectorProps) {
|
||||
const context: InspectorContext = {
|
||||
target: data,
|
||||
projectPath,
|
||||
readonly: false
|
||||
};
|
||||
|
||||
const extensionContent = inspectorRegistry.render(data, context);
|
||||
if (extensionContent) {
|
||||
return extensionContent;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmptyInspector
|
||||
message="未找到合适的检视器"
|
||||
description="此对象类型未注册检视器扩展"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import {
|
||||
Settings,
|
||||
RefreshCw,
|
||||
Activity,
|
||||
Tag,
|
||||
Layers,
|
||||
ArrowUpDown,
|
||||
GitBranch
|
||||
} from 'lucide-react';
|
||||
import { RemoteEntity, EntityDetails } from '../types';
|
||||
import { getProfilerService } from '../utils';
|
||||
import { PropertyRendererRegistry, PropertyContext } from '@esengine/editor-core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import '../../../styles/EntityInspector.css';
|
||||
|
||||
interface RemoteEntityInspectorProps {
|
||||
entity: RemoteEntity;
|
||||
details?: EntityDetails;
|
||||
autoRefresh: boolean;
|
||||
onAutoRefreshChange: (value: boolean) => void;
|
||||
decimalPlaces: number;
|
||||
}
|
||||
|
||||
export function RemoteEntityInspector({
|
||||
entity,
|
||||
details,
|
||||
autoRefresh,
|
||||
onAutoRefreshChange,
|
||||
decimalPlaces
|
||||
}: RemoteEntityInspectorProps) {
|
||||
const handleManualRefresh = () => {
|
||||
const profilerService = getProfilerService();
|
||||
if (profilerService && entity?.id !== undefined) {
|
||||
profilerService.requestEntityDetails(entity.id);
|
||||
}
|
||||
};
|
||||
|
||||
const renderRemoteProperty = (key: string, value: any) => {
|
||||
const registry = Core.services.resolve(PropertyRendererRegistry);
|
||||
const context: PropertyContext = {
|
||||
name: key,
|
||||
decimalPlaces,
|
||||
readonly: true,
|
||||
depth: 0
|
||||
};
|
||||
|
||||
const rendered = registry.render(value, context);
|
||||
if (rendered) {
|
||||
return <div key={key}>{rendered}</div>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="entity-inspector">
|
||||
<div className="inspector-header">
|
||||
<Settings size={16} />
|
||||
<span className="entity-name">运行时实体 #{entity.id}</span>
|
||||
{entity.destroyed && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '8px',
|
||||
padding: '2px 6px',
|
||||
backgroundColor: '#dc2626',
|
||||
color: '#fff',
|
||||
borderRadius: '4px',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600
|
||||
}}
|
||||
>
|
||||
已销毁
|
||||
</span>
|
||||
)}
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<button
|
||||
onClick={handleManualRefresh}
|
||||
title="刷新"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#888',
|
||||
cursor: 'pointer',
|
||||
padding: '2px',
|
||||
borderRadius: '3px',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.color = '#4ade80')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.color = '#888')}
|
||||
>
|
||||
<RefreshCw size={14} className={autoRefresh ? 'spin-slow' : ''} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAutoRefreshChange(!autoRefresh)}
|
||||
title={autoRefresh ? '关闭自动刷新' : '开启自动刷新'}
|
||||
style={{
|
||||
background: autoRefresh ? '#2d4a3e' : 'transparent',
|
||||
border: 'none',
|
||||
color: autoRefresh ? '#4ade80' : '#888',
|
||||
cursor: 'pointer',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '3px',
|
||||
fontSize: '10px',
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
{autoRefresh ? '自动' : '手动'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inspector-content">
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">基本信息</div>
|
||||
<div className="property-field">
|
||||
<label className="property-label">Entity ID</label>
|
||||
<span className="property-value-text">{entity.id}</span>
|
||||
</div>
|
||||
{entity.name && (
|
||||
<div className="property-field">
|
||||
<label className="property-label">Name</label>
|
||||
<span className="property-value-text">{entity.name}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="property-field">
|
||||
<label className="property-label">
|
||||
<Activity size={12} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
|
||||
Enabled
|
||||
</label>
|
||||
<span
|
||||
className="property-value-text"
|
||||
style={{
|
||||
color: entity.enabled ? '#4ade80' : '#f87171'
|
||||
}}
|
||||
>
|
||||
{entity.enabled ? 'true' : 'false'}
|
||||
</span>
|
||||
</div>
|
||||
{entity.tag !== undefined && entity.tag !== 0 && (
|
||||
<div className="property-field">
|
||||
<label className="property-label">
|
||||
<Tag size={12} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
|
||||
Tag
|
||||
</label>
|
||||
<span
|
||||
className="property-value-text"
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
color: '#fbbf24'
|
||||
}}
|
||||
>
|
||||
{entity.tag}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(entity.depth !== undefined ||
|
||||
entity.updateOrder !== undefined ||
|
||||
entity.parentId !== undefined ||
|
||||
entity.childCount !== undefined) && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">层级信息</div>
|
||||
{entity.depth !== undefined && (
|
||||
<div className="property-field">
|
||||
<label className="property-label">
|
||||
<Layers size={12} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
|
||||
深度
|
||||
</label>
|
||||
<span className="property-value-text">{entity.depth}</span>
|
||||
</div>
|
||||
)}
|
||||
{entity.updateOrder !== undefined && (
|
||||
<div className="property-field">
|
||||
<label className="property-label">
|
||||
<ArrowUpDown size={12} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
|
||||
更新顺序
|
||||
</label>
|
||||
<span className="property-value-text">{entity.updateOrder}</span>
|
||||
</div>
|
||||
)}
|
||||
{entity.parentId !== undefined && (
|
||||
<div className="property-field">
|
||||
<label className="property-label">
|
||||
<GitBranch size={12} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
|
||||
父实体 ID
|
||||
</label>
|
||||
<span
|
||||
className="property-value-text"
|
||||
style={{
|
||||
color: entity.parentId === null ? '#666' : '#90caf9'
|
||||
}}
|
||||
>
|
||||
{entity.parentId === null ? '无' : entity.parentId}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{entity.childCount !== undefined && (
|
||||
<div className="property-field">
|
||||
<label className="property-label">子实体数量</label>
|
||||
<span className="property-value-text">{entity.childCount}</span>
|
||||
</div>
|
||||
)}
|
||||
{entity.activeInHierarchy !== undefined && (
|
||||
<div className="property-field">
|
||||
<label className="property-label">层级中激活</label>
|
||||
<span
|
||||
className="property-value-text"
|
||||
style={{
|
||||
color: entity.activeInHierarchy ? '#4ade80' : '#f87171'
|
||||
}}
|
||||
>
|
||||
{entity.activeInHierarchy ? 'true' : 'false'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entity.componentMask !== undefined && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">调试信息</div>
|
||||
<div className="property-field">
|
||||
<label className="property-label">Component Mask</label>
|
||||
<span
|
||||
className="property-value-text"
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '10px',
|
||||
color: '#a78bfa',
|
||||
wordBreak: 'break-all'
|
||||
}}
|
||||
>
|
||||
{entity.componentMask}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{details &&
|
||||
details.components &&
|
||||
Array.isArray(details.components) &&
|
||||
details.components.length > 0 && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">组件 ({details.components.length})</div>
|
||||
{details.components.map((comp, index) => {
|
||||
const registry = Core.services.resolve(PropertyRendererRegistry);
|
||||
const context: PropertyContext = {
|
||||
name: comp.typeName || `Component ${index}`,
|
||||
decimalPlaces,
|
||||
readonly: true,
|
||||
expandByDefault: true,
|
||||
depth: 0
|
||||
};
|
||||
const rendered = registry.render(comp, context);
|
||||
return rendered ? <div key={index}>{rendered}</div> : null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{details &&
|
||||
Object.entries(details).filter(([key]) => key !== 'components' && key !== 'componentTypes')
|
||||
.length > 0 && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">其他信息</div>
|
||||
{Object.entries(details)
|
||||
.filter(([key]) => key !== 'components' && key !== 'componentTypes')
|
||||
.map(([key, value]) => renderRemoteProperty(key, value))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { EmptyInspector } from './EmptyInspector';
|
||||
export { ExtensionInspector } from './ExtensionInspector';
|
||||
export { AssetFileInspector } from './AssetFileInspector';
|
||||
export { RemoteEntityInspector } from './RemoteEntityInspector';
|
||||
export { EntityInspector } from './EntityInspector';
|
||||
Reference in New Issue
Block a user