Dock系统,支持Tab和拖放
This commit is contained in:
@@ -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 (
|
||||
<div className="asset-browser">
|
||||
<div className="asset-browser-header">
|
||||
<h3>{t.title}</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||
<h3 style={{ margin: 0 }}>{t.title}</h3>
|
||||
{canGoBack && (
|
||||
<button
|
||||
onClick={handleBackToParent}
|
||||
className="back-button"
|
||||
title={t.backToParent}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
background: '#0e639c',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
↑
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="asset-path">{currentPath}</div>
|
||||
</div>
|
||||
|
||||
|
||||
165
packages/editor-app/src/components/DockContainer.tsx
Normal file
165
packages/editor-app/src/components/DockContainer.tsx
Normal file
@@ -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<DockPosition, DockablePanel[]>);
|
||||
|
||||
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 (
|
||||
<TabPanel
|
||||
tabs={tabs}
|
||||
onTabClose={onPanelClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 = (
|
||||
<div className="dock-center">
|
||||
{renderPanelGroup('center')}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (hasTop || hasBottom) {
|
||||
content = (
|
||||
<ResizablePanel
|
||||
direction="vertical"
|
||||
defaultSize={200}
|
||||
minSize={100}
|
||||
maxSize={400}
|
||||
leftOrTop={content}
|
||||
rightOrBottom={
|
||||
<div className="dock-bottom">
|
||||
{renderPanelGroup('bottom')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasTop) {
|
||||
content = (
|
||||
<ResizablePanel
|
||||
direction="vertical"
|
||||
defaultSize={200}
|
||||
minSize={100}
|
||||
maxSize={400}
|
||||
leftOrTop={
|
||||
<div className="dock-top">
|
||||
{renderPanelGroup('top')}
|
||||
</div>
|
||||
}
|
||||
rightOrBottom={content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasLeft || hasRight) {
|
||||
if (hasLeft && hasRight) {
|
||||
content = (
|
||||
<ResizablePanel
|
||||
direction="horizontal"
|
||||
defaultSize={250}
|
||||
minSize={150}
|
||||
maxSize={400}
|
||||
leftOrTop={
|
||||
<div className="dock-left">
|
||||
{renderPanelGroup('left')}
|
||||
</div>
|
||||
}
|
||||
rightOrBottom={
|
||||
<ResizablePanel
|
||||
direction="horizontal"
|
||||
side="right"
|
||||
defaultSize={280}
|
||||
minSize={200}
|
||||
maxSize={500}
|
||||
leftOrTop={content}
|
||||
rightOrBottom={
|
||||
<div className="dock-right">
|
||||
{renderPanelGroup('right')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (hasLeft) {
|
||||
content = (
|
||||
<ResizablePanel
|
||||
direction="horizontal"
|
||||
defaultSize={250}
|
||||
minSize={150}
|
||||
maxSize={400}
|
||||
leftOrTop={
|
||||
<div className="dock-left">
|
||||
{renderPanelGroup('left')}
|
||||
</div>
|
||||
}
|
||||
rightOrBottom={content}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<ResizablePanel
|
||||
direction="horizontal"
|
||||
side="right"
|
||||
defaultSize={280}
|
||||
minSize={200}
|
||||
maxSize={500}
|
||||
leftOrTop={content}
|
||||
rightOrBottom={
|
||||
<div className="dock-right">
|
||||
{renderPanelGroup('right')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="dock-container">{content}</div>;
|
||||
}
|
||||
62
packages/editor-app/src/components/TabPanel.tsx
Normal file
62
packages/editor-app/src/components/TabPanel.tsx
Normal file
@@ -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 (
|
||||
<div className="tab-panel">
|
||||
<div className="tab-header">
|
||||
<div className="tab-list">
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`tab-item ${activeTab === tab.id ? 'active' : ''}`}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
>
|
||||
<span className="tab-title">{tab.title}</span>
|
||||
{tab.closable && (
|
||||
<button
|
||||
className="tab-close"
|
||||
onClick={(e) => handleCloseTab(e, tab.id)}
|
||||
title="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
{currentTab?.content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user