Compare commits

...

207 Commits
0.12.2 ... main

Author SHA1 Message Date
街角小林
1c5a243c2f update readme 2026-03-09 09:15:15 +08:00
街角小林
3541b7df40 update 2026-03-04 08:59:15 +08:00
街角小林
9eab537b2e
Merge pull request #1254 from cuikaipeng/readme
Update README.md
2026-01-04 08:50:33 +08:00
街角小林
31d71db611
Merge branch 'main' into readme 2026-01-04 08:50:25 +08:00
街角小林
cfe07aa32e update 2025-09-23 14:44:52 +08:00
街角小林
b8ac079009 update 2025-08-28 08:52:06 +08:00
街角小林
040ce6601b update 2025-08-01 14:44:57 +08:00
街角小林
77dd62477e update 2025-07-06 22:11:15 +08:00
街角小林
b831d95063 打包Demo 2025-07-04 18:44:51 +08:00
街角小林
bff6024e2e Demo:修复双击节点图片预览图片不显示的问题;修复节点图片弹窗中图片不显示的问题 2025-07-04 18:43:05 +08:00
街角小林
919b1517d9 Feat:节点实例新增获取图片真实url的方法 2025-07-04 18:42:49 +08:00
街角小林
2cbd08c532 update 2025-07-03 11:33:39 +08:00
cuikaipeng
c26cc5af83 Update README.md 2025-05-27 10:42:05 +08:00
街角小林
5e300c0320 update 2025-05-23 10:10:33 +08:00
街角小林
714567a733 update 2025-05-15 09:27:17 +08:00
街角小林
28d6bb7d90 update 2025-05-15 08:57:26 +08:00
街角小林
ecb2fbab48 打包demo 2025-05-14 10:04:43 +08:00
街角小林
a8c13b8f9a update 2025-05-14 09:52:27 +08:00
街角小林
475dd4754a update 2025-05-13 10:30:30 +08:00
街角小林
8a59185156 打包Demo 2025-04-22 11:49:53 +08:00
街角小林
80ca74e477 Demo:部分功能转为网页版试用功能 2025-04-22 11:47:58 +08:00
街角小林
c67bebb384 update 2025-04-22 09:47:22 +08:00
街角小林
3a9002821c Feat:新增越南语翻译 2025-04-22 09:46:51 +08:00
街角小林
d0b289ed28
Merge pull request #1217 from googlesky/main
Feat: Add Vietnamese language support and update i18n configuration
2025-04-22 08:56:00 +08:00
wanglin2
84782f924b Demo:优化导出弹窗的界面 2025-04-20 22:33:27 +08:00
googlesky
2dd3db4c9d Feat: Add Vietnamese language support and update i18n configuration 2025-04-20 11:15:17 +07:00
wanglin2
eb61e24746 Fix:修复导入md时,如果存在嵌套的无序列表是解析异常的问题 2025-04-20 10:55:15 +08:00
wanglin2
abb332fd46 Fix:修复在刚创建的节点中选中文本时富文本工具栏不出现的问题 2025-04-20 10:29:15 +08:00
wanglin2
0c4fadb211 Demo:1.优化侧边栏的样式;2.修复初始结构为带鱼头的结构时节点样式形状列表出现空项的问题 2025-04-20 10:13:05 +08:00
wanglin2
cd361c1f6e Demo:右侧侧边栏的AI和快捷键按钮移到右下角的更多按钮中 2025-04-20 09:51:35 +08:00
wanglin2
e0dc13c9f8 Demo:去掉文字平滑样式,避免在mac电脑上文字看不清的问题;所有弹窗增加圆角 2025-04-20 09:49:54 +08:00
街角小林
670114d8d8 Feat:新增自定义内容的节点的内容更新方法 2025-04-18 17:45:19 +08:00
街角小林
c5b5fd86de Fix:自定义节点内容忽略resetRichText字段,避免无意义的重新渲染 2025-04-18 17:42:14 +08:00
wanglin2
493e0da7ae 打包Demo 2025-04-13 13:47:23 +08:00
wanglin2
896121f6b6 Demo:支持设置是否开启演示模式的填空功能 2025-04-13 13:42:18 +08:00
wanglin2
b79076baa3 Feat:演示插件配置同步思维导图的选项配置 2025-04-13 13:41:09 +08:00
wanglin2
715627727e Demo:修复粘贴导入md后弹窗没有关闭的问题 2025-04-13 10:24:03 +08:00
街角小林
5ed5f0ff0d 打包0.14.0-fix.1 2025-04-10 19:04:32 +08:00
街角小林
c12189ca87 Demo:支持粘贴md内容进行导入 2025-04-10 18:57:02 +08:00
街角小林
be38eb2ca6 Fix:修复节点数量很多的情况下,导出图片节点显示不全的问题 2025-04-10 18:08:39 +08:00
街角小林
e80890aa7e 打包Demo 2025-04-10 09:11:33 +08:00
wanglin2
e0ca3a5d12 Fix:ai服务端口被占用后不启动,避免报错 2025-04-09 22:25:42 +08:00
街角小林
30404721fa '打包Demo' 2025-04-09 09:20:32 +08:00
wanglin2
c4565143e8 打包0.14.0 2025-04-08 22:02:16 +08:00
wanglin2
2b25e28cb8 Demo:修复激活节点,移动画布就关闭侧边栏的问题 2025-04-08 21:57:01 +08:00
街角小林
b543cfde38 Update README 2025-04-08 17:41:42 +08:00
wanglin2
f36bcbe39a Demo:AI续写支持修改提示词 2025-04-07 20:46:05 +08:00
wanglin2
328aef5308 Demo:支持添加和删除节点链接 2025-04-07 20:19:01 +08:00
街角小林
697cee0b46 Demo:支持节点链接开发中 2025-04-07 17:45:09 +08:00
街角小林
0f9ae45784 update README 2025-04-07 17:01:22 +08:00
街角小林
dcf4234ce2 Feat:支持添加库后置内容 2025-04-07 16:59:37 +08:00
wanglin2
f9406011e2 Demo:导出图片支持选择格式 2025-04-06 20:20:48 +08:00
wanglin2
7d4acd15d0 Feat:支持导出jpg格式 2025-04-06 20:19:01 +08:00
wanglin2
6d729c53ab Feat:导出图片和pdf支持设置是否显示完整背景图片 2025-04-06 17:09:36 +08:00
wanglin2
08df73aec4 Demo:导出图片支持选择是否显示完整背景图片 2025-04-06 17:08:57 +08:00
wanglin2
f9eff11a27 Demo:支持扩展节点形状列表 2025-04-06 14:59:08 +08:00
wanglin2
0146e43815 Demo:添加内置背景图片 2025-04-06 09:53:16 +08:00
wanglin2
cd2d5943c2 Demo:优化主题侧边栏的交互 2025-04-05 21:01:38 +08:00
wanglin2
121eba1799 Demo:支持扩展侧边主题列表 2025-04-05 20:48:00 +08:00
wanglin2
978c088d95 Demo:结构侧边栏样式调整 2025-04-03 23:34:22 +08:00
wanglin2
bb4a07b151 Feat:新增两种垂直时间轴结构 2025-04-03 21:50:43 +08:00
街角小林
7f0368c2c8 Feat:带鱼头鱼尾的鱼骨图二级节点定位优化 2025-04-03 17:16:32 +08:00
街角小林
402e0908b0 Feat:1.思维导图实例新增getSvgObjects方法;2.支持向右结构图2 2025-04-03 16:35:43 +08:00
街角小林
428ac15499 Feat:1.新增带鱼头鱼尾的鱼骨图结构;2.渲染节点连线逻辑去除对鱼骨图的硬编码 2025-04-03 11:53:56 +08:00
街角小林
0f9f057d65 Demo:新增向右鱼骨图 2025-04-03 10:05:14 +08:00
街角小林
a4fe5e7765 Feat:节点形状不存在时默认使用矩形形状 2025-04-03 10:04:41 +08:00
街角小林
1b7aad3de2 Feat:支持扩展节点形状 2025-04-03 09:50:01 +08:00
街角小林
866287402f Feat:支持向右鱼骨图结构 2025-04-02 09:47:11 +08:00
街角小林
3b10b2b229 Feat:支持从思维导图实例上读取结构类 2025-04-02 09:45:45 +08:00
街角小林
9661aa55c5 Feat:插件新增preload配置,支持在核心类实例化前加载 2025-04-02 09:44:42 +08:00
wanglin2
94ed53b31f Demo:修复画布边缘节点备注浮层显示不完全的问题 2025-03-30 16:04:08 +08:00
wanglin2
bd2cfda905 Demo:点击备注图标会在侧边栏形式备注内容 2025-03-30 15:55:52 +08:00
wanglin2
a0e54870b5 Demo:唤出搜索时自动聚焦搜索框 2025-03-30 15:27:39 +08:00
wanglin2
bb9dd123f1 Demo:外框样式设置改为侧边栏形式,并支持文字样式设置 2025-03-30 15:15:46 +08:00
wanglin2
e5ee2f19d1 Fix:修复修改关联线文字后文字位置没有更新的问题 2025-03-30 15:14:11 +08:00
wanglin2
87d1b95dd9 Feat:外框支持添加文字 2025-03-30 15:13:15 +08:00
街角小林
6f0face378 Feat:外框支持文本编辑,开发中 2025-03-28 17:52:01 +08:00
街角小林
37eab5f084 Feat:1.支持暂停检查鼠标是否在画布内;2.快捷键响应目标由硬编码改为由插件控制 2025-03-28 09:47:56 +08:00
街角小林
92d01e510b update 2025-03-28 09:22:47 +08:00
街角小林
324652b1ba Demo:修复存在多个外框时,激活某个外框时外框样式默认回填错误的问题 2025-03-27 18:10:57 +08:00
街角小林
43c41e7ed2 Feat:只读模式下节点实例不保存nodeDataSnapshot数据 2025-03-26 11:56:57 +08:00
街角小林
a55afdd252 Feat:如果没有注册Painter插件,那么节点实例不保存effectiveStyles数据 2025-03-26 11:38:48 +08:00
街角小林
8414d39c4c Feat:历史堆栈列表由存储对象改为存储字符串,减少内存占用 2025-03-25 19:03:41 +08:00
街角小林
e53e41dadc Feat:1.新增beforeAddHistory事件;2.新增处理base64格式存储的插件 2025-03-18 09:35:32 +08:00
街角小林
0321946b41 Demo:新增思维导图数据超过浏览器本地允许保存的上限提示 2025-03-14 09:42:22 +08:00
街角小林
6071c7e021 Demo: update 2025-03-14 09:26:30 +08:00
街角小林
17e9c29f1d Fix:修复性能模式下每次渲染还是会重新创建所有节点的问题 2025-03-14 09:24:17 +08:00
街角小林
d486cbd157 Feat:优化render逻辑 2025-03-13 17:53:44 +08:00
街角小林
70cc88efbd Fix:修复导入Markdown文件时对于加粗、代码等语法识别错误的问题 2025-03-12 16:39:31 +08:00
街角小林
0f6f714303 Demo:1.接管模式新增读取和存储配置的方法;2.获取和保存区分思维导图数据和思维导图配置 2025-03-12 09:59:14 +08:00
街角小林
45418d803c Fix:导出png、svg、pdf时先结束当前正在进行的文本编辑,防止导出的节点文本显示空白的问题 2025-03-12 09:12:10 +08:00
街角小林
e5648728c4 Feat:构造函数新增扩展nodeDataNoStylePropList列表的静态方法 2025-03-11 17:35:29 +08:00
wanglin2
c387d78bfe 打包Demo 2025-03-05 22:07:05 +08:00
街角小林
469f5b26cd Fix:开启性能模式后,修复先移动画布再清空节点数据后画布上的节点还存在的问题 2025-03-05 18:08:19 +08:00
街角小林
53fecc062f Demo: update 2025-03-04 10:31:18 +08:00
街角小林
ec9f55e068 Demo:修复一些未知情况下在根节点上点击鼠标右键会同时显示两个右键菜单的问题 2025-03-03 17:58:36 +08:00
街角小林
76b5f7d22a Demo:当移动画布、删除图片、按住调整图片按钮时隐藏节点图片位置设置工具栏 2025-03-03 17:24:17 +08:00
街角小林
3b9cced7ea Fix:修复节点图片调整按钮当画布缩放时位置没有更新的问题;Feat:新增抛出两个事件 2025-03-03 17:23:03 +08:00
街角小林
c7cd19f956 Demo:优化代码,去除无用的注释、去除组件name、引入vue组件添加后缀 2025-03-03 16:58:50 +08:00
街角小林
2a8959fb3b Demo:只读模式不隐藏AI侧边栏按钮 2025-03-03 16:39:50 +08:00
街角小林
b836ca87b2 Demo:统一虚线样式值的风格,去掉无用的空格 2025-03-03 16:38:39 +08:00
街角小林
f1a97e4ced Fix:修复默认主题配置中关联线样式值类型为数组的问题,应为字符串 2025-03-03 16:37:22 +08:00
街角小林
3cb035e365 Demo:节点文字字号列表增加14号 2025-03-03 16:31:42 +08:00
街角小林
2001bdd3ff Fix:修复拖动关联线控制点更新后,再次激活关联线控制点位置显示错误的问题 2025-03-03 16:29:57 +08:00
街角小林
10e9fa3f22 update 2025-02-26 08:50:25 +08:00
街角小林
cbd57d2f36 Demo:放开AI功能入口 2025-02-21 09:47:01 +08:00
街角小林
cef586cc5c 打包Demo 2025-02-21 09:42:59 +08:00
街角小林
b0c0c58bec Demo:暂时隐藏AI功能入口 2025-02-21 09:41:28 +08:00
街角小林
ed2fed78f7 打包0.13.1-fix.2 2025-02-21 09:37:58 +08:00
街角小林
75322ddc20 Fix:回退修复文本编辑实时渲染模式编辑中展开收起按钮闪烁的问题的代码,避免带来更多更严重的问题 2025-02-21 09:31:59 +08:00
街角小林
2577da10d0 Demo:优化AI生成逻辑 2025-02-20 09:29:20 +08:00
街角小林
4f2d4f8e36 打包Demo 2025-02-19 11:53:29 +08:00
街角小林
6ec552d9fb Demo: update 2025-02-19 11:46:56 +08:00
街角小林
0ecae72fff 打包Demo 2025-02-19 11:10:10 +08:00
街角小林
91cdb24a62 Demo:ai能力默认关闭 2025-02-19 11:01:56 +08:00
街角小林
f8149ce383 update 2025-02-19 10:26:02 +08:00
wanglin2
b834b6fdd7 update 2025-02-18 22:18:00 +08:00
wanglin2
fe11d1152e Demo:优化ai生成逻辑 2025-02-18 22:08:34 +08:00
wanglin2
c27ca12489 update 2025-02-18 21:34:07 +08:00
街角小林
e05df4e92b Merge branch 'test' into feature 2025-02-18 17:30:45 +08:00
街角小林
ad63b4c72c Demo:初步接入AI生成思维导图和AI对话能力 2025-02-18 17:29:58 +08:00
街角小林
09e393b174 Feat:checkNodeOuter工具方法支持传递内边距参数 2025-02-18 09:45:44 +08:00
街角小林
43c7f0551a Fix:修复传入空数据时RichText插件会报错的问题 2025-02-18 09:12:55 +08:00
wanglin2
d0253ecf6c 打包0.13.1-fix.1 2025-02-14 22:22:37 +08:00
wanglin2
063742cb9b Fix:修复开启了实时渲染效果后删除一个节点的所有节点后会显示展开收起按钮并且不会消失的问题 2025-02-14 22:15:00 +08:00
街角小林
0a8b14ddd8 update 2025-02-14 17:45:22 +08:00
街角小林
1bdbe0881e 打包Demo 2025-02-14 09:31:08 +08:00
街角小林
bd04516a7c Merge branch 'main' of https://github.com/wanglin2/mind-map into main 2025-02-14 09:25:27 +08:00
街角小林
75742bef27 Demo:支持设置开启拖动画布的动量效果 2025-02-14 09:24:47 +08:00
wanglin2
31070c95fb update 2025-02-13 21:43:59 +08:00
街角小林
193ef7f776 打包0.13.1 2025-02-13 11:00:31 +08:00
街角小林
3d80c8698c update 2025-02-13 09:38:12 +08:00
街角小林
88cea27d1e Demo:新增节点连线样式是否允许继承祖先的样式的设置、优化页面各处滚动条样式 2025-02-13 09:22:53 +08:00
街角小林
86c84cba0b Feat:新增节点连线样式是否允许继承祖先的样式的实例化选项 2025-02-13 09:22:28 +08:00
wanglin2
8c7d11c629 update 2025-02-12 21:23:45 +08:00
街角小林
84cb4e9420 update 2025-02-12 10:21:02 +08:00
街角小林
119ff12339 update 2025-02-12 09:59:37 +08:00
街角小林
f754bd538b Doc update 2025-02-12 09:46:45 +08:00
街角小林
26e956ad44 Doc update 2025-02-12 09:05:50 +08:00
街角小林
aa41d23505 Merge branch 'feature' of https://github.com/wanglin2/mind-map into feature 2025-02-11 17:31:01 +08:00
街角小林
19bffea87c Demo:调整导出弹窗的样式 2025-02-11 17:30:40 +08:00
wanglin2
3128a546e0 Merge branch 'feature' of https://github.com/wanglin2/mind-map into feature 2025-02-10 20:52:29 +08:00
wanglin2
68f99d5236 Feat:新增添加节点自定义内容的实例化选项 2025-02-10 20:52:11 +08:00
街角小林
0991315422 Feat:新增自定义节点图片调整插件中删除和调整按钮内容的实例化选项 2025-02-10 17:38:08 +08:00
街角小林
361edea91a Fix:修复当节点标签数据为对象数组时导出xmind文件无法打开的问题 2025-02-10 17:28:15 +08:00
街角小林
9666f06631 Demo:支持打印大纲 2025-02-10 10:09:59 +08:00
wanglin2
24365a22c3 Demo:导入数据时隐藏搜索框 2025-02-01 11:34:17 +08:00
wanglin2
c95919a1a0 update 2025-02-01 11:32:07 +08:00
wanglin2
b0a5c8c12a Feat:createUidForAppointNodes工具方法支持处理概要数据 2025-02-01 11:29:00 +08:00
wanglin2
1770cb98aa Feat:自动给没有uid的节点数据添加uid 2025-02-01 11:28:25 +08:00
wanglin2
b3705712f2 Fix:修复概要节点会显示快速创建子节点按钮的问题 2025-01-30 21:39:04 +08:00
wanglin2
f8c71321e6 打包demo 2025-01-25 12:34:28 +08:00
wanglin2
9a289e19b3 Demo:设置里增加节点内容间距的设置 2025-01-24 21:58:04 +08:00
wanglin2
c774bf01ef Demo:支持调整节点图片和标签布局方式的设置 2025-01-24 21:30:43 +08:00
wanglin2
774609f209 Feat:新增节点图片点击事件 2025-01-24 21:30:19 +08:00
wanglin2
4ba82cd7f0 Feat:去掉tagPosition实例化选项,改为主题配置tagPlacement 2025-01-24 20:13:12 +08:00
wanglin2
0c23ff6527 update 2025-01-24 19:46:18 +08:00
wanglin2
99ff9bdff2 update 2025-01-24 19:45:01 +08:00
街角小林
c97d92af25 update 2025-01-22 18:57:38 +08:00
街角小林
01d332009c Demo:增加是否允许拖拽文件进行导入的设置 2025-01-22 18:53:52 +08:00
街角小林
b5209118b5 Feat:主题新增节点图片布局位置的配置 2025-01-22 18:42:06 +08:00
街角小林
d3353d50c5 Demo:导入了富文本内容自动开启富文本模式设置 2025-01-21 16:31:46 +08:00
街角小林
9b2c9ad1d2 Demo:工具栏中的公式按钮根据是否开启节点富文本配置进行显示和隐藏 2025-01-21 16:14:45 +08:00
街角小林
34f6fdd8e2 Fix:修复文本编辑实时渲染模式编辑中展开收起按钮闪烁的问题 2025-01-21 15:47:50 +08:00
街角小林
407b86c5ee Fix:修复debounce方法逻辑错误的问题 2025-01-17 17:23:55 +08:00
街角小林
e228386222 update 2025-01-17 09:34:24 +08:00
街角小林
0cb12dcf9f Feat:非富文本支持文本对齐属性 2025-01-17 09:26:57 +08:00
街角小林
fdecf8a308 Demo:富文本模式支持设置节点文本的对齐方式 2025-01-16 18:17:00 +08:00
街角小林
4435feb014 Feat:富文本支持设置对齐方式 2025-01-16 18:08:41 +08:00
街角小林
57f4fb923c 整理svg图标字符串 2025-01-15 09:11:27 +08:00
街角小林
281902d962
Merge pull request #1074 from Sallyfafafa/fix/text
fix: en_ui 文案错误
2025-01-15 09:06:09 +08:00
街角小林
e6a075d9a0
Merge pull request #1077 from Sallyfafafa/fix/type
fix: 整理 svg 图标,添加  xmlns=http://www.w3.org/2000/svg 告诉浏览器这是一个 SVG 文档
2025-01-15 09:05:44 +08:00
街角小林
db468770ce Feat:新增显示快捷创建子节点按钮 2025-01-14 18:34:49 +08:00
sallyfafafa
3ef6097ee5 fix: 整理 svg 图标,添加 xmlns=http://www.w3.org/2000/svg 告诉浏览器这是一个 SVG 文档 2025-01-03 17:39:18 +08:00
sallyfafafa
586d4b74e0 fix: 整理 svg 图标,添加 xmlns=http://www.w3.org/2000/svg 告诉浏览器这是一个 SVG 文档 2025-01-03 17:33:39 +08:00
街角小林
333e5cc878 代码优化:将所有replaceAll方法改为replace方法 2025-01-03 15:36:44 +08:00
sallyfafafa
84b08d410a fix: en_ui 文案错误 2025-01-03 11:25:48 +08:00
街角小林
d5d01c5f19 update 2024-12-31 13:44:52 +08:00
街角小林
5688bb6821 update 2024-12-31 13:38:52 +08:00
街角小林
c9d0b6c916 Doc: update 2024-12-31 11:36:34 +08:00
街角小林
5a116d952a 打包0.13.0 2024-12-30 18:01:44 +08:00
街角小林
5717b4fa1d Doc: update 2024-12-30 17:49:23 +08:00
街角小林
e04a5d4a6f Fix:修复富文本模式下编辑时减少文字,编辑器高度没有改变的问题 2024-12-30 09:30:40 +08:00
街角小林
a7d97065c6 Fix:修复非富文本模式编辑时粘贴文本不会触发node_text_edit_change事件的问题 2024-12-30 09:25:14 +08:00
街角小林
4332abce4d Fix:优化开启原地编辑模式下且不使用富文本text编辑时输入框定位会抖动的问题 2024-12-27 16:53:06 +08:00
街角小林
f71b47b215 Feat:新增自定义节点备注、超链接、附件图标样式的实例化选项 2024-12-27 09:30:51 +08:00
街角小林
656cfa50c6 Fix:修复复制多个节点然后连续粘贴会导致布局混乱的问题 2024-12-26 19:19:16 +08:00
街角小林
f10f8e0610 Fix:修改http和https协议下的粘贴行为不一致的问题 2024-12-26 18:55:04 +08:00
街角小林
7533599cac Fix:修复copyNodeTree工具方法克隆节点对象的数据时会把节点实例的属性也克隆的问题 2024-12-26 18:25:16 +08:00
街角小林
f34de3acd9 Feat:编辑文本实时更新增加防抖操作,避免快速输入时不必要的计算 2024-12-26 17:54:12 +08:00
街角小林
4a5501f7a3 Fix:修复初始渲染时概要会重叠的问题 2024-12-26 17:52:54 +08:00
街角小林
f52fd2ff48 Fix:禁止展开收起按钮的文字可被选中 2024-12-25 18:50:14 +08:00
街角小林
628a6b72a2 Feat:富文本插件增加对老版本数据的处理 2024-12-25 18:28:00 +08:00
街角小林
3642763301 Feat:节点数据增加字段,保存当前库的版本号 2024-12-25 09:57:50 +08:00
街角小林
11c6fa3e45 Feat:copyRenderTree和copyNodeTree两个工具方法支持保留节点data和children以外的其他字段 2024-12-25 09:57:04 +08:00
街角小林
d85210372d Demo:去除不必要的代码 2024-12-25 09:17:15 +08:00
街角小林
62e02ae956 Feat:在中文输入法时激活节点,直接输入进入编辑时忽略第一个按键的值 2024-12-24 09:44:39 +08:00
街角小林
799b46c68e Fix:修复节点处于编辑状态时给节点添加图标等内容时会导致图标和编辑框重叠的问题 2024-12-23 18:12:40 +08:00
街角小林
6479841dee Fix:修复代码错误,注册了富文本插件后进入文本编辑时不应在原有文本编辑类中保存节点实例 2024-12-23 18:02:52 +08:00
街角小林
f1622e1a15 Fix:修复多选节点时双击其中某个节点进入编辑,再双击其中另一个节点时会导致该节点显示空白的问题 2024-12-23 17:42:13 +08:00
街角小林
f490ac6f8d Fix:修复只读模式下连续输入不同文字搜索,上一个搜索到的高亮节点没有清除的问题 2024-12-23 09:33:00 +08:00
街角小林
c342fbbe75 Fix:修复粘贴<a格式的文本生成节点后,文本显示&lt;a的问题 2024-12-20 17:25:12 +08:00
街角小林
dde085b54e Fix:修复非富文本模式下文本编辑时不能粘贴<a格式的文本的问题 2024-12-20 17:02:25 +08:00
街角小林
f8f126e8de BreakChange:重构富文本渲染逻辑 2024-12-20 16:45:35 +08:00
街角小林
71f92c985f Demo:切换是否开启富文本的设置增加二次提示 2024-12-20 15:47:57 +08:00
街角小林
3f2b5be4aa Fix:修复开启实时渲染特性时节点存在数学公式的时候进入文本编辑,节点大小没有适应的问题 2024-12-20 15:44:07 +08:00
街角小林
07712e7ac3 Fix:修复富文本模式下,处于节点文本编辑中时通过鼠标滚轮缩放画布时,节点文本会消失的问题 2024-12-20 15:42:46 +08:00
232 changed files with 10528 additions and 5524 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules
dist_electron dist_electron
simple-mind-map/dist simple-mind-map/dist
simple-mind-map/types simple-mind-map/types
utools/dist

550
README.md
View File

@ -7,542 +7,78 @@
[![GitHub stars](https://img.shields.io/github/stars/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/stargazers) [![GitHub stars](https://img.shields.io/github/stars/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/network/members) [![GitHub forks](https://img.shields.io/github/forks/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/network/members)
> 中文名:思绪思维导图。一个简单&强大的 Web 思维导图。 [English](./README_EN.md) | 中文
本项目包含两部分: > 中文名:思绪思维导图。一个简单&强大的 Web 思维导图库和思维导图软件。
1.一个 js 思维导图库,不依赖任何框架,可以使用它来快速完成 Web 思维导图产品的开发 本项目包含两部分开源的JavaScript库和闭源的客户端软件
开发文档:[https://wanglin2.github.io/mind-map-docs/](https://wanglin2.github.io/mind-map-docs/)。 # 库、Web
2.一个 Web 思维导图基于思维导图库、Vue2.x、ElementUI 开发,可以操作电脑本地文件,可以当做一个在线版思维导图应用使用,也可以自部署和二次开发 > 即本仓库中的代码,目前已进入低维护状态
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/) - 一个 `js` 思维导图库,不依赖任何框架,可以用来快速完成 Web 思维导图产品的开发
此外也提供了客户端可供下载使用,支持`Windows`、`Mac`及`Linux`,下载地址: > 开发文档:[https://wanglin2.github.io/mind-map-docs/](https://wanglin2.github.io/mind-map-docs/)
Github[releases](https://github.com/wanglin2/mind-map/releases)。百度云盘:[地址](https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3) - 一个 Web 思维导图,基于思维导图库、`Vue2.x`、`ElementUI` 开发,支持操作电脑本地文件,可以当做一个在线版思维导图应用使用,也可以自部署和二次开发
> 客户端版本会落后于在线版本,尝试最新功能请优先使用在线版。 > 在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
【云存储版本】如果你需要带后端的云存储版本,可以尝试我们开发的另一个项目[理想文档](https://github.com/wanglin2/lx-doc)。 了解更多信息:[README](./README_MORE_ZH.md)。
# 特性 # 客户端、插件
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积 > 客户端和插件代码不开源,正在积极开发维护中。
- [x] 支持逻辑结构图(向左、向右逻辑结构图)、思维导图、组织结构图、目录组织图、时间轴(横向、竖向)、鱼骨图等结构
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
- [x] 节点内容支持文本(普通文本、富文本)、图片、图标、超链接、备注、标签、概要、数学公式
- [x] 节点支持拖拽(拖拽移动、自由调整)、多种节点形状;支持扩展节点内容、支持使用 DDM 完全自定义节点内容
- [x] 支持画布拖动、缩放
- [x] 支持鼠标按键拖动选择和 Ctrl+左键两种多选节点方式
- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`、`xmind`、`txt`,支持从`json`、`xmind`、`markdown`导入
- [x] 支持快捷键、前进后退、关联线、搜索替换、小地图、水印、滚动条、手绘风格、彩虹线条、标记、外框
- [x] 提供丰富的配置,满足各种场景各种使用习惯
- [x] 支持协同编辑
- [x] 支持演示模式
官方提供了如下插件,可根据需求按需引入(某个功能不生效大概率是因为你没有引入对应的插件),具体使用方式请查看文档: - 思绪思维导图客户端
> RichText节点富文本插件、Select鼠标多选节点插件、Drag节点拖拽插件、AssociativeLine关联线插件、Export导出插件、KeyboardNavigation键盘导航插件、MiniMap小地图插件、Watermark水印插件、TouchEvent移动端触摸事件支持插件、NodeImgAdjust拖拽调整节点图片大小插件、Search搜索插件、Painter节点格式刷插件、Scrollbar滚动条插件、Formula数学公式插件、Cooperate协同编辑插件、RainbowLines彩虹线条插件、Demonstrate演示模式插件、OuterFrame外框插件、MindMapLayoutPro思维导图布局插件、HandDrawnLikeStyle手绘风格插件[收费]、Notation节点标记插件[收费]、Numbers节点编号插件[收费]、FreemindFreemind格式导入导出插件[收费]、ExcelExcel格式导入导出插件[收费]、Checkbox待办插件[收费]、Lineflow节点连线流动插件[收费] 本地化存储,隐私优先,数据安全,软件无需联网即可使用!
本项目不会实现的特性: - [x] 1.支持创建无限数量的文件、节点(自由节点);支持创建使用模板;
- [x] 2.提供丰富的设置:基础设置、自定义字体/快捷键/右键菜单/图标、图床配置、AI配置、webdav云同步配置等等可玩性很高
- [x] 3.支持思维导图、逻辑结构图、目录组织图、组织结构图、时间轴、鱼骨图、表格等多种结构类型;
- [x] 4.内置上百个丰富好看的主题也支持自定义主题及AI生成主题
- [x] 5.节点支持添加文本、图片、链接、图标、备注、附件、标签、概要节点、关联线、外框、标记、待办、描述、编号、数学公式等丰富内容;
- [x] 6.支持导入XMind、FreeMind、Markdown、Txt、Xlsx等格式文件支持导出为PNG、XMind、SVG、PDF、Markdown、Txt、Xlsx、FreeMind、Mermaid、Html等格式
- [x] 7.丰富的样式设置:文字、边框、背景、形状、线条、内外边距、图片标签布局等等;
- [x] 8.支持历史版本管理、演示模式、AI生成、手绘风格、大纲编辑、水印、滚动条、同级节点对齐、小地图、进入指定节点、彩虹线条、节点双向链接、搜索替换等等实用有趣的功能
> 1.自由节点,即多个根节点; 支持Windows、Mac及Linux系统支持中文、英文、中文繁体、越南语、俄语语言。
>
> 2.概要节点后面继续添加节点;
>
> 如果你需要以上特性,那么本库可能无法满足你的需求。
# 安装 下载地址:[Github](https://github.com/wanglin2/mind-map/releases)、[百度网盘](https://pan.baidu.com/s/1C8phEJ5pagAAa-o1tU42Uw?pwd=jqfb)、[夸克网盘](https://pan.quark.cn/s/2733982f1976)
```bash > 如果在macOS上安装后无法打开报错**不受信任**或者**移到垃圾箱**,执行下面命令后再启动即可:
npm i simple-mind-map > ``` shell
``` > sudo xattr -d com.apple.quarantine /Applications/思绪思维导图.app
> ```
# 使用 ![](./assets/client/client1.png)
提供一个宽高不为 0 的容器元素: ![](./assets/client/client2.png)
```html ![](./assets/client/client3.png)
<div id="mindMapContainer"></div>
```
另外再设置一下`css`样式: ![](./assets/client/client4.png)
```css ![](./assets/client/client5.png)
#mindMapContainer * {
margin: 0;
padding: 0;
}
```
然后创建一个实例: ![](./assets/client/client6.png)
```js - Obsidian插件
import MindMap from "simple-mind-map";
const mindMap = new MindMap({ 下载地址:[Github](https://github.com/wanglin2/obsidian-simplemindmap/releases)
el: document.getElementById("mindMapContainer"),
data: {
data: {
text: "根节点",
},
children: [],
},
});
```
即可得到一个思维导图。想要实现更多功能?可以查看[开发文档](https://wanglin2.github.io/mind-map-docs/)。 ![](./assets/ob/ob1.png)
# License ![](./assets/ob/ob2.png)
[MIT](./LICENSE)。保留`mind-map`版权声明的情况下可随意商用,如不想保留可联系作者。 ![](./assets/ob/ob3.png)
# 微信交流群 ![](./assets/ob/ob4.png)
微信添加`wanglinguanfang`拉你入群。根据过往的经验大部分问题都可以通过查看issue列表或文档解决所以提问前请确保你已经阅读完了所有文档文档里没有的可在群里提问不必私聊作者如果你一定要私聊请先发红包¥9.9+每次)。 ![](./assets/ob/ob5.png)
# star - UTools插件
如果喜欢本项目,欢迎点个 star这对我们很重要。 已上架[uTools](https://www.u.tools/)插件应用市场,可直接在`uTools`插件应用市场中搜索`思绪`进行安装,也可以直接访问该地址:[主页](https://www.u-tools.cn/plugins/detail/%E6%80%9D%E7%BB%AA%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE/),点击右侧的【启动】按钮进行安装。
[![Star History Chart](https://api.star-history.com/svg?repos=wanglin2/mind-map&type=Date)](https://star-history.com/#wanglin2/mind-map&Date)
# 关于定制
如果你有个性化的商用定制需求,可以联系我们,我们提供付费开发服务,无论前端、后端、还是部署,都可以帮你一站式搞定。
# 请作者喝杯咖啡
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡~你的赞助对项目的可持续发展非常重要,是作者持续维护的最大动力。
> 推荐使用支付宝,微信获取不到头像。转账请备注【思维导图】。
>
> 也可以通过购买付费插件来支持我们:[付费插件](https://wanglin2.github.io/mind-map-docs/plugins/about.html)。
>
> 赞助等级最强王者¥500+、星耀赞助¥300+、钻石赞助¥150+、黄金赞助¥50+)、青铜赞助
<p>
<img src="./web/src/assets/img/alipay.jpg" style="width: 300px" />
<img src="./web/src/assets/img/wechat.jpg" style="width: 300px" />
</p>
## 钻石赞助
<p>
<span>
<img src="./web/src/assets/avatar/黄智彪@一米一栗科技.png" style="width: 50px;height: 50px;" />
<span>黄智彪@一米一栗科技</span>
</span>
</p>
## 黄金赞助
<p>
<span>
<img src="./web/src/assets/avatar/小土渣的宇宙.jpeg" style="width: 50px;height: 50px;" />
<span>小土渣的宇宙</span>
</span>
<span>
<img src="./web/src/assets/avatar/Chris.jpg" style="width: 50px;height: 50px;" />
<span>Chris</span>
</span>
<span>
<img src="./web/src/assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;" />
<span>仓鼠</span>
</span>
<span>
<img src="./web/src/assets/avatar/风格.jpg" style="width: 50px;height: 50px;" />
<span>风格</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>LiuJL</span>
</span>
<span>
<img src="./web/src/assets/avatar/Kyle.jpg" style="width: 50px;height: 50px;" />
<span>Kyle</span>
</span>
<span>
<img src="./web/src/assets/avatar/秀树因馨雨.jpg" style="width: 50px;height: 50px;" />
<span>秀树因馨雨</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>黄泳</span>
</span>
<span>
<img src="./web/src/assets/avatar/ccccs.jpg" style="width: 50px;height: 50px;" />
<span>ccccs</span>
</span>
<span>
<img src="./web/src/assets/avatar/炫.jpg" style="width: 50px;height: 50px;" />
<span></span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>晏江</span>
</span>
<span>
<img src="./web/src/assets/avatar/梁辉.jpg" style="width: 50px;height: 50px;" />
<span>梁辉</span>
</span>
<span>
<img src="./web/src/assets/avatar/千帆.jpg" style="width: 50px;height: 50px;" />
<span>千帆</span>
</span>
<span>
<img src="./web/src/assets/avatar/布林.jpg" style="width: 50px;height: 50px;" />
<span>布林</span>
</span>
<span>
<img src="./web/src/assets/avatar/达仁科技.jpg" style="width: 50px;height: 50px;" />
<span>达仁科技</span>
</span>
<span>
<img src="./web/src/assets/avatar/沐风牧草.jpg" style="width: 50px;height: 50px;" />
<span>沐风牧草</span>
</span>
<span>
<img src="./web/src/assets/avatar/俊奇.jpg" style="width: 50px;height: 50px;" />
<span>俊奇</span>
</span>
<span>
<img src="./web/src/assets/avatar/庆国.jpg" style="width: 50px;height: 50px;" />
<span>庆国</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>Matt</span>
</span>
<span>
<img src="./web/src/assets/avatar/雨馨.jpg" style="width: 50px;height: 50px;" />
<span>雨馨</span>
</span>
<span>
<img src="./web/src/assets/avatar/峰.jpg" style="width: 50px;height: 50px;" />
<span></span>
</span>
</p>
## 青铜赞助
<p>
<span>
<img src="./web/src/assets/avatar/Think.jpg" style="width: 50px;height: 50px;" />
<span>Think</span>
</span>
<span>
<img src="./web/src/assets/avatar/志斌.jpg" style="width: 50px;height: 50px;" />
<span>志斌</span>
</span>
<span>
<img src="./web/src/assets/avatar/qp.jpg" style="width: 50px;height: 50px;" />
<span>qp</span>
</span>
<span>
<img src="./web/src/assets/avatar/ZXR.jpg" style="width: 50px;height: 50px;" />
<span>ZXR</span>
</span>
<span>
<img src="./web/src/assets/avatar/花儿朵朵.jpg" style="width: 50px;height: 50px;" />
<span>花儿朵朵</span>
</span>
<span>
<img src="./web/src/assets/avatar/suka.jpg" style="width: 50px;height: 50px;" />
<span>suka</span>
</span>
<span>
<img src="./web/src/assets/avatar/水车.jpg" style="width: 50px;height: 50px;" />
<span>水车</span>
</span>
<span>
<img src="./web/src/assets/avatar/才镇.jpg" style="width: 50px;height: 50px;" />
<span>才镇</span>
</span>
<span>
<img src="./web/src/assets/avatar/小米.jpg" style="width: 50px;height: 50px;" />
<span>小米bbᯤ²ᴳ</span>
</span>
<span>
<img src="./web/src/assets/avatar/棐.jpg" style="width: 50px;height: 50px;" />
<span>*棐</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>Luke</span>
</span>
<span>
<img src="./web/src/assets/avatar/南风.jpg" style="width: 50px;height: 50px;" />
<span>南风</span>
</span>
<span>
<img src="./web/src/assets/avatar/蜉蝣撼大叔.jpg" style="width: 50px;height: 50px;" />
<span>蜉蝣撼大叔</span>
</span>
<span>
<img src="./web/src/assets/avatar/乙.jpg" style="width: 50px;height: 50px;" />
<span></span>
</span>
<span>
<img src="./web/src/assets/avatar/敏.jpg" style="width: 50px;height: 50px;" />
<span></span>
</span>
<span>
<img src="./web/src/assets/avatar/有希.jpg" style="width: 50px;height: 50px;" />
<span>有希</span>
</span>
<span>
<img src="./web/src/assets/avatar/樊笼.jpg" style="width: 50px;height: 50px;" />
<span>樊笼</span>
</span>
<span>
<img src="./web/src/assets/avatar/小逗比.png" style="width: 50px;height: 50px;" />
<span>小逗比</span>
</span>
<span>
<img src="./web/src/assets/avatar/天清如愿.jpg" style="width: 50px;height: 50px;" />
<span>天清如愿</span>
</span>
<span>
<img src="./web/src/assets/avatar/敬明朗.jpg" style="width: 50px;height: 50px;" />
<span>敬明朗</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>飞箭</span>
</span>
<span>
<img src="./web/src/assets/avatar/戚永峰.png" style="width: 50px;height: 50px;" />
<span>戚永峰</span>
</span>
<span>
<img src="./web/src/assets/avatar/moom.jpg" style="width: 50px;height: 50px;" />
<span>moom</span>
</span>
<span>
<img src="./web/src/assets/avatar/张扬.png" style="width: 50px;height: 50px;" />
<span>张扬</span>
</span>
<span>
<img src="./web/src/assets/avatar/长沙利奥软件.jpg" style="width: 50px;height: 50px;" />
<span>长沙利奥软件</span>
</span>
<span>
<img src="./web/src/assets/avatar/HaHN.jpg" style="width: 50px;height: 50px;" />
<span>HaHN</span>
</span>
<span>
<img src="./web/src/assets/avatar/继龙.jpg" style="width: 50px;height: 50px;" />
<span>继龙</span>
</span>
<span>
<img src="./web/src/assets/avatar/欣.jpg" style="width: 50px;height: 50px;" />
<span></span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>易空小易</span>
</span>
<span>
<img src="./web/src/assets/avatar/国发.jpg" style="width: 50px;height: 50px;" />
<span>国发</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>建明</span>
</span>
<span>
<img src="./web/src/assets/avatar/汪津合.jpg" style="width: 50px;height: 50px;" />
<span>汪津合</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>博文</span>
</span>
<span>
<img src="./web/src/assets/avatar/慕智打印-兰兰.jpg" style="width: 50px;height: 50px;" />
<span>慕智打印-兰兰</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>锦冰</span>
</span>
<span>
<img src="./web/src/assets/avatar/旭东.png" style="width: 50px;height: 50px;" />
<span>旭东</span>
</span>
<span>
<img src="./web/src/assets/avatar/橘半.jpg" style="width: 50px;height: 50px;" />
<span>橘半</span>
</span>
<span>
<img src="./web/src/assets/avatar/pluvet.jpg" style="width: 50px;height: 50px;" />
<span>pluvet</span>
</span>
<span>
<img src="./web/src/assets/avatar/皇登攀.jpg" style="width: 50px;height: 50px;" />
<span>皇登攀</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>SR</span>
</span>
<span>
<img src="./web/src/assets/avatar/逆水行舟.jpg" style="width: 50px;height: 50px;" />
<span>逆水行舟</span>
</span>
<span>
<img src="./web/src/assets/avatar/L.jpg" style="width: 50px;height: 50px;" />
<span>L</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>sunniberg</span>
</span>
<span>
<img src="./web/src/assets/avatar/在下青铜五.jpg" style="width: 50px;height: 50px;" />
<span>在下青铜五</span>
</span>
<span>
<img src="./web/src/assets/avatar/木星二号.jpg" style="width: 50px;height: 50px;" />
<span>木星二号</span>
</span>
<span>
<img src="./web/src/assets/avatar/阿晨.jpg" style="width: 50px;height: 50px;" />
<span>阿晨</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span></span>
</span>
<span>
<img src="./web/src/assets/avatar/Alex.jpg" style="width: 50px;height: 50px;" />
<span>Alex</span>
</span>
<span>
<img src="./web/src/assets/avatar/子豪.jpg" style="width: 50px;height: 50px;" />
<span>子豪</span>
</span>
<span>
<img src="./web/src/assets/avatar/宏涛.jpg" style="width: 50px;height: 50px;" />
<span>宏涛</span>
</span>
<span>
<img src="./web/src/assets/avatar/最多5个字.jpg" style="width: 50px;height: 50px;" />
<span>最多5个字</span>
</span>
<span>
<img src="./web/src/assets/avatar/ZX.jpg" style="width: 50px;height: 50px;" />
<span>ZX</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>协成</span>
</span>
<span>
<img src="./web/src/assets/avatar/木木.jpg" style="width: 50px;height: 50px;" />
<span>木木</span>
</span>
<span>
<img src="./web/src/assets/avatar/好名字.jpg" style="width: 50px;height: 50px;" />
<span>好名字</span>
</span>
<span>
<img src="./web/src/assets/avatar/lsytyrt.jpg" style="width: 50px;height: 50px;" />
<span>lsytyrt</span>
</span>
<span>
<img src="./web/src/assets/avatar/buddy.jpg" style="width: 50px;height: 50px;" />
<span>buddy</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>小川</span>
</span>
<span>
<img src="./web/src/assets/avatar/Tobin.jpg" style="width: 50px;height: 50px;" />
<span>Tobin</span>
</span>
<span>
<img src="./web/src/assets/avatar/夏虫不语冰.jpg" style="width: 50px;height: 50px;" />
<span>夏虫不语冰</span>
</span>
<span>
<img src="./web/src/assets/avatar/晴空.jpg" style="width: 50px;height: 50px;" />
<span>晴空</span>
</span>
<span>
<img src="./web/src/assets/avatar/。.png" style="width: 50px;height: 50px;" />
<span></span>
</span>
<span>
<img src="./web/src/assets/avatar/Jeffrey.jpg" style="width: 50px;height: 50px;" />
<span>Jeffrey</span>
</span>
<span>
<img src="./web/src/assets/avatar/张文建.jpg" style="width: 50px;height: 50px;" />
<span>张文建</span>
</span>
<span>
<img src="./web/src/assets/avatar/Lawliet.jpg" style="width: 50px;height: 50px;" />
<span>Lawliet</span>
</span>
<span>
<img src="./web/src/assets/avatar/一叶孤舟.jpg" style="width: 50px;height: 50px;" />
<span>一叶孤舟</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>Eric</span>
</span>
<span>
<img src="./web/src/assets/avatar/Joe.jpg" style="width: 50px;height: 50px;" />
<span>Joe</span>
</span>
<span>
<img src="./web/src/assets/avatar/default.png" style="width: 50px;height: 50px;" />
<span>中文网字计划-江夏尧</span>
</span>
<span>
<img src="./web/src/assets/avatar/海云.jpg" style="width: 50px;height: 50px;" />
<span>海云</span>
</span>
<span>
<img src="./web/src/assets/avatar/皮老板.jpg" style="width: 50px;height: 50px;" />
<span>皮老板</span>
</span>
<span>
<img src="./web/src/assets/avatar/h.r.w.jpg" style="width: 50px;height: 50px;" />
<span>h.r.w</span>
</span>
<span>
<img src="./web/src/assets/avatar/时光匆匆.png" style="width: 50px;height: 50px;" />
<span>时光匆匆</span>
</span>
<span>
<img src="./web/src/assets/avatar/广兴.jpg" style="width: 50px;height: 50px;" />
<span>广兴</span>
</span>
<span>
<img src="./web/src/assets/avatar/一亩三.jpg" style="width: 50px;height: 50px;" />
<span>一亩三</span>
</span>
<span>
<img src="./web/src/assets/avatar/xbkkjbs0246658.png" style="width: 50px;height: 50px;" />
<span>xbkkjbs0246658</span>
</span>
<span>
<img src="./web/src/assets/avatar/4399行星元帅.jpg" style="width: 50px;height: 50px;" />
<span>4399行星元帅</span>
</span>
<span>
<img src="./web/src/assets/avatar/Xavier.png" style="width: 50px;height: 50px;" />
<span>Xavier</span>
</span>
<span>
<img src="./web/src/assets/avatar/冒号括号.png" style="width: 50px;height: 50px;" />
<span>:)</span>
</span>
</p>

84
README_EN.md Normal file
View File

@ -0,0 +1,84 @@
<h1 align="center">Simple mind map</h1>
[![npm-version](https://img.shields.io/npm/v/simple-mind-map)](https://www.npmjs.com/package/simple-mind-map)
![npm download](https://img.shields.io/npm/dm/simple-mind-map)
[![GitHub issues](https://img.shields.io/github/issues/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/issues)
![license](https://img.shields.io/npm/l/express.svg)
[![GitHub stars](https://img.shields.io/github/stars/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/network/members)
English | [中文](./README.md)
> Chinese name: 思绪思维导图. A simple & powerful web mind map library and mind map software.
This project consists of two parts: an open-source JavaScript library and closed-source client software.
# Library, Web
> Refers to the code in this repository, currently in low-maintenance status.
- A `js` mind map library, independent of any framework, which can be used to quickly develop web-based mind map products.
> Documentation: [https://wanglin2.github.io/mind-map-docs/](https://wanglin2.github.io/mind-map-docs/)
- A web-based mind map application, developed using the mind map library, `Vue2.x`, and `ElementUI`. It supports operations on local computer files, can be used as an online mind map application, and is open for self-deployment and secondary development.
> Online address: [https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
Learn more: [README](./README_MORE_EN.md).
# Client, Plugins
> The client and plugin code are not open source and are under active development and maintenance.
- 思绪思维导图 Client
Local storage, privacy-first, data security. The software can be used without an internet connection!
- [x] 1. Supports creating unlimited files and nodes (free nodes); supports creating and using templates.
- [x] 2. Offers rich settings: basic settings, custom fonts/shortcuts/right-click menus/icons, image hosting configuration, AI configuration, WebDAV cloud sync configuration, etc., highly customizable.
- [x] 3. Supports various structure types: mind maps, logical structure diagrams, directory organization charts, organizational charts, timelines, fishbone diagrams, tables, etc.
- [x] 4. Built-in hundreds of rich and beautiful themes, also supports custom themes and AI-generated themes.
- [x] 5. Nodes support adding rich content: text, images, links, icons, notes, attachments, tags, summary nodes, association lines, borders, markers, to-dos, descriptions, numbering, mathematical formulas, etc.
- [x] 6. Supports importing files in XMind, FreeMind, Markdown, Txt, Xlsx, etc.; supports exporting to PNG, XMind, SVG, PDF, Markdown, Txt, Xlsx, FreeMind, Mermaid, Html, etc.
- [x] 7. Rich style settings: text, borders, background, shape, lines, inner/outer margins, image tag layout, etc.
- [x] 8. Supports practical and interesting features: historical version management, presentation mode, AI generation, hand-drawn style, outline editing, watermark, scrollbars, sibling node alignment, minimap, entering specific nodes, rainbow lines, bidirectional node linking, search and replace, etc.
Supports Windows, Mac, and Linux systems; supports Chinese, English, Traditional Chinese, Vietnamese, and Russian languages.
Download links: [Github](https://github.com/wanglin2/mind-map/releases), [Baidu Netdisk](https://pan.baidu.com/s/1C8phEJ5pagAAa-o1tU42Uw?pwd=jqfb), [Quark Netdisk](https://pan.quark.cn/s/2733982f1976)
> If the software fails to open after installation on macOS, showing an error like **untrusted** or **moved to trash**, execute the following command and then restart:
> ``` shell
> sudo xattr -d com.apple.quarantine /Applications/思绪思维导图.app
> ```
![](./assets/client/clienten1.png)
![](./assets/client/clienten2.png)
![](./assets/client/clienten3.png)
![](./assets/client/clienten4.png)
![](./assets/client/clienten5.png)
![](./assets/client/clienten6.png)
- Obsidian Plugin
Download link: [Github](https://github.com/wanglin2/obsidian-simplemindmap/releases)
![](./assets/ob/oben1.png)
![](./assets/ob/oben2.png)
![](./assets/ob/oben3.png)
![](./assets/ob/oben4.png)
![](./assets/ob/oben5.png)
- UTools Plugin
Available in the [uTools](https://www.u.tools/) plugin market. You can search for `思绪` directly in the uTools plugin market to install it, or visit this address directly: [Homepage](https://www.u-tools.cn/plugins/detail/%E6%80%9D%E7%BB%AA%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE/), and click the 【Launch】 button on the right to install.

91
README_MORE_EN.md Normal file
View File

@ -0,0 +1,91 @@
# Features
- [x] Plugin-based architecture. Apart from core functionalities, other features are provided as plugins, allowing on-demand use to reduce bundle size.
- [x] Supports various structures: Logical Structure Diagrams (left, right), Mind Maps, Organizational Charts, Directory Organization Charts, Timelines (horizontal, vertical), Fishbone Diagrams, etc.
- [x] Built-in multiple themes, allows high customization of styles, supports registering new themes.
- [x] Node content supports text (plain text, rich text), images, icons, hyperlinks, notes, tags, summaries, mathematical formulas.
- [x] Nodes support drag-and-drop (move, free resize), multiple node shapes; supports extending node content, supports using DDM for fully custom node content.
- [x] Supports canvas dragging and zooming.
- [x] Supports two methods for multi-selecting nodes: mouse button drag selection and Ctrl+left click.
- [x] Supports export to `json`, `png`, `svg`, `pdf`, `markdown`, `xmind`, `txt`; supports import from `json`, `xmind`, `markdown`.
- [x] Supports shortcuts, undo/redo, associative lines, search/replace, mini-map, watermark, scrollbars, hand-drawn style, rainbow lines, markers, outer frames.
- [x] Provides rich configuration options to meet various scenarios and usage habits.
- [x] Supports collaborative editing.
- [x] Supports presentation mode.
- [x] More features await your discovery.
The following plugins are officially provided and can be imported as needed (if a feature doesn't work, it's likely because the corresponding plugin hasn't been imported). Please refer to the documentation for specific usage:
| RichText (Node Rich Text Plugin) | Select (Mouse Multi-Select Node Plugin) | Drag (Node Drag Plugin) | AssociativeLine (Associative Line Plugin) |
| ------------------------------------- | ----------------------------------------- | ------------------------------------- | ----------------------------------------- |
| Export (Export Plugin) | KeyboardNavigation (Keyboard Navigation Plugin) | MiniMap (Mini-Map Plugin) | Watermark (Watermark Plugin) |
| TouchEvent (Mobile Touch Event Support Plugin) | NodeImgAdjust (Drag to Adjust Node Image Size Plugin) | Search (Search Plugin) | Painter (Node Format Painter Plugin) |
| Scrollbar (Scrollbar Plugin) | Formula (Mathematical Formula Plugin) | Cooperate (Collaborative Editing Plugin) | RainbowLines (Rainbow Lines Plugin) |
| Demonstrate (Presentation Mode Plugin) | OuterFrame (Outer Frame Plugin) | MindMapLayoutPro (Mind Map Layout Plugin) | |
Features that will **not** be implemented in this project:
> 1. Free nodes, i.e., multiple root nodes.
>
> 2. Adding nodes after a summary node.
>
> If you need the above features, this library may not meet your requirements.
# Installation
```bash
npm i simple-mind-map
```
# Usage
Provide a container element with non-zero width and height:
```html
<div id="mindMapContainer"></div>
```
Also, set the following CSS styles:
```css
#mindMapContainer * {
margin: 0;
padding: 0;
}
```
Then create an instance:
```js
import MindMap from "simple-mind-map";
const mindMap = new MindMap({
el: document.getElementById("mindMapContainer"),
data: {
data: {
text: "Root Node",
},
children: [],
},
});
```
You will get a mind map. Want to implement more features? Check the [Development Documentation](https://wanglin2.github.io/mind-map-docs/).
# License
[MIT](./LICENSE). Commercial use is permitted freely as long as the `simple-mind-map` copyright notice and attribution are retained. If you have questions or wish to remove these requirements, please contact the author (WeChat: wanglinguanfang) for a paid option to remove them.
> Example: You can add the following content on any page of your application, such as the About page, Help page, Documentation page, Open Source Notice, etc.:
>
> The mind map feature of this product is developed based on the SimpleMindMap project. The copyright belongs to the original project. [Open Source License](https://github.com/wanglin2/mind-map/blob/main/LICENSE).
# Development Help / Technical Support / Consulting
Due to limited time and a shift in focus, we currently do not provide any development support (including paid support). Thank you for your understanding!
# Star
If you like this project, welcome to give it a star. It means a lot to us.
[![Star History Chart](https://api.star-history.com/svg?repos=wanglin2/mind-map&type=Date)](https://star-history.com/#wanglin2/mind-map&Date)

986
README_MORE_ZH.md Normal file
View File

@ -0,0 +1,986 @@
# 特性
- [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积
- [x] 支持逻辑结构图(向左、向右逻辑结构图)、思维导图、组织结构图、目录组织图、时间轴(横向、竖向)、鱼骨图等结构
- [x] 内置多种主题,允许高度自定义样式,支持注册新主题
- [x] 节点内容支持文本(普通文本、富文本)、图片、图标、超链接、备注、标签、概要、数学公式
- [x] 节点支持拖拽(拖拽移动、自由调整)、多种节点形状;支持扩展节点内容、支持使用 DDM 完全自定义节点内容
- [x] 支持画布拖动、缩放
- [x] 支持鼠标按键拖动选择和 Ctrl+左键两种多选节点方式
- [x] 支持导出为`json`、`png`、`svg`、`pdf`、`markdown`、`xmind`、`txt`,支持从`json`、`xmind`、`markdown`导入
- [x] 支持快捷键、前进后退、关联线、搜索替换、小地图、水印、滚动条、手绘风格、彩虹线条、标记、外框
- [x] 提供丰富的配置,满足各种场景各种使用习惯
- [x] 支持协同编辑
- [x] 支持演示模式
- [x] 更多功能等你来发现
官方提供了如下插件,可根据需求按需引入(某个功能不生效大概率是因为你没有引入对应的插件),具体使用方式请查看文档:
| RichText节点富文本插件 | Select鼠标多选节点插件 | Drag节点拖拽插件 | AssociativeLine关联线插件 |
| ------------------------------------ | ----------------------------------------- | ------------------------------------ | ------------------------------------ |
| Export导出插件 | KeyboardNavigation键盘导航插件 | MiniMap小地图插件 | Watermark水印插件 |
| TouchEvent移动端触摸事件支持插件 | NodeImgAdjust拖拽调整节点图片大小插件 | Search搜索插件 | Painter节点格式刷插件 |
| Scrollbar滚动条插件 | Formula数学公式插件 | Cooperate协同编辑插件 | RainbowLines彩虹线条插件 |
| Demonstrate演示模式插件 | OuterFrame外框插件 | MindMapLayoutPro思维导图布局插件 | |
本项目不会实现的特性:
> 1.自由节点,即多个根节点;
>
> 2.概要节点后面继续添加节点;
>
> 如果你需要以上特性,那么本库可能无法满足你的需求。
# 安装
```bash
npm i simple-mind-map
```
# 使用
提供一个宽高不为 0 的容器元素:
```html
<div id="mindMapContainer"></div>
```
另外再设置一下`css`样式:
```css
#mindMapContainer * {
margin: 0;
padding: 0;
}
```
然后创建一个实例:
```js
import MindMap from "simple-mind-map";
const mindMap = new MindMap({
el: document.getElementById("mindMapContainer"),
data: {
data: {
text: "根节点",
},
children: [],
},
});
```
即可得到一个思维导图。想要实现更多功能?可以查看[开发文档](https://wanglin2.github.io/mind-map-docs/)。
# License
[MIT](./LICENSE)。保留`simple-mind-map`版权声明和注明来源的情况下可随意商用如有疑问或不想保留可联系作者微信wanglinguanfang通过付费的方式去除。
> 示例:可以在你应用中的关于页面、帮助页面、文档页面、开源声明等任何页面添加以下内容:
>
> 本产品思维导图基于SimpleMindMap项目开发版权归源项目所有[开源协议](https://github.com/wanglin2/mind-map/blob/main/LICENSE)。
# 开发帮助/技术支持/咨询等
因精力有限,及重心转变,暂不提供任何开发支持(包括有偿),请见谅!
# star
如果喜欢本项目,欢迎点个 star这对我们很重要。
[![Star History Chart](https://api.star-history.com/svg?repos=wanglin2/mind-map&type=Date)](https://star-history.com/#wanglin2/mind-map&Date)
# 关于定制
如果你有个性化的商用定制需求,可以联系我们,我们提供付费开发服务,无论前端、后端、还是部署,都可以帮你一站式搞定。
# 谁在使用
<table>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="http://drawon.cn/">
<img src="./web/src/assets/avatar/桌案.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>drawon.cn(桌案)</b></sub>
</a>
</td>
</tr>
</table>
# 感谢赞赏过本项目的人
## 最强王者
<table>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/hi.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>hi</b></sub>
</a>
</td>
</tr>
</table>
## 钻石赞助
<table>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/黄智彪@一米一栗科技.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>黄智彪@一米一栗科技</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/沨沄.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>沨沄</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/行.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b></b></sub>
</a>
</td>
</tr>
</table>
## 黄金赞助
<table>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/小土渣的宇宙.jpeg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>小土渣的宇宙</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/Chris.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Chris</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/仓鼠.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>仓鼠</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/风格.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>风格</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>LiuJL</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/Kyle.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Kyle</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/秀树因馨雨.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>秀树因馨雨</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>黄泳</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/ccccs.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>ccccs</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/炫.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b></b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>晏江</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/梁辉.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>梁辉</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/千帆.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>千帆</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/布林.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>布林</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/达仁科技.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>达仁科技</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/沐风牧草.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>沐风牧草</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/俊奇.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>俊奇</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/庆国.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>庆国</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Matt</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/雨馨.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>雨馨</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/峰.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b></b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/御风.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>御风</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/兔子快跑.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>兔子快跑</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>LSHM</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>newplayer</b></sub>
</a>
</td>
</tr>
</table>
## 青铜赞助
<table>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/Think.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Think</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/志斌.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>志斌</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/qp.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>qp</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/ZXR.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>ZXR</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/花儿朵朵.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>花儿朵朵</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/suka.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>suka</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/水车.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>水车</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/才镇.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>才镇</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/小米.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>小米bbᯤ²ᴳ</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/棐.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>*棐</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/南风.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>南风</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/蜉蝣撼大叔.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>蜉蝣撼大叔</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/乙.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b></b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/敏.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b></b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/有希.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>有希</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/樊笼.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>樊笼</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/小逗比.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>小逗比</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/天清如愿.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>天清如愿</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/敬明朗.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>敬明朗</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>飞箭</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/戚永峰.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>戚永峰</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/moom.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>moom</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/张扬.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>张扬</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/长沙利奥软件.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>长沙利奥软件</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/HaHN.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>HaHN</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/继龙.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>继龙</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/欣.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b></b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>易空小易</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/国发.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>国发</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>建明</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/汪津合.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>汪津合</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>博文</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/慕智打印-兰兰.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>慕智打印-兰兰</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>锦冰</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/旭东.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>旭东</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/橘半.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>橘半</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/pluvet.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>pluvet</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/皇登攀.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>皇登攀</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>SR</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/逆水行舟.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>逆水行舟</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/L.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>L</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>sunniberg</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/在下青铜五.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>sunniberg</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/在下青铜五.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>在下青铜五</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/木星二号.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>木星二号</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/阿晨.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>阿晨</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b></b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/Alex.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Alex</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/子豪.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>子豪</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/宏涛.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>宏涛</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/最多5个字.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>最多5个字</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/ZX.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>ZX</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>协成</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/木木.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>木木</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/好名字.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>好名字</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/lsytyrt.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>lsytyrt</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/buddy.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>buddy</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>小川</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/Tobin.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Tobin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/夏虫不语冰.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>夏虫不语冰</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/晴空.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>晴空</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/。.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b></b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/Jeffrey.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Jeffrey</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/张文建.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>张文建</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/Lawliet.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Lawliet</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/一叶孤舟.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>一叶孤舟</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Eric</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/Joe.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Joe</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>中文网字计划-江夏尧</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/海云.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>海云</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/皮老板.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>皮老板</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/h.r.w.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>h.r.w</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/时光匆匆.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>时光匆匆</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/广兴.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>广兴</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/一亩三.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>一亩三</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/xbkkjbs0246658.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>xbkkjbs0246658</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/4399行星元帅.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>4399行星元帅</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/Xavier.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Xavier</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/冒号括号.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>:)</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/可米阳光.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>可米阳光</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/MrFujing.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>MrFujing</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/Sword.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Sword</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/好好先生Ervin.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>好好先生Ervin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/胡永刚.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>胡永刚</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/旋风.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>旋风</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/星夜寒.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>星夜寒</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/神话.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>神话</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>Towards the future</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/default.png" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>安嘉</b></sub>
</a>
</td>
</tr>
</table>

BIN
assets/client/client1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
assets/client/client2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
assets/client/client3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
assets/client/client4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
assets/client/client5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
assets/client/client6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
assets/client/clienten1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
assets/client/clienten2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
assets/client/clienten3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
assets/client/clienten4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
assets/client/clienten5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
assets/client/clienten6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
assets/ob/ob1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
assets/ob/ob2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
assets/ob/ob3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
assets/ob/ob4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
assets/ob/ob5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
assets/ob/oben1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
assets/ob/oben2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
assets/ob/oben3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
assets/ob/oben4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
assets/ob/oben5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@ -13,4 +13,4 @@ if (fs.existsSync(src)) {
fs.unlinkSync(src) fs.unlinkSync(src)
} }
console.warn('请检查付费插件是否启用!!!') // console.warn('请检查付费插件是否启用!!!')

2
dist/css/app.css vendored
View File

@ -1 +1 @@
*{margin:0;padding:0;box-sizing:border-box}#app{font-family:Avenir,Helvetica,Arial,sans-serif;color:#2c3e50}@font-face{font-family:iconfont;src:url(../fonts/iconfont.woff2) format("woff2"),url(../fonts/iconfont.woff) format("woff"),url(../fonts/iconfont.ttf) format("truetype")}#app,.iconfont{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal}.iconfile-excel:before{content:"\e7b7"}.iconfreemind:before{content:"\e97d"}.iconwaikuang:before{content:"\e640"}.iconhighlight:before{content:"\e6b8"}.iconyanshibofang:before{content:"\e648"}.iconfujian:before{content:"\e88a"}.icongeshihua:before{content:"\e7a3"}.iconyuanma:before{content:"\e658"}.icongundongtiao:before{content:"\e670"}.iconxietongwendang:before{content:"\e60d"}.iconTXT:before{content:"\e6e1"}.iconwenjian1:before{content:"\e69f"}.icondodeparent:before{content:"\e70f"}.icongongshi:before{content:"\e617"}.icontouming:before{content:"\e60c"}.iconlieri:before{content:"\e60b"}.iconmoon_line:before{content:"\e745"}.iconsousuo:before{content:"\e693"}.iconjiantouyou:before{content:"\e62d"}.iconbianji1:before{content:"\e60a"}.icondaohang1:before{content:"\e632"}.iconyanjing:before{content:"\e8bf"}.iconwangzhan:before{content:"\e628"}.iconcsdn:before{content:"\e608"}.iconshejiaotubiao-10:before{content:"\e644"}.iconstar:before{content:"\e7df"}.iconfork:before{content:"\e641"}.iconxiazai:before{content:"\e613"}.iconteamwork:before{content:"\e870"}.iconshuiyin:before{content:"\e67a"}.iconxmind:before{content:"\ea57"}.iconmouseR:before{content:"\e6bd"}.iconmouseL:before{content:"\e6c0"}.iconwenjian:before{content:"\e607"}.iconpdf:before{content:"\e740"}.iconPNG:before{content:"\ec18"}.iconSVG:before{content:"\e621"}.iconmarkdown:before{content:"\ec04"}.iconjson:before{content:"\ea42"}.iconlianjiexian:before{content:"\e75b"}.iconbangzhu:before{content:"\e620"}.iconshezhi:before{content:"\e8b7"}.iconwushuju:before{content:"\e643"}.iconzuijinliulan:before{content:"\e62f"}.icon3zuidahua-3:before{content:"\e692"}.iconzuixiaohua:before{content:"\e650"}.iconzuidahua:before{content:"\e651"}.iconguanbi:before{content:"\e652"}.icondiannao:before{content:"\eac0"}.iconzhuye:before{content:"\e65c"}.iconbendi1x:before{content:"\e606"}.iconbeijingyanse:before{content:"\e6f8"}.iconqingchu:before{content:"\e605"}.iconcase:before{content:"\e6c6"}.iconxingzhuang-wenzi:before{content:"\eb99"}.iconzitijiacu:before{content:"\ec83"}.iconzitixiahuaxian:before{content:"\ec85"}.iconzitixieti:before{content:"\ec86"}.iconshanchuxian:before{content:"\e612"}.iconzitiyanse:before{content:"\e854"}.icongithub:before{content:"\e64f"}.iconchoose1:before{content:"\e6c5"}.iconzhuti:before{content:"\e7aa"}.icondaochu1:before{content:"\e63e"}.iconlingcunwei:before{content:"\e657"}.iconexport:before{content:"\e642"}.icondakai:before{content:"\ebdf"}.iconxinjian:before{content:"\e64e"}.iconjianqie:before{content:"\e601"}.iconzhengli:before{content:"\e83b"}.iconfuzhi:before{content:"\e604"}.iconniantie:before{content:"\e63f"}.iconshangyi:before{content:"\e6be"}.iconxiayi:before{content:"\e6bf"}.icongaikuozonglan:before{content:"\e609"}.iconquanxuan:before{content:"\f199"}.icondaoru:before{content:"\e6a3"}.iconhoutui-shi:before{content:"\e656"}.iconqianjin1:before{content:"\e654"}.iconwithdraw:before{content:"\e603"}.iconqianjin:before{content:"\e600"}.iconhuifumoren:before{content:"\e60e"}.iconhuanhang:before{content:"\e61e"}.iconsuoxiao:before{content:"\ec13"}.iconbianji:before{content:"\e626"}.iconfangda:before{content:"\e663"}.iconquanping1:before{content:"\e664"}.icondingwei:before{content:"\e616"}.icondaohang:before{content:"\e611"}.iconjianpan:before{content:"\e64d"}.iconquanping:before{content:"\e602"}.icondaochu:before{content:"\e63d"}.iconbiaoqian:before{content:"\e63c"}.iconflow-Mark:before{content:"\e65b"}.iconchaolianjie:before{content:"\e6f4"}.iconjingzi:before{content:"\e610"}.iconxiaolian:before{content:"\e60f"}.iconimage:before{content:"\e629"}.iconjiegou:before{content:"\e61d"}.iconyangshi:before{content:"\e631"}.iconfuhao-dagangshu:before{content:"\e71f"}.icontianjiazijiedian:before{content:"\e622"}.iconjiedian:before{content:"\e655"}.iconshanchu:before{content:"\e696"}.iconzhankai:before{content:"\e64c"}.iconzhankai1:before{content:"\e673"} *{margin:0;padding:0;box-sizing:border-box}#app{font-family:Avenir,Helvetica,Arial,sans-serif;color:#2c3e50}.customScrollbar::-webkit-scrollbar{width:7px;height:7px}.customScrollbar::-webkit-scrollbar-thumb{border-radius:7px;background-color:rgba(0,0,0,.3);cursor:pointer}.customScrollbar::-webkit-scrollbar-track{box-shadow:none;background:transparent;display:none}.el-dialog{border-radius:10px}@font-face{font-family:iconfont;src:url(../fonts/iconfont.woff2) format("woff2"),url(../fonts/iconfont.woff) format("woff"),url(../fonts/iconfont.ttf) format("truetype")}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.iconAIshengcheng:before{content:"\e6b5"}.iconprinting:before{content:"\ea28"}.iconwenjianjia:before{content:"\e614"}.iconcontentleft:before{content:"\e8c9"}.iconjuzhongduiqi:before{content:"\ec80"}.iconfile-excel:before{content:"\e7b7"}.iconfreemind:before{content:"\e97d"}.iconwaikuang:before{content:"\e640"}.iconhighlight:before{content:"\e6b8"}.iconyanshibofang:before{content:"\e648"}.iconfujian:before{content:"\e88a"}.icongeshihua:before{content:"\e7a3"}.iconyuanma:before{content:"\e658"}.icongundongtiao:before{content:"\e670"}.iconxietongwendang:before{content:"\e60d"}.iconTXT:before{content:"\e6e1"}.iconwenjian1:before{content:"\e69f"}.icondodeparent:before{content:"\e70f"}.icongongshi:before{content:"\e617"}.icontouming:before{content:"\e60c"}.iconlieri:before{content:"\e60b"}.iconmoon_line:before{content:"\e745"}.iconsousuo:before{content:"\e693"}.iconjiantouyou:before{content:"\e62d"}.iconbianji1:before{content:"\e60a"}.icondaohang1:before{content:"\e632"}.iconyanjing:before{content:"\e8bf"}.iconwangzhan:before{content:"\e628"}.iconcsdn:before{content:"\e608"}.iconshejiaotubiao-10:before{content:"\e644"}.iconstar:before{content:"\e7df"}.iconfork:before{content:"\e641"}.iconxiazai:before{content:"\e613"}.iconteamwork:before{content:"\e870"}.iconshuiyin:before{content:"\e67a"}.iconxmind:before{content:"\ea57"}.iconmouseR:before{content:"\e6bd"}.iconmouseL:before{content:"\e6c0"}.iconwenjian:before{content:"\e607"}.iconpdf:before{content:"\e740"}.iconPNG:before{content:"\ec18"}.iconSVG:before{content:"\e621"}.iconmarkdown:before{content:"\ec04"}.iconjson:before{content:"\ea42"}.iconlianjiexian:before{content:"\e75b"}.iconbangzhu:before{content:"\e620"}.iconshezhi:before{content:"\e8b7"}.iconwushuju:before{content:"\e643"}.iconzuijinliulan:before{content:"\e62f"}.icon3zuidahua-3:before{content:"\e692"}.iconzuixiaohua:before{content:"\e650"}.iconzuidahua:before{content:"\e651"}.iconguanbi:before{content:"\e652"}.icondiannao:before{content:"\eac0"}.iconzhuye:before{content:"\e65c"}.iconbendi1x:before{content:"\e606"}.iconbeijingyanse:before{content:"\e6f8"}.iconqingchu:before{content:"\e605"}.iconcase:before{content:"\e6c6"}.iconxingzhuang-wenzi:before{content:"\eb99"}.iconzitijiacu:before{content:"\ec83"}.iconzitixiahuaxian:before{content:"\ec85"}.iconzitixieti:before{content:"\ec86"}.iconshanchuxian:before{content:"\e612"}.iconzitiyanse:before{content:"\e854"}.icongithub:before{content:"\e64f"}.iconchoose1:before{content:"\e6c5"}.iconzhuti:before{content:"\e7aa"}.icondaochu1:before{content:"\e63e"}.iconlingcunwei:before{content:"\e657"}.iconexport:before{content:"\e642"}.icondakai:before{content:"\ebdf"}.iconxinjian:before{content:"\e64e"}.iconjianqie:before{content:"\e601"}.iconzhengli:before{content:"\e83b"}.iconfuzhi:before{content:"\e604"}.iconniantie:before{content:"\e63f"}.iconshangyi:before{content:"\e6be"}.iconxiayi:before{content:"\e6bf"}.icongaikuozonglan:before{content:"\e609"}.iconquanxuan:before{content:"\f199"}.icondaoru:before{content:"\e6a3"}.iconhoutui-shi:before{content:"\e656"}.iconqianjin1:before{content:"\e654"}.iconwithdraw:before{content:"\e603"}.iconqianjin:before{content:"\e600"}.iconhuifumoren:before{content:"\e60e"}.iconhuanhang:before{content:"\e61e"}.iconsuoxiao:before{content:"\ec13"}.iconbianji:before{content:"\e626"}.iconfangda:before{content:"\e663"}.iconquanping1:before{content:"\e664"}.icondingwei:before{content:"\e616"}.icondaohang:before{content:"\e611"}.iconjianpan:before{content:"\e64d"}.iconquanping:before{content:"\e602"}.icondaochu:before{content:"\e63d"}.iconbiaoqian:before{content:"\e63c"}.iconflow-Mark:before{content:"\e65b"}.iconchaolianjie:before{content:"\e6f4"}.iconjingzi:before{content:"\e610"}.iconxiaolian:before{content:"\e60f"}.iconimage:before{content:"\e629"}.iconjiegou:before{content:"\e61d"}.iconyangshi:before{content:"\e631"}.iconfuhao-dagangshu:before{content:"\e71f"}.icontianjiazijiedian:before{content:"\e622"}.iconjiedian:before{content:"\e655"}.iconshanchu:before{content:"\e696"}.iconzhankai:before{content:"\e64c"}.iconzhankai1:before{content:"\e673"}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
dist/img/catalogOrganization.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

BIN
dist/img/fishbone.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
dist/img/fishbone.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

BIN
dist/img/fishbone2.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
dist/img/logicalStructure.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
dist/img/mindMap.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
dist/img/mindMap.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

BIN
dist/img/organizationStructure.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

BIN
dist/img/rightFishbone.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
dist/img/rightFishbone2.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
dist/img/timeline.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
dist/img/timeline.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

BIN
dist/img/timeline2.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
dist/img/timeline2.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

BIN
dist/img/verticalTimeline.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

BIN
dist/img/verticalTimeline2.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
dist/img/verticalTimeline3.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

2
dist/js/app.js vendored

File diff suppressed because one or more lines are too long

65
dist/js/chunk-183b683c.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@
}) })
} catch (error) { } catch (error) {
console.log(error) console.log(error)
}</script><link href="dist/css/chunk-vendors.css?f5839763ea0d5c47d80a" rel="stylesheet"><link href="dist/css/app.css?f5839763ea0d5c47d80a" 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>const getDataFromBackend = () => { }</script><link href="dist/css/chunk-vendors.css?227f61428db154a5d9bc" rel="stylesheet"><link href="dist/css/app.css?227f61428db154a5d9bc" 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>const getDataFromBackend = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
resolve({ resolve({
@ -28,6 +28,7 @@
config: {}, config: {},
view: null view: null
}, },
mindMapConfig: {},
lang: 'zh', lang: 'zh',
localConfig: null localConfig: null
}) })
@ -44,6 +45,14 @@
window.takeOverAppMethods.saveMindMapData = data => { window.takeOverAppMethods.saveMindMapData = data => {
console.log(data) console.log(data)
} }
// 获取思维导图配置,也就是实例化时会传入的选项
window.takeOverAppMethods.getMindMapConfig = () => {
return data.mindMapConfig
}
// 保存思维导图配置
window.takeOverAppMethods.saveMindMapConfig = config => {
console.log(config)
}
// 获取语言的函数 // 获取语言的函数
window.takeOverAppMethods.getLanguage = () => { window.takeOverAppMethods.getLanguage = () => {
return data.lang return data.lang
@ -74,4 +83,4 @@
// 可以通过window.$bus.$on()来监听应用的一些事件 // 可以通过window.$bus.$on()来监听应用的一些事件
// 实例化页面 // 实例化页面
window.initApp() window.initApp()
}</script><script src="dist/js/chunk-vendors.js?f5839763ea0d5c47d80a"></script><script src="dist/js/app.js?f5839763ea0d5c47d80a"></script></body></html> }</script><script src="dist/js/chunk-vendors.js?227f61428db154a5d9bc"></script><script src="dist/js/app.js?227f61428db154a5d9bc"></script></body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@ -17,11 +17,7 @@ const createFullData = () => {
}; };
} }
/** // 节点较多示例数据
* @Author: 王林
* @Date: 2021-04-15 22:23:24
* @Desc: 节点较多示例数据
*/
const data1 = { const data1 = {
"root": { "root": {
"data": { "data": {
@ -936,6 +932,5 @@ export default {
"layout": "logicalStructure", "layout": "logicalStructure",
// "layout": "mindMap", // "layout": "mindMap",
// "layout": "catalogOrganization" // "layout": "catalogOrganization"
// "layout": "organizationStructure", // "layout": "organizationStructure"
"config": {}
} }

View File

@ -19,6 +19,7 @@ import RainbowLines from './src/plugins/RainbowLines.js'
import Demonstrate from './src/plugins/Demonstrate.js' import Demonstrate from './src/plugins/Demonstrate.js'
import OuterFrame from './src/plugins/OuterFrame.js' import OuterFrame from './src/plugins/OuterFrame.js'
import MindMapLayoutPro from './src/plugins/MindMapLayoutPro.js' import MindMapLayoutPro from './src/plugins/MindMapLayoutPro.js'
import NodeBase64ImageStorage from './src/plugins/NodeBase64ImageStorage.js'
import xmind from './src/parse/xmind.js' import xmind from './src/parse/xmind.js'
import markdown from './src/parse/markdown.js' import markdown from './src/parse/markdown.js'
import icons from './src/svg/icons.js' import icons from './src/svg/icons.js'
@ -30,7 +31,7 @@ MindMap.markdown = markdown
MindMap.iconList = icons.nodeIconList MindMap.iconList = icons.nodeIconList
MindMap.constants = constants MindMap.constants = constants
MindMap.defaultTheme = defaultTheme MindMap.defaultTheme = defaultTheme
MindMap.version = '0.12.2' MindMap.version = '0.14.0-fix.1'
MindMap.usePlugin(MiniMap) MindMap.usePlugin(MiniMap)
.usePlugin(Watermark) .usePlugin(Watermark)
@ -52,5 +53,6 @@ MindMap.usePlugin(MiniMap)
.usePlugin(Demonstrate) .usePlugin(Demonstrate)
.usePlugin(OuterFrame) .usePlugin(OuterFrame)
.usePlugin(MindMapLayoutPro) .usePlugin(MindMapLayoutPro)
.usePlugin(NodeBase64ImageStorage)
export default MindMap export default MindMap

View File

@ -11,16 +11,18 @@ import {
layoutValueList, layoutValueList,
CONSTANTS, CONSTANTS,
ERROR_TYPES, ERROR_TYPES,
cssContent cssContent,
nodeDataNoStylePropList
} from './src/constants/constant' } from './src/constants/constant'
import { SVG } from '@svgdotjs/svg.js' import { SVG, G, Rect } from '@svgdotjs/svg.js'
import { import {
simpleDeepClone, simpleDeepClone,
getObjectChangedProps, getObjectChangedProps,
isUndef, isUndef,
handleGetSvgDataExtraContent, handleGetSvgDataExtraContent,
getNodeTreeBoundingRect, getNodeTreeBoundingRect,
mergeTheme mergeTheme,
createUidForAppointNodes
} from './src/utils' } from './src/utils'
import defaultTheme, { import defaultTheme, {
checkIsNodeSizeIndependenceConfig checkIsNodeSizeIndependenceConfig
@ -56,7 +58,7 @@ class MindMap {
this.cssEl = null this.cssEl = null
this.cssTextMap = {} // 该样式在实例化时会动态添加到页面同时导出为svg时也会添加到svg源码中 this.cssTextMap = {} // 该样式在实例化时会动态添加到页面同时导出为svg时也会添加到svg源码中
// 节点前置内容列表 // 节点前置/后置内容列表
/* /*
{ {
name: '',// 一个唯一的类型标识 name: '',// 一个唯一的类型标识
@ -75,6 +77,27 @@ class MindMap {
} }
*/ */
this.nodeInnerPrefixList = [] this.nodeInnerPrefixList = []
this.nodeInnerPostfixList = []
// 编辑节点的类名列表快捷键响应会检查事件目标是否是body或该列表中的元素是的话才会响应
// 该检查可以通过customCheckEnableShortcut选项来覆盖
this.editNodeClassList = []
// 扩展的节点形状列表
/*
{
createShape: (node) => {
return path
},
getPadding: ({ node, width, height, paddingX, paddingY }) => {
return {
paddingX: 0,
paddingY: 0
}
}
}
*/
this.extendShapeList = []
// 画布 // 画布
this.initContainer() this.initContainer()
@ -85,6 +108,15 @@ class MindMap {
// 初始化缓存数据 // 初始化缓存数据
this.initCache() this.initCache()
// 注册插件
MindMap.pluginList
.filter(plugin => {
return plugin.preload
})
.forEach(plugin => {
this.initPlugin(plugin)
})
// 事件类 // 事件类
this.event = new Event({ this.event = new Event({
mindMap: this mindMap: this
@ -114,7 +146,11 @@ class MindMap {
this.batchExecution = new BatchExecution() this.batchExecution = new BatchExecution()
// 注册插件 // 注册插件
MindMap.pluginList.forEach(plugin => { MindMap.pluginList
.filter(plugin => {
return !plugin.preload
})
.forEach(plugin => {
this.initPlugin(plugin) this.initPlugin(plugin)
}) })
@ -149,6 +185,8 @@ class MindMap {
if (data.data && !data.data.expand) { if (data.data && !data.data.expand) {
data.data.expand = true data.data.expand = true
} }
// 给没有uid的节点添加uid
createUidForAppointNodes([data], false, null, true)
return data return data
} }
@ -237,12 +275,33 @@ class MindMap {
if (this.cssEl) document.head.removeChild(this.cssEl) if (this.cssEl) document.head.removeChild(this.cssEl)
} }
// 检查某个编辑节点类名是否存在,返回索引
checkEditNodeClassIndex(className) {
return this.editNodeClassList.findIndex(item => {
return item === className
})
}
// 添加一个编辑节点类名
addEditNodeClass(className) {
const index = this.checkEditNodeClassIndex(className)
if (index === -1) {
this.editNodeClassList.push(className)
}
}
// 删除一个编辑节点类名
deleteEditNodeClass(className) {
const index = this.checkEditNodeClassIndex(className)
if (index !== -1) {
this.editNodeClassList.splice(index, 1)
}
}
// 渲染,部分渲染 // 渲染,部分渲染
render(callback, source = '') { render(callback, source = '') {
this.batchExecution.push('render', () => {
this.initTheme() this.initTheme()
this.renderer.render(callback, source) this.renderer.render(callback, source)
})
} }
// 重新渲染 // 重新渲染
@ -395,6 +454,7 @@ class MindMap {
// 更新画布数据,如果新的数据是在当前画布节点数据基础上增删改查后形成的,那么可以使用该方法来更新画布数据 // 更新画布数据,如果新的数据是在当前画布节点数据基础上增删改查后形成的,那么可以使用该方法来更新画布数据
updateData(data) { updateData(data) {
data = this.handleData(data)
this.emit('before_update_data', data) this.emit('before_update_data', data)
this.renderer.setData(data) this.renderer.setData(data)
this.render() this.render()
@ -411,7 +471,7 @@ class MindMap {
this.command.clearHistory() this.command.clearHistory()
this.command.addHistory() this.command.addHistory()
this.renderer.setData(data) this.renderer.setData(data)
this.reRender(() => {}, CONSTANTS.SET_DATA) this.reRender()
this.emit('set_data', data) this.emit('set_data', data)
} }
@ -632,6 +692,35 @@ class MindMap {
} }
} }
// 扩展节点形状
addShape(shape) {
if (!shape) return
const exist = this.extendShapeList.find(item => {
return item.name === shape.name
})
if (exist) return
this.extendShapeList.push(shape)
}
// 删除扩展的形状
removeShape(name) {
const index = this.extendShapeList.findIndex(item => {
return item.name === name
})
if (index !== -1) {
this.extendShapeList.splice(index, 1)
}
}
// 获取SVG.js库的一些对象
getSvgObjects() {
return {
SVG,
G,
Rect
}
}
// 添加插件 // 添加插件
addPlugin(plugin, opt) { addPlugin(plugin, opt) {
let index = MindMap.hasPlugin(plugin) let index = MindMap.hasPlugin(plugin)
@ -695,6 +784,39 @@ class MindMap {
} }
} }
// 扩展节点数据中非样式的字段列表
// 内部会根据这个列表判断,如果不在这个列表里的字段都会认为是样式字段
/*
比如一个节点的数据为
{
data: {
text: '',
note: '',
color: ''
},
children: []
}
color字段不在nodeDataNoStylePropList列表中所以是样式内部一些操作的方法会用到所以如果你新增了自定义的节点数据并且不是`_`开头的那么需要通过该方法扩展
*/
let _extendNodeDataNoStylePropList = []
MindMap.extendNodeDataNoStylePropList = (list = []) => {
_extendNodeDataNoStylePropList.push(...list)
nodeDataNoStylePropList.push(...list)
}
MindMap.resetNodeDataNoStylePropList = () => {
_extendNodeDataNoStylePropList.forEach(item => {
const index = nodeDataNoStylePropList.findIndex(item2 => {
return item2 === item
})
if (index !== -1) {
nodeDataNoStylePropList.splice(index, 1)
}
})
_extendNodeDataNoStylePropList = []
}
// 插件列表 // 插件列表
MindMap.pluginList = [] MindMap.pluginList = []
MindMap.usePlugin = (plugin, opt = {}) => { MindMap.usePlugin = (plugin, opt = {}) => {

View File

@ -1,11 +1,11 @@
{ {
"name": "simple-mind-map", "name": "simple-mind-map",
"version": "0.12.1", "version": "0.14.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "0.12.1", "version": "0.14.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@svgdotjs/svg.js": "3.2.0", "@svgdotjs/svg.js": "3.2.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "simple-mind-map", "name": "simple-mind-map",
"version": "0.12.2", "version": "0.14.0-fix.1",
"description": "一个简单的web在线思维导图", "description": "一个简单的web在线思维导图",
"authors": [ "authors": [
{ {

View File

@ -2,8 +2,6 @@
export const CONSTANTS = { export const CONSTANTS = {
CHANGE_THEME: 'changeTheme', CHANGE_THEME: 'changeTheme',
CHANGE_LAYOUT: 'changeLayout', CHANGE_LAYOUT: 'changeLayout',
SET_DATA: 'setData',
TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode',
MODE: { MODE: {
READONLY: 'readonly', READONLY: 'readonly',
EDIT: 'edit' EDIT: 'edit'
@ -17,7 +15,12 @@ export const CONSTANTS = {
TIMELINE: 'timeline', TIMELINE: 'timeline',
TIMELINE2: 'timeline2', TIMELINE2: 'timeline2',
FISHBONE: 'fishbone', FISHBONE: 'fishbone',
VERTICAL_TIMELINE: 'verticalTimeline' FISHBONE2: 'fishbone2',
RIGHT_FISHBONE: 'rightFishbone',
RIGHT_FISHBONE2: 'rightFishbone2',
VERTICAL_TIMELINE: 'verticalTimeline',
VERTICAL_TIMELINE2: 'verticalTimeline2',
VERTICAL_TIMELINE3: 'verticalTimeline3'
}, },
DIR: { DIR: {
UP: 'up', UP: 'up',
@ -72,14 +75,15 @@ export const CONSTANTS = {
NOT_ACTIVE: 'notActive', NOT_ACTIVE: 'notActive',
ACTIVE_ONLY: 'activeOnly' ACTIVE_ONLY: 'activeOnly'
}, },
TAG_POSITION: { TAG_PLACEMENT: {
RIGHT: 'right', RIGHT: 'right',
BOTTOM: 'bottom' BOTTOM: 'bottom'
}, },
EDIT_NODE_CLASS: { IMG_PLACEMENT: {
SMM_NODE_EDIT_WRAP: 'smm-node-edit-wrap', LEFT: 'left',
RICH_TEXT_EDIT_WRAP: 'ql-editor', TOP: 'top',
ASSOCIATIVE_LINE_TEXT_EDIT_WRAP: 'associative-line-text-edit-warp' RIGHT: 'right',
BOTTOM: 'bottom'
} }
} }
@ -125,9 +129,29 @@ export const layoutList = [
name: '竖向时间轴', name: '竖向时间轴',
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE
}, },
{
name: '竖向时间轴2',
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE2
},
{
name: '竖向时间轴3',
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE3
},
{ {
name: '鱼骨图', name: '鱼骨图',
value: CONSTANTS.LAYOUT.FISHBONE value: CONSTANTS.LAYOUT.FISHBONE
},
{
name: '鱼骨图2',
value: CONSTANTS.LAYOUT.FISHBONE2
},
{
name: '向右鱼骨图',
value: CONSTANTS.LAYOUT.RIGHT_FISHBONE
},
{
name: '向右鱼骨图2',
value: CONSTANTS.LAYOUT.RIGHT_FISHBONE2
} }
] ]
export const layoutValueList = [ export const layoutValueList = [
@ -139,7 +163,12 @@ export const layoutValueList = [
CONSTANTS.LAYOUT.TIMELINE, CONSTANTS.LAYOUT.TIMELINE,
CONSTANTS.LAYOUT.TIMELINE2, CONSTANTS.LAYOUT.TIMELINE2,
CONSTANTS.LAYOUT.VERTICAL_TIMELINE, CONSTANTS.LAYOUT.VERTICAL_TIMELINE,
CONSTANTS.LAYOUT.FISHBONE CONSTANTS.LAYOUT.VERTICAL_TIMELINE2,
CONSTANTS.LAYOUT.VERTICAL_TIMELINE3,
CONSTANTS.LAYOUT.FISHBONE,
CONSTANTS.LAYOUT.FISHBONE2,
CONSTANTS.LAYOUT.RIGHT_FISHBONE,
CONSTANTS.LAYOUT.RIGHT_FISHBONE2
] ]
// 节点数据中非样式的字段 // 节点数据中非样式的字段
@ -157,7 +186,7 @@ export const nodeDataNoStylePropList = [
'isActive', 'isActive',
'generalization', 'generalization',
'richText', 'richText',
'resetRichText', 'resetRichText', // 重新创建富文本内容,去掉原有样式
'uid', 'uid',
'activeStyle', 'activeStyle',
'associativeLineTargets', 'associativeLineTargets',
@ -174,7 +203,10 @@ export const nodeDataNoStylePropList = [
'customTop', 'customTop',
'customTextWidth', 'customTextWidth',
'checkbox', 'checkbox',
'dir' 'dir',
'needUpdate', // 重新创建节点内容
'imgMap',
'nodeLink'
] ]
// 错误类型 // 错误类型
@ -208,7 +240,7 @@ export const cssContent = `
stroke-width: 2; stroke-width: 2;
} }
.smm-text-node-wrap { .smm-text-node-wrap, .smm-expand-btn-text {
user-select: none; user-select: none;
} }
` `
@ -226,3 +258,14 @@ export const selfCloseTagList = [
// 非富文本模式下的节点文本行高 // 非富文本模式下的节点文本行高
export const noneRichTextNodeLineHeight = 1.2 export const noneRichTextNodeLineHeight = 1.2
// 富文本支持的样式列表
export const richTextSupportStyleList = [
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color',
'textAlign'
]

View File

@ -35,8 +35,6 @@ export const defaultOpt = {
mouseScaleCenterUseMousePosition: true, mouseScaleCenterUseMousePosition: true,
// 最多显示几个标签 // 最多显示几个标签
maxTag: 5, maxTag: 5,
// 标签显示的位置相对于节点文本bottom下方、right右侧
tagPosition: CONSTANTS.TAG_POSITION.RIGHT,
// 展开收缩按钮尺寸 // 展开收缩按钮尺寸
expandBtnSize: 20, expandBtnSize: 20,
// 节点里图片和文字的间距 // 节点里图片和文字的间距
@ -268,6 +266,63 @@ export const defaultOpt = {
// 实例化完后是否立刻进行一次历史数据入栈操作 // 实例化完后是否立刻进行一次历史数据入栈操作
// 即调用mindMap.command.addHistory方法 // 即调用mindMap.command.addHistory方法
addHistoryOnInit: true, addHistoryOnInit: true,
// 自定义节点备注图标
noteIcon: {
icon: '', // svg字符串如果不是确定要使用svg自带的样式否则请去除其中的fill等样式属性
style: {
// size: 20,// 图标大小不手动设置则会使用主题的iconSize配置
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
}
},
// 自定义节点超链接图标
hyperlinkIcon: {
icon: '', // svg字符串如果不是确定要使用svg自带的样式否则请去除其中的fill等样式属性
style: {
// size: 20,// 图标大小不手动设置则会使用主题的iconSize配置
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
}
},
// 自定义节点附件图标
attachmentIcon: {
icon: '', // svg字符串如果不是确定要使用svg自带的样式否则请去除其中的fill等样式属性
style: {
// size: 20,// 图标大小不手动设置则会使用主题的iconSize配置
// color: '',// 图标颜色,不手动设置则会使用节点文本的颜色
}
},
// 是否显示快捷创建子节点按钮
isShowCreateChildBtnIcon: true,
// 自定义快捷创建子节点按钮图标
quickCreateChildBtnIcon: {
icon: '', // svg字符串如果不是确定要使用svg自带的样式否则请去除其中的fill等样式属性
style: {
// 图标大小使用的是expandBtnSize选项
// color: '',// 图标颜色不手动设置则会使用expandBtnStyle选项的color字段
}
},
// 自定义快捷创建子节点按钮的点击操作,
customQuickCreateChildBtnClick: null,
// 添加自定义的节点内容
// 可传递一个对象,格式如下:
/*
{
// 返回要添加的DOM元素详细
create: (node) => {
return {
el, // DOM节点
width: 20, // 宽高
height: 20
}
},
// 处理生成的@svgdotjs/svg.js库的ForeignObject节点实例可以设置其在节点内的位置
handle: ({ content, element, node }) => {
}
}
*/
addCustomContentToNode: null,
// 节点连线样式是否允许继承祖先的连线样式
enableInheritAncestorLineStyle: true,
// 【Select插件】 // 【Select插件】
// 多选节点时鼠标移动到边缘时的画布移动偏移量 // 多选节点时鼠标移动到边缘时的画布移动偏移量
@ -441,6 +496,7 @@ export const defaultOpt = {
// 【OuterFrame】插件 // 【OuterFrame】插件
outerFramePaddingX: 10, outerFramePaddingX: 10,
outerFramePaddingY: 10, outerFramePaddingY: 10,
defaultOuterFrameText: '外框',
// 【Painter】插件 // 【Painter】插件
// 是否只格式刷节点手动设置的样式,不考虑节点通过主题的应用的样式 // 是否只格式刷节点手动设置的样式,不考虑节点通过主题的应用的样式
@ -458,5 +514,10 @@ export const defaultOpt = {
maxImgResizeWidthInheritTheme: false, maxImgResizeWidthInheritTheme: false,
// 最大允许缩放的尺寸maxImgResizeWidthInheritTheme选项设置为false时生效不限制最大值可传递Infinity // 最大允许缩放的尺寸maxImgResizeWidthInheritTheme选项设置为false时生效不限制最大值可传递Infinity
maxImgResizeWidth: Infinity, maxImgResizeWidth: Infinity,
maxImgResizeHeight: Infinity maxImgResizeHeight: Infinity,
// 自定义删除按钮和尺寸调整按钮的内容
// 默认为内置图标你可以传递一个svg字符串或者其他的html字符串
// 整体大小请使用上面的minImgResizeWidth和minImgResizeHeight选项设置
customDeleteBtnInnerHTML: '',
customResizeBtnInnerHTML: ''
} }

View File

@ -6,6 +6,7 @@ import {
transformTreeDataToObject transformTreeDataToObject
} from '../../utils' } from '../../utils'
import { ERROR_TYPES } from '../../constants/constant' import { ERROR_TYPES } from '../../constants/constant'
import pkg from '../../../package.json'
// 命令类 // 命令类
class Command { class Command {
@ -14,7 +15,7 @@ class Command {
this.opt = opt this.opt = opt
this.mindMap = opt.mindMap this.mindMap = opt.mindMap
this.commands = {} this.commands = {}
this.history = [] this.history = [] // 字符串形式存储
this.activeHistoryIndex = 0 this.activeHistoryIndex = 0
// 注册快捷键 // 注册快捷键
this.registerShortcutKeys() this.registerShortcutKeys()
@ -105,18 +106,19 @@ class Command {
if (this.mindMap.opt.readonly || this.isPause) { if (this.mindMap.opt.readonly || this.isPause) {
return return
} }
const lastData = this.mindMap.emit('beforeAddHistory')
const lastDataStr =
this.history.length > 0 ? this.history[this.activeHistoryIndex] : null this.history.length > 0 ? this.history[this.activeHistoryIndex] : null
const data = this.getCopyData() const data = this.getCopyData()
const dataStr = JSON.stringify(data)
// 此次数据和上次一样则不重复添加 // 此次数据和上次一样则不重复添加
if (lastData === data) return if (lastDataStr && lastDataStr === dataStr) {
if (lastData && JSON.stringify(lastData) === JSON.stringify(data)) {
return return
} }
this.emitDataUpdatesEvent(lastData, data) this.emitDataUpdatesEvent(lastDataStr, dataStr)
// 删除当前历史指针后面的数据 // 删除当前历史指针后面的数据
this.history = this.history.slice(0, this.activeHistoryIndex + 1) this.history = this.history.slice(0, this.activeHistoryIndex + 1)
this.history.push(simpleDeepClone(data)) this.history.push(dataStr)
// 历史记录数超过最大数量 // 历史记录数超过最大数量
if (this.history.length > this.mindMap.opt.maxHistoryCount) { if (this.history.length > this.mindMap.opt.maxHistoryCount) {
this.history.shift() this.history.shift()
@ -136,15 +138,16 @@ class Command {
return return
} }
if (this.activeHistoryIndex - step >= 0) { if (this.activeHistoryIndex - step >= 0) {
const lastData = this.history[this.activeHistoryIndex] const lastDataStr = this.history[this.activeHistoryIndex]
this.activeHistoryIndex -= step this.activeHistoryIndex -= step
this.mindMap.emit( this.mindMap.emit(
'back_forward', 'back_forward',
this.activeHistoryIndex, this.activeHistoryIndex,
this.history.length this.history.length
) )
const data = simpleDeepClone(this.history[this.activeHistoryIndex]) const dataStr = this.history[this.activeHistoryIndex]
this.emitDataUpdatesEvent(lastData, data) const data = JSON.parse(dataStr)
this.emitDataUpdatesEvent(lastDataStr, dataStr)
return data return data
} }
} }
@ -156,15 +159,16 @@ class Command {
} }
let len = this.history.length let len = this.history.length
if (this.activeHistoryIndex + step <= len - 1) { if (this.activeHistoryIndex + step <= len - 1) {
const lastData = this.history[this.activeHistoryIndex] const lastDataStr = this.history[this.activeHistoryIndex]
this.activeHistoryIndex += step this.activeHistoryIndex += step
this.mindMap.emit( this.mindMap.emit(
'back_forward', 'back_forward',
this.activeHistoryIndex, this.activeHistoryIndex,
this.history.length this.history.length
) )
const data = simpleDeepClone(this.history[this.activeHistoryIndex]) const dataStr = this.history[this.activeHistoryIndex]
this.emitDataUpdatesEvent(lastData, data) const data = JSON.parse(dataStr)
this.emitDataUpdatesEvent(lastDataStr, dataStr)
return data return data
} }
} }
@ -172,7 +176,9 @@ class Command {
// 获取渲染树数据副本 // 获取渲染树数据副本
getCopyData() { getCopyData() {
if (!this.mindMap.renderer.renderTree) return null if (!this.mindMap.renderer.renderTree) return null
return copyRenderTree({}, this.mindMap.renderer.renderTree, true) const res = copyRenderTree({}, this.mindMap.renderer.renderTree, true)
res.smmVersion = pkg.version
return res
} }
// 移除节点数据中的uid // 移除节点数据中的uid
@ -191,12 +197,14 @@ class Command {
} }
// 派发思维导图更新明细事件 // 派发思维导图更新明细事件
emitDataUpdatesEvent(lastData, data) { emitDataUpdatesEvent(lastDataStr, dataStr) {
try { try {
// 如果data_change_detail没有监听者那么不进行计算节省性能 // 如果data_change_detail没有监听者那么不进行计算节省性能
const eventName = 'data_change_detail' const eventName = 'data_change_detail'
const count = this.mindMap.event.listenerCount(eventName) const count = this.mindMap.event.listenerCount(eventName)
if (count > 0 && lastData && data) { if (count > 0 && lastDataStr && dataStr) {
const lastData = JSON.parse(lastDataStr)
const data = JSON.parse(dataStr)
const lastDataObj = simpleDeepClone(transformTreeDataToObject(lastData)) const lastDataObj = simpleDeepClone(transformTreeDataToObject(lastData))
const dataObj = simpleDeepClone(transformTreeDataToObject(data)) const dataObj = simpleDeepClone(transformTreeDataToObject(data))
const res = [] const res = []

View File

@ -1,5 +1,4 @@
import { keyMap } from './keyMap' import { keyMap } from './keyMap'
import { CONSTANTS } from '../../constants/constant'
// 快捷按键、命令处理类 // 快捷按键、命令处理类
export default class KeyCommand { export default class KeyCommand {
@ -13,6 +12,8 @@ export default class KeyCommand {
this.shortcutMapCache = {} this.shortcutMapCache = {}
this.isPause = false this.isPause = false
this.isInSvg = false this.isInSvg = false
this.isStopCheckInSvg = false
this.defaultEnableCheck = this.defaultEnableCheck.bind(this)
this.bindEvent() this.bindEvent()
} }
@ -58,6 +59,22 @@ export default class KeyCommand {
this.shortcutMapCache = {} this.shortcutMapCache = {}
} }
// 停止对鼠标是否在画布内的检查前提是开启了enableShortcutOnlyWhenMouseInSvg选项
// 库内部节点文本编辑、关联线文本编辑、外框文本编辑前都会暂停检查,否则无法响应回车快捷键用于结束编辑
// 如果你新增了额外的文本编辑,也可以在编辑前调用此方法
stopCheckInSvg() {
const { enableShortcutOnlyWhenMouseInSvg } = this.mindMap.opt
if (!enableShortcutOnlyWhenMouseInSvg) return
this.isStopCheckInSvg = true
}
// 恢复对鼠标是否在画布内的检查
recoveryCheckInSvg() {
const { enableShortcutOnlyWhenMouseInSvg } = this.mindMap.opt
if (!enableShortcutOnlyWhenMouseInSvg) return
this.isStopCheckInSvg = true
}
// 绑定事件 // 绑定事件
bindEvent() { bindEvent() {
this.onKeydown = this.onKeydown.bind(this) this.onKeydown = this.onKeydown.bind(this)
@ -66,13 +83,6 @@ export default class KeyCommand {
this.isInSvg = true this.isInSvg = true
}) })
this.mindMap.on('svg_mouseleave', () => { this.mindMap.on('svg_mouseleave', () => {
if (this.mindMap.renderer.textEdit.isShowTextEdit()) return
if (
this.mindMap.associativeLine &&
this.mindMap.associativeLine.showTextEdit
) {
return
}
this.isInSvg = false this.isInSvg = false
}) })
window.addEventListener('keydown', this.onKeydown) window.addEventListener('keydown', this.onKeydown)
@ -89,12 +99,14 @@ export default class KeyCommand {
// 根据事件目标判断是否响应快捷键事件 // 根据事件目标判断是否响应快捷键事件
defaultEnableCheck(e) { defaultEnableCheck(e) {
const target = e.target const target = e.target
return ( if (target === document.body) return true
target === document.body || for (let i = 0; i < this.mindMap.editNodeClassList.length; i++) {
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.SMM_NODE_EDIT_WRAP) || const cur = this.mindMap.editNodeClassList[i]
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP) || if (target.classList.contains(cur)) {
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.ASSOCIATIVE_LINE_TEXT_EDIT_WRAP) return true
) }
}
return false
} }
// 按键事件 // 按键事件
@ -109,7 +121,12 @@ export default class KeyCommand {
? customCheckEnableShortcut ? customCheckEnableShortcut
: this.defaultEnableCheck : this.defaultEnableCheck
if (!checkFn(e)) return if (!checkFn(e)) return
if (this.isPause || (enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) { if (
this.isPause ||
(enableShortcutOnlyWhenMouseInSvg &&
!this.isStopCheckInSvg &&
!this.isInSvg)
) {
return return
} }
Object.keys(this.shortcutMap).forEach(key => { Object.keys(this.shortcutMap).forEach(key => {

View File

@ -30,10 +30,10 @@ import {
createSmmFormatData, createSmmFormatData,
checkSmmFormatData, checkSmmFormatData,
checkIsNodeStyleDataKey, checkIsNodeStyleDataKey,
removeRichTextStyes,
formatGetNodeGeneralization, formatGetNodeGeneralization,
sortNodeList, sortNodeList,
throttle, throttle,
debounce,
checkClipboardReadEnable, checkClipboardReadEnable,
isNodeNotNeedRenderData isNodeNotNeedRenderData
} from '../../utils' } from '../../utils'
@ -60,8 +60,14 @@ const layouts = {
[CONSTANTS.LAYOUT.TIMELINE2]: Timeline, [CONSTANTS.LAYOUT.TIMELINE2]: Timeline,
// 竖向时间轴 // 竖向时间轴
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline, [CONSTANTS.LAYOUT.VERTICAL_TIMELINE]: VerticalTimeline,
// 竖向时间轴2
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE2]: VerticalTimeline,
// 竖向时间轴3
[CONSTANTS.LAYOUT.VERTICAL_TIMELINE3]: VerticalTimeline,
// 鱼骨图 // 鱼骨图
[CONSTANTS.LAYOUT.FISHBONE]: Fishbone [CONSTANTS.LAYOUT.FISHBONE]: Fishbone,
// 鱼骨图2
[CONSTANTS.LAYOUT.FISHBONE2]: Fishbone
} }
// 渲染 // 渲染
@ -81,14 +87,18 @@ class Render {
this.isRendering = false this.isRendering = false
// 是否存在等待渲染 // 是否存在等待渲染
this.hasWaitRendering = false this.hasWaitRendering = false
this.waitRenderingParams = []
// 用于缓存节点 // 用于缓存节点
this.nodeCache = {} this.nodeCache = {}
this.lastNodeCache = {} this.lastNodeCache = {}
// 触发render的来源 // 收集触发render的来源
this.renderSource = '' this.renderSourceList = []
// 收集render的回调函数
this.renderCallbackList = []
// 当前激活的节点列表 // 当前激活的节点列表
this.activeNodeList = [] this.activeNodeList = []
// 防抖定时器
this.emitNodeActiveEventTimer = null
this.renderTimer = null
// 根节点 // 根节点
this.root = null this.root = null
// 文本编辑框需要再bindEvent之前实例化否则单击事件只能触发隐藏文本编辑框而无法保存文本修改 // 文本编辑框需要再bindEvent之前实例化否则单击事件只能触发隐藏文本编辑框而无法保存文本修改
@ -112,12 +122,16 @@ class Render {
// 设置布局结构 // 设置布局结构
setLayout() { setLayout() {
if (this.layout && this.layout.beforeChange) {
this.layout.beforeChange()
}
const { layout } = this.mindMap.opt const { layout } = this.mindMap.opt
this.layout = new ( let L = layouts[layout] || this.mindMap[layout]
layouts[layout] if (!L) {
? layouts[layout] L = layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE]
: layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE] this.mindMap.opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
)(this, layout) }
this.layout = new L(this, layout)
} }
// 重新设置思维导图数据 // 重新设置思维导图数据
@ -147,6 +161,9 @@ class Render {
}) })
// 性能模式 // 性能模式
const onViewDataChange = throttle(() => { const onViewDataChange = throttle(() => {
if (!this.renderTree) {
return
}
if (this.root) { if (this.root) {
this.mindMap.emit('node_tree_render_start') this.mindMap.emit('node_tree_render_start')
this.root.render( this.root.render(
@ -162,7 +179,7 @@ class Render {
this.mindMap.on('view_data_change', onViewDataChange) this.mindMap.on('view_data_change', onViewDataChange)
} }
// 文本编辑时实时更新节点大小 // 文本编辑时实时更新节点大小
this.onNodeTextEditChange = this.onNodeTextEditChange.bind(this) this.onNodeTextEditChange = debounce(this.onNodeTextEditChange, 100, this)
if (openRealtimeRenderOnNodeTextEdit) { if (openRealtimeRenderOnNodeTextEdit) {
this.mindMap.on('node_text_edit_change', this.onNodeTextEditChange) this.mindMap.on('node_text_edit_change', this.onNodeTextEditChange)
} }
@ -443,9 +460,10 @@ class Render {
) )
if (!isChange) return if (!isChange) return
this.lastActiveNodeList = [...activeNodeList] this.lastActiveNodeList = [...activeNodeList]
this.mindMap.batchExecution.push('emitNodeActiveEvent', () => { clearTimeout(this.emitNodeActiveEventTimer)
this.emitNodeActiveEventTimer = setTimeout(() => {
this.mindMap.emit('node_active', node, activeNodeList) this.mindMap.emit('node_active', node, activeNodeList)
}) }, 0)
} }
// 鼠标点击画布时清空当前激活节点列表 // 鼠标点击画布时清空当前激活节点列表
@ -488,22 +506,71 @@ class Render {
this.lastNodeCache = {} this.lastNodeCache = {}
} }
// 保存触发渲染的参数
addRenderParams(callback, source) {
if (callback) {
const index = this.renderCallbackList.findIndex(fn => {
return fn === callback
})
if (index === -1) {
this.renderCallbackList.push(callback)
}
}
if (source) {
const index = this.renderSourceList.findIndex(s => {
return s === source
})
if (index === -1) {
this.renderSourceList.push(source)
}
}
}
// 判断是否包含某种触发渲染源
checkHasRenderSource(val) {
val = Array.isArray(val) ? val : [val]
for (let i = 0; i < this.renderSourceList.length; i++) {
if (val.includes(this.renderSourceList[i])) {
return true
}
}
return false
}
// 渲染完毕的操作
onRenderEnd() {
this.renderCallbackList.forEach(fn => {
fn()
})
this.isRendering = false
this.reRender = false
this.renderCallbackList = []
this.renderSourceList = []
this.mindMap.emit('node_tree_render_end')
}
// 渲染 // 渲染
render(callback = () => {}, source) { render(callback, source) {
this.addRenderParams(callback, source)
clearTimeout(this.renderTimer)
this.renderTimer = setTimeout(() => {
this._render()
}, 0)
}
// 真正的渲染
_render() {
// 切换主题时,被收起的节点需要添加样式复位的标注 // 切换主题时,被收起的节点需要添加样式复位的标注
if (source === CONSTANTS.CHANGE_THEME) { if (this.checkHasRenderSource(CONSTANTS.CHANGE_THEME)) {
this.resetUnExpandNodeStyle() this.resetUnExpandNodeStyle()
} }
// 如果当前还没有渲染完毕,不再触发渲染 // 如果当前还没有渲染完毕,不再触发渲染
if (this.isRendering) { if (this.isRendering) {
// 等待当前渲染完毕后再进行一次渲染 // 等待当前渲染完毕后再进行一次渲染
this.hasWaitRendering = true this.hasWaitRendering = true
this.waitRenderingParams = [callback, source]
return return
} }
this.isRendering = true this.isRendering = true
// 触发当前重新渲染的来源
this.renderSource = source
// 节点缓存 // 节点缓存
this.lastNodeCache = this.nodeCache this.lastNodeCache = this.nodeCache
this.nodeCache = {} this.nodeCache = {}
@ -513,8 +580,7 @@ class Render {
} }
// 如果没有节点数据 // 如果没有节点数据
if (!this.renderTree) { if (!this.renderTree) {
this.isRendering = false this.onRenderEnd()
this.mindMap.emit('node_tree_render_end')
return return
} }
this.mindMap.emit('node_tree_render_start') this.mindMap.emit('node_tree_render_start')
@ -536,38 +602,32 @@ class Render {
// 渲染节点 // 渲染节点
this.root.render(() => { this.root.render(() => {
this.isRendering = false this.isRendering = false
callback && callback()
if (this.hasWaitRendering) { if (this.hasWaitRendering) {
const params = this.waitRenderingParams
this.hasWaitRendering = false this.hasWaitRendering = false
this.waitRenderingParams = [] this.render()
this.render(...params) return
} else {
this.renderSource = ''
if (this.reRender) {
this.reRender = false
} }
// 触发一次保存,因为修改了渲染树的数据 this.onRenderEnd()
if (
this.hasRichTextPlugin() &&
[CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)
) {
this.mindMap.command.addHistory()
}
}
this.mindMap.emit('node_tree_render_end')
}) })
}) })
this.emitNodeActiveEvent() this.emitNodeActiveEvent()
} }
// 给当前被收起来的节点数据添加文本复位标志 // 当某个自定义节点内容改变后,可以调用该方法实时更新该节点大小和整体节点的定位
renderByCustomNodeContentNode(node) {
node.getSize()
node.customNodeContentRealtimeLayout()
this.mindMap.render()
}
// 给当前被收起来的节点数据添加更新标志
resetUnExpandNodeStyle() { resetUnExpandNodeStyle() {
if (!this.renderTree || !this.hasRichTextPlugin()) return if (!this.renderTree) return
walk(this.renderTree, null, node => { walk(this.renderTree, null, node => {
if (!node.data.expand) { if (!node.data.expand) {
walk(node, null, node2 => { walk(node, null, node2 => {
node2.data.resetRichText = true // 主要是触发数据新旧对比,不一样则会重新创建节点
node2.data['needUpdate'] = true
}) })
return true return true
} }
@ -750,15 +810,16 @@ class Render {
richText: isRichText, richText: isRichText,
isActive: focusNewNode // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态 isActive: focusNewNode // 如果同时对多个节点插入子节点,那么需要把新增的节点设为激活状态。如果不进入编辑状态,那么也需要手动设为激活状态
} }
if (isRichText) params.resetRichText = isRichText if (isRichText) params.resetRichText = true
// 动态指定的子节点数据也需要添加相关属性 // 动态指定的子节点数据也需要添加相关属性
appointChildren = addDataToAppointNodes(appointChildren, { appointChildren = addDataToAppointNodes(appointChildren, params)
...params const alreadyIsRichText = appointData && appointData.richText
}) let createNewId = false
list.forEach(node => { list.forEach(node => {
if (node.isGeneralization || node.isRoot) { if (node.isGeneralization || node.isRoot) {
return return
} }
appointChildren = simpleDeepClone(appointChildren)
const parent = node.parent const parent = node.parent
const isOneLayer = node.layerIndex === 1 const isOneLayer = node.layerIndex === 1
// 新插入节点的默认文本 // 新插入节点的默认文本
@ -767,6 +828,10 @@ class Render {
: defaultInsertBelowSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
// 计算插入位置 // 计算插入位置
const index = getNodeDataIndex(node) const index = getNodeDataIndex(node)
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNodeData = { const newNodeData = {
inserting, inserting,
data: { data: {
@ -775,8 +840,9 @@ class Render {
uid: createUid(), uid: createUid(),
...(appointData || {}) ...(appointData || {})
}, },
children: [...createUidForAppointNodes(appointChildren)] children: [...createUidForAppointNodes(appointChildren, createNewId)]
} }
createNewId = true
parent.nodeData.children.splice(index + 1, 0, newNodeData) parent.nodeData.children.splice(index + 1, 0, newNodeData)
}) })
// 如果同时对多个节点插入子节点,需要清除原来激活的节点 // 如果同时对多个节点插入子节点,需要清除原来激活的节点
@ -802,16 +868,19 @@ class Render {
richText: isRichText, richText: isRichText,
isActive: focusNewNode isActive: focusNewNode
} }
if (isRichText) params.resetRichText = isRichText if (isRichText) params.resetRichText = true
nodeList = addDataToAppointNodes(nodeList, params) nodeList = addDataToAppointNodes(nodeList, params)
let createNewId = false
list.forEach(node => { list.forEach(node => {
if (node.isGeneralization || node.isRoot) { if (node.isGeneralization || node.isRoot) {
return return
} }
nodeList = simpleDeepClone(nodeList)
const parent = node.parent const parent = node.parent
// 计算插入位置 // 计算插入位置
const index = getNodeDataIndex(node) const index = getNodeDataIndex(node)
const newNodeList = createUidForAppointNodes(simpleDeepClone(nodeList)) const newNodeList = createUidForAppointNodes(nodeList, createNewId)
createNewId = true
parent.nodeData.children.splice(index + 1, 0, ...newNodeList) parent.nodeData.children.splice(index + 1, 0, ...newNodeList)
}) })
if (focusNewNode) { if (focusNewNode) {
@ -848,21 +917,26 @@ class Render {
richText: isRichText, richText: isRichText,
isActive: focusNewNode isActive: focusNewNode
} }
if (isRichText) params.resetRichText = isRichText if (isRichText) params.resetRichText = true
// 动态指定的子节点数据也需要添加相关属性 // 动态指定的子节点数据也需要添加相关属性
appointChildren = addDataToAppointNodes(appointChildren, { appointChildren = addDataToAppointNodes(appointChildren, params)
...params const alreadyIsRichText = appointData && appointData.richText
}) let createNewId = false
list.forEach(node => { list.forEach(node => {
if (node.isGeneralization) { if (node.isGeneralization) {
return return
} }
appointChildren = simpleDeepClone(appointChildren)
if (!node.nodeData.children) { if (!node.nodeData.children) {
node.nodeData.children = [] node.nodeData.children = []
} }
const text = node.isRoot const text = node.isRoot
? defaultInsertSecondLevelNodeText ? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNode = { const newNode = {
inserting, inserting,
data: { data: {
@ -871,8 +945,9 @@ class Render {
...params, ...params,
...(appointData || {}) ...(appointData || {})
}, },
children: [...createUidForAppointNodes(appointChildren)] children: [...createUidForAppointNodes(appointChildren, createNewId)]
} }
createNewId = true
node.nodeData.children.push(newNode) node.nodeData.children.push(newNode)
// 插入子节点时自动展开子节点 // 插入子节点时自动展开子节点
node.setData({ node.setData({
@ -902,16 +977,20 @@ class Render {
richText: isRichText, richText: isRichText,
isActive: focusNewNode isActive: focusNewNode
} }
if (isRichText) params.resetRichText = isRichText if (isRichText) params.resetRichText = true
childList = addDataToAppointNodes(childList, params) childList = addDataToAppointNodes(childList, params)
let createNewId = false
list.forEach(node => { list.forEach(node => {
if (node.isGeneralization) { if (node.isGeneralization) {
return return
} }
childList = simpleDeepClone(childList)
if (!node.nodeData.children) { if (!node.nodeData.children) {
node.nodeData.children = [] node.nodeData.children = []
} }
childList = createUidForAppointNodes(childList) childList = createUidForAppointNodes(childList, createNewId)
// 第一个引用不需要重新创建uid后面的需要重新创建否则id会重复
createNewId = true
node.nodeData.children.push(...childList) node.nodeData.children.push(...childList)
// 插入子节点时自动展开子节点 // 插入子节点时自动展开子节点
node.setData({ node.setData({
@ -947,7 +1026,8 @@ class Render {
richText: isRichText, richText: isRichText,
isActive: focusNewNode isActive: focusNewNode
} }
if (isRichText) params.resetRichText = isRichText if (isRichText) params.resetRichText = true
const alreadyIsRichText = appointData && appointData.richText
list.forEach(node => { list.forEach(node => {
if (node.isGeneralization || node.isRoot) { if (node.isGeneralization || node.isRoot) {
return return
@ -956,6 +1036,10 @@ class Render {
node.layerIndex === 1 node.layerIndex === 1
? defaultInsertSecondLevelNodeText ? defaultInsertSecondLevelNodeText
: defaultInsertBelowSecondLevelNodeText : defaultInsertBelowSecondLevelNodeText
// 如果指定的数据就是富文本格式,那么不需要重新创建
if (alreadyIsRichText && params.resetRichText) {
delete params.resetRichText
}
const newNode = { const newNode = {
inserting, inserting,
data: { data: {
@ -966,11 +1050,6 @@ class Render {
}, },
children: [node.nodeData] children: [node.nodeData]
} }
if (isRichText) {
node.setData({
resetRichText: true
})
}
const parent = node.parent const parent = node.parent
// 获取当前节点所在位置 // 获取当前节点所在位置
const index = getNodeDataIndex(node) const index = getNodeDataIndex(node)
@ -1046,7 +1125,6 @@ class Render {
const index = getNodeIndexInNodeList(node, parent.children) const index = getNodeIndexInNodeList(node, parent.children)
const parentIndex = getNodeIndexInNodeList(parent, grandpa.children) const parentIndex = getNodeIndexInNodeList(parent, grandpa.children)
// 节点数据 // 节点数据
this.checkNodeLayerChange(node, parent)
parent.nodeData.children.splice(index, 1) parent.nodeData.children.splice(index, 1)
grandpa.nodeData.children.splice(parentIndex + 1, 0, node.nodeData) grandpa.nodeData.children.splice(parentIndex + 1, 0, node.nodeData)
this.mindMap.render() this.mindMap.render()
@ -1061,10 +1139,10 @@ class Render {
delete nodeData[key] delete nodeData[key]
} }
}) })
// 如果是富文本,那么还要处理富文本内容 // 如果是富文本,那么直接全部重新创建,因为有些样式是通过标签来渲染的
if (hasCustomStyles && this.hasRichTextPlugin()) { if (this.hasRichTextPlugin()) {
hasCustomStyles = true
nodeData.resetRichText = true nodeData.resetRichText = true
nodeData.text = removeRichTextStyes(nodeData.text)
} }
return hasCustomStyles return hasCustomStyles
} }
@ -1208,7 +1286,10 @@ class Render {
Array.isArray(smmData) ? smmData : [smmData] Array.isArray(smmData) ? smmData : [smmData]
) )
} else { } else {
// 如果是富文本模式,那么需要转义特殊字符
if (this.hasRichTextPlugin()) {
text = htmlEscape(text) text = htmlEscape(text)
}
const textArr = text const textArr = text
.split(new RegExp('\r?\n|(?<!\n)\r', 'g')) .split(new RegExp('\r?\n|(?<!\n)\r', 'g'))
.filter(item => { .filter(item => {
@ -1302,7 +1383,6 @@ class Render {
nodeList.reverse() nodeList.reverse()
} }
nodeList.forEach(item => { nodeList.forEach(item => {
this.checkNodeLayerChange(item, exist)
// 移动节点 // 移动节点
let nodeParent = item.parent let nodeParent = item.parent
let nodeBorthers = nodeParent.children let nodeBorthers = nodeParent.children
@ -1329,25 +1409,6 @@ class Render {
this.mindMap.render() this.mindMap.render()
} }
// 如果是富文本模式,那么某些层级变化需要更新样式
checkNodeLayerChange(node, toNode, toNodeIsParent = false) {
if (this.hasRichTextPlugin()) {
// 如果设置了自定义样式那么不需要更新
if (this.mindMap.richText.checkNodeHasCustomRichTextStyle(node)) {
return
}
const toIndex = toNodeIsParent ? toNode.layerIndex + 1 : toNode.layerIndex
let nodeLayerChanged =
(node.layerIndex === 1 && toIndex !== 1) ||
(node.layerIndex !== 1 && toIndex === 1)
if (nodeLayerChanged) {
node.setData({
resetRichText: true
})
}
}
}
// 移除节点 // 移除节点
removeNode(appointNodes = []) { removeNode(appointNodes = []) {
appointNodes = formatDataToArray(appointNodes) appointNodes = formatDataToArray(appointNodes)
@ -1531,7 +1592,6 @@ class Render {
return !item.isRoot return !item.isRoot
}) })
nodeList.forEach(item => { nodeList.forEach(item => {
this.checkNodeLayerChange(item, toNode, true)
this.removeNodeFromActiveList(item) this.removeNodeFromActiveList(item)
removeFromParentNodeData(item) removeFromParentNodeData(item)
toNode.setData({ toNode.setData({
@ -1546,35 +1606,7 @@ class Render {
// 粘贴节点到节点 // 粘贴节点到节点
pasteNode(data) { pasteNode(data) {
data = formatDataToArray(data) data = formatDataToArray(data)
if (this.activeNodeList.length <= 0 || data.length <= 0) { this.mindMap.execCommand('INSERT_MULTI_CHILD_NODE', [], data)
return
}
this.activeNodeList.forEach(node => {
// 概要节点不允许添加下级节点
if (node.isGeneralization) return
node.setData({
expand: true
})
node.nodeData.children.push(
...data.map(item => {
const newData = simpleDeepClone(item)
createUidForAppointNodes([newData], true, node => {
// 可能跨层级复制,那么富文本样式需要更新
if (this.hasRichTextPlugin()) {
// 如果设置了自定义样式那么不需要更新
if (
this.mindMap.richText.checkNodeHasCustomRichTextStyle(node.data)
) {
return
}
node.data.resetRichText = true
}
})
return newData
})
)
})
this.mindMap.render()
} }
// 设置节点样式 // 设置节点样式
@ -1582,13 +1614,6 @@ class Render {
const data = { const data = {
[prop]: value [prop]: value
} }
// 如果开启了富文本,则需要应用到富文本上
if (
this.hasRichTextPlugin() &&
this.mindMap.richText.isHasRichTextStyle(data)
) {
data.resetRichText = true
}
this.setNodeDataRender(node, data) this.setNodeDataRender(node, data)
// 更新了连线的样式 // 更新了连线的样式
if (lineStyleProps.includes(prop)) { if (lineStyleProps.includes(prop)) {
@ -1599,13 +1624,6 @@ class Render {
// 设置节点多个样式 // 设置节点多个样式
setNodeStyles(node, style) { setNodeStyles(node, style) {
const data = { ...style } const data = { ...style }
// 如果开启了富文本,则需要应用到富文本上
if (
this.hasRichTextPlugin() &&
this.mindMap.richText.isHasRichTextStyle(data)
) {
data.resetRichText = true
}
this.setNodeDataRender(node, data) this.setNodeDataRender(node, data)
// 更新了连线的样式 // 更新了连线的样式
let props = Object.keys(style) let props = Object.keys(style)
@ -1826,6 +1844,7 @@ class Render {
list.length > 1 list.length > 1
) )
let needRender = false let needRender = false
const alreadyIsRichText = data && data.richText
list.forEach(item => { list.forEach(item => {
const newData = { const newData = {
inserting, inserting,
@ -1837,7 +1856,7 @@ class Render {
richText: isRichText, richText: isRichText,
isActive: focusNewNode isActive: focusNewNode
} }
if (isRichText) newData.resetRichText = isRichText if (isRichText && !alreadyIsRichText) newData.resetRichText = isRichText
let generalization = item.node.getData('generalization') let generalization = item.node.getData('generalization')
generalization = generalization generalization = generalization
? Array.isArray(generalization) ? Array.isArray(generalization)

View File

@ -16,6 +16,8 @@ import {
noneRichTextNodeLineHeight noneRichTextNodeLineHeight
} from '../../constants/constant' } from '../../constants/constant'
const SMM_NODE_EDIT_WRAP = 'smm-node-edit-wrap'
// 节点文字编辑类 // 节点文字编辑类
export default class TextEdit { export default class TextEdit {
// 构造函数 // 构造函数
@ -33,6 +35,8 @@ export default class TextEdit {
this.hasBodyMousedown = false this.hasBodyMousedown = false
this.textNodePaddingX = 5 this.textNodePaddingX = 5
this.textNodePaddingY = 3 this.textNodePaddingY = 3
this.isNeedUpdateTextEditNode = false
this.mindMap.addEditNodeClass(SMM_NODE_EDIT_WRAP)
this.bindEvent() this.bindEvent()
} }
@ -91,7 +95,7 @@ export default class TextEdit {
}) })
}) })
this.mindMap.on('scale', this.onScale) this.mindMap.on('scale', this.onScale)
// // 监听按键事件,判断是否自动进入文本编辑模式 // 监听按键事件,判断是否自动进入文本编辑模式
if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) { if (this.mindMap.opt.enableAutoEnterTextEditWhenKeydown) {
window.addEventListener('keydown', this.onKeydown) window.addEventListener('keydown', this.onKeydown)
} }
@ -124,6 +128,18 @@ export default class TextEdit {
]('keydown', this.onKeydown) ]('keydown', this.onKeydown)
} }
}) })
// 正在编辑文本时,给节点添加了图标等其他内容时需要更新编辑框的位置
this.mindMap.on('afterExecCommand', () => {
if (!this.isShowTextEdit()) return
this.isNeedUpdateTextEditNode = true
})
this.mindMap.on('node_tree_render_end', () => {
if (!this.isShowTextEdit()) return
if (this.isNeedUpdateTextEditNode) {
this.isNeedUpdateTextEditNode = false
this.updateTextEditNode()
}
})
} }
// 解绑事件 // 解绑事件
@ -139,6 +155,9 @@ export default class TextEdit {
const node = activeNodeList[0] const node = activeNodeList[0]
// 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式 // 当正在输入中文或英文或数字时,如果没有按下组合键,那么自动进入文本编辑模式
if (node && this.checkIsAutoEnterTextEditKey(e)) { if (node && this.checkIsAutoEnterTextEditKey(e)) {
// 忽略第一个键值,避免中文输入法时进入编辑会导致第一个键值变成字母的问题
// 带来的问题是按的第一下纯粹是进入文本编辑,但没有变成输入
e.preventDefault()
this.show({ this.show({
node, node,
e, e,
@ -161,7 +180,6 @@ export default class TextEdit {
// 注册临时快捷键 // 注册临时快捷键
registerTmpShortcut() { registerTmpShortcut() {
// 注册回车快捷键
this.mindMap.keyCommand.addShortcut('Enter', () => { this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox() this.hideEditTextBox()
}) })
@ -178,6 +196,16 @@ export default class TextEdit {
return this.showTextEdit return this.showTextEdit
} }
// 设置文本编辑框是否处于显示状态
setIsShowTextEdit(val) {
this.showTextEdit = val
if (val) {
this.mindMap.keyCommand.stopCheckInSvg()
} else {
this.mindMap.keyCommand.recoveryCheckInSvg()
}
}
// 显示文本编辑框 // 显示文本编辑框
// isInserting是否是刚创建的节点 // isInserting是否是刚创建的节点
// isFromKeyDown是否是在按键事件进入的编辑 // isFromKeyDown是否是在按键事件进入的编辑
@ -191,6 +219,11 @@ export default class TextEdit {
if (node.isUseCustomNodeContent()) { if (node.isUseCustomNodeContent()) {
return return
} }
// 如果有正在编辑中的节点,那么先结束它
const currentEditNode = this.getCurrentEditNode()
if (currentEditNode) {
this.hideEditTextBox()
}
const { beforeTextEdit, openRealtimeRenderOnNodeTextEdit } = const { beforeTextEdit, openRealtimeRenderOnNodeTextEdit } =
this.mindMap.opt this.mindMap.opt
if (typeof beforeTextEdit === 'function') { if (typeof beforeTextEdit === 'function') {
@ -203,10 +236,13 @@ export default class TextEdit {
} }
if (!isShow) return if (!isShow) return
} }
this.currentNode = node
const { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node) const { offsetLeft, offsetTop } = checkNodeOuter(this.mindMap, node)
this.mindMap.view.translateXY(offsetLeft, offsetTop) this.mindMap.view.translateXY(offsetLeft, offsetTop)
const g = node._textData.node const g = node._textData.node
// 需要先显示不然宽高获取到的可能是0
if (openRealtimeRenderOnNodeTextEdit) {
g.show()
}
const rect = g.node.getBoundingClientRect() const rect = g.node.getBoundingClientRect()
// 如果开启了大小实时更新,那么直接隐藏节点原文本 // 如果开启了大小实时更新,那么直接隐藏节点原文本
if (openRealtimeRenderOnNodeTextEdit) { if (openRealtimeRenderOnNodeTextEdit) {
@ -223,6 +259,7 @@ export default class TextEdit {
this.mindMap.richText.showEditText(params) this.mindMap.richText.showEditText(params)
return return
} }
this.currentNode = node
this.showEditTextBox(params) this.showEditTextBox(params)
} }
@ -251,7 +288,7 @@ export default class TextEdit {
this.mindMap.richText.showTextEdit = false this.mindMap.richText.showTextEdit = false
} else { } else {
this.cacheEditingText = this.getEditText() this.cacheEditingText = this.getEditText()
this.showTextEdit = false this.setIsShowTextEdit(false)
} }
this.show({ this.show({
node, node,
@ -275,9 +312,7 @@ export default class TextEdit {
this.registerTmpShortcut() this.registerTmpShortcut()
if (!this.textEditNode) { if (!this.textEditNode) {
this.textEditNode = document.createElement('div') this.textEditNode = document.createElement('div')
this.textEditNode.classList.add( this.textEditNode.classList.add(SMM_NODE_EDIT_WRAP)
CONSTANTS.EDIT_NODE_CLASS.SMM_NODE_EDIT_WRAP
)
this.textEditNode.style.cssText = ` this.textEditNode.style.cssText = `
position: fixed; position: fixed;
box-sizing: border-box; box-sizing: border-box;
@ -317,13 +352,10 @@ export default class TextEdit {
} else { } else {
handleInputPasteText(e) handleInputPasteText(e)
} }
this.emitTextChangeEvent()
}) })
this.textEditNode.addEventListener('input', () => { this.textEditNode.addEventListener('input', () => {
this.mindMap.emit('node_text_edit_change', { this.emitTextChangeEvent()
node: this.currentNode,
text: this.getEditText(),
richText: false
})
}) })
const targetNode = const targetNode =
this.mindMap.opt.customInnerElsAppendTo || document.body this.mindMap.opt.customInnerElsAppendTo || document.body
@ -350,8 +382,8 @@ export default class TextEdit {
this.textEditNode.style.minWidth = this.textEditNode.style.minWidth =
rect.width + this.textNodePaddingX * 2 + 'px' rect.width + this.textNodePaddingX * 2 + 'px'
this.textEditNode.style.minHeight = rect.height + 'px' this.textEditNode.style.minHeight = rect.height + 'px'
this.textEditNode.style.left = rect.left + 'px' this.textEditNode.style.left = Math.floor(rect.left) + 'px'
this.textEditNode.style.top = rect.top + 'px' this.textEditNode.style.top = Math.floor(rect.top) + 'px'
this.textEditNode.style.display = 'block' this.textEditNode.style.display = 'block'
this.textEditNode.style.maxWidth = textAutoWrapWidth * scale + 'px' this.textEditNode.style.maxWidth = textAutoWrapWidth * scale + 'px'
if (isMultiLine) { if (isMultiLine) {
@ -362,7 +394,7 @@ export default class TextEdit {
} else { } else {
this.textEditNode.style.lineHeight = 'normal' this.textEditNode.style.lineHeight = 'normal'
} }
this.showTextEdit = true this.setIsShowTextEdit(true)
// 选中文本 // 选中文本
// if (!this.cacheEditingText) { // if (!this.cacheEditingText) {
// selectAllInput(this.textEditNode) // selectAllInput(this.textEditNode)
@ -375,6 +407,15 @@ export default class TextEdit {
this.cacheEditingText = '' this.cacheEditingText = ''
} }
// 派发节点文本编辑事件
emitTextChangeEvent() {
this.mindMap.emit('node_text_edit_change', {
node: this.currentNode,
text: this.getEditText(),
richText: false
})
}
// 更新文本编辑框的大小和位置 // 更新文本编辑框的大小和位置
updateTextEditNode() { updateTextEditNode() {
if (this.mindMap.richText) { if (this.mindMap.richText) {
@ -389,8 +430,8 @@ export default class TextEdit {
rect.width + this.textNodePaddingX * 2 + 'px' rect.width + this.textNodePaddingX * 2 + 'px'
this.textEditNode.style.minHeight = this.textEditNode.style.minHeight =
rect.height + this.textNodePaddingY * 2 + 'px' rect.height + this.textNodePaddingY * 2 + 'px'
this.textEditNode.style.left = rect.left + 'px' this.textEditNode.style.left = Math.floor(rect.left) + 'px'
this.textEditNode.style.top = rect.top + 'px' this.textEditNode.style.top = Math.floor(rect.top) + 'px'
} }
// 获取编辑区域的背景填充 // 获取编辑区域的背景填充
@ -447,7 +488,7 @@ export default class TextEdit {
this.textEditNode.style.fontSize = 'inherit' this.textEditNode.style.fontSize = 'inherit'
this.textEditNode.style.fontWeight = 'normal' this.textEditNode.style.fontWeight = 'normal'
this.textEditNode.style.transform = 'translateY(0)' this.textEditNode.style.transform = 'translateY(0)'
this.showTextEdit = false this.setIsShowTextEdit(false)
this.mindMap.execCommand('SET_NODE_TEXT', currentNode, text) this.mindMap.execCommand('SET_NODE_TEXT', currentNode, text)
// if (currentNode.isGeneralization) { // if (currentNode.isGeneralization) {
// // 概要节点 // // 概要节点

View File

@ -8,13 +8,10 @@ import nodeCreateContentsMethods from './nodeCreateContents'
import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect' import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect'
import nodeModifyWidthMethods from './nodeModifyWidth' import nodeModifyWidthMethods from './nodeModifyWidth'
import nodeCooperateMethods from './nodeCooperate' import nodeCooperateMethods from './nodeCooperate'
import quickCreateChildBtnMethods from './quickCreateChildBtn'
import nodeLayoutMethods from './nodeLayout'
import { CONSTANTS } from '../../../constants/constant' import { CONSTANTS } from '../../../constants/constant'
import { import { copyNodeTree, createUid, addXmlns } from '../../../utils/index'
copyNodeTree,
createForeignObjectNode,
createUid,
addXmlns
} from '../../../utils/index'
// 节点类 // 节点类
class MindMapNode { class MindMapNode {
@ -102,20 +99,16 @@ class MindMapNode {
this._generalizationList = [] this._generalizationList = []
this._unVisibleRectRegionNode = null this._unVisibleRectRegionNode = null
this._isMouseenter = false this._isMouseenter = false
this._customContentAddToNodeAdd = null
// 尺寸信息 // 尺寸信息
this._rectInfo = { this._rectInfo = {
imgContentWidth: 0,
imgContentHeight: 0,
textContentWidth: 0, textContentWidth: 0,
textContentHeight: 0 textContentHeight: 0,
textContentWidthWithoutTag: 0
} }
// 概要节点的宽高 // 概要节点的宽高
this._generalizationNodeWidth = 0 this._generalizationNodeWidth = 0
this._generalizationNodeHeight = 0 this._generalizationNodeHeight = 0
// 各种文字信息的间距
this.textContentItemMargin = this.mindMap.opt.textContentMargin
// 图片和文字节点的间距
this.blockContentMargin = this.mindMap.opt.imgTextMargin
// 展开收缩按钮尺寸 // 展开收缩按钮尺寸
this.expandBtnSize = this.mindMap.opt.expandBtnSize this.expandBtnSize = this.mindMap.opt.expandBtnSize
// 是否是多选节点 // 是否是多选节点
@ -126,6 +119,10 @@ class MindMapNode {
this.isHide = false this.isHide = false
const proto = Object.getPrototypeOf(this) const proto = Object.getPrototypeOf(this)
if (!proto.bindEvent) { if (!proto.bindEvent) {
// 节点尺寸计算和布局相关方法
Object.keys(nodeLayoutMethods).forEach(item => {
proto[item] = nodeLayoutMethods[item]
})
// 概要相关方法 // 概要相关方法
Object.keys(nodeGeneralizationMethods).forEach(item => { Object.keys(nodeGeneralizationMethods).forEach(item => {
proto[item] = nodeGeneralizationMethods[item] proto[item] = nodeGeneralizationMethods[item]
@ -156,10 +153,19 @@ class MindMapNode {
Object.keys(nodeModifyWidthMethods).forEach(item => { Object.keys(nodeModifyWidthMethods).forEach(item => {
proto[item] = nodeModifyWidthMethods[item] proto[item] = nodeModifyWidthMethods[item]
}) })
// 快捷创建子节点按钮
if (this.mindMap.opt.isShowCreateChildBtnIcon) {
Object.keys(quickCreateChildBtnMethods).forEach(item => {
proto[item] = quickCreateChildBtnMethods[item]
})
this.initQuickCreateChildBtn()
}
proto.bindEvent = true proto.bindEvent = true
} }
// 初始化 // 初始化
this.getSize() this.getSize()
// 初始需要计算一下概要节点的大小,否则计算布局时获取不到概要的大小
this.updateGeneralization()
this.initDragHandle() this.initDragHandle()
} }
@ -211,7 +217,8 @@ class MindMapNode {
isUseCustomNodeContent, isUseCustomNodeContent,
customCreateNodeContent, customCreateNodeContent,
createNodePrefixContent, createNodePrefixContent,
createNodePostfixContent createNodePostfixContent,
addCustomContentToNode
} = this.mindMap.opt } = this.mindMap.opt
// 需要创建的内容类型 // 需要创建的内容类型
const typeList = [ const typeList = [
@ -227,6 +234,9 @@ class MindMapNode {
'postfix', 'postfix',
...this.mindMap.nodeInnerPrefixList.map(item => { ...this.mindMap.nodeInnerPrefixList.map(item => {
return item.name return item.name
}),
...this.mindMap.nodeInnerPostfixList.map(item => {
return item.name
}) })
] ]
const createTypes = {} const createTypes = {}
@ -284,6 +294,23 @@ class MindMapNode {
addXmlns(this._postfixData.el) addXmlns(this._postfixData.el)
} }
} }
this.mindMap.nodeInnerPostfixList.forEach(item => {
if (createTypes[item.name]) {
this[`_${item.name}Data`] = item.createContent(this)
}
})
if (
addCustomContentToNode &&
typeof addCustomContentToNode.create === 'function'
) {
this._customContentAddToNodeAdd = addCustomContentToNode.create(this)
if (
this._customContentAddToNodeAdd &&
this._customContentAddToNodeAdd.el
) {
addXmlns(this._customContentAddToNodeAdd.el)
}
}
} }
// 计算节点的宽高 // 计算节点的宽高
@ -305,330 +332,6 @@ class MindMapNode {
return changed return changed
} }
// 计算节点尺寸信息
getNodeRect() {
// 自定义节点内容
if (this.isUseCustomNodeContent()) {
const rect = this.measureCustomNodeContentSize(this._customNodeContent)
return {
width: this.hasCustomWidth() ? this.customTextWidth : rect.width,
height: rect.height
}
}
const { tagPosition } = this.mindMap.opt
const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM
// 宽高
let imgContentWidth = 0
let imgContentHeight = 0
let textContentWidth = 0
let textContentHeight = 0
let tagContentWidth = 0
let tagContentHeight = 0
// 存在图片
if (this._imgData) {
this._rectInfo.imgContentWidth = imgContentWidth = this._imgData.width
this._rectInfo.imgContentHeight = imgContentHeight = this._imgData.height
}
// 库前置内容
this.mindMap.nodeInnerPrefixList.forEach(item => {
const itemData = this[`_${item.name}Data`]
if (itemData) {
textContentWidth += itemData.width
textContentHeight = Math.max(textContentHeight, itemData.height)
}
})
// 自定义前置内容
if (this._prefixData) {
textContentWidth += this._prefixData.width
textContentHeight = Math.max(textContentHeight, this._prefixData.height)
}
// 图标
if (this._iconData.length > 0) {
textContentWidth += this._iconData.reduce((sum, cur) => {
textContentHeight = Math.max(textContentHeight, cur.height)
return (sum += cur.width + this.textContentItemMargin)
}, 0)
}
// 文字
if (this._textData) {
textContentWidth += this._textData.width
textContentHeight = Math.max(textContentHeight, this._textData.height)
}
// 超链接
if (this._hyperlinkData) {
textContentWidth += this._hyperlinkData.width
textContentHeight = Math.max(
textContentHeight,
this._hyperlinkData.height
)
}
// 标签
if (this._tagData.length > 0) {
let maxTagHeight = 0
const totalTagWidth = this._tagData.reduce((sum, cur) => {
maxTagHeight = Math.max(maxTagHeight, cur.height)
return (sum += cur.width + this.textContentItemMargin)
}, 0)
if (tagIsBottom) {
// 文字下方
tagContentWidth = totalTagWidth
tagContentHeight = maxTagHeight
} else {
// 否则在右侧
textContentWidth += totalTagWidth
textContentHeight = Math.max(textContentHeight, maxTagHeight)
}
}
// 备注
if (this._noteData) {
textContentWidth += this._noteData.width
textContentHeight = Math.max(textContentHeight, this._noteData.height)
}
// 附件
if (this._attachmentData) {
textContentWidth += this._attachmentData.width
textContentHeight = Math.max(
textContentHeight,
this._attachmentData.height
)
}
// 自定义后置内容
if (this._postfixData) {
textContentWidth += this._postfixData.width
textContentHeight = Math.max(textContentHeight, this._postfixData.height)
}
// 文字内容部分的尺寸
this._rectInfo.textContentWidth = textContentWidth
this._rectInfo.textContentHeight = textContentHeight
// 间距
let margin =
imgContentHeight > 0 && textContentHeight > 0
? this.blockContentMargin
: 0
const { paddingX, paddingY } = this.getPaddingVale()
// 纯内容宽高
let _width = Math.max(imgContentWidth, textContentWidth)
let _height = imgContentHeight + textContentHeight
// 如果标签在文字下方
if (tagIsBottom && tagContentHeight > 0 && textContentHeight > 0) {
// 那么文字和标签之间也需要间距
margin += this.blockContentMargin
// 整体高度要考虑标签宽度
_width = Math.max(_width, tagContentWidth)
// 整体高度要加上标签的高度
_height += tagContentHeight
}
// 计算节点形状需要的附加内边距
const { paddingX: shapePaddingX, paddingY: shapePaddingY } =
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
this.shapePadding.paddingX = shapePaddingX
this.shapePadding.paddingY = shapePaddingY
// 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点
const borderWidth = this.getBorderWidth()
return {
width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth,
height: _height + paddingY * 2 + margin + shapePaddingY * 2 + borderWidth
}
}
// 定位节点内容
layout() {
if (!this.group) return
// 清除之前的内容
this.group.clear()
const { hoverRectPadding, tagPosition, openRealtimeRenderOnNodeTextEdit } =
this.mindMap.opt
let { width, height, textContentItemMargin } = this
let { paddingY } = this.getPaddingVale()
const halfBorderWidth = this.getBorderWidth() / 2
paddingY += this.shapePadding.paddingY + halfBorderWidth
// 节点形状
this.shapeNode = this.shapeInstance.createShape()
this.shapeNode.addClass('smm-node-shape')
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
this.style.shape(this.shapeNode)
this.group.add(this.shapeNode)
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
this.renderExpandBtnPlaceholderRect()
// 创建协同头像节点
if (this.createUserListNode) this.createUserListNode()
// 概要节点添加一个带所属节点id的类名
if (this.isGeneralization && this.generalizationBelongNode) {
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
}
// 激活hover和激活边框
const addHoverNode = () => {
this.hoverNode = new Rect()
.size(width + hoverRectPadding * 2, height + hoverRectPadding * 2)
.x(-hoverRectPadding)
.y(-hoverRectPadding)
this.hoverNode.addClass('smm-hover-node')
this.style.hoverNode(this.hoverNode, width, height)
this.group.add(this.hoverNode)
}
// 如果存在自定义节点内容,那么使用自定义节点内容
if (this.isUseCustomNodeContent()) {
const foreignObject = createForeignObjectNode({
el: this._customNodeContent,
width,
height
})
this.group.add(foreignObject)
addHoverNode()
return
}
const tagIsBottom = tagPosition === CONSTANTS.TAG_POSITION.BOTTOM
const { textContentHeight } = this._rectInfo
// 图片节点
let imgHeight = 0
if (this._imgData) {
imgHeight = this._imgData.height
this.group.add(this._imgData.node)
this._imgData.node.cx(width / 2).y(paddingY)
}
// 内容节点
let textContentNested = new G()
let textContentOffsetX = 0
// 库前置内容
this.mindMap.nodeInnerPrefixList.forEach(item => {
const itemData = this[`_${item.name}Data`]
if (itemData) {
itemData.node
.x(textContentOffsetX)
.y((textContentHeight - itemData.height) / 2)
textContentNested.add(itemData.node)
textContentOffsetX += itemData.width + textContentItemMargin
}
})
// 自定义前置内容
if (this._prefixData) {
const foreignObject = createForeignObjectNode({
el: this._prefixData.el,
width: this._prefixData.width,
height: this._prefixData.height
})
foreignObject
.x(textContentOffsetX)
.y((textContentHeight - this._prefixData.height) / 2)
textContentNested.add(foreignObject)
textContentOffsetX += this._prefixData.width + textContentItemMargin
}
// icon
let iconNested = new G()
if (this._iconData && this._iconData.length > 0) {
let iconLeft = 0
this._iconData.forEach(item => {
item.node
.x(textContentOffsetX + iconLeft)
.y((textContentHeight - item.height) / 2)
iconNested.add(item.node)
iconLeft += item.width + textContentItemMargin
})
textContentNested.add(iconNested)
textContentOffsetX += iconLeft
}
// 文字
if (this._textData) {
const oldX = this._textData.node.attr('data-offsetx') || 0
this._textData.node.attr('data-offsetx', textContentOffsetX)
// 修复safari浏览器节点存在图标时文字位置不正确的问题
;(this._textData.nodeContent || this._textData.node)
.x(-oldX) // 修复非富文本模式下同时存在图标和换行的文本时,被收起和展开时图标与文字距离会逐渐拉大的问题
.x(textContentOffsetX)
.y((textContentHeight - this._textData.height) / 2)
// 如果开启了文本编辑实时渲染需要判断当前渲染的节点是否是正在编辑的节点是的话将透明度设置为0不显示
if (openRealtimeRenderOnNodeTextEdit) {
this._textData.node.opacity(
this.mindMap.renderer.textEdit.getCurrentEditNode() === this ? 0 : 1
)
}
textContentNested.add(this._textData.node)
textContentOffsetX += this._textData.width + textContentItemMargin
}
// 超链接
if (this._hyperlinkData) {
this._hyperlinkData.node
.x(textContentOffsetX)
.y((textContentHeight - this._hyperlinkData.height) / 2)
textContentNested.add(this._hyperlinkData.node)
textContentOffsetX += this._hyperlinkData.width + textContentItemMargin
}
// 标签
let tagNested = new G()
if (this._tagData && this._tagData.length > 0) {
if (tagIsBottom) {
// 标签显示在文字下方
let tagLeft = 0
this._tagData.forEach(item => {
item.node.x(tagLeft).y(0)
tagNested.add(item.node)
tagLeft += item.width + textContentItemMargin
})
tagNested.cx(width / 2).y(
paddingY + // 内边距
imgHeight + // 图片高度
textContentHeight + // 文本区域高度
(imgHeight > 0 && textContentHeight > 0
? this.blockContentMargin
: 0) + // 图片和文本之间的间距
this.blockContentMargin // 标签和文本之间的间距
)
this.group.add(tagNested)
} else {
// 标签显示在文字右侧
let tagLeft = 0
this._tagData.forEach(item => {
item.node
.x(textContentOffsetX + tagLeft)
.y((textContentHeight - item.height) / 2)
tagNested.add(item.node)
tagLeft += item.width + textContentItemMargin
})
textContentNested.add(tagNested)
textContentOffsetX += tagLeft
}
}
// 备注
if (this._noteData) {
this._noteData.node
.x(textContentOffsetX)
.y((textContentHeight - this._noteData.height) / 2)
textContentNested.add(this._noteData.node)
textContentOffsetX += this._noteData.width
}
// 附件
if (this._attachmentData) {
this._attachmentData.node
.x(textContentOffsetX)
.y((textContentHeight - this._attachmentData.height) / 2)
textContentNested.add(this._attachmentData.node)
textContentOffsetX += this._attachmentData.width
}
// 自定义后置内容
if (this._postfixData) {
const foreignObject = createForeignObjectNode({
el: this._postfixData.el,
width: this._postfixData.width,
height: this._postfixData.height
})
foreignObject
.x(textContentOffsetX)
.y((textContentHeight - this._postfixData.height) / 2)
textContentNested.add(foreignObject)
textContentOffsetX += this._postfixData.width
}
this.group.add(textContentNested)
// 文字内容整体
textContentNested.translate(
width / 2 - textContentNested.bbox().width / 2,
paddingY + // 内边距
imgHeight + // 图片高度
(imgHeight > 0 && textContentHeight > 0 ? this.blockContentMargin : 0) // 和图片的间距
)
addHoverNode()
this.mindMap.emit('node_layout_end', this)
}
// 给节点绑定事件 // 给节点绑定事件
bindGroupEvent() { bindGroupEvent() {
// 单击事件,选中节点 // 单击事件,选中节点
@ -780,10 +483,15 @@ class MindMapNode {
return return
} }
this.updateNodeActiveClass() this.updateNodeActiveClass()
const { alwaysShowExpandBtn, notShowExpandBtn } = this.mindMap.opt const {
alwaysShowExpandBtn,
notShowExpandBtn,
isShowCreateChildBtnIcon,
readonly
} = this.mindMap.opt
const childrenLength = this.getChildrenLength()
// 不显示展开收起按钮则不需要处理 // 不显示展开收起按钮则不需要处理
if (!notShowExpandBtn) { if (!notShowExpandBtn) {
const childrenLength = this.nodeData.children.length
if (alwaysShowExpandBtn) { if (alwaysShowExpandBtn) {
// 需要移除展开收缩按钮 // 需要移除展开收缩按钮
if (this._expandBtn && childrenLength <= 0) { if (this._expandBtn && childrenLength <= 0) {
@ -804,6 +512,19 @@ class MindMapNode {
} }
} }
} }
// 更新快速创建子节点按钮
if (isShowCreateChildBtnIcon) {
if (childrenLength > 0) {
this.removeQuickCreateChildBtn()
} else {
const { isActive } = this.getData()
if (isActive) {
this.showQuickCreateChildBtn()
} else {
this.hideQuickCreateChildBtn()
}
}
}
// 更新拖拽手柄的显示与否 // 更新拖拽手柄的显示与否
this.updateDragHandle() this.updateDragHandle()
// 更新概要 // 更新概要
@ -813,7 +534,7 @@ class MindMapNode {
// 更新节点位置 // 更新节点位置
const t = this.group.transform() const t = this.group.transform()
// 保存一份当前节点数据快照 // 保存一份当前节点数据快照
this.nodeDataSnapshot = JSON.stringify(this.getData()) this.nodeDataSnapshot = readonly ? '' : JSON.stringify(this.getData())
// 节点位置变化才更新,因为即使值没有变化属性设置操作也是耗时的 // 节点位置变化才更新,因为即使值没有变化属性设置操作也是耗时的
if (this.left !== t.translateX || this.top !== t.translateY) { if (this.left !== t.translateX || this.top !== t.translateY) {
this.group.translate(this.left - t.translateX, this.top - t.translateY) this.group.translate(this.left - t.translateX, this.top - t.translateY)
@ -861,11 +582,18 @@ class MindMapNode {
// 根据是否激活更新节点 // 根据是否激活更新节点
updateNodeByActive(active) { updateNodeByActive(active) {
if (this.group) { if (this.group) {
const { isShowCreateChildBtnIcon } = this.mindMap.opt
// 切换激活状态,需要切换展开收起按钮的显隐 // 切换激活状态,需要切换展开收起按钮的显隐
if (active) { if (active) {
this.showExpandBtn() this.showExpandBtn()
if (isShowCreateChildBtnIcon) {
this.showQuickCreateChildBtn()
}
} else { } else {
this.hideExpandBtn() this.hideExpandBtn()
if (isShowCreateChildBtnIcon) {
this.hideQuickCreateChildBtn()
}
} }
this.updateNodeActiveClass() this.updateNodeActiveClass()
this.updateDragHandle() this.updateDragHandle()
@ -1088,14 +816,13 @@ class MindMapNode {
if (this.getData('expand') === false) { if (this.getData('expand') === false) {
return return
} }
let childrenLen = this.nodeData.children.length let childrenLen = this.getChildrenLength()
// 切换为鱼骨结构时,清空根节点和二级节点的连线 // 切换为鱼骨结构时,清空根节点和二级节点的连线
if ( if (this.mindMap.renderer.layout.nodeIsRemoveAllLines) {
this.mindMap.opt.layout === CONSTANTS.LAYOUT.FISHBONE && if (this.mindMap.renderer.layout.nodeIsRemoveAllLines(this)) {
(this.isRoot || this.layerIndex === 1)
) {
childrenLen = 0 childrenLen = 0
} }
}
if (childrenLen > this._lines.length) { if (childrenLen > this._lines.length) {
// 创建缺少的线 // 创建缺少的线
new Array(childrenLen - this._lines.length).fill(0).forEach(() => { new Array(childrenLen - this._lines.length).fill(0).forEach(() => {
@ -1170,15 +897,18 @@ class MindMapNode {
// 设置连线样式 // 设置连线样式
styleLine(line, childNode, enableMarker) { styleLine(line, childNode, enableMarker) {
const { enableInheritAncestorLineStyle } = this.mindMap.opt
const getName = enableInheritAncestorLineStyle
? 'getSelfInhertStyle'
: 'getSelfStyle'
const width = const width =
childNode.getSelfInhertStyle('lineWidth') || childNode[getName]('lineWidth') || childNode.getStyle('lineWidth', true)
childNode.getStyle('lineWidth', true)
const color = const color =
childNode.getSelfInhertStyle('lineColor') || childNode[getName]('lineColor') ||
this.getRainbowLineColor(childNode) || this.getRainbowLineColor(childNode) ||
childNode.getStyle('lineColor', true) childNode.getStyle('lineColor', true)
const dasharray = const dasharray =
childNode.getSelfInhertStyle('lineDasharray') || childNode[getName]('lineDasharray') ||
childNode.getStyle('lineDasharray', true) childNode.getStyle('lineDasharray', true)
this.style.line( this.style.line(
line, line,
@ -1405,6 +1135,11 @@ class MindMapNode {
this.customTextWidth !== undefined this.customTextWidth !== undefined
) )
} }
// 获取子节点的数量
getChildrenLength() {
return this.nodeData.children ? this.nodeData.children.length : 0
}
} }
export default MindMapNode export default MindMapNode

View File

@ -52,7 +52,22 @@ export default class Shape {
paddingX: actHeight > actWidth ? actOffset / 2 : 0, paddingX: actHeight > actWidth ? actOffset / 2 : 0,
paddingY: actHeight < actWidth ? actOffset / 2 : 0 paddingY: actHeight < actWidth ? actOffset / 2 : 0
} }
default: }
const extendShape = this.getShapeFromExtendList(shape)
if (extendShape) {
return (
extendShape.getPadding({
node: this.node,
width,
height,
paddingX,
paddingY
}) || {
paddingX: 0,
paddingY: 0
}
)
} else {
return { return {
paddingX: 0, paddingX: 0,
paddingY: 0 paddingY: 0
@ -60,6 +75,13 @@ export default class Shape {
} }
} }
// 从形状扩展列表里获取指定名称的形状
getShapeFromExtendList(shape) {
return this.mindMap.extendShapeList.find(item => {
return item.name === shape
})
}
// 创建形状节点 // 创建形状节点
createShape() { createShape() {
const shape = this.node.getShape() const shape = this.node.getShape()
@ -92,7 +114,13 @@ export default class Shape {
// 圆 // 圆
node = this.createCircle() node = this.createCircle()
} }
return node if (!node) {
const extendShape = this.getShapeFromExtendList(shape)
if (extendShape) {
node = extendShape.createShape(this.node)
}
}
return node || this.createRect()
} }
// 获取节点减去节点边框宽度、hover节点边框宽度后的尺寸 // 获取节点减去节点边框宽度、hover节点边框宽度后的尺寸

View File

@ -8,6 +8,18 @@ const backgroundStyleProps = [
'backgroundSize' 'backgroundSize'
] ]
export const shapeStyleProps = [
'gradientStyle',
'startColor',
'endColor',
'startDir',
'endDir',
'fillColor',
'borderColor',
'borderWidth',
'borderDasharray'
]
// 样式类 // 样式类
class Style { class Style {
// 设置背景样式 // 设置背景样式
@ -112,6 +124,8 @@ class Style {
// 更新当前节点生效的样式数据 // 更新当前节点生效的样式数据
addToEffectiveStyles(styles) { addToEffectiveStyles(styles) {
// effectiveStyles目前只提供给格式刷插件使用所以如果没有注册该插件那么不需要保存该数据
if (!this.ctx.mindMap.painter) return
this.ctx.effectiveStyles = { this.ctx.effectiveStyles = {
...this.ctx.effectiveStyles, ...this.ctx.effectiveStyles,
...styles ...styles
@ -126,17 +140,10 @@ class Style {
// 形状 // 形状
shape(node) { shape(node) {
const styles = { const styles = {}
gradientStyle: this.merge('gradientStyle'), shapeStyleProps.forEach(key => {
startColor: this.merge('startColor'), styles[key] = this.merge(key)
endColor: this.merge('endColor'), })
startDir: this.merge('startDir'),
endDir: this.merge('endDir'),
fillColor: this.merge('fillColor'),
borderColor: this.merge('borderColor'),
borderWidth: this.merge('borderWidth'),
borderDasharray: this.merge('borderDasharray')
}
if (styles.gradientStyle) { if (styles.gradientStyle) {
if (!this._gradient) { if (!this._gradient) {
this._gradient = this.ctx.nodeDraw.gradient('linear') this._gradient = this.ctx.nodeDraw.gradient('linear')
@ -191,45 +198,6 @@ class Style {
}) })
} }
// 生成内联样式
createStyleText(customStyle = {}) {
const styles = {
color: this.merge('color'),
fontFamily: this.merge('fontFamily'),
fontSize: this.merge('fontSize'),
fontWeight: this.merge('fontWeight'),
fontStyle: this.merge('fontStyle'),
textDecoration: this.merge('textDecoration'),
...customStyle
}
return `
color: ${styles.color};
font-family: ${styles.fontFamily};
font-size: ${styles.fontSize + 'px'};
font-weight: ${styles.fontWeight};
font-style: ${styles.fontStyle};
text-decoration: ${styles.textDecoration}
`
}
// 获取文本样式
getTextFontStyle() {
const styles = {
color: this.merge('color'),
fontFamily: this.merge('fontFamily'),
fontSize: this.merge('fontSize'),
fontWeight: this.merge('fontWeight'),
fontStyle: this.merge('fontStyle'),
textDecoration: this.merge('textDecoration')
}
return {
italic: styles.fontStyle === 'italic',
bold: styles.fontWeight,
fontSize: styles.fontSize,
fontFamily: styles.fontFamily
}
}
// html文字节点 // html文字节点
domText(node, fontSizeScale = 1) { domText(node, fontSizeScale = 1) {
const styles = { const styles = {
@ -238,7 +206,8 @@ class Style {
fontSize: this.merge('fontSize'), fontSize: this.merge('fontSize'),
fontWeight: this.merge('fontWeight'), fontWeight: this.merge('fontWeight'),
fontStyle: this.merge('fontStyle'), fontStyle: this.merge('fontStyle'),
textDecoration: this.merge('textDecoration') textDecoration: this.merge('textDecoration'),
textAlign: this.merge('textAlign')
} }
node.style.color = styles.color node.style.color = styles.color
node.style.textDecoration = styles.textDecoration node.style.textDecoration = styles.textDecoration
@ -246,6 +215,7 @@ class Style {
node.style.fontSize = styles.fontSize * fontSizeScale + 'px' node.style.fontSize = styles.fontSize * fontSizeScale + 'px'
node.style.fontWeight = styles.fontWeight || 'normal' node.style.fontWeight = styles.fontWeight || 'normal'
node.style.fontStyle = styles.fontStyle node.style.fontStyle = styles.fontStyle
node.style.textAlign = styles.textAlign
} }
// 标签文字 // 标签文字
@ -270,9 +240,9 @@ class Style {
} }
// 内置图标 // 内置图标
iconNode(node) { iconNode(node, color) {
node.attr({ node.attr({
fill: this.merge('color') fill: color || this.merge('color')
}) })
} }

View File

@ -1,19 +1,17 @@
import { import {
resizeImgSize, resizeImgSize,
removeHtmlStyle, removeRichTextStyes,
addHtmlStyle,
checkIsRichText, checkIsRichText,
isUndef, isUndef,
createForeignObjectNode, createForeignObjectNode,
addXmlns, addXmlns,
generateColorByContent generateColorByContent,
camelCaseToHyphen,
getNodeRichTextStyles
} from '../../../utils' } from '../../../utils'
import { Image as SVGImage, SVG, A, G, Rect, Text } from '@svgdotjs/svg.js' import { Image as SVGImage, SVG, A, G, Rect, Text } from '@svgdotjs/svg.js'
import iconsSvg from '../../../svg/icons' import iconsSvg from '../../../svg/icons'
import { import { noneRichTextNodeLineHeight } from '../../../constants/constant'
CONSTANTS,
noneRichTextNodeLineHeight
} from '../../../constants/constant'
// 测量svg文本宽高 // 测量svg文本宽高
const measureText = (text, style) => { const measureText = (text, style) => {
@ -34,12 +32,20 @@ const defaultTagStyle = {
//width: 30 // 标签矩形的宽度,如果不设置,默认以文字的宽度+paddingX*2为宽度 //width: 30 // 标签矩形的宽度,如果不设置,默认以文字的宽度+paddingX*2为宽度
} }
// 获取图片的真实url
// 因为如果注册了NodeBase64ImageStorage插件那么节点图片字段保存的实际是一个id所以如果要获取图片真实的url可以通过该方法
function getImageUrl() {
const img = this.getData('image')
return (this.mindMap.renderer.renderTree.data.imgMap || {})[img] || img
}
// 创建图片节点 // 创建图片节点
function createImgNode() { function createImgNode() {
const img = this.getData('image') const img = this.getImageUrl()
if (!img) { if (!img) {
return return
} }
img = (this.mindMap.renderer.renderTree.data.imgMap || {})[img] || img
const imgSize = this.getImgShowSize() const imgSize = this.getImgShowSize()
const node = new SVGImage().load(img).size(...imgSize) const node = new SVGImage().load(img).size(...imgSize)
// 如果指定了加载失败显示的图片,那么加载一下图片检测是否失败 // 如果指定了加载失败显示的图片,那么加载一下图片检测是否失败
@ -54,8 +60,11 @@ function createImgNode() {
if (this.getData('imageTitle')) { if (this.getData('imageTitle')) {
node.attr('title', this.getData('imageTitle')) node.attr('title', this.getData('imageTitle'))
} }
node.on('click', e => {
this.mindMap.emit('node_img_click', this, node, e)
})
node.on('dblclick', e => { node.on('dblclick', e => {
this.mindMap.emit('node_img_dblclick', this, e) this.mindMap.emit('node_img_dblclick', this, e, node)
}) })
node.on('mouseenter', e => { node.on('mouseenter', e => {
this.mindMap.emit('node_img_mouseenter', this, node, e) this.mindMap.emit('node_img_mouseenter', this, node, e)
@ -124,20 +133,6 @@ function createIconNode() {
}) })
} }
// 尝试给html指定标签添加内联样式
function tryAddHtmlStyle(text, style) {
const tagList = ['span', 'strong', 's', 'em', 'u']
// let _text = text
// for (let i = 0; i < tagList.length; i++) {
// text = addHtmlStyle(text, tagList[i], style)
// if (text !== _text) {
// break
// }
// }
// return text
return addHtmlStyle(text, tagList, style)
}
// 创建富文本节点 // 创建富文本节点
function createRichTextNode(specifyText) { function createRichTextNode(specifyText) {
const hasCustomWidth = this.hasCustomWidth() const hasCustomWidth = this.hasCustomWidth()
@ -145,40 +140,32 @@ function createRichTextNode(specifyText) {
typeof specifyText === 'string' ? specifyText : this.getData('text') typeof specifyText === 'string' ? specifyText : this.getData('text')
let { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt let { textAutoWrapWidth, emptyTextMeasureHeightText } = this.mindMap.opt
textAutoWrapWidth = hasCustomWidth ? this.customTextWidth : textAutoWrapWidth textAutoWrapWidth = hasCustomWidth ? this.customTextWidth : textAutoWrapWidth
let g = new G() const g = new G()
// 重新设置富文本节点内容 // 创建富文本结构,或复位富文本样式
let recoverText = false let recoverText = false
if (this.getData('resetRichText')) { if (this.getData('resetRichText')) {
delete this.nodeData.data.resetRichText delete this.nodeData.data.resetRichText
recoverText = true recoverText = true
} }
if ([CONSTANTS.CHANGE_THEME].includes(this.mindMap.renderer.renderSource)) {
// 如果自定义过样式则不允许覆盖
// if (!this.hasCustomStyle() ) {
recoverText = true
// }
}
if (recoverText && !isUndef(text)) { if (recoverText && !isUndef(text)) {
// 判断节点内容是否是富文本 if (checkIsRichText(text)) {
const isRichText = checkIsRichText(text) // 如果是富文本那么移除内联样式
// 获取自定义样式 text = removeRichTextStyes(text)
const customStyle = this.style.getCustomStyle()
// 样式字符串
const style = this.style.createStyleText(customStyle)
if (isRichText) {
// 如果是富文本那么线移除内联样式
text = removeHtmlStyle(text)
// 再添加新的内联样式
text = this.tryAddHtmlStyle(text, style)
} else { } else {
// 非富文本 // 非富文本则改为富文本结构
text = `<p><span style="${style}">${text}</span></p>` text = `<p>${text}</p>`
} }
this.setData({ this.setData({
text: text text
}) })
} }
let html = `<div>${text}</div>` // 节点的富文本样式数据
const nodeTextStyleList = []
const nodeRichTextStyles = getNodeRichTextStyles(this)
Object.keys(nodeRichTextStyles).forEach(prop => {
nodeTextStyleList.push([prop, nodeRichTextStyles[prop]])
})
// 测量文本大小
if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) { if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) {
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl =
document.createElement('div') document.createElement('div')
@ -190,9 +177,15 @@ function createRichTextNode(specifyText) {
this.mindMap.commonCaches.measureRichtextNodeTextSizeEl this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
) )
} }
let div = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl const div = this.mindMap.commonCaches.measureRichtextNodeTextSizeEl
// 应用节点的文本样式
nodeTextStyleList.forEach(([prop, value]) => {
div.style[prop] = value
})
div.style.lineHeight = 1.2
const html = `<div>${text}</div>`
div.innerHTML = html div.innerHTML = html
let el = div.children[0] const el = div.children[0]
el.classList.add('smm-richtext-node-wrap') el.classList.add('smm-richtext-node-wrap')
addXmlns(el) addXmlns(el)
el.style.maxWidth = textAutoWrapWidth + 'px' el.style.maxWidth = textAutoWrapWidth + 'px'
@ -219,6 +212,15 @@ function createRichTextNode(specifyText) {
width, width,
height height
}) })
// 应用节点文本样式
// 进入文本编辑时,这个样式也会同样添加到文本编辑框的元素上
const foreignObjectStyle = {
'line-height': 1.2
}
nodeTextStyleList.forEach(([prop, value]) => {
foreignObjectStyle[camelCaseToHyphen(prop)] = value
})
foreignObject.css(foreignObjectStyle)
g.add(foreignObject) g.add(foreignObject)
return { return {
node: g, node: g,
@ -230,6 +232,10 @@ function createRichTextNode(specifyText) {
// 创建文本节点 // 创建文本节点
function createTextNode(specifyText) { function createTextNode(specifyText) {
if (this.getData('needUpdate')) {
delete this.nodeData.data.needUpdate
}
// 如果是富文本内容,那么转给富文本函数
if (this.getData('richText')) { if (this.getData('richText')) {
return this.createRichTextNode(specifyText) return this.createRichTextNode(specifyText)
} }
@ -238,8 +244,9 @@ function createTextNode(specifyText) {
if (this.getData('resetRichText')) { if (this.getData('resetRichText')) {
delete this.nodeData.data.resetRichText delete this.nodeData.data.resetRichText
} }
let g = new G() const g = new G()
let fontSize = this.getStyle('fontSize', false) const fontSize = this.getStyle('fontSize', false)
const textAlign = this.getStyle('textAlign', false)
// 文本超长自动换行 // 文本超长自动换行
let textArr = [] let textArr = []
if (!isUndef(text)) { if (!isUndef(text)) {
@ -279,6 +286,14 @@ function createTextNode(specifyText) {
} }
const node = new Text().text(item) const node = new Text().text(item)
node.addClass('smm-text-node-wrap') node.addClass('smm-text-node-wrap')
node.attr(
'text-anchor',
{
left: 'start',
center: 'middle',
right: 'end'
}[textAlign] || 'start'
)
this.style.text(node) this.style.text(node)
node.y( node.y(
fontSize * noneRichTextNodeLineHeight * index + fontSize * noneRichTextNodeLineHeight * index +
@ -308,15 +323,16 @@ function createTextNode(specifyText) {
// 创建超链接节点 // 创建超链接节点
function createHyperlinkNode() { function createHyperlinkNode() {
let { hyperlink, hyperlinkTitle } = this.getData() const { hyperlink, hyperlinkTitle } = this.getData()
if (!hyperlink) { if (!hyperlink) {
return return
} }
const { customHyperlinkJump } = this.mindMap.opt const { customHyperlinkJump, hyperlinkIcon } = this.mindMap.opt
let iconSize = this.mindMap.themeConfig.iconSize const { icon, style } = hyperlinkIcon
let node = new SVG().size(iconSize, iconSize) const iconSize = this.getNodeIconSize('hyperlinkIcon')
const node = new SVG().size(iconSize, iconSize)
// 超链接节点 // 超链接节点
let a = new A().to(hyperlink).target('_blank') const a = new A().to(hyperlink).target('_blank')
a.node.addEventListener('click', e => { a.node.addEventListener('click', e => {
if (typeof customHyperlinkJump === 'function') { if (typeof customHyperlinkJump === 'function') {
e.preventDefault() e.preventDefault()
@ -329,8 +345,8 @@ function createHyperlinkNode() {
// 添加一个透明的层,作为鼠标区域 // 添加一个透明的层,作为鼠标区域
a.rect(iconSize, iconSize).fill({ color: 'transparent' }) a.rect(iconSize, iconSize).fill({ color: 'transparent' })
// 超链接图标 // 超链接图标
let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize) const iconNode = SVG(icon || iconsSvg.hyperlink).size(iconSize, iconSize)
this.style.iconNode(iconNode) this.style.iconNode(iconNode, style.color)
a.add(iconNode) a.add(iconNode)
node.add(a) node.add(a)
return { return {
@ -415,16 +431,17 @@ function createNoteNode() {
if (!this.getData('note')) { if (!this.getData('note')) {
return null return null
} }
let iconSize = this.mindMap.themeConfig.iconSize const { icon, style } = this.mindMap.opt.noteIcon
let node = new SVG() const iconSize = this.getNodeIconSize('noteIcon')
const node = new SVG()
.attr('cursor', 'pointer') .attr('cursor', 'pointer')
.addClass('smm-node-note') .addClass('smm-node-note')
.size(iconSize, iconSize) .size(iconSize, iconSize)
// 透明的层,用来作为鼠标区域 // 透明的层,用来作为鼠标区域
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' })) node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
// 备注图标 // 备注图标
let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize) const iconNode = SVG(icon || iconsSvg.note).size(iconSize, iconSize)
this.style.iconNode(iconNode) this.style.iconNode(iconNode, style.color)
node.add(iconNode) node.add(iconNode)
// 备注tooltip // 备注tooltip
if (!this.mindMap.opt.customNoteContentShow) { if (!this.mindMap.opt.customNoteContentShow) {
@ -486,7 +503,8 @@ function createAttachmentNode() {
if (!attachmentUrl) { if (!attachmentUrl) {
return return
} }
const iconSize = this.mindMap.themeConfig.iconSize const iconSize = this.getNodeIconSize('attachmentIcon')
const { icon, style } = this.mindMap.opt.attachmentIcon
const node = new SVG().attr('cursor', 'pointer').size(iconSize, iconSize) const node = new SVG().attr('cursor', 'pointer').size(iconSize, iconSize)
if (attachmentName) { if (attachmentName) {
node.add(SVG(`<title>${attachmentName}</title>`)) node.add(SVG(`<title>${attachmentName}</title>`))
@ -494,8 +512,8 @@ function createAttachmentNode() {
// 透明的层,用来作为鼠标区域 // 透明的层,用来作为鼠标区域
node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' })) node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' }))
// 备注图标 // 备注图标
const iconNode = SVG(iconsSvg.attachment).size(iconSize, iconSize) const iconNode = SVG(icon || iconsSvg.attachment).size(iconSize, iconSize)
this.style.iconNode(iconNode) this.style.iconNode(iconNode, style.color)
node.add(iconNode) node.add(iconNode)
node.on('click', e => { node.on('click', e => {
this.mindMap.emit('node_attachmentClick', this, e, node) this.mindMap.emit('node_attachmentClick', this, e, node)
@ -510,9 +528,15 @@ function createAttachmentNode() {
} }
} }
// 获取节点图标大小
function getNodeIconSize(prop) {
const { style } = this.mindMap.opt[prop]
return isUndef(style.size) ? this.mindMap.themeConfig.iconSize : style.size
}
// 获取节点备注显示位置 // 获取节点备注显示位置
function getNoteContentPosition() { function getNoteContentPosition() {
const iconSize = this.mindMap.themeConfig.iconSize const iconSize = this.getNodeIconSize('noteIcon')
const { scaleY } = this.mindMap.view.getTransformData().transform const { scaleY } = this.mindMap.view.getTransformData().transform
const iconSizeAddScale = iconSize * scaleY const iconSizeAddScale = iconSize * scaleY
let { left, top } = this._noteData.node.node.getBoundingClientRect() let { left, top } = this._noteData.node.node.getBoundingClientRect()
@ -553,10 +577,10 @@ function isUseCustomNodeContent() {
} }
export default { export default {
getImageUrl,
createImgNode, createImgNode,
getImgShowSize, getImgShowSize,
createIconNode, createIconNode,
tryAddHtmlStyle,
createRichTextNode, createRichTextNode,
createTextNode, createTextNode,
createHyperlinkNode, createHyperlinkNode,
@ -564,6 +588,7 @@ export default {
createNoteNode, createNoteNode,
createAttachmentNode, createAttachmentNode,
getNoteContentPosition, getNoteContentPosition,
getNodeIconSize,
measureCustomNodeContentSize, measureCustomNodeContentSize,
isUseCustomNodeContent isUseCustomNodeContent
} }

View File

@ -7,34 +7,36 @@ function createExpandNodeContent() {
if (this._openExpandNode) { if (this._openExpandNode) {
return return
} }
let { close, open } = this.mindMap.opt.expandBtnIcon || {} const { expandBtnSize, expandBtnIcon, isShowExpandNum } = this.mindMap.opt
let { close, open } = expandBtnIcon || {}
// 根据配置判断是否显示数量按钮 // 根据配置判断是否显示数量按钮
if (this.mindMap.opt.isShowExpandNum) { if (isShowExpandNum) {
// 展开的节点 // 展开的节点
this._openExpandNode = new Text() this._openExpandNode = new Text()
this._openExpandNode.addClass('smm-expand-btn-text')
// 文本垂直居中 // 文本垂直居中
this._openExpandNode.attr({ this._openExpandNode.attr({
'text-anchor': 'middle', 'text-anchor': 'middle',
'dominant-baseline': 'middle', 'dominant-baseline': 'middle',
x: this.expandBtnSize / 2, x: expandBtnSize / 2,
y: 2 y: 2
}) })
} else { } else {
this._openExpandNode = SVG(open || btnsSvg.open).size( this._openExpandNode = SVG(open || btnsSvg.open).size(
this.expandBtnSize, expandBtnSize,
this.expandBtnSize expandBtnSize
) )
this._openExpandNode.x(0).y(-this.expandBtnSize / 2) this._openExpandNode.x(0).y(-expandBtnSize / 2)
} }
// 收起的节点 // 收起的节点
this._closeExpandNode = SVG(close || btnsSvg.close).size( this._closeExpandNode = SVG(close || btnsSvg.close).size(
this.expandBtnSize, expandBtnSize,
this.expandBtnSize expandBtnSize
) )
this._closeExpandNode.x(0).y(-this.expandBtnSize / 2) this._closeExpandNode.x(0).y(-expandBtnSize / 2)
// 填充节点 // 填充节点
this._fillExpandNode = new Circle().size(this.expandBtnSize) this._fillExpandNode = new Circle().size(expandBtnSize)
this._fillExpandNode.x(0).y(-this.expandBtnSize / 2) this._fillExpandNode.x(0).y(-expandBtnSize / 2)
// 设置样式 // 设置样式
this.style.iconBtn( this.style.iconBtn(
@ -78,7 +80,7 @@ function updateExpandBtnNode() {
color: expandBtnStyle.strokeColor color: expandBtnStyle.strokeColor
}) })
// 计算子节点数量 // 计算子节点数量
let count = this.sumNode(this.nodeData.children) let count = this.sumNode(this.nodeData.children || [])
if (typeof expandBtnNumHandler === 'function') { if (typeof expandBtnNumHandler === 'function') {
const res = expandBtnNumHandler(count, this) const res = expandBtnNumHandler(count, this)
if (!isUndef(res)) { if (!isUndef(res)) {
@ -104,11 +106,7 @@ function updateExpandBtnPos() {
// 创建展开收缩按钮 // 创建展开收缩按钮
function renderExpandBtn() { function renderExpandBtn() {
if ( if (this.getChildrenLength() <= 0 || this.isRoot) {
!this.nodeData.children ||
this.nodeData.children.length <= 0 ||
this.isRoot
) {
return return
} }
if (this._expandBtn) { if (this._expandBtn) {

View File

@ -3,15 +3,12 @@ import { Rect } from '@svgdotjs/svg.js'
// 渲染展开收起按钮的隐藏占位元素 // 渲染展开收起按钮的隐藏占位元素
function renderExpandBtnPlaceholderRect() { function renderExpandBtnPlaceholderRect() {
// 根节点或没有子节点不需要渲染 // 根节点或没有子节点不需要渲染
if ( if (this.getChildrenLength() <= 0 || this.isRoot) {
!this.nodeData.children ||
this.nodeData.children.length <= 0 ||
this.isRoot
) {
return return
} }
// 默认显示展开按钮的情况下或不显示展开收起按钮的情况下不需要渲染 // 默认显示展开按钮的情况下或不显示展开收起按钮的情况下不需要渲染
const { alwaysShowExpandBtn, notShowExpandBtn } = this.mindMap.opt const { alwaysShowExpandBtn, notShowExpandBtn, expandBtnSize } =
this.mindMap.opt
if (!alwaysShowExpandBtn && !notShowExpandBtn) { if (!alwaysShowExpandBtn && !notShowExpandBtn) {
let { width, height } = this let { width, height } = this
if (!this._unVisibleRectRegionNode) { if (!this._unVisibleRectRegionNode) {
@ -23,7 +20,7 @@ function renderExpandBtnPlaceholderRect() {
this.group.add(this._unVisibleRectRegionNode) this.group.add(this._unVisibleRectRegionNode)
this.renderer.layout.renderExpandBtnRect( this.renderer.layout.renderExpandBtnRect(
this._unVisibleRectRegionNode, this._unVisibleRectRegionNode,
this.expandBtnSize, expandBtnSize,
width, width,
height, height,
this this
@ -48,7 +45,7 @@ function updateExpandBtnPlaceholderRect() {
this.renderExpandBtnPlaceholderRect() this.renderExpandBtnPlaceholderRect()
} }
// 没有子节点到有子节点需要渲染 // 没有子节点到有子节点需要渲染
if (this.nodeData.children && this.nodeData.children.length > 0) { if (this.getChildrenLength() > 0) {
if (!this._unVisibleRectRegionNode) { if (!this._unVisibleRectRegionNode) {
this.renderExpandBtnPlaceholderRect() this.renderExpandBtnPlaceholderRect()
} }

View File

@ -106,7 +106,7 @@ function renderGeneralization(forceRender) {
// 更新节点概要数据 // 更新节点概要数据
function updateGeneralizationData() { function updateGeneralizationData() {
const childrenLength = this.nodeData.children.length const childrenLength = this.getChildrenLength()
const list = this.formatGetGeneralization() const list = this.formatGetGeneralization()
const newList = [] const newList = []
list.forEach(item => { list.forEach(item => {

View File

@ -0,0 +1,516 @@
import { CONSTANTS } from '../../../constants/constant'
import { G, Rect } from '@svgdotjs/svg.js'
import { createForeignObjectNode } from '../../../utils/index'
// 根据图片放置位置返回图片和文本的间距值
function getImgTextMarin(dir, imgWidth, textWidth, imgHeight, textHeight) {
// 图片和文字节点的间距
const { imgTextMargin } = this.mindMap.opt
if (dir === 'v') {
// 垂直
return imgHeight > 0 && textHeight > 0 ? imgTextMargin : 0
} else {
// 水平
return imgWidth > 0 && textWidth > 0 ? imgTextMargin : 0
}
}
// 获取标签内容的大小
function getTagContentSize(space) {
let maxTagHeight = 0
let width = this._tagData.reduce((sum, cur) => {
maxTagHeight = Math.max(maxTagHeight, cur.height)
return (sum += cur.width)
}, 0)
width += (this._tagData.length - 1) * space
return {
width,
height: maxTagHeight
}
}
// 计算节点尺寸信息
function getNodeRect() {
// 自定义节点内容
if (this.isUseCustomNodeContent()) {
const rect = this.measureCustomNodeContentSize(
this._customNodeContent.cloneNode(true)
)
return {
width: this.hasCustomWidth() ? this.customTextWidth : rect.width,
height: rect.height
}
}
const { TAG_PLACEMENT, IMG_PLACEMENT } = CONSTANTS
const { textContentMargin } = this.mindMap.opt
const tagPlacement = this.getStyle('tagPlacement') || TAG_PLACEMENT.RIGHT
const tagIsBottom = tagPlacement === TAG_PLACEMENT.BOTTOM
const imgPlacement = this.getStyle('imgPlacement') || IMG_PLACEMENT.TOP
// 宽高
let imgContentWidth = 0
let imgContentHeight = 0
let textContentWidth = 0
let textContentHeight = 0
let tagContentWidth = 0
let tagContentHeight = 0
let spaceCount = 0
// 存在图片
if (this._imgData) {
imgContentWidth = this._imgData.width
imgContentHeight = this._imgData.height
}
// 库前置内容
this.mindMap.nodeInnerPrefixList.forEach(item => {
const itemData = this[`_${item.name}Data`]
if (itemData) {
textContentWidth += itemData.width
textContentHeight = Math.max(textContentHeight, itemData.height)
spaceCount++
}
})
// 自定义前置内容
if (this._prefixData) {
textContentWidth += this._prefixData.width
textContentHeight = Math.max(textContentHeight, this._prefixData.height)
spaceCount++
}
// 图标
if (this._iconData.length > 0) {
textContentWidth +=
this._iconData.reduce((sum, cur) => {
textContentHeight = Math.max(textContentHeight, cur.height)
return (sum += cur.width)
}, 0) +
(this._iconData.length - 1) * textContentMargin
spaceCount++
}
// 文字
if (this._textData) {
textContentWidth += this._textData.width
textContentHeight = Math.max(textContentHeight, this._textData.height)
spaceCount++
}
// 超链接
if (this._hyperlinkData) {
textContentWidth += this._hyperlinkData.width
textContentHeight = Math.max(textContentHeight, this._hyperlinkData.height)
spaceCount++
}
// 标签
if (this._tagData.length > 0) {
const { width: totalTagWidth, height: maxTagHeight } =
this.getTagContentSize(textContentMargin)
if (tagIsBottom) {
// 文字下方
tagContentWidth = totalTagWidth
tagContentHeight = maxTagHeight
} else {
// 否则在右侧
textContentWidth += totalTagWidth
textContentHeight = Math.max(textContentHeight, maxTagHeight)
spaceCount++
}
}
// 备注
if (this._noteData) {
textContentWidth += this._noteData.width
textContentHeight = Math.max(textContentHeight, this._noteData.height)
spaceCount++
}
// 附件
if (this._attachmentData) {
textContentWidth += this._attachmentData.width
textContentHeight = Math.max(textContentHeight, this._attachmentData.height)
spaceCount++
}
// 自定义后置内容
if (this._postfixData) {
textContentWidth += this._postfixData.width
textContentHeight = Math.max(textContentHeight, this._postfixData.height)
spaceCount++
}
// 库后置内容
this.mindMap.nodeInnerPostfixList.forEach(item => {
const itemData = this[`_${item.name}Data`]
if (itemData) {
textContentWidth += itemData.width
textContentHeight = Math.max(textContentHeight, itemData.height)
spaceCount++
}
})
textContentWidth += (spaceCount - 1) * textContentMargin
// 文字内容部分的尺寸
if (tagIsBottom && textContentWidth > 0 && tagContentHeight > 0) {
this._rectInfo.textContentWidthWithoutTag = textContentWidth
textContentWidth = Math.max(textContentWidth, tagContentWidth)
textContentHeight = textContentHeight + textContentMargin + tagContentHeight
}
this._rectInfo.textContentWidth = textContentWidth
this._rectInfo.textContentHeight = textContentHeight
// 纯内容宽高
let _width = 0
let _height = 0
if ([IMG_PLACEMENT.TOP, IMG_PLACEMENT.BOTTOM].includes(imgPlacement)) {
// 图片在上下
_width = Math.max(imgContentWidth, textContentWidth)
_height =
imgContentHeight +
textContentHeight +
this.getImgTextMarin('v', 0, 0, imgContentHeight, textContentHeight)
} else {
// 图片在左右
_width =
imgContentWidth +
textContentWidth +
this.getImgTextMarin('h', imgContentWidth, textContentWidth)
_height = Math.max(imgContentHeight, textContentHeight)
}
const { paddingX, paddingY } = this.getPaddingVale()
// 计算节点形状需要的附加内边距
const { paddingX: shapePaddingX, paddingY: shapePaddingY } =
this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
this.shapePadding.paddingX = shapePaddingX
this.shapePadding.paddingY = shapePaddingY
// 边框宽度,因为边框是以中线向两端发散,所以边框会超出节点
const borderWidth = this.getBorderWidth()
return {
width: _width + paddingX * 2 + shapePaddingX * 2 + borderWidth,
height: _height + paddingY * 2 + shapePaddingY * 2 + borderWidth
}
}
// 激活hover和激活边框
function addHoverNode(width, height) {
const { hoverRectPadding } = this.mindMap.opt
this.hoverNode = new Rect()
.size(width + hoverRectPadding * 2, height + hoverRectPadding * 2)
.x(-hoverRectPadding)
.y(-hoverRectPadding)
this.hoverNode.addClass('smm-hover-node')
this.style.hoverNode(this.hoverNode, width, height)
this.group.add(this.hoverNode)
}
// 当使用了完全自定义节点内容后,可以通过该方法实时更新节点大小
function customNodeContentRealtimeLayout() {
if (!this.group) return
if (!this.isUseCustomNodeContent()) return
// 删除除foreignObject外的其他元素
if (this.shapeNode) this.shapeNode.remove()
if (this._unVisibleRectRegionNode) this._unVisibleRectRegionNode.remove()
if (this.hoverNode) this.hoverNode.remove()
const { width, height } = this
const halfBorderWidth = this.getBorderWidth() / 2
// 节点形状
this.shapeNode = this.shapeInstance.createShape()
this.shapeNode.addClass('smm-node-shape')
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
this.style.shape(this.shapeNode)
this.group.add(this.shapeNode)
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
this.renderExpandBtnPlaceholderRect()
// 概要节点添加一个带所属节点id的类名
if (this.isGeneralization && this.generalizationBelongNode) {
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
}
// 激活hover和激活边框
this.addHoverNode(width, height)
// 将形状元素移至底层避免遮挡foreignObject
this.shapeNode.back()
// 更新foreignObject元素大小
this.group.findOne('foreignObject').size(width, height)
}
// 定位节点内容
function layout() {
if (!this.group) return
// 清除之前的内容
this.group.clear()
const {
openRealtimeRenderOnNodeTextEdit,
textContentMargin,
addCustomContentToNode
} = this.mindMap.opt
// 避免编辑过程中展开收起按钮闪烁的问题
// 暂时去掉,带来的问题太多
// if (
// openRealtimeRenderOnNodeTextEdit &&
// this._expandBtn &&
// this.getChildrenLength() > 0
// ) {
// this.group.add(this._expandBtn)
// }
const { width, height } = this
let { paddingX, paddingY } = this.getPaddingVale()
const halfBorderWidth = this.getBorderWidth() / 2
paddingX += this.shapePadding.paddingX + halfBorderWidth
paddingY += this.shapePadding.paddingY + halfBorderWidth
// 节点形状
this.shapeNode = this.shapeInstance.createShape()
this.shapeNode.addClass('smm-node-shape')
this.shapeNode.translate(halfBorderWidth, halfBorderWidth)
this.style.shape(this.shapeNode)
this.group.add(this.shapeNode)
// 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示
this.renderExpandBtnPlaceholderRect()
// 创建协同头像节点
if (this.createUserListNode) this.createUserListNode()
// 概要节点添加一个带所属节点id的类名
if (this.isGeneralization && this.generalizationBelongNode) {
this.group.addClass('generalization_' + this.generalizationBelongNode.uid)
}
// 如果存在自定义节点内容,那么使用自定义节点内容
if (this.isUseCustomNodeContent()) {
const foreignObject = createForeignObjectNode({
el: this._customNodeContent,
width,
height
})
this.group.add(foreignObject)
this.addHoverNode(width, height)
return
}
const { IMG_PLACEMENT, TAG_PLACEMENT } = CONSTANTS
const imgPlacement = this.getStyle('imgPlacement') || IMG_PLACEMENT.TOP
const tagPlacement = this.getStyle('tagPlacement') || TAG_PLACEMENT.RIGHT
const tagIsBottom = tagPlacement === TAG_PLACEMENT.BOTTOM
let { textContentWidth, textContentHeight, textContentWidthWithoutTag } =
this._rectInfo
const textContentHeightWithTag = textContentHeight
// 如果存在显示在文本下方的标签,那么非标签内容的整体高度需要减去标签高度
let totalTagWidth = 0
let maxTagHeight = 0
const hasTagContent = this._tagData && this._tagData.length > 0
if (hasTagContent) {
const res = this.getTagContentSize(textContentMargin)
totalTagWidth = res.width
maxTagHeight = res.height
if (tagIsBottom) {
textContentHeight -= maxTagHeight + textContentMargin
}
}
// 图片节点
let imgWidth = 0
let imgHeight = 0
if (this._imgData) {
imgWidth = this._imgData.width
imgHeight = this._imgData.height
this.group.add(this._imgData.node)
switch (imgPlacement) {
case IMG_PLACEMENT.TOP:
this._imgData.node.cx(width / 2).y(paddingY)
break
case IMG_PLACEMENT.BOTTOM:
this._imgData.node.cx(width / 2).y(height - paddingY - imgHeight)
break
case IMG_PLACEMENT.LEFT:
this._imgData.node.x(paddingX).cy(height / 2)
break
case IMG_PLACEMENT.RIGHT:
this._imgData.node.x(width - paddingX - imgWidth).cy(height / 2)
break
default:
break
}
}
// 内容节点
let textContentNested = new G()
let textContentOffsetX = 0
if (hasTagContent && tagIsBottom) {
textContentOffsetX =
textContentWidthWithoutTag < textContentWidth
? (textContentWidth - textContentWidthWithoutTag) / 2
: 0
}
// 库前置内容
this.mindMap.nodeInnerPrefixList.forEach(item => {
const itemData = this[`_${item.name}Data`]
if (itemData) {
itemData.node
.x(textContentOffsetX)
.y((textContentHeight - itemData.height) / 2)
textContentNested.add(itemData.node)
textContentOffsetX += itemData.width + textContentMargin
}
})
// 自定义前置内容
if (this._prefixData) {
const foreignObject = createForeignObjectNode({
el: this._prefixData.el,
width: this._prefixData.width,
height: this._prefixData.height
})
foreignObject
.x(textContentOffsetX)
.y((textContentHeight - this._prefixData.height) / 2)
textContentNested.add(foreignObject)
textContentOffsetX += this._prefixData.width + textContentMargin
}
// icon
let iconNested = new G()
if (this._iconData && this._iconData.length > 0) {
let iconLeft = 0
this._iconData.forEach(item => {
item.node
.x(textContentOffsetX + iconLeft)
.y((textContentHeight - item.height) / 2)
iconNested.add(item.node)
iconLeft += item.width + textContentMargin
})
textContentNested.add(iconNested)
textContentOffsetX += iconLeft
}
// 文字
if (this._textData) {
const oldX = this._textData.node.attr('data-offsetx') || 0
this._textData.node.attr('data-offsetx', textContentOffsetX)
// 修复safari浏览器节点存在图标时文字位置不正确的问题
;(this._textData.nodeContent || this._textData.node)
.x(-oldX) // 修复非富文本模式下同时存在图标和换行的文本时,被收起和展开时图标与文字距离会逐渐拉大的问题
.x(textContentOffsetX)
.y((textContentHeight - this._textData.height) / 2)
// 如果开启了文本编辑实时渲染需要判断当前渲染的节点是否是正在编辑的节点是的话将透明度设置为0不显示
if (openRealtimeRenderOnNodeTextEdit) {
this._textData.node.opacity(
this.mindMap.renderer.textEdit.getCurrentEditNode() === this ? 0 : 1
)
}
textContentNested.add(this._textData.node)
textContentOffsetX += this._textData.width + textContentMargin
}
// 超链接
if (this._hyperlinkData) {
this._hyperlinkData.node
.x(textContentOffsetX)
.y((textContentHeight - this._hyperlinkData.height) / 2)
textContentNested.add(this._hyperlinkData.node)
textContentOffsetX += this._hyperlinkData.width + textContentMargin
}
// 标签
let tagNested = new G()
if (hasTagContent) {
if (tagIsBottom) {
// 标签显示在文字下方
let tagLeft = 0
this._tagData.forEach(item => {
item.node.x(tagLeft).y((maxTagHeight - item.height) / 2)
tagNested.add(item.node)
tagLeft += item.width + textContentMargin
})
tagNested
.x((textContentWidth - totalTagWidth) / 2)
.y(textContentHeightWithTag - maxTagHeight)
textContentNested.add(tagNested)
} else {
// 标签显示在文字右侧
let tagLeft = 0
this._tagData.forEach(item => {
item.node
.x(textContentOffsetX + tagLeft)
.y((textContentHeight - item.height) / 2)
tagNested.add(item.node)
tagLeft += item.width + textContentMargin
})
textContentNested.add(tagNested)
textContentOffsetX += tagLeft
}
}
// 备注
if (this._noteData) {
this._noteData.node
.x(textContentOffsetX)
.y((textContentHeight - this._noteData.height) / 2)
textContentNested.add(this._noteData.node)
textContentOffsetX += this._noteData.width + textContentMargin
}
// 附件
if (this._attachmentData) {
this._attachmentData.node
.x(textContentOffsetX)
.y((textContentHeight - this._attachmentData.height) / 2)
textContentNested.add(this._attachmentData.node)
textContentOffsetX += this._attachmentData.width + textContentMargin
}
// 自定义后置内容
if (this._postfixData) {
const foreignObject = createForeignObjectNode({
el: this._postfixData.el,
width: this._postfixData.width,
height: this._postfixData.height
})
foreignObject
.x(textContentOffsetX)
.y((textContentHeight - this._postfixData.height) / 2)
textContentNested.add(foreignObject)
textContentOffsetX += this._postfixData.width + textContentMargin
}
// 库后置内容
this.mindMap.nodeInnerPostfixList.forEach(item => {
const itemData = this[`_${item.name}Data`]
if (itemData) {
itemData.node
.x(textContentOffsetX)
.y((textContentHeight - itemData.height) / 2)
textContentNested.add(itemData.node)
textContentOffsetX += itemData.width + textContentMargin
}
})
this.group.add(textContentNested)
// 文字内容整体
const { width: bboxWidth, height: bboxHeight } = textContentNested.bbox()
let translateX = 0
let translateY = 0
switch (imgPlacement) {
case IMG_PLACEMENT.TOP:
translateX = width / 2 - bboxWidth / 2
translateY =
paddingY + // 内边距
imgHeight + // 图片高度
this.getImgTextMarin('v', 0, 0, imgHeight, textContentHeightWithTag) // 和图片的间距
break
case IMG_PLACEMENT.BOTTOM:
translateX = width / 2 - bboxWidth / 2
translateY = paddingY
break
case IMG_PLACEMENT.LEFT:
translateX =
imgWidth +
paddingX +
this.getImgTextMarin('h', imgWidth, textContentWidth)
translateY = height / 2 - bboxHeight / 2
break
case IMG_PLACEMENT.RIGHT:
translateX = paddingX
translateY = height / 2 - bboxHeight / 2
break
}
textContentNested.translate(translateX, translateY)
this.addHoverNode(width, height)
if (this._customContentAddToNodeAdd && this._customContentAddToNodeAdd.el) {
const foreignObject = createForeignObjectNode(
this._customContentAddToNodeAdd
)
this.group.add(foreignObject)
if (
addCustomContentToNode &&
typeof addCustomContentToNode.handle === 'function'
) {
addCustomContentToNode.handle({
content: this._customContentAddToNodeAdd,
element: foreignObject,
node: this
})
}
}
this.mindMap.emit('node_layout_end', this)
}
export default {
getImgTextMarin,
getTagContentSize,
getNodeRect,
addHoverNode,
layout,
customNodeContentRealtimeLayout
}

View File

@ -0,0 +1,88 @@
import btnsSvg from '../../../svg/btns'
import { SVG, Circle, G } from '@svgdotjs/svg.js'
function initQuickCreateChildBtn() {
if (this.isGeneralization) return
this._quickCreateChildBtn = null
this._showQuickCreateChildBtn = false
}
// 显示按钮
function showQuickCreateChildBtn() {
if (this.isGeneralization || this.getChildrenLength() > 0) return
// 创建按钮
if (this._quickCreateChildBtn) {
this.group.add(this._quickCreateChildBtn)
} else {
const { quickCreateChildBtnIcon, expandBtnStyle, expandBtnSize } =
this.mindMap.opt
const { icon, style } = quickCreateChildBtnIcon
let { color, fill } = expandBtnStyle || {
color: '#808080',
fill: '#fff'
}
color = style.color || color
// 图标节点
const iconNode = SVG(icon || btnsSvg.quickCreateChild).size(
expandBtnSize,
expandBtnSize
)
iconNode.css({
cursor: 'pointer'
})
iconNode.x(0).y(-expandBtnSize / 2)
this.style.iconNode(iconNode, color)
// 填充节点
const fillNode = new Circle().size(expandBtnSize)
fillNode.x(0).y(-expandBtnSize / 2)
fillNode.fill({ color: fill }).css({
cursor: 'pointer'
})
// 容器节点
this._quickCreateChildBtn = new G()
this._quickCreateChildBtn.add(fillNode).add(iconNode)
this._quickCreateChildBtn.on('click', e => {
e.stopPropagation()
this.mindMap.emit('quick_create_btn_click', this)
const { customQuickCreateChildBtnClick } = this.mindMap.opt
if (typeof customQuickCreateChildBtnClick === 'function') {
customQuickCreateChildBtnClick(this)
return
}
this.mindMap.execCommand('INSERT_CHILD_NODE', true, [this])
})
this._quickCreateChildBtn.on('dblclick', e => {
e.stopPropagation()
})
this._quickCreateChildBtn.addClass('smm-quick-create-child-btn')
this.group.add(this._quickCreateChildBtn)
}
this._showQuickCreateChildBtn = true
// 更新按钮
this.renderer.layout.renderExpandBtn(this, this._quickCreateChildBtn)
}
// 移除按钮
function removeQuickCreateChildBtn() {
if (this.isGeneralization) return
if (this._quickCreateChildBtn && this._showQuickCreateChildBtn) {
this._quickCreateChildBtn.remove()
this._showQuickCreateChildBtn = false
}
}
// 隐藏按钮
function hideQuickCreateChildBtn() {
if (this.isGeneralization) return
const { isActive } = this.getData()
if (!isActive) {
this.removeQuickCreateChildBtn()
}
}
export default {
initQuickCreateChildBtn,
showQuickCreateChildBtn,
removeQuickCreateChildBtn,
hideQuickCreateChildBtn
}

View File

@ -49,10 +49,7 @@ class Base {
// 检查当前来源是否需要重新计算节点大小 // 检查当前来源是否需要重新计算节点大小
checkIsNeedResizeSources() { checkIsNeedResizeSources() {
return [ return this.renderer.checkHasRenderSource(CONSTANTS.CHANGE_THEME)
CONSTANTS.CHANGE_THEME,
CONSTANTS.TRANSFORM_TO_NORMAL_NODE
].includes(this.renderer.renderSource)
} }
// 层级类型改变 // 层级类型改变
@ -64,7 +61,7 @@ class Base {
// 检查是否是结构布局改变重新渲染展开收起按钮占位元素 // 检查是否是结构布局改变重新渲染展开收起按钮占位元素
checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) { checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) {
if (this.renderer.renderSource === CONSTANTS.CHANGE_LAYOUT) { if (this.renderer.checkHasRenderSource(CONSTANTS.CHANGE_LAYOUT)) {
node.needRerenderExpandBtnPlaceholderRect = true node.needRerenderExpandBtnPlaceholderRect = true
} }
} }
@ -77,10 +74,38 @@ class Base {
lastData.isActive = curData.isActive lastData.isActive = curData.isActive
lastData.expand = curData.expand lastData.expand = curData.expand
lastData = JSON.stringify(lastData) lastData = JSON.stringify(lastData)
} else {
// 只在都有数据时才进行对比
return false
} }
return lastData !== JSON.stringify(curData) return lastData !== JSON.stringify(curData)
} }
// 检查库前置或后置内容是否改变了
checkNodeFixChange(newNode, nodeInnerPrefixData, nodeInnerPostfixData) {
// 库前置内容是否改变了
let isNodeInnerPrefixChange = false
this.mindMap.nodeInnerPrefixList.forEach(item => {
if (item.updateNodeData) {
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData)
if (isChange) {
isNodeInnerPrefixChange = isChange
}
}
})
// 库后置内容是否改变了
let isNodeInnerPostfixChange = false
this.mindMap.nodeInnerPostfixList.forEach(item => {
if (item.updateNodeData) {
const isChange = item.updateNodeData(newNode, nodeInnerPostfixData)
if (isChange) {
isNodeInnerPostfixChange = isChange
}
}
})
return isNodeInnerPrefixChange || isNodeInnerPostfixChange
}
// 创建节点实例 // 创建节点实例
createNode(data, parent, isRoot, layerIndex, index, ancestors) { createNode(data, parent, isRoot, layerIndex, index, ancestors) {
// 创建节点 // 创建节点
@ -98,6 +123,20 @@ class Base {
nodeInnerPrefixData[key] = value nodeInnerPrefixData[key] = value
} }
}) })
// 库后置内容数据
const nodeInnerPostfixData = {}
this.mindMap.nodeInnerPostfixList.forEach(item => {
if (item.createNodeData) {
const [key, value] = item.createNodeData({
data,
parent,
ancestors,
layerIndex,
index
})
nodeInnerPostfixData[key] = value
}
})
const uid = data.data.uid const uid = data.data.uid
let newNode = null let newNode = null
// 数据上保存了节点引用,那么直接复用节点 // 数据上保存了节点引用,那么直接复用节点
@ -117,16 +156,12 @@ class Base {
} }
this.cacheNode(data._node.uid, newNode) this.cacheNode(data._node.uid, newNode)
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode) this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
// 库前置内容是否改变了 // 库前置或后置内容是否改变了
let isNodeInnerPrefixChange = false const isNodeInnerFixChange = this.checkNodeFixChange(
this.mindMap.nodeInnerPrefixList.forEach(item => { newNode,
if (item.updateNodeData) { nodeInnerPrefixData,
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData) nodeInnerPostfixData
if (isChange) { )
isNodeInnerPrefixChange = isChange
}
}
})
// 主题或主题配置改变了 // 主题或主题配置改变了
const isResizeSource = this.checkIsNeedResizeSources() const isResizeSource = this.checkIsNeedResizeSources()
// 节点数据改变了 // 节点数据改变了
@ -139,8 +174,10 @@ class Base {
isResizeSource || isResizeSource ||
isNodeDataChange || isNodeDataChange ||
isLayerTypeChange || isLayerTypeChange ||
newNode.getData('resetRichText') || (newNode.getData('resetRichText') && // 自定义节点内容可以直接忽略resetRichText
isNodeInnerPrefixChange !newNode.isUseCustomNodeContent()) ||
newNode.getData('needUpdate') ||
isNodeInnerFixChange
) { ) {
newNode.getSize() newNode.getSize()
newNode.needLayout = true newNode.needLayout = true
@ -177,23 +214,21 @@ class Base {
const isResizeSource = this.checkIsNeedResizeSources() const isResizeSource = this.checkIsNeedResizeSources()
// 点数据改变了 // 点数据改变了
const isNodeDataChange = this.checkIsNodeDataChange(lastData, data.data) const isNodeDataChange = this.checkIsNodeDataChange(lastData, data.data)
// 库前置内容是否改变了 // 库前置或后置内容是否改变了
let isNodeInnerPrefixChange = false const isNodeInnerFixChange = this.checkNodeFixChange(
this.mindMap.nodeInnerPrefixList.forEach(item => { newNode,
if (item.updateNodeData) { nodeInnerPrefixData,
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData) nodeInnerPostfixData
if (isChange) { )
isNodeInnerPrefixChange = isChange
}
}
})
// 重新计算节点大小和布局 // 重新计算节点大小和布局
if ( if (
isResizeSource || isResizeSource ||
isNodeDataChange || isNodeDataChange ||
isLayerTypeChange || isLayerTypeChange ||
newNode.getData('resetRichText') || (newNode.getData('resetRichText') &&
isNodeInnerPrefixChange !newNode.isUseCustomNodeContent()) ||
newNode.getData('needUpdate') ||
isNodeInnerFixChange
) { ) {
newNode.getSize() newNode.getSize()
newNode.needLayout = true newNode.needLayout = true

View File

@ -2,14 +2,78 @@ import Base from './Base'
import { walk, asyncRun, degToRad, getNodeIndexInNodeList } from '../utils' import { walk, asyncRun, degToRad, getNodeIndexInNodeList } from '../utils'
import { CONSTANTS } from '../constants/constant' import { CONSTANTS } from '../constants/constant'
import utils from './fishboneUtils' import utils from './fishboneUtils'
import { SVG } from '@svgdotjs/svg.js'
import { shapeStyleProps } from '../core/render/node/Style'
// 鱼骨图 // 鱼骨图
class Fishbone extends Base { class Fishbone extends Base {
// 构造函数 // 构造函数
constructor(opt = {}) { constructor(opt = {}, layout) {
super(opt) super(opt)
this.layout = layout
this.indent = 0.3 this.indent = 0.3
this.childIndent = 0.5 this.childIndent = 0.5
this.fishTail = null
this.maxx = 0
this.headRatio = 1
this.tailRatio = 0.6
this.paddingXRatio = 0.3
this.fishHeadPathStr =
'M4,181 C4,181, 0,177, 4,173 Q 96.09523809523809,0, 288.2857142857143,0 L 288.2857142857143,354 Q 48.047619047619044,354, 8,218.18367346938777 C8,218.18367346938777, 6,214.18367346938777, 8,214.18367346938777 L 41.183673469387756,214.18367346938777 Z'
this.fishTailPathStr =
'M 606.9342905223708 0 Q 713.1342905223709 -177 819.3342905223708 -177 L 766.2342905223709 0 L 819.3342905223708 177 Q 713.1342905223709 177 606.9342905223708 0 z'
this.bindEvent()
this.extendShape()
this.beforeChange = this.beforeChange.bind(this)
}
// 重新渲染时,节点连线是否全部删除
// 鱼尾鱼骨图会多渲染一些连线,按需删除无法删除掉,只能全部删除重新创建
nodeIsRemoveAllLines(node) {
return node.isRoot || node.layerIndex === 1
}
// 是否是带鱼头鱼尾的鱼骨图
isFishbone2() {
return this.layout === CONSTANTS.LAYOUT.FISHBONE2
}
bindEvent() {
if (!this.isFishbone2()) return
this.onCheckUpdateFishTail = this.onCheckUpdateFishTail.bind(this)
this.mindMap.on('afterExecCommand', this.onCheckUpdateFishTail)
}
unBindEvent() {
this.mindMap.off('afterExecCommand', this.onCheckUpdateFishTail)
}
// 扩展节点形状
extendShape() {
if (!this.isFishbone2()) return
// 扩展鱼头形状
this.mindMap.addShape({
name: 'fishHead',
createShape: node => {
const rect = SVG(`<path d="${this.fishHeadPathStr}"></path>`)
const { width, height } = node.shapeInstance.getNodeSize()
rect.size(width, height)
return rect
},
getPadding: ({ width, height, paddingX, paddingY }) => {
width += paddingX * 2
height += paddingY * 2
let shapePaddingX = this.paddingXRatio * width
let shapePaddingY = 0
width += shapePaddingX * 2
const newHeight = width / this.headRatio
shapePaddingY = (newHeight - height) / 2
return {
paddingX: shapePaddingX,
paddingY: shapePaddingY
}
}
})
} }
// 布局 // 布局
@ -17,12 +81,14 @@ class Fishbone extends Base {
let task = [ let task = [
() => { () => {
this.computedBaseValue() this.computedBaseValue()
this.addFishTail()
}, },
() => { () => {
this.computedLeftTopValue() this.computedLeftTopValue()
}, },
() => { () => {
this.adjustLeftTopValue() this.adjustLeftTopValue()
this.updateFishTailPosition()
}, },
() => { () => {
callback(this.root) callback(this.root)
@ -31,14 +97,75 @@ class Fishbone extends Base {
asyncRun(task) asyncRun(task)
} }
// 创建鱼尾
addFishTail() {
if (!this.isFishbone2()) return
const exist = this.mindMap.lineDraw.findOne('.smm-layout-fishbone-tail')
if (!exist) {
this.fishTail = SVG(`<path d="${this.fishTailPathStr}"></path>`)
this.fishTail.addClass('smm-layout-fishbone-tail')
} else {
this.fishTail = exist
}
const tailHeight = this.root.height
const tailWidth = tailHeight * this.tailRatio
this.fishTail.size(tailWidth, tailHeight)
this.styleFishTail()
this.mindMap.lineDraw.add(this.fishTail)
}
// 如果根节点更新了形状样式,那么鱼尾也要更新
onCheckUpdateFishTail(name, node, data) {
if (name === 'SET_NODE_DATA') {
let hasShapeProp = false
Object.keys(data).forEach(key => {
if (shapeStyleProps.includes(key)) {
hasShapeProp = true
}
})
if (hasShapeProp) {
this.styleFishTail()
}
}
}
styleFishTail() {
this.root.style.shape(this.fishTail)
}
// 删除鱼尾
removeFishTail() {
const exist = this.mindMap.lineDraw.findOne('.smm-layout-fishbone-tail')
if (exist) {
exist.remove()
}
}
// 更新鱼尾形状位置
updateFishTailPosition() {
if (!this.isFishbone2()) return
this.fishTail.x(this.maxx).cy(this.root.top + this.root.height / 2)
}
// 遍历数据创建节点、计算根节点的位置计算根节点的子节点的top值 // 遍历数据创建节点、计算根节点的位置计算根节点的子节点的top值
computedBaseValue() { computedBaseValue() {
walk( walk(
this.renderer.renderTree, this.renderer.renderTree,
null, null,
(node, parent, isRoot, layerIndex, index, ancestors) => { (node, parent, isRoot, layerIndex, index, ancestors) => {
if (isRoot && this.isFishbone2()) {
// 将根节点形状强制修改为鱼头
node.data.shape = 'fishHead'
}
// 创建节点 // 创建节点
let newNode = this.createNode(node, parent, isRoot, layerIndex, index, ancestors) let newNode = this.createNode(
node,
parent,
isRoot,
layerIndex,
index,
ancestors
)
// 根节点定位在画布中心位置 // 根节点定位在画布中心位置
if (isRoot) { if (isRoot) {
this.setNodeCenter(newNode) this.setNodeCenter(newNode)
@ -57,10 +184,14 @@ class Fishbone extends Base {
// 计算二级节点的top值 // 计算二级节点的top值
if (parent._node.isRoot) { if (parent._node.isRoot) {
let marginY = this.getMarginY(layerIndex) let marginY = this.getMarginY(layerIndex)
// 带鱼头鱼尾的鱼骨图因为根节点高度比较大,所以二级节点需要向中间靠一点
const topOffset = this.isFishbone2() ? parent._node.height / 4 : 0
if (this.checkIsTop(newNode)) { if (this.checkIsTop(newNode)) {
newNode.top = parent._node.top - newNode.height - marginY newNode.top =
parent._node.top - newNode.height - marginY + topOffset
} else { } else {
newNode.top = parent._node.top + parent._node.height + marginY newNode.top =
parent._node.top + parent._node.height + marginY - topOffset
} }
} }
} }
@ -82,8 +213,11 @@ class Fishbone extends Base {
(node, parent, isRoot, layerIndex) => { (node, parent, isRoot, layerIndex) => {
if (node.isRoot) { if (node.isRoot) {
let marginX = this.getMarginX(layerIndex + 1) let marginX = this.getMarginX(layerIndex + 1)
let topTotalLeft = node.left + node.width + node.height + marginX const heightOffsetRatio = this.isFishbone2() ? 2 : 1
let bottomTotalLeft = node.left + node.width + node.height + marginX let topTotalLeft =
node.left + node.width + node.height / heightOffsetRatio + marginX
let bottomTotalLeft =
node.left + node.width + node.height / heightOffsetRatio + marginX
node.children.forEach(item => { node.children.forEach(item => {
if (this.checkIsTop(item)) { if (this.checkIsTop(item)) {
item.left = topTotalLeft item.left = topTotalLeft
@ -133,19 +267,27 @@ class Fishbone extends Base {
if (node.isRoot) { if (node.isRoot) {
let topTotalLeft = 0 let topTotalLeft = 0
let bottomTotalLeft = 0 let bottomTotalLeft = 0
let maxx = -Infinity
node.children.forEach(item => { node.children.forEach(item => {
if (this.checkIsTop(item)) { if (this.checkIsTop(item)) {
item.left += topTotalLeft item.left += topTotalLeft
this.updateChildren(item.children, 'left', topTotalLeft) this.updateChildren(item.children, 'left', topTotalLeft)
let { left, right } = this.getNodeBoundaries(item, 'h') let { left, right } = this.getNodeBoundaries(item, 'h')
if (right > maxx) {
maxx = right
}
topTotalLeft += right - left topTotalLeft += right - left
} else { } else {
item.left += bottomTotalLeft item.left += bottomTotalLeft
this.updateChildren(item.children, 'left', bottomTotalLeft) this.updateChildren(item.children, 'left', bottomTotalLeft)
let { left, right } = this.getNodeBoundaries(item, 'h') let { left, right } = this.getNodeBoundaries(item, 'h')
if (right > maxx) {
maxx = right
}
bottomTotalLeft += right - left bottomTotalLeft += right - left
} }
}) })
this.maxx = maxx
} }
}, },
true true
@ -249,7 +391,8 @@ class Fishbone extends Base {
// 水平线段到二级节点的连线 // 水平线段到二级节点的连线
let marginY = this.getMarginY(item.layerIndex) let marginY = this.getMarginY(item.layerIndex)
let nodeLineX = item.left let nodeLineX = item.left
let offset = node.height / 2 + marginY let offset =
node.height / 2 + marginY - (this.isFishbone2() ? node.height / 4 : 0)
let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
let line = this.lineDraw.path() let line = this.lineDraw.path()
if (this.checkIsTop(item)) { if (this.checkIsTop(item)) {
@ -277,11 +420,14 @@ class Fishbone extends Base {
let nodeHalfTop = node.top + node.height / 2 let nodeHalfTop = node.top + node.height / 2
let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1) let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1)
let line = this.lineDraw.path() let line = this.lineDraw.path()
const lineEndX = this.isFishbone2()
? this.maxx
: maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
line.plot( line.plot(
this.transformPath( this.transformPath(
`M ${node.left + node.width},${nodeHalfTop} L ${ `M ${
maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) node.left + node.width
},${nodeHalfTop}` },${nodeHalfTop} L ${lineEndX},${nodeHalfTop}`
) )
) )
node.style.line(line) node.style.line(line)
@ -406,6 +552,16 @@ class Fishbone extends Base {
rect.size(width, expandBtnSize).x(0).y(height) rect.size(width, expandBtnSize).x(0).y(height)
} }
} }
// 切换切换为其他结构时的处理
beforeChange() {
// 删除鱼尾
if (!this.isFishbone2()) return
this.root.nodeData.data.shape = CONSTANTS.SHAPE.RECTANGLE
this.removeFishTail()
this.unBindEvent()
this.mindMap.removeShape('fishHead')
}
} }
export default Fishbone export default Fishbone

View File

@ -1,370 +0,0 @@
import Base from './Base'
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
import { CONSTANTS } from '../utils/constant'
const degToRad = deg => {
return (Math.PI / 180) * deg
}
// 下方鱼骨图
class Fishbone extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedLeftTopValue()
},
() => {
this.adjustLeftTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据创建节点、计算根节点的位置计算根节点的子节点的top值
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(node, parent, isRoot, layerIndex, index) => {
// 创建节点
let newNode = this.createNode(node, parent, isRoot, layerIndex)
// 根节点定位在画布中心位置
if (isRoot) {
this.setNodeCenter(newNode)
} else {
// 非根节点
// 三级及以下节点以上级方向为准
if (parent._node.dir) {
newNode.dir = parent._node.dir
} else {
// 节点生长方向
newNode.dir =
index % 2 === 0
? CONSTANTS.LAYOUT_GROW_DIR.TOP
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
}
// 计算二级节点的top值
if (parent._node.isRoot) {
newNode.top = parent._node.top + parent._node.height
}
}
if (!node.data.expand) {
return true
}
},
null,
true,
0
)
}
// 遍历节点树计算节点的left、top
computedLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex, index) => {
if (node.isRoot) {
let totalLeft = node.left + node.width
node.children.forEach(item => {
item.left = totalLeft
totalLeft += item.width
})
}
if (layerIndex === 1 && node.children) {
// 遍历二级节点的子节点
let startLeft = node.left + node.width * 0.5
let totalTop =
node.top +
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(item => {
item.left = startLeft
item.top =
totalTop +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
totalTop +=
item.height +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
if (layerIndex > 1 && node.children) {
// 遍历三级及以下节点的子节点
let startLeft = node.left + node.width * 0.5
let totalTop =
node.top -
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(item => {
item.left = startLeft
item.top = totalTop - item.height
totalTop -=
item.height +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
},
null,
true
)
}
// 调整节点left、top
adjustLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.getData('expand')) {
return
}
// 调整top
let len = node.children.length
// 调整三级节点的top
// if (layerIndex === 2 && len > 0) {
// let totalHeight = node.children.reduce((h, item) => {
// return h + item.height
// }, 0)
// this.updateBrothersTop(node, totalHeight)
// }
if (layerIndex > 2 && len > 0) {
let totalHeight = node.children.reduce((h, item) => {
return (
h +
item.height +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
)
}, 0)
this.updateBrothersTop(node, -totalHeight)
}
},
(node, parent) => {
// 将二级节点的子节点移到上方
if (parent && parent.isRoot) {
// 遍历二级节点的子节点
let totalHeight = 0
let totalHeight2 = 0
node.children.forEach(item => {
// 调整top
let hasChildren = this.getNodeActChildrenLength(item) > 0
let nodeTotalHeight = this.getNodeAreaHeight(item)
let offset =
hasChildren > 0
? nodeTotalHeight -
item.height -
(hasChildren ? item.expandBtnSize : 0)
: 0
let _top = totalHeight + offset
item.top += _top
// 调整left
let offsetLeft =
(totalHeight2 + nodeTotalHeight) /
Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
item.left += offsetLeft
totalHeight += offset
totalHeight2 += nodeTotalHeight
// 同步更新后代节点
this.updateChildrenPro(item.children, {
top: _top,
left: offsetLeft
})
})
}
// 调整二级节点的子节点的left值
if (node.isRoot) {
let totalLeft = 0
node.children.forEach(item => {
item.left += totalLeft
this.updateChildren(item.children, 'left', totalLeft)
let { left, right } = this.getNodeBoundaries(item, 'h')
totalLeft += right - left
})
}
},
true
)
}
// 递归计算节点的宽度
getNodeAreaHeight(node) {
let totalHeight = 0
let loop = node => {
totalHeight +=
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
if (node.children.length) {
node.children.forEach(item => {
loop(item)
})
}
}
loop(node)
return totalHeight
}
// 调整兄弟节点的left
updateBrothersLeft(node) {
let childrenList = node.children
let totalAddWidth = 0
childrenList.forEach(item => {
item.left += totalAddWidth
if (item.children && item.children.length) {
this.updateChildren(item.children, 'left', totalAddWidth)
}
// let areaWidth = this.getNodeAreaWidth(item)
let { left, right } = this.getNodeBoundaries(item, 'h')
let areaWidth = right - left
let difference = areaWidth - item.width
if (difference > 0) {
totalAddWidth += difference
}
})
}
// 调整兄弟节点的top
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
return
}
let _offset = 0
// 下面的节点往下移
if (_index > index) {
_offset = addHeight
}
item.top += _offset
// 同步更新子节点的位置
if (item.children && item.children.length) {
this.updateChildren(item.children, 'top', _offset)
}
})
// 更新父节点的位置
this.updateBrothersTop(node.parent, node.layerIndex === 3 ? 0 : addHeight)
}
}
// 绘制连线,连接该节点到其子节点
renderLine(node, lines, style) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
let len = node.children.length
if (node.isRoot) {
// 当前节点是根节点
let prevBother = node
// 根节点的子节点是和根节点同一水平线排列
node.children.forEach((item, index) => {
let x1 = prevBother.left + prevBother.width
let x2 = item.left
let y = node.top + node.height / 2
let path = `M ${x1},${y} L ${x2},${y}`
lines[index].plot(path)
style && style(lines[index], item)
prevBother = item
})
} else {
// 当前节点为非根节点
let maxy = -Infinity
let miny = Infinity
let maxx = -Infinity
let x = node.left + node.width * 0.3
node.children.forEach((item, index) => {
if (item.left > maxx) {
maxx = item.left
}
let y = item.top + item.height / 2
if (y > maxy) {
maxy = y
}
if (y < miny) {
miny = y
}
// 水平线
if (node.layerIndex > 1) {
let path = `M ${x},${y} L ${item.left},${y}`
lines[index].plot(path)
style && style(lines[index], item)
}
})
// 竖线
if (len > 0) {
let line = this.lineDraw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
let lineLength = maxx - node.left - node.width * 0.3
if (node.parent && node.parent.isRoot) {
line.plot(
`M ${x},${top + height} L ${x + lineLength},${
top +
height +
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
}`
)
} else {
line.plot(`M ${x},${top} L ${x},${miny}`)
}
node.style.line(line)
node._lines.push(line)
style && style(line, node)
}
}
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
if (node.parent && node.parent.isRoot) {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
height + expandBtnSize / 2 - translateY
)
} else {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
-expandBtnSize / 2 - translateY
)
}
}
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeBoundaries(node, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
let x2 = right + generalizationLineMargin
let y2 = bottom
let cx = x1 + 20
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
gLine.plot(path)
gNode.left = right + generalizationNodeMargin
gNode.top = top + (bottom - top - gNode.height) / 2
}
}
export default Fishbone

View File

@ -1,351 +0,0 @@
import Base from './Base'
import { walk, asyncRun, getNodeIndexInNodeList } from '../utils'
import { CONSTANTS } from '../utils/constant'
const degToRad = deg => {
return (Math.PI / 180) * deg
}
// 上方鱼骨图
class Fishbone extends Base {
// 构造函数
constructor(opt = {}) {
super(opt)
}
// 布局
doLayout(callback) {
let task = [
() => {
this.computedBaseValue()
},
() => {
this.computedLeftTopValue()
},
() => {
this.adjustLeftTopValue()
},
() => {
callback(this.root)
}
]
asyncRun(task)
}
// 遍历数据创建节点、计算根节点的位置计算根节点的子节点的top值
computedBaseValue() {
walk(
this.renderer.renderTree,
null,
(node, parent, isRoot, layerIndex, index) => {
// 创建节点
let newNode = this.createNode(node, parent, isRoot, layerIndex)
// 根节点定位在画布中心位置
if (isRoot) {
this.setNodeCenter(newNode)
} else {
// 非根节点
// 三级及以下节点以上级方向为准
if (parent._node.dir) {
newNode.dir = parent._node.dir
} else {
// 节点生长方向
newNode.dir =
index % 2 === 0
? CONSTANTS.LAYOUT_GROW_DIR.TOP
: CONSTANTS.LAYOUT_GROW_DIR.BOTTOM
}
// 计算二级节点的top值
if (parent._node.isRoot) {
newNode.top = parent._node.top - newNode.height
}
}
if (!node.data.expand) {
return true
}
},
null,
true,
0
)
}
// 遍历节点树计算节点的left、top
computedLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex, index) => {
if (node.isRoot) {
let totalLeft = node.left + node.width
node.children.forEach(item => {
item.left = totalLeft
totalLeft += item.width
})
}
if (layerIndex >= 1 && node.children) {
// 遍历三级及以下节点的子节点
let startLeft = node.left + node.width * 0.5
let totalTop =
node.top +
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
node.children.forEach(item => {
item.left = startLeft
item.top += totalTop
totalTop +=
item.height +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
})
}
},
null,
true
)
}
// 调整节点left、top
adjustLeftTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.getData('expand')) {
return
}
// 调整top
let len = node.children.length
// 调整三级及以下节点的top
if (parent && !parent.isRoot && len > 0) {
let totalHeight = node.children.reduce((h, item) => {
return (
h +
item.height +
(this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0)
)
}, 0)
this.updateBrothersTop(node, totalHeight)
}
},
(node, parent) => {
// 将二级节点的子节点移到上方
if (parent && parent.isRoot) {
// 遍历二级节点的子节点
let totalHeight = 0
node.children.forEach(item => {
// 调整top
let nodeTotalHeight = this.getNodeAreaHeight(item)
let _top = item.top
item.top =
node.top - (item.top - node.top) - nodeTotalHeight + node.height
// 调整left
let offsetLeft =
(nodeTotalHeight + totalHeight) /
Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
item.left += offsetLeft
totalHeight += nodeTotalHeight
// 同步更新后代节点
this.updateChildrenPro(item.children, {
top: item.top - _top,
left: offsetLeft
})
})
}
// 调整二级节点的子节点的left值
if (node.isRoot) {
let totalLeft = 0
node.children.forEach(item => {
item.left += totalLeft
this.updateChildren(item.children, 'left', totalLeft)
let { left, right } = this.getNodeBoundaries(item, 'h')
totalLeft += right - left
})
}
},
true
)
}
// 递归计算节点的宽度
getNodeAreaHeight(node) {
let totalHeight = 0
let loop = node => {
totalHeight +=
node.height +
(this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0)
if (node.children.length) {
node.children.forEach(item => {
loop(item)
})
}
}
loop(node)
return totalHeight
}
// 调整兄弟节点的left
updateBrothersLeft(node) {
let childrenList = node.children
let totalAddWidth = 0
childrenList.forEach(item => {
item.left += totalAddWidth
if (item.children && item.children.length) {
this.updateChildren(item.children, 'left', totalAddWidth)
}
// let areaWidth = this.getNodeAreaWidth(item)
let { left, right } = this.getNodeBoundaries(item, 'h')
let areaWidth = right - left
let difference = areaWidth - item.width
if (difference > 0) {
totalAddWidth += difference
}
})
}
// 调整兄弟节点的top
updateBrothersTop(node, addHeight) {
if (node.parent && !node.parent.isRoot) {
let childrenList = node.parent.children
let index = getNodeIndexInNodeList(node, childrenList)
childrenList.forEach((item, _index) => {
if (item.hasCustomPosition()) {
// 适配自定义位置
return
}
let _offset = 0
// 下面的节点往下移
if (_index > index) {
_offset = addHeight
}
item.top += _offset
// 同步更新子节点的位置
if (item.children && item.children.length) {
this.updateChildren(item.children, 'top', _offset)
}
})
// 更新父节点的位置
this.updateBrothersTop(node.parent, addHeight)
}
}
// 绘制连线,连接该节点到其子节点
renderLine(node, lines, style) {
if (node.children.length <= 0) {
return []
}
let { left, top, width, height, expandBtnSize } = node
let len = node.children.length
if (node.isRoot) {
// 当前节点是根节点
let prevBother = node
// 根节点的子节点是和根节点同一水平线排列
node.children.forEach((item, index) => {
let x1 = prevBother.left + prevBother.width
let x2 = item.left
let y = node.top + node.height / 2
let path = `M ${x1},${y} L ${x2},${y}`
lines[index].plot(path)
style && style(lines[index], item)
prevBother = item
})
} else {
// 当前节点为非根节点
let maxy = -Infinity
let miny = Infinity
let maxx = -Infinity
let x = node.left + node.width * 0.3
node.children.forEach((item, index) => {
if (item.left > maxx) {
maxx = item.left
}
let y = item.top + item.height / 2
if (y > maxy) {
maxy = y
}
if (y < miny) {
miny = y
}
// 水平线
if (node.layerIndex > 1) {
let path = `M ${x},${y} L ${item.left},${y}`
lines[index].plot(path)
style && style(lines[index], item)
}
})
// 竖线
if (len > 0) {
let line = this.lineDraw.path()
expandBtnSize = len > 0 ? expandBtnSize : 0
let lineLength = maxx - node.left - node.width * 0.3
if (
node.parent &&
node.parent.isRoot &&
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP
) {
line.plot(
`M ${x},${top} L ${x + lineLength},${
top -
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
}`
)
} else {
if (node.parent && node.parent.isRoot) {
line.plot(
`M ${x},${top} L ${x + lineLength},${
top -
Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength
}`
)
} else {
line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`)
}
}
node.style.line(line)
node._lines.push(line)
style && style(line, node)
}
}
}
// 渲染按钮
renderExpandBtn(node, btn) {
let { width, height, expandBtnSize, isRoot } = node
if (!isRoot) {
let { translateX, translateY } = btn.transform()
if (node.parent && node.parent.isRoot) {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
-expandBtnSize / 2 - translateY
)
} else {
btn.translate(
width * 0.3 - expandBtnSize / 2 - translateX,
height + expandBtnSize / 2 - translateY
)
}
}
}
// 创建概要节点
renderGeneralization(node, gLine, gNode) {
let {
top,
bottom,
right,
generalizationLineMargin,
generalizationNodeMargin
} = this.getNodeBoundaries(node, 'h')
let x1 = right + generalizationLineMargin
let y1 = top
let x2 = right + generalizationLineMargin
let y2 = bottom
let cx = x1 + 20
let cy = y1 + (y2 - y1) / 2
let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}`
gLine.plot(path)
gNode.left = right + generalizationNodeMargin
gNode.top = top + (bottom - top - gNode.height) / 2
}
}
export default Fishbone

View File

@ -35,7 +35,14 @@ class VerticalTimeline extends Base {
this.renderer.renderTree, this.renderer.renderTree,
null, null,
(cur, parent, isRoot, layerIndex, index, ancestors) => { (cur, parent, isRoot, layerIndex, index, ancestors) => {
let newNode = this.createNode(cur, parent, isRoot, layerIndex, index, ancestors) let newNode = this.createNode(
cur,
parent,
isRoot,
layerIndex,
index,
ancestors
)
// 根节点定位在画布中心位置 // 根节点定位在画布中心位置
if (isRoot) { if (isRoot) {
this.setNodeCenter(newNode) this.setNodeCenter(newNode)
@ -45,12 +52,18 @@ class VerticalTimeline extends Base {
// 三级及以下节点以上级为准 // 三级及以下节点以上级为准
if (parent._node.dir) { if (parent._node.dir) {
newNode.dir = parent._node.dir newNode.dir = parent._node.dir
} else {
if (this.layout === CONSTANTS.LAYOUT.VERTICAL_TIMELINE2) {
newNode.dir = CONSTANTS.LAYOUT_GROW_DIR.LEFT
} else if (this.layout === CONSTANTS.LAYOUT.VERTICAL_TIMELINE3) {
newNode.dir = CONSTANTS.LAYOUT_GROW_DIR.RIGHT
} else { } else {
newNode.dir = newNode.dir =
index % 2 === 0 index % 2 === 0
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT ? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
: CONSTANTS.LAYOUT_GROW_DIR.LEFT : CONSTANTS.LAYOUT_GROW_DIR.LEFT
} }
}
// 定位二级节点的left // 定位二级节点的left
if (parent._node.isRoot) { if (parent._node.isRoot) {
newNode.left = newNode.left =

View File

@ -1,19 +1,18 @@
import { fromMarkdown } from 'mdast-util-from-markdown' import { fromMarkdown } from 'mdast-util-from-markdown'
const getNodeText = node => { const getNodeText = node => {
// 优先找出其中的text类型的子节点 if (node.type === 'list') return ''
let textChild = (node.children || []).find(item => { let textStr = ''
return item.type === 'text'
;(node.children || []).forEach(item => {
if (['inlineCode', 'text'].includes(item.type)) {
textStr += item.value || ''
} else {
textStr += getNodeText(item)
}
}) })
// 没有找到,那么直接使用第一个子节点
textChild = textChild || node.children[0] return textStr
if (textChild) {
if (textChild.value !== undefined) {
return textChild.value
}
return getNodeText(textChild)
}
return ''
} }
// 处理list的情况 // 处理list的情况

View File

@ -253,7 +253,9 @@ const transformToXmind = async (data, name) => {
} }
// 标签 // 标签
if (node.data.tag !== undefined) { if (node.data.tag !== undefined) {
newData.labels = node.data.tag || [] newData.labels = (node.data.tag || []).map(item => {
return typeof item === 'object' && item !== null ? item.text : item
})
} }
// 图片 // 图片
handleNodeImageToXmind(node, newNode, waitLoadImageList, imageList) handleNodeImageToXmind(node, newNode, waitLoadImageList, imageList)

View File

@ -23,6 +23,8 @@ const styleProps = [
'associativeLineTextFontFamily' 'associativeLineTextFontFamily'
] ]
const ASSOCIATIVE_LINE_TEXT_EDIT_WRAP = 'associative-line-text-edit-warp'
// 关联线插件 // 关联线插件
class AssociativeLine { class AssociativeLine {
constructor(opt = {}) { constructor(opt = {}) {
@ -62,9 +64,11 @@ class AssociativeLine {
this[item] = associativeLineControlsMethods[item].bind(this) this[item] = associativeLineControlsMethods[item].bind(this)
}) })
// 关联线文字相关方法 // 关联线文字相关方法
this.showTextEdit = false
Object.keys(associativeLineTextMethods).forEach(item => { Object.keys(associativeLineTextMethods).forEach(item => {
this[item] = associativeLineTextMethods[item].bind(this) this[item] = associativeLineTextMethods[item].bind(this)
}) })
this.mindMap.addEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
this.bindEvent() this.bindEvent()
} }
@ -157,6 +161,7 @@ class AssociativeLine {
// 取消激活关联线 // 取消激活关联线
if (!this.isControlPointMousedown) { if (!this.isControlPointMousedown) {
this.clearActiveLine() this.clearActiveLine()
this.renderAllLines()
} }
} }
@ -166,6 +171,7 @@ class AssociativeLine {
this.completeCreateLine(node) this.completeCreateLine(node)
} else { } else {
this.clearActiveLine() this.clearActiveLine()
this.renderAllLines()
} }
} }
@ -280,7 +286,7 @@ class AssociativeLine {
.stroke({ .stroke({
width: associativeLineWidth, width: associativeLineWidth,
color: associativeLineColor, color: associativeLineColor,
dasharray: associativeLineDasharray || [6, 4] dasharray: associativeLineDasharray || '6,4'
}) })
.fill({ color: 'none' }) .fill({ color: 'none' })
path.plot(pathStr) path.plot(pathStr)
@ -348,7 +354,7 @@ class AssociativeLine {
.stroke({ .stroke({
width: associativeLineWidth, width: associativeLineWidth,
color: associativeLineColor, color: associativeLineColor,
dasharray: associativeLineDasharray || [6, 4] dasharray: associativeLineDasharray || '6,4'
}) })
.fill({ color: 'none' }) .fill({ color: 'none' })
clickPath clickPath
@ -382,6 +388,7 @@ class AssociativeLine {
if (this.controlPoint2) { if (this.controlPoint2) {
this.controlPoint2.stroke({ color: associativeLineActiveColor }) this.controlPoint2.stroke({ color: associativeLineActiveColor })
} }
this.updateTextPos(path, text)
} }
// 激活某根关联线 // 激活某根关联线
@ -461,7 +468,7 @@ class AssociativeLine {
.stroke({ .stroke({
width: associativeLineWidth, width: associativeLineWidth,
color: associativeLineColor, color: associativeLineColor,
dasharray: associativeLineDasharray || [6, 4] dasharray: associativeLineDasharray || '6,4'
}) })
.fill({ color: 'none' }) .fill({ color: 'none' })
// 箭头 // 箭头
@ -742,11 +749,13 @@ class AssociativeLine {
// 插件被移除前做的事情 // 插件被移除前做的事情
beforePluginRemove() { beforePluginRemove() {
this.mindMap.deleteEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
this.unBindEvent() this.unBindEvent()
} }
// 插件被卸载前做的事情 // 插件被卸载前做的事情
beforePluginDestroy() { beforePluginDestroy() {
this.mindMap.deleteEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
this.unBindEvent() this.unBindEvent()
} }
} }

View File

@ -43,6 +43,18 @@ class Demonstrate {
this.mindMap.opt.demonstrateConfig || {} this.mindMap.opt.demonstrateConfig || {}
) )
this.needRestorePerformanceMode = false this.needRestorePerformanceMode = false
this.onConfigUpdate = this.onConfigUpdate.bind(this)
this.mindMap.on('after_update_config', this.onConfigUpdate)
}
// 监听配置更新
onConfigUpdate(opt) {
if (typeof opt.demonstrateConfig !== 'undefined') {
this.config = {
...this.config,
...opt.demonstrateConfig
}
}
} }
// 进入演示模式 // 进入演示模式
@ -417,11 +429,13 @@ class Demonstrate {
// 插件被移除前做的事情 // 插件被移除前做的事情
beforePluginRemove() { beforePluginRemove() {
this.unBindEvent() this.unBindEvent()
this.mindMap.off('after_update_config', this.onConfigUpdate)
} }
// 插件被卸载前做的事情 // 插件被卸载前做的事情
beforePluginDestroy() { beforePluginDestroy() {
this.unBindEvent() this.unBindEvent()
this.mindMap.off('after_update_config', this.onConfigUpdate)
} }
} }

View File

@ -407,7 +407,12 @@ class Drag extends Base {
TIMELINE, TIMELINE,
TIMELINE2, TIMELINE2,
VERTICAL_TIMELINE, VERTICAL_TIMELINE,
FISHBONE VERTICAL_TIMELINE2,
VERTICAL_TIMELINE3,
FISHBONE,
FISHBONE2,
RIGHT_FISHBONE,
RIGHT_FISHBONE2
} = CONSTANTS.LAYOUT } = CONSTANTS.LAYOUT
this.overlapNode = null this.overlapNode = null
this.prevNode = null this.prevNode = null
@ -443,9 +448,14 @@ class Drag extends Base {
this.handleTimeLine2(node) this.handleTimeLine2(node)
break break
case VERTICAL_TIMELINE: case VERTICAL_TIMELINE:
case VERTICAL_TIMELINE2:
case VERTICAL_TIMELINE3:
this.handleLogicalStructure(node) this.handleLogicalStructure(node)
break break
case FISHBONE: case FISHBONE:
case FISHBONE2:
case RIGHT_FISHBONE:
case RIGHT_FISHBONE2:
this.handleFishbone(node) this.handleFishbone(node)
break break
default: default:
@ -469,7 +479,12 @@ class Drag extends Base {
TIMELINE, TIMELINE,
TIMELINE2, TIMELINE2,
VERTICAL_TIMELINE, VERTICAL_TIMELINE,
FISHBONE VERTICAL_TIMELINE2,
VERTICAL_TIMELINE3,
FISHBONE,
FISHBONE2,
RIGHT_FISHBONE,
RIGHT_FISHBONE2
} = CONSTANTS.LAYOUT } = CONSTANTS.LAYOUT
const { LEFT, TOP, RIGHT, BOTTOM } = CONSTANTS.LAYOUT_GROW_DIR const { LEFT, TOP, RIGHT, BOTTOM } = CONSTANTS.LAYOUT_GROW_DIR
const layerIndex = this.overlapNode.layerIndex const layerIndex = this.overlapNode.layerIndex
@ -563,6 +578,8 @@ class Drag extends Base {
} }
break break
case VERTICAL_TIMELINE: case VERTICAL_TIMELINE:
case VERTICAL_TIMELINE2:
case VERTICAL_TIMELINE3:
if (layerIndex === 0) { if (layerIndex === 0) {
x = x =
lastNodeRect.originLeft + lastNodeRect.originLeft +
@ -580,6 +597,9 @@ class Drag extends Base {
} }
break break
case FISHBONE: case FISHBONE:
case FISHBONE2:
case RIGHT_FISHBONE:
case RIGHT_FISHBONE2:
if (layerIndex <= 1) { if (layerIndex <= 1) {
notRenderPlaceholder = true notRenderPlaceholder = true
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true) this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
@ -655,6 +675,8 @@ class Drag extends Base {
} }
break break
case VERTICAL_TIMELINE: case VERTICAL_TIMELINE:
case VERTICAL_TIMELINE2:
case VERTICAL_TIMELINE3:
if (layerIndex === 0) { if (layerIndex === 0) {
rotate = true rotate = true
} }
@ -668,6 +690,9 @@ class Drag extends Base {
halfPlaceholderHeight halfPlaceholderHeight
break break
case FISHBONE: case FISHBONE:
case FISHBONE2:
case RIGHT_FISHBONE:
case RIGHT_FISHBONE2:
if (layerIndex <= 1) { if (layerIndex <= 1) {
notRenderPlaceholder = true notRenderPlaceholder = true
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true) this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
@ -703,7 +728,12 @@ class Drag extends Base {
MIND_MAP, MIND_MAP,
TIMELINE2, TIMELINE2,
VERTICAL_TIMELINE, VERTICAL_TIMELINE,
FISHBONE VERTICAL_TIMELINE2,
VERTICAL_TIMELINE3,
FISHBONE,
FISHBONE2,
RIGHT_FISHBONE,
RIGHT_FISHBONE2
} = CONSTANTS.LAYOUT } = CONSTANTS.LAYOUT
switch (this.mindMap.opt.layout) { switch (this.mindMap.opt.layout) {
case LOGICAL_STRUCTURE: case LOGICAL_STRUCTURE:
@ -713,7 +743,12 @@ class Drag extends Base {
case MIND_MAP: case MIND_MAP:
case TIMELINE2: case TIMELINE2:
case VERTICAL_TIMELINE: case VERTICAL_TIMELINE:
case VERTICAL_TIMELINE2:
case VERTICAL_TIMELINE3:
case FISHBONE: case FISHBONE:
case FISHBONE2:
case RIGHT_FISHBONE:
case RIGHT_FISHBONE2:
return node.dir return node.dir
default: default:
return '' return ''
@ -725,17 +760,22 @@ class Drag extends Base {
handleVerticalCheck(node, checkList, isReverse = false) { handleVerticalCheck(node, checkList, isReverse = false) {
const { layout } = this.mindMap.opt const { layout } = this.mindMap.opt
const { LAYOUT, LAYOUT_GROW_DIR } = CONSTANTS const { LAYOUT, LAYOUT_GROW_DIR } = CONSTANTS
const { VERTICAL_TIMELINE, FISHBONE } = LAYOUT const {
const { BOTTOM, LEFT } = LAYOUT_GROW_DIR VERTICAL_TIMELINE,
VERTICAL_TIMELINE2,
VERTICAL_TIMELINE3,
FISHBONE,
FISHBONE2,
RIGHT_FISHBONE,
RIGHT_FISHBONE2
} = LAYOUT
const { LEFT } = LAYOUT_GROW_DIR
const mouseMoveX = this.mouseMoveX const mouseMoveX = this.mouseMoveX
const mouseMoveY = this.mouseMoveY const mouseMoveY = this.mouseMoveY
const nodeRect = this.getNodeRect(node) const nodeRect = this.getNodeRect(node)
const dir = this.getNewChildNodeDir(node) const dir = this.getNewChildNodeDir(node)
const layerIndex = node.layerIndex const layerIndex = node.layerIndex
if ( if (isReverse) {
isReverse ||
(layout === FISHBONE && dir === BOTTOM && layerIndex >= 3)
) {
checkList = checkList.reverse() checkList = checkList.reverse()
} }
let oneFourthHeight = nodeRect.originHeight / 4 let oneFourthHeight = nodeRect.originHeight / 4
@ -770,6 +810,8 @@ class Drag extends Base {
let notRenderLine = false let notRenderLine = false
switch (layout) { switch (layout) {
case VERTICAL_TIMELINE: case VERTICAL_TIMELINE:
case VERTICAL_TIMELINE2:
case VERTICAL_TIMELINE3:
if (layerIndex === 1) { if (layerIndex === 1) {
x = x =
nodeRect.originLeft + nodeRect.originLeft +
@ -777,6 +819,11 @@ class Drag extends Base {
this.placeholderWidth / 2 this.placeholderWidth / 2
} }
break break
case RIGHT_FISHBONE:
case RIGHT_FISHBONE2:
x =
nodeRect.originLeft + nodeRect.originWidth - this.placeholderWidth
break
default: default:
} }
if (checkIsPrevNode) { if (checkIsPrevNode) {
@ -791,6 +838,9 @@ class Drag extends Base {
this.placeholderHeight / 2 this.placeholderHeight / 2
switch (layout) { switch (layout) {
case FISHBONE: case FISHBONE:
case FISHBONE2:
case RIGHT_FISHBONE:
case RIGHT_FISHBONE2:
if (layerIndex === 2) { if (layerIndex === 2) {
notRenderLine = true notRenderLine = true
y = y =
@ -820,6 +870,9 @@ class Drag extends Base {
this.placeholderHeight / 2 this.placeholderHeight / 2
switch (layout) { switch (layout) {
case FISHBONE: case FISHBONE:
case FISHBONE2:
case RIGHT_FISHBONE:
case RIGHT_FISHBONE2:
if (layerIndex === 2) { if (layerIndex === 2) {
notRenderLine = true notRenderLine = true
y = y =
@ -856,7 +909,14 @@ class Drag extends Base {
handleHorizontalCheck(node, checkList) { handleHorizontalCheck(node, checkList) {
const { layout } = this.mindMap.opt const { layout } = this.mindMap.opt
const { LAYOUT } = CONSTANTS const { LAYOUT } = CONSTANTS
const { FISHBONE, TIMELINE, TIMELINE2 } = LAYOUT const {
FISHBONE,
FISHBONE2,
RIGHT_FISHBONE,
RIGHT_FISHBONE2,
TIMELINE,
TIMELINE2
} = LAYOUT
let mouseMoveX = this.mouseMoveX let mouseMoveX = this.mouseMoveX
let mouseMoveY = this.mouseMoveY let mouseMoveY = this.mouseMoveY
let nodeRect = this.getNodeRect(node) let nodeRect = this.getNodeRect(node)
@ -896,6 +956,9 @@ class Drag extends Base {
this.placeholderWidth / 2 this.placeholderWidth / 2
break break
case FISHBONE: case FISHBONE:
case FISHBONE2:
case RIGHT_FISHBONE:
case RIGHT_FISHBONE2:
if (layerIndex === 1) { if (layerIndex === 1) {
notRenderLine = true notRenderLine = true
y = y =
@ -907,7 +970,11 @@ class Drag extends Base {
default: default:
} }
if (checkIsPrevNode) { if (checkIsPrevNode) {
if ([RIGHT_FISHBONE, RIGHT_FISHBONE2].includes(layout)) {
this.nextNode = node
} else {
this.prevNode = node this.prevNode = node
}
this.setPlaceholderRect({ this.setPlaceholderRect({
x: x:
nodeRect.originRight + nodeRect.originRight +
@ -918,7 +985,11 @@ class Drag extends Base {
notRenderLine notRenderLine
}) })
} else if (checkIsNextNode) { } else if (checkIsNextNode) {
if ([RIGHT_FISHBONE, RIGHT_FISHBONE2].includes(layout)) {
this.prevNode = node
} else {
this.nextNode = node this.nextNode = node
}
this.setPlaceholderRect({ this.setPlaceholderRect({
x: x:
nodeRect.originLeft - nodeRect.originLeft -
@ -1142,7 +1213,11 @@ class Drag extends Base {
this.handleHorizontalCheck(node, checkList) this.handleHorizontalCheck(node, checkList)
} else { } else {
// 处于上方的三级节点需要特殊处理,因为节点排列方向反向了 // 处于上方的三级节点需要特殊处理,因为节点排列方向反向了
if (node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP && node.layerIndex === 2) { const is2LayerTop =
node.dir === CONSTANTS.LAYOUT_GROW_DIR.TOP && node.layerIndex === 2
const is2MoreLayerBottom =
node.dir === CONSTANTS.LAYOUT_GROW_DIR.BOTTOM && node.layerIndex >= 3
if (is2LayerTop || is2MoreLayerBottom) {
this.handleVerticalCheck(node, checkList, true) this.handleVerticalCheck(node, checkList, true)
} else { } else {
this.handleVerticalCheck(node, checkList) this.handleVerticalCheck(node, checkList)

View File

@ -128,7 +128,13 @@ class Export {
} }
// svg转png // svg转png
svgToPng(svgSrc, transparent, clipData = null) { svgToPng(
svgSrc,
transparent,
clipData = null,
fitBg = false,
format = 'image/png'
) {
const { maxCanvasSize, minExportImgCanvasScale } = this.mindMap.opt const { maxCanvasSize, minExportImgCanvasScale } = this.mindMap.opt
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image() const img = new Image()
@ -138,6 +144,7 @@ class Export {
try { try {
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
const dpr = Math.max(window.devicePixelRatio, minExportImgCanvasScale) const dpr = Math.max(window.devicePixelRatio, minExportImgCanvasScale)
// 图片原始大小
let imgWidth = img.width let imgWidth = img.width
let imgHeight = img.height let imgHeight = img.height
// 如果是裁减操作的话,那么需要手动添加内边距,及调整图片大小为实际的裁减区域的大小,不要忘了内边距哦 // 如果是裁减操作的话,那么需要手动添加内边距,及调整图片大小为实际的裁减区域的大小,不要忘了内边距哦
@ -149,10 +156,39 @@ class Export {
imgWidth = clipData.width + paddingX * 2 imgWidth = clipData.width + paddingX * 2
imgHeight = clipData.height + paddingY * 2 imgHeight = clipData.height + paddingY * 2
} }
// 适配背景图片的大小
let fitBgImgWidth = 0
let fitBgImgHeight = 0
const { backgroundImage } = this.mindMap.themeConfig
if (fitBg && backgroundImage && !transparent) {
const bgImgSize = await new Promise(resolve => {
const bgImg = new Image()
bgImg.onload = () => {
resolve([bgImg.width, bgImg.height])
}
bgImg.onerror = () => {
resolve(null)
}
bgImg.src = backgroundImage
})
if (bgImgSize) {
const imgRatio = imgWidth / imgHeight
const bgRatio = bgImgSize[0] / bgImgSize[1]
if (imgRatio > bgRatio) {
fitBgImgWidth = imgWidth
fitBgImgHeight = imgWidth / bgRatio
} else {
fitBgImgHeight = imgHeight
fitBgImgWidth = imgHeight * bgRatio
}
}
}
// 检查是否超出canvas支持的像素上限 // 检查是否超出canvas支持的像素上限
// canvas大小需要乘以dpr // canvas大小需要乘以dpr
let canvasWidth = imgWidth * dpr let scaleX = 1
let canvasHeight = imgHeight * dpr let scaleY = 1
let canvasWidth = (fitBgImgWidth || imgWidth) * dpr
let canvasHeight = (fitBgImgHeight || imgHeight) * dpr
if (canvasWidth > maxCanvasSize || canvasHeight > maxCanvasSize) { if (canvasWidth > maxCanvasSize || canvasHeight > maxCanvasSize) {
let newWidth = null let newWidth = null
let newHeight = null let newHeight = null
@ -170,6 +206,8 @@ class Export {
newWidth, newWidth,
newHeight newHeight
) )
scaleX = res[0] / canvasWidth
scaleY = res[1] / canvasHeight
canvasWidth = res[0] canvasWidth = res[0]
canvasHeight = res[1] canvasHeight = res[1]
} }
@ -177,6 +215,7 @@ class Export {
canvas.height = canvasHeight canvas.height = canvasHeight
const styleWidth = canvasWidth / dpr const styleWidth = canvasWidth / dpr
const styleHeight = canvasHeight / dpr const styleHeight = canvasHeight / dpr
// canvas元素实际上的大小
canvas.style.width = styleWidth + 'px' canvas.style.width = styleWidth + 'px'
canvas.style.height = styleHeight + 'px' canvas.style.height = styleHeight + 'px'
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
@ -187,6 +226,10 @@ class Export {
} }
// 图片绘制到canvas里 // 图片绘制到canvas里
// 如果有裁减数据,那么需要进行裁减 // 如果有裁减数据,那么需要进行裁减
const fitBgLeft =
(fitBgImgWidth > 0 ? (fitBgImgWidth - imgWidth) / 2 : 0) * scaleX
const fitBgTop =
(fitBgImgHeight > 0 ? (fitBgImgHeight - imgHeight) / 2 : 0) * scaleY
if (clipData) { if (clipData) {
ctx.drawImage( ctx.drawImage(
img, img,
@ -194,15 +237,21 @@ class Export {
clipData.top, clipData.top,
clipData.width, clipData.width,
clipData.height, clipData.height,
paddingX, paddingX * scaleX + fitBgLeft,
paddingY, paddingY * scaleY + fitBgTop,
clipData.width, clipData.width * scaleX,
clipData.height clipData.height * scaleY
) )
} else { } else {
ctx.drawImage(img, 0, 0, styleWidth, styleHeight) ctx.drawImage(
img,
fitBgLeft,
fitBgTop,
imgWidth * scaleX,
imgHeight * scaleY
)
} }
resolve(canvas.toDataURL()) resolve(canvas.toDataURL(format))
} catch (error) { } catch (error) {
reject(error) reject(error)
} }
@ -280,16 +329,35 @@ class Export {
}) })
} }
// 导出为指定格式的图片
async _image(format, name, transparent = false, node = null, fitBg = false) {
this.mindMap.renderer.textEdit.hideEditTextBox()
this.handleNodeExport(node)
const { str, clipData } = await this.getSvgData(node)
const svgUrl = await this.fixSvgStrAndToBlob(str)
const res = await this.svgToPng(
svgUrl,
transparent,
clipData,
fitBg,
format
)
return res
}
// 导出为png // 导出为png
/** /**
* 方法1.把svg的图片都转化成data:url格式再转换 * 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换 * 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/ */
async png(name, transparent = false, node = null) { async png(...args) {
this.handleNodeExport(node) const res = await this._image('image/png', ...args)
const { str, clipData } = await this.getSvgData(node) return res
const svgUrl = await this.fixSvgStrAndToBlob(str) }
const res = await this.svgToPng(svgUrl, transparent, clipData)
// 导出为jpg
async jpg(...args) {
const res = await this._image('image/jpg', ...args)
return res return res
} }
@ -305,11 +373,11 @@ class Export {
} }
// 导出为pdf // 导出为pdf
async pdf(name, transparent = false) { async pdf(name, transparent = false, fitBg = false) {
if (!this.mindMap.doExportPDF) { if (!this.mindMap.doExportPDF) {
throw new Error('请注册ExportPDF插件') throw new Error('请注册ExportPDF插件')
} }
const img = await this.png(name, transparent) const img = await this.png(name, transparent, null, fitBg)
// 使用jspdf库 // 使用jspdf库
// await this.mindMap.doExportPDF.pdf(name, img) // await this.mindMap.doExportPDF.pdf(name, img)
// 使用pdf-lib库 // 使用pdf-lib库
@ -330,6 +398,7 @@ class Export {
// 导出为svg // 导出为svg
async svg(name) { async svg(name) {
this.mindMap.renderer.textEdit.hideEditTextBox()
const { node } = await this.getSvgData() const { node } = await this.getSvgData()
node.first().before(SVG(`<title>${name}</title>`)) node.first().before(SVG(`<title>${name}</title>`))
await this.drawBackgroundToSvg(node) await this.drawBackgroundToSvg(node)

View File

@ -107,14 +107,13 @@ class Formula {
// 给指定的节点插入指定公式 // 给指定的节点插入指定公式
insertFormulaToNode(node, formula) { insertFormulaToNode(node, formula) {
let richTextPlugin = this.mindMap.richText const richTextPlugin = this.mindMap.richText
richTextPlugin.showEditText({ node }) richTextPlugin.showEditText({ node })
richTextPlugin.quill.insertEmbed( richTextPlugin.quill.insertEmbed(
richTextPlugin.quill.getLength() - 1, richTextPlugin.quill.getLength() - 1,
'formula', 'formula',
formula formula
) )
richTextPlugin.setTextStyleIfNotRichText(richTextPlugin.node)
richTextPlugin.hideEditText([node]) richTextPlugin.hideEditText([node])
} }
@ -127,8 +126,18 @@ class Formula {
for (const el of els) for (const el of els)
nodeText = nodeText.replace( nodeText = nodeText.replace(
el.outerHTML, el.outerHTML,
`\$${el.getAttribute('data-value')}\$` `$${el.getAttribute('data-value')}$`
) )
// 如果开启了实时渲染,那么意味公式转换为源码时会影响节点尺寸,需要派发事件触发渲染
if (this.mindMap.opt.openRealtimeRenderOnNodeTextEdit) {
setTimeout(() => {
this.mindMap.emit('node_text_edit_change', {
node: this.mindMap.richText.node,
text: this.mindMap.richText.getEditText(),
richText: true
})
}, 0)
}
} }
return nodeText return nodeText
} }

View File

@ -1,8 +1,7 @@
import { import {
isWhite, isWhite,
isTransparent, isTransparent,
getVisibleColorFromTheme, getVisibleColorFromTheme
readBlob
} from '../utils/index' } from '../utils/index'
// 小地图插件 // 小地图插件

View File

@ -0,0 +1,100 @@
import { walk, createUid } from '../utils/index'
// 修改base64格式的节点图片在数据中的存储方式
// 将base64格式的图片以key-map的形式存储在根节点的imgMap字段里其他节点只保存key避免不同的节点引用相同的图片重复存储的问题普通url格式的图片不处理
class NodeBase64ImageStorage {
constructor(opt) {
this.opt = opt
this.mindMap = opt.mindMap
this.bindEvent()
}
bindEvent() {
this.onBeforeAddHistory = this.onBeforeAddHistory.bind(this)
this.mindMap.on('beforeAddHistory', this.onBeforeAddHistory)
}
unBindEvent() {
this.mindMap.off('beforeAddHistory', this.onBeforeAddHistory)
}
isBase64ImgUrl(url) {
return /^data:/.test(url)
}
isImageKey(url) {
return /^smm_img_key_/.test(url)
}
createImageKey() {
return 'smm_img_key_' + createUid()
}
onBeforeAddHistory() {
const renderTree = this.mindMap.renderer.renderTree
if (!renderTree) return
let imgMap = renderTree.data.imgMap
if (!imgMap) {
imgMap = renderTree.data.imgMap = {}
}
const useIds = []
const getImgIds = () => {
return Object.keys(imgMap)
}
const getImgId = image => {
return getImgIds().find(id => {
return imgMap[id] === image
})
}
walk(renderTree, null, node => {
const image = node.data.image
if (image) {
// 如果是base64图片url
if (this.isBase64ImgUrl(image)) {
// 检查该图片是否已存在
const hasId = getImgId(image)
if (hasId) {
// 已存在则直接使用现有的key
useIds.push(hasId)
node.data.image = hasId
} else {
// 不存在则生成key并存储
const newId = this.createImageKey()
node.data.image = newId
imgMap[newId] = image
useIds.push(newId)
}
} else if (this.isImageKey(image)) {
// 如果是key那么收集一下
if (getImgIds().includes(image)) {
useIds.push(image)
}
}
}
})
// 删除已无节点引用的图片
getImgIds().forEach(id => {
if (!useIds.includes(id)) {
delete imgMap[id]
}
})
}
// 插件被移除前做的事情
beforePluginRemove() {
this.unBindEvent()
}
// 插件被卸载前做的事情
beforePluginDestroy() {
this.unBindEvent()
}
}
NodeBase64ImageStorage.instanceName = 'nodeBase64ImageStorage'
export default NodeBase64ImageStorage

View File

@ -31,12 +31,14 @@ class NodeImgAdjust {
this.onMousemove = this.onMousemove.bind(this) this.onMousemove = this.onMousemove.bind(this)
this.onMouseup = this.onMouseup.bind(this) this.onMouseup = this.onMouseup.bind(this)
this.onRenderEnd = this.onRenderEnd.bind(this) this.onRenderEnd = this.onRenderEnd.bind(this)
this.onScale = this.onScale.bind(this)
this.mindMap.on('node_img_mouseleave', this.onNodeImgMouseleave) this.mindMap.on('node_img_mouseleave', this.onNodeImgMouseleave)
this.mindMap.on('node_img_mousemove', this.onNodeImgMousemove) this.mindMap.on('node_img_mousemove', this.onNodeImgMousemove)
this.mindMap.on('mousemove', this.onMousemove) this.mindMap.on('mousemove', this.onMousemove)
this.mindMap.on('mouseup', this.onMouseup) this.mindMap.on('mouseup', this.onMouseup)
this.mindMap.on('node_mouseup', this.onMouseup) this.mindMap.on('node_mouseup', this.onMouseup)
this.mindMap.on('node_tree_render_end', this.onRenderEnd) this.mindMap.on('node_tree_render_end', this.onRenderEnd)
this.mindMap.on('scale', this.onScale)
} }
// 解绑事件 // 解绑事件
@ -47,6 +49,15 @@ class NodeImgAdjust {
this.mindMap.off('mouseup', this.onMouseup) this.mindMap.off('mouseup', this.onMouseup)
this.mindMap.off('node_mouseup', this.onMouseup) this.mindMap.off('node_mouseup', this.onMouseup)
this.mindMap.off('node_tree_render_end', this.onRenderEnd) this.mindMap.off('node_tree_render_end', this.onRenderEnd)
this.mindMap.off('scale', this.onScale)
}
// 如果当前操作按钮正在显示时缩放了画布,那么需要更新位置
onScale() {
if (this.node && this.img && this.isShowHandleEl) {
this.rect = this.img.rbox()
this.setHandleElRect()
}
} }
// 节点图片鼠标移动事件 // 节点图片鼠标移动事件
@ -122,7 +133,11 @@ class NodeImgAdjust {
// 创建调整按钮元素 // 创建调整按钮元素
createResizeBtnEl() { createResizeBtnEl() {
const { imgResizeBtnSize } = this.mindMap.opt const {
imgResizeBtnSize,
customResizeBtnInnerHTML,
customDeleteBtnInnerHTML
} = this.mindMap.opt
// 容器元素 // 容器元素
this.handleEl = document.createElement('div') this.handleEl = document.createElement('div')
this.handleEl.style.cssText = ` this.handleEl.style.cssText = `
@ -134,7 +149,7 @@ class NodeImgAdjust {
this.handleEl.className = 'node-img-handle' this.handleEl.className = 'node-img-handle'
// 调整按钮元素 // 调整按钮元素
const btnEl = document.createElement('div') const btnEl = document.createElement('div')
btnEl.innerHTML = btnsSvg.imgAdjust btnEl.innerHTML = customResizeBtnInnerHTML || btnsSvg.imgAdjust
btnEl.style.cssText = ` btnEl.style.cssText = `
position: absolute; position: absolute;
right: 0; right: 0;
@ -179,7 +194,7 @@ class NodeImgAdjust {
const btnRemove = document.createElement('div') const btnRemove = document.createElement('div')
this.handleEl.prepend(btnRemove) this.handleEl.prepend(btnRemove)
btnRemove.className = 'node-image-remove' btnRemove.className = 'node-image-remove'
btnRemove.innerHTML = btnsSvg.remove btnRemove.innerHTML = customDeleteBtnInnerHTML || btnsSvg.remove
btnRemove.style.cssText = ` btnRemove.style.cssText = `
position: absolute; position: absolute;
right: 0;top:0;color:#fff; right: 0;top:0;color:#fff;
@ -206,6 +221,7 @@ class NodeImgAdjust {
} }
if (!stop) { if (!stop) {
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null }) this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null })
this.mindMap.emit('delete_node_img_from_delete_btn', this.node)
} }
}) })
// 添加元素到页面 // 添加元素到页面
@ -215,6 +231,7 @@ class NodeImgAdjust {
// 鼠标按钮按下事件 // 鼠标按钮按下事件
onMousedown(e) { onMousedown(e) {
this.mindMap.emit('node_img_adjust_btn_mousedown', this.node)
this.isMousedown = true this.isMousedown = true
this.mousedownDrawTransform = this.mindMap.draw.transform() this.mousedownDrawTransform = this.mindMap.draw.transform()
// 隐藏节点实际图片 // 隐藏节点实际图片

View File

@ -1,149 +1,68 @@
import { import {
formatDataToArray, formatDataToArray,
walk, walk,
getTopAncestorsFomNodeList,
getNodeListBoundingRect, getNodeListBoundingRect,
createUid createUid
} from '../utils' } from '../utils'
import {
// 解析要添加外框的节点实例列表 parseAddNodeList,
const parseAddNodeList = list => { getNodeOuterFrameList
// 找出顶层节点 } from './outerFrame/outerFrameUtils'
list = getTopAncestorsFomNodeList(list) import outerFrameTextMethods from './outerFrame/outerFrameText'
const cache = {}
const uidToParent = {}
// 找出列表中节点在兄弟节点中的索引,并和父节点关联起来
list.forEach(node => {
const parent = node.parent
if (parent) {
const pUid = parent.uid
uidToParent[pUid] = parent
const index = node.getIndexInBrothers()
const data = {
node,
index
}
if (cache[pUid]) {
if (
!cache[pUid].find(item => {
return item.index === data.index
})
) {
cache[pUid].push(data)
}
} else {
cache[pUid] = [data]
}
}
})
const res = []
Object.keys(cache).forEach(uid => {
const indexList = cache[uid]
const parentNode = uidToParent[uid]
if (indexList.length > 1) {
// 多个节点
const rangeList = indexList
.map(item => {
return item.index
})
.sort((a, b) => {
return a - b
})
const minIndex = rangeList[0]
const maxIndex = rangeList[rangeList.length - 1]
let curStart = -1
let curEnd = -1
for (let i = minIndex; i <= maxIndex; i++) {
// 连续索引
if (rangeList.includes(i)) {
if (curStart === -1) {
curStart = i
}
curEnd = i
} else {
// 连续断开
if (curStart !== -1 && curEnd !== -1) {
res.push({
node: parentNode,
range: [curStart, curEnd]
})
}
curStart = -1
curEnd = -1
}
}
// 不要忘了最后一段索引
if (curStart !== -1 && curEnd !== -1) {
res.push({
node: parentNode,
range: [curStart, curEnd]
})
}
} else {
// 单个节点
res.push({
node: parentNode,
range: [indexList[0].index, indexList[0].index]
})
}
})
return res
}
// 解析获取节点的子节点生成的外框列表
const getNodeOuterFrameList = node => {
const children = node.children
if (!children || children.length <= 0) return
const res = []
const map = {}
children.forEach((item, index) => {
const outerFrameData = item.getData('outerFrame')
if (!outerFrameData) return
const groupId = outerFrameData.groupId
if (groupId) {
if (!map[groupId]) {
map[groupId] = []
}
map[groupId].push({
node: item,
index
})
} else {
res.push({
nodeList: [item],
range: [index, index]
})
}
})
Object.keys(map).forEach(id => {
const list = map[id]
res.push({
nodeList: list.map(item => {
return item.node
}),
range: [list[0].index, list[list.length - 1].index]
})
})
return res
}
// 默认外框样式 // 默认外框样式
const defaultStyle = { const defaultStyle = {
// 外框圆角大小
radius: 5, radius: 5,
// 外框边框宽度
strokeWidth: 2, strokeWidth: 2,
// 外框边框颜色
strokeColor: '#0984e3', strokeColor: '#0984e3',
// 外框边框虚线样式
strokeDasharray: '5,5', strokeDasharray: '5,5',
fill: 'rgba(9,132,227,0.05)' // 外框填充颜色
fill: 'rgba(9,132,227,0.05)',
// 外框文字字号
fontSize: 14,
// 外框文字字体
fontFamily: '微软雅黑, Microsoft YaHei',
// 加粗
fontWeight: 'normal', // bold
// 斜体
fontStyle: 'normal', // italic
// 外框文字颜色
color: '#fff',
// 外框文字行高
lineHeight: 1.2,
// 外框文字背景
textFill: '#0984e3',
// 外框文字圆角
textFillRadius: 5,
// 外框文字矩内边距,左上右下
textFillPadding: [5, 5, 5, 5],
// 外框文字水平显示位置,相对于外框
textAlign: 'left' // left、center、right
} }
const OUTER_FRAME_TEXT_EDIT_WRAP = 'outer-frame-text-edit-warp'
// 外框插件 // 外框插件
class OuterFrame { class OuterFrame {
constructor(opt = {}) { constructor(opt = {}) {
this.mindMap = opt.mindMap this.mindMap = opt.mindMap
this.draw = null this.draw = null
this.createDrawContainer() this.createDrawContainer()
this.isNotRenderOuterFrames = false
this.textNodeList = []
this.outerFrameElList = [] this.outerFrameElList = []
this.activeOuterFrame = null this.activeOuterFrame = null
// 文字相关方法
this.textEditNode = null
this.showTextEdit = false
Object.keys(outerFrameTextMethods).forEach(item => {
this[item] = outerFrameTextMethods[item].bind(this)
})
this.mindMap.addEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
this.bindEvent() this.bindEvent()
} }
@ -164,6 +83,11 @@ class OuterFrame {
this.clearActiveOuterFrame = this.clearActiveOuterFrame.bind(this) this.clearActiveOuterFrame = this.clearActiveOuterFrame.bind(this)
this.mindMap.on('draw_click', this.clearActiveOuterFrame) this.mindMap.on('draw_click', this.clearActiveOuterFrame)
this.mindMap.on('node_click', this.clearActiveOuterFrame) this.mindMap.on('node_click', this.clearActiveOuterFrame)
// 缩放事件
this.mindMap.on('scale', this.onScale)
// 实例销毁事件
this.onBeforeDestroy = this.onBeforeDestroy.bind(this)
this.mindMap.on('beforeDestroy', this.onBeforeDestroy)
this.addOuterFrame = this.addOuterFrame.bind(this) this.addOuterFrame = this.addOuterFrame.bind(this)
this.mindMap.command.add('ADD_OUTER_FRAME', this.addOuterFrame) this.mindMap.command.add('ADD_OUTER_FRAME', this.addOuterFrame)
@ -181,6 +105,8 @@ class OuterFrame {
this.mindMap.off('data_change', this.renderOuterFrames) this.mindMap.off('data_change', this.renderOuterFrames)
this.mindMap.off('draw_click', this.clearActiveOuterFrame) this.mindMap.off('draw_click', this.clearActiveOuterFrame)
this.mindMap.off('node_click', this.clearActiveOuterFrame) this.mindMap.off('node_click', this.clearActiveOuterFrame)
this.mindMap.off('scale', this.onScale)
this.mindMap.off('beforeDestroy', this.onBeforeDestroy)
this.mindMap.command.remove('ADD_OUTER_FRAME', this.addOuterFrame) this.mindMap.command.remove('ADD_OUTER_FRAME', this.addOuterFrame)
this.mindMap.keyCommand.removeShortcut( this.mindMap.keyCommand.removeShortcut(
'Del|Backspace', 'Del|Backspace',
@ -188,6 +114,12 @@ class OuterFrame {
) )
} }
// 实例销毁时清除关联线文字编辑框
onBeforeDestroy() {
this.hideEditTextBox()
this.removeTextEditEl()
}
// 给节点添加外框数据 // 给节点添加外框数据
/* /*
config: { config: {
@ -256,20 +188,47 @@ class OuterFrame {
this.mindMap.emit('outer_frame_delete') this.mindMap.emit('outer_frame_delete')
} }
// 删除当前激活外框的文字
removeActiveOuterFrameText() {
this.updateActiveOuterFrame({
text: ''
})
}
// 更新当前激活的外框 // 更新当前激活的外框
// 执行了该方法后请立即隐藏你的样式面板,因为会清除当前激活的外框
updateActiveOuterFrame(config = {}) { updateActiveOuterFrame(config = {}) {
if (!this.activeOuterFrame) return if (!this.activeOuterFrame) return
const { node, range } = this.activeOuterFrame this.isNotRenderOuterFrames = true
const { el, node, range } = this.activeOuterFrame
let newStrokeDasharray = ''
this.getRangeNodeList(node, range).forEach(node => { this.getRangeNodeList(node, range).forEach(node => {
const outerFrame = node.getData('outerFrame') const outerFrame = node.getData('outerFrame')
this.mindMap.execCommand('SET_NODE_DATA', node, { const newData = {
outerFrame: {
...outerFrame, ...outerFrame,
...config ...config
} }
newStrokeDasharray = newData.strokeDasharray
this.mindMap.execCommand('SET_NODE_DATA', node, {
outerFrame: newData
}) })
}) })
el.cacheStyle = {
dasharray: newStrokeDasharray
}
this.updateOuterFrameStyle()
}
// 更新当前激活外框的样式
updateOuterFrameStyle() {
const { el, node, range, textNode } = this.activeOuterFrame
const firstNode = this.getNodeRangeFirstNode(node, range)
const styleConfig = this.getStyle(firstNode)
this.styleOuterFrame(el, {
...styleConfig,
strokeDasharray: 'none'
})
const text = this.getText(firstNode)
this.renderText(text, el, textNode, node, range)
} }
// 获取某个节点指定范围的带外框的子节点列表 // 获取某个节点指定范围的带外框的子节点列表
@ -279,8 +238,19 @@ class OuterFrame {
}) })
} }
// 获取某个节点指定范围的带外框的第一个子节点
getNodeRangeFirstNode(node, range) {
return node.children[range[0]]
}
// 渲染外框 // 渲染外框
renderOuterFrames() { renderOuterFrames() {
if (this.isNotRenderOuterFrames) {
this.isNotRenderOuterFrames = false
return
}
this.clearActiveOuterFrame()
this.clearTextNodes()
this.clearOuterFrameElList() this.clearOuterFrameElList()
let tree = this.mindMap.renderer.root let tree = this.mindMap.renderer.root
if (!tree) return if (!tree) return
@ -317,11 +287,15 @@ class OuterFrame {
t.scaleY, t.scaleY,
(width + outerFramePaddingX * 2) / t.scaleX, (width + outerFramePaddingX * 2) / t.scaleX,
(height + outerFramePaddingY * 2) / t.scaleY, (height + outerFramePaddingY * 2) / t.scaleY,
nodeList[0].getData('outerFrame') // 使用第一个节点的外框样式 this.getStyle(nodeList[0]) // 使用第一个节点的外框样式
) )
// 渲染文字,如果有的话
const textNode = this.createText(el, cur, range)
this.textNodeList.push(textNode)
this.renderText(this.getText(nodeList[0]), el, textNode, cur, range)
el.on('click', e => { el.on('click', e => {
e.stopPropagation() e.stopPropagation()
this.setActiveOuterFrame(el, cur, range) this.setActiveOuterFrame(el, cur, range, textNode)
}) })
}) })
} }
@ -333,37 +307,67 @@ class OuterFrame {
} }
// 激活外框 // 激活外框
setActiveOuterFrame(el, node, range) { setActiveOuterFrame(el, node, range, textNode) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE') this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
this.clearActiveOuterFrame() this.clearActiveOuterFrame()
this.activeOuterFrame = { this.activeOuterFrame = {
el, el,
node, node,
range range,
textNode
} }
el.stroke({ el.stroke({
dasharray: 'none' dasharray: 'none'
}) })
// 如果没有输入过文字,那么显示默认文字
if (!this.getText(this.getNodeRangeFirstNode(node, range))) {
this.renderText(
this.mindMap.opt.defaultOuterFrameText,
el,
textNode,
node,
range
)
}
this.mindMap.emit('outer_frame_active', el, node, range) this.mindMap.emit('outer_frame_active', el, node, range)
} }
// 清除当前激活的外框 // 清除当前激活的外框
clearActiveOuterFrame() { clearActiveOuterFrame() {
if (!this.activeOuterFrame) return if (!this.activeOuterFrame) return
const { el } = this.activeOuterFrame const { el, textNode, node, range } = this.activeOuterFrame
el.stroke({ el.stroke({
dasharray: el.cacheStyle.dasharray || defaultStyle.strokeDasharray dasharray: el.cacheStyle.dasharray || defaultStyle.strokeDasharray
}) })
// 隐藏文本编辑框
this.hideEditTextBox()
// 如果没有输入过文字,那么隐藏
if (!this.getText(this.getNodeRangeFirstNode(node, range))) {
textNode.clear()
}
this.activeOuterFrame = null this.activeOuterFrame = null
this.mindMap.emit('outer_frame_deactivate')
}
// 获取指定外框的样式
getStyle(node) {
return { ...defaultStyle, ...(node.getData('outerFrame') || {}) }
} }
// 创建外框元素 // 创建外框元素
createOuterFrameEl(x, y, width, height, styleConfig = {}) { createOuterFrameEl(x, y, width, height, styleConfig = {}) {
styleConfig = { ...defaultStyle, ...styleConfig } const el = this.draw.rect().size(width, height).x(x).y(y)
const el = this.draw this.styleOuterFrame(el, styleConfig)
.rect() el.cacheStyle = {
.size(width, height) dasharray: styleConfig.strokeDasharray
.radius(styleConfig.radius) }
this.outerFrameElList.push(el)
return el
}
// 设置外框样式
styleOuterFrame(el, styleConfig) {
el.radius(styleConfig.radius)
.stroke({ .stroke({
width: styleConfig.strokeWidth, width: styleConfig.strokeWidth,
color: styleConfig.strokeColor, color: styleConfig.strokeColor,
@ -372,13 +376,13 @@ class OuterFrame {
.fill({ .fill({
color: styleConfig.fill color: styleConfig.fill
}) })
.x(x)
.y(y)
el.cacheStyle = {
dasharray: styleConfig.strokeDasharray
} }
this.outerFrameElList.push(el)
return el // 清除文本元素
clearTextNodes() {
this.textNodeList.forEach(item => {
item.remove()
})
} }
// 清除外框元素 // 清除外框元素
@ -392,15 +396,18 @@ class OuterFrame {
// 插件被移除前做的事情 // 插件被移除前做的事情
beforePluginRemove() { beforePluginRemove() {
this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
this.unBindEvent() this.unBindEvent()
} }
// 插件被卸载前做的事情 // 插件被卸载前做的事情
beforePluginDestroy() { beforePluginDestroy() {
this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
this.unBindEvent() this.unBindEvent()
} }
} }
OuterFrame.instanceName = 'outerFrame' OuterFrame.instanceName = 'outerFrame'
OuterFrame.defaultStyle = defaultStyle
export default OuterFrame export default OuterFrame

View File

@ -6,11 +6,13 @@ import {
getTextFromHtml, getTextFromHtml,
isUndef, isUndef,
checkSmmFormatData, checkSmmFormatData,
removeHtmlNodeByClass,
formatGetNodeGeneralization, formatGetNodeGeneralization,
nodeRichTextToTextWithWrap nodeRichTextToTextWithWrap,
getNodeRichTextStyles,
htmlEscape,
compareVersion
} from '../utils' } from '../utils'
import { CONSTANTS } from '../constants/constant' import { richTextSupportStyleList } from '../constants/constant'
import MindMapNode from '../core/render/node/MindMapNode' import MindMapNode from '../core/render/node/MindMapNode'
import { Scope } from 'parchment' import { Scope } from 'parchment'
@ -38,6 +40,8 @@ let fontSizeList = new Array(100).fill(0).map((_, index) => {
return index + 'px' return index + 'px'
}) })
const RICH_TEXT_EDIT_WRAP = 'ql-editor'
// 富文本编辑插件 // 富文本编辑插件
class RichText { class RichText {
constructor({ mindMap, pluginOpt }) { constructor({ mindMap, pluginOpt }) {
@ -53,27 +57,16 @@ class RichText {
this.isInserting = false this.isInserting = false
this.styleEl = null this.styleEl = null
this.cacheEditingText = '' this.cacheEditingText = ''
this.lostStyle = false
this.isCompositing = false this.isCompositing = false
this.textNodePaddingX = 6 this.textNodePaddingX = 6
this.textNodePaddingY = 4 this.textNodePaddingY = 4
this.supportStyleProps = [ this.mindMap.addEditNodeClass(RICH_TEXT_EDIT_WRAP)
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'textDecoration',
'color'
]
this.initOpt() this.initOpt()
this.extendQuill() this.extendQuill()
this.appendCss() this.appendCss()
this.bindEvent() this.bindEvent()
// 处理数据,转成富文本格式 this.handleDataToRichTextOnInit()
if (this.mindMap.opt.data) {
this.mindMap.opt.data = this.handleSetData(this.mindMap.opt.data)
}
} }
// 绑定事件 // 绑定事件
@ -108,18 +101,28 @@ class RichText {
user-select: none; user-select: none;
} }
.smm-richtext-node-wrap p { .ql-editor .ql-align-left,
font-family: auto; .smm-richtext-node-wrap .ql-align-left {
text-align: left;
}
.smm-richtext-node-wrap .ql-align-right {
text-align: right;
}
.smm-richtext-node-wrap .ql-align-center {
text-align: center;
} }
` `
) )
let cssText = ` let cssText = `
.${CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP} { .${RICH_TEXT_EDIT_WRAP} {
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
height: auto; height: auto;
line-height: normal; line-height: 1.2;
-webkit-user-select: text; -webkit-user-select: text;
text-align: inherit;
} }
.ql-container { .ql-container {
@ -130,10 +133,6 @@ class RichText {
.ql-container.ql-snow { .ql-container.ql-snow {
border: none; border: none;
} }
.smm-richtext-node-edit-wrap p {
font-family: auto;
}
` `
this.styleEl = document.createElement('style') this.styleEl = document.createElement('style')
this.styleEl.type = 'text/css' this.styleEl.type = 'text/css'
@ -166,6 +165,8 @@ class RichText {
this.extendFont([]) this.extendFont([])
this.extendAlign()
// 扩展quill的字号列表 // 扩展quill的字号列表
const SizeAttributor = Quill.import('attributors/class/size') const SizeAttributor = Quill.import('attributors/class/size')
SizeAttributor.whitelist = fontSizeList SizeAttributor.whitelist = fontSizeList
@ -190,6 +191,13 @@ class RichText {
Quill.register(FontStyle, true) Quill.register(FontStyle, true)
} }
// 扩展文本对齐方式
extendAlign() {
const AlignFormat = Quill.import('formats/align')
AlignFormat.whitelist = ['right', 'center', 'justify', 'left']
Quill.register(AlignFormat, true)
}
// 显示文本编辑控件 // 显示文本编辑控件
showEditText({ node, rect, isInserting, isFromKeyDown, isFromScale }) { showEditText({ node, rect, isInserting, isFromKeyDown, isFromScale }) {
if (this.showTextEdit) { if (this.showTextEdit) {
@ -238,6 +246,7 @@ class RichText {
outline: none; outline: none;
word-break: break-all; word-break: break-all;
padding: ${paddingY}px ${paddingX}px; padding: ${paddingY}px ${paddingX}px;
line-height: 1.2;
` `
this.textEditNode.addEventListener('click', e => { this.textEditNode.addEventListener('click', e => {
e.stopPropagation() e.stopPropagation()
@ -253,6 +262,7 @@ class RichText {
const targetNode = customInnerElsAppendTo || document.body const targetNode = customInnerElsAppendTo || document.body
targetNode.appendChild(this.textEditNode) targetNode.appendChild(this.textEditNode)
} }
this.addNodeTextStyleToTextEditNode(node)
this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px` this.textEditNode.style.marginLeft = `-${paddingX * scaleX}px`
this.textEditNode.style.marginTop = `-${paddingY * scaleY}px` this.textEditNode.style.marginTop = `-${paddingY * scaleY}px`
this.textEditNode.style.zIndex = nodeTextEditZIndex this.textEditNode.style.zIndex = nodeTextEditZIndex
@ -277,13 +287,8 @@ class RichText {
const isEmptyText = isUndef(nodeText) const isEmptyText = isUndef(nodeText)
// 是否是非空的非富文本 // 是否是非空的非富文本
const noneEmptyNoneRichText = !node.getData('richText') && !isEmptyText const noneEmptyNoneRichText = !node.getData('richText') && !isEmptyText
// 如果是空文本,那么设置为丢失样式状态,否则输入不会带上样式
if (isEmptyText) {
this.lostStyle = true
}
if (isFromKeyDown && autoEmptyTextWhenKeydownEnterEdit) { if (isFromKeyDown && autoEmptyTextWhenKeydownEnterEdit) {
this.textEditNode.innerHTML = '' this.textEditNode.innerHTML = ''
this.lostStyle = true
} else if (noneEmptyNoneRichText) { } else if (noneEmptyNoneRichText) {
// 还不是富文本 // 还不是富文本
let text = String(nodeText).split(/\n/gim).join('<br>') let text = String(nodeText).split(/\n/gim).join('<br>')
@ -294,19 +299,13 @@ class RichText {
this.textEditNode.innerHTML = this.cacheEditingText || nodeText this.textEditNode.innerHTML = this.cacheEditingText || nodeText
} }
this.initQuillEditor() this.initQuillEditor()
document.querySelector( this.setQuillContainerMinHeight(originHeight)
'.' + CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP this.setIsShowTextEdit(true)
).style.minHeight = originHeight + 'px'
this.showTextEdit = true
// 如果是刚创建的节点那么默认全选否则普通激活不全选除非selectTextOnEnterEditText配置为true // 如果是刚创建的节点那么默认全选否则普通激活不全选除非selectTextOnEnterEditText配置为true
// 在selectTextOnEnterEditText时如果是在keydown事件进入的节点编辑也不需要全选 // 在selectTextOnEnterEditText时如果是在keydown事件进入的节点编辑也不需要全选
this.focus( this.focus(
isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null isInserting || (selectTextOnEnterEditText && !isFromKeyDown) ? 0 : null
) )
if (noneEmptyNoneRichText) {
// 如果是非富文本的情况,需要手动应用文本样式
this.setTextStyleIfNotRichText(node)
}
this.cacheEditingText = '' this.cacheEditingText = ''
} }
@ -325,6 +324,20 @@ class RichText {
: '0 0 20px rgba(0,0,0,.5)' : '0 0 20px rgba(0,0,0,.5)'
} }
// 将指定节点的文本样式添加到编辑框元素上
addNodeTextStyleToTextEditNode(node) {
const style = getNodeRichTextStyles(node)
Object.keys(style).forEach(prop => {
this.textEditNode.style[prop] = style[prop]
})
}
// 设置quill编辑器容器的最小高度
setQuillContainerMinHeight(minHeight) {
document.querySelector('.' + RICH_TEXT_EDIT_WRAP).style.minHeight =
minHeight + 'px'
}
// 更新文本编辑框的大小和位置 // 更新文本编辑框的大小和位置
updateTextEditNode() { updateTextEditNode() {
if (!this.node) return if (!this.node) return
@ -337,6 +350,7 @@ class RichText {
this.textEditNode.style.minHeight = originHeight + 'px' this.textEditNode.style.minHeight = originHeight + 'px'
this.textEditNode.style.left = rect.left + 'px' this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px' this.textEditNode.style.top = rect.top + 'px'
this.setQuillContainerMinHeight(originHeight)
} }
// 删除文本编辑框元素 // 删除文本编辑框元素
@ -346,24 +360,10 @@ class RichText {
targetNode.removeChild(this.textEditNode) targetNode.removeChild(this.textEditNode)
} }
// 如果是非富文本的情况,需要手动应用文本样式
setTextStyleIfNotRichText(node) {
let style = {
font: node.style.merge('fontFamily'),
color: node.style.merge('color'),
italic: node.style.merge('fontStyle') === 'italic',
bold: node.style.merge('fontWeight') === 'bold',
size: node.style.merge('fontSize') + 'px',
underline: node.style.merge('textDecoration') === 'underline',
strike: node.style.merge('textDecoration') === 'line-through'
}
this.pureFormatAllText(style)
}
// 获取当前正在编辑的内容 // 获取当前正在编辑的内容
getEditText() { getEditText() {
// https://github.com/slab/quill/issues/4509 // https://github.com/slab/quill/issues/4509
return this.quill.container.firstChild.innerHTML.replaceAll(/ +/g, match => return this.quill.container.firstChild.innerHTML.replace(/ +/g, match =>
'&nbsp;'.repeat(match.length) '&nbsp;'.repeat(match.length)
) )
// 去除ql-cursor节点 // 去除ql-cursor节点
@ -374,18 +374,6 @@ class RichText {
// return html.replace(/<p><br><\/p>$/, '') // return html.replace(/<p><br><\/p>$/, '')
} }
// 给html字符串中的节点样式按样式名首字母排序
sortHtmlNodeStyles(html) {
return html.replace(/(<[^<>]+\s+style=")([^"]+)("\s*>)/g, (_, a, b, c) => {
let arr = b.match(/[^:]+:[^:]+;/g) || []
arr = arr.map(item => {
return item.trim()
})
arr.sort()
return a + arr.join('') + c
})
}
// 隐藏文本编辑控件,即完成编辑 // 隐藏文本编辑控件,即完成编辑
hideEditText(nodes) { hideEditText(nodes) {
if (!this.showTextEdit) { if (!this.showTextEdit) {
@ -395,12 +383,11 @@ class RichText {
if (typeof beforeHideRichTextEdit === 'function') { if (typeof beforeHideRichTextEdit === 'function') {
beforeHideRichTextEdit(this) beforeHideRichTextEdit(this)
} }
let html = this.getEditText() const html = this.getEditText()
html = this.sortHtmlNodeStyles(html)
const list = nodes && nodes.length > 0 ? nodes : [this.node] const list = nodes && nodes.length > 0 ? nodes : [this.node]
const node = this.node const node = this.node
this.textEditNode.style.display = 'none' this.textEditNode.style.display = 'none'
this.showTextEdit = false this.setIsShowTextEdit(false)
this.mindMap.emit('rich_text_selection_change', false) this.mindMap.emit('rich_text_selection_change', false)
this.node = null this.node = null
this.isInserting = false this.isInserting = false
@ -482,7 +469,8 @@ class RichText {
'background', 'background',
'font', 'font',
'size', 'size',
'formula' 'formula',
'align'
], // 明确指定允许的格式,不包含有序列表,无序列表等 ], // 明确指定允许的格式,不包含有序列表,无序列表等
theme: 'snow' theme: 'snow'
}) })
@ -503,7 +491,10 @@ class RichText {
}) })
this.quill.on('selection-change', range => { this.quill.on('selection-change', range => {
// 刚创建的节点全选不需要显示操作条 // 刚创建的节点全选不需要显示操作条
if (this.isInserting) return if (this.isInserting) {
this.isInserting = false
return
}
this.lastRange = this.range this.lastRange = this.range
this.range = null this.range = null
if (range) { if (range) {
@ -536,18 +527,6 @@ class RichText {
} }
}) })
this.quill.on('text-change', () => { this.quill.on('text-change', () => {
let contents = this.quill.getContents()
let len = contents.ops.length
// 如果编辑过程中删除所有字符,那么会丢失主题的样式
if (len <= 0 || (len === 1 && contents.ops[0].insert === '\n')) {
this.lostStyle = true
// 需要删除节点的样式数据
this.syncFormatToNodeConfig(null, true)
} else if (this.lostStyle && !this.isCompositing) {
// 如果处于样式丢失状态,那么需要进行格式化加回样式
this.setTextStyleIfNotRichText(this.node)
this.lostStyle = false
}
this.mindMap.emit('node_text_edit_change', { this.mindMap.emit('node_text_edit_change', {
node: this.node, node: this.node,
text: this.getEditText(), text: this.getEditText(),
@ -638,10 +617,16 @@ class RichText {
return return
} }
this.isCompositing = false this.isCompositing = false
if (!this.lostStyle) {
return
} }
this.setTextStyleIfNotRichText(this.node)
// 设置文本编辑框是否处于显示状态
setIsShowTextEdit(val) {
this.showTextEdit = val
if (val) {
this.mindMap.keyCommand.stopCheckInSvg()
} else {
this.mindMap.keyCommand.recoveryCheckInSvg()
}
} }
// 选中全部 // 选中全部
@ -651,19 +636,28 @@ class RichText {
// 聚焦 // 聚焦
focus(start) { focus(start) {
let len = this.quill.getLength() const len = this.quill.getLength()
this.quill.setSelection(typeof start === 'number' ? start : len, len) this.quill.setSelection(typeof start === 'number' ? start : len, len)
} }
// 格式化当前选中的文本 // 格式化当前选中的文本
formatText(config = {}, clear = false, pure = false) { formatText(config = {}, clear = false) {
if (!this.range && !this.lastRange) return if (!this.range && !this.lastRange) return
if (!pure) this.syncFormatToNodeConfig(config, clear) const rangeLost = !this.range
let rangeLost = !this.range const range = rangeLost ? this.lastRange : this.range
let range = rangeLost ? this.lastRange : this.range if (clear) {
clear this.quill.removeFormat(range.index, range.length)
? this.quill.removeFormat(range.index, range.length) } else {
: this.quill.formatText(range.index, range.length, config) const { align, ...rest } = config
// 文本对齐需要对行进行格式化
if (align) {
this.quill.formatLine(range.index, range.length, 'align', align)
}
// 其他内容对文本
if (Object.keys(rest).length > 0) {
this.quill.formatText(range.index, range.length, rest)
}
}
if (rangeLost) { if (rangeLost) {
this.quill.setSelection(this.lastRange.index, this.lastRange.length) this.quill.setSelection(this.lastRange.index, this.lastRange.length)
} }
@ -671,56 +665,25 @@ class RichText {
// 清除当前选中文本的样式 // 清除当前选中文本的样式
removeFormat() { removeFormat() {
// 先移除全部样式
this.formatText({}, true) this.formatText({}, true)
// 再将样式恢复为当前主题改节点的默认样式
const style = {}
if (this.node) {
this.supportStyleProps.forEach(key => {
style[key] = this.node.style.merge(key)
})
}
const config = this.normalStyleToRichTextStyle(style)
this.formatText(config, false, true)
} }
// 格式化指定范围的文本 // 格式化指定范围的文本
formatRangeText(range, config = {}) { formatRangeText(range, config = {}) {
if (!range) return if (!range) return
this.syncFormatToNodeConfig(config)
this.quill.formatText(range.index, range.length, config) this.quill.formatText(range.index, range.length, config)
} }
// 格式化所有文本 // 格式化所有文本
formatAllText(config = {}) { formatAllText(config = {}) {
this.syncFormatToNodeConfig(config)
this.pureFormatAllText(config)
}
// 纯粹的格式化所有文本
pureFormatAllText(config = {}) {
this.quill.formatText(0, this.quill.getLength(), config) this.quill.formatText(0, this.quill.getLength(), config)
} }
// 同步格式化到节点样式配置
syncFormatToNodeConfig(config, clear) {
if (!this.node) return
if (clear) {
// 清除文本样式
this.supportStyleProps.forEach(prop => {
delete this.node.nodeData.data[prop]
})
} else {
let data = this.richTextStyleToNormalStyle(config)
this.mindMap.execCommand('SET_NODE_DATA', this.node, data)
}
}
// 将普通节点样式对象转换成富文本样式对象 // 将普通节点样式对象转换成富文本样式对象
normalStyleToRichTextStyle(style) { normalStyleToRichTextStyle(style) {
let config = {} const config = {}
Object.keys(style).forEach(prop => { Object.keys(style).forEach(prop => {
let value = style[prop] const value = style[prop]
switch (prop) { switch (prop) {
case 'fontFamily': case 'fontFamily':
config.font = value config.font = value
@ -741,6 +704,9 @@ class RichText {
case 'color': case 'color':
config.color = value config.color = value
break break
case 'textAlign':
config.align = value
break
default: default:
break break
} }
@ -750,9 +716,9 @@ class RichText {
// 将富文本样式对象转换成普通节点样式对象 // 将富文本样式对象转换成普通节点样式对象
richTextStyleToNormalStyle(config) { richTextStyleToNormalStyle(config) {
let data = {} const data = {}
Object.keys(config).forEach(prop => { Object.keys(config).forEach(prop => {
let value = config[prop] const value = config[prop]
switch (prop) { switch (prop) {
case 'font': case 'font':
data.fontFamily = value data.fontFamily = value
@ -775,6 +741,9 @@ class RichText {
case 'color': case 'color':
data.color = value data.color = value
break break
case 'align':
data.textAlign = value
break
default: default:
break break
} }
@ -787,39 +756,50 @@ class RichText {
const keys = Object.keys(obj) const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const key = keys[i] const key = keys[i]
if (this.supportStyleProps.includes(key)) { if (richTextSupportStyleList.includes(key)) {
return true return true
} }
} }
return false return false
} }
// 给未激活的节点设置富文本样式
setNotActiveNodeStyle(node, style) {
const config = this.normalStyleToRichTextStyle(style)
if (Object.keys(config).length > 0) {
this.showEditText({ node })
this.formatAllText(config)
this.hideEditText([node])
}
}
// 检查指定节点是否存在自定义的富文本样式 // 检查指定节点是否存在自定义的富文本样式
checkNodeHasCustomRichTextStyle(node) { checkNodeHasCustomRichTextStyle(node) {
const nodeData = node instanceof MindMapNode ? node.getData() : node const nodeData = node instanceof MindMapNode ? node.getData() : node
for (let i = 0; i < this.supportStyleProps.length; i++) { for (let i = 0; i < richTextSupportStyleList.length; i++) {
if (nodeData[this.supportStyleProps[i]] !== undefined) { if (nodeData[richTextSupportStyleList[i]] !== undefined) {
return true return true
} }
} }
return false return false
} }
// 转换数据后的渲染操作
afterHandleData() {
// 清空历史数据,并且触发数据变化
this.mindMap.command.clearHistory()
this.mindMap.command.addHistory()
this.mindMap.render()
}
// 插件实例化时处理思维导图数据,转换为富文本数据
handleDataToRichTextOnInit() {
// 处理数据,转成富文本格式
if (this.mindMap.renderer.renderTree) {
// 如果已经存在渲染树了,那么直接更新渲染树,并且触发重新渲染
this.handleSetData(this.mindMap.renderer.renderTree)
this.afterHandleData()
} else if (this.mindMap.opt.data) {
this.handleSetData(this.mindMap.opt.data)
}
}
// 将所有节点转换成非富文本节点 // 将所有节点转换成非富文本节点
transformAllNodesToNormalNode() { transformAllNodesToNormalNode() {
if (!this.mindMap.renderer.renderTree) return const renderTree = this.mindMap.renderer.renderTree
if (!renderTree) return
walk( walk(
this.mindMap.renderer.renderTree, renderTree,
null, null,
node => { node => {
if (node.data.richText) { if (node.data.richText) {
@ -840,25 +820,36 @@ class RichText {
0, 0,
0 0
) )
// 清空历史数据,并且触发数据变化 this.afterHandleData()
this.mindMap.command.clearHistory() }
this.mindMap.command.addHistory()
this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE) handleDataToRichText(data) {
const oldIsRichText = data.richText
data.richText = true
data.resetRichText = true
// 如果原本就是富文本,那么不能转换
if (!oldIsRichText) {
data.text = htmlEscape(data.text)
}
} }
// 处理导入数据 // 处理导入数据
handleSetData(data) { handleSetData(data) {
let walk = root => { if (!data) return
if (root.data && !root.data.richText) { // 短期处理,为了兼容老数据,长期会去除
root.data.richText = true const isOldRichTextVersion =
root.data.resetRichText = true !data.smmVersion || compareVersion(data.smmVersion, '0.13.0') === '<'
const walk = root => {
if (root.data && (!root.data.richText || isOldRichTextVersion)) {
this.handleDataToRichText(root.data)
} }
// 概要 // 概要
if (root.data) { if (root.data) {
const generalizationList = formatGetNodeGeneralization(root.data) const generalizationList = formatGetNodeGeneralization(root.data)
generalizationList.forEach(item => { generalizationList.forEach(item => {
item.richText = true if (!item.richText || isOldRichTextVersion) {
item.resetRichText = true this.handleDataToRichText(item)
}
}) })
} }
if (root.children && root.children.length > 0) { if (root.children && root.children.length > 0) {
@ -877,12 +868,14 @@ class RichText {
document.head.removeChild(this.styleEl) document.head.removeChild(this.styleEl)
this.unbindEvent() this.unbindEvent()
this.mindMap.removeAppendCss('richText') this.mindMap.removeAppendCss('richText')
this.mindMap.deleteEditNodeClass(RICH_TEXT_EDIT_WRAP)
} }
// 插件被卸载前做的事情 // 插件被卸载前做的事情
beforePluginDestroy() { beforePluginDestroy() {
document.head.removeChild(this.styleEl) document.head.removeChild(this.styleEl)
this.unbindEvent() this.unbindEvent()
this.mindMap.deleteEditNodeClass(RICH_TEXT_EDIT_WRAP)
} }
} }

View File

@ -107,6 +107,7 @@ class Search {
// 搜索匹配的节点 // 搜索匹配的节点
doSearch() { doSearch() {
this.clearHighlightOnReadonly()
this.updateMatchNodeList([]) this.updateMatchNodeList([])
this.currentIndex = -1 this.currentIndex = -1
const { isOnlySearchCurrentRenderNodes } = this.mindMap.opt const { isOnlySearchCurrentRenderNodes } = this.mindMap.opt
@ -174,19 +175,17 @@ class Search {
} }
} }
const { readonly } = this.mindMap.opt const { readonly } = this.mindMap.opt
// 只读模式下需要激活之前节点的高亮 // 只读模式下需要清除之前节点的高亮
if (readonly) { this.clearHighlightOnReadonly()
this.matchNodeList.forEach(node => {
if (this.isNodeInstance(node)) {
node.closeHighlight()
}
})
}
const currentNode = this.matchNodeList[this.currentIndex] const currentNode = this.matchNodeList[this.currentIndex]
this.notResetSearchText = true this.notResetSearchText = true
const uid = this.isNodeInstance(currentNode) const uid = this.isNodeInstance(currentNode)
? currentNode.getData('uid') ? currentNode.getData('uid')
: currentNode.data.uid : currentNode.data.uid
if (!uid) {
callback()
return
}
const targetNode = this.mindMap.renderer.findNodeByUid(uid) const targetNode = this.mindMap.renderer.findNodeByUid(uid)
this.mindMap.execCommand('GO_TARGET_NODE', uid, node => { this.mindMap.execCommand('GO_TARGET_NODE', uid, node => {
if (!this.isNodeInstance(currentNode)) { if (!this.isNodeInstance(currentNode)) {
@ -205,6 +204,18 @@ class Search {
}) })
} }
// 只读模式下清除现有匹配节点的高亮
clearHighlightOnReadonly() {
const { readonly } = this.mindMap.opt
if (readonly) {
this.matchNodeList.forEach(node => {
if (this.isNodeInstance(node)) {
node.closeHighlight()
}
})
}
}
// 定位到指定搜索结果索引的节点 // 定位到指定搜索结果索引的节点
jump(index, callback = () => {}) { jump(index, callback = () => {}) {
this.searchNext(callback, index) this.searchNext(callback, index)
@ -228,7 +239,7 @@ class Search {
const keep = replaceText.includes(this.searchText) const keep = replaceText.includes(this.searchText)
const text = this.getReplacedText(currentNode, this.searchText, replaceText) const text = this.getReplacedText(currentNode, this.searchText, replaceText)
this.notResetSearchText = true this.notResetSearchText = true
currentNode.setText(text, currentNode.getData('richText'), true) currentNode.setText(text, currentNode.getData('richText'))
if (keep) { if (keep) {
this.updateMatchNodeList(this.matchNodeList) this.updateMatchNodeList(this.matchNodeList)
return return
@ -258,18 +269,15 @@ class Search {
// 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的 // 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的
const keep = replaceText.includes(this.searchText) const keep = replaceText.includes(this.searchText)
this.notResetSearchText = true this.notResetSearchText = true
const hasRichTextPlugin = this.mindMap.renderer.hasRichTextPlugin()
this.matchNodeList.forEach(node => { this.matchNodeList.forEach(node => {
const text = this.getReplacedText(node, this.searchText, replaceText) const text = this.getReplacedText(node, this.searchText, replaceText)
if (this.isNodeInstance(node)) { if (this.isNodeInstance(node)) {
const data = { const data = {
text text
} }
if (hasRichTextPlugin) data.resetRichText = !!node.getData('richText')
this.mindMap.renderer.setNodeDataRender(node, data, true) this.mindMap.renderer.setNodeDataRender(node, data, true)
} else { } else {
node.data.text = text node.data.text = text
if (hasRichTextPlugin) node.data.resetRichText = !!node.data.richText
} }
}) })
this.mindMap.render() this.mindMap.render()
@ -289,7 +297,7 @@ class Search {
if (richText) { if (richText) {
return replaceHtmlText(text, searchText, replaceText) return replaceHtmlText(text, searchText, replaceText)
} else { } else {
return text.replaceAll(searchText, replaceText) return text.replace(new RegExp(searchText, 'g'), replaceText)
} }
} }

View File

@ -5,6 +5,8 @@ import {
selectAllInput selectAllInput
} from '../../utils/index' } from '../../utils/index'
const ASSOCIATIVE_LINE_TEXT_EDIT_WRAP = 'associative-line-text-edit-warp'
// 创建文字节点 // 创建文字节点
function createText(data) { function createText(data) {
let g = this.associativeLineDraw.group() let g = this.associativeLineDraw.group()
@ -43,7 +45,7 @@ function showEditTextBox(g) {
// 输入框元素没有创建过,则先创建 // 输入框元素没有创建过,则先创建
if (!this.textEditNode) { if (!this.textEditNode) {
this.textEditNode = document.createElement('div') this.textEditNode = document.createElement('div')
this.textEditNode.className = 'associative-line-text-edit-warp' this.textEditNode.className = ASSOCIATIVE_LINE_TEXT_EDIT_WRAP
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;` this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
this.textEditNode.setAttribute('contenteditable', true) this.textEditNode.setAttribute('contenteditable', true)
this.textEditNode.addEventListener('keyup', e => { this.textEditNode.addEventListener('keyup', e => {
@ -73,7 +75,7 @@ function showEditTextBox(g) {
this.textEditNode.innerHTML = textLines.join('<br>') this.textEditNode.innerHTML = textLines.join('<br>')
this.textEditNode.style.display = 'block' this.textEditNode.style.display = 'block'
this.updateTextEditBoxPos(g) this.updateTextEditBoxPos(g)
this.showTextEdit = true this.setIsShowTextEdit(true)
// 如果是默认文本要全选输入框 // 如果是默认文本要全选输入框
if (text === '' || text === defaultAssociativeLineText) { if (text === '' || text === defaultAssociativeLineText) {
selectAllInput(this.textEditNode) selectAllInput(this.textEditNode)
@ -83,6 +85,16 @@ function showEditTextBox(g) {
} }
} }
// 设置文本编辑框是否处于显示状态
function setIsShowTextEdit(val) {
this.showTextEdit = val
if (val) {
this.mindMap.keyCommand.stopCheckInSvg()
} else {
this.mindMap.keyCommand.recoveryCheckInSvg()
}
}
// 删除文本编辑框元素 // 删除文本编辑框元素
function removeTextEditEl() { function removeTextEditEl() {
if (!this.textEditNode) return if (!this.textEditNode) return
@ -124,7 +136,7 @@ function hideEditTextBox() {
}) })
this.textEditNode.style.display = 'none' this.textEditNode.style.display = 'none'
this.textEditNode.innerHTML = '' this.textEditNode.innerHTML = ''
this.showTextEdit = false this.setIsShowTextEdit(false)
this.renderText(str, path, text, node, toNode) this.renderText(str, path, text, node, toNode)
this.mindMap.emit('hide_text_edit') this.mindMap.emit('hide_text_edit')
} }
@ -192,6 +204,7 @@ export default {
styleText, styleText,
onScale, onScale,
showEditTextBox, showEditTextBox,
setIsShowTextEdit,
removeTextEditEl, removeTextEditEl,
hideEditTextBox, hideEditTextBox,
updateTextEditBoxPos, updateTextEditBoxPos,

Some files were not shown because too many files have changed in this diff Show More