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:
YHH
2025-11-18 22:28:13 +08:00
committed by GitHub
parent bce3a6e253
commit caed5428d5
48 changed files with 2221 additions and 44 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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="此对象类型未注册检视器扩展"
/>
);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,5 @@
export { EmptyInspector } from './EmptyInspector';
export { ExtensionInspector } from './ExtensionInspector';
export { AssetFileInspector } from './AssetFileInspector';
export { RemoteEntityInspector } from './RemoteEntityInspector';
export { EntityInspector } from './EntityInspector';