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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './commands';
|
||||
export * from './state';
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}));
|
||||
@@ -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
|
||||
})
|
||||
}));
|
||||
@@ -1,2 +0,0 @@
|
||||
export { useUIStore } from './UIStore';
|
||||
export { useEditorStore } from './EditorStore';
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 (<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 (>16ms)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (<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 (>16ms)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export { AssetField } from './AssetField';
|
||||
export { CollisionLayerField } from './CollisionLayerField';
|
||||
export { EntityRefField } from './EntityRefField';
|
||||
export { TransformRow, RotationRow, MobilityRow, TransformSection } from './TransformField';
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
178
packages/editor-app/src/contexts/EditorServicesContext.tsx
Normal file
178
packages/editor-app/src/contexts/EditorServicesContext.tsx
Normal 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 };
|
||||
20
packages/editor-app/src/contexts/index.ts
Normal file
20
packages/editor-app/src/contexts/index.ts
Normal 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
60
packages/editor-app/src/global.d.ts
vendored
Normal 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 {};
|
||||
314
packages/editor-app/src/hooks/useProjectActions.ts
Normal file
314
packages/editor-app/src/hooks/useProjectActions.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
187
packages/editor-app/src/hooks/useSceneActions.ts
Normal file
187
packages/editor-app/src/hooks/useSceneActions.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()) {
|
||||
// 清理全局容器中的插件
|
||||
|
||||
@@ -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__`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user