Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eab537b2e | ||
|
|
31d71db611 | ||
|
|
cfe07aa32e | ||
|
|
b8ac079009 | ||
|
|
040ce6601b | ||
|
|
77dd62477e | ||
|
|
b831d95063 | ||
|
|
bff6024e2e | ||
|
|
919b1517d9 | ||
|
|
2cbd08c532 | ||
|
|
c26cc5af83 | ||
|
|
5e300c0320 | ||
|
|
714567a733 | ||
|
|
28d6bb7d90 | ||
|
|
ecb2fbab48 | ||
|
|
a8c13b8f9a | ||
|
|
475dd4754a | ||
|
|
8a59185156 | ||
|
|
80ca74e477 | ||
|
|
c67bebb384 | ||
|
|
3a9002821c | ||
|
|
d0b289ed28 | ||
|
|
84782f924b | ||
|
|
2dd3db4c9d | ||
|
|
eb61e24746 | ||
|
|
abb332fd46 | ||
|
|
0c4fadb211 | ||
|
|
cd361c1f6e | ||
|
|
e0dc13c9f8 | ||
|
|
670114d8d8 | ||
|
|
c5b5fd86de | ||
|
|
493e0da7ae | ||
|
|
896121f6b6 | ||
|
|
b79076baa3 | ||
|
|
715627727e | ||
|
|
5ed5f0ff0d | ||
|
|
c12189ca87 | ||
|
|
be38eb2ca6 | ||
|
|
e80890aa7e | ||
|
|
e0ca3a5d12 | ||
|
|
30404721fa | ||
|
|
c4565143e8 | ||
|
|
2b25e28cb8 | ||
|
|
b543cfde38 | ||
|
|
f36bcbe39a | ||
|
|
328aef5308 | ||
|
|
697cee0b46 | ||
|
|
0f9ae45784 | ||
|
|
dcf4234ce2 | ||
|
|
f9406011e2 | ||
|
|
7d4acd15d0 | ||
|
|
6d729c53ab | ||
|
|
08df73aec4 | ||
|
|
f9eff11a27 | ||
|
|
0146e43815 | ||
|
|
cd2d5943c2 | ||
|
|
121eba1799 | ||
|
|
978c088d95 | ||
|
|
bb4a07b151 | ||
|
|
7f0368c2c8 | ||
|
|
402e0908b0 | ||
|
|
428ac15499 | ||
|
|
0f9f057d65 | ||
|
|
a4fe5e7765 | ||
|
|
1b7aad3de2 | ||
|
|
866287402f | ||
|
|
3b10b2b229 | ||
|
|
9661aa55c5 | ||
|
|
94ed53b31f | ||
|
|
bd2cfda905 | ||
|
|
a0e54870b5 | ||
|
|
bb9dd123f1 | ||
|
|
e5ee2f19d1 | ||
|
|
87d1b95dd9 | ||
|
|
6f0face378 | ||
|
|
37eab5f084 | ||
|
|
92d01e510b | ||
|
|
324652b1ba | ||
|
|
43c41e7ed2 | ||
|
|
a55afdd252 | ||
|
|
8414d39c4c | ||
|
|
e53e41dadc | ||
|
|
0321946b41 | ||
|
|
6071c7e021 | ||
|
|
17e9c29f1d | ||
|
|
d486cbd157 | ||
|
|
70cc88efbd | ||
|
|
0f6f714303 | ||
|
|
45418d803c | ||
|
|
e5648728c4 | ||
|
|
c387d78bfe | ||
|
|
469f5b26cd | ||
|
|
53fecc062f | ||
|
|
ec9f55e068 | ||
|
|
76b5f7d22a | ||
|
|
3b9cced7ea | ||
|
|
c7cd19f956 | ||
|
|
2a8959fb3b | ||
|
|
b836ca87b2 | ||
|
|
f1a97e4ced | ||
|
|
3cb035e365 | ||
|
|
2001bdd3ff | ||
|
|
10e9fa3f22 | ||
|
|
cbd57d2f36 | ||
|
|
cef586cc5c | ||
|
|
b0c0c58bec | ||
|
|
ed2fed78f7 | ||
|
|
75322ddc20 | ||
|
|
2577da10d0 | ||
|
|
4f2d4f8e36 | ||
|
|
6ec552d9fb | ||
|
|
0ecae72fff | ||
|
|
91cdb24a62 | ||
|
|
f8149ce383 | ||
|
|
b834b6fdd7 | ||
|
|
fe11d1152e | ||
|
|
c27ca12489 | ||
|
|
e05df4e92b | ||
|
|
ad63b4c72c | ||
|
|
09e393b174 | ||
|
|
43c7f0551a | ||
|
|
d0253ecf6c | ||
|
|
063742cb9b | ||
|
|
0a8b14ddd8 | ||
|
|
1bdbe0881e | ||
|
|
bd04516a7c | ||
|
|
75742bef27 | ||
|
|
31070c95fb |
133
README.md
@ -9,25 +9,36 @@
|
||||
|
||||
> 中文名:思绪思维导图。一个简单&强大的 Web 思维导图库和思维导图软件。
|
||||
|
||||
本项目主要包含以下内容:
|
||||
# 客户端和插件
|
||||
|
||||
1.一个 js 思维导图库,不依赖任何框架,可以使用它来快速完成 Web 思维导图产品的开发。
|
||||
- 思绪思维导图客户端
|
||||
|
||||
开发文档:[https://wanglin2.github.io/mind-map-docs/](https://wanglin2.github.io/mind-map-docs/)。
|
||||
支持Windows、Mac及Linux系统。下载地址:[Github](https://github.com/wanglin2/mind-map/releases)、[百度网盘](https://pan.baidu.com/s/1C8phEJ5pagAAa-o1tU42Uw?pwd=jqfb)、[夸克网盘](https://pan.quark.cn/s/2733982f1976)
|
||||
|
||||
2.一个 Web 思维导图,基于思维导图库、Vue2.x、ElementUI 开发,可以操作电脑本地文件,可以当做一个在线版思维导图应用使用,也可以自部署和二次开发。
|
||||
> 如果在macOS上安装后无法打开,报错**不受信任**或者**移到垃圾箱**,执行下面命令后再启动即可:
|
||||
> ``` shell
|
||||
> sudo xattr -d com.apple.quarantine /Applications/思绪思维导图.app
|
||||
> ```
|
||||
|
||||
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)。
|
||||
- Obsidian插件
|
||||
|
||||
3.此外也支持以客户端的方式使用,现已上架[uTools](https://www.u.tools/)插件应用市场,强烈建议通过`uTools`来体验。
|
||||
下载地址:[Github](https://github.com/wanglin2/obsidian-simplemindmap/releases)
|
||||
|
||||
可直接在`uTools`插件应用市场中搜索`思绪`进行安装,也可以直接访问该地址:[在utools中搜索](https://www.u-tools.cn/plugins/search/?t=%E6%80%9D%E7%BB%AA%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE),点击右侧的【启动】按钮进行安装。
|
||||
- UTools插件
|
||||
|
||||
> 独立客户端下载:Github:[releases](https://github.com/wanglin2/mind-map/releases)。百度云盘:[地址](https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3)。
|
||||
>
|
||||
> 后续不会投入太多精力在独立客户端上,建议通过`uTools`来使用,功能更强,体验更好。
|
||||
已上架[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/),点击右侧的【启动】按钮进行安装
|
||||
|
||||
4.【云存储版本】如果你需要带后端的云存储版本,可以尝试我们开发的另一个项目[理想文档](https://github.com/wanglin2/lx-doc)。
|
||||
# 库
|
||||
|
||||
- 一个 `js` 思维导图库,不依赖任何框架,可以用来快速完成 Web 思维导图产品的开发。
|
||||
|
||||
> 开发文档:[https://wanglin2.github.io/mind-map-docs/](https://wanglin2.github.io/mind-map-docs/)
|
||||
|
||||
- 一个 Web 思维导图,基于思维导图库、`Vue2.x`、`ElementUI` 开发,支持操作电脑本地文件,可以当做一个在线版思维导图应用使用,也可以自部署和二次开发。
|
||||
|
||||
> 在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)
|
||||
|
||||
- 云存储版本,如果你需要带后端的云存储版本,可以尝试我们开发的另一个项目[理想文档](https://github.com/wanglin2/lx-doc)。
|
||||
|
||||
# 特性
|
||||
|
||||
@ -43,10 +54,17 @@
|
||||
- [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(节点编号插件)[收费]、Freemind(Freemind格式导入导出插件)[收费]、Excel(Excel格式导入导出插件)[收费]、Checkbox(待办插件)[收费]、Lineflow(节点连线流动插件)[收费]
|
||||
| RichText(节点富文本插件) | Select(鼠标多选节点插件) | Drag(节点拖拽插件) | AssociativeLine(关联线插件) |
|
||||
| ------------------------------------ | ----------------------------------------- | ------------------------------------ | ------------------------------------ |
|
||||
| Export(导出插件) | KeyboardNavigation(键盘导航插件) | MiniMap(小地图插件) | Watermark(水印插件) |
|
||||
| TouchEvent(移动端触摸事件支持插件) | NodeImgAdjust(拖拽调整节点图片大小插件) | Search(搜索插件) | Painter(节点格式刷插件) |
|
||||
| Scrollbar(滚动条插件) | Formula(数学公式插件) | Cooperate(协同编辑插件) | RainbowLines(彩虹线条插件) |
|
||||
| Demonstrate(演示模式插件) | OuterFrame(外框插件) | MindMapLayoutPro(思维导图布局插件) | |
|
||||
|
||||
|
||||
本项目不会实现的特性:
|
||||
|
||||
@ -99,13 +117,15 @@ const mindMap = new MindMap({
|
||||
|
||||
# License
|
||||
|
||||
[MIT](./LICENSE)。保留`mind-map`版权声明和注明来源的情况下可随意商用,如有疑问或不想保留可联系作者。
|
||||
[MIT](./LICENSE)。保留`simple-mind-map`版权声明和注明来源的情况下可随意商用,如有疑问或不想保留可联系作者(微信:wanglinguanfang)通过付费的方式去除。
|
||||
|
||||
# 微信交流群
|
||||
> 示例:可以在你应用中的关于页面、帮助页面、文档页面、开源声明等任何页面添加以下内容:
|
||||
>
|
||||
> 本产品思维导图基于SimpleMindMap项目开发,版权归源项目所有,[开源协议](https://github.com/wanglin2/mind-map/blob/main/LICENSE)。
|
||||
|
||||
微信添加`wanglinguanfang`拉你入群。根据过往的经验,大部分问题都可以通过查看issue列表或文档解决,所以提问前请确保你已经阅读完了所有文档,文档里没有的可在群里提问,不必私聊作者,如果你一定要私聊,请先发红包(¥9.9+每次)。
|
||||
# 开发帮助/技术支持/咨询等
|
||||
|
||||
如果你在杭州,也欢迎来找我面基。
|
||||
因精力有限,及重心转变,暂不提供任何开发支持(包括有偿),请见谅!
|
||||
|
||||
# star
|
||||
|
||||
@ -131,20 +151,21 @@ const mindMap = new MindMap({
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
# 请作者喝杯咖啡
|
||||
# 感谢赞赏过本项目的人
|
||||
|
||||
开源不易,如果本项目有帮助到你的话,可以考虑请作者喝杯咖啡~你的赞助对项目的可持续发展非常重要,是作者持续维护的最大动力。
|
||||
## 最强王者
|
||||
|
||||
> 推荐使用支付宝,微信获取不到头像。转账请备注【思维导图】。
|
||||
>
|
||||
> 也可以通过购买付费插件来支持我们:[付费插件](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>
|
||||
<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>
|
||||
|
||||
## 钻石赞助
|
||||
|
||||
@ -164,6 +185,13 @@ const mindMap = new MindMap({
|
||||
<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>
|
||||
|
||||
@ -336,6 +364,20 @@ const mindMap = new MindMap({
|
||||
<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>
|
||||
|
||||
@ -947,5 +989,40 @@ const mindMap = new MindMap({
|
||||
<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>
|
||||
2
copy.js
@ -13,4 +13,4 @@ if (fs.existsSync(src)) {
|
||||
fs.unlinkSync(src)
|
||||
}
|
||||
|
||||
console.warn('请检查付费插件是否启用!!!')
|
||||
// console.warn('请检查付费插件是否启用!!!')
|
||||
2
dist/css/app.css
vendored
@ -1 +1 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#2c3e50}.customScrollbar{&::-webkit-scrollbar{width:7px;height:7px}&::-webkit-scrollbar-thumb{border-radius:7px;background-color:rgba(0,0,0,.3);cursor:pointer}&::-webkit-scrollbar-track{box-shadow:none;background:transparent;display:none}}@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}.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"}
|
||||
*{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"}
|
||||
BIN
dist/fonts/iconfont.ttf
vendored
BIN
dist/fonts/iconfont.woff
vendored
BIN
dist/fonts/iconfont.woff2
vendored
BIN
dist/img/catalogOrganization.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
dist/img/catalogOrganization.png
vendored
|
Before Width: | Height: | Size: 6.6 KiB |
BIN
dist/img/fishbone.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
dist/img/fishbone.png
vendored
|
Before Width: | Height: | Size: 7.1 KiB |
BIN
dist/img/fishbone2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
dist/img/logicalStructure.jpg
vendored
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
dist/img/logicalStructure.png
vendored
|
Before Width: | Height: | Size: 5.9 KiB |
BIN
dist/img/logicalStructureLeft.jpg
vendored
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
dist/img/mindMap.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
dist/img/mindMap.png
vendored
|
Before Width: | Height: | Size: 7.0 KiB |
BIN
dist/img/organizationStructure.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
dist/img/organizationStructure.png
vendored
|
Before Width: | Height: | Size: 7.1 KiB |
BIN
dist/img/rightFishbone.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
dist/img/rightFishbone2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
dist/img/timeline.jpg
vendored
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
dist/img/timeline.png
vendored
|
Before Width: | Height: | Size: 6.2 KiB |
BIN
dist/img/timeline2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
dist/img/timeline2.png
vendored
|
Before Width: | Height: | Size: 6.6 KiB |
BIN
dist/img/verticalTimeline.jpg
vendored
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
dist/img/verticalTimeline.png
vendored
|
Before Width: | Height: | Size: 7.2 KiB |
BIN
dist/img/verticalTimeline2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
dist/img/verticalTimeline3.jpg
vendored
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
2
dist/js/app.js
vendored
65
dist/js/chunk-183b683c.js
vendored
Normal file
69
dist/js/chunk-2b84b760.js
vendored
14
dist/js/chunk-vendors.js
vendored
13
index.html
@ -9,7 +9,7 @@
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}</script><link href="dist/css/chunk-vendors.css?81d94c3507392bec62bc" rel="stylesheet"><link href="dist/css/app.css?81d94c3507392bec62bc" 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) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
@ -28,6 +28,7 @@
|
||||
config: {},
|
||||
view: null
|
||||
},
|
||||
mindMapConfig: {},
|
||||
lang: 'zh',
|
||||
localConfig: null
|
||||
})
|
||||
@ -44,6 +45,14 @@
|
||||
window.takeOverAppMethods.saveMindMapData = data => {
|
||||
console.log(data)
|
||||
}
|
||||
// 获取思维导图配置,也就是实例化时会传入的选项
|
||||
window.takeOverAppMethods.getMindMapConfig = () => {
|
||||
return data.mindMapConfig
|
||||
}
|
||||
// 保存思维导图配置
|
||||
window.takeOverAppMethods.saveMindMapConfig = config => {
|
||||
console.log(config)
|
||||
}
|
||||
// 获取语言的函数
|
||||
window.takeOverAppMethods.getLanguage = () => {
|
||||
return data.lang
|
||||
@ -74,4 +83,4 @@
|
||||
// 可以通过window.$bus.$on()来监听应用的一些事件
|
||||
// 实例化页面
|
||||
window.initApp()
|
||||
}</script><script src="dist/js/chunk-vendors.js?81d94c3507392bec62bc"></script><script src="dist/js/app.js?81d94c3507392bec62bc"></script></body></html>
|
||||
}</script><script src="dist/js/chunk-vendors.js?227f61428db154a5d9bc"></script><script src="dist/js/app.js?227f61428db154a5d9bc"></script></body></html>
|
||||
@ -17,11 +17,7 @@ const createFullData = () => {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: 王林
|
||||
* @Date: 2021-04-15 22:23:24
|
||||
* @Desc: 节点较多示例数据
|
||||
*/
|
||||
// 节点较多示例数据
|
||||
const data1 = {
|
||||
"root": {
|
||||
"data": {
|
||||
@ -936,6 +932,5 @@ export default {
|
||||
"layout": "logicalStructure",
|
||||
// "layout": "mindMap",
|
||||
// "layout": "catalogOrganization"
|
||||
// "layout": "organizationStructure",
|
||||
"config": {}
|
||||
// "layout": "organizationStructure"
|
||||
}
|
||||
@ -19,6 +19,7 @@ import RainbowLines from './src/plugins/RainbowLines.js'
|
||||
import Demonstrate from './src/plugins/Demonstrate.js'
|
||||
import OuterFrame from './src/plugins/OuterFrame.js'
|
||||
import MindMapLayoutPro from './src/plugins/MindMapLayoutPro.js'
|
||||
import NodeBase64ImageStorage from './src/plugins/NodeBase64ImageStorage.js'
|
||||
import xmind from './src/parse/xmind.js'
|
||||
import markdown from './src/parse/markdown.js'
|
||||
import icons from './src/svg/icons.js'
|
||||
@ -30,7 +31,7 @@ MindMap.markdown = markdown
|
||||
MindMap.iconList = icons.nodeIconList
|
||||
MindMap.constants = constants
|
||||
MindMap.defaultTheme = defaultTheme
|
||||
MindMap.version = '0.13.1'
|
||||
MindMap.version = '0.14.0-fix.1'
|
||||
|
||||
MindMap.usePlugin(MiniMap)
|
||||
.usePlugin(Watermark)
|
||||
@ -52,5 +53,6 @@ MindMap.usePlugin(MiniMap)
|
||||
.usePlugin(Demonstrate)
|
||||
.usePlugin(OuterFrame)
|
||||
.usePlugin(MindMapLayoutPro)
|
||||
.usePlugin(NodeBase64ImageStorage)
|
||||
|
||||
export default MindMap
|
||||
|
||||
@ -11,9 +11,10 @@ import {
|
||||
layoutValueList,
|
||||
CONSTANTS,
|
||||
ERROR_TYPES,
|
||||
cssContent
|
||||
cssContent,
|
||||
nodeDataNoStylePropList
|
||||
} from './src/constants/constant'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import { SVG, G, Rect } from '@svgdotjs/svg.js'
|
||||
import {
|
||||
simpleDeepClone,
|
||||
getObjectChangedProps,
|
||||
@ -57,7 +58,7 @@ class MindMap {
|
||||
this.cssEl = null
|
||||
this.cssTextMap = {} // 该样式在实例化时会动态添加到页面,同时导出为svg时也会添加到svg源码中
|
||||
|
||||
// 节点前置内容列表
|
||||
// 节点前置/后置内容列表
|
||||
/*
|
||||
{
|
||||
name: '',// 一个唯一的类型标识
|
||||
@ -76,6 +77,27 @@ class MindMap {
|
||||
}
|
||||
*/
|
||||
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()
|
||||
@ -86,6 +108,15 @@ class MindMap {
|
||||
// 初始化缓存数据
|
||||
this.initCache()
|
||||
|
||||
// 注册插件
|
||||
MindMap.pluginList
|
||||
.filter(plugin => {
|
||||
return plugin.preload
|
||||
})
|
||||
.forEach(plugin => {
|
||||
this.initPlugin(plugin)
|
||||
})
|
||||
|
||||
// 事件类
|
||||
this.event = new Event({
|
||||
mindMap: this
|
||||
@ -115,9 +146,13 @@ class MindMap {
|
||||
this.batchExecution = new BatchExecution()
|
||||
|
||||
// 注册插件
|
||||
MindMap.pluginList.forEach(plugin => {
|
||||
this.initPlugin(plugin)
|
||||
})
|
||||
MindMap.pluginList
|
||||
.filter(plugin => {
|
||||
return !plugin.preload
|
||||
})
|
||||
.forEach(plugin => {
|
||||
this.initPlugin(plugin)
|
||||
})
|
||||
|
||||
// 添加必要的css样式
|
||||
this.addCss()
|
||||
@ -240,12 +275,33 @@ class MindMap {
|
||||
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 = '') {
|
||||
this.batchExecution.push('render', () => {
|
||||
this.initTheme()
|
||||
this.renderer.render(callback, source)
|
||||
})
|
||||
this.initTheme()
|
||||
this.renderer.render(callback, source)
|
||||
}
|
||||
|
||||
// 重新渲染
|
||||
@ -415,7 +471,7 @@ class MindMap {
|
||||
this.command.clearHistory()
|
||||
this.command.addHistory()
|
||||
this.renderer.setData(data)
|
||||
this.reRender(() => {}, CONSTANTS.SET_DATA)
|
||||
this.reRender()
|
||||
this.emit('set_data', data)
|
||||
}
|
||||
|
||||
@ -587,7 +643,7 @@ class MindMap {
|
||||
this.watermark.isInExport = false
|
||||
}
|
||||
// 添加必要的样式
|
||||
;[this.joinCss(), ...cssTextList].forEach(s => {
|
||||
[this.joinCss(), ...cssTextList].forEach(s => {
|
||||
clone.add(SVG(`<style>${s}</style>`))
|
||||
})
|
||||
// 附加内容
|
||||
@ -636,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) {
|
||||
let index = MindMap.hasPlugin(plugin)
|
||||
@ -699,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.usePlugin = (plugin, opt = {}) => {
|
||||
|
||||
4
simple-mind-map/package-lock.json
generated
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.12.1",
|
||||
"version": "0.14.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.12.1",
|
||||
"version": "0.14.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "3.2.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simple-mind-map",
|
||||
"version": "0.13.1",
|
||||
"version": "0.14.0-fix.1",
|
||||
"description": "一个简单的web在线思维导图",
|
||||
"authors": [
|
||||
{
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
export const CONSTANTS = {
|
||||
CHANGE_THEME: 'changeTheme',
|
||||
CHANGE_LAYOUT: 'changeLayout',
|
||||
SET_DATA: 'setData',
|
||||
MODE: {
|
||||
READONLY: 'readonly',
|
||||
EDIT: 'edit'
|
||||
@ -16,7 +15,12 @@ export const CONSTANTS = {
|
||||
TIMELINE: 'timeline',
|
||||
TIMELINE2: 'timeline2',
|
||||
FISHBONE: 'fishbone',
|
||||
VERTICAL_TIMELINE: 'verticalTimeline'
|
||||
FISHBONE2: 'fishbone2',
|
||||
RIGHT_FISHBONE: 'rightFishbone',
|
||||
RIGHT_FISHBONE2: 'rightFishbone2',
|
||||
VERTICAL_TIMELINE: 'verticalTimeline',
|
||||
VERTICAL_TIMELINE2: 'verticalTimeline2',
|
||||
VERTICAL_TIMELINE3: 'verticalTimeline3'
|
||||
},
|
||||
DIR: {
|
||||
UP: 'up',
|
||||
@ -80,11 +84,6 @@ export const CONSTANTS = {
|
||||
TOP: 'top',
|
||||
RIGHT: 'right',
|
||||
BOTTOM: 'bottom'
|
||||
},
|
||||
EDIT_NODE_CLASS: {
|
||||
SMM_NODE_EDIT_WRAP: 'smm-node-edit-wrap',
|
||||
RICH_TEXT_EDIT_WRAP: 'ql-editor',
|
||||
ASSOCIATIVE_LINE_TEXT_EDIT_WRAP: 'associative-line-text-edit-warp'
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,9 +129,29 @@ export const layoutList = [
|
||||
name: '竖向时间轴',
|
||||
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE
|
||||
},
|
||||
{
|
||||
name: '竖向时间轴2',
|
||||
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE2
|
||||
},
|
||||
{
|
||||
name: '竖向时间轴3',
|
||||
value: CONSTANTS.LAYOUT.VERTICAL_TIMELINE3
|
||||
},
|
||||
{
|
||||
name: '鱼骨图',
|
||||
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 = [
|
||||
@ -144,7 +163,12 @@ export const layoutValueList = [
|
||||
CONSTANTS.LAYOUT.TIMELINE,
|
||||
CONSTANTS.LAYOUT.TIMELINE2,
|
||||
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
|
||||
]
|
||||
|
||||
// 节点数据中非样式的字段
|
||||
@ -162,7 +186,7 @@ export const nodeDataNoStylePropList = [
|
||||
'isActive',
|
||||
'generalization',
|
||||
'richText',
|
||||
'resetRichText',// 重新创建富文本内容,去掉原有样式
|
||||
'resetRichText', // 重新创建富文本内容,去掉原有样式
|
||||
'uid',
|
||||
'activeStyle',
|
||||
'associativeLineTargets',
|
||||
@ -180,7 +204,9 @@ export const nodeDataNoStylePropList = [
|
||||
'customTextWidth',
|
||||
'checkbox',
|
||||
'dir',
|
||||
'needUpdate'// 重新创建节点内容
|
||||
'needUpdate', // 重新创建节点内容
|
||||
'imgMap',
|
||||
'nodeLink'
|
||||
]
|
||||
|
||||
// 错误类型
|
||||
|
||||
@ -496,6 +496,7 @@ export const defaultOpt = {
|
||||
// 【OuterFrame】插件
|
||||
outerFramePaddingX: 10,
|
||||
outerFramePaddingY: 10,
|
||||
defaultOuterFrameText: '外框',
|
||||
|
||||
// 【Painter】插件
|
||||
// 是否只格式刷节点手动设置的样式,不考虑节点通过主题的应用的样式
|
||||
|
||||
@ -15,7 +15,7 @@ class Command {
|
||||
this.opt = opt
|
||||
this.mindMap = opt.mindMap
|
||||
this.commands = {}
|
||||
this.history = []
|
||||
this.history = [] // 字符串形式存储
|
||||
this.activeHistoryIndex = 0
|
||||
// 注册快捷键
|
||||
this.registerShortcutKeys()
|
||||
@ -106,18 +106,19 @@ class Command {
|
||||
if (this.mindMap.opt.readonly || this.isPause) {
|
||||
return
|
||||
}
|
||||
const lastData =
|
||||
this.mindMap.emit('beforeAddHistory')
|
||||
const lastDataStr =
|
||||
this.history.length > 0 ? this.history[this.activeHistoryIndex] : null
|
||||
const data = this.getCopyData()
|
||||
const dataStr = JSON.stringify(data)
|
||||
// 此次数据和上次一样则不重复添加
|
||||
if (lastData === data) return
|
||||
if (lastData && JSON.stringify(lastData) === JSON.stringify(data)) {
|
||||
if (lastDataStr && lastDataStr === dataStr) {
|
||||
return
|
||||
}
|
||||
this.emitDataUpdatesEvent(lastData, data)
|
||||
this.emitDataUpdatesEvent(lastDataStr, dataStr)
|
||||
// 删除当前历史指针后面的数据
|
||||
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) {
|
||||
this.history.shift()
|
||||
@ -137,15 +138,16 @@ class Command {
|
||||
return
|
||||
}
|
||||
if (this.activeHistoryIndex - step >= 0) {
|
||||
const lastData = this.history[this.activeHistoryIndex]
|
||||
const lastDataStr = this.history[this.activeHistoryIndex]
|
||||
this.activeHistoryIndex -= step
|
||||
this.mindMap.emit(
|
||||
'back_forward',
|
||||
this.activeHistoryIndex,
|
||||
this.history.length
|
||||
)
|
||||
const data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.emitDataUpdatesEvent(lastData, data)
|
||||
const dataStr = this.history[this.activeHistoryIndex]
|
||||
const data = JSON.parse(dataStr)
|
||||
this.emitDataUpdatesEvent(lastDataStr, dataStr)
|
||||
return data
|
||||
}
|
||||
}
|
||||
@ -157,15 +159,16 @@ class Command {
|
||||
}
|
||||
let len = this.history.length
|
||||
if (this.activeHistoryIndex + step <= len - 1) {
|
||||
const lastData = this.history[this.activeHistoryIndex]
|
||||
const lastDataStr = this.history[this.activeHistoryIndex]
|
||||
this.activeHistoryIndex += step
|
||||
this.mindMap.emit(
|
||||
'back_forward',
|
||||
this.activeHistoryIndex,
|
||||
this.history.length
|
||||
)
|
||||
const data = simpleDeepClone(this.history[this.activeHistoryIndex])
|
||||
this.emitDataUpdatesEvent(lastData, data)
|
||||
const dataStr = this.history[this.activeHistoryIndex]
|
||||
const data = JSON.parse(dataStr)
|
||||
this.emitDataUpdatesEvent(lastDataStr, dataStr)
|
||||
return data
|
||||
}
|
||||
}
|
||||
@ -194,12 +197,14 @@ class Command {
|
||||
}
|
||||
|
||||
// 派发思维导图更新明细事件
|
||||
emitDataUpdatesEvent(lastData, data) {
|
||||
emitDataUpdatesEvent(lastDataStr, dataStr) {
|
||||
try {
|
||||
// 如果data_change_detail没有监听者,那么不进行计算,节省性能
|
||||
const eventName = 'data_change_detail'
|
||||
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 dataObj = simpleDeepClone(transformTreeDataToObject(data))
|
||||
const res = []
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { keyMap } from './keyMap'
|
||||
import { CONSTANTS } from '../../constants/constant'
|
||||
|
||||
// 快捷按键、命令处理类
|
||||
export default class KeyCommand {
|
||||
@ -13,6 +12,8 @@ export default class KeyCommand {
|
||||
this.shortcutMapCache = {}
|
||||
this.isPause = false
|
||||
this.isInSvg = false
|
||||
this.isStopCheckInSvg = false
|
||||
this.defaultEnableCheck = this.defaultEnableCheck.bind(this)
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@ -58,6 +59,22 @@ export default class KeyCommand {
|
||||
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() {
|
||||
this.onKeydown = this.onKeydown.bind(this)
|
||||
@ -66,13 +83,6 @@ export default class KeyCommand {
|
||||
this.isInSvg = true
|
||||
})
|
||||
this.mindMap.on('svg_mouseleave', () => {
|
||||
if (this.mindMap.renderer.textEdit.isShowTextEdit()) return
|
||||
if (
|
||||
this.mindMap.associativeLine &&
|
||||
this.mindMap.associativeLine.showTextEdit
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.isInSvg = false
|
||||
})
|
||||
window.addEventListener('keydown', this.onKeydown)
|
||||
@ -89,12 +99,14 @@ export default class KeyCommand {
|
||||
// 根据事件目标判断是否响应快捷键事件
|
||||
defaultEnableCheck(e) {
|
||||
const target = e.target
|
||||
return (
|
||||
target === document.body ||
|
||||
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.SMM_NODE_EDIT_WRAP) ||
|
||||
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP) ||
|
||||
target.classList.contains(CONSTANTS.EDIT_NODE_CLASS.ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
||||
)
|
||||
if (target === document.body) return true
|
||||
for (let i = 0; i < this.mindMap.editNodeClassList.length; i++) {
|
||||
const cur = this.mindMap.editNodeClassList[i]
|
||||
if (target.classList.contains(cur)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 按键事件
|
||||
@ -109,7 +121,12 @@ export default class KeyCommand {
|
||||
? customCheckEnableShortcut
|
||||
: this.defaultEnableCheck
|
||||
if (!checkFn(e)) return
|
||||
if (this.isPause || (enableShortcutOnlyWhenMouseInSvg && !this.isInSvg)) {
|
||||
if (
|
||||
this.isPause ||
|
||||
(enableShortcutOnlyWhenMouseInSvg &&
|
||||
!this.isStopCheckInSvg &&
|
||||
!this.isInSvg)
|
||||
) {
|
||||
return
|
||||
}
|
||||
Object.keys(this.shortcutMap).forEach(key => {
|
||||
|
||||
@ -60,8 +60,14 @@ const layouts = {
|
||||
[CONSTANTS.LAYOUT.TIMELINE2]: Timeline,
|
||||
// 竖向时间轴
|
||||
[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.hasWaitRendering = false
|
||||
this.waitRenderingParams = []
|
||||
// 用于缓存节点
|
||||
this.nodeCache = {}
|
||||
this.lastNodeCache = {}
|
||||
// 触发render的来源
|
||||
this.renderSource = ''
|
||||
// 收集触发render的来源
|
||||
this.renderSourceList = []
|
||||
// 收集render的回调函数
|
||||
this.renderCallbackList = []
|
||||
// 当前激活的节点列表
|
||||
this.activeNodeList = []
|
||||
// 防抖定时器
|
||||
this.emitNodeActiveEventTimer = null
|
||||
this.renderTimer = null
|
||||
// 根节点
|
||||
this.root = null
|
||||
// 文本编辑框,需要再bindEvent之前实例化,否则单击事件只能触发隐藏文本编辑框,而无法保存文本修改
|
||||
@ -112,12 +122,16 @@ class Render {
|
||||
|
||||
// 设置布局结构
|
||||
setLayout() {
|
||||
if (this.layout && this.layout.beforeChange) {
|
||||
this.layout.beforeChange()
|
||||
}
|
||||
const { layout } = this.mindMap.opt
|
||||
this.layout = new (
|
||||
layouts[layout]
|
||||
? layouts[layout]
|
||||
: layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE]
|
||||
)(this, layout)
|
||||
let L = layouts[layout] || this.mindMap[layout]
|
||||
if (!L) {
|
||||
L = layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE]
|
||||
this.mindMap.opt.layout = CONSTANTS.LAYOUT.LOGICAL_STRUCTURE
|
||||
}
|
||||
this.layout = new L(this, layout)
|
||||
}
|
||||
|
||||
// 重新设置思维导图数据
|
||||
@ -147,6 +161,9 @@ class Render {
|
||||
})
|
||||
// 性能模式
|
||||
const onViewDataChange = throttle(() => {
|
||||
if (!this.renderTree) {
|
||||
return
|
||||
}
|
||||
if (this.root) {
|
||||
this.mindMap.emit('node_tree_render_start')
|
||||
this.root.render(
|
||||
@ -443,9 +460,10 @@ class Render {
|
||||
)
|
||||
if (!isChange) return
|
||||
this.lastActiveNodeList = [...activeNodeList]
|
||||
this.mindMap.batchExecution.push('emitNodeActiveEvent', () => {
|
||||
clearTimeout(this.emitNodeActiveEventTimer)
|
||||
this.emitNodeActiveEventTimer = setTimeout(() => {
|
||||
this.mindMap.emit('node_active', node, activeNodeList)
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 鼠标点击画布时清空当前激活节点列表
|
||||
@ -488,22 +506,71 @@ class Render {
|
||||
this.lastNodeCache = {}
|
||||
}
|
||||
|
||||
// 渲染
|
||||
render(callback = () => {}, source) {
|
||||
// 保存触发渲染的参数
|
||||
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) {
|
||||
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()
|
||||
}
|
||||
// 如果当前还没有渲染完毕,不再触发渲染
|
||||
if (this.isRendering) {
|
||||
// 等待当前渲染完毕后再进行一次渲染
|
||||
this.hasWaitRendering = true
|
||||
this.waitRenderingParams = [callback, source]
|
||||
return
|
||||
}
|
||||
this.isRendering = true
|
||||
// 触发当前重新渲染的来源
|
||||
this.renderSource = source
|
||||
// 节点缓存
|
||||
this.lastNodeCache = this.nodeCache
|
||||
this.nodeCache = {}
|
||||
@ -513,8 +580,7 @@ class Render {
|
||||
}
|
||||
// 如果没有节点数据
|
||||
if (!this.renderTree) {
|
||||
this.isRendering = false
|
||||
this.mindMap.emit('node_tree_render_end')
|
||||
this.onRenderEnd()
|
||||
return
|
||||
}
|
||||
this.mindMap.emit('node_tree_render_start')
|
||||
@ -536,24 +602,24 @@ class Render {
|
||||
// 渲染节点
|
||||
this.root.render(() => {
|
||||
this.isRendering = false
|
||||
callback && callback()
|
||||
if (this.hasWaitRendering) {
|
||||
const params = this.waitRenderingParams
|
||||
this.hasWaitRendering = false
|
||||
this.waitRenderingParams = []
|
||||
this.render(...params)
|
||||
} else {
|
||||
this.renderSource = ''
|
||||
if (this.reRender) {
|
||||
this.reRender = false
|
||||
}
|
||||
this.render()
|
||||
return
|
||||
}
|
||||
this.mindMap.emit('node_tree_render_end')
|
||||
this.onRenderEnd()
|
||||
})
|
||||
})
|
||||
this.emitNodeActiveEvent()
|
||||
}
|
||||
|
||||
// 当某个自定义节点内容改变后,可以调用该方法实时更新该节点大小和整体节点的定位
|
||||
renderByCustomNodeContentNode(node) {
|
||||
node.getSize()
|
||||
node.customNodeContentRealtimeLayout()
|
||||
this.mindMap.render()
|
||||
}
|
||||
|
||||
// 给当前被收起来的节点数据添加更新标志
|
||||
resetUnExpandNodeStyle() {
|
||||
if (!this.renderTree) return
|
||||
|
||||
@ -16,6 +16,8 @@ import {
|
||||
noneRichTextNodeLineHeight
|
||||
} from '../../constants/constant'
|
||||
|
||||
const SMM_NODE_EDIT_WRAP = 'smm-node-edit-wrap'
|
||||
|
||||
// 节点文字编辑类
|
||||
export default class TextEdit {
|
||||
// 构造函数
|
||||
@ -34,6 +36,7 @@ export default class TextEdit {
|
||||
this.textNodePaddingX = 5
|
||||
this.textNodePaddingY = 3
|
||||
this.isNeedUpdateTextEditNode = false
|
||||
this.mindMap.addEditNodeClass(SMM_NODE_EDIT_WRAP)
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@ -193,6 +196,16 @@ export default class TextEdit {
|
||||
return this.showTextEdit
|
||||
}
|
||||
|
||||
// 设置文本编辑框是否处于显示状态
|
||||
setIsShowTextEdit(val) {
|
||||
this.showTextEdit = val
|
||||
if (val) {
|
||||
this.mindMap.keyCommand.stopCheckInSvg()
|
||||
} else {
|
||||
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||
}
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
// isInserting:是否是刚创建的节点
|
||||
// isFromKeyDown:是否是在按键事件进入的编辑
|
||||
@ -275,7 +288,7 @@ export default class TextEdit {
|
||||
this.mindMap.richText.showTextEdit = false
|
||||
} else {
|
||||
this.cacheEditingText = this.getEditText()
|
||||
this.showTextEdit = false
|
||||
this.setIsShowTextEdit(false)
|
||||
}
|
||||
this.show({
|
||||
node,
|
||||
@ -299,9 +312,7 @@ export default class TextEdit {
|
||||
this.registerTmpShortcut()
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.classList.add(
|
||||
CONSTANTS.EDIT_NODE_CLASS.SMM_NODE_EDIT_WRAP
|
||||
)
|
||||
this.textEditNode.classList.add(SMM_NODE_EDIT_WRAP)
|
||||
this.textEditNode.style.cssText = `
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
@ -383,7 +394,7 @@ export default class TextEdit {
|
||||
} else {
|
||||
this.textEditNode.style.lineHeight = 'normal'
|
||||
}
|
||||
this.showTextEdit = true
|
||||
this.setIsShowTextEdit(true)
|
||||
// 选中文本
|
||||
// if (!this.cacheEditingText) {
|
||||
// selectAllInput(this.textEditNode)
|
||||
@ -477,7 +488,7 @@ export default class TextEdit {
|
||||
this.textEditNode.style.fontSize = 'inherit'
|
||||
this.textEditNode.style.fontWeight = 'normal'
|
||||
this.textEditNode.style.transform = 'translateY(0)'
|
||||
this.showTextEdit = false
|
||||
this.setIsShowTextEdit(false)
|
||||
this.mindMap.execCommand('SET_NODE_TEXT', currentNode, text)
|
||||
// if (currentNode.isGeneralization) {
|
||||
// // 概要节点
|
||||
|
||||
@ -234,6 +234,9 @@ class MindMapNode {
|
||||
'postfix',
|
||||
...this.mindMap.nodeInnerPrefixList.map(item => {
|
||||
return item.name
|
||||
}),
|
||||
...this.mindMap.nodeInnerPostfixList.map(item => {
|
||||
return item.name
|
||||
})
|
||||
]
|
||||
const createTypes = {}
|
||||
@ -291,6 +294,11 @@ class MindMapNode {
|
||||
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'
|
||||
@ -475,8 +483,12 @@ class MindMapNode {
|
||||
return
|
||||
}
|
||||
this.updateNodeActiveClass()
|
||||
const { alwaysShowExpandBtn, notShowExpandBtn, isShowCreateChildBtnIcon } =
|
||||
this.mindMap.opt
|
||||
const {
|
||||
alwaysShowExpandBtn,
|
||||
notShowExpandBtn,
|
||||
isShowCreateChildBtnIcon,
|
||||
readonly
|
||||
} = this.mindMap.opt
|
||||
const childrenLength = this.getChildrenLength()
|
||||
// 不显示展开收起按钮则不需要处理
|
||||
if (!notShowExpandBtn) {
|
||||
@ -522,7 +534,7 @@ class MindMapNode {
|
||||
// 更新节点位置
|
||||
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) {
|
||||
this.group.translate(this.left - t.translateX, this.top - t.translateY)
|
||||
@ -806,11 +818,10 @@ class MindMapNode {
|
||||
}
|
||||
let childrenLen = this.getChildrenLength()
|
||||
// 切换为鱼骨结构时,清空根节点和二级节点的连线
|
||||
if (
|
||||
this.mindMap.opt.layout === CONSTANTS.LAYOUT.FISHBONE &&
|
||||
(this.isRoot || this.layerIndex === 1)
|
||||
) {
|
||||
childrenLen = 0
|
||||
if (this.mindMap.renderer.layout.nodeIsRemoveAllLines) {
|
||||
if (this.mindMap.renderer.layout.nodeIsRemoveAllLines(this)) {
|
||||
childrenLen = 0
|
||||
}
|
||||
}
|
||||
if (childrenLen > this._lines.length) {
|
||||
// 创建缺少的线
|
||||
|
||||
@ -52,14 +52,36 @@ export default class Shape {
|
||||
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
|
||||
paddingY: actHeight < actWidth ? actOffset / 2 : 0
|
||||
}
|
||||
default:
|
||||
return {
|
||||
}
|
||||
const extendShape = this.getShapeFromExtendList(shape)
|
||||
if (extendShape) {
|
||||
return (
|
||||
extendShape.getPadding({
|
||||
node: this.node,
|
||||
width,
|
||||
height,
|
||||
paddingX,
|
||||
paddingY
|
||||
}) || {
|
||||
paddingX: 0,
|
||||
paddingY: 0
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return {
|
||||
paddingX: 0,
|
||||
paddingY: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从形状扩展列表里获取指定名称的形状
|
||||
getShapeFromExtendList(shape) {
|
||||
return this.mindMap.extendShapeList.find(item => {
|
||||
return item.name === shape
|
||||
})
|
||||
}
|
||||
|
||||
// 创建形状节点
|
||||
createShape() {
|
||||
const shape = this.node.getShape()
|
||||
@ -92,7 +114,13 @@ export default class Shape {
|
||||
// 圆
|
||||
node = this.createCircle()
|
||||
}
|
||||
return node
|
||||
if (!node) {
|
||||
const extendShape = this.getShapeFromExtendList(shape)
|
||||
if (extendShape) {
|
||||
node = extendShape.createShape(this.node)
|
||||
}
|
||||
}
|
||||
return node || this.createRect()
|
||||
}
|
||||
|
||||
// 获取节点减去节点边框宽度、hover节点边框宽度后的尺寸
|
||||
|
||||
@ -8,6 +8,18 @@ const backgroundStyleProps = [
|
||||
'backgroundSize'
|
||||
]
|
||||
|
||||
export const shapeStyleProps = [
|
||||
'gradientStyle',
|
||||
'startColor',
|
||||
'endColor',
|
||||
'startDir',
|
||||
'endDir',
|
||||
'fillColor',
|
||||
'borderColor',
|
||||
'borderWidth',
|
||||
'borderDasharray'
|
||||
]
|
||||
|
||||
// 样式类
|
||||
class Style {
|
||||
// 设置背景样式
|
||||
@ -112,6 +124,8 @@ class Style {
|
||||
|
||||
// 更新当前节点生效的样式数据
|
||||
addToEffectiveStyles(styles) {
|
||||
// effectiveStyles目前只提供给格式刷插件使用,所以如果没有注册该插件,那么不需要保存该数据
|
||||
if (!this.ctx.mindMap.painter) return
|
||||
this.ctx.effectiveStyles = {
|
||||
...this.ctx.effectiveStyles,
|
||||
...styles
|
||||
@ -126,17 +140,10 @@ class Style {
|
||||
|
||||
// 形状
|
||||
shape(node) {
|
||||
const styles = {
|
||||
gradientStyle: this.merge('gradientStyle'),
|
||||
startColor: this.merge('startColor'),
|
||||
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')
|
||||
}
|
||||
const styles = {}
|
||||
shapeStyleProps.forEach(key => {
|
||||
styles[key] = this.merge(key)
|
||||
})
|
||||
if (styles.gradientStyle) {
|
||||
if (!this._gradient) {
|
||||
this._gradient = this.ctx.nodeDraw.gradient('linear')
|
||||
|
||||
@ -32,12 +32,20 @@ const defaultTagStyle = {
|
||||
//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() {
|
||||
const img = this.getData('image')
|
||||
const img = this.getImageUrl()
|
||||
if (!img) {
|
||||
return
|
||||
}
|
||||
img = (this.mindMap.renderer.renderTree.data.imgMap || {})[img] || img
|
||||
const imgSize = this.getImgShowSize()
|
||||
const node = new SVGImage().load(img).size(...imgSize)
|
||||
// 如果指定了加载失败显示的图片,那么加载一下图片检测是否失败
|
||||
@ -569,6 +577,7 @@ function isUseCustomNodeContent() {
|
||||
}
|
||||
|
||||
export default {
|
||||
getImageUrl,
|
||||
createImgNode,
|
||||
getImgShowSize,
|
||||
createIconNode,
|
||||
|
||||
@ -33,7 +33,9 @@ function getTagContentSize(space) {
|
||||
function getNodeRect() {
|
||||
// 自定义节点内容
|
||||
if (this.isUseCustomNodeContent()) {
|
||||
const rect = this.measureCustomNodeContentSize(this._customNodeContent)
|
||||
const rect = this.measureCustomNodeContentSize(
|
||||
this._customNodeContent.cloneNode(true)
|
||||
)
|
||||
return {
|
||||
width: this.hasCustomWidth() ? this.customTextWidth : rect.width,
|
||||
height: rect.height
|
||||
@ -127,6 +129,15 @@ function getNodeRect() {
|
||||
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) {
|
||||
@ -169,21 +180,67 @@ function getNodeRect() {
|
||||
}
|
||||
}
|
||||
|
||||
// 激活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 {
|
||||
hoverRectPadding,
|
||||
openRealtimeRenderOnNodeTextEdit,
|
||||
textContentMargin,
|
||||
addCustomContentToNode
|
||||
} = this.mindMap.opt
|
||||
// 避免编辑过程中展开收起按钮闪烁的问题
|
||||
if (openRealtimeRenderOnNodeTextEdit && this._expandBtn) {
|
||||
this.group.add(this._expandBtn)
|
||||
}
|
||||
// 暂时去掉,带来的问题太多
|
||||
// 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
|
||||
@ -203,16 +260,6 @@ function layout() {
|
||||
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({
|
||||
@ -221,7 +268,7 @@ function layout() {
|
||||
height
|
||||
})
|
||||
this.group.add(foreignObject)
|
||||
addHoverNode()
|
||||
this.addHoverNode(width, height)
|
||||
return
|
||||
}
|
||||
const { IMG_PLACEMENT, TAG_PLACEMENT } = CONSTANTS
|
||||
@ -396,8 +443,19 @@ function layout() {
|
||||
.x(textContentOffsetX)
|
||||
.y((textContentHeight - this._postfixData.height) / 2)
|
||||
textContentNested.add(foreignObject)
|
||||
textContentOffsetX += this._postfixData.width
|
||||
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()
|
||||
@ -428,7 +486,7 @@ function layout() {
|
||||
break
|
||||
}
|
||||
textContentNested.translate(translateX, translateY)
|
||||
addHoverNode()
|
||||
this.addHoverNode(width, height)
|
||||
if (this._customContentAddToNodeAdd && this._customContentAddToNodeAdd.el) {
|
||||
const foreignObject = createForeignObjectNode(
|
||||
this._customContentAddToNodeAdd
|
||||
@ -452,5 +510,7 @@ export default {
|
||||
getImgTextMarin,
|
||||
getTagContentSize,
|
||||
getNodeRect,
|
||||
layout
|
||||
addHoverNode,
|
||||
layout,
|
||||
customNodeContentRealtimeLayout
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ class Base {
|
||||
|
||||
// 检查当前来源是否需要重新计算节点大小
|
||||
checkIsNeedResizeSources() {
|
||||
return [CONSTANTS.CHANGE_THEME].includes(this.renderer.renderSource)
|
||||
return this.renderer.checkHasRenderSource(CONSTANTS.CHANGE_THEME)
|
||||
}
|
||||
|
||||
// 层级类型改变
|
||||
@ -61,7 +61,7 @@ class Base {
|
||||
|
||||
// 检查是否是结构布局改变重新渲染展开收起按钮占位元素
|
||||
checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(node) {
|
||||
if (this.renderer.renderSource === CONSTANTS.CHANGE_LAYOUT) {
|
||||
if (this.renderer.checkHasRenderSource(CONSTANTS.CHANGE_LAYOUT)) {
|
||||
node.needRerenderExpandBtnPlaceholderRect = true
|
||||
}
|
||||
}
|
||||
@ -74,10 +74,38 @@ class Base {
|
||||
lastData.isActive = curData.isActive
|
||||
lastData.expand = curData.expand
|
||||
lastData = JSON.stringify(lastData)
|
||||
} else {
|
||||
// 只在都有数据时才进行对比
|
||||
return false
|
||||
}
|
||||
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) {
|
||||
// 创建节点
|
||||
@ -95,6 +123,20 @@ class Base {
|
||||
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
|
||||
let newNode = null
|
||||
// 数据上保存了节点引用,那么直接复用节点
|
||||
@ -114,16 +156,12 @@ class Base {
|
||||
}
|
||||
this.cacheNode(data._node.uid, newNode)
|
||||
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
|
||||
// 库前置内容是否改变了
|
||||
let isNodeInnerPrefixChange = false
|
||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||||
if (item.updateNodeData) {
|
||||
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData)
|
||||
if (isChange) {
|
||||
isNodeInnerPrefixChange = isChange
|
||||
}
|
||||
}
|
||||
})
|
||||
// 库前置或后置内容是否改变了
|
||||
const isNodeInnerFixChange = this.checkNodeFixChange(
|
||||
newNode,
|
||||
nodeInnerPrefixData,
|
||||
nodeInnerPostfixData
|
||||
)
|
||||
// 主题或主题配置改变了
|
||||
const isResizeSource = this.checkIsNeedResizeSources()
|
||||
// 节点数据改变了
|
||||
@ -136,9 +174,10 @@ class Base {
|
||||
isResizeSource ||
|
||||
isNodeDataChange ||
|
||||
isLayerTypeChange ||
|
||||
newNode.getData('resetRichText') ||
|
||||
(newNode.getData('resetRichText') && // 自定义节点内容可以直接忽略resetRichText
|
||||
!newNode.isUseCustomNodeContent()) ||
|
||||
newNode.getData('needUpdate') ||
|
||||
isNodeInnerPrefixChange
|
||||
isNodeInnerFixChange
|
||||
) {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
@ -175,24 +214,21 @@ class Base {
|
||||
const isResizeSource = this.checkIsNeedResizeSources()
|
||||
// 点数据改变了
|
||||
const isNodeDataChange = this.checkIsNodeDataChange(lastData, data.data)
|
||||
// 库前置内容是否改变了
|
||||
let isNodeInnerPrefixChange = false
|
||||
this.mindMap.nodeInnerPrefixList.forEach(item => {
|
||||
if (item.updateNodeData) {
|
||||
const isChange = item.updateNodeData(newNode, nodeInnerPrefixData)
|
||||
if (isChange) {
|
||||
isNodeInnerPrefixChange = isChange
|
||||
}
|
||||
}
|
||||
})
|
||||
// 库前置或后置内容是否改变了
|
||||
const isNodeInnerFixChange = this.checkNodeFixChange(
|
||||
newNode,
|
||||
nodeInnerPrefixData,
|
||||
nodeInnerPostfixData
|
||||
)
|
||||
// 重新计算节点大小和布局
|
||||
if (
|
||||
isResizeSource ||
|
||||
isNodeDataChange ||
|
||||
isLayerTypeChange ||
|
||||
newNode.getData('resetRichText') ||
|
||||
(newNode.getData('resetRichText') &&
|
||||
!newNode.isUseCustomNodeContent()) ||
|
||||
newNode.getData('needUpdate') ||
|
||||
isNodeInnerPrefixChange
|
||||
isNodeInnerFixChange
|
||||
) {
|
||||
newNode.getSize()
|
||||
newNode.needLayout = true
|
||||
|
||||
@ -2,14 +2,78 @@ import Base from './Base'
|
||||
import { walk, asyncRun, degToRad, getNodeIndexInNodeList } from '../utils'
|
||||
import { CONSTANTS } from '../constants/constant'
|
||||
import utils from './fishboneUtils'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import { shapeStyleProps } from '../core/render/node/Style'
|
||||
|
||||
// 鱼骨图
|
||||
class Fishbone extends Base {
|
||||
// 构造函数
|
||||
constructor(opt = {}) {
|
||||
constructor(opt = {}, layout) {
|
||||
super(opt)
|
||||
this.layout = layout
|
||||
this.indent = 0.3
|
||||
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 = [
|
||||
() => {
|
||||
this.computedBaseValue()
|
||||
this.addFishTail()
|
||||
},
|
||||
() => {
|
||||
this.computedLeftTopValue()
|
||||
},
|
||||
() => {
|
||||
this.adjustLeftTopValue()
|
||||
this.updateFishTailPosition()
|
||||
},
|
||||
() => {
|
||||
callback(this.root)
|
||||
@ -31,14 +97,75 @@ class Fishbone extends Base {
|
||||
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值
|
||||
computedBaseValue() {
|
||||
walk(
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(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) {
|
||||
this.setNodeCenter(newNode)
|
||||
@ -57,10 +184,14 @@ class Fishbone extends Base {
|
||||
// 计算二级节点的top值
|
||||
if (parent._node.isRoot) {
|
||||
let marginY = this.getMarginY(layerIndex)
|
||||
// 带鱼头鱼尾的鱼骨图因为根节点高度比较大,所以二级节点需要向中间靠一点
|
||||
const topOffset = this.isFishbone2() ? parent._node.height / 4 : 0
|
||||
if (this.checkIsTop(newNode)) {
|
||||
newNode.top = parent._node.top - newNode.height - marginY
|
||||
newNode.top =
|
||||
parent._node.top - newNode.height - marginY + topOffset
|
||||
} 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) => {
|
||||
if (node.isRoot) {
|
||||
let marginX = this.getMarginX(layerIndex + 1)
|
||||
let topTotalLeft = node.left + node.width + node.height + marginX
|
||||
let bottomTotalLeft = node.left + node.width + node.height + marginX
|
||||
const heightOffsetRatio = this.isFishbone2() ? 2 : 1
|
||||
let topTotalLeft =
|
||||
node.left + node.width + node.height / heightOffsetRatio + marginX
|
||||
let bottomTotalLeft =
|
||||
node.left + node.width + node.height / heightOffsetRatio + marginX
|
||||
node.children.forEach(item => {
|
||||
if (this.checkIsTop(item)) {
|
||||
item.left = topTotalLeft
|
||||
@ -133,19 +267,27 @@ class Fishbone extends Base {
|
||||
if (node.isRoot) {
|
||||
let topTotalLeft = 0
|
||||
let bottomTotalLeft = 0
|
||||
let maxx = -Infinity
|
||||
node.children.forEach(item => {
|
||||
if (this.checkIsTop(item)) {
|
||||
item.left += topTotalLeft
|
||||
this.updateChildren(item.children, 'left', topTotalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
if (right > maxx) {
|
||||
maxx = right
|
||||
}
|
||||
topTotalLeft += right - left
|
||||
} else {
|
||||
item.left += bottomTotalLeft
|
||||
this.updateChildren(item.children, 'left', bottomTotalLeft)
|
||||
let { left, right } = this.getNodeBoundaries(item, 'h')
|
||||
if (right > maxx) {
|
||||
maxx = right
|
||||
}
|
||||
bottomTotalLeft += right - left
|
||||
}
|
||||
})
|
||||
this.maxx = maxx
|
||||
}
|
||||
},
|
||||
true
|
||||
@ -249,7 +391,8 @@ class Fishbone extends Base {
|
||||
// 水平线段到二级节点的连线
|
||||
let marginY = this.getMarginY(item.layerIndex)
|
||||
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 line = this.lineDraw.path()
|
||||
if (this.checkIsTop(item)) {
|
||||
@ -277,11 +420,14 @@ class Fishbone extends Base {
|
||||
let nodeHalfTop = node.top + node.height / 2
|
||||
let offset = node.height / 2 + this.getMarginY(node.layerIndex + 1)
|
||||
let line = this.lineDraw.path()
|
||||
const lineEndX = this.isFishbone2()
|
||||
? this.maxx
|
||||
: maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
line.plot(
|
||||
this.transformPath(
|
||||
`M ${node.left + node.width},${nodeHalfTop} L ${
|
||||
maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg))
|
||||
},${nodeHalfTop}`
|
||||
`M ${
|
||||
node.left + node.width
|
||||
},${nodeHalfTop} L ${lineEndX},${nodeHalfTop}`
|
||||
)
|
||||
)
|
||||
node.style.line(line)
|
||||
@ -406,6 +552,16 @@ class Fishbone extends Base {
|
||||
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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -35,7 +35,14 @@ class VerticalTimeline extends Base {
|
||||
this.renderer.renderTree,
|
||||
null,
|
||||
(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) {
|
||||
this.setNodeCenter(newNode)
|
||||
@ -46,10 +53,16 @@ class VerticalTimeline extends Base {
|
||||
if (parent._node.dir) {
|
||||
newNode.dir = parent._node.dir
|
||||
} else {
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
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 {
|
||||
newNode.dir =
|
||||
index % 2 === 0
|
||||
? CONSTANTS.LAYOUT_GROW_DIR.RIGHT
|
||||
: CONSTANTS.LAYOUT_GROW_DIR.LEFT
|
||||
}
|
||||
}
|
||||
// 定位二级节点的left
|
||||
if (parent._node.isRoot) {
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
|
||||
const getNodeText = node => {
|
||||
// 优先找出其中的text类型的子节点
|
||||
let textChild = (node.children || []).find(item => {
|
||||
return item.type === 'text'
|
||||
})
|
||||
// 没有找到,那么直接使用第一个子节点
|
||||
textChild = textChild || node.children[0]
|
||||
if (textChild) {
|
||||
if (textChild.value !== undefined) {
|
||||
return textChild.value
|
||||
if (node.type === 'list') return ''
|
||||
let textStr = ''
|
||||
|
||||
;(node.children || []).forEach(item => {
|
||||
if (['inlineCode', 'text'].includes(item.type)) {
|
||||
textStr += item.value || ''
|
||||
} else {
|
||||
textStr += getNodeText(item)
|
||||
}
|
||||
return getNodeText(textChild)
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
return textStr
|
||||
}
|
||||
|
||||
// 处理list的情况
|
||||
|
||||
@ -23,6 +23,8 @@ const styleProps = [
|
||||
'associativeLineTextFontFamily'
|
||||
]
|
||||
|
||||
const ASSOCIATIVE_LINE_TEXT_EDIT_WRAP = 'associative-line-text-edit-warp'
|
||||
|
||||
// 关联线插件
|
||||
class AssociativeLine {
|
||||
constructor(opt = {}) {
|
||||
@ -62,9 +64,11 @@ class AssociativeLine {
|
||||
this[item] = associativeLineControlsMethods[item].bind(this)
|
||||
})
|
||||
// 关联线文字相关方法
|
||||
this.showTextEdit = false
|
||||
Object.keys(associativeLineTextMethods).forEach(item => {
|
||||
this[item] = associativeLineTextMethods[item].bind(this)
|
||||
})
|
||||
this.mindMap.addEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
||||
this.bindEvent()
|
||||
}
|
||||
|
||||
@ -157,6 +161,7 @@ class AssociativeLine {
|
||||
// 取消激活关联线
|
||||
if (!this.isControlPointMousedown) {
|
||||
this.clearActiveLine()
|
||||
this.renderAllLines()
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,6 +171,7 @@ class AssociativeLine {
|
||||
this.completeCreateLine(node)
|
||||
} else {
|
||||
this.clearActiveLine()
|
||||
this.renderAllLines()
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,7 +286,7 @@ class AssociativeLine {
|
||||
.stroke({
|
||||
width: associativeLineWidth,
|
||||
color: associativeLineColor,
|
||||
dasharray: associativeLineDasharray || [6, 4]
|
||||
dasharray: associativeLineDasharray || '6,4'
|
||||
})
|
||||
.fill({ color: 'none' })
|
||||
path.plot(pathStr)
|
||||
@ -348,7 +354,7 @@ class AssociativeLine {
|
||||
.stroke({
|
||||
width: associativeLineWidth,
|
||||
color: associativeLineColor,
|
||||
dasharray: associativeLineDasharray || [6, 4]
|
||||
dasharray: associativeLineDasharray || '6,4'
|
||||
})
|
||||
.fill({ color: 'none' })
|
||||
clickPath
|
||||
@ -382,6 +388,7 @@ class AssociativeLine {
|
||||
if (this.controlPoint2) {
|
||||
this.controlPoint2.stroke({ color: associativeLineActiveColor })
|
||||
}
|
||||
this.updateTextPos(path, text)
|
||||
}
|
||||
|
||||
// 激活某根关联线
|
||||
@ -461,7 +468,7 @@ class AssociativeLine {
|
||||
.stroke({
|
||||
width: associativeLineWidth,
|
||||
color: associativeLineColor,
|
||||
dasharray: associativeLineDasharray || [6, 4]
|
||||
dasharray: associativeLineDasharray || '6,4'
|
||||
})
|
||||
.fill({ color: 'none' })
|
||||
// 箭头
|
||||
@ -742,11 +749,13 @@ class AssociativeLine {
|
||||
|
||||
// 插件被移除前做的事情
|
||||
beforePluginRemove() {
|
||||
this.mindMap.deleteEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
||||
this.unBindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.mindMap.deleteEditNodeClass(ASSOCIATIVE_LINE_TEXT_EDIT_WRAP)
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,6 +43,18 @@ class Demonstrate {
|
||||
this.mindMap.opt.demonstrateConfig || {}
|
||||
)
|
||||
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() {
|
||||
this.unBindEvent()
|
||||
this.mindMap.off('after_update_config', this.onConfigUpdate)
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.unBindEvent()
|
||||
this.mindMap.off('after_update_config', this.onConfigUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -407,7 +407,12 @@ class Drag extends Base {
|
||||
TIMELINE,
|
||||
TIMELINE2,
|
||||
VERTICAL_TIMELINE,
|
||||
FISHBONE
|
||||
VERTICAL_TIMELINE2,
|
||||
VERTICAL_TIMELINE3,
|
||||
FISHBONE,
|
||||
FISHBONE2,
|
||||
RIGHT_FISHBONE,
|
||||
RIGHT_FISHBONE2
|
||||
} = CONSTANTS.LAYOUT
|
||||
this.overlapNode = null
|
||||
this.prevNode = null
|
||||
@ -443,9 +448,14 @@ class Drag extends Base {
|
||||
this.handleTimeLine2(node)
|
||||
break
|
||||
case VERTICAL_TIMELINE:
|
||||
case VERTICAL_TIMELINE2:
|
||||
case VERTICAL_TIMELINE3:
|
||||
this.handleLogicalStructure(node)
|
||||
break
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
this.handleFishbone(node)
|
||||
break
|
||||
default:
|
||||
@ -469,7 +479,12 @@ class Drag extends Base {
|
||||
TIMELINE,
|
||||
TIMELINE2,
|
||||
VERTICAL_TIMELINE,
|
||||
FISHBONE
|
||||
VERTICAL_TIMELINE2,
|
||||
VERTICAL_TIMELINE3,
|
||||
FISHBONE,
|
||||
FISHBONE2,
|
||||
RIGHT_FISHBONE,
|
||||
RIGHT_FISHBONE2
|
||||
} = CONSTANTS.LAYOUT
|
||||
const { LEFT, TOP, RIGHT, BOTTOM } = CONSTANTS.LAYOUT_GROW_DIR
|
||||
const layerIndex = this.overlapNode.layerIndex
|
||||
@ -563,6 +578,8 @@ class Drag extends Base {
|
||||
}
|
||||
break
|
||||
case VERTICAL_TIMELINE:
|
||||
case VERTICAL_TIMELINE2:
|
||||
case VERTICAL_TIMELINE3:
|
||||
if (layerIndex === 0) {
|
||||
x =
|
||||
lastNodeRect.originLeft +
|
||||
@ -580,6 +597,9 @@ class Drag extends Base {
|
||||
}
|
||||
break
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
if (layerIndex <= 1) {
|
||||
notRenderPlaceholder = true
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
|
||||
@ -655,6 +675,8 @@ class Drag extends Base {
|
||||
}
|
||||
break
|
||||
case VERTICAL_TIMELINE:
|
||||
case VERTICAL_TIMELINE2:
|
||||
case VERTICAL_TIMELINE3:
|
||||
if (layerIndex === 0) {
|
||||
rotate = true
|
||||
}
|
||||
@ -668,6 +690,9 @@ class Drag extends Base {
|
||||
halfPlaceholderHeight
|
||||
break
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
if (layerIndex <= 1) {
|
||||
notRenderPlaceholder = true
|
||||
this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
|
||||
@ -703,7 +728,12 @@ class Drag extends Base {
|
||||
MIND_MAP,
|
||||
TIMELINE2,
|
||||
VERTICAL_TIMELINE,
|
||||
FISHBONE
|
||||
VERTICAL_TIMELINE2,
|
||||
VERTICAL_TIMELINE3,
|
||||
FISHBONE,
|
||||
FISHBONE2,
|
||||
RIGHT_FISHBONE,
|
||||
RIGHT_FISHBONE2
|
||||
} = CONSTANTS.LAYOUT
|
||||
switch (this.mindMap.opt.layout) {
|
||||
case LOGICAL_STRUCTURE:
|
||||
@ -713,7 +743,12 @@ class Drag extends Base {
|
||||
case MIND_MAP:
|
||||
case TIMELINE2:
|
||||
case VERTICAL_TIMELINE:
|
||||
case VERTICAL_TIMELINE2:
|
||||
case VERTICAL_TIMELINE3:
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
return node.dir
|
||||
default:
|
||||
return ''
|
||||
@ -725,17 +760,22 @@ class Drag extends Base {
|
||||
handleVerticalCheck(node, checkList, isReverse = false) {
|
||||
const { layout } = this.mindMap.opt
|
||||
const { LAYOUT, LAYOUT_GROW_DIR } = CONSTANTS
|
||||
const { VERTICAL_TIMELINE, FISHBONE } = LAYOUT
|
||||
const { BOTTOM, LEFT } = LAYOUT_GROW_DIR
|
||||
const {
|
||||
VERTICAL_TIMELINE,
|
||||
VERTICAL_TIMELINE2,
|
||||
VERTICAL_TIMELINE3,
|
||||
FISHBONE,
|
||||
FISHBONE2,
|
||||
RIGHT_FISHBONE,
|
||||
RIGHT_FISHBONE2
|
||||
} = LAYOUT
|
||||
const { LEFT } = LAYOUT_GROW_DIR
|
||||
const mouseMoveX = this.mouseMoveX
|
||||
const mouseMoveY = this.mouseMoveY
|
||||
const nodeRect = this.getNodeRect(node)
|
||||
const dir = this.getNewChildNodeDir(node)
|
||||
const layerIndex = node.layerIndex
|
||||
if (
|
||||
isReverse ||
|
||||
(layout === FISHBONE && dir === BOTTOM && layerIndex >= 3)
|
||||
) {
|
||||
if (isReverse) {
|
||||
checkList = checkList.reverse()
|
||||
}
|
||||
let oneFourthHeight = nodeRect.originHeight / 4
|
||||
@ -770,6 +810,8 @@ class Drag extends Base {
|
||||
let notRenderLine = false
|
||||
switch (layout) {
|
||||
case VERTICAL_TIMELINE:
|
||||
case VERTICAL_TIMELINE2:
|
||||
case VERTICAL_TIMELINE3:
|
||||
if (layerIndex === 1) {
|
||||
x =
|
||||
nodeRect.originLeft +
|
||||
@ -777,6 +819,11 @@ class Drag extends Base {
|
||||
this.placeholderWidth / 2
|
||||
}
|
||||
break
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
x =
|
||||
nodeRect.originLeft + nodeRect.originWidth - this.placeholderWidth
|
||||
break
|
||||
default:
|
||||
}
|
||||
if (checkIsPrevNode) {
|
||||
@ -791,6 +838,9 @@ class Drag extends Base {
|
||||
this.placeholderHeight / 2
|
||||
switch (layout) {
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
if (layerIndex === 2) {
|
||||
notRenderLine = true
|
||||
y =
|
||||
@ -820,6 +870,9 @@ class Drag extends Base {
|
||||
this.placeholderHeight / 2
|
||||
switch (layout) {
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
if (layerIndex === 2) {
|
||||
notRenderLine = true
|
||||
y =
|
||||
@ -856,7 +909,14 @@ class Drag extends Base {
|
||||
handleHorizontalCheck(node, checkList) {
|
||||
const { layout } = this.mindMap.opt
|
||||
const { LAYOUT } = CONSTANTS
|
||||
const { FISHBONE, TIMELINE, TIMELINE2 } = LAYOUT
|
||||
const {
|
||||
FISHBONE,
|
||||
FISHBONE2,
|
||||
RIGHT_FISHBONE,
|
||||
RIGHT_FISHBONE2,
|
||||
TIMELINE,
|
||||
TIMELINE2
|
||||
} = LAYOUT
|
||||
let mouseMoveX = this.mouseMoveX
|
||||
let mouseMoveY = this.mouseMoveY
|
||||
let nodeRect = this.getNodeRect(node)
|
||||
@ -896,6 +956,9 @@ class Drag extends Base {
|
||||
this.placeholderWidth / 2
|
||||
break
|
||||
case FISHBONE:
|
||||
case FISHBONE2:
|
||||
case RIGHT_FISHBONE:
|
||||
case RIGHT_FISHBONE2:
|
||||
if (layerIndex === 1) {
|
||||
notRenderLine = true
|
||||
y =
|
||||
@ -907,7 +970,11 @@ class Drag extends Base {
|
||||
default:
|
||||
}
|
||||
if (checkIsPrevNode) {
|
||||
this.prevNode = node
|
||||
if ([RIGHT_FISHBONE, RIGHT_FISHBONE2].includes(layout)) {
|
||||
this.nextNode = node
|
||||
} else {
|
||||
this.prevNode = node
|
||||
}
|
||||
this.setPlaceholderRect({
|
||||
x:
|
||||
nodeRect.originRight +
|
||||
@ -918,7 +985,11 @@ class Drag extends Base {
|
||||
notRenderLine
|
||||
})
|
||||
} else if (checkIsNextNode) {
|
||||
this.nextNode = node
|
||||
if ([RIGHT_FISHBONE, RIGHT_FISHBONE2].includes(layout)) {
|
||||
this.prevNode = node
|
||||
} else {
|
||||
this.nextNode = node
|
||||
}
|
||||
this.setPlaceholderRect({
|
||||
x:
|
||||
nodeRect.originLeft -
|
||||
@ -1142,7 +1213,11 @@ class Drag extends Base {
|
||||
this.handleHorizontalCheck(node, checkList)
|
||||
} 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)
|
||||
} else {
|
||||
this.handleVerticalCheck(node, checkList)
|
||||
|
||||
@ -128,7 +128,13 @@ class Export {
|
||||
}
|
||||
|
||||
// svg转png
|
||||
svgToPng(svgSrc, transparent, clipData = null) {
|
||||
svgToPng(
|
||||
svgSrc,
|
||||
transparent,
|
||||
clipData = null,
|
||||
fitBg = false,
|
||||
format = 'image/png'
|
||||
) {
|
||||
const { maxCanvasSize, minExportImgCanvasScale } = this.mindMap.opt
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
@ -138,6 +144,7 @@ class Export {
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
const dpr = Math.max(window.devicePixelRatio, minExportImgCanvasScale)
|
||||
// 图片原始大小
|
||||
let imgWidth = img.width
|
||||
let imgHeight = img.height
|
||||
// 如果是裁减操作的话,那么需要手动添加内边距,及调整图片大小为实际的裁减区域的大小,不要忘了内边距哦
|
||||
@ -149,10 +156,39 @@ class Export {
|
||||
imgWidth = clipData.width + paddingX * 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大小需要乘以dpr
|
||||
let canvasWidth = imgWidth * dpr
|
||||
let canvasHeight = imgHeight * dpr
|
||||
let scaleX = 1
|
||||
let scaleY = 1
|
||||
let canvasWidth = (fitBgImgWidth || imgWidth) * dpr
|
||||
let canvasHeight = (fitBgImgHeight || imgHeight) * dpr
|
||||
if (canvasWidth > maxCanvasSize || canvasHeight > maxCanvasSize) {
|
||||
let newWidth = null
|
||||
let newHeight = null
|
||||
@ -170,6 +206,8 @@ class Export {
|
||||
newWidth,
|
||||
newHeight
|
||||
)
|
||||
scaleX = res[0] / canvasWidth
|
||||
scaleY = res[1] / canvasHeight
|
||||
canvasWidth = res[0]
|
||||
canvasHeight = res[1]
|
||||
}
|
||||
@ -177,6 +215,7 @@ class Export {
|
||||
canvas.height = canvasHeight
|
||||
const styleWidth = canvasWidth / dpr
|
||||
const styleHeight = canvasHeight / dpr
|
||||
// canvas元素实际上的大小
|
||||
canvas.style.width = styleWidth + 'px'
|
||||
canvas.style.height = styleHeight + 'px'
|
||||
const ctx = canvas.getContext('2d')
|
||||
@ -187,6 +226,10 @@ class Export {
|
||||
}
|
||||
// 图片绘制到canvas里
|
||||
// 如果有裁减数据,那么需要进行裁减
|
||||
const fitBgLeft =
|
||||
(fitBgImgWidth > 0 ? (fitBgImgWidth - imgWidth) / 2 : 0) * scaleX
|
||||
const fitBgTop =
|
||||
(fitBgImgHeight > 0 ? (fitBgImgHeight - imgHeight) / 2 : 0) * scaleY
|
||||
if (clipData) {
|
||||
ctx.drawImage(
|
||||
img,
|
||||
@ -194,15 +237,21 @@ class Export {
|
||||
clipData.top,
|
||||
clipData.width,
|
||||
clipData.height,
|
||||
paddingX,
|
||||
paddingY,
|
||||
clipData.width,
|
||||
clipData.height
|
||||
paddingX * scaleX + fitBgLeft,
|
||||
paddingY * scaleY + fitBgTop,
|
||||
clipData.width * scaleX,
|
||||
clipData.height * scaleY
|
||||
)
|
||||
} 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) {
|
||||
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
|
||||
/**
|
||||
* 方法1.把svg的图片都转化成data:url格式,再转换
|
||||
* 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换
|
||||
*/
|
||||
async png(name, transparent = false, node = null) {
|
||||
this.handleNodeExport(node)
|
||||
const { str, clipData } = await this.getSvgData(node)
|
||||
const svgUrl = await this.fixSvgStrAndToBlob(str)
|
||||
const res = await this.svgToPng(svgUrl, transparent, clipData)
|
||||
async png(...args) {
|
||||
const res = await this._image('image/png', ...args)
|
||||
return res
|
||||
}
|
||||
|
||||
// 导出为jpg
|
||||
async jpg(...args) {
|
||||
const res = await this._image('image/jpg', ...args)
|
||||
return res
|
||||
}
|
||||
|
||||
@ -305,11 +373,11 @@ class Export {
|
||||
}
|
||||
|
||||
// 导出为pdf
|
||||
async pdf(name, transparent = false) {
|
||||
async pdf(name, transparent = false, fitBg = false) {
|
||||
if (!this.mindMap.doExportPDF) {
|
||||
throw new Error('请注册ExportPDF插件')
|
||||
}
|
||||
const img = await this.png(name, transparent)
|
||||
const img = await this.png(name, transparent, null, fitBg)
|
||||
// 使用jspdf库
|
||||
// await this.mindMap.doExportPDF.pdf(name, img)
|
||||
// 使用pdf-lib库
|
||||
@ -330,6 +398,7 @@ class Export {
|
||||
|
||||
// 导出为svg
|
||||
async svg(name) {
|
||||
this.mindMap.renderer.textEdit.hideEditTextBox()
|
||||
const { node } = await this.getSvgData()
|
||||
node.first().before(SVG(`<title>${name}</title>`))
|
||||
await this.drawBackgroundToSvg(node)
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import {
|
||||
isWhite,
|
||||
isTransparent,
|
||||
getVisibleColorFromTheme,
|
||||
readBlob
|
||||
getVisibleColorFromTheme
|
||||
} from '../utils/index'
|
||||
|
||||
// 小地图插件
|
||||
|
||||
100
simple-mind-map/src/plugins/NodeBase64ImageStorage.js
Normal 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
|
||||
@ -31,12 +31,14 @@ class NodeImgAdjust {
|
||||
this.onMousemove = this.onMousemove.bind(this)
|
||||
this.onMouseup = this.onMouseup.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_mousemove', this.onNodeImgMousemove)
|
||||
this.mindMap.on('mousemove', this.onMousemove)
|
||||
this.mindMap.on('mouseup', this.onMouseup)
|
||||
this.mindMap.on('node_mouseup', this.onMouseup)
|
||||
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('node_mouseup', this.onMouseup)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// 节点图片鼠标移动事件
|
||||
@ -210,6 +221,7 @@ class NodeImgAdjust {
|
||||
}
|
||||
if (!stop) {
|
||||
this.mindMap.execCommand('SET_NODE_IMAGE', this.node, { url: null })
|
||||
this.mindMap.emit('delete_node_img_from_delete_btn', this.node)
|
||||
}
|
||||
})
|
||||
// 添加元素到页面
|
||||
@ -219,6 +231,7 @@ class NodeImgAdjust {
|
||||
|
||||
// 鼠标按钮按下事件
|
||||
onMousedown(e) {
|
||||
this.mindMap.emit('node_img_adjust_btn_mousedown', this.node)
|
||||
this.isMousedown = true
|
||||
this.mousedownDrawTransform = this.mindMap.draw.transform()
|
||||
// 隐藏节点实际图片
|
||||
|
||||
@ -1,149 +1,68 @@
|
||||
import {
|
||||
formatDataToArray,
|
||||
walk,
|
||||
getTopAncestorsFomNodeList,
|
||||
getNodeListBoundingRect,
|
||||
createUid
|
||||
} from '../utils'
|
||||
|
||||
// 解析要添加外框的节点实例列表
|
||||
const parseAddNodeList = list => {
|
||||
// 找出顶层节点
|
||||
list = getTopAncestorsFomNodeList(list)
|
||||
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
|
||||
}
|
||||
import {
|
||||
parseAddNodeList,
|
||||
getNodeOuterFrameList
|
||||
} from './outerFrame/outerFrameUtils'
|
||||
import outerFrameTextMethods from './outerFrame/outerFrameText'
|
||||
|
||||
// 默认外框样式
|
||||
const defaultStyle = {
|
||||
// 外框圆角大小
|
||||
radius: 5,
|
||||
// 外框边框宽度
|
||||
strokeWidth: 2,
|
||||
// 外框边框颜色
|
||||
strokeColor: '#0984e3',
|
||||
// 外框边框虚线样式
|
||||
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 {
|
||||
constructor(opt = {}) {
|
||||
this.mindMap = opt.mindMap
|
||||
this.draw = null
|
||||
this.createDrawContainer()
|
||||
this.isNotRenderOuterFrames = false
|
||||
this.textNodeList = []
|
||||
this.outerFrameElList = []
|
||||
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()
|
||||
}
|
||||
|
||||
@ -164,6 +83,11 @@ class OuterFrame {
|
||||
this.clearActiveOuterFrame = this.clearActiveOuterFrame.bind(this)
|
||||
this.mindMap.on('draw_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.mindMap.command.add('ADD_OUTER_FRAME', this.addOuterFrame)
|
||||
@ -181,6 +105,8 @@ class OuterFrame {
|
||||
this.mindMap.off('data_change', this.renderOuterFrames)
|
||||
this.mindMap.off('draw_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.keyCommand.removeShortcut(
|
||||
'Del|Backspace',
|
||||
@ -188,6 +114,12 @@ class OuterFrame {
|
||||
)
|
||||
}
|
||||
|
||||
// 实例销毁时清除关联线文字编辑框
|
||||
onBeforeDestroy() {
|
||||
this.hideEditTextBox()
|
||||
this.removeTextEditEl()
|
||||
}
|
||||
|
||||
// 给节点添加外框数据
|
||||
/*
|
||||
config: {
|
||||
@ -256,20 +188,47 @@ class OuterFrame {
|
||||
this.mindMap.emit('outer_frame_delete')
|
||||
}
|
||||
|
||||
// 删除当前激活外框的文字
|
||||
removeActiveOuterFrameText() {
|
||||
this.updateActiveOuterFrame({
|
||||
text: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 更新当前激活的外框
|
||||
// 执行了该方法后请立即隐藏你的样式面板,因为会清除当前激活的外框
|
||||
updateActiveOuterFrame(config = {}) {
|
||||
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 => {
|
||||
const outerFrame = node.getData('outerFrame')
|
||||
const newData = {
|
||||
...outerFrame,
|
||||
...config
|
||||
}
|
||||
newStrokeDasharray = newData.strokeDasharray
|
||||
this.mindMap.execCommand('SET_NODE_DATA', node, {
|
||||
outerFrame: {
|
||||
...outerFrame,
|
||||
...config
|
||||
}
|
||||
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() {
|
||||
if (this.isNotRenderOuterFrames) {
|
||||
this.isNotRenderOuterFrames = false
|
||||
return
|
||||
}
|
||||
this.clearActiveOuterFrame()
|
||||
this.clearTextNodes()
|
||||
this.clearOuterFrameElList()
|
||||
let tree = this.mindMap.renderer.root
|
||||
if (!tree) return
|
||||
@ -317,11 +287,15 @@ class OuterFrame {
|
||||
t.scaleY,
|
||||
(width + outerFramePaddingX * 2) / t.scaleX,
|
||||
(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 => {
|
||||
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.clearActiveOuterFrame()
|
||||
this.activeOuterFrame = {
|
||||
el,
|
||||
node,
|
||||
range
|
||||
range,
|
||||
textNode
|
||||
}
|
||||
el.stroke({
|
||||
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)
|
||||
}
|
||||
|
||||
// 清除当前激活的外框
|
||||
clearActiveOuterFrame() {
|
||||
if (!this.activeOuterFrame) return
|
||||
const { el } = this.activeOuterFrame
|
||||
const { el, textNode, node, range } = this.activeOuterFrame
|
||||
el.stroke({
|
||||
dasharray: el.cacheStyle.dasharray || defaultStyle.strokeDasharray
|
||||
})
|
||||
// 隐藏文本编辑框
|
||||
this.hideEditTextBox()
|
||||
// 如果没有输入过文字,那么隐藏
|
||||
if (!this.getText(this.getNodeRangeFirstNode(node, range))) {
|
||||
textNode.clear()
|
||||
}
|
||||
this.activeOuterFrame = null
|
||||
this.mindMap.emit('outer_frame_deactivate')
|
||||
}
|
||||
|
||||
// 获取指定外框的样式
|
||||
getStyle(node) {
|
||||
return { ...defaultStyle, ...(node.getData('outerFrame') || {}) }
|
||||
}
|
||||
|
||||
// 创建外框元素
|
||||
createOuterFrameEl(x, y, width, height, styleConfig = {}) {
|
||||
styleConfig = { ...defaultStyle, ...styleConfig }
|
||||
const el = this.draw
|
||||
.rect()
|
||||
.size(width, height)
|
||||
.radius(styleConfig.radius)
|
||||
const el = this.draw.rect().size(width, height).x(x).y(y)
|
||||
this.styleOuterFrame(el, styleConfig)
|
||||
el.cacheStyle = {
|
||||
dasharray: styleConfig.strokeDasharray
|
||||
}
|
||||
this.outerFrameElList.push(el)
|
||||
return el
|
||||
}
|
||||
|
||||
// 设置外框样式
|
||||
styleOuterFrame(el, styleConfig) {
|
||||
el.radius(styleConfig.radius)
|
||||
.stroke({
|
||||
width: styleConfig.strokeWidth,
|
||||
color: styleConfig.strokeColor,
|
||||
@ -372,13 +376,13 @@ class OuterFrame {
|
||||
.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() {
|
||||
this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||
this.unBindEvent()
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
this.mindMap.deleteEditNodeClass(OUTER_FRAME_TEXT_EDIT_WRAP)
|
||||
this.unBindEvent()
|
||||
}
|
||||
}
|
||||
|
||||
OuterFrame.instanceName = 'outerFrame'
|
||||
OuterFrame.defaultStyle = defaultStyle
|
||||
|
||||
export default OuterFrame
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
htmlEscape,
|
||||
compareVersion
|
||||
} from '../utils'
|
||||
import { CONSTANTS, richTextSupportStyleList } from '../constants/constant'
|
||||
import { richTextSupportStyleList } from '../constants/constant'
|
||||
import MindMapNode from '../core/render/node/MindMapNode'
|
||||
import { Scope } from 'parchment'
|
||||
|
||||
@ -40,6 +40,8 @@ let fontSizeList = new Array(100).fill(0).map((_, index) => {
|
||||
return index + 'px'
|
||||
})
|
||||
|
||||
const RICH_TEXT_EDIT_WRAP = 'ql-editor'
|
||||
|
||||
// 富文本编辑插件
|
||||
class RichText {
|
||||
constructor({ mindMap, pluginOpt }) {
|
||||
@ -58,6 +60,7 @@ class RichText {
|
||||
this.isCompositing = false
|
||||
this.textNodePaddingX = 6
|
||||
this.textNodePaddingY = 4
|
||||
this.mindMap.addEditNodeClass(RICH_TEXT_EDIT_WRAP)
|
||||
this.initOpt()
|
||||
this.extendQuill()
|
||||
this.appendCss()
|
||||
@ -113,7 +116,7 @@ class RichText {
|
||||
`
|
||||
)
|
||||
let cssText = `
|
||||
.${CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP} {
|
||||
.${RICH_TEXT_EDIT_WRAP} {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
@ -297,7 +300,7 @@ class RichText {
|
||||
}
|
||||
this.initQuillEditor()
|
||||
this.setQuillContainerMinHeight(originHeight)
|
||||
this.showTextEdit = true
|
||||
this.setIsShowTextEdit(true)
|
||||
// 如果是刚创建的节点,那么默认全选,否则普通激活不全选,除非selectTextOnEnterEditText配置为true
|
||||
// 在selectTextOnEnterEditText时,如果是在keydown事件进入的节点编辑,也不需要全选
|
||||
this.focus(
|
||||
@ -331,9 +334,8 @@ class RichText {
|
||||
|
||||
// 设置quill编辑器容器的最小高度
|
||||
setQuillContainerMinHeight(minHeight) {
|
||||
document.querySelector(
|
||||
'.' + CONSTANTS.EDIT_NODE_CLASS.RICH_TEXT_EDIT_WRAP
|
||||
).style.minHeight = minHeight + 'px'
|
||||
document.querySelector('.' + RICH_TEXT_EDIT_WRAP).style.minHeight =
|
||||
minHeight + 'px'
|
||||
}
|
||||
|
||||
// 更新文本编辑框的大小和位置
|
||||
@ -385,7 +387,7 @@ class RichText {
|
||||
const list = nodes && nodes.length > 0 ? nodes : [this.node]
|
||||
const node = this.node
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.showTextEdit = false
|
||||
this.setIsShowTextEdit(false)
|
||||
this.mindMap.emit('rich_text_selection_change', false)
|
||||
this.node = null
|
||||
this.isInserting = false
|
||||
@ -489,7 +491,10 @@ class RichText {
|
||||
})
|
||||
this.quill.on('selection-change', range => {
|
||||
// 刚创建的节点全选不需要显示操作条
|
||||
if (this.isInserting) return
|
||||
if (this.isInserting) {
|
||||
this.isInserting = false
|
||||
return
|
||||
}
|
||||
this.lastRange = this.range
|
||||
this.range = null
|
||||
if (range) {
|
||||
@ -614,6 +619,16 @@ class RichText {
|
||||
this.isCompositing = false
|
||||
}
|
||||
|
||||
// 设置文本编辑框是否处于显示状态
|
||||
setIsShowTextEdit(val) {
|
||||
this.showTextEdit = val
|
||||
if (val) {
|
||||
this.mindMap.keyCommand.stopCheckInSvg()
|
||||
} else {
|
||||
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||
}
|
||||
}
|
||||
|
||||
// 选中全部
|
||||
selectAll() {
|
||||
this.quill.setSelection(0, this.quill.getLength())
|
||||
@ -820,6 +835,7 @@ class RichText {
|
||||
|
||||
// 处理导入数据
|
||||
handleSetData(data) {
|
||||
if (!data) return
|
||||
// 短期处理,为了兼容老数据,长期会去除
|
||||
const isOldRichTextVersion =
|
||||
!data.smmVersion || compareVersion(data.smmVersion, '0.13.0') === '<'
|
||||
@ -852,12 +868,14 @@ class RichText {
|
||||
document.head.removeChild(this.styleEl)
|
||||
this.unbindEvent()
|
||||
this.mindMap.removeAppendCss('richText')
|
||||
this.mindMap.deleteEditNodeClass(RICH_TEXT_EDIT_WRAP)
|
||||
}
|
||||
|
||||
// 插件被卸载前做的事情
|
||||
beforePluginDestroy() {
|
||||
document.head.removeChild(this.styleEl)
|
||||
this.unbindEvent()
|
||||
this.mindMap.deleteEditNodeClass(RICH_TEXT_EDIT_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import {
|
||||
selectAllInput
|
||||
} from '../../utils/index'
|
||||
|
||||
const ASSOCIATIVE_LINE_TEXT_EDIT_WRAP = 'associative-line-text-edit-warp'
|
||||
|
||||
// 创建文字节点
|
||||
function createText(data) {
|
||||
let g = this.associativeLineDraw.group()
|
||||
@ -43,7 +45,7 @@ function showEditTextBox(g) {
|
||||
// 输入框元素没有创建过,则先创建
|
||||
if (!this.textEditNode) {
|
||||
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.setAttribute('contenteditable', true)
|
||||
this.textEditNode.addEventListener('keyup', e => {
|
||||
@ -73,7 +75,7 @@ function showEditTextBox(g) {
|
||||
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.updateTextEditBoxPos(g)
|
||||
this.showTextEdit = true
|
||||
this.setIsShowTextEdit(true)
|
||||
// 如果是默认文本要全选输入框
|
||||
if (text === '' || text === defaultAssociativeLineText) {
|
||||
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() {
|
||||
if (!this.textEditNode) return
|
||||
@ -124,7 +136,7 @@ function hideEditTextBox() {
|
||||
})
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.textEditNode.innerHTML = ''
|
||||
this.showTextEdit = false
|
||||
this.setIsShowTextEdit(false)
|
||||
this.renderText(str, path, text, node, toNode)
|
||||
this.mindMap.emit('hide_text_edit')
|
||||
}
|
||||
@ -192,6 +204,7 @@ export default {
|
||||
styleText,
|
||||
onScale,
|
||||
showEditTextBox,
|
||||
setIsShowTextEdit,
|
||||
removeTextEditEl,
|
||||
hideEditTextBox,
|
||||
updateTextEditBoxPos,
|
||||
|
||||
234
simple-mind-map/src/plugins/outerFrame/outerFrameText.js
Normal file
@ -0,0 +1,234 @@
|
||||
import { Text, Rect, G } from '@svgdotjs/svg.js'
|
||||
import {
|
||||
getStrWithBrFromHtml,
|
||||
focusInput,
|
||||
selectAllInput
|
||||
} from '../../utils/index'
|
||||
|
||||
const OUTER_FRAME_TEXT_EDIT_WRAP = 'outer-frame-text-edit-warp'
|
||||
|
||||
// 创建文字节点
|
||||
function createText(el, cur, range) {
|
||||
const g = this.draw.group()
|
||||
const setActive = () => {
|
||||
if (!this.activeOuterFrame || this.activeOuterFrame.el !== el) {
|
||||
this.setActiveOuterFrame(el, cur, range, g)
|
||||
}
|
||||
}
|
||||
g.click(e => {
|
||||
e.stopPropagation()
|
||||
setActive()
|
||||
})
|
||||
g.on('dblclick', e => {
|
||||
e.stopPropagation()
|
||||
setActive()
|
||||
this.showEditTextBox(g)
|
||||
})
|
||||
return g
|
||||
}
|
||||
|
||||
// 显示文本编辑框
|
||||
function showEditTextBox(g) {
|
||||
this.mindMap.emit('before_show_text_edit')
|
||||
// 注册回车快捷键
|
||||
this.mindMap.keyCommand.addShortcut('Enter', () => {
|
||||
this.hideEditTextBox()
|
||||
})
|
||||
// 输入框元素没有创建过,则先创建
|
||||
if (!this.textEditNode) {
|
||||
this.textEditNode = document.createElement('div')
|
||||
this.textEditNode.className = OUTER_FRAME_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);
|
||||
outline: none;
|
||||
word-break: break-all;
|
||||
`
|
||||
this.textEditNode.setAttribute('contenteditable', true)
|
||||
this.textEditNode.addEventListener('keyup', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.textEditNode.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.appendChild(this.textEditNode)
|
||||
}
|
||||
const { node, range } = this.activeOuterFrame
|
||||
const style = this.getStyle(this.getNodeRangeFirstNode(node, range))
|
||||
const [pl, pt, pr, pb] = style.textFillPadding
|
||||
let { defaultOuterFrameText, nodeTextEditZIndex } = this.mindMap.opt
|
||||
let scale = this.mindMap.view.scale
|
||||
let text = this.getText(this.getNodeRangeFirstNode(node, range))
|
||||
let textLines = (text || defaultOuterFrameText).split(/\n/gim)
|
||||
this.textEditNode.style.padding = `${pl}px ${pt}px ${pr}px ${pb}px`
|
||||
this.textEditNode.style.fontFamily = style.fontFamily
|
||||
this.textEditNode.style.fontSize = style.fontSize * scale + 'px'
|
||||
this.textEditNode.style.fontWeight = style.fontWeight
|
||||
this.textEditNode.style.fontStyle = style.fontStyle
|
||||
this.textEditNode.style.lineHeight =
|
||||
textLines.length > 1 ? style.lineHeight : 'normal'
|
||||
this.textEditNode.style.zIndex = nodeTextEditZIndex
|
||||
this.textEditNode.innerHTML = textLines.join('<br>')
|
||||
this.textEditNode.style.display = 'block'
|
||||
this.updateTextEditBoxPos(g)
|
||||
this.setIsShowTextEdit(true)
|
||||
// 如果是默认文本要全选输入框
|
||||
if (text === '' || text === defaultOuterFrameText) {
|
||||
selectAllInput(this.textEditNode)
|
||||
} else {
|
||||
// 否则聚焦即可
|
||||
focusInput(this.textEditNode)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置文本编辑框是否处于显示状态
|
||||
function setIsShowTextEdit(val) {
|
||||
this.showTextEdit = val
|
||||
if (val) {
|
||||
this.mindMap.keyCommand.stopCheckInSvg()
|
||||
} else {
|
||||
this.mindMap.keyCommand.recoveryCheckInSvg()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文本编辑框元素
|
||||
function removeTextEditEl() {
|
||||
if (!this.textEditNode) return
|
||||
const targetNode = this.mindMap.opt.customInnerElsAppendTo || document.body
|
||||
targetNode.removeChild(this.textEditNode)
|
||||
}
|
||||
|
||||
// 处理画布缩放
|
||||
function onScale() {
|
||||
this.hideEditTextBox()
|
||||
}
|
||||
|
||||
// 更新文本编辑框位置
|
||||
function updateTextEditBoxPos(g) {
|
||||
let rect = g.node.getBoundingClientRect()
|
||||
if (this.textEditNode) {
|
||||
this.textEditNode.style.minWidth = `${rect.width}px`
|
||||
this.textEditNode.style.minHeight = `${rect.height}px`
|
||||
this.textEditNode.style.left = `${rect.left}px`
|
||||
this.textEditNode.style.top = `${rect.top}px`
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏文本编辑框
|
||||
function hideEditTextBox() {
|
||||
if (!this.showTextEdit) {
|
||||
return
|
||||
}
|
||||
let { el, textNode, node, range } = this.activeOuterFrame
|
||||
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
|
||||
// 如果是默认文本,那么不保存
|
||||
let isDefaultText = str === this.mindMap.opt.defaultOuterFrameText
|
||||
str = isDefaultText ? '' : str
|
||||
this.updateActiveOuterFrame({
|
||||
text: str
|
||||
})
|
||||
this.textEditNode.style.display = 'none'
|
||||
this.textEditNode.innerHTML = ''
|
||||
this.setIsShowTextEdit(false)
|
||||
this.renderText(str, el, textNode, node, range)
|
||||
this.mindMap.emit('hide_text_edit')
|
||||
}
|
||||
|
||||
// 渲染文字
|
||||
function renderText(str, rect, textNode, node, range) {
|
||||
if (!str) return
|
||||
// 先清空文字节点原内容
|
||||
textNode.clear()
|
||||
// 创建背景矩形
|
||||
const shape = new Rect()
|
||||
textNode.add(shape)
|
||||
// 获取样式配置
|
||||
const style = this.getStyle(this.getNodeRangeFirstNode(node, range))
|
||||
const [pl, pt, pr, pb] = style.textFillPadding
|
||||
// 创建文本节点
|
||||
let textArr = str.replace(/\n$/g, '').split(/\n/gim)
|
||||
const g = new G()
|
||||
textArr.forEach((item, index) => {
|
||||
// 避免尾部的空行不占宽度,导致文本编辑框定位异常的问题
|
||||
if (item === '') {
|
||||
item = ''
|
||||
}
|
||||
let text = new Text().text(item)
|
||||
text.y(style.fontSize * style.lineHeight * index)
|
||||
this.styleText(text, style)
|
||||
g.add(text)
|
||||
})
|
||||
textNode.add(g)
|
||||
// 计算高度
|
||||
const { width: textWidth, height: textHeight } = textNode.bbox()
|
||||
const totalWidth = textWidth + pl + pr
|
||||
const totalHeight = textHeight + pt + pb
|
||||
shape.size(totalWidth, totalHeight).x(0).dy(0)
|
||||
this.styleTextShape(shape, style)
|
||||
// 设置节点位置
|
||||
let tx = 0
|
||||
switch (style.textAlign) {
|
||||
case 'left':
|
||||
tx = rect.x()
|
||||
break
|
||||
case 'center':
|
||||
tx = rect.x() + rect.width() / 2 - totalWidth / 2
|
||||
break
|
||||
case 'right':
|
||||
tx = rect.x() + rect.width() - totalWidth
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
const ty = rect.y() - totalHeight
|
||||
shape.x(tx)
|
||||
shape.y(ty)
|
||||
g.x(tx + pl)
|
||||
g.y(ty + pt)
|
||||
}
|
||||
|
||||
// 给文本背景设置样式
|
||||
function styleTextShape(shape, style) {
|
||||
shape
|
||||
.fill({
|
||||
color: style.textFill
|
||||
})
|
||||
.radius(style.textFillRadius)
|
||||
}
|
||||
|
||||
// 给文本设置样式
|
||||
function styleText(textNode, style) {
|
||||
textNode
|
||||
.fill({
|
||||
color: style.color
|
||||
})
|
||||
.css({
|
||||
'font-family': style.fontFamily,
|
||||
'font-size': style.fontSize + 'px',
|
||||
'font-weight': style.fontWeight,
|
||||
'font-style': style.fontStyle
|
||||
})
|
||||
}
|
||||
|
||||
// 获取外框文字
|
||||
function getText(node) {
|
||||
const data = node.getData('outerFrame')
|
||||
return data && data.text ? data.text : ''
|
||||
}
|
||||
|
||||
export default {
|
||||
getText,
|
||||
createText,
|
||||
styleTextShape,
|
||||
styleText,
|
||||
onScale,
|
||||
showEditTextBox,
|
||||
setIsShowTextEdit,
|
||||
removeTextEditEl,
|
||||
hideEditTextBox,
|
||||
updateTextEditBoxPos,
|
||||
renderText
|
||||
}
|
||||
122
simple-mind-map/src/plugins/outerFrame/outerFrameUtils.js
Normal file
@ -0,0 +1,122 @@
|
||||
import { getTopAncestorsFomNodeList } from '../../utils'
|
||||
|
||||
// 解析要添加外框的节点实例列表
|
||||
export const parseAddNodeList = list => {
|
||||
// 找出顶层节点
|
||||
list = getTopAncestorsFomNodeList(list)
|
||||
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
|
||||
}
|
||||
|
||||
// 解析获取节点的子节点生成的外框列表
|
||||
export 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
|
||||
}
|
||||
@ -48,7 +48,7 @@ export default {
|
||||
// 关联线激活状态的颜色
|
||||
associativeLineActiveColor: 'rgba(2, 167, 240, 1)',
|
||||
// 关联线样式
|
||||
associativeLineDasharray: [6, 4],
|
||||
associativeLineDasharray: '6,4',
|
||||
// 关联线文字颜色
|
||||
associativeLineTextColor: 'rgb(51, 51, 51)',
|
||||
// 关联线文字大小
|
||||
|
||||
@ -398,7 +398,7 @@ export const nextTick = function (fn, ctx) {
|
||||
}
|
||||
|
||||
// 检查节点是否超出画布
|
||||
export const checkNodeOuter = (mindMap, node) => {
|
||||
export const checkNodeOuter = (mindMap, node, offsetX = 0, offsetY = 0) => {
|
||||
let elRect = mindMap.elRect
|
||||
let { scaleX, scaleY, translateX, translateY } = mindMap.draw.transform()
|
||||
let { left, top, width, height } = node
|
||||
@ -408,17 +408,17 @@ export const checkNodeOuter = (mindMap, node) => {
|
||||
top = top * scaleY + translateY
|
||||
let offsetLeft = 0
|
||||
let offsetTop = 0
|
||||
if (left < 0) {
|
||||
offsetLeft = -left
|
||||
if (left < 0 + offsetX) {
|
||||
offsetLeft = -left + offsetX
|
||||
}
|
||||
if (right > elRect.width) {
|
||||
offsetLeft = -(right - elRect.width)
|
||||
if (right > elRect.width - offsetX) {
|
||||
offsetLeft = -(right - elRect.width) - offsetX
|
||||
}
|
||||
if (top < 0) {
|
||||
offsetTop = -top
|
||||
if (top < 0 + offsetY) {
|
||||
offsetTop = -top + offsetY
|
||||
}
|
||||
if (bottom > elRect.height) {
|
||||
offsetTop = -(bottom - elRect.height)
|
||||
if (bottom > elRect.height - offsetY) {
|
||||
offsetTop = -(bottom - elRect.height) - offsetY
|
||||
}
|
||||
return {
|
||||
isOuter: offsetLeft !== 0 || offsetTop !== 0,
|
||||
@ -508,7 +508,7 @@ export const loadImage = imgFile => {
|
||||
|
||||
// 移除字符串中的html实体
|
||||
export const removeHTMLEntities = str => {
|
||||
;[[' ', ' ']].forEach(item => {
|
||||
[[' ', ' ']].forEach(item => {
|
||||
str = str.replace(new RegExp(item[0], 'g'), item[1])
|
||||
})
|
||||
return str
|
||||
@ -1069,7 +1069,7 @@ export const generateColorByContent = str => {
|
||||
|
||||
// html转义
|
||||
export const htmlEscape = str => {
|
||||
;[
|
||||
[
|
||||
['&', '&'],
|
||||
['<', '<'],
|
||||
['>', '>']
|
||||
|
||||
542
web/package-lock.json
generated
@ -8,10 +8,12 @@
|
||||
"lint": "vue-cli-service lint",
|
||||
"buildLibrary": "node ./scripts/updateVersion.js && vue-cli-service build --mode library --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js && esbuild ../simple-mind-map/full.js --bundle --minify --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.min.js",
|
||||
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
|
||||
"createNodeImageList": "node ./scripts/createNodeImageList.js"
|
||||
"createNodeImageList": "node ./scripts/createNodeImageList.js",
|
||||
"ai:serve": "node ./scripts/ai.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@toast-ui/editor": "^3.1.5",
|
||||
"axios": "^1.7.9",
|
||||
"codemirror": "^5.65.16",
|
||||
"core-js": "^3.6.5",
|
||||
"element-ui": "^2.15.1",
|
||||
@ -34,6 +36,7 @@
|
||||
"esbuild": "^0.17.15",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"express": "^4.21.2",
|
||||
"less": "^3.12.2",
|
||||
"less-loader": "^7.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
|
||||
@ -62,6 +62,7 @@
|
||||
config: {},
|
||||
view: null
|
||||
},
|
||||
mindMapConfig: {},
|
||||
lang: 'zh',
|
||||
localConfig: null
|
||||
})
|
||||
@ -78,6 +79,14 @@
|
||||
window.takeOverAppMethods.saveMindMapData = data => {
|
||||
console.log(data)
|
||||
}
|
||||
// 获取思维导图配置,也就是实例化时会传入的选项
|
||||
window.takeOverAppMethods.getMindMapConfig = () => {
|
||||
return data.mindMapConfig
|
||||
}
|
||||
// 保存思维导图配置
|
||||
window.takeOverAppMethods.saveMindMapConfig = config => {
|
||||
console.log(config)
|
||||
}
|
||||
// 获取语言的函数
|
||||
window.takeOverAppMethods.getLanguage = () => {
|
||||
return data.lang
|
||||
|
||||
81
web/scripts/ai.js
Normal file
@ -0,0 +1,81 @@
|
||||
const express = require('express')
|
||||
const axios = require('axios')
|
||||
const net = require('net')
|
||||
|
||||
const port = 3456
|
||||
|
||||
const isPortUsed = port => {
|
||||
return new Promise(resolve => {
|
||||
const server = net.createServer()
|
||||
server.once('error', err => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
resolve(true) // 端口被占用
|
||||
} else {
|
||||
resolve(false) // 其他错误
|
||||
}
|
||||
})
|
||||
server.once('listening', () => {
|
||||
server.close(() => resolve(false)) // 端口可用
|
||||
})
|
||||
server.listen(port) // 尝试监听端口
|
||||
})
|
||||
}
|
||||
|
||||
const createServe = () => {
|
||||
// 起个服务
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
// 允许跨域
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*') // 允许所有来源的跨域请求,或者指定一个域名
|
||||
res.header('Access-Control-Allow-Methods', '*') // 允许的方法
|
||||
res.header('Access-Control-Allow-Headers', '*') // 允许的头部信息
|
||||
next()
|
||||
})
|
||||
|
||||
// 监听对话请求
|
||||
app.get('/ai/test', (req, res) => {
|
||||
res
|
||||
.json({
|
||||
code: 0,
|
||||
data: null,
|
||||
msg: '连接成功'
|
||||
})
|
||||
.end()
|
||||
})
|
||||
app.post('/ai/chat', async (req, res, next) => {
|
||||
// 设置SSE响应头
|
||||
res.setHeader('Content-Type', 'text/event-stream')
|
||||
res.setHeader('Cache-Control', 'no-cache')
|
||||
res.setHeader('Connection', 'keep-alive')
|
||||
|
||||
const { api, method = 'POST', headers = {}, data } = req.body
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: api,
|
||||
method,
|
||||
headers,
|
||||
data,
|
||||
responseType: 'stream'
|
||||
})
|
||||
response.data.pipe(res)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`app listening on port ${port}`)
|
||||
})
|
||||
}
|
||||
|
||||
isPortUsed(port).then(isUsed => {
|
||||
if (isUsed) {
|
||||
console.error('端口被占用')
|
||||
} else {
|
||||
createServe()
|
||||
}
|
||||
})
|
||||
@ -6,12 +6,11 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {}
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style lang="less">
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@ -19,8 +18,6 @@ export default {
|
||||
}
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
@ -42,4 +39,8 @@ export default {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog{
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,21 +4,20 @@ import Vue from 'vue'
|
||||
import vuexStore from '@/store'
|
||||
|
||||
const SIMPLE_MIND_MAP_DATA = 'SIMPLE_MIND_MAP_DATA'
|
||||
const SIMPLE_MIND_MAP_CONFIG = 'SIMPLE_MIND_MAP_CONFIG'
|
||||
const SIMPLE_MIND_MAP_LANG = 'SIMPLE_MIND_MAP_LANG'
|
||||
const SIMPLE_MIND_MAP_LOCAL_CONFIG = 'SIMPLE_MIND_MAP_LOCAL_CONFIG'
|
||||
|
||||
let mindMapData = null
|
||||
|
||||
/**
|
||||
* @Author: 王林
|
||||
* @Date: 2021-08-01 10:10:49
|
||||
* @Desc: 获取缓存的思维导图数据
|
||||
*/
|
||||
// 获取缓存的思维导图数据
|
||||
export const getData = () => {
|
||||
// 接管模式
|
||||
if (window.takeOverApp) {
|
||||
mindMapData = window.takeOverAppMethods.getMindMapData()
|
||||
return mindMapData
|
||||
}
|
||||
// 操作本地文件模式
|
||||
if (vuexStore.state.isHandleLocalFile) {
|
||||
return Vue.prototype.getCurrentData()
|
||||
}
|
||||
@ -34,11 +33,7 @@ export const getData = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: 王林
|
||||
* @Date: 2021-08-01 10:14:28
|
||||
* @Desc: 存储思维导图数据
|
||||
*/
|
||||
// 存储思维导图数据
|
||||
export const storeData = data => {
|
||||
try {
|
||||
let originData = null
|
||||
@ -47,39 +42,12 @@ export const storeData = data => {
|
||||
} else {
|
||||
originData = getData()
|
||||
}
|
||||
originData.root = data
|
||||
if (window.takeOverApp) {
|
||||
mindMapData = originData
|
||||
window.takeOverAppMethods.saveMindMapData(originData)
|
||||
return
|
||||
}
|
||||
Vue.prototype.$bus.$emit('write_local_file', originData)
|
||||
if (vuexStore.state.isHandleLocalFile) {
|
||||
return
|
||||
}
|
||||
let dataStr = JSON.stringify(originData)
|
||||
localStorage.setItem(SIMPLE_MIND_MAP_DATA, dataStr)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: 王林
|
||||
* @Date: 2021-08-01 10:24:56
|
||||
* @Desc: 存储思维导图配置数据
|
||||
*/
|
||||
export const storeConfig = config => {
|
||||
try {
|
||||
let originData = null
|
||||
if (window.takeOverApp) {
|
||||
originData = mindMapData
|
||||
} else {
|
||||
originData = getData()
|
||||
if (!originData) {
|
||||
originData = {}
|
||||
}
|
||||
originData = {
|
||||
...originData,
|
||||
...config
|
||||
...data
|
||||
}
|
||||
if (window.takeOverApp) {
|
||||
mindMapData = originData
|
||||
@ -90,19 +58,42 @@ export const storeConfig = config => {
|
||||
if (vuexStore.state.isHandleLocalFile) {
|
||||
return
|
||||
}
|
||||
let dataStr = JSON.stringify(originData)
|
||||
localStorage.setItem(SIMPLE_MIND_MAP_DATA, dataStr)
|
||||
localStorage.setItem(SIMPLE_MIND_MAP_DATA, JSON.stringify(originData))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
if ('exceeded') {
|
||||
Vue.prototype.$bus.$emit('localStorageExceeded')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取思维导图配置数据
|
||||
export const getConfig = () => {
|
||||
if (window.takeOverApp) {
|
||||
window.takeOverAppMethods.getMindMapConfig()
|
||||
return
|
||||
}
|
||||
let config = localStorage.getItem(SIMPLE_MIND_MAP_CONFIG)
|
||||
if (config) {
|
||||
return JSON.parse(config)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 存储思维导图配置数据
|
||||
export const storeConfig = config => {
|
||||
try {
|
||||
if (window.takeOverApp) {
|
||||
window.takeOverAppMethods.saveMindMapConfig(config)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(SIMPLE_MIND_MAP_CONFIG, JSON.stringify(config))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* javascript comment
|
||||
* @Author: 王林
|
||||
* @Date: 2022-11-05 14:36:50
|
||||
* @Desc: 存储语言
|
||||
*/
|
||||
// 存储语言
|
||||
export const storeLang = lang => {
|
||||
if (window.takeOverApp) {
|
||||
window.takeOverAppMethods.saveLanguage(lang)
|
||||
@ -111,12 +102,7 @@ export const storeLang = lang => {
|
||||
localStorage.setItem(SIMPLE_MIND_MAP_LANG, lang)
|
||||
}
|
||||
|
||||
/**
|
||||
* javascript comment
|
||||
* @Author: 王林
|
||||
* @Date: 2022-11-05 14:37:36
|
||||
* @Desc: 获取存储的语言
|
||||
*/
|
||||
// 获取存储的语言
|
||||
export const getLang = () => {
|
||||
if (window.takeOverApp) {
|
||||
return window.takeOverAppMethods.getLanguage() || 'zh'
|
||||
@ -129,12 +115,7 @@ export const getLang = () => {
|
||||
return 'zh'
|
||||
}
|
||||
|
||||
/**
|
||||
* javascript comment
|
||||
* @Author: 王林25
|
||||
* @Date: 2022-11-14 18:57:31
|
||||
* @Desc: 存储本地配置
|
||||
*/
|
||||
// 存储本地配置
|
||||
export const storeLocalConfig = config => {
|
||||
if (window.takeOverApp) {
|
||||
return window.takeOverAppMethods.saveLocalConfig(config)
|
||||
@ -142,12 +123,7 @@ export const storeLocalConfig = config => {
|
||||
localStorage.setItem(SIMPLE_MIND_MAP_LOCAL_CONFIG, JSON.stringify(config))
|
||||
}
|
||||
|
||||
/**
|
||||
* javascript comment
|
||||
* @Author: 王林25
|
||||
* @Date: 2022-11-14 18:57:37
|
||||
* @Desc: 获取本地配置
|
||||
*/
|
||||
// 获取本地配置
|
||||
export const getLocalConfig = () => {
|
||||
if (window.takeOverApp) {
|
||||
return window.takeOverAppMethods.getLocalConfig()
|
||||
|
||||
BIN
web/src/assets/.DS_Store
vendored
BIN
web/src/assets/avatar/hi.jpg
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
web/src/assets/avatar/旋风.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
web/src/assets/avatar/星夜寒.jpg
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
web/src/assets/avatar/神话.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
web/src/assets/avatar/行.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 2479351 */
|
||||
src: url('iconfont.woff2?t=1739152990179') format('woff2'),
|
||||
url('iconfont.woff?t=1739152990179') format('woff'),
|
||||
url('iconfont.ttf?t=1739152990179') format('truetype');
|
||||
src: url('iconfont.woff2?t=1739843331607') format('woff2'),
|
||||
url('iconfont.woff?t=1739843331607') format('woff'),
|
||||
url('iconfont.ttf?t=1739843331607') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@ -13,6 +13,10 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.iconAIshengcheng:before {
|
||||
content: "\e6b5";
|
||||
}
|
||||
|
||||
.iconprinting:before {
|
||||
content: "\ea28";
|
||||
}
|
||||
|
||||
BIN
web/src/assets/img/foramt/1.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
web/src/assets/img/foramt/10.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
web/src/assets/img/foramt/2.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
web/src/assets/img/foramt/3.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
web/src/assets/img/foramt/4.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
web/src/assets/img/foramt/5.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
web/src/assets/img/foramt/6.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
web/src/assets/img/foramt/7.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
web/src/assets/img/foramt/8.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
web/src/assets/img/foramt/9.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
web/src/assets/img/gzh.jpeg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
web/src/assets/img/structures/catalogOrganization.jpg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
BIN
web/src/assets/img/structures/fishbone.jpg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |