Compare commits

...

21 Commits

Author SHA1 Message Date
wanglin25
35d6297b72 Merge branch 'dev6' into main 2022-10-11 18:54:24 +08:00
wanglin25
5afe0a8c41 打包 2022-10-11 18:53:30 +08:00
wanglin25
81f86ccb30 优化:插入子节点时自动展开 2022-10-11 18:48:52 +08:00
wanglin25
87383498c8 修复小地图关闭时报错的问题 2022-10-11 18:36:16 +08:00
wanglin25
c68608c72c 合并 2022-10-11 11:10:00 +08:00
wanglin25
9d325d99a2 更新版本 2022-10-11 11:08:58 +08:00
wanglin2
bc59fa6dc7 修改文档 2022-10-10 22:40:44 +08:00
wanglin2
2daa59679a 修改文档 2022-10-10 22:40:12 +08:00
wanglin25
da456eeb8f 打包 2022-10-10 16:27:49 +08:00
wanglin25
5cd36b57d5 修复子节点收起状态复制时丢失的问题 2022-10-10 16:11:00 +08:00
wanglin25
e2b239fcbb 更新文档 2022-10-10 15:02:22 +08:00
wanglin25
5449e79d49 打包 2022-10-10 14:39:09 +08:00
wanglin25
c2045ddedc 支持小地图 2022-10-10 14:31:43 +08:00
wanglin2
5dfa215538 修改文档 2022-09-30 22:16:27 +08:00
wanglin25
d90013da71 打包 2022-09-30 14:48:08 +08:00
wanglin25
b3b74323f7 逻辑结构图、思维导图新增直线连接风格、直连风格 2022-09-30 14:44:40 +08:00
wanglin2
f9000ea478 逻辑结构图和思维导图支持直线连接线开发中 2022-09-25 22:07:21 +08:00
wanglin2
830e7e2482 打包 2022-09-24 20:42:53 +08:00
wanglin2
13ed7f28df 优化:手动创建节点时立即聚焦 2022-09-24 20:38:47 +08:00
wanglin2
17e79a0b23 修复连线样式深度更新问题 2022-09-24 20:13:53 +08:00
wanglin2
eee310ba49 支持新建、打开、 2022-09-24 17:08:11 +08:00
33 changed files with 1188 additions and 90 deletions

134
README.md
View File

@@ -3,24 +3,16 @@
## 特性
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图四种结构
- [x] 内置多种主题,允许高度自定义样式
- [x] 支持快捷键
- [x] 节点内容支持图片、图标、超链接、备注、标签、概要
- [x] 支持前进后退
- [x] 支持拖动、缩放
- [x] 支持右键按住多选
- [x] 支持节点自由拖拽、拖拽调整
- [x] 支持多种节点形状
- [x] 支持导出为`json``png``svg``pdf`,支持从`json``xmind`导入
- [x] 支持小地图
## 目录介绍
@@ -30,7 +22,17 @@
2.`web`
使用`simple-mind-map`工具库,基于`vue2.x``ElementUI`搭建的在线思维导图。
使用`simple-mind-map`工具库,基于`vue2.x``ElementUI`搭建的在线思维导图。特性:
- [x] 工具栏,支持插入节点、删除节点;编辑节点图片、图标、超链接、备注、标签、概要
- [x] 侧边栏,基础样式设置面板、节点样式设置面板、大纲面板、主题选择面板、结构选择面板
- [x] 导入导出功能;数据默认保存在浏览器本地存储,也支持直接创建、打开、编辑电脑本地文件
- [x] 右键菜单,支持展开、收起、整理布局等操作
- [x] 底部栏,支持节点数量、字数统计;支持切换编辑和只读模式;支持放大缩小;支持全屏切换
3.`dist`
@@ -91,7 +93,7 @@ npm run build
# 安装
> 仓库版本0.2.8当前npm版本0.2.8
> 仓库版本0.2.14当前npm版本0.2.14
```bash
npm i simple-mind-map
@@ -303,8 +305,6 @@ v0.1.7+。切换模式为只读或编辑。
| RESET_LAYOUTv0.2.0+ | 一键整理布局 | |
| SET_NODE_SHAPEv0.2.4+ | 设置节点形状 | node要设置的节点、shape形状全部形状https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/Shape.js |
#### setData(data)
动态设置思维导图数据,纯节点数据
@@ -319,6 +319,14 @@ v0.2.7+
`data`:完整数据,结构可参考[exportFullData](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exportFullData.json)
#### getData(withConfig)
v0.2.9+
获取思维导图数据
`withConfig``Boolean`,默认为`false`,即获取的数据只包括节点树,如果传`true`则会包含主题、布局、视图等数据
#### export(type, isDownload, fileName)
导出
@@ -504,6 +512,18 @@ v0.2.3+。恢复保存的快捷键数据,然后清空缓存数据
`y`方向进行平移,`step`:要平移的像素
#### translateXTo(x)
v0.2.11+
平移`x`方向到指定位置
#### translateYTo(y)
v0.2.11+
平移`y`方向到指定位置
#### reset()
恢复到默认的变换
@@ -528,6 +548,94 @@ v0.1.1+
动态设置变换数据可以通过getTransformData方法获取变换数据
## MiniMap实例
v0.2.11+
用于帮助快速开发小地图功能,小地图由两部分组成,一个是当前的画布内容,一个是视口框,当缩放、移动、元素过多时画布上可能只显示了思维导图的部分内容,可以通过视口框来查看当前视口所在位置,以及可以通过在小地图上拖动来快速定位。
可通过`mindMap.miniMap`获取到该实例。
### 方法
#### getMiniMap()
获取小地图相关数据,这个函数一般不会直接使用,函数返回的内容:
```js
{
svg, // Element思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组
svgHTML, // Stringsvg字符串即html字符串可以直接渲染到你准备的小地图容器内
rect: // Object思维导图图形未缩放时的位置尺寸等信息
origWidth, // Number画布宽度
origHeight, // Number画布高度
scaleX, // Number思维导图图形的水平缩放值
scaleY, // Number思维导图图形的垂直缩放值
}
```
#### calculationMiniMap(boxWidth, boxHeight)
计算小地图的渲染数据,该函数内会调用`getMiniMap()`方法,所以一般使用该函数即可。
`boxWidth`:小地图容器的宽度
`boxHeight`:小地图容器的高度
函数返回内容:
```js
{
svgHTML, // 小地图html
viewBoxStyle, // 视图框的位置信息
miniMapBoxScale, // 视图框的缩放值
miniMapBoxLeft, // 视图框的left值
miniMapBoxTop, // 视图框的top值
}
```
小地图思路:
1.准备一个容器元素`container`,定位不为`static`
2.在`container`内创建一个小地图容器元素`miniMapContainer`,绝对定位
3.在`container`内创建一个视口框元素`viewBoxContainer`,绝对定位,设置边框样式,过渡属性(可选)
4.监听`data_change``view_data_change`事件,在该事件内调用`calculationMiniMap`方法获取计算数据,然后将`svgHTML`渲染到`miniMapContainer`元素内,并且设置它的样式:
```js
:style="{
transform: `scale(${svgBoxScale})`,
left: svgBoxLeft + 'px',
top: svgBoxTop + 'px',
}"
```
5.将`viewBoxStyle`对象设置为`viewBoxContainer`元素的样式
到这一步,当画布上的思维导图变化了,小地图也会实时更新,并且视口框元素会实时反映视口在思维导图图形上的位置
6.监听`container`元素的`mousedown``mousemove``mouseup`事件,分别调用下面即将介绍的三个方法即可实现鼠标拖动时画布上的思维导图也随之拖动的效果
#### onMousedown(e)
小地图鼠标按下事件执行该函数
`e`:事件对象
#### onMousemove(e, sensitivityNum = 5)
小地图鼠标移动事件执行该函数
`e`:事件对象
`sensitivityNum`:拖动灵敏度,灵敏度越大,在小地图上拖动相同距离时实际上的画布拖动距离就越大
#### onMouseup()
小地图鼠标松开事件执行该函数
## doExport实例
`doExport`实例负责导出,可通过`mindMap.doExport`获取到该实例

View File

@@ -1 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>一个简单的web思维导图实现</title><link href="dist/js/chunk-2d20ec02.10aa67e3.js" rel="prefetch"><link href="dist/js/chunk-2d216b67.2d06497a.js" rel="prefetch"><link href="dist/js/chunk-5397ae43.9dca0f90.js" rel="prefetch"><link href="dist/css/app.a172fa3f.css" rel="preload" as="style"><link href="dist/css/chunk-vendors.597033a2.css" rel="preload" as="style"><link href="dist/js/app.cb927d32.js" rel="preload" as="script"><link href="dist/js/chunk-vendors.07ae01de.js" rel="preload" as="script"><link href="dist/css/chunk-vendors.597033a2.css" rel="stylesheet"><link href="dist/css/app.a172fa3f.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="dist/js/chunk-vendors.07ae01de.js"></script><script src="dist/js/app.cb927d32.js"></script></body></html>
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>一个简单的web思维导图实现</title><link href="dist/js/chunk-2d20ec02.81d632f4.js" rel="prefetch"><link href="dist/js/chunk-2d216b67.228f2009.js" rel="prefetch"><link href="dist/js/chunk-35b0a040.cb76da7d.js" rel="prefetch"><link href="dist/css/app.8e12b326.css" rel="preload" as="style"><link href="dist/css/chunk-vendors.6fd71983.css" rel="preload" as="style"><link href="dist/js/app.5b772a90.js" rel="preload" as="script"><link href="dist/js/chunk-vendors.d724da21.js" rel="preload" as="script"><link href="dist/css/chunk-vendors.6fd71983.css" rel="stylesheet"><link href="dist/css/app.8e12b326.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="dist/js/chunk-vendors.d724da21.js"></script><script src="dist/js/app.5b772a90.js"></script></body></html>

View File

@@ -10,6 +10,7 @@ import BatchExecution from './src/BatchExecution'
import Export from './src/Export'
import Select from './src/Select'
import Drag from './src/Drag'
import MiniMap from './src/MiniMap';
import {
layoutValueList
} from './src/utils/constant'
@@ -17,6 +18,7 @@ import {
SVG
} from '@svgdotjs/svg.js'
import xmind from './src/parse/xmind'
import { simpleDeepClone } from './src/utils';
// 默认选项配置
const defaultOpt = {
@@ -115,6 +117,11 @@ class MindMap {
draw: this.draw
})
// 小地图类
this.miniMap = new MiniMap({
mindMap: this
})
// 导出类
this.doExport = new Export({
mindMap: this
@@ -355,6 +362,31 @@ class MindMap {
}
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-24 14:42:07
* @Desc: 获取思维导图数据,节点树、主题、布局等
*/
getData(withConfig) {
let nodeData = this.command.getCopyData()
let data = {}
if (withConfig) {
data = {
layout: this.getLayout(),
root: nodeData,
theme: {
template: this.getTheme(),
config: this.getCustomThemeConfig()
},
view: this.view.getTransformData()
}
} else {
data = nodeData
}
return simpleDeepClone(data)
}
/**
* @Author: 王林
* @Date: 2021-07-01 22:06:38

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.2.8",
"version": "0.2.14",
"description": "一个简单的web在线思维导图",
"authors": [
{

View File

@@ -44,28 +44,9 @@ class Export {
* @Desc: 获取svg数据
*/
async getSvgData() {
const svg = this.mindMap.svg
const draw = this.mindMap.draw
// 保存原始信息
const origWidth = svg.width()
const origHeight = svg.height()
const origTransform = draw.transform()
const elRect = this.mindMap.el.getBoundingClientRect()
// 去除放大缩小的变换效果
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
// 克隆一份数据
const clone = svg.clone()
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight)
draw.transform(origTransform)
let { svg, svgHTML } = this.mindMap.miniMap.getMiniMap()
// 把图片的url转换成data:url类型否则导出会丢失图片
let imageList = clone.find('image')
let imageList = svg.find('image')
let task = imageList.map(async (item) => {
let imgUlr = item.attr('href') || item.attr('xlink:href')
let imgData = await imgToDataUrl(imgUlr)
@@ -73,8 +54,8 @@ class Export {
})
await Promise.all(task)
return {
node: clone,
str: clone.svg()
node: svg,
str: svgHTML
}
}
@@ -249,21 +230,7 @@ class Export {
* @Desc: 导出为json
*/
json (name, withConfig = true) {
let nodeData = this.mindMap.command.getCopyData()
let data = {}
if (withConfig) {
data = {
layout: this.mindMap.getLayout(),
root: nodeData,
theme: {
template: this.mindMap.getTheme(),
config: this.mindMap.getCustomThemeConfig()
},
view: this.mindMap.view.getTransformData()
}
} else {
data = nodeData
}
let data = this.mindMap.getData(withConfig)
let str = JSON.stringify(data)
let blob = new Blob([str])
return URL.createObjectURL(blob)

View File

@@ -0,0 +1,175 @@
// 小地图类
class MiniMap {
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:00:45
* @Desc: 构造函数
*/
constructor(opt) {
this.mindMap = opt.mindMap;
this.isMousedown = false;
this.mousedownPos = {
x: 0,
y: 0,
};
this.startViewPos = {
x: 0,
y: 0,
};
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:00:43
* @Desc: 获取小地图相关数据
*/
getMiniMap() {
const svg = this.mindMap.svg;
const draw = this.mindMap.draw;
// 保存原始信息
const origWidth = svg.width();
const origHeight = svg.height();
const origTransform = draw.transform();
const elRect = this.mindMap.el.getBoundingClientRect();
// 去除放大缩小的变换效果
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY);
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox();
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height);
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top);
// 克隆一份数据
const clone = svg.clone();
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight);
draw.transform(origTransform);
return {
svg: clone, // 思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组
svgHTML: clone.svg(), // svg字符串
rect: {
...rect, // 思维导图图形未缩放时的位置尺寸等信息
ratio: rect.width / rect.height, // 思维导图图形的宽高比
},
origWidth, // 画布宽度
origHeight, // 画布高度
scaleX: origTransform.scaleX, // 思维导图图形的水平缩放值
scaleY: origTransform.scaleY, // 思维导图图形的垂直缩放值
};
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:05:51
* @Desc: 计算小地图的渲染数据
* boxWidth小地图容器的宽度
* boxHeight小地图容器的高度
*/
calculationMiniMap(boxWidth, boxHeight) {
let { svgHTML, rect, origWidth, origHeight, scaleX, scaleY } =
this.getMiniMap();
// 计算数据
let boxRatio = boxWidth / boxHeight;
let actWidth = 0;
let actHeight = 0;
if (boxRatio > rect.ratio) {
// 高度以box为准缩放宽度
actHeight = boxHeight;
actWidth = rect.ratio * actHeight;
} else {
// 宽度以box为准缩放高度
actWidth = boxWidth;
actHeight = actWidth / rect.ratio;
}
// svg图形的缩放及位置
let miniMapBoxScale = actWidth / rect.width;
let miniMapBoxLeft = (boxWidth - actWidth) / 2;
let miniMapBoxTop = (boxHeight - actHeight) / 2;
// 视口框大小及位置
let _rectX = rect.x - (rect.width * scaleX - rect.width) / 2;
let _rectX2 = rect.x2 + (rect.width * scaleX - rect.width) / 2;
let _rectY = rect.y - (rect.height * scaleY - rect.height) / 2;
let _rectY2 = rect.y2 + (rect.height * scaleY - rect.height) / 2;
let _rectWidth = rect.width * scaleX;
let _rectHeight = rect.height * scaleY;
let viewBoxStyle = {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
viewBoxStyle.left =
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft + "px";
viewBoxStyle.right =
Math.max(0, ((_rectX2 - origWidth) / _rectWidth) * actWidth) +
miniMapBoxLeft +
"px";
viewBoxStyle.top =
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop + "px";
viewBoxStyle.bottom =
Math.max(0, ((_rectY2 - origHeight) / _rectHeight) * actHeight) +
miniMapBoxTop +
"px";
return {
svgHTML, // 小地图html
viewBoxStyle, // 视图框的位置信息
miniMapBoxScale, // 视图框的缩放值
miniMapBoxLeft, // 视图框的left值
miniMapBoxTop, // 视图框的top值
};
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:22:40
* @Desc: 小地图鼠标按下事件
*/
onMousedown(e) {
this.isMousedown = true;
this.mousedownPos = {
x: e.clientX,
y: e.clientY,
};
// 保存视图当前的偏移量
let transformData = this.mindMap.view.getTransformData();
this.startViewPos = {
x: transformData.state.x,
y: transformData.state.y,
};
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:22:55
* @Desc: 小地图鼠标移动事件
*/
onMousemove(e, sensitivityNum = 5) {
if (!this.isMousedown) {
return;
}
let ox = e.clientX - this.mousedownPos.x;
let oy = e.clientY - this.mousedownPos.y;
// 在视图最初偏移量上累加更新量
this.mindMap.view.translateXTo(ox * sensitivityNum + this.startViewPos.x);
this.mindMap.view.translateYTo(oy * sensitivityNum + this.startViewPos.y);
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:23:01
* @Desc: 小地图鼠标松开事件
*/
onMouseup() {
this.isMousedown = false;
}
}
export default MiniMap;

View File

@@ -694,7 +694,7 @@ class Node {
if (this.mindMap.opt.readonly) {
return
}
e.stopPropagation()
e && e.stopPropagation()
if (this.nodeData.data.isActive) {
return
}
@@ -769,6 +769,12 @@ class Node {
}
}))
}
// 手动插入的节点立即获得焦点并且开启编辑模式
if (this.nodeData.inserting) {
delete this.nodeData.inserting
this.active()
this.mindMap.emit('node_dblclick', this)
}
}
/**
@@ -845,7 +851,7 @@ class Node {
* @Date: 2021-04-10 22:01:53
* @Desc: 连线
*/
renderLine() {
renderLine(deep = false) {
if (this.nodeData.data.expand === false) {
return
}
@@ -866,10 +872,12 @@ class Node {
this.renderer.layout.renderLine(this, this._lines, (line, node) => {
// 添加样式
this.styleLine(line, node)
})
// 和父级的连线也需要更新
if (this.parent) {
this.parent.renderLine()
}, this.style.getStyle('lineStyle', true))
// 级的连线也需要更新
if (deep && this.children && this.children.length > 0) {
this.children.forEach((item) => {
item.renderLine(deep)
})
}
}

View File

@@ -5,7 +5,8 @@ import CatalogOrganization from './layouts/CatalogOrganization'
import OrganizationStructure from './layouts/OrganizationStructure'
import TextEdit from './TextEdit'
import { copyNodeTree, simpleDeepClone, walk } from './utils'
import { shapeList } from './Shape';
import { shapeList } from './Shape'
import { lineStyleProps } from './themes/default'
// 布局列表
const layouts = {
@@ -412,6 +413,7 @@ class Render {
}
let index = this.getNodeIndex(first)
first.parent.nodeData.children.splice(index + 1, 0, {
"inserting": true,
"data": {
"text": text,
"expand": true
@@ -437,12 +439,15 @@ class Render {
}
let text = node.isRoot ? '二级节点' : '分支主题'
node.nodeData.children.push({
"inserting": true,
"data": {
"text": text,
"expand": true
},
"children": []
})
// 插入子节点时自动展开子节点
node.nodeData.data.expand = true
if (node.isRoot) {
node.initRender = true
// this.mindMap.batchExecution.push('renderNode' + index, () => {
@@ -655,7 +660,7 @@ class Render {
if (this.activeNodeList.length <= 0) {
return
}
return copyNodeTree({}, this.activeNodeList[0])
return copyNodeTree({}, this.activeNodeList[0], true)
}
/**
@@ -671,7 +676,7 @@ class Render {
if (node.isRoot) {
return null
}
let copyData = copyNodeTree({}, node)
let copyData = copyNodeTree({}, node, true)
this.removeActiveNode(node)
this.removeOneNode(node)
this.mindMap.emit('node_active', null, this.activeNodeList)
@@ -734,6 +739,10 @@ class Render {
}
}
this.setNodeDataRender(node, data)
// 更新了连线的样式
if (lineStyleProps.includes(prop)) {
(node.parent || node).renderLine(true)
}
}
/**

View File

@@ -126,6 +126,17 @@ class View {
this.transform()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:03:53
* @Desc: 平移x方式到
*/
translateXTo(x) {
this.x = x
this.transform()
}
/**
* javascript comment
* @Author: 王林25
@@ -137,6 +148,17 @@ class View {
this.transform()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:04:10
* @Desc: 平移y方向到
*/
translateYTo(y) {
this.y = y
this.transform()
}
/**
* @Author: 王林
* @Date: 2021-07-04 17:13:14

View File

@@ -146,7 +146,81 @@ class LogicalStructure extends Base {
* @Date: 2021-04-11 14:42:48
* @Desc: 绘制连线,连接该节点到其子节点
*/
renderLine(node, lines, style) {
renderLine(node, lines, style, lineStyle) {
if (lineStyle === 'curve') {
this.renderLineCurve(node, lines, style)
} else if (lineStyle === 'direct') {
this.renderLineDirect(node, lines, style)
} else {
this.renderLineStraight(node, lines, style)
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-30 14:17:30
* @Desc: 直线风格连线
*/
renderLineStraight(node, lines, style) {
if (node.children.length <= 0) {
return [];
}
let {
left,
top,
width,
height,
expandBtnSize
} = node
let marginX = this.getMarginX(node.layerIndex + 1)
let s1 = (marginX - expandBtnSize) * 0.6
node.children.forEach((item, index) => {
let x1 = node.layerIndex === 0 ? left + width : left + width + expandBtnSize
let y1 = top + height / 2
let x2 = item.left
let y2 = item.top + item.height / 2
let path = `M ${x1},${y1} L ${x1 + s1},${y1} L ${x1 + s1},${y2} L ${x2},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
})
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-30 14:34:41
* @Desc: 直连风格
*/
renderLineDirect(node, lines, style) {
if (node.children.length <= 0) {
return [];
}
let {
left,
top,
width,
height,
expandBtnSize
} = node
node.children.forEach((item, index) => {
let x1 = node.layerIndex === 0 ? left + width / 2 : left + width + expandBtnSize
let y1 = top + height / 2
let x2 = item.left
let y2 = item.top + item.height / 2
let path = `M ${x1},${y1} L ${x2},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
})
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-30 14:17:43
* @Desc: 曲线风格连线
*/
renderLineCurve(node, lines, style) {
if (node.children.length <= 0) {
return [];
}

View File

@@ -182,7 +182,89 @@ class MindMap extends Base {
* @Date: 2021-04-11 14:42:48
* @Desc: 绘制连线,连接该节点到其子节点
*/
renderLine(node, lines, style) {
renderLine(node, lines, style, lineStyle) {
if (lineStyle === 'curve') {
this.renderLineCurve(node, lines, style)
} else if (lineStyle === 'direct') {
this.renderLineDirect(node, lines, style)
} else {
this.renderLineStraight(node, lines, style)
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-30 14:10:47
* @Desc: 直线风格连线
*/
renderLineStraight(node, lines, style) {
if (node.children.length <= 0) {
return [];
}
let {
left,
top,
width,
height,
expandBtnSize
} = node
let marginX = this.getMarginX(node.layerIndex + 1)
let s1 = (marginX - expandBtnSize) * 0.6
node.children.forEach((item, index) => {
let x1 = 0
let _s = 0
if (item.dir === 'left') {
_s = -s1
x1 = node.layerIndex === 0 ? left : left - expandBtnSize
} else {
_s = s1
x1 = node.layerIndex === 0 ? left + width : left + width + expandBtnSize
}
let y1 = top + height / 2
let x2 = item.dir === 'left' ? item.left + item.width : item.left
let y2 = item.top + item.height / 2
let path = `M ${x1},${y1} L ${x1 + _s},${y1} L ${x1 + _s},${y2} L ${x2},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
})
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-30 14:34:41
* @Desc: 直连风格
*/
renderLineDirect(node, lines, style) {
if (node.children.length <= 0) {
return [];
}
let {
left,
top,
width,
height,
expandBtnSize
} = node
node.children.forEach((item, index) => {
let x1 = node.layerIndex === 0 ? left + width / 2 : item.dir === 'left' ? left - expandBtnSize : left + width + expandBtnSize
let y1 = top + height / 2
let x2 = item.dir === 'left' ? item.left + item.width : item.left
let y2 = item.top + item.height / 2
let path = `M ${x1},${y1} L ${x2},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
})
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-30 14:10:56
* @Desc: 曲线风格连线
*/
renderLineCurve(node, lines, style) {
if (node.children.length <= 0) {
return [];
}

View File

@@ -147,7 +147,48 @@ class OrganizationStructure extends Base {
* @Date: 2021-04-11 14:42:48
* @Desc: 绘制连线,连接该节点到其子节点
*/
renderLine(node, lines, style) {
renderLine(node, lines, style, lineStyle) {
if (lineStyle === 'direct') {
this.renderLineDirect(node, lines, style)
} else {
this.renderLineStraight(node, lines, style)
}
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-30 14:34:41
* @Desc: 直连风格
*/
renderLineDirect(node, lines, style) {
if (node.children.length <= 0) {
return [];
}
let {
left,
top,
width,
height,
} = node
let x1 = left + width / 2
let y1 = top + height
node.children.forEach((item, index) => {
let x2 = item.left + item.width / 2
let y2 = item.top
let path = `M ${x1},${y1} L ${x2},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
})
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-09-30 14:39:07
* @Desc: 直线风格连线
*/
renderLineStraight(node, lines, style) {
if (node.children.length <= 0) {
return [];
}

View File

@@ -19,6 +19,8 @@ export default {
lineColor: '#549688',
// 连线样式
lineDasharray: 'none',
// 连线风格
lineStyle: 'straight',// 针对logicalStructure、mindMap两种结构。曲线curve、直线straight、直连direct
// 概要连线的粗细
generalizationLineWidth: 1,
// 概要连线的颜色
@@ -127,4 +129,6 @@ export default {
// 支持激活样式的属性
// 简单来说,会改变节点大小的都不支持在激活时设置,为了性能考虑,节点切换激活态时不会重新计算节点大小
export const supportActiveStyle = ['fillColor', 'color', 'fontWeight', 'fontStyle', 'borderColor', 'borderWidth', 'borderDasharray', 'borderRadius', 'textDecoration']
export const supportActiveStyle = ['fillColor', 'color', 'fontWeight', 'fontStyle', 'borderColor', 'borderWidth', 'borderDasharray', 'borderRadius', 'textDecoration']
export const lineStyleProps = ['lineColor', 'lineDasharray', 'lineWidth']

View File

@@ -147,13 +147,19 @@ export const copyRenderTree = (tree, root) => {
* @Date: 2021-05-04 14:40:11
* @Desc: 复制节点树数据
*/
export const copyNodeTree = (tree, root) => {
tree.data = simpleDeepClone(root.nodeData.data)
// tree.data.isActive = false
export const copyNodeTree = (tree, root, removeActiveState = false) => {
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data)
if (removeActiveState) {
tree.data.isActive = false
}
tree.children = []
if (root.children && root.children.length > 0) {
root.children.forEach((item, index) => {
tree.children[index] = copyNodeTree({}, item)
tree.children[index] = copyNodeTree({}, item, removeActiveState)
})
} else if (root.nodeData && root.nodeData.children && root.nodeData.children.length > 0) {
root.nodeData.children.forEach((item, index) => {
tree.children[index] = copyNodeTree({}, item, removeActiveState)
})
}
return tree;

BIN
web/src/.DS_Store vendored

Binary file not shown.

View File

@@ -1,5 +1,6 @@
import exampleData from "simple-mind-map/example/exampleData"
import { simpleDeepClone } from 'simple-mind-map/src/utils/index'
import Vue from 'vue'
const SIMPLE_MIND_MAP_DATA = 'SIMPLE_MIND_MAP_DATA'
@@ -47,6 +48,7 @@ export const storeData = (data) => {
try {
let originData = getData()
originData.root = copyMindMapTreeData({}, data)
Vue.prototype.$bus.$emit('write_local_file', originData)
let dataStr = JSON.stringify(originData)
localStorage.setItem(SIMPLE_MIND_MAP_DATA, dataStr)
} catch (error) {
@@ -66,6 +68,7 @@ export const storeConfig = (config) => {
...originData,
...config
}
Vue.prototype.$bus.$emit('write_local_file', originData)
let dataStr = JSON.stringify(originData)
localStorage.setItem(SIMPLE_MIND_MAP_DATA, dataStr)
} catch (error) {

Binary file not shown.

Binary file not shown.

View File

@@ -3,8 +3,8 @@
<head>
<meta charset="utf-8"/>
<title>iconfont Demo</title>
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i2/O1CN01ZyAlrn1MwaMhqz36G_!!6000000001499-73-tps-64-64.ico" type="image/x-icon"/>
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01EYTRnJ297D6vehehJ_!!6000000008020-55-tps-64-64.svg"/>
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
<link rel="stylesheet" href="demo.css">
<link rel="stylesheet" href="iconfont.css">
@@ -54,6 +54,36 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe63e;</span>
<div class="name">导出</div>
<div class="code-name">&amp;#xe63e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe657;</span>
<div class="name">另存为</div>
<div class="code-name">&amp;#xe657;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe642;</span>
<div class="name">export</div>
<div class="code-name">&amp;#xe642;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xebdf;</span>
<div class="name">打开</div>
<div class="code-name">&amp;#xebdf;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe64e;</span>
<div class="name">新建</div>
<div class="code-name">&amp;#xe64e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe601;</span>
<div class="name">剪切</div>
@@ -300,9 +330,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1659615576455') format('woff2'),
url('iconfont.woff?t=1659615576455') format('woff'),
url('iconfont.ttf?t=1659615576455') format('truetype');
src: url('iconfont.woff2?t=1664005697217') format('woff2'),
url('iconfont.woff?t=1664005697217') format('woff'),
url('iconfont.ttf?t=1664005697217') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -328,6 +358,51 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icondaochu1"></span>
<div class="name">
导出
</div>
<div class="code-name">.icondaochu1
</div>
</li>
<li class="dib">
<span class="icon iconfont iconlingcunwei"></span>
<div class="name">
另存为
</div>
<div class="code-name">.iconlingcunwei
</div>
</li>
<li class="dib">
<span class="icon iconfont iconexport"></span>
<div class="name">
export
</div>
<div class="code-name">.iconexport
</div>
</li>
<li class="dib">
<span class="icon iconfont icondakai"></span>
<div class="name">
打开
</div>
<div class="code-name">.icondakai
</div>
</li>
<li class="dib">
<span class="icon iconfont iconxinjian"></span>
<div class="name">
新建
</div>
<div class="code-name">.iconxinjian
</div>
</li>
<li class="dib">
<span class="icon iconfont iconjianqie"></span>
<div class="name">
@@ -697,6 +772,46 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icondaochu1"></use>
</svg>
<div class="name">导出</div>
<div class="code-name">#icondaochu1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#iconlingcunwei"></use>
</svg>
<div class="name">另存为</div>
<div class="code-name">#iconlingcunwei</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#iconexport"></use>
</svg>
<div class="name">export</div>
<div class="code-name">#iconexport</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icondakai"></use>
</svg>
<div class="name">打开</div>
<div class="code-name">#icondakai</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#iconxinjian"></use>
</svg>
<div class="name">新建</div>
<div class="code-name">#iconxinjian</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#iconjianqie"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2479351 */
src: url('iconfont.woff2?t=1659615576455') format('woff2'),
url('iconfont.woff?t=1659615576455') format('woff'),
url('iconfont.ttf?t=1659615576455') format('truetype');
src: url('iconfont.woff2?t=1664005697217') format('woff2'),
url('iconfont.woff?t=1664005697217') format('woff'),
url('iconfont.ttf?t=1664005697217') format('truetype');
}
.iconfont {
@@ -13,6 +13,26 @@
-moz-osx-font-smoothing: grayscale;
}
.icondaochu1:before {
content: "\e63e";
}
.iconlingcunwei:before {
content: "\e657";
}
.iconexport:before {
content: "\e642";
}
.icondakai:before {
content: "\ebdf";
}
.iconxinjian:before {
content: "\e64e";
}
.iconjianqie:before {
content: "\e601";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,41 @@
"css_prefix_text": "icon",
"description": "思维导图",
"glyphs": [
{
"icon_id": "1305460",
"name": "导出",
"font_class": "daochu1",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "4784101",
"name": "另存为",
"font_class": "lingcunwei",
"unicode": "e657",
"unicode_decimal": 58967
},
{
"icon_id": "9929033",
"name": "export",
"font_class": "export",
"unicode": "e642",
"unicode_decimal": 58946
},
{
"icon_id": "4570294",
"name": "打开",
"font_class": "dakai",
"unicode": "ebdf",
"unicode_decimal": 60383
},
{
"icon_id": "5086088",
"name": "新建",
"font_class": "xinjian",
"unicode": "e64e",
"unicode_decimal": 58958
},
{
"icon_id": "1117",
"name": "剪切",

View File

@@ -130,6 +130,22 @@ export const borderRadiusList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 线宽
export const lineWidthList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 连线风格
export const lineStyleList = [
{
name: '直线',
value: 'straight'
},
{
name: '曲线',
value: 'curve'
},
{
name: '直连',
value: 'direct'
}
]
// 图片重复方式
export const backgroundRepeatList = [
{

View File

@@ -94,6 +94,30 @@
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">风格</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.lineStyle"
placeholder=""
@change="
(value) => {
update('lineStyle', value);
}
"
>
<el-option
v-for="item in lineStyleList"
:key="item.value"
:label="item.name"
:value="item.value"
>
</el-option>
</el-select>
</div>
</div>
<!-- 概要连线 -->
<div class="title noTop">概要的连线</div>
<div class="row">
@@ -267,6 +291,7 @@ import Sidebar from "./Sidebar";
import Color from "./Color";
import {
lineWidthList,
lineStyleList,
backgroundRepeatList
} from "@/config";
import ImgUpload from "@/components/ImgUpload";
@@ -296,6 +321,7 @@ export default {
data() {
return {
lineWidthList,
lineStyleList,
backgroundRepeatList,
activeTab: "color",
marginActiveTab: "second",
@@ -303,6 +329,7 @@ export default {
backgroundColor: "",
lineColor: "",
lineWidth: "",
lineStyle: "",
generalizationLineWidth: "",
generalizationLineColor: "",
paddingX: 0,
@@ -336,6 +363,7 @@ export default {
[
"backgroundColor",
"lineWidth",
"lineStyle",
"lineColor",
"generalizationLineWidth",
"generalizationLineColor",

View File

@@ -2,6 +2,7 @@
<div class="editContainer">
<div class="mindMapContainer" ref="mindMapContainer"></div>
<Count></Count>
<Navigator :mindMap="mindMap"></Navigator>
<NavigatorToolbar :mindMap="mindMap"></NavigatorToolbar>
<Outline></Outline>
<Style></Style>
@@ -27,6 +28,7 @@ import ShortcutKey from './ShortcutKey'
import Contextmenu from './Contextmenu'
import NodeNoteContentShow from './NodeNoteContentShow.vue'
import { getData, storeData, storeConfig } from '@/api'
import Navigator from './Navigator.vue';
/**
* @Author: 王林
@@ -45,7 +47,8 @@ export default {
NavigatorToolbar,
ShortcutKey,
Contextmenu,
NodeNoteContentShow
NodeNoteContentShow,
Navigator
},
data() {
return {
@@ -154,12 +157,8 @@ export default {
if (this.openTest) {
return
}
let data = this.mindMap.command.getCopyData()
storeData(data)
let viewData = this.mindMap.view.getTransformData()
storeConfig({
view: viewData,
})
let data = this.mindMap.getData(true)
storeConfig(data)
},
/**

View File

@@ -82,6 +82,7 @@ export default {
if (this.fileList.length <= 0) {
return this.$message.error("请选择要导入的文件");
}
this.$store.commit('setIsHandleLocalFile', false);
let file = this.fileList[0];
if (/\.(smm|json)$/.test(file.name)) {
this.handleSmm(file)

View File

@@ -0,0 +1,143 @@
<template>
<div
v-if="showMiniMap"
class="navigatorBox"
ref="navigatorBox"
@mousedown="onMousedown"
@mousemove="onMousemove"
@mouseup="onMouseup"
>
<div
class="svgBox"
ref="svgBox"
:style="{
transform: `scale(${svgBoxScale})`,
left: svgBoxLeft + 'px',
top: svgBoxTop + 'px',
}"
></div>
<div class="windowBox" :style="viewBoxStyle"></div>
</div>
</template>
<script>
export default {
props: {
mindMap: {
type: Object,
},
},
data() {
return {
showMiniMap: false,
timer: null,
boxWidth: 0,
boxHeight: 0,
svgBoxScale: 1,
svgBoxLeft: 0,
svgBoxTop: 0,
viewBoxStyle: {
left: 0,
top: 0,
bottom: 0,
right: 0,
},
};
},
mounted() {
this.$bus.$on("toggle_mini_map", (show) => {
this.showMiniMap = show;
this.$nextTick(() => {
if (show) {
this.init();
this.drawMiniMap();
}
});
});
this.$bus.$on("data_change", () => {
if (!this.showMiniMap) {
return;
}
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.drawMiniMap();
}, 500);
});
this.$bus.$on("view_data_change", () => {
if (!this.showMiniMap) {
return;
}
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.drawMiniMap();
}, 500);
});
},
methods: {
init() {
let { width, height } = this.$refs.navigatorBox.getBoundingClientRect();
this.boxWidth = width;
this.boxHeight = height;
},
drawMiniMap() {
let {
svgHTML,
viewBoxStyle,
miniMapBoxScale,
miniMapBoxLeft,
miniMapBoxTop,
} = this.mindMap.miniMap.calculationMiniMap(
this.boxWidth,
this.boxHeight
);
// 渲染到小地图
this.$refs.svgBox.innerHTML = svgHTML;
this.viewBoxStyle = viewBoxStyle;
this.svgBoxScale = miniMapBoxScale;
this.svgBoxLeft = miniMapBoxLeft;
this.svgBoxTop = miniMapBoxTop;
},
onMousedown(e) {
this.mindMap.miniMap.onMousedown(e);
},
onMousemove(e) {
this.mindMap.miniMap.onMousemove(e);
},
onMouseup(e) {
this.mindMap.miniMap.onMouseup(e);
},
},
};
</script>
<style lang="less" scoped>
.navigatorBox {
position: absolute;
width: 350px;
height: 220px;
background-color: #fff;
bottom: 80px;
right: 20px;
box-shadow: 0 0 16px #989898;
border-radius: 4px;
border: 1px solid #eee;
cursor: pointer;
user-select: none;
.svgBox {
position: absolute;
left: 0;
transform-origin: left top;
}
.windowBox {
position: absolute;
border: 2px solid rgb(238, 69, 69);
transition: all 0.3s;
}
}
</style>

View File

@@ -1,5 +1,8 @@
<template>
<div class="navigatorContainer">
<div class="item">
<el-checkbox v-model="openMiniMap" @change="toggleMiniMap">开启小地图</el-checkbox>
</div>
<div class="item">
<el-switch
v-model="isReadonly"
@@ -40,12 +43,20 @@ export default {
},
data () {
return {
isReadonly: false
isReadonly: false,
openMiniMap: false
}
},
mounted () {
this.toggleMiniMap(this.openMiniMap)
},
methods: {
readonlyChange(value) {
this.mindMap.setMode(value ? 'readonly' : 'edit')
},
toggleMiniMap(show) {
this.$bus.$emit('toggle_mini_map', show)
}
}
};

View File

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

View File

@@ -6,7 +6,8 @@ Vue.use(Vuex)
const store = new Vuex.Store({
state: {
mindMapData: null // 思维导图数据
mindMapData: null, // 思维导图数据
isHandleLocalFile: false// 是否操作的是本地文件
},
mutations: {
/**
@@ -16,6 +17,16 @@ const store = new Vuex.Store({
*/
setMindMapData(state, data) {
state.mindMapData = data
},
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-24 13:55:38
* @Desc: 设置操作本地文件标志位
*/
setIsHandleLocalFile(state, data) {
state.isHandleLocalFile = data
}
},
actions: {