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:
1
packages/editor-app/src/infrastructure/events/index.ts
Normal file
1
packages/editor-app/src/infrastructure/events/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
packages/editor-app/src/infrastructure/github/index.ts
Normal file
1
packages/editor-app/src/infrastructure/github/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
5
packages/editor-app/src/infrastructure/index.ts
Normal file
5
packages/editor-app/src/infrastructure/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './tauri';
|
||||
export * from './github';
|
||||
export * from './plugins';
|
||||
export * from './serialization';
|
||||
export * from './events';
|
||||
1
packages/editor-app/src/infrastructure/plugins/index.ts
Normal file
1
packages/editor-app/src/infrastructure/plugins/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './PrimitiveRenderers';
|
||||
export * from './VectorRenderers';
|
||||
export * from './ComponentRenderer';
|
||||
export * from './FallbackRenderer';
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
packages/editor-app/src/infrastructure/tauri/index.ts
Normal file
1
packages/editor-app/src/infrastructure/tauri/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Reference in New Issue
Block a user