在项目中遇到组织架构或思维导图的需求,选的技术是 jsmind
官方文档给的示例,有需要的可以参考:示例
先来看看效果图:
查看完整代码
如想查看完整代码:请访问 jsmind_demo
尝试了两种类型:第一张截图是普通菜单类型,第二张截图可以进行右键菜单操作。最终选用了普通菜单类型。
jsmind
可以导入 jm
文件并渲染出来,也可以对编辑后的文件进行保存,也可以下载编辑好的思维导图,可以展开指定层级节点,修改主题,以及对节点进行增删改查等操作。
演示:
接下来如何使用:
安装
npm i jsmind // 或者 yarn add jsmind
只听到从知秋君办公室传来知秋君的声音: 此中有真意,欲辨已忘言。有谁来对上联或下联?
普通菜单代码
此代码由一叶知秋网-知秋君整理<template> <!-- 普通菜单 --> <div class="jsmind_layout"> <div class="jsmind_toolbar" v-if="showBar"> <el-upload class="pad" :multiple="false" ref="upload" action="action" :before-upload="beforeUpload" :http-request="upload"> <el-button type="primary" size="medium">导入</el-button> </el-upload> <el-button @click="save_nodearray_file" size="medium">保存</el-button> <el-button @click="screen_shot" size="medium">下载导图</el-button> <el-button @click="get_nodearray_data" size="medium">获取数据</el-button> <el-button @click="addNode" size="medium">新增节点</el-button> <el-button @click="addBrotherNode" size="medium">新增兄弟节点</el-button> <el-button @click="editNode" size="medium">编辑节点</el-button> <el-button @click="removeNode" size="medium">删除节点</el-button> <el-button @click="zoomIn" size="medium" :disabled="isZoomIn">放大</el-button> <el-button @click="zoomOut" size="medium" :disabled="isZoomOut" class="pad">缩小</el-button> <span>展开:</span> <el-select v-model="level" placeholder="展开节点" @change="expand_to_level" class="pad pad-left" size="medium"> <el-option v-for="item in nodeOptions" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> <span>主题:</span> <el-select v-model="localTheme" placeholder="选择主题" @change="set_theme" size="medium"> <el-option v-for="item in themeOptions" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> </div> <div id="jsmind_container" ref="container"> </div> <el-drawer title="编辑节点" :visible.sync="dialogVisible" size="500px"> <el-form label-width="80px" class="form-con"> <el-form-item label="字体大小"> <el-input-number controls-position="right" v-model.number="nodeOption.fontSize" class="ele-width" :min="1" :max="30" maxLength="2"></el-input-number> </el-form-item> <el-form-item label="字体粗细"> <el-select v-model="nodeOption.fontWeight" class="ele-width"> <el-option value="normal" label="常规"></el-option> <el-option value="bold" label="粗体"></el-option> <el-option value="bolder" label="更粗"></el-option> </el-select> </el-form-item> <el-form-item label="字体样式"> <el-select v-model="nodeOption.fontStyle" class="ele-width"> <el-option value="normal" label="标准"></el-option> <el-option value="italic" label="斜体"></el-option> <el-option value="oblique" label="倾斜"></el-option> </el-select> </el-form-item> <el-row> <el-col :span="12"> <el-form-item label="背景颜色"> <el-color-picker v-model="nodeOption.bgColor" show-alpha size="mini"></el-color-picker> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="字体颜色"> <el-color-picker v-model="nodeOption.fontColor" show-alpha size="mini"></el-color-picker> </el-form-item> </el-col> </el-row> <el-form-item label="节点内容"> <el-input type="textarea" :rows="2" v-model="nodeOption.content" class="ele-width" maxLength="64"></el-input> </el-form-item> </el-form> <template v-slot:footer> <div class="right mr-10"> <el-button type="primary" class="common-btn" @click="sureEditNode" size="medium">确 定</el-button> </div> </template> </el-drawer> </div> </template>
<script> import 'jsmind/style/jsmind.css' import jsMind from 'jsmind/js/jsmind.js' window.jsMind = jsMind require('jsmind/js/jsmind.draggable.js') require('jsmind/js/jsmind.screenshot.js') export default { props: { showBar: { // 是否显示工具栏,显示启用编辑 type: Boolean, default: true }, theme: { // 主题 type: String, default: 'info' }, lineColor: { // 线条颜色 type: String, default: 'skyblue' } }, data() { return { mind: { }, jm: null, isZoomIn: false, isZoomOut: false, level: 0, nodeOptions: [ { value: 1, label: '展开到一级节点' }, { value: 2, label: '展开到二级节点' }, { value: 3, label: '展开到三级节点' }, { value: 0, label: '展开全部节点' }, { value: -1, label: '隐藏全部节点' } ], themeOptions: [ { value: 'default', label: 'default' }, { value: 'primary', label: 'primary' }, { value: 'warning', label: 'warning' }, { value: 'danger', label: 'danger' }, { value: 'success', label: 'success' }, { value: 'info', label: 'info' }, { value: 'greensea', label: 'greensea' }, { value: 'nephrite', label: 'nephrite' }, { value: 'belizehole', label: 'belizehole' }, { value: 'wisteria', label: 'wisteria' }, { value: 'asphalt', label: 'asphalt' }, { value: 'orange', label: 'orange' }, { value: 'pumpkin', label: 'pumpkin' }, { value: 'pomegranate', label: 'pomegranate' }, { value: 'clouds', label: 'clouds' }, { value: 'asbestos', label: 'asbestos' } ], localTheme: this.theme, dialogVisible: false, nodeOption: { content: '', bgColor: '', fontColor: '', fontSize: '', fontWeight: '', fontStyle: '' } } }, created() { }, mounted() { this.getData() this.mouseWheel() }, methods: { beforeUpload (file) { // 上传文件之前钩子 if (file) { jsMind.util.file.read(file, (jsmindData) => { const mind = jsMind.util.json.string2json(jsmindData) if (mind) { this.jm.show(mind) this.$message({ type: 'success', message: '打开成功' }) } else { this.prompt_info('不能打开mindmap文件') } }) } else { this.prompt_info('请先选择文件') return false } }, upload() { }, getData() { this.$API({ name: 'getMind' }).then(res => { this.mind = res.data this.open_empty() }).catch(error => { this.$message.error(error) }) }, open_empty() { const options = { container: 'jsmind_container', // 必选,容器ID editable: this.showBar, // 可选,是否启用编辑 theme: this.localTheme, // 可选,主题 view: { line_width: 2, // 思维导图线条的粗细 line_color: this.lineColor // 思维导图线条的颜色 }, shortcut: { enable: true // 禁用快捷键 }, layout: { hspace: 50, // 节点之间的水平间距 vspace: 20, // 节点之间的垂直间距 pspace: 13 // 节点与连接线之间的水平间距(用于容纳节点收缩/展开控制器) }, mode: 'side' // 显示模式,子节点只分布在根节点右侧 } this.jm = jsMind.show(options, this.mind) // 改变窗口大小重置画布 window.onresize = () => { this.jm.resize() } }, save_nodearray_file() { const mindData = this.jm.get_data('node_array') const mindName = mindData.meta.name const mindStr = jsMind.util.json.json2string(mindData) jsMind.util.file.save(mindStr, 'text/jsmind', mindName + '.jm') }, screen_shot() { this.jm.screenshot.shootDownload() }, expand_all() { this.jm.expand_all() }, collapse_all() { this.jm.collapse_all() }, expand_to_level(num) { switch (num) { case -1: this.collapse_all() break case 0: this.expand_all() break default: this.jm.expand_to_depth(num) break } }, zoomIn() { if (this.jm.view.zoomIn()) { this.isZoomOut = false } else { this.isZoomIn = true } }, zoomOut() { if (this.jm.view.zoomOut()) { this.isZoomIn = false } else { this.isZoomOut = true } }, prompt_info(msg) { this.$message({ type: 'warning', message: msg}) }, get_nodearray_data() { const mindData = this.jm.get_data('node_array') const mindString = jsMind.util.json.json2string(mindData) this.$message({ type: 'info', message: mindString}) }, set_theme(themeName) { this.jm.set_theme(themeName) }, scrollFunc(e) { e = e || window.event if (e.wheelDelta) { if (e.wheelDelta > 0) { this.zoomIn() } else { this.zoomOut() } } else if (e.detail) { if (e.detail > 0) { this.zoomIn() } else { this.zoomOut() } } this.jm.resize() }, // 鼠标滚轮放大缩小 mouseWheel() { if (document.addEventListener) { document.addEventListener('domMouseScroll', this.scrollFunc, false) } this.$refs.container.onmousewheel = this.scrollFunc }, // 新增节点 addNode() { let selectedNode = this.jm.get_selected_node() if (!selectedNode) { this.$message({ type: 'warning', message: '请先选择一个节点!'}) return } let nodeid = jsMind.util.uuid.newid() let topic = 'new Node' let newNode = this.jm.add_node(selectedNode, nodeid, topic) if (newNode) { this.jm.select_node(nodeid) this.jm.begin_edit(nodeid) } }, // 新增兄弟节点 addBrotherNode() { let selectedNode = this.jm.get_selected_node() if (!selectedNode) { this.$message({ type: 'warning', message: '请先选择一个节点!'}) return } else if (selectedNode.isroot) { this.$message({ type: 'warning', message: '不能在根节点添加,请重新选择节点!'}) return } let nodeid = jsMind.util.uuid.newid() let topic = 'new Node' let newNode = this.jm.insert_node_after(selectedNode, nodeid, topic) if (newNode) { this.jm.select_node(nodeid) this.jm.begin_edit(nodeid) } }, // 获取选中标签的 ID get_selected_nodeid () { let selectedNode = this.jm.get_selected_node() if (selectedNode) { return selectedNode.id } else { return null } }, // 删除节点 removeNode() { let selectedId = this.get_selected_nodeid() if (!selectedId) { this.$message({ type: 'warning', message: '请先选择一个节点!' }) return } this.jm.remove_node(selectedId) }, // 编辑节点 editNode () { let selectedId = this.get_selected_nodeid() if (!selectedId) { this.$message({ type: 'warning', message: '请先选择一个节点!'}) return } let nodeObj = this.jm.get_node(selectedId) this.nodeOption.content = nodeObj.topic this.nodeOption.bgColor = nodeObj.data['background-color'] this.nodeOption.fontColor = nodeObj.data['foreground-color'] this.nodeOption.fontSize = nodeObj.data['font-size'] this.nodeOption.fontWeight = nodeObj.data['font-weight'] this.nodeOption.fontStyle = nodeObj.data['font-style'] this.dialogVisible = true }, sureEditNode () { let selectedId = this.get_selected_nodeid() this.jm.update_node(selectedId, this.nodeOption.content) this.jm.set_node_font_style(selectedId, this.nodeOption.fontSize, this.nodeOption.fontWeight, this.nodeOption.fontStyle) this.jm.set_node_color(selectedId, this.nodeOption.bgColor, this.nodeOption.fontColor) this.nodeOption = { content: '', bgColor: '', fontColor: '', fontSize: '', fontWeight: '', fontStyle: '' } this.dialogVisible = false } }, beforeDestroy() { document.removeEventListener('domMouseScroll', this.scrollFunc, false) } } </script>
此代码由一叶知秋网-知秋君整理<style lang="less" scoped> .jsmind_layout { display: flex; flex-direction: column; width: 100%; height: calc(100% - 40px); overflow: hidden; .jsmind_toolbar { width: 100%; padding: 0 10px 10px 10px; height: auto; flex-shrink: 0; display: flex; align-items: center; flex-wrap: wrap; background-color: #f8f9fa; box-shadow: 0 0 4px #b8b8b8; } /deep/ .el-button--medium, /deep/ .el-input--medium { margin-top: 10px; } #jsmind_container { flex: 1 1 auto; } /deep/.jsmind-inner { overflow: hidden auto !important; } /deep/.el-upload-list { display: none !important; } /* 隐藏滚动条 */ .jsmind-inner::-webkit-scrollbar { display: none; } .pad { margin-right: 10px; } .pad-left { margin-left: 10px; } /deep/ jmnode.selected { background-color: #b9b9b9; color: #fff; box-shadow: 2px 2px 8px #777; } /deep/ jmnode:hover { box-shadow: 2px 2px 8px #777; } .form-con { padding-top:20px; } .ele-width { width: 96%; } } </style>
右键菜单代码
<template> <!-- 右键菜单 --> <div class="jsmind_layout"> <div class="jsmind_toolbar" v-if="showBar"> <el-upload class="pad" :multiple="false" ref="upload" action="action" :before-upload="beforeUpload" :http-request="upload"> <el-button type="primary" size="medium">导入</el-button> </el-upload> <el-button @click="save_nodearray_file" size="medium">保存</el-button> <el-button @click="screen_shot" size="medium">下载导图</el-button> <el-button @click="get_nodearray_data" size="medium">获取数据</el-button> <span class="pad-left">展开:</span> <el-select v-model="level" placeholder="展开节点" @change="expand_to_level" class="pad pad-left"> <el-option v-for="item in nodeOptions" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> <span>主题:</span> <el-select v-model="theme" placeholder="选择主题" @change="set_theme"> <el-option v-for="item in themeOptions" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> </div> <div id="jsmind_container" ref="container"> <div class="zoom_in_out"> <span><i class="zoom-icon el-icon-plus" @click="zoomIn" :class="isZoomIn === true ? 'disabled' : ''"></i></span> <span><i class="zoom-icon el-icon-minus" @click="zoomOut" :class="isZoomOut === true ? 'disabled' : ''"></i></span> </div> </div> </div> </template>
<script> import 'jsmind/style/jsmind.css' import jsMind from 'jsmind/js/jsmind.js' window.jsMind = jsMind const { init, reBuild } = require('@/assets/js/jsmind.menu.js') require('jsmind/js/jsmind.draggable.js') require('jsmind/js/jsmind.screenshot.js') init(jsMind) export default { props: { showBar: { // 是否显示工具栏 type: Boolean, default: true }, isEdit: { // 是否启用编辑,启用编辑可以显示右键功能 type: Boolean, default: true } }, data() { return { mind: { }, jm: null, isZoomIn: false, isZoomOut: false, level: 0, nodeOptions: [ { value: 1, label: '展开到一级节点' }, { value: 2, label: '展开到二级节点' }, { value: 3, label: '展开到三级节点' }, { value: 4, label: '展开到四级节点' }, { value: 0, label: '展开全部节点' }, { value: -1, label: '隐藏全部节点' } ], themeOptions: [ { value: 'default', label: 'default' }, { value: 'primary', label: 'primary' }, { value: 'warning', label: 'warning' }, { value: 'danger', label: 'danger' }, { value: 'success', label: 'success' }, { value: 'info', label: 'info' }, { value: 'greensea', label: 'greensea' }, { value: 'nephrite', label: 'nephrite' }, { value: 'belizehole', label: 'belizehole' }, { value: 'wisteria', label: 'wisteria' }, { value: 'asphalt', label: 'asphalt' }, { value: 'orange', label: 'orange' }, { value: 'pumpkin', label: 'pumpkin' }, { value: 'pomegranate', label: 'pomegranate' }, { value: 'clouds', label: 'clouds' }, { value: 'asbestos', label: 'asbestos' } ], theme: 'success' } }, created() { }, mounted() { this.getData() this.mouseWheel() }, methods: { beforeUpload (file) { // 上传文件之前钩子 if (file) { jsMind.util.file.read(file,(jsmind_data) => { const mind = jsMind.util.json.string2json(jsmind_data) if(mind){ this.jm.show(mind) reBuild() }else{ this.prompt_info('can not open this file as mindmap') } }) } else { this.prompt_info('please choose a file first') return false } }, upload() { }, getData() { this.$API({ name: 'getMind' }).then(res => { this.mind = res.data this.open_empty() }).catch(error => { this.$message.error(error) }) }, open_empty() { const options = { container: 'jsmind_container', // 必选,容器ID editable: this.isEdit, // 可选,是否启用编辑 theme: this.theme, // 可选,主题 view: { line_width: 2, // 思维导图线条的粗细 line_color: 'skyblue' // 思维导图线条的颜色 }, shortcut: { enable: false // 禁用快捷键 }, mode: 'side', // 显示模式,子节点只分布在根节点右侧 menuOpts:{ // 这里加入一个专门配置menu的对象 showMenu: this.isEdit, //showMenu 为 true 则打开右键功能 ,反之关闭 injectionList: [ { target: 'edit', text: '编辑节点' }, { target: 'delete', text: '删除节点' }, { target: 'addChild', text: '添加子节点' }, { target: 'addBrother', text: '添加兄弟节点' } ], style: { menuItem:{ 'line-height': '28px' } } } } this.jm = jsMind.show(options, this.mind) // 改变窗口大小重置画布 window.onresize = () => { this.jm.resize() } }, save_nodearray_file(){ const mind_data = this.jm.get_data('node_array') const mind_name = mind_data.meta.name const mind_str = jsMind.util.json.json2string(mind_data) jsMind.util.file.save(mind_str, 'text/jsmind', mind_name + '.jm') }, screen_shot(){ this.jm.screenshot.shootDownload() }, expand_all(){ this.jm.expand_all() }, collapse_all(){ this.jm.collapse_all() }, expand_to_level(num){ switch(num) { case -1: this.collapse_all() break case 0: this.expand_all() break default: this.jm.expand_to_depth(num) break } }, zoomIn() { if (this.jm.view.zoomIn()) { this.isZoomOut = false } else { this.isZoomIn = true } }, zoomOut() { if (this.jm.view.zoomOut()) { this.isZoomIn = false } else { this.isZoomOut = true } }, prompt_info(msg){ alert(msg) }, get_nodearray_data() { const mind_data = this.jm.get_data('node_array') const mind_string = jsMind.util.json.json2string(mind_data) this.prompt_info(mind_string) }, set_theme(theme_name){ this.jm.set_theme(theme_name) }, scrollFunc(e) { e = e || window.event if (e.wheelDelta) { if (e.wheelDelta > 0) { this.zoomIn() } else { this.zoomOut() } } else if (e.detail) { if (e.detail > 0) { this.zoomIn() } else { this.zoomOut() } } this.jm.resize() }, // 鼠标滚轮放大缩小 mouseWheel() { if (document.addEventListener) { document.addEventListener('domMouseScroll', this.scrollFunc, false) } this.$refs.container.onmousewheel = this.scrollFunc } }, beforeDestroy() { document.removeEventListener('domMouseScroll', this.scrollFunc, false) } } </script>
<style lang="less"> .jsmind_layout { position: relative; display: flex; flex-direction: column; width: 100%; height: calc(100% - 40px); overflow: hidden; /deep/ .el-button--medium, /deep/ .el-input--medium { margin-top: 10px; } } jmnode.selected { background-color: #b9b9b9 !important; box-shadow: 2px 2px 8px #777 !important; } jmnode:hover { box-shadow: 2px 2px 8px #777 !important; } .jsmind_toolbar { width: 100%; padding: 10px; height: auto; flex-shrink: 0; display: flex; align-items: center; flex-wrap: wrap; background-color: #f8f9fa; box-shadow: 0 0 4px #b8b8b8; } #jsmind_container { flex: 1 1 auto; position: relative; } .jsmind-inner { overflow: hidden auto !important; } .el-upload-list { display: none !important; } .zoom_in_out { position: absolute; bottom: 20px; right: 20px; height: auto; display: flex; align-items: center; flex-direction: column; padding: 5px; background-color: #fff; border-radius: 4px; box-shadow: 2px 2px 4px #e0e0e0; border: 1px solid #f5f5f5; z-index: 999; } .zoom-icon { cursor: pointer; font-size: 20px; padding: 6px 5px; } .disabled { color: #bdbcbc; cursor: not-allowed; } .el-icon-plus { border-bottom: 1px solid #9f9f9f; } /* 隐藏滚动条 */ .jsmind-inner::-webkit-scrollbar { display: none; } .pad { margin-right: 10px; } .pad-left { margin-left: 10px; } </style>