Compare commits

...

22 Commits
0.2.3 ... 0.2.8

Author SHA1 Message Date
wanglin2
36c8927dd0 修改文档 2022-09-24 10:51:09 +08:00
wanglin2
05333daa63 支持展开到指定层级 2022-09-24 10:49:40 +08:00
wanglin25
5aed681198 支持展开到指定层级 2022-09-23 17:41:38 +08:00
wanglin25
8a438f2906 修复xmind8版本文件导入失败的问题 2022-09-23 16:16:08 +08:00
wanglin2
260de4987d 修改README 2022-09-21 21:02:17 +08:00
wanglin25
31cc658c06 打包 2022-09-21 20:11:12 +08:00
wanglin25
be6b41d74d 支持导入.xmind文件 2022-09-21 20:07:46 +08:00
wanglin25
4beeead53d 修复根节点添加多个节点爆栈的问题 2022-09-21 09:38:41 +08:00
wanglin25
af2df6acd3 导出svg增加title标签 2022-09-19 09:36:19 +08:00
wanglin2
c08d66acf2 打包 2022-09-17 22:00:15 +08:00
wanglin2
5a9cb9ac07 节点支持设置自定义线条样式 2022-09-17 16:18:38 +08:00
wanglin2
1662cd1be7 优化节点自定义线条样式 2022-09-17 15:09:34 +08:00
wanglin2
9dd5b3d47e 节点支持自定义线条样式 2022-09-17 15:00:12 +08:00
wanglin2
ece116317b 修改文档 2022-09-13 22:30:45 +08:00
wanglin25
d24d5c8281 修复节点展开收起的bug&打包 2022-09-13 10:34:20 +08:00
wanglin25
97c01cda3a 打包 2022-09-13 09:43:18 +08:00
wanglin25
6d520ece7e 修改文档 2022-09-13 09:33:05 +08:00
wanglin2
745041deef 节点支持多种形状开发完成 2022-09-12 23:07:01 +08:00
wanglin25
ae6c10cdf9 打包 2022-09-01 11:11:33 +08:00
街角小林
f5338d62fc Merge pull request #26 from huangyuanyin/main
fix 二级节点后无子节点,展开所有/收起所有操作后的报错信息
2022-09-01 11:07:38 +08:00
liuzhanghao
19fc12ff20 fix 2022-09-01 10:28:26 +08:00
liuzhanghao
d7640bb026 fix 二级节点后无子节点,展开所有/收起所有操作后的报错信息 2022-09-01 10:22:01 +08:00
25 changed files with 1194 additions and 359 deletions

440
README.md
View File

@@ -18,11 +18,15 @@
- [x] 支持节点自由拖拽、拖拽调整
- [x] 支持多种节点形状
- [x] 支持导出为`json``png``svg``pdf`,支持从`json``xmind`导入
## 目录介绍
1.`simple-mind-map`
思维导图工具库。
思维导图工具库,框架无关,`Vue``React`等框架或无框架都可以使用
2.`web`
@@ -30,7 +34,7 @@
3.`dist`
打包后的资源文件夹。
打包`web`后的资源文件夹。
4.`docs`
@@ -78,6 +82,7 @@ npm run buildLibrary
cd web
npm run build
```
会自动把`index.html`移动到根目录。
## 相关文章
@@ -86,7 +91,7 @@ npm run build
# 安装
> 当然仓库版本0.2.3当前npm版本0.2.3
> 当然仓库版本0.2.8当前npm版本0.2.8
```bash
npm i simple-mind-map
@@ -94,15 +99,15 @@ npm i simple-mind-map
`0.2.0`版本之前的注意事项:
>注意本项目为源码直接发布并未进行打包如果出现编译失败的情况Vue CLI创建的项目可以在vue.config.js文件中增加如下配置来让babel-loader编译本依赖
>
>```js
>module.exports = {
> transpileDependencies: ['simple-mind-map']
>}
>```
>
>其他项目请自行修改打包配置。
> 注意本项目为源码直接发布并未进行打包如果出现编译失败的情况Vue CLI创建的项目可以在vue.config.js文件中增加如下配置来让babel-loader编译本依赖
>
> ```js
> module.exports = {
> transpileDependencies: ['simple-mind-map']
> }
> ```
>
> 其他项目请自行修改打包配置。
# API
@@ -121,27 +126,58 @@ const mindMap = new MindMap({
});
```
### Xmind解析方法
v0.2.7+
可以通过如下方法获取解析`Xmind`文件的方法:
```js
import MindMap from "simple-mind-map";
console.log(MindMap.xmind)
```
`MindMap.xmind`对象上挂载了两个方法:
#### parseXmindFile(file)
解析`.xmind`文件,返回解析后的数据,注意是完整的数据,包含节点树、主题、结构等,可以使用`mindMap.setFullData(data)`来将返回的数据渲染到画布上
`file``File`对象
#### transformXmind(content)
转换`xmind`数据,`.xmind`文件本质上是一个压缩包,改成`zip`后缀可以解压缩,里面存在一个`content.json`文件,如果你自己解析出了这个文件,那么可以把这个文件内容传递给这个方法进行转换,转换后的数据,注意是完整的数据,包含节点树、主题、结构等,可以使用`mindMap.setFullData(data)`来将返回的数据渲染到画布上
`content``.xmind`压缩包内的`content.json`文件内容
#### transformOldXmind(content)
v0.2.8+
针对`xmind8`版本的数据解析,因为该版本的`.xmind`文件内没有`content.json`,对应的是`content.xml`
`content``.xmind`压缩包内的`content.xml`文件内容
### 实例化选项:
| 字段名称 | 类型 | 默认值 | 描述 | 是否必填 |
| -------------------- | ------- | ---------------- | ------------------------------------------------------------ | -------- |
| el | Element | | 容器元素必须为DOM元素 | 是 |
| data | Object | {} | 思维导图数据,可参考:[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) | |
| layout | String | logicalStructure | 布局类型可选列表logicalStructure逻辑结构图、mindMap思维导图、catalogOrganization目录组织图、organizationStructure组织结构图 | |
| theme | String | default | 主题可选列表default默认、classic脑图经典、minions小黄人、pinkGrape粉红葡萄、mint薄荷、gold金色vip、vitalityOrange活力橙、greenLeaf绿叶、dark2暗色2、skyGreen天清绿、classic2脑图经典2、classic3脑图经典3、classic4脑图经典4v0.2.0+、classicGreen经典绿、classicBlue经典蓝、blueSky天空蓝、brainImpairedPink脑残粉、dark暗色、earthYellow泥土黄、freshGreen清新绿、freshRed清新红、romanticPurple浪漫紫 | |
| themeConfig | Object | {} | 主题配置,会和所选择的主题进行合并,可用字段可参考:[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js) | |
| scaleRatio | Number | 0.1 | 放大缩小的增量比例 | |
| maxTag | Number | 5 | 节点里最多显示的标签数量,多余的会被丢弃 | |
| exportPadding | Number | 20 | 导出图片时的内边距 | |
| imgTextMargin | Number | 5 | 节点里图片和文字的间距 | |
| textContentMargin | Number | 2 | 节点里各种文字信息的间距,如图标和文字的间距 | |
| selectTranslateStep | Number | 3 | 多选节点时鼠标移动到边缘时的画布移动偏移量 | |
| selectTranslateLimit | Number | 20 | 多选节点时鼠标移动距边缘多少距离时开始偏移 | |
| customNoteContentShowv0.1.6+ | Object | null | 自定义节点备注内容显示Object类型结构为{show: (noteContent, left, top) => {// 你的显示节点备注逻辑 }, hide: () => {// 你的隐藏节点备注逻辑 }} | |
| readonlyv0.1.7+ | Boolean | false | 是否是只读模式 | |
| 字段名称 | 类型 | 默认值 | 描述 | 是否必填 |
| ------------------------------ | ------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
| el | Element | | 容器元素必须为DOM元素 | 是 |
| data | Object | {} | 思维导图数据,可参考:[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) | |
| layout | String | logicalStructure | 布局类型可选列表logicalStructure逻辑结构图、mindMap思维导图、catalogOrganization目录组织图、organizationStructure组织结构图 | |
| theme | String | default | 主题可选列表default默认、classic脑图经典、minions小黄人、pinkGrape粉红葡萄、mint薄荷、gold金色vip、vitalityOrange活力橙、greenLeaf绿叶、dark2暗色2、skyGreen天清绿、classic2脑图经典2、classic3脑图经典3、classic4脑图经典4v0.2.0+、classicGreen经典绿、classicBlue经典蓝、blueSky天空蓝、brainImpairedPink脑残粉、dark暗色、earthYellow泥土黄、freshGreen清新绿、freshRed清新红、romanticPurple浪漫紫 | |
| themeConfig | Object | {} | 主题配置,会和所选择的主题进行合并,可用字段可参考:[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js) | |
| scaleRatio | Number | 0.1 | 放大缩小的增量比例 | |
| maxTag | Number | 5 | 节点里最多显示的标签数量,多余的会被丢弃 | |
| exportPadding | Number | 20 | 导出图片时的内边距 | |
| imgTextMargin | Number | 5 | 节点里图片和文字的间距 | |
| textContentMargin | Number | 2 | 节点里各种文字信息的间距,如图标和文字的间距 | |
| selectTranslateStep | Number | 3 | 多选节点时鼠标移动到边缘时的画布移动偏移量 | |
| selectTranslateLimit | Number | 20 | 多选节点时鼠标移动距边缘多少距离时开始偏移 | |
| customNoteContentShowv0.1.6+ | Object | null | 自定义节点备注内容显示Object类型结构为{show: (noteContent, left, top) => {// 你的显示节点备注逻辑 }, hide: () => {// 你的隐藏节点备注逻辑 }} | |
| readonlyv0.1.7+ | Boolean | false | 是否是只读模式 | |
### 实例方法:
@@ -149,14 +185,10 @@ const mindMap = new MindMap({
触发整体渲染,会进行节点复用,性能较`reRender`会更好一点,如果只是节点位置变化了可以调用该方法进行渲染
#### reRender()
整体重新渲染,会清空画布,节点也会重新创建,性能不好,慎重使用
#### resize()
容器尺寸变化后,需要调用该方法进行适应
@@ -171,57 +203,47 @@ v0.1.7+。切换模式为只读或编辑。
监听事件,事件列表:
| 事件名称 | 描述 | 回调参数 |
| --------------------------- | ------------------------------------------ | ------------------------------------------------------------ |
| data_change | 渲染树数据变化,可以监听该方法获取最新数据 | data当前渲染树数据 |
| view_data_changev0.1.1+ | 视图变化数据,比如拖动或缩放时会触发 | data当前视图状态数据 |
| back_forward | 前进或回退 | activeHistoryIndex当前在历史数据数组里的索引、length当前历史数据数组的长度 |
| draw_click | *画布的单击事件* | e事件对象 |
| svg_mousedown | svg画布的鼠标按下事件 | e事件对象 |
| mousedown | el元素的鼠标按下事件 | e事件对象、thisEvent事件类实例 |
| mousemove | el元素的鼠标移动事件 | e事件对象、thisEvent事件类实例 |
| drag | 如果是按住左键拖动的话会触发拖动事件 | e事件对象、thisEvent事件类实例 |
| mouseup | el元素的鼠标松开事件 | e事件对象、thisEvent事件类实例 |
| mousewheel | 鼠标滚动事件 | e事件对象、dir向上up还是向下down滚动、thisEvent事件类实例 |
| contextmenu | svg画布的鼠标右键菜单事件 | e事件对象 |
| node_click | 节点的单击事件 | this节点实例、e事件对象 |
| node_mousedown | 节点的鼠标按下事件 | this节点实例、e事件对象 |
| node_mouseup | 节点的鼠标松开事件 | this节点实例、e事件对象 |
| node_dblclick | 节点的双击事件 | this节点实例、e事件对象 |
| node_contextmenu | 节点的右键菜单事件 | e事件对象、this节点实例 |
| before_node_active | 节点激活前事件 | this节点实例、activeNodeList当前激活的所有节点列表 |
| node_active | 节点激活事件 | this节点实例、activeNodeList当前激活的所有节点列表 |
| expand_btn_click | 节点展开或收缩事件 | this节点实例 |
| before_show_text_edit | 节点文本编辑框即将打开事件 | |
| hide_text_edit | 节点文本编辑框关闭事件 | textEditNode文本编辑框DOM节点、activeNodeList当前激活的所有节点列表 |
| scale | 放大缩小事件 | scale缩放比例 |
| 事件名称 | 描述 | 回调参数 |
| ------------------------- | --------------------- | ----------------------------------------------------- |
| data_change | 渲染树数据变化,可以监听该方法获取最新数据 | data当前渲染树数据 |
| view_data_changev0.1.1+ | 视图变化数据,比如拖动或缩放时会触发 | data当前视图状态数据 |
| back_forward | 前进或回退 | activeHistoryIndex当前在历史数据数组里的索引、length当前历史数据数组的长度 |
| draw_click | *画布的单击事件* | e事件对象 |
| svg_mousedown | svg画布的鼠标按下事件 | e事件对象 |
| mousedown | el元素的鼠标按下事件 | e事件对象、thisEvent事件类实例 |
| mousemove | el元素的鼠标移动事件 | e事件对象、thisEvent事件类实例 |
| drag | 如果是按住左键拖动的话会触发拖动事件 | e事件对象、thisEvent事件类实例 |
| mouseup | el元素的鼠标松开事件 | e事件对象、thisEvent事件类实例 |
| mousewheel | 鼠标滚动事件 | e事件对象、dir向上up还是向下down滚动、thisEvent事件类实例 |
| contextmenu | svg画布的鼠标右键菜单事件 | e事件对象 |
| node_click | 节点的单击事件 | this节点实例、e事件对象 |
| node_mousedown | 节点的鼠标按下事件 | this节点实例、e事件对象 |
| node_mouseup | 节点的鼠标松开事件 | this节点实例、e事件对象 |
| node_dblclick | 节点的双击事件 | this节点实例、e事件对象 |
| node_contextmenu | 节点的右键菜单事件 | e事件对象、this节点实例 |
| before_node_active | 节点激活前事件 | this节点实例、activeNodeList当前激活的所有节点列表 |
| node_active | 节点激活事件 | this节点实例、activeNodeList当前激活的所有节点列表 |
| expand_btn_click | 节点展开或收缩事件 | this节点实例 |
| before_show_text_edit | 节点文本编辑框即将打开事件 | |
| hide_text_edit | 节点文本编辑框关闭事件 | textEditNode文本编辑框DOM节点、activeNodeList当前激活的所有节点列表 |
| scale | 放大缩小事件 | scale缩放比例 |
#### emit(event, ...args)
触发事件,可以是上面表格里的事件,也可以是自定义事件
#### off(event, fn)
解绑事件
#### setTheme(theme)
切换主题,可选主题见上面的选项表格
#### getTheme()
获取当前主题
#### setThemeConfig(config)
设置主题配置,`config`同上面选项表格里的选项`themeConfig`
@@ -234,64 +256,68 @@ v0.1.7+。切换模式为只读或编辑。
获取某个主题配置属性值
#### getLayout()
获取当前的布局结构
#### setLayout(layout)
设置布局结构,可选值见上面选项表格的`layout`字段
#### execCommand(name, ...args)
执行命令,每执行一个命令就会在历史堆栈里添加一条记录用于回退或前进。所有命令如下:
| 命令名称 | 描述 | 参数 |
| ----------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| SELECT_ALL | 全选 | |
| BACK | 回退指定的步数 | step要回退的步数默认为1 |
| FORWARD | 前进指定的步数 | step要前进的步数默认为1 |
| INSERT_NODE | 插入同级节点,操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效 | |
| INSERT_CHILD_NODE | 插入子节点,操作节点为当前激活的节点 | |
| UP_NODE | 上移节点,操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点或在列表里的第一个节点使用无效 | |
| DOWN_NODE | 操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点或在列表里的最后一个节点使用无效 | |
| REMOVE_NODE | 删除节点,操作节点为当前激活的节点 | |
| PASTE_NODE | 粘贴节点到节点,操作节点为当前激活的节点 | data要粘贴的节点数据一般通过`renderer.copyNode()`方法和`renderer.cutNode()`方法获取) |
| CUT_NODE | 剪切节点,操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点使用无效 | callback(回调函数,剪切的节点数据会通过调用该函数并通过参数返回) |
| SET_NODE_STYLE | 修改节点样式 | node要设置样式的节点、prop样式属性、value样式属性值、isActive布尔值是否设置的是激活状态的样式 |
| SET_NODE_ACTIVE | 设置节点是否激活 | node要设置的节点、active布尔值是否激活 |
| CLEAR_ACTIVE_NODE | 清除当前已激活节点的激活状态,操作节点为当前激活的节点 | |
| SET_NODE_EXPAND | 设置节点是否展开 | node要设置的节点、expand布尔值是否展开 |
| EXPAND_ALL | 展开所有节点 | |
| UNEXPAND_ALL | 收起所有节点 | |
| SET_NODE_DATA | 更新节点数据,即更新节点数据对象里`data`对象的数据 | node要设置的节点、data对象要更新的数据如`{expand: true}` |
| SET_NODE_TEXT | 设置节点文本 | node要设置的节点、text要设置的文本字符串换行可以使用`\n` |
| SET_NODE_IMAGE | 设置节点图片 | node要设置的节点imgData对象图片信息结构为`{url, title, width, height}`,图片的宽高必须要传) |
| SET_NODE_ICON | 设置节点图 | node要设置的节点、icons数组预定义的图片名称组成的数组可用图标可在[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/svg/icons.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/svg/icons.js)文件里的`nodeIconList`列表里获取到,图标名称为`type_name`,如`['priority_1']` |
| SET_NODE_HYPERLINK | 设置节点超链接 | node要设置的节点link超链接地址、title超链接名称可选 |
| SET_NODE_NOTE | 设置节点备注 | node要设置的节点note备注文字 |
| SET_NODE_TAG | 设置节点标签 | node要设置的节点tag字符串数组内置颜色信息可在[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/utils/constant.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/utils/constant.js)里获取到) |
| INSERT_AFTERv0.1.5+ | 将节点移动到另一个节点的后面 | node移动的节点)、 exist目标节点 |
| INSERT_BEFOREv0.1.5+ | 将节点移动到另一个节点的前面 | node要移动的节点、 exist目标节点 |
| MOVE_NODE_TOv0.1.5+ | 移动一个节点作为另一个节点的子节点 | node要移动的节点、 toNode目标节点 |
| ADD_GENERALIZATIONv0.2.0+ | 添加节点概要 | data概要的数据对象格式节点的数字段都支持默认为{text: '概要'} |
| REMOVE_GENERALIZATIONv0.2.0+ | 删除节点概要 | |
| SET_NODE_CUSTOM_POSITIONv0.2.0+ | 设置节点自定义位置 | node要设置的节点、 left自定义的x坐标默认为undefined、 top自定义的y坐标默认为undefined |
| RESET_LAYOUTv0.2.0+ | 一键整理布局 | |
| 命令名称 | 描述 | 参数 |
| --------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| SELECT_ALL | 全选 | |
| BACK | 回退指定的步数 | step要回退的步数默认为1 |
| FORWARD | 前进指定的步数 | step要前进的步数默认为1 |
| INSERT_NODE | 插入同级节点,操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效 | |
| INSERT_CHILD_NODE | 插入子节点,操作节点为当前激活的节点 | |
| UP_NODE | 上移节点,操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点或在列表里的第一个节点使用无效 | |
| DOWN_NODE | 操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点或在列表里的最后一个节点使用无效 | |
| REMOVE_NODE | 删除节点,操作节点为当前激活的节点 | |
| PASTE_NODE | 粘贴节点到节点,操作节点为当前激活的节点 | data要粘贴的节点数据一般通过`renderer.copyNode()`方法和`renderer.cutNode()`方法获取) |
| CUT_NODE | 剪切节点,操作节点为当前激活的节点,如果有多个激活节点,只会对第一个有效,对根节点使用无效 | callback(回调函数,剪切的节点数据会通过调用该函数并通过参数返回) |
| SET_NODE_STYLE | 修改节点样式 | node要设置样式的节点、prop样式属性、value样式属性值、isActive布尔值是否设置的是激活状态的样式 |
| SET_NODE_ACTIVE | 设置节点是否激活 | node要设置的节点、active布尔值是否激活 |
| CLEAR_ACTIVE_NODE | 清除当前已激活节点的激活状态,操作节点为当前激活的节点 | |
| SET_NODE_EXPAND | 设置节点是否展开 | node要设置的节点、expand布尔值是否展开 |
| EXPAND_ALL | 展开所有节点 | |
| UNEXPAND_ALL | 收起所有节点 | |
| UNEXPAND_TO_LEVELv0.2.8+ | 展开到指定层级 | level要展开到的层级1、2、3... |
| SET_NODE_DATA | 更新节点数据,即更新节点数据对象里`data`对象的数据 | node设置节点、data对象要更新的数据`{expand: true}` |
| SET_NODE_TEXT | 设置节点文本 | node要设置的节点text要设置的文本字符串换行可以使用`\n` |
| SET_NODE_IMAGE | 设置节点图 | node要设置的节点、imgData对象图片信息结构为`{url, title, width, height}`,图片的宽高必须要传) |
| SET_NODE_ICON | 设置节点图标 | node要设置的节点icons数组预定义的图片名称组成的数组可用图标可在[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/svg/icons.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/svg/icons.js)文件里的`nodeIconList`列表里获取到,图标名称为`type_name`,如`['priority_1']` |
| SET_NODE_HYPERLINK | 设置节点超链接 | node要设置的节点link超链接地址、title超链接名称可选 |
| SET_NODE_NOTE | 设置节点备注 | node要设置的节点note备注文字 |
| SET_NODE_TAG | 设置节点标签 | node设置的节点)、tag字符串数组内置颜色信息可在[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/utils/constant.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/utils/constant.js)里获取到) |
| INSERT_AFTERv0.1.5+ | 将节点移动到另一个节点的后面 | node要移动的节点、 exist目标节点 |
| INSERT_BEFOREv0.1.5+ | 将节点移动到另一个节点的前面 | node要移动的节点、 exist目标节点 |
| MOVE_NODE_TOv0.1.5+ | 移动一个节点作为另一个节点的子节点 | node要移动的节点、 toNode目标节点 |
| ADD_GENERALIZATIONv0.2.0+ | 添加节点概要 | data概要的数据对象格式节点的数字段都支持默认为{text: '概要'} |
| REMOVE_GENERALIZATIONv0.2.0+ | 删除节点概要 | |
| SET_NODE_CUSTOM_POSITIONv0.2.0+ | 设置节点自定义位置 | node要设置的节点、 left自定义的x坐标默认为undefined、 top自定义的y坐标默认为undefined |
| RESET_LAYOUTv0.2.0+ | 一键整理布局 | |
| SET_NODE_SHAPEv0.2.4+ | 设置节点形状 | node要设置的节点、shape形状全部形状https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/Shape.js |
#### setData(data)
动态设置思维导图数据
动态设置思维导图数据,纯节点数据
`data`:思维导图结构数据
#### setFullData(*data*)
v0.2.7+
动态设置思维导图数据,包括节点数据、布局、主题、视图
`data`:完整数据,结构可参考[exportFullData](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exportFullData.json)
#### export(type, isDownload, fileName)
@@ -313,22 +339,16 @@ v0.1.5+
`render`实例负载整个渲染过程,可通过`mindMap.renderer`获取到
### 属性
#### activeNodeList
获取当前激活的节点列表
#### root
获取节点树的根节点
### 方法
#### clearActive()
@@ -351,74 +371,52 @@ v0.1.5+
添加节点到激活列表里
#### removeActiveNode(node)
在激活列表里移除某个节点
#### findActiveNodeIndex(node)
检索某个节点在激活列表里的索引
#### getNodeIndex(node)
获取节点在同级里的位置索引
#### removeOneNode(node)
删除某个指定节点
#### copyNode()
复制节点,操作节点为当前激活节点,有多个激活节点只会操作第一个节点
#### setNodeDataRender(node, data)
设置节点数据,即`data`字段的数据,并会根据节点大小是否变化来判断是否需要重新渲染该节点,`data`为对象,如:`{text: '我是新文本'}`
#### moveNodeTo(node, toNode)
v0.1.5+
移动一个节点作为另一个节点的子节点
#### insertBefore(node, exist)
v0.1.5+
将节点移动到另一个节点的前面
#### insertAfter(node, exist)
v0.1.5+
将节点移动到另一个节点的后面
## keyCommand实例
`keyCommand`实例负责快捷键的添加及触发,内置了一些快捷键,也可以自行添加。可通过`mindMap.keyCommand`获取到该实例。
### 方法
#### addShortcut(key, fn)
@@ -462,13 +460,10 @@ v0.2.3+。保存当前注册的快捷键数据,然后清空快捷键数据
v0.2.3+。恢复保存的快捷键数据,然后清空缓存数据
## command实例
`command`实例负责命令的添加及执行,内置了很多命令,也可以自行添加,命令指需要在历史堆栈数据里添加副本的操作。可通过`mindMap.command`获取到该实例
### 方法
#### add(name, fn)
@@ -479,8 +474,6 @@ v0.2.3+。恢复保存的快捷键数据,然后清空缓存数据
`fn`:命令要执行的方法
#### remove(name, fn)
移除命令。
@@ -489,8 +482,6 @@ v0.2.3+。恢复保存的快捷键数据,然后清空缓存数据
`fn`:要移除的方法,不传的话移除该命令所有的方法
#### getCopyData()
获取渲染树数据副本
@@ -499,81 +490,58 @@ v0.2.3+。恢复保存的快捷键数据,然后清空缓存数据
清空历史堆栈数据
## view实例
`view`实例负责视图操作,可通过`mindMap.view`获取到该实例
### 方法
#### translateX(step)
`x`方向进行平移,`step`:要平移的像素
#### translateY(step)
`y`方向进行平移,`step`:要平移的像素
#### reset()
恢复到默认的变换
#### narrow()
缩小
#### enlarge()
放大
#### getTransformData()
v0.1.1+
获取当前变换数据,可用于回显
#### setTransformData(data)
v0.1.1+
动态设置变换数据可以通过getTransformData方法获取变换数据
## doExport实例
`doExport`实例负责导出,可通过`mindMap.doExport`获取到该实例
### 方法
#### png()
导出为`png`,异步方法,返回图片数据,`data:url`数据,可以自行下载或显示
#### svg()
导出为`svg`,异步方法,返回`svg`数据,`data:url`数据,可以自行下载或显示
#### getSvgData()
获取`svg`数据,异步方法,返回一个对象:
@@ -585,28 +553,20 @@ v0.1.1+
}
```
## select实例
`select`实例负责鼠标右键多选节点操作,可通过`mindMap.select`获取到该实例
### 方法
#### toPos(x, y)
转换鼠标位置为相对于容器`el`的位置
## batchExecution实例
`batchExecution`用来批量异步的执行一些操作,如果某个操作同时多次调用,那么只会在下一个事件循环里执行一次。可以通过`mindMap.batchExecution`获取到该实例
### 方法
#### push(name, fn)
@@ -617,146 +577,100 @@ v0.1.1+
`fn`:任务
## node实例
每个节点都会实例化一个`node`实例
### 属性
#### nodeData
该节点对应的真实数据
#### uid
该节点唯一的标识
#### isRoot
是否是根节点
#### layerIndex
节点层级
#### width
节点的宽
#### height
节点的高
#### left
节点的`left`位置
#### top
节点的`top`位置
#### parent
节点的父节点
#### children
节点的子节点列表
#### group
节点是内容容器,`svg`对象
#### isDrag
v0.1.5+
节点是否正在拖拽中
### 方法
#### addChildren(node)
添加子节点
#### getSize()
计算节点的宽高,返回一个布尔值,代表是否宽高发生了变化
#### renderNode()
渲染节点到画布,会移除旧的内容节点,创建新的
#### render()
递归渲染该节点及其所有子节点,第一次调用会创建节点内容,后续只会更新节点位置,想要重新渲染内容,可以先把`initRender`属性设为`true`
#### remove()
递归删除该节点及其所有子节点
#### renderLine()
重新渲染该节点到其子节点之间的连线
#### removeLine()
移除该节点到其子节点之间的连线
#### renderExpandBtn()
渲染展开收缩按钮的内容
#### removeExpandBtn()
移除展开收缩按钮
#### getStyle(prop, root, isActive)
获取某个最终应用到该节点的样式值
@@ -767,141 +681,131 @@ v0.1.5+
`isActive`:获取的是否是激活状态的样式值,默认`false`
#### setStyle(prop, value, isActive)
修改节点的某个样式,`SET_NODE_STYLE`命令的快捷方法
#### getData(key)
获取该节点真实数据`nodeData``data`对象里的指定值,`key`不传返回这个`data`对象
#### setData(data)
设置节点数据,`SET_NODE_DATA`命令的快捷方法
#### setText(text)
设置节点文本,`SET_NODE_TEXT`命令的快捷方法
#### setImage(imgData)
设置节点图片,`SET_NODE_IMAGE`命令的快捷方法
#### setIcon(icons)
设置节点图标,`SET_NODE_ICON`命令的快捷方法
#### setHyperlink(link, title)
设置节点超链接,`SET_NODE_HYPERLINK`命令的快捷方法
#### setNote(note)
设置节点备注,`SET_NODE_NOTE`命令的快捷方法
#### setTag(tag)
设置节点标签,`SET_NODE_TAG`的快捷方法
#### hide()
v0.1.5+
隐藏节点及其下级节点
#### show()
v0.1.5+
显示节点及其下级节点
#### isParent(node)
v0.1.5+
检测当前节点是否是某个节点的祖先节点
#### isBrother(node)
v0.1.5+
检测当前节点是否是某个节点的兄弟节点
#### checkHasGeneralization()
v0.2.0+
检查是否存在概要
#### hideGeneralization()
v0.2.0+
隐藏概要节点
#### showGeneralization()
v0.2.0+
显示概要节点
#### updateGeneralization()
v0.2.0+
更新概要节点
#### hasCustomPosition()
v0.2.0+
检查节点是否存在自定义数据
#### ancestorHasCustomPosition()
v0.2.0+
检查节点是否存在自定义位置的祖先节点
#### getShape()
v0.2.4+
获取节点形状
#### setShape(shape)
v0.2.4+
设置节点形状,`SET_NODE_SHAPE`命令的快捷方法
#### getSelfStyle(prop)
v0.2.5+
获取节点自身的自定义样式
#### getParentSelfStyle(prop)
v0.2.5+
获取最近一个存在自身自定义样式的祖先节点的自定义样式
#### getSelfInhertStyle(prop)
v0.2.5+
获取自身可继承的自定义样式
## 内置工具方法
@@ -911,8 +815,6 @@ v0.2.0+
import {walk, ...} from 'simple-mind-map/src/utils'
```
### 方法
#### walk(root, parent, beforeCallback, afterCallback, isRoot, layerIndex = 0, index = 0)
@@ -939,14 +841,10 @@ import {walk, ...} from 'simple-mind-map/src/utils'
walk(tree, null, () => {}, () => {}, false, 0, 0)
```
#### bfsWalk(root, callback)
广度优先遍历树
#### resizeImgSize(width, height, maxWidth, maxHeight)
缩放图片的尺寸
@@ -961,20 +859,14 @@ walk(tree, null, () => {}, () => {}, false, 0, 0)
`maxWidth``maxHeight`可以同时都传,也可以只传一个
#### resizeImg(imgUrl, maxWidth, maxHeight)
缩放图片,内部先加载图片,然后调用`resizeImgSize`方法,返回一个`promise`
#### simpleDeepClone(data)
极简的深拷贝方法,只能针对全是基本数据的对象,否则会报错
#### copyRenderTree(tree, root)
复制渲染树数据,示例:
@@ -983,8 +875,6 @@ walk(tree, null, () => {}, () => {}, false, 0, 0)
copyRenderTree({}, this.mindMap.renderer.renderTree)
```
#### copyNodeTree(tree, root)
复制节点树数据,主要是剔除其中的引用`node`实例的`_node`,然后复制`data`对象的数据,示例:
@@ -993,32 +883,22 @@ copyRenderTree({}, this.mindMap.renderer.renderTree)
copyNodeTree({}, node)
```
#### imgToDataUrl(src)
图片转成dataURL
#### downloadFile(file, fileName)
下载文件
#### throttle(fn, time = 300, ctx)
节流函数
#### asyncRun(taskList, callback = () => {})
异步执行任务队列,多个任务是同步执行的,没有先后顺序
# 特别说明
本项目较粗糙,未进行完整测试,功能尚不是很完善,性能也存在一些问题,仅用于学习和参考,请勿用于实际项目。

View File

@@ -1 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>一个简单的web思维导图实现</title><link href="dist/js/chunk-2d20ec02.81d632f4.js" rel="prefetch"><link href="dist/js/chunk-2d216b67.228f2009.js" rel="prefetch"><link href="dist/js/chunk-e86f1494.f8dd20e2.js" rel="prefetch"><link href="dist/css/app.d29688d8.css" rel="preload" as="style"><link href="dist/css/chunk-vendors.6fd71983.css" rel="preload" as="style"><link href="dist/js/app.88b2cb65.js" rel="preload" as="script"><link href="dist/js/chunk-vendors.094115db.js" rel="preload" as="script"><link href="dist/css/chunk-vendors.6fd71983.css" rel="stylesheet"><link href="dist/css/app.d29688d8.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="dist/js/chunk-vendors.094115db.js"></script><script src="dist/js/app.88b2cb65.js"></script></body></html>
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>一个简单的web思维导图实现</title><link href="dist/js/chunk-2d20ec02.10aa67e3.js" rel="prefetch"><link href="dist/js/chunk-2d216b67.2d06497a.js" rel="prefetch"><link href="dist/js/chunk-5397ae43.9dca0f90.js" rel="prefetch"><link href="dist/css/app.a172fa3f.css" rel="preload" as="style"><link href="dist/css/chunk-vendors.597033a2.css" rel="preload" as="style"><link href="dist/js/app.cb927d32.js" rel="preload" as="script"><link href="dist/js/chunk-vendors.07ae01de.js" rel="preload" as="script"><link href="dist/css/chunk-vendors.597033a2.css" rel="stylesheet"><link href="dist/css/app.a172fa3f.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="dist/js/chunk-vendors.07ae01de.js"></script><script src="dist/js/app.cb927d32.js"></script></body></html>

View File

@@ -0,0 +1,66 @@
{
"layout": "logicalStructure",
"root": {
"data": {
"text": "根节点",
"expand": true,
"isActive": false
},
"children": [{
"data": {
"text": "二级节点",
"generalization": {
"text": "概要",
"expand": true,
"isActive": false
},
"expand": true,
"isActive": false
},
"children": [{
"data": {
"text": "分支主题",
"expand": true,
"isActive": false
},
"children": []
}, {
"data": {
"text": "分支主题",
"expand": true,
"isActive": false
},
"children": []
}]
}]
},
"theme": {
"template": "classic4",
"config": {}
},
"view": {
"transform": {
"scaleX": 1,
"scaleY": 1,
"shear": 0,
"rotate": 0,
"translateX": 0,
"translateY": 0,
"originX": 0,
"originY": 0,
"a": 1,
"b": 0,
"c": 0,
"d": 1,
"e": 0,
"f": 0
},
"state": {
"scale": 1,
"x": 0,
"y": 0,
"sx": 0,
"sy": 0
}
}
}

View File

@@ -16,6 +16,7 @@ import {
import {
SVG
} from '@svgdotjs/svg.js'
import xmind from './src/parse/xmind'
// 默认选项配置
const defaultOpt = {
@@ -319,7 +320,7 @@ class MindMap {
/**
* @Author: 王林
* @Date: 2021-08-03 22:58:12
* @Desc: 动态设置思维导图数据
* @Desc: 动态设置思维导图数据,纯节点数据
*/
setData(data) {
this.execCommand('CLEAR_ACTIVE_NODE')
@@ -328,6 +329,32 @@ class MindMap {
this.reRender()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-21 16:39:13
* @Desc: 动态设置思维导图数据,包括节点数据、布局、主题、视图
*/
setFullData(data) {
if (data.root) {
this.setData(data.root)
}
if (data.layout) {
this.setLayout(data.layout)
}
if (data.theme) {
if (data.theme.template) {
this.setTheme(data.theme.template)
}
if (data.theme.config) {
this.setThemeConfig(data.theme.config)
}
}
if (data.view) {
this.view.setTransformData(data.view)
}
}
/**
* @Author: 王林
* @Date: 2021-07-01 22:06:38
@@ -369,4 +396,6 @@ class MindMap {
}
}
MindMap.xmind = xmind
export default MindMap

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.2.3",
"version": "0.2.8",
"description": "一个简单的web在线思维导图",
"authors": [
{
@@ -25,7 +25,9 @@
"canvg": "^3.0.7",
"deepmerge": "^1.5.2",
"eventemitter3": "^4.0.7",
"jspdf": "^2.5.1"
"jspdf": "^2.5.1",
"jszip": "^3.10.1",
"xml-js": "^1.6.11"
},
"keywords": [
"javascript",

View File

@@ -189,7 +189,7 @@ class Drag extends Base {
// 连接线
this.line = this.draw.path()
this.line.opacity(0.5)
this.node.style.line(this.line)
this.node.styleLine(this.line, this.node)
// 同级位置占位符
this.placeholder = this.draw.rect().fill({
color: this.node.style.merge('lineColor', true)

View File

@@ -1,5 +1,8 @@
import { imgToDataUrl, downloadFile } from './utils'
import JsPDF from 'jspdf'
import {
SVG,
} from '@svgdotjs/svg.js'
const URL = window.URL || window.webkitURL || window
/**
@@ -23,9 +26,9 @@ class Export {
* @Date: 2021-07-02 07:44:06
* @Desc: 导出
*/
async export(type, isDownload = true, name = '思维导图') {
async export(type, isDownload = true, name = '思维导图', ...args) {
if (this[type]) {
let result = await this[type](name)
let result = await this[type](name, ...args)
if (isDownload && type !== 'pdf') {
downloadFile(result, name + '.' + type)
}
@@ -228,8 +231,9 @@ class Export {
* @Date: 2021-07-04 14:54:07
* @Desc: 导出为svg
*/
async svg() {
async svg(name) {
let { node } = await this.getSvgData()
node.first().before(SVG(`<title>${name}</title>`))
await this.drawBackgroundToSvg(node)
let str = node.svg()
// 转换成blob数据
@@ -244,8 +248,22 @@ class Export {
* @Date: 2021-08-03 22:19:17
* @Desc: 导出为json
*/
json () {
let data = this.mindMap.command.getCopyData()
json (name, withConfig = true) {
let nodeData = this.mindMap.command.getCopyData()
let data = {}
if (withConfig) {
data = {
layout: this.mindMap.getLayout(),
root: nodeData,
theme: {
template: this.mindMap.getTheme(),
config: this.mindMap.getCustomThemeConfig()
},
view: this.mindMap.view.getTransformData()
}
} else {
data = nodeData
}
let str = JSON.stringify(data)
let blob = new Blob([str])
return URL.createObjectURL(blob)
@@ -256,8 +274,8 @@ class Export {
* @Date: 2021-08-03 22:24:24
* @Desc: 专有文件其实就是json文件
*/
smm () {
return this.json();
smm (name, withConfig) {
return this.json(name, withConfig);
}
}

View File

@@ -1,4 +1,5 @@
import Style from './Style'
import Shape from './Shape'
import {
resizeImgSize,
asyncRun
@@ -43,6 +44,12 @@ class Node {
this.themeConfig = this.mindMap.themeConfig
// 样式实例
this.style = new Style(this, this.themeConfig)
// 形状实例
this.shapeInstance = new Shape(this)
this.shapePadding = {
paddingX: 0,
paddingY: 0
}
// 是否是根节点
this.isRoot = opt.isRoot === undefined ? false : opt.isRoot
// 是否是概要节点
@@ -331,9 +338,16 @@ class Node {
// 间距
let margin = imgContentHeight > 0 && textContentHeight > 0 ? this.blockContentMargin : 0
let { paddingX, paddingY } = this.getPaddingVale()
// 纯内容宽高
let _width = Math.max(imgContentWidth, textContentWidth)
let _height = imgContentHeight + textContentHeight
// 计算节点形状需要的附加内边距
let { paddingX: shapePaddingX, paddingY: shapePaddingY } = this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
this.shapePadding.paddingX = shapePaddingX
this.shapePadding.paddingY = shapePaddingY
return {
width: Math.max(imgContentWidth, textContentWidth) + paddingX * 2,
height: imgContentHeight + textContentHeight + paddingY * 2 + margin
width: _width + paddingX * 2 + shapePaddingX * 2,
height: _height + paddingY * 2 + margin + shapePaddingY * 2
}
}
@@ -542,6 +556,16 @@ class Node {
}
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 22:02:07
* @Desc: 获取节点形状
*/
getShape() {
return this.style.getStyle('shape', false, false)
}
/**
* javascript comment
* @Author: 王林25
@@ -555,6 +579,7 @@ class Node {
textContentItemMargin
} = this
let { paddingY } = this.getPaddingVale()
paddingY += this.shapePadding.paddingY
// 创建组
this.group = new G()
// 概要节点添加一个带所属节点id的类名
@@ -563,8 +588,9 @@ class Node {
}
this.draw.add(this.group)
this.update(true)
// 节点
this.style.rect(this.group.rect(width, height))
// 节点形
const shape = this.getShape()
this.style[shape === 'rectangle' ? 'rect' : 'shape'](this.shapeInstance.createShape())
// 图片节点
let imgHeight = 0
if (this._imgData) {
@@ -685,6 +711,8 @@ class Node {
* @Desc: 渲染节点到画布,会移除旧的,创建新的
*/
renderNode() {
// 连线
this.renderLine()
this.removeAllEvent()
this.removeAllNode()
this.createNodeData()
@@ -724,13 +752,13 @@ class Node {
* @Desc: 递归渲染
*/
render() {
// 连线
this.renderLine()
// 节点
if (this.initRender) {
this.initRender = false
this.renderNode()
} else {
// 连线
this.renderLine()
this.update()
}
// 子节点
@@ -800,7 +828,7 @@ class Node {
this.showGeneralization()
if (this.parent) {
let index = this.parent.children.indexOf(this)
this.parent._lines[index].show()
this.parent._lines[index] && this.parent._lines[index].show()
}
// 子节点
if (this.children && this.children.length) {
@@ -835,10 +863,30 @@ class Node {
this._lines = this._lines.slice(0, childrenLen)
}
// 画线
this.renderer.layout.renderLine(this, this._lines)
// 添加样式
this._lines.forEach((line) => {
this.style.line(line)
this.renderer.layout.renderLine(this, this._lines, (line, node) => {
// 添加样式
this.styleLine(line, node)
})
// 和父级的连线也需要更新
if (this.parent) {
this.parent.renderLine()
}
}
/**
* javascript comment
* @Author: flydreame
* @Date: 2022-09-17 12:41:29
* @Desc: 设置连线样式
*/
styleLine(line, node) {
let width = node.getSelfInhertStyle('lineWidth') || node.getStyle('lineWidth', true)
let color = node.getSelfInhertStyle('lineColor') || node.getStyle('lineColor', true)
let dasharray = node.getSelfInhertStyle('lineDasharray') || node.getStyle('lineDasharray', true)
this.style.line(line, {
width,
color,
dasharray,
})
}
@@ -1004,7 +1052,7 @@ class Node {
node.x(0).y(-this.expandBtnSize / 2)
fillNode.x(0).y(-this.expandBtnSize / 2)
this.style.iconBtn(node, fillNode)
this._expandBtn.add(fillNode).add(node)
if (this._expandBtn) this._expandBtn.add(fillNode).add(node)
}
/**
@@ -1125,6 +1173,40 @@ class Node {
return v === undefined ? '' : v
}
/**
* javascript comment
* @Author: flydreame
* @Date: 2022-09-17 11:21:15
* @Desc: 获取自定义样式
*/
getSelfStyle(prop) {
return this.style.getSelfStyle(prop)
}
/**
* javascript comment
* @Author: flydreame
* @Date: 2022-09-17 11:21:26
* @Desc: 获取最近一个存在自身自定义样式的祖先节点的自定义样式
*/
getParentSelfStyle(prop) {
if (this.parent) {
return this.parent.getSelfStyle(prop) || this.parent.getParentSelfStyle(prop)
}
return null
}
/**
* javascript comment
* @Author: flydreame
* @Date: 2022-09-17 12:15:30
* @Desc: 获取自身可继承的自定义样式
*/
getSelfInhertStyle(prop) {
return this.getSelfStyle(prop) // 自身
|| this.getParentSelfStyle(prop) // 父级
}
/**
* @Author: 王林
* @Date: 2021-05-04 22:18:07
@@ -1205,6 +1287,16 @@ class Node {
setTag(tag) {
this.mindMap.execCommand('SET_NODE_TAG', this, tag)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:47:45
* @Desc: 设置形状
*/
setShape(shape) {
this.mindMap.execCommand('SET_NODE_SHAPE', this, shape)
}
}
export default Node

View File

@@ -5,6 +5,7 @@ import CatalogOrganization from './layouts/CatalogOrganization'
import OrganizationStructure from './layouts/OrganizationStructure'
import TextEdit from './TextEdit'
import { copyNodeTree, simpleDeepClone, walk } from './utils'
import { shapeList } from './Shape';
// 布局列表
const layouts = {
@@ -142,6 +143,9 @@ class Render {
// 收起所有节点
this.unexpandAllNode = this.unexpandAllNode.bind(this)
this.mindMap.command.add('UNEXPAND_ALL', this.unexpandAllNode)
// 展开到指定层级
this.expandToLevel = this.expandToLevel.bind(this)
this.mindMap.command.add('UNEXPAND_TO_LEVEL', this.expandToLevel)
// 设置节点数据
this.setNodeData = this.setNodeData.bind(this)
this.mindMap.command.add('SET_NODE_DATA', this.setNodeData)
@@ -175,6 +179,9 @@ class Render {
// 一键整理布局
this.resetLayout = this.resetLayout.bind(this)
this.mindMap.command.add('RESET_LAYOUT', this.resetLayout)
// 设置节点形状
this.setNodeShape = this.setNodeShape.bind(this)
this.mindMap.command.add('SET_NODE_SHAPE', this.setNodeShape)
}
/**
@@ -777,10 +784,7 @@ class Render {
node.data.expand = true
}
}, null, true, 0, 0)
this.mindMap.render()
this.root.children.forEach((item) => {
item.updateExpandBtnNode()
})
this.mindMap.reRender()
}
/**
@@ -789,14 +793,27 @@ class Render {
* @Desc: 收起所有
*/
unexpandAllNode() {
this.root.children.forEach((item) => {
this.setNodeExpand(item, false)
})
walk(this.renderTree, null, (node, parent, isRoot) => {
node._node = null
if (!isRoot) {
node.data.expand = false
}
}, null, true, 0, 0)
this.mindMap.reRender()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-23 16:31:27
* @Desc: 展开到指定层级
*/
expandToLevel(level) {
walk(this.renderTree, null, (node, parent, isRoot, layerIndex) => {
node._node = null
node.data.expand = layerIndex < level
}, null, true, 0, 0)
this.mindMap.reRender()
}
/**
@@ -977,6 +994,22 @@ class Render {
}, null, true, 0, 0)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:44:01
* @Desc: 设置节点形状
*/
setNodeShape(node, shape) {
if (!shape || !shapeList.includes(shape)) {
return
}
let nodeList = [node] || this.activeNodeList
nodeList.forEach((item) => {
this.setNodeStyle(item, 'shape', shape)
})
}
/**
* @Author: 王林
* @Date: 2021-05-04 14:19:48

View File

@@ -0,0 +1,265 @@
/**
* @Author: 王林
* @Date: 2022-08-22 21:32:50
* @Desc: 节点形状类
*/
export default class Shape {
constructor(node) {
this.node = node
}
/**
* @Author: 王林
* @Date: 2022-08-17 22:32:32
* @Desc: 形状需要的padding
*/
getShapePadding(width, height, paddingX, paddingY) {
const shape = this.node.getShape()
const defaultPaddingX = 15
const defaultPaddingY = 5
const actWidth = width + paddingX * 2
const actHeight = height + paddingY * 2
const actOffset = Math.abs(actWidth - actHeight)
switch (shape) {
case 'roundedRectangle':
return {
paddingX: height > width ? (height - width) / 2 : 0,
paddingY: 0
}
case 'diamond':
return {
paddingX: width / 2,
paddingY: height / 2
}
case 'parallelogram':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'outerTriangularRectangle':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'innerTriangularRectangle':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'ellipse':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: paddingY <= 0 ? defaultPaddingY : 0
}
case 'circle':
return {
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
paddingY: actHeight < actWidth ? actOffset / 2 : 0,
}
default:
return {
paddingX: 0,
paddingY: 0
}
}
}
/**
* @Author: 王林
* @Date: 2022-08-17 22:22:53
* @Desc: 创建形状节点
*/
createShape() {
const shape = this.node.getShape()
let { width, height } = this.node
let node = null
// 矩形
if (shape === 'rectangle') {
node = this.node.group.rect(width, height)
} else if (shape === 'diamond') {
// 菱形
node = this.createDiamond()
} else if (shape === 'parallelogram') {
// 平行四边形
node = this.createParallelogram()
} else if (shape === 'roundedRectangle') {
// 圆角矩形
node = this.createRoundedRectangle()
} else if (shape === 'octagonalRectangle') {
// 八角矩形
node = this.createOctagonalRectangle()
} else if (shape === 'outerTriangularRectangle') {
// 外三角矩形
node = this.createOuterTriangularRectangle()
} else if (shape === 'innerTriangularRectangle') {
// 内三角矩形
node = this.createInnerTriangularRectangle()
} else if (shape === 'ellipse') {
// 椭圆
node = this.createEllipse()
} else if (shape === 'circle') {
// 圆
node = this.createCircle()
}
return node
}
/**
* @Author: 王林
* @Date: 2022-09-04 09:08:54
* @Desc: 创建菱形
*/
createDiamond() {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
let topX = halfWidth
let topY = 0
let rightX = width
let rightY = halfHeight
let bottomX = halfWidth
let bottomY = height
let leftX = 0
let leftY = halfHeight
return this.node.group.polygon(`
${topX}, ${topY}
${rightX}, ${rightY}
${bottomX}, ${bottomY}
${leftX}, ${leftY}
`)
}
/**
* @Author: 王林
* @Date: 2022-09-03 16:14:12
* @Desc: 创建平行四边形
*/
createParallelogram() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${paddingX}, ${0}
${width}, ${0}
${width - paddingX}, ${height}
${0}, ${height}
`)
}
/**
* @Author: 王林
* @Date: 2022-09-03 16:50:23
* @Desc: 创建圆角矩形
*/
createRoundedRectangle() {
let { width, height } = this.node
let halfHeight = height / 2
return this.node.group.path(`
M${halfHeight},0
L${width - halfHeight},0
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
L${halfHeight},${height}
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 16:14:08
* @Desc: 创建八角矩形
*/
createOctagonalRectangle() {
let w = 5
let { width, height } = this.node
return this.node.group.polygon(`
${0}, ${w}
${w}, ${0}
${width - w}, ${0}
${width}, ${w}
${width}, ${height - w}
${width - w}, ${height}
${w}, ${height}
${0}, ${height - w}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 20:55:50
* @Desc: 创建外三角矩形
*/
createOuterTriangularRectangle() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${paddingX}, ${0}
${width - paddingX}, ${0}
${width}, ${height / 2}
${width - paddingX}, ${height}
${paddingX}, ${height}
${0}, ${height / 2}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 20:59:37
* @Desc: 创建内三角矩形
*/
createInnerTriangularRectangle() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${0}, ${0}
${width}, ${0}
${width - paddingX / 2}, ${height / 2}
${width}, ${height}
${0}, ${height}
${paddingX / 2}, ${height / 2}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:06:31
* @Desc: 创建椭圆
*/
createEllipse() {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return this.node.group.path(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:14:04
* @Desc: 创建圆
*/
createCircle() {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return this.node.group.path(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
}
// 形状列表
export const shapeList = ['rectangle', 'diamond', 'parallelogram', 'roundedRectangle', 'octagonalRectangle', 'outerTriangularRectangle', 'innerTriangularRectangle', 'ellipse', 'circle']

View File

@@ -66,7 +66,27 @@ class Style {
}
}
// 优先使用节点本身的样式
return this.ctx.nodeData.data[prop] !== undefined ? this.ctx.nodeData.data[prop] : defaultConfig[prop]
return this.getSelfStyle(prop) !== undefined ? this.getSelfStyle(prop) : defaultConfig[prop]
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:55:57
* @Desc: 获取某个样式值
*/
getStyle(prop, root, isActive) {
return this.merge(prop, root, isActive)
}
/**
* javascript comment
* @Author: flydreame
* @Date: 2022-09-17 12:09:39
* @Desc: 获取自身自定义样式
*/
getSelfStyle(prop) {
return this.ctx.nodeData.data[prop]
}
/**
@@ -75,13 +95,24 @@ class Style {
* @Desc: 矩形
*/
rect(node) {
this.shape(node)
node.radius(this.merge('borderRadius'))
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 15:04:28
* @Desc: 矩形外的其他形状
*/
shape(node) {
node.fill({
color: this.merge('fillColor')
}).stroke({
color: this.merge('borderColor'),
width: this.merge('borderWidth'),
dasharray: this.merge('borderDasharray')
}).radius(this.merge('borderRadius'))
})
}
/**
@@ -152,8 +183,8 @@ class Style {
* @Date: 2021-04-11 14:50:49
* @Desc: 连线
*/
line(node) {
node.stroke({ width: this.merge('lineWidth', true), color: this.merge('lineColor', true) }).fill({ color: 'none' })
line(node, { width, color, dasharray } = {}) {
node.stroke({ width, color, dasharray }).fill({ color: 'none' })
}
/**

View File

@@ -231,7 +231,7 @@ class CatalogOrganization extends Base {
* @Date: 2021-04-11 14:42:48
* @Desc: 绘制连线,连接该节点到其子节点
*/
renderLine(node, lines) {
renderLine(node, lines, style) {
if (node.children.length <= 0) {
return [];
}
@@ -263,6 +263,7 @@ class CatalogOrganization extends Base {
let path = `M ${x2},${y1 + s1} L ${x2},${y1 + s1 > y2 ? y2 + item.height : y2}`
// 竖线
lines[index].plot(path)
style && style(lines[index], item)
})
minx = Math.min(minx, x1)
maxx = Math.max(maxx, x1)
@@ -271,12 +272,14 @@ class CatalogOrganization extends Base {
node.style.line(line1)
line1.plot(`M ${x1},${y1} L ${x1},${y1 + s1}`)
node._lines.push(line1)
style && style(line1, node)
// 水平线
if (len > 0) {
let lin2 = this.draw.path()
node.style.line(lin2)
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
node._lines.push(lin2)
style && style(lin2, node)
}
} else {
// 非根节点
@@ -320,6 +323,7 @@ class CatalogOrganization extends Base {
path = `M ${x2},${y2} L ${_left},${y2}`
}
lines[index].plot(path)
style && style(lines[index], item)
})
// 竖线
if (len > 0) {
@@ -333,6 +337,7 @@ class CatalogOrganization extends Base {
lin2.show()
}
node._lines.push(lin2)
style && style(lin2, node)
}
}
}

View File

@@ -146,7 +146,7 @@ class LogicalStructure extends Base {
* @Date: 2021-04-11 14:42:48
* @Desc: 绘制连线,连接该节点到其子节点
*/
renderLine(node, lines) {
renderLine(node, lines, style) {
if (node.children.length <= 0) {
return [];
}
@@ -169,6 +169,7 @@ class LogicalStructure extends Base {
path = this.cubicBezierPath(x1, y1, x2, y2)
}
lines[index].plot(path)
style && style(lines[index], item)
})
}

View File

@@ -182,7 +182,7 @@ class MindMap extends Base {
* @Date: 2021-04-11 14:42:48
* @Desc: 绘制连线,连接该节点到其子节点
*/
renderLine(node, lines) {
renderLine(node, lines, style) {
if (node.children.length <= 0) {
return [];
}
@@ -205,6 +205,7 @@ class MindMap extends Base {
path = this.cubicBezierPath(x1, y1, x2, y2)
}
lines[index].plot(path)
style && style(lines[index], item)
})
}

View File

@@ -147,7 +147,7 @@ class OrganizationStructure extends Base {
* @Date: 2021-04-11 14:42:48
* @Desc: 绘制连线,连接该节点到其子节点
*/
renderLine(node, lines) {
renderLine(node, lines, style) {
if (node.children.length <= 0) {
return [];
}
@@ -177,6 +177,7 @@ class OrganizationStructure extends Base {
}
let path = `M ${x2},${y1 + s1} L ${x2},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
})
minx = Math.min(x1, minx)
maxx = Math.max(x1, maxx)
@@ -186,12 +187,14 @@ class OrganizationStructure extends Base {
expandBtnSize = len > 0 && !isRoot ? expandBtnSize : 0
line1.plot(`M ${x1},${y1 + expandBtnSize} L ${x1},${y1 + s1}`)
node._lines.push(line1)
style && style(line1, node)
// 水平线
if (len > 0) {
let lin2 = this.draw.path()
node.style.line(lin2)
lin2.plot(`M ${minx},${y1 + s1} L ${maxx},${y1 + s1}`)
node._lines.push(lin2)
style && style(lin2, node)
}
}

View File

@@ -0,0 +1,173 @@
import JSZip from "jszip";
import xmlConvert from "xml-js";
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-21 14:07:47
* @Desc: 解析.xmind文件
*/
const parseXmindFile = (file) => {
return new Promise((resolve, reject) => {
JSZip.loadAsync(file).then(
async (zip) => {
try {
let content = "";
if (zip.files["content.json"]) {
let json = await zip.files["content.json"].async("string");
content = transformXmind(json);
} else if (zip.files["content.xml"]) {
let xml = await zip.files["content.xml"].async("string");
let json = xmlConvert.xml2json(xml);
content = transformOldXmind(json);
}
if (content) {
resolve(content);
} else {
reject(new Error("解析失败"));
}
} catch (error) {
reject(error);
}
},
(e) => {
reject(e);
}
);
});
};
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-21 18:57:25
* @Desc: 转换xmind数据
*/
const transformXmind = (content) => {
let data = JSON.parse(content)[0];
let nodeTree = data.rootTopic;
let newTree = {};
let walk = (node, newNode) => {
newNode.data = {
// 节点内容
text: node.title,
};
// 节点备注
if (node.notes) {
newNode.data.note = (node.notes.realHTML || node.notes.plain).content;
}
// 超链接
if (node.href && /^https?:\/\//.test(node.href)) {
newNode.data.hyperlink = node.href;
}
// 标签
if (node.labels && node.labels.length > 0) {
newNode.data.tag = node.labels;
}
// 子节点
newNode.children = [];
if (
node.children &&
node.children.attached &&
node.children.attached.length > 0
) {
node.children.attached.forEach((item) => {
let newChild = {};
newNode.children.push(newChild);
walk(item, newChild);
});
}
};
walk(nodeTree, newTree);
return newTree;
};
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-23 15:51:51
* @Desc: 转换旧版xmind数据xmind8
*/
const transformOldXmind = (content) => {
let data = JSON.parse(content);
let elements = data.elements;
let root = null;
let getRoot = (arr) => {
for (let i = 0; i < arr.length; i++) {
if (!root && arr[i].name === "topic") {
root = arr[i];
return;
}
}
arr.forEach((item) => {
getRoot(item.elements);
});
};
getRoot(elements);
let newTree = {};
let getItemByName = (arr, name) => {
return arr.find((item) => {
return item.name === name;
});
};
let walk = (node, newNode) => {
let nodeElements = node.elements;
newNode.data = {
// 节点内容
text: getItemByName(nodeElements, "title").elements[0].text,
};
try {
// 节点备注
let notesElement = getItemByName(nodeElements, "notes");
if (notesElement) {
newNode.data.note =
notesElement.elements[0].elements[0].elements[0].text;
}
} catch (error) {}
try {
// 超链接
if (
node.attributes &&
node.attributes["xlink:href"] &&
/^https?:\/\//.test(node.attributes["xlink:href"])
) {
newNode.data.hyperlink = node.attributes["xlink:href"];
}
} catch (error) {}
try {
// 标签
let labelsElement = getItemByName(nodeElements, "labels");
if (labelsElement) {
newNode.data.tag = labelsElement.elements.map((item) => {
return item.elements[0].text;
});
}
} catch (error) {}
// 子节点
newNode.children = [];
let _children = getItemByName(nodeElements, "children");
if (_children && _children.elements && _children.elements.length > 0) {
_children.elements.forEach((item) => {
if (item.name === "topics") {
item.elements.forEach((item2) => {
let newChild = {};
newNode.children.push(newChild);
walk(item2, newChild);
});
} else {
let newChild = {};
newNode.children.push(newChild);
walk(item, newChild);
}
});
}
};
walk(root, newTree);
return newTree;
};
export default {
parseXmindFile,
transformXmind,
transformOldXmind,
};

View File

@@ -17,6 +17,8 @@ export default {
lineWidth: 1,
// 连线的颜色
lineColor: '#549688',
// 连线样式
lineDasharray: 'none',
// 概要连线的粗细
generalizationLineWidth: 1,
// 概要连线的颜色
@@ -33,6 +35,7 @@ export default {
backgroundRepeat: 'no-repeat',
// 根节点样式
root: {
shape: 'rectangle',
fillColor: '#549688',
fontFamily: '微软雅黑, Microsoft YaHei',
color: '#fff',
@@ -53,6 +56,7 @@ export default {
},
// 二级节点样式
second: {
shape: 'rectangle',
marginX: 100,
marginY: 40,
fillColor: '#fff',
@@ -75,6 +79,7 @@ export default {
},
// 三级及以下节点样式
node: {
shape: 'rectangle',
marginX: 50,
marginY: 0,
fillColor: 'transparent',
@@ -97,6 +102,7 @@ export default {
},
// 概要节点样式
generalization: {
shape: 'rectangle',
marginX: 100,
marginY: 40,
fillColor: '#fff',
@@ -117,4 +123,8 @@ export default {
borderDasharray: 'none',
}
}
}
}
// 支持激活样式的属性
// 简单来说,会改变节点大小的都不支持在激活时设置,为了性能考虑,节点切换激活态时不会重新计算节点大小
export const supportActiveStyle = ['fillColor', 'color', 'fontWeight', 'fontStyle', 'borderColor', 'borderWidth', 'borderDasharray', 'borderRadius', 'textDecoration']

View File

@@ -311,4 +311,44 @@ export const shortcutKeyList = [
}
]
}
]
// 形状列表
export const shapeList = [
{
name: '矩形',
value: 'rectangle'
},
{
name: '菱形',
value: 'diamond'
},
{
name: '平行四边形',
value: 'parallelogram'
},
{
name: '圆角矩形',
value: 'roundedRectangle'
},
{
name: '八角矩形',
value: 'octagonalRectangle'
},
{
name: '外三角矩形',
value: 'outerTriangularRectangle'
},
{
name: '内三角矩形',
value: 'innerTriangularRectangle'
},
{
name: '椭圆',
value: 'ellipse'
},
{
name: '圆',
value: 'circle'
}
]

View File

@@ -101,10 +101,10 @@
<span class="name">颜色</span>
<span
class="block"
v-popover:popover
v-popover:popover2
:style="{ backgroundColor: style.generalizationLineColor }"
></span>
<el-popover ref="popover" placement="bottom" trigger="click">
<el-popover ref="popover2" placement="bottom" trigger="click">
<Color
:color="style.generalizationLineColor"
@change="

View File

@@ -1,6 +1,6 @@
<template>
<div
class="contextmenuContainer"
class="contextmenuContainer listBox"
v-if="isShow"
:style="{ left: left + 'px', top: top + 'px' }"
>
@@ -62,6 +62,12 @@
<div class="item" @click="exec('RETURN_CENTER')">回到中心</div>
<div class="item" @click="exec('EXPAND_ALL')">展开所有</div>
<div class="item" @click="exec('UNEXPAND_ALL')">收起所有</div>
<div class="item">
展开到
<div class="subItems listBox">
<div class="item" v-for="(item, index) in expandList" :key="item" @click="exec('UNEXPAND_TO_LEVEL', false, index + 1)">{{item}}</div>
</div>
</div>
<div class="item" @click="exec('RESET_LAYOUT')">
一键整理布局
<span class="desc">Ctrl + L</span>
@@ -93,7 +99,8 @@ export default {
type: "",
isMousedown: false,
mosuedownX: 0,
mosuedownY: 0
mosuedownY: 0,
expandList: ['一级主题', '二级主题', '三级主题', '四级主题', '五级主题', '六级主题']
};
},
computed: {
@@ -221,7 +228,7 @@ export default {
* @Date: 2021-07-14 23:27:54
* @Desc: 执行命令
*/
exec(key, disabled) {
exec(key, disabled, ...args) {
if (disabled) {
return;
}
@@ -241,7 +248,7 @@ export default {
this.mindMap.view.reset();
break;
default:
this.$bus.$emit("execCommand", key);
this.$bus.$emit("execCommand", key, ...args);
break;
}
this.hide();
@@ -278,20 +285,23 @@ export default {
</script>
<style lang="less" scoped>
.contextmenuContainer {
position: fixed;
.listBox {
width: 200px;
background: #fff;
box-shadow: 0 4px 12px 0 hsla(0, 0%, 69%, 0.5);
border-radius: 4px;
padding-top: 16px;
padding-bottom: 16px;
}
.contextmenuContainer {
position: fixed;
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #1a1a1a;
.item {
position: relative;
height: 28px;
line-height: 28px;
padding: 0 16px;
@@ -305,6 +315,10 @@ export default {
&:hover {
background: #f5f5f5;
.subItems {
visibility: visible;
}
}
&.disabled {
@@ -320,6 +334,13 @@ export default {
.desc {
color: #999;
}
.subItems {
position: absolute;
left: 100%;
top: 0;
visibility: hidden;
}
}
}
</style>

View File

@@ -215,7 +215,11 @@ export default {
* @Desc: 动态设置思维导图数据
*/
setData(data) {
this.mindMap.setData(data)
if (data.root) {
this.mindMap.setFullData(data)
} else {
this.mindMap.setData(data)
}
this.manualSave()
},

View File

@@ -3,21 +3,22 @@
class="nodeDialog"
title="导出"
:visible.sync="dialogVisible"
width="500"
width="700px"
>
<div>
<div class="nameInputBox">
<span class="name">导出文件名称</span>
<el-input style="width: 300px" v-model="fileName" size="mini"></el-input>
<el-checkbox v-show="['smm', 'json'].includes(exportType)" v-model="widthConfig" style="margin-left: 12px">是否包含主题结构等配置数据</el-checkbox>
</div>
<el-radio-group v-model="exportType">
<el-radio label="smm">专有文件.smm</el-radio>
<el-radio label="json">json文件.json</el-radio>
<el-radio label="png">图片文件.png</el-radio>
<el-radio label="svg">svg文件.svg</el-radio>
<el-radio label="pdf">pdf文件.pdf</el-radio>
<el-radio-group v-model="exportType" size="mini">
<el-radio-button label="smm">专有文件.smm</el-radio-button>
<el-radio-button label="json">json文件.json</el-radio-button>
<el-radio-button label="png">图片文件.png</el-radio-button>
<el-radio-button label="svg">svg文件.svg</el-radio-button>
<el-radio-button label="pdf">pdf文件.pdf</el-radio-button>
</el-radio-group>
<div class="tip">tips.smm文件可用于导入</div>
<div class="tip">tips.smm.json文件可用于导入</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="cancel"> </el-button>
@@ -38,7 +39,8 @@ export default {
return {
dialogVisible: false,
exportType: "smm",
fileName: '思维导图'
fileName: '思维导图',
widthConfig: true
};
},
created() {
@@ -62,7 +64,7 @@ export default {
* @Desc: 确定
*/
confirm() {
this.$bus.$emit("export", this.exportType, true, this.fileName);
this.$bus.$emit("export", this.exportType, true, this.fileName, this.widthConfig);
this.$notify.info({
title: '消息',
message: '如果没有触发下载,请检查是否被浏览器拦截了'

View File

@@ -1,8 +1,8 @@
<template>
<el-dialog class="nodeDialog" title="导入" :visible.sync="dialogVisible" width="500">
<el-dialog class="nodeDialog" title="导入" :visible.sync="dialogVisible" width="300px">
<el-upload ref="upload" action="x" :file-list="fileList" :auto-upload="false" :multiple="false" :on-change="onChange" :limit="1" :on-exceed="onExceed">
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<div slot="tip" class="el-upload__tip">只能上传.smm文件</div>
<div slot="tip" class="el-upload__tip">支持.smm.json.xmind文件</div>
</el-upload>
<span slot="footer" class="dialog-footer">
<el-button @click="cancel"> </el-button>
@@ -12,6 +12,8 @@
</template>
<script>
import MindMap from 'simple-mind-map'
/**
* @Author: 王林
* @Date: 2021-06-24 22:53:54
@@ -44,9 +46,9 @@ export default {
* @Desc: 文件选择
*/
onChange(file) {
let reg = /\.smm$/;
let reg = /\.(smm|xmind|json)$/;
if (!reg.test(file.name)) {
this.$message.error("请选择.smm文件");
this.$message.error("请选择.smm、.json、.xmind文件");
this.fileList = [];
} else {
this.fileList.push(file)
@@ -81,6 +83,15 @@ export default {
return this.$message.error("请选择要导入的文件");
}
let file = this.fileList[0];
if (/\.(smm|json)$/.test(file.name)) {
this.handleSmm(file)
} else if (/\.xmind$/.test(file.name)) {
this.handleXmind(file)
}
this.cancel();
},
handleSmm(file) {
let fileReader = new FileReader()
fileReader.readAsText(file.raw)
fileReader.onload = (evt) => {
@@ -96,8 +107,18 @@ export default {
this.$message.error("文件解析失败");
}
}
this.cancel();
},
async handleXmind(file) {
try {
let data = await MindMap.xmind.parseXmindFile(file.raw)
this.$bus.$emit('setData', data)
this.$message.success("导入成功");
} catch (error) {
console.log(error)
this.$message.error("文件解析失败");
}
}
},
};
</script>

View File

@@ -15,6 +15,7 @@
size="mini"
v-model="style.fontFamily"
placeholder=""
:disabled="checkDisabled('fontFamily')"
@change="update('fontFamily')"
>
<el-option
@@ -36,6 +37,7 @@
style="width: 80px"
v-model="style.fontSize"
placeholder=""
:disabled="checkDisabled('fontSize')"
@change="update('fontSize')"
>
<el-option
@@ -54,6 +56,7 @@
style="width: 80px"
v-model="style.lineHeight"
placeholder=""
:disabled="checkDisabled('lineHeight')"
@change="update('lineHeight')"
>
<el-option
@@ -69,7 +72,7 @@
<div class="row">
<div class="btnGroup">
<el-tooltip content="颜色" placement="bottom">
<div class="styleBtn" v-popover:popover>
<div class="styleBtn" v-popover:popover :class="{ disabled: checkDisabled('color') }">
A
<span
class="colorShow"
@@ -80,7 +83,7 @@
<el-tooltip content="加粗" placement="bottom">
<div
class="styleBtn"
:class="{ actived: style.fontWeight === 'bold' }"
:class="{ actived: style.fontWeight === 'bold', disabled: checkDisabled('fontWeight') }"
@click="toggleFontWeight"
>
B
@@ -89,7 +92,7 @@
<el-tooltip content="斜体" placement="bottom">
<div
class="styleBtn i"
:class="{ actived: style.fontStyle === 'italic' }"
:class="{ actived: style.fontStyle === 'italic', disabled: checkDisabled('fontStyle') }"
@click="toggleFontStyle"
>
I
@@ -99,16 +102,17 @@
<div
class="styleBtn u"
:style="{ textDecoration: style.textDecoration || 'none' }"
:class="{ disabled: checkDisabled('textDecoration') }"
v-popover:popover2
>
U
</div>
</el-tooltip>
</div>
<el-popover ref="popover" placement="bottom" trigger="click">
<el-popover ref="popover" placement="bottom" trigger="click" :disabled="checkDisabled('color')">
<Color :color="style.color" @change="changeFontColor"></Color>
</el-popover>
<el-popover ref="popover2" placement="bottom" trigger="click">
<el-popover ref="popover2" placement="bottom" trigger="click" :disabled="checkDisabled('textDecoration')">
<el-radio-group
size="mini"
v-model="style.textDecoration"
@@ -129,8 +133,9 @@
class="block"
v-popover:popover3
:style="{ width: '80px', backgroundColor: style.borderColor }"
:class="{ disabled: checkDisabled('borderColor') }"
></span>
<el-popover ref="popover3" placement="bottom" trigger="click">
<el-popover ref="popover3" placement="bottom" trigger="click" :disabled="checkDisabled('borderColor')">
<Color
:color="style.borderColor"
@change="changeBorderColor"
@@ -138,12 +143,13 @@
</el-popover>
</div>
<div class="rowItem">
<span class="name" v-popover:popover5>样式</span>
<span class="name">样式</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.borderDasharray"
placeholder=""
:disabled="checkDisabled('borderDasharray')"
@change="update('borderDasharray')"
>
<el-option
@@ -164,6 +170,7 @@
style="width: 80px"
v-model="style.borderWidth"
placeholder=""
:disabled="checkDisabled('borderWidth')"
@change="update('borderWidth')"
>
<el-option
@@ -182,6 +189,7 @@
style="width: 80px"
v-model="style.borderRadius"
placeholder=""
:disabled="checkDisabled('borderRadius')"
@change="update('borderRadius')"
>
<el-option
@@ -203,12 +211,95 @@
class="block"
v-popover:popover4
:style="{ backgroundColor: style.fillColor }"
:class="{ disabled: checkDisabled('fillColor') }"
></span>
<el-popover ref="popover4" placement="bottom" trigger="click">
<el-popover ref="popover4" placement="bottom" trigger="click" :disabled="checkDisabled('fillColor')">
<Color :color="style.fillColor" @change="changeFillColor"></Color>
</el-popover>
</div>
</div>
<!-- 形状 -->
<div class="title">形状</div>
<div class="row">
<div class="rowItem">
<span class="name">形状</span>
<el-select
size="mini"
style="width: 120px"
v-model="style.shape"
placeholder=""
:disabled="checkDisabled('shape')"
@change="update('shape')"
>
<el-option
v-for="item in shapeList"
:key="item.value"
:label="item.name"
:value="item.value"
>
</el-option>
</el-select>
</div>
</div>
<!-- 线条 -->
<div class="title">线条</div>
<div class="row">
<div class="rowItem">
<span class="name">颜色</span>
<span
class="block"
v-popover:popover5
:style="{ width: '80px', backgroundColor: style.lineColor }"
:class="{ disabled: checkDisabled('lineColor') }"
></span>
<el-popover ref="popover5" placement="bottom" trigger="click" :disabled="checkDisabled('lineColor')">
<Color
:color="style.lineColor"
@change="changeLineColor"
></Color>
</el-popover>
</div>
<div class="rowItem">
<span class="name">样式</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.lineDasharray"
placeholder=""
:disabled="checkDisabled('lineDasharray')"
@change="update('lineDasharray')"
>
<el-option
v-for="item in borderDasharrayList"
:key="item.value"
:label="item.name"
:value="item.value"
>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">宽度</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.lineWidth"
placeholder=""
:disabled="checkDisabled('lineWidth')"
@change="update('lineWidth')"
>
<el-option
v-for="item in borderWidthList"
:key="item"
:label="item"
:value="item"
>
</el-option>
</el-select>
</div>
</div>
<!-- 节点内边距 -->
<div class="title noTop">节点内边距</div>
<div class="row">
@@ -217,6 +308,7 @@
<el-slider
style="width: 200px"
v-model="style.paddingX"
:disabled="checkDisabled('paddingX')"
@change="update('paddingX')"
></el-slider>
</div>
@@ -227,6 +319,7 @@
<el-slider
style="width: 200px"
v-model="style.paddingY"
:disabled="checkDisabled('paddingY')"
@change="update('paddingY')"
></el-slider>
</div>
@@ -246,7 +339,9 @@ import {
borderDasharrayList,
borderRadiusList,
lineHeightList,
shapeList,
} from "@/config";
import { supportActiveStyle } from 'simple-mind-map/src/themes/default';
/**
* @Author: 王林
@@ -261,6 +356,8 @@ export default {
},
data() {
return {
supportActiveStyle,
shapeList,
fontFamilyList,
fontSizeList,
borderWidthList,
@@ -270,6 +367,7 @@ export default {
activeNodes: [],
activeTab: "normal",
style: {
shape: '',
paddingX: 0,
paddingY: 0,
color: "",
@@ -284,16 +382,19 @@ export default {
fillColor: "",
borderDasharray: "",
borderRadius: "",
lineColor: "",
lineDasharray: "",
lineWidth: "",
},
};
},
created() {
this.$bus.$on("node_active", (...args) => {
this.$refs.sidebar.show = false;
if (this.$refs.sidebar) this.$refs.sidebar.show = false;
this.$nextTick(() => {
this.activeTab = "normal";
this.activeNodes = args[1];
this.$refs.sidebar.show = this.activeNodes.length > 0;
if (this.$refs.sidebar) this.$refs.sidebar.show = this.activeNodes.length > 0;
this.initNodeStyle();
});
});
@@ -308,6 +409,15 @@ export default {
this.initNodeStyle();
},
/**
* @Author: 王林
* @Date: 2022-09-12 22:16:56
* @Desc: 检查是否禁用
*/
checkDisabled(prop) {
return this.activeTab === 'active' && !this.supportActiveStyle.includes(prop)
},
/**
* @Author: 王林
* @Date: 2021-05-05 09:48:52
@@ -319,6 +429,7 @@ export default {
return;
}
[
"shape",
"paddingX",
"paddingY",
"color",
@@ -333,6 +444,9 @@ export default {
"fillColor",
"borderDasharray",
"borderRadius",
"lineColor",
"lineDasharray",
"lineWidth",
].forEach((item) => {
this.style[item] = this.activeNodes[0].getStyle(
item,
@@ -401,6 +515,16 @@ export default {
this.update("borderColor");
},
/**
* @Author: flydreame
* @Date: 2022-09-17 10:18:15
* @Desc: 修改线条颜色
*/
changeLineColor(color) {
this.style.lineColor = color;
this.update("lineColor");
},
/**
* @Author: 王林
* @Date: 2021-05-05 10:18:59
@@ -472,6 +596,13 @@ export default {
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
&.disabled {
background-color: #F5F7FA !important;
border-color: #E4E7ED !important;
color: #C0C4CC !important;
cursor: not-allowed !important;
}
}
}
@@ -492,6 +623,13 @@ export default {
background-color: #eee;
}
&.disabled {
background-color: #F5F7FA !important;
border-color: #E4E7ED !important;
color: #C0C4CC !important;
cursor: not-allowed !important;
}
&.i {
font-style: italic;
}

View File

@@ -106,7 +106,7 @@
<div
class="toolbarBtn"
:class="{
disabled: activeNodes.length <= 0 || hasRoot,
disabled: activeNodes.length <= 0 || hasRoot || hasGeneralization,
}"
@click="$bus.$emit('execCommand', 'ADD_GENERALIZATION')"
>
@@ -194,14 +194,14 @@ export default {
},
computed: {
hasRoot() {
return this.activeNodes.find((node) => {
return this.activeNodes.findIndex((node) => {
return node.isRoot;
});
}) !== -1;
},
hasGeneralization() {
return this.activeNodes.find((node) => {
return this.activeNodes.findIndex((node) => {
return node.isGeneralization;
});
}) !== -1;;
}
},
created() {