├── .browserslistrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .prettierrc.js
├── .sentryclirc
├── .stylelintignore
├── .stylelintrc.js
├── LICENSE
├── README.md
├── assets
├── export02.png
├── export03.png
├── export04.png
├── export05.png
├── export06.png
├── export07.png
├── mindmap-datastream.png
└── mindmap.drawio
├── auto-imports.d.ts
├── babel.config.js
├── commitlint.config.js
├── components.d.ts
├── jsconfig.json
├── note.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
└── index.html
├── src
├── App.vue
├── assets
│ ├── css
│ │ ├── handler.scss
│ │ ├── mixin.scss
│ │ ├── reset.css
│ │ └── themes.scss
│ ├── logo.svg
│ ├── map
│ │ ├── add.svg
│ │ ├── arrow-left.svg
│ │ └── loading.svg
│ └── pic
│ │ ├── 404.svg
│ │ ├── add-file.svg
│ │ ├── add-quick.svg
│ │ ├── add.svg
│ │ ├── arrow-left.svg
│ │ ├── arrow-right.svg
│ │ ├── delete.svg
│ │ ├── download.svg
│ │ ├── empty.png
│ │ ├── file-large.svg
│ │ ├── file-small.svg
│ │ ├── fit-view.svg
│ │ ├── folder-large.svg
│ │ ├── folder.svg
│ │ ├── grid.svg
│ │ ├── hamberger.svg
│ │ ├── home.svg
│ │ ├── latest.svg
│ │ ├── loading.svg
│ │ ├── logout.svg
│ │ ├── marker.svg
│ │ ├── more.svg
│ │ ├── note.svg
│ │ ├── pc.png
│ │ ├── qrcode.png
│ │ ├── quick.svg
│ │ ├── refresh.png
│ │ ├── remove.svg
│ │ ├── rename.svg
│ │ ├── search.svg
│ │ ├── settings.svg
│ │ ├── skin.svg
│ │ ├── success.png
│ │ ├── table.svg
│ │ ├── theme.svg
│ │ ├── trash.svg
│ │ ├── tree.svg
│ │ ├── triangle.svg
│ │ ├── upload.svg
│ │ └── workwith.svg
├── components
│ ├── DocPopover.vue
│ ├── SvgIcon.vue
│ ├── map
│ │ ├── Map-beta.vue
│ │ ├── Map.vue
│ │ ├── MapBar.vue
│ │ ├── MapOpPopover.vue
│ │ └── MapRender.vue
│ └── note
│ │ ├── Note.vue
│ │ └── NotePopover.vue
├── hooks
│ ├── http
│ │ ├── apis.js
│ │ ├── axios.js
│ │ └── index.js
│ ├── map.worker.js
│ ├── map
│ │ ├── LogicTree-beta.js
│ │ ├── LogicTree.js
│ │ ├── LogicTree1.js
│ │ ├── Tree.js
│ │ ├── TreeTable.js
│ │ ├── useAutoZoom.js
│ │ ├── useMap-beta.js
│ │ ├── useMap.js
│ │ ├── useMap1.js
│ │ └── zoomMap.js
│ ├── note
│ │ └── useNote.js
│ ├── svg2Png.js
│ ├── useContent.js
│ ├── useIntroStyle.js
│ ├── useLogin.js
│ ├── useMapStyle.js
│ ├── useSnapshot.js
│ ├── useWorker.js
│ └── utils.js
├── main.js
├── router
│ └── index.js
├── store
│ ├── doc.js
│ ├── index.js
│ ├── map.js
│ ├── user.js
│ └── website.js
└── views
│ ├── Edit.vue
│ ├── Folder
│ ├── components
│ │ └── BreadCrumb.vue
│ └── index.vue
│ ├── Home
│ ├── components
│ │ ├── ProfilePopover.vue
│ │ └── Sider.vue
│ └── index.vue
│ ├── Intro.vue
│ ├── Login
│ ├── components
│ │ └── Qrcode.vue
│ └── index.vue
│ └── NotFound.vue
└── vue.config.js
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 | [*]
7 | # lf, cr, or crlf
8 | end_of_line = lf
9 | # ensure file ends with a newline when saving
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | vue.config.js
2 | src/views/A.js
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | extends: [
7 | 'airbnb-base',
8 | 'plugin:vue/vue3-essential',
9 | '@vue/standard',
10 | 'prettier',
11 | 'plugin:prettier/recommended'
12 | ],
13 | parserOptions: {
14 | parser: 'babel-eslint'
15 | },
16 | rules: {
17 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
19 | 'vue/multi-word-component-names': 0,
20 | 'import/extensions': 'off',
21 | 'arrow-parens': 'off',
22 | 'import/no-cycle': 'warn',
23 | 'no-underscore-dangle': 'off',
24 | 'class-methods-use-this': 'off',
25 | 'prefer-destructuring': 'off'
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the action will run.
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the master branch
8 | # push:
9 | # branches: [ main ]
10 | # pull_request:
11 | # branches: [ main ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | # This workflow contains a single job called "build"
19 | build:
20 | # The type of runner that the job will run on
21 | runs-on: ubuntu-latest
22 |
23 | # Steps represent a sequence of tasks that will be executed as part of the job
24 | steps:
25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
26 | - uses: actions/checkout@v2
27 | - uses: actions/setup-node@v2 #nodejs 环境
28 | with:
29 | node-version: '14' #nodejs 版本
30 | - run: npm install #安装依赖
31 | - run: npm run build --if-present #打包
32 |
33 | - name: Tar dist file
34 | run: |
35 | tar -zcvf mindmap.tar.gz dist # 打包dist文件夹
36 | - name: Scp_upload_tar_file # 上传至服务器
37 | uses: garygrossgarten/github-action-scp@release
38 | with:
39 | local: 'mindmap.tar.gz'
40 | remote: '/www/wwwroot/mindmap.tar.gz'
41 | host: ${{ secrets.HOST }}
42 | username: ${{ secrets.USERNAME }}
43 | password: ${{ secrets.PASSWORD }}
44 | port: ${{ secrets.PORT }}
45 | - name: Deploy_app_tar #解压压缩包
46 | uses: appleboy/ssh-action@master
47 | with:
48 | host: ${{ secrets.HOST }}
49 | username: ${{ secrets.USERNAME }}
50 | password: ${{ secrets.PASSWORD }}
51 | port: ${{ secrets.PORT }}
52 | script: |
53 | cd /www/wwwroot/
54 | rm -rf mindmap
55 | tar -zxvf mindmap.tar.gz
56 | mv dist mindmap
57 | rm -rf mindmap.tar.gz
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | output_prod.js
6 | output_dev.js
7 |
8 | # local env files
9 | .env.local
10 | .env.*.local
11 | .sentryclirc
12 | # Log files
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | pnpm-debug.log*
17 |
18 | # Editor directories and files
19 | .idea
20 | .vscode
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 | components.d.ts
27 | auto-imports.d.ts
28 | /loaders
29 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | # npx --no-install pretty-quick --staged
5 | npx --no-install lint-staged
6 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // 代码结尾是否加分号
3 | semi: false,
4 | // 是否使用单引号
5 | singleQuote: true,
6 | // 对象大括号内两边是否加空格 { a:0 }
7 | bracketSpacing: true,
8 | // 单个参数的箭头函数不加括号 x => x
9 | arrowParens: 'avoid',
10 | // 超过多少字符强制换行
11 | printWidth: 80,
12 | // 文件换行格式 LF/CRLF
13 | endOfLine: 'lf',
14 | // 使用 2 个空格缩进
15 | tabWidth: 2,
16 | // 不使用缩进符,而使用空格
17 | useTabs: false,
18 | // 对象的 key 仅在必要时用引号
19 | quoteProps: 'as-needed',
20 | // jsx 不使用单引号,而使用双引号
21 | jsxSingleQuote: false,
22 | // 末尾不需要逗号
23 | trailingComma: 'none',
24 | // jsx 标签的反尖括号需要换行
25 | jsxBracketSameLine: false,
26 | // 每个文件格式化的范围是文件的全部内容
27 | rangeStart: 0,
28 | rangeEnd: Infinity,
29 | // 不需要写文件开头的 @prettier
30 | requirePragma: false,
31 | // 不需要自动在文件开头插入 @prettier
32 | insertPragma: false,
33 | // 使用默认的折行标准
34 | proseWrap: 'preserve',
35 | // 根据显示样式决定 html 要不要折行
36 | htmlWhitespaceSensitivity: 'css'
37 | }
38 |
--------------------------------------------------------------------------------
/.sentryclirc:
--------------------------------------------------------------------------------
1 | [auth]
2 | token = 0a50065d170e45a6b066c1b53800496ea72ef418688f4fa68c31b2c88fc646ab
3 |
4 | [defaults]
5 | url = https://sentry.io/
6 | org = zy1996
7 | project = zmindmap
8 |
9 | [attention]
10 | tip = please use your own configs!!!
11 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | # .stylelintignore
2 | # 旧的不需打包的样式库
3 | *.min.css
4 |
5 | # 其他类型文件
6 | *.js
7 | *.jpg
8 | *.woff
9 |
10 | # 测试和打包目录
11 | /test/
12 | /dist/
13 |
14 | # 通过反取忽略目录
15 | # /src/component/*
16 | # !/src/component/CompA
17 | # !/src/component/CompB
18 | # 这样的效果是除 CompA 和 CompB 外其他目录都会被忽略
19 |
--------------------------------------------------------------------------------
/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'stylelint-config-standard',
4 | 'stylelint-config-recess-order',
5 | 'stylelint-config-prettier'
6 | ],
7 | rules: {
8 | 'at-rule-no-unknown': [
9 | true,
10 | {
11 | ignoreAtRules: [
12 | 'mixin',
13 | 'include',
14 | 'extend',
15 | 'each',
16 | 'function',
17 | 'return'
18 | ]
19 | }
20 | ],
21 | // "order/properties-order": null,
22 | 'declaration-empty-line-before': null,
23 | 'no-descending-specificity': null,
24 | 'selector-pseudo-class-no-unknown': null
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 zyascend
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.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## 简介
19 | **仿[幕布](https://mubu.com)思维导图网站。支持导图编辑、大纲编辑、图片导出、扫码登录。**
20 |
21 | ## 项目地址:
22 | 项目总结:[ZMindMap-Wiki](https://github.com/zyascend/ZMindMap/wiki)
23 |
24 | 预览地址:[ZMind思维导图](https://map.kimjisoo.cn)
25 |
26 | 移动端地址: [ZMindMap-Mobile](https://github.com/zyascend/ZMindMap-Mobile)
27 |
28 | Node端地址:[mind-map-node](https://github.com/zyascend/mind-map-node)
29 |
30 | ## 下载&安装
31 |
32 | - 下载
33 |
34 | ```bash
35 | git clone --depth=1 https://github.com/zyascend/ZMindMap.git
36 | ```
37 |
38 | - 进入项目目录
39 | ```bash
40 | cd ZmindMap
41 | ```
42 | - 安装依赖
43 |
44 | ```bash
45 | npm install
46 | ```
47 |
48 | - 运行
49 | ```bash
50 | npm run serve
51 | ```
52 | ## 效果图
53 |
54 | |
|
|
55 | | :------------------------------------------------------------------------------: | -------------------------------------------------------------------------------- |
56 | |
|
|
57 | |
|
|
58 |
59 | 视频版:[点击播放](https://cdn.kimjisoo.cn/videos%2Fpresentation_v1.0.mp4)
60 |
61 | ## Features
62 | - Vue3 CompositionApi
63 | - Pinia状态管理
64 | - VueRouter路由控制
65 | - SVG画图
66 | - 类幕布思维导图的文档构建方式实现
67 | - 数据驱动UI的思路
68 | - svg导出为png图片
69 | - Element-plus
70 | - splitChunks单独打包
71 | - 基于七牛云的CDN加速
72 | - JWT & 二维码扫码登录
73 | - 夜间模式
74 | - 前端监控
75 | - 使用Sentry收集错误信息
76 | - 百度统计
77 |
78 | ## TODOs
79 | - [x] 基于vue响应式,通过数据驱动svg子元素更新
80 | - [x] 对于大纲编辑,如何不通过递归查找的方式在源数据中定位到待更新的节点
81 | - [x] key-value形式构建map
82 | - [x] Vuex切换为pinia
83 | - [x] store分模块维护
84 | - [x] 支持撤回操作
85 | - [x] bug fix
86 | - [x] 导图风格切换
87 | - [x] 支持导出
88 | - [x] 导出为图片
89 | - [x] 图片不显示 bug fix
90 | - [ ] 导出为其他格式
91 | - [x] 二维码扫码登录
92 | - [x] 轮询接口查状态 => websocket
93 | - [x] 大纲编辑页相关优化
94 | - [x] 防止XSS攻击
95 | - [x] 支持添加图片
96 | - [x] 重写节点宽高计算逻辑
97 | - [x] 全面重写MindMap组件
98 | - [x] 代码臃肿:分离UI渲染部分和数据部分
99 | - [x] 可拓展性:提取各种样式导图的公共dom结构 方便切换导图风格
100 | - [x] 公共逻辑抽取 方便随时切换颜色样式
101 | - [x] 导图计算:抽取公共逻辑 + 继承封装
102 | - [x] map store 逻辑优化
103 | - [ ] 页面的loading 和 错误处理
104 | - [ ] 监听全局异常 error boundary
105 | - [ ] 使用自定义的loader处理svg图标
106 | - [ ] 将svg icon处理为SFC
107 | - [ ] 封装SFC为Icon组件:绑定属性 灵活使用
108 | - [x] 所有配置项由`window.CFG`注入
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/assets/export02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/assets/export02.png
--------------------------------------------------------------------------------
/assets/export03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/assets/export03.png
--------------------------------------------------------------------------------
/assets/export04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/assets/export04.png
--------------------------------------------------------------------------------
/assets/export05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/assets/export05.png
--------------------------------------------------------------------------------
/assets/export06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/assets/export06.png
--------------------------------------------------------------------------------
/assets/export07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/assets/export07.png
--------------------------------------------------------------------------------
/assets/mindmap-datastream.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/assets/mindmap-datastream.png
--------------------------------------------------------------------------------
/assets/mindmap.drawio:
--------------------------------------------------------------------------------
1 | 5Ztbc6M2FIB/jR7jAUncHsHG2850O51mOu0+EiPbdLHlAk7s/fWVQMJc5BhscJzZ7MxGHIEQ5/Lp6BKAppvDlyTYrb/SkMQAauEBoBmA0LF19j8XHAuBgWAhWCVRWIj0k+A5+kGEUBPSfRSStHZjRmmcRbu6cEG3W7LIarIgSehb/bYljetv3QUr0hI8L4K4Lf07CrN1IbWhdZL/QqLVWr5ZN52iZhPIm8WXpOsgpG8VEfIBmiaUZkVpc5iSmOtO6qV4bn6mtuxYQrZZlwewWTzxGsR78XGiY9lRfm1C99uQ8Ac0gLy3dZSR512w4LVvzLxMts42MbvSWbHdAdGnV5Jk5FARiQ59IXRDsuTIbhG1liOUI7xDl8p6O+kaSVdYV/QMLSEMhH1XZdsnFbCC0IJaI/DxFKKjywrBaCyFYNxSAAlZOIjLLd2S+hezD02O/wjt5Bff+MXEkJezQ7VydhRXZzWV0n2yEK9GItyDZEXkV9qFjPfqXXUmJA6y6LUexCrV5I+6SRIcKzfsaLTN0krLf3DByUoQGzUrYaw11Fy0eFJ62bVudjDanumbwPGAh/OCCTwN+AZw58Cb8gKv8nnB9oEzz6ssYHv5zUZ+swnsGXB9pYF/C14YtWuWDeJotWXlBbMQSZiA+3DEuOiKik0UhrwNLyFp9CN4ydvjthXKY40bHjBmSmtLR2sFRgly0WCNlaqAedImlmHaNXM8iab6OUHLynqtUVR/nC6XKfPKZnD1szNSAMiMmY68Jc17sqAxTfIa8789HyiYLtFyidhPVWSu+O+vwe5PwmiVTF5Zc6Khl0RWcxfwbOAVrjQHDgI+5hLmMr4DHMy9w7e4Kzl2XuXmHsQKzLnM3KcwsHF+jwZsU76CfWXRXfGeD2eoWWcodNoM1S0FQ80hxhTUE6GdUIhHwV7L47FR15zkWtlEwWLx1E2uDxXZSCfEmcDVgGdxCZPb80dFXBncNyNOm7AcCI5POGMEwuG2lW8jnBe8g7emZ7DhkOHNkp4BH5VYyOpALHMkYiGjJ7FOSd8pz/tWS/N6Jn21DM+6D+pgY5DQm6gr8NtC3a2poj5wqohUHP28iaF0xiGoaWJkjU/NJ73hOINwUzVXv4Gb0TZk7KyC8xICmfa5PM0S+p1MxcsEC5ZRHDdEFQfhzYp1HIZ/cS0a1t/zl5ZnDUBW2GGBQdcUZG3i4KpcsL2ecIGshyirgJVdlVxl5RNW+UWvqbRu3oeqGF6g6pkEsi9VTWtcqqryFgVDVdnpI1JVOuIQVEVIMwfBqGONwE1s9wy5Cq10SatpyVY0Zz/aR4fZqNHRetq2r4vha6zlnB3m0l2wrdlRjmncQE9pbjGX3aDru0N7wOsyWGo2/6d49spAPzOuluLii84Mt3fAwvWeLoNqkNU6pDlwmDTMHoEfeodNknQd7HixyJBCun8pc5t7TxxNrRGrDpwY7QTHUCQ4enO+f02Go1tnA/jd0NOWS95zxfz+OaPJz5akmo1Nn3IWdI8kVc6RPm76Xx1B5fS/uiSAhh5UO6tGMTz5Noe9q/NVcFZw8uUmvhxerJ27+QL5Z8kLpe2HAbtRzzqGofwTVDU6KPPhRYhdzkJwPQs5hz0Ovjb21pR+Z+ncfJ8SPkv/N+3KvwcaiXS7kXOqRyKooJgxBMX67lx3ohFSbEEPvhXT+RNlFlbfMbHnHD98l87nsDm3Cs5BZXNuueoZ0v3ZM9gWsDbB0MC30aZczDNHAAy6uM97ETCwI2DUedWMLnrlVQ/EFdRcwFNtjYxFFTzKZu5Ih1jaywCNiTyCo03koYJNHzWETlm7TP2fcxg19brNsKZwd6xwd3sId79uNndu1+F3mpGfcMvBdhqZ0D23HAyoSBLygzuuxScongs8+NevirNATWtc8PUg3RXHbZfRgcdHbXkJqpaXcicZRsO4MV/W25mmPVKMGE7PIaGcLpcX3Y5IntMf2YYuP9d8ehmTzCPe4fy5InzkWeXOg5LxuKctWbCMe9xSHgXou4p1dgF5qKOa3bh5p7XjbsEs42OQtB5bwgNvPewxxha9ocjquenYpMupHKjsco7y86DXMRuLPOOhl12e/jiisM/pL0yQ/z8=
--------------------------------------------------------------------------------
/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by 'unplugin-auto-import'
2 | // We suggest you to commit this file into source control
3 | declare global {
4 | const ElTable: typeof import('element-plus/es')['ElTable']
5 | const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
6 | }
7 | export {}
8 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/cli-plugin-babel/preset'],
3 | plugins: [
4 | [
5 | 'import',
6 | {
7 | libraryName: 'vant',
8 | libraryDirectory: 'es',
9 | style: true
10 | }
11 | ]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | // git commit 规范
2 | // <类型>[可选的作用域]: <描述>
3 |
4 | // # 主要type
5 | // feat: 增加新功能
6 | // fix: 修复bug
7 |
8 | // # 特殊type
9 | // docs: 只改动了文档相关的内容
10 | // style: 不影响代码含义的改动,例如去掉空格、改变缩进、增删分号
11 | // build: 构造工具的或者外部依赖的改动,例如webpack,npm
12 | // refactor: 代码重构时使用
13 | // revert: 执行git revert打印的message
14 |
15 | // # 暂不使用type
16 | // test: 添加测试或者修改现有测试
17 | // perf: 提高性能的改动
18 | // ci: 与CI(持续集成服务)有关的改动
19 | // chore: 不修改src或者test的其余修改,例如构建过程或辅助工具的变动
20 |
21 | module.exports = {
22 | extends: ['@commitlint/config-conventional'],
23 | rules: {
24 | 'body-leading-blank': [2, 'always'],
25 | 'footer-leading-blank': [1, 'always'],
26 | 'header-max-length': [2, 'always', 108],
27 | 'subject-empty': [2, 'never'],
28 | 'type-empty': [2, 'never'],
29 | 'type-enum': [
30 | 2,
31 | 'always',
32 | [
33 | 'feat',
34 | 'fix',
35 | 'perf',
36 | 'style',
37 | 'docs',
38 | 'test',
39 | 'refactor',
40 | 'build',
41 | 'ci',
42 | 'chore',
43 | 'revert',
44 | 'wip',
45 | 'workflow',
46 | 'types',
47 | 'release'
48 | ]
49 | ]
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/components.d.ts:
--------------------------------------------------------------------------------
1 | // generated by unplugin-vue-components
2 | // We suggest you to commit this file into source control
3 | // Read more: https://github.com/vuejs/vue-next/pull/3399
4 | import '@vue/runtime-core'
5 |
6 | declare module '@vue/runtime-core' {
7 | export interface GlobalComponents {
8 | BreadCrumb: typeof import('./src/components/BreadCrumb.vue')['default']
9 | DocPopover: typeof import('./src/components/DocPopover.vue')['default']
10 | ElTable: typeof import('element-plus/es')['ElTable']
11 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
12 | Map: typeof import('./src/components/map/Map.vue')['default']
13 | Map1: typeof import('./src/components/map/Map1.vue')['default']
14 | MapBar: typeof import('./src/components/map/MapBar.vue')['default']
15 | MapOpPopover: typeof import('./src/components/map/MapOpPopover.vue')['default']
16 | MapRender: typeof import('./src/components/map/MapRender.vue')['default']
17 | Note: typeof import('./src/components/note/Note.vue')['default']
18 | NotePopover: typeof import('./src/components/note/NotePopover.vue')['default']
19 | ProfilePopover: typeof import('./src/components/ProfilePopover.vue')['default']
20 | Qrcode: typeof import('./src/components/Qrcode.vue')['default']
21 | RouterLink: typeof import('vue-router')['RouterLink']
22 | RouterView: typeof import('vue-router')['RouterView']
23 | Sider: typeof import('./src/components/Sider.vue')['default']
24 | SvgIcon: typeof import('./src/components/SvgIcon.vue')['default']
25 | }
26 | }
27 |
28 | export {}
29 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": [
6 | "src/*"
7 | ],
8 | "assets/*": [
9 | "src/assets/*"
10 | ],
11 | "components/*": [
12 | "src/components/*"
13 | ],
14 | "hooks/*": [
15 | "src/hooks/*"
16 | ],
17 | "store/*": [
18 | "src/store/*"
19 | ],
20 | "views/*": [
21 | "src/views/*"
22 | ]
23 | },
24 | },
25 | "include": [
26 | "src/**/*"
27 | ],
28 | "exclude": [
29 | "node_modules"
30 | ]
31 | }
--------------------------------------------------------------------------------
/note.md:
--------------------------------------------------------------------------------
1 | ### 出现的问题记录
2 | > vue3.x:route路径相同参数不同页面不刷新解决
3 | https://blog.csdn.net/AIB_Kasic/article/details/122985366
4 |
5 | > import router from '@/router/index'
6 | **useRoute, useRouter**必须写到setup中,详见vue-next-router.强行在函数中使用这两会报undefined,导致无法获取路由数据和路由方法
7 |
8 | > **for in** & **for of**
9 | - for in遍历的是数组的索引(即键名)。
10 | 而for of遍历的是数组元素值。
11 | - for in 是es5中有的,for of是es6的
12 |
13 | - for-in是为遍历对象而设计的,不适用于遍历数组。它可以正确响应break、continue和return语句
14 | for-in遍历数组的缺点:
15 | 因为for-in遍历的index值"0","1","2"等是字符串而不是数字
16 | for-in循环存在缺陷:会遍历对象自身的和继承的可枚举属性(不含Symbol属性)
17 |
18 | - for-of这个方法避开了for-in循环的所有缺陷
19 | 适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合
20 | 它可以正确响应break、continue和return语句
21 |
22 | **for in 得到的数组下标是字符串形式的**
23 |
24 | > vuex state持久化——vuex-persistedstate
25 |
26 | > 使用svg:https://blog.csdn.net/qq_37059838/article/details/108980970
27 | 如果需要修改svg的颜色,svg文件中的填充色fill必须删除
28 |
29 | > vue div contenteditable属性,模拟v-model双向数据绑定功能
30 | `
31 | `
32 |
33 | > TODO 数据更新后取消默认收起
34 | https://blog.csdn.net/qq_52151772/article/details/119756511
35 |
36 | > TODO 右键菜单
37 | https://www.cnblogs.com/gaofz/p/11995001.html
38 |
39 | > TODO 头像上传裁剪
40 | https://www.cnblogs.com/eightFlying/p/cropper-demo.html
41 |
42 | ### chunk-vendors太大了怎么办?
43 | - splitChunks:elementUI单独打包900kb--->300kb
44 | - publicPath: 'cdn' ----->干扰了路由:https://blog.csdn.net/weixin_29491885/article/details/119253898
45 | 解决:修改createWebHistory()
46 |
47 | ### 打包构建优化
48 | - 缩小文件检索解析范围 添加别名 避免无用的检索与递归遍历,使用alias指定引用时候的模块
49 | `config.resolve.alias.set('@', getAliasPath('src'))`
50 |
51 | ### Map数据流向修改
52 | - edit页集中维护 ---> map.store集中维护
53 | - 流向如何
54 |
55 | ### 优化措施
56 | - v-show替换v-if ---> 导图与Note切换时
57 | - svg相关
58 | - 封装SvgIcon组件 传入icon="name"属性方便使用
59 | - main.js 一次性引入所有svg图标 不用单独引入
60 | - svgo-loader 压缩了svg冗余内容 减小体积
61 | - 网络传输相关
62 | - DNS Prefetch
63 | ```js
64 |
65 |
66 |
67 | ```
68 |
69 | ### css书写顺序
70 | https://markdotto.com/2011/11/29/css-property-order/
71 | (1)定位属性:position display float left top right bottom overflow clear z-index
72 |
73 | (2)自身属性:width height padding border margin background
74 |
75 | (3)文字样式:font-family font-size font-style font-weight font-varient color
76 |
77 | (4)文本属性:text-align vertical-align text-wrap text-transform text-indent text-decoration letter-spacing word-spacing white-space text-overflow
78 |
79 | (5)css3中新增属性:content box-shadow border-radius transform……
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ZMindMap",
3 | "version": "0.2.3",
4 | "author": "zyascend@qq.com",
5 | "private": true,
6 | "scripts": {
7 | "serve": "vue-cli-service serve",
8 | "build": "vue-cli-service build",
9 | "report": "vue-cli-service build --report",
10 | "lint": "vue-cli-service lint",
11 | "inspect:prod": "vue-cli-service inspect > output_prod.js --mode production",
12 | "inspect:dev": "vue-cli-service inspect > output_dev.js --mode development",
13 | "stylelint:fix": "stylelint **/*.{vue,css,scss} --fix",
14 | "postinstall": "husky install",
15 | "commitlint": "commitlint --from=master",
16 | "lint:prettier": "npx prettier --write src/"
17 | },
18 | "lint-staged": {
19 | "**/*.{scss}": [
20 | "npm run stylelint:fix",
21 | "git add --force"
22 | ]
23 | },
24 | "dependencies": {
25 | "@sentry/tracing": "^6.19.7",
26 | "@sentry/vue": "^6.19.7",
27 | "axios": "^0.26.1",
28 | "core-js": "^3.6.5",
29 | "cropperjs": "^1.5.12",
30 | "d3-hierarchy": "^3.0.1",
31 | "d3-selection": "^3.0.0",
32 | "d3-shape": "^3.0.1",
33 | "d3-transition": "^3.0.1",
34 | "d3-zoom": "^3.0.0",
35 | "element-plus": "^2.1.11",
36 | "js-md5": "^0.7.3",
37 | "nanoid": "^3.3.3",
38 | "pinia": "^2.0.13",
39 | "pinia-plugin-persist": "^1.0.0",
40 | "save-svg-as-png": "^1.4.17",
41 | "socket.io-client": "^4.5.1",
42 | "vue": "^3.2.31",
43 | "vue-qr": "^4.0.6",
44 | "vue-router": "^4.0.0-0",
45 | "xss": "^1.0.11"
46 | },
47 | "devDependencies": {
48 | "@commitlint/cli": "^16.2.4",
49 | "@commitlint/config-conventional": "^16.2.4",
50 | "@sentry/webpack-plugin": "^1.18.9",
51 | "@types/js-md5": "^0.4.3",
52 | "@vue/cli-plugin-babel": "~4.5.0",
53 | "@vue/cli-plugin-eslint": "~4.5.0",
54 | "@vue/cli-plugin-router": "~4.5.0",
55 | "@vue/cli-plugin-vuex": "~4.5.0",
56 | "@vue/cli-service": "~4.5.0",
57 | "@vue/compiler-sfc": "^3.0.0",
58 | "@vue/eslint-config-standard": "^5.1.2",
59 | "babel-eslint": "^10.1.0",
60 | "babel-plugin-import": "^1.13.5",
61 | "compression-webpack-plugin": "^5.0.2",
62 | "esbuild-loader": "^2.19.0",
63 | "eslint": "^7.32.0",
64 | "eslint-config-airbnb-base": "^15.0.0",
65 | "eslint-config-prettier": "^8.5.0",
66 | "eslint-plugin-import": "^2.20.2",
67 | "eslint-plugin-node": "^11.1.0",
68 | "eslint-plugin-prettier": "^4.2.1",
69 | "eslint-plugin-promise": "^4.2.1",
70 | "eslint-plugin-standard": "^4.0.0",
71 | "eslint-plugin-vue": "^8.7.1",
72 | "hard-source-webpack-plugin": "^0.13.1",
73 | "husky": "^8.0.1",
74 | "lint-staged": "^12.4.1",
75 | "node-sass": "^7.0.1",
76 | "pre-commit": "^1.2.2",
77 | "prettier": "^2.7.1",
78 | "sass": "^1.26.5",
79 | "sass-loader": "^8.0.2",
80 | "speed-measure-webpack-plugin": "^1.5.0",
81 | "stylelint": "^13.13.1",
82 | "stylelint-config-prettier": "^8.0.2",
83 | "stylelint-config-recess-order": "^2.4.0",
84 | "stylelint-config-standard": "^22.0.0",
85 | "svg-sprite-loader": "^6.0.11",
86 | "svgo-loader": "^3.0.0",
87 | "unplugin-auto-import": "^0.7.1",
88 | "unplugin-element-plus": "^0.4.0",
89 | "unplugin-vue-components": "^0.22.4",
90 | "worker-loader": "^3.0.8"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | <%= htmlWebpackPlugin.options.title %>
14 |
37 |
47 |
48 |
49 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
--------------------------------------------------------------------------------
/src/assets/css/handler.scss:
--------------------------------------------------------------------------------
1 | @import "./themes.scss";
2 | @import "./mixin";
3 |
4 | //遍历主题map
5 | @mixin themeify {
6 | @each $theme-name, $theme-map in $themes {
7 | // !global 把局部变量强升为全局变量
8 | $theme-map: $theme-map !global;
9 | //判断html的data-theme的属性值 #{}是sass的插值表达式
10 | //& sass嵌套里的父容器标识 @content是混合器插槽,像vue的slot
11 | [data-theme="#{$theme-name}"] & {
12 | @content;
13 | }
14 | }
15 | }
16 |
17 | //声明一个根据Key获取颜色的function
18 | @function themed($key) {
19 | @return map-get($theme-map, $key);
20 | }
21 |
22 | //获取背景颜色
23 | @mixin background_color($color) {
24 | @include themeify {
25 | background-color: themed($color) !important;
26 | }
27 | }
28 |
29 | //获取字体颜色
30 | @mixin font_color($color) {
31 | @include themeify {
32 | color: themed($color) !important;
33 | }
34 | }
35 |
36 | //获取字体颜色
37 | @mixin fill_color($color) {
38 | @include themeify {
39 | fill: themed($color) !important;
40 | }
41 | }
42 |
43 | //获取边框颜色
44 | @mixin border_color($color) {
45 | @include themeify {
46 | border-color: themed($color) !important;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/assets/css/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin fullFixed {
2 | position: fixed;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | }
8 |
9 | @mixin wh100 {
10 | width: 100%;
11 | height: 100%;
12 | }
13 |
14 | @mixin vertFlex {
15 | display: flex;
16 | flex-direction: column;
17 | }
18 |
19 | @mixin horiFlex {
20 | display: flex;
21 | flex-direction: row;
22 | }
23 |
24 | @mixin centerFlex {
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 | }
29 |
30 | @mixin wh($w, $h) {
31 | width: $w;
32 | height: $h;
33 | }
34 |
35 | @mixin abs-center {
36 | position: absolute;
37 | top: 50%;
38 | left: 50%;
39 | transform: translate(-50%, -50%);
40 | }
41 | $color-base: #5856d5
42 |
--------------------------------------------------------------------------------
/src/assets/css/reset.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable comment-empty-line-before */
2 | html,
3 | body,
4 | div,
5 | span,
6 | applet,
7 | object,
8 | iframe,
9 | h1,
10 | h2,
11 | h3,
12 | h4,
13 | h5,
14 | h6,
15 | p,
16 | blockquote,
17 | pre,
18 | a,
19 | abbr,
20 | acronym,
21 | address,
22 | big,
23 | cite,
24 | code,
25 | del,
26 | dfn,
27 | em,
28 | img,
29 | ins,
30 | kbd,
31 | q,
32 | s,
33 | samp,
34 | small,
35 | strike,
36 | strong,
37 | sub,
38 | sup,
39 | tt,
40 | var,
41 | b,
42 | u,
43 | i,
44 | center,
45 | dl,
46 | dt,
47 | dd,
48 | ol,
49 | ul,
50 | li,
51 | fieldset,
52 | form,
53 | label,
54 | legend,
55 | table,
56 | caption,
57 | tbody,
58 | tfoot,
59 | thead,
60 | tr,
61 | th,
62 | td,
63 | article,
64 | aside,
65 | canvas,
66 | details,
67 | embed,
68 | figure,
69 | figcaption,
70 | footer,
71 | header,
72 | hgroup,
73 | menu,
74 | nav,
75 | output,
76 | ruby,
77 | section,
78 | summary,
79 | time,
80 | mark,
81 | audio,
82 | video {
83 | box-sizing: border-box;
84 | padding: 0;
85 | margin: 0;
86 | font: inherit;
87 | font-size: 100%;
88 | vertical-align: baseline;
89 | user-select: none;
90 | border: 0;
91 | outline: 0;
92 | }
93 |
94 | /* HTML5 display-role reset for older browsers */
95 | article,
96 | aside,
97 | details,
98 | figcaption,
99 | figure,
100 | footer,
101 | header,
102 | hgroup,
103 | menu,
104 | nav,
105 | section {
106 | display: block;
107 | }
108 |
109 | ol,
110 | ul {
111 | list-style: none;
112 | }
113 |
114 | blockquote,
115 | q {
116 | quotes: none;
117 | }
118 |
119 | blockquote::before,
120 | blockquote::after,
121 | q::before,
122 | q::after {
123 | content: '';
124 | content: none;
125 | }
126 |
127 | table {
128 | border-spacing: 0;
129 | border-collapse: collapse;
130 | }
131 |
132 | body {
133 | width: 100%;
134 | height: 100%;
135 | /* overflow: hidden; */
136 | line-height: 1;
137 | }
138 |
139 | html {
140 | width: 100%;
141 | height: 100%;
142 | /* overflow: hidden; */
143 | }
144 |
145 | a,
146 | button {
147 | text-decoration: none;
148 | cursor: pointer;
149 | }
150 | input:-webkit-autofill,
151 | input:-webkit-autofill:hover,
152 | input:-webkit-autofill:focus,
153 | input:-webkit-autofill:active {
154 | -webkit-transition-delay: 111111s;
155 | -webkit-transition: color 11111s ease-out, background-color 111111s ease-out;
156 | }
157 | /* div::-webkit-scrollbar { width: 0 !important } */
158 |
--------------------------------------------------------------------------------
/src/assets/css/themes.scss:
--------------------------------------------------------------------------------
1 | $themes: (
2 | light: (
3 | //字体
4 | fc_pop: #1d1d1f,
5 | fc_normal: #1d1d1f,
6 | fc_nickname: #1d1d1f,
7 | fc_girdtable: #1d1d1f,
8 | fc_bread_active: #1d1d1f,
9 | fc_side_link: #75757d,
10 | fc_tree_node: #75757d,
11 | fc_side_link_hover: #5856d5,
12 | fc_side_link_active: #5856d5,
13 | fc_input: #92929c,
14 | fc_grid: #2c2c2f,
15 | fc_bread: #92929c,
16 | //背景
17 | bc_sidebar: #f4f4f5,
18 | bc_content: #fff,
19 | bc_collapser: #fff,
20 | bc_divider: #e9e9eb,
21 | bc_popover: #fff,
22 | bc_hover_nickname: #e9e9eb,
23 | bc_griditem_hover: #f4f4f5,
24 | bc_td_hover: #f5f7fa,
25 | bc_side_link_hover: #deddf7,
26 | bc_side_link_active: #deddf7,
27 | bc_tree_node: #f5f7fa,
28 | bc_tree_node_hover: #e9e9eb,
29 | bc_pop_hover: #f7f7f7,
30 | bc_input: #e9e9eb,
31 | bc_adder: #2c2c2f,
32 | bc_dialog_left: #f4f4f5,
33 | bc_dialog_right: #fff,
34 | bc_avatar_btn_hover:#f8f8fd,
35 | // border
36 | bdc_grid: #dedee1,
37 | bdc_show_btn: #c9c9ce,
38 | bdc_table_divider: #dedee1,
39 | bdc_dialog_left: #dedee1,
40 | ),
41 | dark: (
42 | //字体
43 | fc_normal: #f4f4f5,
44 | fc_pop: #f4f4f5,
45 | fc_nickname: #f4f4f5,
46 | fc_bread_active: #f4f4f5,
47 | fc_side_link_hover: #f4f4f5,
48 | fc_side_link_active: #f4f4f5,
49 | fc_bread: #9d9da6,
50 | fc_input: #9d9da6,
51 | fc_grid: #fff,
52 | fc_girdtable: #c9c9ce,
53 | fc_side_link: #c9c9ce,
54 | fc_tree_node: #dedee1,
55 | //背景
56 | bc_sidebar: #2c2c2f,
57 | bc_content: #1d1d1f,
58 | bc_collapser: #1d1d1f,
59 | bc_divider: #66666d,
60 | bc_popover: #49494e,
61 | bc_hover_nickname: transparent,
62 | bc_griditem_hover: #49494e80,
63 | bc_td_hover: #49494e80,
64 | bc_side_link_hover: #5856d5,
65 | bc_side_link_active: #5856d5,
66 | bc_tree_node: #f5f7fa,
67 | bc_tree_node_hover: #49494e,
68 | bc_pop_hover: #ffffff0f,
69 | bc_input: #49494e,
70 | bc_adder: #5856d5,
71 | bc_dialog_left: #2c2c2f,
72 | bc_dialog_right: #49494e,
73 | bc_avatar_btn_hover: #25245b,
74 | // border
75 | bdc_grid: transparent,
76 | bdc_show_btn: #696a6b,
77 | bdc_table_divider: #66666d,
78 | bdc_dialog_left: #66666d,
79 | ),
80 | );
81 |
--------------------------------------------------------------------------------
/src/assets/map/add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/map/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/map/loading.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/assets/pic/404.svg:
--------------------------------------------------------------------------------
1 |
78 |
--------------------------------------------------------------------------------
/src/assets/pic/add-file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/add-quick.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/add.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/assets/pic/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/src/assets/pic/empty.png
--------------------------------------------------------------------------------
/src/assets/pic/file-large.svg:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/src/assets/pic/file-small.svg:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/src/assets/pic/fit-view.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/folder-large.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/src/assets/pic/folder.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/src/assets/pic/grid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/hamberger.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/latest.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/loading.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/assets/pic/logout.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/marker.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/more.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/note.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/pc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/src/assets/pic/pc.png
--------------------------------------------------------------------------------
/src/assets/pic/qrcode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/src/assets/pic/qrcode.png
--------------------------------------------------------------------------------
/src/assets/pic/quick.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/src/assets/pic/refresh.png
--------------------------------------------------------------------------------
/src/assets/pic/remove.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/rename.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/pic/settings.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/skin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zyascend/ZMindMap/54c169b9d6293502aefdeabf29e9cc31c5e988c0/src/assets/pic/success.png
--------------------------------------------------------------------------------
/src/assets/pic/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/trash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/triangle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/upload.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/pic/workwith.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/DocPopover.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 新建文件夹
16 |
17 |
18 |
19 | 新建文件
20 |
21 |
22 |
23 | 重命名
24 |
25 |
26 |
27 | 添加到快捷访问
28 |
29 |
30 |
31 | 删除
32 |
33 |
34 |
41 | 确认删除此文档吗?
42 |
43 |
49 |
50 |
51 |
58 |
63 |
64 |
68 |
69 |
70 |
71 |
72 |
155 |
156 |
211 |
--------------------------------------------------------------------------------
/src/components/SvgIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
33 |
34 |
40 |
--------------------------------------------------------------------------------
/src/components/map/Map-beta.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
57 |
--------------------------------------------------------------------------------
/src/components/map/Map.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
41 |
--------------------------------------------------------------------------------
/src/components/map/MapBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
{{ marker.category }}
28 |
29 |
34 |
![marker]()
35 |
36 |
37 |
38 |
39 |
40 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
63 |
![color]()
64 |
65 |
66 |
67 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
90 |
![map]()
91 |
92 |
93 |
94 |
95 |
96 |
97 |
142 |
143 |
266 |
--------------------------------------------------------------------------------
/src/components/map/MapOpPopover.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 导出为图片
16 |
17 |
18 |
19 |
20 |
72 |
73 |
124 |
--------------------------------------------------------------------------------
/src/components/note/NotePopover.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
17 |
18 |
19 |
20 | 新建文件夹
21 |
22 |
23 |
24 | 新建文件
25 |
26 |
27 |
28 |
34 | A
35 |
36 |
37 |
38 |
39 |
40 | 删除
41 |
42 |
43 |
44 |
45 |
92 |
93 |
174 |
--------------------------------------------------------------------------------
/src/hooks/http/apis.js:
--------------------------------------------------------------------------------
1 | const apis = {
2 | register: '/users/register',
3 | login: '/users/login',
4 | editProfile: 'users/editProfile',
5 | getCurrentUser: 'users/getUser',
6 |
7 | getCode: '/code/generate',
8 | getCodeStatus: '/code/getStatus',
9 |
10 | getAllDocs: 'docs/getAllDocs',
11 | getDocContent: 'docs/getDocContent',
12 | setFolder: 'docs/setFolder',
13 | setDoc: 'docs/setDoc',
14 | setDocContent: 'docs/setDocContent',
15 | remove: 'docs/remove',
16 | uploadImg: 'docs/uploadImg',
17 |
18 | getStyles: 'styles'
19 | }
20 |
21 | export default apis
22 |
--------------------------------------------------------------------------------
/src/hooks/http/axios.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import axios from 'axios'
3 | import { ErrorTip } from '@/hooks/utils'
4 |
5 | /**
6 | * 跳转登录页
7 | * 携带当前页面路由,在登录页面完成登录后返回当前页面
8 | */
9 | const toLoginPage = () => {
10 | localStorage.clear()
11 | window.location.replace(`/login?redirect=${window.location.pathname}`)
12 | }
13 |
14 | const errorMap = {
15 | 401: toLoginPage,
16 | 403: () => ErrorTip('操作失败 无权限'),
17 | 404: msg => {
18 | ErrorTip(msg?.error || '【404】找不到资源')
19 | }
20 | }
21 |
22 | /**
23 | * 请求失败后的错误统一处理
24 | * @param {Number} status 请求失败的状态码
25 | */
26 | const errorHandle = (status, msg) => {
27 | // 状态码判断
28 | const handler = errorMap[status]
29 | if (handler) handler(msg)
30 | else {
31 | ErrorTip('系统错误 请稍后再试')
32 | }
33 | }
34 |
35 | // 创建axios实例
36 | const isPrd = process.env.NODE_ENV === 'production'
37 | const { apiCfg } = window.CFG
38 | const instance = axios.create({
39 | timeout: 1000 * 12,
40 | baseURL: isPrd ? apiCfg.prdHost : apiCfg.devHost
41 | })
42 | // 设置post请求头
43 | instance.defaults.headers.post['Content-Type'] = 'application/json'
44 | /**
45 | * 请求拦截器
46 | * 每次请求前,如果存在token则在请求头中携带token
47 | */
48 | instance.interceptors.request.use(
49 | config => {
50 | const token = localStorage.getItem('token') || ''
51 | token && (config.headers.Authorization = `Bearer ${token}`)
52 | return config
53 | },
54 | error => Promise.error(error)
55 | )
56 |
57 | // 响应拦截器
58 | instance.interceptors.response.use(
59 | // 请求成功
60 | res => (res.status === 200 ? Promise.resolve(res.data) : Promise.reject(res)),
61 | // 请求失败
62 | error => {
63 | const { response } = error
64 | if (response) {
65 | // 请求已发出,但是不在2xx的范围
66 | errorHandle(response.status, response.data)
67 | return Promise.reject(response)
68 | }
69 | // TODO 处理断网的情况
70 | return Promise.reject(error)
71 | }
72 | )
73 |
74 | export default instance
75 |
--------------------------------------------------------------------------------
/src/hooks/http/index.js:
--------------------------------------------------------------------------------
1 | import axios from './axios'
2 | import apis from './apis'
3 |
4 | const asyncHttp = async (
5 | url,
6 | config = { method: 'get' },
7 | extraData = undefined
8 | ) => {
9 | const [error, result] = await axios(url, config)
10 | .then(res => [null, res])
11 | .catch(err => [err, null])
12 | if (error || !result) {
13 | return null
14 | }
15 | return result.data
16 | }
17 |
18 | const withUserIdUrl = url => {
19 | const { user } = JSON.parse(localStorage.getItem('zmindmap_user'))
20 | return `${url}/${user?._id}`
21 | }
22 |
23 | const normalGet = async (url, withUserId = true) => {
24 | let res
25 | if (withUserId) {
26 | res = await asyncHttp(withUserIdUrl(url))
27 | } else {
28 | res = await asyncHttp(url)
29 | }
30 | return res
31 | }
32 |
33 | const normalPost = async (url, data, withUserId = true) => {
34 | let requestUrl
35 | if (withUserId) {
36 | requestUrl = withUserIdUrl(url)
37 | } else {
38 | requestUrl = url
39 | }
40 | const res = await asyncHttp(requestUrl, {
41 | method: 'post',
42 | data
43 | })
44 | return res
45 | }
46 |
47 | export const docApi = {
48 | fetchAllDocuments: async () => {
49 | const res = await normalGet(apis.getAllDocs)
50 | return res
51 | },
52 | postSetFolder: async data => {
53 | const res = await normalPost(apis.setFolder, data)
54 | return res
55 | },
56 | postSetDoc: async data => {
57 | const res = await normalPost(apis.setDoc, data)
58 | return res
59 | },
60 | postRemove: async data => {
61 | const res = await normalPost(apis.remove, data)
62 | return res
63 | }
64 | }
65 | export const mapApi = {
66 | fetchMap: async docId => {
67 | const url = `${withUserIdUrl(apis.getDocContent)}/${docId}`
68 | const res = await normalGet(url, false)
69 | return res
70 | },
71 | remoteUpdateMap: async data => {
72 | const res = await normalPost(apis.setDocContent, data)
73 | return res
74 | },
75 | uploadImg: async config => {
76 | const res = await asyncHttp(withUserIdUrl(apis.uploadImg), {
77 | method: 'post',
78 | ...config
79 | })
80 | return res
81 | }
82 | }
83 |
84 | export const userApi = {
85 | login: async data => {
86 | const res = await normalPost(apis.login, data, false)
87 | return res
88 | },
89 | register: async data => {
90 | const res = await normalPost(apis.register, data, false)
91 | return res
92 | },
93 | updateUser: async config => {
94 | const res = await asyncHttp(withUserIdUrl(apis.editProfile), {
95 | method: 'post',
96 | ...config
97 | })
98 | return res
99 | },
100 | generateCode: async () => {
101 | const res = await normalGet(apis.getCode, false)
102 | return res
103 | },
104 | getCodeStatus: async qid => {
105 | const res = await normalGet(`${apis.getCodeStatus}?qid=${qid}`, false)
106 | return res
107 | }
108 | }
109 |
110 | export const websiteApi = {
111 | fetchMapStyles: async () => {
112 | const res = await normalGet(apis.getStyles, false)
113 | return res
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/hooks/map.worker.js:
--------------------------------------------------------------------------------
1 | import md5 from 'js-md5'
2 |
3 | onmessage = function (e) {
4 | const { source, type } = e.data
5 | let result
6 | switch (type) {
7 | case 'MD5':
8 | result = md5(source)
9 | break
10 | default:
11 | break
12 | }
13 | postMessage({ type, result })
14 | }
15 |
--------------------------------------------------------------------------------
/src/hooks/map/LogicTree-beta.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import { linkHorizontal } from 'd3-shape'
3 | import Tree from './Tree'
4 |
5 | export default class LogicTree extends Tree {
6 | constructor(nodeMap) {
7 | super()
8 | this.defaultWidth = 30
9 | this.maxWidth = 250
10 | this.defaultHeight = 40
11 | this.defaultRootHeight = 60
12 | this.padding = 10
13 | this.defaultMarkerHeight = 18
14 | this.defaultMarkerWidth = 18
15 | this.markerOverlap = 7
16 | this.textMarkersGap = 10
17 | this.gapY = 20
18 | this.gapX = 40
19 | this.rectRadius = 5
20 | this.strokeWidth = 0
21 |
22 | this.map = nodeMap
23 | console.log('map ', this.map)
24 |
25 | this.bézierCurveGenerator = linkHorizontal()
26 | .x(d => d.x)
27 | .y(d => d.y)
28 | }
29 |
30 | create(root) {
31 | console.time('TIME WH')
32 | this.measureWidthAndHeight(root)
33 | console.timeEnd('TIME WH')
34 | console.time('TIME XY')
35 | this.calculateXY(root)
36 | console.timeEnd('TIME XY')
37 | console.time('TIME PATH')
38 | const paths = this.calculatePath(root)
39 | console.timeEnd('TIME PATH')
40 | return {
41 | paths,
42 | root
43 | }
44 | }
45 |
46 | measureWidthAndHeight(root) {
47 | // 后续遍历 初步计算 父依赖于子
48 | root.eachAfter(node => {
49 | const { id } = node.data
50 | const preNode = this.map.get(id)
51 | if (!preNode) {
52 | console.log('新增的节点')
53 | // 新增的节点
54 | if (node.parent) {
55 | node.parent.data.patchFlags = 1
56 | }
57 | this.measureTextSize(node)
58 | this.measureMarkers(node)
59 | this.measureWH(node)
60 | } else {
61 | // 是否需要重新计算
62 | const flag = root.data?.patchFlags ?? 0
63 | const needUpdate = flag > 0
64 | if (needUpdate) {
65 | console.log('重新计算 > ', root.data.name)
66 | // 需要 那么父节点也需要
67 | if (node.parent) {
68 | node.parent.data.patchFlags = 1
69 | }
70 | // 重新计算
71 | this.measureTextSize(node)
72 | this.measureMarkers(node)
73 | this.measureWH(node)
74 | } else {
75 | // 不需要重新计算
76 | console.log('不需要重新计算 > ', root.data.name)
77 | root = preNode
78 | }
79 | }
80 | })
81 | }
82 |
83 | measureWH(node) {
84 | node.rectRadius = this.rectRadius
85 | node.strokeWidth = this.strokeWidth
86 |
87 | node.outLineOffset = 0
88 |
89 | const tmGap = node.mw ? this.textMarkersGap : 0
90 | const tiGap = node.ih ? this.textMarkersGap : 0
91 | node.cw = Math.max(
92 | Math.max(node.tw, node.iw) + node.mw + this.padding * 2 + tmGap,
93 | this.defaultWidth
94 | )
95 | node.ch = Math.max(
96 | this.padding * 2 + node.ih + tiGap + node.th,
97 | this.defaultHeight
98 | )
99 | const { children } = node
100 | if (!children) {
101 | node.w = node.cw
102 | node.h = node.ch
103 | } else {
104 | const maxW = Math.max(...children.map(c => c.w))
105 | const sumH = children.reduce((p, c) => p + c.h, 0)
106 | node.h = sumH + this.gapY * (children.length - 1)
107 | node.w = node.cw + this.gapX + maxW
108 | }
109 |
110 | node.outLineW = node.cw - node.outLineOffset * 2
111 | node.outLineH = node.ch - node.outLineOffset * 2
112 | }
113 |
114 | calculateInnerXY(node) {
115 | const { mw, th, mh, ch } = node
116 | node.mx = this.padding
117 | node.tx = node.mx + mw + (mw ? this.textMarkersGap : 0)
118 | node.ty = ch - this.padding - th - 4
119 | node.my = node.ty + th / 2 - mh / 2 + 4
120 |
121 | node.ix = node.tx
122 | node.iy = this.padding
123 | }
124 |
125 | calculateXY(root) {
126 | let anchor
127 | // 前序遍历 计算X
128 | root.eachBefore(node => {
129 | this.calculateInnerXY(node)
130 | const { depth } = node
131 | if (depth === 0) {
132 | node.x = 140
133 | anchor = node
134 | return
135 | }
136 | const { depth: lastDepth, cw, x } = anchor
137 | if (depth === lastDepth) {
138 | node.x = x
139 | } else if (depth > lastDepth) {
140 | node.x = x + cw + this.gapX
141 | } else {
142 | const bro = this.findPrevBrother(node)
143 | node.x = bro.x
144 | }
145 | anchor = node
146 | })
147 | // 后序遍历 计算Y
148 | anchor = undefined
149 | root.eachAfter(node => {
150 | const { depth } = node
151 | if (!anchor) {
152 | node.y = 100
153 | anchor = node
154 | return
155 | }
156 | const { depth: lastDepth, ch, y } = anchor
157 | if (depth < lastDepth) {
158 | const firstChild = node.children[0]
159 | node.y = firstChild.y + (y - firstChild.y + ch) / 2 - node.ch / 2
160 | // ![BUG]父节点很高 超过了第一个子节点
161 | if (node.y - node.ch / 2 < firstChild.y - firstChild.ch / 2) {
162 | // !TODO 还需要递归的处理该子节点的所有子代
163 | // !F**K 麻烦啊
164 | node.y = firstChild.y + 1
165 | }
166 | } else {
167 | const bottom = this.findBottom(anchor)
168 | node.y = Math.max(bottom.y + bottom.ch + this.gapY, y + ch + this.gapY)
169 | }
170 | anchor = node
171 | })
172 | }
173 |
174 | /**
175 | * 在已经计算过的节点中寻找与之最靠近的节点
176 | * @param {*} node
177 | * @returns
178 | */
179 | findBottom(node) {
180 | let bottom = node
181 | while (bottom?.children) {
182 | bottom = bottom.children[bottom.children.length - 1]
183 | }
184 | return bottom
185 | }
186 |
187 | calculatePath(root) {
188 | const links = root.links()
189 | const paths = links.map(l => this.getPathData(l))
190 | return paths
191 | }
192 |
193 | getPathData(link) {
194 | const { source, target } = link
195 | const { x: sx, y: sy, cw, ch: sh, id: sid } = source
196 | const { x: tx, y: ty, ch, id: tid } = target
197 | // 生成从一个源点到目标点的光滑的三次贝塞尔曲线
198 | const bezierLine = this.bézierCurveGenerator({
199 | source: {
200 | x: sx + cw,
201 | y: sy + sh / 2
202 | },
203 | target: {
204 | x: tx,
205 | y: ty + ch / 2
206 | }
207 | })
208 | return {
209 | data: bezierLine,
210 | id: `path-${sid}-${tid}`
211 | }
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/src/hooks/map/LogicTree.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import { linkHorizontal } from 'd3-shape'
3 | import Tree from './Tree'
4 |
5 | export default class LogicTree extends Tree {
6 | constructor() {
7 | super()
8 | this.defaultWidth = 30
9 | this.maxWidth = 250
10 | this.defaultHeight = 40
11 | this.defaultRootHeight = 60
12 | this.padding = 10
13 | this.defaultMarkerHeight = 18
14 | this.defaultMarkerWidth = 18
15 | this.markerOverlap = 7
16 | this.textMarkersGap = 10
17 | this.gapY = 20
18 | this.gapX = 40
19 | this.rectRadius = 5
20 | this.strokeWidth = 0
21 |
22 | this.bézierCurveGenerator = linkHorizontal()
23 | .x(d => d.x)
24 | .y(d => d.y)
25 | }
26 |
27 | create(root) {
28 | const tagWH = '[TIME] 计算宽高耗时: '
29 | const tagXY = '[TIME] 计算位置耗时: '
30 | const tagPATH = '[TIME] 计算路径耗时: '
31 | const tagAll = '[TIME] 计算总耗时: '
32 | console.time(tagAll)
33 |
34 | console.time(tagWH)
35 | this.measureWidthAndHeight(root)
36 | console.timeEnd(tagWH)
37 |
38 | console.time(tagXY)
39 | this.calculateXY(root)
40 | console.timeEnd(tagXY)
41 |
42 | console.time(tagPATH)
43 | const paths = this.calculatePath(root)
44 | console.timeEnd(tagPATH)
45 |
46 | console.timeEnd(tagAll)
47 | return {
48 | paths,
49 | nodes: root.descendants()
50 | }
51 | }
52 |
53 | measureWidthAndHeight(root) {
54 | // 后续遍历 初步计算 父依赖于子
55 | root.eachAfter(node => {
56 | this.measureImageSize(node)
57 | this.measureTextSize(node)
58 | this.measureMarkers(node)
59 | this.measureWH(node)
60 | })
61 | }
62 |
63 | measureWH(node) {
64 | node.rectRadius = this.rectRadius
65 | node.strokeWidth = this.strokeWidth
66 |
67 | node.outLineOffset = 0
68 |
69 | const tmGap = node.mw ? this.textMarkersGap : 0
70 | const tiGap = node.ih ? this.textMarkersGap : 0
71 | node.cw = Math.max(
72 | Math.max(node.tw, node.iw) + node.mw + this.padding * 2 + tmGap,
73 | this.defaultWidth
74 | )
75 | node.ch = Math.max(
76 | this.padding * 2 + node.ih + tiGap + node.th,
77 | this.defaultHeight
78 | )
79 | const { children } = node
80 | if (!children) {
81 | node.w = node.cw
82 | node.h = node.ch
83 | } else {
84 | const maxW = Math.max(...children.map(c => c.w))
85 | const sumH = children.reduce((p, c) => p + c.h, 0)
86 | node.h = sumH + this.gapY * (children.length - 1)
87 | node.w = node.cw + this.gapX + maxW
88 | }
89 |
90 | node.outLineW = node.cw - node.outLineOffset * 2
91 | node.outLineH = node.ch - node.outLineOffset * 2
92 | }
93 |
94 | calculateInnerXY(node) {
95 | const { mw, th, mh, ch } = node
96 | node.mx = this.padding
97 | node.tx = node.mx + mw + (mw ? this.textMarkersGap : 0)
98 | node.ty = ch - this.padding - th - 4
99 | node.my = node.ty + th / 2 - mh / 2 + 4
100 |
101 | node.ix = node.tx
102 | node.iy = this.padding
103 | }
104 |
105 | calculateXY(root) {
106 | let anchor
107 | // 前序遍历 计算X
108 | root.eachBefore(node => {
109 | this.calculateInnerXY(node)
110 | const { depth } = node
111 | if (depth === 0) {
112 | node.x = 140
113 | anchor = node
114 | return
115 | }
116 | const { depth: lastDepth, cw, x } = anchor
117 | if (depth === lastDepth) {
118 | node.x = x
119 | } else if (depth > lastDepth) {
120 | node.x = x + cw + this.gapX
121 | } else {
122 | const bro = this.findPrevBrother(node)
123 | node.x = bro.x
124 | }
125 | anchor = node
126 | })
127 | // 后序遍历 计算Y
128 | anchor = undefined
129 | root.eachAfter(node => {
130 | const { depth } = node
131 | if (!anchor) {
132 | node.y = 100
133 | anchor = node
134 | return
135 | }
136 | const { depth: lastDepth, ch, y } = anchor
137 | if (depth < lastDepth) {
138 | const firstChild = node.children[0]
139 | node.y = firstChild.y + (y - firstChild.y + ch) / 2 - node.ch / 2
140 | // ![BUG]父节点很高 超过了第一个子节点
141 | if (node.y - node.ch / 2 < firstChild.y - firstChild.ch / 2) {
142 | // !TODO 还需要递归的处理该子节点的所有子代
143 | // !F**K 麻烦啊
144 | node.y = firstChild.y + 1
145 | }
146 | } else {
147 | const bottom = this.findBottom(anchor)
148 | node.y = Math.max(bottom.y + bottom.ch + this.gapY, y + ch + this.gapY)
149 | }
150 | anchor = node
151 | })
152 | }
153 |
154 | /**
155 | * 在已经计算过的节点中寻找与之最靠近的节点
156 | * @param {*} node
157 | * @returns
158 | */
159 | findBottom(node) {
160 | let bottom = node
161 | while (bottom?.children) {
162 | bottom = bottom.children[bottom.children.length - 1]
163 | }
164 | return bottom
165 | }
166 |
167 | calculatePath(root) {
168 | const links = root.links()
169 | const paths = links.map(l => this.getPathData(l))
170 | return paths
171 | }
172 |
173 | getPathData(link) {
174 | const { source, target } = link
175 | const { x: sx, y: sy, cw, ch: sh, id: sid } = source
176 | const { x: tx, y: ty, ch, id: tid } = target
177 | // 生成从一个源点到目标点的光滑的三次贝塞尔曲线
178 | const bezierLine = this.bézierCurveGenerator({
179 | source: {
180 | x: sx + cw,
181 | y: sy + sh / 2
182 | },
183 | target: {
184 | x: tx,
185 | y: ty + ch / 2
186 | }
187 | })
188 | return {
189 | data: bezierLine,
190 | id: `path-${sid}-${tid}`
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/hooks/map/LogicTree1.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import { linkHorizontal } from 'd3-shape'
3 | import Tree from './Tree'
4 |
5 | export default class LogicTree extends Tree {
6 | constructor(nodeMap) {
7 | super()
8 | this.defaultWidth = 30
9 | this.maxWidth = 250
10 | this.defaultHeight = 40
11 | this.defaultRootHeight = 60
12 | this.padding = 10
13 | this.defaultMarkerHeight = 18
14 | this.defaultMarkerWidth = 18
15 | this.markerOverlap = 7
16 | this.textMarkersGap = 10
17 | this.gapY = 20
18 | this.gapX = 40
19 | this.rectRadius = 5
20 | this.strokeWidth = 0
21 |
22 | this.map = nodeMap
23 | console.log('map ', this.map)
24 |
25 | this.bézierCurveGenerator = linkHorizontal()
26 | .x(d => d.x)
27 | .y(d => d.y)
28 | }
29 |
30 | create(root) {
31 | console.time('TIME WH')
32 | this.measureWidthAndHeight(root)
33 | console.timeEnd('TIME WH')
34 | console.time('TIME XY')
35 | this.calculateXY(root)
36 | console.timeEnd('TIME XY')
37 | console.time('TIME PATH')
38 | const paths = this.calculatePath(root)
39 | console.timeEnd('TIME PATH')
40 | return {
41 | paths,
42 | root
43 | }
44 | }
45 |
46 | measureWidthAndHeight(root) {
47 | // 后续遍历 初步计算 父依赖于子
48 | root.eachAfter(node => {
49 | const { id } = node.data
50 | const preNode = this.map.get(id)
51 | if (!preNode) {
52 | console.log('新增的节点')
53 | // 新增的节点
54 | if (node.parent) {
55 | node.parent.data.patchFlags = 1
56 | }
57 | this.measureTextSize(node)
58 | this.measureMarkers(node)
59 | this.measureWH(node)
60 | } else {
61 | // 是否需要重新计算
62 | const flag = root.data?.patchFlags ?? 0
63 | const needUpdate = flag > 0
64 | if (needUpdate) {
65 | console.log('重新计算 > ', root.data.name)
66 | // 需要 那么父节点也需要
67 | if (node.parent) {
68 | node.parent.data.patchFlags = 1
69 | }
70 | // 重新计算
71 | this.measureTextSize(node)
72 | this.measureMarkers(node)
73 | this.measureWH(node)
74 | } else {
75 | // 不需要重新计算
76 | console.log('不需要重新计算 > ', root.data.name)
77 | root = preNode
78 | }
79 | }
80 | })
81 | }
82 |
83 | measureWH(node) {
84 | node.rectRadius = this.rectRadius
85 | node.strokeWidth = this.strokeWidth
86 |
87 | node.outLineOffset = 0
88 |
89 | const tmGap = node.mw ? this.textMarkersGap : 0
90 | const tiGap = node.ih ? this.textMarkersGap : 0
91 | node.cw = Math.max(
92 | Math.max(node.tw, node.iw) + node.mw + this.padding * 2 + tmGap,
93 | this.defaultWidth
94 | )
95 | node.ch = Math.max(
96 | this.padding * 2 + node.ih + tiGap + node.th,
97 | this.defaultHeight
98 | )
99 | const { children } = node
100 | if (!children) {
101 | node.w = node.cw
102 | node.h = node.ch
103 | } else {
104 | const maxW = Math.max(...children.map(c => c.w))
105 | const sumH = children.reduce((p, c) => p + c.h, 0)
106 | node.h = sumH + this.gapY * (children.length - 1)
107 | node.w = node.cw + this.gapX + maxW
108 | }
109 |
110 | node.outLineW = node.cw - node.outLineOffset * 2
111 | node.outLineH = node.ch - node.outLineOffset * 2
112 | }
113 |
114 | calculateInnerXY(node) {
115 | const { mw, th, mh, ch } = node
116 | node.mx = this.padding
117 | node.tx = node.mx + mw + (mw ? this.textMarkersGap : 0)
118 | node.ty = ch - this.padding - th - 4
119 | node.my = node.ty + th / 2 - mh / 2 + 4
120 |
121 | node.ix = node.tx
122 | node.iy = this.padding
123 | }
124 |
125 | calculateXY(root) {
126 | let anchor
127 | // 前序遍历 计算X
128 | root.eachBefore(node => {
129 | this.calculateInnerXY(node)
130 | const { depth } = node
131 | if (depth === 0) {
132 | node.x = 140
133 | anchor = node
134 | return
135 | }
136 | const { depth: lastDepth, cw, x } = anchor
137 | if (depth === lastDepth) {
138 | node.x = x
139 | } else if (depth > lastDepth) {
140 | node.x = x + cw + this.gapX
141 | } else {
142 | const bro = this.findPrevBrother(node)
143 | node.x = bro.x
144 | }
145 | anchor = node
146 | })
147 | // 后序遍历 计算Y
148 | anchor = undefined
149 | root.eachAfter(node => {
150 | const { depth } = node
151 | if (!anchor) {
152 | node.y = 100
153 | anchor = node
154 | return
155 | }
156 | const { depth: lastDepth, ch, y } = anchor
157 | if (depth < lastDepth) {
158 | const firstChild = node.children[0]
159 | node.y = firstChild.y + (y - firstChild.y + ch) / 2 - node.ch / 2
160 | // ![BUG]父节点很高 超过了第一个子节点
161 | if (node.y - node.ch / 2 < firstChild.y - firstChild.ch / 2) {
162 | // !TODO 还需要递归的处理该子节点的所有子代
163 | // !F**K 麻烦啊
164 | node.y = firstChild.y + 1
165 | }
166 | } else {
167 | const bottom = this.findBottom(anchor)
168 | node.y = Math.max(bottom.y + bottom.ch + this.gapY, y + ch + this.gapY)
169 | }
170 | anchor = node
171 | })
172 | }
173 |
174 | /**
175 | * 在已经计算过的节点中寻找与之最靠近的节点
176 | * @param {*} node
177 | * @returns
178 | */
179 | findBottom(node) {
180 | let bottom = node
181 | while (bottom?.children) {
182 | bottom = bottom.children[bottom.children.length - 1]
183 | }
184 | return bottom
185 | }
186 |
187 | calculatePath(root) {
188 | const links = root.links()
189 | const paths = links.map(l => this.getPathData(l))
190 | return paths
191 | }
192 |
193 | getPathData(link) {
194 | const { source, target } = link
195 | const { x: sx, y: sy, cw, ch: sh, id: sid } = source
196 | const { x: tx, y: ty, ch, id: tid } = target
197 | // 生成从一个源点到目标点的光滑的三次贝塞尔曲线
198 | const bezierLine = this.bézierCurveGenerator({
199 | source: {
200 | x: sx + cw,
201 | y: sy + sh / 2
202 | },
203 | target: {
204 | x: tx,
205 | y: ty + ch / 2
206 | }
207 | })
208 | return {
209 | data: bezierLine,
210 | id: `path-${sid}-${tid}`
211 | }
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/src/hooks/map/Tree.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import { select } from 'd3-selection'
3 | import { escape2Html } from '@/hooks/utils'
4 | /**
5 | * 所有导图计算class的父类
6 | * 提取了公共计算方法 这些方法的逻辑不会随导图的风格变化所改变
7 | */
8 | export default class Tree {
9 | constructor() {
10 | this.elMeasureSvg = select(document.getElementById('measureSvg'))
11 | }
12 |
13 | /**
14 | * 计算单张图片的尺寸
15 | * @param {} node
16 | */
17 | measureImageSize(node) {
18 | const { imgInfo } = node.data
19 | if (imgInfo) {
20 | node.iw = imgInfo.width
21 | node.ih = imgInfo.height
22 | } else {
23 | node.iw = 0
24 | node.ih = 0
25 | }
26 | }
27 |
28 | /**
29 | * 计算节点文字的尺寸 处理多行情况
30 | * @param {*} node
31 | * @returns
32 | */
33 | measureTextSize(node) {
34 | const {
35 | depth,
36 | data: { html }
37 | } = node
38 | const fontSize = depth === 0 ? 16 : 14
39 | const lineHeight = fontSize + 2
40 | const t = this.elMeasureSvg.append('text')
41 | t.selectAll('tspan')
42 | .data([html])
43 | .enter()
44 | .append('tspan')
45 | .text(d => d)
46 | .attr('x', 0)
47 | .attr('style', `font-size:${fontSize}px;line-height:${lineHeight}px;`)
48 | const { width, height } = t.node().getBBox()
49 | t.remove()
50 |
51 | if (width < this.maxWidth) {
52 | // 可以不用分为多行
53 | node.multiline = [html]
54 | node.tw = width
55 | node.th = height
56 | node.tspanDy = height
57 | return
58 | }
59 |
60 | // 文字太长 需要分为多行
61 | // 计算行数
62 | const lines =
63 | Math.floor(width / this.maxWidth) + (width % this.maxWidth ? 1 : 0)
64 | const multiline = []
65 | // 计算一行消耗多少字符
66 | const lineLength = Math.floor((html.length * this.maxWidth) / width)
67 | for (let i = 0; i < html.length; i += lineLength) {
68 | // 切分lines行 每行lineLength长
69 | multiline.push(escape2Html(html.substr(i, lineLength)))
70 | }
71 | node.multiline = multiline
72 | node.tw = this.maxWidth
73 | node.th = height * lines
74 | node.tspanDy = height
75 | }
76 |
77 | /**
78 | * 计算贴纸标签的尺寸
79 | * @param {*} node
80 | * @returns
81 | */
82 | measureMarkers(node) {
83 | const {
84 | data: { markerList }
85 | } = node
86 | if (!markerList?.length) {
87 | node.mw = 0
88 | node.mh = 0
89 | return
90 | }
91 | node.mh = this.defaultMarkerHeight
92 | const size = markerList.length
93 | node.mw = this.defaultMarkerWidth * size - this.markerOverlap * (size - 1)
94 | }
95 |
96 | /**
97 | * 找到前一个兄弟节点
98 | * @param {*} node
99 | * @returns
100 | */
101 | findPrevBrother(node) {
102 | const brothers = node.parent.children
103 | let bro
104 | brothers.every((item, index) => {
105 | if (node.data.id === item.data.id) {
106 | bro = brothers[index - 1]
107 | return false
108 | }
109 | return true
110 | })
111 | return bro
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/hooks/map/TreeTable.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import Tree from './Tree'
3 |
4 | export default class TableTree extends Tree {
5 | constructor() {
6 | super()
7 |
8 | this.defaultWidth = 150
9 | this.maxWidth = 350
10 | this.defaultHeight = 60
11 | this.defaultRootHeight = 80
12 | this.padding = 20
13 | this.defaultMarkerHeight = 18
14 | this.defaultMarkerWidth = 18
15 | this.markerOverlap = 7
16 | this.textMarkersGap = 10
17 | this.rectRadius = 0
18 | this.strokeWidth = 1.5
19 | }
20 |
21 | create(root) {
22 | this.measureWidthAndHeight(root)
23 | this.calculateXY(root)
24 | return {
25 | paths: undefined,
26 | nodes: root.descendants()
27 | }
28 | }
29 |
30 | measureWidthAndHeight(root) {
31 | // 后序遍历 初步计算 父依赖于子
32 | root.eachAfter(node => {
33 | this.measureTextSize(node)
34 | this.measureMarkers(node)
35 | this.measureImageSize(node)
36 | this.measureWH(node)
37 | })
38 | // 前序遍历 修正计算 用于宽度充满 子依赖于父
39 | root.eachBefore(node => {
40 | const { children, parent } = node
41 | if (node.depth === 0) return
42 | if (node.depth === 1) {
43 | node.w = parent.w
44 | if (!children) {
45 | // 宽度充满
46 | node.cw = node.w
47 | }
48 | } else {
49 | node.w = parent.w - parent.cw
50 | if (!children) {
51 | node.cw = node.w
52 | }
53 | }
54 | node.outLineW = node.cw - 2 * node.outLineOffset
55 | node.outLineH = node.ch - 2 * node.outLineOffset
56 | })
57 | }
58 |
59 | measureWH(node) {
60 | node.rectRadius = this.rectRadius
61 | node.strokeWidth = this.strokeWidth
62 |
63 | node.outLineOffset = 2
64 |
65 | const tmGap = node.mw ? this.textMarkersGap : 0
66 | const tiGap = node.ih ? this.textMarkersGap : 0
67 | node.cw = Math.max(
68 | Math.max(node.tw, node.iw) + node.mw + this.padding * 2 + tmGap,
69 | this.defaultWidth
70 | )
71 | node.ch = Math.max(
72 | this.padding * 2 + node.ih + tiGap + node.th,
73 | this.defaultHeight
74 | )
75 |
76 | const { children, depth } = node
77 | if (!children) {
78 | node.w = node.cw
79 | node.h = node.ch
80 | } else {
81 | const maxW = Math.max(...children.map(c => c.w))
82 | const sumH = children.reduce((p, c) => p + c.h, 0)
83 | if (depth === 0) {
84 | node.cw = Math.max(maxW, node.cw)
85 | node.w = node.cw
86 | node.h = node.ch + sumH
87 | } else {
88 | // TODO 假如父元素自身高度超过子元素之和 要扩充最后一个子元素的宽度
89 | // TODO 但是子元素还有子元素呢???
90 | node.ch = Math.max(node.ch, sumH)
91 | node.w = node.cw + maxW
92 | node.h = node.ch
93 | }
94 | }
95 | }
96 |
97 | measureSelf(node) {
98 | node.rectRadius = this.rectRadius
99 | node.cw = Math.max(
100 | node.tw + node.mw + this.textMarkersGap + this.padding * 2,
101 | this.defaultWidth
102 | )
103 | node.ch = Math.max(node.th + this.padding * 2, this.defaultHeight)
104 | node.w = node.cw
105 | node.h = node.ch
106 | }
107 |
108 | measureWithChildren(node) {
109 | const { children, depth } = node
110 | const maxW = Math.max(...children.map(c => c.w))
111 | const sumH = this.sumH(children)
112 | if (depth === 0) {
113 | node.cw = maxW
114 | node.ch = Math.max(node.th + this.padding * 2, this.defaultRootHeight)
115 | node.w = node.cw
116 | node.h = node.ch + sumH
117 | } else {
118 | node.cw = Math.max(
119 | node.tw + node.mw + this.textMarkersGap + this.padding * 2,
120 | this.defaultWidth
121 | )
122 | // TODO 假如父元素自身高度超过子元素之和 要扩充最后一个子元素的宽度
123 | // TODO 但是子元素还有子元素呢???
124 | node.ch = Math.max(node.th + this.padding * 2, sumH)
125 | node.w = node.cw + maxW
126 | node.h = node.ch
127 | }
128 | }
129 |
130 | /**
131 | * 计算节点内部元素的位置
132 | * @param {*} node
133 | */
134 | calculateInnerXY(node) {
135 | const { mw, cw, tw, th, mh, ch, iw, children } = node
136 | if (children) {
137 | node.mx = this.padding
138 | node.tx = node.mx + mw + (mw ? this.textMarkersGap : 0)
139 | node.ty = ch - this.padding - th - 4
140 | node.my = node.ty + th / 2 - mh / 2 + 4
141 | node.ix = node.tx
142 | node.iy = this.padding
143 | } else {
144 | // 文字
145 | node.ty = ch - this.padding - th - 4
146 | node.tx = cw - this.padding - tw
147 | // 标记
148 | node.mx = node.tx - (mw ? this.textMarkersGap : 0) - mw
149 | node.my = node.ty + th / 2 - mh / 2 + 4
150 | // 图片
151 | node.ix = cw - this.padding - iw
152 | node.iy = this.padding
153 | }
154 | }
155 |
156 | /**
157 | * 计算节点的位置
158 | * @param {*} root
159 | */
160 | calculateXY(root) {
161 | let anchor
162 | root.eachBefore(node => {
163 | this.calculateInnerXY(node)
164 | const { depth } = node
165 | if (depth === 0) {
166 | node.x = 140
167 | node.y = 100
168 | } else if (depth === 1) {
169 | // 第一层的节点需要特殊处理
170 | if (depth < anchor.depth) {
171 | // 上一个被计算的节点不是当前节点的真正兄弟 需要重新找
172 | const realAnchor = this.findPrevBrother(node)
173 | node.x = realAnchor.x
174 | node.y = realAnchor.y + realAnchor.h
175 | } else {
176 | // 上一个被计算的节点是当前节点的上一个兄弟
177 | node.x = anchor.x
178 | node.y = anchor.y + anchor.ch
179 | }
180 | } else if (depth < anchor.depth) {
181 | // 上一个被计算的节点不是当前节点的真正兄弟 需要重新找
182 | const realAnchor = this.findPrevBrother(node)
183 | node.x = realAnchor.x
184 | node.y = realAnchor.y + realAnchor.h
185 | } else if (depth === anchor.depth) {
186 | // 上一个被计算的节点是当前节点的上一个兄弟
187 | node.x = anchor.x
188 | node.y = anchor.y + anchor.h
189 | } else {
190 | // 上一个被计算的节点是当前节点的父节点 当前节点是第一个孩子
191 | node.x = anchor.x + anchor.cw
192 | node.y = anchor.y
193 | }
194 | anchor = node
195 | })
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/hooks/map/useAutoZoom.js:
--------------------------------------------------------------------------------
1 | import { onMounted, onUnmounted, onActivated, watch, nextTick } from 'vue'
2 | import zoomMap from './zoomMap'
3 | import { debounce } from '../utils'
4 |
5 | export default function useAutoZoom(deps) {
6 | const observedEles = []
7 | const observers = []
8 |
9 | const updateCb = debounce(() => {
10 | zoomMap()
11 | }, 500)
12 |
13 | onMounted(() => {
14 | // 监听侧边栏折叠
15 | const $sideContent = document.getElementById('siderContent')
16 | $sideContent && observedEles.push($sideContent)
17 |
18 | observedEles.forEach(ele => {
19 | const watcher = new MutationObserver(updateCb)
20 | watcher.observe(ele, {
21 | attributes: true,
22 | attributeFilter: ['style']
23 | })
24 | observers.push(watcher)
25 | })
26 | // 监听窗口大小变化
27 | window.onresize = updateCb
28 | })
29 | // 监听导图数据变化
30 | watch(
31 | deps,
32 | () => {
33 | nextTick(updateCb)
34 | },
35 | {
36 | immediate: true
37 | }
38 | )
39 | onUnmounted(() => {
40 | window.onresize = null
41 | observers.forEach(watcher => {
42 | watcher.disconnect()
43 | watcher.takeRecords()
44 | })
45 | observers.length = 0
46 | })
47 |
48 | onActivated(() => {
49 | updateCb()
50 | })
51 | }
52 |
--------------------------------------------------------------------------------
/src/hooks/map/useMap-beta.js:
--------------------------------------------------------------------------------
1 | import { hierarchy } from 'd3-hierarchy'
2 | import TreeTable from './TreeTable'
3 | import LogicTree from './LogicTree-beta.js'
4 |
5 | const useMap = (content, name, nodeMap, mapStyleId) => {
6 | const timerTag = `[TIMER] > ${name} > `
7 | console.time(timerTag)
8 | const hierarchyData = hierarchy(content)
9 | // TODO content 变动的patchFlags的还原
10 | let renderData
11 | if (mapStyleId === 'MAPID-TreeTable') {
12 | const treeTable = new TreeTable()
13 | renderData = treeTable.create(hierarchyData)
14 | }
15 | const logicTree = new LogicTree(nodeMap)
16 | console.timeEnd(timerTag)
17 | renderData = logicTree.create(hierarchyData)
18 | console.timeEnd(timerTag)
19 | return renderData
20 | }
21 |
22 | export default useMap
23 |
--------------------------------------------------------------------------------
/src/hooks/map/useMap.js:
--------------------------------------------------------------------------------
1 | import { hierarchy } from 'd3-hierarchy'
2 | import TreeTable from './TreeTable'
3 | import LogicTree from './LogicTree'
4 |
5 | const useMap = (content, mapStyleId) => {
6 | const hierarchyData = hierarchy(content)
7 | // TODO 当导图风格变多的时候 怎么优雅地处理?
8 | // 考虑proxy策略模式?
9 | let renderData
10 | if (mapStyleId === 'MAPID-TreeTable') {
11 | const treeTable = new TreeTable()
12 | renderData = treeTable.create(hierarchyData)
13 | return renderData
14 | }
15 | const logicTree = new LogicTree()
16 | renderData = logicTree.create(hierarchyData)
17 | return renderData
18 | }
19 |
20 | export default useMap
21 |
--------------------------------------------------------------------------------
/src/hooks/map/useMap1.js:
--------------------------------------------------------------------------------
1 | import { hierarchy } from 'd3-hierarchy'
2 | import TreeTable from './TreeTable'
3 | import LogicTree from './LogicTree1.js'
4 |
5 | const useMap = (content, name, nodeMap, mapStyleId) => {
6 | const timerTag = `[TIMER] > ${name} > `
7 | console.time(timerTag)
8 | const hierarchyData = hierarchy(content)
9 | // TODO content 变动的patchFlags的还原
10 | let renderData
11 | if (mapStyleId === 'MAPID-TreeTable') {
12 | const treeTable = new TreeTable()
13 | renderData = treeTable.create(hierarchyData)
14 | }
15 | const logicTree = new LogicTree(nodeMap)
16 | console.timeEnd(timerTag)
17 | renderData = logicTree.create(hierarchyData)
18 | console.timeEnd(timerTag)
19 | return renderData
20 | }
21 |
22 | export default useMap
23 |
--------------------------------------------------------------------------------
/src/hooks/map/zoomMap.js:
--------------------------------------------------------------------------------
1 | import { zoom, zoomIdentity } from 'd3-zoom'
2 | import { select } from 'd3-selection'
3 |
4 | /**
5 | * 使图具有缩放能力
6 | */
7 | function registerZoom(svgSele, gSele) {
8 | const zoomer = zoom()
9 | .on('zoom', event => {
10 | gSele.attr('transform', event.transform)
11 | })
12 | .scaleExtent([0.1, 4])
13 | // .translateExtent([[-1000, -1000], [1000, 800]])
14 | zoomer(svgSele)
15 | svgSele.on('dblclick.zoom', null)
16 | return zoomer
17 | }
18 | /**
19 | * 使导图适应当前屏幕大小
20 | */
21 | const zoomMap = () => {
22 | const elMainSvg = document.getElementById('mainSvg')
23 | const elMainG = document.getElementById('mainG')
24 | if (!elMainSvg || !elMainG) return
25 |
26 | const mainGSelection = select(elMainG)
27 | const mainSvgSelection = select(elMainSvg)
28 |
29 | const gMetrics = elMainG.getBBox()
30 | const svgMetrics = elMainSvg.getBoundingClientRect()
31 |
32 | // 计算缩放尺度
33 | // [+20]的目的是留出一部分边界空隙
34 | const scale = Math.min(
35 | svgMetrics.width / (gMetrics.width + 140),
36 | svgMetrics.height / (gMetrics.height + 100)
37 | )
38 |
39 | // 计算移动的中心
40 | const svgCenter = { x: svgMetrics.width / 2, y: svgMetrics.height / 2 }
41 | const gCenter = {
42 | x: (gMetrics.width * scale) / 2,
43 | y: (gMetrics.height * scale) / 2
44 | }
45 | const center = zoomIdentity
46 | .translate(
47 | -gMetrics.x * scale + svgCenter.x - gCenter.x,
48 | -gMetrics.y * scale + svgCenter.y - gCenter.y
49 | )
50 | .scale(scale)
51 | const zoomer = registerZoom(mainSvgSelection, mainGSelection)
52 | if (!zoomer) return
53 | mainSvgSelection.transition().duration(500).call(zoomer.transform, center)
54 | }
55 |
56 | export default zoomMap
57 |
--------------------------------------------------------------------------------
/src/hooks/note/useNote.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import { watchEffect, ref } from 'vue'
3 | import useMapStore from '@/store/map'
4 |
5 | export default function useNoteList() {
6 | const store = useMapStore()
7 | const rootNode = ref(null)
8 | const childNodes = ref([])
9 | watchEffect(() => {
10 | if (!store.treeContent) return
11 | // 监听treeContent的变化 改变大纲编辑页需要展示的数据
12 | // TODO 【可优化】:需要每次更新都重新全量地获取先序序列么?
13 | const preOrder = cyclePreOrder(store.treeContent)
14 | // eslint-disable-next-line prefer-destructuring
15 | rootNode.value = preOrder[0]
16 | childNodes.value = preOrder.slice(1)
17 | })
18 | return [rootNode, childNodes]
19 | }
20 |
21 | /**
22 | * 获取多叉树的先序序列 = 大纲编辑页的展示顺序
23 | * @param {*} root
24 | * @returns
25 | */
26 | function cyclePreOrder(root) {
27 | const stack = []
28 | const res = []
29 | root.level = 0
30 | stack.push(root)
31 | while (stack.length) {
32 | const cur = stack.pop()
33 | res.push(cur)
34 | const len = cur?.children?.length
35 | if (len) {
36 | cur.children.forEach((v, i) => {
37 | // ! 倒序入栈才能顺序出栈
38 | const child = cur.children[len - 1 - i]
39 | child.level = cur.level + 1
40 | stack.push(child)
41 | })
42 | }
43 | }
44 | return res
45 | }
46 |
--------------------------------------------------------------------------------
/src/hooks/svg2Png.js:
--------------------------------------------------------------------------------
1 | import saveSvg from 'save-svg-as-png'
2 |
3 | export default function svg2Png(svgId, picName) {
4 | return new Promise((resolve, reject) => {
5 | const $svg = document.getElementById(svgId)
6 | if (!$svg) reject(new Error('SVG未找到'))
7 |
8 | const $mainG = $svg.firstChild
9 | if (!$mainG) reject(new Error('SVG无子元素'))
10 |
11 | // https://developer.mozilla.org/zh-CN/docs/Web/API/SVGGraphicsElement/getBBox
12 | // getBBox相对于svg空间 返回不受transform影响的原始宽高
13 | const gMetrics = $mainG.getBBox()
14 |
15 | const svgMetrics = {
16 | width: gMetrics.width + 20,
17 | height: gMetrics.height + 20
18 | }
19 | // 不让[saveSvg]中的图片下载操作影响到源svg 需要拷贝副本
20 | const $clonedSvg = $svg.cloneNode(true)
21 | const bgColor = $svg.style.backgroundColor
22 | // 为svg拷贝设置新的尺寸 -> 基于标签的原始大小
23 | $clonedSvg.setAttribute(
24 | 'style',
25 | `width:${svgMetrics.width}px;height:${svgMetrics.height}px;background-color:${bgColor}`
26 | )
27 |
28 | // 计算标签的缩放尺度
29 | // [+20]的目的是留出一部分边界空隙
30 | const scale = Math.min(
31 | svgMetrics.width / (gMetrics.width + 20),
32 | svgMetrics.height / (gMetrics.height + 10)
33 | )
34 |
35 | // 计算标签移动的距离
36 | const svgCenter = { x: svgMetrics.width / 2, y: svgMetrics.height / 2 }
37 | const gCenter = {
38 | x: (gMetrics.width * scale) / 2,
39 | y: (gMetrics.height * scale) / 2
40 | }
41 |
42 | // 标签移动到svg拷贝的中心并适配大小
43 | const transX = -gMetrics.x * scale + svgCenter.x - gCenter.x
44 | const transY = -gMetrics.y * scale + svgCenter.y - gCenter.y
45 | $clonedSvg.firstChild.setAttribute(
46 | 'transform',
47 | `translate(${transX}, ${transY}) scale(${scale})`
48 | )
49 |
50 | // 添加水印
51 | const $waterPrint = document.createElementNS(
52 | 'http://www.w3.org/2000/svg',
53 | 'text'
54 | )
55 | $waterPrint.setAttribute('x', svgMetrics.width - 150)
56 | $waterPrint.setAttribute('y', svgMetrics.height - 10)
57 | // TODO 可以优化 水印的颜色设置为当前背景的反转就不用担心水印看不到了
58 | $waterPrint.setAttribute('style', 'color:#000;opacity:0.2;font-size:16px;')
59 | $waterPrint.innerHTML = '@map.kimjisoo.cn'
60 | $clonedSvg.appendChild($waterPrint)
61 | // 准备工作完成 开始转换
62 | resolve($clonedSvg)
63 | }).then(ele => {
64 | // saveSvg.saveSvgAsPng 返回转化处理的Promise
65 | return saveSvg.saveSvgAsPng(ele, `${picName}.png`, {
66 | excludeCss: true
67 | })
68 | })
69 | }
70 |
--------------------------------------------------------------------------------
/src/hooks/useContent.js:
--------------------------------------------------------------------------------
1 | import useMapStore from '@/store/map'
2 | import { xss, getImageWH } from '@/hooks/utils'
3 | import { customAlphabet } from 'nanoid'
4 |
5 | const nanoid = customAlphabet('1234567890abcdef', 5)
6 | const store = useMapStore()
7 |
8 | export async function addNode(pid, options = { isMap: false, cid: undefined }) {
9 | const { content } = store
10 | const node = content[pid]
11 | if (!node) return null
12 | // 如果当前节点折叠 先打开
13 | if (node._children.length) {
14 | ;[node.children, node._children] = [node._children, node.children]
15 | }
16 | const id = nanoid()
17 | const { isMap, cid } = options
18 | if (!cid) {
19 | // 添加孩子 => 添加到第一个
20 | if (isMap) {
21 | node.children.push(id)
22 | } else {
23 | node.children.splice(0, 0, id)
24 | }
25 | } else {
26 | // 添加兄弟 => 添加到当前位置的下一个
27 | const index = node.children.indexOf(options.cid)
28 | node.children.splice(index + 1, 0, id)
29 | }
30 | const child = {
31 | html: '请输入内容',
32 | id,
33 | children: [],
34 | _children: [],
35 | parent: pid,
36 | markerList: [],
37 | collapsed: false
38 | }
39 | content[id] = child
40 | await store.setContent(content)
41 | return id
42 | }
43 |
44 | export async function deleteNode(id, list = undefined) {
45 | // 更新其父节点的信息
46 | const { content } = store
47 | console.log(content[id], id)
48 | const p = content[content[id].parent]
49 | // 没有父节点 => 是根节点 => 根节点不能被删除
50 | if (!p) return null
51 | p.children = p.children.filter(v => v !== id)
52 | // 删除此节点和其所有后代
53 | const queue = [id]
54 | while (queue.length) {
55 | const front = queue.shift()
56 | queue.push(...content[front].children)
57 | delete content[front]
58 | }
59 | await store.setContent(content)
60 | // list==undefined => 是在map中删除 不需要定位焦点
61 | if (!list) return null
62 | // 返回上一个ID
63 | let prevId
64 | // 删除的是第一个节点 焦点将给到第二个节点
65 | if (list[0].id === id) {
66 | prevId = list[1].id
67 | } else {
68 | // eslint-disable-next-line no-restricted-syntax
69 | for (const index in list) {
70 | if (list[index].id === id) {
71 | prevId = list[index - 1].id
72 | break
73 | }
74 | }
75 | }
76 | return prevId
77 | }
78 | export async function collapse(id) {
79 | const { content } = store
80 | const node = content[id]
81 | ;[node.children, node._children] = [node._children, node.children]
82 | await store.setContent(content)
83 | }
84 |
85 | export function moveToLastFocus(id) {
86 | // 把光标移到文字最后面
87 | const $el = document.getElementById(id)
88 | const range = document.createRange()
89 | range.selectNodeContents($el)
90 | range.collapse(false)
91 | const sel = window.getSelection()
92 | sel.removeAllRanges()
93 | sel.addRange(range)
94 | }
95 |
96 | export async function tabNode(id, noteList) {
97 | // 1. 首先判断能不能tab
98 | // 该节点作为第一个孩子节点时 不能tab
99 | const { content } = store
100 | const node = content[id]
101 | const pNode = content[node.parent]
102 | const index = pNode.children.indexOf(id)
103 | const canTab = index !== 0
104 | if (!canTab) return null
105 | // 2. 找到要tab到哪个节点下
106 | let newPid
107 | // eslint-disable-next-line no-restricted-syntax
108 | for (const i in noteList) {
109 | if (id === noteList[i].id) {
110 | newPid = noteList[i - 1].id
111 | break
112 | }
113 | }
114 | // 3. 旧的父node删除该节点
115 | pNode.children.splice(index, 1)
116 | // 4. 该节点重新赋值新的父node id
117 | node.parent = newPid
118 | // 5. 如果newPid折叠了,打开
119 | const newPNode = content[newPid]
120 | if (newPNode._children.length) {
121 | ;[newPNode.children, newPNode._children] = [
122 | newPNode._children,
123 | newPNode.children
124 | ]
125 | }
126 | // 6. 该节点作为newPid的最后一个孩子
127 | newPNode.children.push(id)
128 | await store.setContent(content)
129 | return id
130 | }
131 |
132 | export async function changeNodeHtml(id, html) {
133 | const { content } = store
134 | // ! 由于debounce 此事件可能发生在deleteNode之后 此id节点可能被删除 需要判空
135 | if (!content[id]) return
136 | content[id].html = xss(html)
137 | await store.setContent(content, true)
138 | }
139 |
140 | export async function changeNodeMarkers(id, markerList) {
141 | const { content } = store
142 | // ! 由于debounce 此事件可能发生在deleteNode之后 此id节点可能被删除 需要判空
143 | if (!content[id]) return
144 | content[id].markerList = markerList
145 | await store.setContent(content)
146 | }
147 |
148 | export async function pasteImg(file, nodeId) {
149 | const { width, height } = await getImageWH(file)
150 | const { content } = store
151 | // TODO 把这些默认值写在一个统一的配置文件里
152 | // 在上传过程种设置一个loading占位
153 | content[nodeId].imgInfo = {
154 | url: 'https://cdn.kimjisoo.cn/pic%2Floading.svg',
155 | width: 250,
156 | height: (250 * height) / width
157 | }
158 | store.setContent(content, true)
159 | await store.pasteImg({ file, nodeId, width, height })
160 | }
161 |
162 | export async function deleteImg(nodeId) {
163 | const { content } = store
164 | content[nodeId].imgInfo = undefined
165 | await store.setContent(content)
166 | }
167 |
--------------------------------------------------------------------------------
/src/hooks/useIntroStyle.js:
--------------------------------------------------------------------------------
1 | import { ref, computed } from 'vue'
2 |
3 | const threshold = 590
4 | // const thresholdX = 947
5 | // --slogan-svg-offset-y: 0px;
6 | // --slogan-svg-scale-rate: 0.6435;
7 | // --hero-slogan-color-2: rgb(255 55 55);
8 | // --hero-slogan-color-1: rgb(108 105 255);
9 | export default function useIntroStyle() {
10 | const sloganColorA = ref('rgba(0,0,0,1)')
11 | const sloganColorB = ref('rgba(0,0,0,1)')
12 | const sloganOffsetY = ref(104)
13 | const sloganScaleRate = ref(1)
14 | const sloganStyle = computed(() => {
15 | return {
16 | '--hero-slogan-color-1': sloganColorA.value,
17 | '--hero-slogan-color-2': sloganColorB.value,
18 | transform: `translate3d(0,${sloganOffsetY.value}px,0) scale(${sloganScaleRate.value})`
19 | }
20 | })
21 | const updateStyle = scrollTop => {
22 | let rate = scrollTop / threshold
23 | if (rate > 1) rate = 1
24 | sloganColorA.value = `rgba(${rate * 255},${rate * 55},${rate * 55},1)`
25 | sloganColorB.value = `rgba(${rate * 108},${rate * 105},${rate * 255},1)`
26 | sloganOffsetY.value = (1 - rate) * 104
27 | sloganScaleRate.value = 1 - 0.4 * rate
28 | }
29 | return [sloganStyle, updateStyle]
30 | }
31 |
--------------------------------------------------------------------------------
/src/hooks/useLogin.js:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/hooks/useMapStyle.js:
--------------------------------------------------------------------------------
1 | import useWebsiteStore from '@/store/website'
2 |
3 | const websiteStore = useWebsiteStore()
4 |
5 | export default function useMapStyle(styles) {
6 | const { colorId } = styles
7 | const allColors = websiteStore.styles.colorList
8 | const {
9 | style: { colors }
10 | } = allColors.find(item => item.id === colorId)
11 | return {
12 | svgStyle: {
13 | width: '100%',
14 | height: '100%',
15 | backgroundColor: colors.bgSvg
16 | },
17 | pathStyle: {
18 | stroke: colors.path,
19 | fill: 'none',
20 | strokeWidth: '1.5px'
21 | },
22 | imageStyle: {
23 | display: 'none'
24 | },
25 | rectStyle: node => {
26 | let style = {
27 | stroke: colors.border,
28 | strokeWidth: `${node.strokeWidth}px`
29 | }
30 | switch (node.depth) {
31 | case 0:
32 | style = { ...style, fill: colors.bgRoot }
33 | break
34 | case 1:
35 | style = { ...style, fill: colors.bgSubRoot }
36 | break
37 | default:
38 | style = { ...style, fill: colors.bgLeaf }
39 | }
40 | return style
41 | },
42 | textStyle: node => {
43 | const _fontSize = node.depth === 0 ? 16 : 14
44 | const _lineHeight = _fontSize + 2
45 | let style = {
46 | fontSize: `${_fontSize}px`,
47 | lineHeight: `${_lineHeight}px`,
48 | textAnchor: 'start',
49 | whiteSpace: 'initial'
50 | }
51 | switch (node.depth) {
52 | case 0:
53 | style = { ...style, color: colors.textRoot }
54 | break
55 | case 1:
56 | style = { ...style, color: colors.textSubRoot }
57 | break
58 | default:
59 | style = { ...style, color: colors.textLeaf }
60 | }
61 | return style
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/hooks/useSnapshot.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 配合[ctrl + z]实现操作可撤回
3 | */
4 | import { onMounted, onUnmounted } from 'vue'
5 | import useMapStore from '@/store/map'
6 | import { deepClone } from './utils'
7 |
8 | class Snapshot {
9 | constructor(length = 20) {
10 | this.length = length
11 | this.snapshots = []
12 | this.cursor = -1
13 | }
14 |
15 | get hasPrev() {
16 | return this.cursor > 0
17 | }
18 |
19 | snap(data) {
20 | // 记录数据快照
21 | const snapshot = deepClone(data)
22 | // 去除旧分支
23 | while (this.cursor < this.snapshots.length - 1) {
24 | this.snapshots.pop()
25 | }
26 | this.snapshots.push(snapshot)
27 | // 确保历史记录条数限制
28 | if (this.snapshots.length > this.length) {
29 | this.snapshots.shift()
30 | }
31 | this.cursor = this.snapshots.length - 1
32 | }
33 |
34 | prev() {
35 | if (this.hasPrev) {
36 | this.cursor -= 1
37 | return deepClone(this.snapshots[this.cursor])
38 | }
39 | return null
40 | }
41 | }
42 |
43 | export default function useSnapShot() {
44 | const snapshot = new Snapshot()
45 | const store = useMapStore()
46 | onMounted(() => {
47 | document.onkeydown = ev => {
48 | if (ev.ctrlKey && ev.keyCode === 90) {
49 | ev.preventDefault()
50 | if (snapshot.hasPrev) {
51 | store.setContent(snapshot.prev().content)
52 | }
53 | }
54 | }
55 | })
56 | onUnmounted(() => {
57 | document.onkeydown = undefined
58 | })
59 | const addSnapShot = () => {
60 | snapshot.snap({ content: store.content })
61 | }
62 | return addSnapShot
63 | }
64 |
--------------------------------------------------------------------------------
/src/hooks/useWorker.js:
--------------------------------------------------------------------------------
1 | import Worker from './map.worker'
2 |
3 | const myWorker = new Worker()
4 | const callbackMap = new WeakMap()
5 |
6 | myWorker.onmessage = e => {
7 | const { result, type } = e.data
8 | const callback = callbackMap?.get(type)
9 | if (callback) {
10 | callback(result)
11 | }
12 | }
13 | /**
14 | * TODO 回调函数 不够优雅 怎么解决
15 | * @param {*} source
16 | * @param {*} callback
17 | */
18 | export default function getMd5(source, callback) {
19 | callbackMap.set('MD5', callback)
20 | myWorker.postMessage({ source, type: 'MD5' })
21 | }
22 |
--------------------------------------------------------------------------------
/src/hooks/utils.js:
--------------------------------------------------------------------------------
1 | import { ElMessage } from 'element-plus'
2 | import filterXSS from 'xss'
3 |
4 | /**
5 | * 深拷贝:新对象的改变不影响旧对象
6 | * 1. [JSON.stringfy JSON.parse]: 无法克隆方法/循环引用无法解决
7 | * 2. [递归]: 循环引用无法解决 改进: 使用Map记录是否克隆过
8 | */
9 | export const deepClone = (target, map = new Map()) => {
10 | if (target === null || typeof target !== 'object') {
11 | return target
12 | }
13 | const cache = map.get(target)
14 | if (cache) {
15 | return cache
16 | }
17 | const isArray = Array.isArray(target)
18 | const result = isArray ? [] : {}
19 | map.set(target, result)
20 | if (isArray) {
21 | target.forEach((item, index) => {
22 | result[index] = deepClone(item, map)
23 | })
24 | } else {
25 | Object.keys(target).forEach(key => {
26 | result[key] = deepClone(target[key], map)
27 | })
28 | }
29 | return result
30 | }
31 |
32 | export const ErrorTip = msg => {
33 | ElMessage.error(msg)
34 | }
35 |
36 | export const dateFormatter = timestamp => {
37 | if (!timestamp) return ''
38 | const date = new Date(timestamp)
39 | let year = date.getFullYear()
40 | let month = date.getMonth() + 1
41 | let day = date.getDate()
42 | const hour = date.getHours()
43 | let minutes = date.getMinutes()
44 | if (year === new Date().getFullYear()) {
45 | year = ''
46 | }
47 | if (month < 10) {
48 | month = `0${month}`
49 | }
50 | if (day < 10) {
51 | day = `0${day}`
52 | }
53 | if (minutes < 10) {
54 | minutes = `0${minutes}`
55 | }
56 | return `${year ? `${year}年` : ''}${month}月${day}日 ${hour}:${minutes}`
57 | }
58 |
59 | // 防抖:动作绑定事件,动作发生后一定时间后触发事件,在这段时间内,如果该动作又发生,则重新等待一定时间再触发事件。
60 | export function debounce(fn, wait) {
61 | let timer
62 | return function (...args) {
63 | const _this = this
64 | if (timer) {
65 | clearTimeout(timer)
66 | }
67 | timer = setTimeout(function () {
68 | fn.apply(_this, args)
69 | }, wait)
70 | }
71 | }
72 |
73 | // 节流: 动作绑定事件,动作发生后一段时间后触发事件,在这段时间内,如果动作又发生,则无视该动作,直到事件执行完后,才能重新触发。
74 | export function throttle(fn, wait) {
75 | let timer
76 | return function (...args) {
77 | const _this = this
78 | if (!timer) {
79 | timer = setTimeout(function () {
80 | timer = null
81 | fn.apply(_this, args)
82 | }, wait)
83 | }
84 | }
85 | }
86 |
87 | // TODO 需要抽取到一个统一的website配置json里 通过http接口返回 不直接嵌入代码
88 | const xssFilterOptions = {
89 | stripIgnoreTagBody: true, // 不在白名单中的标签以及标签里面的内容直接删除
90 | whiteList: {
91 | h1: ['style'],
92 | h2: ['style'],
93 | h3: ['style'],
94 | h4: ['style'],
95 | h5: ['style'],
96 | h6: ['style'],
97 | hr: ['style'],
98 | span: ['style'],
99 | strong: ['style'],
100 | b: ['style'],
101 | i: ['style'],
102 | br: [],
103 | p: ['style'],
104 | pre: ['style'],
105 | code: ['style'],
106 | a: ['style', 'target', 'href', 'title', 'rel'],
107 | img: ['style', 'src', 'title'],
108 | div: ['style'],
109 | table: ['style', 'width', 'border'],
110 | tr: ['style'],
111 | td: ['style', 'width', 'colspan'],
112 | th: ['style', 'width', 'colspan'],
113 | tbody: ['style'],
114 | ul: ['style'],
115 | li: ['style'],
116 | ol: ['style'],
117 | dl: ['style'],
118 | dt: ['style'],
119 | em: ['style'],
120 | cite: ['style'],
121 | section: ['style'],
122 | header: ['style'],
123 | footer: ['style'],
124 | blockquote: ['style'],
125 | audio: ['autoplay', 'controls', 'loop', 'preload', 'src'],
126 | video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width']
127 | },
128 | css: {
129 | whiteList: {
130 | color: true,
131 | 'background-color': true,
132 | width: true,
133 | height: true,
134 | 'max-width': true,
135 | 'max-height': true,
136 | 'min-width': true,
137 | 'min-height': true,
138 | 'font-size': true
139 | }
140 | }
141 | }
142 | export function xss(content) {
143 | return filterXSS(content, xssFilterOptions)
144 | }
145 | /**
146 | * 获取图片的原始宽高
147 | * TODO 错误处理 reject
148 | * @param {*} url 可能是文件或者string
149 | * @returns
150 | */
151 | export function getImageWH(url) {
152 | return new Promise(resolve => {
153 | if (url instanceof File) {
154 | const reader = new FileReader()
155 | reader.onload = e => {
156 | // 把获取到的图片展示
157 | resolve(e.target.result)
158 | }
159 | reader.readAsDataURL(url)
160 | } else {
161 | resolve(url)
162 | }
163 | }).then(res => {
164 | return new Promise(resolve => {
165 | const img = new Image()
166 | img.onload = () => {
167 | const width = img.naturalWidth
168 | const height = img.naturalHeight
169 | resolve({ width, height })
170 | }
171 | img.src = res
172 | })
173 | })
174 | }
175 |
176 | export function escape2Html(str) {
177 | const escapeMap = { lt: '<', gt: '>', nbsp: ' ', amp: '&', quot: '"' }
178 | return str.replace(/&(lt|gt|nbsp|amp|quot);/gi, (all, t) => escapeMap[t])
179 | }
180 |
181 | export function betterInterval(cb, wait) {
182 | let timer
183 | const timeUp = () => {
184 | clearTimeout(timer)
185 | cb && cb()
186 | timer = setTimeout(timeUp, wait)
187 | }
188 | timer = setTimeout(timeUp, wait)
189 | return {
190 | clear: () => clearTimeout(timer)
191 | }
192 | }
193 |
194 | // 自定义验证规则
195 | const validatePass = (rule, value, callback) => {
196 | // 密码只能由大小写英文字母或数字开头,且由大小写英文字母_.组成
197 | const reg = /^[A-Za-z0-9][A-Za-z0-9_.]{5,14}$/
198 | if (!value.match(reg)) {
199 | callback(new Error('密码由字母或数字开头,且只能为字母,数字,下划线及(.)'))
200 | } else {
201 | callback()
202 | }
203 | }
204 |
205 | export function getLoginRules() {
206 | return {
207 | email: [
208 | { required: true, message: '邮箱不能为空', trigger: 'blur' },
209 | { type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
210 | ],
211 | pwd: [
212 | { required: true, message: '密码不能为空', trigger: 'blur' },
213 | { min: 6, max: 15, message: '密码位数只能在6~15之间', trigger: 'blur' },
214 | { validator: validatePass, trigger: 'blur' }
215 | ]
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import * as Sentry from '@sentry/vue'
3 | import { BrowserTracing } from '@sentry/tracing'
4 | import App from './App.vue'
5 | import router from './router'
6 | import store from './store'
7 | import '@/assets/css/reset.css'
8 |
9 | // 一次性引入所有svg图
10 | const req = require.context('./assets/pic', false, /\.svg$/)
11 | const requireAll = requireContext => requireContext.keys().map(requireContext)
12 | requireAll(req)
13 |
14 | const app = createApp(App)
15 |
16 | // TODO 待完善全局异常捕获
17 | // app.config.errorHandler = (err, vm, info) => {
18 | // console.log('[全局异常]', err, vm, info)
19 | // }
20 |
21 | const websiteCfg = JSON.parse(localStorage.getItem('zmindmap_website') || '{}')
22 | const isDark = websiteCfg?.isDark
23 | window.document.documentElement.setAttribute(
24 | 'data-theme',
25 | isDark ? 'dark' : 'light'
26 | )
27 | const { sentryCfg } = window.CFG
28 | if (sentryCfg.tracingOrigins.includes(window.location.hostname)) {
29 | Sentry.init({
30 | app,
31 | dsn: sentryCfg.dsn,
32 | integrations: [
33 | new BrowserTracing({
34 | routingInstrumentation: Sentry.vueRouterInstrumentation(router),
35 | tracingOrigins: sentryCfg.tracingOrigins
36 | })
37 | ],
38 | tracesSampleRate: sentryCfg.tracesSampleRate
39 | })
40 | }
41 |
42 | app.use(store).use(router).mount('#app')
43 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHistory } from 'vue-router'
2 | import useUserStore from '@/store/user'
3 | import Home from '@/views/Home'
4 | import Folder from '@/views/Folder'
5 | import NotFound from '@/views/NotFound.vue'
6 | import Intro from '@/views/Intro.vue'
7 |
8 | const routes = [
9 | {
10 | path: '/',
11 | redirect: '/app'
12 | },
13 | {
14 | path: '/app',
15 | name: 'Home',
16 | component: Home,
17 | redirect: '/app/folder',
18 | children: [
19 | {
20 | path: 'edit/:id/:view?',
21 | component: () =>
22 | import(/* webpackChunkName: "edit-patch" */ '@/views/Edit.vue')
23 | },
24 | {
25 | path: 'folder/:id?',
26 | component: Folder
27 | }
28 | ]
29 | },
30 | {
31 | path: '/login',
32 | name: 'Login',
33 | meta: {
34 | isLogin: true
35 | },
36 | // route level code-splitting
37 | // this generates a separate chunk (about.[hash].js) for this route
38 | // which is lazy-loaded when the route is visited.
39 | component: () =>
40 | import(/* webpackChunkName: "login-patch" */ '../views/Login')
41 | },
42 | {
43 | path: '/404',
44 | component: NotFound
45 | },
46 | {
47 | path: '/intro',
48 | component: Intro
49 | },
50 | {
51 | path: '/:catchAll(.*)', // 不识别的path自动匹配404
52 | redirect: '/404'
53 | }
54 | ]
55 |
56 | const router = createRouter({
57 | history: createWebHistory(),
58 | routes
59 | })
60 | router.beforeEach((to, from, next) => {
61 | // 百度统计
62 | if (to.path && window._hmt) {
63 | window._hmt.push(['_trackPageview', to.fullPath])
64 | }
65 | const store = useUserStore()
66 | if (to.meta.isLogin) {
67 | // 如果去登陆页的话 不用验证token
68 | next()
69 | } else if (store?.token || localStorage.getItem('token')) {
70 | // 验证是否登录了
71 | next()
72 | } else {
73 | next({ path: '/login', query: { redirect: to.path } })
74 | }
75 | })
76 |
77 | export default router
78 |
--------------------------------------------------------------------------------
/src/store/doc.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | /**
3 | * 文档相关状态
4 | */
5 | import { defineStore } from 'pinia'
6 | import { dateFormatter } from '@/hooks/utils'
7 | import { docApi } from '@/hooks/http'
8 |
9 | const useDocStore = defineStore({
10 | id: 'doc',
11 | state: () => ({
12 | originAllDocs: undefined,
13 | allTreeDocs: undefined
14 | }),
15 | getters: {
16 | getAllDocuments: state => id => {
17 | if (!state.originAllDocs) return []
18 | if (!id) return state.allTreeDocs
19 | const { folders, documents } = state.originAllDocs
20 | return [...folders, ...documents].filter(doc => doc.folderId === id)
21 | },
22 | getNavigationLists: state => curFolderId => {
23 | const paths = []
24 | const folderList = state.originAllDocs?.folders
25 | if (!folderList || !folderList.length) return []
26 | const curFolder = folderList.find(f => f.id === curFolderId)
27 | if (curFolder) {
28 | paths.unshift(curFolder)
29 | let prevFolderId = curFolder.folderId
30 | while (prevFolderId !== '0') {
31 | // eslint-disable-next-line no-loop-func
32 | const prevFolder = folderList.find(f => f.id === prevFolderId)
33 | if (!prevFolder) break
34 | paths.unshift(prevFolder)
35 | prevFolderId = prevFolder.folderId
36 | }
37 | }
38 | paths.unshift({ name: '我的文件', id: '0' })
39 | return paths
40 | }
41 | },
42 | actions: {
43 | setDoc(data) {
44 | if (!data) return
45 | this.originAllDocs = data
46 | this.allTreeDocs = processTreeData(data)
47 | },
48 | async fetchAllDocuments() {
49 | const data = await docApi.fetchAllDocuments()
50 | this.setDoc(data)
51 | },
52 | async postSetFolder(data) {
53 | const res = await docApi.postSetFolder(data)
54 | this.setDoc(res)
55 | },
56 | async postSetDoc(data) {
57 | const res = await docApi.postSetDoc(data)
58 | this.setDoc(res)
59 | },
60 | async postRemove(data) {
61 | const res = await docApi.postRemove(data)
62 | this.setDoc(res)
63 | }
64 | },
65 | persist: {
66 | enabled: true,
67 | strategies: [
68 | {
69 | key: 'zmindmap_docs',
70 | storage: localStorage
71 | }
72 | ]
73 | }
74 | })
75 |
76 | function processTreeData(data) {
77 | if (!data) return null
78 | const docs = [...data.folders, ...data.documents]
79 | const treeData = docs.filter(item => {
80 | item.formatedUpdateTime = dateFormatter(item.updateTime)
81 | item.formatedCreateTime = dateFormatter(item.createTime)
82 | item.children = docs.filter(e => {
83 | return item.id === e.folderId
84 | })
85 | return item.folderId === '0'
86 | })
87 | return treeData
88 | }
89 |
90 | export default useDocStore
91 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createPinia } from 'pinia'
2 | import piniaPluginPersist from 'pinia-plugin-persist'
3 |
4 | const store = createPinia()
5 | store.use(piniaPluginPersist)
6 |
7 | export default store
8 |
--------------------------------------------------------------------------------
/src/store/map.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | /* eslint-disable no-restricted-syntax */
3 | /**
4 | * 导图和大纲笔记页面相关状态
5 | */
6 | import { defineStore } from 'pinia'
7 | import { ErrorTip } from '@/hooks/utils'
8 | import { mapApi } from '@/hooks/http'
9 |
10 | const useMapStore = defineStore('map', {
11 | state: () => ({
12 | // ? 【还是TypeScript好】
13 | mapData: undefined, // 导图所有原始数据:包括id,内容,风格等
14 | content: undefined, // 导图内容的纯数据扁平结构
15 | treeContent: undefined, // 导图内容的纯数据树形结构
16 | isSaving: false, // 设置Edit页面的数据的加载状态
17 | idFocused: undefined // 当前时刻被选中的节点ID
18 | }),
19 | actions: {
20 | setIdFocused(id) {
21 | this.idFocused = id
22 | },
23 | // 更新content 同时统一处理isSaving的状态
24 | async updateWithLoading(makeNewData) {
25 | if (!makeNewData || typeof makeNewData !== 'function') return
26 | if (this.isSaving) {
27 | ErrorTip('操作过于频繁!')
28 | return
29 | }
30 | this.isSaving = true
31 | const data = makeNewData()
32 | await this.remoteUpdateMap(data)
33 | this.isSaving = false
34 | },
35 | // 一般的导图节点级别更新
36 | async setContent(content) {
37 | await this.updateWithLoading(() => {
38 | this.content = content
39 | this.treeContent = flatToTree(content)
40 | return {
41 | ...this.mapData,
42 | definition: JSON.stringify(content)
43 | }
44 | })
45 | },
46 | // 整个导图的风格更新
47 | async setStyle(newStyle) {
48 | this.updateWithLoading(() => {
49 | return {
50 | ...this.mapData,
51 | styles: newStyle
52 | }
53 | })
54 | },
55 | /**
56 | * 从网络拿到导图信息后 放入store
57 | * @param {*} data
58 | */
59 | setDataFromRemote(data) {
60 | if (data && data.definition) {
61 | this.mapData = data
62 | this.content = JSON.parse(data.definition)
63 | this.treeContent = flatToTree(this.content)
64 | } else {
65 | ErrorTip('保存失败')
66 | }
67 | },
68 | /**
69 | * 初始获取导图数据
70 | * @param {*} docId
71 | */
72 | async fetchMap(docId) {
73 | const res = await mapApi.fetchMap(docId)
74 | this.setDataFromRemote(res)
75 | },
76 | /**
77 | * 向数据库提交更新
78 | * @param {*} data
79 | * @param {*} onFinish
80 | */
81 | async remoteUpdateMap(data) {
82 | const res = await mapApi.remoteUpdateMap(data)
83 | this.setDataFromRemote(res)
84 | },
85 | /**
86 | * 粘贴图片的逻辑
87 | * @param {*} param0
88 | * @returns
89 | */
90 | async pasteImg({ file, nodeId, width, height }) {
91 | const formData = new FormData()
92 | if (file) {
93 | formData.append('file', file)
94 | }
95 | // 首先上传图片,获得图片的url
96 | const imgUrl = await mapApi.uploadImg({
97 | data: formData,
98 | headers: {
99 | 'Content-Type': 'multipart/form-data;'
100 | },
101 | timeout: 20000
102 | })
103 | if (!imgUrl) {
104 | ErrorTip('图片上传失败')
105 | return
106 | }
107 | // 图片信息绑定到节点上
108 | this.content[nodeId].imgInfo = {
109 | url: imgUrl,
110 | width: 250,
111 | height: (250 * height) / width
112 | }
113 | await this.setContent(this.content)
114 | }
115 | },
116 | persist: {
117 | enabled: false
118 | }
119 | })
120 |
121 | function flatToTree(data) {
122 | if (!data) return null
123 | const values = Object.values(data)
124 | const treeData = values.filter(item => {
125 | const { _children, id } = item
126 | if (_children.length) {
127 | item._children = values.filter(e => {
128 | return id === e.parent
129 | })
130 | } else {
131 | item.children = values.filter(e => {
132 | return id === e.parent
133 | })
134 | }
135 | return item.parent === '-1'
136 | })
137 | return treeData[0]
138 | }
139 |
140 | export default useMapStore
141 |
--------------------------------------------------------------------------------
/src/store/user.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 用户相关状态
3 | */
4 | import { defineStore } from 'pinia'
5 | import { userApi } from '@/hooks/http'
6 |
7 | const useUserStore = defineStore('user', {
8 | state: () => ({
9 | token: undefined,
10 | user: undefined
11 | }),
12 | getters: {
13 | getUser: state => state.user,
14 | getToken: state => state.token || localStorage.getItem('token')
15 | },
16 | actions: {
17 | async login(payload) {
18 | const { isLogin, loginForm } = payload
19 | let data
20 | if (isLogin) {
21 | data = await userApi.login(loginForm)
22 | } else {
23 | data = await userApi.register(loginForm)
24 | }
25 | this.setUser(data)
26 | },
27 | setUser(data) {
28 | if (!data) return
29 | this.token = data?.token
30 | // ? 更安全的做法:不用[token]关键字
31 | localStorage.setItem('token', this.token)
32 | this.user = data?.user
33 | },
34 | logout() {
35 | this.token = undefined
36 | this.user = undefined
37 | localStorage.clear()
38 | },
39 | async updateUser(data) {
40 | const formData = new FormData()
41 | formData.append('user', encodeURIComponent(JSON.stringify(data.user)))
42 | if (data.file) {
43 | formData.append('file', data.file)
44 | }
45 | const res = await userApi.updateUser({
46 | data: formData,
47 | headers: {
48 | 'Content-Type': 'multipart/form-data;'
49 | },
50 | timeout: 20000
51 | })
52 | this.user = res
53 | }
54 | },
55 | persist: {
56 | enabled: true,
57 | strategies: [
58 | {
59 | key: 'zmindmap_user',
60 | storage: localStorage
61 | }
62 | ]
63 | }
64 | })
65 |
66 | export default useUserStore
67 |
--------------------------------------------------------------------------------
/src/store/website.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 网站页面相关状态
3 | */
4 | import { defineStore } from 'pinia'
5 | import { websiteApi } from '@/hooks/http'
6 |
7 | const useWebsiteStore = defineStore({
8 | id: 'website',
9 | state: () => ({
10 | // 文件列表展示方式
11 | showTable: false,
12 | // 主题模式
13 | isDark: false,
14 | // 侧边栏是否折叠
15 | siderCollapse: true,
16 | // 导图风格
17 | styles: {
18 | colorList: [],
19 | mapList: [],
20 | markerList: []
21 | }
22 | }),
23 | actions: {
24 | toggleShowTable() {
25 | this.showTable = !this.showTable
26 | },
27 | toggleSiderCollapse() {
28 | this.siderCollapse = !this.siderCollapse
29 | },
30 | switchMapStyle(id) {
31 | this.mapStyle = id
32 | },
33 | switchMapColor(id) {
34 | this.mapColor = id
35 | },
36 | toggleDarkMode() {
37 | this.isDark = !this.isDark
38 | const mode = this.isDark ? 'dark' : 'light'
39 | window.document.documentElement.setAttribute('data-theme', mode)
40 | },
41 | async fetchMapStyles() {
42 | const data = await websiteApi.fetchMapStyles()
43 | this.styles = data
44 | }
45 | },
46 | persist: {
47 | enabled: true,
48 | strategies: [
49 | {
50 | key: 'zmindmap_website',
51 | storage: localStorage
52 | // paths: ['name', 'age']
53 | }
54 | ]
55 | }
56 | })
57 | export default useWebsiteStore
58 |
--------------------------------------------------------------------------------
/src/views/Edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
26 |
27 |
28 |
29 |
30 |
80 |
199 |
--------------------------------------------------------------------------------
/src/views/Folder/components/BreadCrumb.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 | {{ item.name }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
41 |
42 |
76 |
--------------------------------------------------------------------------------
/src/views/Folder/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
18 |
19 |
20 |
21 |
25 |
{{ scope.row.name }}
26 |
27 |
28 |
29 |
30 |
35 |
36 |
37 |
38 |
{{ scope.row.formatedCreateTime }}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
51 |
55 |
{{ row.name }}
56 |
57 |
58 |
59 |
60 |
61 |
62 |
![]()
63 |
暂无文件,点击左上角"+"新建文件
64 |
65 |
66 |
67 |
122 |
123 |
280 |
--------------------------------------------------------------------------------
/src/views/Home/components/Sider.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
68 |
69 |
146 |
--------------------------------------------------------------------------------
/src/views/Intro.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
37 |
38 |
39 |
44 | 思如泉涌
成竹在图
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
86 |
222 |
--------------------------------------------------------------------------------
/src/views/Login/components/Qrcode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
![用户头像]()
21 |
{{ username }}
22 |
23 |
24 |
{{ tip }}
25 |
扫码登录,更易、更快、更安全
26 |
27 |
28 |
29 |
30 |
117 |
118 |
195 |
--------------------------------------------------------------------------------
/src/views/Login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
没有账号?
6 | 去注册
9 |
10 |
11 |
已有账号?
12 | 去登录
15 |
16 |
66 |
67 |
68 |
69 |
70 |
131 |
132 |
215 |
--------------------------------------------------------------------------------
/src/views/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
页面走丢了
5 | 回到主页
6 |
7 |
8 |
15 |
30 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const AutoImport = require('unplugin-auto-import/webpack')
4 | const Components = require('unplugin-vue-components/webpack')
5 | const SentryWebpackPlugin = require('@sentry/webpack-plugin')
6 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
7 | const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
8 | const { ESBuildMinifyPlugin } = require('esbuild-loader')
9 |
10 | const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
11 |
12 | const IS_PROD = process.env.NODE_ENV === 'production'
13 | const getAliasPath = dir => path.join(__dirname, dir)
14 |
15 | module.exports = {
16 | productionSourceMap: false,
17 | lintOnSave: !IS_PROD,
18 | publicPath: IS_PROD ? 'https://cdn.kimjisoo.cn/' : '/',
19 | pages: {
20 | index: {
21 | entry: './src/main.js',
22 | template: './public/index.html',
23 | title: 'ZMind思维导图',
24 | chunks: [
25 | 'chunk-vendors',
26 | 'chunk-vendors-2',
27 | 'chunk-ele',
28 | 'index'
29 | ]
30 | }
31 | },
32 | chainWebpack: config => {
33 | config.when(!IS_PROD, config => {
34 | const rule = config.module.rule('js')
35 | // 清理自带的 babel-loader
36 | rule.uses.clear();
37 | // 添加 esbuild-loader
38 | rule
39 | .use('esbuild-loader')
40 | .loader('esbuild-loader')
41 | .options({
42 | target: 'es2015'
43 | })
44 | // 删除底层 terser, 换用esbuild-minimize-plugin
45 | config.optimization.minimizers.delete('terser')
46 | // 使用 esbuild 优化 css 压缩
47 | config.optimization
48 | .minimizer('esbuild')
49 | .use(ESBuildMinifyPlugin, [{ minify: true, css: true }])
50 | })
51 |
52 | config.when(IS_PROD, config => {
53 | // 移除prefetch插件 减少对首页加载的带宽占用
54 | // config.plugins.delete('prefetch')
55 | // config.optimization.runtimeChunk({
56 | // // https://webpack.docschina.org/configuration/optimization/#optimizationruntimechunk
57 | // name: (entrypoint) => `runtime~${entrypoint.name}`,
58 | // }),
59 | config.optimization.splitChunks({
60 | cacheGroups: {
61 | // 其他的第三方库集中在一起
62 | vendors: {
63 | name: 'chunk-vendors',
64 | test: /[\\/]node_modules[\\/]/,
65 | chunks: 'initial',
66 | priority: 2,
67 | reuseExistingChunk: true,
68 | enforce: true
69 | },
70 | // @sentry|@vueuse|cropperjs 很大 单独分包
71 | vendors2: {
72 | name: 'chunk-vendors-2',
73 | test: /[\\/]node_modules[\\/]@sentry|@vueuse|cropperjs/,
74 | chunks: 'initial',
75 | priority: 3,
76 | reuseExistingChunk: true,
77 | enforce: true
78 | },
79 | // 只有index入口用 且很大 需要单独打包
80 | elementui: {
81 | name: 'chunk-ele',
82 | test: /[\\/]node_modules[\\/]@?element-plus[\\/]/,
83 | chunks: 'initial',
84 | priority: 5,
85 | reuseExistingChunk: true,
86 | enforce: true
87 | }
88 | }
89 | }),
90 | config.optimization.minimizer('terser').tap(args => {
91 | // 移除 debugger
92 | args[0].terserOptions.compress.drop_debugger = true
93 | // 移除 console.log
94 | args[0].terserOptions.compress.pure_funcs = ['console.log']
95 | // 移除 注释
96 | args[0].terserOptions.output = {
97 | comments: false
98 | }
99 | return args
100 | })
101 | })
102 | const svgRule = config.module.rule('svg')
103 | svgRule.uses.clear()
104 | svgRule
105 | .test(/\.svg$/)
106 | .include.add(path.resolve(__dirname, 'src/assets/pic'))
107 | .end()
108 | .use('svg-sprite-loader')
109 | .loader('svg-sprite-loader')
110 | .options({
111 | symbolId: 'icon-[name]'
112 | })
113 | .end()
114 | .use('svgo-loader')
115 | .loader('svgo-loader')
116 | .options({
117 | name: 'removeAttrs',
118 | params: { attrs: 'fill' }
119 | })
120 | .end()
121 | config.module
122 | .rule('worker')
123 | .test(/\.worker\.js$/)
124 | .use('worker-loader')
125 | .loader('worker-loader')
126 | .end()
127 | // 解决:worker 热更新问题
128 | config.module.rule('js').exclude.add(/\.worker\.js$/)
129 | config.resolve.alias
130 | .set('@', getAliasPath('src'))
131 | .set('assets', getAliasPath('src/assets'))
132 | .set('hooks', getAliasPath('src/hooks'))
133 | .set('views', getAliasPath('src/views'))
134 | .set('store', getAliasPath('src/store'))
135 | .set('components', getAliasPath('src/components'))
136 | },
137 | configureWebpack: {
138 | resolveLoader: {
139 | modules: ['node_modules', path.resolve(__dirname, 'loaders')],
140 | },
141 | module: {
142 | rules: [
143 | {
144 | test: /\.mjs$/,
145 | include: /node_modules/,
146 | type: 'javascript/auto'
147 | }
148 | ]
149 | },
150 | plugins: [
151 | new HardSourceWebpackPlugin(),
152 | new webpack.DefinePlugin({
153 | BASE_API_URL: IS_PROD
154 | ? JSON.stringify('https://mapapi.kimjisoo.cn')
155 | : JSON.stringify('http://localhost:3003')
156 | }),
157 | AutoImport({
158 | resolvers: [ElementPlusResolver()]
159 | }),
160 | Components({
161 | resolvers: [ElementPlusResolver()]
162 | }),
163 | require('unplugin-element-plus/webpack')({}),
164 | new SentryWebpackPlugin({
165 | dryRun: !IS_PROD, // 只有生成环境才上传source map
166 | include: 'dist',
167 | ignore: ['node_modules', 'vue.config.js']
168 | }),
169 | //* 实际上 七牛cdn对打包好的源文件自动进行了gzip
170 | // new CompressionWebpackPlugin({
171 | // filename: '[path].gz[query]',
172 | // algorithm: 'gzip',
173 | // test: /\.js$|\.css$|\.html$|\.ttf$|\.eot$|\.woff$/,
174 | // threshold: 10240,
175 | // minRatio: 0.8,
176 | // deleteOriginalAssets: false
177 | // }),
178 | new SpeedMeasurePlugin()
179 | ]
180 | }
181 | }
182 |
--------------------------------------------------------------------------------