This commit is contained in:
街角小林 2025-02-20 08:48:19 +08:00
commit bcffecab40
79 changed files with 3589 additions and 750 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@ node_modules
.DS_Store
dist_electron
simple-mind-map/dist
simple-mind-map/types
simple-mind-map/types
utools/dist

View File

@ -7,9 +7,9 @@
[![GitHub stars](https://img.shields.io/github/stars/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/wanglin2/mind-map)](https://github.com/wanglin2/mind-map/network/members)
> 中文名:思绪思维导图。一个简单&强大的 Web 思维导图。
> 中文名:思绪思维导图。一个简单&强大的 Web 思维导图库和思维导图软件
本项目包含两部分
本项目主要包含以下内容
1.一个 js 思维导图库,不依赖任何框架,可以使用它来快速完成 Web 思维导图产品的开发。
@ -19,13 +19,15 @@
在线地址:[https://wanglin2.github.io/mind-map/](https://wanglin2.github.io/mind-map/)。
此外也提供了客户端可供下载使用,支持`Windows`、`Mac`及`Linux`,下载地址:
3.此外也支持以客户端的方式使用,现已上架[uTools](https://www.u.tools/)插件应用市场,强烈建议通过`uTools`来体验。
Github[releases](https://github.com/wanglin2/mind-map/releases)。百度云盘:[地址](https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3)
可直接在`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/),点击右侧的【启动】按钮进行安装
> 客户端版本会落后于在线版本,尝试最新功能请优先使用在线版。
> 独立客户端下载Github[releases](https://github.com/wanglin2/mind-map/releases)。百度云盘:[地址](https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3)。
>
> 后续不会投入太多精力在独立客户端上,建议通过`uTools`来使用,功能更强,体验更好。
【云存储版本】如果你需要带后端的云存储版本,可以尝试我们开发的另一个项目[理想文档](https://github.com/wanglin2/lx-doc)。
4.【云存储版本】如果你需要带后端的云存储版本,可以尝试我们开发的另一个项目[理想文档](https://github.com/wanglin2/lx-doc)。
# 特性
@ -44,7 +46,7 @@ Github[releases](https://github.com/wanglin2/mind-map/releases)。百度云
官方提供了如下插件,可根据需求按需引入(某个功能不生效大概率是因为你没有引入对应的插件),具体使用方式请查看文档:
> RichText节点富文本插件、Select鼠标多选节点插件、Drag节点拖拽插件、AssociativeLine关联线插件、Export导出插件、KeyboardNavigation键盘导航插件、MiniMap小地图插件、Watermark水印插件、TouchEvent移动端触摸事件支持插件、NodeImgAdjust拖拽调整节点图片大小插件、Search搜索插件、Painter节点格式刷插件、Scrollbar滚动条插件、Formula数学公式插件、Cooperate协同编辑插件、RainbowLines彩虹线条插件、Demonstrate演示模式插件、OuterFrame外框插件、MindMapLayoutPro思维导图布局插件、HandDrawnLikeStyle手绘风格插件[收费]、Notation节点标记插件[收费]、Numbers节点编号插件[收费]、FreemindFreemind格式导入导出插件[收费]、ExcelExcel格式导入导出插件[收费]、Checkbox待办插件[收费]、Lineflow节点连线流动插件[收费]
> RichText节点富文本插件、Select鼠标多选节点插件、Drag节点拖拽插件、AssociativeLine关联线插件、Export导出插件、KeyboardNavigation键盘导航插件、MiniMap小地图插件、Watermark水印插件、TouchEvent移动端触摸事件支持插件、NodeImgAdjust拖拽调整节点图片大小插件、Search搜索插件、Painter节点格式刷插件、Scrollbar滚动条插件、Formula数学公式插件、Cooperate协同编辑插件、RainbowLines彩虹线条插件、Demonstrate演示模式插件、OuterFrame外框插件、MindMapLayoutPro思维导图布局插件、HandDrawnLikeStyle手绘风格插件[收费]、Notation节点标记插件[收费]、Numbers节点编号插件[收费]、FreemindFreemind格式导入导出插件[收费]、ExcelExcel格式导入导出插件[收费]、Checkbox待办插件[收费]、Lineflow节点连线流动插件[收费]、Momentum动量效果插件[收费]
本项目不会实现的特性:
@ -103,6 +105,8 @@ const mindMap = new MindMap({
微信添加`wanglinguanfang`拉你入群。根据过往的经验大部分问题都可以通过查看issue列表或文档解决所以提问前请确保你已经阅读完了所有文档文档里没有的可在群里提问不必私聊作者如果你一定要私聊请先发红包¥9.9+每次)。
如果你在杭州,也欢迎来找我面基。
# star
如果喜欢本项目,欢迎点个 star这对我们很重要。
@ -153,6 +157,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>
@ -929,5 +940,12 @@ const mindMap = new MindMap({
<sub style="font-size:14px"><b>好好先生Ervin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 75.0; height: 75.0">
<a href="#">
<img src="./web/src/assets/avatar/胡永刚.jpg" width="50;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px"/>
<br />
<sub style="font-size:14px"><b>胡永刚</b></sub>
</a>
</td>
</tr>
</table>

2
dist/css/app.css vendored
View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

2
dist/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@
})
} catch (error) {
console.log(error)
}</script><link href="dist/css/chunk-vendors.css?74531016fcc3dfd0eba4" rel="stylesheet"><link href="dist/css/app.css?74531016fcc3dfd0eba4" 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?137a23e103fd70ffd3cb" rel="stylesheet"><link href="dist/css/app.css?137a23e103fd70ffd3cb" 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({
@ -74,4 +74,4 @@
// 可以通过window.$bus.$on()来监听应用的一些事件
// 实例化页面
window.initApp()
}</script><script src="dist/js/chunk-vendors.js?fc8e52cca177f49cac0d"></script><script src="dist/js/app.js?fc8e52cca177f49cac0d"></script></body></html>
}</script><script src="dist/js/chunk-vendors.js?137a23e103fd70ffd3cb"></script><script src="dist/js/app.js?137a23e103fd70ffd3cb"></script></body></html>

View File

@ -30,7 +30,7 @@ MindMap.markdown = markdown
MindMap.iconList = icons.nodeIconList
MindMap.constants = constants
MindMap.defaultTheme = defaultTheme
MindMap.version = '0.13.0'
MindMap.version = '0.13.1-fix.1'
MindMap.usePlugin(MiniMap)
.usePlugin(Watermark)

View File

@ -20,7 +20,8 @@ import {
isUndef,
handleGetSvgDataExtraContent,
getNodeTreeBoundingRect,
mergeTheme
mergeTheme,
createUidForAppointNodes
} from './src/utils'
import defaultTheme, {
checkIsNodeSizeIndependenceConfig
@ -149,6 +150,8 @@ class MindMap {
if (data.data && !data.data.expand) {
data.data.expand = true
}
// 给没有uid的节点添加uid
createUidForAppointNodes([data], false, null, true)
return data
}
@ -395,6 +398,7 @@ class MindMap {
// 更新画布数据,如果新的数据是在当前画布节点数据基础上增删改查后形成的,那么可以使用该方法来更新画布数据
updateData(data) {
data = this.handleData(data)
this.emit('before_update_data', data)
this.renderer.setData(data)
this.render()
@ -583,7 +587,7 @@ class MindMap {
this.watermark.isInExport = false
}
// 添加必要的样式
[this.joinCss(), ...cssTextList].forEach(s => {
;[this.joinCss(), ...cssTextList].forEach(s => {
clone.add(SVG(`<style>${s}</style>`))
})
// 附加内容

View File

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

View File

@ -302,6 +302,27 @@ export const defaultOpt = {
},
// 自定义快捷创建子节点按钮的点击操作,
customQuickCreateChildBtnClick: null,
// 添加自定义的节点内容
// 可传递一个对象,格式如下:
/*
{
// 返回要添加的DOM元素详细
create: (node) => {
return {
el, // DOM节点
width: 20, // 宽高
height: 20
}
},
// 处理生成的@svgdotjs/svg.js库的ForeignObject节点实例可以设置其在节点内的位置
handle: ({ content, element, node }) => {
}
}
*/
addCustomContentToNode: null,
// 节点连线样式是否允许继承祖先的连线样式
enableInheritAncestorLineStyle: true,
// 【Select插件】
// 多选节点时鼠标移动到边缘时的画布移动偏移量
@ -492,5 +513,10 @@ export const defaultOpt = {
maxImgResizeWidthInheritTheme: false,
// 最大允许缩放的尺寸maxImgResizeWidthInheritTheme选项设置为false时生效不限制最大值可传递Infinity
maxImgResizeWidth: Infinity,
maxImgResizeHeight: Infinity
maxImgResizeHeight: Infinity,
// 自定义删除按钮和尺寸调整按钮的内容
// 默认为内置图标你可以传递一个svg字符串或者其他的html字符串
// 整体大小请使用上面的minImgResizeWidth和minImgResizeHeight选项设置
customDeleteBtnInnerHTML: '',
customResizeBtnInnerHTML: ''
}

View File

@ -99,6 +99,7 @@ class MindMapNode {
this._generalizationList = []
this._unVisibleRectRegionNode = null
this._isMouseenter = false
this._customContentAddToNodeAdd = null
// 尺寸信息
this._rectInfo = {
textContentWidth: 0,
@ -216,7 +217,8 @@ class MindMapNode {
isUseCustomNodeContent,
customCreateNodeContent,
createNodePrefixContent,
createNodePostfixContent
createNodePostfixContent,
addCustomContentToNode
} = this.mindMap.opt
// 需要创建的内容类型
const typeList = [
@ -289,6 +291,18 @@ class MindMapNode {
addXmlns(this._postfixData.el)
}
}
if (
addCustomContentToNode &&
typeof addCustomContentToNode.create === 'function'
) {
this._customContentAddToNodeAdd = addCustomContentToNode.create(this)
if (
this._customContentAddToNodeAdd &&
this._customContentAddToNodeAdd.el
) {
addXmlns(this._customContentAddToNodeAdd.el)
}
}
}
// 计算节点的宽高
@ -872,15 +886,18 @@ class MindMapNode {
// 设置连线样式
styleLine(line, childNode, enableMarker) {
const { enableInheritAncestorLineStyle } = this.mindMap.opt
const getName = enableInheritAncestorLineStyle
? 'getSelfInhertStyle'
: 'getSelfStyle'
const width =
childNode.getSelfInhertStyle('lineWidth') ||
childNode.getStyle('lineWidth', true)
childNode[getName]('lineWidth') || childNode.getStyle('lineWidth', true)
const color =
childNode.getSelfInhertStyle('lineColor') ||
childNode[getName]('lineColor') ||
this.getRainbowLineColor(childNode) ||
childNode.getStyle('lineColor', true)
const dasharray =
childNode.getSelfInhertStyle('lineDasharray') ||
childNode[getName]('lineDasharray') ||
childNode.getStyle('lineDasharray', true)
this.style.line(
line,

View File

@ -177,10 +177,15 @@ function layout() {
const {
hoverRectPadding,
openRealtimeRenderOnNodeTextEdit,
textContentMargin
textContentMargin,
addCustomContentToNode
} = this.mindMap.opt
// 避免编辑过程中展开收起按钮闪烁的问题
if (openRealtimeRenderOnNodeTextEdit && this._expandBtn) {
if (
openRealtimeRenderOnNodeTextEdit &&
this._expandBtn &&
this.getChildrenLength() > 0
) {
this.group.add(this._expandBtn)
}
const { width, height } = this
@ -428,6 +433,22 @@ function layout() {
}
textContentNested.translate(translateX, translateY)
addHoverNode()
if (this._customContentAddToNodeAdd && this._customContentAddToNodeAdd.el) {
const foreignObject = createForeignObjectNode(
this._customContentAddToNodeAdd
)
this.group.add(foreignObject)
if (
addCustomContentToNode &&
typeof addCustomContentToNode.handle === 'function'
) {
addCustomContentToNode.handle({
content: this._customContentAddToNodeAdd,
element: foreignObject,
node: this
})
}
}
this.mindMap.emit('node_layout_end', this)
}

View File

@ -2,13 +2,14 @@ import btnsSvg from '../../../svg/btns'
import { SVG, Circle, G } from '@svgdotjs/svg.js'
function initQuickCreateChildBtn() {
if (this.isGeneralization) return
this._quickCreateChildBtn = null
this._showQuickCreateChildBtn = false
}
// 显示按钮
function showQuickCreateChildBtn() {
if (this.getChildrenLength() > 0) return
if (this.isGeneralization || this.getChildrenLength() > 0) return
// 创建按钮
if (this._quickCreateChildBtn) {
this.group.add(this._quickCreateChildBtn)
@ -63,6 +64,7 @@ function showQuickCreateChildBtn() {
// 移除按钮
function removeQuickCreateChildBtn() {
if (this.isGeneralization) return
if (this._quickCreateChildBtn && this._showQuickCreateChildBtn) {
this._quickCreateChildBtn.remove()
this._showQuickCreateChildBtn = false
@ -71,6 +73,7 @@ function removeQuickCreateChildBtn() {
// 隐藏按钮
function hideQuickCreateChildBtn() {
if (this.isGeneralization) return
const { isActive } = this.getData()
if (!isActive) {
this.removeQuickCreateChildBtn()

View File

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

View File

@ -122,7 +122,11 @@ class NodeImgAdjust {
// 创建调整按钮元素
createResizeBtnEl() {
const { imgResizeBtnSize } = this.mindMap.opt
const {
imgResizeBtnSize,
customResizeBtnInnerHTML,
customDeleteBtnInnerHTML
} = this.mindMap.opt
// 容器元素
this.handleEl = document.createElement('div')
this.handleEl.style.cssText = `
@ -134,7 +138,7 @@ class NodeImgAdjust {
this.handleEl.className = 'node-img-handle'
// 调整按钮元素
const btnEl = document.createElement('div')
btnEl.innerHTML = btnsSvg.imgAdjust
btnEl.innerHTML = customResizeBtnInnerHTML || btnsSvg.imgAdjust
btnEl.style.cssText = `
position: absolute;
right: 0;
@ -179,7 +183,7 @@ class NodeImgAdjust {
const btnRemove = document.createElement('div')
this.handleEl.prepend(btnRemove)
btnRemove.className = 'node-image-remove'
btnRemove.innerHTML = btnsSvg.remove
btnRemove.innerHTML = customDeleteBtnInnerHTML || btnsSvg.remove
btnRemove.style.cssText = `
position: absolute;
right: 0;top:0;color:#fff;

View File

@ -820,6 +820,7 @@ class RichText {
// 处理导入数据
handleSetData(data) {
if (!data) return
// 短期处理,为了兼容老数据,长期会去除
const isOldRichTextVersion =
!data.smmVersion || compareVersion(data.smmVersion, '0.13.0') === '<'

View File

@ -182,6 +182,10 @@ class Search {
const uid = this.isNodeInstance(currentNode)
? currentNode.getData('uid')
: currentNode.data.uid
if (!uid) {
callback()
return
}
const targetNode = this.mindMap.renderer.findNodeByUid(uid)
this.mindMap.execCommand('GO_TARGET_NODE', uid, node => {
if (!this.isNodeInstance(currentNode)) {

View File

@ -95,7 +95,7 @@ export default {
// 点鼠标hover和激活时显示的矩形边框的圆角大小
hoverRectRadius: 5,
// 文本对齐
align: 'left',
textAlign: 'left',// right、center、justify、left
// 图片放置位置,相对于整个文本内容
imgPlacement: 'top', // left、right、bottom、top
// 标签放置位置

View File

@ -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,
@ -1002,7 +1002,8 @@ export const addDataToAppointNodes = (appointNodes, data = {}) => {
export const createUidForAppointNodes = (
appointNodes,
createNewId = false,
handle = null
handle = null,
handleGeneralization = false
) => {
const walk = list => {
list.forEach(node => {
@ -1012,6 +1013,14 @@ export const createUidForAppointNodes = (
if (createNewId || isUndef(node.data.uid)) {
node.data.uid = createUid()
}
if (handleGeneralization) {
const generalizationList = formatGetNodeGeneralization(node.data)
generalizationList.forEach(gNode => {
if (createNewId || isUndef(gNode.uid)) {
gNode.uid = createUid()
}
})
}
handle && handle(node)
if (node.children && node.children.length > 0) {
walk(node.children)

491
web/package-lock.json generated
View File

@ -1,15 +1,16 @@
{
"name": "thoughts",
"version": "0.11.2",
"version": "0.12.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "thoughts",
"version": "0.11.2",
"version": "0.12.1",
"hasInstallScript": true,
"dependencies": {
"@toast-ui/editor": "^3.1.5",
"axios": "^1.7.9",
"codemirror": "^5.65.16",
"core-js": "^3.6.5",
"electron-json-storage": "^4.6.0",
@ -37,6 +38,7 @@
"esbuild": "^0.17.15",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"express": "^4.21.2",
"hasown": "^2.0.2",
"less": "^3.12.2",
"less-loader": "^7.1.0",
@ -4152,8 +4154,7 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/at-least-node": {
"version": "1.0.0",
@ -4227,6 +4228,30 @@
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==",
"dev": true
},
"node_modules/axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/babel-eslint": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
@ -4443,9 +4468,9 @@
"dev": true
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dev": true,
"dependencies": {
"bytes": "3.1.2",
@ -4456,7 +4481,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@ -4482,12 +4507,12 @@
"dev": true
},
"node_modules/body-parser/node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dev": true,
"dependencies": {
"side-channel": "^1.0.4"
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
@ -5212,6 +5237,18 @@
"node": ">= 0.4"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
@ -5790,7 +5827,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@ -5995,9 +6031,9 @@
"dev": true
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"dev": true,
"engines": {
"node": ">= 0.6"
@ -7087,7 +7123,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@ -7435,6 +7470,19 @@
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==",
"dev": true
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -8008,9 +8056,9 @@
}
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true,
"engines": {
"node": ">= 0.8"
@ -8164,13 +8212,9 @@
"dev": true
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
@ -8179,7 +8223,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@ -8188,7 +8231,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0"
},
@ -8197,14 +8239,14 @@
}
},
"node_modules/es-set-tostringtag": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
"dev": true,
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"get-intrinsic": "^1.2.4",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.1"
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@ -8753,37 +8795,37 @@
"dev": true
},
"node_modules/express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.2",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@ -8792,6 +8834,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/debug": {
@ -8810,12 +8856,12 @@
"dev": true
},
"node_modules/express/node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dev": true,
"dependencies": {
"side-channel": "^1.0.4"
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
@ -9159,13 +9205,13 @@
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dev": true,
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@ -9264,7 +9310,6 @@
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"engines": {
"node": ">=4.0"
}
@ -9525,8 +9570,7 @@
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"node_modules/function.prototype.name": {
"version": "1.1.6",
@ -9574,16 +9618,32 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dev": true,
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.0",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@ -9777,7 +9837,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.1.3"
}
@ -9920,10 +9979,9 @@
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
}
@ -9932,7 +9990,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@ -10021,7 +10078,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@ -12101,6 +12157,14 @@
"node": ">=10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -12144,10 +12208,13 @@
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
"dev": true
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge-source-map": {
"version": "1.1.0",
@ -12323,7 +12390,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@ -12332,7 +12398,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@ -13676,9 +13741,9 @@
"dev": true
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"dev": true
},
"node_modules/path-type": {
@ -14683,6 +14748,11 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@ -15582,9 +15652,9 @@
}
},
"node_modules/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dev": true,
"dependencies": {
"debug": "2.6.9",
@ -15620,6 +15690,15 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"dev": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -15737,15 +15816,15 @@
}
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dev": true,
"dependencies": {
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
@ -23640,8 +23719,7 @@
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"at-least-node": {
"version": "1.0.0",
@ -23699,6 +23777,29 @@
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==",
"dev": true
},
"axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
},
"dependencies": {
"form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
}
}
}
},
"babel-eslint": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
@ -23898,9 +23999,9 @@
"dev": true
},
"body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dev": true,
"requires": {
"bytes": "3.1.2",
@ -23911,7 +24012,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@ -23933,12 +24034,12 @@
"dev": true
},
"qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dev": true,
"requires": {
"side-channel": "^1.0.4"
"side-channel": "^1.0.6"
}
}
}
@ -24562,6 +24663,15 @@
"set-function-length": "^1.2.1"
}
},
"call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
}
},
"call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
@ -25032,7 +25142,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
@ -25210,9 +25319,9 @@
"dev": true
},
"cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"dev": true
},
"cookie-signature": {
@ -26098,8 +26207,7 @@
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"depd": {
"version": "2.0.0",
@ -26394,6 +26502,16 @@
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==",
"dev": true
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
}
},
"duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -26869,9 +26987,9 @@
"dev": true
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true
},
"end-of-stream": {
@ -27006,38 +27124,32 @@
"dev": true
},
"es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dev": true,
"requires": {
"get-intrinsic": "^1.2.4"
}
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-object-atoms": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
"dev": true,
"requires": {
"es-errors": "^1.3.0"
}
},
"es-set-tostringtag": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
"dev": true,
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"requires": {
"get-intrinsic": "^1.2.4",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.1"
"hasown": "^2.0.2"
}
},
"es-to-primitive": {
@ -27474,37 +27586,37 @@
}
},
"express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.2",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@ -27528,12 +27640,12 @@
"dev": true
},
"qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dev": true,
"requires": {
"side-channel": "^1.0.4"
"side-channel": "^1.0.6"
}
},
"safe-buffer": {
@ -27823,13 +27935,13 @@
}
},
"finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dev": true,
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@ -27916,8 +28028,7 @@
"follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
},
"for-each": {
"version": "0.3.3",
@ -28119,8 +28230,7 @@
"function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"function.prototype.name": {
"version": "1.1.6",
@ -28159,16 +28269,29 @@
"dev": true
},
"get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dev": true,
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.0",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}
},
"get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
}
},
"get-stream": {
@ -28322,7 +28445,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dev": true,
"requires": {
"get-intrinsic": "^1.1.3"
}
@ -28440,16 +28562,14 @@
"dev": true
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"requires": {
"has-symbols": "^1.0.3"
}
@ -28522,7 +28642,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"requires": {
"function-bind": "^1.1.2"
}
@ -30208,6 +30327,11 @@
}
}
},
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -30248,9 +30372,9 @@
}
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"dev": true
},
"merge-source-map": {
@ -30398,14 +30522,12 @@
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"requires": {
"mime-db": "1.52.0"
}
@ -31523,9 +31645,9 @@
"dev": true
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"dev": true
},
"path-type": {
@ -32399,6 +32521,11 @@
"ipaddr.js": "1.9.1"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@ -33170,9 +33297,9 @@
}
},
"send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dev": true,
"requires": {
"debug": "2.6.9",
@ -33207,6 +33334,12 @@
}
}
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"dev": true
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -33312,15 +33445,15 @@
}
},
"serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dev": true,
"requires": {
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
"send": "0.19.0"
}
},
"set-blocking": {

View File

@ -18,11 +18,13 @@
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps",
"createNodeImageList": "node ./scripts/createNodeImageList.js"
"createNodeImageList": "node ./scripts/createNodeImageList.js",
"ai:serve": "node ./scripts/ai.js"
},
"main": "background.js",
"dependencies": {
"@toast-ui/editor": "^3.1.5",
"axios": "^1.7.9",
"codemirror": "^5.65.16",
"core-js": "^3.6.5",
"electron-json-storage": "^4.6.0",
@ -50,6 +52,7 @@
"esbuild": "^0.17.15",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"express": "^4.21.2",
"hasown": "^2.0.2",
"less": "^3.12.2",
"less-loader": "^7.1.0",

View File

@ -15,22 +15,6 @@
// 接管应用
window.takeOverApp = false
</script>
<script
charset="UTF-8"
id="LA_COLLECT"
src="//sdk.51.la/js-sdk-pro.min.js"
></script>
<script>
try {
LA.init({
id: 'KRO0WxK8GT66tYCQ',
ck: 'KRO0WxK8GT66tYCQ',
autoTrack: false
})
} catch (error) {
console.log(error)
}
</script>
</head>
<body>
<noscript>

55
web/scripts/ai.js Normal file
View File

@ -0,0 +1,55 @@
const express = require('express')
let axios = require('axios')
axios = typeof axios === 'function' ? axios : axios.default
const port = 3456
// 起个服务
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}`)
})

View File

@ -11,7 +11,7 @@ export default {
}
</script>
<style>
<style lang="less">
* {
margin: 0;
padding: 0;
@ -23,4 +23,24 @@ export default {
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
.customScrollbar,
.el-table--scrollable-y .el-table__body-wrapper {
&::-webkit-scrollbar {
width: 7px;
height: 7px;
}
&::-webkit-scrollbar-thumb {
border-radius: 7px;
background-color: rgba(0, 0, 0, 0.3);
cursor: pointer;
}
&::-webkit-scrollbar-track {
box-shadow: none;
background: transparent;
display: none;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2479351 */
src: url('iconfont.woff2?t=1737722825571') format('woff2'),
url('iconfont.woff?t=1737722825571') format('woff'),
url('iconfont.ttf?t=1737722825571') 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,18 @@
-moz-osx-font-smoothing: grayscale;
}
.iconAIshengcheng:before {
content: "\e6b5";
}
.iconprinting:before {
content: "\ea28";
}
.iconwenjianjia:before {
content: "\e614";
}
.iconcontentleft:before {
content: "\e8c9";
}

View File

@ -5,6 +5,7 @@ import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import path from 'path'
import { bindFileHandleEvent } from './electron/fileHandle'
import { bindOtherHandleEvent } from './electron/otherHandle'
import '../scripts/ai'
const isDevelopment = process.env.NODE_ENV !== 'production'

View File

@ -444,6 +444,11 @@ export const sidebarTriggerList = [
value: 'setting',
icon: 'iconshezhi'
},
{
name: 'AI',
value: 'ai',
icon: 'iconAIshengcheng'
},
{
name: 'ShortcutKey',
value: 'shortcutKey',
@ -457,19 +462,20 @@ export const downTypeList = [
name: 'Dedicated file',
type: 'smm',
icon: 'iconwenjian',
desc: 'Available for import'
desc:
'SimpleMindMap private format, can be used for re import, and the client can directly edit it'
},
{
name: 'JSON',
type: 'json',
icon: 'iconjson',
desc: 'Popular data exchange formats, Available for import'
desc: 'Popular data exchange format that can be used for re importing'
},
{
name: 'Image',
type: 'png',
icon: 'iconPNG',
desc: 'Suitable for viewing and sharing'
desc: 'Common image formats, suitable for viewing and sharing'
},
{
name: 'SVG',
@ -481,19 +487,19 @@ export const downTypeList = [
name: 'PDF',
type: 'pdf',
icon: 'iconpdf',
desc: 'Suitable for printing'
desc: 'Suitable for viewing, browsing, and printing'
},
{
name: 'Markdown',
type: 'md',
icon: 'iconmarkdown',
desc: 'Easy for other software to open'
desc: 'MD text format, easy for other software to open'
},
{
name: 'XMind',
type: 'xmind',
icon: 'iconxmind',
desc: 'XMind file'
desc: 'XMind software file'
},
{
name: 'Txt',
@ -511,7 +517,7 @@ export const downTypeList = [
name: 'Excel',
type: 'xlsx',
icon: 'iconfile-excel',
desc: 'Excel software format'
desc: 'Table text format, editable with Excel software'
}
]
@ -641,4 +647,4 @@ export const alignList = [
name: 'Align right',
value: 'right'
}
]
]

View File

@ -534,6 +534,11 @@ export const sidebarTriggerList = [
value: 'outline',
icon: 'iconfuhao-dagangshu'
},
{
name: 'AI',
value: 'ai',
icon: 'iconAIshengcheng'
},
{
name: '设置',
value: 'setting',
@ -552,19 +557,19 @@ export const downTypeList = [
name: '专有文件',
type: 'smm',
icon: 'iconwenjian',
desc: '可用于导入'
desc: 'SimpleMindMap私有格式可用于再次导入,客户端可直接编辑'
},
{
name: 'JSON',
type: 'json',
icon: 'iconjson',
desc: '流行的数据交换格式,可用于导入'
desc: '流行的数据交换格式,可用于再次导入'
},
{
name: '图片',
type: 'png',
icon: 'iconPNG',
desc: '适合查看分享'
desc: '常用图片格式,适合查看分享'
},
{
name: 'SVG',
@ -576,19 +581,19 @@ export const downTypeList = [
name: 'PDF',
type: 'pdf',
icon: 'iconpdf',
desc: '适合打印'
desc: '适合查看浏览和打印'
},
{
name: 'Markdown',
type: 'md',
icon: 'iconmarkdown',
desc: '便于其他软件打开'
desc: 'md文本格式便于其他软件打开'
},
{
name: 'XMind',
type: 'xmind',
icon: 'iconxmind',
desc: 'XMind格式'
desc: 'XMind软件格式'
},
{
name: 'Txt',
@ -606,7 +611,7 @@ export const downTypeList = [
name: 'Excel',
type: 'xlsx',
icon: 'iconfile-excel',
desc: 'Excel软件格式'
desc: '表格文本形式可用Excel软件编辑'
}
]

View File

@ -439,6 +439,11 @@ export const sidebarTriggerList = [
value: 'outline',
icon: 'iconfuhao-dagangshu'
},
{
name: 'AI',
value: 'ai',
icon: 'iconAIshengcheng'
},
{
name: '設置',
value: 'setting',
@ -457,61 +462,61 @@ export const downTypeList = [
name: '專用檔案',
type: 'smm',
icon: 'iconwenjian',
desc: '可用於匯入'
desc: 'SimpleMindMap私有格式可用于再次導入客戶端可直接編輯'
},
{
name: 'JSON',
type: 'json',
icon: 'iconjson',
desc: '常見的資料交換格式,可用於匯入'
desc: '流行的數據交換格式,可用于再次導入'
},
{
name: '圖片',
type: 'png',
icon: 'iconPNG',
desc: '適合檢視與分享'
desc: '常用圖片格式,適合查看分享'
},
{
name: 'SVG',
type: 'svg',
icon: 'iconSVG',
desc: '可縮放量圖形'
desc: '可縮放量圖形'
},
{
name: 'PDF',
type: 'pdf',
icon: 'iconpdf',
desc: '適合印'
desc: '適合查看浏覽和打印'
},
{
name: 'Markdown',
type: 'md',
icon: 'iconmarkdown',
desc: '方便其他軟體開啟'
desc: 'md文本格式便于其他軟件打開'
},
{
name: 'XMind',
type: 'xmind',
icon: 'iconxmind',
desc: 'XMind 檔案'
desc: 'XMind軟件格式'
},
{
name: 'Txt',
type: 'txt',
icon: 'iconTXT',
desc: '純文字檔案'
desc: '純文本文件'
},
{
name: 'FreeMind',
type: 'mm',
icon: 'iconfreemind',
desc: 'FreeMind軟格式'
desc: 'FreeMind軟格式'
},
{
name: 'Excel',
type: 'xlsx',
icon: 'iconfile-excel',
desc: 'Excel軟體格式'
desc: '表格文本形式可用Excel軟件編輯'
}
]

View File

@ -5,6 +5,7 @@ import {
saveToRecent,
clearRecent,
removeFileInRecent,
removeMultiFileInRecent,
replaceFileInRecent,
getRecent,
saveFileListToRecent,
@ -104,27 +105,31 @@ export const bindFileHandleEvent = ({ mainWindow }) => {
// 保存文件
const idToFilePath = {}
ipcMain.handle('save', async (event, id, data, fileName = '未命名') => {
if (!idToFilePath[id]) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
const res = dialog.showSaveDialogSync(win, {
title: '保存',
defaultPath: fileName + '.smm',
filters: [{ name: '思维导图', extensions: ['smm'] }]
})
if (res) {
idToFilePath[id] = res
fs.writeFile(res, data)
saveToRecent(res).then(() => {
notifyMainWindowRefreshRecentFileList()
ipcMain.handle(
'save',
async (event, id, data, fileName = '未命名', defaultPath = '') => {
if (!idToFilePath[id]) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
const res = dialog.showSaveDialogSync(win, {
title: '保存',
defaultPath:
(defaultPath ? defaultPath + '/' : '') + fileName + '.smm',
filters: [{ name: '思维导图', extensions: ['smm'] }]
})
return path.parse(idToFilePath[id]).name
if (res) {
idToFilePath[id] = res
fs.writeFile(res, data)
saveToRecent(res).then(() => {
notifyMainWindowRefreshRecentFileList()
})
return path.parse(idToFilePath[id]).name
}
} else {
fs.writeFile(idToFilePath[id], data)
}
} else {
fs.writeFile(idToFilePath[id], data)
}
})
)
// 打开文件
const openFile = (event, file) => {
@ -157,16 +162,49 @@ export const bindFileHandleEvent = ({ mainWindow }) => {
ipcMain.handle('openFile', openFile)
// 选择打开本地文件
ipcMain.on('selectOpenFile', event => {
ipcMain.handle('selectOpenFile', event => {
const res = dialog.showOpenDialogSync({
title: '选择',
filters: [{ name: '思维导图', extensions: ['smm'] }]
})
if (res && res[0]) {
openFile(null, res[0])
return res[0]
} else {
return null
}
})
// 选择目录
ipcMain.handle('selectOpenFolder', event => {
const res = dialog.showOpenDialogSync({
title: '选择',
properties: ['openDirectory']
})
if (res && res[0]) {
return res[0]
} else {
return null
}
})
// 获取指定目录下指定类型的文件列表
ipcMain.handle('getFilesInDir', (event, dir, ext) => {
return new Promise((resolve, reject) => {
fs.readdir(dir, (err, files) => {
if (err) {
reject(err)
} else {
const reg = new RegExp(ext + '$')
files = files.filter(item => {
return reg.test(item)
})
resolve(files)
}
})
})
})
// 选择本地文件
ipcMain.handle(
'selectFile',
@ -266,6 +304,30 @@ export const bindFileHandleEvent = ({ mainWindow }) => {
}
})
// 从最近文件列表中删除指定文件
ipcMain.handle('removeFileInRecent', async (event, file) => {
try {
removeFileInRecent(file).then(() => {
notifyMainWindowRefreshRecentFileList()
})
return ''
} catch (error) {
return '清空失败'
}
})
// 从最近文件列表中删除指定的多个文件
ipcMain.handle('removeMultiFileInRecent', async (event, fileList) => {
try {
removeMultiFileInRecent(fileList).then(() => {
notifyMainWindowRefreshRecentFileList()
})
return ''
} catch (error) {
return '删除失败'
}
})
// 添加到最近文件列表
ipcMain.handle('addRecentFileList', async (event, fileList) => {
try {
@ -288,6 +350,19 @@ export const bindFileHandleEvent = ({ mainWindow }) => {
shell.showItemInFolder(file)
})
// 检查文件是否存在
ipcMain.handle('checkFileExist', (event, filePath) => {
return new Promise((resolve, reject) => {
fs.access(filePath, err => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
})
// 打开指定文件
ipcMain.handle('openPath', (event, file, relativePath = '') => {
if (!path.isAbsolute(file) && relativePath) {
@ -301,7 +376,7 @@ export const bindFileHandleEvent = ({ mainWindow }) => {
})
// 删除指定文件
ipcMain.handle('deleteFile', (event, file) => {
const deleteFile = async (event, file) => {
let res = ''
let id = Object.keys(idToFilePath).find(item => {
return idToFilePath[item] === file
@ -316,11 +391,31 @@ export const bindFileHandleEvent = ({ mainWindow }) => {
try {
fs.rmSync(file)
} catch (error) {}
removeFileInRecent(file)
await removeFileInRecent(file)
} else {
res = '该文件正在编辑,请关闭后再试'
}
return res
}
ipcMain.handle('deleteFile', deleteFile)
// 删除指定的多个文件
ipcMain.handle('deleteMultiFile', (event, fileList) => {
return new Promise(resolve => {
const total = fileList.length
let count = 0
const failList = []
fileList.forEach(item => {
const error = deleteFile(null, item)
count++
if (error) {
failList.push(item)
}
if (count >= total) {
resolve(failList)
}
})
})
})
// 复制文件

View File

@ -12,19 +12,24 @@ contextBridge.exposeInMainWorld('electronAPI', {
create: id => ipcRenderer.send('create', id),
getFileContent: id => ipcRenderer.invoke('getFileContent', id),
getFilePath: id => ipcRenderer.invoke('getFilePath', id),
save: (id, data, fileName) => ipcRenderer.invoke('save', id, data, fileName),
save: (id, data, fileName, defaultPath) =>
ipcRenderer.invoke('save', id, data, fileName, defaultPath),
rename: (id, name) => ipcRenderer.invoke('rename', id, name),
openUrl: url => ipcRenderer.send('openUrl', url),
addRecentFileList: fileList =>
ipcRenderer.invoke('addRecentFileList', fileList),
getRecentFileList: () => ipcRenderer.invoke('getRecentFileList'),
clearRecentFileList: () => ipcRenderer.invoke('clearRecentFileList'),
removeFileInRecent: file => ipcRenderer.invoke('removeFileInRecent', file),
removeMultiFileInRecent: fileList =>
ipcRenderer.invoke('removeMultiFileInRecent', fileList),
openFileInDir: file => ipcRenderer.invoke('openFileInDir', file),
deleteFile: file => ipcRenderer.invoke('deleteFile', file),
deleteMultiFile: fileList => ipcRenderer.invoke('deleteMultiFile', fileList),
onRefreshRecentFileList: callback =>
ipcRenderer.on('refreshRecentFileList', callback),
openFile: file => ipcRenderer.invoke('openFile', file),
selectOpenFile: () => ipcRenderer.send('selectOpenFile'),
selectOpenFile: () => ipcRenderer.invoke('selectOpenFile'),
copyFile: file => ipcRenderer.invoke('copyFile', file),
selectFile: (openDirectory, relativePath) =>
ipcRenderer.invoke('selectFile', openDirectory, relativePath),
@ -32,5 +37,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.invoke('openPath', path, relativePath),
saveClientConfig: config => ipcRenderer.invoke('saveClientConfig', config),
getClientConfig: () => ipcRenderer.invoke('getClientConfig'),
getIsMaximize: id => ipcRenderer.invoke('getIsMaximize', id)
getIsMaximize: id => ipcRenderer.invoke('getIsMaximize', id),
selectOpenFolder: () => ipcRenderer.invoke('selectOpenFolder'),
getFilesInDir: (dir, ext) => ipcRenderer.invoke('getFilesInDir', dir, ext),
checkFileExist: filePath => ipcRenderer.invoke('checkFileExist', filePath)
})

View File

@ -89,6 +89,23 @@ export const removeFileInRecent = file => {
})
}
// 从最近文件列表中移除指定的多个文件
export const removeMultiFileInRecent = fileList => {
return new Promise((resolve, reject) => {
let list = getRecent()
list = list.filter(item => {
return !fileList.includes(item)
})
storage.set(RECENT_FILE_LIST, list, err => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
// 替换指定文件
export const replaceFileInRecent = (oldFile, newFile) => {
return new Promise((resolve, reject) => {

View File

@ -61,6 +61,7 @@ export default {
'Enable real-time rendering effect for text editing',
isShowScrollbar: 'Is show scrollbar',
isUseHandDrawnLikeStyle: 'Is use hand drawn like style',
isUseMomentum: 'Is open drag momentum',
watermark: 'Watermark',
showWatermark: 'Is show watermark',
onlyExport: 'Only export',
@ -84,7 +85,8 @@ export default {
changeRichTextTip3: 'Do you want to switch to non rich text mode?',
enableDragImport: 'Is it allowed to directly drag and drop files to the page for import',
imgTextMargin: 'Node image and text margin',
textContentMargin: 'Node contents margin'
textContentMargin: 'Node contents margin',
enableInheritAncestorLineStyle: 'Node connection style inherits the style of ancestor nodes',
},
color: {
moreColor: 'More color'
@ -133,7 +135,8 @@ export default {
expandNodeChild: 'Expand all sub nodes',
unExpandNodeChild: 'Un expand all sub nodes',
addToDo: 'Add toDo',
removeToDo: 'Remove toDo'
removeToDo: 'Remove toDo',
aiCreate: 'AI Continuation'
},
count: {
words: 'Words',
@ -153,11 +156,7 @@ export default {
svgFile: 'svg file',
pdfFile: 'pdf file',
markdownFile: 'markdown file',
tips: 'tips: .smm and .json file can be import',
isTransparent: 'Background is transparent',
pngTips:
'tips: Exporting pictures in rich text mode is time-consuming. It is recommended to export to svg format',
svgTips: 'tips: Exporting pictures in rich text mode is time-consuming',
transformingDomToImages: 'Converting nodes: ',
notifyTitle: 'Info',
notifyMessage:
@ -167,7 +166,9 @@ export default {
useMultiPageExport: 'Export multi page',
defaultFileName: 'Mind map',
addFooterTextPlaceholder: 'For example: From simple-mind-map',
addFooterText: 'Add text at the footer'
addFooterText: 'Add text at the footer',
desc: 'Desc',
options: 'Options'
},
fullscreen: {
fullscreenShow: 'Full screen show',
@ -218,7 +219,9 @@ export default {
},
outline: {
title: 'Outline',
nodeDefaultText: 'Branch node'
nodeDefaultText: 'Branch node',
print: 'Print',
fullscreen: 'Fullscreen'
},
scale: {
zoomIn: 'Zoom in',
@ -331,7 +334,8 @@ export default {
openFileTip:
'Please export the currently edited file before opening it, Beware of content loss',
isRelative: 'Relative path',
selectFolder: 'Select folder'
selectFolder: 'Select folder',
ai: 'AI'
},
edit: {
newFeatureNoticeTitle: 'New feature reminder',
@ -424,5 +428,58 @@ export default {
nodeTagStyle: {
placeholder: 'Please enter the tag content',
delete: 'Delete this tag'
},
ai: {
chatTitle: 'AI dialogue',
clearRecords: 'Clear records',
connectFailedTitle: 'Client connection failure prompt',
connectFailedTip: 'Client connection failed, please check:',
connectFailedCheckTip1:
'1. Have you installed the mind mapping client? If not, please click here to install:',
connectFailedCheckTip2: '2. If the client is installed, please confirm if the client is opened.',
connectFailedCheckTip3:
'If it has already been installed and started, you can try closing and restarting it.',
connectFailedCheckTip4: 'After completing the above steps, you can click on:',
baiduNetdisk: 'Baidu Netdisk',
createMindMapTitle: 'One click generation of mind maps',
createTip:
'Please enter a theme, and AI will generate a mind map based on your theme, such as: Hangzhou weekend travel plan.',
importantTip: 'Important note: One click generation will overwrite existing data. It is recommended to export the current data first.',
wantModifyAiConfigTip: 'Do you want to modify the AI configuration? Please click on:',
modifyAIConfiguration: 'Modify AI configuration',
chatInputPlaceholder: 'Enter to send, Shift+Enter to wrap.',
send: 'Send',
stopGenerating: 'Stop generating',
generationFailed: 'Generation failed',
aiGenerationSuccess: 'AI generation completed',
stoppedGenerating: 'Stopped generating',
AIConfiguration: 'AI configuration',
VolcanoArkLargeModelConfiguration: 'Volcano Ark Large Model Configuration:',
configTip: 'At present, only the Volcano Ark model is supported, and you need to obtain the key yourself. For detailed operation steps, please refer to:',
course: 'Course',
inferenceAccessPoint: 'Inference access point',
mindMappingClientConfiguration: 'Mind mapping client configuration:',
port: 'Port',
cancel: 'Cancel',
confirm: 'Confirm',
close: 'Close',
configSaveSuccessTip: 'Configuration saved successfully',
apiValidateTip: 'Please enter the interface',
keyValidateTip: 'Please enter the API Key',
modelValidateTip: 'Please enter the inference access point',
portValidateTip: 'Please enter the port',
methodValidateTip: 'Please select the request method',
noInputTip: 'Please enter the content',
connectSuccessful: 'Connection successful',
connectFailed: 'connection failed',
connectionDetection: 'Connection detection',
configurationMissing: 'Configuration missing',
aiCreateMsgPrefix: 'Help me write one【',
aiCreateMsgPostfix:
'】. It needs to be returned in Markdown format and can only use two syntax: Markdown title and unordered list. It can support multiple layers of nesting. Just return the content.',
aiCreatePartMsgPrefix: 'I have a theme for【',
aiCreatePartMsgCenter: '】Can you help me continue writing one of the contents of the mind map【',
aiCreatePartMsgPostfix:
'】The subordinate content of the node needs to be returned in Markdown format and can only use two syntax: Markdown title and unordered list. It can support multi-level nesting. Just return the content.'
}
}

View File

@ -59,6 +59,7 @@ export default {
openRealtimeRenderOnNodeTextEdit: '开启文本编辑实时渲染效果',
isShowScrollbar: '是否显示滚动条',
isUseHandDrawnLikeStyle: '是否开启手绘风格',
isUseMomentum: '是否开启拖动画布的动量效果',
watermark: '水印',
showWatermark: '是否显示水印',
watermarkDefaultText: '水印文字',
@ -75,9 +76,11 @@ export default {
tagPositionBottom: '文本下面',
alwaysShowExpandBtn: '是否一直显示展开收起按钮',
enableAutoEnterTextEditWhenKeydown: '键盘输入时自动进入文本编辑',
enableInheritAncestorLineStyle: '节点连线样式继承祖先节点的样式',
confirm: '确定',
cancel: '取消',
changeRichTextTip: '该操作会清空所有历史修改记录,并且修改思维导图数据,是否继续?',
changeRichTextTip:
'该操作会清空所有历史修改记录,并且修改思维导图数据,是否继续?',
changeRichTextTip2: '是否切换为富文本模式?',
changeRichTextTip3: '是否切换为非富文本模式?',
enableDragImport: '是否允许直接拖拽文件到页面进行导入',
@ -131,7 +134,8 @@ export default {
expandNodeChild: '展开所有下级节点',
unExpandNodeChild: '收起所有下级节点',
addToDo: '添加待办',
removeToDo: '删除待办'
removeToDo: '删除待办',
aiCreate: 'AI续写'
},
count: {
words: '字数',
@ -151,10 +155,7 @@ export default {
svgFile: 'svg文件',
pdfFile: 'pdf文件',
markdownFile: 'markdown文件',
tips: 'tips.smm和.json文件可用于导入',
isTransparent: '背景是否透明',
pngTips: 'tips富文本模式导出图片非常耗时建议导出为svg格式',
svgTips: 'tips富文本模式导出图片非常耗时',
transformingDomToImages: '正在转换节点:',
notifyTitle: '消息',
notifyMessage: '如果没有触发下载,请检查是否被浏览器拦截了',
@ -163,7 +164,9 @@ export default {
useMultiPageExport: '是否多页导出',
defaultFileName: '思维导图',
addFooterText: '底部添加文字',
addFooterTextPlaceholder: '比如来自simple-mind-map'
addFooterTextPlaceholder: '比如来自simple-mind-map',
desc: '说明',
options: '选项'
},
fullscreen: {
fullscreenShow: '全屏查看',
@ -214,7 +217,9 @@ export default {
},
outline: {
title: '大纲',
nodeDefaultText: '分支节点'
nodeDefaultText: '分支节点',
print: '打印',
fullscreen: '全屏'
},
scale: {
zoomIn: '放大',
@ -269,7 +274,7 @@ export default {
bottom: '下',
left: '左',
right: '右',
tag: '标签',
tag: '标签'
},
theme: {
title: '主题',
@ -323,7 +328,8 @@ export default {
newFileTip: '新建文件前请先导出当前编辑的文件,谨防内容丢失',
openFileTip: '打开文件前请先导出当前编辑的文件,谨防内容丢失',
isRelative: '相对路径',
selectFolder: '选择文件夹'
selectFolder: '选择文件夹',
ai: 'AI'
},
edit: {
newFeatureNoticeTitle: '新特性提醒',
@ -414,5 +420,58 @@ export default {
nodeTagStyle: {
placeholder: '请输入标签内容',
delete: '删除此标签'
},
ai: {
chatTitle: 'AI对话',
clearRecords: '清空记录',
connectFailedTitle: '客户端连接失败提示',
connectFailedTip: '客户端连接失败,请检查:',
connectFailedCheckTip1:
'1.是否安装了思绪思维导图客户端,如果没有请点此安装:',
connectFailedCheckTip2: '2.如果安装了客户端,请确认是否打开了客户端。',
connectFailedCheckTip3:
'3.如果已经安装并启动了,那么可以尝试关闭然后重新启动。',
connectFailedCheckTip4: '完成以上步骤后可点击:',
baiduNetdisk: '百度网盘',
createMindMapTitle: '一键生成思维导图',
createTip:
'请输入一个主题AI会根据你的主题生成思维导图杭州周末出游计划。',
importantTip: '重要提示:一键生成会覆盖现有数据,建议先导出当前数据。',
wantModifyAiConfigTip: '想要修改AI配置请点击',
modifyAIConfiguration: '修改AI配置',
chatInputPlaceholder: 'Enter 发送Shift + Enter 换行。',
send: '发送',
stopGenerating: '停止生成',
generationFailed: '生成失败',
aiGenerationSuccess: 'AI生成完成',
stoppedGenerating: '已停止生成',
AIConfiguration: 'AI配置',
VolcanoArkLargeModelConfiguration: '火山方舟大模型配置:',
configTip: '目前仅支持火山方舟大模型需要自行去获取key详细操作步骤见',
course: '教程',
inferenceAccessPoint: '推理接入点',
mindMappingClientConfiguration: '思绪思维导图客户端配置:',
port: '端口',
cancel: '取消',
confirm: '确认',
close: '关闭',
configSaveSuccessTip: '配置保存成功',
apiValidateTip: '请输入接口',
keyValidateTip: '请输入API Key',
modelValidateTip: '请输入推理接入点',
portValidateTip: '请输入端口',
methodValidateTip: '请选择请求方式',
noInputTip: '请输入内容',
connectSuccessful: '连接成功',
connectFailed: '连接失败',
connectionDetection: '连接检测',
configurationMissing: '配置缺失',
aiCreateMsgPrefix: '帮我写一个【',
aiCreateMsgPostfix:
'】需要以Markdown格式返回并且只能使用Markdown的标题和无序列表两种语法可以支持多层嵌套。只需返回内容即可。',
aiCreatePartMsgPrefix: '我有一个主题为【',
aiCreatePartMsgCenter: '】的思维导图,帮我续写其中一个内容为【',
aiCreatePartMsgPostfix:
'】的节点的下级内容需要以Markdown格式返回并且只能使用Markdown的标题和无序列表两种语法可以支持多层嵌套。只需返回内容即可。'
}
}

View File

@ -42,9 +42,7 @@ export default {
notUseRainbowLines: '不使用彩虹線條',
outerFramePadding: '外框內距',
tagPositionRight: '文本右側',
tagPositionBottom: '文本下面',
alwaysShowExpandBtn: '是否壹直顯示展開收起按鈕',
enableAutoEnterTextEditWhenKeydown: '鍵盤輸入時自動進入文本編輯'
tagPositionBottom: '文本下面'
},
setting: {
title: '設置',
@ -64,6 +62,7 @@ export default {
openRealtimeRenderOnNodeTextEdit: '開啟文本編輯實時渲染效果',
isShowScrollbar: '顯示捲軸',
isUseHandDrawnLikeStyle: '使用手繪風格',
isUseMomentum: '是否開啓拖動畫布的動量效果',
watermark: '浮水印',
showWatermark: '顯示浮水印',
onlyExport: '僅在匯出時顯示',
@ -78,12 +77,16 @@ export default {
belowNode: '顯示在節點下方',
confirm: '確定',
cancel: '取消',
changeRichTextTip: '該操作會清空所有曆史修改記錄,並且修改思維導圖數據,是否繼續?',
changeRichTextTip:
'該操作會清空所有曆史修改記錄,並且修改思維導圖數據,是否繼續?',
changeRichTextTip2: '是否切換爲富文本模式?',
changeRichTextTip3: '是否切換爲非富文本模式?',
enableDragImport: '是否允許直接拖拽文件到頁面進行導入',
imgTextMargin: '節點圖片和文本間隔',
textContentMargin: '節點各種內容間隔'
textContentMargin: '節點各種內容間隔',
enableAutoEnterTextEditWhenKeydown: '鍵盤輸入時自動進入文本編輯',
enableInheritAncestorLineStyle: '節點連線樣式繼承祖先節點的樣式',
alwaysShowExpandBtn: '是否壹直顯示展開收起按鈕'
},
color: {
moreColor: '更多顏色'
@ -131,7 +134,8 @@ export default {
expandNodeChild: '展開所有下級節點',
unExpandNodeChild: '收起所有下級節點',
addToDo: '添加待辦',
removeToDo: '刪除待辦'
removeToDo: '刪除待辦',
aiCreate: 'AI續寫'
},
count: {
words: '字數',
@ -151,10 +155,7 @@ export default {
svgFile: 'SVG 檔案',
pdfFile: 'PDF 檔案',
markdownFile: 'Markdown 檔案',
tips: '提示:.smm 和 .json 檔案可以匯入',
isTransparent: '背景透明',
pngTips: '提示:在豐富文字模式下匯出圖片非常耗時,建議匯出為 SVG 格式',
svgTips: '提示:在豐富文字模式下匯出圖片非常耗時',
transformingDomToImages: '正在轉換節點:',
notifyTitle: '訊息',
notifyMessage: '如果沒有觸發下載,請檢查是否被瀏覽器封鎖',
@ -163,7 +164,9 @@ export default {
useMultiPageExport: '多頁匯出',
defaultFileName: '心智圖',
addFooterText: '在底部新增文字',
addFooterTextPlaceholder: '例如:來自 simple-mind-map'
addFooterTextPlaceholder: '例如:來自 simple-mind-map',
desc: '說明',
options: '選項'
},
fullscreen: {
fullscreenShow: '全螢幕檢視',
@ -214,7 +217,9 @@ export default {
},
outline: {
title: '大綱',
nodeDefaultText: '分支節點'
nodeDefaultText: '分支節點',
print: '打印',
fullscreen: '全屏'
},
scale: {
zoomIn: '放大',
@ -268,7 +273,7 @@ export default {
bottom: '下',
left: '左',
right: '右',
tag: '標簽',
tag: '標簽'
},
theme: {
title: '主題',
@ -319,7 +324,8 @@ export default {
creatingTip: '正在建立檔案',
directory: '目錄',
newFileTip: '新增檔案前,請先匯出目前編輯的檔案,以免內容遺失',
openFileTip: '開啟檔案前,請先匯出目前編輯的檔案,以免內容遺失'
openFileTip: '開啟檔案前,請先匯出目前編輯的檔案,以免內容遺失',
ai: 'AI'
},
edit: {
newFeatureNoticeTitle: '新功能提醒',
@ -407,5 +413,58 @@ export default {
nodeTagStyle: {
placeholder: '請輸入標籤內容',
delete: '刪除此標籤'
},
ai: {
chatTitle: 'AI對話',
clearRecords: '清空記錄',
connectFailedTitle: '客戶端連接失敗提示',
connectFailedTip: '客戶端連接失敗,請檢查:',
connectFailedCheckTip1:
'1.是否安裝了思緒思維導圖客戶端,如果沒有請點此安裝:',
connectFailedCheckTip2: '2.如果安裝了客戶端,請確認是否打開了客戶端。',
connectFailedCheckTip3:
'3.如果已經安裝並啓動了,那麽可以嘗試關閉然後重新啓動。',
connectFailedCheckTip4: '完成以上步驟後可點擊:',
baiduNetdisk: '百度網盤',
createMindMapTitle: '一鍵生成思維導圖',
createTip:
'請輸入一個主題AI會根據你的主題生成思維導圖杭州周末出遊計劃。',
importantTip: '重要提示:一鍵生成會覆蓋現有數據,建議先導出當前數據。',
wantModifyAiConfigTip: '想要修改AI配置請點擊',
modifyAIConfiguration: '修改AI配置',
chatInputPlaceholder: 'Enter 發送Shift Enter 換行。',
send: '發送',
stopGenerating: '停止生成',
generationFailed: '生成失敗',
aiGenerationSuccess: 'AI生成完成',
stoppedGenerating: '已停止生成',
AIConfiguration: 'AI配置',
VolcanoArkLargeModelConfiguration: '火山方舟大模型配置:',
configTip: '目前僅支持火山方舟大模型需要自行去獲取key詳細操作步驟見',
course: '教程',
inferenceAccessPoint: '推理接入點',
mindMappingClientConfiguration: '思緒思維導圖客戶端配置:',
port: '端口',
cancel: '取消',
confirm: '確認',
close: '關閉',
configSaveSuccessTip: '配置保存成功',
apiValidateTip: '請輸入接口',
keyValidateTip: '請輸入API Key',
modelValidateTip: '請輸入推理接入點',
portValidateTip: '請輸入端口',
methodValidateTip: '請選擇請求方式',
noInputTip: '請輸入內容',
connectSuccessful: '連接成功',
connectFailed: '連接失敗',
connectionDetection: '連接檢測',
configurationMissing: '配置缺失',
aiCreateMsgPrefix: '幫我寫一個【',
aiCreateMsgPostfix:
'】需要以Markdown格式返回並且只能使用Markdown的標題和無序列表兩種語法可以支持多層嵌套。只需返回內容即可。',
aiCreatePartMsgPrefix: '我有一個主題爲【',
aiCreatePartMsgCenter: '】的思維導圖,幫我續寫其中一個內容爲【',
aiCreatePartMsgPostfix:
'】的節點的下級內容需要以Markdown格式返回並且只能使用Markdown的標題和無序列表兩種語法可以支持多層嵌套。只需返回內容即可。'
}
}

View File

@ -46,4 +46,3 @@ if (window.takeOverApp) {
} else {
initApp()
}

View File

@ -0,0 +1,340 @@
<template>
<Sidebar ref="sidebar" :title="$t('ai.chatTitle')">
<div class="aiChatBox" :class="{ isDark: isDark }">
<div class="chatHeader">
<el-button size="mini" @click="clear">
<span class="el-icon-delete"></span>
{{ $t('ai.clearRecords') }}
</el-button>
<el-button size="mini" @click="modifyAiConfig">
<span class="el-icon-edit"></span>
{{ $t('ai.modifyAIConfiguration') }}
</el-button>
</div>
<div class="chatResBox customScrollbar" ref="chatResBoxRef">
<div
class="chatItem"
v-for="item in chatList"
:key="item.id"
:class="[item.type]"
>
<div class="chatItemInner" v-if="item.type === 'user'">
<div class="avatar">
<span class="icon el-icon-user"></span>
</div>
<div class="content">{{ item.content }}</div>
</div>
<div class="chatItemInner" v-else-if="item.type === 'ai'">
<div class="avatar">
<span class="icon iconfont iconAIshengcheng"></span>
</div>
<div class="content" v-html="item.content"></div>
</div>
</div>
</div>
<div class="chatInputBox">
<textarea
v-model="text"
class="customScrollbar"
:placeholder="$t('ai.chatInputPlaceholder')"
@keydown="onKeydown"
></textarea>
<el-button class="btn" size="mini" @click="send" :loading="isCreating">
{{ $t('ai.send') }}
<span class="el-icon-position"></span>
</el-button>
<el-button
class="stop"
size="mini"
type="warning"
@click="stop"
v-show="isCreating"
>
{{ $t('ai.stopGenerating') }}
</el-button>
</div>
</div>
</Sidebar>
</template>
<script>
import Sidebar from './Sidebar'
import { mapState } from 'vuex'
import { createUid } from 'simple-mind-map/src/utils'
import MarkdownIt from 'markdown-it'
let md = null
export default {
components: {
Sidebar
},
data() {
return {
text: '',
chatList: [],
isCreating: false
}
},
computed: {
...mapState({
isDark: state => state.localConfig.isDark,
activeSidebar: state => state.activeSidebar
})
},
watch: {
activeSidebar(val) {
if (val === 'ai') {
this.$refs.sidebar.show = true
} else {
this.$refs.sidebar.show = false
}
}
},
created() {},
beforeDestroy() {},
methods: {
onKeydown(e) {
if (e.keyCode === 13) {
if (!e.shiftKey) {
e.preventDefault()
this.send()
} else {
}
}
},
send() {
if (this.isCreating) return
const text = this.text.trim()
if (!text) {
return
}
this.text = ''
const historyUserMsgList = this.chatList
.filter(item => {
return item.type === 'user'
})
.map(item => {
return item.content
})
this.chatList.push({
id: createUid(),
type: 'user',
content: text
})
this.chatList.push({
id: createUid(),
type: 'ai',
content: ''
})
this.isCreating = true
const textList = [...historyUserMsgList, text]
this.$bus.$emit(
'ai_chat',
textList,
res => {
if (!md) {
md = new MarkdownIt()
}
this.chatList[this.chatList.length - 1].content = md.render(res)
this.$refs.chatResBoxRef.scrollTop = this.$refs.chatResBoxRef.scrollHeight
},
() => {
this.isCreating = false
},
() => {
this.isCreating = false
this.$message.error(this.$t('ai.generationFailed'))
}
)
},
stop() {
this.$bus.$emit('ai_chat_stop')
this.isCreating = false
},
clear() {
this.chatList = []
},
modifyAiConfig() {
this.$bus.$emit('showAiConfigDialog')
}
}
}
</script>
<style lang="less" scoped>
.aiChatBox {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
&.isDark {
}
.chatHeader {
height: 50px;
border-bottom: 1px solid #e8e8e8;
display: flex;
align-items: center;
padding: 0 12px;
}
.chatResBox {
width: 100%;
height: 100%;
padding: 0 12px;
margin: 12px 0;
overflow-y: auto;
overflow-x: hidden;
.chatItem {
margin-bottom: 20px;
border: 1px solid;
position: relative;
border-radius: 10px;
&:last-of-type {
margin-bottom: 0;
}
&.ai {
border-color: #409eff;
.chatItemInner {
.avatar {
border-color: #409eff;
left: -12px;
top: -12px;
.icon {
color: #409eff;
}
}
}
}
&.user {
border-color: #f56c6c;
.chatItemInner {
.avatar {
border-color: #f56c6c;
right: -12px;
top: -12px;
.icon {
color: #f56c6c;
}
}
}
}
.chatItemInner {
width: 100%;
padding: 12px;
.avatar {
width: 30px;
height: 30px;
border: 1px solid;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: absolute;
background-color: #fff;
.icon {
font-size: 18px;
font-weight: bold;
}
}
/deep/ .content {
width: 100%;
overflow: hidden;
color: #3f4a54;
font-size: 14px;
line-height: 1.5;
p {
margin-bottom: 12px;
&:last-of-type {
margin-bottom: 0;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 24px;
margin-bottom: 16px;
}
code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
white-space: break-spaces;
background-color: rgba(175, 184, 193, 0.2);
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
}
pre {
padding: 12px;
background-color: rgba(175, 184, 193, 0.2);
code {
background-color: transparent;
padding: 0;
overflow: hidden;
}
}
}
}
}
}
.chatInputBox {
flex-shrink: 0;
width: 100%;
height: 150px;
border-top: 1px solid #e8e8e8;
position: relative;
textarea {
width: 100%;
height: 100%;
outline: none;
padding: 12px;
border: none;
}
.btn {
position: absolute;
right: 12px;
bottom: 12px;
}
.stop {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -30px;
}
}
}
</style>

View File

@ -0,0 +1,186 @@
<template>
<el-dialog
class="aiConfigDialog"
:title="$t('ai.AIConfiguration')"
:visible.sync="aiConfigDialogVisible"
width="550px"
append-to-body
>
<div class="aiConfigBox">
<el-form
:model="ruleForm"
:rules="rules"
ref="ruleFormRef"
label-width="100px"
>
<p class="title">{{ $t('ai.VolcanoArkLargeModelConfiguration') }}</p>
<p class="desc">
{{ $t('ai.configTip') }}<a href="">{{ $t('ai.course') }}</a
>
</p>
<el-form-item label="API Key" prop="key">
<el-input v-model="ruleForm.key"></el-input>
</el-form-item>
<el-form-item :label="$t('ai.inferenceAccessPoint')" prop="model">
<el-input v-model="ruleForm.model"></el-input>
</el-form-item>
<!-- <el-form-item label="接口" prop="api">
<el-input v-model="ruleForm.api"></el-input>
</el-form-item>
<el-form-item label="请求方式" prop="method">
<el-select v-model="ruleForm.method" placeholder="请选择">
<el-option key="POST" label="POST" value="POST"></el-option>
<el-option key="GET" label="GET" value="GET"></el-option>
</el-select>
</el-form-item> -->
<p class="title">{{ $t('ai.mindMappingClientConfiguration') }}</p>
<el-form-item :label="$t('ai.port')" prop="port">
<el-input v-model="ruleForm.port"></el-input>
</el-form-item>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="cancel">{{ $t('ai.cancel') }}</el-button>
<el-button type="primary" @click="confirm">{{
$t('ai.confirm')
}}</el-button>
</div>
</el-dialog>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
export default {
model: {
prop: 'visible',
event: 'change'
},
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
aiConfigDialogVisible: false,
ruleForm: {
api: '',
key: '',
model: '',
port: '',
method: ''
},
rules: {
api: [
{
required: true,
message: this.$t('ai.apiValidateTip'),
trigger: 'blur'
}
],
key: [
{
required: true,
message: this.$t('ai.keyValidateTip'),
trigger: 'blur'
}
],
model: [
{
required: true,
message: this.$t('ai.modelValidateTip'),
trigger: 'blur'
}
],
port: [
{
required: true,
message: this.$t('ai.portValidateTip'),
trigger: 'blur'
}
],
method: [
{
required: true,
message: this.$t('ai.methodValidateTip'),
trigger: 'blur'
}
]
}
}
},
computed: {
...mapState(['aiConfig'])
},
watch: {
visible(val) {
this.aiConfigDialogVisible = val
},
aiConfigDialogVisible(val, oldVal) {
if (!val && oldVal) {
this.close()
}
}
},
created() {
this.initFormData()
},
methods: {
...mapMutations(['setLocalConfig']),
close() {
this.$emit('change', false)
},
initFormData() {
Object.keys(this.aiConfig).forEach(key => {
this.ruleForm[key] = this.aiConfig[key]
})
},
cancel() {
this.close()
this.initFormData()
},
confirm() {
this.$refs.ruleFormRef.validate(valid => {
if (valid) {
this.close()
this.setLocalConfig({
...this.ruleForm
})
this.$message.success(this.$t('ai.configSaveSuccessTip'))
}
})
}
}
}
</script>
<style lang="less" scoped>
.aiConfigDialog {
/deep/ .el-dialog__body {
padding: 12px 20px;
}
.aiConfigBox {
a {
color: #409eff;
}
.title {
margin-bottom: 12px;
font-weight: bold;
}
.desc {
margin-bottom: 12px;
padding-left: 12px;
border-left: 5px solid #ccc;
}
}
}
</style>

View File

@ -0,0 +1,587 @@
<template>
<div>
<!-- 客户端连接失败提示弹窗 -->
<el-dialog
class="clientTipDialog"
:title="$t('ai.connectFailedTitle')"
:visible.sync="clientTipDialogVisible"
width="400px"
append-to-body
>
<div class="tipBox">
<p>{{ $t('ai.connectFailedTip') }}</p>
<p>
{{ $t('ai.connectFailedCheckTip1')
}}<a
href="https://pan.baidu.com/s/1huasEbKsGNH2Af68dvWiOg?pwd=3bp3"
>{{ $t('ai.baiduNetdisk') }}</a
><a href="https://github.com/wanglin2/mind-map/releases">Github</a>
</p>
<p>{{ $t('ai.connectFailedCheckTip2') }}</p>
<P>{{ $t('ai.connectFailedCheckTip3') }}</P>
<p>
{{ $t('ai.connectFailedCheckTip4')
}}<el-button size="small" @click="testConnect">{{
$t('ai.connectionDetection')
}}</el-button>
</p>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="clientTipDialogVisible = false">{{
$t('ai.close')
}}</el-button>
</div>
</el-dialog>
<!-- ai内容输入弹窗 -->
<el-dialog
class="createDialog"
:title="$t('ai.createMindMapTitle')"
:visible.sync="createDialogVisible"
width="450px"
append-to-body
>
<div class="inputBox">
<el-input
type="textarea"
:rows="5"
:placeholder="$t('ai.createTip')"
v-model="aiInput"
>
</el-input>
<div class="tip warning">
{{ $t('ai.importantTip') }}
</div>
<div class="tip">
{{ $t('ai.wantModifyAiConfigTip')
}}<el-button size="small" @click="showAiConfigDialog">{{
$t('ai.modifyAIConfiguration')
}}</el-button>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="closeAiCreateDialog">{{
$t('ai.cancel')
}}</el-button>
<el-button type="primary" @click="doAiCreate">{{
$t('ai.confirm')
}}</el-button>
</div>
</el-dialog>
<!-- ai生成中添加一个透明层防止期间用户进行操作 -->
<div
class="aiCreatingMask"
ref="aiCreatingMaskRef"
v-show="aiCreatingMaskVisible"
>
<el-button type="warning" class="btn" @click="stopCreate">{{
$t('ai.stopGenerating')
}}</el-button>
</div>
<AiConfigDialog v-model="aiConfigDialogVisible"></AiConfigDialog>
</div>
</template>
<script>
import Ai from '@/utils/ai'
import { transformMarkdownTo } from 'simple-mind-map/src/parse/markdownTo'
import {
createUid,
isUndef,
checkNodeOuter,
getStrWithBrFromHtml
} from 'simple-mind-map/src/utils'
import { mapState } from 'vuex'
import AiConfigDialog from './AiConfigDialog.vue'
export default {
components: {
AiConfigDialog
},
props: {
mindMap: {
type: Object
}
},
data() {
return {
aiInstance: null,
isAiCreating: false,
aiCreatingContent: '',
isLoopRendering: false,
uidMap: {},
latestUid: '',
clientTipDialogVisible: false,
createDialogVisible: false,
aiInput: '',
aiCreatingMaskVisible: false,
aiConfigDialogVisible: false,
mindMapDataCache: '',
beingAiCreateNodeUid: ''
}
},
computed: {
...mapState(['aiConfig'])
},
created() {
this.$bus.$on('ai_create_all', this.aiCrateAll)
this.$bus.$on('ai_create_part', this.aiCreatePart)
this.$bus.$on('ai_chat', this.aiChat)
this.$bus.$on('ai_chat_stop', this.aiChatStop)
this.$bus.$on('showAiConfigDialog', this.showAiConfigDialog)
},
mounted() {
document.body.appendChild(this.$refs.aiCreatingMaskRef)
},
beforeDestroy() {
this.$bus.$off('ai_create_all', this.aiCrateAll)
this.$bus.$off('ai_create_part', this.aiCreatePart)
this.$bus.$off('ai_chat', this.aiChat)
this.$bus.$off('ai_chat_stop', this.aiChatStop)
this.$bus.$off('showAiConfigDialog', this.showAiConfigDialog)
},
methods: {
// AI
showAiConfigDialog() {
this.aiConfigDialogVisible = true
},
//
async testConnect() {
try {
await fetch(`http://localhost:${this.aiConfig.port}/ai/test`, {
method: 'GET'
})
this.$message.success(this.$t('ai.connectSuccessful'))
this.clientTipDialogVisible = false
this.createDialogVisible = true
} catch (error) {
console.log(error)
this.$message.error(this.$t('ai.connectFailed'))
}
},
// ai
async aiTest() {
//
if (
!(
this.aiConfig.api &&
this.aiConfig.key &&
this.aiConfig.model &&
this.aiConfig.port
)
) {
this.showAiConfigDialog()
throw new Error(this.$t('ai.configurationMissing'))
}
//
let isConnect = false
try {
await fetch(`http://localhost:${this.aiConfig.port}/ai/test`, {
method: 'GET'
})
isConnect = true
} catch (error) {
console.log(error)
this.clientTipDialogVisible = true
}
if (!isConnect) {
throw new Error(this.$t('ai.connectFailed'))
}
},
// AI
async aiCrateAll() {
try {
await this.aiTest()
this.createDialogVisible = true
} catch (error) {
console.log(error)
}
},
// ai
closeAiCreateDialog() {
this.createDialogVisible = false
this.aiInput = ''
},
//
doAiCreate() {
const aiInputText = this.aiInput.trim()
if (!aiInputText) {
this.$message.warning(this.$t('ai.noInputTip'))
return
}
this.closeAiCreateDialog()
this.aiCreatingMaskVisible = true
//
this.isAiCreating = true
this.aiInstance = new Ai({
port: this.aiConfig.port
})
this.aiInstance.init('huoshan', this.aiConfig)
this.mindMap.renderer.setRootNodeCenter()
this.mindMap.setData(null)
this.aiInstance.request(
{
messages: [
{
role: 'user',
content: `${this.$t(
'ai.aiCreateMsgPrefix'
)}${aiInputText}${this.$t('ai.aiCreateMsgPostfix')}`
}
]
},
content => {
if (content && /\n$/.test(content)) {
this.aiCreatingContent = content
}
this.loopRenderOnAiCreating()
},
content => {
this.aiCreatingContent = content
this.resetOnAiCreatingStop()
this.$message.success(this.$t('ai.aiGenerationSuccess'))
},
() => {
this.resetOnAiCreatingStop()
this.resetOnRenderEnd()
this.$message.error(this.$t('ai.generationFailed'))
}
)
},
// AI
resetOnAiCreatingStop() {
this.aiCreatingMaskVisible = false
this.isAiCreating = false
this.aiInstance = null
},
//
resetOnRenderEnd() {
this.isLoopRendering = false
this.uidMap = {}
this.aiCreatingContent = ''
this.mindMapDataCache = ''
this.beingAiCreateNodeUid = ''
},
//
stopCreate() {
this.aiInstance.stop()
this.isAiCreating = false
this.aiCreatingMaskVisible = false
this.$message.success(this.$t('ai.stoppedGenerating'))
},
//
loopRenderOnAiCreating() {
if (!this.aiCreatingContent.trim() || this.isLoopRendering) return
this.isLoopRendering = true
const treeData = transformMarkdownTo(this.aiCreatingContent)
this.addUid(treeData)
let lastTreeData = JSON.stringify(treeData)
//
const onRenderEnd = () => {
//
this.checkNodeOuter()
//
if (!this.isAiCreating && !this.aiCreatingContent) {
this.mindMap.off('node_tree_render_end', onRenderEnd)
this.latestUid = ''
return
}
const treeData = transformMarkdownTo(this.aiCreatingContent)
this.addUid(treeData)
//
if (this.isAiCreating) {
//
const curTreeData = JSON.stringify(treeData)
if (curTreeData === lastTreeData) {
setTimeout(() => {
onRenderEnd()
}, 500)
return
}
lastTreeData = curTreeData
this.mindMap.updateData(treeData)
} else {
//
//
this.mindMap.updateData(treeData)
this.resetOnRenderEnd()
}
}
this.mindMap.on('node_tree_render_end', onRenderEnd)
this.mindMap.setData(treeData)
},
//
checkNodeOuter() {
if (this.latestUid) {
const latestNode = this.mindMap.renderer.findNodeByUid(this.latestUid)
if (latestNode) {
const { isOuter, offsetLeft, offsetTop } = checkNodeOuter(
this.mindMap,
latestNode,
100,
100
)
if (isOuter) {
this.mindMap.view.translateXY(offsetLeft, offsetTop)
}
}
}
},
// AIuid
addUid(data) {
const checkRepeatUidMap = {}
const walk = (node, pUid = '') => {
if (!node.data) {
node.data = {}
}
if (isUndef(node.data.uid)) {
// pUid+uid
const key = pUid + '-' + node.data.text
node.data.uid = this.uidMap[key] || createUid()
// uid
if (checkRepeatUidMap[node.data.uid]) {
node.data.uid = createUid()
}
this.latestUid = this.uidMap[key] = node.data.uid
checkRepeatUidMap[node.data.uid] = true
}
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
walk(child, node.data.uid)
})
}
}
walk(data)
},
// AI
async aiCreatePart(node) {
try {
await this.aiTest()
this.beingAiCreateNodeUid = node.getData('uid')
const currentMindMapData = this.mindMap.getData()
this.mindMapDataCache = JSON.stringify(currentMindMapData)
this.aiCreatingMaskVisible = true
//
this.isAiCreating = true
this.aiInstance = new Ai({
port: this.aiConfig.port
})
this.aiInstance.init('huoshan', this.aiConfig)
this.aiInstance.request(
{
messages: [
{
role: 'user',
content: `${this.$t(
'ai.aiCreatePartMsgPrefix'
)}${getStrWithBrFromHtml(
currentMindMapData.data.text
)}${this.$t('ai.aiCreatePartMsgCenter')}${getStrWithBrFromHtml(
node.getData('text')
)}${this.$t('ai.aiCreatePartMsgPostfix')}`
}
]
},
content => {
if (content && /\n$/.test(content)) {
this.aiCreatingContent = content
}
this.loopRenderOnAiCreatingPart()
},
content => {
this.aiCreatingContent = content
this.resetOnAiCreatingStop()
this.$message.success(this.$t('ai.aiGenerationSuccess'))
},
() => {
this.resetOnAiCreatingStop()
this.resetOnRenderEnd()
this.$message.error(this.$t('ai.generationFailed'))
}
)
} catch (error) {
console.log(error)
}
},
//
addToTargetNode(newChildren = []) {
const initData = JSON.parse(this.mindMapDataCache)
const walk = node => {
if (node.data.uid === this.beingAiCreateNodeUid) {
if (!node.children) {
node.children = []
}
node.children.push(...newChildren)
return
}
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
walk(child)
})
}
}
walk(initData)
return initData
},
//
loopRenderOnAiCreatingPart() {
if (!this.aiCreatingContent.trim() || this.isLoopRendering) return
this.isLoopRendering = true
const partData = transformMarkdownTo(this.aiCreatingContent)
this.addUid(partData)
let lastPartData = JSON.stringify(partData)
const treeData = this.addToTargetNode(partData.children || [])
//
const onRenderEnd = () => {
//
this.checkNodeOuter()
//
if (!this.isAiCreating && !this.aiCreatingContent) {
this.mindMap.off('node_tree_render_end', onRenderEnd)
this.latestUid = ''
return
}
const partData = transformMarkdownTo(this.aiCreatingContent)
this.addUid(partData)
const treeData = this.addToTargetNode(partData.children || [])
if (this.isAiCreating) {
//
const curPartData = JSON.stringify(partData)
if (curPartData === lastPartData) {
setTimeout(() => {
onRenderEnd()
}, 500)
return
}
lastPartData = curPartData
this.mindMap.updateData(treeData)
} else {
this.mindMap.updateData(treeData)
this.resetOnRenderEnd()
}
}
this.mindMap.on('node_tree_render_end', onRenderEnd)
// 使updateData
this.mindMap.updateData(treeData)
},
// AI
async aiChat(
messageList = [],
progress = () => {},
end = () => {},
err = () => {}
) {
try {
await this.aiTest()
//
this.isAiCreating = true
this.aiInstance = new Ai({
port: this.aiConfig.port
})
this.aiInstance.init('huoshan', this.aiConfig)
this.aiInstance.request(
{
messages: messageList.map(msg => {
return {
role: 'user',
content: msg
}
})
},
content => {
progress(content)
},
content => {
end(content)
},
error => {
err(error)
}
)
} catch (error) {
console.log(error)
}
},
// AI
aiChatStop() {
if (this.aiInstance) {
this.aiInstance.stop()
this.isAiCreating = false
this.aiInstance = null
}
}
}
}
</script>
<style lang="less" scoped>
.clientTipDialog,
.createDialog {
/deep/ .el-dialog__body {
padding: 12px 20px;
}
}
.tipBox {
p {
margin-bottom: 12px;
a {
color: #409eff;
}
}
}
.inputBox {
.tip {
margin-top: 12px;
&.warning {
color: #f56c6c;
}
}
}
.aiCreatingMask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 99999;
background-color: transparent;
.btn {
position: absolute;
left: 50%;
top: 100px;
transform: translateX(-50%);
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<Sidebar ref="sidebar" :title="$t('baseStyle.title')">
<div class="sidebarContent" :class="{ isDark: isDark }" v-if="data">
<div class="sidebarContent customScrollbar" :class="{ isDark: isDark }" v-if="data">
<!-- 背景 -->
<div class="title noTop">{{ $t('baseStyle.background') }}</div>
<div class="row">
@ -995,8 +995,6 @@ export default {
this.$bus.$off('setData', this.onSetData)
},
methods: {
...mapMutations(['setLocalConfig']),
onSetData() {
if (this.activeSidebar !== 'baseStyle') return
setTimeout(() => {

View File

@ -140,6 +140,10 @@
<div class="item" @click="exec('EXPORT_CUR_NODE_TO_PNG')">
<span class="name">{{ $t('contextmenu.exportNodeToPng') }}</span>
</div>
<div class="splitLine" v-if="enableAi"></div>
<div class="item" @click="aiCreate" v-if="enableAi">
<span class="name">{{ $t('contextmenu.aiCreate') }}</span>
</div>
</template>
<template v-if="type === 'svg'">
<div class="item" @click="exec('RETURN_CENTER')">
@ -257,7 +261,8 @@ export default {
isZenMode: state => state.localConfig.isZenMode,
isDark: state => state.localConfig.isDark,
supportNumbers: state => state.supportNumbers,
supportCheckbox: state => state.supportCheckbox
supportCheckbox: state => state.supportCheckbox,
enableAi: state => state.enableAi
}),
expandList() {
return [
@ -578,6 +583,12 @@ export default {
console.log(error)
this.$message.error(this.$t('contextmenu.copyFail'))
}
},
// AI
aiCreate() {
this.$bus.$emit('ai_create_part', this.node)
this.hide()
}
}
}

View File

@ -119,7 +119,7 @@ export default {
}
}
@media screen and (max-width: 740px) {
@media screen and (max-width: 900px) {
.countContainer {
display: none;
}

View File

@ -48,6 +48,8 @@
v-if="mindMap"
:mindMap="mindMap"
></NodeImgPlacementToolbar>
<AiCreate v-if="mindMap && enableAi" :mindMap="mindMap"></AiCreate>
<AiChat v-if="enableAi"></AiChat>
<div
class="dragMask"
v-if="showDragMask"
@ -90,7 +92,7 @@ import MindMapLayoutPro from 'simple-mind-map/src/plugins/MindMapLayoutPro.js'
import Themes from 'simple-mind-map-plugin-themes'
//
// import Cooperate from 'simple-mind-map/src/plugins/Cooperate.js'
// FreemindExcel线
// FreemindExcel线
// import HandDrawnLikeStyle from 'simple-mind-map-plugin-handdrawnlikestyle'
// import Notation from 'simple-mind-map-plugin-notation'
// import Numbers from 'simple-mind-map-plugin-numbers'
@ -98,7 +100,8 @@ import Themes from 'simple-mind-map-plugin-themes'
// import Excel from 'simple-mind-map-plugin-excel'
// import Checkbox from 'simple-mind-map-plugin-checkbox'
// import LineFlow from 'simple-mind-map-plugin-lineflow'
// npm link simple-mind-map-plugin-excel simple-mind-map-plugin-freemind simple-mind-map-plugin-numbers simple-mind-map-plugin-notation simple-mind-map-plugin-handdrawnlikestyle simple-mind-map-plugin-checkbox simple-mind-map simple-mind-map-plugin-themes simple-mind-map-plugin-lineflow
// import Momentum from 'simple-mind-map-plugin-momentum'
// npm link simple-mind-map-plugin-excel simple-mind-map-plugin-freemind simple-mind-map-plugin-numbers simple-mind-map-plugin-notation simple-mind-map-plugin-handdrawnlikestyle simple-mind-map-plugin-checkbox simple-mind-map simple-mind-map-plugin-themes simple-mind-map-plugin-lineflow simple-mind-map-plugin-momentum
import OutlineSidebar from './OutlineSidebar'
import Style from './Style'
import BaseStyle from './BaseStyle'
@ -142,6 +145,8 @@ import NodeTagStyle from './NodeTagStyle.vue'
import Setting from './Setting.vue'
import AssociativeLineStyle from './AssociativeLineStyle.vue'
import NodeImgPlacementToolbar from './NodeImgPlacementToolbar.vue'
import AiCreate from './AiCreate.vue'
import AiChat from './AiChat.vue'
//
MindMap.usePlugin(MiniMap)
@ -196,7 +201,9 @@ export default {
NodeTagStyle,
Setting,
AssociativeLineStyle,
NodeImgPlacementToolbar
NodeImgPlacementToolbar,
AiCreate,
AiChat
},
data() {
return {
@ -224,8 +231,11 @@ export default {
state.localConfig.useLeftKeySelectionRightKeyDrag,
isUseHandDrawnLikeStyle: state =>
state.localConfig.isUseHandDrawnLikeStyle,
isUseMomentum: state => state.localConfig.isUseMomentum,
extraTextOnExport: state => state.extraTextOnExport,
isDragOutlineTreeNode: state => state.isDragOutlineTreeNode
isDragOutlineTreeNode: state => state.isDragOutlineTreeNode,
enableAi: state => state.enableAi,
isDark: state => state.localConfig.isDark
})
},
watch: {
@ -249,6 +259,18 @@ export default {
} else {
this.removeHandDrawnLikeStylePlugin()
}
},
isUseMomentum() {
if (this.isUseMomentum) {
this.addMomentumPlugin()
} else {
this.removeMomentumPlugin()
}
}
},
created() {
if (this.$route.query && this.$route.query.ai) {
this.setEnableAi(true)
}
},
async mounted() {
@ -288,7 +310,7 @@ export default {
this.mindMap.destroy()
},
methods: {
...mapMutations(['setFileName', 'setIsUnSave']),
...mapMutations(['setFileName', 'setIsUnSave', 'setEnableAi']),
handleStartTextEdit() {
this.mindMap.renderer.startTextEdit()
@ -376,7 +398,8 @@ export default {
})
this.$bus.$on('view_data_change', data => {
if (
(!this.clientConfig || !this.clientConfig.viewTranslateChangeTriggerAutoSave) &&
(!this.clientConfig ||
!this.clientConfig.viewTranslateChangeTriggerAutoSave) &&
this.lastViewData.transform.scaleX === data.transform.scaleX &&
this.lastViewData.transform.scaleY === data.transform.scaleY
) {
@ -465,7 +488,8 @@ export default {
{
confirmButtonText: this.$t('edit.yes'),
cancelButtonText: this.$t('edit.no'),
type: 'warning'
type: 'warning',
customClass: this.isDark ? 'darkElMessageBox' : ''
}
)
},
@ -513,7 +537,8 @@ export default {
{
confirmButtonText: this.$t('edit.yes'),
cancelButtonText: this.$t('edit.no'),
type: 'warning'
type: 'warning',
customClass: this.isDark ? 'darkElMessageBox' : ''
}
)
.then(() => {
@ -639,38 +664,7 @@ export default {
// }
})
this.lastViewData = simpleDeepClone(this.mindMap.view.getTransformData())
if (this.openNodeRichText) this.addRichTextPlugin()
if (this.isShowScrollbar) this.addScrollbarPlugin()
if (this.isUseHandDrawnLikeStyle) this.addHandDrawnLikeStylePlugin()
if (typeof HandDrawnLikeStyle !== 'undefined') {
this.$store.commit('setSupportHandDrawnLikeStyle', true)
}
if (typeof Notation !== 'undefined') {
this.mindMap.addPlugin(Notation)
this.$store.commit('setSupportMark', true)
}
if (typeof Numbers !== 'undefined') {
this.mindMap.addPlugin(Numbers)
this.$store.commit('setSupportNumbers', true)
}
if (typeof Freemind !== 'undefined') {
this.mindMap.addPlugin(Freemind)
this.$store.commit('setSupportFreemind', true)
Vue.prototype.Freemind = Freemind
}
if (typeof Excel !== 'undefined') {
this.mindMap.addPlugin(Excel)
this.$store.commit('setSupportExcel', true)
Vue.prototype.Excel = Excel
}
if (typeof Checkbox !== 'undefined') {
this.mindMap.addPlugin(Checkbox)
this.$store.commit('setSupportCheckbox', true)
}
if (typeof LineFlow !== 'undefined') {
this.mindMap.addPlugin(LineFlow)
this.$store.commit('setSupportLineFlow', true)
}
this.loadPlugins()
this.mindMap.keyCommand.addShortcut('Control+s', () => {
this.manualSave()
})
@ -742,6 +736,46 @@ export default {
// }, 5000)
},
//
loadPlugins() {
if (this.openNodeRichText) this.addRichTextPlugin()
if (this.isShowScrollbar) this.addScrollbarPlugin()
if (typeof HandDrawnLikeStyle !== 'undefined') {
this.$store.commit('setSupportHandDrawnLikeStyle', true)
if (this.isUseHandDrawnLikeStyle) this.addHandDrawnLikeStylePlugin()
}
if (typeof Momentum !== 'undefined') {
this.$store.commit('setSupportMomentum', true)
if (this.isUseMomentum) this.addMomentumPlugin()
}
if (typeof Notation !== 'undefined') {
this.mindMap.addPlugin(Notation)
this.$store.commit('setSupportMark', true)
}
if (typeof Numbers !== 'undefined') {
this.mindMap.addPlugin(Numbers)
this.$store.commit('setSupportNumbers', true)
}
if (typeof Freemind !== 'undefined') {
this.mindMap.addPlugin(Freemind)
this.$store.commit('setSupportFreemind', true)
Vue.prototype.Freemind = Freemind
}
if (typeof Excel !== 'undefined') {
this.mindMap.addPlugin(Excel)
this.$store.commit('setSupportExcel', true)
Vue.prototype.Excel = Excel
}
if (typeof Checkbox !== 'undefined') {
this.mindMap.addPlugin(Checkbox)
this.$store.commit('setSupportCheckbox', true)
}
if (typeof LineFlow !== 'undefined') {
this.mindMap.addPlugin(LineFlow)
this.$store.commit('setSupportLineFlow', true)
}
},
// url
hasFileURL() {
const fileURL = this.$route.query.fileURL
@ -841,10 +875,12 @@ export default {
let id = this.$route.params.id
let data = this.mindMap.getData(true)
removeMindMapNodeStickerProtocol(data.root)
const currentFolder = localStorage.getItem('currentFolder')
let res = await window.electronAPI.save(
id,
JSON.stringify(data),
this.fileName
this.fileName,
currentFolder || ''
)
if (res) {
this.isNewFile = false
@ -885,6 +921,25 @@ export default {
}
},
//
addMomentumPlugin() {
try {
if (!this.mindMap) return
this.mindMap.addPlugin(Momentum)
} catch (error) {
console.log('动量效果插件不存在')
}
},
//
removeMomentumPlugin() {
try {
this.mindMap.removePlugin(Momentum)
} catch (error) {
console.log('动量效果插件不存在')
}
},
//
testDynamicCreateNodes() {
// return

View File

@ -1,16 +1,18 @@
<template>
<el-dialog
class="nodeExportDialog"
:class="{ isMobile: isMobile, isDark: isDark }"
:title="$t('export.title')"
:visible.sync="dialogVisible"
v-loading.fullscreen.lock="loading"
:element-loading-text="loadingText"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
:width="isMobile ? '90%' : '50%'"
:width="isMobile ? '90%' : '800px'"
:top="isMobile ? '20px' : '15vh'"
>
<div class="exportContainer" :class="{ isDark: isDark }">
<!-- 文件名称输入 -->
<div class="nameInputBox">
<span class="name">{{ $t('export.filename') }}</span>
<el-input
@ -19,71 +21,90 @@
size="mini"
@keydown.native.stop
></el-input>
<el-checkbox
v-show="['smm', 'json'].includes(exportType)"
v-model="widthConfig"
style="margin-left: 12px"
>{{ $t('export.include') }}</el-checkbox
>
</div>
<div
class="paddingInputBox"
v-show="['svg', 'png', 'pdf'].includes(exportType)"
>
<div class="paddingInputGroup">
<span class="name">{{ $t('export.paddingX') }}</span>
<el-input
style="max-width: 100px"
v-model="paddingX"
size="mini"
@change="onPaddingChange"
@keydown.native.stop
></el-input>
</div>
<div class="paddingInputGroup">
<span class="name">{{ $t('export.paddingY') }}</span>
<el-input
style="width: 100px"
v-model="paddingY"
size="mini"
@change="onPaddingChange"
@keydown.native.stop
></el-input>
</div>
<div class="paddingInputGroup">
<span class="name">{{ this.$t('export.addFooterText') }}</span>
<el-input
style="width: 200px"
v-model="extraText"
size="mini"
:placeholder="$t('export.addFooterTextPlaceholder')"
@keydown.native.stop
></el-input>
</div>
<div class="paddingInputGroup">
<el-checkbox
v-show="['png', 'pdf'].includes(exportType)"
v-model="isTransparent"
>{{ $t('export.isTransparent') }}</el-checkbox
<!-- 导出类型选择 -->
<div class="downloadTypeSelectBox">
<!-- 类型列表 -->
<div class="downloadTypeList customScrollbar">
<div
class="downloadTypeItem"
v-for="item in downTypeList"
:key="item.type"
:class="{ active: exportType === item.type }"
@click="exportType = item.type"
>
</div>
</div>
<div class="downloadTypeList">
<div
class="downloadTypeItem"
v-for="item in downTypeList"
:key="item.type"
:class="{ active: exportType === item.type }"
@click="exportType = item.type"
>
<div class="icon iconfont" :class="[item.icon, item.type]"></div>
<div class="info">
<div class="icon iconfont" :class="[item.icon, item.type]"></div>
<div class="name">{{ item.name }}</div>
<div class="desc">{{ item.desc }}</div>
<div class="icon checked el-icon-check"></div>
</div>
</div>
<!-- 类型内容 -->
<div class="downloadTypeContent customScrollbar">
<div class="contentRow">
<div class="contentName">{{ $t('export.desc') }}</div>
<div class="contentValue">
{{ currentTypeData ? currentTypeData.desc : '' }}
</div>
</div>
<div class="contentRow">
<div class="contentName">{{ $t('export.options') }}</div>
<div class="contentValue">
<div
class="valueItem"
v-show="['smm', 'json'].includes(exportType)"
>
<el-checkbox v-model="widthConfig">{{
$t('export.include')
}}</el-checkbox>
</div>
<div
class="valueItem"
v-show="['svg', 'png', 'pdf'].includes(exportType)"
>
<div class="valueSubItem">
<span class="name">{{ $t('export.paddingX') }}</span>
<el-input
style="width: 200px"
v-model="paddingX"
size="mini"
@change="onPaddingChange"
@keydown.native.stop
></el-input>
</div>
<div class="valueSubItem">
<span class="name">{{ $t('export.paddingY') }}</span>
<el-input
style="width: 200px"
v-model="paddingY"
size="mini"
@change="onPaddingChange"
@keydown.native.stop
></el-input>
</div>
<div class="valueSubItem">
<span class="name">{{
this.$t('export.addFooterText')
}}</span>
<el-input
style="width: 200px"
v-model="extraText"
size="mini"
:placeholder="$t('export.addFooterTextPlaceholder')"
@keydown.native.stop
></el-input>
</div>
<div class="valueSubItem">
<el-checkbox
v-show="['png', 'pdf'].includes(exportType)"
v-model="isTransparent"
>{{ $t('export.isTransparent') }}</el-checkbox
>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="tip">{{ $t('export.tips') }}</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="cancel">{{ $t('dialog.cancel') }}</el-button>
@ -144,6 +165,13 @@ export default {
return true
}
})
},
currentTypeData() {
const cur = this.downTypeList.find(item => {
return item.type === this.exportType
})
return cur
}
},
created() {
@ -249,15 +277,39 @@ export default {
</script>
<style lang="less" scoped>
.exportContainer {
&.isDark {
.downloadTypeList {
.downloadTypeItem {
background-color: #363b3f;
.nodeExportDialog {
.exportContainer {
&.isDark {
.nameInputBox {
.name {
color: hsla(0, 0%, 100%, 0.6);
}
}
.info {
.name {
color: hsla(0, 0%, 100%, 0.9);
.downloadTypeSelectBox {
.downloadTypeList {
.downloadTypeItem {
background-color: #363b3f;
&.active {
background-color: #262a2e;
}
.name {
color: hsla(0, 0%, 100%, 0.9);
}
}
}
.downloadTypeContent {
.contentRow {
.contentName {
color: hsla(0, 0%, 100%, 0.6);
}
.contentValue {
color: hsla(0, 0%, 100%, 0.6);
}
}
}
}
@ -266,125 +318,228 @@ export default {
}
.nodeExportDialog {
/deep/ .el-dialog__body {
background-color: #f2f4f7;
}
.nameInputBox {
margin-bottom: 20px;
.name {
margin-right: 10px;
}
}
.paddingInputBox {
display: flex;
align-items: center;
flex-wrap: wrap;
.paddingInputGroup {
margin-right: 12px;
margin-bottom: 12px;
&:last-of-type {
margin-right: 0;
&.isDark {
/deep/ .el-dialog__body {
.el-checkbox {
.el-checkbox__label {
color: hsla(0, 0%, 100%, 0.6);
}
}
}
}
.name {
margin-right: 10px;
/deep/ .el-dialog__body {
padding: 0;
border-top: 1px solid #f2f4f7;
border-bottom: 1px solid #f2f4f7;
.el-checkbox__input.is-checked + .el-checkbox__label {
color: #409eff !important;
}
.el-checkbox {
.el-checkbox__label {
color: #1a1a1a;
}
}
}
.tip {
margin-top: 10px;
&.isMobile {
.exportContainer {
.downloadTypeSelectBox {
flex-direction: column;
&.warning {
color: #f56c6c;
.downloadTypeList {
width: 100%;
display: flex;
align-items: center;
overflow-x: auto;
height: 60px;
.downloadTypeItem {
width: 100px;
flex-shrink: 0;
padding-left: 10px;
.icon {
margin-right: 5px;
&.checked {
display: none !important;
}
}
}
}
.downloadTypeContent {
.contentRow {
flex-direction: column;
.contentName {
margin-bottom: 10px;
}
.contentValue {
.valueItem {
.valueSubItem {
display: flex;
flex-direction: column;
.name {
margin-bottom: 5px;
}
}
}
}
}
}
}
}
}
.downloadTypeList {
.exportContainer {
width: 100%;
height: 450px;
overflow: hidden;
display: flex;
flex-wrap: wrap;
.downloadTypeItem {
width: 200px;
height: 88px;
padding: 22px;
overflow: hidden;
margin: 10px;
border-radius: 11px;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.02);
background-color: #fff;
flex-direction: column;
.nameInputBox {
display: flex;
align-items: center;
cursor: pointer;
border: 2px solid transparent;
justify-content: center;
flex-wrap: wrap;
height: 50px;
flex-shrink: 0;
border-bottom: 1px solid #f2f4f7;
&.active {
border-color: #409eff;
}
.icon {
font-size: 30px;
.name {
margin-right: 10px;
}
}
.downloadTypeSelectBox {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
.downloadTypeList {
width: 210px;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
background-color: #f2f4f7;
flex-shrink: 0;
&.png {
color: #ffc038;
}
.downloadTypeItem {
width: 100%;
height: 60px;
padding-left: 28px;
overflow: hidden;
display: flex;
align-items: center;
cursor: pointer;
&.pdf {
color: #ff6c4d;
}
&.active {
background-color: #fff;
&.md {
color: #2b2b2b;
}
.icon {
&.checked {
display: block;
}
}
}
&.json {
color: #12c87e;
}
.icon {
font-size: 25px;
margin-right: 15px;
flex-shrink: 0;
&.svg {
color: #4380ff;
}
&.png {
color: #ffc038;
}
&.smm {
color: #409eff;
}
&.pdf {
color: #ff6c4d;
}
&.xmind {
color: #f55e5e;
}
&.md {
color: #2b2b2b;
}
&.txt {
color: #70798e;
&.json {
color: #12c87e;
}
&.svg {
color: #4380ff;
}
&.smm {
color: #409eff;
}
&.xmind {
color: #f55e5e;
}
&.txt {
color: #70798e;
}
&.checked {
color: #409eff;
font-size: 20px;
margin-left: auto;
display: none;
}
}
.name {
color: #1a1a1a;
font-size: 15px;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.info {
width: 100%;
overflow: hidden;
.downloadTypeContent {
padding: 30px;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
.name {
color: #1a1a1a;
font-size: 15px;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.contentRow {
display: flex;
font-size: 14px;
margin-bottom: 20px;
.desc {
color: #999;
font-size: 12px;
display: -webkit-box; /* 必须设置display属性为-webkit-box */
overflow: hidden; /* 超出部分隐藏 */
text-overflow: ellipsis; /* 显示省略号 */
-webkit-line-clamp: 2; /* 限制显示两行 */
-webkit-box-orient: vertical; /* 垂直方向上的换行 */
.contentName {
width: 80px;
color: #666;
}
.contentValue {
color: #1a1a1a;
.valueItem {
.valueSubItem {
margin-bottom: 12px;
&:last-of-type {
margin-right: 0;
}
.name {
margin-right: 12px;
}
}
}
}
}
}
}

View File

@ -18,7 +18,7 @@
>
</div>
<div class="title">{{ $t('formulaSidebar.common') }}</div>
<div class="formulaList">
<div class="formulaList customScrollbar">
<div class="formulaItem" v-for="(item, index) in list" :key="index">
<div class="overview" v-html="item.overview"></div>
<div class="text" @click="formulaText = item.text">

View File

@ -1,5 +1,5 @@
<template>
<div class="navigatorContainer" :class="{ isDark: isDark }">
<div class="navigatorContainer customScrollbar" :class="{ isDark: isDark }">
<div class="item">
<el-select
v-model="lang"
@ -197,7 +197,8 @@ export default {
url = 'https://wanglin2.github.io/mind-map-docs/help/help1.html'
break
case 'devDoc':
url = 'https://wanglin2.github.io/mind-map-docs/start/introduction.html'
url =
'https://wanglin2.github.io/mind-map-docs/start/introduction.html'
break
case 'site':
url = 'https://wanglin2.github.io/mind-map-docs/'
@ -276,7 +277,7 @@ export default {
}
}
@media screen and (max-width: 590px) {
@media screen and (max-width: 700px) {
.navigatorContainer {
left: 20px;
overflow-x: auto;

View File

@ -91,8 +91,10 @@
slot="reference"
class="toolbarBtn"
:style="{
marginLeft: dir === 'v' ? '0px' : '20px',
marginTop: dir === 'v' ? '10px' : '0px'
marginLeft: dir === 'v' || rightHasBtn ? '0px' : '20px',
marginTop: dir === 'v' ? '10px' : '0px',
marginRight: rightHasBtn ? '20px' : '0px',
marginBottom: dir === 'v' && rightHasBtn ? '10px' : '0px'
}"
:class="{
disabled: activeNodes.length <= 0 || hasGeneralization
@ -128,6 +130,10 @@ export default {
dir: {
type: String,
default: ''
},
rightHasBtn: {
type: Boolean,
default: false
}
},
data() {

View File

@ -1,6 +1,6 @@
<template>
<div
class="noteContentViewer"
class="noteContentViewer customScrollbar"
ref="noteContentViewer"
:style="{
left: this.left + 'px',
@ -124,22 +124,5 @@ export default {
overflow-y: auto;
box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.06);
&::-webkit-scrollbar {
width: 7px;
height: 7px;
}
&::-webkit-scrollbar-thumb {
border-radius: 7px;
background-color: rgba(0, 0, 0, 0.3);
cursor: pointer;
}
&::-webkit-scrollbar-track {
box-shadow: none;
background: transparent;
display: none;
}
}
</style>

View File

@ -6,10 +6,26 @@
:style="{top: IS_ELECTRON ? '40px' : 0}"
v-if="isOutlineEdit"
>
<div class="closeBtn" @click="onClose">
<span class="icon iconfont iconguanbi"></span>
<div class="btnList">
<el-tooltip
class="item"
effect="dark"
:content="$t('outline.print')"
placement="top"
>
<div class="btn" @click="onPrint">
<span class="icon iconfont iconprinting"></span>
</div>
</el-tooltip>
<div class="btn" @click="onClose">
<span class="icon iconfont iconguanbi"></span>
</div>
</div>
<div class="outlineEditBox" ref="outlineEditBox">
<div
class="outlineEditBox"
id="fullScreenOutlineEditBox"
ref="outlineEditBox"
>
<div class="outlineEdit">
<el-tree
ref="tree"
@ -59,6 +75,7 @@ import {
handleInputPasteText
} from 'simple-mind-map/src/utils'
import { storeData } from '@/api'
import { printOutline } from '@/utils'
//
export default {
@ -226,6 +243,11 @@ export default {
return Math.random()
},
//
onPrint() {
printOutline(this.$refs.outlineEditBox)
},
//
onClose() {
this.setIsOutlineEdit(false)
@ -275,28 +297,36 @@ export default {
top: 0;
width: 100%;
height: 100%;
z-index: 9999;
z-index: 1999;
background-color: #fff;
overflow: hidden;
&.isDark {
background-color: #262a2e;
.closeBtn {
.icon {
color: #fff;
.btnList {
.btn {
.icon {
color: #fff;
}
}
}
}
.closeBtn {
.btnList {
position: absolute;
right: 40px;
top: 20px;
cursor: pointer;
display: flex;
align-items: center;
.icon {
font-size: 28px;
.btn {
cursor: pointer;
margin-left: 12px;
.icon {
font-size: 28px;
}
}
}

View File

@ -1,16 +1,36 @@
<template>
<Sidebar ref="sidebar" :title="$t('outline.title')">
<div
class="changeBtn"
:class="{ isDark: isDark }"
@click="onChangeToOutlineEdit"
>
<span class="icon iconfont iconquanping1"></span>
<div class="btnList">
<el-tooltip
class="item"
effect="dark"
:content="$t('outline.print')"
placement="top"
>
<div class="btn" @click="onPrint">
<span class="icon iconfont iconprinting"></span>
</div>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
:content="$t('outline.fullscreen')"
placement="top"
>
<div
class="btn"
:class="{ isDark: isDark }"
@click="onChangeToOutlineEdit"
>
<span class="icon iconfont iconquanping1"></span>
</div>
</el-tooltip>
</div>
<Outline
:mindMap="mindMap"
v-if="activeSidebar === 'outline'"
@scrollTo="onScrollTo"
ref="outlineRef"
></Outline>
</Sidebar>
</template>
@ -19,6 +39,7 @@
import Sidebar from './Sidebar'
import { mapState, mapMutations } from 'vuex'
import Outline from './Outline.vue'
import { printOutline } from '@/utils'
//
export default {
@ -62,20 +83,31 @@ export default {
if (y > top + height) {
container.scrollTo(0, y - height / 2)
}
},
//
onPrint() {
printOutline(this.$refs.outlineRef.$el)
}
}
}
</script>
<style lang="less" scoped>
.changeBtn {
.btnList {
position: absolute;
right: 50px;
top: 12px;
cursor: pointer;
display: flex;
align-items: center;
&.isDark {
color: #fff;
.btn {
cursor: pointer;
margin-left: 12px;
&.isDark {
color: #fff;
}
}
}
</style>

View File

@ -125,6 +125,7 @@ export default {
)
this.mindMap.keyCommand.addShortcut('Control+f', this.showSearch)
window.addEventListener('resize', this.setSearchResultListHeight)
this.$bus.$on('setData', this.close)
},
mounted() {
this.setSearchResultListHeight()
@ -141,6 +142,7 @@ export default {
)
this.mindMap.keyCommand.removeShortcut('Control+f', this.showSearch)
window.removeEventListener('resize', this.setSearchResultListHeight)
this.$bus.$off('setData', this.close)
},
methods: {
isUndef,

View File

@ -1,6 +1,10 @@
<template>
<Sidebar ref="sidebar" :title="$t('setting.title')">
<div class="sidebarContent" :class="{ isDark: isDark }" v-if="data">
<div
class="sidebarContent customScrollbar"
:class="{ isDark: isDark }"
v-if="data"
>
<!-- 水印 -->
<div class="row">
<!-- 是否显示水印 -->
@ -230,6 +234,18 @@
>
</div>
</div>
<!-- 节点连线样式是否允许继承祖先的连线样式 -->
<div class="row">
<div class="rowItem">
<el-checkbox
v-model="config.enableInheritAncestorLineStyle"
@change="
updateOtherConfig('enableInheritAncestorLineStyle', $event)
"
>{{ $t('setting.enableInheritAncestorLineStyle') }}</el-checkbox
>
</div>
</div>
<!-- 是否开启手绘风格 -->
<div class="row" v-if="supportHandDrawnLikeStyle">
<div class="rowItem">
@ -240,6 +256,16 @@
>
</div>
</div>
<!-- 是否开启动量效果 -->
<div class="row" v-if="supportMomentum">
<div class="rowItem">
<el-checkbox
v-model="localConfigs.isUseMomentum"
@change="updateLocalConfig('isUseMomentum', $event)"
>{{ $t('setting.isUseMomentum') }}</el-checkbox
>
</div>
</div>
<!-- 配置鼠标滚轮行为 -->
<div class="row">
<div class="rowItem">
@ -387,7 +413,8 @@ export default {
alwaysShowExpandBtn: false,
enableAutoEnterTextEditWhenKeydown: true,
imgTextMargin: 0,
textContentMargin: 0
textContentMargin: 0,
enableInheritAncestorLineStyle: false
},
watermarkConfig: {
show: false,
@ -407,6 +434,7 @@ export default {
localConfigs: {
isShowScrollbar: false,
isUseHandDrawnLikeStyle: false,
isUseMomentum: false,
enableDragImport: false
}
}
@ -416,7 +444,8 @@ export default {
activeSidebar: state => state.activeSidebar,
localConfig: state => state.localConfig,
isDark: state => state.localConfig.isDark,
supportHandDrawnLikeStyle: state => state.supportHandDrawnLikeStyle
supportHandDrawnLikeStyle: state => state.supportHandDrawnLikeStyle,
supportMomentum: state => state.supportMomentum
})
},
watch: {
@ -479,7 +508,14 @@ export default {
storeConfig({
config: this.data.config
})
if (['alwaysShowExpandBtn', 'imgTextMargin', 'textContentMargin'].includes(key)) {
if (
[
'alwaysShowExpandBtn',
'imgTextMargin',
'textContentMargin',
'enableInheritAncestorLineStyle'
].includes(key)
) {
this.mindMap.reRender()
}
},
@ -524,7 +560,8 @@ export default {
{
confirmButtonText: this.$t('setting.confirm'),
cancelButtonText: this.$t('setting.cancel'),
type: 'warning'
type: 'warning',
customClass: this.isDark ? 'darkElMessageBox' : ''
}
)
.then(() => {

View File

@ -9,7 +9,7 @@
<div class="sidebarHeader" v-if="title">
{{ title }}
</div>
<div class="sidebarContent" ref="sidebarContent">
<div class="sidebarContent customScrollbar" ref="sidebarContent">
<slot></slot>
</div>
</div>

View File

@ -1,13 +1,14 @@
<template>
<div
class="sidebarTriggerContainer"
class="sidebarTriggerContainer "
@click.stop
:class="{ hasActive: show && activeSidebar, show: show, isDark: isDark }"
:style="{ maxHeight: maxHeight + 'px' }"
>
<div class="toggleShowBtn" :class="{ hide: !show }" @click="show = !show">
<span class="iconfont iconjiantouyou"></span>
</div>
<div class="trigger">
<div class="trigger customScrollbar">
<div
class="triggerItem"
v-for="item in triggerList"
@ -35,14 +36,16 @@ export default {
name: 'SidebarTrigger',
data() {
return {
show: true
show: true,
maxHeight: 0
}
},
computed: {
...mapState({
isDark: state => state.localConfig.isDark,
activeSidebar: state => state.activeSidebar,
isReadonly: state => state.isReadonly
isReadonly: state => state.isReadonly,
enableAi: state => state.enableAi
}),
triggerList() {
@ -52,6 +55,11 @@ export default {
return ['outline', 'shortcutKey'].includes(item.value)
})
}
if (!this.enableAi) {
list = list.filter(item => {
return item.value !== 'ai'
})
}
return list
}
},
@ -62,11 +70,28 @@ export default {
}
}
},
created() {
window.addEventListener('resize', this.onResize)
this.updateSize()
},
beforeDestroy() {
window.removeEventListener('resize', this.onResize)
},
methods: {
...mapMutations(['setActiveSidebar']),
trigger(item) {
this.setActiveSidebar(item.value)
},
onResize() {
this.updateSize()
},
updateSize() {
const topMargin = 110
const bottomMargin = 80
this.maxHeight = window.innerHeight - topMargin - bottomMargin
}
}
}
@ -75,11 +100,13 @@ export default {
<style lang="less" scoped>
.sidebarTriggerContainer {
position: fixed;
top: 110px;
bottom: 80px;
right: -60px;
margin-top: 110px;
transition: all 0.3s;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
justify-content: center;
&.isDark {
.trigger {
@ -145,7 +172,9 @@ export default {
background-color: #fff;
box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.06);
border-radius: 6px;
overflow: hidden;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
.triggerItem {
height: 60px;

View File

@ -5,7 +5,7 @@
:class="{ isDark: isDark }"
v-if="activeNodes.length > 0"
>
<div class="sidebarContent">
<div class="sidebarContent customScrollbar">
<!-- 文字 -->
<div class="title noTop">{{ $t('style.text') }}</div>
<div class="row">

View File

@ -162,6 +162,7 @@ export default {
cancelButtonText: this.$t('theme.reserve'),
type: 'warning',
distinguishCancelAndClose: true,
customClass: this.isDark ? 'darkElMessageBox' : '',
callback: action => {
if (action === 'confirm') {
this.mindMap.setThemeConfig({}, true)

View File

@ -185,6 +185,27 @@ import { throttle, isMobile } from 'simple-mind-map/src/utils/index'
* @Desc: 工具栏
*/
let fileHandle = null
const defaultBtnList = [
'back',
'forward',
'painter',
'siblingNode',
'childNode',
'deleteNode',
'image',
'icon',
'link',
'note',
'tag',
'summary',
'associativeLine',
'formula',
'attachment',
'outerFrame',
'annotation',
'ai'
]
export default {
name: 'Toolbar',
components: {
@ -200,25 +221,6 @@ export default {
data() {
return {
isMobile: isMobile(),
list: [
'back',
'forward',
'painter',
'siblingNode',
'childNode',
'deleteNode',
'image',
'icon',
'link',
'note',
'tag',
'summary',
'associativeLine',
'formula',
'attachment',
'outerFrame',
'annotation'
],
horizontalList: [],
verticalList: [],
showMoreBtn: true,
@ -238,8 +240,24 @@ export default {
...mapState({
isDark: state => state.localConfig.isDark,
isHandleLocalFile: state => state.isHandleLocalFile,
openNodeRichText: state => state.localConfig.openNodeRichText
})
openNodeRichText: state => state.localConfig.openNodeRichText,
enableAi: state => state.enableAi
}),
btnLit() {
let res = [...defaultBtnList]
if (!this.openNodeRichText) {
res = res.filter(item => {
return item !== 'formula'
})
}
if (!this.enableAi) {
res = res.filter(item => {
return item !== 'ai'
})
}
return res
}
},
watch: {
isHandleLocalFile(val) {
@ -247,21 +265,9 @@ export default {
Notification.closeAll()
}
},
openNodeRichText: {
immediate: true,
handler(val) {
const index = this.list.findIndex(item => {
return item === 'formula'
})
if (val) {
if (index === -1) {
this.list.splice(13, 0, 'formula')
}
} else {
if (index !== -1) {
this.list.splice(index, 1)
}
}
btnLit: {
deep: true,
handler() {
this.computeToolbarShow()
}
}
@ -289,7 +295,7 @@ export default {
computeToolbarShow() {
if (!this.$refs.toolbarRef) return
const windowWidth = window.innerWidth - 40
const all = [...this.list]
const all = [...this.btnLit]
let index = 1
const loopCheck = () => {
if (index > all.length) return done()

View File

@ -192,8 +192,20 @@
v-if="item === 'annotation' && supportMark"
:isDark="isDark"
:dir="dir"
:rightHasBtn="annotationRightHasBtn"
@setAnnotation="onSetAnnotation"
></NodeAnnotationBtn>
<div
v-if="item === 'ai'"
class="toolbarBtn"
:class="{
disabled: hasGeneralization
}"
@click="aiCrate"
>
<span class="icon iconfont iconAIshengcheng"></span>
<span class="text">{{ $t('toolbar.ai') }}</span>
</div>
</template>
</div>
</template>
@ -246,6 +258,12 @@ export default {
return node.isGeneralization
}) !== -1
)
},
annotationRightHasBtn() {
const index = this.list.findIndex(item => {
return item === 'annotation'
})
return index !== -1 && index < this.list.length - 1
}
},
created() {
@ -313,6 +331,11 @@ export default {
//
onSetAnnotation(...args) {
this.$bus.$emit('execCommand', 'SET_NOTATION', this.activeNodes, ...args)
},
// AI
aiCrate() {
this.$bus.$emit('ai_create_all')
}
}
}
@ -405,6 +428,7 @@ export default {
.text {
margin-top: 3px;
text-align: center;
}
.subToolbar {

View File

@ -1,32 +1,24 @@
<template>
<div class="workbencheContainer">
<div class="workbencheContent">
<router-view></router-view>
</div>
</div>
<div class="workbencheContainer">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'Workbenche',
created () {
document.title = '思绪思维导图'
}
name: 'Workbenche',
created() {
document.title = '思绪思维导图'
}
}
</script>
<style lang="less" scoped>
.workbencheContainer {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.workbencheContent {
flex-grow: 1;
}
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
</style>
</style>

View File

@ -6,7 +6,7 @@
width="480px"
@close="onClose"
>
<div class="aboutBox">
<div class="aboutBox" :class="{ isDark: isDark }">
<img src="../../../assets/img/icon.png" alt="" />
<h2>思绪思维导图</h2>
<p>版本{{ version }}</p>
@ -29,6 +29,7 @@
<script>
import pkg from '../../../../package.json'
import { mapState } from 'vuex'
export default {
model: {
@ -47,6 +48,11 @@ export default {
version: pkg.version
}
},
computed: {
...mapState({
isDark: state => state.localConfig.isDark
})
},
watch: {
value(val, oldVal) {
this.dialogVisible = val
@ -95,6 +101,13 @@ export default {
justify-content: center;
padding-bottom: 30px;
&.isDark {
.h2,
p {
color: hsla(0, 0%, 100%, 0.6);
}
}
img {
width: 100px;
height: 100px;

View File

@ -1,39 +1,76 @@
<template>
<div class="workbencheFileListContainer">
<div class="title">
<span>最近</span>
<span class="clearBtn" @click="clear">清空</span>
<div class="workbencheFileListContainer" :class="{ isDark: isDark }">
<div class="header">
<div class="headerLeft">
<span class="title">{{ currentFolderName }}</span>
</div>
<div class="headerRight">
<span
class="textBtn"
@click="deleteMultiFile"
v-if="multipleSelection.length > 0"
>删除文件</span
>
<template v-if="this.isRecent">
<span
class="textBtn"
@click="deleteMultiRecord"
v-if="multipleSelection.length > 0"
>删除记录</span
>
<span class="textBtn" @click="clearRecent">清空记录</span>
</template>
<template v-if="!this.isRecent">
<span
class="textBtn"
@click="deleteMultiFromList"
v-if="multipleSelection.length > 0"
>从列表删除</span
>
<span class="textBtn" @click="closeList">关闭文件夹</span>
<span class="textBtn" @click="refreshList(true)">刷新</span>
</template>
</div>
</div>
<div class="fileListBox">
<Empty v-if="list.length <= 0"></Empty>
<el-table v-else :data="list" style="width: 100%">
<el-table
v-else
:data="list"
style="width: 100%"
height="100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column label="名称">
<template slot-scope="scope">
<span class="textBtn" @click="openFile(scope.row.url)">{{
scope.row.name
}}</span>
<span
class="textBtn"
@click="openFile(scope.row.url, scope.$index)"
>{{ scope.row.name }}</span
>
</template>
</el-table-column>
<el-table-column prop="url" label="文件路径"> </el-table-column>
<el-table-column label="操作">
<el-table-column label="操作" width="170">
<template slot-scope="scope">
<el-tooltip effect="light" content="编辑" placement="top">
<el-tooltip effect="dark" content="编辑" placement="top">
<el-button
icon="el-icon-edit"
circle
size="mini"
@click="openFile(scope.row.url)"
@click="openFile(scope.row.url, scope.$index)"
></el-button>
</el-tooltip>
<el-tooltip effect="light" content="复制" placement="top">
<el-tooltip effect="dark" content="复制" placement="top">
<el-button
icon="el-icon-document-copy"
circle
size="mini"
@click="copyFile(scope.row.url)"
@click="copyFile(scope.row.url, scope.$index)"
></el-button>
</el-tooltip>
<el-tooltip effect="light" content="删除" placement="top">
<el-tooltip effect="dark" content="删除" placement="top">
<el-button
type="danger"
icon="el-icon-delete"
@ -43,7 +80,7 @@
></el-button>
</el-tooltip>
<el-tooltip
effect="light"
effect="dark"
content="打开文件所在目录"
placement="top"
>
@ -51,7 +88,7 @@
icon="el-icon-folder-opened"
circle
size="mini"
@click="openFileInDir(scope.row.url)"
@click="openFileInDir(scope.row.url, scope.$index)"
></el-button>
</el-tooltip>
</template>
@ -63,6 +100,8 @@
<script>
import Empty from '../components/Empty.vue'
import { getFileName } from '@/utils'
import { mapState, mapMutations } from 'vuex'
export default {
components: {
@ -70,7 +109,20 @@ export default {
},
data() {
return {
list: []
currentFolder: 'recent',
currentFolderName: '最近',
list: [],
recentList: [],
multipleSelection: []
}
},
computed: {
...mapState({
isDark: state => state.localConfig.isDark
}),
isRecent() {
return this.currentFolder === 'recent'
}
},
created() {
@ -78,20 +130,59 @@ export default {
window.electronAPI.onRefreshRecentFileList(() => {
this.getRecentFileList()
})
this.$bus.$on('changeFolder', this.onChangeFolder)
},
beforeDestroy() {
this.$bus.$off('changeFolder', this.onChangeFolder)
},
methods: {
//
onChangeFolder(folder, files) {
this.multipleSelection = []
this.currentFolder = folder
if (folder === 'recent') {
this.currentFolderName = '最近'
this.list = this.recentList
} else {
const arr = folder.split(/[\/\\]/g)
this.currentFolderName = arr[arr.length - 1]
this.list = files
}
},
//
handleSelectionChange(val) {
this.multipleSelection = val
},
//
async getRecentFileList() {
this.multipleSelection = []
let list = await window.electronAPI.getRecentFileList()
this.list = list.reverse()
this.recentList = list.reverse()
if (this.isRecent) {
this.list = this.recentList
}
},
//
async checkExist(file, index, cb = () => {}) {
try {
await window.electronAPI.checkFileExist(file)
cb()
} catch (error) {
console.log(error)
this.list.splice(index, 1)
window.electronAPI.removeFileInRecent(file)
this.$message.error('文件不存在')
}
},
//
async openFileInDir(file) {
const res = await window.electronAPI.openFileInDir(file)
if (res) {
this.$message.error(res)
}
openFileInDir(file, index) {
this.checkExist(file, index, () => {
window.electronAPI.openFileInDir(file)
})
},
//
@ -99,59 +190,180 @@ export default {
this.$confirm('确定删除该文件?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
customClass: this.isDark ? 'darkElMessageBox' : ''
})
.then(() => {
this.checkExist(file, index, async () => {
try {
const error = await window.electronAPI.deleteFile(file)
if (error) {
this.$message.error(error || '删除失败')
} else {
this.list.splice(index, 1)
this.$message.success('删除成功')
}
} catch (error) {
this.$message.error(error || '删除失败')
}
})
})
.catch(() => {})
},
//
openFile(file, index) {
this.checkExist(file, index, async () => {
const res = await window.electronAPI.openFile(file)
if (res) {
this.$message.error(res)
}
})
},
//
copyFile(file, index) {
this.checkExist(file, index, async () => {
try {
const error = await window.electronAPI.copyFile(file)
if (error) {
this.$message.error(error || '复制失败')
} else {
this.$message.success('复制成功')
}
} catch (error) {
console.log(error)
this.$message.error('复制失败')
}
})
},
//
deleteMultiFile() {
if (this.multipleSelection.length <= 0) {
return
}
this.$confirm('是否确定删除所选文件?会删除源文件。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: this.isDark ? 'darkElMessageBox' : ''
})
.then(async () => {
let res = await window.electronAPI.deleteFile(file)
if (res) {
this.$message.error(res || '删除失败')
const fileList = this.multipleSelection.map(item => {
return item.url
})
const failList = await window.electronAPI.deleteMultiFile(fileList)
const succList = fileList.filter(item => {
return !failList.includes(item)
})
window.electronAPI.removeMultiFileInRecent(succList)
if (!this.isRecent) {
this.list = this.list.filter(item => {
return !succList.includes(item.url)
})
}
if (failList.length > 0) {
this.$message.error('部分文件删除失败')
} else {
this.list.splice(index, 1)
this.$message.success('删除成功')
}
})
.catch(() => {})
},
//
async openFile(file) {
const res = await window.electronAPI.openFile(file)
if (res) {
this.$message.error(res)
//
deleteMultiRecord() {
if (this.multipleSelection.length <= 0) {
return
}
this.$confirm('是否确定删除所选记录?不会删除源文件。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: this.isDark ? 'darkElMessageBox' : ''
})
.then(async () => {
const error = await window.electronAPI.removeMultiFileInRecent(
this.multipleSelection.map(item => {
return item.url
})
)
if (error) {
this.$message.error(error || '删除失败')
} else {
this.$message.success('删除成功')
}
})
.catch(error => {
this.$message.error(error || '删除失败')
})
},
//
clear() {
this.$confirm('确定清空最近文件?', '提示', {
clearRecent() {
this.$confirm('确定清空最近文件?不会删除源文件。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
customClass: this.isDark ? 'darkElMessageBox' : ''
})
.then(async () => {
let res = await window.electronAPI.clearRecentFileList()
if (res) {
this.$message.error('清空失败')
const error = await window.electronAPI.clearRecentFileList()
if (error) {
this.$message.error(error || '清空失败')
} else {
this.list = []
this.recentList = []
this.$message.success('清空成功')
}
})
.catch(error => {
this.$message.error(error || '清空失败')
})
},
//
deleteMultiFromList() {
if (this.multipleSelection.length <= 0) {
return
}
const urlList = this.multipleSelection.map(item => {
return item.url
})
this.$confirm('是否确定从列表中删除所选文件?不会删除源文件。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: this.isDark ? 'darkElMessageBox' : ''
})
.then(() => {
this.list = this.list.filter(item => {
return !urlList.includes(item.url)
})
this.$message.success('删除成功')
})
.catch(() => {})
},
//
async copyFile(file) {
try {
const res = await window.electronAPI.copyFile(file)
if (res) {
this.$message.error(res)
} else {
this.$message.success('复制成功')
}
} catch (error) {
this.$message.error('复制失败')
}
//
closeList() {
this.$confirm('是否确定关闭该文件夹?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: this.isDark ? 'darkElMessageBox' : ''
})
.then(() => {
this.$bus.$emit('closeFolder', this.currentFolder)
this.$message.success('关闭成功')
})
.catch(() => {})
},
//
refreshList(showTip = true) {
this.$bus.$emit('refreshFolder', this.currentFolder, showTip)
}
}
}
@ -159,7 +371,7 @@ export default {
<style lang="less" scoped>
.workbencheFileListContainer {
flex-grow: 1;
width: 100%;
height: 100%;
background-color: #fff;
border-radius: 10px;
@ -167,30 +379,89 @@ export default {
padding-top: 0;
display: flex;
flex-direction: column;
overflow: hidden;
.title {
font-weight: bold;
font-size: 18px;
border-bottom: 1px solid #e4e7ed;
height: 65px;
flex-shrink: 0;
&.isDark {
background-color: rgb(55, 59, 63);
.header {
border-bottom: 1px solid hsla(0, 0%, 100%, 0.1);
.headerLeft {
.title {
color: #fff;
}
}
}
/deep/ .el-table {
background-color: rgb(55, 59, 63);
.cell {
color: hsla(0, 0%, 100%, 0.6);
}
th.el-table__cell.is-leaf,
td.el-table__cell {
border-bottom: 1px solid hsla(0, 0%, 100%, 0.1);
}
td.el-table__cell,
th.el-table__cell,
.el-table--border::after,
.el-table--group::after,
&::before {
background-color: rgb(55, 59, 63);
}
}
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 65px;
flex-shrink: 0;
border-bottom: 1px solid #e4e7ed;
.clearBtn {
cursor: pointer;
font-size: 14px;
color: #409eff;
.headerLeft {
display: flex;
align-items: flex-end;
.title {
font-size: 18px;
cursor: pointer;
}
.tip {
font-size: 12px;
margin-left: 12px;
color: #f56c6c;
}
}
.headerRight {
display: flex;
align-items: center;
.textBtn {
cursor: pointer;
font-size: 14px;
color: #409eff;
margin-left: 12px;
user-select: none;
}
}
}
.fileListBox {
flex-grow: 1;
width: 100%;
height: 100%;
overflow: hidden;
.textBtn {
cursor: pointer;
user-select: none;
}
}
}

View File

@ -6,7 +6,7 @@
width="480px"
@close="onClose"
>
<div class="settingBox">
<div class="settingBox" :class="{ isDark: isDark }">
<div class="row">
<div class="label">默认结构</div>
<el-select
@ -58,6 +58,13 @@
@change="onChange"
></el-checkbox>
</div>
<div class="row">
<div class="label">暗黑模式</div>
<el-checkbox
v-model="otherConfig.isDark"
@change="toggleDark"
></el-checkbox>
</div>
</div>
</el-dialog>
</template>
@ -67,6 +74,7 @@ import { layoutList } from 'simple-mind-map/src/constants/constant'
import { layoutImgMap } from '@/config/constant.js'
import themeList from 'simple-mind-map-plugin-themes/themeList'
import themeImgMap from 'simple-mind-map-plugin-themes/themeImgMap'
import { mapState, mapMutations } from 'vuex'
export default {
model: {
@ -98,14 +106,23 @@ export default {
theme: '',
viewTranslateChangeTriggerAutoSave: false
},
clientConfig: null
clientConfig: null,
otherConfig: {
isDark: false
}
}
},
computed: {
...mapState({
isDark: state => state.localConfig.isDark
})
},
watch: {
value(val, oldVal) {
this.dialogVisible = val
if (val && !oldVal) {
this.getConfig()
this.getOtherConfig()
}
}
},
@ -113,6 +130,8 @@ export default {
this.onClose()
},
methods: {
...mapMutations(['setLocalConfig']),
onClose() {
this.$emit('change', false)
},
@ -130,6 +149,16 @@ export default {
...this.clientConfig,
...this.config
})
},
getOtherConfig() {
this.otherConfig.isDark = this.isDark
},
toggleDark(val) {
this.setLocalConfig({
isDark: val
})
}
}
}
@ -144,6 +173,14 @@ export default {
.settingBox {
padding: 20px;
&.isDark {
.row {
.label {
color: hsla(0, 0%, 100%, 0.6);
}
}
}
.row {
display: flex;
align-items: center;

View File

@ -1,27 +1,153 @@
<template>
<div class="workbencheSidebarContainer">
<div class="workbencheSidebarContainer" :class="{ isDark: isDark }">
<div class="createBtn" @click="create">开始新建</div>
<div class="line"></div>
<div class="btn" @click="openLocalFile">
<span class="icon iconfont icondakai"></span>
<span class="icon iconfont iconwenjian1"></span>
<span class="text">打开本地文件</span>
</div>
<div class="btn active">
<div class="btn" @click="openLocalFolder">
<span class="icon iconfont icondakai"></span>
<span class="text">打开本地文件夹</span>
</div>
<div
class="btn"
:class="{ active: currentActive == 'recent' }"
@click="changeTab('recent')"
>
<span class="icon iconfont iconzuijinliulan"></span>
<span class="text">最近文件</span>
</div>
<div class="folderList">
<div
class="folderItem"
v-for="item in dirList"
:class="{ active: currentActive == item.dir }"
@click="changeTab(item.dir)"
>
<span class="icon iconfont iconwenjianjia"></span>
<span class="text"> {{ item.dirName }}</span>
</div>
</div>
</div>
</template>
<script>
import { create } from '../utils'
import { mapState, mapMutations } from 'vuex'
export default {
data() {
return {
dirList: [],
currentActive: 'recent'
}
},
computed: {
...mapState({
isDark: state => state.localConfig.isDark
})
},
created() {
this.$bus.$on('closeFolder', this.onCloseFolder)
this.$bus.$on('refreshFolder', this.onRefreshFolder)
},
beforeDestroy() {
this.$bus.$off('closeFolder', this.onCloseFolder)
this.$bus.$off('refreshFolder', this.onRefreshFolder)
},
methods: {
...mapMutations(['setCurrentFolder']),
create,
openLocalFile() {
window.electronAPI.selectOpenFile()
//
async openLocalFile() {
const file = await window.electronAPI.selectOpenFile()
if (file) {
this.changeTab('recent')
}
},
//
async openLocalFolder() {
const dir = await window.electronAPI.selectOpenFolder()
if (dir) {
const files = await window.electronAPI.getFilesInDir(dir, '.smm')
this.addDirFileList({
dir,
files
})
}
},
//
onCloseFolder(folder) {
const index = this.dirList.findIndex(item => {
return item.dir === folder
})
if (index !== -1) {
const newFolder = index > 0 ? this.dirList[index - 1].dir : 'recent'
this.dirList.splice(index, 1)
this.changeTab(newFolder)
}
},
//
addDirFileList({ dir, files }, force) {
const sep = this.IS_WIN ? '\\' : '/'
files = files.map(name => {
return {
url: (dir[dir.length - 1] === sep ? dir : dir + sep) + name,
name
}
})
const exist = this.dirList.find(item => {
return item.dir === dir
})
if (exist) {
exist.files = files
} else {
const arr = dir.split(/[\/\\]/g)
this.dirList.push({
dir,
dirName: arr[arr.length - 1],
files
})
}
this.changeTab(dir, force)
},
//
changeTab(tab, force) {
if (this.currentActive === tab && !force) return
this.currentActive = tab
let files = []
if (tab !== 'recent') {
const folder = this.dirList.find(item => {
return item.dir === tab
})
if (folder) {
files = folder.files
}
}
this.setCurrentFolder(tab === 'recent' ? '' : tab)
this.$bus.$emit('changeFolder', tab, files)
},
//
async onRefreshFolder(folder, showTip = true) {
const files = await window.electronAPI.getFilesInDir(folder, '.smm')
this.addDirFileList(
{
dir: folder,
files
},
true
)
if (showTip) {
this.$message.success('刷新成功')
}
}
}
}
@ -36,6 +162,24 @@ export default {
border-radius: 10px;
margin-right: 20px;
padding: 15px 20px;
display: flex;
flex-direction: column;
overflow: hidden;
&.isDark {
background-color: rgb(55, 59, 63);
.line {
background-color: hsla(0, 0%, 100%, 0.1);
}
.btn,
.folderItem {
background-color: rgb(39, 42, 46);
color: #fff;
}
}
.createBtn {
width: 100%;
@ -49,6 +193,7 @@ export default {
cursor: pointer;
font-size: 14px;
user-select: none;
flex-shrink: 0;
&:hover {
opacity: 0.9;
@ -60,9 +205,11 @@ export default {
height: 1px;
background-color: #e4e7ed;
margin: 20px 0;
flex-shrink: 0;
}
.btn {
.btn,
.folderItem {
width: 100%;
height: 30px;
background-color: #fff;
@ -74,7 +221,8 @@ export default {
font-size: 14px;
user-select: none;
padding: 0 10px;
margin-bottom: 10px;
margin-bottom: 8px;
flex-shrink: 0;
&.active,
&:hover {
@ -86,5 +234,11 @@ export default {
margin-right: 10px;
}
}
.folderList {
width: 100%;
height: 100%;
overflow-y: auto;
}
}
</style>

View File

@ -1,5 +1,9 @@
<template>
<div class="winControl noDrag" v-if="IS_WIN || IS_LINUX">
<div
class="winControl noDrag"
:class="{ isDark: isDark }"
v-if="IS_WIN || IS_LINUX"
>
<div class="winControlBtn iconfont iconzuixiaohua" @click="minimize"></div>
<div
class="winControlBtn iconfont"
@ -11,12 +15,19 @@
</template>
<script>
import { mapState } from 'vuex'
export default {
data() {
return {
isMaximize: false
}
},
computed: {
...mapState({
isDark: state => state.localConfig.isDark
})
},
async created() {
try {
this.isMaximize = await window.electronAPI.getIsMaximize(
@ -54,6 +65,17 @@ export default {
align-items: center;
flex-shrink: 0;
height: 100%;
&.isDark {
.winControlBtn {
color: #fff;
&:hover {
background-color: #373b3f;
}
}
}
.winControlBtn {
width: 40px;
height: 100%;

View File

@ -9,7 +9,9 @@
placeholder=""
@blur="rename"
@keyup.enter="rename"
></el-input>
>
<template slot="append">.smm</template>
</el-input>
<div class="modifyDotBox">
<div class="modifyDot" v-show="isUnSave"></div>
</div>
@ -83,7 +85,8 @@ export default {
this.$confirm('有操作尚未保存,是否确认关闭?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
type: 'warning',
customClass: this.isDark ? 'darkElMessageBox' : ''
})
.then(async () => {
resolve()
@ -99,7 +102,6 @@ export default {
<style lang="less" scoped>
.workbencheEditContainer {
&.isDark {
.workbencheEditHeader {
background-color: #262a2e;

View File

@ -1,6 +1,7 @@
<template>
<div
class="workbencheHomeContainer"
:class="{ isDark: isDark }"
@drop="onDrop"
@dragenter="onDragenter"
@dragover="onDragover"
@ -40,6 +41,8 @@ import FileList from '../components/FileList.vue'
import AboutDialog from '../components/AboutDialog.vue'
import SponsorDialog from '../components/SponsorDialog.vue'
import SettingDialog from '../components/SettingDialog.vue'
import { getLocalConfig } from '@/api'
import { mapState, mapActions, mapMutations } from 'vuex'
export default {
components: {
@ -58,7 +61,33 @@ export default {
showSettingDialog: false
}
},
computed: {
...mapState({
isDark: state => state.localConfig.isDark
})
},
watch: {
isDark() {
this.setBodyDark()
}
},
created() {
this.initLocalConfig()
this.setBodyDark()
},
methods: {
...mapMutations(['setLocalConfig']),
initLocalConfig() {
let config = getLocalConfig()
if (config) {
this.setLocalConfig({
...this.$store.state.localConfig,
...config
})
}
},
handleCommand(command) {
switch (command) {
case 'about':
@ -85,6 +114,12 @@ export default {
}
},
setBodyDark() {
this.isDark
? document.body.classList.add('isDark')
: document.body.classList.remove('isDark')
},
onDrop(e) {
e.preventDefault()
e.stopPropagation()
@ -141,6 +176,21 @@ export default {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
&.isDark {
background: rgb(39, 42, 46);
.workbencheHomeHeader {
background-color: #262a2e;
.rightBar {
.settingBtn {
color: #fff;
}
}
}
}
.workbencheHomeHeader {
position: relative;
@ -169,7 +219,8 @@ export default {
}
.workbencheHomeContent {
flex-grow: 1;
width: 100%;
height: 100%;
padding: 20px;
display: flex;
overflow: hidden;

View File

@ -7,8 +7,8 @@ Vue.use(Vuex)
const store = new Vuex.Store({
state: {
fileName: '',// 本地的文件名
isUnSave: false,// 当前操作是否未保存
fileName: '', // 本地的文件名
isUnSave: false, // 当前操作是否未保存
mindMapData: null, // 思维导图数据
isHandleLocalFile: false, // 是否操作的是本地文件
localConfig: {
@ -22,12 +22,14 @@ const store = new Vuex.Store({
isShowScrollbar: false,
// 是否开启手绘风格
isUseHandDrawnLikeStyle: false,
// 是否开启动量效果
isUseMomentum: true,
// 是否是暗黑模式
isDark: false
},
activeSidebar: '', // 当前显示的侧边栏
localEditList: [],// 客户端中正在编辑的思维导图列表
isOutlineEdit: false,// 是否是大纲编辑模式
localEditList: [], // 客户端中正在编辑的思维导图列表
isOutlineEdit: false, // 是否是大纲编辑模式
isReadonly: false, // 是否只读
isSourceCodeEdit: false, // 是否是源码编辑模式
extraTextOnExport: '', // 导出时底部添加的文字
@ -38,7 +40,17 @@ const store = new Vuex.Store({
supportExcel: false, // 是否支持Excel插件
supportCheckbox: false, // 是否支持Checkbox插件
supportLineFlow: false, // 是否支持LineFlow插件
isDragOutlineTreeNode: false // 当前是否正在拖拽大纲树的节点
supportMomentum: false, // 是否支持Momentum插件
isDragOutlineTreeNode: false, // 当前是否正在拖拽大纲树的节点
aiConfig: {
api: 'http://ark.cn-beijing.volces.com/api/v3/chat/completions',
key: '',
model: '',
port: 3456,
method: 'POST'
},
enableAi: false, // 是否开启AI功能
currentFolder: '' // 当前打开的目录
},
mutations: {
// 设置本地文件名
@ -51,6 +63,11 @@ const store = new Vuex.Store({
state.isUnSave = data
},
setCurrentFolder(state, data) {
localStorage.setItem('currentFolder', data)
state.currentFolder = data
},
/**
* @Author: 王林
* @Date: 2021-04-10 14:50:01
@ -67,11 +84,18 @@ const store = new Vuex.Store({
// 设置本地配置
setLocalConfig(state, data) {
state.localConfig = {
const aiConfigKeys = Object.keys(state.aiConfig)
Object.keys(data).forEach(key => {
if (aiConfigKeys.includes(key)) {
state.aiConfig[key] = data[key]
} else {
state.localConfig[key] = data[key]
}
})
storeLocalConfig({
...state.localConfig,
...data
}
storeLocalConfig(state.localConfig)
...state.aiConfig
})
},
// 设置当前显示的侧边栏
@ -139,9 +163,19 @@ const store = new Vuex.Store({
state.supportLineFlow = data
},
// 设置是否支持Momentum插件
setSupportMomentum(state, data) {
state.supportMomentum = data
},
// 设置树节点拖拽
setIsDragOutlineTreeNode(state, data) {
state.isDragOutlineTreeNode = data
},
// 设置是否启用AI功能
setEnableAi(state, data) {
state.enableAi = data
}
},
actions: {

120
web/src/utils/ai.js Normal file
View File

@ -0,0 +1,120 @@
class Ai {
constructor(options = {}) {
this.options = options
this.baseData = {}
this.controller = null
this.currentChunk = ''
this.content = ''
}
init(type = 'huoshan', options = {}) {
// 火山引擎接口
if (type === 'huoshan') {
this.baseData = {
api: options.api,
method: options.method,
headers: {
Authorization: 'Bearer ' + options.key
},
data: {
model: options.model,
stream: true
}
}
}
}
async request(data, progress = () => {}, end = () => {}, err = () => {}) {
try {
const res = await this.postMsg(data)
const decoder = new TextDecoder()
while (1) {
const { done, value } = await res.read()
if (done) {
return
}
// 拿到当前切片的数据
const text = decoder.decode(value)
// 处理切片数据
let chunk = this.handleChunkData(text)
// 判断是否有不完整切片,如果有,合并下一次处理,没有则获取数据
if (this.currentChunk) continue
let isEnd = false
const list = chunk
.split('\n')
.filter(item => {
isEnd = item.includes('[DONE]')
return !!item && !isEnd
})
.map(item => {
return JSON.parse(item.replace(/^data:/, ''))
})
list.forEach(item => {
this.content += item.choices
.map(item2 => {
return item2.delta.content
})
.join('')
})
progress(this.content)
if (isEnd) {
end(this.content)
}
}
} catch (error) {
console.log(error)
// 手动停止请求不需要触发错误回调
if (!(error && error.name === 'AbortError')) {
err(error)
}
}
}
async postMsg(data) {
this.controller = new AbortController()
const res = await fetch(`http://localhost:${this.options.port}/ai/chat`, {
signal: this.controller.signal,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...this.baseData,
data: {
...this.baseData.data,
...data
}
})
})
if (res.status && res.status !== 200) {
return false
}
return res.body.getReader()
}
handleChunkData(chunk) {
chunk = chunk.trim()
// 如果存在上一个切片
if (this.currentChunk) {
chunk = this.currentChunk + chunk
this.currentChunk = ''
}
// 如果存在done,认为是完整切片且是最后一个切片
if (chunk.includes('[DONE]')) {
return chunk
}
// 最后一个字符串不为},则默认切片不完整,保存与下次拼接使用(这种方法不严谨,但已经能解决大部分场景的问题)
if (chunk[chunk.length - 1] !== '}') {
this.currentChunk = chunk
}
return chunk
}
stop() {
this.controller.abort()
this.controller = new AbortController()
}
}
export default Ai

View File

@ -118,3 +118,35 @@ export const setImgToClipboard = img => {
navigator.clipboard.write(data)
}
}
// 获取一个文件路径中的文件名
export const getFileName = (filePath) => {
const res = filePath.match(/([^/]+)\.smm$/)
if (res && res[1]) {
return res[1]
} else {
return ''
}
}
// 打印大纲
export const printOutline = el => {
const printContent = el.outerHTML
const iframe = document.createElement('iframe')
iframe.setAttribute('style', 'position: absolute; width: 0; height: 0;')
document.body.appendChild(iframe)
const iframeDoc = iframe.contentWindow.document
// 将当前页面的所有样式添加到iframe中
const styleList = document.querySelectorAll('style')
Array.from(styleList).forEach(el => {
iframeDoc.write(el.outerHTML)
})
// 设置打印展示方式 - 纵向展示
iframeDoc.write('<style media="print">@page {size: portrait;}</style>')
// 写入内容
iframeDoc.write('<div>' + printContent + '</div>')
setTimeout(function() {
iframe.contentWindow?.print()
document.body.removeChild(iframe)
}, 500)
}