MindMap.vue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. <template>
  2. <div class="mind-map-container">
  3. <div class="toolbar">
  4. <button v-if="isEdit" @click="addParentNode">添加父节点</button>
  5. <button v-if="isEdit" @click="addNode">添加节点</button>
  6. <button v-if="isEdit" @click="addChildNode">添加子节点</button>
  7. <button v-if="isEdit" @click="removeNode">删除节点</button>
  8. <button v-if="isEdit" @click="forward">前进</button>
  9. <button v-if="isEdit" @click="back">回退</button>
  10. <button @click="zoomIn">放大</button>
  11. <button @click="zoomOut">缩小</button>
  12. <button @click="resetZoom">重置缩放</button>
  13. <button v-if="isEdit" @click="exportToPNG">导出PNG</button>
  14. <!-- <button @click="exportToSvg">导出SVG</button> -->
  15. <!-- <button @click="exportToJson">导出JSON</button> -->
  16. <!-- <button @click="importFromJson">导入JSON</button> -->
  17. <!-- <button @click="saveData">保存数据</button> -->
  18. </div>
  19. <div ref="mindMapContainer" class="mind-map"></div>
  20. </div>
  21. </template>
  22. <script>
  23. import MindMap from 'simple-mind-map';
  24. import Export from 'simple-mind-map/src/plugins/Export.js';
  25. MindMap.usePlugin(Export);
  26. export default {
  27. name: 'MindMap',
  28. props: {
  29. isEdit: {
  30. type: Boolean,
  31. default: false,
  32. },
  33. projectId: {
  34. type: String,
  35. required: true,
  36. },
  37. mindMapJsonData: {
  38. type: Object,
  39. required: true,
  40. },
  41. },
  42. data() {
  43. return {
  44. mindMap: null,
  45. activeNodes: [],
  46. activeNodeIsRoot: false,
  47. isStart: true,
  48. isEnd: true,
  49. scale: 1,
  50. defaultData: {
  51. data: {
  52. uid: '001',
  53. text: '中心主题',
  54. },
  55. children: [],
  56. },
  57. };
  58. },
  59. mounted() {
  60. this.initMindMap();
  61. this.bindKeyEvents();
  62. },
  63. beforeDestroy() {
  64. this.destroyMindMap();
  65. },
  66. methods: {
  67. initMindMap() {
  68. let rootData = this.mindMapJsonData;
  69. rootData = { data: rootData?.root || this.defaultData };
  70. this.mindMap = new MindMap({
  71. el: this.$refs.mindMapContainer,
  72. mousewheelAction: 'zoom(放大缩小)、move(上下移动)', // zoom(放大缩小)、move(上下移动)
  73. // 当mousewheelAction设为move时,可以通过该属性控制鼠标滚动一下视图移动的步长,单位px
  74. mousewheelMoveStep: 100,
  75. // 鼠标缩放是否以鼠标当前位置为中心点,否则以画布中心点
  76. mouseScaleCenterUseMousePosition: true,
  77. // 当mousewheelAction设为zoom时,或者按住Ctrl键时,默认向前滚动是缩小,向后滚动是放大,如果该属性设为true,那么会反过来
  78. mousewheelZoomActionReverse: true,
  79. // 禁止鼠标滚轮缩放,你仍旧可以使用api进行缩放
  80. disableMouseWheelZoom: false,
  81. data: rootData.data,
  82. // theme: { template: 'default' },
  83. // theme: {
  84. // name: 'classic',
  85. // palette: ['#5B8FF9', '#5AD8A6', '#5D7092', '#F6BD16', '#E86452'],
  86. // bgColor: '#fff',
  87. // color: '#333',
  88. // root: {
  89. // fillColor: '#5B8FF9',
  90. // },
  91. // second: {
  92. // fillColor: '#5AD8A6',
  93. // color: '#fff',
  94. // borderColor: '#5AD8A6',
  95. // borderWidth: 1,
  96. // fontSize: 14,
  97. // },
  98. // },
  99. readonly: !this.isEdit,
  100. });
  101. // this.mindMap.renderer.activeNodeList
  102. // 激活节点
  103. this.mindMap.on('node_active', (node, activeNodeList) => {
  104. this.activeNodes = activeNodeList;
  105. this.activeNodeIsRoot = activeNodeList.some((p) => p.isRoot);
  106. });
  107. this.mindMap.on('back_forward', (index, len) => {
  108. this.isStart = index <= 0;
  109. this.isEnd = index >= len - 1;
  110. });
  111. this.mindMap.view.translateX(-400);
  112. this.mindMap.view.translateY(-50);
  113. if (!this.isEdit) {
  114. // 监听所有节点点击事件
  115. this.mindMap.on('node_click', (node) => {
  116. // console.log('点击的节点:', node?.nodeData?.data?.text || '文本');
  117. this.$emit('child-click', `${node.uid}`);
  118. });
  119. }
  120. },
  121. bindKeyEvents() {
  122. document.addEventListener('keydown', this.handleDeleteKey);
  123. },
  124. destroyMindMap() {
  125. if (this.mindMap) {
  126. this.mindMap.destroy();
  127. }
  128. document.removeEventListener('keydown', this.handleDeleteKey);
  129. },
  130. addParentNode() {
  131. if (!this.mindMap) return;
  132. if (this.activeNodeIsRoot) {
  133. this.$message.error('根节点不能添加父节点!');
  134. return;
  135. }
  136. this.mindMap.execCommand('INSERT_PARENT_NODE');
  137. },
  138. addNode() {
  139. if (!this.mindMap) return;
  140. if (this.activeNodeIsRoot) {
  141. this.$message.error('只能有一个根节点!');
  142. return;
  143. }
  144. this.mindMap.execCommand(
  145. 'INSERT_NODE',
  146. true,
  147. [],
  148. {
  149. uid: Math.random() * 100000,
  150. text: '请添加内容',
  151. },
  152. [
  153. {
  154. data: {
  155. text: '下级节点',
  156. },
  157. children: [],
  158. },
  159. ],
  160. );
  161. },
  162. addChildNode() {
  163. if (!this.mindMap) return;
  164. this.mindMap.execCommand('INSERT_CHILD_NODE');
  165. },
  166. removeNode() {
  167. if (!this.mindMap) return;
  168. const currentNode = this.mindMap.renderer.activeNodeList[0];
  169. const nodeData = currentNode?.nodeData.data;
  170. if (nodeData && (nodeData.type === 0 || nodeData.type === 1)) {
  171. this.$message.warning('章节节点不能删除!');
  172. return;
  173. }
  174. this.mindMap.execCommand('REMOVE_NODE');
  175. },
  176. handleDeleteKey(e) {
  177. if (e.key === 'Delete' || e.key === 'Del') {
  178. const currentNode = this.mindMap?.renderer.activeNodeList[0];
  179. const nodeData = currentNode?.nodeData.data;
  180. if (nodeData && (nodeData.type === 0 || nodeData.type === 1)) {
  181. e.preventDefault();
  182. e.stopPropagation();
  183. e.returnValue = false;
  184. this.$message.warning('章节节点不能删除!');
  185. return false;
  186. }
  187. }
  188. return true;
  189. },
  190. forward() {
  191. this.mindMap.execCommand('FORWARD');
  192. },
  193. back() {
  194. this.mindMap.execCommand('BACK');
  195. },
  196. zoomIn() {
  197. if (!this.mindMap) return;
  198. this.mindMap.view.setScale((this.scale += 0.1));
  199. },
  200. zoomOut() {
  201. if (!this.mindMap) return;
  202. this.mindMap.view.setScale((this.scale -= 0.1));
  203. },
  204. resetZoom() {
  205. if (!this.mindMap) return;
  206. this.mindMap.view.reset();
  207. },
  208. exportToPNG() {
  209. if (!this.mindMap) return;
  210. this.mindMap.export('png', true, 'mind-map.png');
  211. },
  212. exportToSvg() {
  213. if (!this.mindMap) return;
  214. this.mindMap.export('svg', true, 'mind-map.svg');
  215. },
  216. exportToPDF() {
  217. if (!this.mindMap) return;
  218. this.mindMap.export('pdf', true, 'mind-map.pdf');
  219. },
  220. exportToJson() {
  221. if (!this.mindMap) return;
  222. this.mindMap.export('json', true, 'mind-map.json');
  223. },
  224. importFromJson() {
  225. const input = document.createElement('input');
  226. input.type = 'file';
  227. input.accept = '.json';
  228. input.onchange = (e) => {
  229. const file = e.target.files[0];
  230. const reader = new FileReader();
  231. reader.onload = (event) => {
  232. try {
  233. const data = JSON.parse(event.target.result);
  234. this.destroyMindMap();
  235. this.$nextTick(() => {
  236. this.initMindMap();
  237. this.$nextTick(() => {
  238. this.mindMap.setData(data);
  239. });
  240. });
  241. } catch (error) {
  242. alert(`导入失败:${error.message}`);
  243. }
  244. };
  245. reader.readAsText(file);
  246. };
  247. input.click();
  248. },
  249. saveData() {
  250. // 获取完整数据
  251. const data = this.mindMap.getData();
  252. console.info('data', data);
  253. return data;
  254. },
  255. },
  256. };
  257. </script>
  258. <style lang="scss" scoped>
  259. .mind-map-container {
  260. .toolbar {
  261. display: flex;
  262. flex-wrap: wrap;
  263. gap: 8px;
  264. // justify-content: center;
  265. margin-bottom: 6px;
  266. button {
  267. padding: 6px 12px;
  268. color: #fff;
  269. cursor: pointer;
  270. background-color: #2d9aff;
  271. border: none;
  272. border-radius: 4px;
  273. }
  274. button:hover {
  275. background-color: #165dff;
  276. }
  277. }
  278. .mind-map {
  279. height: calc(100vh - 204px);
  280. background-color: #fff !important;
  281. }
  282. }
  283. </style>