# 📝如何将思维导图导出导入为 XMind 文件

Xmind 文件本质上是一个 ZIP 压缩包。其中包含了多个 JSON 文件 (如 content.jsonmetadata.json 等),这些文件描述了思维导图的内容和结构

# 1. 🛠️ 准备工作

  • JSZip: 用于创建 ZIP 文件。允许我们轻松创建符合 XMind 规范的 ZIP 文件
  • file-saver: 用于保存生成的文件。这个库简化了浏览器环境下的文件下载过程。

# 2. 🔍 定义数据结构

# Node 接口

interface Node {
    id: string; // 节点 ID
    text: string; // 节点文本
    position: [number, number]; // 节点位置
    children: string[]; // 子节点 ID 列表
    size: [number, number]; // 节点尺寸
    collapsed: boolean; // 折叠状态
    direction?: 'left' | 'right' | 'none'; // 方向属性
  }

清晰的表示每个节点及其关系

# Topic 接口

// 定义思维导图主题类型
  interface Topic {
    id: string;
    structureClass: string;
    title: string;
    children?: {
      attached: Topic[]; // 子节点
    };
  }

Topic 是 XMind 中的主体 (节点) 模型,它不仅包含节点的基本属性,还允许递归地定义子节点。能够构建复杂的树状结构

# Sheet 接口

// 定义思维导图工作表类型
  interface Sheet {
    id: string;
    class: string;
    title: string;
    extensions: unknown[];
    topicPositioning: string;
    topicOverlapping: string;
    coreVersion: string;
    rootTopic: Topic;
  }

Sheet 是 XMind 中的工作表模型,它包含一个根主题,以及各种属性,如标题、扩展、主题定位、主题重叠等。有助于我们组织整个思维导图的数据结构。

# ContentJSON , MetadataJSON , ManifestJSON 类型

// 定义 content.json 的类型
  type ContentJSON = Sheet[];
  
  // 定义 metadata.json 的类型
  interface MetadataJSON {
    modifier: string;
    dataStructureVersion: string;
    creator: { name: string };
    layoutEngineVersion: string;
    activeSheetId: string;
  }
  
  // 定义 manifest.json 的类型
  interface ManifestJSON {
    "file-entries": {
      [filename: string]: { "media-type": string };
    };
  }

这些事 XMind 文件中的核心 JSON 文件, ContentJSON 包含了思维导图的所有内容; MetadataJSON 提供了元数据信息,如版本号和创建者; ManifestJSON 描述了文件清单,指定了每个文件的名称和媒体类型。这些文件共同构成了一个完整的 XMind 文件。

# 3. 🧩 辅助函数

我们将编写辅助函数来构建思维导图的内容

# buildChildren 函数

递归的构建子节点树状结构, 将每个节点转换为 Topic 对象。

// 辅助函数:递归构建子节点
const buildChildren = (nodes: Record<string, Node>, parentId: string): Topic[] => {
    console.log(`Processing parentId: ${parentId}`);
    if (!nodes[parentId]) {
        console.warn(`Node with id "${parentId}" not found`);
        return [];
    }
    const childrenIds = nodes[parentId].children; // 获取当前父节点的子节点 ID 列表
    console.log(`Children IDs for parentId "${parentId}":`, childrenIds);
    // 根据子节点 ID 构建子节点列表
    return childrenIds
        .map((childId): Topic | null => {
            const childNode = nodes[childId];
            if (!childNode) {
                console.warn(`Child node with id "${childId}" not found`);
                return null;
            }
            return {
                id: childNode.id,
                structureClass: 'org.xmind.ui.logic.right',
                title: childNode.text,
                children: {
                    attached: buildChildren(nodes, childNode.id), // 递归处理子节点
                },
            };
        })
        .filter((topic): topic is Topic => !!topic); // 过滤掉无效的子节点
};

# createContentJSON 函数

用于构建 content.json 文件,该文件包含了整个思维导图的所有节点信息,需要确保所有节点都正确转换成 XMind 格式

// 辅助函数:构建 content.json
const createContentJSON = (): ContentJSON => {
    const { nodes } = useMindmapStore.getState();
    // 构建根节点
    const rootNode = nodes['root'];
    if (!rootNode) {
        throw new Error('未找到根节点');
    }
    const rootTopic: Topic = {
        id: 'root',
        structureClass: 'org.xmind.ui.logic.right',
        title: rootNode.text,
        children: {
            attached: buildChildren(nodes, 'root'), // 从根节点开始递归
        },
    };
    return [
        {
            id: 'simpleMindMap_1744799393059',
            class: 'sheet',
            title: '思维导图标题',
            extensions: [],
            topicPositioning: 'fixed',
            topicOverlapping: 'overlap',
            coreVersion: '2.100.0',
            rootTopic,
        },
    ];
};

# createMetadataJSON 函数

用于构建 metadata.json 文件,该文件包含了思维导图的元数据信息,如版本号、创建者等。

// 辅助函数:构建 metadata.json
const createMetadataJSON = (): MetadataJSON => {
    return {
        modifier: '', // 修改者(可为空)
        dataStructureVersion: '2', // 数据结构版本
        creator: {
            name: 'mind-map', // 创建者名称
        },
        layoutEngineVersion: '3', // 布局引擎版本
        activeSheetId: 'simpleMindMap_1744799393059', // 当前活动的工作表 ID
    };
};

# createManifestJSON 函数

用于构建 manifest.json 文件,该文件描述了文件清单,指定了每个文件的名称和媒体类型。这对解压和解析 XMind 文件很重要。

// 辅助函数:构建 manifest.json
const createManifestJSON = (): ManifestJSON => {
    return {
        "file-entries": {
            "content.json": { "media-type": "application/json" }, //content.json 的媒体类型
            "metadata.json": { "media-type": "application/json" }, //metadata.json 的媒体类型
            "manifest.json": { "media-type": "application/json" }, //manifest.json 的媒体类型
        },
    };
};

# 4. 🚀 导出 XMind 文件

最后,我们将使用这些内容打包成 .xmind 文件下载。
通过 JSZip 库,我们可以将多个 JSON 文件打包成一个 ZIP 文件。模拟 XMind 文件的内部结构。生成的文件可以直接被 XMind 应用程序打开。

// 导出为 XMind 文件
export const exportAsXMind = (): void => {
    try {
        // 获取节点数据
        const { nodes } = useMindmapStore.getState();
        // 检查 nodes 是否为空
        if (!nodes || Object.keys(nodes).length === 0) {
            console.error('没有节点可以导出');
            return;
        }
        // 构建 content.json
        const contentJson: ContentJSON = createContentJSON();
        // 构建 metadata.json
        const metadataJson: MetadataJSON = createMetadataJSON();
        // 构建 manifest.json
        const manifestJson: ManifestJSON = createManifestJSON();
        // 使用 JSZip 打包
        const zip = new JSZip();
        zip.file('content.json', JSON.stringify(contentJson));
        zip.file('metadata.json', JSON.stringify(metadataJson));
        zip.file('manifest.json', JSON.stringify(manifestJson));
        // 生成并下载文件
        zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }).then((blob) => {
            saveAs(blob, 'mindmap.xmind');
        });
    } catch (error) {
        console.error('导出失败:', error);
    }
};

# 🎉

通过以上步骤,我们可以轻松地将思维导图数据导出为 XMind 格式的文件。


# ✨更新:2025-4-17

更新内容:

  • 添加了导入 xmind 格式文件的功能。

# 1. 数据结构定义

这里和之前类似

// 定义思维导图主题类型
interface Topic {
    id: string;
    structureClass: string;
    title: string;
    size: [number, number]; // 节点尺寸
    position: [number, number]; // 节点位置
    collapsed: boolean; // 折叠状态
    children?: {
        attached: Topic[]; // 子节点
    };
}
// 定义思维导图工作表类型
interface Sheet {
    id: string;
    class: string;
    title: string;
    extensions: unknown[];
    topicPositioning: string;
    topicOverlapping: string;
    coreVersion: string;
    rootTopic: Topic;
}
// 定义 content.json 的类型
type ContentJSON = Sheet[];
// 定义解析后的数据结构
interface ParsedData {
    nodes: Record<string, Node>; // 节点记录
    connections: string[]; // 连接关系列表
}

# 2. 递归解析子节点函数

通过递归的方式解析子节点,并构建节点记录和连接关系列表。

// 辅助函数:递归解析子节点
const parseChildren = (topic: Topic, parentId: string, parsedData: ParsedData): void => {
    const { id, title, position, size, collapsed, children } = topic;
    // 创建当前节点
    const node: Node = {
        id,
        text: title,
        position: position || [0, 0], // 如果没有提供位置,则使用默认值
        children: [], // 子节点 ID 列表
        size: size || [200, 60], // 如果没有提供尺寸,则使用默认值
        collapsed: collapsed || false, // 如果没有提供折叠状态,则使用默认值
        direction: 'right', // 方向属性是可选的
    };
    // 添加到节点记录
    parsedData.nodes[id] = node;
    // 如果有父节点,则建立连接
    if (parentId) {
        parsedData.connections.push(`${parentId}---${id}`);
        parsedData.nodes[parentId].children.push(id);
    }
    // 递归处理子节点
    if (children?.attached) {
        children.attached.forEach((childTopic) => {
            parseChildren(childTopic, id, parsedData);
        });
    }
};

# 3. 解析 XMind 文件函数

读入文件返回一个 Promise,解析文件内容并返回解析后的数据。
由于 XMind 文件是一个 ZIP 格式的文件,所以需要使用 JSZip 库来解析。

// 导入 xmind 文件
export const importFromXMind = (file: File): Promise<void> => {
    return new Promise((resolve, reject) => {
        const zip = new JSZip();
        zip.loadAsync(file)
            .then((unzipped) => {
                // 读取 content.json
                return unzipped.file('content.json')?.async('text');
            })
            .then((contentJsonText) => {
                if (!contentJsonText) {
                    throw new Error('Missing content.json');
                }
                // 解析 content.json
                const contentJson: ContentJSON = JSON.parse(contentJsonText);
                // 初始化解析数据
                const parsedData: ParsedData = {
                    nodes: {},
                    connections: [],
                };
                // 解析根节点
                const rootSheet = contentJson[0];
                if (!rootSheet || !rootSheet.rootTopic) {
                    throw new Error('Invalid content.json structure');
                }
                parseChildren(rootSheet.rootTopic, '', parsedData);
                // 更新 store 状态
                useMindmapStore.setState({
                    nodes: parsedData.nodes,
                    connections: parsedData.connections,
                    selectedNodeId: null, // 如果需要,可以从其他地方获取
                    layoutStyle: 'left-to-right', // 如果需要,可以从 metadata.json 获取
                });
                resolve();
            })
            .catch((error) => {
                reject(new Error(`Failed to import XMind file: ${error.message}`));
            });
    });
};

# 😘

现在不仅可以导出为 Xmind 文件,也可以导入 Xmind 文件了,虽然导入只限于本思维导图的导出的导入,但是导出的是可以适配 Xmind 的,所以可以很方便的导入到 Xmind 中。