From 00fc6dfd6706ff3108b592e0bf31f24bbe2787a1 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Wed, 15 Oct 2025 09:58:45 +0800 Subject: [PATCH] =?UTF-8?q?Dock=E7=B3=BB=E7=BB=9F,=E6=94=AF=E6=8C=81Tab?= =?UTF-8?q?=E5=92=8C=E6=8B=96=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/editor-app/src-tauri/src/main.rs | 2 +- packages/editor-app/src/App.tsx | 115 ++++++------ .../src/components/AssetBrowser.tsx | 45 ++++- .../src/components/DockContainer.tsx | 165 ++++++++++++++++++ .../editor-app/src/components/TabPanel.tsx | 62 +++++++ .../editor-app/src/styles/DockContainer.css | 33 ++++ packages/editor-app/src/styles/TabPanel.css | 88 ++++++++++ 7 files changed, 451 insertions(+), 59 deletions(-) create mode 100644 packages/editor-app/src/components/DockContainer.tsx create mode 100644 packages/editor-app/src/components/TabPanel.tsx create mode 100644 packages/editor-app/src/styles/DockContainer.css create mode 100644 packages/editor-app/src/styles/TabPanel.css diff --git a/packages/editor-app/src-tauri/src/main.rs b/packages/editor-app/src-tauri/src/main.rs index 8e488609..474db795 100644 --- a/packages/editor-app/src-tauri/src/main.rs +++ b/packages/editor-app/src-tauri/src/main.rs @@ -46,7 +46,7 @@ async fn open_project_dialog(app: AppHandle) -> Result, String> { #[tauri::command] fn scan_directory(path: String, pattern: String) -> Result, String> { use glob::glob; - use std::path::{Path, MAIN_SEPARATOR}; + use std::path::Path; let base_path = Path::new(&path); if !base_path.exists() { diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 17e27ba5..a834c2a4 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -6,7 +6,7 @@ import { StartupPage } from './components/StartupPage'; import { SceneHierarchy } from './components/SceneHierarchy'; import { EntityInspector } from './components/EntityInspector'; import { AssetBrowser } from './components/AssetBrowser'; -import { ResizablePanel } from './components/ResizablePanel'; +import { DockContainer, DockablePanel } from './components/DockContainer'; import { TauriAPI } from './api/tauri'; import { TransformComponent } from './example-components/TransformComponent'; import { SpriteComponent } from './example-components/SpriteComponent'; @@ -61,6 +61,7 @@ function App() { const [messageHub, setMessageHub] = useState(null); const { t, locale, changeLocale } = useLocale(); const [status, setStatus] = useState(t('header.status.initializing')); + const [panels, setPanels] = useState([]); useEffect(() => { const initializeEditor = async () => { @@ -199,6 +200,63 @@ function App() { changeLocale(newLocale); }; + useEffect(() => { + if (projectLoaded && entityStore && messageHub) { + const defaultPanels: DockablePanel[] = [ + { + id: 'scene-hierarchy', + title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', + position: 'left', + content: , + closable: false + }, + { + id: 'inspector', + title: locale === 'zh' ? '检视器' : 'Inspector', + position: 'right', + content: , + closable: false + }, + { + id: 'viewport', + title: locale === 'zh' ? '视口' : 'Viewport', + position: 'center', + content: ( +
+

{t('viewport.title')}

+

{t('viewport.placeholder')}

+
+ ), + closable: false + }, + { + id: 'assets', + title: locale === 'zh' ? '资产' : 'Assets', + position: 'bottom', + content: , + closable: false + }, + { + id: 'console', + title: locale === 'zh' ? '控制台' : 'Console', + position: 'bottom', + content: ( +
+

+ {locale === 'zh' ? '控制台' : 'Console'} +

+

+ {locale === 'zh' ? '控制台输出将显示在这里...' : 'Console output will appear here...'} +

+
+ ), + closable: false + } + ]; + setPanels(defaultPanels); + } + }, [projectLoaded, entityStore, messageHub, locale, currentProjectPath, t]); + if (!initialized) { return (
@@ -237,60 +295,7 @@ function App() {
- - {entityStore && messageHub ? ( - - ) : ( -
Loading...
- )} -
- } - rightOrBottom={ - -
-

{t('viewport.title')}

-

{t('viewport.placeholder')}

-
- - } - rightOrBottom={ -
- -
- } - /> - } - rightOrBottom={ -
- {entityStore && messageHub ? ( - - ) : ( -
Loading...
- )} -
- } - /> - } - /> +
diff --git a/packages/editor-app/src/components/AssetBrowser.tsx b/packages/editor-app/src/components/AssetBrowser.tsx index 8af28f95..f168344b 100644 --- a/packages/editor-app/src/components/AssetBrowser.tsx +++ b/packages/editor-app/src/components/AssetBrowser.tsx @@ -29,7 +29,8 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) { name: 'Name', type: 'Type', file: 'File', - folder: 'Folder' + folder: 'Folder', + backToParent: 'Back to parent folder' }, zh: { title: '资产', @@ -39,7 +40,8 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) { name: '名称', type: '类型', file: '文件', - folder: '文件夹' + folder: '文件夹', + backToParent: '返回上一级' } }; @@ -93,6 +95,17 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) { console.log('Open asset:', asset); }; + const handleBackToParent = () => { + if (!currentPath || !projectPath) return; + if (currentPath === projectPath) return; + + const parentPath = currentPath.split(/[\\/]/).slice(0, -1).join(currentPath.includes('\\') ? '\\' : '/'); + setCurrentPath(parentPath); + loadAssets(parentPath); + }; + + const canGoBack = currentPath && projectPath && currentPath !== projectPath; + const getFileIcon = (extension?: string) => { switch (extension?.toLowerCase()) { case 'ts': @@ -156,7 +169,33 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) { return (
-

{t.title}

+
+

{t.title}

+ {canGoBack && ( + + )} +
{currentPath}
diff --git a/packages/editor-app/src/components/DockContainer.tsx b/packages/editor-app/src/components/DockContainer.tsx new file mode 100644 index 00000000..6efe627f --- /dev/null +++ b/packages/editor-app/src/components/DockContainer.tsx @@ -0,0 +1,165 @@ +import { ReactNode } from 'react'; +import { TabPanel, TabItem } from './TabPanel'; +import { ResizablePanel } from './ResizablePanel'; +import '../styles/DockContainer.css'; + +export type DockPosition = 'left' | 'right' | 'top' | 'bottom' | 'center'; + +export interface DockablePanel { + id: string; + title: string; + content: ReactNode; + position: DockPosition; + closable?: boolean; +} + +interface DockContainerProps { + panels: DockablePanel[]; + onPanelClose?: (panelId: string) => void; + onPanelMove?: (panelId: string, newPosition: DockPosition) => void; +} + +export function DockContainer({ panels, onPanelClose }: DockContainerProps) { + const groupedPanels = panels.reduce((acc, panel) => { + if (!acc[panel.position]) { + acc[panel.position] = []; + } + acc[panel.position].push(panel); + return acc; + }, {} as Record); + + const renderPanelGroup = (position: DockPosition) => { + const positionPanels = groupedPanels[position]; + if (!positionPanels || positionPanels.length === 0) return null; + + const tabs: TabItem[] = positionPanels.map(panel => ({ + id: panel.id, + title: panel.title, + content: panel.content, + closable: panel.closable + })); + + return ( + + ); + }; + + const leftPanel = groupedPanels['left']; + const rightPanel = groupedPanels['right']; + const topPanel = groupedPanels['top']; + const bottomPanel = groupedPanels['bottom']; + + const hasLeft = leftPanel && leftPanel.length > 0; + const hasRight = rightPanel && rightPanel.length > 0; + const hasTop = topPanel && topPanel.length > 0; + const hasBottom = bottomPanel && bottomPanel.length > 0; + + let content = ( +
+ {renderPanelGroup('center')} +
+ ); + + if (hasTop || hasBottom) { + content = ( + + {renderPanelGroup('bottom')} +
+ } + /> + ); + } + + if (hasTop) { + content = ( + + {renderPanelGroup('top')} +
+ } + rightOrBottom={content} + /> + ); + } + + if (hasLeft || hasRight) { + if (hasLeft && hasRight) { + content = ( + + {renderPanelGroup('left')} + + } + rightOrBottom={ + + {renderPanelGroup('right')} + + } + /> + } + /> + ); + } else if (hasLeft) { + content = ( + + {renderPanelGroup('left')} + + } + rightOrBottom={content} + /> + ); + } else { + content = ( + + {renderPanelGroup('right')} + + } + /> + ); + } + } + + return
{content}
; +} diff --git a/packages/editor-app/src/components/TabPanel.tsx b/packages/editor-app/src/components/TabPanel.tsx new file mode 100644 index 00000000..f77af7cf --- /dev/null +++ b/packages/editor-app/src/components/TabPanel.tsx @@ -0,0 +1,62 @@ +import { useState, ReactNode } from 'react'; +import '../styles/TabPanel.css'; + +export interface TabItem { + id: string; + title: string; + content: ReactNode; + closable?: boolean; +} + +interface TabPanelProps { + tabs: TabItem[]; + activeTabId?: string; + onTabChange?: (tabId: string) => void; + onTabClose?: (tabId: string) => void; +} + +export function TabPanel({ tabs, activeTabId, onTabChange, onTabClose }: TabPanelProps) { + const [activeTab, setActiveTab] = useState(activeTabId || tabs[0]?.id); + + const handleTabClick = (tabId: string) => { + setActiveTab(tabId); + onTabChange?.(tabId); + }; + + const handleCloseTab = (e: React.MouseEvent, tabId: string) => { + e.stopPropagation(); + onTabClose?.(tabId); + }; + + const currentTab = tabs.find(tab => tab.id === activeTab); + + return ( +
+
+
+ {tabs.map(tab => ( +
handleTabClick(tab.id)} + > + {tab.title} + {tab.closable && ( + + )} +
+ ))} +
+
+
+ {currentTab?.content} +
+
+ ); +} diff --git a/packages/editor-app/src/styles/DockContainer.css b/packages/editor-app/src/styles/DockContainer.css new file mode 100644 index 00000000..021d631a --- /dev/null +++ b/packages/editor-app/src/styles/DockContainer.css @@ -0,0 +1,33 @@ +.dock-container { + width: 100%; + height: 100%; + overflow: hidden; +} + +.dock-left, +.dock-right, +.dock-top, +.dock-bottom, +.dock-center { + background: #1e1e1e; + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +.dock-left { + border-right: 1px solid #3e3e3e; +} + +.dock-right { + border-left: 1px solid #3e3e3e; +} + +.dock-top { + border-bottom: 1px solid #3e3e3e; +} + +.dock-bottom { + border-top: 1px solid #3e3e3e; +} diff --git a/packages/editor-app/src/styles/TabPanel.css b/packages/editor-app/src/styles/TabPanel.css new file mode 100644 index 00000000..aa77f165 --- /dev/null +++ b/packages/editor-app/src/styles/TabPanel.css @@ -0,0 +1,88 @@ +.tab-panel { + display: flex; + flex-direction: column; + height: 100%; + background: #1e1e1e; +} + +.tab-header { + background: #252526; + border-bottom: 1px solid #3e3e3e; + min-height: 35px; + display: flex; + align-items: flex-end; +} + +.tab-list { + display: flex; + gap: 0; + height: 100%; +} + +.tab-item { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: transparent; + color: #969696; + border-right: 1px solid #3e3e3e; + cursor: pointer; + transition: all 0.2s ease; + font-size: 13px; + user-select: none; + position: relative; +} + +.tab-item:hover { + background: #2a2d2e; + color: #cccccc; +} + +.tab-item.active { + background: #1e1e1e; + color: #ffffff; + border-bottom: 2px solid #007acc; +} + +.tab-item.active::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + right: 0; + height: 1px; + background: #1e1e1e; +} + +.tab-title { + white-space: nowrap; +} + +.tab-close { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + color: #858585; + cursor: pointer; + border-radius: 3px; + font-size: 18px; + line-height: 1; + padding: 0; + transition: all 0.2s ease; +} + +.tab-close:hover { + background: #4e4e4e; + color: #ffffff; +} + +.tab-content { + flex: 1; + overflow: hidden; + position: relative; +}