效果图:
业务需要根据后端返回的数据,渲染出思维导图,在这个过程中因为网络上教程不多,而且大部分教程的都是好几年前的,具有滞后性,我在其中也遇到了许多问题,所以将我的思路和解决方法分享出来供大家参考。
该项目功能包括:1.渲染思维导图 2.查看节点信息 3.编辑节点信息 4.导入xmind 5.导出xmind 6.保存为当前版本 7.保存为新版本 8.右键新增节点
说一下前置知识,可以先模仿这两篇文章,自己用静态数据跑出思维图,方便之后的改造升级,Jsmind的官方文档:
3.operation | zh | docs | jsMind
一、数据处理
文章里,我们可以很容易看出,jsmind渲染出思维导图的数据格式是固定的,所以我们在与后端进行对接的时候,后端应该返回这种格式的数据报。
var mind = {
/* 元数据,定义思维导图的名称、作者、版本等信息 */
"meta":{
"name":"jsMind-demo-tree",
"author":"hizzgdev@163.com",
"version":"0.2"
},
/* 数据格式声明 */
"format":"node_tree",
/* 数据内容 */
"data":{"id":"root","topic":"jsMind","children":[
{"id":"easy","topic":"Easy","direction":"left","expanded":false,"children":[
{"id":"easy1","topic":"Easy to show"},
{"id":"easy2","topic":"Easy to edit"},
{"id":"easy3","topic":"Easy to store"},
{"id":"easy4","topic":"Easy to embed"}
]},
{"id":"open","topic":"Open Source","direction":"right","expanded":true,"children":[
{"id":"open1","topic":"on GitHub"},
{"id":"open2","topic":"BSD License"}
]},
{"id":"powerful","topic":"Powerful","direction":"right","children":[
{"id":"powerful1","topic":"Base on Javascript"},
{"id":"powerful2","topic":"Base on HTML5"},
{"id":"powerful3","topic":"Depends on you"}
]},
{"id":"other","topic":"test node","direction":"left","children":[
{"id":"other1","topic":"I'm from local variable"},
{"id":"other2","topic":"I can do everything"}
]}
]}
};
其中 ,data需要后端返回,前面的配置都可以在前端进行配置,正式开发时,在<script>的data()中,新增mind数据结构,初始化data,在fetchdata的时候,拿到后端的数值进行赋值。
mind: {
meta: { name: '思维导图', author: '', version: '0.2' },
format: 'node_tree',
data: {},
},
options: {
container: 'jsmind_container',
// editable: true,
enable_dblclick_handle: false,
// theme: 'wisteria',
view: {
engine: 'canvas',
hmargin: 20,
vmargin: 20,
line_width: 1,
line_color: '#999999',
hide_scrollbars_when_draggable: false,
draggable: true,
zoom: { min: 0.5, max: 5.0, step: 0.1 },
},
layout: { hspace: 100, vspace: 20, pspace: 20 },
shortcut: { enable: false },
},
在fetchdata的时候,可能由于数据量过大,导致速度慢,这个时候需要控制时序,直接fetch并赋值的话可能会导致取得的数据压根没有成功赋值到data中,正确的处理方式应为
async mounted() {
this.setHeaderName()
const savedData = JSON.parse(localStorage.getItem('tempMindData'))
// 检查临时状态是否有数据
if (savedData && this.entranceFlag !== 1) {
console.log('进入savedData', savedData)
this.mind.data = savedData
} else {
await this.fetchData()// fetchData就是向后端请求数据,也需要用异步执行
}
}
这里顺带提一下async/await的知识
async
async
是一个关键字,用于声明一个函数是异步的。- 一个
async
函数会返回一个Promise
对象。如果函数正常执行结束,Promise
会以函数的返回值被解决(resolved);如果函数中抛出错误,Promise
会被拒绝(rejected)。async
函数可以包含零个或多个await
表达式。await
await
是一个关键字,用于等待一个Promise
解决(resolve)或拒绝(reject)。await
只能在async
函数内部使用。- 当执行到
await
表达式时,JavaScript 会暂停该async
函数的执行,直到等待的Promise
被解决或拒绝,然后继续执行async
函数的剩余部分。- 如果
Promise
被解决,await
表达式的结果是Promise
的值;如果Promise
被拒绝,会抛出拒绝的值。
example:
function fetchData() {
return new Promise((resolve) => {
console.log("1");
resolve('data'); // 模拟异步操作完成后的返回值
});
}
async function getData() {
try {
const data = await fetchData(); // 这里会等待fetchData中的Promise解决
console.log("2");
console.log(data); // 打印从Promise中获取的数据
} catch (error) {
console.error('Failed to fetch data:', error);
}
console.log("3");
}
getData();
如上的代码会打印出 1 2 data 3,这也是面试题的考点
二、导入xmind/导出xmind
点击导入xmind会自动跳出上传文件的窗口,加上校验(只能上传.xmind文件)
<label for="file-upload" class="custom-file-upload">导入Xmind</label>
<input
id="file-upload"
class="uploadFile"
type="file"
accept=".xmind"
@change="readXmind($event)"
ref="fileInput" <!-- 这个输入框就被命名为“inputRef” -->
/>
其实ref在这里没什么用,ref的官方定义有点抽象,简单来说
在代码中,
ref
用来给元素或组件起一个名字,这样你就可以在代码的其他部分通过这个名字找到并操作这个元素或组件。这就像是你对电脑说:“嘿,电脑,去找到那个叫做‘inputRef’的输入框,然后让它获得焦点。”电脑就会按照你的命令去做。
导入这个地方有个难点,就是如何去渲染,
导入xmind重新渲染时,我们是将data赋值了,但是fetchData还是会在页面刷新的时候照常执行,由于jsmind自身设计会有个初始化的div块,新传入的xmind导图会被置于下面,上方有个空白div
这样就是不是我们想要的那种覆盖型,所以为了实现纯粹覆盖,我想到用临时数据savadData来存储导入的xmind,在页面刷新的时候存储在localstorage里
LocalStorage
是 Web 存储的一部分,它允许网站和应用在用户的浏览器中存储数据。这些数据会保存在本地(即用户的电脑上),即使浏览器关闭后再打开,数据依然存在。数据以字符串的形式保存。
Vuex
是一个专为 Vue.js 应用程序开发的状态管理模式和库。它集中化管理所有组件的状态。
LocalStorage
是 Web 标准的一部分,不依赖于特定的框架。Vuex
专为 Vue.js 设计,与 Vue.js 框架紧密集成。
这里选择localstorage是因为更加简洁方便。
在页面刷新的时候,先检查localstorage是否有数据,如果有就优先渲染本地数据;如果无就渲染后端返回数据。
this.setHeaderName()
const savedData = JSON.parse(localStorage.getItem('tempMindData'))
// 检查临时状态是否有数据
if (savedData && this.entranceFlag !== 1) {
console.log('进入savedData', savedData)
this.mind.data = savedData
} else {
await this.fetchData()
}
async readXmind(e) {
const formData = new FormData()
const file = e?.target?.files?.[0]
formData.append('file', file)
if (!file) return
const res = await importXmindFile(formData)
this.tempMindData = res.mindMapNodeVO
localStorage.setItem('tempMindData', JSON.stringify(this.tempMindData))
console.log('执行redxmind之后this.mind', this.tempMindData)
this.entranceFlag = 0
this.$router.go(0)
// this.$router.go(1)
},
⚠️注意:记得在离开当前页面的时候,将localstorage的数据清空
goBack() {
localStorage.removeItem('tempMindData')
this.$router.go(-1)
},
三、编辑/展示..响应鼠标操作
在这个地方我也是苦恼多天,也是查阅了网上的资料,许多人是用jsmind.menu.js实现,但这套在我的项目里不尽如人意,有兴趣的可以自己试试,除此之外,addEventListener也效果不好。
我又看了一下开发者hizzgdev的github的issue:Issues · hizzgdev/jsmind · GitHub,发现也有许多人提出这个问题,根据有个人提出的jquery实现。
jQuery 是一个快速、小巧且功能丰富的 JavaScript 库。通过 jQuery,你可以以更简洁的方式绑定事件处理器到 DOM 元素上,处理常见的用户交互事件。
单击查看,双击编辑,右键额外的操作。具体的响应函数就不赘述。
// 在mounted中
$('#jsmind_container').on('click', (event) => {
// 清除计时器,避免重复触发
clearTimeout(clickTimer)
clickTimer = setTimeout(() => {
// 如果300毫秒内没有第二次点击,则认为是单击事件
this.handleJsmindClick(event)
}, 300) // 300毫秒的阈值可以根据实际情况调整
})
$('#jsmind_container').on('dblclick', (event) => {
// 阻止默认的双击事件
event.preventDefault()
// 清除计时器,避免双击事件被误识别为单击
clearTimeout(clickTimer)
this.handleJsmindDBLClick(event)
})
$('#jsmind_container').bind('contextmenu', function () {
return false
})
// document.oncontextmenu = function() { return false;}
$('#jsmind_container').mousedown(function (e) {
if (e.which === 3) {
console.log('右键点击')
}
})
然后再用jsmind自带的this.jm.get_selected_node()去获得节点数据/id进行交互。
值得一提的事,一开始并不奏效,但我在把options里的双击属性禁用后enable_dblclick_handle: false,就阴差阳错奏效,但我也不知道原因,有佬知道可以解答下。
四、样式/折叠/缩放/移动
官方文档提供了许多主题,但这些主题都是单一色调,可能不是很好能看出层级关系,特别是在很大的图中。而且如果初始图很大的话,一开始就展开不方便查看。
根据属性expanded,可以让后端在返回数据报的时候决定节点是否展开,当然在前端也可以控制整体的默认展开程度,但不能控制单一节点。
也可以在数据里设置background-color 。这样就可以使每级颜色不一样。
至于缩放和拖拽,在view里面设置draggale和zoom即可
options: {
container: 'jsmind_container',
// editable: true,
enable_dblclick_handle: false,
// theme: 'wisteria',
view: {
engine: 'canvas',
hmargin: 20,
vmargin: 20,
line_width: 1,
line_color: '#999999',
hide_scrollbars_when_draggable: false,
draggable: true, // 拖拽
zoom: { min: 0.5, max: 5.0, step: 0.1 }, // 缩放
},
暂时应该就这些要点,想到了再补充,欢迎提出问题和改进~