关于 flexlayout-react
flexlayout-react 是用于构建可定制停靠界面的 React 包。它结合标签页、可拖动面板、可调整大小的窗格及可保存布局,适合复杂应用界面。
主要功能
- 01拖放停靠面板
- 02支持窗口操作的标签面板
- 03可调整大小的窗格与边缘区域
- 04保存和恢复布局
- 05以代码控制标签与布局
- 06自定义样式并支持 TypeScript
适用场景
- 01构建 IDE 风格工作区
- 02创建可配置仪表板
- 03整理管理面板中的窗格
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill flexlayout-react安装后在 Claude Code 中输入/flexlayout-react确认已加载。
附带文件metadata.json
详细介绍
FlexLayout-React - 专业的停靠布局
概述
FlexLayout-React 提供 IDE 级别的停靠布局,支持拖放、标签页、分隔条和复杂的窗口管理。非常适合仪表板、IDE、管理面板,以及任何需要灵活且可由用户自定义布局的界面。
主要功能:
- 拖放重新定位面板
- 支持关闭、最大化和最小化的标签式界面
- 用于调整窗格大小的分隔条
- 边缘停靠区域
- 布局持久化(保存/恢复)
- 以编程方式控制布局
- 支持 TypeScript
安装:
npm install flexlayout-react
基本设置
1. 定义布局模型
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);
2. 创建布局组件
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>
);
}
3. 组件实现
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>
);
}
高级布局配置
复杂多窗格布局
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',
}
]
}
]
}
};
嵌套行和列
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' }
]
}
]
}
]
}
};
布局持久化
保存和恢复布局
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}
/>
);
}
重置为默认布局
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>
);
}
动态管理标签页
以编程方式添加标签页
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());
}
};
关闭标签页
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()));
}
});
}
});
}
标签页上下文和属性
向组件传递数据
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,
}
};
访问标签页状态
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>
);
}
样式和主题
自定义 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;
}
深色/浅色主题切换
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>
);
}
与 Tauri 集成
将布局持久化到 Tauri 后端
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(())
// }
窗口专属布局
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}
/>
);
}
高级模式
自定义标签页标题
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}
/>
);
}
标签页操作(自定义按钮)
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>
);
};
最佳实践
- 持久化布局 - 保存到 localStorage 或后端,提升用户体验
- 使用唯一的组件名称 - 避免在工厂函数中发生冲突
- 处理缺失的组件 - 工厂函数应包含默认分支
- 记忆化工厂函数 - 防止不必要的重新渲染
- 使用 config 存储标签页数据 - 在 config 中保存标签页专属属性
- 提供重置机制 - 让用户能够恢复默认布局
- 测试布局变更 - 验证持久化是否正常工作
- 处理边缘情况 - 空标签集、已删除的组件
- 合理使用边缘区域 - 左侧/右侧/顶部/底部用于工具,主区域用于内容
- 优化大型布局 - 尽可能延迟加载组件
常见问题
❌ 未对模型进行记忆化:
// 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} />;
}
❌ 忘记导入 CSS:
// WRONG - layout won't display correctly
import { Layout } from 'flexlayout-react';
// Missing: import 'flexlayout-react/style/dark.css';
❌ 未处理 onModelChange:
// WRONG - layout changes not persisted
<Layout model={model} factory={factory} />
// CORRECT
<Layout
model={model}
factory={factory}
onModelChange={saveLayout}
/>
资源
- 文档:https://github.com/caplin/FlexLayout
- 示例:https://rawgit.com/caplin/FlexLayout/demos/demos/index.html
- TypeScript 类型:包含在软件包中
总结
- FlexLayout 提供 IDE 级别的停靠布局
- 模型驱动 - 将布局定义为 JSON,并以编程方式控制
- 持久化 - 轻松保存/恢复用户布局
- 可自定义 - 自定义标签页、边缘区域和主题
- 适用于 React - 支持 Hooks 和 TypeScript
- 非常适合 - IDE、仪表板、管理面板和复杂 UI
- Tauri 集成 - 持久化到后端,支持窗口专属布局
相关技能
使用 FlexLayout-React 时,以下技能有助于提升工作流:
- react-state-machine:用于复杂 UI 逻辑、多步骤表单和异步流程的 XState v5 状态机与 actor 模型
- react-hooks-composition:用于数据获取和性能优化的高级 React Hooks 组合模式
- tanstack-query:用于 React 应用的服务端状态管理,支持缓存和重新获取
- zustand:轻量级客户端状态管理方案,可替代 Redux
- nextjs:支持 SSR、路由和全栈功能的 React 框架
- test-driven-development:适用于 React 组件和 Hooks 的 TDD 模式
[如果已部署在你的工具包中,可在这些技能中查看完整文档]
评论
支持 Markdown