Dock系统,支持Tab和拖放

This commit is contained in:
YHH
2025-10-15 09:58:45 +08:00
parent 82451e9fd3
commit 00fc6dfd67
7 changed files with 451 additions and 59 deletions

View File

@@ -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>

View 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>;
}

View 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>
);
}