mcpField
F
flexlayout-react

flexlayout-react

github.com/bobmatnyc设计与 UI78332 次安装15 周前更新
代码仓库

关于 flexlayout-react

flexlayout-react 是用于构建可定制停靠界面的 React 包。它结合标签页、可拖动面板、可调整大小的窗格及可保存布局,适合复杂应用界面。

主要功能

  1. 01拖放停靠面板
  2. 02支持窗口操作的标签面板
  3. 03可调整大小的窗格与边缘区域
  4. 04保存和恢复布局
  5. 05以代码控制标签与布局
  6. 06自定义样式并支持 TypeScript

适用场景

  1. 01构建 IDE 风格工作区
  2. 02创建可配置仪表板
  3. 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>
    );
};

最佳实践

  1. 持久化布局 - 保存到 localStorage 或后端,提升用户体验
  2. 使用唯一的组件名称 - 避免在工厂函数中发生冲突
  3. 处理缺失的组件 - 工厂函数应包含默认分支
  4. 记忆化工厂函数 - 防止不必要的重新渲染
  5. 使用 config 存储标签页数据 - 在 config 中保存标签页专属属性
  6. 提供重置机制 - 让用户能够恢复默认布局
  7. 测试布局变更 - 验证持久化是否正常工作
  8. 处理边缘情况 - 空标签集、已删除的组件
  9. 合理使用边缘区域 - 左侧/右侧/顶部/底部用于工具,主区域用于内容
  10. 优化大型布局 - 尽可能延迟加载组件

常见问题

❌ 未对模型进行记忆化:

// 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}
/>

资源

总结

  • 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