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 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,5 @@
export * from './tauri';
export * from './github';
export * from './plugins';
export * from './serialization';
export * from './events';

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,88 @@
import React, { useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { IPropertyRenderer, PropertyContext, PropertyRendererRegistry } from '@esengine/editor-core';
import { Core } from '@esengine/ecs-framework';
interface ComponentData {
typeName: string;
properties: Record<string, any>;
}
export class ComponentRenderer implements IPropertyRenderer<ComponentData> {
readonly id = 'app.component';
readonly name = 'Component Renderer';
readonly priority = 75;
canHandle(value: any, _context: PropertyContext): value is ComponentData {
return (
typeof value === 'object' &&
value !== null &&
typeof value.typeName === 'string' &&
typeof value.properties === 'object' &&
value.properties !== null
);
}
render(value: ComponentData, context: PropertyContext): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(context.expandByDefault ?? false);
const depth = context.depth ?? 0;
return (
<div style={{ marginLeft: depth > 0 ? '12px' : 0 }}>
<div
onClick={() => setIsExpanded(!isExpanded)}
style={{
display: 'flex',
alignItems: 'center',
padding: '6px 8px',
backgroundColor: '#3a3a3a',
cursor: 'pointer',
userSelect: 'none',
borderRadius: '4px',
marginBottom: '2px'
}}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
<span
style={{
marginLeft: '6px',
fontSize: '12px',
fontWeight: 500,
color: '#e0e0e0'
}}
>
{value.typeName}
</span>
</div>
{isExpanded && (
<div style={{ marginLeft: '8px', borderLeft: '1px solid #444', paddingLeft: '8px' }}>
{Object.entries(value.properties).map(([key, propValue]) => {
const registry = Core.services.resolve(PropertyRendererRegistry);
const propContext: PropertyContext = {
...context,
name: key,
depth: depth + 1,
path: [...(context.path || []), key]
};
const rendered = registry.render(propValue, propContext);
if (rendered) {
return <div key={key}>{rendered}</div>;
}
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<span className="property-value-text" style={{ color: '#666', fontStyle: 'italic' }}>
[No Renderer]
</span>
</div>
);
})}
</div>
)}
</div>
);
}
}

View File

@@ -0,0 +1,139 @@
import React from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { IPropertyRenderer, PropertyContext } from '@esengine/editor-core';
export class FallbackRenderer implements IPropertyRenderer<any> {
readonly id = 'app.fallback';
readonly name = 'Fallback Renderer';
readonly priority = -1000;
canHandle(_value: any, _context: PropertyContext): _value is any {
return true;
}
render(value: any, context: PropertyContext): React.ReactElement {
const typeInfo = this.getTypeInfo(value);
return (
<div className="property-field" style={{ opacity: 0.6 }}>
<label className="property-label">{context.name}</label>
<span
className="property-value-text"
style={{ color: '#888', fontStyle: 'italic', fontSize: '0.9em' }}
title="No renderer registered for this type"
>
{typeInfo}
</span>
</div>
);
}
private getTypeInfo(value: any): string {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
const type = typeof value;
if (type === 'object') {
if (Array.isArray(value)) {
return `Array(${value.length})`;
}
const constructor = value.constructor?.name;
if (constructor && constructor !== 'Object') {
return `[${constructor}]`;
}
const keys = Object.keys(value);
if (keys.length === 0) return '{}';
if (keys.length <= 3) {
return `{${keys.join(', ')}}`;
}
return `{${keys.slice(0, 3).join(', ')}...}`;
}
return `[${type}]`;
}
}
export class ArrayRenderer implements IPropertyRenderer<any[]> {
readonly id = 'app.array';
readonly name = 'Array Renderer';
readonly priority = 50;
canHandle(value: any, _context: PropertyContext): value is any[] {
return Array.isArray(value);
}
render(value: any[], context: PropertyContext): React.ReactElement {
const [isExpanded, setIsExpanded] = React.useState(false);
const depth = context.depth ?? 0;
if (value.length === 0) {
return (
<div className="property-field">
<label className="property-label">{context.name}</label>
<span className="property-value-text" style={{ color: '#666' }}>
[]
</span>
</div>
);
}
const isStringArray = value.every(item => typeof item === 'string');
if (isStringArray && value.length <= 5) {
return (
<div className="property-field">
<label className="property-label">{context.name}</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '4px' }}>
{(value as string[]).map((item, index) => (
<span
key={index}
style={{
padding: '2px 8px',
backgroundColor: '#2d4a3e',
color: '#8fbc8f',
borderRadius: '4px',
fontSize: '11px',
fontFamily: 'monospace'
}}
>
{item}
</span>
))}
</div>
</div>
);
}
return (
<div style={{ marginLeft: depth > 0 ? '12px' : 0 }}>
<div
onClick={() => setIsExpanded(!isExpanded)}
style={{
display: 'flex',
alignItems: 'center',
padding: '3px 0',
fontSize: '11px',
borderBottom: '1px solid #333',
cursor: 'pointer',
userSelect: 'none'
}}
>
{isExpanded ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
<span style={{ color: '#9cdcfe', marginLeft: '4px' }}>{context.name}</span>
<span
style={{
color: '#666',
fontFamily: 'monospace',
marginLeft: '8px',
fontSize: '10px'
}}
>
Array({value.length})
</span>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,94 @@
import React from 'react';
import { IPropertyRenderer, PropertyContext } from '@esengine/editor-core';
import { formatNumber } from '../../components/inspectors/utils';
export class StringRenderer implements IPropertyRenderer<string> {
readonly id = 'app.string';
readonly name = 'String Renderer';
readonly priority = 100;
canHandle(value: any, _context: PropertyContext): value is string {
return typeof value === 'string';
}
render(value: string, context: PropertyContext): React.ReactElement {
const displayValue = value.length > 50 ? `${value.substring(0, 50)}...` : value;
return (
<div className="property-field">
<label className="property-label">{context.name}</label>
<span className="property-value-text" title={value}>
{displayValue}
</span>
</div>
);
}
}
export class NumberRenderer implements IPropertyRenderer<number> {
readonly id = 'app.number';
readonly name = 'Number Renderer';
readonly priority = 100;
canHandle(value: any, _context: PropertyContext): value is number {
return typeof value === 'number';
}
render(value: number, context: PropertyContext): React.ReactElement {
const decimalPlaces = context.decimalPlaces ?? 4;
const displayValue = formatNumber(value, decimalPlaces);
return (
<div className="property-field">
<label className="property-label">{context.name}</label>
<span className="property-value-text" style={{ color: '#b5cea8' }}>
{displayValue}
</span>
</div>
);
}
}
export class BooleanRenderer implements IPropertyRenderer<boolean> {
readonly id = 'app.boolean';
readonly name = 'Boolean Renderer';
readonly priority = 100;
canHandle(value: any, _context: PropertyContext): value is boolean {
return typeof value === 'boolean';
}
render(value: boolean, context: PropertyContext): React.ReactElement {
return (
<div className="property-field">
<label className="property-label">{context.name}</label>
<span
className="property-value-text"
style={{ color: value ? '#4ade80' : '#f87171' }}
>
{value ? 'true' : 'false'}
</span>
</div>
);
}
}
export class NullRenderer implements IPropertyRenderer<null> {
readonly id = 'app.null';
readonly name = 'Null Renderer';
readonly priority = 100;
canHandle(value: any, _context: PropertyContext): value is null {
return value === null || value === undefined;
}
render(_value: null, context: PropertyContext): React.ReactElement {
return (
<div className="property-field">
<label className="property-label">{context.name}</label>
<span className="property-value-text" style={{ color: '#666' }}>
null
</span>
</div>
);
}
}

View File

@@ -0,0 +1,128 @@
import React from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { IPropertyRenderer, PropertyContext } from '@esengine/editor-core';
import { formatNumber } from '../../components/inspectors/utils';
interface Vector2 {
x: number;
y: number;
}
interface Vector3 extends Vector2 {
z: number;
}
interface Vector4 extends Vector3 {
w: number;
}
interface Color {
r: number;
g: number;
b: number;
a: number;
}
export class Vector2Renderer implements IPropertyRenderer<Vector2> {
readonly id = 'app.vector2';
readonly name = 'Vector2 Renderer';
readonly priority = 80;
canHandle(value: any, _context: PropertyContext): value is Vector2 {
return (
typeof value === 'object' &&
value !== null &&
typeof value.x === 'number' &&
typeof value.y === 'number' &&
!('z' in value) &&
Object.keys(value).length === 2
);
}
render(value: Vector2, context: PropertyContext): React.ReactElement {
const decimals = context.decimalPlaces ?? 2;
return (
<div className="property-field">
<label className="property-label">{context.name}</label>
<span className="property-value-text" style={{ color: '#9cdcfe', fontFamily: 'monospace' }}>
({formatNumber(value.x, decimals)}, {formatNumber(value.y, decimals)})
</span>
</div>
);
}
}
export class Vector3Renderer implements IPropertyRenderer<Vector3> {
readonly id = 'app.vector3';
readonly name = 'Vector3 Renderer';
readonly priority = 80;
canHandle(value: any, _context: PropertyContext): value is Vector3 {
return (
typeof value === 'object' &&
value !== null &&
typeof value.x === 'number' &&
typeof value.y === 'number' &&
typeof value.z === 'number' &&
!('w' in value) &&
Object.keys(value).length === 3
);
}
render(value: Vector3, context: PropertyContext): React.ReactElement {
const decimals = context.decimalPlaces ?? 2;
return (
<div className="property-field">
<label className="property-label">{context.name}</label>
<span className="property-value-text" style={{ color: '#9cdcfe', fontFamily: 'monospace' }}>
({formatNumber(value.x, decimals)}, {formatNumber(value.y, decimals)}, {formatNumber(value.z, decimals)})
</span>
</div>
);
}
}
export class ColorRenderer implements IPropertyRenderer<Color> {
readonly id = 'app.color';
readonly name = 'Color Renderer';
readonly priority = 85;
canHandle(value: any, _context: PropertyContext): value is Color {
return (
typeof value === 'object' &&
value !== null &&
typeof value.r === 'number' &&
typeof value.g === 'number' &&
typeof value.b === 'number' &&
typeof value.a === 'number' &&
Object.keys(value).length === 4
);
}
render(value: Color, context: PropertyContext): React.ReactElement {
const r = Math.round(value.r * 255);
const g = Math.round(value.g * 255);
const b = Math.round(value.b * 255);
const colorHex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
return (
<div className="property-field">
<label className="property-label">{context.name}</label>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div
style={{
width: '20px',
height: '20px',
backgroundColor: colorHex,
border: '1px solid #444',
borderRadius: '2px'
}}
/>
<span className="property-value-text" style={{ fontFamily: 'monospace' }}>
rgba({r}, {g}, {b}, {value.a.toFixed(2)})
</span>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,4 @@
export * from './PrimitiveRenderers';
export * from './VectorRenderers';
export * from './ComponentRenderer';
export * from './FallbackRenderer';

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};