flexlayout-react
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFlexLayout-React - Professional Docking Layouts
FlexLayout-React - 专业级停靠布局
Overview
概述
FlexLayout-React provides IDE-quality docking layouts with drag-and-drop, tabs, splitters, and complex window management. Perfect for dashboards, IDEs, admin panels, and any interface requiring flexible, user-customizable layouts.
Key Features:
- Drag-and-drop panel repositioning
- Tabbed interfaces with close, maximize, minimize
- Splitters for resizable panes
- Border docking areas
- Layout persistence (save/restore)
- Programmatic layout control
- TypeScript support
Installation:
bash
npm install flexlayout-reactFlexLayout-React提供IDE级别的停靠布局,支持拖拽、标签页、分割器和复杂窗口管理功能,非常适合仪表盘、IDE、管理面板以及任何需要灵活、可由用户自定义布局的界面。
核心特性:
- 面板拖拽重定位
- 带关闭、最大化、最小化功能的标签页界面
- 可调整大小的窗格分割器
- 边框停靠区域
- 布局持久化(保存/恢复)
- 程序化布局控制
- TypeScript支持
安装:
bash
npm install flexlayout-reactBasic Setup
基础设置
1. Define Layout Model
1. 定义布局模型
typescript
import { Model, IJsonModel } from 'flexlayout-react';
const initialLayout: IJsonModel = {
global: {
tabEnableClose: true,
tabEnableRename: false,
},
borders: [],
layout: {
type: 'row',
weight: 100,
children: [
{
type: 'tabset',
weight: 50,
children: [
{
type: 'tab',
name: 'Explorer',
component: 'explorer',
}
]
},
{
type: 'tabset',
weight: 50,
children: [
{
type: 'tab',
name: 'Editor',
component: 'editor',
}
]
}
]
}
};
// Create model
const model = Model.fromJson(initialLayout);typescript
import { Model, IJsonModel } from 'flexlayout-react';
const initialLayout: IJsonModel = {
global: {
tabEnableClose: true,
tabEnableRename: false,
},
borders: [],
layout: {
type: 'row',
weight: 100,
children: [
{
type: 'tabset',
weight: 50,
children: [
{
type: 'tab',
name: 'Explorer',
component: 'explorer',
}
]
},
{
type: 'tabset',
weight: 50,
children: [
{
type: 'tab',
name: 'Editor',
component: 'editor',
}
]
}
]
}
};
// 创建模型
const model = Model.fromJson(initialLayout);2. Create Layout Component
2. 创建布局组件
typescript
import React, { useRef } from 'react';
import { Layout, Model, TabNode, IJsonTabNode } from 'flexlayout-react';
import 'flexlayout-react/style/dark.css'; // or light.css
interface ComponentRegistry {
explorer: React.ComponentType;
editor: React.ComponentType;
terminal: React.ComponentType;
}
function App() {
const modelRef = useRef(Model.fromJson(initialLayout));
const factory = (node: TabNode) => {
const component = node.getComponent();
switch (component) {
case 'explorer':
return <ExplorerPanel />;
case 'editor':
return <EditorPanel />;
case 'terminal':
return <TerminalPanel />;
default:
return <div>Unknown component: {component}</div>;
}
};
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Layout
model={modelRef.current}
factory={factory}
/>
</div>
);
}typescript
import React, { useRef } from 'react';
import { Layout, Model, TabNode, IJsonTabNode } from 'flexlayout-react';
import 'flexlayout-react/style/dark.css'; // 或 light.css
interface ComponentRegistry {
explorer: React.ComponentType;
editor: React.ComponentType;
terminal: React.ComponentType;
}
function App() {
const modelRef = useRef(Model.fromJson(initialLayout));
const factory = (node: TabNode) => {
const component = node.getComponent();
switch (component) {
case 'explorer':
return <ExplorerPanel />;
case 'editor':
return <EditorPanel />;
case 'terminal':
return <TerminalPanel />;
default:
return <div>未知组件: {component}</div>;
}
};
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Layout
model={modelRef.current}
factory={factory}
/>
</div>
);
}3. Component Implementation
3. 组件实现
typescript
function ExplorerPanel() {
return (
<div className="panel-explorer">
<h3>File Explorer</h3>
<ul>
<li>src/</li>
<li>public/</li>
<li>package.json</li>
</ul>
</div>
);
}
function EditorPanel() {
return (
<div className="panel-editor">
<textarea
style={{ width: '100%', height: '100%' }}
placeholder="Start typing..."
/>
</div>
);
}typescript
function ExplorerPanel() {
return (
<div className="panel-explorer">
<h3>文件资源管理器</h3>
<ul>
<li>src/</li>
<li>public/</li>
<li>package.json</li>
</ul>
</div>
);
}
function EditorPanel() {
return (
<div className="panel-editor">
<textarea
style={{ width: '100%', height: '100%' }}
placeholder="开始输入..."
/>
</div>
);
}Advanced Layout Configurations
高级布局配置
Complex Multi-Pane Layout
复杂多窗格布局
typescript
const complexLayout: IJsonModel = {
global: {
tabEnableClose: true,
tabEnableRename: false,
tabEnableDrag: true,
tabEnableFloat: true,
borderSize: 200,
},
borders: [
{
type: 'border',
location: 'left',
size: 250,
children: [
{
type: 'tab',
name: 'Explorer',
component: 'explorer',
}
]
},
{
type: 'border',
location: 'bottom',
size: 200,
children: [
{
type: 'tab',
name: 'Terminal',
component: 'terminal',
},
{
type: 'tab',
name: 'Output',
component: 'output',
}
]
}
],
layout: {
type: 'row',
weight: 100,
children: [
{
type: 'tabset',
weight: 70,
children: [
{
type: 'tab',
name: 'Editor 1',
component: 'editor',
},
{
type: 'tab',
name: 'Editor 2',
component: 'editor',
}
]
},
{
type: 'tabset',
weight: 30,
children: [
{
type: 'tab',
name: 'Properties',
component: 'properties',
},
{
type: 'tab',
name: 'Outline',
component: 'outline',
}
]
}
]
}
};typescript
const complexLayout: IJsonModel = {
global: {
tabEnableClose: true,
tabEnableRename: false,
tabEnableDrag: true,
tabEnableFloat: true,
borderSize: 200,
},
borders: [
{
type: 'border',
location: 'left',
size: 250,
children: [
{
type: 'tab',
name: 'Explorer',
component: 'explorer',
}
]
},
{
type: 'border',
location: 'bottom',
size: 200,
children: [
{
type: 'tab',
name: 'Terminal',
component: 'terminal',
},
{
type: 'tab',
name: 'Output',
component: 'output',
}
]
}
],
layout: {
type: 'row',
weight: 100,
children: [
{
type: 'tabset',
weight: 70,
children: [
{
type: 'tab',
name: 'Editor 1',
component: 'editor',
},
{
type: 'tab',
name: 'Editor 2',
component: 'editor',
}
]
},
{
type: 'tabset',
weight: 30,
children: [
{
type: 'tab',
name: 'Properties',
component: 'properties',
},
{
type: 'tab',
name: 'Outline',
component: 'outline',
}
]
}
]
}
};Nested Rows and Columns
嵌套行和列
typescript
const nestedLayout: IJsonModel = {
global: {},
borders: [],
layout: {
type: 'row',
children: [
{
type: 'col',
weight: 50,
children: [
{
type: 'tabset',
weight: 70,
children: [
{ type: 'tab', name: 'Top Left', component: 'panel-a' }
]
},
{
type: 'tabset',
weight: 30,
children: [
{ type: 'tab', name: 'Bottom Left', component: 'panel-b' }
]
}
]
},
{
type: 'col',
weight: 50,
children: [
{
type: 'tabset',
weight: 30,
children: [
{ type: 'tab', name: 'Top Right', component: 'panel-c' }
]
},
{
type: 'tabset',
weight: 70,
children: [
{ type: 'tab', name: 'Bottom Right', component: 'panel-d' }
]
}
]
}
]
}
};typescript
const nestedLayout: IJsonModel = {
global: {},
borders: [],
layout: {
type: 'row',
children: [
{
type: 'col',
weight: 50,
children: [
{
type: 'tabset',
weight: 70,
children: [
{ type: 'tab', name: 'Top Left', component: 'panel-a' }
]
},
{
type: 'tabset',
weight: 30,
children: [
{ type: 'tab', name: 'Bottom Left', component: 'panel-b' }
]
}
]
},
{
type: 'col',
weight: 50,
children: [
{
type: 'tabset',
weight: 30,
children: [
{ type: 'tab', name: 'Top Right', component: 'panel-c' }
]
},
{
type: 'tabset',
weight: 70,
children: [
{ type: 'tab', name: 'Bottom Right', component: 'panel-d' }
]
}
]
}
]
}
};Layout Persistence
布局持久化
Save and Restore Layout
保存和恢复布局
typescript
import { useState, useEffect } from 'react';
import { Model, Actions } from 'flexlayout-react';
function LayoutManager() {
const [model, setModel] = useState(() => {
// Load from localStorage
const saved = localStorage.getItem('layout');
return saved
? Model.fromJson(JSON.parse(saved))
: Model.fromJson(defaultLayout);
});
// Save on model change
const onModelChange = (newModel: Model) => {
const json = newModel.toJson();
localStorage.setItem('layout', JSON.stringify(json));
};
return (
<Layout
model={model}
factory={factory}
onModelChange={onModelChange}
/>
);
}typescript
import { useState, useEffect } from 'react';
import { Model, Actions } from 'flexlayout-react';
function LayoutManager() {
const [model, setModel] = useState(() => {
// 从localStorage加载
const saved = localStorage.getItem('layout');
return saved
? Model.fromJson(JSON.parse(saved))
: Model.fromJson(defaultLayout);
});
// 模型变更时保存
const onModelChange = (newModel: Model) => {
const json = newModel.toJson();
localStorage.setItem('layout', JSON.stringify(json));
};
return (
<Layout
model={model}
factory={factory}
onModelChange={onModelChange}
/>
);
}Reset to Default Layout
重置为默认布局
typescript
function LayoutControls({ model }: { model: Model }) {
const resetLayout = () => {
const newModel = Model.fromJson(defaultLayout);
// Need to replace model reference
window.location.reload(); // Simple approach
};
const saveLayout = () => {
const json = model.toJson();
const blob = new Blob([JSON.stringify(json, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'layout.json';
a.click();
};
return (
<div className="layout-controls">
<button onClick={resetLayout}>Reset Layout</button>
<button onClick={saveLayout}>Export Layout</button>
</div>
);
}typescript
function LayoutControls({ model }: { model: Model }) {
const resetLayout = () => {
const newModel = Model.fromJson(defaultLayout);
// 需要替换模型引用
window.location.reload(); // 简单实现方式
};
const saveLayout = () => {
const json = model.toJson();
const blob = new Blob([JSON.stringify(json, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'layout.json';
a.click();
};
return (
<div className="layout-controls">
<button onClick={resetLayout}>重置布局</button>
<button onClick={saveLayout}>导出布局</button>
</div>
);
}Dynamic Tab Management
动态标签页管理
Adding Tabs Programmatically
程序化添加标签页
typescript
import { Actions, DockLocation } from 'flexlayout-react';
function addNewTab(model: Model, tabsetId: string) {
model.doAction(Actions.addNode(
{
type: 'tab',
name: `New Tab ${Date.now()}`,
component: 'editor',
},
tabsetId,
DockLocation.CENTER,
-1
));
}
// Add to specific tabset
const addToExplorer = () => {
addNewTab(model, 'explorer-tabset-id');
};
// Add to active tabset
const addToActive = () => {
const activeTabset = model.getActiveTabset();
if (activeTabset) {
addNewTab(model, activeTabset.getId());
}
};typescript
import { Actions, DockLocation } from 'flexlayout-react';
function addNewTab(model: Model, tabsetId: string) {
model.doAction(Actions.addNode(
{
type: 'tab',
name: `New Tab ${Date.now()}`,
component: 'editor',
},
tabsetId,
DockLocation.CENTER,
-1
));
}
// 添加到指定标签组
const addToExplorer = () => {
addNewTab(model, 'explorer-tabset-id');
};
// 添加到当前激活的标签组
const addToActive = () => {
const activeTabset = model.getActiveTabset();
if (activeTabset) {
addNewTab(model, activeTabset.getId());
}
};Closing Tabs
关闭标签页
typescript
function closeTab(model: Model, tabId: string) {
model.doAction(Actions.deleteTab(tabId));
}
function closeAllTabs(model: Model) {
const tabsets = model.getRoot().getChildren();
tabsets.forEach(tabset => {
if (tabset.getType() === 'tabset') {
const tabs = tabset.getChildren();
tabs.forEach(tab => {
if (tab.getType() === 'tab') {
model.doAction(Actions.deleteTab(tab.getId()));
}
});
}
});
}typescript
function closeTab(model: Model, tabId: string) {
model.doAction(Actions.deleteTab(tabId));
}
function closeAllTabs(model: Model) {
const tabsets = model.getRoot().getChildren();
tabsets.forEach(tabset => {
if (tabset.getType() === 'tabset') {
const tabs = tabset.getChildren();
tabs.forEach(tab => {
if (tab.getType() === 'tab') {
model.doAction(Actions.deleteTab(tab.getId()));
}
});
}
});
}Tab Context and Props
标签页上下文与属性
Passing Data to Components
向组件传递数据
typescript
interface EditorTabProps {
node: TabNode;
}
function EditorTab({ node }: EditorTabProps) {
const filepath = node.getConfig()?.filepath as string;
const readonly = node.getConfig()?.readonly as boolean;
return (
<div>
<p>Editing: {filepath}</p>
<textarea readOnly={readonly} />
</div>
);
}
// Factory with data passing
const factory = (node: TabNode) => {
const component = node.getComponent();
switch (component) {
case 'editor':
return <EditorTab node={node} />;
default:
return <div>Unknown</div>;
}
};
// Create tab with config
const newTab: IJsonTabNode = {
type: 'tab',
name: 'my-file.ts',
component: 'editor',
config: {
filepath: '/src/my-file.ts',
readonly: false,
}
};typescript
interface EditorTabProps {
node: TabNode;
}
function EditorTab({ node }: EditorTabProps) {
const filepath = node.getConfig()?.filepath as string;
const readonly = node.getConfig()?.readonly as boolean;
return (
<div>
<p>正在编辑: {filepath}</p>
<textarea readOnly={readonly} />
</div>
);
}
// 带数据传递的工厂函数
const factory = (node: TabNode) => {
const component = node.getComponent();
switch (component) {
case 'editor':
return <EditorTab node={node} />;
default:
return <div>未知组件</div>;
}
};
// 创建带配置的标签页
const newTab: IJsonTabNode = {
type: 'tab',
name: 'my-file.ts',
component: 'editor',
config: {
filepath: '/src/my-file.ts',
readonly: false,
}
};Accessing Tab State
访问标签页状态
typescript
function SmartPanel({ node }: { node: TabNode }) {
const name = node.getName();
const isActive = node.isSelected();
const isVisible = node.isVisible();
return (
<div className={isActive ? 'active' : 'inactive'}>
<h3>{name}</h3>
{isVisible && <p>This tab is visible</p>}
</div>
);
}typescript
function SmartPanel({ node }: { node: TabNode }) {
const name = node.getName();
const isActive = node.isSelected();
const isVisible = node.isVisible();
return (
<div className={isActive ? 'active' : 'inactive'}>
<h3>{name}</h3>
{isVisible && <p>此标签页可见</p>}
</div>
);
}Styling and Theming
样式与主题
Custom CSS
自定义CSS
css
/* Override FlexLayout styles */
.flexlayout__layout {
background: #1e1e1e;
}
.flexlayout__tab {
background: #2d2d2d;
color: #cccccc;
}
.flexlayout__tab:hover {
background: #3e3e3e;
}
.flexlayout__tab_button--selected {
background: #1e1e1e;
border-bottom: 2px solid #007acc;
}
.flexlayout__splitter {
background: #2d2d2d;
}
.flexlayout__splitter:hover {
background: #007acc;
}css
/* 覆盖FlexLayout样式 */
.flexlayout__layout {
background: #1e1e1e;
}
.flexlayout__tab {
background: #2d2d2d;
color: #cccccc;
}
.flexlayout__tab:hover {
background: #3e3e3e;
}
.flexlayout__tab_button--selected {
background: #1e1e1e;
border-bottom: 2px solid #007acc;
}
.flexlayout__splitter {
background: #2d2d2d;
}
.flexlayout__splitter:hover {
background: #007acc;
}Dark/Light Theme Toggle
深色/浅色主题切换
typescript
import 'flexlayout-react/style/dark.css';
// or
import 'flexlayout-react/style/light.css';
function ThemeToggle() {
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
useEffect(() => {
// Dynamically load theme
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `flexlayout-react/style/${theme}.css`;
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}, [theme]);
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle Theme
</button>
);
}typescript
import 'flexlayout-react/style/dark.css';
// 或
import 'flexlayout-react/style/light.css';
function ThemeToggle() {
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
useEffect(() => {
// 动态加载主题
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `flexlayout-react/style/${theme}.css`;
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}, [theme]);
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
切换主题
</button>
);
}Integration with Tauri
与Tauri集成
Persisting Layout to Tauri Backend
将布局持久化到Tauri后端
typescript
import { invoke } from '@tauri-apps/api/core';
async function saveLayoutToTauri(model: Model) {
const json = model.toJson();
await invoke('save_layout', {
layout: JSON.stringify(json)
});
}
async function loadLayoutFromTauri(): Promise<Model> {
const layout = await invoke<string>('load_layout');
return Model.fromJson(JSON.parse(layout));
}
// Tauri command (Rust)
// #[tauri::command]
// async fn save_layout(layout: String) -> Result<(), String> {
// let app_dir = app.path_resolver().app_data_dir()?;
// let layout_file = app_dir.join("layout.json");
// tokio::fs::write(layout_file, layout).await?;
// Ok(())
// }typescript
import { invoke } from '@tauri-apps/api/core';
async function saveLayoutToTauri(model: Model) {
const json = model.toJson();
await invoke('save_layout', {
layout: JSON.stringify(json)
});
}
async function loadLayoutFromTauri(): Promise<Model> {
const layout = await invoke<string>('load_layout');
return Model.fromJson(JSON.parse(layout));
}
// Tauri命令(Rust)
// #[tauri::command]
// async fn save_layout(layout: String) -> Result<(), String> {
// let app_dir = app.path_resolver().app_data_dir()?;
// let layout_file = app_dir.join("layout.json");
// tokio::fs::write(layout_file, layout).await?;
// Ok(())
// }Window-Specific Layouts
窗口专属布局
typescript
import { invoke } from '@tauri-apps/api/core';
import { getCurrent } from '@tauri-apps/api/window';
function WindowLayout() {
const [model, setModel] = useState<Model | null>(null);
useEffect(() => {
const currentWindow = getCurrent();
const windowLabel = currentWindow.label;
// Load layout for this specific window
invoke<string>('load_window_layout', { windowLabel })
.then(layout => {
setModel(Model.fromJson(JSON.parse(layout)));
})
.catch(() => {
setModel(Model.fromJson(defaultLayout));
});
}, []);
const onModelChange = (newModel: Model) => {
const currentWindow = getCurrent();
const json = newModel.toJson();
invoke('save_window_layout', {
windowLabel: currentWindow.label,
layout: JSON.stringify(json)
});
};
if (!model) return <div>Loading...</div>;
return (
<Layout
model={model}
factory={factory}
onModelChange={onModelChange}
/>
);
}typescript
import { invoke } from '@tauri-apps/api/core';
import { getCurrent } from '@tauri-apps/api/window';
function WindowLayout() {
const [model, setModel] = useState<Model | null>(null);
useEffect(() => {
const currentWindow = getCurrent();
const windowLabel = currentWindow.label;
// 加载当前窗口的专属布局
invoke<string>('load_window_layout', { windowLabel })
.then(layout => {
setModel(Model.fromJson(JSON.parse(layout)));
})
.catch(() => {
setModel(Model.fromJson(defaultLayout));
});
}, []);
const onModelChange = (newModel: Model) => {
const currentWindow = getCurrent();
const json = newModel.toJson();
invoke('save_window_layout', {
windowLabel: currentWindow.label,
layout: JSON.stringify(json)
});
};
if (!model) return <div>加载中...</div>;
return (
<Layout
model={model}
factory={factory}
onModelChange={onModelChange}
/>
);
}Advanced Patterns
高级模式
Custom Tab Headers
自定义标签页头部
typescript
import { Layout, Model, TabNode, ITabRenderValues } from 'flexlayout-react';
function App() {
const onRenderTab = (
node: TabNode,
renderValues: ITabRenderValues
) => {
const modified = node.getConfig()?.modified as boolean;
renderValues.content = (
<div className="custom-tab-header">
<span>{node.getName()}</span>
{modified && <span className="modified-indicator">●</span>}
</div>
);
};
return (
<Layout
model={model}
factory={factory}
onRenderTab={onRenderTab}
/>
);
}typescript
import { Layout, Model, TabNode, ITabRenderValues } from 'flexlayout-react';
function App() {
const onRenderTab = (
node: TabNode,
renderValues: ITabRenderValues
) => {
const modified = node.getConfig()?.modified as boolean;
renderValues.content = (
<div className="custom-tab-header">
<span>{node.getName()}</span>
{modified && <span className="modified-indicator">●</span>}
</div>
);
};
return (
<Layout
model={model}
factory={factory}
onRenderTab={onRenderTab}
/>
);
}Tab Actions (Custom Buttons)
标签页操作(自定义按钮)
typescript
const onRenderTab = (node: TabNode, renderValues: ITabRenderValues) => {
renderValues.buttons.push(
<button
key="save"
onClick={() => saveTabContent(node)}
title="Save"
>
💾
</button>
);
renderValues.buttons.push(
<button
key="duplicate"
onClick={() => duplicateTab(node)}
title="Duplicate"
>
📋
</button>
);
};typescript
const onRenderTab = (node: TabNode, renderValues: ITabRenderValues) => {
renderValues.buttons.push(
<button
key="save"
onClick={() => saveTabContent(node)}
title="保存"
>
💾
</button>
);
renderValues.buttons.push(
<button
key="duplicate"
onClick={() => duplicateTab(node)}
title="复制"
>
📋
</button>
);
};Best Practices
最佳实践
- Persist layouts - Save to localStorage or backend for user experience
- Use unique component names - Avoid collisions in factory function
- Handle missing components - Factory should have default case
- Memoize factory function - Prevent unnecessary re-renders
- Use config for tab data - Store tab-specific props in config
- Provide reset mechanism - Users can restore default layout
- Test layout changes - Verify persistence works correctly
- Handle edge cases - Empty tabsets, deleted components
- Use borders wisely - Left/right/top/bottom for tools, main area for content
- Optimize large layouts - Lazy-load components when possible
- 持久化布局 - 保存到localStorage或后端以提升用户体验
- 使用唯一组件名称 - 避免工厂函数中的命名冲突
- 处理缺失组件 - 工厂函数应包含默认分支
- 记忆化工厂函数 - 防止不必要的重渲染
- 使用配置存储标签页数据 - 将标签页专属属性存储在配置中
- 提供重置机制 - 允许用户恢复默认布局
- 测试布局变更 - 验证持久化功能是否正常工作
- 处理边缘情况 - 空标签组、已删除组件等场景
- 合理使用边框区域 - 左右上下边框用于工具,主区域用于内容
- 优化大型布局 - 尽可能懒加载组件
Common Pitfalls
常见陷阱
❌ Not memoizing model:
typescript
// WRONG - creates new model on every render
function App() {
const model = Model.fromJson(layout); // Bad!
return <Layout model={model} />;
}
// CORRECT
function App() {
const modelRef = useRef(Model.fromJson(layout));
return <Layout model={modelRef.current} />;
}❌ Forgetting CSS import:
typescript
// WRONG - layout won't display correctly
import { Layout } from 'flexlayout-react';
// Missing: import 'flexlayout-react/style/dark.css';❌ Not handling onModelChange:
typescript
// WRONG - layout changes not persisted
<Layout model={model} factory={factory} />
// CORRECT
<Layout
model={model}
factory={factory}
onModelChange={saveLayout}
/>❌ 未记忆化模型:
typescript
// 错误 - 每次渲染都会创建新模型
function App() {
const model = Model.fromJson(layout); // 不好的写法!
return <Layout model={model} />;
}
// 正确写法
function App() {
const modelRef = useRef(Model.fromJson(layout));
return <Layout model={modelRef.current} />;
}❌ 忘记导入CSS:
typescript
// 错误 - 布局无法正确显示
import { Layout } from 'flexlayout-react';
// 缺失: import 'flexlayout-react/style/dark.css';❌ 未处理onModelChange:
typescript
// 错误 - 布局变更不会被持久化
<Layout model={model} factory={factory} />
// 正确写法
<Layout
model={model}
factory={factory}
onModelChange={saveLayout}
/>Resources
资源
- Documentation: https://github.com/caplin/FlexLayout
- Examples: https://rawgit.com/caplin/FlexLayout/demos/demos/index.html
- TypeScript Types: Included in package
- 文档: https://github.com/caplin/FlexLayout
- 示例: https://rawgit.com/caplin/FlexLayout/demos/demos/index.html
- TypeScript类型: 包含在包内
Related Sub-Skills
相关子技能
- state-machine: XState v5 state machines and actor model for complex UI logic, multi-step forms, async flows
- state-machine: 用于复杂UI逻辑、多步骤表单和异步流程的XState v5状态机和参与者模型
Summary
总结
- FlexLayout provides IDE-quality docking layouts
- Model-driven - Define layout as JSON, control programmatically
- Persistent - Save/restore user layouts easily
- Customizable - Custom tabs, borders, themes
- React-friendly - Hooks, TypeScript support
- Perfect for - IDEs, dashboards, admin panels, complex UIs
- Tauri integration - Persist to backend, window-specific layouts
- FlexLayout 提供IDE级别的停靠布局
- 模型驱动 - 以JSON定义布局,支持程序化控制
- 可持久化 - 轻松保存/恢复用户布局
- 可自定义 - 自定义标签页、边框、主题
- React友好 - 支持Hooks、TypeScript
- 适用场景 - IDE、仪表盘、管理面板、复杂UI
- Tauri集成 - 持久化到后端、窗口专属布局
Related Skills
相关技能
When using React, these skills enhance your workflow:
- tanstack-query: Server-state management for React apps with caching and refetching
- zustand: Lightweight client-state management alternative to Redux
- nextjs: React framework with SSR, routing, and full-stack capabilities
- test-driven-development: TDD patterns for React components and hooks
[Full documentation available in these skills if deployed in your bundle]
使用React时,以下技能可提升你的工作流:
- tanstack-query: 用于React应用的服务端状态管理,具备缓存和重新获取功能
- zustand: 轻量级客户端状态管理方案,可替代Redux
- nextjs: 具备SSR、路由和全栈能力的React框架
- test-driven-development: 用于React组件和Hooks的TDD模式
[如果已部署到你的技能包中,可在这些技能中查看完整文档]