refactor: 代码规范化与依赖清理 (#317)

* refactor(deps): 统一编辑器包依赖配置 & 优化分层架构

- 将 ecs-engine-bindgen 提升为 Layer 1 核心包
- 统一 9 个编辑器包的依赖声明模式
- 清理废弃的包目录 (ui, ui-editor, network-*)

* refactor(tokens): 修复 PrefabService 令牌冲突 & 补充 module.json

- 将 editor-core 的 PrefabServiceToken 改名为 EditorPrefabServiceToken
  避免与 asset-system 的 PrefabServiceToken 冲突 (Symbol.for 冲突)
- 为 mesh-3d 添加 module.json
- 为 world-streaming 添加 module.json

* refactor(editor-core): 整理导出结构 & 添加 blueprint tokens.ts

- 按功能分组整理 editor-core 的 65 行导出
- 添加清晰的分组注释 (中英双语)
- 为 blueprint 添加占位符 tokens.ts

* chore(editor): 为 14 个编辑器插件包添加 module.json

统一编辑器包的模块配置,包含:
- isEditorPlugin 标识
- runtimeModule 关联
- exports 导出清单 (inspectors, panels, gizmos)

* refactor(core): 改进类型安全 - 减少 as any 使用

- 添加 GlobalTypes.ts 定义小游戏平台和 Chrome API 类型
- SoAStorage 使用 IComponentTypeMetadata 替代 as any
- PlatformDetector 使用类型安全的平台检测
- 添加 ISoAStorageStats/ISoAFieldStats 接口

* feat(editor): 添加 EditorServicesContext 解决 prop drilling

- 新增 contexts/EditorServicesContext.tsx 提供统一服务访问
- App.tsx 包裹 EditorServicesProvider
- 提供 useEditorServices/useMessageHub 等便捷 hooks
- SceneHierarchy 添加迁移注释,后续可移除 props

* docs(editor): 澄清 inspector 目录架构关系

- inspector/ 标记为内部实现,添加 @deprecated 警告
- inspectors/ 标记为公共 API 入口点
- 添加架构说明文档

* refactor(editor): 添加全局类型声明消除 window as any

- 创建 editor-app/src/global.d.ts 声明 Window 接口扩展
- 创建 editor-core/src/global.d.ts 声明 Window 接口扩展
- 更新 App.tsx 使用类型安全的 window 属性访问
- 更新 PluginLoader.ts 使用 window.__ESENGINE_PLUGINS__
- 更新 PluginSDKRegistry.ts 使用 window.__ESENGINE_SDK__
- 更新 UserCodeService.ts 使用类型安全的全局变量访问

* refactor(editor): 提取项目和场景操作到独立 hooks

- 创建 useProjectActions hook 封装项目操作
- 创建 useSceneActions hook 封装场景操作
- 为渐进式重构 App.tsx 做准备

* refactor(editor): 清理冗余代码和未使用文件

删除的目录和文件:
- application/state/ - 重复的状态管理(与 stores/ 重复)
- 8 个孤立 CSS 文件(对应组件不存在)
- AssetBrowser.tsx - 仅为 ContentBrowser 的向后兼容包装
- AssetPicker.tsx - 未被使用
- AssetPickerDialog.tsx (顶级) - 已被 dialogs/ 版本取代
- EntityInspector.tsx (顶级) - 已被 inspectors/views/ 版本取代

修复:
- 移除 App.tsx 中未使用的导入
- 更新 application/index.ts 移除已删除模块
- 修复 useProjectActions.ts 的 MutableRefObject 类型

* refactor(editor): 统一 inspectors 模块导出结构

- 在 inspectors/index.ts 重新导出 PropertyInspector
- 创建 inspectors/fields/index.ts barrel export
- 导出 views、fields、common 子模块
- 更新 EntityInspector 使用统一入口导入

* refactor(editor): 删除废弃的 Profiler 组件

删除未使用的组件(共 1059 行):
- ProfilerPanel.tsx (229 行)
- ProfilerWindow.tsx (589 行)
- ProfilerDockPanel.tsx (241 行)
- ProfilerPanel.css
- ProfilerDockPanel.css

保留:AdvancedProfiler + AdvancedProfilerWindow(正在使用)

* refactor(runtime-core): 统一依赖处理与插件状态管理

- 新增 DependencyUtils 统一拓扑排序和依赖验证
- 新增 PluginState 定义插件生命周期状态机
- 合并 UnifiedPluginLoader 到 PluginLoader
- 清理 index.ts 移除不必要的 Token re-exports
- 新增 RuntimeMode/UserCodeRealm/ImportMapGenerator

* refactor(editor-core): 使用统一的 ImportMapGenerator

- WebBuildPipeline 使用 runtime-core 的 generateImportMap
- UserCodeService 添加 ImportMap 相关接口

* feat(compiler): 增强 esbuild 查找策略

- 支持本地 node_modules、pnpm exec、npx、全局多种来源
- EngineService 使用 RuntimeMode

* refactor(runtime-core): 简化 GameRuntime 代码

- 合并 _disableGameLogicSystems/_enableGameLogicSystems 为 _setGameLogicSystemsEnabled
- 精简本地 Token 定义的注释

* refactor(editor-core): 引入 BaseRegistry 基类消除代码重复

- 新增 BaseRegistry 和 PrioritizedRegistry 基类
- 重构 CompilerRegistry, InspectorRegistry, FieldEditorRegistry
- 统一注册表的日志记录和错误处理

* refactor(editor-core): 扩展 BaseRegistry 重构

- ComponentInspectorRegistry 继承 PrioritizedRegistry
- EditorComponentRegistry 继承 BaseRegistry
- EntityCreationRegistry 继承 BaseRegistry
- PropertyRendererRegistry 继承 PrioritizedRegistry
- 导出 BaseRegistry 基类供外部使用
- 统一双语注释格式

* refactor(editor-core): 代码优雅性优化

CommandManager:
- 提取 tryMergeWithLast() 和 pushToUndoStack() 消除重复代码
- 统一双语注释格式

FileActionRegistry:
- 提取 normalizeExtension() 消除扩展名规范化重复
- 统一私有属性命名风格(_前缀)
- 使用 createRegistryToken 统一 Token 创建

BaseRegistry:
- 添加 IOrdered 接口
- 添加 sortByOrder() 排序辅助方法

EntityCreationRegistry:
- 使用 sortByOrder() 简化排序逻辑

* refactor(editor-core): 统一日志系统 & 代码规范优化

- GizmoRegistry: 使用 createLogger 替代 console.warn
- VirtualNodeRegistry: 使用 createLogger 替代 console.warn
- WindowRegistry: 使用 logger、添加 _ 前缀、导出 IWindowRegistry token
- EditorViewportService: 使用 createLogger 替代 console.warn
- ComponentActionRegistry: 使用 logger、添加 _ 前缀、返回值改进
- SettingsRegistry: 使用 logger、提取 ensureCategory/ensureSection 方法
- 添加 WindowRegistry 到主导出

* refactor(editor-core): ModuleRegistry 使用 logger 替代 console

* refactor(editor-core): SerializerRegistry/UIRegistry 添加 token 和 _ 前缀

* refactor(editor-core): UIRegistry 代码优雅性 & Token 命名统一

- UIRegistry: 提取 _sortByOrder 消除 6 处重复排序逻辑
- UIRegistry: 添加分节注释和双语文档
- FieldEditorRegistry: Token 重命名为 FieldEditorRegistryToken
- PropertyRendererRegistry: Token 重命名为 PropertyRendererRegistryToken

* refactor(core): 统一日志系统 - console 替换为 logger

- ComponentSerializer: 使用 logger 替代 console.warn
- ComponentRegistry: console.warn → logger.warn (已有 logger)
- SceneSerializer: 添加 logger,替换 console.warn/error
- SystemScheduler: 添加 logger,替换 console.warn
- VersionMigration: 添加 logger,替换所有 console.warn
- RuntimeModeService: console.error → logger.error
- Core.ts: _logger 改为 readonly,双语错误消息
- SceneSerializer 修复:使用 getComponentTypeName 替代 constructor.name

* fix(core): 修复 constructor.name 压缩后失效问题

- Scene.ts: 使用 system.systemName 替代 system.constructor.name
- CommandBuffer.ts: 使用 getComponentTypeName() 替代 constructor.name

* refactor(editor-core): 代码规范优化 - 私有方法命名 & 日志统一

- BuildService: console → logger
- FileActionRegistry: 添加 logger, 私有方法 _ 前缀
- SettingsRegistry: 私有方法 _ 前缀 (ensureCategory → _ensureCategory)

* refactor(core): Scene.ts 私有变量命名规范化

- logger → _logger (遵循私有变量 _ 前缀规范)

* refactor(editor-core): 服务类私有成员命名规范化

- CommandManager: 私有变量/方法添加 _ 前缀
  - undoStack/redoStack/config/isExecuting
  - tryMergeWithLast/pushToUndoStack
- LocaleService: 私有变量/方法添加 _ 前缀
  - currentLocale/translations/changeListeners
  - deepMerge/getNestedValue/loadSavedLocale/saveLocale

* refactor(core): 私有成员命名规范化 & 单例模式优化

- Component.ts: _idGenerator 私有静态变量规范化
- PlatformManager.ts: _instance, _adapter, _logger 规范化
- AutoProfiler.ts: _instance, _config 及所有私有方法规范化
- ProfilerSDK.ts: _instance, _config 及所有私有方法规范化
- ComponentPoolManager: _instance, _pools, _usageTracker 规范化
- GlobalEventBus: _instance 规范化
- 添加中英双语 JSDoc 注释

* refactor(editor-app,behavior-tree-editor): 私有成员 & 单例模式命名规范化

editor-app:
- EngineService: private static instance → _instance
- EditorEngineSync: 所有私有成员添加 _ 前缀
- RuntimeResolver: 所有私有成员和方法添加 _ 前缀
- SettingsService: 所有私有成员和方法添加 _ 前缀

behavior-tree-editor:
- GlobalBlackboardService: 所有私有成员和方法添加 _ 前缀
- NotificationService: private static instance → _instance
- NodeRegistryService: 所有私有成员和方法添加 _ 前缀
- TreeStateAdapter: private static instance → _instance

* fix(editor-runtime): 添加 editor-core 到 external 避免传递依赖问题

将 @esengine/editor-core 添加到 vite external 配置,
避免 editor-core → runtime-core → ecs-engine-bindgen 的传递依赖
被错误地打包进 editor-runtime.js,导致 CI 构建失败。

* fix(core): 修复空接口 lint 错误

将 IByteDanceMiniGameAPI、IAlipayMiniGameAPI、IBaiduMiniGameAPI 从空接口改为类型别名,修复 no-empty-object-type 规则报错
This commit is contained in:
YHH
2025-12-24 20:57:08 +08:00
committed by GitHub
parent 58f70a5783
commit dbc6793dc4
133 changed files with 6880 additions and 9141 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import * as ReactDOM from 'react-dom';
import * as ReactJSXRuntime from 'react/jsx-runtime';
import { Core, createLogger, Scene } from '@esengine/ecs-framework';
@@ -7,9 +7,9 @@ import { getProfilerService } from './services/getService';
// 将 React 暴露到全局,供动态加载的插件使用
// editor-runtime.js 将 React 设为 external需要从全局获取
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
(window as any).ReactJSXRuntime = ReactJSXRuntime;
window.React = React;
window.ReactDOM = ReactDOM;
window.ReactJSXRuntime = ReactJSXRuntime;
import {
PluginManager,
UIRegistry,
@@ -37,7 +37,6 @@ import { ProjectCreationWizard } from './components/ProjectCreationWizard';
import { SceneHierarchy } from './components/SceneHierarchy';
import { ContentBrowser } from './components/ContentBrowser';
import { Inspector } from './components/inspectors/Inspector';
import { AssetBrowser } from './components/AssetBrowser';
import { Viewport } from './components/Viewport';
import { AdvancedProfilerWindow } from './components/AdvancedProfilerWindow';
import { RenderDebugPanel } from './components/debug/RenderDebugPanel';
@@ -66,6 +65,7 @@ import { CompilerConfigDialog } from './components/CompilerConfigDialog';
import { checkForUpdatesOnStartup } from './utils/updater';
import { useLocale } from './hooks/useLocale';
import { useStoreSubscriptions } from './hooks/useStoreSubscriptions';
import { EditorServicesProvider, type EditorServices } from './contexts';
import { en, zh, es } from './locales';
import type { Locale } from '@esengine/editor-core';
import { UserCodeService } from '@esengine/editor-core';
@@ -163,6 +163,29 @@ function App() {
const [commandManager] = useState(() => new CommandManager());
const { t, locale, changeLocale } = useLocale();
// 编辑器服务对象(用于 Context 传递)| Editor services object (for Context)
const editorServices = useMemo<EditorServices>(() => ({
entityStore: entityStoreRef.current,
messageHub: messageHubRef.current,
commandManager,
sceneManager: sceneManagerRef.current,
projectService: projectServiceRef.current,
pluginManager: pluginManagerRef.current,
inspectorRegistry: inspectorRegistryRef.current,
uiRegistry: uiRegistryRef.current,
settingsRegistry: settingsRegistryRef.current,
buildService: buildServiceRef.current,
logService: logServiceRef.current,
notification: notificationRef.current,
dialog: dialogRef.current,
projectPath: currentProjectPath,
}), [
commandManager,
currentProjectPath,
// 注意: refs 不会变化,但为了初始化后更新需要依赖 initialized
initialized,
]);
// Play 模式状态(用于层级面板实时同步)
// Play mode state (for hierarchy panel real-time sync)
const [isPlaying, setIsPlaying] = useState(false);
@@ -1322,6 +1345,7 @@ function App() {
const projectName = currentProjectPath ? currentProjectPath.split(/[\\/]/).pop() : 'Untitled';
return (
<EditorServicesProvider services={editorServices}>
<div className="editor-container">
{!isEditorFullscreen && (
<>
@@ -1511,6 +1535,7 @@ function App() {
/>
)}
</div>
</EditorServicesProvider>
);
}

View File

@@ -1,2 +1 @@
export * from './commands';
export * from './state';

View File

@@ -1,88 +0,0 @@
import { create } from 'zustand';
/**
* 编辑器交互状态
* 管理编辑器的交互状态(连接、框选、菜单等)
*/
interface EditorState {
/**
* 正在连接的源节点ID
*/
connectingFrom: string | null;
/**
* 正在连接的源属性
*/
connectingFromProperty: string | null;
/**
* 连接目标位置(鼠标位置)
*/
connectingToPos: { x: number; y: number } | null;
/**
* 是否正在框选
*/
isBoxSelecting: boolean;
/**
* 框选起始位置
*/
boxSelectStart: { x: number; y: number } | null;
/**
* 框选结束位置
*/
boxSelectEnd: { x: number; y: number } | null;
// Actions
setConnectingFrom: (nodeId: string | null) => void;
setConnectingFromProperty: (propertyName: string | null) => void;
setConnectingToPos: (pos: { x: number; y: number } | null) => void;
clearConnecting: () => void;
setIsBoxSelecting: (isSelecting: boolean) => void;
setBoxSelectStart: (pos: { x: number; y: number } | null) => void;
setBoxSelectEnd: (pos: { x: number; y: number } | null) => void;
clearBoxSelect: () => void;
}
/**
* Editor Store
*/
export const useEditorStore = create<EditorState>((set) => ({
connectingFrom: null,
connectingFromProperty: null,
connectingToPos: null,
isBoxSelecting: false,
boxSelectStart: null,
boxSelectEnd: null,
setConnectingFrom: (nodeId: string | null) => set({ connectingFrom: nodeId }),
setConnectingFromProperty: (propertyName: string | null) =>
set({ connectingFromProperty: propertyName }),
setConnectingToPos: (pos: { x: number; y: number } | null) => set({ connectingToPos: pos }),
clearConnecting: () =>
set({
connectingFrom: null,
connectingFromProperty: null,
connectingToPos: null
}),
setIsBoxSelecting: (isSelecting: boolean) => set({ isBoxSelecting: isSelecting }),
setBoxSelectStart: (pos: { x: number; y: number } | null) => set({ boxSelectStart: pos }),
setBoxSelectEnd: (pos: { x: number; y: number } | null) => set({ boxSelectEnd: pos }),
clearBoxSelect: () =>
set({
isBoxSelecting: false,
boxSelectStart: null,
boxSelectEnd: null
})
}));

View File

@@ -1,131 +0,0 @@
import { create } from 'zustand';
/**
* UI 状态
* 管理UI相关的状态选中、拖拽、画布
*/
interface UIState {
/**
* 选中的节点ID列表
*/
selectedNodeIds: string[];
/**
* 正在拖拽的节点ID
*/
draggingNodeId: string | null;
/**
* 拖拽起始位置映射
*/
dragStartPositions: Map<string, { x: number; y: number }>;
/**
* 是否正在拖拽节点
*/
isDraggingNode: boolean;
/**
* 拖拽偏移量
*/
dragDelta: { dx: number; dy: number };
/**
* 画布偏移
*/
canvasOffset: { x: number; y: number };
/**
* 画布缩放
*/
canvasScale: number;
/**
* 是否正在平移画布
*/
isPanning: boolean;
/**
* 平移起始位置
*/
panStart: { x: number; y: number };
// Actions
setSelectedNodeIds: (nodeIds: string[]) => void;
toggleNodeSelection: (nodeId: string) => void;
clearSelection: () => void;
startDragging: (nodeId: string, startPositions: Map<string, { x: number; y: number }>) => void;
stopDragging: () => void;
setIsDraggingNode: (isDragging: boolean) => void;
setDragDelta: (delta: { dx: number; dy: number }) => void;
setCanvasOffset: (offset: { x: number; y: number }) => void;
setCanvasScale: (scale: number) => void;
setIsPanning: (isPanning: boolean) => void;
setPanStart: (panStart: { x: number; y: number }) => void;
resetView: () => void;
}
/**
* UI Store
*/
export const useUIStore = create<UIState>((set, get) => ({
selectedNodeIds: [],
draggingNodeId: null,
dragStartPositions: new Map(),
isDraggingNode: false,
dragDelta: { dx: 0, dy: 0 },
canvasOffset: { x: 0, y: 0 },
canvasScale: 1,
isPanning: false,
panStart: { x: 0, y: 0 },
setSelectedNodeIds: (nodeIds: string[]) => set({ selectedNodeIds: nodeIds }),
toggleNodeSelection: (nodeId: string) => {
const { selectedNodeIds } = get();
if (selectedNodeIds.includes(nodeId)) {
set({ selectedNodeIds: selectedNodeIds.filter((id) => id !== nodeId) });
} else {
set({ selectedNodeIds: [...selectedNodeIds, nodeId] });
}
},
clearSelection: () => set({ selectedNodeIds: [] }),
startDragging: (nodeId: string, startPositions: Map<string, { x: number; y: number }>) =>
set({
draggingNodeId: nodeId,
dragStartPositions: startPositions,
isDraggingNode: true
}),
stopDragging: () =>
set({
draggingNodeId: null,
dragStartPositions: new Map(),
isDraggingNode: false,
dragDelta: { dx: 0, dy: 0 }
}),
setIsDraggingNode: (isDragging: boolean) => set({ isDraggingNode: isDragging }),
setDragDelta: (delta: { dx: number; dy: number }) => set({ dragDelta: delta }),
setCanvasOffset: (offset: { x: number; y: number }) => set({ canvasOffset: offset }),
setCanvasScale: (scale: number) => set({ canvasScale: scale }),
setIsPanning: (isPanning: boolean) => set({ isPanning }),
setPanStart: (panStart: { x: number; y: number }) => set({ panStart }),
resetView: () =>
set({
canvasOffset: { x: 0, y: 0 },
canvasScale: 1,
isPanning: false
})
}));

View File

@@ -1,2 +0,0 @@
export { useUIStore } from './UIStore';
export { useEditorStore } from './EditorStore';

View File

@@ -1,22 +0,0 @@
/**
* Asset Browser - 资产浏览器
* 包装 ContentBrowser 组件,保持向后兼容
*/
import { ContentBrowser } from './ContentBrowser';
interface AssetBrowserProps {
projectPath: string | null;
locale: string;
onOpenScene?: (scenePath: string) => void;
}
export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserProps) {
return (
<ContentBrowser
projectPath={projectPath}
locale={locale}
onOpenScene={onOpenScene}
/>
);
}

View File

@@ -1,141 +0,0 @@
import { useState, useEffect } from 'react';
import { RefreshCw, Folder } from 'lucide-react';
import { TauriAPI } from '../api/tauri';
interface AssetPickerProps {
value: string;
onChange: (value: string) => void;
projectPath: string | null;
filter?: 'btree' | 'ecs';
label?: string;
}
/**
* 资产选择器组件
* 用于选择项目中的资产文件
*/
export function AssetPicker({ value, onChange, projectPath, filter = 'btree', label }: AssetPickerProps) {
const [assets, setAssets] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (projectPath) {
loadAssets();
}
}, [projectPath]);
const loadAssets = async () => {
if (!projectPath) return;
setLoading(true);
try {
if (filter === 'btree') {
const btrees = await TauriAPI.scanBehaviorTrees(projectPath);
setAssets(btrees);
}
} catch (error) {
console.error('Failed to load assets:', error);
setAssets([]);
} finally {
setLoading(false);
}
};
const handleBrowse = async () => {
try {
if (filter === 'btree') {
const path = await TauriAPI.openBehaviorTreeDialog();
if (path && projectPath) {
const behaviorsPath = `${projectPath}\\.ecs\\behaviors\\`.replace(/\\/g, '\\\\');
const relativePath = path.replace(behaviorsPath, '')
.replace(/\\/g, '/')
.replace('.btree', '');
onChange(relativePath);
await loadAssets();
}
}
} catch (error) {
console.error('Failed to browse asset:', error);
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{label && (
<label style={{ fontSize: '11px', color: '#aaa', fontWeight: '500' }}>
{label}
</label>
)}
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={loading || !projectPath}
style={{
flex: 1,
padding: '4px 8px',
backgroundColor: '#1e1e1e',
border: '1px solid #3e3e42',
borderRadius: '3px',
color: '#cccccc',
fontSize: '12px',
cursor: loading || !projectPath ? 'not-allowed' : 'pointer'
}}
>
<option value="">{loading ? '加载中...' : '选择资产...'}</option>
{assets.map((asset) => (
<option key={asset} value={asset}>
{asset}
</option>
))}
</select>
<button
onClick={loadAssets}
disabled={loading || !projectPath}
style={{
padding: '4px 8px',
backgroundColor: '#0e639c',
border: 'none',
borderRadius: '3px',
color: '#fff',
cursor: loading || !projectPath ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
opacity: loading || !projectPath ? 0.5 : 1
}}
title="刷新资产列表"
>
<RefreshCw size={14} />
</button>
<button
onClick={handleBrowse}
disabled={loading || !projectPath}
style={{
padding: '4px 8px',
backgroundColor: '#0e639c',
border: 'none',
borderRadius: '3px',
color: '#fff',
cursor: loading || !projectPath ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
opacity: loading || !projectPath ? 0.5 : 1
}}
title="浏览文件..."
>
<Folder size={14} />
</button>
</div>
{!projectPath && (
<div style={{ fontSize: '10px', color: '#ff6b6b', marginTop: '2px' }}>
</div>
)}
{value && assets.length > 0 && !assets.includes(value) && (
<div style={{ fontSize: '10px', color: '#ffa726', marginTop: '2px' }}>
警告: 资产 "{value}"
</div>
)}
</div>
);
}

View File

@@ -1,316 +0,0 @@
import { useState, useEffect } from 'react';
import { X, Folder, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react';
import { TauriAPI, DirectoryEntry } from '../api/tauri';
import { useLocale } from '../hooks/useLocale';
import '../styles/AssetPickerDialog.css';
interface AssetPickerDialogProps {
projectPath: string;
fileExtension: string;
onSelect: (assetId: string) => void;
onClose: () => void;
locale: string;
/** 资产基础路径(相对于项目根目录),用于计算 assetId */
assetBasePath?: string;
}
interface AssetItem {
name: string;
path: string;
isDir: boolean;
extension?: string;
size?: number;
modified?: number;
}
type ViewMode = 'list' | 'grid';
export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClose, locale, assetBasePath }: AssetPickerDialogProps) {
const { t, locale: currentLocale } = useLocale();
// 计算实际的资产目录路径
const actualAssetPath = assetBasePath
? `${projectPath}/${assetBasePath}`.replace(/\\/g, '/').replace(/\/+/g, '/')
: projectPath;
const [currentPath, setCurrentPath] = useState(actualAssetPath);
const [assets, setAssets] = useState<AssetItem[]>([]);
const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [viewMode, setViewMode] = useState<ViewMode>('list');
useEffect(() => {
loadAssets(currentPath);
}, [currentPath]);
const loadAssets = async (path: string) => {
setLoading(true);
try {
const entries = await TauriAPI.listDirectory(path);
const assetItems: AssetItem[] = entries
.map((entry: DirectoryEntry) => {
const extension = entry.is_dir ? undefined :
(entry.name.includes('.') ? entry.name.split('.').pop() : undefined);
return {
name: entry.name,
path: entry.path,
isDir: entry.is_dir,
extension,
size: entry.size,
modified: entry.modified
};
})
.filter((item) => item.isDir || item.extension === fileExtension)
.sort((a, b) => {
if (a.isDir === b.isDir) return a.name.localeCompare(b.name);
return a.isDir ? -1 : 1;
});
setAssets(assetItems);
} catch (error) {
console.error('Failed to load assets:', error);
setAssets([]);
} finally {
setLoading(false);
}
};
// 过滤搜索结果
const filteredAssets = assets.filter((item) =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
// 格式化文件大小
const formatFileSize = (bytes?: number): string => {
if (!bytes) return '';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
// 格式化修改时间
const formatDate = (timestamp?: number): string => {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
const localeMap: Record<string, string> = { zh: 'zh-CN', en: 'en-US', es: 'es-ES' };
return date.toLocaleDateString(localeMap[currentLocale] || 'en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
// 返回上级目录
const handleGoBack = () => {
const parentPath = currentPath.split(/[/\\]/).slice(0, -1).join('/');
const minPath = actualAssetPath.replace(/[/\\]$/, '');
if (parentPath && parentPath !== minPath) {
setCurrentPath(parentPath);
} else if (currentPath !== actualAssetPath) {
setCurrentPath(actualAssetPath);
}
};
// 只能返回到资产基础目录,不能再往上
const canGoBack = currentPath !== actualAssetPath;
const handleItemClick = (item: AssetItem) => {
if (item.isDir) {
setCurrentPath(item.path);
} else {
setSelectedPath(item.path);
}
};
const handleItemDoubleClick = (item: AssetItem) => {
if (!item.isDir) {
const assetId = calculateAssetId(item.path);
onSelect(assetId);
}
};
const handleSelect = () => {
if (selectedPath) {
const assetId = calculateAssetId(selectedPath);
onSelect(assetId);
}
};
/**
* 计算资产ID
* 将绝对路径转换为相对于资产基础目录的assetId不含扩展名
*/
const calculateAssetId = (absolutePath: string): string => {
const normalized = absolutePath.replace(/\\/g, '/');
const baseNormalized = actualAssetPath.replace(/\\/g, '/');
// 获取相对于资产基础目录的路径
let relativePath = normalized;
if (normalized.startsWith(baseNormalized)) {
relativePath = normalized.substring(baseNormalized.length);
}
// 移除开头的斜杠
relativePath = relativePath.replace(/^\/+/, '');
// 移除文件扩展名
const assetId = relativePath.replace(new RegExp(`\\.${fileExtension}$`), '');
return assetId;
};
const getBreadcrumbs = () => {
const basePathNormalized = actualAssetPath.replace(/\\/g, '/');
const currentPathNormalized = currentPath.replace(/\\/g, '/');
const relative = currentPathNormalized.replace(basePathNormalized, '');
const parts = relative.split('/').filter((p) => p);
// 根路径名称(显示"行为树"或"Assets"
const rootName = assetBasePath
? assetBasePath.split('/').pop() || 'Assets'
: 'Content';
const crumbs = [{ name: rootName, path: actualAssetPath }];
let accPath = actualAssetPath;
for (const part of parts) {
accPath = `${accPath}/${part}`;
crumbs.push({ name: part, path: accPath });
}
return crumbs;
};
const breadcrumbs = getBreadcrumbs();
return (
<div className="asset-picker-overlay" onClick={onClose}>
<div className="asset-picker-dialog" onClick={(e) => e.stopPropagation()}>
<div className="asset-picker-header">
<h3>{t('assetPicker.title')}</h3>
<button className="asset-picker-close" onClick={onClose}>
<X size={18} />
</button>
</div>
<div className="asset-picker-toolbar">
<button
className="toolbar-button"
onClick={handleGoBack}
disabled={!canGoBack}
title={t('assetPicker.back')}
>
<ArrowLeft size={16} />
</button>
<div className="asset-picker-breadcrumb">
{breadcrumbs.map((crumb, index) => (
<span key={crumb.path}>
<span
className="breadcrumb-item"
onClick={() => setCurrentPath(crumb.path)}
>
{crumb.name}
</span>
{index < breadcrumbs.length - 1 && <span className="breadcrumb-separator"> / </span>}
</span>
))}
</div>
<div className="view-mode-buttons">
<button
className={`toolbar-button ${viewMode === 'list' ? 'active' : ''}`}
onClick={() => setViewMode('list')}
title={t('assetPicker.listView')}
>
<List size={16} />
</button>
<button
className={`toolbar-button ${viewMode === 'grid' ? 'active' : ''}`}
onClick={() => setViewMode('grid')}
title={t('assetPicker.gridView')}
>
<Grid size={16} />
</button>
</div>
</div>
<div className="asset-picker-search">
<Search size={16} className="search-icon" />
<input
type="text"
placeholder={t('assetPicker.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
{searchQuery && (
<button
className="search-clear"
onClick={() => setSearchQuery('')}
>
<X size={14} />
</button>
)}
</div>
<div className="asset-picker-content">
{loading ? (
<div className="asset-picker-loading">{t('assetPicker.loading')}</div>
) : filteredAssets.length === 0 ? (
<div className="asset-picker-empty">{t('assetPicker.empty')}</div>
) : (
<div className={`asset-picker-list ${viewMode}`}>
{filteredAssets.map((item, index) => (
<div
key={index}
className={`asset-picker-item ${selectedPath === item.path ? 'selected' : ''}`}
onClick={() => handleItemClick(item)}
onDoubleClick={() => handleItemDoubleClick(item)}
>
<div className="asset-icon">
{item.isDir ? (
<Folder size={viewMode === 'grid' ? 32 : 18} style={{ color: '#ffa726' }} />
) : (
<FileCode size={viewMode === 'grid' ? 32 : 18} style={{ color: '#66bb6a' }} />
)}
</div>
<div className="asset-info">
<span className="asset-name">{item.name}</span>
{viewMode === 'list' && !item.isDir && (
<div className="asset-meta">
{item.size && <span className="asset-size">{formatFileSize(item.size)}</span>}
{item.modified && <span className="asset-date">{formatDate(item.modified)}</span>}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
<div className="asset-picker-footer">
<div className="footer-info">
{t('assetPicker.itemCount', { count: filteredAssets.length })}
</div>
<div className="footer-buttons">
<button className="asset-picker-cancel" onClick={onClose}>
{t('assetPicker.cancel')}
</button>
<button
className="asset-picker-select"
onClick={handleSelect}
disabled={!selectedPath}
>
{t('assetPicker.select')}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,541 +0,0 @@
import { useState, useEffect } from 'react';
import { Entity } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
import { PropertyInspector } from './PropertyInspector';
import { FileSearch, ChevronDown, ChevronRight, X, Settings } from 'lucide-react';
import '../styles/EntityInspector.css';
interface EntityInspectorProps {
entityStore: EntityStoreService;
messageHub: MessageHub;
}
export function EntityInspector({ entityStore: _entityStore, messageHub }: EntityInspectorProps) {
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
const [remoteEntity, setRemoteEntity] = useState<any | null>(null);
const [remoteEntityDetails, setRemoteEntityDetails] = useState<any | null>(null);
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
const [componentVersion, setComponentVersion] = useState(0);
useEffect(() => {
const handleSelection = (data: { entity: Entity | null }) => {
setSelectedEntity((prev) => {
// Only reset version when selecting a different entity
// 只在选择不同实体时重置版本
if (prev?.id !== data.entity?.id) {
setComponentVersion(0);
} else {
// Same entity re-selected, trigger refresh
// 同一实体重新选择,触发刷新
setComponentVersion((v) => v + 1);
}
return data.entity;
});
setRemoteEntity(null);
setRemoteEntityDetails(null);
};
const handleRemoteSelection = (data: { entity: any }) => {
setRemoteEntity(data.entity);
setRemoteEntityDetails(null);
setSelectedEntity(null);
};
const handleEntityDetails = (event: Event) => {
const customEvent = event as CustomEvent;
const details = customEvent.detail;
setRemoteEntityDetails(details);
};
const handleComponentChange = () => {
setComponentVersion((prev) => prev + 1);
};
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
const unsubRemoteSelect = messageHub.subscribe('remote-entity:selected', handleRemoteSelection);
const unsubComponentAdded = messageHub.subscribe('component:added', handleComponentChange);
const unsubComponentRemoved = messageHub.subscribe('component:removed', handleComponentChange);
const unsubPropertyChanged = messageHub.subscribe('component:property:changed', handleComponentChange);
window.addEventListener('profiler:entity-details', handleEntityDetails);
return () => {
unsubSelect();
unsubRemoteSelect();
unsubComponentAdded();
unsubComponentRemoved();
unsubPropertyChanged();
window.removeEventListener('profiler:entity-details', handleEntityDetails);
};
}, [messageHub]);
const handleRemoveComponent = (index: number) => {
if (!selectedEntity) return;
const component = selectedEntity.components[index];
if (component) {
selectedEntity.removeComponent(component);
messageHub.publish('component:removed', { entity: selectedEntity, component });
}
};
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;
}
// Actually update the component property
// 实际更新组件属性
component[propertyName] = value;
messageHub.publish('component:property:changed', {
entity: selectedEntity,
component,
propertyName,
value
});
// Also publish scene:modified so other panels can react
messageHub.publish('scene:modified', {});
};
const renderRemoteProperty = (key: string, value: any) => {
if (value === null || value === undefined) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<span className="property-value-text">null</span>
</div>
);
}
if (Array.isArray(value)) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div style={{ flex: 1, display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{value.length === 0 ? (
<span className="property-value-text" style={{ opacity: 0.5 }}>Empty Array</span>
) : (
value.map((item, index) => (
<span
key={index}
style={{
padding: '2px 6px',
background: 'var(--color-bg-inset)',
border: '1px solid var(--color-border-default)',
borderRadius: '3px',
fontSize: '10px',
color: 'var(--color-text-primary)',
fontFamily: 'var(--font-family-mono)'
}}
>
{typeof item === 'object' ? JSON.stringify(item) : String(item)}
</span>
))
)}
</div>
</div>
);
}
const valueType = typeof value;
if (valueType === 'boolean') {
return (
<div key={key} className="property-field property-field-boolean">
<label className="property-label">{key}</label>
<div className={`property-toggle ${value ? 'property-toggle-on' : 'property-toggle-off'} property-toggle-readonly`}>
<span className="property-toggle-thumb" />
</div>
</div>
);
}
if (valueType === 'number') {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<input
type="number"
className="property-input property-input-number"
value={value}
disabled
/>
</div>
);
}
if (valueType === 'string') {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<input
type="text"
className="property-input property-input-text"
value={value}
disabled
/>
</div>
);
}
if (valueType === 'object' && value.r !== undefined && value.g !== undefined && value.b !== undefined) {
const r = Math.round(value.r * 255);
const g = Math.round(value.g * 255);
const b = Math.round(value.b * 255);
const a = value.a !== undefined ? value.a : 1;
const hexColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-color-wrapper">
<div className="property-color-preview" style={{ backgroundColor: hexColor, opacity: a }} />
<input
type="text"
className="property-input property-input-color-text"
value={`${hexColor.toUpperCase()} (${a.toFixed(2)})`}
disabled
style={{ flex: 1 }}
/>
</div>
</div>
);
}
if (valueType === 'object' && value.minX !== undefined && value.maxX !== undefined && value.minY !== undefined && value.maxY !== undefined) {
return (
<div key={key} className="property-field" style={{ flexDirection: 'column', alignItems: 'stretch' }}>
<label className="property-label" style={{ flex: 'none', marginBottom: '4px' }}>{key}</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.minX}
disabled
placeholder="Min"
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.maxX}
disabled
placeholder="Max"
/>
</div>
</div>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.minY}
disabled
placeholder="Min"
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.maxY}
disabled
placeholder="Max"
/>
</div>
</div>
</div>
</div>
);
}
if (valueType === 'object' && value.x !== undefined && value.y !== undefined) {
if (value.z !== undefined) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.x}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.y}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-z">Z</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.z}
disabled
/>
</div>
</div>
</div>
);
} else {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.x}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.y}
disabled
/>
</div>
</div>
</div>
);
}
}
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<span className="property-value-text">{JSON.stringify(value)}</span>
</div>
);
};
if (!selectedEntity && !remoteEntity) {
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content">
<div className="empty-state">
<FileSearch size={48} strokeWidth={1.5} className="empty-icon" />
<div className="empty-title">No entity selected</div>
<div className="empty-hint">Select an entity from the hierarchy</div>
</div>
</div>
</div>
);
}
// 显示远程实体
if (remoteEntity) {
const displayData = remoteEntityDetails || remoteEntity;
const hasDetailedComponents = remoteEntityDetails && remoteEntityDetails.components && remoteEntityDetails.components.length > 0;
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content scrollable">
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Entity Info (Remote)</span>
</div>
<div className="section-content">
<div className="info-row">
<span className="info-label">ID:</span>
<span className="info-value">{displayData.id}</span>
</div>
<div className="info-row">
<span className="info-label">Name:</span>
<span className="info-value">{displayData.name}</span>
</div>
<div className="info-row">
<span className="info-label">Enabled:</span>
<span className="info-value">{displayData.enabled ? 'Yes' : 'No'}</span>
</div>
{displayData.scene && (
<div className="info-row">
<span className="info-label">Scene:</span>
<span className="info-value">{displayData.scene}</span>
</div>
)}
</div>
</div>
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Components ({displayData.componentCount})</span>
</div>
<div className="section-content">
{hasDetailedComponents ? (
<ul className="component-list">
{remoteEntityDetails!.components.map((component: any, index: number) => {
const isExpanded = expandedComponents.has(index);
return (
<li key={index} className={`component-item ${isExpanded ? 'expanded' : ''}`}>
<div className="component-header" onClick={() => toggleComponentExpanded(index)}>
<button
className="component-expand-btn"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
<Settings size={14} className="component-icon" />
<span className="component-name">{component.typeName}</span>
</div>
{isExpanded && (
<div className="component-properties animate-slideDown">
<div className="property-inspector">
{Object.entries(component.properties).map(([key, value]) =>
renderRemoteProperty(key, value)
)}
</div>
</div>
)}
</li>
);
})}
</ul>
) : displayData.componentTypes && displayData.componentTypes.length > 0 ? (
<ul className="component-list">
{displayData.componentTypes.map((componentType: string, index: number) => (
<li key={index} className="component-item">
<div className="component-header">
<Settings size={14} className="component-icon" />
<span className="component-name">{componentType}</span>
</div>
</li>
))}
</ul>
) : (
<div className="empty-state-small">No components</div>
)}
</div>
</div>
</div>
</div>
);
}
const components = selectedEntity!.components;
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content scrollable">
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Entity Info</span>
</div>
<div className="section-content">
<div className="info-row">
<span className="info-label">ID:</span>
<span className="info-value">{selectedEntity!.id}</span>
</div>
<div className="info-row">
<span className="info-label">Name:</span>
<span className="info-value">Entity {selectedEntity!.id}</span>
</div>
<div className="info-row">
<span className="info-label">Enabled:</span>
<span className="info-value">{selectedEntity!.enabled ? 'Yes' : 'No'}</span>
</div>
</div>
</div>
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Components ({components.length})</span>
</div>
<div className="section-content">
{components.length === 0 ? (
<div className="empty-state-small">No components</div>
) : (
<ul className="component-list" key={componentVersion}>
{components.map((component, index) => {
const isExpanded = expandedComponents.has(index);
return (
<li key={index} className={`component-item ${isExpanded ? 'expanded' : ''}`}>
<div className="component-header" onClick={() => toggleComponentExpanded(index)}>
<button
className="component-expand-btn"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
<Settings size={14} className="component-icon" />
<span className="component-name">{component.constructor.name}</span>
<button
className="remove-component-btn"
onClick={(e) => {
e.stopPropagation();
handleRemoveComponent(index);
}}
title="Remove Component"
>
<X size={14} />
</button>
</div>
{isExpanded && (
<div className="component-properties animate-slideDown">
<PropertyInspector
key={`${index}-${componentVersion}`}
component={component}
onChange={(propertyName, value) => handlePropertyChange(component, propertyName, value)}
/>
</div>
)}
</li>
);
})}
</ul>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,241 +0,0 @@
import { useState, useEffect } from 'react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play, BarChart3 } from 'lucide-react';
import type { ProfilerData } from '../services/tokens';
import { SettingsService } from '../services/SettingsService';
import { Core } from '@esengine/ecs-framework';
import { MessageHub } from '@esengine/editor-core';
import { getProfilerService } from '../services/getService';
import '../styles/ProfilerDockPanel.css';
export function ProfilerDockPanel() {
const [profilerData, setProfilerData] = useState<ProfilerData | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080');
const [isPaused, setIsPaused] = useState(false);
useEffect(() => {
const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort) {
setPort(newPort);
}
}) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange);
return () => {
window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
useEffect(() => {
const profilerService = getProfilerService();
if (!profilerService) {
console.warn('[ProfilerDockPanel] ProfilerService not available - plugin may be disabled');
setIsServerRunning(false);
setIsConnected(false);
return;
}
// 订阅数据更新
const unsubscribe = profilerService.subscribe((data: ProfilerData) => {
if (!isPaused) {
setProfilerData(data);
}
});
// 定期检查连接状态
const checkStatus = () => {
setIsConnected(profilerService.isConnected());
setIsServerRunning(profilerService.isServerActive());
};
checkStatus();
const interval = setInterval(checkStatus, 1000);
return () => {
unsubscribe();
clearInterval(interval);
};
}, [isPaused]);
const fps = profilerData?.fps || 0;
const totalFrameTime = profilerData?.totalFrameTime || 0;
const systems = (profilerData?.systems || []).slice(0, 5); // Only show top 5 systems in dock panel
const entityCount = profilerData?.entityCount || 0;
const componentCount = profilerData?.componentCount || 0;
const targetFrameTime = 16.67;
const handleOpenDetails = () => {
const messageHub = Core.services.resolve(MessageHub);
if (messageHub) {
messageHub.publish('ui:openWindow', { windowId: 'profiler' });
}
};
const handleOpenAdvancedProfiler = () => {
const messageHub = Core.services.resolve(MessageHub);
if (messageHub) {
messageHub.publish('ui:openWindow', { windowId: 'advancedProfiler' });
}
};
const handleTogglePause = () => {
setIsPaused(!isPaused);
};
return (
<div className="profiler-dock-panel">
<div className="profiler-dock-header">
<h3>Performance Monitor</h3>
<div className="profiler-dock-header-actions">
{isConnected && (
<>
<button
className="profiler-dock-pause-btn"
onClick={handleTogglePause}
title={isPaused ? 'Resume data updates' : 'Pause data updates'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-dock-details-btn"
onClick={handleOpenAdvancedProfiler}
title="Open advanced profiler"
>
<BarChart3 size={14} />
</button>
<button
className="profiler-dock-details-btn"
onClick={handleOpenDetails}
title="Open detailed profiler"
>
<Maximize2 size={14} />
</button>
</>
)}
<div className="profiler-dock-status">
{isConnected ? (
<>
<Wifi size={12} />
<span className="status-text connected">Connected</span>
</>
) : isServerRunning ? (
<>
<WifiOff size={12} />
<span className="status-text waiting">Waiting...</span>
</>
) : (
<>
<WifiOff size={12} />
<span className="status-text disconnected">Server Off</span>
</>
)}
</div>
</div>
</div>
{!isServerRunning ? (
<div className="profiler-dock-empty">
<Cpu size={32} />
<p>Profiler server not running</p>
<p className="hint">Open Profiler window and connect to start monitoring</p>
</div>
) : !isConnected ? (
<div className="profiler-dock-empty">
<Activity size={32} />
<p>Waiting for game connection...</p>
<p className="hint">Connect to: <code>ws://localhost:{port}</code></p>
</div>
) : (
<div className="profiler-dock-content">
<div className="profiler-dock-stats">
<div className="stat-card">
<div className="stat-icon">
<Activity size={16} />
</div>
<div className="stat-info">
<div className="stat-label">FPS</div>
<div className={`stat-value ${fps < 55 ? 'warning' : ''}`}>{fps}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Cpu size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Frame Time</div>
<div className={`stat-value ${totalFrameTime > targetFrameTime ? 'warning' : ''}`}>
{totalFrameTime.toFixed(1)}ms
</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Layers size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Entities</div>
<div className="stat-value">{entityCount}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Package size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Components</div>
<div className="stat-value">{componentCount}</div>
</div>
</div>
</div>
{systems.length > 0 && (
<div className="profiler-dock-systems">
<h4>Top Systems</h4>
<div className="systems-list">
{systems.map((system) => (
<div key={system.name} className="system-item">
<div className="system-item-header">
<span className="system-item-name">{system.name}</span>
<span className="system-item-time">
{system.executionTime.toFixed(2)}ms
</span>
</div>
<div className="system-item-bar">
<div
className="system-item-bar-fill"
style={{
width: `${Math.min(system.percentage, 100)}%`,
backgroundColor: system.executionTime > targetFrameTime
? 'var(--color-danger)'
: system.executionTime > targetFrameTime * 0.5
? 'var(--color-warning)'
: 'var(--color-success)'
}}
/>
</div>
<div className="system-item-footer">
<span className="system-item-percentage">{system.percentage.toFixed(1)}%</span>
{system.entityCount > 0 && (
<span className="system-item-entities">{system.entityCount} entities</span>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -1,229 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import { Core } from '@esengine/ecs-framework';
import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play } from 'lucide-react';
import '../styles/ProfilerPanel.css';
interface SystemPerformanceData {
name: string;
executionTime: number;
entityCount: number;
averageTime: number;
minTime: number;
maxTime: number;
percentage: number;
}
export function ProfilerPanel() {
const [systems, setSystems] = useState<SystemPerformanceData[]>([]);
const [totalFrameTime, setTotalFrameTime] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [sortBy, setSortBy] = useState<'time' | 'average' | 'name'>('time');
const animationRef = useRef<number>();
useEffect(() => {
const updateProfilerData = () => {
if (isPaused) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const performanceMonitor = Core.performanceMonitor;
if (!performanceMonitor?.isEnabled) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const systemDataMap = performanceMonitor.getAllSystemData();
const systemStatsMap = performanceMonitor.getAllSystemStats();
const systemsData: SystemPerformanceData[] = [];
let total = 0;
for (const [name, data] of systemDataMap.entries()) {
const stats = systemStatsMap.get(name);
if (stats) {
systemsData.push({
name,
executionTime: data.executionTime,
entityCount: data.entityCount,
averageTime: stats.averageTime,
minTime: stats.minTime,
maxTime: stats.maxTime,
percentage: 0
});
total += data.executionTime;
}
}
// Calculate percentages
systemsData.forEach((system) => {
system.percentage = total > 0 ? (system.executionTime / total) * 100 : 0;
});
// Sort systems
systemsData.sort((a, b) => {
switch (sortBy) {
case 'time':
return b.executionTime - a.executionTime;
case 'average':
return b.averageTime - a.averageTime;
case 'name':
return a.name.localeCompare(b.name);
default:
return 0;
}
});
setSystems(systemsData);
setTotalFrameTime(total);
animationRef.current = requestAnimationFrame(updateProfilerData);
};
animationRef.current = requestAnimationFrame(updateProfilerData);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isPaused, sortBy]);
const handleReset = () => {
Core.performanceMonitor?.reset();
};
const fps = totalFrameTime > 0 ? Math.round(1000 / totalFrameTime) : 0;
const targetFrameTime = 16.67; // 60 FPS
const isOverBudget = totalFrameTime > targetFrameTime;
return (
<div className="profiler-panel">
<div className="profiler-toolbar">
<div className="profiler-toolbar-left">
<div className="profiler-stats-summary">
<div className="summary-item">
<Clock size={14} />
<span className="summary-label">Frame:</span>
<span className={`summary-value ${isOverBudget ? 'over-budget' : ''}`}>
{totalFrameTime.toFixed(2)}ms
</span>
</div>
<div className="summary-item">
<Activity size={14} />
<span className="summary-label">FPS:</span>
<span className={`summary-value ${fps < 55 ? 'low-fps' : ''}`}>{fps}</span>
</div>
<div className="summary-item">
<BarChart3 size={14} />
<span className="summary-label">Systems:</span>
<span className="summary-value">{systems.length}</span>
</div>
</div>
</div>
<div className="profiler-toolbar-right">
<select
className="profiler-sort"
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
>
<option value="time">Sort by Time</option>
<option value="average">Sort by Average</option>
<option value="name">Sort by Name</option>
</select>
<button
className="profiler-btn"
onClick={() => setIsPaused(!isPaused)}
title={isPaused ? 'Resume' : 'Pause'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-btn"
onClick={handleReset}
title="Reset Statistics"
>
<RefreshCw size={14} />
</button>
</div>
</div>
<div className="profiler-content">
{systems.length === 0 ? (
<div className="profiler-empty">
<Cpu size={48} />
<p>No performance data available</p>
<p className="profiler-empty-hint">
Make sure Core debug mode is enabled and systems are running
</p>
</div>
) : (
<div className="profiler-systems">
{systems.map((system, index) => (
<div key={system.name} className="system-row">
<div className="system-header">
<div className="system-info">
<span className="system-rank">#{index + 1}</span>
<span className="system-name">{system.name}</span>
{system.entityCount > 0 && (
<span className="system-entities">
({system.entityCount} entities)
</span>
)}
</div>
<div className="system-metrics">
<span className="metric-time">{system.executionTime.toFixed(2)}ms</span>
<span className="metric-percentage">{system.percentage.toFixed(1)}%</span>
</div>
</div>
<div className="system-bar">
<div
className="system-bar-fill"
style={{
width: `${Math.min(system.percentage, 100)}%`,
backgroundColor: system.executionTime > targetFrameTime
? 'var(--color-danger)'
: system.executionTime > targetFrameTime * 0.5
? 'var(--color-warning)'
: 'var(--color-success)'
}}
/>
</div>
<div className="system-stats">
<div className="stat-item">
<span className="stat-label">Avg:</span>
<span className="stat-value">{system.averageTime.toFixed(2)}ms</span>
</div>
<div className="stat-item">
<span className="stat-label">Min:</span>
<span className="stat-value">{system.minTime.toFixed(2)}ms</span>
</div>
<div className="stat-item">
<span className="stat-label">Max:</span>
<span className="stat-value">{system.maxTime.toFixed(2)}ms</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="profiler-footer">
<div className="profiler-legend">
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-success)' }} />
<span>Good (&lt;8ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-warning)' }} />
<span>Warning (8-16ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-danger)' }} />
<span>Critical (&gt;16ms)</span>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,589 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import { Core } from '@esengine/ecs-framework';
import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play, X, Wifi, WifiOff, Server, Search, Table2, TreePine } from 'lucide-react';
import { ProfilerService } from '../services/ProfilerService';
import { SettingsService } from '../services/SettingsService';
import { getProfilerService } from '../services/getService';
import '../styles/ProfilerWindow.css';
interface SystemPerformanceData {
name: string;
executionTime: number;
entityCount: number;
averageTime: number;
minTime: number;
maxTime: number;
percentage: number;
level: number;
children?: SystemPerformanceData[];
isExpanded?: boolean;
}
interface ProfilerWindowProps {
onClose: () => void;
}
type DataSource = 'local' | 'remote';
export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
const [systems, setSystems] = useState<SystemPerformanceData[]>([]);
const [totalFrameTime, setTotalFrameTime] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [sortBy] = useState<'time' | 'average' | 'name'>('time');
const [dataSource, setDataSource] = useState<DataSource>('local');
const [viewMode, setViewMode] = useState<'tree' | 'table'>('table');
const [searchQuery, setSearchQuery] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080');
const animationRef = useRef<number>();
const frameTimesRef = useRef<number[]>([]);
const lastFpsRef = useRef<number>(0);
useEffect(() => {
const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort) {
setPort(newPort);
}
}) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange);
return () => {
window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
// Check ProfilerService connection status
useEffect(() => {
const profilerService = getProfilerService();
if (!profilerService) {
return;
}
const checkStatus = () => {
setIsConnected(profilerService.isConnected());
setIsServerRunning(profilerService.isServerActive());
};
checkStatus();
const interval = setInterval(checkStatus, 1000);
return () => clearInterval(interval);
}, []);
const buildSystemTree = (flatSystems: Map<string, any>, statsMap: Map<string, any>): SystemPerformanceData[] => {
const coreUpdate = flatSystems.get('Core.update');
const servicesUpdate = flatSystems.get('Services.update');
if (!coreUpdate) return [];
const coreStats = statsMap.get('Core.update');
const coreNode: SystemPerformanceData = {
name: 'Core.update',
executionTime: coreUpdate.executionTime,
entityCount: 0,
averageTime: coreStats?.averageTime || 0,
minTime: coreStats?.minTime || 0,
maxTime: coreStats?.maxTime || 0,
percentage: 100,
level: 0,
children: [],
isExpanded: true
};
if (servicesUpdate) {
const servicesStats = statsMap.get('Services.update');
coreNode.children!.push({
name: 'Services.update',
executionTime: servicesUpdate.executionTime,
entityCount: 0,
averageTime: servicesStats?.averageTime || 0,
minTime: servicesStats?.minTime || 0,
maxTime: servicesStats?.maxTime || 0,
percentage: coreUpdate.executionTime > 0
? (servicesUpdate.executionTime / coreUpdate.executionTime) * 100
: 0,
level: 1,
isExpanded: false
});
}
const sceneSystems: SystemPerformanceData[] = [];
for (const [name, data] of flatSystems.entries()) {
if (name !== 'Core.update' && name !== 'Services.update') {
const stats = statsMap.get(name);
if (stats) {
sceneSystems.push({
name,
executionTime: data.executionTime,
entityCount: data.entityCount,
averageTime: stats.averageTime,
minTime: stats.minTime,
maxTime: stats.maxTime,
percentage: 0,
level: 1,
isExpanded: false
});
}
}
}
sceneSystems.forEach((system) => {
system.percentage = coreUpdate.executionTime > 0
? (system.executionTime / coreUpdate.executionTime) * 100
: 0;
});
sceneSystems.sort((a, b) => b.executionTime - a.executionTime);
coreNode.children!.push(...sceneSystems);
return [coreNode];
};
// Subscribe to local performance data
useEffect(() => {
if (dataSource !== 'local') return;
const updateProfilerData = () => {
if (isPaused) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const performanceMonitor = Core.performanceMonitor;
if (!performanceMonitor?.isEnabled) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const systemDataMap = performanceMonitor.getAllSystemData();
const systemStatsMap = performanceMonitor.getAllSystemStats();
const tree = buildSystemTree(systemDataMap, systemStatsMap);
const coreData = systemDataMap.get('Core.update');
setSystems(tree);
setTotalFrameTime(coreData?.executionTime || 0);
animationRef.current = requestAnimationFrame(updateProfilerData);
};
animationRef.current = requestAnimationFrame(updateProfilerData);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isPaused, sortBy, dataSource]);
// Subscribe to remote performance data from ProfilerService
useEffect(() => {
if (dataSource !== 'remote') return;
const profilerService = getProfilerService();
if (!profilerService) {
console.warn('[ProfilerWindow] ProfilerService not available');
return;
}
const unsubscribe = profilerService.subscribe((data) => {
if (isPaused) return;
handleRemoteDebugData({
performance: {
frameTime: data.totalFrameTime,
systemPerformance: data.systems.map((sys) => ({
systemName: sys.name,
lastExecutionTime: sys.executionTime,
averageTime: sys.averageTime,
minTime: 0,
maxTime: 0,
entityCount: sys.entityCount,
percentage: sys.percentage
}))
}
});
});
return () => unsubscribe();
}, [dataSource, isPaused]);
const handleReset = () => {
if (dataSource === 'local') {
Core.performanceMonitor?.reset();
} else {
// Reset remote data
setSystems([]);
setTotalFrameTime(0);
}
};
const handleRemoteDebugData = (debugData: any) => {
if (isPaused) return;
const performance = debugData.performance;
if (!performance) return;
if (!performance.systemPerformance || !Array.isArray(performance.systemPerformance)) {
return;
}
const flatSystemsMap = new Map();
const statsMap = new Map();
for (const system of performance.systemPerformance) {
flatSystemsMap.set(system.systemName, {
executionTime: system.lastExecutionTime || system.averageTime || 0,
entityCount: system.entityCount || 0
});
statsMap.set(system.systemName, {
averageTime: system.averageTime || 0,
minTime: system.minTime || 0,
maxTime: system.maxTime || 0
});
}
const tree = buildSystemTree(flatSystemsMap, statsMap);
setSystems(tree);
setTotalFrameTime(performance.frameTime || 0);
};
const handleDataSourceChange = (newSource: DataSource) => {
if (newSource === 'remote' && dataSource === 'local') {
// Switching to remote
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
}
setDataSource(newSource);
setSystems([]);
setTotalFrameTime(0);
};
const toggleExpand = (systemName: string) => {
const toggleNode = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => {
return nodes.map((node) => {
if (node.name === systemName) {
return { ...node, isExpanded: !node.isExpanded };
}
if (node.children) {
return { ...node, children: toggleNode(node.children) };
}
return node;
});
};
setSystems(toggleNode(systems));
};
const flattenTree = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => {
const result: SystemPerformanceData[] = [];
for (const node of nodes) {
result.push(node);
if (node.isExpanded && node.children) {
result.push(...flattenTree(node.children));
}
}
return result;
};
// Calculate FPS using rolling average for stability
// 使用滑动平均计算 FPS 以保持稳定
const calculateFps = () => {
// Add any positive frame time
// 添加任何正数的帧时间
if (totalFrameTime > 0) {
frameTimesRef.current.push(totalFrameTime);
// Keep last 60 samples
if (frameTimesRef.current.length > 60) {
frameTimesRef.current.shift();
}
}
if (frameTimesRef.current.length > 0) {
const avgFrameTime = frameTimesRef.current.reduce((a, b) => a + b, 0) / frameTimesRef.current.length;
// Cap FPS between 0-999, and ensure avgFrameTime is reasonable
if (avgFrameTime > 0.01) {
lastFpsRef.current = Math.min(999, Math.round(1000 / avgFrameTime));
}
}
return lastFpsRef.current;
};
const fps = calculateFps();
const targetFrameTime = 16.67;
const isOverBudget = totalFrameTime > targetFrameTime;
let displaySystems = viewMode === 'tree' ? flattenTree(systems) : systems;
// Apply search filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
if (viewMode === 'tree') {
displaySystems = displaySystems.filter((sys) =>
sys.name.toLowerCase().includes(query)
);
} else {
// For table view, flatten and filter
const flatList: SystemPerformanceData[] = [];
const flatten = (nodes: SystemPerformanceData[]) => {
for (const node of nodes) {
flatList.push(node);
if (node.children) flatten(node.children);
}
};
flatten(systems);
displaySystems = flatList.filter((sys) =>
sys.name.toLowerCase().includes(query)
);
}
} else if (viewMode === 'table') {
// For table view without search, flatten all
const flatList: SystemPerformanceData[] = [];
const flatten = (nodes: SystemPerformanceData[]) => {
for (const node of nodes) {
flatList.push(node);
if (node.children) flatten(node.children);
}
};
flatten(systems);
displaySystems = flatList;
}
return (
<div className="profiler-window-overlay" onClick={onClose}>
<div className="profiler-window" onClick={(e) => e.stopPropagation()}>
<div className="profiler-window-header">
<div className="profiler-window-title">
<BarChart3 size={20} />
<h2>Performance Profiler</h2>
{isPaused && (
<span className="paused-indicator">PAUSED</span>
)}
</div>
<button className="profiler-window-close" onClick={onClose} title="Close">
<X size={20} />
</button>
</div>
<div className="profiler-window-toolbar">
<div className="profiler-toolbar-left">
<div className="profiler-mode-switch">
<button
className={`mode-btn ${dataSource === 'local' ? 'active' : ''}`}
onClick={() => handleDataSourceChange('local')}
title="Local Core Instance"
>
<Cpu size={14} />
<span>Local</span>
</button>
<button
className={`mode-btn ${dataSource === 'remote' ? 'active' : ''}`}
onClick={() => handleDataSourceChange('remote')}
title="Remote Game Connection"
>
<Server size={14} />
<span>Remote</span>
</button>
</div>
{dataSource === 'remote' && (
<div className="profiler-connection">
<div className="connection-port-display">
<Server size={14} />
<span>ws://localhost:{port}</span>
</div>
{isConnected ? (
<div className="connection-status-indicator connected">
<Wifi size={14} />
<span>Connected</span>
</div>
) : isServerRunning ? (
<div className="connection-status-indicator waiting">
<WifiOff size={14} />
<span>Waiting for game...</span>
</div>
) : (
<div className="connection-status-indicator disconnected">
<WifiOff size={14} />
<span>Server Off</span>
</div>
)}
</div>
)}
{dataSource === 'local' && (
<div className="profiler-stats-summary">
<div className="summary-item">
<Clock size={14} />
<span className="summary-label">Frame:</span>
<span className={`summary-value ${isOverBudget ? 'over-budget' : ''}`}>
{totalFrameTime.toFixed(2)}ms
</span>
</div>
<div className="summary-item">
<Activity size={14} />
<span className="summary-label">FPS:</span>
<span className={`summary-value ${fps < 55 ? 'low-fps' : ''}`}>{fps}</span>
</div>
<div className="summary-item">
<BarChart3 size={14} />
<span className="summary-label">Systems:</span>
<span className="summary-value">{systems.length}</span>
</div>
</div>
)}
</div>
<div className="profiler-toolbar-right">
<div className="profiler-search">
<Search size={14} />
<input
type="text"
placeholder="Search systems..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
</div>
<div className="view-mode-switch">
<button
className={`view-mode-btn ${viewMode === 'table' ? 'active' : ''}`}
onClick={() => setViewMode('table')}
title="Table View"
>
<Table2 size={14} />
</button>
<button
className={`view-mode-btn ${viewMode === 'tree' ? 'active' : ''}`}
onClick={() => setViewMode('tree')}
title="Tree View"
>
<TreePine size={14} />
</button>
</div>
<button
className={`profiler-btn ${isPaused ? 'paused' : ''}`}
onClick={() => setIsPaused(!isPaused)}
title={isPaused ? 'Resume' : 'Pause'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-btn"
onClick={handleReset}
title="Reset Statistics"
>
<RefreshCw size={14} />
</button>
</div>
</div>
<div className="profiler-window-content">
{displaySystems.length === 0 ? (
<div className="profiler-empty">
<Cpu size={48} />
<p>No performance data available</p>
<p className="profiler-empty-hint">
{searchQuery ? 'No systems match your search' : 'Make sure Core debug mode is enabled and systems are running'}
</p>
</div>
) : viewMode === 'table' ? (
<table className="profiler-table">
<thead>
<tr>
<th className="col-name">System Name</th>
<th className="col-time">Current</th>
<th className="col-time">Average</th>
<th className="col-time">Min</th>
<th className="col-time">Max</th>
<th className="col-percent">%</th>
<th className="col-entities">Entities</th>
</tr>
</thead>
<tbody>
{displaySystems.map((system) => (
<tr key={system.name} className={`level-${system.level}`}>
<td className="col-name">
<span className="system-name-cell" style={{ paddingLeft: `${system.level * 16}px` }}>
{system.name}
</span>
</td>
<td className="col-time">
<span className={`time-value ${system.executionTime > targetFrameTime ? 'critical' : system.executionTime > targetFrameTime * 0.5 ? 'warning' : ''}`}>
{system.executionTime.toFixed(2)}ms
</span>
</td>
<td className="col-time">{system.averageTime.toFixed(2)}ms</td>
<td className="col-time">{system.minTime.toFixed(2)}ms</td>
<td className="col-time">{system.maxTime.toFixed(2)}ms</td>
<td className="col-percent">{system.percentage.toFixed(1)}%</td>
<td className="col-entities">{system.entityCount || '-'}</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="profiler-tree">
{displaySystems.map((system) => (
<div key={system.name} className={`tree-row level-${system.level}`}>
<div className="tree-row-header">
<div className="tree-row-left">
{system.children && system.children.length > 0 && (
<button
className="expand-btn"
onClick={() => toggleExpand(system.name)}
>
{system.isExpanded ? '▼' : '▶'}
</button>
)}
<span className="system-name">{system.name}</span>
{system.entityCount > 0 && (
<span className="system-entities">({system.entityCount})</span>
)}
</div>
<div className="tree-row-right">
<span className={`time-value ${system.executionTime > targetFrameTime ? 'critical' : system.executionTime > targetFrameTime * 0.5 ? 'warning' : ''}`}>
{system.executionTime.toFixed(2)}ms
</span>
<span className="percentage-badge">{system.percentage.toFixed(1)}%</span>
</div>
</div>
<div className="tree-row-stats">
<span>Avg: {system.averageTime.toFixed(2)}ms</span>
<span>Min: {system.minTime.toFixed(2)}ms</span>
<span>Max: {system.maxTime.toFixed(2)}ms</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="profiler-window-footer">
<div className="profiler-legend">
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-success)' }} />
<span>Good (&lt;8ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-warning)' }} />
<span>Warning (8-16ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-danger)' }} />
<span>Critical (&gt;16ms)</span>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -96,6 +96,13 @@ type ViewMode = 'local' | 'remote';
type SortColumn = 'name' | 'type';
type SortDirection = 'asc' | 'desc';
/**
* @zh SceneHierarchy Props
* @en SceneHierarchy Props
*
* @zh 注意:后续版本将移除这些 props改用 useEditorServices() Context
* @en Note: These props will be removed in future versions, use useEditorServices() Context instead
*/
interface SceneHierarchyProps {
entityStore: EntityStoreService;
messageHub: MessageHub;
@@ -155,7 +162,12 @@ function isEntityVisible(entity: Entity): boolean {
return entity.enabled;
}
export function SceneHierarchy({ entityStore, messageHub, commandManager, isProfilerMode = false }: SceneHierarchyProps) {
export function SceneHierarchy({
entityStore,
messageHub,
commandManager,
isProfilerMode = false
}: SceneHierarchyProps) {
// ===== 从 HierarchyStore 获取状态 | Get state from HierarchyStore =====
const {
sceneInfo,

View File

@@ -1,6 +1,22 @@
/**
* Inspector Components
* Inspector 组件导出
* @zh Inspector 内部组件 - 被 inspectors/ 目录使用
* @en Inspector Internal Components - Used by inspectors/ directory
*
* @zh 注意:这是内部实现目录,外部应使用 inspectors/ 目录
* @en Note: This is internal implementation, external code should use inspectors/ directory
*
* @zh 架构说明:
* - inspectors/Inspector.tsx - 入口组件,处理不同类型的检查器路由
* - inspector/EntityInspectorPanel.tsx - 实体检查器核心实现
* - inspector/ComponentPropertyEditor.tsx - 组件属性编辑器
*
* @en Architecture:
* - inspectors/Inspector.tsx - Entry component, routes to different inspector types
* - inspector/EntityInspectorPanel.tsx - Core entity inspector implementation
* - inspector/ComponentPropertyEditor.tsx - Component property editor
*
* @deprecated 外部代码请使用 '@/components/inspectors' 导入
* @deprecated External code should import from '@/components/inspectors'
*/
// 主组件 | Main components

View File

@@ -0,0 +1,4 @@
export { AssetField } from './AssetField';
export { CollisionLayerField } from './CollisionLayerField';
export { EntityRefField } from './EntityRefField';
export { TransformRow, RotationRow, MobilityRow, TransformSection } from './TransformField';

View File

@@ -1,2 +1,26 @@
/**
* @zh 检查器模块 - 公共 API
* @en Inspector Module - Public API
*
* @zh 这是检查器的主要入口点,所有外部代码应从这里导入
* @en This is the main entry point for inspectors, all external code should import from here
*
* @example
* ```tsx
* import { Inspector, PropertyInspector } from '@/components/inspectors';
* ```
*/
// 主入口组件 | Main entry component
export { Inspector } from './Inspector';
// 属性检查器 | Property Inspector
export { PropertyInspector } from '../PropertyInspector';
// 类型 | Types
export type { InspectorProps, InspectorTarget, AssetFileInfo } from './types';
// 子组件 (按需导入) | Sub-components (import as needed)
export * from './views';
export * from './fields';
export * from './common';

View File

@@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { Settings, ChevronDown, ChevronRight, X, Plus, Box, Search, Lock, Unlock } from 'lucide-react';
import { Entity, Component, Core, getComponentDependencies, getComponentTypeName, getComponentInstanceTypeName, isComponentInstanceHiddenInInspector, PrefabInstanceComponent } from '@esengine/ecs-framework';
import { MessageHub, CommandManager, EditorComponentRegistry, ComponentActionRegistry, ComponentInspectorRegistry, PrefabService } from '@esengine/editor-core';
import { PropertyInspector } from '../../PropertyInspector';
import { PropertyInspector } from '..';
import { NotificationService } from '../../../services/NotificationService';
import { RemoveComponentCommand, UpdateComponentCommand, AddComponentCommand } from '../../../application/commands/component';
import { PrefabInstanceInfo } from '../common/PrefabInstanceInfo';

View File

@@ -0,0 +1,178 @@
/**
* @zh 编辑器服务上下文 - 解决 prop drilling 问题
* @en Editor Services Context - Solves prop drilling issue
*
* @zh 提供统一的服务访问入口,避免在组件树中层层传递服务实例
* @en Provides unified service access, avoiding passing service instances through component tree
*/
import React, { createContext, useContext, useMemo } from 'react';
import type {
EntityStoreService,
MessageHub,
CommandManager,
InspectorRegistry,
SceneManagerService,
ProjectService,
PluginManager,
UIRegistry,
SettingsRegistry,
BuildService,
LogService,
EntityCreationRegistry,
AssetRegistryService,
} from '@esengine/editor-core';
import type { IDialogExtended } from '../services/TauriDialogService';
import type { INotification } from '@esengine/editor-core';
/**
* @zh 编辑器核心服务接口
* @en Editor core services interface
*/
export interface EditorServices {
// 核心服务 | Core services
entityStore: EntityStoreService | null;
messageHub: MessageHub | null;
commandManager: CommandManager;
// 场景与项目 | Scene & Project
sceneManager: SceneManagerService | null;
projectService: ProjectService | null;
// 插件与注册表 | Plugin & Registries
pluginManager: PluginManager | null;
inspectorRegistry: InspectorRegistry | null;
uiRegistry: UIRegistry | null;
settingsRegistry: SettingsRegistry | null;
entityCreationRegistry?: EntityCreationRegistry | null;
assetRegistry?: AssetRegistryService | null;
// 构建与日志 | Build & Logging
buildService: BuildService | null;
logService: LogService | null;
// UI 服务 | UI Services
notification: INotification | null;
dialog: IDialogExtended | null;
// 项目路径 | Project path
projectPath: string | null;
}
/**
* @zh 编辑器服务上下文
* @en Editor services context
*/
const EditorServicesContext = createContext<EditorServices | null>(null);
/**
* @zh 编辑器服务提供者 Props
* @en Editor services provider props
*/
export interface EditorServicesProviderProps {
children: React.ReactNode;
services: EditorServices;
}
/**
* @zh 编辑器服务提供者组件
* @en Editor services provider component
*
* @example
* ```tsx
* <EditorServicesProvider services={services}>
* <SceneHierarchy />
* <Inspector />
* </EditorServicesProvider>
* ```
*/
export function EditorServicesProvider({ children, services }: EditorServicesProviderProps) {
const value = useMemo(() => services, [
services.entityStore,
services.messageHub,
services.commandManager,
services.sceneManager,
services.projectService,
services.pluginManager,
services.inspectorRegistry,
services.projectPath,
]);
return (
<EditorServicesContext.Provider value={value}>
{children}
</EditorServicesContext.Provider>
);
}
/**
* @zh 获取编辑器服务的 Hook
* @en Hook to get editor services
*
* @zh 必须在 EditorServicesProvider 内部使用
* @en Must be used within EditorServicesProvider
*
* @example
* ```tsx
* function MyComponent() {
* const { entityStore, messageHub, commandManager } = useEditorServices();
* // 使用服务...
* }
* ```
*/
export function useEditorServices(): EditorServices {
const context = useContext(EditorServicesContext);
if (!context) {
throw new Error(
'useEditorServices must be used within EditorServicesProvider. ' +
'Make sure your component is wrapped in <EditorServicesProvider>.'
);
}
return context;
}
/**
* @zh 可选的编辑器服务 Hook不抛出错误
* @en Optional editor services hook (does not throw)
*
* @zh 在 Provider 外部使用时返回 null
* @en Returns null when used outside Provider
*/
export function useEditorServicesOptional(): EditorServices | null {
return useContext(EditorServicesContext);
}
/**
* @zh 获取特定服务的便捷 Hooks
* @en Convenience hooks for specific services
*/
export function useEntityStore(): EntityStoreService | null {
return useEditorServices().entityStore;
}
export function useMessageHub(): MessageHub | null {
return useEditorServices().messageHub;
}
export function useCommandManager(): CommandManager {
return useEditorServices().commandManager;
}
export function useSceneManager(): SceneManagerService | null {
return useEditorServices().sceneManager;
}
export function useProjectService(): ProjectService | null {
return useEditorServices().projectService;
}
export function useInspectorRegistry(): InspectorRegistry | null {
return useEditorServices().inspectorRegistry;
}
export function useProjectPath(): string | null {
return useEditorServices().projectPath;
}
export { EditorServicesContext };

View File

@@ -0,0 +1,20 @@
/**
* @zh 上下文模块导出
* @en Context module exports
*/
export {
EditorServicesContext,
EditorServicesProvider,
useEditorServices,
useEditorServicesOptional,
useEntityStore,
useMessageHub,
useCommandManager,
useSceneManager,
useProjectService,
useInspectorRegistry,
useProjectPath,
type EditorServices,
type EditorServicesProviderProps,
} from './EditorServicesContext';

60
packages/editor-app/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,60 @@
/**
* @zh 全局类型声明
* @en Global type declarations
*
* @zh 扩展 Window 接口以支持编辑器运行时全局变量
* @en Extend Window interface to support editor runtime global variables
*/
import type * as React from 'react';
import type * as ReactDOM from 'react-dom';
import type * as ReactJSXRuntime from 'react/jsx-runtime';
import type { IRuntimePlugin } from '@esengine/editor-core';
/**
* @zh SDK 全局对象结构
* @en SDK global object structure
*/
interface ESEngineSDK {
Core: typeof import('@esengine/ecs-framework').Core;
Scene: typeof import('@esengine/ecs-framework').Scene;
Entity: typeof import('@esengine/ecs-framework').Entity;
Component: typeof import('@esengine/ecs-framework').Component;
System: typeof import('@esengine/ecs-framework').System;
[key: string]: unknown;
}
/**
* @zh 插件容器结构
* @en Plugin container structure
*/
interface PluginContainer {
[pluginName: string]: IRuntimePlugin | undefined;
}
/**
* @zh 用户代码导出结构
* @en User code exports structure
*/
interface UserExports {
[name: string]: unknown;
}
declare global {
interface Window {
// React 全局变量 - 供动态加载的插件使用
// React globals - for dynamically loaded plugins
React: typeof React;
ReactDOM: typeof ReactDOM;
ReactJSXRuntime: typeof ReactJSXRuntime;
// ESEngine 全局变量(与 EditorConfig.globals 对应)
// ESEngine globals (matching EditorConfig.globals)
__ESENGINE_SDK__: ESEngineSDK | undefined;
__ESENGINE_PLUGINS__: PluginContainer | undefined;
__USER_RUNTIME_EXPORTS__: UserExports | undefined;
__USER_EDITOR_EXPORTS__: UserExports | undefined;
}
}
export {};

View File

@@ -0,0 +1,314 @@
/**
* @zh 项目操作 Hook
* @en Project Actions Hook
*
* @zh 封装项目相关的操作(打开、创建、关闭项目)
* @en Encapsulates project-related operations (open, create, close project)
*/
import { useCallback } from 'react';
import { Core } from '@esengine/ecs-framework';
import {
ProjectService,
PluginManager,
SceneManagerService,
UserCodeService
} from '@esengine/editor-core';
import { useEditorStore, useDialogStore } from '../stores';
import { TauriAPI } from '../api/tauri';
import { SettingsService } from '../services/SettingsService';
import { EngineService } from '../services/EngineService';
import { PluginLoader } from '../services/PluginLoader';
import { useLocale } from './useLocale';
interface UseProjectActionsParams {
pluginLoader: PluginLoader;
pluginManagerRef: React.RefObject<PluginManager | null>;
projectServiceRef: React.MutableRefObject<ProjectService | null>;
showToast: (message: string, type: 'success' | 'error' | 'warning' | 'info') => void;
}
export function useProjectActions({
pluginLoader,
pluginManagerRef,
projectServiceRef,
showToast,
}: UseProjectActionsParams) {
const { t } = useLocale();
const {
setProjectLoaded,
setCurrentProjectPath,
setAvailableScenes,
setIsLoading,
setStatus,
setShowProjectWizard,
} = useEditorStore();
const { setErrorDialog, setConfirmDialog } = useDialogStore();
/**
* @zh 打开最近项目
* @en Open recent project
*/
const handleOpenRecentProject = useCallback(async (projectPath: string) => {
try {
setIsLoading(true, t('loading.step1'));
const projectService = Core.services.resolve(ProjectService);
if (!projectService) {
console.error('Required services not available');
setIsLoading(false);
return;
}
projectServiceRef.current = projectService;
await projectService.openProject(projectPath);
await TauriAPI.setProjectBasePath(projectPath);
try {
await TauriAPI.updateProjectTsconfig(projectPath);
} catch (e) {
console.warn('[useProjectActions] Failed to update project tsconfig:', e);
}
const settings = SettingsService.getInstance();
settings.addRecentProject(projectPath);
setCurrentProjectPath(projectPath);
try {
const sceneFiles = await TauriAPI.scanDirectory(`${projectPath}/scenes`, '*.ecs');
const sceneNames = sceneFiles.map(f => `scenes/${f.split(/[\\/]/).pop()}`);
setAvailableScenes(sceneNames);
} catch (e) {
console.warn('[useProjectActions] Failed to scan scenes:', e);
}
setProjectLoaded(true);
setIsLoading(true, t('loading.step2'));
const engineService = EngineService.getInstance();
const engineReady = await engineService.waitForInitialization(30000);
if (!engineReady) {
throw new Error(t('loading.engineTimeoutError'));
}
if (pluginManagerRef.current) {
const pluginSettings = projectService.getPluginSettings();
if (pluginSettings && pluginSettings.enabledPlugins.length > 0) {
await pluginManagerRef.current.loadConfig({ enabledPlugins: pluginSettings.enabledPlugins });
}
}
await engineService.initializeModuleSystems();
const uiResolution = projectService.getUIDesignResolution();
engineService.setUICanvasSize(uiResolution.width, uiResolution.height);
setStatus(t('header.status.projectOpened'));
setIsLoading(true, t('loading.step3'));
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
await userCodeService.waitForReady();
}
const sceneManagerService = Core.services.resolve(SceneManagerService);
if (sceneManagerService) {
await sceneManagerService.newScene();
}
if (pluginManagerRef.current) {
setIsLoading(true, t('loading.loadingPlugins'));
await pluginLoader.loadProjectPlugins(projectPath, pluginManagerRef.current);
}
setIsLoading(false);
} catch (error) {
console.error('Failed to open project:', error);
setStatus(t('header.status.failed'));
setIsLoading(false);
const errorMessage = error instanceof Error ? error.message : String(error);
setErrorDialog({
title: t('project.openFailed'),
message: `${t('project.openFailed')}:\n${errorMessage}`
});
}
}, [t, pluginLoader, pluginManagerRef, projectServiceRef, setProjectLoaded, setCurrentProjectPath, setAvailableScenes, setIsLoading, setStatus, setErrorDialog]);
/**
* @zh 打开项目对话框
* @en Open project dialog
*/
const handleOpenProject = useCallback(async () => {
try {
const projectPath = await TauriAPI.openProjectDialog();
if (!projectPath) return;
await handleOpenRecentProject(projectPath);
} catch (error) {
console.error('Failed to open project dialog:', error);
}
}, [handleOpenRecentProject]);
/**
* @zh 显示创建项目向导
* @en Show create project wizard
*/
const handleCreateProject = useCallback(() => {
setShowProjectWizard(true);
}, [setShowProjectWizard]);
/**
* @zh 从向导创建项目
* @en Create project from wizard
*/
const handleCreateProjectFromWizard = useCallback(async (
projectName: string,
projectPath: string,
_templateId: string
) => {
const sep = projectPath.includes('/') ? '/' : '\\';
const fullProjectPath = `${projectPath}${sep}${projectName}`;
try {
setIsLoading(true, t('project.creating'));
const projectService = Core.services.resolve(ProjectService);
if (!projectService) {
console.error('ProjectService not available');
setIsLoading(false);
setErrorDialog({
title: t('project.createFailed'),
message: t('project.serviceUnavailable')
});
return;
}
await projectService.createProject(fullProjectPath);
setIsLoading(true, t('project.createdOpening'));
await handleOpenRecentProject(fullProjectPath);
} catch (error) {
console.error('Failed to create project:', error);
setIsLoading(false);
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('already exists')) {
setConfirmDialog({
title: t('project.alreadyExists'),
message: t('project.existsQuestion'),
confirmText: t('project.open'),
cancelText: t('common.cancel'),
onConfirm: () => {
setConfirmDialog(null);
setIsLoading(true, t('project.opening'));
handleOpenRecentProject(fullProjectPath).catch((err) => {
console.error('Failed to open project:', err);
setIsLoading(false);
setErrorDialog({
title: t('project.openFailed'),
message: `${t('project.openFailed')}:\n${err instanceof Error ? err.message : String(err)}`
});
});
}
});
} else {
setStatus(t('project.createFailed'));
setErrorDialog({
title: t('project.createFailed'),
message: `${t('project.createFailed')}:\n${errorMessage}`
});
}
}
}, [t, handleOpenRecentProject, setIsLoading, setStatus, setErrorDialog, setConfirmDialog]);
/**
* @zh 浏览项目路径
* @en Browse project path
*/
const handleBrowseProjectPath = useCallback(async (): Promise<string | null> => {
try {
const path = await TauriAPI.openProjectDialog();
return path || null;
} catch (error) {
console.error('Failed to browse path:', error);
return null;
}
}, []);
/**
* @zh 关闭项目
* @en Close project
*/
const handleCloseProject = useCallback(async () => {
if (pluginManagerRef.current) {
await pluginLoader.unloadProjectPlugins(pluginManagerRef.current);
}
const scene = Core.scene;
if (scene) {
scene.end();
}
const engineService = EngineService.getInstance();
engineService.clearModuleSystems();
const projectService = Core.services.tryResolve(ProjectService);
if (projectService) {
await projectService.closeProject();
}
setProjectLoaded(false);
setCurrentProjectPath(null);
setStatus(t('header.status.ready'));
}, [t, pluginLoader, pluginManagerRef, setProjectLoaded, setCurrentProjectPath, setStatus]);
/**
* @zh 删除项目
* @en Delete project
*/
const handleDeleteProject = useCallback(async (projectPath: string) => {
console.log('[useProjectActions] handleDeleteProject called with path:', projectPath);
try {
console.log('[useProjectActions] Calling TauriAPI.deleteFolder...');
await TauriAPI.deleteFolder(projectPath);
console.log('[useProjectActions] deleteFolder succeeded');
const settings = SettingsService.getInstance();
settings.removeRecentProject(projectPath);
setStatus(t('header.status.ready'));
} catch (error) {
console.error('[useProjectActions] Failed to delete project:', error);
setErrorDialog({
title: t('project.deleteFailed'),
message: `${t('project.deleteFailed')}:\n${error instanceof Error ? error.message : String(error)}`
});
}
}, [t, setStatus, setErrorDialog]);
/**
* @zh 从最近项目列表移除
* @en Remove from recent projects
*/
const handleRemoveRecentProject = useCallback((projectPath: string) => {
const settings = SettingsService.getInstance();
settings.removeRecentProject(projectPath);
setStatus(t('header.status.ready'));
}, [t, setStatus]);
return {
handleOpenProject,
handleOpenRecentProject,
handleCreateProject,
handleCreateProjectFromWizard,
handleBrowseProjectPath,
handleCloseProject,
handleDeleteProject,
handleRemoveRecentProject,
};
}

View File

@@ -0,0 +1,187 @@
/**
* @zh 场景操作 Hook
* @en Scene Actions Hook
*
* @zh 封装场景相关的操作(新建、打开、保存场景)
* @en Encapsulates scene-related operations (new, open, save scene)
*/
import { useCallback } from 'react';
import { Core } from '@esengine/ecs-framework';
import { SceneManagerService, UserCodeService } from '@esengine/editor-core';
import { useEditorStore, useDialogStore } from '../stores';
import { useLocale } from './useLocale';
interface UseSceneActionsParams {
sceneManagerRef: React.RefObject<SceneManagerService | null>;
showToast: (message: string, type: 'success' | 'error' | 'warning' | 'info') => void;
}
export function useSceneActions({
sceneManagerRef,
showToast,
}: UseSceneActionsParams) {
const { t } = useLocale();
const { setStatus } = useEditorStore();
const { setErrorDialog } = useDialogStore();
/**
* @zh 新建场景
* @en Create new scene
*/
const handleNewScene = useCallback(async () => {
const sceneManager = sceneManagerRef.current;
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
await sceneManager.newScene();
setStatus(t('scene.newCreated'));
} catch (error) {
console.error('Failed to create new scene:', error);
setStatus(t('scene.createFailed'));
}
}, [t, sceneManagerRef, setStatus]);
/**
* @zh 打开场景(通过对话框选择)
* @en Open scene (via dialog)
*/
const handleOpenScene = useCallback(async () => {
const sceneManager = sceneManagerRef.current;
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
await userCodeService.waitForReady();
}
await sceneManager.openScene();
const sceneState = sceneManager.getSceneState();
setStatus(t('scene.openedSuccess', { name: sceneState.sceneName }));
} catch (error) {
console.error('Failed to open scene:', error);
setStatus(t('scene.openFailed'));
}
}, [t, sceneManagerRef, setStatus]);
/**
* @zh 通过路径打开场景
* @en Open scene by path
*/
const handleOpenSceneByPath = useCallback(async (scenePath: string) => {
console.log('[useSceneActions] handleOpenSceneByPath called:', scenePath);
const sceneManager = sceneManagerRef.current;
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
console.log('[useSceneActions] Waiting for user code service...');
await userCodeService.waitForReady();
console.log('[useSceneActions] User code service ready');
}
console.log('[useSceneActions] Calling sceneManager.openScene...');
await sceneManager.openScene(scenePath);
console.log('[useSceneActions] Scene opened successfully');
const sceneState = sceneManager.getSceneState();
setStatus(t('scene.openedSuccess', { name: sceneState.sceneName }));
} catch (error) {
console.error('Failed to open scene:', error);
setStatus(t('scene.openFailed'));
setErrorDialog({
title: t('scene.openFailed'),
message: `${t('scene.openFailed')}:\n${error instanceof Error ? error.message : String(error)}`
});
}
}, [t, sceneManagerRef, setStatus, setErrorDialog]);
/**
* @zh 保存场景
* @en Save scene
*/
const handleSaveScene = useCallback(async () => {
const sceneManager = sceneManagerRef.current;
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
await sceneManager.saveScene();
const sceneState = sceneManager.getSceneState();
setStatus(t('scene.savedSuccess', { name: sceneState.sceneName }));
} catch (error) {
console.error('Failed to save scene:', error);
setStatus(t('scene.saveFailed'));
}
}, [t, sceneManagerRef, setStatus]);
/**
* @zh 另存为场景
* @en Save scene as
*/
const handleSaveSceneAs = useCallback(async () => {
const sceneManager = sceneManagerRef.current;
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
await sceneManager.saveSceneAs();
const sceneState = sceneManager.getSceneState();
setStatus(t('scene.savedSuccess', { name: sceneState.sceneName }));
} catch (error) {
console.error('Failed to save scene as:', error);
setStatus(t('scene.saveAsFailed'));
}
}, [t, sceneManagerRef, setStatus]);
/**
* @zh 保存预制体或场景(用于快捷键)
* @en Save prefab or scene (for shortcut)
*/
const handleSave = useCallback(async () => {
const sceneManager = sceneManagerRef.current;
if (!sceneManager) return;
try {
if (sceneManager.isPrefabEditMode()) {
await sceneManager.savePrefab();
const prefabState = sceneManager.getPrefabEditModeState();
showToast(t('editMode.prefab.savedSuccess', { name: prefabState?.prefabName ?? 'Prefab' }), 'success');
} else {
await sceneManager.saveScene();
const sceneState = sceneManager.getSceneState();
showToast(t('scene.savedSuccess', { name: sceneState.sceneName }), 'success');
}
} catch (error) {
console.error('Failed to save:', error);
if (sceneManager.isPrefabEditMode()) {
showToast(t('editMode.prefab.saveFailed'), 'error');
} else {
showToast(t('scene.saveFailed'), 'error');
}
}
}, [t, sceneManagerRef, showToast]);
return {
handleNewScene,
handleOpenScene,
handleOpenSceneByPath,
handleSaveScene,
handleSaveSceneAs,
handleSave,
};
}

View File

@@ -13,22 +13,22 @@ import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite';
import { EngineService } from './EngineService';
export class EditorEngineSync {
private static instance: EditorEngineSync | null = null;
private static _instance: EditorEngineSync | null = null;
private engineService: EngineService;
private messageHub: MessageHub | null = null;
private entityStore: EntityStoreService | null = null;
private _engineService: EngineService;
private _messageHub: MessageHub | null = null;
private _entityStore: EntityStoreService | null = null;
// Track synced entities: editor entity id -> engine entity id
private syncedEntities: Map<number, Entity> = new Map();
private _syncedEntities: Map<number, Entity> = new Map();
// Subscription IDs
private subscriptions: Array<() => void> = [];
private _subscriptions: Array<() => void> = [];
private initialized = false;
private _initialized = false;
private constructor() {
this.engineService = EngineService.getInstance();
this._engineService = EngineService.getInstance();
}
/**
@@ -36,10 +36,10 @@ export class EditorEngineSync {
* 获取单例实例。
*/
static getInstance(): EditorEngineSync {
if (!EditorEngineSync.instance) {
EditorEngineSync.instance = new EditorEngineSync();
if (!EditorEngineSync._instance) {
EditorEngineSync._instance = new EditorEngineSync();
}
return EditorEngineSync.instance;
return EditorEngineSync._instance;
}
/**
@@ -47,75 +47,75 @@ export class EditorEngineSync {
* 初始化同步服务。
*/
initialize(messageHub: MessageHub, entityStore: EntityStoreService): void {
if (this.initialized) {
if (this._initialized) {
return;
}
this.messageHub = messageHub;
this.entityStore = entityStore;
this._messageHub = messageHub;
this._entityStore = entityStore;
// Subscribe to entity events
this.subscribeToEvents();
this._subscribeToEvents();
// Sync existing entities
this.syncAllEntities();
this._syncAllEntities();
this.initialized = true;
this._initialized = true;
}
/**
* Subscribe to MessageHub events.
* 订阅MessageHub事件。
*/
private subscribeToEvents(): void {
if (!this.messageHub) return;
private _subscribeToEvents(): void {
if (!this._messageHub) return;
// Entity added
const unsubAdd = this.messageHub.subscribe('entity:added', (data: { entity: Entity }) => {
this.syncEntity(data.entity);
const unsubAdd = this._messageHub.subscribe('entity:added', (data: { entity: Entity }) => {
this._syncEntity(data.entity);
});
this.subscriptions.push(unsubAdd);
this._subscriptions.push(unsubAdd);
// Entity removed
const unsubRemove = this.messageHub.subscribe('entity:removed', (data: { entity: Entity }) => {
this.removeEntityFromEngine(data.entity);
const unsubRemove = this._messageHub.subscribe('entity:removed', (data: { entity: Entity }) => {
this._removeEntityFromEngine(data.entity);
});
this.subscriptions.push(unsubRemove);
this._subscriptions.push(unsubRemove);
// Component property changed - need to re-sync entity
const unsubComponent = this.messageHub.subscribe('component:property:changed', (data: { entity: Entity; component: Component; propertyName: string; value: any }) => {
this.updateEntityInEngine(data.entity, data.component, data.propertyName, data.value);
const unsubComponent = this._messageHub.subscribe('component:property:changed', (data: { entity: Entity; component: Component; propertyName: string; value: any }) => {
this._updateEntityInEngine(data.entity, data.component, data.propertyName, data.value);
});
this.subscriptions.push(unsubComponent);
this._subscriptions.push(unsubComponent);
// Component added - sync entity if it has sprite
const unsubComponentAdded = this.messageHub.subscribe('component:added', (data: { entity: Entity; component: Component }) => {
this.syncEntity(data.entity);
const unsubComponentAdded = this._messageHub.subscribe('component:added', (data: { entity: Entity; component: Component }) => {
this._syncEntity(data.entity);
});
this.subscriptions.push(unsubComponentAdded);
this._subscriptions.push(unsubComponentAdded);
// Entities cleared
const unsubClear = this.messageHub.subscribe('entities:cleared', () => {
this.clearAllFromEngine();
const unsubClear = this._messageHub.subscribe('entities:cleared', () => {
this._clearAllFromEngine();
});
this.subscriptions.push(unsubClear);
this._subscriptions.push(unsubClear);
// Entity selected - update gizmo display
const unsubSelected = this.messageHub.subscribe('entity:selected', (data: { entity: Entity | null }) => {
this.updateSelectedEntity(data.entity);
const unsubSelected = this._messageHub.subscribe('entity:selected', (data: { entity: Entity | null }) => {
this._updateSelectedEntity(data.entity);
});
this.subscriptions.push(unsubSelected);
this._subscriptions.push(unsubSelected);
}
/**
* Update selected entity for gizmo display.
* 更新选中的实体用于Gizmo显示。
*/
private updateSelectedEntity(entity: Entity | null): void {
private _updateSelectedEntity(entity: Entity | null): void {
if (entity) {
this.engineService.setSelectedEntityIds([entity.id]);
this._engineService.setSelectedEntityIds([entity.id]);
} else {
this.engineService.setSelectedEntityIds([]);
this._engineService.setSelectedEntityIds([]);
}
}
@@ -123,12 +123,12 @@ export class EditorEngineSync {
* Sync all existing entities.
* 同步所有现有实体。
*/
private syncAllEntities(): void {
if (!this.entityStore) return;
private _syncAllEntities(): void {
if (!this._entityStore) return;
const entities = this.entityStore.getAllEntities();
const entities = this._entityStore.getAllEntities();
for (const entity of entities) {
this.syncEntity(entity);
this._syncEntity(entity);
}
}
@@ -140,7 +140,7 @@ export class EditorEngineSync {
* via Rust engine's path-based texture loading.
* 注意纹理加载现在由EngineRenderSystem通过Rust引擎的路径加载自动处理。
*/
private syncEntity(entity: Entity): void {
private _syncEntity(entity: Entity): void {
// Check if entity has sprite component
const spriteComponent = entity.getComponent(SpriteComponent);
if (!spriteComponent) {
@@ -151,7 +151,7 @@ export class EditorEngineSync {
// 预加载动画纹理并设置第一帧
const animator = entity.getComponent(SpriteAnimatorComponent);
if (animator && animator.clips) {
const bridge = this.engineService.getBridge();
const bridge = this._engineService.getBridge();
if (bridge) {
for (const clip of animator.clips) {
for (const frame of clip.frames) {
@@ -177,40 +177,40 @@ export class EditorEngineSync {
}
// Track synced entity
this.syncedEntities.set(entity.id, entity);
this._syncedEntities.set(entity.id, entity);
}
/**
* Remove entity from tracking.
* 从跟踪中移除实体。
*/
private removeEntityFromEngine(entity: Entity): void {
private _removeEntityFromEngine(entity: Entity): void {
if (!entity) {
return;
}
// Just remove from tracking, entity destruction is handled by the command
this.syncedEntities.delete(entity.id);
this._syncedEntities.delete(entity.id);
}
/**
* Update entity in engine when component changes.
* 当组件变化时更新引擎中的实体。
*/
private updateEntityInEngine(entity: Entity, component: Component, propertyName: string, value: any): void {
const engineEntity = this.syncedEntities.get(entity.id);
private _updateEntityInEngine(entity: Entity, component: Component, propertyName: string, value: any): void {
const engineEntity = this._syncedEntities.get(entity.id);
if (!engineEntity) {
// Entity not synced yet, try to sync it
this.syncEntity(entity);
this._syncEntity(entity);
return;
}
// Update based on component type
if (component instanceof TransformComponent) {
this.updateTransform(engineEntity, component);
this._updateTransform(engineEntity, component);
} else if (component instanceof SpriteComponent) {
this.updateSprite(engineEntity, component, propertyName, value);
this._updateSprite(engineEntity, component, propertyName, value);
} else if (component instanceof SpriteAnimatorComponent) {
this.updateAnimator(engineEntity, component, propertyName);
this._updateAnimator(engineEntity, component, propertyName);
}
}
@@ -218,10 +218,10 @@ export class EditorEngineSync {
* Update animator - preload textures and set initial frame.
* 更新动画器 - 预加载纹理并设置初始帧。
*/
private updateAnimator(entity: Entity, animator: SpriteAnimatorComponent, propertyName: string): void {
private _updateAnimator(entity: Entity, animator: SpriteAnimatorComponent, propertyName: string): void {
// In editor mode, only preload textures and show first frame (no animation playback)
// 编辑模式下只预加载纹理并显示第一帧(不播放动画)
const bridge = this.engineService.getBridge();
const bridge = this._engineService.getBridge();
const sprite = entity.getComponent(SpriteComponent);
if (bridge && animator.clips) {
@@ -252,7 +252,7 @@ export class EditorEngineSync {
* Update transform in engine entity.
* 更新引擎实体的变换。
*/
private updateTransform(engineEntity: Entity, transform: TransformComponent): void {
private _updateTransform(engineEntity: Entity, transform: TransformComponent): void {
// Get engine transform component (same type as editor)
const engineTransform = engineEntity.getComponent(TransformComponent);
if (engineTransform) {
@@ -281,11 +281,11 @@ export class EditorEngineSync {
* Preloads textures when textureGuid changes to ensure they're available for rendering.
* 当 textureGuid 变更时预加载纹理以确保渲染时可用。
*/
private updateSprite(entity: Entity, sprite: SpriteComponent, property: string, value: any): void {
private _updateSprite(entity: Entity, sprite: SpriteComponent, property: string, value: any): void {
// When textureGuid changes, trigger texture preload
// 当 textureGuid 变更时,触发纹理预加载
if (property === 'textureGuid' && value) {
const bridge = this.engineService.getBridge();
const bridge = this._engineService.getBridge();
if (bridge) {
// Preload the texture so it's ready for the next render frame
// 预加载纹理以便下一渲染帧时可用
@@ -298,9 +298,9 @@ export class EditorEngineSync {
* Clear all synced entities from tracking.
* 清除所有已同步实体的跟踪。
*/
private clearAllFromEngine(): void {
private _clearAllFromEngine(): void {
// Just clear tracking, entity destruction is handled elsewhere
this.syncedEntities.clear();
this._syncedEntities.clear();
}
/**
@@ -308,7 +308,7 @@ export class EditorEngineSync {
* 检查是否已初始化。
*/
isInitialized(): boolean {
return this.initialized;
return this._initialized;
}
/**
@@ -316,7 +316,7 @@ export class EditorEngineSync {
* 获取已同步实体数量。
*/
getSyncedCount(): number {
return this.syncedEntities.size;
return this._syncedEntities.size;
}
/**
@@ -325,15 +325,15 @@ export class EditorEngineSync {
*/
dispose(): void {
// Unsubscribe from all events
for (const unsub of this.subscriptions) {
for (const unsub of this._subscriptions) {
unsub();
}
this.subscriptions = [];
this._subscriptions = [];
// Clear synced entities
this.syncedEntities.clear();
this._syncedEntities.clear();
this.initialized = false;
this._initialized = false;
}
}

View File

@@ -50,6 +50,7 @@ import {
GameRuntime,
createGameRuntime,
EditorPlatformAdapter,
RuntimeMode,
type GameRuntimeConfig
} from '@esengine/runtime-core';
import { BehaviorTreeSystemToken } from '@esengine/behavior-tree';
@@ -72,7 +73,7 @@ const logger = createLogger('EngineService');
* Internally uses GameRuntime, maintains original API compatibility externally
*/
export class EngineService {
private static instance: EngineService | null = null;
private static _instance: EngineService | null = null;
private _runtime: GameRuntime | null = null;
private _initialized = false;
@@ -102,10 +103,10 @@ export class EngineService {
* 获取单例实例。
*/
static getInstance(): EngineService {
if (!EngineService.instance) {
EngineService.instance = new EngineService();
if (!EngineService._instance) {
EngineService._instance = new EngineService();
}
return EngineService.instance;
return EngineService._instance;
}
/**

View File

@@ -81,8 +81,8 @@ export class PluginLoader {
* 初始化插件容器
*/
private initPluginContainer(): void {
if (!(window as any)[PLUGINS_GLOBAL_NAME]) {
(window as any)[PLUGINS_GLOBAL_NAME] = {};
if (!window.__ESENGINE_PLUGINS__) {
window.__ESENGINE_PLUGINS__ = {};
}
}
@@ -167,7 +167,7 @@ export class PluginLoader {
): Promise<IRuntimePlugin | null> {
const pluginKey = this.sanitizePluginKey(pluginName);
const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record<string, any>;
const pluginsContainer = window.__ESENGINE_PLUGINS__ ?? {};
try {
// 插件代码是 IIFE 格式,会自动导出到全局插件容器
@@ -338,7 +338,7 @@ export class PluginLoader {
* 卸载所有已加载的插件
*/
async unloadProjectPlugins(_pluginManager: PluginManager): Promise<void> {
const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record<string, any> | undefined;
const pluginsContainer = window.__ESENGINE_PLUGINS__;
for (const pluginName of this.loadedPlugins.keys()) {
// 清理全局容器中的插件

View File

@@ -101,12 +101,11 @@ export class PluginSDKRegistry {
// 设置全局对象
// Set global object
const sdkGlobalName = EditorConfig.globals.sdk;
(window as any)[sdkGlobalName] = sdkGlobal;
window.__ESENGINE_SDK__ = sdkGlobal;
this.initialized = true;
console.log(`[PluginSDKRegistry] Initialized SDK at window.${sdkGlobalName}`);
console.log(`[PluginSDKRegistry] Initialized SDK at window.__ESENGINE_SDK__`);
}
/**

View File

@@ -45,18 +45,18 @@ export interface ModuleManifest {
}
export class RuntimeResolver {
private static instance: RuntimeResolver;
private baseDir: string = '';
private engineModulesPath: string = '';
private initialized: boolean = false;
private static _instance: RuntimeResolver;
private _baseDir: string = '';
private _engineModulesPath: string = '';
private _initialized: boolean = false;
private constructor() {}
static getInstance(): RuntimeResolver {
if (!RuntimeResolver.instance) {
RuntimeResolver.instance = new RuntimeResolver();
if (!RuntimeResolver._instance) {
RuntimeResolver._instance = new RuntimeResolver();
}
return RuntimeResolver.instance;
return RuntimeResolver._instance;
}
/**
@@ -64,23 +64,23 @@ export class RuntimeResolver {
* Initialize the runtime resolver
*/
async initialize(): Promise<void> {
if (this.initialized) return;
if (this._initialized) return;
// 查找工作区根目录 | Find workspace root
const currentDir = await TauriAPI.getCurrentDir();
this.baseDir = await this.findWorkspaceRoot(currentDir);
this._baseDir = await this._findWorkspaceRoot(currentDir);
// 查找引擎模块路径 | Find engine modules path
this.engineModulesPath = await this.findEngineModulesPath();
this._engineModulesPath = await this._findEngineModulesPath();
this.initialized = true;
this._initialized = true;
}
/**
* 查找工作区根目录
* Find workspace root by looking for workspace markers
*/
private async findWorkspaceRoot(startPath: string): Promise<string> {
private async _findWorkspaceRoot(startPath: string): Promise<string> {
let currentPath = startPath;
for (let i = 0; i < 5; i++) {
@@ -122,7 +122,7 @@ export class RuntimeResolver {
* 使用环境变量和标准路径,避免硬编码
* Use environment variables and standard paths, avoid hardcoding
*/
private getInstalledEnginePaths(): string[] {
private _getInstalledEnginePaths(): string[] {
const paths: string[] = [];
// 1. 使用环境变量(如果设置) | Use environment variable if set
@@ -147,16 +147,16 @@ export class RuntimeResolver {
* Find engine modules path (where compiled modules with module.json are)
* 查找引擎模块路径(编译后的模块和 module.json 所在位置)
*/
private async findEngineModulesPath(): Promise<string> {
private async _findEngineModulesPath(): Promise<string> {
// Try installed editor locations first (production mode)
for (const installedPath of this.getInstalledEnginePaths()) {
for (const installedPath of this._getInstalledEnginePaths()) {
if (await TauriAPI.pathExists(`${installedPath}/index.json`)) {
return installedPath;
}
}
// Try workspace packages directory (dev mode)
const workspacePath = `${this.baseDir}\\packages`;
const workspacePath = `${this._baseDir}\\packages`;
if (await TauriAPI.pathExists(`${workspacePath}\\core\\module.json`)) {
return workspacePath;
}
@@ -172,14 +172,14 @@ export class RuntimeResolver {
* 扫描 packages 目录查找 module.json 文件,而不是硬编码
*/
async getAvailableModules(): Promise<ModuleManifest[]> {
if (!this.initialized) {
if (!this._initialized) {
await this.initialize();
}
const modules: ModuleManifest[] = [];
// Try to read index.json if it exists (installed editor)
const indexPath = `${this.engineModulesPath}\\index.json`;
const indexPath = `${this._engineModulesPath}\\index.json`;
if (await TauriAPI.pathExists(indexPath)) {
try {
const indexContent = await TauriAPI.readFileContent(indexPath);
@@ -191,11 +191,11 @@ export class RuntimeResolver {
}
// Scan packages directory for module.json files
const packageEntries = await TauriAPI.listDirectory(this.engineModulesPath);
const packageEntries = await TauriAPI.listDirectory(this._engineModulesPath);
for (const entry of packageEntries) {
if (!entry.is_dir) continue;
const manifestPath = `${this.engineModulesPath}\\${entry.name}\\module.json`;
const manifestPath = `${this._engineModulesPath}\\${entry.name}\\module.json`;
if (await TauriAPI.pathExists(manifestPath)) {
try {
const content = await TauriAPI.readFileContent(manifestPath);
@@ -210,14 +210,14 @@ export class RuntimeResolver {
}
// Sort by dependencies
return this.sortModulesByDependencies(modules);
return this._sortModulesByDependencies(modules);
}
/**
* Sort modules by dependencies (topological sort)
* 按依赖排序模块(拓扑排序)
*/
private sortModulesByDependencies(modules: ModuleManifest[]): ModuleManifest[] {
private _sortModulesByDependencies(modules: ModuleManifest[]): ModuleManifest[] {
const sorted: ModuleManifest[] = [];
const visited = new Set<string>();
const moduleMap = new Map(modules.map(m => [m.id, m]));
@@ -246,7 +246,7 @@ export class RuntimeResolver {
* 创建与发布构建一致的 libs/{moduleId}/{moduleId}.js 结构
*/
async prepareRuntimeFiles(targetDir: string): Promise<{ modules: ModuleManifest[], importMap: Record<string, string> }> {
if (!this.initialized) {
if (!this._initialized) {
await this.initialize();
}
@@ -267,7 +267,7 @@ export class RuntimeResolver {
// Copy each module's dist files
const missingModules: string[] = [];
for (const module of modules) {
const moduleDistDir = `${this.engineModulesPath}\\${module.id}\\dist`;
const moduleDistDir = `${this._engineModulesPath}\\${module.id}\\dist`;
const moduleSrcFile = `${moduleDistDir}\\index.mjs`;
// Check for index.mjs or index.js
@@ -287,7 +287,7 @@ export class RuntimeResolver {
// Copy all chunk files (code splitting creates chunk-*.js files)
// 复制所有 chunk 文件(代码分割会创建 chunk-*.js 文件)
await this.copyChunkFiles(moduleDistDir, dstModuleDir);
await this._copyChunkFiles(moduleDistDir, dstModuleDir);
// Add to import map using module.name from module.json
// 使用 module.json 中的 module.name 作为 import map 的 key
@@ -310,13 +310,13 @@ export class RuntimeResolver {
}
// Copy external dependencies (e.g., rapier2d)
await this.copyExternalDependencies(modules, libsDir, importMap);
await this._copyExternalDependencies(modules, libsDir, importMap);
// Copy engine WASM files to libs/es-engine/
await this.copyEngineWasm(libsDir);
await this._copyEngineWasm(libsDir);
// Copy module-specific WASM files
await this.copyModuleWasm(modules, targetDir);
await this._copyModuleWasm(modules, targetDir);
console.log(`[RuntimeResolver] Prepared ${copiedModules.length} modules for browser preview`);
@@ -327,7 +327,7 @@ export class RuntimeResolver {
* Copy chunk files from dist directory (for code-split modules)
* 复制 dist 目录中的 chunk 文件(用于代码分割的模块)
*/
private async copyChunkFiles(srcDir: string, dstDir: string): Promise<void> {
private async _copyChunkFiles(srcDir: string, dstDir: string): Promise<void> {
try {
const entries = await TauriAPI.listDirectory(srcDir);
for (const entry of entries) {
@@ -347,7 +347,7 @@ export class RuntimeResolver {
* Copy external dependencies like rapier2d
* 复制外部依赖如 rapier2d
*/
private async copyExternalDependencies(
private async _copyExternalDependencies(
modules: ModuleManifest[],
libsDir: string,
importMap: Record<string, string>
@@ -363,7 +363,7 @@ export class RuntimeResolver {
for (const dep of externalDeps) {
const depId = dep.startsWith('@esengine/') ? dep.slice(10) : dep.replace(/^@[^/]+\//, '');
const srcDistDir = `${this.engineModulesPath}\\${depId}\\dist`;
const srcDistDir = `${this._engineModulesPath}\\${depId}\\dist`;
let srcFile = `${srcDistDir}\\index.mjs`;
if (!await TauriAPI.pathExists(srcFile)) {
srcFile = `${srcDistDir}\\index.js`;
@@ -379,7 +379,7 @@ export class RuntimeResolver {
await TauriAPI.copyFile(srcFile, dstFile);
// Copy chunk files for external dependencies too
await this.copyChunkFiles(srcDistDir, dstModuleDir);
await this._copyChunkFiles(srcDistDir, dstModuleDir);
importMap[dep] = `./libs/${depId}/${depId}.js`;
console.log(`[RuntimeResolver] Copied external dependency: ${depId}`);
@@ -391,17 +391,17 @@ export class RuntimeResolver {
* 获取引擎 WASM 文件的搜索路径
* Get search paths for engine WASM files
*/
private getEngineWasmSearchPaths(): string[] {
private _getEngineWasmSearchPaths(): string[] {
const paths: string[] = [];
// 1. 开发模式:工作区内的 engine 包 | Dev mode: engine package in workspace
paths.push(`${this.baseDir}\\packages\\${ENGINE_WASM_CONFIG.packageName}\\pkg`);
paths.push(`${this._baseDir}\\packages\\${ENGINE_WASM_CONFIG.packageName}\\pkg`);
// 2. 相对于引擎模块路径 | Relative to engine modules path
paths.push(`${this.engineModulesPath}\\..\\..\\${ENGINE_WASM_CONFIG.packageName}\\pkg`);
paths.push(`${this._engineModulesPath}\\..\\..\\${ENGINE_WASM_CONFIG.packageName}\\pkg`);
// 3. 生产模式:安装目录中的 wasm 文件夹 | Production mode: wasm folder in install dir
for (const installedPath of this.getInstalledEnginePaths()) {
for (const installedPath of this._getInstalledEnginePaths()) {
// 将 /engine 替换为 /wasm | Replace /engine with /wasm
const wasmPath = installedPath.replace(/[/\\]engine$/, '/wasm');
paths.push(wasmPath);
@@ -414,14 +414,14 @@ export class RuntimeResolver {
* Copy engine WASM files
* 复制引擎 WASM 文件
*/
private async copyEngineWasm(libsDir: string): Promise<void> {
private async _copyEngineWasm(libsDir: string): Promise<void> {
const esEngineDir = `${libsDir}\\${ENGINE_WASM_CONFIG.dirName}`;
if (!await TauriAPI.pathExists(esEngineDir)) {
await TauriAPI.createDirectory(esEngineDir);
}
// Try different locations for engine WASM
const wasmSearchPaths = this.getEngineWasmSearchPaths();
const wasmSearchPaths = this._getEngineWasmSearchPaths();
for (const searchPath of wasmSearchPaths) {
if (await TauriAPI.pathExists(searchPath)) {
@@ -444,7 +444,7 @@ export class RuntimeResolver {
* Copy module-specific WASM files (e.g., physics)
* 复制模块特定的 WASM 文件(如物理)
*/
private async copyModuleWasm(modules: ModuleManifest[], targetDir: string): Promise<void> {
private async _copyModuleWasm(modules: ModuleManifest[], targetDir: string): Promise<void> {
for (const module of modules) {
if (!module.requiresWasm || !module.wasmPaths?.length) continue;
@@ -463,16 +463,16 @@ export class RuntimeResolver {
// Build search paths - check module's own pkg, external deps, and common locations
const searchPaths: string[] = [
`${this.engineModulesPath}\\${module.id}\\pkg\\${wasmFileName}`,
`${this.baseDir}\\packages\\${module.id}\\pkg\\${wasmFileName}`,
`${this._engineModulesPath}\\${module.id}\\pkg\\${wasmFileName}`,
`${this._baseDir}\\packages\\${module.id}\\pkg\\${wasmFileName}`,
];
// Check external dependencies for WASM (e.g., physics-rapier2d uses rapier2d's WASM)
if (module.externalDependencies) {
for (const dep of module.externalDependencies) {
const depId = dep.startsWith('@esengine/') ? dep.slice(10) : dep.replace(/^@[^/]+\//, '');
searchPaths.push(`${this.engineModulesPath}\\${depId}\\pkg\\${wasmFileName}`);
searchPaths.push(`${this.baseDir}\\packages\\${depId}\\pkg\\${wasmFileName}`);
searchPaths.push(`${this._engineModulesPath}\\${depId}\\pkg\\${wasmFileName}`);
searchPaths.push(`${this._baseDir}\\packages\\${depId}\\pkg\\${wasmFileName}`);
}
}
@@ -501,7 +501,7 @@ export class RuntimeResolver {
* 获取工作区根目录
*/
getBaseDir(): string {
return this.baseDir;
return this._baseDir;
}
/**
@@ -509,6 +509,6 @@ export class RuntimeResolver {
* 获取引擎模块路径
*/
getEngineModulesPath(): string {
return this.engineModulesPath;
return this._engineModulesPath;
}
}

View File

@@ -1,68 +1,68 @@
export class SettingsService {
private static instance: SettingsService;
private settings: Map<string, any> = new Map();
private storageKey = 'editor-settings';
private static _instance: SettingsService;
private _settings: Map<string, any> = new Map();
private _storageKey = 'editor-settings';
private constructor() {
this.loadSettings();
this._loadSettings();
}
public static getInstance(): SettingsService {
if (!SettingsService.instance) {
SettingsService.instance = new SettingsService();
if (!SettingsService._instance) {
SettingsService._instance = new SettingsService();
}
return SettingsService.instance;
return SettingsService._instance;
}
private loadSettings(): void {
private _loadSettings(): void {
try {
const stored = localStorage.getItem(this.storageKey);
const stored = localStorage.getItem(this._storageKey);
if (stored) {
const data = JSON.parse(stored);
this.settings = new Map(Object.entries(data));
this._settings = new Map(Object.entries(data));
}
} catch (error) {
console.error('[SettingsService] Failed to load settings:', error);
}
}
private saveSettings(): void {
private _saveSettings(): void {
try {
const data = Object.fromEntries(this.settings);
localStorage.setItem(this.storageKey, JSON.stringify(data));
const data = Object.fromEntries(this._settings);
localStorage.setItem(this._storageKey, JSON.stringify(data));
} catch (error) {
console.error('[SettingsService] Failed to save settings:', error);
}
}
public get<T>(key: string, defaultValue: T): T {
if (this.settings.has(key)) {
return this.settings.get(key) as T;
if (this._settings.has(key)) {
return this._settings.get(key) as T;
}
return defaultValue;
}
public set<T>(key: string, value: T): void {
this.settings.set(key, value);
this.saveSettings();
this._settings.set(key, value);
this._saveSettings();
}
public has(key: string): boolean {
return this.settings.has(key);
return this._settings.has(key);
}
public delete(key: string): void {
this.settings.delete(key);
this.saveSettings();
this._settings.delete(key);
this._saveSettings();
}
public clear(): void {
this.settings.clear();
this.saveSettings();
this._settings.clear();
this._saveSettings();
}
public getAll(): Record<string, any> {
return Object.fromEntries(this.settings);
return Object.fromEntries(this._settings);
}
public getRecentProjects(): string[] {

View File

@@ -1,235 +0,0 @@
.game-view {
position: relative;
width: 100%;
height: 100%;
background: #000;
display: flex;
flex-direction: column;
overflow: hidden;
}
.game-view-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border-default);
flex-shrink: 0;
gap: 8px;
height: 26px;
z-index: var(--z-index-above);
}
.game-view-toolbar-left {
display: flex;
align-items: center;
gap: 4px;
}
.game-view-toolbar-right {
display: flex;
align-items: center;
gap: 4px;
}
.game-view-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
min-width: 32px;
height: 32px;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
padding: 0 6px;
}
.game-view-btn:hover:not(:disabled) {
background: var(--color-bg-hover);
color: var(--color-text-primary);
border-color: var(--color-border-hover);
}
.game-view-btn.active {
background: var(--color-primary);
color: var(--color-text-inverse);
border-color: var(--color-primary);
}
.game-view-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.game-view-btn:active:not(:disabled) {
transform: scale(0.95);
}
.game-view-divider {
width: 1px;
height: 24px;
background: var(--color-border-default);
margin: 0 4px;
}
.game-view-dropdown {
position: relative;
}
.game-view-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-lg);
z-index: var(--z-index-dropdown);
min-width: 160px;
padding: 4px;
animation: dropdownFadeIn 0.15s ease-out;
}
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.game-view-dropdown-menu button {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 12px;
background: transparent;
border: none;
border-radius: var(--radius-xs);
color: var(--color-text-primary);
font-size: 13px;
cursor: pointer;
text-align: left;
}
.game-view-dropdown-menu button:hover {
background: var(--color-bg-hover);
}
.game-view-canvas {
flex: 1;
width: 100%;
height: 100%;
display: block;
background: #000;
user-select: none;
}
.game-view-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
pointer-events: none;
}
.game-view-overlay-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: var(--color-text-secondary);
}
.game-view-overlay-content svg {
opacity: 0.5;
}
.game-view-overlay-content span {
font-size: 14px;
}
.game-view-stats {
position: absolute;
bottom: 12px;
right: 12px;
display: flex;
flex-direction: column;
gap: 4px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
padding: 8px 12px;
font-family: var(--font-family-mono);
font-size: 11px;
pointer-events: none;
z-index: var(--z-index-above);
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.game-view-stat {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.game-view-stat-label {
color: var(--color-text-secondary);
font-weight: 500;
}
.game-view-stat-value {
color: var(--color-primary);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.game-view:fullscreen {
background: #000;
}
.game-view:fullscreen .game-view-toolbar {
display: none;
}
.game-view:fullscreen .game-view-overlay {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.game-view-btn {
transition: none;
}
.game-view-dropdown-menu {
animation: none;
}
}

View File

@@ -1,509 +0,0 @@
.plugin-manager-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index-modal);
backdrop-filter: blur(4px);
}
.plugin-manager-window {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border-default);
border-radius: 8px;
width: 90%;
max-width: 1000px;
height: 80%;
max-height: 700px;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.plugin-manager-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: var(--color-bg-overlay);
border-bottom: 1px solid var(--color-border-default);
}
.plugin-manager-title {
display: flex;
align-items: center;
gap: 12px;
color: var(--color-text-primary);
}
.plugin-manager-title h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.plugin-manager-close {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 6px;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-manager-close:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.plugin-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border-default);
gap: 12px;
}
.plugin-toolbar-left,
.plugin-toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.plugin-search {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 4px;
color: var(--color-text-secondary);
}
.plugin-search input {
border: none;
background: none;
outline: none;
color: var(--color-text-primary);
font-size: 13px;
min-width: 250px;
}
.plugin-stats {
display: flex;
align-items: center;
gap: 12px;
padding: 0 8px;
font-size: 12px;
}
.plugin-stats .stat-item {
display: flex;
align-items: center;
gap: 4px;
}
.plugin-stats .stat-item.enabled {
color: var(--color-success);
}
.plugin-stats .stat-item.disabled {
color: var(--color-text-secondary);
}
.plugin-view-mode {
display: flex;
gap: 2px;
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 4px;
overflow: hidden;
}
.plugin-view-mode button {
padding: 6px 10px;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-view-mode button:hover {
background: var(--color-bg-hover);
}
.plugin-view-mode button.active {
background: var(--color-primary);
color: white;
}
.plugin-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.plugin-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-secondary);
gap: 16px;
}
.plugin-categories {
display: flex;
flex-direction: column;
gap: 16px;
}
.plugin-category {
background: var(--color-bg-overlay);
border: 1px solid var(--color-border-default);
border-radius: 6px;
overflow: hidden;
}
.plugin-category-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--color-bg-elevated);
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
.plugin-category-header:hover {
background: var(--color-bg-hover);
}
.plugin-category-toggle {
background: none;
border: none;
padding: 0;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
}
.plugin-category-icon {
font-size: 18px;
}
.plugin-category-name {
flex: 1;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
}
.plugin-category-count {
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-bg-overlay);
padding: 3px 10px;
border-radius: 12px;
}
.plugin-category-content {
padding: 16px;
}
.plugin-category-content.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
}
.plugin-category-content.list {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Plugin Card (Grid View) */
.plugin-card {
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 6px;
padding: 14px;
transition: all 0.2s;
}
.plugin-card:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.plugin-card.disabled {
opacity: 0.6;
}
.plugin-card-header {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
.plugin-card-icon {
font-size: 24px;
color: var(--color-primary);
}
.plugin-card-info {
flex: 1;
}
.plugin-card-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-card-version {
font-size: 11px;
color: var(--color-text-secondary);
}
.plugin-toggle {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: var(--color-text-secondary);
transition: all 0.2s;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-toggle:hover {
background: var(--color-bg-hover);
}
.plugin-toggle.enabled {
color: var(--color-success);
}
.plugin-toggle.disabled {
color: var(--color-text-secondary);
}
.plugin-card-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.5;
margin-bottom: 10px;
}
.plugin-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid var(--color-border-default);
font-size: 11px;
}
.plugin-card-category {
color: var(--color-text-secondary);
}
.plugin-card-installed {
color: var(--color-text-tertiary);
}
/* Plugin List (List View) */
.plugin-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 6px;
transition: all 0.2s;
}
.plugin-list-item:hover {
border-color: var(--color-primary);
}
.plugin-list-item.disabled {
opacity: 0.6;
}
.plugin-list-icon {
font-size: 20px;
color: var(--color-primary);
}
.plugin-list-info {
flex: 1;
}
.plugin-list-name {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-list-version {
font-size: 11px;
font-weight: normal;
color: var(--color-text-secondary);
}
.plugin-list-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.4;
}
.plugin-list-status {
margin-right: 8px;
}
.status-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.status-badge.enabled {
background: rgba(34, 197, 94, 0.15);
color: var(--color-success);
}
.status-badge.disabled {
background: var(--color-bg-overlay);
color: var(--color-text-secondary);
}
.plugin-list-toggle {
padding: 6px 14px;
background: var(--color-bg-overlay);
border: 1px solid var(--color-border-default);
border-radius: 4px;
color: var(--color-text-primary);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.plugin-list-toggle:hover {
background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
/* Scrollbar */
.plugin-content::-webkit-scrollbar {
width: 10px;
}
.plugin-content::-webkit-scrollbar-track {
background: var(--color-bg-elevated);
}
.plugin-content::-webkit-scrollbar-thumb {
background: var(--color-border-default);
border-radius: 5px;
}
.plugin-content::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
/* Plugin Manager Tabs */
.plugin-manager-tabs {
display: flex;
gap: 4px;
padding: 0 20px;
background: var(--color-bg-overlay);
border-bottom: 1px solid var(--color-border-default);
}
.plugin-manager-tab {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--color-text-secondary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.plugin-manager-tab:hover {
color: var(--color-text-primary);
background: rgba(255, 255, 255, 0.05);
}
.plugin-manager-tab.active {
color: var(--color-accent);
border-bottom-color: var(--color-accent);
}
.plugin-publish-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--color-accent, #0e639c);
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.plugin-publish-btn:hover {
background: var(--color-accent-hover, #1177bb);
}
.plugin-card-footer .plugin-publish-btn {
margin-left: auto;
}
.plugin-list-item .plugin-publish-btn {
margin-left: 8px;
}

View File

@@ -1,495 +0,0 @@
.plugin-market-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-primary, #1e1e1e);
color: var(--color-text-primary, #cccccc);
}
.plugin-market-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
border-bottom: 1px solid var(--color-border, #333);
background: var(--color-bg-secondary, #252526);
height: 26px;
}
.plugin-market-search {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
}
.plugin-market-search input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--color-text-primary, #cccccc);
font-size: 14px;
}
.plugin-market-filters {
display: flex;
gap: 8px;
align-items: center;
}
.plugin-market-filter-select {
padding: 8px 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #cccccc);
font-size: 13px;
cursor: pointer;
}
.plugin-market-filter-select:hover {
border-color: var(--color-accent, #0e639c);
}
.plugin-market-refresh {
padding: 8px 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #cccccc);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-market-refresh:hover {
background: var(--color-bg-hover, #2d2d30);
}
.plugin-market-publish {
padding: 8px 16px;
background: var(--color-accent, #0e639c);
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.plugin-market-publish:hover {
background: var(--color-accent-hover, #1177bb);
}
.plugin-market-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.plugin-market-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.plugin-market-card {
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
transition: all 0.2s;
}
.plugin-market-card:hover {
border-color: var(--color-accent, #0e639c);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.plugin-market-card-header {
display: flex;
gap: 12px;
align-items: flex-start;
}
.plugin-market-card-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-accent-bg, rgba(14, 99, 156, 0.1));
border-radius: 8px;
color: var(--color-accent, #0e639c);
}
.plugin-market-card-info {
flex: 1;
min-width: 0;
}
.plugin-market-card-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.plugin-market-card-title span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plugin-market-badge {
display: inline-flex;
align-items: center;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.plugin-market-badge.official {
background: rgba(52, 199, 89, 0.15);
color: #34c759;
}
.plugin-market-card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary, #858585);
}
.plugin-market-card-author {
display: flex;
align-items: center;
gap: 4px;
}
.plugin-market-card-description {
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.plugin-market-card-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.plugin-market-tag {
padding: 4px 8px;
background: var(--color-bg-tertiary, #333);
border-radius: 4px;
font-size: 11px;
color: var(--color-text-secondary, #858585);
}
.plugin-market-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid var(--color-border, #333);
}
.plugin-market-card-link {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--color-accent, #0e639c);
text-decoration: none;
background: transparent;
border: none;
padding: 0;
cursor: pointer;
transition: opacity 0.2s;
}
.plugin-market-card-link:hover {
opacity: 0.8;
}
.plugin-market-card-actions {
display: flex;
gap: 8px;
}
.plugin-market-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.plugin-market-btn.install {
background: var(--color-accent, #0e639c);
color: white;
}
.plugin-market-btn.install:hover {
background: var(--color-accent-hover, #1177bb);
}
.plugin-market-btn.installed {
background: rgba(52, 199, 89, 0.15);
color: #34c759;
}
.plugin-market-btn.installed:hover {
background: rgba(52, 199, 89, 0.25);
}
.plugin-market-btn.update {
background: rgba(255, 149, 0, 0.15);
color: #ff9500;
}
.plugin-market-btn.update:hover {
background: rgba(255, 149, 0, 0.25);
}
.plugin-market-btn.installing {
background: var(--color-bg-tertiary, #333);
color: var(--color-text-secondary, #858585);
cursor: not-allowed;
}
.plugin-market-loading,
.plugin-market-error,
.plugin-market-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 16px;
color: var(--color-text-secondary, #858585);
}
.plugin-market-error {
gap: 12px;
max-width: 600px;
margin: 0 auto;
padding: 40px 20px;
text-align: center;
}
.plugin-market-error .error-icon {
color: #ff9500;
margin-bottom: 8px;
}
.plugin-market-error h3 {
font-size: 20px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
margin: 0;
}
.plugin-market-error .error-description {
font-size: 14px;
color: var(--color-text-secondary, #858585);
line-height: 1.6;
margin: 8px 0;
}
.plugin-market-error .error-details {
background: rgba(255, 59, 48, 0.1);
border: 1px solid rgba(255, 59, 48, 0.3);
border-radius: 6px;
padding: 12px 16px;
margin: 16px 0;
max-width: 100%;
}
.plugin-market-error .error-message {
font-size: 12px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
color: #ff3b30;
margin: 0;
word-break: break-word;
}
.plugin-market-error .retry-button,
.plugin-market-loading button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--color-accent, #0e639c);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
margin-top: 8px;
}
.plugin-market-error .retry-button:hover,
.plugin-market-loading button:hover {
background: var(--color-accent-hover, #1177bb);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(14, 99, 156, 0.3);
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.plugin-market-direct-source-toggle {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: var(--color-accent, #0e639c);
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.plugin-market-direct-source-toggle:hover {
background: rgba(14, 99, 156, 0.15);
border-color: rgba(14, 99, 156, 0.5);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(14, 99, 156, 0.2);
}
.plugin-market-direct-source-toggle input[type="checkbox"] {
position: relative;
width: 38px;
height: 20px;
margin: 0;
cursor: pointer;
appearance: none;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
transition: all 0.3s ease;
}
.plugin-market-direct-source-toggle input[type="checkbox"]:checked {
background: var(--color-accent, #0e639c);
border-color: var(--color-accent, #0e639c);
}
.plugin-market-direct-source-toggle input[type="checkbox"]::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
top: 1px;
left: 1px;
background: white;
border-radius: 50%;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.plugin-market-direct-source-toggle input[type="checkbox"]:checked::before {
left: 19px;
}
.plugin-market-direct-source-toggle .toggle-label {
white-space: nowrap;
font-weight: 500;
}
/* 版本选择器 */
.plugin-market-version-select {
padding: 2px 6px;
background: var(--color-bg-tertiary, #333);
border: 1px solid var(--color-border, #333);
border-radius: 3px;
color: var(--color-text-primary, #cccccc);
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.plugin-market-version-select:hover {
border-color: var(--color-accent, #0e639c);
background: var(--color-bg-hover, #2d2d30);
}
.plugin-market-version-select:focus {
outline: none;
border-color: var(--color-accent, #0e639c);
}
/* 更新日志 */
.plugin-market-version-changes {
margin: 8px 0;
padding: 8px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.2);
border-radius: 4px;
}
.plugin-market-version-changes summary {
cursor: pointer;
font-size: 12px;
font-weight: 500;
color: var(--color-accent, #0e639c);
padding: 4px;
user-select: none;
}
.plugin-market-version-changes summary:hover {
opacity: 0.8;
}
.plugin-market-version-changes p {
margin: 8px 0 0 0;
padding-left: 4px;
font-size: 12px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}

View File

@@ -1,382 +0,0 @@
.plugin-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-secondary);
color: var(--color-text-primary);
}
.plugin-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
background: var(--color-bg-tertiary);
border-bottom: 1px solid var(--color-border);
gap: 8px;
height: 26px;
}
.plugin-toolbar-left,
.plugin-toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.plugin-search {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 4px;
color: var(--color-text-secondary);
}
.plugin-search input {
border: none;
background: none;
outline: none;
color: var(--color-text-primary);
font-size: 12px;
min-width: 200px;
}
.plugin-stats {
display: flex;
align-items: center;
gap: 12px;
padding: 0 8px;
font-size: 12px;
}
.plugin-stats .stat-item {
display: flex;
align-items: center;
gap: 4px;
}
.plugin-stats .stat-item.enabled {
color: var(--color-success);
}
.plugin-stats .stat-item.disabled {
color: var(--color-text-secondary);
}
.plugin-view-mode {
display: flex;
gap: 2px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 4px;
overflow: hidden;
}
.plugin-view-mode button {
padding: 4px 8px;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.plugin-view-mode button:hover {
background: var(--color-bg-hover);
}
.plugin-view-mode button.active {
background: var(--color-primary);
color: white;
}
.plugin-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.plugin-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-secondary);
gap: 16px;
}
.plugin-categories {
display: flex;
flex-direction: column;
gap: 16px;
}
.plugin-category {
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 6px;
overflow: hidden;
}
.plugin-category-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--color-bg-secondary);
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
.plugin-category-header:hover {
background: var(--color-bg-hover);
}
.plugin-category-toggle {
background: none;
border: none;
padding: 0;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
}
.plugin-category-icon {
font-size: 18px;
}
.plugin-category-name {
flex: 1;
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
}
.plugin-category-count {
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-bg-tertiary);
padding: 2px 8px;
border-radius: 10px;
}
.plugin-category-content {
padding: 12px;
}
.plugin-category-content.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.plugin-category-content.list {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Plugin Card (Grid View) */
.plugin-card {
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 12px;
transition: all 0.2s;
}
.plugin-card:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.plugin-card.disabled {
opacity: 0.6;
}
.plugin-card-header {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
.plugin-card-icon {
font-size: 24px;
color: var(--color-primary);
}
.plugin-card-info {
flex: 1;
}
.plugin-card-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-card-version {
font-size: 11px;
color: var(--color-text-secondary);
}
.plugin-toggle {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: var(--color-text-secondary);
transition: all 0.2s;
border-radius: 4px;
}
.plugin-toggle:hover {
background: var(--color-bg-hover);
}
.plugin-toggle.enabled {
color: var(--color-success);
}
.plugin-toggle.disabled {
color: var(--color-text-secondary);
}
.plugin-card-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.5;
margin-bottom: 10px;
}
.plugin-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid var(--color-border);
font-size: 11px;
}
.plugin-card-category {
color: var(--color-text-secondary);
}
.plugin-card-installed {
color: var(--color-text-tertiary);
}
/* Plugin List (List View) */
.plugin-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 6px;
transition: all 0.2s;
}
.plugin-list-item:hover {
border-color: var(--color-primary);
}
.plugin-list-item.disabled {
opacity: 0.6;
}
.plugin-list-icon {
font-size: 20px;
color: var(--color-primary);
}
.plugin-list-info {
flex: 1;
}
.plugin-list-name {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-list-version {
font-size: 11px;
font-weight: normal;
color: var(--color-text-secondary);
}
.plugin-list-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.4;
}
.plugin-list-status {
margin-right: 8px;
}
.status-badge {
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.status-badge.enabled {
background: rgba(34, 197, 94, 0.15);
color: var(--color-success);
}
.status-badge.disabled {
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
}
.plugin-list-toggle {
padding: 6px 12px;
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 4px;
color: var(--color-text-primary);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.plugin-list-toggle:hover {
background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
/* Scrollbar */
.plugin-content::-webkit-scrollbar {
width: 8px;
}
.plugin-content::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
}
.plugin-content::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}
.plugin-content::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}

View File

@@ -1,861 +0,0 @@
/* 统一滚动条样式 */
.plugin-publish-wizard ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.plugin-publish-wizard ::-webkit-scrollbar-track {
background: var(--color-bg-primary, #1e1e1e);
border-radius: 4px;
}
.plugin-publish-wizard ::-webkit-scrollbar-thumb {
background: var(--color-border, #333);
border-radius: 4px;
transition: background 0.2s;
}
.plugin-publish-wizard ::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary, #858585);
}
.plugin-publish-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index-modal);
}
.plugin-publish-wizard {
background: var(--color-bg-primary, #1e1e1e);
border-radius: 12px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.plugin-publish-wizard.inline {
width: 100%;
max-width: none;
max-height: 100%;
height: 100%;
border-radius: 0;
box-shadow: none;
background: transparent;
overflow: hidden;
}
.plugin-publish-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--color-border, #333);
}
.plugin-publish-wizard.inline .plugin-publish-header {
padding: 16px 20px;
background: var(--color-bg-secondary, #252526);
}
.plugin-publish-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
}
.plugin-publish-close {
background: none;
border: none;
cursor: pointer;
color: var(--color-text-secondary, #858585);
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.plugin-publish-close:hover {
background: var(--color-bg-hover, #2d2d30);
color: var(--color-text-primary, #cccccc);
}
.plugin-publish-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.plugin-publish-wizard.inline .plugin-publish-content {
padding: 20px;
background: var(--color-bg-primary, #1e1e1e);
height: 0;
min-height: 0;
}
.publish-step {
display: flex;
flex-direction: column;
gap: 20px;
}
.publish-step h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
}
.github-auth {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.form-group label {
font-size: 13px;
font-weight: 500;
color: var(--color-text-primary, #cccccc);
}
.form-group input,
.form-group textarea,
.form-group select {
padding: 8px 12px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #cccccc);
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
border-color: var(--color-accent, #0e639c);
}
.form-group small {
font-size: 12px;
color: var(--color-text-secondary, #858585);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
font-family: inherit;
}
.btn-primary,
.btn-secondary,
.btn-link {
padding: 8px 16px;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn-primary {
background: var(--color-accent, #0e639c);
color: white;
border: none;
}
.btn-primary:hover {
background: var(--color-accent-hover, #1177bb);
}
.btn-secondary {
background: var(--color-bg-secondary, #252526);
color: var(--color-text-primary, #cccccc);
border: 1px solid var(--color-border, #333);
}
.btn-secondary:hover {
background: var(--color-bg-hover, #2d2d30);
}
.btn-link {
background: none;
color: var(--color-accent, #0e639c);
border: none;
padding: 6px 12px;
}
.btn-link:hover {
opacity: 0.8;
}
.button-group {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.error-message {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: rgba(255, 59, 48, 0.1);
border: 1px solid rgba(255, 59, 48, 0.3);
border-radius: 4px;
color: #ff3b30;
font-size: 13px;
}
.confirm-details {
background: var(--color-bg-secondary, #252526);
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-row {
display: flex;
gap: 12px;
}
.detail-label {
font-weight: 600;
color: var(--color-text-secondary, #858585);
min-width: 120px;
}
.detail-value {
color: var(--color-text-primary, #cccccc);
word-break: break-all;
}
.publish-step.publishing,
.publish-step.success,
.publish-step.error {
align-items: center;
text-align: center;
padding: 40px 20px;
}
.publish-step.publishing svg,
.publish-step.success svg,
.publish-step.error svg {
margin-bottom: 16px;
}
.review-message {
color: var(--color-text-secondary, #858585);
font-size: 13px;
line-height: 1.5;
max-width: 400px;
margin: 16px 0;
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* OAuth Authentication Styles */
.auth-tabs {
display: flex;
gap: 8px;
margin: 16px 0;
width: 100%;
}
.auth-tab {
flex: 1;
padding: 10px 16px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 6px;
color: var(--color-text-secondary, #858585);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.auth-tab:hover {
background: var(--color-bg-hover, #2d2d30);
border-color: var(--color-accent, #0e639c);
}
.auth-tab.active {
background: var(--color-accent, #0e639c);
border-color: var(--color-accent, #0e639c);
color: white;
}
.oauth-auth,
.token-auth {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
align-items: center;
}
.oauth-instructions {
text-align: left;
width: 100%;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border-radius: 8px;
border: 1px solid var(--color-border, #333);
}
.oauth-instructions p {
margin: 8px 0;
font-size: 13px;
color: var(--color-text-secondary, #858585);
line-height: 1.6;
}
.oauth-pending,
.oauth-success,
.oauth-error {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 32px 16px;
width: 100%;
}
.oauth-pending h4,
.oauth-success h4,
.oauth-error h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.user-code-display {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
max-width: 400px;
margin-top: 16px;
}
.user-code-display label {
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary, #858585);
}
.code-box {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border: 2px solid var(--color-accent, #0e639c);
border-radius: 8px;
}
.code-text {
flex: 1;
font-size: 24px;
font-weight: 700;
font-family: 'Courier New', monospace;
letter-spacing: 4px;
color: var(--color-accent, #0e639c);
text-align: center;
}
.btn-copy {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s;
}
.btn-copy:hover {
background: var(--color-bg-hover, #2d2d30);
}
.error-details {
width: 100%;
max-width: 500px;
max-height: 200px;
overflow-y: auto;
background: var(--color-bg-secondary, #252526);
border: 1px solid #ff3b30;
border-radius: 8px;
padding: 12px;
margin: 12px 0;
}
.error-details pre {
margin: 0;
font-size: 12px;
font-family: 'Courier New', monospace;
color: #ff3b30;
white-space: pre-wrap;
word-break: break-word;
}
/* 发布进度样式 */
.publish-progress {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 20px;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border-radius: 8px;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--color-bg-hover, #2d2d30);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007acc 0%, #4fc3f7 100%);
transition: width 0.3s ease;
}
.progress-message {
margin: 0;
font-size: 14px;
color: var(--color-text-primary, #cccccc);
text-align: center;
}
.progress-percent {
margin: 0;
font-size: 12px;
color: var(--color-text-secondary, #858585);
text-align: center;
font-family: monospace;
}
.build-log {
width: 100%;
max-height: 300px;
overflow-y: auto;
background: var(--color-bg-hover, #2d2d30);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
padding: 12px;
margin-top: 16px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.5;
}
.log-line {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
color: var(--color-text-primary, #cccccc);
word-break: break-all;
}
.log-line svg {
flex-shrink: 0;
}
/* 现有 PR 提示框 */
.existing-pr-notice {
display: flex;
gap: 12px;
padding: 16px;
margin: 16px 0;
background: rgba(244, 180, 0, 0.1);
border: 1px solid rgba(244, 180, 0, 0.3);
border-radius: 8px;
color: var(--color-text-primary, #cccccc);
}
.existing-pr-notice svg {
color: #f4b400;
flex-shrink: 0;
margin-top: 2px;
}
.existing-pr-notice .notice-content {
flex: 1;
}
.existing-pr-notice strong {
display: block;
margin-bottom: 8px;
color: #f4b400;
font-size: 14px;
}
.existing-pr-notice p {
margin: 0 0 12px 0;
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}
.existing-pr-notice .btn-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(74, 158, 255, 0.15);
border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 6px;
color: #4a9eff;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.existing-pr-notice .btn-link:hover {
background: rgba(74, 158, 255, 0.25);
border-color: #4a9eff;
}
/* 版本信息样式 */
.version-info {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
padding: 12px;
background: rgba(52, 199, 89, 0.1);
border: 1px solid rgba(52, 199, 89, 0.3);
border-radius: 6px;
}
.version-notice {
display: flex;
align-items: center;
gap: 8px;
color: #34c759;
font-size: 13px;
font-weight: 500;
}
.btn-version-suggest {
align-self: flex-start;
padding: 6px 12px;
background: rgba(14, 99, 156, 0.15);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 4px;
color: var(--color-accent, #0e639c);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-version-suggest:hover {
background: rgba(14, 99, 156, 0.25);
border-color: var(--color-accent, #0e639c);
transform: translateY(-1px);
}
.version-history {
margin-top: 8px;
padding: 12px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 6px;
}
.version-history summary {
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: var(--color-text-primary, #cccccc);
padding: 4px;
user-select: none;
}
.version-history summary:hover {
color: var(--color-accent, #0e639c);
}
.version-history ul {
list-style: none;
padding: 0;
margin: 12px 0 0 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.version-history li {
padding: 6px 12px;
background: var(--color-bg-primary, #1e1e1e);
border-radius: 4px;
font-size: 12px;
font-family: 'Consolas', 'Monaco', monospace;
color: var(--color-text-secondary, #858585);
}
/* 插件源选择样式 */
.source-type-selection {
display: flex;
flex-direction: column;
gap: 12px;
margin: 20px 0;
}
.source-type-btn {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 20px;
background: var(--color-bg-secondary, #252526);
border: 2px solid var(--color-border, #333);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
text-align: left;
width: 100%;
}
.source-type-btn:hover {
background: var(--color-bg-hover, #2d2d30);
border-color: var(--color-accent, #0e639c);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.source-type-btn.active {
background: rgba(14, 99, 156, 0.15);
border-color: var(--color-accent, #0e639c);
box-shadow: 0 0 0 3px rgba(14, 99, 156, 0.1);
}
.source-type-btn svg {
color: var(--color-accent, #0e639c);
flex-shrink: 0;
margin-top: 2px;
}
.source-type-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.source-type-info strong {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
}
.source-type-info p {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}
.selected-source {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
background: rgba(52, 199, 89, 0.1);
border: 1px solid rgba(52, 199, 89, 0.3);
border-radius: 8px;
margin-top: 16px;
}
.selected-source svg {
color: #34c759;
flex-shrink: 0;
margin-top: 2px;
}
.source-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.source-path {
font-size: 12px;
color: var(--color-text-secondary, #858585);
word-break: break-all;
font-family: 'Consolas', 'Monaco', monospace;
}
.source-name {
font-size: 14px;
font-weight: 600;
color: #34c759;
}
/* ZIP 文件要求说明 */
.zip-requirements-details {
margin-top: 20px;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
}
.zip-requirements-details summary {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
padding: 8px;
user-select: none;
list-style: none;
transition: color 0.2s;
}
.zip-requirements-details summary::-webkit-details-marker {
display: none;
}
.zip-requirements-details summary:hover {
color: var(--color-accent, #0e639c);
}
.zip-requirements-details summary svg {
color: #f4b400;
flex-shrink: 0;
}
.zip-requirements-details[open] summary {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border, #333);
}
.zip-requirements-content {
display: flex;
flex-direction: column;
gap: 20px;
padding: 4px 8px;
}
.requirement-section h4 {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 600;
color: var(--color-accent, #0e639c);
}
.requirement-section p {
margin: 0 0 12px 0;
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}
.requirement-section ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.requirement-section li {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
line-height: 1.5;
color: var(--color-text-primary, #cccccc);
}
.requirement-section li::before {
content: '✓';
color: #34c759;
font-weight: bold;
flex-shrink: 0;
}
.requirement-section code {
padding: 2px 6px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
color: #4ec9b0;
}
.build-script-example {
margin: 0;
padding: 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: var(--color-text-secondary, #858585);
overflow-x: auto;
}
.recommendation-notice {
padding: 12px 16px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 6px;
font-size: 13px;
line-height: 1.5;
color: var(--color-accent, #0e639c);
text-align: center;
}

View File

@@ -1,287 +0,0 @@
.plugin-update-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index-modal);
}
.plugin-update-dialog {
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.update-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border, #333);
}
.update-dialog-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.update-dialog-close {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: var(--color-text-secondary, #888);
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.update-dialog-close:hover {
background: var(--color-bg-hover, rgba(255, 255, 255, 0.1));
color: var(--color-text-primary, #fff);
}
.update-dialog-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.update-dialog-step {
display: flex;
flex-direction: column;
gap: 16px;
}
.update-dialog-step h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.step-description {
color: var(--color-text-secondary, #888);
margin: 0;
}
.current-plugin-info {
padding: 12px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 6px;
}
.current-plugin-info h4 {
margin: 0 0 8px 0;
font-size: 14px;
}
.current-plugin-info p {
margin: 0;
color: var(--color-text-secondary, #888);
}
.selected-folder-info {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--color-bg-secondary, #252525);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
font-size: 13px;
color: var(--color-text-secondary, #888);
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 13px;
font-weight: 500;
}
.form-group input,
.form-group textarea {
padding: 8px 12px;
background: var(--color-bg-secondary, #252525);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #fff);
font-size: 13px;
font-family: inherit;
}
.form-group textarea {
resize: vertical;
}
.version-input-group {
display: flex;
gap: 8px;
}
.version-input-group input {
flex: 1;
}
.btn-browse,
.btn-suggest,
.btn-view-pr,
.btn-close,
.btn-back,
.btn-primary {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.btn-browse {
background: var(--color-accent, #0e639c);
color: white;
}
.btn-browse:hover {
background: var(--color-accent-hover, #0d5a8c);
}
.btn-suggest {
background: rgba(14, 99, 156, 0.15);
color: var(--color-accent, #0e639c);
border: 1px solid rgba(14, 99, 156, 0.3);
white-space: nowrap;
}
.btn-suggest:hover {
background: rgba(14, 99, 156, 0.25);
}
.update-dialog-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 8px;
}
.btn-back {
background: var(--color-bg-secondary, #252525);
color: var(--color-text-primary, #fff);
border: 1px solid var(--color-border, #333);
}
.btn-back:hover {
background: var(--color-bg-hover, rgba(255, 255, 255, 0.1));
}
.btn-primary {
background: var(--color-accent, #0e639c);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-accent-hover, #0d5a8c);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.progress-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--color-bg-secondary, #252525);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-accent, #0e639c);
transition: width 0.3s ease;
}
.progress-message {
margin: 0;
font-size: 13px;
color: var(--color-text-secondary, #888);
}
.build-log {
margin-top: 12px;
padding: 12px;
background: var(--color-bg-secondary, #252525);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.log-line {
margin-bottom: 4px;
color: var(--color-text-secondary, #888);
}
.success-step,
.error-step {
align-items: center;
text-align: center;
padding: 20px;
}
.success-icon {
color: var(--color-success, #52c41a);
}
.error-icon {
color: var(--color-error, #ff4d4f);
}
.success-message,
.error-message {
margin: 16px 0;
color: var(--color-text-secondary, #888);
}
.btn-view-pr,
.btn-close {
background: var(--color-accent, #0e639c);
color: white;
margin-top: 8px;
}
.btn-view-pr:hover,
.btn-close:hover {
background: var(--color-accent-hover, #0d5a8c);
}

View File

@@ -1,278 +0,0 @@
.profiler-dock-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-elevated);
overflow: hidden;
}
.profiler-dock-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
border-bottom: 1px solid var(--color-border-default);
flex-shrink: 0;
height: 26px;
}
.profiler-dock-header h3 {
margin: 0;
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
}
.profiler-dock-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.profiler-dock-pause-btn,
.profiler-dock-details-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
background: var(--color-bg-inset);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.profiler-dock-pause-btn:hover,
.profiler-dock-details-btn:hover {
background: var(--color-bg-hover);
border-color: var(--color-border-strong);
color: var(--color-text-primary);
}
.profiler-dock-pause-btn:active,
.profiler-dock-details-btn:active {
transform: scale(0.95);
}
.profiler-dock-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
padding: 4px 8px;
border-radius: var(--radius-sm);
background: var(--color-bg-inset);
}
.status-text {
font-weight: 500;
}
.status-text.connected {
color: var(--color-success);
}
.status-text.waiting {
color: var(--color-warning);
}
.status-text.disconnected {
color: var(--color-text-tertiary);
}
.profiler-dock-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: 24px;
text-align: center;
color: var(--color-text-tertiary);
gap: 12px;
}
.profiler-dock-empty p {
margin: 0;
font-size: 13px;
}
.profiler-dock-empty .hint {
font-size: 11px;
opacity: 0.7;
}
.profiler-dock-content {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.profiler-dock-content::-webkit-scrollbar {
width: 6px;
}
.profiler-dock-content::-webkit-scrollbar-track {
background: transparent;
}
.profiler-dock-content::-webkit-scrollbar-thumb {
background: var(--color-border-default);
border-radius: 3px;
}
.profiler-dock-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--color-bg-inset);
border-radius: var(--radius-md);
border: 1px solid var(--color-border-default);
transition: all var(--transition-fast);
}
.stat-card:hover {
border-color: var(--color-border-strong);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
background: rgba(99, 102, 241, 0.1);
color: rgb(99, 102, 241);
flex-shrink: 0;
}
.stat-info {
flex: 1;
min-width: 0;
}
.stat-label {
font-size: 10px;
color: var(--color-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
margin-bottom: 2px;
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
}
.stat-value.warning {
color: var(--color-warning);
}
.profiler-dock-systems {
display: flex;
flex-direction: column;
gap: 12px;
}
.profiler-dock-systems h4 {
margin: 0;
font-size: 12px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.systems-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.system-item {
padding: 10px 12px;
background: var(--color-bg-inset);
border-radius: var(--radius-md);
border: 1px solid var(--color-border-default);
transition: all var(--transition-fast);
}
.system-item:hover {
border-color: var(--color-border-strong);
}
.system-item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.system-item-name {
font-size: 11px;
font-weight: 500;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.system-item-time {
font-size: 11px;
font-weight: 600;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
flex-shrink: 0;
margin-left: 8px;
}
.system-item-bar {
width: 100%;
height: 4px;
background: var(--color-bg-elevated);
border-radius: 2px;
overflow: hidden;
margin-bottom: 6px;
}
.system-item-bar-fill {
height: 100%;
transition: width 0.3s ease;
border-radius: 2px;
}
.system-item-footer {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 10px;
color: var(--color-text-tertiary);
}
.system-item-percentage {
font-weight: 600;
font-family: var(--font-family-mono);
}
.system-item-entities {
font-size: 9px;
}

View File

@@ -1,304 +0,0 @@
.profiler-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-base);
}
.profiler-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border-default);
flex-shrink: 0;
gap: 8px;
height: 26px;
}
.profiler-toolbar-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.profiler-toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.profiler-stats-summary {
display: flex;
align-items: center;
gap: 20px;
}
.summary-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--color-text-secondary);
}
.summary-item svg {
color: var(--color-primary);
}
.summary-label {
font-weight: 500;
}
.summary-value {
font-family: var(--font-family-mono);
font-weight: 600;
color: var(--color-text-primary);
}
.summary-value.over-budget {
color: var(--color-danger);
}
.summary-value.low-fps {
color: var(--color-warning);
}
.profiler-sort {
padding: 4px 8px;
background: var(--color-bg-inset);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
color: var(--color-text-primary);
font-size: 11px;
cursor: pointer;
outline: none;
transition: all var(--transition-fast);
}
.profiler-sort:hover {
border-color: var(--color-primary);
}
.profiler-sort:focus {
border-color: var(--color-primary);
}
.profiler-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px;
background: transparent;
border: none;
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.profiler-btn:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.profiler-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 12px;
}
.profiler-content::-webkit-scrollbar {
width: 8px;
}
.profiler-content::-webkit-scrollbar-track {
background: var(--color-bg-elevated);
}
.profiler-content::-webkit-scrollbar-thumb {
background: var(--color-border-default);
border-radius: 4px;
}
.profiler-content::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
.profiler-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-tertiary);
gap: 12px;
}
.profiler-empty p {
margin: 0;
font-size: 13px;
}
.profiler-empty-hint {
font-size: 11px !important;
opacity: 0.7;
}
.profiler-systems {
display: flex;
flex-direction: column;
gap: 12px;
}
.system-row {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
padding: 12px;
transition: all var(--transition-fast);
}
.system-row:hover {
border-color: var(--color-border-strong);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.system-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.system-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.system-rank {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 22px;
background: var(--color-bg-inset);
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 600;
font-family: var(--font-family-mono);
color: var(--color-text-secondary);
}
.system-name {
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
}
.system-entities {
font-size: 11px;
color: var(--color-text-tertiary);
}
.system-metrics {
display: flex;
align-items: center;
gap: 12px;
}
.metric-time {
font-size: 13px;
font-weight: 600;
font-family: var(--font-family-mono);
color: var(--color-text-primary);
}
.metric-percentage {
font-size: 12px;
font-family: var(--font-family-mono);
color: var(--color-text-secondary);
background: var(--color-bg-inset);
padding: 2px 6px;
border-radius: var(--radius-sm);
}
.system-bar {
width: 100%;
height: 6px;
background: var(--color-bg-inset);
border-radius: 3px;
overflow: hidden;
margin-bottom: 8px;
}
.system-bar-fill {
height: 100%;
transition: width 0.3s ease;
border-radius: 3px;
}
.system-stats {
display: flex;
align-items: center;
gap: 16px;
}
.stat-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
}
.stat-label {
color: var(--color-text-tertiary);
}
.stat-value {
font-family: var(--font-family-mono);
font-weight: 500;
color: var(--color-text-secondary);
}
.profiler-footer {
padding: 10px 12px;
background: var(--color-bg-elevated);
border-top: 1px solid var(--color-border-default);
flex-shrink: 0;
}
.profiler-legend {
display: flex;
align-items: center;
gap: 16px;
justify-content: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--color-text-secondary);
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
.system-row,
.system-bar-fill,
.profiler-btn {
transition: none;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,174 +0,0 @@
.user-profile {
position: relative;
display: flex;
align-items: center;
}
.login-button {
display: flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: transparent;
border: none;
border-radius: 2px;
color: #888;
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.1s;
}
.login-button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08);
color: #ccc;
}
.login-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-button .spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.user-avatar-button {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 6px 2px 2px;
background: transparent;
border: none;
border-radius: 2px;
color: #888;
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.1s;
}
.user-avatar-button:hover {
background: rgba(255, 255, 255, 0.08);
color: #ccc;
}
.user-avatar,
.user-avatar-placeholder {
width: 16px;
height: 16px;
border-radius: 50%;
}
.user-avatar {
object-fit: cover;
border: none;
}
.user-avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
color: #888;
border: none;
}
.user-name {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 220px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: var(--z-index-dropdown);
overflow: hidden;
}
.user-menu-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--color-bg-tertiary, #333);
}
.user-menu-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--color-accent, #0e639c);
}
.user-menu-info {
flex: 1;
min-width: 0;
}
.user-menu-name {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu-login {
font-size: 12px;
color: var(--color-text-secondary, #858585);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu-divider {
height: 1px;
background: var(--color-border, #333);
}
.user-menu-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 12px 16px;
background: transparent;
border: none;
color: var(--color-text-primary, #cccccc);
font-size: 13px;
cursor: pointer;
text-align: left;
transition: background 0.2s;
}
.user-menu-item:hover {
background: var(--color-bg-hover, #2d2d30);
}
.user-menu-item:last-child {
color: #ff3b30;
}
.user-menu-item:last-child:hover {
background: rgba(255, 59, 48, 0.1);
}