├── .gitignore ├── LICENSE ├── README.CN.md ├── README.md ├── babel.config.js ├── jsconfig.json ├── package.json ├── public ├── css │ └── iconfont.css ├── favicon.ico ├── img │ ├── left_main.gif │ ├── logo.png │ ├── 定时提醒功能.gif │ ├── 幕布大纲效果 .gif │ └── 黑夜模式.gif └── index.html ├── script └── deleteBuild.js ├── src ├── App.vue ├── assets │ ├── logo.png │ ├── main.scss │ ├── maximize.png │ └── reduction_window.png ├── background.ts ├── components │ ├── collapse-transition.js │ ├── context_menu.vue │ ├── drag.vue │ ├── menu_style.vue │ ├── mindMap │ │ ├── Contextmenu.vue │ │ ├── assistant.ts │ │ ├── attribute │ │ │ ├── get.ts │ │ │ ├── index.ts │ │ │ └── set.ts │ │ ├── css │ │ │ ├── Mindmap.module.scss │ │ │ ├── Mindmap.module.scss.d.ts │ │ │ └── index.ts │ │ ├── d3 │ │ │ └── index.ts │ │ ├── data.json │ │ ├── data │ │ │ ├── ImData.ts │ │ │ ├── flextree │ │ │ │ ├── algorithm.ts │ │ │ │ ├── helper.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── draw │ │ │ └── index.ts │ │ ├── index.vue │ │ ├── interface.ts │ │ ├── listener │ │ │ ├── index.ts │ │ │ ├── listener.ts │ │ │ └── switcher.ts │ │ ├── mind_map.vue │ │ ├── state │ │ │ ├── Snapshot.ts │ │ │ └── index.ts │ │ └── variable │ │ │ ├── contextmenu.ts │ │ │ ├── element.ts │ │ │ ├── index.ts │ │ │ └── selection.ts │ ├── newTree │ │ ├── model │ │ │ ├── emitter.js │ │ │ ├── node.js │ │ │ ├── tree-store.js │ │ │ └── util.js │ │ ├── tree-node.vue │ │ └── tree.vue │ ├── note_editor.vue │ └── note_header.vue ├── i18n │ └── index.ts ├── main.ts ├── mainProcess.ts ├── mitt.ts ├── on.ts ├── preload.ts ├── router │ └── index.ts ├── server │ └── index.ts ├── shims-vue.d.ts ├── store │ ├── index.ts │ └── modules │ │ ├── header.ts │ │ ├── note.ts │ │ └── user.ts ├── types │ ├── custom-types.d.ts │ ├── index.d.ts │ └── store.d.ts ├── utils │ └── index.ts └── views │ ├── edited.vue │ ├── home.vue │ ├── index.vue │ ├── menu.vue │ ├── outline.vue │ └── setting.vue ├── tsconfig.json ├── vue.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | #Electron-builder output 26 | /dist_electron -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 MoNaiZi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Note

5 | 6 | # 介绍 7 | 现在很多electron项目都是11版本的或者之前的有点落后了,electron11版本之后进行了重大更改去除了remote模块禁止在渲染层调用主进程的api。 8 | 本次项目是一个加强版的便利贴记事本功能。vue方面就不过多介绍了,electron实现点:开机自启,黑夜模式,悬浮窗,用于定时提醒,优化的窗口拖动,优雅丝滑的改变窗口大小和位置显示界面,自定义安装,以及自定义图标,最小化,自定义托盘。 9 | 10 | ### 更新功能记录 11 | 大纲模式 12 | 13 | 思维导图(开发完,打磨中) 14 | 15 | ### 欢迎大家给建议提问题, (๑•̀ㅂ•́)و✧ 点点星星谢谢 16 | 17 | 18 | ### Note 使用技术 19 | 20 | ``` 21 | vue3 22 | ts 23 | electron19 24 | vuex 25 | vur-router 26 | node 16.15.0 27 | lowdb 28 | 29 | 安装 yarn install 30 | 运行 yarn serve 31 | 打包 yarn build 32 | ``` 33 | 34 |
35 |

36 | 37 |
38 |

大纲模式

39 | 40 |

定时提醒功能

41 | 42 |
43 | 44 |
45 |

侧边栏

46 | 47 |

黑夜模式

48 | 49 |
50 | 51 | ### 感谢支持 52 | [![Stargazers repo roster for @MoNaiZi/Note](https://reporoster.com/stars/MoNaiZi/Note)](https://github.com/MoNaiZi/Note/stargazers) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Note

5 | 6 | English | [简体中文](./README.CN.md) 7 | # Introduction 8 | 9 | Now many electron projects are in version 11 or before, which is a little behind. Major changes have been made after version 11, and the remote module has been removed. It is forbidden to call the API of the main process in the rendering layer. 10 | This project is an enhanced version of the post it note pad function. Vue is not enough to introduce. The implementation points of electron are: automatic startup, night mode, floating window, timing reminder, optimized window dragging, elegant and smooth change of window size and position display interface, custom installation, custom icons, minimization, and self-defined tray. 11 | 12 | ### Update function record 13 | outline mode 14 | 15 | Mind mapping (Development completed, grinding in progress) 16 | 17 | ### You are welcome to give suggestions and questions, (๑ • ̀ ㅂ• ́)و ✧ stars, thank you 18 | 19 | 20 | ### Note using technology 21 | 22 | ``` 23 | vue3 24 | ts 25 | electron19 26 | vuex 27 | vur-router 28 | node 16.15.0 29 | lowdb 30 | 31 | Install yarn install 32 | Run yarn serve 33 | Package yarn build 34 | ``` 35 | 36 |
37 |

38 | 39 |
40 | 41 | 42 |
43 |
44 | 45 | 46 |
47 | 48 | ### Thank you for your support 49 | [![Stargazers repo roster for @MoNaiZi/Note](https://reporoster.com/stars/MoNaiZi/Note)](https://github.com/MoNaiZi/Note/stargazers) 50 | 51 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitThis": false, 4 | "target": "es5", 5 | "module": "esnext", 6 | "baseUrl": "./", 7 | "moduleResolution": "node", 8 | "paths": { 9 | "@/*": [ 10 | "src/*" 11 | ] 12 | }, 13 | "lib": [ 14 | "esnext", 15 | "dom", 16 | "dom.iterable", 17 | "scripthost" 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Note", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service electron:serve", 7 | "build": "node script/deleteBuild && vue-cli-service electron:build", 8 | "lint": "vue-cli-service lint", 9 | "html": "vue-cli-service serve", 10 | "html:build": "vue-cli-service build", 11 | "postinstall": "electron-builder install-app-deps", 12 | "postuninstall": "electron-builder install-app-deps" 13 | }, 14 | "main": "background.js", 15 | "dependencies": { 16 | "@icon-park/vue-next": "^1.4.2", 17 | "@types/lowdb": "1.0.9", 18 | "@wangeditor/editor": "^5.1.1", 19 | "@wangeditor/editor-for-vue": "^5.1.12", 20 | "core-js": "^3.6.5", 21 | "d3": "^7.6.1", 22 | "d3-drag": "2.0.0", 23 | "d3-interpolate": "^2.0.1", 24 | "d3-scale": "3.3.0", 25 | "d3-scale-chromatic": "2.0.0", 26 | "d3-selection": "2.0.0", 27 | "d3-shape": "2.1.0", 28 | "d3-transition": "2.0.0", 29 | "d3-zoom": "2.0.0", 30 | "dayjs": "^1.11.3", 31 | "electron-rebuild": "^3.2.7", 32 | "element-plus": "^2.2.6", 33 | "i18next": "^21.9.1", 34 | "javascript-obfuscator": "^4.0.0", 35 | "lodash": "^4.17.21", 36 | "lowdb": "1.0.0", 37 | "mitt": "^3.0.0", 38 | "node-polyfill-webpack-plugin": "^1.1.4", 39 | "pg-hstore": "2.3.4", 40 | "rimraf": "3.0.2", 41 | "sass": "1.51.0", 42 | "sass-loader": "7.3.1", 43 | "sequelize": "6.6.5", 44 | "sqlite3": "5.0.0", 45 | "ts-loader": "~8.2.0", 46 | "vue": "^3.2.13", 47 | "vue-class-component": "^8.0.0-0", 48 | "vue-router": "^4.0.13", 49 | "vuex": "^4.0.2", 50 | "webpack-obfuscator": "^3.5.1" 51 | }, 52 | "devDependencies": { 53 | "@babel/core": "^7.12.16", 54 | "@babel/eslint-parser": "^7.12.16", 55 | "@types/d3": "^7.4.0", 56 | "@types/d3-drag": "2.0.0", 57 | "@types/d3-ease": "2.0.0", 58 | "@types/d3-scale": "3.2.2", 59 | "@types/d3-scale-chromatic": "2.0.0", 60 | "@types/d3-selection": "2.0.0", 61 | "@types/d3-shape": "2.0.0", 62 | "@types/d3-transition": "2.0.0", 63 | "@types/d3-zoom": "2.0.0", 64 | "@typescript-eslint/eslint-plugin": "^5.4.0", 65 | "@typescript-eslint/parser": "^5.4.0", 66 | "@vue/cli-plugin-babel": "~5.0.0", 67 | "@vue/cli-plugin-eslint": "~5.0.0", 68 | "@vue/cli-plugin-typescript": "~5.0.0", 69 | "@vue/cli-service": "~5.0.0", 70 | "@vue/eslint-config-typescript": "^9.1.0", 71 | "electron": "^19.0.0", 72 | "electron-devtools-installer": "^3.1.0", 73 | "eslint": "^7.32.0", 74 | "eslint-plugin-vue": "^8.0.3", 75 | "typescript": "~4.5.5", 76 | "vue-cli-plugin-electron-builder": "~2.1.1" 77 | }, 78 | "peerDependencies": { 79 | "snabbdom": "^3.5.1" 80 | }, 81 | "eslintConfig": { 82 | "root": true, 83 | "env": { 84 | "node": true 85 | }, 86 | "extends": [ 87 | "plugin:vue/vue3-essential", 88 | "eslint:recommended", 89 | "@vue/typescript" 90 | ], 91 | "parserOptions": { 92 | "parser": "@typescript-eslint/parser" 93 | }, 94 | "rules": { 95 | "vue/multi-word-component-names": 0, 96 | "no-debugger": "off" 97 | } 98 | }, 99 | "browserslist": [ 100 | "> 1%", 101 | "last 2 versions", 102 | "not dead", 103 | "not ie 11" 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /public/css/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; 3 | src: url('iconfont.eot?t=1608620514957'); 4 | /* IE9 */ 5 | src: url('iconfont.eot?t=1608620514957#iefix') format('embedded-opentype'), 6 | /* IE6-IE8 */ 7 | url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), 8 | url('iconfont.woff?t=1608620514957') format('woff'), 9 | url('iconfont.ttf?t=1608620514957') format('truetype'), 10 | /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 11 | url('iconfont.svg?t=1608620514957#iconfont') format('svg'); 12 | /* iOS 4.1- */ 13 | } 14 | 15 | .iconfont { 16 | font-family: "iconfont" !important; 17 | font-size: 16px; 18 | font-style: normal; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | .icon-link:before { 24 | content: "\e701"; 25 | } 26 | 27 | .icon-arrow-up:before { 28 | content: "\e6f0"; 29 | } 30 | 31 | .icon-arrow-down:before { 32 | content: "\e6f1"; 33 | } 34 | 35 | .icon-kong_neirong:before { 36 | content: "\e6ce"; 37 | } 38 | 39 | .icon-newopen:before { 40 | content: "\e68d"; 41 | } 42 | 43 | .icon-delete:before { 44 | content: "\e605"; 45 | } 46 | 47 | .icon-editor-bold:before { 48 | content: "\e8ce"; 49 | } 50 | 51 | .icon-ol:before { 52 | content: "\e600"; 53 | } 54 | 55 | .icon-ul:before { 56 | content: "\e601"; 57 | } 58 | 59 | .icon-export:before { 60 | content: "\e802"; 61 | } 62 | 63 | .icon-inport:before { 64 | content: "\e803"; 65 | } 66 | 67 | .icon-thepin-active:before { 68 | content: "\e715"; 69 | } 70 | 71 | .icon-bold:before { 72 | content: "\e6f5"; 73 | } 74 | 75 | .icon-italic:before { 76 | content: "\e6f7"; 77 | } 78 | 79 | .icon-underline:before { 80 | content: "\e6f8"; 81 | } 82 | 83 | .icon-strikethrough:before { 84 | content: "\e6fb"; 85 | } 86 | 87 | .icon-image:before { 88 | content: "\e702"; 89 | } 90 | 91 | .icon-unordered-list:before { 92 | content: "\e70e"; 93 | } 94 | 95 | .icon-ordered-list:before { 96 | content: "\e710"; 97 | } 98 | 99 | .icon-list:before { 100 | content: "\e61f"; 101 | } 102 | 103 | .icon-back:before { 104 | content: "\e636"; 105 | } 106 | 107 | .icon-thepin:before { 108 | content: "\e714"; 109 | } 110 | 111 | .icon-refresh:before { 112 | content: "\e602"; 113 | } 114 | 115 | .icon-close:before { 116 | content: "\e603"; 117 | } 118 | 119 | .icon-setting:before { 120 | content: "\e604"; 121 | } 122 | 123 | .icon-search:before { 124 | content: "\e60c"; 125 | } 126 | 127 | .icon-warning:before { 128 | content: "\e60e"; 129 | } 130 | 131 | .icon-mail:before { 132 | content: "\e60f"; 133 | } 134 | 135 | .icon-picture:before { 136 | content: "\e612"; 137 | } 138 | 139 | .icon-more:before { 140 | content: "\e62a"; 141 | } 142 | 143 | .icon-add:before { 144 | content: "\e62b"; 145 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoNaiZi/Note/6e3820233212b976d5a5cf010cd76187ccedc633/public/favicon.ico -------------------------------------------------------------------------------- /public/img/left_main.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoNaiZi/Note/6e3820233212b976d5a5cf010cd76187ccedc633/public/img/left_main.gif -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoNaiZi/Note/6e3820233212b976d5a5cf010cd76187ccedc633/public/img/logo.png -------------------------------------------------------------------------------- /public/img/定时提醒功能.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoNaiZi/Note/6e3820233212b976d5a5cf010cd76187ccedc633/public/img/定时提醒功能.gif -------------------------------------------------------------------------------- /public/img/幕布大纲效果 .gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoNaiZi/Note/6e3820233212b976d5a5cf010cd76187ccedc633/public/img/幕布大纲效果 .gif -------------------------------------------------------------------------------- /public/img/黑夜模式.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoNaiZi/Note/6e3820233212b976d5a5cf010cd76187ccedc633/public/img/黑夜模式.gif -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /script/deleteBuild.js: -------------------------------------------------------------------------------- 1 | const rm = require('rimraf'); 2 | const path = require('path'); 3 | 4 | // 删除作用只用于删除打包前的buildPath || dist_electron 5 | // dist_electron是默认打包文件夹 6 | rm(path.join(__dirname, `../../${'dist_electron'}`), () => { }); 7 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 87 | 88 | 89 | 90 | 128 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoNaiZi/Note/6e3820233212b976d5a5cf010cd76187ccedc633/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/main.scss: -------------------------------------------------------------------------------- 1 | html { 2 | overflow: hidden; 3 | } 4 | 5 | body { 6 | margin: 0px; 7 | } 8 | 9 | .divider { 10 | height: 1px; 11 | background: #d2d2d2; 12 | width: 100%; 13 | margin: 0 auto; 14 | margin-bottom: 10px; 15 | } 16 | 17 | .main-fade-enter, 18 | .main-fade-leave-to { 19 | display: none; 20 | opacity: 0; 21 | animation: main-fade 0.4s reverse; 22 | } 23 | 24 | .main-fade-enter-active, 25 | .main-fade-leave-active { 26 | opacity: 0; 27 | animation: main-fade 0.4s; 28 | } 29 | 30 | @keyframes main-fade { 31 | from { 32 | opacity: 0; 33 | transform: scale(0.96); 34 | } 35 | 36 | to { 37 | opacity: 1; 38 | transform: scale(1); 39 | } 40 | } 41 | 42 | //富文本 43 | .w-e-text-container * { 44 | margin: 4px 1px !important; 45 | position: relative; 46 | top: -0.5px; 47 | } 48 | 49 | .w-e-bar-bottom .w-e-select-list { 50 | margin-bottom: -140px !important; 51 | 52 | .w-e-select-list ul li { 53 | top: 0%; 54 | } 55 | 56 | .editor-content-view input[type="checkbox"] { 57 | margin-right: 5px; 58 | } 59 | } 60 | 61 | .w-e-text-placeholder{ 62 | top:6px !important; 63 | } 64 | 65 | .w-e-scroll, 66 | .w-e-scroll>div { 67 | margin: 0px !important; 68 | } 69 | 70 | .w-e-text-placeholder { 71 | position: absolute; 72 | margin: 0px !important; 73 | top: 0px; 74 | } 75 | 76 | .w-e-textarea-divider { 77 | padding: 3px; 78 | } 79 | 80 | .w-e-hover-bar { 81 | border: 1px solid var(--w-e-toolbar-border-color); 82 | border-radius: 3px; 83 | box-shadow: 0 2px 5px #0000001f; 84 | position: absolute; 85 | top: 28px; 86 | 87 | .w-e-select-list ul li { 88 | margin: 0px !important; 89 | } 90 | 91 | .w-e-select-list ul li svg { 92 | margin: 0px !important; 93 | top: 28% !important; 94 | } 95 | } 96 | 97 | .w-e-text-container [data-slate-editor] ul { 98 | top: 0px !important; 99 | 100 | li { 101 | top: 0px !important; 102 | } 103 | } 104 | 105 | .w-e-text-container [data-slate-editor] { 106 | padding: 0px; 107 | height: 100% 108 | } 109 | 110 | 111 | .w-e-text-container [data-slate-editor] .table-container { 112 | width: 99%; 113 | 114 | tbody * { 115 | margin: 0px !important; 116 | top: 0px; 117 | } 118 | 119 | 120 | } 121 | 122 | .mind_map { 123 | .hasPrevAndNextActive { 124 | svg { 125 | path { 126 | stroke: #979797; 127 | } 128 | } 129 | } 130 | 131 | .hasPrevAndNextDef { 132 | svg { 133 | path { 134 | stroke: #c2c2c2; 135 | } 136 | } 137 | } 138 | } 139 | 140 | .dark { 141 | // background: #393939; 142 | $darkBg: #393939; 143 | height: 100vh; 144 | width: 100vw; 145 | // --w-e-toolbar-color: #fff; //文字svg颜色 146 | 147 | // --w-e-toolbar-active-color: #fff !important; //提示框文字颜色 148 | // --w-e-toolbar-active-bg-color: #000 !important; //button 背景active颜色 149 | // --el-bg-color: #393939; 150 | // --w-e-toolbar-bg-color: $darkBg; //menu背景颜色 151 | 152 | // --w-e-toolbar-active-bg-color: rgb(150, 150, 150); //hover 的背景颜色 153 | // --w-e-toolbar-active-color: #333; 154 | // --w-e-textarea-slight-bg-color: #333; //表格头部栏 155 | // --el-fill-color-blank: $darkBg; //树组件背景颜色 156 | // --el-text-color-regular: #fff; //树组件文字颜色 157 | // --el-fill-color-light: #5b5b5b; //树组件行鼠标停留 158 | // --mind-map-has-prev: #979797; 159 | // --mind-map-has-prev-active: #c2c2c2; 160 | 161 | // --w-e-toolbar-disabled-color: red; 162 | // --w-e-toolbar-border-color: red; //分割线,还有边框颜色 163 | // --w-e-modal-button-bg-color: red; 164 | //// 165 | 166 | --w-e-textarea-bg-color: #fff; 167 | --w-e-textarea-color: #333; 168 | --w-e-textarea-border-color: #ccc; 169 | --w-e-textarea-slight-border-color: #e8e8e8; 170 | --w-e-textarea-slight-color: #d4d4d4; 171 | --w-e-textarea-slight-bg-color: #f5f2f0; 172 | --w-e-textarea-selected-border-color: #B4D5FF; // 选中的元素,如选中了分割线 173 | --w-e-textarea-handler-bg-color: #4290f7; // 工具,如图片拖拽按钮 174 | 175 | // toolbar - css vars 176 | --w-e-toolbar-color: #eaeaea; 177 | --w-e-toolbar-bg-color: #fff; 178 | --w-e-toolbar-active-color: #333; 179 | --w-e-toolbar-active-bg-color: #000; 180 | --w-e-toolbar-disabled-color: #999; 181 | --w-e-toolbar-border-color: #000; 182 | 183 | // modal - css vars 184 | --w-e-modal-button-bg-color: #fafafa; 185 | --w-e-modal-button-border-color: #d9d9d9; 186 | 187 | @mixin drak { 188 | background: $darkBg !important; 189 | color: #fff !important; 190 | } 191 | 192 | .w-e-bar-item-menus-container { 193 | background: #2d2d2d; 194 | 195 | .w-e-bar-item button:hover { 196 | background-color: #2d2d2d; 197 | color: #fff 198 | } 199 | } 200 | 201 | .index_main { 202 | --index-main-icon: #fff; //首页图标 203 | } 204 | 205 | .wrap, 206 | .context_menu, 207 | .el-input__wrapper { 208 | @include drak 209 | } 210 | 211 | .item { 212 | @include drak; 213 | 214 | .ArrowDownBold { 215 | background: #636363 !important; 216 | } 217 | } 218 | 219 | .w-e-text-container { 220 | @include drak 221 | } 222 | 223 | .w-e-toolbar { 224 | @include drak; 225 | 226 | .w-e-bar-item .active { 227 | background-color: $darkBg !important; 228 | } 229 | 230 | .w-e-bar-item:hover { 231 | background-color: $darkBg !important; 232 | } 233 | } 234 | 235 | .header_main, 236 | .set_main, 237 | .home, 238 | .loadMore { 239 | color: #fff; 240 | } 241 | 242 | .mind_map { 243 | svg { 244 | background-color: $darkBg; 245 | 246 | foreignObject { 247 | color: #000; 248 | } 249 | 250 | .root_rect>rect { 251 | fill: $darkBg !important; 252 | } 253 | 254 | text { 255 | fill: #fff; 256 | } 257 | } 258 | 259 | .hasPrevAndNextActive { 260 | svg { 261 | path { 262 | stroke: #c2c2c2; 263 | } 264 | } 265 | } 266 | 267 | .hasPrevAndNextDef { 268 | svg { 269 | path { 270 | stroke: #979797; 271 | } 272 | } 273 | } 274 | } 275 | 276 | .el-tree-node { 277 | .row { 278 | .no-left { 279 | color: #f2f2f2; 280 | } 281 | 282 | .no-left:hover { 283 | color: #fff; 284 | box-shadow: 0 0 4px #cbcbcb; 285 | } 286 | 287 | 288 | } 289 | 290 | .line_menu::before { 291 | background-color: #fff !important; 292 | } 293 | 294 | } 295 | 296 | .w-e-hover-bar { 297 | background: #2d2d2d; 298 | 299 | .w-e-select-list ul li { 300 | background: #3a3a3a; 301 | } 302 | } 303 | } 304 | 305 | .index_main { 306 | --index-main-icon: #717171 307 | } 308 | 309 | .right_main { 310 | width: 100%; 311 | position: absolute; 312 | right: 0px; 313 | height: 100%; 314 | } 315 | 316 | .isLeft { 317 | .left_main { 318 | width: 69%; 319 | 320 | .ly-tree-container { 321 | width: 86% !important; 322 | } 323 | 324 | .header_main { 325 | padding: 8px 5px; 326 | 327 | .title { 328 | width: 136%; 329 | text-align: center; 330 | margin-left: 10%; 331 | padding: 0px 10px; 332 | } 333 | } 334 | 335 | 336 | } 337 | 338 | .right_main { 339 | width: 30%; 340 | 341 | // transition: all 0.5s; 342 | 343 | 344 | .index_main { 345 | width: 100%; 346 | 347 | // .content { 348 | // height: 0px !important; 349 | // transition: all 0.5s; 350 | 351 | .editor { 352 | height: 0px !important; 353 | transition: all 0.5s; 354 | } 355 | 356 | // } 357 | 358 | } 359 | } 360 | } 361 | 362 | 363 | 364 | //树组件 365 | .expandedBtn { 366 | 367 | svg { 368 | transition: all 0.5s; 369 | } 370 | 371 | .no-left { 372 | svg { 373 | transform: rotate(90deg) !important; 374 | } 375 | } 376 | 377 | .is-leaf { 378 | visibility: hidden; 379 | } 380 | 381 | .expanded { 382 | svg { 383 | transform: rotate(180deg) !important; 384 | } 385 | } 386 | } 387 | 388 | .el-tree-node { 389 | white-space: normal !important; 390 | word-wrap: break-word !important; 391 | 392 | } 393 | 394 | .ly-tree-container { 395 | width: 90%; 396 | padding: 0px 20px 20px 38px; 397 | white-space: normal !important; 398 | word-wrap: break-word !important; 399 | height: 80%; 400 | overflow: auto; 401 | 402 | span { 403 | font-size: 14px; 404 | } 405 | 406 | .el-tree-node__content:first-child { 407 | 408 | &::before, 409 | &::after { 410 | border: none; 411 | } 412 | } 413 | 414 | .ly-visible { 415 | margin-left: 50px; 416 | visibility: hidden; 417 | } 418 | 419 | .ly-edit__text { 420 | width: 25%; 421 | height: 25px; 422 | border: 1px solid #e6e6e6; 423 | border-radius: 3px; 424 | color: #666; 425 | text-indent: 10px; 426 | } 427 | 428 | .ly-tree__loading { 429 | color: #666; 430 | font-weight: bold; 431 | } 432 | 433 | .node_main { 434 | position: relative; 435 | 436 | 437 | } 438 | 439 | .ly-tree-node { 440 | flex: 1; 441 | display: flex; 442 | // align-items: center; 443 | // justify-content: space-between; 444 | justify-content: flex-start; 445 | font-size: 14px; 446 | padding-right: 8px; 447 | text-align: left; 448 | margin-bottom: 3px; 449 | 450 | 451 | .line_menu { 452 | list-style: none; 453 | text-align: center; 454 | border-radius: 50%; 455 | width: 20px; 456 | height: 20px; 457 | z-index: 99; 458 | line-height: 22px; 459 | position: relative; 460 | top: 2px; 461 | } 462 | 463 | .line_menu::before { 464 | content: ""; 465 | display: inline-block; 466 | width: 6px; 467 | height: 6px; 468 | background-color: black; 469 | vertical-align: middle; 470 | border-radius: 50%; 471 | margin-top: -3px; 472 | } 473 | } 474 | 475 | .ly-tree-node>div>span:last-child { 476 | display: inline-block; 477 | width: 110px; 478 | text-align: left; 479 | } 480 | 481 | .ly-tree-node>span:last-child { 482 | display: inline-block; 483 | width: 110px; 484 | text-align: left; 485 | } 486 | 487 | .el-tree-node>.el-tree-node__children { 488 | overflow: visible; 489 | transition: all 0.5s; 490 | } 491 | 492 | .el-tree-node .el-tree-node__content { 493 | height: auto; 494 | margin-top: 10px; 495 | 496 | .line { 497 | height: 2px; 498 | background-color: rgba(67, 67, 255, 0.786); 499 | width: 100%; 500 | position: absolute; 501 | z-index: 10; 502 | top: 21px; 503 | display: none; 504 | } 505 | 506 | &:hover .ly-visible { 507 | visibility: visible; 508 | } 509 | 510 | &::before, 511 | &::after { 512 | content: ''; 513 | position: absolute; 514 | right: auto; 515 | } 516 | 517 | &::before { 518 | border-left: 3px solid #e6e6e6; 519 | bottom: 50px; 520 | height: 100%; 521 | top: 0; 522 | width: 1px; 523 | margin-left: -9px; 524 | } 525 | 526 | // &::after { 527 | // border-top: 3px solid #e6e6e6; 528 | // height: 20px; 529 | // top: 14px; 530 | // width: 10px; 531 | // margin-left: -5px; 532 | // } 533 | } 534 | 535 | .el-tree .el-tree-node { 536 | position: relative; 537 | width: 50vw 538 | } 539 | } -------------------------------------------------------------------------------- /src/assets/maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoNaiZi/Note/6e3820233212b976d5a5cf010cd76187ccedc633/src/assets/maximize.png -------------------------------------------------------------------------------- /src/assets/reduction_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoNaiZi/Note/6e3820233212b976d5a5cf010cd76187ccedc633/src/assets/reduction_window.png -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { app, protocol, BrowserWindow, ipcMain, session, MessageChannelMain, screen } from 'electron' 4 | import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' 5 | import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer' 6 | const isDevelopment = process.env.NODE_ENV !== 'production' 7 | const path = require('path'); 8 | const mainProcess = require('./mainProcess') 9 | const logo = mainProcess.logo() 10 | import db from './server' 11 | 12 | import('@/on') 13 | // Scheme must be registered before the app is ready 14 | protocol.registerSchemesAsPrivileged([ 15 | { scheme: 'app', privileges: { secure: true, standard: true } } 16 | ]) 17 | 18 | 19 | //关闭当前要打开的应用 20 | const gotTheLock = app.requestSingleInstanceLock() 21 | if (!gotTheLock) { 22 | app.quit() 23 | } 24 | app.on('second-instance', (event, commandLine, workingDirectory) => { 25 | // 输出从第二个实例中接收到的数据 26 | // 有人试图运行第二个实例,我们应该关注我们的窗口 27 | const myWindow = global.mainWin 28 | console.log('myWindow', myWindow) 29 | if (myWindow) { 30 | if (myWindow.isMinimized()) myWindow.restore() 31 | myWindow.focus() 32 | } 33 | }) 34 | 35 | 36 | 37 | //此方法将在Electron完成后调用 38 | //初始化并准备创建浏览器窗口。 39 | //某些API只能在此事件发生后使用。 40 | app.on('ready', async () => { 41 | if (isDevelopment && !process.env.IS_TEST) { 42 | // Install Vue Devtools 43 | try { 44 | await installExtension(VUEJS3_DEVTOOLS) 45 | } catch (e: any) { 46 | console.error('Vue Devtools failed to install:', e.toString()) 47 | } 48 | } 49 | // if (!isDevelopment) launchAtStartup() 50 | createWindow() 51 | 52 | }) 53 | 54 | 55 | 56 | async function createWindow() { 57 | const mainWindows = mainProcess.mainWindows() 58 | const { config, winURL } = mainWindows 59 | const size = screen.getPrimaryDisplay().workAreaSize 60 | config.x = size.width - 380 61 | config.y = size.height - 700 62 | config.resizable = false 63 | const win = new BrowserWindow(config) 64 | const bounds = win.getBounds() 65 | console.log('初始bounds', bounds) 66 | win.setIcon(logo) 67 | 68 | await mainProcess.initDevTool(session) 69 | 70 | if (process.env.WEBPACK_DEV_SERVER_URL) { 71 | // Load the url of the dev server if in development mode 72 | await win.loadURL(winURL) 73 | // if (!process.env.IS_TEST) win.webContents.openDevTools() 74 | } else { 75 | createProtocol('app') 76 | // Load the index.html when not in development 77 | win.loadURL(winURL) 78 | } 79 | win.on('closed', async () => { 80 | console.log('主窗口关闭') 81 | await db.get('NoteList').find({ timingStatus: 1 }).assign({ timingStatus: 0 }).write() 82 | app.exit() 83 | }) 84 | global.mainWin = win 85 | 86 | 87 | const { port1, port2 } = new MessageChannelMain() 88 | //允许在另一端还没有注册监听器的情况下就通过通道向其发送消息 消息将排队等待,直到有一个监听器注册为止。 89 | port2.postMessage({ test: 21 }) 90 | 91 | // 我们也可以接收来自渲染器主进程的消息。 92 | port2.on('message', (event) => { 93 | console.log('from renderer main world:', event) 94 | }) 95 | 96 | port2.start() 97 | // 预加载脚本将接收此 IPC 消息并将端口 98 | // 传输到主进程。 99 | // console.log('111111111') 100 | win.webContents.postMessage('main-world-port', null, [port1]) 101 | 102 | } 103 | 104 | // Quit when all windows are closed. 105 | app.on('window-all-closed', () => { 106 | // On macOS it is common for applications and their menu bar 107 | // to stay active until the user quits explicitly with Cmd + Q 108 | if (process.platform !== 'darwin') { 109 | app.quit() 110 | } 111 | }) 112 | 113 | app.on('activate', () => { 114 | // On macOS it's common to re-create a window in the app when the 115 | // dock icon is clicked and there are no other windows open. 116 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 117 | }) 118 | 119 | app.on('before-quit', event => { 120 | // event.preventDefault(); 121 | }) 122 | 123 | // Exit cleanly on request from parent process in development mode. 124 | if (isDevelopment) { 125 | if (process.platform === 'win32') { 126 | process.on('message', (data) => { 127 | if (data === 'graceful-exit') { 128 | app.quit() 129 | } 130 | }) 131 | } else { 132 | process.on('SIGTERM', () => { 133 | app.quit() 134 | }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/components/collapse-transition.js: -------------------------------------------------------------------------------- 1 | import { addClass, removeClass } from './newTree/model/util'; 2 | // import h from 'vue' 3 | 4 | class Transition { 5 | beforeEnter(el) { 6 | addClass(el, 'collapse-transition'); 7 | if (!el.dataset) el.dataset = {}; 8 | 9 | el.dataset.oldPaddingTop = el.style.paddingTop; 10 | el.dataset.oldPaddingBottom = el.style.paddingBottom; 11 | 12 | el.style.height = '0'; 13 | el.style.paddingTop = 0; 14 | el.style.paddingBottom = 0; 15 | } 16 | 17 | enter(el) { 18 | el.dataset.oldOverflow = el.style.overflow; 19 | if (el.scrollHeight !== 0) { 20 | el.style.height = el.scrollHeight + 'px'; 21 | el.style.paddingTop = el.dataset.oldPaddingTop; 22 | el.style.paddingBottom = el.dataset.oldPaddingBottom; 23 | } else { 24 | el.style.height = ''; 25 | el.style.paddingTop = el.dataset.oldPaddingTop; 26 | el.style.paddingBottom = el.dataset.oldPaddingBottom; 27 | } 28 | 29 | el.style.overflow = 'hidden'; 30 | } 31 | 32 | afterEnter(el) { 33 | // for safari: remove class then reset height is necessary 34 | removeClass(el, 'collapse-transition'); 35 | el.style.height = ''; 36 | el.style.overflow = el.dataset.oldOverflow; 37 | } 38 | 39 | beforeLeave(el) { 40 | if (!el.dataset) el.dataset = {}; 41 | el.dataset.oldPaddingTop = el.style.paddingTop; 42 | el.dataset.oldPaddingBottom = el.style.paddingBottom; 43 | el.dataset.oldOverflow = el.style.overflow; 44 | 45 | el.style.height = el.scrollHeight + 'px'; 46 | el.style.overflow = 'hidden'; 47 | } 48 | 49 | leave(el) { 50 | if (el.scrollHeight !== 0) { 51 | // for safari: add class after set height, or it will jump to zero height suddenly, weired 52 | addClass(el, 'collapse-transition'); 53 | el.style.height = 0; 54 | el.style.paddingTop = 0; 55 | el.style.paddingBottom = 0; 56 | } 57 | } 58 | 59 | afterLeave(el) { 60 | removeClass(el, 'collapse-transition'); 61 | el.style.height = ''; 62 | el.style.overflow = el.dataset.oldOverflow; 63 | el.style.paddingTop = el.dataset.oldPaddingTop; 64 | el.style.paddingBottom = el.dataset.oldPaddingBottom; 65 | } 66 | } 67 | 68 | export default { 69 | name: 'collapseTransition', 70 | functional: true, 71 | render(h, { children }) { 72 | const data = { 73 | on: new Transition() 74 | }; 75 | console.log('h', h) 76 | return h('transition', data, children); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/context_menu.vue: -------------------------------------------------------------------------------- 1 | 19 | 93 | 94 | -------------------------------------------------------------------------------- /src/components/drag.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 131 | 132 | 144 | -------------------------------------------------------------------------------- /src/components/menu_style.vue: -------------------------------------------------------------------------------- 1 | 2 15 | 23 | -------------------------------------------------------------------------------- /src/components/mindMap/Contextmenu.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 86 | 87 | 159 | -------------------------------------------------------------------------------- /src/components/mindMap/assistant.ts: -------------------------------------------------------------------------------- 1 | import emitter from '@/mitt' 2 | // import html2canvas from 'html2canvas' 3 | import { getDataId, getGTransform, getPath } from './attribute' 4 | import * as d3 from './d3' 5 | import style from './css' 6 | import { Data, Mdata, TwoNumber } from './interface' 7 | import { observer, selection, zoom, zoomTransform } from './variable' 8 | import { afterOperation, mmdata } from './data/index' 9 | import { snapshot } from './state' 10 | import { foreignDivEle, gEle, svgEle, wrapperEle } from './variable/element' 11 | import { onEditBlur } from './listener' 12 | 13 | /** 14 | * 使页面重排 15 | * @param ele - Element 16 | */ 17 | export const reflow = (ele: Element): number => ele.clientHeight 18 | 19 | /** 20 | * 获取一个加号图形的path路径,图形的中心坐标为(0,0) 21 | * @param stroke - 线条的粗细 22 | * @param side - 图形的边长 23 | */ 24 | export const getAddPath = (stroke: number, side: number): string => { 25 | const temp0 = -side / 2 26 | const temp1 = -stroke / 2 27 | const temp2 = stroke / 2 28 | const temp3 = side / 2 29 | return `M${temp3},${temp2}H${temp2}V${temp3}H${temp1}V${temp2}H${temp0}V${temp1}H${temp1}V${temp0}H${temp2}V${temp1}H${temp3}V${temp2}Z` 30 | } 31 | 32 | /** 33 | * 将一个字符串按换行符切分,返回字符串数组 34 | * @param str - 字符串 35 | */ 36 | export const getMultiline = (str: string): string[] => { 37 | 38 | const multiline = str.split('\n') 39 | if (multiline.length > 1 && multiline[multiline.length - 1] === '') { 40 | multiline.pop() 41 | } 42 | return multiline 43 | } 44 | 45 | export const convertToImg = (svgdiv: HTMLDivElement, name: string): void => { 46 | // html2canvas(svgdiv).then((canvas) => { 47 | // const dataUrl = canvas.toDataURL() 48 | // const window = open() 49 | // if (window) { 50 | // window.document.write(``) 51 | // window.document.title = name 52 | // window.document.close() 53 | // } 54 | // }) 55 | } 56 | 57 | export const makeTransition = ( 58 | dura: number, easingFn: (normalizedTime: number) => number 59 | ): d3.Transition => { 60 | return d3.transition().duration(dura).ease(easingFn) as d3.Transition 61 | } 62 | 63 | export const getRelativePos = (wrapper: HTMLElement, e: MouseEvent): { left: number, top: number } => { 64 | const { pageX, pageY } = e 65 | const wrapperPos = wrapper.getBoundingClientRect() 66 | const wrapperLeft = wrapperPos.left + window.pageXOffset 67 | const wrapperTop = wrapperPos.top + window.pageYOffset 68 | 69 | return { 70 | left: pageX - wrapperLeft, 71 | top: pageY - wrapperTop 72 | } 73 | } 74 | 75 | /** 76 | * @param this - gText 77 | */ 78 | export function getDragContainer(this: SVGGElement): SVGGElement { 79 | return this.parentNode?.parentNode?.parentNode as SVGGElement 80 | } 81 | 82 | export function selectGNode(d: SVGGElement): void 83 | export function selectGNode(d: Mdata): void 84 | export function selectGNode(d: SVGGElement | Mdata): void { 85 | 86 | const ele = d instanceof SVGGElement ? d : document.querySelector(`g[data-id='${getDataId(d)}']`) 87 | console.log('ele', ele) 88 | const oldSele = document.getElementsByClassName(style.selected)[0] 89 | if (ele) { 90 | if (oldSele) { 91 | if (oldSele !== ele) { 92 | oldSele.classList.remove(style.selected) 93 | ele.classList.add(style.selected) 94 | } else { 95 | emitter.emit('edit-flag', true) 96 | } 97 | } else { 98 | ele.classList.add(style.selected) 99 | } 100 | } else { 101 | throw new Error('selectGNode failed') 102 | } 103 | } 104 | 105 | export function getSelectedGData(): Mdata { 106 | const sele = d3.select(`.${style.selected}`) 107 | return sele.data()[0] 108 | } 109 | 110 | /** 111 | * 获取文本在tspan中的宽度与高度 112 | * @param text - 113 | * @returns - 114 | */ 115 | export const getSize = (text: string): { width: number, height: number } => { 116 | // console.log('selection', selection) 117 | const { asstSvg }: { asstSvg: any } = selection 118 | if (!asstSvg) { throw new Error('asstSvg undefined') } 119 | const multiline = getMultiline(text) 120 | const t = asstSvg.append('text') 121 | t.selectAll('tspan').data(multiline).enter().append('tspan').text((d: any) => d).attr('x', 0) 122 | const tBox = (t.node() as SVGTextElement).getBBox() 123 | t.remove() 124 | return { 125 | width: Math.max(tBox.width, 22), 126 | height: Math.max(tBox.height, 22) * multiline.length 127 | } 128 | } 129 | 130 | export const moveNode = (node: SVGGElement, d: Mdata, p: TwoNumber, dura = 0): void => { 131 | const tran = makeTransition(dura, d3.easePolyOut) 132 | d.px = p[0] 133 | d.py = p[1] 134 | const select: any = d3.select(node) 135 | select.transition(tran).attr('transform', getGTransform) 136 | const select2: any = d3.select(`g[data-id='${getDataId(d)}'] > path`) 137 | select2.transition(tran).attr('d', (d: Mdata) => getPath(d)) 138 | } 139 | 140 | export const centerView = (): void => { 141 | const svg: any = selection.svg 142 | if (!svg) { return } 143 | const data = mmdata.data 144 | zoom.translateTo(svg, 0 + data.width / 2, 0 + data.height / 2) 145 | } 146 | 147 | /** 148 | * 缩放至合适大小并移动至全部可见 149 | */ 150 | export const fitView = (e: any): void => { 151 | const { svg }: { svg: any } = selection 152 | if (!svg || !gEle.value || !svgEle.value) { return } 153 | const gBB = gEle.value.getBBox() 154 | const svgBCR = svgEle.value.getBoundingClientRect() 155 | const multiple = Math.min(svgBCR.width / gBB.width, svgBCR.height / gBB.height) 156 | const svgCenter = { x: svgBCR.width / 2, y: svgBCR.height / 2 } 157 | // after scale 158 | const gCenter = { x: gBB.width * multiple / 2, y: gBB.height * multiple / 2 } 159 | if (e && e.k) { 160 | svg.transition().call(zoom.transform, d3.zoomIdentity.translate(e.x, e.y).scale(e.k)); 161 | } else { 162 | const center = d3.zoomIdentity.translate( 163 | -gBB.x * multiple + svgCenter.x - gCenter.x, 164 | -gBB.y * multiple + svgCenter.y - gCenter.y 165 | ).scale(multiple) 166 | zoom.transform(svg, center) 167 | } 168 | } 169 | 170 | /** 171 | * 元素被遮挡时,移动视图使其处于可见区域 172 | * @param ele - 元素 173 | */ 174 | export const moveView = (ele: Element): void => { 175 | const { svg }: { svg: any } = selection 176 | // 得到d相对于视图左上角的坐标 177 | if (svg && svgEle.value) { 178 | const { k } = zoomTransform.value 179 | const gBCR = ele.getBoundingClientRect() 180 | const { x, y, width, height } = svgEle.value.getBoundingClientRect() 181 | const gLeft = gBCR.x - x 182 | const gRight = gLeft + gBCR.width 183 | const gTop = gBCR.y - y 184 | const gBottom = gTop + gBCR.height 185 | const space = 2 // 元素与视图的空隙,方便区分 186 | let x1 = 0 187 | let y1 = 0 188 | 189 | if (gLeft < 0) { x1 = -gLeft / k + space } 190 | if (gBCR.width > width || gRight > width) { x1 = -(gRight - width) / k - space } 191 | 192 | if (gTop < 0) { y1 = -gTop / k + space } 193 | if (gBCR.height > height || gBottom > height) { y1 = -(gBottom - height) / k - space } 194 | 195 | zoom.translateBy(svg, x1, y1) 196 | } 197 | } 198 | 199 | /** 200 | * 按一定程度缩放 201 | * @param flag - 为true时放大,false缩小 202 | */ 203 | export const scaleView = (flag: boolean): void => { 204 | const { svg }: { svg: any } = selection 205 | if (!svg) { return } 206 | zoom.scaleBy(svg, flag ? 1.1 : 0.9) 207 | } 208 | export const download = (): void => { 209 | if (!wrapperEle.value) { return } 210 | convertToImg(wrapperEle.value, mmdata.data.name) 211 | } 212 | export const next = (): void => { 213 | const nextData = snapshot.next() 214 | if (nextData) { 215 | mmdata.data = nextData 216 | afterOperation(false) 217 | } 218 | } 219 | export const prev = (): void => { 220 | const prevData = snapshot.prev() 221 | if (prevData) { 222 | mmdata.data = prevData 223 | afterOperation(false) 224 | } 225 | } 226 | 227 | /** 228 | * foreignDivEle事件监听与观察 229 | */ 230 | export const bindForeignDiv = (): void => { 231 | 232 | if (foreignDivEle.value) { 233 | observer.observe(foreignDivEle.value) 234 | foreignDivEle.value.addEventListener('blur', onEditBlur) 235 | foreignDivEle.value.addEventListener('mousedown', (e: MouseEvent) => e.stopPropagation()) 236 | } 237 | } 238 | 239 | /** 240 | * 判断字符串是否符合Data的数据格式,如果是,则返回格式化的数据,如果不是,返回false 241 | */ 242 | export const isData = (str: string): Data | false => { 243 | let data 244 | try { 245 | data = JSON.parse(str) 246 | return 'name' in data ? data : false 247 | } catch (error) { 248 | return false 249 | } 250 | } -------------------------------------------------------------------------------- /src/components/mindMap/attribute/get.ts: -------------------------------------------------------------------------------- 1 | import { Mdata, TspanData, TwoNumber } from '@/components/mindMap/interface' 2 | import { getMultiline } from '../assistant' 3 | import style from '../css' 4 | import { addBtnSide, link, textRectPadding, sharpCorner, expandBtnRect, addBtnRect } from '../variable' 5 | const getYOffset = () => 3 // max-branch / 2 6 | 7 | export const getSiblingGClass = (d?: Mdata): string[] => { 8 | 9 | const arr = ['node'] 10 | if (d) { arr.push(`depth-${d.depth}`) } 11 | return arr 12 | } 13 | export const getGClass = (d?: Mdata): string[] => { 14 | const arr = getSiblingGClass(d) 15 | if (d) { 16 | 17 | if (d.depth === 0) { arr.push(style.root) } 18 | if (!d.isExpand) { 19 | arr.push(style['isExpand']) 20 | } else if (!d.children || d.children.length === 0) { 21 | arr.push('leaf') 22 | } 23 | } 24 | return arr 25 | } 26 | export const getAddBtnClass = (d: Mdata): string[] => { 27 | const arr = [style['add-btn']] 28 | if (!d.isExpand) { 29 | arr.push(style['hidden']) 30 | } 31 | return arr 32 | } 33 | export const getGTransform = (d: Mdata): string => { 34 | // console.log('d111', d) 35 | return `translate(${d.dx + d.px},${d.dy + d.py})` 36 | } 37 | export const getDataId = (d: Mdata): string => { return d.id } 38 | export const getTspanData = (d: Mdata): TspanData[] => { 39 | const multiline = getMultiline(d.name) 40 | const height = d.height / multiline.length 41 | return multiline.map((name) => ({ name, height })) 42 | } 43 | export const getPath = (d: Mdata): string => { 44 | let dpw = 0 45 | let dph = 0 46 | const trp = Math.max(textRectPadding - 3, 0) // -3为了不超过选中框 47 | let w = d.width + trp 48 | const targetOffset = getYOffset() 49 | let sourceOffset = targetOffset 50 | const { parent } = d 51 | if (parent) { 52 | dpw = parent.width 53 | dph = parent.height 54 | if (parent.depth === 0) { 55 | if (!sharpCorner) { dpw /= 2 } 56 | dph /= 2 57 | sourceOffset = 0 58 | } 59 | } 60 | if (d.left) { 61 | if (parent) { 62 | if (parent.depth !== 0) { 63 | dpw = -dpw 64 | } else if (sharpCorner) { 65 | dpw = 0 66 | } 67 | } 68 | w = -w 69 | } 70 | const source: TwoNumber = [-d.dx + dpw - d.px, -d.dy + dph + sourceOffset - d.py] 71 | const target: TwoNumber = [0, d.height + targetOffset] 72 | return `${link({ source, target })}L${w},${target[1]}` 73 | } 74 | export const getAddBtnTransform = (d: Mdata, trp: number): string => { 75 | const y = d.depth === 0 ? d.height / 2 : d.height + getYOffset() 76 | let x = d.width + trp + addBtnSide / 2 + addBtnRect.margin 77 | if (d.left) { x = -x } 78 | return `translate(${x},${y})` 79 | } 80 | export const getExpandBtnTransform = (d: Mdata, trp: number): string => { 81 | const gap = 4 82 | const y = d.depth === 0 ? d.height / 2 : d.height + getYOffset() 83 | let x = d.width + trp + expandBtnRect.width / 2 + gap 84 | if (d.left) { x = -x } 85 | return `translate(${x},${y})` 86 | } -------------------------------------------------------------------------------- /src/components/mindMap/attribute/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get' 2 | export * from './set' -------------------------------------------------------------------------------- /src/components/mindMap/attribute/set.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsMdata, Mdata, SelectionCircle, SelectionG, SelectionRect, 3 | Transition, TspanData 4 | } from '../interface' 5 | import * as d3 from '../d3' 6 | import { transition as d3Transition } from 'd3-transition'; 7 | // import d3 from 'd3' 8 | import { 9 | addBtnRect, addBtnSide, branch, changeSharpCorner, 10 | expandBtnRect, rootTextRectPadding, rootTextRectRadius, textRectPadding 11 | } from '../variable' 12 | import { 13 | getAddBtnClass, getAddBtnTransform, getDataId, 14 | getExpandBtnTransform, getGClass, getGTransform, getPath 15 | } from './get' 16 | import style from '../css' 17 | 18 | /** 19 | * 根据该节点是否是根节点,绘制不同的效果 20 | */ 21 | export const attrA = ( 22 | isRoot: boolean, 23 | gTrigger: SelectionRect, 24 | gTextRect: SelectionRect, 25 | gExpandBtn: SelectionG, 26 | gAddBtn?: SelectionG 27 | ): void => { 28 | 29 | if (isRoot) { 30 | attrTrigger(gTrigger, rootTextRectPadding) 31 | attrTextRect(gTextRect, rootTextRectPadding, rootTextRectRadius) 32 | attrExpandBtn(gExpandBtn, rootTextRectPadding) 33 | if (gAddBtn) { attrAddBtn(gAddBtn, rootTextRectPadding) } 34 | } else { 35 | attrTrigger(gTrigger, textRectPadding) 36 | attrTextRect(gTextRect, textRectPadding) 37 | attrExpandBtn(gExpandBtn, textRectPadding) 38 | if (gAddBtn) { attrAddBtn(gAddBtn, textRectPadding) } 39 | } 40 | } 41 | 42 | export const attrG = (g: SelectionG, tran?: Transition): void => { 43 | 44 | const g1: any = g.attr('class', (d) => { 45 | const result = getGClass(d).join(' ') 46 | 47 | return result 48 | }).attr('data-id', getDataId) 49 | // g1.transition = d3Transition 50 | const g2 = tran ? g1.transition(tran) : g1 51 | g2.attr('transform', getGTransform) 52 | } 53 | 54 | export const attrText = (text: d3.Selection, tran?: Transition): void => { 55 | const text2: any = text 56 | const t1 = tran ? text2.transition(tran) : text 57 | // console.log('attrText', t1) 58 | // debugger 59 | t1.attr('transform', (d: { left: any; width: number }) => { 60 | // console.log('d', d) 61 | return `translate(${d.left ? -d.width : 0},0)` 62 | }) 63 | } 64 | 65 | export const attrTspan = (tspan: d3.Selection): void => { 66 | tspan.attr('alignment-baseline', 'text-before-edge') 67 | .text((d) => d.name || ' ') 68 | .attr('x', 0) 69 | .attr('dy', (d, i) => i ? d.height : 0) 70 | } 71 | 72 | export const attrAddBtnRect = (rect: SelectionRect): void => { 73 | const { side, padding } = addBtnRect 74 | const radius = 4 75 | const temp0 = -padding - side / 2 76 | const temp1 = side + padding * 2 77 | rect.attr('x', temp0) 78 | .attr('y', temp0) 79 | .attr('rx', radius) 80 | .attr('ry', radius).attr('width', temp1).attr('height', temp1) 81 | } 82 | 83 | export const attrExpandBtnRect = (rect: SelectionRect): void => { 84 | rect.attr('x', -expandBtnRect.width / 2).attr('y', -expandBtnRect.height / 2) 85 | .attr('width', expandBtnRect.width).attr('height', expandBtnRect.height) 86 | .attr('rx', expandBtnRect.radius).attr('ry', expandBtnRect.radius) 87 | .attr('stroke', (d) => d.color || 'grey') 88 | .attr('fill', (d) => d.color || 'grey') 89 | } 90 | 91 | export const attrExpandBtnCircle = (circle: SelectionCircle, cx: number): void => { 92 | circle.attr('cx', cx).attr('cy', 0).attr('r', 1) 93 | } 94 | 95 | export const attrTextRect = (rect: SelectionRect, padding: number, radius = 4): void => { 96 | rect.attr('x', (d) => -padding - (d.left ? d.width : 0)) 97 | .attr('y', -padding) 98 | .attr('rx', radius).attr('ry', radius) 99 | .attr('width', (d) => d.width + padding * 2) 100 | .attr('height', (d) => d.height + padding * 2) 101 | } 102 | 103 | export const attrExpandBtn = (g: SelectionG, trp: number): void => { 104 | g.attr('class', style['expand-btn']) 105 | .attr('transform', (d) => getExpandBtnTransform(d, trp)) 106 | .style('color', d => d.color).style('visibility', (d: any) => { 107 | 108 | if (d && !d.isExpand && d._children && d._children.length) { 109 | return 'visible' 110 | } 111 | return '' 112 | }) 113 | } 114 | 115 | export const attrAddBtn = (g: SelectionG, trp: number): void => { 116 | g.attr('class', (d) => getAddBtnClass(d).join(' ')).attr('transform', (d) => getAddBtnTransform(d, trp)) 117 | } 118 | 119 | export const attrTrigger = (rect: SelectionRect, padding: number): void => { 120 | const w = addBtnSide + addBtnRect.margin 121 | const p = padding * 2 122 | rect.attr('class', style.trigger) 123 | .attr('x', (d) => -padding - (d.left ? d.width + w : 0)) 124 | .attr('y', -padding) 125 | .attr('width', (d) => d.width + p + w) 126 | .attr('height', (d) => d.height + p) 127 | } 128 | 129 | export const attrPath = ( 130 | p: d3.Selection, 131 | tran?: Transition 132 | ): void => { 133 | const p1: any = p.attr('stroke', (d) => d.color).attr('stroke-width', branch) 134 | 135 | if (tran) { 136 | const p2 = p1.transition(tran) 137 | if (changeSharpCorner.value) { // 只有在改变sharpCorner的时候才应该调用 138 | p2.attrTween('d', pathTween) 139 | } else { 140 | p2.attr('d', getPath) 141 | } 142 | } else { 143 | p1.attr('d', getPath) 144 | } 145 | } 146 | 147 | function pathTween(data: Mdata, index: number, paths: ArrayLike) { 148 | const precision = 10 149 | const d = getPath(data) 150 | const path0 = paths[index] 151 | const path1 = path0.cloneNode() as SVGPathElement 152 | const n0 = path0.getTotalLength() 153 | 154 | path1.setAttribute('d', d) 155 | const n1 = path1.getTotalLength() 156 | 157 | // Uniform sampling of distance based on specified precision. 158 | const distances = [0] 159 | const dt = precision / Math.max(n0, n1) 160 | let i = 0 161 | while ((i += dt) < 1) distances.push(i) 162 | distances.push(1) 163 | 164 | // Compute point-interpolators at each distance. 165 | const points = distances.map((t) => { 166 | const p0 = path0.getPointAtLength(t * n0) 167 | const p1 = path1.getPointAtLength(t * n1) 168 | return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]) 169 | }) 170 | 171 | return (t: number) => { 172 | return t < 1 ? 'M' + points.map(p => p(t)).join('L') : d 173 | } 174 | } -------------------------------------------------------------------------------- /src/components/mindMap/css/Mindmap.module.scss: -------------------------------------------------------------------------------- 1 | $seleColor: rgba($color: blue, $alpha: 0.15); 2 | 3 | .btn { 4 | cursor: pointer; 5 | } 6 | 7 | .container { 8 | position: relative; 9 | height: 100%; 10 | 11 | svg text { 12 | white-space: pre; 13 | } 14 | 15 | #svg-wrapper { 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | .asst-svg { 21 | position: absolute; 22 | width: 0; 23 | height: 0; 24 | } 25 | } 26 | 27 | .svg { 28 | display: block; 29 | width: 100%; 30 | height: 100%; 31 | background-color: #eeeef3; 32 | 33 | path { 34 | fill: none; 35 | stroke-linecap: round; 36 | } 37 | 38 | text { 39 | fill: #4B4B4B; 40 | cursor: default; 41 | } 42 | 43 | foreignObject { 44 | background-color: white; 45 | border: 1px solid #aaa; 46 | 47 | div { 48 | display: inline-block; 49 | outline: none; 50 | width: max-content; 51 | min-width: 22px; 52 | padding: 6px; 53 | white-space: pre; 54 | text-align: left; 55 | } 56 | } 57 | 58 | .trigger { 59 | fill: transparent; 60 | } 61 | 62 | &.dragging .add-btn { 63 | visibility: hidden; 64 | } 65 | 66 | .add-btn { 67 | opacity: 0; 68 | cursor: pointer; 69 | 70 | &.hidden { 71 | visibility: hidden; 72 | } 73 | 74 | rect { 75 | stroke-width: 1; 76 | stroke: grey; 77 | fill: white; 78 | } 79 | 80 | path { 81 | // fill: #8685ff 82 | } 83 | } 84 | 85 | .expand-btn { 86 | visibility: hidden; 87 | 88 | &:hover { 89 | rect { 90 | fill: white 91 | } 92 | 93 | circle { 94 | fill: currentColor; 95 | } 96 | } 97 | } 98 | 99 | .text>rect { 100 | opacity: 0; 101 | fill: $seleColor; 102 | stroke-width: 1; 103 | stroke: white; 104 | } 105 | 106 | // 关于selected 107 | .selected>.content>.text>rect { 108 | opacity: 1; 109 | } 110 | 111 | .root.selected>.content>.text>rect { 112 | stroke-width: 3; 113 | stroke: $seleColor; 114 | } 115 | 116 | // 关于edited 117 | .edited>.content>.text { 118 | opacity: 0; 119 | } 120 | 121 | // outline 122 | .outline>.content>.text>rect { 123 | opacity: 1; 124 | fill: transparent; 125 | stroke-width: 3; 126 | stroke: $seleColor; 127 | } 128 | 129 | .root>.content>.text>rect { 130 | opacity: 1; 131 | fill: white; 132 | } 133 | 134 | .collapse>.content>.expand-btn { 135 | visibility: visible; 136 | } 137 | } 138 | 139 | .button-list { 140 | position: absolute; 141 | 142 | &.right-bottom { 143 | bottom: 40px; 144 | right: 0; 145 | } 146 | 147 | &.right-top { 148 | top: 0; 149 | right: 0; 150 | display: flex; 151 | } 152 | } 153 | 154 | .button-list button { 155 | position: relative; 156 | cursor: pointer; 157 | width: 36px; 158 | height: 36px; 159 | border-radius: 50%; 160 | background-color: transparent; 161 | display: flex; 162 | align-items: center; 163 | justify-content: center; 164 | padding: 0; 165 | border: 0; 166 | // color: #3f51b5; 167 | 168 | &::before { 169 | background-color: currentColor; 170 | border-radius: inherit; 171 | content: ""; 172 | opacity: 0; 173 | position: absolute; 174 | left: 0; 175 | right: 0; 176 | top: 0; 177 | bottom: 0; 178 | transition: opacity 0.2s cubic-bezier(0.4, 0, 0.6, 1); 179 | } 180 | 181 | &:hover::before { 182 | opacity: 0.1; 183 | } 184 | 185 | &.disabled { 186 | pointer-events: none; 187 | 188 | i { 189 | filter: invert(85%) sepia(20%) saturate(0%) hue-rotate(125deg) brightness(86%) contrast(93%); 190 | } 191 | } 192 | 193 | i { 194 | filter: invert(25%) sepia(40%) saturate(5050%) hue-rotate(227deg) brightness(78%) contrast(74%); 195 | width: 24px; 196 | height: 24px; 197 | } 198 | } -------------------------------------------------------------------------------- /src/components/mindMap/css/Mindmap.module.scss.d.ts: -------------------------------------------------------------------------------- 1 | declare const style: { 2 | readonly container: string 3 | readonly 'svg-wrapper': string 4 | readonly svg: string 5 | readonly 'asst-svg': string 6 | readonly dragging: string 7 | readonly 'button-list': string 8 | readonly 'right-bottom': string 9 | readonly 'right-top': string 10 | readonly disabled: string 11 | readonly gps: string 12 | readonly fit: string 13 | readonly root: string 14 | readonly selected: string 15 | readonly edited: string 16 | readonly text: string 17 | readonly outline: string 18 | readonly download: string 19 | readonly 'add-btn': string 20 | readonly 'hidden': string 21 | readonly 'expand-btn': string 22 | readonly 'isExpand': string 23 | readonly content: string 24 | readonly trigger: string 25 | readonly menu: string 26 | readonly prev: string 27 | readonly next: string 28 | } 29 | export default style 30 | -------------------------------------------------------------------------------- /src/components/mindMap/css/index.ts: -------------------------------------------------------------------------------- 1 | import style from './Mindmap.module.scss' 2 | export default style -------------------------------------------------------------------------------- /src/components/mindMap/d3/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'd3-shape' 2 | export * from 'd3-ease' 3 | export * from 'd3-zoom' 4 | export * from 'd3-selection' 5 | export * from 'd3-drag' 6 | export * from 'd3-transition' 7 | export * from 'd3-interpolate' -------------------------------------------------------------------------------- /src/components/mindMap/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "如何学习D3", 4 | "isExpand": true, 5 | "children": [ 6 | { 7 | "name": "预备知识", 8 | "isExpand": true, 9 | "children": [ 10 | { 11 | "name": "HTML & CSS" 12 | }, 13 | { 14 | "name": "JavaScript" 15 | }, 16 | { 17 | "name": "DOM" 18 | }, 19 | { 20 | "name": "SVG" 21 | }, 22 | { 23 | "name": "test\ntest" 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "安装", 29 | "children": [ 30 | { 31 | "name": "打开节点" 32 | } 33 | ] 34 | }, 35 | { 36 | "name": "入门", 37 | "children": [ 38 | { 39 | "name": "选择集" 40 | }, 41 | { 42 | "name": "test" 43 | }, 44 | { 45 | "name": "绑定数据" 46 | }, 47 | { 48 | "name": "添加删除元素" 49 | }, 50 | { 51 | "name": "简单图形", 52 | "children": [ 53 | { 54 | "name": "柱形图" 55 | }, 56 | { 57 | "name": "折线图" 58 | }, 59 | { 60 | "name": "散点图" 61 | } 62 | ] 63 | }, 64 | { 65 | "name": "比例尺" 66 | }, 67 | { 68 | "name": "生成器" 69 | }, 70 | { 71 | "name": "过渡" 72 | } 73 | ], 74 | "left": true 75 | }, 76 | { 77 | "name": "进阶", 78 | "left": true 79 | }, 80 | { 81 | "name": "一级节点", 82 | "children": [ 83 | { 84 | "name": "子节点1" 85 | }, 86 | { 87 | "name": "子节点2" 88 | }, 89 | { 90 | "name": "子节点3" 91 | } 92 | ] 93 | } 94 | ] 95 | } 96 | ] -------------------------------------------------------------------------------- /src/components/mindMap/data/flextree/algorithm.ts: -------------------------------------------------------------------------------- 1 | class Tree { 2 | w: number 3 | h: number 4 | y: number 5 | c: Tree[] 6 | cs: number 7 | x: number 8 | prelim: number 9 | mod: number 10 | shift: number 11 | change: number 12 | tl: Tree | null 13 | tr: Tree | null 14 | el: Tree | null 15 | er: Tree | null 16 | msel: number 17 | mser: number 18 | 19 | constructor (width: number, height: number, y: number, children: Tree[]) { 20 | this.w = width 21 | this.h = height 22 | this.y = y 23 | this.c = children 24 | this.cs = children.length 25 | 26 | this.x = 0 27 | this.prelim = 0 28 | this.mod = 0 29 | this.shift = 0 30 | this.change = 0 31 | this.tl = null // Left thread 32 | this.tr = null // Right thread 33 | this.el = null // extreme left nodes 34 | this.er = null // extreme right nodes 35 | // sum of modifiers at the extreme nodes 36 | this.msel = 0 37 | this.mser = 0 38 | } 39 | } 40 | 41 | function setExtremes (tree: Tree) { 42 | if (tree.cs === 0) { 43 | tree.el = tree 44 | tree.er = tree 45 | tree.msel = tree.mser = 0 46 | } else { 47 | tree.el = tree.c[0].el 48 | tree.msel = tree.c[0].msel 49 | tree.er = tree.c[tree.cs - 1].er 50 | tree.mser = tree.c[tree.cs - 1].mser 51 | } 52 | } 53 | 54 | function bottom (tree: Tree) { 55 | return tree.y + tree.h 56 | } 57 | 58 | /* A linked list of the indexes of left siblings and their lowest vertical coordinate. 59 | */ 60 | class IYL { 61 | lowY: number 62 | index: number 63 | next: IYL | null 64 | constructor (lowY: number, index: number, next: IYL | null) { 65 | this.lowY = lowY 66 | this.index = index 67 | this.next = next 68 | } 69 | } 70 | 71 | function updateIYL (minY: number, i: number, ih: IYL | null) { 72 | // Remove siblings that are hidden by the new subtree. 73 | while (ih !== null && minY >= ih.lowY) { 74 | // Prepend the new subtree 75 | ih = ih.next 76 | } 77 | return new IYL(minY, i, ih) 78 | } 79 | 80 | function distributeExtra (tree: Tree, i: number, si: number, distance: number) { 81 | // Are there intermediate children? 82 | if (si !== i - 1) { 83 | const nr = i - si 84 | tree.c[si + 1].shift += distance / nr 85 | tree.c[i].shift -= distance / nr 86 | tree.c[i].change -= distance - distance / nr 87 | } 88 | } 89 | 90 | function moveSubtree (tree: Tree, i: number, si: number, distance: number) { 91 | // Move subtree by changing mod. 92 | tree.c[i].mod += distance 93 | tree.c[i].msel += distance 94 | tree.c[i].mser += distance 95 | distributeExtra(tree, i, si, distance) 96 | } 97 | 98 | function nextLeftContour (tree: Tree) { 99 | return tree.cs === 0 ? tree.tl : tree.c[0] 100 | } 101 | 102 | function nextRightContour (tree: Tree) { 103 | return tree.cs === 0 ? tree.tr : tree.c[tree.cs - 1] 104 | } 105 | 106 | function setLeftThread (tree: Tree, i: number, cl: Tree, modsumcl: number) { 107 | const li = tree.c[0].el as Tree 108 | li.tl = cl 109 | // Change mod so that the sum of modifier after following thread is correct. 110 | const diff = (modsumcl - cl.mod) - tree.c[0].msel 111 | li.mod += diff 112 | // Change preliminary x coordinate so that the node does not move. 113 | li.prelim -= diff 114 | // Update extreme node and its sum of modifiers. 115 | tree.c[0].el = tree.c[i].el 116 | tree.c[0].msel = tree.c[i].msel 117 | } 118 | 119 | // Symmetrical to setLeftThread 120 | function setRightThread (tree: Tree, i: number, sr: Tree, modsumsr: number) { 121 | const ri = tree.c[i].er as Tree 122 | ri.tr = sr 123 | const diff = (modsumsr - sr.mod) - tree.c[i].mser 124 | ri.mod += diff 125 | ri.prelim -= diff 126 | tree.c[i].er = tree.c[i - 1].er 127 | tree.c[i].mser = tree.c[i - 1].mser 128 | } 129 | 130 | function seperate (tree: Tree, i: number, ih: IYL) { 131 | // Right contour node of left siblings and its sum of modifiers. 132 | let sr: Tree | null = tree.c[i - 1] 133 | let mssr = sr.mod 134 | // Left contour node of right siblings and its sum of modifiers. 135 | let cl: Tree | null = tree.c[i] 136 | let mscl = cl.mod 137 | while (sr !== null && cl !== null) { 138 | if (bottom(sr) > ih.lowY) { 139 | ih = ih.next as IYL 140 | } 141 | // How far to the left of the right side of sr is the left side of cl. 142 | const distance = mssr + sr.prelim + sr.w - (mscl + cl.prelim) 143 | if (distance > 0) { 144 | mscl += distance 145 | moveSubtree(tree, i, ih.index, distance) 146 | } 147 | 148 | const sy = bottom(sr) 149 | const cy = bottom(cl) 150 | if (sy <= cy) { 151 | sr = nextRightContour(sr) 152 | if (sr !== null) { 153 | mssr += sr.mod 154 | } 155 | } 156 | if (sy >= cy) { 157 | cl = nextLeftContour(cl) 158 | if (cl !== null) { 159 | mscl += cl.mod 160 | } 161 | } 162 | } 163 | 164 | // Set threads and update extreme nodes. 165 | // In the first case, the current subtree must be taller than the left siblings. 166 | if (sr === null && cl !== null) { 167 | setLeftThread(tree, i, cl, mscl) 168 | } else if (sr !== null && cl === null) { 169 | setRightThread(tree, i, sr, mssr) 170 | } 171 | } 172 | 173 | function positionRoot (tree: Tree) { 174 | // Position root between children, taking into account their mod. 175 | tree.prelim = 176 | (tree.c[0].prelim + 177 | tree.c[0].mod + 178 | tree.c[tree.cs - 1].mod + 179 | tree.c[tree.cs - 1].prelim + 180 | tree.c[tree.cs - 1].w) / 181 | 2 - 182 | tree.w / 2 183 | } 184 | 185 | function firstWalk (tree: Tree) { 186 | if (tree.cs === 0) { 187 | setExtremes(tree) 188 | return 189 | } 190 | 191 | firstWalk(tree.c[0]) 192 | let ih = updateIYL(bottom(tree.c[0].el as Tree), 0, null) 193 | for (let i = 1; i < tree.cs; i++) { 194 | firstWalk(tree.c[i]) 195 | const minY = bottom(tree.c[i].er as Tree) 196 | seperate(tree, i, ih) 197 | ih = updateIYL(minY, i, ih) 198 | } 199 | positionRoot(tree) 200 | setExtremes(tree) 201 | } 202 | 203 | function addChildSpacing (tree: Tree) { 204 | let d = 0 205 | let modsumdelta = 0 206 | for (let i = 0; i < tree.cs; i++) { 207 | d += tree.c[i].shift 208 | modsumdelta += d + tree.c[i].change 209 | tree.c[i].mod += modsumdelta 210 | } 211 | } 212 | 213 | function secondWalk (tree: Tree, modsum: number) { 214 | modsum += tree.mod 215 | // Set absolute (no-relative) horizontal coordinates. 216 | tree.x = tree.prelim + modsum 217 | addChildSpacing(tree) 218 | for (let i = 0; i < tree.cs; i++) { 219 | secondWalk(tree.c[i], modsum) 220 | } 221 | } 222 | 223 | function layout (tree: Tree): void { 224 | firstWalk(tree) 225 | secondWalk(tree, 0) 226 | } 227 | 228 | export { Tree, layout } 229 | -------------------------------------------------------------------------------- /src/components/mindMap/data/flextree/helper.ts: -------------------------------------------------------------------------------- 1 | import { layout, Tree } from './algorithm' 2 | 3 | interface TreeData { 4 | width: number 5 | height: number 6 | children?: TreeData[] 7 | x: number 8 | y: number 9 | } 10 | interface Box { 11 | left: number 12 | right: number 13 | top: number 14 | bottom: number 15 | } 16 | 17 | export class BoundingBox { 18 | gap: number 19 | bottomPadding: number 20 | /** 21 | * @param gap - the gap between sibling nodes 22 | * @param bottomPadding - the height reserved for connection drawing 23 | */ 24 | constructor (gap: number, bottomPadding: number) { 25 | this.gap = gap 26 | this.bottomPadding = bottomPadding 27 | } 28 | 29 | addBoundingBox (width: number, height: number): { width: number, height: number } { 30 | return { width: width + this.gap, height: height + this.bottomPadding } 31 | } 32 | 33 | /** 34 | * Return the coordinate without the bounding box for a node 35 | */ 36 | removeBoundingBox (x: number, y: number): { x: number, y: number } { 37 | return { x: x + this.gap / 2, y } 38 | } 39 | } 40 | 41 | export class Layout { 42 | bb: BoundingBox 43 | constructor (boundingBox: BoundingBox) { 44 | this.bb = boundingBox 45 | } 46 | 47 | /** 48 | * Layout treeData. 49 | * Return modified treeData and the bounding box encompassing all the nodes. 50 | * 51 | * See getSize() for more explanation. 52 | */ 53 | layout (treeData: T): { result: T, boundingBox: Box } { 54 | const tree = this.convert(treeData) 55 | layout(tree) 56 | const { boundingBox, result } = this.assignLayout(tree, treeData) 57 | 58 | return { result, boundingBox } 59 | } 60 | 61 | /** 62 | * Returns Tree to layout, with bounding boxes added to each node. 63 | */ 64 | convert (treeData: TreeData, y = 0): Tree { 65 | const { width, height } = this.bb.addBoundingBox( 66 | treeData.width, 67 | treeData.height 68 | ) 69 | const children = [] 70 | if (treeData.children && treeData.children.length) { 71 | for (let i = 0; i < treeData.children.length; i++) { 72 | children[i] = this.convert(treeData.children[i], y + height) 73 | } 74 | } 75 | 76 | return new Tree(width, height, y, children) 77 | } 78 | 79 | /** 80 | * Assign layout tree x, y coordinates back to treeData, 81 | * with bounding boxes removed. 82 | */ 83 | assignCoordinates (tree: Tree, treeData: TreeData): void { 84 | const { x, y } = this.bb.removeBoundingBox(tree.x, tree.y) 85 | treeData.x = x 86 | treeData.y = y 87 | for (let i = 0; i < tree.c.length; i++) { 88 | this.assignCoordinates(tree.c[i], (treeData.children as TreeData[])[i]) 89 | } 90 | } 91 | 92 | /** 93 | * Return the bounding box that encompasses all the nodes. 94 | * The result has a structure of 95 | * \{ left: number, right: number, top: number, bottom: nubmer \}. 96 | * This is not the same bounding box concept as the `BoundingBox` class 97 | * used to construct `Layout` class. 98 | */ 99 | getSize (treeData: TreeData, box?: Box): Box { 100 | const { x, y, width, height } = treeData 101 | if (!box) { 102 | box = { left: x, right: x + width, top: y, bottom: y + height } 103 | } 104 | box.left = Math.min(box.left, x) 105 | box.right = Math.max(box.right, x + width) 106 | box.top = Math.min(box.top, y) 107 | box.bottom = Math.max(box.bottom, y + height) 108 | 109 | if (treeData.children) { 110 | for (const child of treeData.children) { 111 | this.getSize(child, box) 112 | } 113 | } 114 | 115 | return box 116 | } 117 | 118 | /** 119 | * This function does assignCoordinates and getSize in one pass. 120 | */ 121 | assignLayout (tree: Tree, treeData: T, box?: Box): { result: T, boundingBox: Box } { 122 | const { x, y } = this.bb.removeBoundingBox(tree.x, tree.y) 123 | treeData.x = x 124 | treeData.y = y 125 | 126 | const { width, height } = treeData 127 | if (!box) { 128 | box = { left: x, right: x + width, top: y, bottom: y + height } 129 | } 130 | box.left = Math.min(box.left, x) 131 | box.right = Math.max(box.right, x + width) 132 | box.top = Math.min(box.top, y) 133 | box.bottom = Math.max(box.bottom, y + height) 134 | 135 | for (let i = 0; i < tree.c.length; i++) { 136 | this.assignLayout(tree.c[i], (treeData.children as T[])[i], box) 137 | } 138 | 139 | return { result: treeData, boundingBox: box } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/components/mindMap/data/flextree/index.ts: -------------------------------------------------------------------------------- 1 | import { layout } from './algorithm' 2 | import { BoundingBox, Layout } from './helper' 3 | 4 | export { layout, BoundingBox, Layout } 5 | -------------------------------------------------------------------------------- /src/components/mindMap/data/index.ts: -------------------------------------------------------------------------------- 1 | import emitter from '@/mitt' 2 | import { draw } from '../draw' 3 | import { Data, IsMdata } from '../interface' 4 | import { snapshot, updateTimeTravelState } from '../state' 5 | import { mmcontext } from '../variable' 6 | import ImData from './ImData' 7 | // import * as selection from '../variable/selection' 8 | export { ImData } 9 | 10 | // 思维导图数据 11 | export let mmdata: any 12 | emitter.on('mmdata', (val) => { 13 | val ? mmdata = val : null 14 | // console.log('设置数据', mmdata) 15 | }) 16 | 17 | export const afterOperation = (snap = true): void => { 18 | 19 | if (snap) { snapshot.snap(mmdata.data) } 20 | mmcontext.emit('update:modelValue', JSON.parse(JSON.stringify([mmdata.data.rawData]))) 21 | updateTimeTravelState() 22 | 23 | draw(mmdata) 24 | } 25 | export const rename = (id: string, name: string): void => { 26 | mmdata.rename(id, name) 27 | afterOperation() 28 | } 29 | export const moveChild = (pid: string, id: string): void => { 30 | mmdata.moveChild(pid, id) 31 | afterOperation() 32 | } 33 | export const moveSibling = (id: string, referenceId: string, after = 0): void => { 34 | mmdata.moveSibling(id, referenceId, after) 35 | afterOperation() 36 | } 37 | export const add = (id: string, name: string | Data): IsMdata => { 38 | const d = mmdata.add(id, name) 39 | afterOperation() 40 | return d 41 | } 42 | export const del = (id: string): void => { 43 | mmdata.delete(id) 44 | afterOperation() 45 | } 46 | export const delOne = (id: string): void => { 47 | mmdata.deleteOne(id) 48 | afterOperation() 49 | } 50 | export const expand = (id: string): void => { 51 | mmdata.expand(id) 52 | afterOperation() 53 | } 54 | export const collapse = (id: string): void => { 55 | mmdata.collapse(id) 56 | afterOperation() 57 | } 58 | export const addSibling = (id: string, name: string, before = false): IsMdata => { 59 | const d = mmdata.addSibling(id, name, before) 60 | afterOperation() 61 | return d 62 | } 63 | export const addParent = (id: string, name: string): IsMdata => { 64 | const d = mmdata.addParent(id, name) 65 | afterOperation() 66 | return d 67 | } 68 | export const changeLeft = (id: string, left: boolean): void => { 69 | mmdata.changeLeft(id, left) 70 | afterOperation() 71 | } -------------------------------------------------------------------------------- /src/components/mindMap/draw/index.ts: -------------------------------------------------------------------------------- 1 | import { TspanData, Mdata, SelectionG, IsMdata } from '@/components/mindMap/interface' 2 | import * as d3 from '../d3' 3 | import { 4 | attrA, attrAddBtnRect, attrExpandBtnCircle, attrExpandBtnRect, 5 | attrG, attrPath, attrText, attrTspan, getSiblingGClass, getTspanData 6 | } from '../attribute' 7 | import { getAddPath, makeTransition } from '../assistant' 8 | import { addBtnRect, addNodeBtn, drag, mmprops, selection } from '../variable/index' 9 | 10 | import { addAndEdit, onClickExpandBtn, onEdit, onMouseEnter, onMouseLeave, onSelect } from '../listener' 11 | import style from '../css' 12 | 13 | export const appendTspan = ( 14 | enter: d3.Selection 15 | ): d3.Selection => { 16 | const tspan = enter.append('tspan') 17 | attrTspan(tspan) 18 | return tspan 19 | } 20 | 21 | export const updateTspan = ( 22 | update: d3.Selection 23 | ): d3.Selection => { 24 | attrTspan(update) 25 | return update 26 | } 27 | 28 | export const appendAddBtn = (g: SelectionG): d3.Selection => { 29 | const gAddBtn = g.append('g') 30 | attrAddBtnRect(gAddBtn.append('rect')) 31 | gAddBtn.append('path').attr('d', getAddPath(2, addBtnRect.side)) 32 | return gAddBtn 33 | } 34 | 35 | const appendAndBindAddBtn = (g: SelectionG) => { 36 | // const gAddBtn = appendAddBtn(g) 37 | // gAddBtn.on('click', addAndEdit) 38 | // return gAddBtn 39 | const isExpandBtn: any = g.append('g').on('click', onClickExpandBtn) 40 | isExpandBtn.attr('width', 24).attr('height', 24).attr('fill', 'none') 41 | .attr('stroke-width', 2).attr('stroke', (d: any) => { 42 | return '#979797' 43 | }) 44 | isExpandBtn.append('circle').attr('r', 10).attr('fill', '#fff') 45 | 46 | isExpandBtn.append('path').attr('d', 'M-0 -6 L-5 0 L0 5').attr('fill', 'none') 47 | 48 | return isExpandBtn 49 | } 50 | 51 | export const appendExpandBtn = (g: SelectionG): d3.Selection => { 52 | const expandBtn = g.append('g') 53 | 54 | attrExpandBtnRect(expandBtn.append('rect')) 55 | attrExpandBtnCircle(expandBtn.append('circle'), -4) 56 | attrExpandBtnCircle(expandBtn.append('circle'), 0) 57 | attrExpandBtnCircle(expandBtn.append('circle'), 4) 58 | return expandBtn 59 | } 60 | 61 | const bindEvent = (g: SelectionG, isRoot: boolean) => { 62 | const gExpandBtn = g.select(`:scope > g.${style.content} > g.${style['expand-btn']}`) 63 | gExpandBtn.on('click', onClickExpandBtn) 64 | 65 | if (mmprops.value.drag || mmprops.value.edit) { 66 | const gText: any = g.select(`:scope > g.${style.content} > g.${style.text}`) 67 | gText.on('click', onSelect) 68 | if (mmprops.value.drag && !isRoot) { drag(gText) } 69 | 70 | if (mmprops.value.edit) { gText.on('dblclick', onEdit) } 71 | } 72 | if (addNodeBtn.value) { 73 | g.select(`:scope > g.${style.content}`) 74 | .on('mouseenter', onMouseEnter) 75 | .on('mouseleave', onMouseLeave) 76 | } 77 | } 78 | 79 | const appendNode = (enter: d3.Selection) => { 80 | 81 | const isRoot = !enter.data()[0]?.depth 82 | const enterG = enter.append('g') 83 | attrG(enterG) 84 | // 绘制线 85 | attrPath(enterG.append('path')) 86 | // 节点容器 87 | const gContent = enterG.append('g').attr('class', style.content) 88 | const gTrigger = gContent.append('rect') 89 | // 绘制文本 90 | const gText = gContent.append('g').attr('class', (d) => { 91 | // console.log('d', d) 92 | let result = style.text 93 | if (d.depth === 0) { 94 | result += ' ' + 'root_rect' 95 | } 96 | return result 97 | }) 98 | const gTextRect = gText.append('rect') 99 | const text = gText.append('text') 100 | attrText(text) 101 | const tspan = text.selectAll('tspan').data(getTspanData).enter().append('tspan') 102 | attrTspan(tspan) 103 | // 绘制添加按钮 104 | let gAddBtn 105 | if (addNodeBtn.value) { gAddBtn = appendAndBindAddBtn(gContent) } 106 | // 绘制折叠按钮 107 | const gExpandBtn = appendExpandBtn(gContent) 108 | 109 | attrA(isRoot, gTrigger, gTextRect, gExpandBtn, gAddBtn) 110 | 111 | bindEvent(enterG, isRoot) 112 | 113 | enterG.each((d, i) => { 114 | if (!d.children) { return } 115 | draw(d.children, enterG.filter((a, b) => i === b)) 116 | }) 117 | gContent.raise() 118 | return enterG 119 | } 120 | 121 | const updateNode = (update: SelectionG) => { 122 | const isRoot = !update.data()[0]?.depth 123 | const tran = makeTransition(500, d3.easePolyOut) 124 | attrG(update, tran) 125 | attrPath(update.select(':scope > path'), tran) 126 | const gContent = update.select(`:scope > g.${style.content}`) 127 | const gTrigger = gContent.select(':scope > rect') 128 | const gText = gContent.select(`g.${style.text}`) 129 | const gTextRect = gText.select('rect') 130 | const text = gText.select('text') 131 | attrText(text, tran) 132 | text.selectAll('tspan') 133 | .data(getTspanData) 134 | .join(appendTspan, updateTspan, exit => exit.remove()) 135 | let gAddBtn = gContent.select(`g.${style['add-btn']}`) 136 | const gExpandBtn = gContent.select(`g.${style['expand-btn']}`) 137 | // console.log('gAddBtn.node()', gAddBtn.node()) 138 | if (addNodeBtn.value) { 139 | if (!gAddBtn.node()) { gAddBtn = appendAndBindAddBtn(gContent) } 140 | } else { 141 | gAddBtn.remove() 142 | } 143 | 144 | attrA(isRoot, gTrigger, gTextRect, gExpandBtn, gAddBtn) 145 | 146 | update.each((d, i) => { 147 | if (!d.children) { return } 148 | draw(d.children, update.filter((a, b) => i === b)) 149 | }) 150 | gContent.raise() 151 | return update 152 | } 153 | 154 | export const draw = (d: any, sele = selection.g as any): void => { 155 | if (d.data) { 156 | d = [d.data] 157 | } 158 | const temp = sele.selectAll(`g.${getSiblingGClass(d[0]).join('.')}`) 159 | temp.data(d, (d: { gKey: any }) => { 160 | return d.gKey 161 | }).join(appendNode, updateNode) 162 | } -------------------------------------------------------------------------------- /src/components/mindMap/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 115 | 116 | -------------------------------------------------------------------------------- /src/components/mindMap/interface.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from './d3' 2 | 3 | export interface Data { 4 | name: string 5 | children?: Array 6 | left?: boolean 7 | isExpand?: boolean, 8 | id?: string, 9 | level?: number, 10 | } 11 | 12 | export interface TreeData { 13 | rawData: Data 14 | width: number 15 | height: number 16 | x: number 17 | y: number 18 | children: TreeData[] 19 | _children: TreeData[] 20 | left: boolean, 21 | isExpand: boolean 22 | } 23 | 24 | export interface Mdata { 25 | level?: number, 26 | rawData: Data 27 | name: string 28 | parent: IsMdata 29 | children: Array 30 | _children: Array // 当折叠时保存children数据 31 | left: boolean 32 | isExpand: boolean 33 | id: string // 代表着数据的顺序和嵌套层次 34 | color: string 35 | gKey: number 36 | width: number 37 | height: number 38 | depth: number 39 | x: number 40 | y: number 41 | dx: number 42 | dy: number 43 | // 拖拽时的偏移量 44 | px: number 45 | py: number 46 | } 47 | 48 | export interface TspanData { 49 | name: string, 50 | height: number 51 | } 52 | 53 | export interface ScaleData { 54 | k?: number, 55 | x?: number, 56 | y?: number 57 | } 58 | 59 | export type Transition = d3.Transition 60 | export type SelectionG = d3.Selection 61 | export type SelectionRect = d3.Selection 62 | export type SelectionCircle = d3.Selection 63 | export type TwoNumber = [number, number] 64 | export type IsMdata = Mdata | null 65 | export type Locale = 'zh' | 'en' | 'ptBR' -------------------------------------------------------------------------------- /src/components/mindMap/listener/index.ts: -------------------------------------------------------------------------------- 1 | export * from './switcher' 2 | export * from './listener' -------------------------------------------------------------------------------- /src/components/mindMap/listener/listener.ts: -------------------------------------------------------------------------------- 1 | import style from '../css' 2 | import { ctm, zoom, selection, textRectPadding, zoomTransform } from '../variable/index' 3 | import * as d3 from '../d3' 4 | import { Mdata, ScaleData } from '../interface' 5 | import { fitView, getRelativePos, getSelectedGData, isData, moveNode, moveView, scaleView, selectGNode } from '../assistant' 6 | import { 7 | add, addParent, addSibling, changeLeft, collapse, del, 8 | delOne, expand, mmdata, moveChild, moveSibling, rename 9 | } from '../data/index' 10 | import { svgEle, gEle, foreignDivEle, wrapperEle, foreignEle } from '../variable/element' 11 | import emitter from '@/mitt' 12 | import { getDataId, getSiblingGClass } from '../attribute' 13 | import { MenuEvent } from '../variable/contextmenu' 14 | import { ref, Ref } from 'vue' 15 | /** 16 | * @param this - gContent 17 | */ 18 | export function onMouseEnter(this: SVGGElement): void { 19 | const temp = this.querySelector(`g.${style['add-btn']}`) 20 | 21 | if (temp) { temp.style.opacity = '1' } 22 | } 23 | 24 | /** 25 | * @param this - gContent 26 | */ 27 | export function onMouseLeave(this: SVGGElement): void { 28 | const temp = this.querySelector(`g.${style['add-btn']}`) 29 | if (temp) { temp.style.opacity = '0' } 30 | } 31 | 32 | export const scaleData: Ref = ref({ x: 0, y: 0, k: 0 }) 33 | export const onZoomMove = (e: any): void => { 34 | 35 | const { g, svg } = selection 36 | if (!g || !e) { return } 37 | let data = e.transform || {} 38 | if (e.k) { 39 | data = e 40 | svg.transition().call(zoom.transform, d3.zoomIdentity.translate(e.x, e.y).scale(e.k)); 41 | } else { 42 | g.attr('transform', data.toString()) 43 | } 44 | zoomTransform.value = data 45 | scaleData.value = data 46 | } 47 | 48 | export const onSelect = (e: MouseEvent, d: Mdata): void => { 49 | // debugger 50 | e.stopPropagation() 51 | selectGNode(d) 52 | } 53 | 54 | /** 55 | * 进入编辑状态 56 | * @param this - gText 57 | */ 58 | export function onEdit(this: SVGGElement, _e: MouseEvent, d: Mdata): void { 59 | 60 | const gNode = this.parentNode?.parentNode as SVGGElement 61 | const { foreign } = selection 62 | // console.log('editFlag', editFlag) 63 | // console.log('foreignDivEle.value', foreignDivEle.value) 64 | // console.log('foreign', foreign) 65 | if (foreign && foreignDivEle.value) { 66 | 67 | gNode.classList.add(style.edited) 68 | emitter.emit('edit-flag', false) 69 | foreign.attr('x', d.x - 2 - (d.left ? d.width : 0)) 70 | .attr('y', d.y - mmdata.data.y - 2) 71 | .attr('data-id', d.id) 72 | .attr('data-name', d.name) 73 | .style('display', '') 74 | const div = foreignDivEle.value 75 | div.textContent = d.name 76 | div.focus() 77 | getSelection()?.selectAllChildren(div) 78 | const gContent = gNode.querySelector(`:scope > .${style.content}`) 79 | if (gContent) { 80 | moveView(gContent) 81 | } 82 | 83 | 84 | 85 | } 86 | } 87 | 88 | export const onEditBlur = (): void => { 89 | document.getElementsByClassName(style.edited)[0]?.classList.remove(style.edited, style.selected) 90 | 91 | if (foreignEle.value && foreignDivEle.value) { 92 | console.log('foreignEle', foreignEle) 93 | foreignEle.value.style.display = 'none' 94 | const id = foreignEle.value.getAttribute('data-id') 95 | const oldname = foreignEle.value.getAttribute('data-name') 96 | const name = foreignDivEle.value.textContent 97 | if (id && name !== null && name !== oldname) { 98 | rename(id, name) 99 | } 100 | } 101 | } 102 | 103 | export const onContextmenu = (e: MouseEvent): void => { 104 | 105 | e.preventDefault() 106 | if (!wrapperEle.value) { return } 107 | const relativePos = getRelativePos(wrapperEle.value, e) 108 | ctm.pos.value = relativePos 109 | const eventTargets = e.composedPath() as SVGElement[] 110 | const gNode: any = eventTargets.find((et) => et.classList?.contains('node')) 111 | if (gNode) { 112 | const { classList } = gNode 113 | const isRoot = classList.contains(style.root) 114 | const collapseFlag = classList.contains(style['isExpand']) 115 | if (!classList.contains(style.selected)) { selectGNode(gNode as SVGGElement) } 116 | ctm.deleteItem.value.disabled = isRoot 117 | ctm.cutItem.value.disabled = isRoot 118 | ctm.deleteOneItem.value.disabled = isRoot 119 | ctm.addSiblingItem.value.disabled = isRoot 120 | ctm.addSiblingBeforeItem.value.disabled = isRoot 121 | ctm.addParentItem.value.disabled = isRoot 122 | // ctm.expandItem.value.disabled = !collapseFlag 123 | ctm.collapseItem.value.disabled = collapseFlag || classList.contains('leaf') 124 | ctm.showViewMenu.value = false 125 | } else { 126 | ctm.showViewMenu.value = true 127 | } 128 | emitter.emit('showContextmenu', true) 129 | } 130 | 131 | export const onClickMenu = (name: MenuEvent): void => { 132 | 133 | switch (name) { 134 | case 'zoomfit': fitView(scaleData); break 135 | case 'zoomin': scaleView(true); break 136 | case 'zoomout': scaleView(true); break 137 | case 'add': addAndEdit(new MouseEvent('click'), getSelectedGData()); break 138 | case 'delete': del(getSelectedGData().id); break 139 | case 'delete-one': delOne(getSelectedGData().id); break 140 | case 'isExpand': collapse(getSelectedGData().id); break 141 | case 'expand': expand(getSelectedGData().id); break 142 | case 'add-sibling': { 143 | const seleData = getSelectedGData() 144 | const d = addSibling(seleData.id, '') 145 | if (d) { edit(d) } 146 | } break 147 | case 'add-sibling-before': { 148 | const seleData = getSelectedGData() 149 | const d = addSibling(seleData.id, '', true) 150 | if (d) { edit(d) } 151 | } break 152 | case 'add-parent': { 153 | const seleData = getSelectedGData() 154 | const d = addParent(seleData.id, '') 155 | if (d) { edit(d) } 156 | } break 157 | case 'cut': { 158 | const { id } = getSelectedGData() 159 | const rawdata = mmdata.find(id)?.rawData 160 | if (rawdata) { 161 | // navigator.clipboard.write 162 | navigator.clipboard.writeText(JSON.stringify(rawdata)) 163 | } 164 | del(id) 165 | } break 166 | case 'copy': { 167 | const seleData = getSelectedGData() 168 | const rawdata = mmdata.find(seleData.id)?.rawData 169 | if (rawdata) { 170 | // navigator.clipboard.write 171 | navigator.clipboard.writeText(JSON.stringify(rawdata)) 172 | } 173 | } break 174 | case 'paste': { 175 | const seleData = getSelectedGData() 176 | navigator.clipboard.readText().then(clipText => { 177 | const rawdata = isData(clipText) || { name: clipText } 178 | add(seleData.id, rawdata) 179 | }) 180 | } break 181 | default: break 182 | } 183 | } 184 | 185 | /** 186 | * 添加子节点并进入编辑模式 187 | */ 188 | export const addAndEdit = (e: MouseEvent, d: Mdata): void => { 189 | 190 | const child = add(d.id, '') 191 | if (child) { edit(child, e) } 192 | } 193 | 194 | /** 195 | * 选中节点进入编辑模式 196 | */ 197 | export function edit(d: Mdata, e = new MouseEvent('click')): void { 198 | 199 | const { g } = selection 200 | if (!g) { return } 201 | const gText = g.selectAll(`g[data-id='${getDataId(d)}'] > g.${style.content} > g.${style.text}`) 202 | const node = gText.node() 203 | 204 | if (node) { 205 | emitter.emit('edit-flag', true) 206 | onEdit.call(node, e, d) 207 | } 208 | } 209 | 210 | export const onClickExpandBtn = (e: MouseEvent, d: any): void => { 211 | expand(d) 212 | } 213 | 214 | /** 215 | * @param this - gText 216 | */ 217 | export function onDragMove(this: SVGGElement, e: d3.D3DragEvent, d: Mdata): void { 218 | const gNode = this.parentNode?.parentNode as SVGGElement 219 | if (svgEle.value) { svgEle.value.classList.add(style.dragging) } 220 | const { g } = selection 221 | if (!g) { return } 222 | moveNode(gNode, d, [e.x - d.x, e.y - d.y]) 223 | // 鼠标相对gEle左上角的位置 224 | const mousePos = d3.pointer(e, gEle.value) 225 | mousePos[1] += mmdata.data.y 226 | 227 | const temp = g.selectAll('g.node').filter((other: any) => { 228 | if (other !== d && other !== d.parent && !other.id.startsWith(d.id)) { 229 | let diffx0 = textRectPadding 230 | let diffx1 = other.width + textRectPadding 231 | if (other.left && other.depth !== 0) { 232 | [diffx0, diffx1] = [diffx1, diffx0] 233 | } 234 | const rect = { 235 | x0: other.x - diffx0, 236 | x1: other.x + diffx1, 237 | y0: other.y - textRectPadding, 238 | y1: other.y + other.height + textRectPadding 239 | } 240 | 241 | return mousePos[0] > rect.x0 && mousePos[1] > rect.y0 && mousePos[0] < rect.x1 && mousePos[1] < rect.y1 242 | } 243 | return false 244 | }) 245 | const old = Array.from(document.getElementsByClassName(style.outline)) 246 | const n = temp.node() 247 | old.forEach((o) => { if (o !== n) { o.classList.remove(style.outline) } }) 248 | n?.classList.add(style.outline) 249 | } 250 | 251 | /** 252 | * @param this - gText 253 | */ 254 | export function onDragEnd(this: SVGGElement, e: d3.D3DragEvent, d: Mdata): void { 255 | 256 | const gNode = this.parentNode?.parentNode as SVGGElement 257 | if (svgEle.value) { svgEle.value.classList.remove(style.dragging) } 258 | // 判断是否找到了新的父节点 259 | const np = document.getElementsByClassName(style.outline)[0] 260 | if (np) { 261 | np.classList.remove(style.outline) 262 | const pid = np.getAttribute('data-id') 263 | if (pid) { 264 | d.px = 0 265 | d.py = 0 266 | moveChild(pid, d.id) 267 | } else { 268 | throw new Error('outline data-id null') 269 | } 270 | return 271 | } 272 | 273 | // 判断是否变换left 274 | const xToCenter = d.x - mmdata.getRootWidth() / 2 275 | const lr = d.depth === 1 && (xToCenter * (xToCenter + d.px) < 0) 276 | const getSameSide = lr ? (a: Mdata) => a.left !== d.left : (a: Mdata) => a.left === d.left 277 | // 判断是否需要调换节点顺序 278 | const p = gNode.parentNode as SVGGElement 279 | let downD = lr ? { y: Infinity, id: d.id } : d 280 | let upD = lr ? { y: -Infinity, id: d.id } : d 281 | const brothers = d3.select(p) 282 | .selectAll(`g.${getSiblingGClass(d).join('.')}`) 283 | .filter((a) => a !== d && getSameSide(a)) 284 | const endY = d.y + d.py 285 | brothers.each((b) => { 286 | if ((lr || b.y > d.y) && b.y < endY && b.y > upD.y) { upD = b } // 找新哥哥节点 287 | if ((lr || b.y < d.y) && b.y > endY && b.y < downD.y) { downD = b } // 找新弟弟节点 288 | }) 289 | 290 | if (downD.id !== d.id) { 291 | d.px = 0 292 | d.py = 0 293 | moveSibling(d.id, downD.id) 294 | 295 | } else if (upD.id !== d.id) { 296 | d.px = 0 297 | d.py = 0 298 | moveSibling(d.id, upD.id, 1) 299 | } else if (lr) { 300 | d.px = 0 301 | d.py = 0 302 | changeLeft(d.id, !d.left) 303 | } else { 304 | // 复原 305 | moveNode(gNode, d, [0, 0], 500) 306 | } 307 | 308 | 309 | } -------------------------------------------------------------------------------- /src/components/mindMap/listener/switcher.ts: -------------------------------------------------------------------------------- 1 | import style from '../css' 2 | // import { Mdata } from '../interface' 3 | import { onContextmenu, onEdit, onSelect } from './listener' 4 | import { selection, zoom, drag } from '../variable' 5 | import { foreignDivEle, wrapperEle } from '../variable/element' 6 | 7 | export const switchZoom = (zoomable: boolean): void => { 8 | 9 | const svg: any = selection.svg 10 | if (!svg) { return } 11 | if (zoomable) { 12 | zoom(svg) 13 | svg.on('dblclick.zoom', null) 14 | } else { 15 | svg.on('.zoom', null) 16 | } 17 | } 18 | 19 | export const switchEdit = (editable: boolean): void => { 20 | const { g } = selection 21 | if (!foreignDivEle.value || !g) { return } 22 | const gText = g.selectAll(`g.${style.text}`) 23 | if (editable) { 24 | gText.on('click', onEdit) 25 | } else { 26 | gText.on('click', null) 27 | } 28 | } 29 | 30 | export const switchSelect = (selectable: boolean): void => { 31 | const { g } = selection 32 | if (!g) { return } 33 | const gText = g.selectAll(`g.${style.text}`) 34 | if (selectable) { 35 | gText.on('mousedown', onSelect) 36 | } else { 37 | gText.on('mousedown', null) 38 | } 39 | } 40 | 41 | export const switchContextmenu = (val: boolean): void => { 42 | 43 | if (!wrapperEle.value) { return } 44 | if (val) { 45 | wrapperEle.value.addEventListener('contextmenu', onContextmenu) 46 | } else { 47 | wrapperEle.value.removeEventListener('contextmenu', onContextmenu) 48 | } 49 | } 50 | 51 | export const switchDrag = (draggable: boolean): void => { 52 | const { g } = selection 53 | if (!g) { return } 54 | const gText: any = g.selectAll(`g.node:not(.${style.root}) > g > g.${style.text}`) 55 | if (draggable) { 56 | drag(gText) 57 | } else { 58 | gText.on('.drag', null) 59 | } 60 | } -------------------------------------------------------------------------------- /src/components/mindMap/mind_map.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 284 | -------------------------------------------------------------------------------- /src/components/mindMap/state/Snapshot.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash' 2 | 3 | export default class Snapshot { 4 | private length: number // 最大记录数 5 | private snapshots: Array 6 | private cursor: number 7 | 8 | constructor(length = 20) { 9 | this.length = length 10 | this.snapshots = [] 11 | this.cursor = -1 12 | } 13 | 14 | get hasPrev(): boolean { return this.cursor > 0 } 15 | get hasNext(): boolean { return this.snapshots.length > this.cursor + 1 } 16 | 17 | snap(data: T): void { // 记录数据快照 18 | 19 | const snapshot = cloneDeep(data) 20 | // 去除旧分支 21 | while (this.cursor < this.snapshots.length - 1) { this.snapshots.pop() } 22 | this.snapshots.push(snapshot) 23 | // 确保历史记录条数限制 24 | if (this.snapshots.length > this.length) { this.snapshots.shift() } 25 | this.cursor = this.snapshots.length - 1 26 | } 27 | 28 | prev(): T | null { 29 | if (this.hasPrev) { 30 | this.cursor -= 1 31 | return cloneDeep(this.snapshots[this.cursor]) 32 | } 33 | return null 34 | } 35 | 36 | next(): T | null { 37 | if (this.hasNext) { 38 | this.cursor += 1 39 | return cloneDeep(this.snapshots[this.cursor]) 40 | } 41 | return null 42 | } 43 | } -------------------------------------------------------------------------------- /src/components/mindMap/state/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { Mdata } from '../interface' 3 | import Snapshot from './Snapshot' 4 | 5 | export const snapshot = new Snapshot() 6 | 7 | // 时间旅行状态 8 | export const hasPrev = ref(false) 9 | export const hasNext = ref(false) 10 | 11 | export const updateTimeTravelState = (): void => { 12 | hasPrev.value = snapshot.hasPrev 13 | hasNext.value = snapshot.hasNext 14 | } 15 | -------------------------------------------------------------------------------- /src/components/mindMap/variable/contextmenu.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, Ref } from 'vue' 2 | import { mmprops, scaleExtent, zoomTransform } from '.' 3 | 4 | export type MenuEvent = 'zoomin' | 'zoomout' | 'zoomfit' | 'add' | 'delete' | 5 | 'selectall' | 'isExpand' | 'expand' | 'add-sibling' | 'add-sibling-before' | 6 | 'add-parent' | 'copy' | 'paste' | 'cut' | 'delete-one' 7 | export interface MenuItem { 8 | name: string 9 | disabled: boolean 10 | } 11 | export const showViewMenu = ref(true) 12 | export const pos = ref({ left: 0, top: 0 }) 13 | export const collapseItem: Ref = ref({ name: 'isExpand', disabled: true }) 14 | // export const expandItem: Ref = ref({ name: 'expand', disabled: true }) 15 | export const deleteItem: Ref = ref({ name: 'delete', disabled: false }) 16 | export const addItem: Ref = ref({ name: 'add', disabled: false }) 17 | export const addParentItem: Ref = ref({ name: 'add-parent', disabled: false }) 18 | export const addSiblingItem: Ref = ref({ name: 'add-sibling', disabled: false }) 19 | export const addSiblingBeforeItem: Ref = ref({ name: 'add-sibling-before', disabled: true }) 20 | export const cutItem: Ref = ref({ name: 'cut', disabled: false }) 21 | export const copyItem: Ref = ref({ name: 'copy', disabled: false }) 22 | export const pasteItem: Ref = ref({ name: 'paste', disabled: false }) 23 | export const deleteOneItem: Ref = ref({ name: 'delete-one', disabled: false }) 24 | 25 | 26 | const nodeMenu = computed(() => [ 27 | [addItem.value, addParentItem.value, addSiblingItem.value, addSiblingBeforeItem.value], 28 | [cutItem.value, copyItem.value, pasteItem.value, deleteItem.value, deleteOneItem.value], 29 | [{ name: 'selectall', disabled: true }], 30 | [collapseItem.value] 31 | ].filter((item, index) => { 32 | if (index === 0 || index === 1) { 33 | return mmprops.value.edit 34 | } else { 35 | return true 36 | } 37 | })) 38 | 39 | const viewMenu = computed(() => [ 40 | [ 41 | { 42 | name: 'zoomin', 43 | disabled: zoomTransform.value.k >= scaleExtent[1] 44 | }, 45 | { 46 | name: 'zoomout', 47 | disabled: zoomTransform.value.k <= scaleExtent[0] 48 | }, 49 | { name: 'zoomfit', disabled: false } 50 | ], 51 | [ 52 | { name: 'selectall', disabled: true } 53 | ] 54 | ]) 55 | 56 | export const menu = computed(() => showViewMenu.value ? viewMenu.value : nodeMenu.value) -------------------------------------------------------------------------------- /src/components/mindMap/variable/element.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref } from 'vue' 2 | 3 | export const wrapperEle: Ref = ref() 4 | export const svgEle: Ref = ref() 5 | export const gEle: Ref = ref() 6 | export const asstSvgEle: Ref = ref() 7 | export const foreignEle: Ref = ref() 8 | export const foreignDivEle: Ref = ref() -------------------------------------------------------------------------------- /src/components/mindMap/variable/index.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from '../d3' 2 | import { Mdata, TwoNumber } from '../interface' 3 | import emitter from '@/mitt' 4 | import { EmitsOptions, Ref, ref, SetupContext } from 'vue' 5 | import { onDragEnd, onDragMove, onZoomMove } from '../listener' 6 | import * as selection from './selection' 7 | import * as element from './element' 8 | import { getDragContainer, moveView } from '../assistant' 9 | 10 | export * as ctm from './contextmenu' 11 | export { selection, element, onZoomMove } 12 | 13 | // 连线样式 14 | type CurveStepLink = ({ source, target }: { source: TwoNumber, target: TwoNumber }) => string | null 15 | type Link = d3.Link | CurveStepLink 16 | 17 | const linkHorizontal = d3.linkHorizontal().source((d) => d.source).target((d) => d.target) 18 | const curveStepLine = d3.line().curve(d3.curveStep) 19 | 20 | export const curveStepLink: CurveStepLink = ({ source, target }) => curveStepLine([source, target]) 21 | export let sharpCorner = false 22 | export let link: Link = linkHorizontal 23 | export const changeSharpCorner = ref(false) // 指示是否需要使用attrTween('d', pathTween) 24 | emitter.on('sharp-corner', (value: boolean) => { 25 | if (sharpCorner !== value) { changeSharpCorner.value = true } 26 | sharpCorner = !!value 27 | link = value ? curveStepLink : linkHorizontal 28 | }) 29 | 30 | // 连线宽度 31 | export let branch = 4 32 | emitter.on('branch', (value: number) => branch = value || branch) 33 | 34 | // 缩放程度 35 | export let scaleExtent: TwoNumber = [0.1, 8] 36 | emitter.on('scale-extent', (value: TwoNumber) => { 37 | return scaleExtent = value || scaleExtent 38 | }) 39 | 40 | // 可编辑指示 41 | export let editFlag = true 42 | emitter.on('edit-flag', (val: any) => { 43 | editFlag = !!val 44 | console.log('editFlag', editFlag) 45 | }) 46 | 47 | // 节点边距与间隔 48 | export const rootTextRectRadius = 6 49 | export const rootTextRectPadding = 10 50 | export let yGap = 18 51 | export let xGap = 84 52 | export let textRectPadding = Math.min(yGap / 2 - 1, rootTextRectPadding) 53 | emitter.on('gap', (gap: { xGap: number; yGap: number }) => { 54 | if (!gap) { return } 55 | xGap = gap.xGap 56 | yGap = gap.yGap 57 | textRectPadding = Math.min(yGap / 2 - 1, rootTextRectPadding) 58 | textRectPadding = Math.min(xGap / 2 - 1, textRectPadding) 59 | }) 60 | 61 | /** 62 | * 观察foreignDiv,改变foreignObject的宽度和高度,并使其保持可见 63 | */ 64 | export const observer = new ResizeObserver((arr: ResizeObserverEntry[]) => { 65 | const { foreign } = selection 66 | if (!foreign) { return } 67 | const temp = arr[0] 68 | const foreignDiv = temp.target 69 | const { width, height } = temp.contentRect 70 | const pl = parseInt(getComputedStyle(foreignDiv).paddingLeft || '0', 10) 71 | const b = parseInt(getComputedStyle(foreignDiv.parentNode as Element).borderTopWidth || '0', 10) 72 | const gap = (pl + b) * 2 73 | foreign.attr('width', width + gap).attr('height', height + gap) 74 | if (foreign.style('display') !== 'none') { 75 | moveView(foreignDiv) 76 | } 77 | }) 78 | 79 | // 其他 80 | export const addBtnRect = { side: 12, padding: 2, margin: 8 } 81 | export const addBtnSide = addBtnRect.side + addBtnRect.padding * 2 82 | export const expandBtnRect = { width: 16, height: 4, radius: 2 } 83 | export const zoomTransform: Ref = ref(d3.zoomIdentity) 84 | export const zoom = d3.zoom().on('zoom', onZoomMove).scaleExtent(scaleExtent) 85 | export const drag = d3.drag().container(getDragContainer).on('drag', onDragMove).on('end', onDragEnd) 86 | export const addNodeBtn = ref(false) 87 | export let mmcontext: SetupContext 88 | emitter.on('mindmap-context', (val: SetupContext) => val ? mmcontext = val : null) 89 | export const mmprops = ref({ 90 | drag: false, 91 | edit: false 92 | }) -------------------------------------------------------------------------------- /src/components/mindMap/variable/selection.ts: -------------------------------------------------------------------------------- 1 | import emitter from '@/mitt' 2 | 3 | // import * as d3 from '../d3' 4 | 5 | export let svg: any 6 | export let g: any 7 | export let asstSvg: any 8 | export let foreign: any 9 | 10 | emitter.on('selection-svg', (val) => svg = val) 11 | emitter.on('selection-g', (val) => { 12 | g = val 13 | }) 14 | emitter.on('selection-asstSvg', (val) => asstSvg = val) 15 | emitter.on('selection-foreign', (val) => foreign = val) 16 | -------------------------------------------------------------------------------- /src/components/newTree/model/emitter.js: -------------------------------------------------------------------------------- 1 | function broadcast(componentName, eventName, params) { 2 | console.log('this.$children', this.$children) 3 | // if (!this.$children) return 4 | this.$children.forEach(child => { 5 | var name = child.$options.componentName; 6 | 7 | if (name === componentName) { 8 | child.$emit.apply(child, [eventName].concat(params)); 9 | } else { 10 | broadcast.apply(child, [componentName, eventName].concat([params])); 11 | } 12 | }); 13 | } 14 | export default { 15 | methods: { 16 | dispatch(componentName, eventName, params) { 17 | var parent = this.$parent || this.$root; 18 | var name = parent.$options.componentName; 19 | 20 | while (parent && (!name || name !== componentName)) { 21 | parent = parent.$parent; 22 | 23 | if (parent) { 24 | name = parent.$options.componentName; 25 | } 26 | } 27 | if (parent) { 28 | parent.$emit.apply(parent, [eventName].concat(params)); 29 | } 30 | }, 31 | broadcast(componentName, eventName, params) { 32 | broadcast.call(this, componentName, eventName, params); 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/newTree/model/tree-store.js: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | import { getNodeKey } from './util'; 3 | 4 | export default class TreeStore { 5 | constructor(options) { 6 | this.currentNode = null; 7 | this.currentNodeKey = null; 8 | 9 | for (let option in options) { 10 | // options.hasOwnProperty(option) 11 | 12 | if (Object.prototype.hasOwnProperty.call(options, option)) { 13 | this[option] = options[option]; 14 | } 15 | } 16 | 17 | this.nodesMap = {}; 18 | 19 | this.root = new Node({ 20 | data: this.data, 21 | store: this 22 | }); 23 | 24 | if (this.lazy && this.load) { 25 | const loadFn = this.load; 26 | loadFn(this.root, (data) => { 27 | this.root.doCreateChildren(data); 28 | this._initDefaultCheckedNodes(); 29 | }); 30 | } else { 31 | this._initDefaultCheckedNodes(); 32 | } 33 | } 34 | 35 | filter(value) { 36 | const filterNodeMethod = this.filterNodeMethod; 37 | const lazy = this.lazy; 38 | const traverse = function (node) { 39 | const childNodes = node.root ? node.root.childNodes : node.childNodes; 40 | 41 | childNodes.forEach((child) => { 42 | child.visible = filterNodeMethod.call(child, value, child.data, child); 43 | 44 | traverse(child); 45 | }); 46 | 47 | if (!node.visible && childNodes.length) { 48 | let allHidden = true; 49 | allHidden = !childNodes.some(child => child.visible); 50 | 51 | if (node.root) { 52 | node.root.visible = allHidden === false; 53 | } else { 54 | node.visible = allHidden === false; 55 | } 56 | } 57 | if (!value) return; 58 | 59 | if (node.visible && !node.isLeaf && !lazy) node.expand(); 60 | }; 61 | 62 | traverse(this); 63 | } 64 | 65 | setData(newVal) { 66 | const instanceChanged = newVal !== this.root.data; 67 | if (instanceChanged) { 68 | this.root.setData(newVal); 69 | this._initDefaultCheckedNodes(); 70 | } else { 71 | this.root.updateChildren(); 72 | } 73 | } 74 | 75 | getNode(data) { 76 | if (data instanceof Node) return data; 77 | const key = typeof data !== 'object' ? data : getNodeKey(this.key, data); 78 | return this.nodesMap[key] || null; 79 | } 80 | 81 | insertBefore(data, refData) { 82 | const refNode = this.getNode(refData); 83 | refNode.parent.insertBefore({ data }, refNode); 84 | } 85 | 86 | insertAfter(data, refData) { 87 | const refNode = this.getNode(refData); 88 | refNode.parent.insertAfter({ data }, refNode); 89 | } 90 | 91 | remove(data) { 92 | const node = this.getNode(data); 93 | 94 | if (node && node.parent) { 95 | if (node === this.currentNode) { 96 | this.currentNode = null; 97 | } 98 | node.parent.removeChild(node); 99 | } 100 | } 101 | 102 | append(data, parentData) { 103 | const parentNode = parentData ? this.getNode(parentData) : this.root; 104 | 105 | if (parentNode) { 106 | parentNode.insertChild({ data }); 107 | } 108 | } 109 | 110 | _initDefaultCheckedNodes() { 111 | const defaultCheckedKeys = this.defaultCheckedKeys || []; 112 | const nodesMap = this.nodesMap; 113 | 114 | defaultCheckedKeys.forEach((checkedKey) => { 115 | const node = nodesMap[checkedKey]; 116 | 117 | if (node) { 118 | node.setChecked(true, !this.checkStrictly); 119 | } 120 | }); 121 | } 122 | 123 | _initDefaultCheckedNode(node) { 124 | const defaultCheckedKeys = this.defaultCheckedKeys || []; 125 | 126 | if (defaultCheckedKeys.indexOf(node.key) !== -1) { 127 | node.setChecked(true, !this.checkStrictly); 128 | } 129 | } 130 | 131 | setDefaultCheckedKey(newVal) { 132 | if (newVal !== this.defaultCheckedKeys) { 133 | this.defaultCheckedKeys = newVal; 134 | this._initDefaultCheckedNodes(); 135 | } 136 | } 137 | 138 | registerNode(node) { 139 | const key = this.key; 140 | if (!key || !node || !node.data) return; 141 | 142 | const nodeKey = node.key; 143 | if (nodeKey !== undefined) this.nodesMap[node.key] = node; 144 | } 145 | 146 | deregisterNode(node) { 147 | const key = this.key; 148 | if (!key || !node || !node.data) return; 149 | 150 | node.childNodes.forEach(child => { 151 | this.deregisterNode(child); 152 | }); 153 | 154 | delete this.nodesMap[node.key]; 155 | } 156 | 157 | getCheckedNodes(leafOnly = false, includeHalfChecked = false) { 158 | const checkedNodes = []; 159 | const traverse = function (node) { 160 | const childNodes = node.root ? node.root.childNodes : node.childNodes; 161 | 162 | childNodes.forEach((child) => { 163 | if ((child.checked || (includeHalfChecked && child.indeterminate)) && (!leafOnly || (leafOnly && child.isLeaf))) { 164 | checkedNodes.push(child.data); 165 | } 166 | 167 | traverse(child); 168 | }); 169 | }; 170 | 171 | traverse(this); 172 | 173 | return checkedNodes; 174 | } 175 | 176 | getCheckedKeys(leafOnly = false) { 177 | return this.getCheckedNodes(leafOnly).map((data) => (data || {})[this.key]); 178 | } 179 | 180 | getHalfCheckedNodes() { 181 | const nodes = []; 182 | const traverse = function (node) { 183 | const childNodes = node.root ? node.root.childNodes : node.childNodes; 184 | 185 | childNodes.forEach((child) => { 186 | if (child.indeterminate) { 187 | nodes.push(child.data); 188 | } 189 | 190 | traverse(child); 191 | }); 192 | }; 193 | 194 | traverse(this); 195 | 196 | return nodes; 197 | } 198 | 199 | getHalfCheckedKeys() { 200 | return this.getHalfCheckedNodes().map((data) => (data || {})[this.key]); 201 | } 202 | 203 | _getAllNodes() { 204 | const allNodes = []; 205 | const nodesMap = this.nodesMap; 206 | for (let nodeKey in nodesMap) { 207 | if (Object.prototype.hasOwnProperty.call(nodesMap, nodeKey)) { 208 | allNodes.push(nodesMap[nodeKey]); 209 | } 210 | } 211 | 212 | return allNodes; 213 | } 214 | 215 | updateChildren(key, data) { 216 | const node = this.nodesMap[key]; 217 | if (!node) return; 218 | const childNodes = node.childNodes; 219 | for (let i = childNodes.length - 1; i >= 0; i--) { 220 | const child = childNodes[i]; 221 | this.remove(child.data); 222 | } 223 | for (let i = 0, j = data.length; i < j; i++) { 224 | const child = data[i]; 225 | this.append(child, node.data); 226 | } 227 | } 228 | 229 | _setCheckedKeys(key, leafOnly = false, checkedKeys) { 230 | const allNodes = this._getAllNodes().sort((a, b) => b.level - a.level); 231 | const cache = Object.create(null); 232 | const keys = Object.keys(checkedKeys); 233 | allNodes.forEach(node => node.setChecked(false, false)); 234 | for (let i = 0, j = allNodes.length; i < j; i++) { 235 | const node = allNodes[i]; 236 | const nodeKey = node.data[key].toString(); 237 | let checked = keys.indexOf(nodeKey) > -1; 238 | if (!checked) { 239 | if (node.checked && !cache[nodeKey]) { 240 | node.setChecked(false, false); 241 | } 242 | continue; 243 | } 244 | 245 | let parent = node.parent; 246 | while (parent && parent.level > 0) { 247 | cache[parent.data[key]] = true; 248 | parent = parent.parent; 249 | } 250 | 251 | if (node.isLeaf || this.checkStrictly) { 252 | node.setChecked(true, false); 253 | continue; 254 | } 255 | node.setChecked(true, true); 256 | 257 | if (leafOnly) { 258 | node.setChecked(false, false); 259 | const traverse = function (node) { 260 | const childNodes = node.childNodes; 261 | childNodes.forEach((child) => { 262 | if (!child.isLeaf) { 263 | child.setChecked(false, false); 264 | } 265 | traverse(child); 266 | }); 267 | }; 268 | traverse(node); 269 | } 270 | } 271 | } 272 | 273 | setCheckedNodes(array, leafOnly = false) { 274 | const key = this.key; 275 | const checkedKeys = {}; 276 | array.forEach((item) => { 277 | checkedKeys[(item || {})[key]] = true; 278 | }); 279 | 280 | this._setCheckedKeys(key, leafOnly, checkedKeys); 281 | } 282 | 283 | setCheckedKeys(keys, leafOnly = false) { 284 | this.defaultCheckedKeys = keys; 285 | const key = this.key; 286 | const checkedKeys = {}; 287 | keys.forEach((key) => { 288 | checkedKeys[key] = true; 289 | }); 290 | 291 | this._setCheckedKeys(key, leafOnly, checkedKeys); 292 | } 293 | 294 | setDefaultExpandedKeys(keys) { 295 | keys = keys || []; 296 | this.defaultExpandedKeys = keys; 297 | 298 | keys.forEach((key) => { 299 | const node = this.getNode(key); 300 | if (node) node.expand(null, this.autoExpandParent); 301 | }); 302 | } 303 | 304 | setChecked(data, checked, deep) { 305 | const node = this.getNode(data); 306 | 307 | if (node) { 308 | node.setChecked(!!checked, deep); 309 | } 310 | } 311 | 312 | getCurrentNode() { 313 | return this.currentNode; 314 | } 315 | 316 | setCurrentNode(currentNode) { 317 | const prevCurrentNode = this.currentNode; 318 | if (prevCurrentNode) { 319 | prevCurrentNode.isCurrent = false; 320 | } 321 | this.currentNode = currentNode; 322 | this.currentNode.isCurrent = true; 323 | } 324 | 325 | setUserCurrentNode(node) { 326 | const key = node[this.key]; 327 | const currNode = this.nodesMap[key]; 328 | this.setCurrentNode(currNode); 329 | } 330 | 331 | setCurrentNodeKey(key) { 332 | if (key === null || key === undefined) { 333 | this.currentNode && (this.currentNode.isCurrent = false); 334 | this.currentNode = null; 335 | return; 336 | } 337 | const node = this.getNode(key); 338 | if (node) { 339 | this.setCurrentNode(node); 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/components/newTree/model/util.js: -------------------------------------------------------------------------------- 1 | export const NODE_KEY = '$treeNodeId'; 2 | // import Vue from 'vue'; 3 | // import deepmerge from 'deepmerge'; 4 | 5 | 6 | export const markNodeData = function (node, data) { 7 | if (!data || data[NODE_KEY]) return; 8 | Object.defineProperty(data, NODE_KEY, { 9 | value: node.id, 10 | enumerable: false, 11 | configurable: false, 12 | writable: false 13 | }); 14 | }; 15 | 16 | export const getNodeKey = function (key, data) { 17 | if (!key) return data[NODE_KEY]; 18 | return data[key]; 19 | }; 20 | 21 | export const findNearestComponent = (element, componentName) => { 22 | let target = element; 23 | while (target && target.tagName !== 'BODY') { 24 | if (target.__vue__ && target.__vue__.$options.name === componentName) { 25 | return target.__vue__; 26 | } 27 | target = target.parentNode; 28 | } 29 | return null; 30 | }; 31 | 32 | let lang = {}; 33 | let merged = false; 34 | let i18nHandler = function () { 35 | return { merged } 36 | // const vuei18n = Object.getPrototypeOf(this); 37 | // if (typeof vuei18n === 'function' && !!Vue.locale) { 38 | // if (!merged) { 39 | // merged = true; 40 | // Vue.locale( 41 | // Vue.config.lang, 42 | // deepmerge(lang, Vue.locale(Vue.config.lang) || {}, { clone: true }) 43 | // ); 44 | // } 45 | // return vuei18n.apply(this, arguments); 46 | // } 47 | }; 48 | 49 | export const t = function (path) {//options 50 | let value = i18nHandler.apply(this, arguments); 51 | if (value !== null && value !== undefined) return value; 52 | 53 | const array = path.split('.'); 54 | let current = lang; 55 | 56 | for (let i = 0, j = array.length; i < j; i++) { 57 | const property = array[i]; 58 | value = current[property]; 59 | // if (i === j - 1) return format(value, options); 60 | if (!value) return ''; 61 | current = value; 62 | } 63 | return ''; 64 | }; 65 | 66 | const trim = function (string) { 67 | return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, ''); 68 | } 69 | 70 | export function hasClass(el, cls) { 71 | if (!el || !cls) return false; 72 | if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.'); 73 | if (el.classList) { 74 | return el.classList.contains(cls); 75 | } else { 76 | return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1; 77 | } 78 | } 79 | 80 | export function addClass(el, cls) { 81 | if (!el) return; 82 | var curClass = el.className; 83 | var classes = (cls || '').split(' '); 84 | 85 | for (var i = 0, j = classes.length; i < j; i++) { 86 | var clsName = classes[i]; 87 | if (!clsName) continue; 88 | 89 | if (el.classList) { 90 | el.classList.add(clsName); 91 | } else if (!hasClass(el, clsName)) { 92 | curClass += ' ' + clsName; 93 | } 94 | } 95 | if (!el.classList) { 96 | el.setAttribute('class', curClass); 97 | } 98 | } 99 | 100 | export function removeClass(el, cls) { 101 | if (!el || !cls) return; 102 | var classes = cls.split(' '); 103 | var curClass = ' ' + el.className + ' '; 104 | 105 | for (var i = 0, j = classes.length; i < j; i++) { 106 | var clsName = classes[i]; 107 | if (!clsName) continue; 108 | 109 | if (el.classList) { 110 | el.classList.remove(clsName); 111 | } else if (hasClass(el, clsName)) { 112 | curClass = curClass.replace(' ' + clsName + ' ', ' '); 113 | } 114 | } 115 | if (!el.classList) { 116 | el.setAttribute('class', trim(curClass)); 117 | } 118 | } -------------------------------------------------------------------------------- /src/components/newTree/tree-node.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 386 | 387 | -------------------------------------------------------------------------------- /src/components/note_editor.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 109 | 110 | -------------------------------------------------------------------------------- /src/components/note_header.vue: -------------------------------------------------------------------------------- 1 | 165 | 331 | 332 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next' 2 | const zh = { 3 | translation: { 4 | contextmenu: { 5 | isExpand: '打开节点', 6 | expand: '展开节点', 7 | delete: '删除节点', 8 | 'delete-one': '删除单个节点', 9 | add: '新建子节点', 10 | 'add-parent': '新建父节点', 11 | 'add-sibling': '新建兄弟节点', 12 | 'add-sibling-before': '在此之前新建兄弟节点', 13 | cut: '剪切', 14 | copy: '拷贝', 15 | paste: '粘贴', 16 | selectall: '全选', 17 | zoomin: '放大', 18 | zoomout: '缩小', 19 | zoomfit: '缩放至合适大小' 20 | } 21 | } 22 | } 23 | 24 | i18next.init({ 25 | fallbackLng: 'zh', 26 | lng: 'zh', 27 | resources: { 28 | zh, 29 | } 30 | }) 31 | 32 | export default i18next -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp, getCurrentInstance } from 'vue' 2 | import App from './App.vue' 3 | import { store } from './store' 4 | import router from './router/index' 5 | import ElementPlus from 'element-plus' 6 | import zhCn from "element-plus/lib/locale/lang/zh-cn"; 7 | import 'element-plus/dist/index.css' 8 | import '@icon-park/vue-next/styles/index.css'; 9 | 10 | import { createNumberString } from '@/utils/index' 11 | 12 | 13 | const app = createApp(App) 14 | 15 | app.config.globalProperties.$createdId = createNumberString 16 | 17 | declare module '@vue/runtime-core' { 18 | export interface ComponentCustomProperties { 19 | $createdId: typeof createNumberString 20 | } 21 | } 22 | 23 | 24 | 25 | app.use(ElementPlus, { locale: zhCn }) 26 | app.use(router) 27 | app.use(store) 28 | app.mount('#app') 29 | -------------------------------------------------------------------------------- /src/mainProcess.ts: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | global.isDevelopment = process.env.NODE_ENV !== 'production' 3 | 4 | const webPreferences = { 5 | // preload: path.join(__dirname, 'preload.js'), 6 | enableRemoteModule: true, 7 | nodeIntegration: true, 8 | contextIsolation: false, 9 | 10 | } 11 | 12 | function mainWindows() { 13 | const config = { 14 | width: 350, 15 | height: 600, 16 | minWidth: 250, 17 | minHeight: 48, 18 | frame: false, 19 | transparent: true, 20 | webPreferences 21 | } 22 | const winURL = global.isDevelopment ? 'http://localhost:55226' : `file://${__dirname}/index.html`; 23 | 24 | return { config, winURL } 25 | } 26 | 27 | async function initDevTool(session: any) { 28 | // const result = await session.defaultSession.removeExtension('nhdogjmejiglipccpnnnanhbledajbpd') 29 | // console.log('result', result) 30 | // const getVueDevTool = await session.defaultSession.getExtension('nhdogjmejiglipccpnnnanhbledajbpd') 31 | // if (!getVueDevTool) { 32 | // await session.defaultSession.loadExtension( 33 | // 'C:/Users/JIEKE/AppData/Local/Google/Chrome/User Data/Default/Extensions/nhdogjmejiglipccpnnnanhbledajbpd/6.1.4_0', 34 | // { allowFileAccess: true } // this is the key line 35 | // ) 36 | // } 37 | } 38 | 39 | console.log('global.isDevelopment', global.isDevelopment) 40 | declare const __static: string; 41 | function logo() { 42 | const imgPath = path.join(__dirname) 43 | // console.log('imgPath', imgPath) 44 | // console.log('22222222', path.join(__static, 'img/logo.png')) 45 | return path.join(__static, 'img/logo.png') 46 | } 47 | 48 | 49 | 50 | module.exports = { 51 | mainWindows, 52 | initDevTool, 53 | logo 54 | } 55 | 56 | // function __static(__static: any, arg1: string) { 57 | // throw new Error("Function not implemented."); 58 | // } 59 | -------------------------------------------------------------------------------- /src/mitt.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | type Events = any 3 | const emitter = mitt(); 4 | export default emitter -------------------------------------------------------------------------------- /src/preload.ts: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron') 2 | 3 | const windowLoaded = new Promise(resolve => { 4 | console.log('2222222222') 5 | window.onload = resolve 6 | }) 7 | 8 | // contextBridge.exposeInMainWorld('ipcRenderer', { 9 | // send: (channel, data) => { 10 | // // whitelist channels 11 | // const validChannels = ['toMain'] 12 | // if (validChannels.includes(channel)) { 13 | // ipcRenderer.send(channel, data) 14 | // } 15 | // }, 16 | // receive: (channel, func) => { 17 | // const validChannels = ['fromMain'] 18 | // if (validChannels.includes(channel)) { 19 | // // Deliberately strip event as it includes `sender` 20 | // ipcRenderer.on(channel, (event, ...args) => func(...args)) 21 | // } 22 | // } 23 | // }) 24 | 25 | ipcRenderer.on('main-world-port', async (event) => { 26 | console.log('preload111111111111') 27 | await windowLoaded 28 | // 我们使用 window.postMessage 将端口 29 | // 发送到主进程 30 | window.postMessage('main-world-port', '*', event.ports) 31 | }) 32 | 33 | contextBridge.exposeInMainWorld('electronAPI', { 34 | getList: (list: []) => ipcRenderer.invoke('getList', list), 35 | setTitle: (obj: object) => ipcRenderer.send('setTitle', obj), 36 | newWindow: (winId: string) => ipcRenderer.send('newWindow', winId), 37 | closeWindow: (winId: string) => ipcRenderer.send('closeWindow', winId), 38 | }) -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRouter, 3 | createWebHashHistory, 4 | RouteRecordRaw 5 | } from 'vue-router' 6 | 7 | const routes: Array = [ 8 | // { 9 | // path: '/', 10 | // name: 'index', 11 | // component: () => import('@/views/index') 12 | // }, 13 | // { 14 | // path: '/set', 15 | // name: 'set', 16 | // component: () => import('@/views/setting') 17 | // }, 18 | // { 19 | // path: '/edited', 20 | // name: 'edited', 21 | // component: () => import('@/views/edited') 22 | // }, 23 | 24 | { 25 | path: '/', 26 | name: 'header', 27 | component: () => import('@/views/home.vue'), 28 | children: [ 29 | { 30 | path: '', 31 | name: 'index', 32 | component: () => import('@/views/index.vue') 33 | }, 34 | { 35 | path: '/set', 36 | name: 'set', 37 | component: () => import('@/views/setting.vue') 38 | }, 39 | { 40 | path: '/edited', 41 | name: 'edited', 42 | component: () => import('@/views/edited.vue') 43 | }, 44 | { 45 | path: '/outline', 46 | name: 'outline', 47 | component: () => import('@/views/outline.vue') 48 | }, 49 | ] 50 | }, 51 | { 52 | path: '/menu', 53 | name: 'menu', 54 | component: () => import('@/views/menu.vue') 55 | } 56 | ] 57 | 58 | 59 | export default createRouter({ 60 | history: createWebHashHistory(), //createMemoryHistory(), 61 | routes 62 | }) 63 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import Datastore from 'lowdb' 2 | import FileSync from 'lowdb/adapters/FileSync' 3 | import path from 'path' 4 | import { app } from 'electron' 5 | 6 | 7 | const STORE_PATH = app.getPath('userData') // 获取electron应用的用户目录 8 | console.log('STORE_PATH', STORE_PATH) 9 | const adapter = new FileSync(path.join(STORE_PATH, '/database.json')) // 初始化lowdb读写的json文件名以及存储路径 10 | 11 | const db: any = Datastore(adapter) // lowdb接管该文件 12 | const initDatabase = { 13 | User: {},//放用户相关配置 14 | NoteList: [] //标签 15 | } 16 | db.defaults(initDatabase).write() 17 | 18 | // function initServer() { 19 | 20 | // } 21 | 22 | 23 | export default db -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | 8 | declare module '@'; 9 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import header from './modules/header' 2 | import note from './modules/note' 3 | import user from './modules/user' 4 | import { createStore } from 'vuex' 5 | 6 | 7 | export const store = createStore({ 8 | modules: { 9 | header, 10 | note, 11 | user 12 | } 13 | }) 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/store/modules/header.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state() { 4 | return { 5 | pageTypeText: 'menu', 6 | note: {}, 7 | isEditedTitle: true, 8 | close: true, 9 | timing: '' 10 | } 11 | }, 12 | getters: { 13 | getPageTypeText(state: any) { 14 | return state.pageTypeText 15 | } 16 | }, 17 | actions: { 18 | setTiming(context: any, str: string) { 19 | context.commit('setTiming', str) 20 | }, 21 | setPageTypeText(context: any, txt: string) { 22 | context.commit('setPageTypeText', txt) 23 | }, 24 | setNote(context: any, note: object) { 25 | context.commit('setNote', note) 26 | }, 27 | setIsEditedTitle(context: any, bool: object) { 28 | context.commit('setIsEditedTitle', bool) 29 | }, 30 | setHeaderClose(context: any, bool: boolean) { 31 | context.commit('setHeaderClose', bool) 32 | } 33 | }, 34 | mutations: { 35 | setTiming(state: any, str: string) { 36 | state.timing = str 37 | }, 38 | setPageTypeText(state: any, txt: string) { 39 | state.pageTypeText = txt 40 | }, 41 | setNote(state: any, note: object) { 42 | state.note = note 43 | }, 44 | setIsEditedTitle(state: any, bool: boolean) { 45 | state.isEditedTitle = bool 46 | }, 47 | setHeaderClose(state: any, bool: boolean) { 48 | state.close = bool 49 | } 50 | } 51 | } 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/store/modules/note.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state() { 4 | return { 5 | list: [], 6 | item: {}, 7 | showToolBar: false, 8 | isLeft: false 9 | } 10 | }, 11 | getters: { 12 | getNoteList(state: any) { 13 | return state.list 14 | }, 15 | getNoteItem(state: any) { 16 | return state.item 17 | } 18 | }, 19 | actions: { 20 | setNoteList(context: any, list: []) { 21 | context.commit('setNoteList', list) 22 | }, 23 | setNoteItem(context: any, item: Object) { 24 | context.commit('setNoteItem', item) 25 | }, 26 | setShowToolBar(context: any, bool: Boolean) { 27 | context.commit('setShowToolBar', bool) 28 | }, 29 | }, 30 | mutations: { 31 | setNoteList(state: any, list: []) { 32 | state.list = list 33 | }, 34 | setNoteItem(state: any, item: object) { 35 | state.item = item 36 | }, 37 | setShowToolBar(state: any, bool: Boolean) { 38 | state.showToolBar = bool 39 | }, 40 | } 41 | 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state() { 4 | return { 5 | user: { 6 | dark: false, 7 | startUp: true, 8 | pageSize: 10 9 | } 10 | } 11 | }, 12 | actions: { 13 | setUser(context: any, user: string) { 14 | context.commit('setUser', user) 15 | } 16 | }, 17 | mutations: { 18 | setUser(state: any, user: any) { 19 | if (!user.pageSize) { 20 | user.pageSize = 10 21 | } 22 | state.user = user 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/types/custom-types.d.ts: -------------------------------------------------------------------------------- 1 | import { SlateDescendant, SlateElement, SlateText } from '@wangeditor/editor' 2 | 3 | declare module '@wangeditor/editor' { 4 | // 扩展 Text 5 | interface SlateText { 6 | text: string 7 | } 8 | 9 | // 扩展 Element 10 | interface SlateElement { 11 | type: string 12 | children: SlateDescendant[] 13 | } 14 | } -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | var mainWin: any; 3 | var isMenu: boolean, 4 | var isDevelopment: any 5 | // function sum(a: number, b: number): number; 6 | } 7 | 8 | 9 | 10 | 11 | export { }; -------------------------------------------------------------------------------- /src/types/store.d.ts: -------------------------------------------------------------------------------- 1 | export interface header { 2 | pageTypeText: string, 3 | note: any, 4 | isEditedTitle: boolean, 5 | close: boolean, 6 | timing: string 7 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | 2 | //生成id 3 | export const createRandomString = function (length: number, possibleString?: string): string { 4 | let text = '' 5 | const possible = possibleString || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 6 | 7 | for (let i = 0; i < length; i++) { 8 | text += possible.charAt(Math.floor(Math.random() * possible.length)) 9 | } 10 | 11 | return text 12 | } 13 | 14 | export const createNumberString = function (length: number = 8): string { 15 | return createRandomString(length) 16 | } 17 | 18 | export const fromNow = function (time: number): string { 19 | if (!time) { 20 | return '' 21 | } 22 | // time必须是毫秒 23 | const curTime = (new Date()).getTime() 24 | const diff: any = curTime - time //time.getTime() 25 | const division = function (number: number): number { 26 | return parseInt((number).toString(), 10); 27 | } 28 | if (0 > diff) { 29 | return "几秒前"; 30 | } else if (1000 * 60 > diff) { 31 | return "刚刚"; 32 | } else if (1000 * 60 <= diff && 1000 * 60 * 60 > diff) { 33 | return division(diff / (1000 * 60)) + "分钟前"; 34 | } else if (1000 * 60 * 60 <= diff && 1000 * 60 * 60 * 24 > diff) { 35 | return division(diff / (1000 * 60 * 60)) + "小时前"; 36 | } else if (1000 * 60 * 60 * 24 <= diff && 1000 * 60 * 60 * 24 * 30 > diff) { 37 | return division(diff / (1000 * 60 * 60 * 24)) + "天前"; 38 | } else if (1000 * 60 * 60 * 24 * 30 <= diff && 1000 * 60 * 60 * 24 * 30 * 12 > diff) { 39 | return division(diff / (1000 * 60 * 60 * 24 * 30)) + "月前"; 40 | } else { 41 | return division(diff / (1000 * 60 * 60 * 24 * 30 * 12)) + "年前"; 42 | } 43 | } 44 | 45 | 46 | export const getQueryByName = (name: string): string => { 47 | const queryNameRegex = new RegExp(`[?&]${name}=([^&]*)(&|$)`); 48 | const queryNameMatch = window.location.hash.match(queryNameRegex); 49 | return queryNameMatch ? decodeURIComponent(queryNameMatch[1]) : ""; 50 | }; 51 | 52 | 53 | export const debounce = (fn: Function, ms = 300) => { 54 | let timeoutId: ReturnType; 55 | return function (this: any, ...args: any[]) { 56 | clearTimeout(timeoutId); 57 | timeoutId = setTimeout(() => fn.apply(this, args), ms); 58 | }; 59 | }; -------------------------------------------------------------------------------- /src/views/edited.vue: -------------------------------------------------------------------------------- 1 | @@ -1,243 +1,254 @@ 2 | 27 | 281 | 282 | 283 | -------------------------------------------------------------------------------- /src/views/menu.vue: -------------------------------------------------------------------------------- 1 | 15 | 103 | 104 | -------------------------------------------------------------------------------- /src/views/setting.vue: -------------------------------------------------------------------------------- 1 | 26 | 77 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitThis": false, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "useDefineForClassFields": true, 16 | "sourceMap": true, 17 | "baseUrl": ".", 18 | "types": [ 19 | "webpack-env" 20 | ], 21 | "paths": { 22 | "@/*": [ 23 | "src/*" 24 | ] 25 | }, 26 | "lib": [ 27 | "esnext", 28 | "dom", 29 | "dom.iterable", 30 | "scripthost" 31 | ] 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "src/**/*.tsx", 36 | "src/**/*.vue", 37 | "tests/**/*.ts", 38 | "tests/**/*.tsx", 39 | "src/types/.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin") 2 | const { defineConfig } = require('@vue/cli-service') 3 | const webpackObfuscator = require('webpack-obfuscator'); 4 | 5 | const isProduction = process.env.NODE_ENV === 'production' 6 | 7 | module.exports = defineConfig({ 8 | publicPath: isProduction ? './' : '/', 9 | productionSourceMap: false, 10 | configureWebpack: isProduction ? { 11 | resolve: { 12 | fallback: { 13 | "fs": false, 14 | } 15 | }, 16 | plugins: [ 17 | new NodePolyfillPlugin(), 18 | new webpackObfuscator({ 19 | rotateStringArray: true, 20 | }, []) 21 | ] 22 | } : {}, 23 | // chainWebpack: webpackConfig => { 24 | 25 | // }, 26 | transpileDependencies: [/\bvue-awesome\b/], 27 | pluginOptions: { 28 | // Use this to change the entry point of your app's render process. default src/[main|index].[js|ts] 29 | rendererProcessFile: 'src/[background|on|mainProcess].ts', 30 | electronBuilder: { 31 | nodeIntegration: true, 32 | preload: 'src/preload.ts', 33 | builderOptions: { 34 | extraResources: ['src', 'src/res/'], 35 | productName: '便利贴', 36 | appId: '202274', 37 | copyright: 'zmy', 38 | compression: 'store', 39 | asar: true, 40 | win: { 41 | icon: 'public/img/logo', 42 | target: ['nsis', 'zip'] 43 | }, 44 | nsis: { 45 | oneClick: false, 46 | perMachine: true, 47 | allowElevation: true, 48 | allowToChangeInstallationDirectory: true, // 允许修改安装目录 49 | createDesktopShortcut: true, // 创建桌面图标 50 | createStartMenuShortcut: true, // 创建开始菜单图标 51 | shortcutName: '便利贴' // 图标名称 52 | } 53 | }, 54 | // externals: ['knex', 'sqlite3'], 55 | }, 56 | }, 57 | devServer: { 58 | port: 55226 59 | } 60 | }) --------------------------------------------------------------------------------