From d1ab67cd4c01d4d3e0927db18dd24911933e319e Mon Sep 17 00:00:00 2001 From: wanglin2 <1013335014@qq.com> Date: Sat, 24 Sep 2022 17:08:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=96=B0=E5=BB=BA=E3=80=81?= =?UTF-8?q?=E6=89=93=E5=BC=80=E3=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 ++- simple-mind-map/index.js | 26 +++ simple-mind-map/package.json | 2 +- simple-mind-map/src/Export.js | 16 +- web/src/.DS_Store | Bin 6148 -> 8196 bytes web/src/api/index.js | 3 + web/src/assets/.DS_Store | Bin 6148 -> 8196 bytes web/src/assets/icon-font/.DS_Store | Bin 6148 -> 6148 bytes web/src/assets/icon-font/demo_index.html | 125 +++++++++++++- web/src/assets/icon-font/iconfont.css | 26 ++- web/src/assets/icon-font/iconfont.js | 2 +- web/src/assets/icon-font/iconfont.json | 35 ++++ web/src/assets/icon-font/iconfont.ttf | Bin 12796 -> 13800 bytes web/src/assets/icon-font/iconfont.woff | Bin 7868 -> 8544 bytes web/src/assets/icon-font/iconfont.woff2 | Bin 6692 -> 7224 bytes web/src/pages/Edit/components/Edit.vue | 8 +- web/src/pages/Edit/components/Import.vue | 1 + web/src/pages/Edit/components/Toolbar.vue | 192 +++++++++++++++++++++- web/src/store.js | 13 +- 19 files changed, 435 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 7f8fe810..c10c720e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,17 @@ 2.`web` -使用`simple-mind-map`工具库,基于`vue2.x`、`ElementUI`搭建的在线思维导图。 +使用`simple-mind-map`工具库,基于`vue2.x`、`ElementUI`搭建的在线思维导图。特性: + +- [x] 工具栏,支持插入节点、删除节点;编辑节点图片、图标、超链接、备注、标签、概要 + +- [x] 侧边栏,基础样式设置面板、节点样式设置面板、大纲面板、主题选择面板、结构选择面板 + +- [x] 导入导出功能;数据默认保存在浏览器本地存储,也支持直接创建、打开、编辑电脑本地文件 + +- [x] 右键菜单,支持展开、收起、整理布局等操作 + +- [x] 底部栏,支持节点数量、字数统计;支持切换编辑和只读模式;支持放大缩小;支持全屏切换 3.`dist` @@ -91,7 +101,7 @@ npm run build # 安装 -> 当然仓库版本:0.2.8,当前npm版本:0.2.8 +> 当然仓库版本:0.2.9,当前npm版本:0.2.9 ```bash npm i simple-mind-map @@ -303,8 +313,6 @@ v0.1.7+。切换模式为只读或编辑。 | RESET_LAYOUT(v0.2.0+) | 一键整理布局 | | | SET_NODE_SHAPE(v0.2.4+) | 设置节点形状 | node(要设置的节点)、shape(形状,全部形状:https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/Shape.js) | - - #### setData(data) 动态设置思维导图数据,纯节点数据 @@ -319,6 +327,14 @@ v0.2.7+ `data`:完整数据,结构可参考[exportFullData](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exportFullData.json) +#### getData(withConfig) + +v0.2.9+ + +获取思维导图数据 + +`withConfig`:`Boolean`,默认为`false`,即获取的数据只包括节点树,如果传`true`则会包含主题、布局、视图等数据 + #### export(type, isDownload, fileName) 导出 diff --git a/simple-mind-map/index.js b/simple-mind-map/index.js index b8738a54..6ed87c18 100644 --- a/simple-mind-map/index.js +++ b/simple-mind-map/index.js @@ -17,6 +17,7 @@ import { SVG } from '@svgdotjs/svg.js' import xmind from './src/parse/xmind' +import { simpleDeepClone } from './src/utils'; // 默认选项配置 const defaultOpt = { @@ -355,6 +356,31 @@ class MindMap { } } + /** + * javascript comment + * @Author: 王林 + * @Date: 2022-09-24 14:42:07 + * @Desc: 获取思维导图数据,节点树、主题、布局等 + */ + getData(withConfig) { + let nodeData = this.command.getCopyData() + let data = {} + if (withConfig) { + data = { + layout: this.getLayout(), + root: nodeData, + theme: { + template: this.getTheme(), + config: this.getCustomThemeConfig() + }, + view: this.view.getTransformData() + } + } else { + data = nodeData + } + return simpleDeepClone(data) + } + /** * @Author: 王林 * @Date: 2021-07-01 22:06:38 diff --git a/simple-mind-map/package.json b/simple-mind-map/package.json index 4fa34939..960626ea 100644 --- a/simple-mind-map/package.json +++ b/simple-mind-map/package.json @@ -1,6 +1,6 @@ { "name": "simple-mind-map", - "version": "0.2.8", + "version": "0.2.9", "description": "一个简单的web在线思维导图", "authors": [ { diff --git a/simple-mind-map/src/Export.js b/simple-mind-map/src/Export.js index f3b25a96..13baac0b 100644 --- a/simple-mind-map/src/Export.js +++ b/simple-mind-map/src/Export.js @@ -249,21 +249,7 @@ class Export { * @Desc: 导出为json */ json (name, withConfig = true) { - let nodeData = this.mindMap.command.getCopyData() - let data = {} - if (withConfig) { - data = { - layout: this.mindMap.getLayout(), - root: nodeData, - theme: { - template: this.mindMap.getTheme(), - config: this.mindMap.getCustomThemeConfig() - }, - view: this.mindMap.view.getTransformData() - } - } else { - data = nodeData - } + let data = this.mindMap.getData(withConfig) let str = JSON.stringify(data) let blob = new Blob([str]) return URL.createObjectURL(blob) diff --git a/web/src/.DS_Store b/web/src/.DS_Store index b6ab753123ec13c61d82fc24a903fc285472cf85..568d022a7a7c2e72c1e0419856484cca85f6af44 100644 GIT binary patch literal 8196 zcmeI1O>7%Q6oB7z(sUWY<`S6JZ9z z41^g7GZ1DV%)tL41N6)mP1)eNFSTJGW+2SKrp$o2Kg8$|G8M>WDg9dqb$$szQj`FG zqcPP1zE3QWsX#7E>ATWr%IX0FR}4xFlRrULq*#*mVy+qgYl%}SSw>Kd(t$@8QwhN*q&zEM!|PBb%RXKOq;gul!u+7t-Jo` zq@_Ypn8Gq#U0m!NNXBD*y~(9`Y_Y#T5sxJk$)zQxw086yJTjNRaB=a;rAsf%xG=ak zKtolWSF3}x+|r1ANfF6cg8aS`BxBpuvBvR38P1EYF=1GmXO@gn%`@B#Z*VEvBRR)D zQ7}pr*>ujaig}wfjXBNA70K8bvaGau(Qs!xg^v)UE6U}18;0%Y?iWOd*)fwC`^@2-J3t9xjDnuP5az* z$#l)EWlY*y-e|-)W4_nUR#lxTX3Zyxrgwq2D~dncwp(RqblTxDM;D)_^N$p1AKR^} zr-f#uU{J3I4!3uxY|^FOaxF0I-pknJ<8;}$8!WZ&Zl+FITGp_*&g43M_b_$3Btzm4 z1(O3!Or6O&`8CipTI_oW=}ln8h4gSi};RaT%Y-7w|=V z317uG@hyBC-@y;?6S=QQ1;oShywc;RG+Om-BM^hPWs!;XK#Yzxk9YnjAl}$6>AShP z<(BrYcu#ULwQ}{UF4-!i{z;LKj%34Urow~|>osC@SG0}osUTd9_7qWOoL29G5U%*AtRvdV2w{#=+Y!A}QFuh^_C*tnsO2?M*T?oN#3$b(wFB$`BZ7IY)E;CH zC&krmd7N*p_3TqxlM^5x%{st__zFGhP delta 195 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{MGjUEV6q~50$jG!YU^nAr0~wad`~ni9Cz1+s zGK)(L46ZXWF|)9;v2$>6aZR=n$k5^Bj295Et~RsOQ82Q!sMS%ZHa9oWQ7|z!tF7ha z5LY#{^-RdEtg5c5t(!6VqJTW(%*`)E<(W1%{9sxPG)0gZXekf~a03Zfko_AAzcWwf amvIC+kb#kbfdxb}L0kwH+Z@j`hZz97Nhd=9 diff --git a/web/src/api/index.js b/web/src/api/index.js index 33b52155..fc3cea4d 100644 --- a/web/src/api/index.js +++ b/web/src/api/index.js @@ -1,5 +1,6 @@ import exampleData from "simple-mind-map/example/exampleData" import { simpleDeepClone } from 'simple-mind-map/src/utils/index' +import Vue from 'vue' const SIMPLE_MIND_MAP_DATA = 'SIMPLE_MIND_MAP_DATA' @@ -47,6 +48,7 @@ export const storeData = (data) => { try { let originData = getData() originData.root = copyMindMapTreeData({}, data) + Vue.prototype.$bus.$emit('write_local_file', originData) let dataStr = JSON.stringify(originData) localStorage.setItem(SIMPLE_MIND_MAP_DATA, dataStr) } catch (error) { @@ -66,6 +68,7 @@ export const storeConfig = (config) => { ...originData, ...config } + Vue.prototype.$bus.$emit('write_local_file', originData) let dataStr = JSON.stringify(originData) localStorage.setItem(SIMPLE_MIND_MAP_DATA, dataStr) } catch (error) { diff --git a/web/src/assets/.DS_Store b/web/src/assets/.DS_Store index a1654cd448333aeeb7b5236e4d0b605c15c6ac3c..96b4153174a7248b59443adb0ccd844313c239e9 100644 GIT binary patch literal 8196 zcmeHLU2GIp6h5aExHC{XAkYr5u%W5c($eL}76_H4T>=%f#4WVtugvZY?ZoNKvNO9a zl$t70Uks>k;Dg5apw`5vzW9Pai9f+aNE&~5;6Yz}G#X9xMbEu=w&`y|OpHe6Cik8@ z_ndQozWd#K_bdQlTfy88Pz?Z#GMCyiD&|RC&-0vA7PfFDl0QHroCgnF2r@%+tfPhq zfe3*Jfe3*Jfe3;B0s{1A^TZZ-_oX%}BLpG@9!LcE{Sc?jWipgwQi`jC3K0U3ET?9X zs7`T!@QH^q8Okv!g)8+bqX!IKF$^(Kx|2T6%}FLhIVPoa2bAuB;m8<9C@4lJzql|5 zOiCG*5dskca}nUjMgbeLB5t1+zx%n2<7G(uMJDIievn9fgJp}#7cW`Ln99@&^-Oxe z8%hU8Q1IJ~{3-r*%m@ZDVZF=qoT<8zIbmBvecI}F&kYRQwerH*HZ3wa-EF(3H`L}8 zT+vDA%-VjhZYqdA#U#io;J)QLibJMKn0mSi(?Gli(S4)6=TLMH;m+6N))FXY`sv@nk zGZ)yN>z}alKFL0<(kE;>vYvY^Z*hHh)^iGjuHUDv%oMh^ z$k-POc5qRQv2l~Go;GPJPk1ID#ndL4A~ZFdbo~_9wC613W*>U$p2u)Ll$7mE(?)u^iddqV8eZ5v98*(Ugs=)K;eS(5?;AB7HIJ z{-my-<-;B|{Gc;uxiqEEY3z!s>a{7FQZLsXFik!oR+Frg+bu9qYLba?_ZjH0jh>vt za2hPg!4-H3uEI5V4c>zHVFGT#XYeI_1-Ic2+=cJqNB9|jfnVV__!Akf!78jq9XI1P zY{CTY!M(T-Td@s~;8A=MJFyGT;8{F}2KHkPFQAVBUdHF~1$+@-#_RYtzKi4d9!}s* z{189FkMS1X#yfI$WhG`GoyJwF;$14{=WKY0%X;VIY%Fz9J@oHz_Pgbhs1L1Pv$3YG zaYxI((;BW<3uiNcs8L+MAw$`1HdMAVo59> z+sueFTKHKT+ro&|TAAqUk@}dbEg}Mo)1q%{Qe)a;;wlr`ox4<3DUWC+=CzBC-|Lcw-_slb~ySMUbD zj&De+O;PW#{4SOF4%OzN-k_ICh3^Ne)bIUqCH~2Qg)Ghcw9F0M&qZBK=UvBh`)Rb& zaVbqUk~0xlBtNTp{C{xa|Nmz?MPCsD5d!}!1h71nPPNgJ7P>HbtR1I(jxtZ&ZcIwy xLIpSB?IIi}8HVGcEgtHUp&XM^xRQ@>B`G8MfBcVt8NY#}_dk07IU66m^cUfrDO~^n delta 212 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{MGjUEV6q~50$jG)aU^gQp+vJPFVhTy+#RW+@ z`AIU8<)aocyo0}Wx zD3}AjHu~2NHo+1YW5HK<@2y9-+n8vnw1EUw?W_AvK4xj>{$am(+{342+ UKzW7)kiy9(Jj$D6L{=~Z04H+~3jhEB delta 223 zcmZoMXfc=|#>B`mF;Q%yo}wrd0|Nsi1A_nqLp*~Kg9(Ecn&(C4pn3%^l*+7J4GdBk}2hjN&6TdT0<`+?91-VmUvZ09ZW)G1)%mAa- BFa!Vq diff --git a/web/src/assets/icon-font/demo_index.html b/web/src/assets/icon-font/demo_index.html index 30c6b028..830536f3 100644 --- a/web/src/assets/icon-font/demo_index.html +++ b/web/src/assets/icon-font/demo_index.html @@ -3,8 +3,8 @@ iconfont Demo - - + + @@ -54,6 +54,36 @@
    +
  • + +
    导出
    +
    &#xe63e;
    +
  • + +
  • + +
    另存为
    +
    &#xe657;
    +
  • + +
  • + +
    export
    +
    &#xe642;
    +
  • + +
  • + +
    打开
    +
    &#xebdf;
    +
  • + +
  • + +
    新建
    +
    &#xe64e;
    +
  • +
  • 剪切
    @@ -300,9 +330,9 @@
    @font-face {
       font-family: 'iconfont';
    -  src: url('iconfont.woff2?t=1659615576455') format('woff2'),
    -       url('iconfont.woff?t=1659615576455') format('woff'),
    -       url('iconfont.ttf?t=1659615576455') format('truetype');
    +  src: url('iconfont.woff2?t=1664005697217') format('woff2'),
    +       url('iconfont.woff?t=1664005697217') format('woff'),
    +       url('iconfont.ttf?t=1664005697217') format('truetype');
     }
     

    第二步:定义使用 iconfont 的样式

    @@ -328,6 +358,51 @@
      +
    • + +
      + 导出 +
      +
      .icondaochu1 +
      +
    • + +
    • + +
      + 另存为 +
      +
      .iconlingcunwei +
      +
    • + +
    • + +
      + export +
      +
      .iconexport +
      +
    • + +
    • + +
      + 打开 +
      +
      .icondakai +
      +
    • + +
    • + +
      + 新建 +
      +
      .iconxinjian +
      +
    • +
    • @@ -697,6 +772,46 @@
        +
      • + +
        导出
        +
        #icondaochu1
        +
      • + +
      • + +
        另存为
        +
        #iconlingcunwei
        +
      • + +
      • + +
        export
        +
        #iconexport
        +
      • + +
      • + +
        打开
        +
        #icondakai
        +
      • + +
      • + +
        新建
        +
        #iconxinjian
        +
      • +
      • +
        + + 新建 +
        +
        + + 打开 +
        +
        + + 另存为 +
        导入
        - + 导出
        @@ -167,12 +179,17 @@ import NodeNote from "./NodeNote"; import NodeTag from "./NodeTag"; import Export from "./Export"; import Import from './Import'; +import { mapState } from 'vuex'; +import { Notification } from 'element-ui'; +import exampleData from 'simple-mind-map/example/exampleData'; +import { getData } from '../../../api'; /** * @Author: 王林 * @Date: 2021-06-24 22:54:58 * @Desc: 工具栏 */ +let fileHandle = null; export default { name: "Toolbar", components: { @@ -189,10 +206,13 @@ export default { activeNodes: [], backEnd: false, forwardEnd: true, - readonly: false + readonly: false, + isFullDataFile: false, + timer: null, }; }, computed: { + ...mapState(['isHandleLocalFile']), hasRoot() { return this.activeNodes.findIndex((node) => { return node.isRoot; @@ -204,6 +224,13 @@ export default { }) !== -1;; } }, + watch: { + isHandleLocalFile(val) { + if (!val) { + Notification.closeAll(); + } + } + }, created() { this.$bus.$on("mode_change", (mode) => { this.readonly = mode === 'readonly' @@ -215,7 +242,168 @@ export default { this.backEnd = index <= 0 this.forwardEnd = index >= len - 1 }); + this.$bus.$on("write_local_file", (content) => { + clearTimeout(this.timer); + this.timer = setTimeout(() => { + this.writeLocalFile(content); + }, 1000); + }); }, + methods: { + /** + * @Author: 王林 + * @Date: 2022-09-24 15:40:09 + * @Desc: 打开本地文件 + */ + async openLocalFile() { + try { + let [ _fileHandle ] = await window.showOpenFilePicker({ + types: [ + { + description: 'file', + accept: { + 'application/*': ['.json', '.smm'] + } + }, + ], + excludeAcceptAllOption: true, + multiple: false + }); + if (!_fileHandle) { + return; + } + fileHandle = _fileHandle; + if (fileHandle.kind === 'directory') { + this.$message.warning('请选择文件'); + return; + } + this.readFile(); + } catch (error) { + console.log(error); + this.$message.warning('你的浏览器可能不支持哦'); + } + }, + + /** + * @Author: 王林 + * @Date: 2022-09-24 15:40:18 + * @Desc: 读取本地文件 + */ + async readFile() { + let file = await fileHandle.getFile(); + let fileReader = new FileReader(); + fileReader.onload = async () => { + this.$store.commit('setIsHandleLocalFile', true); + this.setData(fileReader.result); + Notification.closeAll(); + Notification({ + title: '提示', + message: `当前正在编辑你本机的【${ file.name }】文件`, + duration: 0, + showClose: false + }); + } + fileReader.readAsText(file); + }, + + /** + * @Author: 王林 + * @Date: 2022-09-24 15:40:26 + * @Desc: 渲染读取的数据 + */ + setData(str) { + try { + let data = JSON.parse(str); + if (typeof data !== 'object') { + throw new Error('文件内容有误'); + } + if (data.root) { + this.isFullDataFile = true; + } else { + this.isFullDataFile = false; + data = { + ...exampleData, + root: data + } + } + this.$bus.$emit('setData', data); + } catch (error) { + console.log(error) + this.$message.error("文件打开失败"); + } + }, + + /** + * @Author: 王林 + * @Date: 2022-09-24 15:40:42 + * @Desc: 写入本地文件 + */ + async writeLocalFile(content) { + if (!fileHandle || !this.isHandleLocalFile) { + return; + } + if (!this.isFullDataFile) { + content = content.root; + } + let string = JSON.stringify(content); + const writable = await fileHandle.createWritable(); + await writable.write(string); + await writable.close(); + }, + + /** + * @Author: 王林 + * @Date: 2022-09-24 15:40:48 + * @Desc: 创建本地文件 + */ + async createNewLocalFile() { + await this.createLocalFile(exampleData); + }, + + /** + * @Author: 王林 + * @Date: 2022-09-24 15:49:17 + * @Desc: 另存为 + */ + async saveLocalFile() { + let data = getData(); + await this.createLocalFile(data); + }, + + /** + * @Author: 王林 + * @Date: 2022-09-24 15:50:22 + * @Desc: 创建本地文件 + */ + async createLocalFile(content) { + try { + let _fileHandle = await window.showSaveFilePicker({ + types: [{ + description: 'file', + accept: {'application/*': ['.json', '.smm']}, + }], + }); + if (!_fileHandle) { + return; + } + const loading = this.$loading({ + lock: true, + text: '正在创建文件', + spinner: 'el-icon-loading', + background: 'rgba(0, 0, 0, 0.7)' + }); + fileHandle = _fileHandle; + this.$store.commit('setIsHandleLocalFile', true); + this.isFullDataFile = true; + await this.writeLocalFile(content); + await this.readFile(); + loading.close(); + } catch (error) { + console.log(error); + this.$message.warning('你的浏览器可能不支持哦'); + } + }, + } }; diff --git a/web/src/store.js b/web/src/store.js index 2642bbe2..49db3a16 100644 --- a/web/src/store.js +++ b/web/src/store.js @@ -6,7 +6,8 @@ Vue.use(Vuex) const store = new Vuex.Store({ state: { - mindMapData: null // 思维导图数据 + mindMapData: null, // 思维导图数据 + isHandleLocalFile: false// 是否操作的是本地文件 }, mutations: { /** @@ -16,6 +17,16 @@ const store = new Vuex.Store({ */ setMindMapData(state, data) { state.mindMapData = data + }, + + /** + * javascript comment + * @Author: 王林 + * @Date: 2022-09-24 13:55:38 + * @Desc: 设置操作本地文件标志位 + */ + setIsHandleLocalFile(state, data) { + state.isHandleLocalFile = data } }, actions: {