赞
踩
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
在做的一个项目中,需要制作一个类似于Xmind的里面的一种思维导图的操作页面,里面是可编辑的节点,这里就不好使用echarts里面的树图,最后找到了这个jsmind.js的插件,就是用于生产页面版的思维树图之类的,官方文档篇幅属实有限,而且页面是根据需求来改动的,以此记录这个页面的实现。
一、最终页面效果展示
如图, 这个页面需要能动态添加自定义的节点,且每个节点的数据是可以下拉选择并且每一个都有对应的自定义设置的权重,我们先根据需求整理大体功能思路。
一、先使用jsmind插件来生成一个基本的模块。
二、根节点的模型组名是固定的,这里不应该让它能编辑,同时第一级的模型名节点分为两大类,我们可以在点击开始建模后初始化时生成一个固定的根节点以及两个不同类型的模型名一级节点的卡片。
三、通过自定义右键菜单来添加三个功能,分别是 添加同级、添加子级以及删除卡片。
四、动态获取一级子节点的模型名数据以及对应的二级子节点的标签分类数据以及三级子节点的标签名数据,其中每个数据中都是带有对应的权重的。
五、当每一个卡片节点切换时,对应的子节点的数据也需要动态切换。
六、当每一个卡片重新设置权重时,对应的卡片选项中的权重也要进行关联。
官方链接: jsMind - Developer & Open Source
通过 npm install jsmind --save
安装插件
在 vue 页面文件里面引入并使用( 只需在当前这个需要制作的页面文件中使用即可 ):
- import 'jsmind/style/jsmind.css';
- import jsMind from 'jsmind/js/jsmind.js';
-
- require('jsmind/js/jsmind.draggable.js');
- require('jsmind/js/jsmind.screenshot.js');
代码如下(示例):
基本的使用以及其他的一些功能可以参考下这篇博客,我当初也是从这里进行参考的。
vue实现思维导图_吕振辉的博客-CSDN博客_vue实现思维导图
由于目前的页面已基本完成,所以不可能每部分的代码都是十分清晰的,且由于当时以实现功能为首要目标,代码中存在大量的冗余,尽量在进行优化,主要细说一下每一步的思路以及解决方案和遇到的问题。每个模块的代码由于功能的上下连接,单独模块放出来意义不大,且很难读懂,有些地方附上一些关键代码,其他整个文件代码到时候放在最后。
由设计图可知,该页面需先输入根节点的模型组名然后点击开始建模按钮后,生成基本的架构思维图。这里主要是要注意一点,点击开始建模按钮之前,先判断用户是否有输入,同时,开始建模按钮点击之后,就应该禁用掉,点击了重新建模按钮之后,再放开开始建模按钮。
由于第二子级的标签分类是固定的,所以先按照分类写成固定数据,但是第三子级的标签名得通过不同的分类来进行瓜分,所以这个数据方面还是有点复杂的,参考性不高,更多只是个人记录。数据分类结构如下:
- resultData: {
- personList: [],
- relationList: [],
- labelTypeList: {
- pointType: [
- { label: '自身属性标签', value: 'property', weight: 1 },
- { label: '自身行为标签', value: 'action', weight: 1 }
- ],
- relationType: [
- { label: '实质关系标签', value: 'substance', weight: 1 },
- { label: '疑似关系标签', value: 'suspected', weight: 1 },
- { label: '增强关系标签', value: 'strengthen', weight: 1 }
- ]
- },
- labelNameList: {
- pointLabel: [], // 当前选中的模型 所有的个人标签
- relationLabel: [], // 当前选中的模型 所有的关系标签
- // 五大类的标签名列表数据
- propertyLabelList: [],
- actionLabelList: [],
- substanceLabelList: [],
- suspectedLabelList: [],
- strengthenLabelList: []
- }
- }
上面的 initModelChart() 方法就是初始化生成图形,同时,我们需要给每一个卡片添加点击事件,并且卡片里面的是html元素,这里我们需要在一个js文件里面进行原生html的生成,方便添加。
用于生成卡片节点元素以及其他元素的js文件, createDom.js :
- /* eslint-disable no-useless-escape */
- class CreateDom {
- // 获取根卡片 模型组名
- static getModelRootDom(modelInfo) {
- return `
- <div class="model-edit-card" style="background-color: ${modelInfo.bgc}" >
- <div class="model-info">
- <p class="model-title-p">模型组名</p>
- <div class="model-name-info">
- <p><span>${modelInfo.modelName}</span></p>
- </div>
- </div>
- </div>
- `;
- }
-
- // 获取子级卡片 模型名/标签
- static getModelCardDom(modelInfo) {
- let selectList = ``;
- for (let i = 0; i < modelInfo.modelSelectList.length; i++) {
- selectList += `<option value="${modelInfo.modelSelectList[i].value}">${modelInfo.modelSelectList[i].label}</option>`;
- }
-
-
- return `
- <div class="model-edit-card" style="background-color: ${modelInfo.bgc}" >
- <div class="model-info">
- <p class="model-title-p">${modelInfo.modelTitle}</p>
- <div class="model-name-info">
- <select class="select-model-list" data-type='${modelInfo.modelType}' data-title='${modelInfo.modelTitle}' >
- ${selectList}
- </select>
- </div>
- </div>
- <div class="model-weight" >
- <p>权重</p>
- <input
- type='text'
- data-label='${modelInfo.selectOption}'
- data-model='${modelInfo.selectModelId}'
- data-level='${modelInfo.level}'
- data-type='${modelInfo.modelType}'
- class="model-weight-input"
- placeholder=""
- onkeyup="!/^(\d+\.?)?\d{0,1}$/.test(this.value)?(this.value=this.value.substring(0, this.value.length-1)): ''"
- value=${modelInfo.modelWeight}>
- </input>
- </div>
- </div>
- `;
- }
-
- // 生成自定义右键菜单
- static getContextMenu() {
- return `
- <el-menu
- class="context-menu"
- v-show="showMenu"
- :style="{
- left: menuStyle.left,
- top: menuStyle.top,
- bottom: menuStyle.bottom,
- right: menuStyle.right
- }"
- ref="context">
- <slot>
- <el-menu-item @click="addBrother">插入平级</el-menu-item>
- <el-menu-item @click="addChild">插入子级</el-menu-item>
- <el-menu-item @click="delCard">删除卡片</el-menu-item>
- </slot>
- </el-menu>
- `;
- }
-
-
- // 获取固定的初始化数据
- static getInitData(params) {
- return [
- {
- id: 'point', // 必选 ID, 所有节点的 id 不应该重复,否则 重复id的结节将被忽略
- topic: CreateDom.getModelCardDom({ bgc: '#0FA984', modelTitle: '模型名', modelSelectList: params.personList, modelType: 'point', selectOption: params.personList[0].label, selectModelId: params.personList[0].value, level: 1, modelWeight: 1 }), // 必填 节点上显示的内容
- direction: 'right', // 可选 节点的方向 此数据仅在第一层节点上有效,目前仅支持 left 和 right 两种,默认为 right
- expanded: true, // [可选] 该节点是否是展开状态,默认为 true
- level: 1,
- type: 'point',
- title: '模型名',
- children: []
- },
- {
- id: 'relation', // 必选 ID, 所有节点的 id 不应该重复,否则 重复id的结节将被忽略
- topic: CreateDom.getModelCardDom({ bgc: '#0FA984', modelTitle: '模型名', modelSelectList: params.relationList, modelType: 'relation', selectOption: params.relationList[0].label, selectModelId: params.relationList[0].value, level: 1, modelWeight: 1 }), // 必填 节点上显示的内容
- direction: 'right', // 可选 节点的方向 此数据仅在第一层节点上有效,目前仅支持 left 和 right 两种,默认为 right
- expanded: true, // [可选] 该节点是否是展开状态,默认为 true
- level: 1,
- type: 'relation',
- title: '模型名',
- children: []
- }
- ];
- }
- }
-
- export default CreateDom;
在vue文件里面引入
import CreateDom from '@/tools/CreateDom.js';
其中 initModelChart 方法的代码:
- data() {
- return {
- mind: {
- // 元数据 定义思维导图的名称、 作者、版本等信息
- meta: {
- name: '建模导图',
- author: 'ck',
- version: '0.2'
- },
- // 数据格式声明
- format: 'node_tree',
- // 数据内容
- data: {
- id: 'root',
- topic: '',
- direction: 'right',
- expanded: true,
- children: []
- }
- },
- options: {
- container: 'create-model-chart', // [必选] 容器的ID
- editable: true, // [可选] 是否启用编辑
- theme: '', // [可选] 主题
- support_html: true,
- mode: 'full', // 显示模式
- view: {
- engine: 'canvas', // 思维导图各节点之间线条的绘制引擎
- hmargin: 120, // 思维导图距容器外框的最小水平距离
- vmargin: 50, // 思维导图距容器外框的最小垂直距离
- line_width: 4, // 思维导图线条的粗细
- line_color: '#FFCC73', // 思维导图线条的颜色
- draggable: true, // 当容器不能完全容纳思维导图时,是否允许拖动画布代替鼠标滚动
- hide_scrollbars_when_draggable: true // 当设置 draggable = true 时,是否隐藏滚动条
- },
- layout: {
- hspace: 100, // 节点之间的水平间距
- vspace: 20, // 节点之间的垂直间距
- pspace: 20 // 节点与连接线之间的水平间距(用于容纳节点收缩/展开控制器)
- },
- shortcut: {
- enable: false // 是否启用快捷键 默认为true
- },
- menuOpts: {
- showMenu: true,
- injectionList: [
- { target: 'addBrother', text: '添加同级卡片', callback: function (node) { console.log(node); } },
- { target: 'delete', text: '删除卡片', callback: function (node) { console.log(node); } }
- ]
- },
- isShow: true
- }
- }
- }
-
-
- initModelChart() {
- this.mind.data.topic = CreateDom.getModelRootDom(this.rootModel);
- this.mind.data.children = CreateDom.getInitData(this.resultData);
- this.$nextTick(() => {
- this.jm = jsMind.show(this.options, this.mind);
- this.chartLoading = false;
- // let testSelect = this.jm.get_node('easy');
- // console.log(testSelect, 'tes');
- const modelChart = document.getElementById('create-model-chart');
- modelChart.addEventListener('mousedown', e => {
- e.stopPropagation();
- this.showMenu = false;
- // console.log(e, '99666');
- // this.showTheMenu(e);
- let selectCardId = '';
- if (Array.isArray(e.path)) {
- e.path.map(item => {
- if (item.localName == 'jmnode') {
- // console.log(item.attributes[0].nodeValue, '3030');
- selectCardId = item.attributes[0].nodeValue;
- }
- });
- // console.log(selectCardId, 'sed');
- if (selectCardId == 'root') {
- this.showMenu = false;
- return this.$message.warning('根节点无法编辑');
- } else if (!selectCardId) {
- this.showMenu = false;
- return false;
- }
- this.theSelectNode = this.jm.get_node(selectCardId);
- // console.log(this.theSelectNode, '2200');
- // console.log(selectCardId, '0022');
- this.clickSelectCard(selectCardId);
- this.findClickCardIndex(this.theSelectNode.data.type, this.theSelectNode.data.level, this.theSelectNode.id);
- }
- });
-
- this.addSelectChangeFunc();
- this.addInputBlurFunc();
- });
- }
其中,给每个节点添加点击事件,由于有后续添加的节点,所以最好绑定在父节点上面,通过事件捕获,找到是卡片的节点,给这个卡片添加对应的操作。相关的操作方法代码放在整体里面,这里不做详细展示,主要是看如何通过原生js找到节点位置并对该选中的节点进行添加自定义的鼠标右键菜单, 这里即是 clickSelectCard 这个方法,代码如下:
- clickSelectCard(nodeId) {
- this.editor = this.jm.view.e_editor;
- // jsmind 添加自定义菜单事件
- this.jm.view.add_event(this.editor, 'contextmenu', (e) => {
- const selectedNode = this.jm.get_node(nodeId);
- // && selectedNode.data.type
- if (selectedNode) {
- e.preventDefault();
- const el = document.querySelector('.context-menu .el-menu-item');
- const width = parseFloat(window.getComputedStyle(el).width);
- const height = parseFloat(window.getComputedStyle(el).height) * 3 + 12;
- const windowHeight = window.innerHeight;
- const windowWidth = window.innerWidth;
-
- // 极限位置 避免越界
- if (e.clientY + height > windowHeight) {
- // console.log('23');
- this.menuStyle.left = e.clientX + 'px';
- this.menuStyle.top = 'unset';
- this.menuStyle.bottom = 0;
- } else if (e.clientX + width > windowWidth) {
- // console.log('24');
- this.menuStyle.top = e.clientY + 'px';
- this.menuStyle.left = 'unset';
- this.menuStyle.right = 0;
- } else {
- // console.log('25');
- this.menuStyle.left = e.clientX - 210 + 'px';
- this.menuStyle.top = e.clientY - 150 + 'px';
- this.menuStyle.bottom = 'unset';
- }
- this.showMenu = true;
- } else {
- this.showMenu = false;
- }
- });
- }
这里的菜单位置可能不太对,需要自己来进行调整。
这里主要通过找到是哪一个节点右键了,根据这个节点来做相应的功能,其中菜单的元素代码如下:
- <el-menu
- class="context-menu"
- v-show="showMenu"
- :style="{
- left: menuStyle.left,
- top: menuStyle.top,
- bottom: menuStyle.bottom,
- right: menuStyle.right,
- }"
- ref="context">
- <slot>
- <el-menu-item @click="addBrother">插入平级</el-menu-item>
- <el-menu-item @click="addChild">插入子级</el-menu-item>
- <el-menu-item @click="deleteCard">删除卡片</el-menu-item>
- </slot>
- </el-menu>
-
-
- .context-menu {
- position: absolute;
- width: 8.3333rem;
- z-index: 32;
- }
由于添加同级和添加子级都是用的同一个api,官方里面的往元素后面添加节点的方法其实并不是添加子级,这里添加子级只需要把当前选中的卡片节点的id设置为父级id即可。
这里先写一个方法返回添加节点时需要的对象数据,如下:
- // 添加卡片的方法 获得相关的数据对象
- whileCardAddFunc(type, node) {
- var funcObj = {};
- console.log(node, 'node');
- // type 进行 同级 bother 或者 子级 children 的分类 this.resultData.personList[0].label
- if (type == 'bother') {
- if (node.data.level == '1') {
- node.data.type == 'point' ? this.pointLevel1Index++ : this.relationLevel1Index++;
- funcObj.parentId = node.parent.id;
- funcObj.id = new Date().getTime() + parseFloat(Math.random() * 1000);
- funcObj.topic = CreateDom.getModelCardDom({ bgc: '#0FA984', modelTitle: '模型名', modelSelectList: node.data.type == 'point' ? this.resultData.personList : this.resultData.relationList, modelType: node.data.type, selectOption: node.data.type == 'point' ? this.resultData.personList[0].label : this.resultData.relationList[0].label, selectModelId: node.data.type == 'point' ? this.resultData.personList[0].value : this.resultData.relationList[0].value, level: 1, modelWeight: 1 });
- funcObj.data = {
- direction: 1,
- expanded: true,
- level: 1,
- type: node.data.type,
- children: []
- };
- // 判断是 个人模型 还是 关系 模型 往 已选则的 list 里面进行 push 一个
- this.addLabelValueWeight(node.data.type, 1, funcObj.id);
- } else if (node.data.level == '2') {
- node.data.type == 'point' ? this.pointLevel2Index++ : this.relationLevel2Index++;
- funcObj.parentId = node.parent.id;
- funcObj.id = new Date().getTime() + parseFloat(Math.random() * 1000);
- funcObj.topic = CreateDom.getModelCardDom({ bgc: '#5B9BD5', modelTitle: '标签分类', modelSelectList: node.data.type == 'point' ? this.resultData.labelTypeList.pointType : this.resultData.labelTypeList.relationType, modelType: node.data.type, selectOption: node.data.type == 'point' ? this.resultData.labelTypeList.pointType[0].label : this.resultData.labelTypeList.relationType[0].label, selectModelId: node.data.type == 'point' ? this.resultData.labelTypeList.pointType[0].value : this.resultData.labelTypeList.relationType[0].value, level: 2, modelWeight: this.findTheCardWeight(node.data.type, 2) });
- funcObj.data = {
- direction: 'right',
- expanded: true,
- level: 2,
- type: node.data.type,
- children: []
- };
- this.addLabelValueWeight(node.data.type, 2, funcObj.id);
- } else if (node.data.level == '3') {
- let optionList = this.getLabelNameListBySort(node.data.type, node.data.type == 'point' ? this.selectPonitLabelSort : this.selectRelationLabelSort);
- node.data.type == 'point' ? this.pointLevel3Index++ : this.relationLevel3Index++;
- funcObj.parentId = node.parent.id;
- funcObj.id = new Date().getTime() + parseFloat(Math.random() * 1000);
- funcObj.topic = CreateDom.getModelCardDom({ bgc: '#E3950E', modelTitle: '标签名', modelSelectList: optionList, modelType: node.data.type, selectOption: optionList.length > 0 ? optionList[0].label : '', selectModelId: optionList.length > 0 ? optionList[0].value : '', level: 3, modelWeight: this.findTheCardWeight(node.data.type, 3, optionList.length > 0 ? optionList[0].label : '') });
-
- funcObj.data = {
- level: 3,
- type: node.data.type
- };
- this.addLabelValueWeight(node.data.type, 3, funcObj.id);
- }
- } else if (type == 'children') {
- if (node.data.level == '1') {
- console.log(this.findTheCardWeight(node.data.type, 2), 'classWei');
- node.data.type == 'point' ? this.pointLevel2Index++ : this.relationLevel2Index++;
- funcObj.afterId = node.id;
- funcObj.id = new Date().getTime() + parseFloat(Math.random() * 1000);
- funcObj.topic = CreateDom.getModelCardDom({ bgc: '#5B9BD5', modelTitle: '标签分类', modelSelectList: node.data.type == 'point' ? this.resultData.labelTypeList.pointType : this.resultData.labelTypeList.relationType, modelType: node.data.type, selectOption: node.data.type == 'point' ? this.resultData.labelTypeList.pointType[0].label : this.resultData.labelTypeList.relationType[0].label, selectModelId: node.data.type == 'point' ? this.resultData.labelTypeList.pointType[0].value : this.resultData.labelTypeList.relationType[0].value, level: 2, modelWeight: this.findTheCardWeight(node.data.type, 2) });
- funcObj.data = {
- direction: 'right',
- expanded: true,
- level: 2,
- type: node.data.type,
- children: []
- };
- this.addLabelValueWeight(node.data.type, 2, funcObj.id);
- } else if (node.data.level == '2') {
- let optionsList = this.getLabelNameListBySort(node.data.type, node.data.type == 'point' ? this.selectPonitLabelSort : this.selectRelationLabelSort);
- node.data.type == 'point' ? this.pointLevel3Index++ : this.relationLevel3Index++;
- funcObj.afterId = node.id;
- funcObj.id = new Date().getTime() + parseFloat(Math.random() * 1000);
- funcObj.topic = CreateDom.getModelCardDom({ bgc: '#E3950E', modelTitle: '标签名', modelSelectList: optionsList, modelType: node.data.type, selectOption: optionsList.length > 0 ? optionsList[0].label : '', selectModelId: optionsList.length > 0 ? optionsList[0].value : '', level: 3, modelWeight: this.findTheCardWeight(node.data.type, 3, optionsList.length > 0 ? optionsList[0].label : '') });
- funcObj.data = {
- level: 3,
- type: node.data.type
- };
- this.addLabelValueWeight(node.data.type, 3, funcObj.id);
- } else if (node.data.level == '3') {
- funcObj.error = true;
- funcObj.msg = '无法再进行下一级的分类添加';
- }
- }
- let copyObj = JSON.parse(JSON.stringify(funcObj));
- // console.log(copyObj, '30369');
- return copyObj;
- }
由于最后第三级的标签名卡片是不能再往后面添加子级了,所以设置一个error为true,并返回一个msg,这里需要注意的是,我们需要设置 options 里面的 editable 属性为 true,开启编辑,否则是无法进行节点卡片的添加的,这样有一个不好的地方就是双击卡片会变成html元素的input输入框,在使用时需要注意。
添加同级卡片的方法:
- // 添加同级卡片
- addBrother() {
- // parent_node, node_id, topic, data
- let cardObj = this.whileCardAddFunc('bother', this.theSelectNode);
- // console.log(cardObj, 'cla');
- this.jm.add_node(cardObj.parentId, cardObj.id, cardObj.topic, cardObj.data);
- this.showMenu = false;
- this.addSelectChangeFunc();
- this.addInputBlurFunc();
- }
添加子级卡片的方法:
- // 添加子卡片
- addChild() {
- // node_after, node_id, topic, data
- // 判断是哪个层级 1 模型名 2 标签分类 3 标签名
- let cardObj = this.whileCardAddFunc('children', this.theSelectNode);
- // console.log(cardObj, '6688');
- if (cardObj.error) {
- this.showMenu = false;
- return this.$message.warning(cardObj.msg);
- }
- this.jm.add_node(cardObj.afterId, cardObj.id, cardObj.topic, cardObj.data);
- this.showMenu = false;
- this.addSelectChangeFunc();
- this.addInputBlurFunc();
- }
这里最后的两个方法,是卡片里面的select下拉框的切换方法和权重input输入框的blur方法,在生成新的卡片时,需要给所有的卡片重新绑定事件,否则新生成的卡片是不带下拉框的change和输入框的blur事件的,这里使用 onchange和 onblur 方法来进行监听, 重新设置时会覆盖掉原来的方法,不推荐使用 addEventListener,因为不会覆盖,会重复执行。
删除卡片的方法:
- let that = this;
- // let deleteNode = JSON.parse(JSON.stringify(this.theSelectNode));
- let deleteNode = StringTools.cloneDeepObj(that.theSelectNode);
- this.$confirm(
- '删除此节点卡片(包含所有子级卡片), 是否继续?',
- '提示',
- {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- }
- ).then(() => {
- // 删除卡片
- if (deleteNode.data.type == 'point') {
- if (deleteNode.data.level == 1) {
- if (that.haveSelectPointModelAndWeight.length <= 1) {
- return that.$message.warning('至少保留一个个人模型名卡片');
- }
- that.haveSelectPointModelAndWeight.map((item, index) => {
- if (item.boxId == deleteNode.id) {
- that.haveSelectPointModelAndWeight.splice(index, 1);
- }
- });
- } else if (deleteNode.data.level == 2) {
- that.haveSelectPointModelAndWeight[that.haveSelectPointIndex].children.map((item, index) => {
- if (item.boxId == deleteNode.id) {
- that.haveSelectPointModelAndWeight[that.haveSelectPointIndex].children.splice(index, 1);
- }
- });
- } else if (deleteNode.data.level == 3) {
- that.haveSelectPointModelAndWeight[that.haveSelectPointIndex].children[that.haveSelectPointLabelClassIndex].children.map((item, index) => {
- if (item.boxId == deleteNode.id) {
- that.haveSelectPointModelAndWeight[this.haveSelectPointIndex].children[that.haveSelectPointLabelClassIndex].children.splice(index, 1);
- }
- });
- }
- } else {
- if (deleteNode.data.level == 1) {
- if (that.haveSelectRelationModelAndWeight.length <= 1) {
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。