Compare commits

...

34 Commits

Author SHA1 Message Date
wanglin2
c789a95e4e 支持直接双击文件打开应用进入编辑 2023-05-05 17:37:01 +08:00
wanglin2
b0532072c2 修改打包配置 2023-05-05 16:59:39 +08:00
wanglin2
7d21ae4618 打包 2023-05-05 16:04:15 +08:00
wanglin2
0e608de9da 基本完成 2023-05-05 14:55:01 +08:00
wanglin2
fbff68c635 开发中 2023-05-05 10:51:39 +08:00
wanglin2
ec9517c491 开发中 2023-05-04 22:40:44 +08:00
wanglin2
26e3158bd8 开发中 2023-05-04 17:54:31 +08:00
wanglin2
c2c9de1c03 更新 2023-05-03 22:06:52 +08:00
wanglin2
27d2238d20 更新 2023-05-03 21:26:13 +08:00
wanglin2
01dc98f1f8 打包0.5.11 2023-05-02 10:01:08 +08:00
wanglin2
cbd07246bd 更新文档 2023-05-02 10:00:13 +08:00
wanglin2
5bb23ca738 优化主题配置更新,改变不涉及节点大小的配置不触发节点重新计算 2023-04-28 22:07:53 +08:00
wanglin2
0886ba7698 完善关联线文本编辑 2023-04-28 21:51:58 +08:00
wanglin2
a65cffa58b 完善关联线文本编辑 2023-04-28 17:26:37 +08:00
wanglin2
02e2d432dd Feature:关联线支持文本开发中 2023-04-28 17:10:39 +08:00
wanglin2
5c9c3d7934 打包0.5.10-fix.2 2023-04-26 16:02:47 +08:00
wanglin2
4ca6713675 更新文档 2023-04-26 15:54:28 +08:00
wanglin2
3d18404fd6 Fix:修复富文本模式下,切换主题、导入数据后没有触发数据改变的问题 2023-04-26 15:49:52 +08:00
wanglin2
98dda26bf8 新增三种主题 2023-04-26 15:48:58 +08:00
wanglin2
cd4c5ecd83 'Fix:修复导入出错的问题' 2023-04-26 11:20:34 +08:00
wanglin2
fdc0017ccb '打包0.5.10' 2023-04-26 11:02:51 +08:00
wanglin2
20a5c12bbb '更新文档' 2023-04-26 10:58:01 +08:00
wanglin2
4eacec125e Feature:使用LRU缓存算法优化节点复用 2023-04-26 10:37:32 +08:00
wanglin2
34322ba6d1 更新文档 2023-04-25 17:29:56 +08:00
wanglin2
8210151a7b 打包0.5.9 2023-04-25 17:19:14 +08:00
wanglin2
1e00088608 更新文档 2023-04-25 17:14:19 +08:00
wanglin2
ee59b8002a 修改导出,使用FileReader代替URL.createObjectURL转换blob数据 2023-04-25 17:13:39 +08:00
wanglin2
d6e625efa6 日常提交 2023-03-14 09:40:09 +08:00
wanglin2
fba0ece8fd 合并主分支 2023-03-08 09:47:35 +08:00
wanglin2
78ed038012 重新设计客户端架构 2023-03-07 16:39:33 +08:00
wanglin2
58f7be77ed Merge branch 'main' into electron 2023-03-06 21:56:30 +08:00
wanglin2
362160c67b 使用Vue CLI Plugin Electron Builder插件 2023-03-06 21:29:59 +08:00
wanglin2
c8a3c569e1 新增electron相关页面 2023-03-06 20:59:00 +08:00
wanglin2
6bc309e3fa 增加图标 2023-03-06 20:57:55 +08:00
93 changed files with 15918 additions and 8347 deletions

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,7 @@ import BatchExecution from './src/BatchExecution'
import { layoutValueList, CONSTANTS } from './src/utils/constant'
import { SVG } from '@svgdotjs/svg.js'
import { simpleDeepClone } from './src/utils'
import defaultTheme from './src/themes/default'
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
// 默认选项配置
const defaultOpt = {
@@ -118,7 +118,11 @@ const defaultOpt = {
// }
// ]
// }
]
],
// 节点最大缓存数量
maxNodeCacheCount: 1000,
// 关联线默认文字
defaultAssociativeLineText: '关联'
}
// 思维导图
@@ -208,12 +212,12 @@ class MindMap {
}
// 重新渲染
reRender(callback) {
reRender(callback, source = '') {
this.batchExecution.push('render', () => {
this.draw.clear()
this.initTheme()
this.renderer.reRender = true
this.renderer.render(callback)
this.renderer.render(callback, source)
})
}
@@ -263,7 +267,9 @@ class MindMap {
// 设置主题配置
setThemeConfig(config) {
this.opt.themeConfig = config
this.render(null, CONSTANTS.CHANGE_THEME)
// 检查改变的是否是节点大小无关的主题属性
let res = checkIsNodeSizeIndependenceConfig(config)
this.render(null, res ? '' : CONSTANTS.CHANGE_THEME)
}
// 获取自定义主题配置
@@ -318,7 +324,7 @@ class MindMap {
} else {
this.renderer.renderTree = data
}
this.reRender()
this.reRender(() => {}, CONSTANTS.SET_DATA)
}
// 动态设置思维导图数据,包括节点数据、布局、主题、视图

View File

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

View File

@@ -3,13 +3,13 @@ import { v4 as uuid } from 'uuid'
import {
getAssociativeLineTargetIndex,
computeCubicBezierPathPoints,
joinCubicBezierPath,
cubicBezierPath,
getNodePoint,
computeNodePoints,
getNodeLinePath,
getDefaultControlPointOffsets
getNodeLinePath
} from './utils/associativeLineUtils'
import associativeLineControlsMethods from './utils/associativeLineControls'
import associativeLineTextMethods from './utils/associativeLineText'
// 关联线类
class AssociativeLine {
@@ -46,6 +46,14 @@ class AssociativeLine {
}
// 节流一下,不然很卡
this.checkOverlapNode = throttle(this.checkOverlapNode, 100, this)
// 控制点相关方法
Object.keys(associativeLineControlsMethods).forEach(item => {
this[item] = associativeLineControlsMethods[item].bind(this)
})
// 关联线文字相关方法
Object.keys(associativeLineTextMethods).forEach(item => {
this[item] = associativeLineTextMethods[item].bind(this)
})
this.bindEvent()
}
@@ -89,6 +97,8 @@ class AssociativeLine {
window.addEventListener('mouseup', e => {
this.onControlPointMouseup(e)
})
// 缩放事件
this.mindMap.on('scale', this.onScale)
}
// 创建箭头
@@ -177,36 +187,46 @@ class AssociativeLine {
.stroke({ width: associativeLineActiveWidth, color: 'transparent' })
.fill({ color: 'none' })
clickPath.plot(pathStr)
// 文字
let text = this.createText({ path, clickPath, node, toNode, startPoint, endPoint, controlPoints })
// 点击事件
clickPath.click(e => {
e.stopPropagation()
// 如果当前存在激活节点,那么取消激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.clearActiveNodes()
} else {
// 否则清除当前的关联线的激活状态,如果有的话
this.clearActiveLine()
// 保存当前激活的关联线信息
this.activeLine = [path, clickPath, node, toNode]
// 让不可见的点击线显示
clickPath.stroke({ color: associativeLineActiveColor })
// 渲染控制点和连线
this.renderControls(
startPoint,
endPoint,
controlPoints[0],
controlPoints[1]
)
this.mindMap.emit(
'associative_line_click',
path,
clickPath,
node,
toNode
)
}
this.setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints })
})
this.lineList.push([path, clickPath, node, toNode])
// 渲染关联线文字
this.renderText(this.getText(node, toNode), path, text)
this.lineList.push([path, clickPath, text, node, toNode])
}
// 激活某根关联线
setActiveLine({ path, clickPath, text, node, toNode, startPoint, endPoint, controlPoints }) {
let {
associativeLineActiveColor
} = this.mindMap.themeConfig
// 如果当前存在激活节点,那么取消激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.clearActiveNodes()
} else {
// 否则清除当前的关联线的激活状态,如果有的话
this.clearActiveLine()
// 保存当前激活的关联线信息
this.activeLine = [path, clickPath, text, node, toNode]
// 让不可见的点击线显示
clickPath.stroke({ color: associativeLineActiveColor })
// 如果没有输入过关联线文字,那么显示默认文字
if (!this.getText(node, toNode)) {
this.renderText(this.mindMap.opt.defaultAssociativeLineText, path, text)
}
// 渲染控制点和连线
this.renderControls(
startPoint,
endPoint,
controlPoints[0],
controlPoints[1]
)
this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
}
}
// 移除所有连接线
@@ -214,6 +234,7 @@ class AssociativeLine {
this.lineList.forEach(line => {
line[0].remove()
line[1].remove()
line[2].remove()
})
this.lineList = []
}
@@ -253,7 +274,8 @@ class AssociativeLine {
updateCreatingLine(e) {
let { x, y } = this.getTransformedEventPos(e)
let startPoint = getNodePoint(this.creatingStartNode)
let pathStr = cubicBezierPath(startPoint.x, startPoint.y, x, y)
let offsetX = x > startPoint.x ? -10 : 10
let pathStr = cubicBezierPath(startPoint.x, startPoint.y, x + offsetX, y)
this.creatingLine.plot(pathStr)
this.checkOverlapNode(x, y)
}
@@ -349,20 +371,33 @@ class AssociativeLine {
// 删除连接线
removeLine() {
if (!this.activeLine) return
let [, , node, toNode] = this.activeLine
let [, , , node, toNode] = this.activeLine
this.removeControls()
let { associativeLineTargets, associativeLineTargetControlOffsets } =
let { associativeLineTargets, associativeLineTargetControlOffsets, associativeLineText } =
node.nodeData.data
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
// 更新关联线文本数据
let newAssociativeLineText = {}
if (associativeLineText) {
Object.keys(associativeLineText).forEach((item) => {
if (item !== toNode.nodeData.data.id) {
newAssociativeLineText[item] = associativeLineText[item]
}
})
}
this.mindMap.execCommand('SET_NODE_DATA', node, {
// 目标
associativeLineTargets: associativeLineTargets.filter((_, index) => {
return index !== targetIndex
}),
// 偏移量
associativeLineTargetControlOffsets: associativeLineTargetControlOffsets
? associativeLineTargetControlOffsets.filter((_, index) => {
return index !== targetIndex
})
: []
: [],
// 文本
associativeLineText: newAssociativeLineText
})
}
@@ -376,9 +411,16 @@ class AssociativeLine {
// 清除激活的线
clearActiveLine() {
if (this.activeLine) {
this.activeLine[1].stroke({
let [, clickPath, text, node, toNode] = this.activeLine
clickPath.stroke({
color: 'transparent'
})
// 隐藏关联线文本编辑框
this.hideEditTextBox()
// 如果当前关联线没有文字,则清空文字节点
if (!this.getText(node, toNode)) {
text.clear()
}
this.activeLine = null
this.removeControls()
}
@@ -391,6 +433,7 @@ class AssociativeLine {
this.lineList.forEach(line => {
line[0].hide()
line[1].hide()
line[2].hide()
})
this.hideControls()
}
@@ -401,225 +444,11 @@ class AssociativeLine {
this.lineList.forEach(line => {
line[0].show()
line[1].show()
line[2].show()
})
this.showControls()
this.isNodeDragging = false
}
// 创建控制点、连线节点
createControlNodes() {
let { associativeLineActiveColor } = this.mindMap.themeConfig
// 连线
this.controlLine1 = this.draw
.line()
.stroke({ color: associativeLineActiveColor, width: 2 })
this.controlLine2 = this.draw
.line()
.stroke({ color: associativeLineActiveColor, width: 2 })
// 控制点
this.controlPoint1 = this.createOneControlNode('controlPoint1')
this.controlPoint2 = this.createOneControlNode('controlPoint2')
}
// 创建控制点
createOneControlNode(pointKey) {
let { associativeLineActiveColor } = this.mindMap.themeConfig
return this.draw
.circle(this.controlPointDiameter)
.stroke({ color: associativeLineActiveColor })
.fill({ color: '#fff' })
.click(e => {
e.stopPropagation()
})
.mousedown(e => {
this.onControlPointMousedown(e, pointKey)
})
}
// 控制点的鼠标按下事件
onControlPointMousedown(e, pointKey) {
e.stopPropagation()
this.isControlPointMousedown = true
this.mousedownControlPointKey = pointKey
}
// 控制点的鼠标移动事件
onControlPointMousemove(e) {
if (
!this.isControlPointMousedown ||
!this.mousedownControlPointKey ||
!this[this.mousedownControlPointKey]
)
return
e.stopPropagation()
e.preventDefault()
let radius = this.controlPointDiameter / 2
// 转换鼠标当前的位置
let { x, y } = this.getTransformedEventPos(e)
this.controlPointMousemoveState.pos = {
x,
y
}
// 更新当前拖拽的控制点的位置
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
let [path, clickPath, node, toNode] = this.activeLine
let [startPoint, endPoint] = computeNodePoints(node, toNode)
this.controlPointMousemoveState.startPoint = startPoint
this.controlPointMousemoveState.endPoint = endPoint
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
this.controlPointMousemoveState.targetIndex = targetIndex
let offsets = []
let associativeLineTargetControlOffsets = node.nodeData.data.associativeLineTargetControlOffsets
if (!associativeLineTargetControlOffsets) {
// 兼容0.4.5版本没有associativeLineTargetControlOffsets的情况
offsets = getDefaultControlPointOffsets(startPoint, endPoint)
} else {
offsets = associativeLineTargetControlOffsets[targetIndex]
}
let point1 = null
let point2 = null
// 拖拽的是控制点1
if (this.mousedownControlPointKey === 'controlPoint1') {
point1 = {
x,
y
}
point2 = {
x: endPoint.x + offsets[1].x,
y: endPoint.y + offsets[1].y
}
// 更新控制点1的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
} else {
// 拖拽的是控制点2
point1 = {
x: startPoint.x + offsets[0].x,
y: startPoint.y + offsets[0].y
}
point2 = {
x,
y
}
// 更新控制点2的连线
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
}
// 更新关联线
let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
path.plot(pathStr)
clickPath.plot(pathStr)
}
// 控制点的鼠标移动事件
onControlPointMouseup(e) {
if (!this.isControlPointMousedown) return
e.stopPropagation()
e.preventDefault()
let { pos, startPoint, endPoint, targetIndex } =
this.controlPointMousemoveState
let [, , node] = this.activeLine
let offsetList = []
let associativeLineTargetControlOffsets = node.nodeData.data.associativeLineTargetControlOffsets
if (!associativeLineTargetControlOffsets) {
// 兼容0.4.5版本没有associativeLineTargetControlOffsets的情况
offsetList[targetIndex] = getDefaultControlPointOffsets(startPoint, endPoint)
} else {
offsetList = associativeLineTargetControlOffsets
}
let offset1 = null
let offset2 = null
if (this.mousedownControlPointKey === 'controlPoint1') {
// 更新控制点1数据
offset1 = {
x: pos.x - startPoint.x,
y: pos.y - startPoint.y
}
offset2 = offsetList[targetIndex][1]
} else {
// 更新控制点2数据
offset1 = offsetList[targetIndex][0]
offset2 = {
x: pos.x - endPoint.x,
y: pos.y - endPoint.y
}
}
offsetList[targetIndex] = [offset1, offset2]
this.mindMap.execCommand('SET_NODE_DATA', node, {
associativeLineTargetControlOffsets: offsetList
})
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚所以重置isControlPointMousedown需要等draw_click事件触发完以后
setTimeout(() => {
this.resetControlPoint()
}, 0)
}
// 复位控制点移动
resetControlPoint() {
this.isControlPointMousedown = false
this.mousedownControlPointKey = ''
this.controlPointMousemoveState = {
pos: null,
startPoint: null,
endPoint: null,
targetIndex: ''
}
}
// 渲染控制点
renderControls(startPoint, endPoint, point1, point2) {
if (!this.controlLine1) {
this.createControlNodes()
}
let radius = this.controlPointDiameter / 2
// 控制点和起终点的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
// 控制点
this.controlPoint1.x(point1.x - radius).y(point1.y - radius)
this.controlPoint2.x(point2.x - radius).y(point2.y - radius)
}
// 删除控制点
removeControls() {
if (!this.controlLine1) return
;[
this.controlLine1,
this.controlLine2,
this.controlPoint1,
this.controlPoint2
].forEach(item => {
item.remove()
})
this.controlLine1 = null
this.controlLine2 = null
this.controlPoint1 = null
this.controlPoint2 = null
}
// 隐藏控制点
hideControls() {
if (!this.controlLine1) return
;[
this.controlLine1,
this.controlLine2,
this.controlPoint1,
this.controlPoint2
].forEach(item => {
item.hide()
})
}
// 显示控制点
showControls() {
if (!this.controlLine1) return
;[
this.controlLine1,
this.controlLine2,
this.controlPoint1,
this.controlPoint2
].forEach(item => {
item.show()
})
}
}
AssociativeLine.instanceName = 'associativeLine'

View File

@@ -1,4 +1,4 @@
import { imgToDataUrl, downloadFile } from './utils'
import { imgToDataUrl, downloadFile, readBlob } from './utils'
import JsPDF from 'jspdf'
import { SVG } from '@svgdotjs/svg.js'
import drawBackgroundImageToCanvas from './utils/simulateCSSBackgroundInCanvas'
@@ -127,6 +127,28 @@ class Export {
})
}
// 在svg上绘制思维导图背景
drawBackgroundToSvg(svg) {
return new Promise(async resolve => {
let {
backgroundColor = '#fff',
backgroundImage,
backgroundRepeat = 'repeat'
} = this.mindMap.themeConfig
// 背景颜色
svg.css('background-color', backgroundColor)
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
let imgDataUrl = await imgToDataUrl(backgroundImage)
svg.css('background-image', `url(${imgDataUrl})`)
svg.css('background-repeat', backgroundRepeat)
resolve()
} else {
resolve()
}
})
}
// 导出为png
/**
* 方法1.把svg的图片都转化成data:url格式再转换
@@ -145,11 +167,10 @@ class Export {
type: 'image/svg+xml'
})
// 转换成data:url数据
let svgUrl = URL.createObjectURL(blob)
let svgUrl = await readBlob(blob)
// 绘制到canvas上
let imgDataUrl = await this.svgToPng(svgUrl, transparent)
URL.revokeObjectURL(svgUrl)
return imgDataUrl
let res = await this.svgToPng(svgUrl, transparent)
return res
}
// 导出为pdf
@@ -184,28 +205,6 @@ class Export {
image.src = img
}
// 在svg上绘制思维导图背景
drawBackgroundToSvg(svg) {
return new Promise(async resolve => {
let {
backgroundColor = '#fff',
backgroundImage,
backgroundRepeat = 'repeat'
} = this.mindMap.themeConfig
// 背景颜色
svg.css('background-color', backgroundColor)
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
let imgDataUrl = await imgToDataUrl(backgroundImage)
svg.css('background-image', `url(${imgDataUrl})`)
svg.css('background-repeat', backgroundRepeat)
resolve()
} else {
resolve()
}
})
}
// 导出为svg
// plusCssText附加的css样式如果svg中存在dom节点想要设置一些针对节点的样式可以通过这个参数传入
async svg(name, plusCssText) {
@@ -226,28 +225,32 @@ class Export {
let blob = new Blob([str], {
type: 'image/svg+xml'
})
return URL.createObjectURL(blob)
let res = await readBlob(blob)
return res
}
// 导出为json
json(name, withConfig = true) {
async json(name, withConfig = true) {
let data = this.mindMap.getData(withConfig)
let str = JSON.stringify(data)
let blob = new Blob([str])
return URL.createObjectURL(blob)
let res = await readBlob(blob)
return res
}
// 专有文件其实就是json文件
smm(name, withConfig) {
return this.json(name, withConfig)
async smm(name, withConfig) {
let res = await this.json(name, withConfig)
return res
}
// markdown文件
md() {
async md() {
let data = this.mindMap.getData()
let content = transformToMarkdown(data)
let blob = new Blob([content])
return URL.createObjectURL(blob)
let res = await readBlob(blob)
return res
}
}

View File

@@ -46,7 +46,7 @@ export default class KeyCommand {
if (this.mindMap.richText && this.mindMap.richText.showTextEdit) {
return
}
if (this.mindMap.renderer.textEdit.showTextEdit) {
if (this.mindMap.renderer.textEdit.showTextEdit || (this.mindMap.associativeLine && this.mindMap.associativeLine.showTextEdit)) {
return
}
this.isInSvg = false

View File

@@ -266,7 +266,6 @@ class Render {
// 重新渲染需要清除激活状态
if (this.reRender) {
this.clearActive()
this.layout.clearNodePool()
}
// 计算布局
this.layout.doLayout(root => {
@@ -289,6 +288,11 @@ class Render {
if (this.hasWaitRendering) {
this.hasWaitRendering = false
this.render(callback, source)
} else {
// 触发一次保存,因为修改了渲染树的数据
if (this.mindMap.richText && [CONSTANTS.CHANGE_THEME, CONSTANTS.SET_DATA].includes(source)) {
this.mindMap.command.addHistory()
}
}
}
let { enableNodeTransitionMove, nodeTransitionMoveDuration } =

View File

@@ -1,5 +1,6 @@
import Node from '../Node'
import { CONSTANTS, initRootNodePositionMap } from '../utils/constant'
import Lru from '../utils/Lru'
// 布局基类
class Base {
@@ -13,8 +14,7 @@ class Base {
this.draw = this.mindMap.draw
// 根节点
this.root = null
// 保存所有uid和节点用于复用
this.nodePool = {}
this.lru = new Lru(this.mindMap.opt.maxNodeCacheCount)
}
// 计算节点位置
@@ -40,16 +40,7 @@ class Base {
// 记录本次渲染时的节点
this.renderer.nodeCache[uid] = node
// 记录所有渲染时的节点
this.nodePool[uid] = node
// 如果总缓存数量达到1000直接清空
if (Object.keys(this.nodePool).length > 1000) {
this.clearNodePool()
}
}
// 清空节点存储池
clearNodePool() {
this.nodePool = {}
this.lru.add(uid, node)
}
// 检查当前来源是否需要重新计算节点大小
@@ -72,9 +63,9 @@ class Base {
newNode.getSize()
newNode.needLayout = true
}
} else if (this.nodePool[data.data.uid] && !this.renderer.reRender) {
} else if (this.lru.has(data.data.uid) && !this.renderer.reRender) {
// 数据上没有保存节点引用但是通过uid找到了缓存的节点也可以复用
newNode = this.nodePool[data.data.uid]
newNode = this.lru.get(data.data.uid)
// 保存该节点上一次的数据
let lastData = JSON.stringify(newNode.nodeData.data)
newNode.reset()

View File

@@ -0,0 +1,57 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 秋天
export default merge(defaultTheme, {
// 背景颜色
backgroundColor: '#fff2df',
// 连线的颜色
lineColor: '#b0bc47',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: '#b0bc47',
// 根节点样式
root: {
fillColor: '#e68112',
color: '#fff',
borderColor: '#e68112',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: '#b0bc47',
borderWidth: 3
}
},
// 二级节点样式
second: {
fillColor: '#ffd683',
color: '#8c5416',
borderColor: '#b0bc47',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: '#e68112'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#8c5416',
active: {
borderColor: '#b0bc47'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: '#ffd683',
borderColor: '#b0bc47',
borderWidth: 2,
color: '#8c5416',
active: {
borderColor: '#e68112'
}
}
})

View File

@@ -0,0 +1,57 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 牛油果
export default merge(defaultTheme, {
// 背景颜色
backgroundColor: '#e6f1de',
// 连线的颜色
lineColor: '#f5ffad',
lineWidth: 4,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: '#749336',
// 根节点样式
root: {
fillColor: '#94c143',
color: '#fff',
borderColor: '#94c143',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: '#749336',
borderWidth: 3
}
},
// 二级节点样式
second: {
fillColor: '#cee498',
color: '#749336',
borderColor: '#aec668',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: '#749336'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#749336',
active: {
borderColor: '#749336'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: '#cee498',
borderColor: '#aec668',
borderWidth: 2,
color: '#749336',
active: {
borderColor: '#749336'
}
}
})

View File

@@ -34,6 +34,14 @@ export default {
associativeLineActiveWidth: 8,
// 关联线激活状态的颜色
associativeLineActiveColor: 'rgba(2, 167, 240, 1)',
// 关联线文字颜色
associativeLineTextColor: 'rgb(51, 51, 51)',
// 关联线文字大小
associativeLineTextFontSize: 14,
// 关联线文字行高
associativeLineTextLineHeight: 1.2,
// 关联线文字字体
associativeLineTextFontFamily: '微软雅黑, Microsoft YaHei',
// 背景颜色
backgroundColor: '#fafafa',
// 背景图片
@@ -148,4 +156,38 @@ export const supportActiveStyle = [
'borderRadius'
]
// 检测主题配置是否是节点大小无关的
const nodeSizeIndependenceList = [
'lineWidth',
'lineColor',
'lineDasharray',
'lineStyle',
'generalizationLineWidth',
'generalizationLineColor',
'associativeLineWidth',
'associativeLineColor',
'associativeLineActiveWidth',
'associativeLineActiveColor',
'associativeLineTextColor',
'associativeLineTextFontSize',
'associativeLineTextLineHeight',
'associativeLineTextFontFamily',
'backgroundColor',
'backgroundImage',
'backgroundRepeat',
'backgroundPosition',
'backgroundSize'
]
export const checkIsNodeSizeIndependenceConfig = (config) => {
let keys = Object.keys(config)
for(let i = 0; i < keys.length; i++) {
if (!nodeSizeIndependenceList.find((item) => {
return item === keys[i]
})) {
return false
}
}
return true
}
export const lineStyleProps = ['lineColor', 'lineDasharray', 'lineWidth']

View File

@@ -27,6 +27,9 @@ import redSpirit from './redSpirit'
import blackHumour from './blackHumour'
import lateNightOffice from './lateNightOffice'
import blackGold from './blackGold'
import avocado from './avocado'
import autumn from './autumn'
import orangeJuice from './orangeJuice'
export default {
default: defaultTheme,
@@ -57,5 +60,8 @@ export default {
redSpirit,
blackHumour,
lateNightOffice,
blackGold
blackGold,
avocado,
autumn,
orangeJuice
}

View File

@@ -0,0 +1,57 @@
import defaultTheme from './default'
import merge from 'deepmerge'
// 橙汁
export default merge(defaultTheme, {
// 背景颜色
backgroundColor: '#070616',
// 连线的颜色
lineColor: '#fff',
lineWidth: 3,
// 概要连线的粗细
generalizationLineWidth: 3,
// 概要连线的颜色
generalizationLineColor: '#fff',
// 根节点样式
root: {
fillColor: '#ff6811',
color: '#110501',
borderColor: '#ff6811',
borderWidth: 0,
fontSize: 24,
active: {
borderColor: '#a9a4a9',
borderWidth: 3
}
},
// 二级节点样式
second: {
fillColor: '#070616',
color: '#a9a4a9',
borderColor: '#ff6811',
borderWidth: 2,
fontSize: 18,
active: {
borderColor: '#110501'
}
},
// 三级及以下节点样式
node: {
fontSize: 14,
color: '#a9a4a9',
active: {
borderColor: '#ff6811'
}
},
// 概要节点样式
generalization: {
fontSize: 14,
fillColor: '',
borderColor: '#ff6811',
borderWidth: 2,
color: '#a9a4a9',
active: {
borderColor: '#110501'
}
}
})

View File

@@ -0,0 +1,39 @@
// LRU缓存类
export default class CRU {
constructor(max) {
this.max = max || 1000
this.size = 0
this.pool = new Map()
}
add(key, value) {
// 如果该key是否已经存在则先删除
this.delete(key)
this.pool.set(key, value)
this.size++
// 如果数量超出最大值,则删除最早的
if (this.size > this.max) {
let keys = this.pool.keys()
let last = keys.next()
this.delete(last.value)
}
}
delete(key) {
if (this.pool.has(key)) {
this.pool.delete(key)
this.size--
}
}
has(key) {
return this.pool.has(key)
}
get(key) {
if (this.pool.has(key)) {
return this.pool.get(key)
}
}
}

View File

@@ -0,0 +1,241 @@
import {
getAssociativeLineTargetIndex,
joinCubicBezierPath,
computeNodePoints,
getDefaultControlPointOffsets
} from './associativeLineUtils'
// 创建控制点、连线节点
function createControlNodes() {
let { associativeLineActiveColor } = this.mindMap.themeConfig
// 连线
this.controlLine1 = this.draw
.line()
.stroke({ color: associativeLineActiveColor, width: 2 })
this.controlLine2 = this.draw
.line()
.stroke({ color: associativeLineActiveColor, width: 2 })
// 控制点
this.controlPoint1 = this.createOneControlNode('controlPoint1')
this.controlPoint2 = this.createOneControlNode('controlPoint2')
}
// 创建控制点
function createOneControlNode(pointKey) {
let { associativeLineActiveColor } = this.mindMap.themeConfig
return this.draw
.circle(this.controlPointDiameter)
.stroke({ color: associativeLineActiveColor })
.fill({ color: '#fff' })
.click(e => {
e.stopPropagation()
})
.mousedown(e => {
this.onControlPointMousedown(e, pointKey)
})
}
// 控制点的鼠标按下事件
function onControlPointMousedown(e, pointKey) {
e.stopPropagation()
this.isControlPointMousedown = true
this.mousedownControlPointKey = pointKey
}
// 控制点的鼠标移动事件
function onControlPointMousemove(e) {
if (
!this.isControlPointMousedown ||
!this.mousedownControlPointKey ||
!this[this.mousedownControlPointKey]
)
return
e.stopPropagation()
e.preventDefault()
let radius = this.controlPointDiameter / 2
// 转换鼠标当前的位置
let { x, y } = this.getTransformedEventPos(e)
this.controlPointMousemoveState.pos = {
x,
y
}
// 更新当前拖拽的控制点的位置
this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
let [path, clickPath, text, node, toNode] = this.activeLine
let [startPoint, endPoint] = computeNodePoints(node, toNode)
this.controlPointMousemoveState.startPoint = startPoint
this.controlPointMousemoveState.endPoint = endPoint
let targetIndex = getAssociativeLineTargetIndex(node, toNode)
this.controlPointMousemoveState.targetIndex = targetIndex
let offsets = []
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
if (!associativeLineTargetControlOffsets) {
// 兼容0.4.5版本没有associativeLineTargetControlOffsets的情况
offsets = getDefaultControlPointOffsets(startPoint, endPoint)
} else {
offsets = associativeLineTargetControlOffsets[targetIndex]
}
let point1 = null
let point2 = null
// 拖拽的是控制点1
if (this.mousedownControlPointKey === 'controlPoint1') {
point1 = {
x,
y
}
point2 = {
x: endPoint.x + offsets[1].x,
y: endPoint.y + offsets[1].y
}
// 更新控制点1的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
} else {
// 拖拽的是控制点2
point1 = {
x: startPoint.x + offsets[0].x,
y: startPoint.y + offsets[0].y
}
point2 = {
x,
y
}
// 更新控制点2的连线
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
}
// 更新关联线
let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
path.plot(pathStr)
clickPath.plot(pathStr)
this.updateTextPos(path, text)
this.updateTextEditBoxPos(text)
}
// 控制点的鼠标移动事件
function onControlPointMouseup(e) {
if (!this.isControlPointMousedown) return
e.stopPropagation()
e.preventDefault()
let { pos, startPoint, endPoint, targetIndex } =
this.controlPointMousemoveState
let [, , , node] = this.activeLine
let offsetList = []
let associativeLineTargetControlOffsets =
node.nodeData.data.associativeLineTargetControlOffsets
if (!associativeLineTargetControlOffsets) {
// 兼容0.4.5版本没有associativeLineTargetControlOffsets的情况
offsetList[targetIndex] = getDefaultControlPointOffsets(
startPoint,
endPoint
)
} else {
offsetList = associativeLineTargetControlOffsets
}
let offset1 = null
let offset2 = null
if (this.mousedownControlPointKey === 'controlPoint1') {
// 更新控制点1数据
offset1 = {
x: pos.x - startPoint.x,
y: pos.y - startPoint.y
}
offset2 = offsetList[targetIndex][1]
} else {
// 更新控制点2数据
offset1 = offsetList[targetIndex][0]
offset2 = {
x: pos.x - endPoint.x,
y: pos.y - endPoint.y
}
}
offsetList[targetIndex] = [offset1, offset2]
this.mindMap.execCommand('SET_NODE_DATA', node, {
associativeLineTargetControlOffsets: offsetList
})
// 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚所以重置isControlPointMousedown需要等draw_click事件触发完以后
setTimeout(() => {
this.resetControlPoint()
}, 0)
}
// 复位控制点移动
function resetControlPoint() {
this.isControlPointMousedown = false
this.mousedownControlPointKey = ''
this.controlPointMousemoveState = {
pos: null,
startPoint: null,
endPoint: null,
targetIndex: ''
}
}
// 渲染控制点
function renderControls(startPoint, endPoint, point1, point2) {
if (!this.controlLine1) {
this.createControlNodes()
}
let radius = this.controlPointDiameter / 2
// 控制点和起终点的连线
this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
// 控制点
this.controlPoint1.x(point1.x - radius).y(point1.y - radius)
this.controlPoint2.x(point2.x - radius).y(point2.y - radius)
}
// 删除控制点
function removeControls() {
if (!this.controlLine1) return
;[
this.controlLine1,
this.controlLine2,
this.controlPoint1,
this.controlPoint2
].forEach(item => {
item.remove()
})
this.controlLine1 = null
this.controlLine2 = null
this.controlPoint1 = null
this.controlPoint2 = null
}
// 隐藏控制点
function hideControls() {
if (!this.controlLine1) return
;[
this.controlLine1,
this.controlLine2,
this.controlPoint1,
this.controlPoint2
].forEach(item => {
item.hide()
})
}
// 显示控制点
function showControls() {
if (!this.controlLine1) return
;[
this.controlLine1,
this.controlLine2,
this.controlPoint1,
this.controlPoint2
].forEach(item => {
item.show()
})
}
export default {
createControlNodes,
createOneControlNode,
onControlPointMousedown,
onControlPointMousemove,
onControlPointMouseup,
resetControlPoint,
renderControls,
removeControls,
hideControls,
showControls
}

View File

@@ -0,0 +1,167 @@
import { Text } from '@svgdotjs/svg.js'
import { getStrWithBrFromHtml } from './index'
// 创建文字节点
function createText(data) {
let g = this.draw.group()
const setActive = () => {
if (
!this.activeLine ||
this.activeLine[3] !== data.node ||
this.activeLine[4] !== data.toNode
) {
this.setActiveLine({
...data,
text: g
})
}
}
g.click(e => {
e.stopPropagation()
setActive()
})
g.on('dblclick', e => {
e.stopPropagation()
setActive()
if (!this.activeLine) return
this.showEditTextBox(g)
})
return g
}
// 显示文本编辑框
function showEditTextBox(g) {
this.mindMap.emit('before_show_text_edit')
// 注册回车快捷键
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
if (!this.textEditNode) {
this.textEditNode = document.createElement('div')
this.textEditNode.style.cssText = `position:fixed;box-sizing: border-box;background-color:#fff;box-shadow: 0 0 20px rgba(0,0,0,.5);padding: 3px 5px;margin-left: -5px;margin-top: -3px;outline: none; word-break: break-all;`
this.textEditNode.setAttribute('contenteditable', true)
this.textEditNode.addEventListener('keyup', e => {
e.stopPropagation()
})
this.textEditNode.addEventListener('click', e => {
e.stopPropagation()
})
document.body.appendChild(this.textEditNode)
}
let {
associativeLineTextFontSize,
associativeLineTextFontFamily,
associativeLineTextLineHeight
} = this.mindMap.themeConfig
let scale = this.mindMap.view.scale
let [, , , node, toNode] = this.activeLine
let textLines = (
this.getText(node, toNode) || this.mindMap.opt.defaultAssociativeLineText
).split(/\n/gim)
this.textEditNode.style.fontFamily = associativeLineTextFontFamily
this.textEditNode.style.fontSize = associativeLineTextFontSize * scale + 'px'
this.textEditNode.style.lineHeight = textLines.length > 1 ? associativeLineTextLineHeight : 'normal'
this.textEditNode.style.zIndex = this.mindMap.opt.nodeTextEditZIndex
this.textEditNode.innerHTML = textLines.join('<br>')
this.textEditNode.style.display = 'block'
this.updateTextEditBoxPos(g)
this.showTextEdit = true
}
// 处理画布缩放
function onScale() {
this.hideEditTextBox()
}
// 更新文本编辑框位置
function updateTextEditBoxPos(g) {
let rect = g.node.getBoundingClientRect()
this.textEditNode.style.minWidth = rect.width + 10 + 'px'
this.textEditNode.style.minHeight = rect.height + 6 + 'px'
this.textEditNode.style.left = rect.left + 'px'
this.textEditNode.style.top = rect.top + 'px'
}
// 隐藏文本编辑框
function hideEditTextBox() {
if (!this.showTextEdit) {
return
}
let [path, , text, node, toNode] = this.activeLine
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
this.mindMap.execCommand('SET_NODE_DATA', node, {
associativeLineText: {
...(node.nodeData.data.associativeLineText || {}),
[toNode.nodeData.data.id]: str
}
})
this.textEditNode.style.display = 'none'
this.textEditNode.innerHTML = ''
this.showTextEdit = false
this.renderText(str, path, text)
this.mindMap.emit('hide_text_edit')
}
// 获取某根关联线的文字
function getText(node, toNode) {
let obj = node.nodeData.data.associativeLineText
if (!obj) {
return ''
}
return obj[toNode.nodeData.data.id] || ''
}
// 渲染关联线文字
function renderText(str, path, text) {
if (!str) return
let { associativeLineTextFontSize, associativeLineTextLineHeight } =
this.mindMap.themeConfig
text.clear()
let textArr = str.split(/\n/gim)
textArr.forEach((item, index) => {
let node = new Text().text(item)
node.y(associativeLineTextFontSize * associativeLineTextLineHeight * index)
this.styleText(node)
text.add(node)
})
updateTextPos(path, text)
}
// 给文本设置样式
function styleText(node) {
let {
associativeLineTextColor,
associativeLineTextFontSize,
associativeLineTextFontFamily
} = this.mindMap.themeConfig
node
.fill({
color: associativeLineTextColor
})
.css({
'font-family': associativeLineTextFontFamily,
'font-size': associativeLineTextFontSize
})
}
// 更新关联线文字位置
function updateTextPos(path, text) {
let pathLength = path.length()
let centerPoint = path.pointAt(pathLength / 2)
let { width: textWidth, height: textHeight } = text.bbox()
text.x(centerPoint.x - textWidth / 2)
text.y(centerPoint.y - textHeight / 2)
}
export default {
getText,
createText,
styleText,
onScale,
showEditTextBox,
hideEditTextBox,
updateTextEditBoxPos,
renderText,
updateTextPos
}

View File

@@ -28,34 +28,6 @@ export const themeList = [
name: '默认',
value: 'default',
},
{
name: '脑图经典',
value: 'classic',
},
{
name: '小黄人',
value: 'minions',
},
{
name: '粉红葡萄',
value: 'pinkGrape',
},
{
name: '薄荷',
value: 'mint',
},
{
name: '金色vip',
value: 'gold',
},
{
name: '活力橙',
value: 'vitalityOrange',
},
{
name: '绿叶',
value: 'greenLeaf',
},
{
name: '暗色2',
value: 'dark2',
@@ -72,10 +44,6 @@ export const themeList = [
name: '脑图经典3',
value: 'classic3',
},
{
name: '脑图经典4',
value: 'classic4',
},
{
name: '经典绿',
value: 'classicGreen',
@@ -112,6 +80,38 @@ export const themeList = [
name: '浪漫紫',
value: 'romanticPurple',
},
{
name: '粉红葡萄',
value: 'pinkGrape',
},
{
name: '薄荷',
value: 'mint',
},
{
name: '金色vip',
value: 'gold',
},
{
name: '活力橙',
value: 'vitalityOrange',
},
{
name: '绿叶',
value: 'greenLeaf',
},
{
name: '脑图经典',
value: 'classic',
},
{
name: '脑图经典4',
value: 'classic4',
},
{
name: '小黄人',
value: 'minions',
},
{
name: '简约黑',
value: 'simpleBlack',
@@ -139,12 +139,25 @@ export const themeList = [
{
name: '黑金',
value: 'blackGold',
},
{
name: '牛油果',
value: 'avocado',
},
{
name: '秋天',
value: 'autumn',
},
{
name: '橙汁',
value: 'orangeJuice',
}
]
// 常量
export const CONSTANTS = {
CHANGE_THEME: 'changeTheme',
SET_DATA: 'setData',
TRANSFORM_TO_NORMAL_NODE: 'transformAllNodesToNormalNode',
MODE: {
READONLY: 'readonly',

View File

@@ -342,4 +342,18 @@ export const getTextFromHtml = (html) => {
}
getTextFromHtmlEl.innerHTML = html
return getTextFromHtmlEl.textContent
}
// 将blob转成data:url
export const readBlob = (blob) => {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onload = (evt) => {
resolve(evt.target.result)
}
reader.onerror = (err) => {
reject(err)
}
reader.readAsDataURL(blob)
})
}

BIN
web/build/icons/icon.icns Normal file

Binary file not shown.

BIN
web/build/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
web/build/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

2023
web/dist_electron/index.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,84 @@
{
"name": "thoughts",
"version": "0.1.0",
"private": true,
"description": "一个简洁的思维导图",
"author": "街角小林<1013335014@qq.com>",
"repository": {
"type": "git",
"url": "https://github.com/wanglin2/mind-map"
},
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build && node ../copy.js",
"lint": "vue-cli-service lint",
"autoBuildDoc": "node ./scripts/autoBuildDoc.js",
"buildDoc": "node ./scripts/buildDoc.js",
"electron:build": "vue-cli-service electron:build",
"electron:build-all": "vue-cli-service electron:build -p never -mwl",
"electron:build-mac": "vue-cli-service electron:build -p never -m",
"electron:build-win": "vue-cli-service electron:build -p never -w",
"electron:build-linux": "vue-cli-service electron:build -p never -l",
"electron:serve": "vue-cli-service electron:serve",
"buildLibrary": "vue-cli-service build --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js",
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
"@toast-ui/editor": "^3.1.5",
"core-js": "^3.6.5",
"electron-json-storage": "^4.6.0",
"element-ui": "^2.15.1",
"fs-extra": "^7.0.1",
"highlight.js": "^10.7.3",
"open": "^6.4.0",
"uuid": "^3.4.0",
"v-viewer": "^1.6.4",
"vue": "^2.6.11",
"vue-i18n": "^8.27.2",
"vue-router": "^3.5.1",
"vuex": "^3.6.2",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.0",
"@vue/cli-plugin-eslint": "^4.5.0",
"@vue/cli-service": "^4.5.0",
"babel-eslint": "^10.1.0",
"chokidar": "^3.5.3",
"electron": "^23.1.1",
"electron-devtools-installer": "^3.1.0",
"esbuild": "^0.17.15",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"less": "^3.12.2",
"less-loader": "^7.1.0",
"markdown-it": "^13.0.1",
"markdown-it-checkbox": "^1.1.0",
"prettier": "^1.19.1",
"vue-cli-plugin-electron-builder": "~2.1.1",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.44.2"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

View File

@@ -0,0 +1,122 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/electron/preload.js":
/*!*********************************!*\
!*** ./src/electron/preload.js ***!
\*********************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval("const { contextBridge, ipcRenderer } = __webpack_require__(/*! electron */ \"electron\")\r\n\r\ncontextBridge.exposeInMainWorld('platform', process.platform)\r\ncontextBridge.exposeInMainWorld('IS_ELECTRON', true)\r\n\r\ncontextBridge.exposeInMainWorld('electronAPI', {\r\n minimize: () => ipcRenderer.send('minimize'),\r\n maximize: () => ipcRenderer.send('maximize'),\r\n unmaximize: () => ipcRenderer.send('unmaximize'),\r\n close: () => ipcRenderer.send('close'),\r\n destroy: () => ipcRenderer.send('destroy'),\r\n create: id => ipcRenderer.send('create', id),\r\n getFileContent: id => ipcRenderer.invoke('getFileContent', id),\r\n save: (id, data, fileName) => ipcRenderer.invoke('save', id, data, fileName),\r\n rename: (id, name) => ipcRenderer.invoke('rename', id, name),\r\n openUrl: url => ipcRenderer.send('openUrl', url),\r\n addRecentFileList: (fileList) => ipcRenderer.invoke('addRecentFileList', fileList),\r\n getRecentFileList: () => ipcRenderer.invoke('getRecentFileList'),\r\n clearRecentFileList: () => ipcRenderer.invoke('clearRecentFileList'),\r\n openFileInDir: file => ipcRenderer.send('openFileInDir', file),\r\n deleteFile: file => ipcRenderer.invoke('deleteFile', file),\r\n onRefreshRecentFileList: callback =>\r\n ipcRenderer.on('refreshRecentFileList', callback),\r\n openFile: file => ipcRenderer.send('openFile', file),\r\n selectOpenFile: () => ipcRenderer.send('selectOpenFile'),\r\n copyFile: file => ipcRenderer.invoke('copyFile', file)\r\n})\r\n\n\n//# sourceURL=webpack:///./src/electron/preload.js?");
/***/ }),
/***/ 0:
/*!***************************************!*\
!*** multi ./src/electron/preload.js ***!
\***************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval("module.exports = __webpack_require__(/*! E:\\wanglin\\mind-map\\web\\src\\electron\\preload.js */\"./src/electron/preload.js\");\n\n\n//# sourceURL=webpack:///multi_./src/electron/preload.js?");
/***/ }),
/***/ "electron":
/*!***************************!*\
!*** external "electron" ***!
\***************************/
/*! no static exports found */
/***/ (function(module, exports) {
eval("module.exports = require(\"electron\");\n\n//# sourceURL=webpack:///external_%22electron%22?");
/***/ })
/******/ });

18423
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,20 +2,39 @@
"name": "thoughts",
"version": "0.1.0",
"private": true,
"description": "一个简洁的思维导图",
"author": "街角小林<1013335014@qq.com>",
"repository": {
"type": "git",
"url": "https://github.com/wanglin2/mind-map"
},
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build && node ../copy.js",
"lint": "vue-cli-service lint",
"autoBuildDoc": "node ./scripts/autoBuildDoc.js",
"buildDoc": "node ./scripts/buildDoc.js",
"electron:build": "vue-cli-service electron:build",
"electron:build-all": "vue-cli-service electron:build -p never -mwl",
"electron:build-mac": "vue-cli-service electron:build -p never -m",
"electron:build-win": "vue-cli-service electron:build -p never -w",
"electron:build-linux": "vue-cli-service electron:build -p never -l",
"electron:serve": "vue-cli-service electron:serve",
"buildLibrary": "vue-cli-service build --target lib --name simpleMindMap ../simple-mind-map/full.js --dest ../simple-mind-map/dist && esbuild ../simple-mind-map/full.js --bundle --external:buffer --format=esm --outfile=../simple-mind-map/dist/simpleMindMap.esm.js",
"format": "prettier --write src/* src/*/* src/*/*/* src/*/*/*/*",
"buildDoc": "node ./scripts/buildDoc.js",
"autoBuildDoc": "node ./scripts/autoBuildDoc.js"
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
"@toast-ui/editor": "^3.1.5",
"core-js": "^3.6.5",
"electron-json-storage": "^4.6.0",
"element-ui": "^2.15.1",
"fs-extra": "^7.0.1",
"highlight.js": "^10.7.3",
"open": "^6.4.0",
"uuid": "^3.4.0",
"v-viewer": "^1.6.4",
"vue": "^2.6.11",
"vue-i18n": "^8.27.2",
@@ -29,6 +48,8 @@
"@vue/cli-service": "^4.5.0",
"babel-eslint": "^10.1.0",
"chokidar": "^3.5.3",
"electron": "^23.1.1",
"electron-devtools-installer": "^3.1.0",
"esbuild": "^0.17.15",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
@@ -37,6 +58,7 @@
"markdown-it": "^13.0.1",
"markdown-it-checkbox": "^1.1.0",
"prettier": "^1.19.1",
"vue-cli-plugin-electron-builder": "~2.1.1",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.44.2"
},

BIN
web/src/.DS_Store vendored

Binary file not shown.

View File

@@ -34,7 +34,17 @@ export const getData = () => {
return simpleDeepClone(exampleData)
} else {
try {
return JSON.parse(store)
let parsedData = JSON.parse(store)
if (window.IS_ELECTRON) {
return simpleDeepClone(exampleData)
let { root, ...rest } = parsedData
return {
...rest,
root: simpleDeepClone(exampleData).root
}
} else {
return parsedData
}
} catch (error) {
return simpleDeepClone(exampleData)
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
web/src/assets/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

114
web/src/background.js Normal file
View File

@@ -0,0 +1,114 @@
'use strict'
import { app, protocol, BrowserWindow } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import path from 'path'
import { bindFileHandleEvent } from './electron/fileHandle'
import { bindOtherHandleEvent } from './electron/otherHandle'
const isDevelopment = process.env.NODE_ENV !== 'production'
// 在应用程序准备就绪之前,必须注册方案
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
// 创建主页面
let mainWindow = null
async function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
frame: false,
titleBarStyle: 'hiddenInset',
webPreferences: {
webSecurity: false,
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// 如果处于开发模式则加载开发服务器的url
await mainWindow.loadURL(
process.env.WEBPACK_DEV_SERVER_URL + '/#/workbenche'
)
// if (!process.env.IS_TEST) mainWindow.webContents.openDevTools()
} else {
createProtocol('app')
// 非开发环境时加载index.html
mainWindow.loadURL('app://./index.html/#/workbenche')
}
}
// 绑定事件
const bindEvent = () => {
bindFileHandleEvent({ mainWindow, initOpenFileQueue })
bindOtherHandleEvent()
}
// 关闭所有窗口后退出
app.on('window-all-closed', () => {
// 在macOS上应用程序及其菜单栏通常保持活动状态直到用户使用Cmd+Q明确退出
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// 在macOS上当点击dock图标且没有其他窗口打开时通常会在应用程序中重新创建一个窗口。
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow()
// bindEvent()
}
})
// Attempt to bind file opening #2
// https://stackoverflow.com/questions/62420427/how-do-i-make-my-electron-app-the-default-for-opening-files
// https://github.com/rchrd2/example-electron-file-association
// https://www.jianshu.com/p/a32542277b83
const initOpenFileQueue = []
app.on('will-finish-launching', () => {
// Event fired When someone drags files onto the icon while your app is running
if (process.platform == 'win32') {
const argv = process.argv
if (argv) {
argv.forEach(filePath => {
if (filePath.indexOf('.smm') >= 0) {
initOpenFileQueue.push(filePath)
}
})
}
} else {
app.on('open-file', (event, file) => {
if (app.isReady() === false) {
initOpenFileQueue.push(file)
} else {
console.log(file)
}
event.preventDefault()
})
}
})
app.on('ready', async () => {
createMainWindow()
bindEvent()
})
// 在开发模式下,应父进程的请求干净地退出。
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', data => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}

View File

@@ -40,5 +40,8 @@ export const themeMap = {
blackHumour: require('../assets/img/blackHumour.jpg'),
lateNightOffice: require('../assets/img/lateNightOffice.jpg'),
blackGold: require('../assets/img/blackGold.jpg'),
autumn: require('../assets/img/autumn.jpg'),
avocado: require('../assets/img/avocado.jpg'),
orangeJuice: require('../assets/img/orangeJuice.jpg'),
}

3
web/src/css/global.css Normal file
View File

@@ -0,0 +1,3 @@
.noDrag {
-webkit-app-region: no-drag;
}

View File

@@ -0,0 +1,251 @@
import { BrowserWindow, ipcMain, dialog, shell } from 'electron'
import fs from 'fs-extra'
import path from 'path'
import {
saveToRecent,
clearRecent,
removeFileInRecent,
replaceFileInRecent,
getRecent,
saveFileListToRecent
} from './storage'
import { v4 as uuid } from 'uuid'
export const bindFileHandleEvent = ({ mainWindow, initOpenFileQueue }) => {
// 通知主页面刷新最近文件列表
const notifyMainWindowRefreshRecentFileList = () => {
mainWindow.webContents.send('refreshRecentFileList')
}
// 新建编辑页面
const openIds = []
const createEditWindow = async (event, id) => {
openIds.push(id)
const win = new BrowserWindow({
width: 1200,
height: 800,
frame: false,
titleBarStyle: 'hiddenInset',
webPreferences: {
webSecurity: false,
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
win.on('closed', () => {
// 从openIds数组中删除
let index = openIds.find(item => {
return item === id
})
if (index !== -1) {
openIds.splice(index, 1)
}
// 从idToFilePath中删除
delete idToFilePath[id]
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(
process.env.WEBPACK_DEV_SERVER_URL + '/#/workbenche/edit/' + id
)
// if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
// Load the index.html when not in development
win.loadURL('app://./index.html/#/workbenche/edit/' + id)
}
}
ipcMain.on('create', createEditWindow)
// 保存文件
const idToFilePath = {}
ipcMain.handle('save', async (event, id, data, fileName = '未命名') => {
if (!idToFilePath[id]) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
const res = dialog.showSaveDialogSync(win, {
title: '保存',
defaultPath: fileName + '.smm',
filters: [{ name: '思维导图', extensions: ['smm'] }]
})
if (res) {
idToFilePath[id] = res
fs.writeFile(res, data)
saveToRecent(res).then(() => {
notifyMainWindowRefreshRecentFileList()
})
return path.parse(idToFilePath[id]).name
}
} else {
fs.writeFile(idToFilePath[id], data)
}
})
// 打开文件
const openFile = (event, file) => {
let id = uuid()
idToFilePath[id] = file
saveToRecent(file).then(() => {
notifyMainWindowRefreshRecentFileList()
})
createEditWindow(null, id)
}
ipcMain.on('openFile', openFile)
// 选择打开本地文件
ipcMain.on('selectOpenFile', event => {
const res = dialog.showOpenDialogSync({
title: '选择',
filters: [{ name: '思维导图', extensions: ['smm'] }]
})
if (res && res[0]) {
openFile(null, res[0])
}
})
// 获取文件内容
ipcMain.handle('getFileContent', (event, id) => {
return new Promise(resolve => {
let file = idToFilePath[id]
if (!file) {
resolve(null)
return
}
fs.readFile(file, { encoding: 'utf-8' }, (err, data) => {
resolve({
name: path.parse(file).name,
content: JSON.parse(data)
})
})
})
})
// 重命名文件
ipcMain.handle('rename', (event, id, name) => {
return new Promise(resolve => {
if (!idToFilePath[id]) {
resolve('文件不存在')
return
}
let oldPath = idToFilePath[id]
let { base, ...oldPathData } = path.parse(oldPath)
oldPathData.name = name
let newPath = path.format(oldPathData)
idToFilePath[id] = newPath
fs.rename(oldPath, newPath, err => {
if (err) {
resolve('重命名失败')
} else {
replaceFileInRecent(oldPath, newPath).then(() => {
notifyMainWindowRefreshRecentFileList()
resolve()
})
}
})
})
})
// 获取最近文件列表
ipcMain.handle('getRecentFileList', () => {
return getRecent().map(item => {
let data = path.parse(item)
return {
url: item,
dir: data.dir,
name: data.name
}
})
})
// 清空最近文件列表
ipcMain.handle('clearRecentFileList', async () => {
try {
clearRecent()
return ''
} catch (error) {
return '清空失败'
}
})
// 添加到最近文件列表
ipcMain.handle('addRecentFileList', async (event, fileList) => {
try {
console.log(fileList);
await saveFileListToRecent(fileList)
notifyMainWindowRefreshRecentFileList()
} catch (error) {
return error
}
})
// 打开指定目录
ipcMain.on('openFileInDir', (event, file) => {
shell.showItemInFolder(file)
})
// 删除指定文件
ipcMain.handle('deleteFile', (event, file) => {
let res = ''
let id = Object.keys(idToFilePath).find(item => {
return idToFilePath[item] === file
})
let index = -1
if (id) {
index = openIds.findIndex(item => {
return item === id
})
}
if (index === -1) {
try {
fs.rmSync(file)
} catch (error) {}
removeFileInRecent(file)
} else {
res = '该文件正在编辑,请关闭后再试'
}
return res
})
// 复制文件
ipcMain.handle('copyFile', async (event, file) => {
return new Promise((resolve, reject) => {
fs.pathExists(file, (err, exists) => {
if (err) {
reject(err)
} else {
if (exists) {
let { base, ...oldPathData } = path.parse(file)
let newName = oldPathData.name + '-复制'
let index = 1
oldPathData.name = newName
let newPath = path.format(oldPathData)
// 检查新路径是否已存在
while (fs.pathExistsSync(newPath)) {
oldPathData.name = newName + index
newPath = path.format(oldPathData)
index++
}
fs.copy(file, newPath, err => {
if (err) {
reject(err)
} else {
saveToRecent(newPath).then(() => {
notifyMainWindowRefreshRecentFileList()
})
resolve()
}
})
} else {
reject('文件不存在')
}
}
})
})
})
// 直接双击文件打开应用时,需要直接打开该文件编辑
initOpenFileQueue.forEach((file) => {
openFile(null, file)
})
}

View File

@@ -0,0 +1,18 @@
import { BrowserWindow, ipcMain } from 'electron'
import open from 'open'
export const bindOtherHandleEvent = () => {
// 处理缩放事件
;['minimize', 'maximize', 'unmaximize', 'close', 'destroy'].forEach(item => {
ipcMain.on(item, event => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win[item]()
})
})
// 使用默认浏览器打开指定url
ipcMain.on('openUrl', (event, url) => {
open(url)
})
}

View File

@@ -0,0 +1,27 @@
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('platform', process.platform)
contextBridge.exposeInMainWorld('IS_ELECTRON', true)
contextBridge.exposeInMainWorld('electronAPI', {
minimize: () => ipcRenderer.send('minimize'),
maximize: () => ipcRenderer.send('maximize'),
unmaximize: () => ipcRenderer.send('unmaximize'),
close: () => ipcRenderer.send('close'),
destroy: () => ipcRenderer.send('destroy'),
create: id => ipcRenderer.send('create', id),
getFileContent: id => ipcRenderer.invoke('getFileContent', id),
save: (id, data, fileName) => ipcRenderer.invoke('save', id, data, fileName),
rename: (id, name) => ipcRenderer.invoke('rename', id, name),
openUrl: url => ipcRenderer.send('openUrl', url),
addRecentFileList: (fileList) => ipcRenderer.invoke('addRecentFileList', fileList),
getRecentFileList: () => ipcRenderer.invoke('getRecentFileList'),
clearRecentFileList: () => ipcRenderer.invoke('clearRecentFileList'),
openFileInDir: file => ipcRenderer.send('openFileInDir', file),
deleteFile: file => ipcRenderer.invoke('deleteFile', file),
onRefreshRecentFileList: callback =>
ipcRenderer.on('refreshRecentFileList', callback),
openFile: file => ipcRenderer.send('openFile', file),
selectOpenFile: () => ipcRenderer.send('selectOpenFile'),
copyFile: file => ipcRenderer.invoke('copyFile', file)
})

109
web/src/electron/storage.js Normal file
View File

@@ -0,0 +1,109 @@
import storage from 'electron-json-storage'
export const RECENT_FILE_LIST = 'recentFileList'
// 保存到最近文件
export const saveToRecent = file => {
return new Promise((resolve, reject) => {
let list = getRecent()
let index = list.findIndex(item => {
return item === file
})
if (index !== -1) {
list.splice(index, 1)
}
list.push(file)
storage.set(RECENT_FILE_LIST, list, err => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
// 保存到最近文件
export const saveFileListToRecent = fileList => {
return new Promise((resolve, reject) => {
let list = getRecent()
fileList.forEach(file => {
let index = list.findIndex(item => {
return item === file
})
if (index !== -1) {
list.splice(index, 1)
}
list.push(file)
})
storage.set(RECENT_FILE_LIST, list, err => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
// 获取最近文件列表
export const getRecent = () => {
let res = storage.getSync(RECENT_FILE_LIST)
return (Array.isArray(res) ? res : []).filter(item => {
return !!item
})
}
// 清除最近文件列表
export const clearRecent = () => {
return new Promise((resolve, reject) => {
storage.remove(RECENT_FILE_LIST, err => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
// 从最近文件列表中移除指定文件
export const removeFileInRecent = file => {
return new Promise((resolve, reject) => {
let list = getRecent()
let index = list.findIndex(item => {
return item === file
})
if (index !== -1) {
list.splice(index, 1)
}
storage.set(RECENT_FILE_LIST, list, err => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
// 替换指定文件
export const replaceFileInRecent = (oldFile, newFile) => {
return new Promise((resolve, reject) => {
let list = getRecent()
let index = list.findIndex(item => {
return item === oldFile
})
if (index !== -1) {
list.splice(index, 1)
}
list.push(newFile)
storage.set(RECENT_FILE_LIST, list, err => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}

View File

@@ -8,11 +8,21 @@ import '@/assets/icon-font/iconfont.css'
import 'viewerjs/dist/viewer.css'
import VueViewer from 'v-viewer'
import i18n from './i18n'
import './css/global.css'
Vue.config.productionTip = false
Vue.prototype.$bus = new Vue()
Vue.use(ElementUI)
Vue.use(VueViewer)
Vue.mixin({
data () {
return {
IS_ELECTRON: window.IS_ELECTRON,
IS_MAC: window.platform === 'darwin',
IS_WIN: window.platform === 'win32'
}
}
})
new Vue({
render: h => h(App),

View File

@@ -4,9 +4,9 @@
> The function of adjusting associated line control points is supported from v0.4.6+
This plugin is used to support the addition of associative lines.
> Relevance support for text editing starting from v0.5.11+
The plugin is currently not fully functional, and does not support adding text to association lines.
This plugin is used to support the addition of associative lines.
## Register

View File

@@ -7,8 +7,10 @@
<blockquote>
<p>The function of adjusting associated line control points is supported from v0.4.6+</p>
</blockquote>
<blockquote>
<p>Relevance support for text editing starting from v0.5.11+</p>
</blockquote>
<p>This plugin is used to support the addition of associative lines.</p>
<p>The plugin is currently not fully functional, and does not support adding text to association lines.</p>
<h2>Register</h2>
<pre class="hljs"><code><span class="hljs-keyword">import</span> MindMap <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map&#x27;</span>
<span class="hljs-keyword">import</span> AssociativeLine <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/AssociativeLine.js&#x27;</span>

View File

@@ -1,5 +1,29 @@
# Changelog
## 0.5.11
New: Supports associative text editing.
optimization: Optimizing theme configuration updates, changing configurations that do not involve node size does not trigger node recalculation.
## 0.5.10
New: Optimize node reuse logic using LRU caching algorithm.
## 0.5.10-fix.1
Fix: Fix the issue of import errors.
## 0.5.10-fix.2
Fix: Fixed the issue of switching themes and importing data without triggering data changes in rich text mode.
New: Add three new themes.
## 0.5.9
Change: Unified export method format, using `FileReader` instead of `URL.createObjectURL` to convert `blob` data.
## 0.5.8
optimization: 1.The position setting is not triggered when the node position does not change. 2.The unfolding and folding status does not change and does not trigger button updates.

View File

@@ -1,6 +1,18 @@
<template>
<div>
<h1>Changelog</h1>
<h2>0.5.11</h2>
<p>New: Supports associative text editing.</p>
<p>optimization: Optimizing theme configuration updates, changing configurations that do not involve node size does not trigger node recalculation.</p>
<h2>0.5.10</h2>
<p>New: Optimize node reuse logic using LRU caching algorithm.</p>
<h2>0.5.10-fix.1</h2>
<p>Fix: Fix the issue of import errors.</p>
<h2>0.5.10-fix.2</h2>
<p>Fix: Fixed the issue of switching themes and importing data without triggering data changes in rich text mode.</p>
<p>New: Add three new themes.</p>
<h2>0.5.9</h2>
<p>Change: Unified export method format, using <code>FileReader</code> instead of <code>URL.createObjectURL</code> to convert <code>blob</code> data.</p>
<h2>0.5.8</h2>
<p>optimization: 1.The position setting is not triggered when the node position does not change. 2.The unfolding and folding status does not change and does not trigger button updates.</p>
<p>New: 1.The default setting is to move the mouse over the node to display the expand and collapse buttons. 2.Support the list of icons that can be inserted into extended nodes.</p>

View File

@@ -28,7 +28,7 @@ const mindMap = new MindMap({
| data | Object | {} | Mind map data, refer to: https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js | |
| layout | String | logicalStructure | Layout type, options: logicalStructure (logical structure diagram), mindMap (mind map), catalogOrganization (catalog organization diagram), organizationStructure (organization structure diagram)、timelinev0.5.4+, timeline、timeline2v0.5.4+, up down alternating timeline、fishbonev0.5.4+, fishbone diagram | |
| fishboneDegv0.5.4+ | Number | 45 | Set the diagonal angle of the fishbone structure diagram | |
| theme | String | default | Theme, options: default, classic, minions, pinkGrape, mint, gold, vitalityOrange, greenLeaf, dark2, skyGreen, classic2, classic3, classic4(v0.2.0+), classicGreen, classicBlue, blueSky, brainImpairedPink, dark, earthYellow, freshGreen, freshRed, romanticPurple, simpleBlack(v0.5.4+), courseGreen(v0.5.4+), coffee(v0.5.4+), redSpirit(v0.5.4+), blackHumour(v0.5.4+), lateNightOffice(v0.5.4+), blackGold(v0.5.4+) | |
| theme | String | default | Theme, options: default, classic, minions, pinkGrape, mint, gold, vitalityOrange, greenLeaf, dark2, skyGreen, classic2, classic3, classic4(v0.2.0+), classicGreen, classicBlue, blueSky, brainImpairedPink, dark, earthYellow, freshGreen, freshRed, romanticPurple, simpleBlack(v0.5.4+), courseGreen(v0.5.4+), coffee(v0.5.4+), redSpirit(v0.5.4+), blackHumour(v0.5.4+), lateNightOffice(v0.5.4+), blackGold(v0.5.4+)、、avocado(v.5.10-fix.2+)、autumn(v.5.10-fix.2+)、orangeJuice(v.5.10-fix.2+) | |
| themeConfig | Object | {} | Theme configuration, will be merged with the selected theme, available fields refer to: https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js | |
| scaleRatio | Number | 0.1 | The incremental scaling ratio | |
| maxTag | Number | 5 | The maximum number of tags displayed in the node, any additional tags will be discarded | |
@@ -61,6 +61,7 @@ const mindMap = new MindMap({
| maxHistoryCountv0.5.6+ | Number | 1000 | | Maximum number of history records |
| alwaysShowExpandBtnv0.5.8+ | Boolean | false | Whether to always display the expand and collapse buttons of nodes, which are only displayed when the mouse is moved up and activated by default | |
| iconListv0.5.8+ | Array | [] | The icons that can be inserted into the extension node, and each item in the array is an object. Please refer to the "Icon Configuration" table below for the detailed structure of the object | |
| maxNodeCacheCountv0.5.10+ | Number | 1000 | The maximum number of cached nodes. To optimize performance, an internal node cache pool is maintained to reuse nodes. This attribute allows you to specify the maximum number of caches in the pool | |
### Watermark config
@@ -139,10 +140,14 @@ List of all currently registered plugins.
## Instance methods
### getSvgData()
### getSvgData({ paddingX = 0, paddingY = 0 })
> v0.3.0+
`paddingX`: Padding x
`paddingY`: Padding y
Get the `svg` data and return an object. The detailed structure is as follows:
```js

View File

@@ -60,7 +60,7 @@
<td>theme</td>
<td>String</td>
<td>default</td>
<td>Theme, options: default, classic, minions, pinkGrape, mint, gold, vitalityOrange, greenLeaf, dark2, skyGreen, classic2, classic3, classic4(v0.2.0+), classicGreen, classicBlue, blueSky, brainImpairedPink, dark, earthYellow, freshGreen, freshRed, romanticPurple, simpleBlack(v0.5.4+), courseGreen(v0.5.4+), coffee(v0.5.4+), redSpirit(v0.5.4+), blackHumour(v0.5.4+), lateNightOffice(v0.5.4+), blackGold(v0.5.4+)</td>
<td>Theme, options: default, classic, minions, pinkGrape, mint, gold, vitalityOrange, greenLeaf, dark2, skyGreen, classic2, classic3, classic4(v0.2.0+), classicGreen, classicBlue, blueSky, brainImpairedPink, dark, earthYellow, freshGreen, freshRed, romanticPurple, simpleBlack(v0.5.4+), courseGreen(v0.5.4+), coffee(v0.5.4+), redSpirit(v0.5.4+), blackHumour(v0.5.4+), lateNightOffice(v0.5.4+), blackGold(v0.5.4+)avocado(v.5.10-fix.2+)autumn(v.5.10-fix.2+)orangeJuice(v.5.10-fix.2+)</td>
<td></td>
</tr>
<tr>
@@ -287,6 +287,13 @@
<td>The icons that can be inserted into the extension node, and each item in the array is an object. Please refer to the &quot;Icon Configuration&quot; table below for the detailed structure of the object</td>
<td></td>
</tr>
<tr>
<td>maxNodeCacheCountv0.5.10+</td>
<td>Number</td>
<td>1000</td>
<td>The maximum number of cached nodes. To optimize performance, an internal node cache pool is maintained to reuse nodes. This attribute allows you to specify the maximum number of caches in the pool</td>
<td></td>
</tr>
</tbody>
</table>
<h3>Watermark config</h3>
@@ -406,10 +413,12 @@ mindMap.setTheme(<span class="hljs-string">&#x27;Theme name&#x27;</span>)
</blockquote>
<p>List of all currently registered plugins.</p>
<h2>Instance methods</h2>
<h3>getSvgData()</h3>
<h3>getSvgData({ paddingX = 0, paddingY = 0 })</h3>
<blockquote>
<p>v0.3.0+</p>
</blockquote>
<p><code>paddingX</code>: Padding x</p>
<p><code>paddingY</code>: Padding y</p>
<p>Get the <code>svg</code> data and return an object. The detailed structure is as follows:</p>
<pre class="hljs"><code>{
svg, <span class="hljs-comment">// Element, the overall svg element of the mind map graphics, including: svg (canvas container), g (actual mind map group)</span>

View File

@@ -15,14 +15,35 @@ After registration and instantiation of `MindMap`, the instance can be obtained
## Methods
All exported methods are asynchronous and return an instance of `Promise`. You can use the `then` method to obtain data, or use the `async await` function to obtain:
```js
mindMap.doExport.png().then((data) => {
// ...
})
const export = async () => {
let data = await mindMap.doExport.png()
// ...
}
```
The returned data is in the format of `data:URL`. You can create an `a` tag to trigger the download:
```js
let a = document.createElement('a')
a.href = 'xxx.png'// .png、.svg、.pdf、.md、.json、.smm
a.download = 'xxx'
a.click()
```
### png(name, transparent = false)
- `name`: Name, optional
- `transparent`: v0.5.7+, Specify whether the background of the exported image is transparent
Exports as `png`, an async method that returns image data, `data:url` data which
can be downloaded or displayed.
Exports as `png`.
### svg(name, plusCssText)
@@ -42,20 +63,7 @@ svg(
)
```
Exports as `svg`, an async method that returns `svg` data, `data:url` data which
can be downloaded or displayed.
### getSvgData()
Gets `svg` data, an async method that returns an object:
```js
{
node; // svg object
str; // svg string, if rich text editing is enabled and domToImage is set to true, the dom node in the svg character returned by this value will be converted into the form of an image
nodeWithDomToImg// v0.4.0+The svg object after the DOM node is converted to an image has a value only when rich text editing is enabled and domToImage is set to true, otherwise null
}
```
Exports as `svg`.
### pdf(name)
@@ -63,7 +71,7 @@ Gets `svg` data, an async method that returns an object:
`name`File name
Export as `pdf`
Export as `pdf`. Unlike other export methods, this method does not return data and directly triggers the download.
### json(name, withConfig)
@@ -71,10 +79,21 @@ Export as `pdf`
`withConfig``Boolean`, default `true`, Whether the data contains configuration, otherwise it is pure mind map node data
Return `json` data, `data:url` type, you can download it yourself
Return `json` data.
### md()
> v0.4.7+
Export as `markdown` file, Return `json` data, `data:url` type, you can download it yourself
Export as `markdown` file.
### getSvgData()
Gets `svg` data, an async method that returns an object:
```js
{
node // svg node
str // svg string
}
```

View File

@@ -10,6 +10,22 @@ MindMap.usePlugin(Export)
</code></pre>
<p>After registration and instantiation of <code>MindMap</code>, the instance can be obtained through <code>mindMap.doExport</code>.</p>
<h2>Methods</h2>
<p>All exported methods are asynchronous and return an instance of <code>Promise</code>. You can use the <code>then</code> method to obtain data, or use the <code>async await</code> function to obtain:</p>
<pre class="hljs"><code>mindMap.doExport.png().then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
<span class="hljs-comment">// ...</span>
})
<span class="hljs-keyword">const</span> <span class="hljs-keyword">export</span> = <span class="hljs-keyword">async</span> () =&gt; {
<span class="hljs-keyword">let</span> data = <span class="hljs-keyword">await</span> mindMap.doExport.png()
<span class="hljs-comment">// ...</span>
}
</code></pre>
<p>The returned data is in the format of <code>data:URL</code>. You can create an <code>a</code> tag to trigger the download:</p>
<pre class="hljs"><code><span class="hljs-keyword">let</span> a = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">&#x27;a&#x27;</span>)
a.href = <span class="hljs-string">&#x27;xxx.png&#x27;</span><span class="hljs-comment">// .png、.svg、.pdf、.md、.json、.smm</span>
a.download = <span class="hljs-string">&#x27;xxx&#x27;</span>
a.click()
</code></pre>
<h3>png(name, transparent = false)</h3>
<ul>
<li>
@@ -19,8 +35,7 @@ MindMap.usePlugin(Export)
<p><code>transparent</code>: v0.5.7+, Specify whether the background of the exported image is transparent</p>
</li>
</ul>
<p>Exports as <code>png</code>, an async method that returns image data, <code>data:url</code> data which
can be downloaded or displayed.</p>
<p>Exports as <code>png</code>.</p>
<h3>svg(name, plusCssText)</h3>
<ul>
<li>
@@ -40,31 +55,29 @@ can be downloaded or displayed.</p>
}`</span>
)
</code></pre>
<p>Exports as <code>svg</code>, an async method that returns <code>svg</code> data, <code>data:url</code> data which
can be downloaded or displayed.</p>
<h3>getSvgData()</h3>
<p>Gets <code>svg</code> data, an async method that returns an object:</p>
<pre class="hljs"><code>{
node; <span class="hljs-comment">// svg object</span>
str; <span class="hljs-comment">// svg string, if rich text editing is enabled and domToImage is set to true, the dom node in the svg character returned by this value will be converted into the form of an image</span>
nodeWithDomToImg<span class="hljs-comment">// v0.4.0+The svg object after the DOM node is converted to an image has a value only when rich text editing is enabled and domToImage is set to true, otherwise null</span>
}
</code></pre>
<p>Exports as <code>svg</code>.</p>
<h3>pdf(name)</h3>
<blockquote>
<p>v0.2.1+</p>
</blockquote>
<p><code>name</code>File name</p>
<p>Export as <code>pdf</code></p>
<p>Export as <code>pdf</code>. Unlike other export methods, this method does not return data and directly triggers the download.</p>
<h3>json(name, withConfig)</h3>
<p><code>name</code>It is temporarily useless, just pass an empty string</p>
<p><code>withConfig``Boolean</code>, default <code>true</code>, Whether the data contains configuration, otherwise it is pure mind map node data</p>
<p>Return <code>json</code> data, <code>data:url</code> type, you can download it yourself</p>
<p>Return <code>json</code> data.</p>
<h3>md()</h3>
<blockquote>
<p>v0.4.7+</p>
</blockquote>
<p>Export as <code>markdown</code> file, Return <code>json</code> data, <code>data:url</code> type, you can download it yourself</p>
<p>Export as <code>markdown</code> file.</p>
<h3>getSvgData()</h3>
<p>Gets <code>svg</code> data, an async method that returns an object:</p>
<pre class="hljs"><code>{
node <span class="hljs-comment">// svg node</span>
str <span class="hljs-comment">// svg string</span>
}
</code></pre>
</div>
</template>

View File

@@ -97,6 +97,18 @@ These open-source mind maps are also good, each with its own characteristics, bu
In summary, in open-source mind maps, it is difficult to find a better choice than `simple-mind-map`. Of course, `simple-mind-map` is far from being the best, and it also has many shortcomings, as you saw in the previous [special note]. However, `simple-mind-map` has always been in a fast iteration process, and we welcome you to join and improve it together.
## Browser Compatibility
We recommend using the latest version of the `Chrome` browser.
Limited testing situation:
Normal operation: `360` extreme speed browserv13.5.2036.0)、`opera` browserv71.0.3770.284)。
Abnormal operation: `Firefox`v98.0.2, Node content cannot be displayed in rich text mode. If you want to support the `Firefox` browser, please use non rich text mode.
Unsupported: `IE` browser.
## License
[MIT](https://opensource.org/licenses/MIT)

View File

@@ -7,21 +7,21 @@
</blockquote>
<h2>Features</h2>
<ul>
<li><input type="checkbox" id="checkbox18" checked="true" /><label for="checkbox18">Plugin architecture. In addition to core functions, other functions are provided as plugins, which can be used as needed to reduce the overall volume</label></li>
<li><input type="checkbox" id="checkbox19" checked="true" /><label for="checkbox19">Supports six types of structures: logical structure diagrams, mind maps,</label>
<li><input type="checkbox" id="checkbox216" checked="true" /><label for="checkbox216">Plugin architecture. In addition to core functions, other functions are provided as plugins, which can be used as needed to reduce the overall volume</label></li>
<li><input type="checkbox" id="checkbox217" checked="true" /><label for="checkbox217">Supports six types of structures: logical structure diagrams, mind maps,</label>
organizational structure diagrams, directory organization diagrams, timeline, and fishbone diagrams</li>
<li><input type="checkbox" id="checkbox20" checked="true" /><label for="checkbox20">Built-in multiple themes and allows for highly customized styles, and support register new themes</label></li>
<li><input type="checkbox" id="checkbox21" checked="true" /><label for="checkbox21">Supports shortcuts</label></li>
<li><input type="checkbox" id="checkbox22" checked="true" /><label for="checkbox22">Node content supports images, icons, hyperlinks, notes, tags, and</label>
<li><input type="checkbox" id="checkbox218" checked="true" /><label for="checkbox218">Built-in multiple themes and allows for highly customized styles, and support register new themes</label></li>
<li><input type="checkbox" id="checkbox219" checked="true" /><label for="checkbox219">Supports shortcuts</label></li>
<li><input type="checkbox" id="checkbox220" checked="true" /><label for="checkbox220">Node content supports images, icons, hyperlinks, notes, tags, and</label>
summaries</li>
<li><input type="checkbox" id="checkbox23" checked="true" /><label for="checkbox23">Supports forward and backward navigation</label></li>
<li><input type="checkbox" id="checkbox24" checked="true" /><label for="checkbox24">Supports dragging and scaling</label></li>
<li><input type="checkbox" id="checkbox25" checked="true" /><label for="checkbox25">Supports right-click and Ctrl + left-click to select multiple items</label></li>
<li><input type="checkbox" id="checkbox26" checked="true" /><label for="checkbox26">Supports free dragging and dragging to adjust nodes</label></li>
<li><input type="checkbox" id="checkbox27" checked="true" /><label for="checkbox27">Supports various node shapes</label></li>
<li><input type="checkbox" id="checkbox28" checked="true" /><label for="checkbox28">Supports export to json, png, svg, pdf markdown, and import from json, xmind, markdown</label></li>
<li><input type="checkbox" id="checkbox29" checked="true" /><label for="checkbox29">Supports mini mapsupport watermark</label></li>
<li><input type="checkbox" id="checkbox30" checked="true" /><label for="checkbox30">Supports associative lines</label></li>
<li><input type="checkbox" id="checkbox221" checked="true" /><label for="checkbox221">Supports forward and backward navigation</label></li>
<li><input type="checkbox" id="checkbox222" checked="true" /><label for="checkbox222">Supports dragging and scaling</label></li>
<li><input type="checkbox" id="checkbox223" checked="true" /><label for="checkbox223">Supports right-click and Ctrl + left-click to select multiple items</label></li>
<li><input type="checkbox" id="checkbox224" checked="true" /><label for="checkbox224">Supports free dragging and dragging to adjust nodes</label></li>
<li><input type="checkbox" id="checkbox225" checked="true" /><label for="checkbox225">Supports various node shapes</label></li>
<li><input type="checkbox" id="checkbox226" checked="true" /><label for="checkbox226">Supports export to json, png, svg, pdf markdown, and import from json, xmind, markdown</label></li>
<li><input type="checkbox" id="checkbox227" checked="true" /><label for="checkbox227">Supports mini mapsupport watermark</label></li>
<li><input type="checkbox" id="checkbox228" checked="true" /><label for="checkbox228">Supports associative lines</label></li>
</ul>
<h2>Repository Catalog Introduction</h2>
<p>1.<code>simple-mind-map</code></p>
@@ -31,16 +31,16 @@ frameworks such as Vue and React, or without a framework.</p>
<p>This is an online mind map built using the <code>simple-mind-map</code> library and based
on <code>Vue2.x</code> and <code>ElementUI</code>. Features include:</p>
<ul>
<li><input type="checkbox" id="checkbox31" checked="true" /><label for="checkbox31">Toolbar, which supports inserting and deleting nodes, and editing node</label>
<li><input type="checkbox" id="checkbox229" checked="true" /><label for="checkbox229">Toolbar, which supports inserting and deleting nodes, and editing node</label>
images, icons, hyperlinks, notes, tags, and summaries</li>
<li><input type="checkbox" id="checkbox32" checked="true" /><label for="checkbox32">Sidebar, with panels for basic style settings, node style settings,</label>
<li><input type="checkbox" id="checkbox230" checked="true" /><label for="checkbox230">Sidebar, with panels for basic style settings, node style settings,</label>
outline, theme selection, and structure selection</li>
<li><input type="checkbox" id="checkbox33" checked="true" /><label for="checkbox33">Import and export functionality; data is saved in the browser's local</label>
<li><input type="checkbox" id="checkbox231" checked="true" /><label for="checkbox231">Import and export functionality; data is saved in the browser's local</label>
storage by default, but it also supports creating, opening, and editing
local files on the computer directly</li>
<li><input type="checkbox" id="checkbox34" checked="true" /><label for="checkbox34">Right-click menu, which supports operations such as expanding, collapsing,</label>
<li><input type="checkbox" id="checkbox232" checked="true" /><label for="checkbox232">Right-click menu, which supports operations such as expanding, collapsing,</label>
and organizing layout</li>
<li><input type="checkbox" id="checkbox35" checked="true" /><label for="checkbox35">Bottom bar, which supports node and word count statistics, switching</label>
<li><input type="checkbox" id="checkbox233" checked="true" /><label for="checkbox233">Bottom bar, which supports node and word count statistics, switching</label>
between edit and read-only modes, zooming in and out, and switching to
full screen, support mini map</li>
</ul>
@@ -69,6 +69,12 @@ full screen, support mini map</li>
<p>3.<a href="https://github.com/hizzgdev/jsmind">jsmind</a><a href="https://github.com/ssshooter/mind-elixir-core">Mind-elixir</a><a href="https://github.com/ondras/my-mind">my-mind</a><a href="https://github.com/awehook/blink-mind">blink-mind</a><a href="https://github.com/luvsic3/remind">remind</a><a href="https://github.com/hellowuxin/vue3-mindmap">vue3-mindmap</a><a href="https://github.com/zyascend/ZMindMap">ZMindMap</a>...</p>
<p>These open-source mind maps are also good, each with its own characteristics, but they also have certain drawbacks, such as stopping updates, average interface aesthetics, less functionality, relying on a certain framework, and so on.</p>
<p>In summary, in open-source mind maps, it is difficult to find a better choice than <code>simple-mind-map</code>. Of course, <code>simple-mind-map</code> is far from being the best, and it also has many shortcomings, as you saw in the previous [special note]. However, <code>simple-mind-map</code> has always been in a fast iteration process, and we welcome you to join and improve it together.</p>
<h2>Browser Compatibility</h2>
<p>We recommend using the latest version of the <code>Chrome</code> browser.</p>
<p>Limited testing situation:</p>
<p>Normal operation: <code>360</code> extreme speed browserv13.5.2036.0<code>opera</code> browserv71.0.3770.284</p>
<p>Abnormal operation: <code>Firefox</code>v98.0.2, Node content cannot be displayed in rich text mode. If you want to support the <code>Firefox</code> browser, please use non rich text mode.</p>
<p>Unsupported: <code>IE</code> browser.</p>
<h2>License</h2>
<p><a href="https://opensource.org/licenses/MIT">MIT</a></p>
<h2>Invite the author to a cup of coffee</h2>

View File

@@ -1,6 +1,10 @@
# Participate in translation
Thanks for the first version English translation provided by [Emircan ERKUL](https://github.com/emircanerkul).
> Thanks for the first version English translation provided by [Emircan ERKUL](https://github.com/emircanerkul).
>
> Due to limited energy, most translations currently use machine translation, so accuracy is inevitably problematic.
>
> At present, the 【Course】 section is not translated. If you are interested, please join us.
If you want to participate in the translation of this document, you can clone this repository first.

View File

@@ -1,7 +1,11 @@
<template>
<div>
<h1>Participate in translation</h1>
<blockquote>
<p>Thanks for the first version English translation provided by <a href="https://github.com/emircanerkul">Emircan ERKUL</a>.</p>
<p>Due to limited energy, most translations currently use machine translation, so accuracy is inevitably problematic.</p>
<p>At present, the Course section is not translated. If you are interested, please join us.</p>
</blockquote>
<p>If you want to participate in the translation of this document, you can clone this repository first.</p>
<p>The translated documents are in the <code>/web/src/pages/Doc/</code> directory, and currently support English(<code>en</code>) and Simplified Chinese(<code>zh</code>).</p>
<p>If you are adding a new language type, you can create a new directory under the <code>/web/src/pages/Doc/</code> directory, Then create a folder for each chapter, You can also directly copy all chapter directories under the existing language directory for translation, Note that you only need to write the <code>index.md</code> file, The <code>index.vue</code> file under the chapter directory is automatically generated by the script according to <code>index.md</code>.</p>

View File

@@ -129,6 +129,16 @@ Measure the width and height of the text, return value:
{ width, height }
```
#### getTextFromHtml(html)
Extract plain text content from an HTML string.
#### readBlob(blob)
> v0.5.9+
Convert `blob` data to `data:url` data.
## Simulate CSS background in Canvas
Import:
@@ -157,4 +167,50 @@ drawBackgroundImageToCanvas(ctx, width, height, img, {
// success
}
})
```
```
## LRU cache class
> v0.5.10+
Import:
```js
import Lru from 'simple-mind-map/src/utils/Lru.js'
```
### Constructor
```js
let lru = new Lru(max)
```
`max`: Specify the maximum number of caches.
### Instance properties
#### size
The current number of caches.
#### pool
Get cache pool.
### Instance methods
#### add(key, value)
Add cache.
#### delete(key)
Delete cache.
#### has(key)
Check if a cache exists.
#### get(key)
Gets the value of a cache.

View File

@@ -82,6 +82,13 @@ and copying the <code>data</code> of the data object, example:</p>
<p>Measure the width and height of the text, return value:</p>
<pre class="hljs"><code>{ width, height }
</code></pre>
<h4>getTextFromHtml(html)</h4>
<p>Extract plain text content from an HTML string.</p>
<h4>readBlob(blob)</h4>
<blockquote>
<p>v0.5.9+</p>
</blockquote>
<p>Convert <code>blob</code> data to <code>data:url</code> data.</p>
<h2>Simulate CSS background in Canvas</h2>
<p>Import:</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> drawBackgroundImageToCanvas <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/utils/simulateCSSBackgroundInCanvas&#x27;</span>
@@ -105,6 +112,31 @@ drawBackgroundImageToCanvas(ctx, width, height, img, {
}
})
</code></pre>
<h2>LRU cache class</h2>
<blockquote>
<p>v0.5.10+</p>
</blockquote>
<p>Import:</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> Lru <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/utils/Lru.js&#x27;</span>
</code></pre>
<h3>Constructor</h3>
<pre class="hljs"><code><span class="hljs-keyword">let</span> lru = <span class="hljs-keyword">new</span> Lru(max)
</code></pre>
<p><code>max</code>: Specify the maximum number of caches.</p>
<h3>Instance properties</h3>
<h4>size</h4>
<p>The current number of caches.</p>
<h4>pool</h4>
<p>Get cache pool.</p>
<h3>Instance methods</h3>
<h4>add(key, value)</h4>
<p>Add cache.</p>
<h4>delete(key)</h4>
<p>Delete cache.</p>
<h4>has(key)</h4>
<p>Check if a cache exists.</p>
<h4>get(key)</h4>
<p>Gets the value of a cache.</p>
</div>
</template>

View File

@@ -4,9 +4,9 @@
> 调整关联线控制点的功能从v0.4.6+开始支持
该插件用于支持添加关联线。
> 关联性支持文本编辑从v0.5.11+开始支持
该插件目前功能还不完善,不支持在关联线上添加文字
该插件用于支持添加关联线
## 注册

View File

@@ -7,8 +7,10 @@
<blockquote>
<p>调整关联线控制点的功能从v0.4.6+开始支持</p>
</blockquote>
<blockquote>
<p>关联性支持文本编辑从v0.5.11+开始支持</p>
</blockquote>
<p>该插件用于支持添加关联线</p>
<p>该插件目前功能还不完善不支持在关联线上添加文字</p>
<h2>注册</h2>
<pre class="hljs"><code><span class="hljs-keyword">import</span> MindMap <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map&#x27;</span>
<span class="hljs-keyword">import</span> AssociativeLine <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/AssociativeLine.js&#x27;</span>

View File

@@ -1,5 +1,29 @@
# Changelog
## 0.5.11
新增:支持关联性文本编辑。
优化:优化主题配置更新,改变不涉及节点大小的配置不触发节点重新计算。
## 0.5.10
新增使用LRU缓存算法优化节点复用逻辑。
## 0.5.10-fix.1
修复:修复导入出错的问题。
## 0.5.10-fix.2
修复:修复富文本模式下,切换主题、导入数据后没有触发数据改变的问题。
新增:新增三种主题。
## 0.5.9
修改:统一导出方法的格式,使用`FileReader`代替`URL.createObjectURL`转换`blob`数据。
## 0.5.8
优化1.节点位置没有变化不触发位置设置。 2.展开收起状态没有变化不触发按钮更新。

View File

@@ -1,6 +1,18 @@
<template>
<div>
<h1>Changelog</h1>
<h2>0.5.11</h2>
<p>新增支持关联性文本编辑</p>
<p>优化优化主题配置更新改变不涉及节点大小的配置不触发节点重新计算</p>
<h2>0.5.10</h2>
<p>新增使用LRU缓存算法优化节点复用逻辑</p>
<h2>0.5.10-fix.1</h2>
<p>修复修复导入出错的问题</p>
<h2>0.5.10-fix.2</h2>
<p>修复修复富文本模式下切换主题导入数据后没有触发数据改变的问题</p>
<p>新增新增三种主题</p>
<h2>0.5.9</h2>
<p>修改统一导出方法的格式使用<code>FileReader</code>代替<code>URL.createObjectURL</code>转换<code>blob</code>数据</p>
<h2>0.5.8</h2>
<p>优化1.节点位置没有变化不触发位置设置 2.展开收起状态没有变化不触发按钮更新</p>
<p>新增1.默认改为鼠标移上节点才显示展开收起按钮 2.支持扩展节点可插入的图标列表</p>

View File

@@ -28,7 +28,7 @@ const mindMap = new MindMap({
| data | Object | {} | 思维导图数据,可参考:[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js) | |
| layout | String | logicalStructure | 布局类型可选列表logicalStructure逻辑结构图、mindMap思维导图、catalogOrganization目录组织图、organizationStructure组织结构图、timelinev0.5.4+时间轴、timeline2v0.5.4+上下交替型时间轴、fishbonev0.5.4+,鱼骨图) | |
| fishboneDegv0.5.4+ | Number | 45 | 设置鱼骨结构图的斜线角度 | |
| theme | String | default | 主题可选列表default默认、classic脑图经典、minions小黄人、pinkGrape粉红葡萄、mint薄荷、gold金色vip、vitalityOrange活力橙、greenLeaf绿叶、dark2暗色2、skyGreen天清绿、classic2脑图经典2、classic3脑图经典3、classic4脑图经典4v0.2.0+、classicGreen经典绿、classicBlue经典蓝、blueSky天空蓝、brainImpairedPink脑残粉、dark暗色、earthYellow泥土黄、freshGreen清新绿、freshRed清新红、romanticPurple浪漫紫、simpleBlackv0.5.4+简约黑、courseGreenv0.5.4+课程绿、coffeev0.5.4+咖啡、redSpiritv0.5.4+红色精神、blackHumourv0.5.4+黑色幽默、lateNightOfficev0.5.4+深夜办公室、blackGoldv0.5.4+黑金) | |
| theme | String | default | 主题可选列表default默认、classic脑图经典、minions小黄人、pinkGrape粉红葡萄、mint薄荷、gold金色vip、vitalityOrange活力橙、greenLeaf绿叶、dark2暗色2、skyGreen天清绿、classic2脑图经典2、classic3脑图经典3、classic4脑图经典4v0.2.0+、classicGreen经典绿、classicBlue经典蓝、blueSky天空蓝、brainImpairedPink脑残粉、dark暗色、earthYellow泥土黄、freshGreen清新绿、freshRed清新红、romanticPurple浪漫紫、simpleBlackv0.5.4+简约黑、courseGreenv0.5.4+课程绿、coffeev0.5.4+咖啡、redSpiritv0.5.4+红色精神、blackHumourv0.5.4+黑色幽默、lateNightOfficev0.5.4+深夜办公室、blackGoldv0.5.4+黑金)、avocadov.5.10-fix.2+牛油果、autumnv.5.10-fix.2+秋天、orangeJuicev.5.10-fix.2+橙汁) | |
| themeConfig | Object | {} | 主题配置,会和所选择的主题进行合并,可用字段可参考:[https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js) | |
| scaleRatio | Number | 0.1 | 放大缩小的增量比例 | |
| maxTag | Number | 5 | 节点里最多显示的标签数量,多余的会被丢弃 | |
@@ -61,6 +61,7 @@ const mindMap = new MindMap({
| maxHistoryCountv0.5.6+ | Number | 1000 | 最大历史记录数 | |
| alwaysShowExpandBtnv0.5.8+ | Boolean | false | 是否一直显示节点的展开收起按钮,默认为鼠标移上去和激活时才显示 | |
| iconListv0.5.8+ | Array | [] | 扩展节点可插入的图标,数组的每一项为一个对象,对象详细结构请参考下方【图标配置】表格 | |
| maxNodeCacheCountv0.5.10+ | Number | 1000 | 节点最大缓存数量。为了优化性能,内部会维护一个节点缓存池,用来复用节点,通过该属性可以指定池的最大缓存数量 | |
### 水印配置
@@ -137,10 +138,14 @@ mindMap.setTheme('主题名称')
## 实例方法
### getSvgData()
### getSvgData({ paddingX = 0, paddingY = 0 })
> v0.3.0+
`paddingX`:水平内边距
`paddingY`:垂直内边距
获取`svg`数据,返回一个对象,详细结构如下:
```js

View File

@@ -60,7 +60,7 @@
<td>theme</td>
<td>String</td>
<td>default</td>
<td>主题可选列表default默认classic脑图经典minions小黄人pinkGrape粉红葡萄mint薄荷gold金色vipvitalityOrange活力橙greenLeaf绿叶dark2暗色2skyGreen天清绿classic2脑图经典2classic3脑图经典3classic4脑图经典4v0.2.0+classicGreen经典绿classicBlue经典蓝blueSky天空蓝brainImpairedPink脑残粉dark暗色earthYellow泥土黄freshGreen清新绿freshRed清新红romanticPurple浪漫紫simpleBlackv0.5.4+简约黑courseGreenv0.5.4+课程绿coffeev0.5.4+咖啡redSpiritv0.5.4+红色精神blackHumourv0.5.4+黑色幽默lateNightOfficev0.5.4+深夜办公室blackGoldv0.5.4+黑金</td>
<td>主题可选列表default默认classic脑图经典minions小黄人pinkGrape粉红葡萄mint薄荷gold金色vipvitalityOrange活力橙greenLeaf绿叶dark2暗色2skyGreen天清绿classic2脑图经典2classic3脑图经典3classic4脑图经典4v0.2.0+classicGreen经典绿classicBlue经典蓝blueSky天空蓝brainImpairedPink脑残粉dark暗色earthYellow泥土黄freshGreen清新绿freshRed清新红romanticPurple浪漫紫simpleBlackv0.5.4+简约黑courseGreenv0.5.4+课程绿coffeev0.5.4+咖啡redSpiritv0.5.4+红色精神blackHumourv0.5.4+黑色幽默lateNightOfficev0.5.4+深夜办公室blackGoldv0.5.4+黑金avocadov.5.10-fix.2+牛油果autumnv.5.10-fix.2+秋天orangeJuicev.5.10-fix.2+橙汁</td>
<td></td>
</tr>
<tr>
@@ -287,6 +287,13 @@
<td>扩展节点可插入的图标数组的每一项为一个对象对象详细结构请参考下方图标配置表格</td>
<td></td>
</tr>
<tr>
<td>maxNodeCacheCountv0.5.10+</td>
<td>Number</td>
<td>1000</td>
<td>节点最大缓存数量为了优化性能内部会维护一个节点缓存池用来复用节点通过该属性可以指定池的最大缓存数量</td>
<td></td>
</tr>
</tbody>
</table>
<h3>水印配置</h3>
@@ -406,10 +413,12 @@ mindMap.setTheme(<span class="hljs-string">&#x27;主题名称&#x27;</span>)
</blockquote>
<p>当前注册的所有插件列表</p>
<h2>实例方法</h2>
<h3>getSvgData()</h3>
<h3>getSvgData({ paddingX = 0, paddingY = 0 })</h3>
<blockquote>
<p>v0.3.0+</p>
</blockquote>
<p><code>paddingX</code>水平内边距</p>
<p><code>paddingY</code>垂直内边距</p>
<p>获取<code>svg</code>数据返回一个对象详细结构如下</p>
<pre class="hljs"><code>{
svg, <span class="hljs-comment">// Element思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组</span>

View File

@@ -55,7 +55,7 @@
}
```
`icon`目前只能使用内置的图标,完整图标可以在[icons.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/svg/icons.js)文件中查看。
`icon`可以使用内置的图标,完整图标可以在[icons.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/svg/icons.js)文件中查看。也可以扩展图标,参考[扩展图标](https://wanglin2.github.io/mind-map/#/doc/zh/course19/%E6%89%A9%E5%B1%95%E5%9B%BE%E6%A0%87)。
创建实例时还支持传递其他很多选项参数,完整选项列表可以在[实例化选项](https://wanglin2.github.io/mind-map/#/doc/zh/constructor/%E5%AE%9E%E4%BE%8B%E5%8C%96%E9%80%89%E9%A1%B9)查看。

View File

@@ -46,7 +46,7 @@
<span class="hljs-attr">children</span>: []<span class="hljs-comment">// 子节点</span>
}
</code></pre>
<p><code>icon</code>目前只能使用内置的图标完整图标可以在<a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/svg/icons.js">icons.js</a>文件中查看</p>
<p><code>icon</code>可以使用内置的图标完整图标可以在<a href="https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/svg/icons.js">icons.js</a>文件中查看也可以扩展图标参考<a href="https://wanglin2.github.io/mind-map/#/doc/zh/course19/%E6%89%A9%E5%B1%95%E5%9B%BE%E6%A0%87">扩展图标</a></p>
<p>创建实例时还支持传递其他很多选项参数完整选项列表可以在<a href="https://wanglin2.github.io/mind-map/#/doc/zh/constructor/%E5%AE%9E%E4%BE%8B%E5%8C%96%E9%80%89%E9%A1%B9">实例化选项</a>查看</p>
<p>这样得到的思维导图可以通过鼠标和快捷键进行操作比如单击某个节点可以激活它双击某个节点可以编辑节点文本按下<code>Tab</code>键会给当前激活的节点添加一个子节点按下<code>Enter</code>键会给当前激活的节点添加一个兄弟节点等等完整的快捷键列表可以参考<a href="https://github.com/wanglin2/mind-map/blob/main/web/src/config/zh.js#L246">快捷键列表</a></p>
<p>当然有些功能还是需要UI界面的比如图标列表编辑超链接等等需要注意的是<code>simple-mind-map</code>库并不包含UI界面所以需要你自己开发然后通过<code>simple-mind-map</code>提供的相关API来操作本教程的其他章节会向你介绍如何使用</p>

View File

@@ -8,7 +8,17 @@
`.smm``simple-mind-map`自己定义的一种文件,其实就是`json`文件,换了一个扩展名而已。
导出直接调用`export`方法即可
导出直接调用`export`方法即可
```js
mindMap.export(type, isDownload, fileName, ...)
```
`type`:文件类型
`isDownload`:传`true`会触发下载,`false`则不会,函数会返回导出文件的数据,`data:url`格式,你可以自行下载,`pdf`不支持该参数,默认会直接下载。
`fileName`:下载的文件名称
### 导出为smm、json

View File

@@ -7,7 +7,12 @@
</blockquote>
<p>目前支持导出为<code>.smm</code><code>.json</code><code>.svg</code><code>.png</code><code>.pdf</code><code>.md</code>文件</p>
<p><code>.smm</code><code>simple-mind-map</code>自己定义的一种文件其实就是<code>json</code>文件换了一个扩展名而已</p>
<p>导出直接调用<code>export</code>方法即可</p>
<p>导出直接调用<code>export</code>方法即可</p>
<pre class="hljs"><code>mindMap.export(type, isDownload, fileName, ...)
</code></pre>
<p><code>type</code>文件类型</p>
<p><code>isDownload</code><code>true</code>会触发下载<code>false</code>则不会函数会返回导出文件的数据<code>data:url</code>格式你可以自行下载<code>pdf</code>不支持该参数默认会直接下载</p>
<p><code>fileName</code>下载的文件名称</p>
<h3>导出为smmjson</h3>
<p>这两种文件的导出是一样的</p>
<pre class="hljs"><code>mindMap.export(type, isDownload, fileName, withConfig)

View File

@@ -15,13 +15,35 @@ MindMap.usePlugin(Export)
## 方法
所有导出的方法都是异步方法,返回一个`Promise`实例,你可以使用`then`方法获取数据,或者使用`async await`函数获取:
```js
mindMap.doExport.png().then((data) => {
// ...
})
const export = async () => {
let data = await mindMap.doExport.png()
// ...
}
```
返回的数据为`data:url`格式的,你可以创建一个`a`标签来触发下载:
```js
let a = document.createElement('a')
a.href = 'xxx.png'// .png、.svg、.pdf、.md、.json、.smm
a.download = 'xxx'
a.click()
```
### png(name, transparent = false)
- `name`:名称,可不传
- `transparent`v0.5.7+,指定导出图片的背景是否是透明的
导出为`png`,异步方法,返回图片数据,`data:url`数据,可以自行下载或显示
导出为`png`
### svg(name, plusCssText)
@@ -41,19 +63,7 @@ svg(
)
```
导出为`svg`,异步方法,返回`svg`数据,`data:url`数据,可以自行下载或显示
### getSvgData()
获取`svg`数据,异步方法,返回一个对象:
```js
{
node// svg对象
str// svg字符串如果开启了富文本编辑且domToImage设为true那么该值返回的svg字符内的dom节点会被转换成图片的形式
nodeWithDomToImg// v0.4.0+DOM节点转换为图片后的svg对象只有当开启了富文本编辑且domToImage设为true才有值否则为null
}
```
导出为`svg`
### pdf(name)
@@ -61,7 +71,7 @@ svg(
`name`:文件名称
导出为`pdf`
导出为`pdf`,和其他导出方法不一样,这个方法不会返回数据,会直接触发下载。
### json(name, withConfig)
@@ -69,10 +79,25 @@ svg(
`withConfig``Boolean`, 默认为`true`,数据中是否包含配置,否则为纯思维导图节点数据
返回`json`数据`data:url`数据,可以自行下载
返回`json`数据
### smm(name, withConfig)
`simple-mind-map`自定义的文件格式,其实就是`json`,和`json`方法返回的数据一模一样。
### md()
> v0.4.7+
导出`markdown`文件,返回`data:url`数据,可以自行下载
导出`markdown`文件
### getSvgData()
获取`svg`数据,异步方法,返回一个对象:
```js
{
node// svg节点
str// svg字符串
}
```

View File

@@ -10,6 +10,22 @@ MindMap.usePlugin(Export)
</code></pre>
<p>注册完且实例化<code>MindMap</code>后可通过<code>mindMap.doExport</code>获取到该实例</p>
<h2>方法</h2>
<p>所有导出的方法都是异步方法返回一个<code>Promise</code>实例你可以使用<code>then</code>方法获取数据或者使用<code>async await</code>函数获取</p>
<pre class="hljs"><code>mindMap.doExport.png().then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
<span class="hljs-comment">// ...</span>
})
<span class="hljs-keyword">const</span> <span class="hljs-keyword">export</span> = <span class="hljs-keyword">async</span> () =&gt; {
<span class="hljs-keyword">let</span> data = <span class="hljs-keyword">await</span> mindMap.doExport.png()
<span class="hljs-comment">// ...</span>
}
</code></pre>
<p>返回的数据为<code>data:url</code>格式的你可以创建一个<code>a</code>标签来触发下载</p>
<pre class="hljs"><code><span class="hljs-keyword">let</span> a = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">&#x27;a&#x27;</span>)
a.href = <span class="hljs-string">&#x27;xxx.png&#x27;</span><span class="hljs-comment">// .png、.svg、.pdf、.md、.json、.smm</span>
a.download = <span class="hljs-string">&#x27;xxx&#x27;</span>
a.click()
</code></pre>
<h3>png(name, transparent = false)</h3>
<ul>
<li>
@@ -19,7 +35,7 @@ MindMap.usePlugin(Export)
<p><code>transparent</code>v0.5.7+指定导出图片的背景是否是透明的</p>
</li>
</ul>
<p>导出为<code>png</code>异步方法返回图片数据<code>data:url</code>数据可以自行下载或显示</p>
<p>导出为<code>png</code></p>
<h3>svg(name, plusCssText)</h3>
<ul>
<li>
@@ -39,30 +55,31 @@ MindMap.usePlugin(Export)
}`</span>
)
</code></pre>
<p>导出为<code>svg</code>异步方法返回<code>svg</code>数据<code>data:url</code>数据可以自行下载或显示</p>
<h3>getSvgData()</h3>
<p>获取<code>svg</code>数据异步方法返回一个对象</p>
<pre class="hljs"><code>{
node<span class="hljs-comment">// svg对象</span>
str<span class="hljs-comment">// svg字符串如果开启了富文本编辑且domToImage设为true那么该值返回的svg字符内的dom节点会被转换成图片的形式</span>
nodeWithDomToImg<span class="hljs-comment">// v0.4.0+DOM节点转换为图片后的svg对象只有当开启了富文本编辑且domToImage设为true才有值否则为null</span>
}
</code></pre>
<p>导出为<code>svg</code></p>
<h3>pdf(name)</h3>
<blockquote>
<p>v0.2.1+</p>
</blockquote>
<p><code>name</code>文件名称</p>
<p>导出为<code>pdf</code></p>
<p>导出为<code>pdf</code>和其他导出方法不一样这个方法不会返回数据会直接触发下载</p>
<h3>json(name, withConfig)</h3>
<p><code>name</code>暂时没有用处传空字符串即可</p>
<p><code>withConfig``Boolean</code>, 默认为<code>true</code>数据中是否包含配置否则为纯思维导图节点数据</p>
<p>返回<code>json</code>数据<code>data:url</code>数据可以自行下载</p>
<p>返回<code>json</code>数据</p>
<h3>smm(name, withConfig)</h3>
<p><code>simple-mind-map</code>自定义的文件格式其实就是<code>json</code><code>json</code>方法返回的数据一模一样</p>
<h3>md()</h3>
<blockquote>
<p>v0.4.7+</p>
</blockquote>
<p>导出<code>markdown</code>文件返回<code>data:url</code>数据可以自行下载</p>
<p>导出<code>markdown</code>文件</p>
<h3>getSvgData()</h3>
<p>获取<code>svg</code>数据异步方法返回一个对象</p>
<pre class="hljs"><code>{
node<span class="hljs-comment">// svg节点</span>
str<span class="hljs-comment">// svg字符串</span>
}
</code></pre>
</div>
</template>

View File

@@ -86,6 +86,17 @@
综上,在开源的思维导图中,你很难找到一个比`simple-mind-map`更好的选择。当然,`simple-mind-map`也远远谈不上最好,它也有很多不足,如你在前面的【特别说明】所看到的那样,不过`simple-mind-map`一直处于快速迭代中,欢迎你加入进来一起完善它。
## 浏览器兼容性
推荐使用最新版`chrome`浏览器。
有限测试情况:
正常运行:`360`极速浏览器v13.5.2036.0)、`opera`浏览器v71.0.3770.284)。
非正常运行:`Firefox`v98.0.2),富文本模式下节点内容无法显示,如果要支持`Firefox`浏览器,请使用非富文本模式。
不支持:`IE`浏览器。
## License

View File

@@ -7,19 +7,19 @@
</blockquote>
<h2>特性</h2>
<ul>
<li><input type="checkbox" id="checkbox216" checked="true" /><label for="checkbox216">插件化架构除核心功能外其他功能作为插件提供按需使用减小整体体积</label></li>
<li><input type="checkbox" id="checkbox217" checked="true" /><label for="checkbox217">支持逻辑结构图思维导图组织结构图目录组织图时间轴鱼骨图六种结构</label></li>
<li><input type="checkbox" id="checkbox218" checked="true" /><label for="checkbox218">内置多种主题允许高度自定义样式支持注册新主题</label></li>
<li><input type="checkbox" id="checkbox219" checked="true" /><label for="checkbox219">支持快捷键</label></li>
<li><input type="checkbox" id="checkbox220" checked="true" /><label for="checkbox220">节点内容支持图片图标超链接备注标签概要</label></li>
<li><input type="checkbox" id="checkbox221" checked="true" /><label for="checkbox221">支持前进后退</label></li>
<li><input type="checkbox" id="checkbox222" checked="true" /><label for="checkbox222">支持拖动缩放</label></li>
<li><input type="checkbox" id="checkbox223" checked="true" /><label for="checkbox223">支持右键和Ctrl+左键两种多选方式</label></li>
<li><input type="checkbox" id="checkbox224" checked="true" /><label for="checkbox224">支持节点自由拖拽拖拽调整</label></li>
<li><input type="checkbox" id="checkbox225" checked="true" /><label for="checkbox225">支持多种节点形状</label></li>
<li><input type="checkbox" id="checkbox226" checked="true" /><label for="checkbox226">支持导出为</label><code>json</code><code>png</code><code>svg</code><code>pdf</code><code>markdown</code>支持从<code>json</code><code>xmind</code><code>markdown</code>导入</li>
<li><input type="checkbox" id="checkbox227" checked="true" /><label for="checkbox227">支持小地图支持水印</label></li>
<li><input type="checkbox" id="checkbox228" checked="true" /><label for="checkbox228">支持关联线</label></li>
<li><input type="checkbox" id="checkbox180" checked="true" /><label for="checkbox180">插件化架构除核心功能外其他功能作为插件提供按需使用减小整体体积</label></li>
<li><input type="checkbox" id="checkbox181" checked="true" /><label for="checkbox181">支持逻辑结构图思维导图组织结构图目录组织图时间轴鱼骨图六种结构</label></li>
<li><input type="checkbox" id="checkbox182" checked="true" /><label for="checkbox182">内置多种主题允许高度自定义样式支持注册新主题</label></li>
<li><input type="checkbox" id="checkbox183" checked="true" /><label for="checkbox183">支持快捷键</label></li>
<li><input type="checkbox" id="checkbox184" checked="true" /><label for="checkbox184">节点内容支持图片图标超链接备注标签概要</label></li>
<li><input type="checkbox" id="checkbox185" checked="true" /><label for="checkbox185">支持前进后退</label></li>
<li><input type="checkbox" id="checkbox186" checked="true" /><label for="checkbox186">支持拖动缩放</label></li>
<li><input type="checkbox" id="checkbox187" checked="true" /><label for="checkbox187">支持右键和Ctrl+左键两种多选方式</label></li>
<li><input type="checkbox" id="checkbox188" checked="true" /><label for="checkbox188">支持节点自由拖拽拖拽调整</label></li>
<li><input type="checkbox" id="checkbox189" checked="true" /><label for="checkbox189">支持多种节点形状</label></li>
<li><input type="checkbox" id="checkbox190" checked="true" /><label for="checkbox190">支持导出为</label><code>json</code><code>png</code><code>svg</code><code>pdf</code><code>markdown</code>支持从<code>json</code><code>xmind</code><code>markdown</code>导入</li>
<li><input type="checkbox" id="checkbox191" checked="true" /><label for="checkbox191">支持小地图支持水印</label></li>
<li><input type="checkbox" id="checkbox192" checked="true" /><label for="checkbox192">支持关联线</label></li>
</ul>
<h2>仓库目录介绍</h2>
<p>1.<code>simple-mind-map</code></p>
@@ -27,11 +27,11 @@
<p>2.<code>web</code></p>
<p>使用<code>simple-mind-map</code>基于<code>vue2.x</code><code>ElementUI</code>搭建的在线思维导图特性</p>
<ul>
<li><input type="checkbox" id="checkbox229" checked="true" /><label for="checkbox229">工具栏支持插入节点删除节点编辑节点图片图标超链接备注标签概要</label></li>
<li><input type="checkbox" id="checkbox230" checked="true" /><label for="checkbox230">侧边栏基础样式设置面板节点样式设置面板大纲面板主题选择面板结构选择面板</label></li>
<li><input type="checkbox" id="checkbox231" checked="true" /><label for="checkbox231">导入导出功能数据默认保存在浏览器本地存储也支持直接创建打开编辑电脑本地文件</label></li>
<li><input type="checkbox" id="checkbox232" checked="true" /><label for="checkbox232">右键菜单支持展开收起整理布局等操作</label></li>
<li><input type="checkbox" id="checkbox233" checked="true" /><label for="checkbox233">底部栏支持节点数量字数统计支持切换编辑和只读模式支持放大缩小支持全屏切换支持小地图</label></li>
<li><input type="checkbox" id="checkbox193" checked="true" /><label for="checkbox193">工具栏支持插入节点删除节点编辑节点图片图标超链接备注标签概要</label></li>
<li><input type="checkbox" id="checkbox194" checked="true" /><label for="checkbox194">侧边栏基础样式设置面板节点样式设置面板大纲面板主题选择面板结构选择面板</label></li>
<li><input type="checkbox" id="checkbox195" checked="true" /><label for="checkbox195">导入导出功能数据默认保存在浏览器本地存储也支持直接创建打开编辑电脑本地文件</label></li>
<li><input type="checkbox" id="checkbox196" checked="true" /><label for="checkbox196">右键菜单支持展开收起整理布局等操作</label></li>
<li><input type="checkbox" id="checkbox197" checked="true" /><label for="checkbox197">底部栏支持节点数量字数统计支持切换编辑和只读模式支持放大缩小支持全屏切换支持小地图</label></li>
</ul>
<p>提供文档页面服务</p>
<p>3.<code>dist</code></p>
@@ -58,6 +58,12 @@
<p>3.<a href="https://github.com/hizzgdev/jsmind">jsmind</a><a href="https://github.com/ssshooter/mind-elixir-core">Mind-elixir</a><a href="https://github.com/ondras/my-mind">my-mind</a><a href="https://github.com/awehook/blink-mind">blink-mind</a><a href="https://github.com/luvsic3/remind">remind</a><a href="https://github.com/hellowuxin/vue3-mindmap">vue3-mindmap</a><a href="https://github.com/zyascend/ZMindMap">ZMindMap</a>...</p>
<p>这些开源的思维导图也都不错各有各的特点但是它们也都有一定缺点比如停止更新界面美观度一般功能比较少依赖某个框架等等</p>
<p>综上在开源的思维导图中你很难找到一个比<code>simple-mind-map</code>更好的选择当然<code>simple-mind-map</code>也远远谈不上最好它也有很多不足如你在前面的特别说明所看到的那样不过<code>simple-mind-map</code>一直处于快速迭代中欢迎你加入进来一起完善它</p>
<h2>浏览器兼容性</h2>
<p>推荐使用最新版<code>chrome</code>浏览器</p>
<p>有限测试情况</p>
<p>正常运行<code>360</code>极速浏览器v13.5.2036.0<code>opera</code>浏览器v71.0.3770.284</p>
<p>非正常运行<code>Firefox</code>v98.0.2富文本模式下节点内容无法显示如果要支持<code>Firefox</code>浏览器请使用非富文本模式</p>
<p>不支持<code>IE</code>浏览器</p>
<h2>License</h2>
<p><a href="https://opensource.org/licenses/MIT">MIT</a></p>
<h2>请作者喝杯咖啡</h2>

View File

@@ -1,6 +1,10 @@
# 参与翻译
感谢[Emircan ERKUL](https://github.com/emircanerkul)提供的第一版英文翻译。
> 感谢[Emircan ERKUL](https://github.com/emircanerkul)提供的第一版英文翻译。
>
>因为精力有限,目前大部分翻译都是使用机翻的,所以准确度难免有问题。
>
>目前【教程】部分是没有进行翻译的,如果你有兴趣,欢迎加入我们。
如果你也想参与翻译本文档的话,可以先克隆本仓库。

View File

@@ -1,7 +1,11 @@
<template>
<div>
<h1>参与翻译</h1>
<blockquote>
<p>感谢<a href="https://github.com/emircanerkul">Emircan ERKUL</a>提供的第一版英文翻译</p>
<p>因为精力有限目前大部分翻译都是使用机翻的所以准确度难免有问题</p>
<p>目前教程部分是没有进行翻译的如果你有兴趣欢迎加入我们</p>
</blockquote>
<p>如果你也想参与翻译本文档的话可以先克隆本仓库</p>
<p>翻译的文档在<code>/web/src/pages/Doc/</code>目录下目前支持英文(<code>en</code>)简体中文(<code>zh</code>)两种语言</p>
<p>如果是新增一种语言类型那么可以在<code>/web/src/pages/Doc/</code>目录下创建一个新目录然后给每个章节创建一个文件夹你也可以直接复制已存在的语言目录下的所有章节目录进行翻译注意你只需要编写<code>index.md</code>文件章节目录下的<code>index.vue</code>文件是脚本根据<code>index.md</code>自动生成的</p>

View File

@@ -124,6 +124,16 @@ copyNodeTree({}, node)
{ width, height }
```
#### getTextFromHtml(html)
提取html字符串里的纯文本内容。
#### readBlob(blob)
> v0.5.9+
`blob`数据转成`data:url`数据。
## 在canvas中模拟css的背景属性
引入:
@@ -153,3 +163,49 @@ drawBackgroundImageToCanvas(ctx, width, height, img, {
}
})
```
## LRU缓存类
> v0.5.10+
引入:
```js
import Lru from 'simple-mind-map/src/utils/Lru.js'
```
### 构造函数
```js
let lru = new Lru(max)
```
`max`:指定最大缓存数量。
### 实例属性
#### size
当前缓存的数量。
#### pool
获取缓存池。
### 实例方法
#### add(key, value)
添加缓存。
#### delete(key)
删除指定缓存。
#### has(key)
检查某个缓存是否存在。
#### get(key)
获取某个缓存的值。

View File

@@ -77,6 +77,13 @@
<p>测量文本的宽高返回值</p>
<pre class="hljs"><code>{ width, height }
</code></pre>
<h4>getTextFromHtml(html)</h4>
<p>提取html字符串里的纯文本内容</p>
<h4>readBlob(blob)</h4>
<blockquote>
<p>v0.5.9+</p>
</blockquote>
<p><code>blob</code>数据转成<code>data:url</code>数据</p>
<h2>在canvas中模拟css的背景属性</h2>
<p>引入</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> drawBackgroundImageToCanvas <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/utils/simulateCSSBackgroundInCanvas&#x27;</span>
@@ -100,6 +107,31 @@ drawBackgroundImageToCanvas(ctx, width, height, img, {
}
})
</code></pre>
<h2>LRU缓存类</h2>
<blockquote>
<p>v0.5.10+</p>
</blockquote>
<p>引入</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> Lru <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/utils/Lru.js&#x27;</span>
</code></pre>
<h3>构造函数</h3>
<pre class="hljs"><code><span class="hljs-keyword">let</span> lru = <span class="hljs-keyword">new</span> Lru(max)
</code></pre>
<p><code>max</code>指定最大缓存数量</p>
<h3>实例属性</h3>
<h4>size</h4>
<p>当前缓存的数量</p>
<h4>pool</h4>
<p>获取缓存池</p>
<h3>实例方法</h3>
<h4>add(key, value)</h4>
<p>添加缓存</p>
<h4>delete(key)</h4>
<p>删除指定缓存</p>
<h4>has(key)</h4>
<p>检查某个缓存是否存在</p>
<h4>get(key)</h4>
<p>获取某个缓存的值</p>
</div>
</template>

View File

@@ -295,6 +295,67 @@
</el-select>
</div>
</div>
<!-- 关联线文字 -->
<div class="title noTop">关联线文字</div>
<div class="row">
<div class="rowItem">
<span class="name">字体</span>
<el-select
size="mini"
v-model="style.associativeLineTextFontFamily"
placeholder=""
@change="update('associativeLineTextFontFamily', $event)"
>
<el-option
v-for="item in fontFamilyList"
:key="item.value"
:label="item.name"
:value="item.value"
:style="{ fontFamily: item.value }"
>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<div class="rowItem">
<span class="name">颜色</span>
<span
class="block"
v-popover:popover6
:style="{ backgroundColor: style.associativeLineTextColor }"
></span>
<el-popover ref="popover6" placement="bottom" trigger="click">
<Color
:color="style.associativeLineTextColor"
@change="
color => {
update('associativeLineTextColor', color)
}
"
></Color>
</el-popover>
</div>
<div class="rowItem">
<span class="name">字号</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.associativeLineTextFontSize"
placeholder=""
@change="update('associativeLineTextFontSize', $event)"
>
<el-option
v-for="item in fontSizeList"
:key="item"
:label="item"
:value="item"
:style="{ fontSize: item + 'px' }"
>
</el-option>
</el-select>
</div>
</div>
<!-- 节点边框风格 -->
<div class="title noTop">{{ $t('baseStyle.nodeBorderType') }}</div>
<div class="row">
@@ -546,7 +607,7 @@
<script>
import Sidebar from './Sidebar'
import Color from './Color'
import { lineWidthList, lineStyleList, backgroundRepeatList, backgroundPositionList, backgroundSizeList } from '@/config'
import { lineWidthList, lineStyleList, backgroundRepeatList, backgroundPositionList, backgroundSizeList, fontFamilyList, fontSizeList } from '@/config'
import ImgUpload from '@/components/ImgUpload'
import { storeConfig } from '@/api'
import { mapState, mapMutations } from 'vuex'
@@ -575,6 +636,7 @@ export default {
data() {
return {
lineWidthList,
fontSizeList,
activeTab: 'color',
marginActiveTab: 'second',
style: {
@@ -588,6 +650,9 @@ export default {
associativeLineWidth: 0,
associativeLineActiveWidth: 0,
associativeLineActiveColor: '',
associativeLineTextFontSize: 0,
associativeLineTextColor: '',
associativeLineTextFontFamily: '',
paddingX: 0,
paddingY: 0,
imgMaxWidth: 0,
@@ -636,6 +701,9 @@ export default {
backgroundSizeList() {
return backgroundSizeList[this.$i18n.locale] || backgroundSizeList.zh
},
fontFamilyList() {
return fontFamilyList[this.$i18n.locale] || fontFamilyList.zh
},
},
watch: {
activeSidebar(val) {
@@ -662,7 +730,7 @@ export default {
* @Desc: 初始样式
*/
initStyle() {
;[
[
'backgroundColor',
'lineWidth',
'lineStyle',
@@ -673,6 +741,9 @@ export default {
'associativeLineWidth',
'associativeLineActiveWidth',
'associativeLineActiveColor',
'associativeLineTextFontSize',
'associativeLineTextColor',
'associativeLineTextFontFamily',
'paddingX',
'paddingY',
'imgMaxWidth',
@@ -715,7 +786,7 @@ export default {
* @Desc: margin初始值
*/
initMarginStyle() {
;['marginX', 'marginY'].forEach(key => {
['marginX', 'marginY'].forEach(key => {
this.style[key] = this.mindMap.getThemeConfig()[this.marginActiveTab][
key
]

View File

@@ -1,5 +1,5 @@
<template>
<div class="editContainer">
<div class="editContainer" :style="{top: IS_ELECTRON ? '40px' : 0}">
<div class="mindMapContainer" ref="mindMapContainer"></div>
<Count v-if="!isZenMode"></Count>
<Navigator :mindMap="mindMap"></Navigator>
@@ -46,7 +46,7 @@ import { getData, storeData, storeConfig } from '@/api'
import Navigator from './Navigator.vue'
import NodeImgPreview from './NodeImgPreview.vue'
import SidebarTrigger from './SidebarTrigger.vue'
import { mapState } from 'vuex'
import { mapState, mapMutations } from 'vuex'
import customThemeList from '@/customThemes'
import icon from '@/config/icon'
@@ -93,11 +93,13 @@ export default {
mindMap: null,
mindMapData: null,
prevImg: '',
openTest: false
openTest: false,
isFirst: true
}
},
computed: {
...mapState({
fileName: state => state.fileName,
isZenMode: state => state.localConfig.isZenMode,
openNodeRichText: state => state.localConfig.openNodeRichText,
})
@@ -111,9 +113,9 @@ export default {
}
}
},
mounted() {
async mounted() {
// this.showNewFeatureInfo()
this.getData()
await this.getData()
this.init()
this.$bus.$on('execCommand', this.execCommand)
this.$bus.$on('paddingChange', this.onPaddingChange)
@@ -136,8 +138,13 @@ export default {
this.test()
}, 5000)
}
if (window.IS_ELECTRON) {
this.mindMap.keyCommand.addShortcut('Control+s', this.saveToLocal)
}
},
methods: {
...mapMutations(['setFileName', 'setIsUnSave']),
/**
* @Author: 王林25
* @Date: 2021-11-22 19:39:28
@@ -227,8 +234,16 @@ export default {
* @Date: 2021-07-03 22:11:37
* @Desc: 获取思维导图数据,实际应该调接口获取
*/
getData() {
let storeData = getData()
async getData() {
let data = await window.electronAPI.getFileContent(this.$route.params.id)
let storeData = null
if (data) {
this.setFileName(data.name)
storeData = data.content
} else {
this.setFileName('未命名')
storeData = getData()
}
this.mindMapData = storeData
},
@@ -242,9 +257,15 @@ export default {
return
}
this.$bus.$on('data_change', data => {
if (!this.isFirst) {
this.setIsUnSave(true)
} else {
this.isFirst = false
}
storeData(data)
})
this.$bus.$on('view_data_change', data => {
this.setIsUnSave(true)
storeConfig({
view: data
})
@@ -292,9 +313,9 @@ export default {
iconList: icon
})
if (this.openNodeRichText) this.addRichTextPlugin()
this.mindMap.keyCommand.addShortcut('Control+s', () => {
this.manualSave()
})
// this.mindMap.keyCommand.addShortcut('Control+s', () => {
// this.manualSave()
// })
// 转发事件
;[
'node_active',
@@ -409,6 +430,18 @@ export default {
// 移除节点富文本编辑插件
removeRichTextPlugin() {
this.mindMap.removePlugin(RichText)
},
// 保存到本地文件
async saveToLocal() {
let id = this.$route.params.id
let data = this.mindMap.getData(true)
console.log('保存', id, data)
let res = await window.electronAPI.save(id, JSON.stringify(data), this.fileName)
if (res) {
this.setFileName(res)
}
this.setIsUnSave(false)
}
}
}

View File

@@ -62,7 +62,6 @@
</div>
</div>
<div class="tip">{{ $t('export.tips') }}</div>
<div class="tip warning" v-if="openNodeRichText && exportType === 'svg' && domToImage">{{ $t('export.svgTips') }}</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="cancel">{{ $t('dialog.cancel') }}</el-button>

View File

@@ -36,9 +36,7 @@
<Fullscreen :mindMap="mindMap"></Fullscreen>
</div>
<div class="item">
<a href="https://github.com/wanglin2/mind-map" target="_blank">
<span class="iconfont icongithub"></span>
</a>
<span class="iconfont icongithub" @click="openGithub"></span>
</div>
</div>
</template>
@@ -89,6 +87,10 @@ export default {
onLangChange(lang) {
i18n.locale = lang
storeLang(lang)
},
openGithub() {
window.electronAPI.openUrl('https://github.com/wanglin2/mind-map')
}
}
}

View File

@@ -506,7 +506,7 @@ export default {
this.activeTab = 'normal'
return
}
;[
[
'shape',
'paddingX',
'paddingY',

View File

@@ -42,7 +42,7 @@ export default {
},
data() {
return {
themeList: [...themeList],// ...customThemeList
themeList: [...themeList].reverse(),// ...customThemeList
themeMap,
theme: ''
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="toolbarContainer">
<div class="toolbar">
<div class="toolbar" :style="{top: IS_ELECTRON ? '40px' : 0}">
<!-- 节点操作 -->
<div class="toolbarBlock">
<div
@@ -126,15 +126,15 @@
</div>
<!-- 导出 -->
<div class="toolbarBlock">
<div class="toolbarBtn" @click="createNewLocalFile">
<div class="toolbarBtn" @click="createNewLocalFile" v-if="!IS_ELECTRON">
<span class="icon iconfont iconxinjian"></span>
<span class="text">{{ $t('toolbar.newFile') }}</span>
</div>
<div class="toolbarBtn" @click="openLocalFile">
<div class="toolbarBtn" @click="openLocalFile" v-if="!IS_ELECTRON">
<span class="icon iconfont icondakai"></span>
<span class="text">{{ $t('toolbar.openFile') }}</span>
</div>
<div class="toolbarBtn" @click="saveLocalFile">
<div class="toolbarBtn" @click="saveLocalFile" v-if="!IS_ELECTRON">
<span class="icon iconfont iconlingcunwei"></span>
<span class="text">{{ $t('toolbar.saveAs') }}</span>
</div>

View File

@@ -0,0 +1,32 @@
<template>
<div class="workbencheContainer">
<div class="workbencheContent">
<router-view></router-view>
</div>
</div>
</template>
<script>
export default {
name: 'Workbenche',
created () {
document.title = '思绪思维导图'
}
}
</script>
<style lang="less" scoped>
.workbencheContainer {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.workbencheContent {
flex-grow: 1;
}
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<el-dialog
class="aboutDialog"
title="关于"
:visible.sync="dialogVisible"
width="480px"
@close="onClose"
>
<div class="aboutBox">
<img src="../../../assets/img/icon.png" alt="" />
<h2>思绪思维导图</h2>
<p>版本{{ version }}</p>
<p>
获取源码<a href="https://github.com/wanglin2/mind-map/tree/electron"
>mind-map</a
>
</p>
<p>
下载最新版本<a href="https://github.com/wanglin2/mind-map/releases"
>releases</a
>
</p>
</div>
</el-dialog>
</template>
<script>
import pkg from '../../../../package.json'
export default {
model: {
prop: 'value',
event: 'change'
},
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
dialogVisible: false,
version: pkg.version
}
},
watch: {
value(val, oldVal) {
this.dialogVisible = val
}
},
methods: {
onClose() {
this.$emit('change', false)
}
}
}
</script>
<style lang="less" scoped>
.aboutDialog {
/deep/ .el-dialog__body {
padding: 0;
}
}
.aboutBox {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-bottom: 30px;
img {
width: 100px;
height: 100px;
}
h2 {
margin-bottom: 20px;
}
p {
margin-bottom: 10px;
a {
text-decoration: none;
color: #409eff;
}
}
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<div class="workbencheEmptyContainer">
<div class="icon iconfont iconwushuju"></div>
<div class="tip">暂时没有内容</div>
<div class="desc">没有最近使用的文件记录</div>
<div class="createBtn" @click="create">立即创建</div>
</div>
</template>
<script>
import { create } from '../utils'
export default {
methods: {
create
}
}
</script>
<style lang="less" scoped>
.workbencheEmptyContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.icon {
font-size: 100px;
margin-bottom: 20px;
}
.tip {
font-weight: bold;
margin-bottom: 10px;
}
.desc {
font-size: 12px;
margin-bottom: 20px;
}
.createBtn {
width: 100px;
height: 30px;
border-radius: 5px;
border: 1px solid #409eff;
color: #409eff;
font-size: 14px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
user-select: none;
&:hover {
background-color: rgba(64, 158, 255, 0.3);
}
}
}
</style>

View File

@@ -0,0 +1,165 @@
<template>
<div class="workbencheFileListContainer">
<div class="title">
<span>最近</span>
<span class="clearBtn" @click="clear">清空</span>
</div>
<div class="fileListBox">
<Empty v-if="list.length <= 0"></Empty>
<el-table v-else :data="list" style="width: 100%">
<el-table-column prop="name" label="名称"> </el-table-column>
<el-table-column prop="url" label="文件路径"> </el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button
icon="el-icon-edit"
circle
size="mini"
@click="openFile(scope.row.url)"
></el-button>
<el-button
icon="el-icon-document-copy"
circle
size="mini"
@click="copyFile(scope.row.url)"
></el-button>
<el-button
type="danger"
icon="el-icon-delete"
circle
size="mini"
@click="deleteFile(scope.row.url, scope.$index)"
></el-button>
<el-button
icon="el-icon-folder-opened"
circle
size="mini"
@click="openFileInDir(scope.row.url)"
></el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import Empty from '../components/Empty.vue'
export default {
components: {
Empty
},
data() {
return {
list: []
}
},
created() {
this.getRecentFileList()
window.electronAPI.onRefreshRecentFileList(() => {
this.getRecentFileList()
})
},
methods: {
// 获取最近文件列表
async getRecentFileList() {
let list = await window.electronAPI.getRecentFileList()
this.list = list.reverse()
},
// 在文件夹里打开文件
openFileInDir(file) {
window.electronAPI.openFileInDir(file)
},
// 删除文件
deleteFile(file, index) {
this.$confirm('确定删除该文件?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
let res = await window.electronAPI.deleteFile(file)
if (res) {
this.$message.error('删除失败')
} else {
this.list.splice(index, 1)
this.$message.success('删除成功')
}
})
.catch(() => {})
},
// 编辑文件
openFile(file) {
window.electronAPI.openFile(file)
},
// 清空最近文件列表
clear() {
this.$confirm('确定清空最近文件?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
let res = await window.electronAPI.clearRecentFileList()
if (res) {
this.$message.error('清空失败')
} else {
this.list = []
this.$message.success('清空成功')
}
})
.catch(() => {})
},
// 复制文件
async copyFile(file) {
try {
window.electronAPI.copyFile(file)
this.$message.success('复制成功')
} catch (error) {
this.$message.error('复制失败')
}
}
}
}
</script>
<style lang="less" scoped>
.workbencheFileListContainer {
flex-grow: 1;
height: 100%;
background-color: #fff;
border-radius: 10px;
padding: 20px;
padding-top: 0;
display: flex;
flex-direction: column;
.title {
font-weight: bold;
font-size: 18px;
border-bottom: 1px solid #e4e7ed;
height: 65px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
.clearBtn {
cursor: pointer;
font-size: 14px;
color: #409eff;
}
}
.fileListBox {
flex-grow: 1;
overflow: hidden;
}
}
</style>

View File

@@ -0,0 +1,15 @@
<template>
<div class="macControl" v-if="IS_MAC"></div>
</template>
<script>
export default {}
</script>
<style lang="less" scoped>
.macControl {
width: 100px;
height: 100%;
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div class="workbencheSidebarContainer">
<div class="createBtn" @click="create">开始新建</div>
<div class="line"></div>
<div class="btn" @click="openLocalFile">
<span class="icon iconfont icondakai"></span>
<span class="text">打开本地文件</span>
</div>
<div class="btn active">
<span class="icon iconfont iconzuijinliulan"></span>
<span class="text">最近文件</span>
</div>
</div>
</template>
<script>
import { create } from '../utils'
export default {
methods: {
create,
openLocalFile() {
window.electronAPI.selectOpenFile()
}
}
}
</script>
<style lang="less" scoped>
.workbencheSidebarContainer {
flex-shrink: 0;
width: 200px;
height: 100%;
background-color: #fff;
border-radius: 10px;
margin-right: 20px;
padding: 15px 20px;
.createBtn {
width: 100%;
height: 30px;
background-color: #409eff;
border-radius: 5px;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
font-size: 14px;
user-select: none;
&:hover {
opacity: 0.9;
}
}
.line {
width: 100%;
height: 1px;
background-color: #e4e7ed;
margin: 20px 0;
}
.btn {
width: 100%;
height: 30px;
background-color: #fff;
border-radius: 5px;
color: #000;
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
user-select: none;
padding: 0 10px;
margin-bottom: 10px;
&.active,
&:hover {
background-color: rgba(64, 158, 255, 0.3);
color: rgba(64, 158, 255, 1);
}
.icon {
margin-right: 10px;
}
}
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div class="winControl noDrag" v-if="IS_WIN">
<div class="winControlBtn iconfont iconzuixiaohua" @click="minimize"></div>
<div
class="winControlBtn iconfont"
:class="[isMaximize ? 'icon3zuidahua-3' : 'iconzuidahua']"
@click="toggleMaximize"
></div>
<div class="winControlBtn iconfont iconguanbi" @click="close"></div>
</div>
</template>
<script>
export default {
data() {
return {
isMaximize: false
}
},
methods: {
minimize() {
window.electronAPI.minimize()
},
toggleMaximize() {
if (this.isMaximize) {
this.isMaximize = false
window.electronAPI.unmaximize()
} else {
this.isMaximize = true
window.electronAPI.maximize()
}
},
close() {
window.electronAPI.close()
}
}
}
</script>
<style lang="less" scoped>
.winControl {
display: flex;
align-items: center;
flex-shrink: 0;
height: 100%;
.winControlBtn {
width: 40px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background-color: #ccced1;
}
&.iconguanbi {
&:hover {
background-color: #e81123;
color: #fff;
}
}
}
}
</style>

View File

@@ -0,0 +1,6 @@
import { v4 as uuid } from 'uuid'
// 打开新的编辑窗口
export const create = () => {
window.electronAPI.create(uuid())
}

View File

@@ -0,0 +1,134 @@
<template>
<div class="workbencheEditContainer">
<div class="workbencheEditHeader">
<MacControl></MacControl>
<WinControl></WinControl>
<div class="inputBox">
<el-input
v-model="name"
size="mini"
placeholder=""
@blur="rename"
@keyup.enter="rename"
></el-input>
<div class="modifyDotBox">
<div class="modifyDot" v-show="isUnSave"></div>
</div>
</div>
</div>
<Edit></Edit>
</div>
</template>
<script>
import Edit from '../../Edit/Index.vue'
import WinControl from '../components/WinControl.vue'
import MacControl from '../components/MacControl.vue'
import { mapState, mapMutations } from 'vuex'
export default {
components: {
Edit,
MacControl,
WinControl
},
data() {
return {
name: ''
}
},
computed: {
...mapState(['fileName', 'isUnSave'])
},
watch: {
fileName(val) {
this.name = val
document.title = val
},
name(val) {
if (!val.trim()) return
this.setFileName(val.trim())
}
},
created() {
window.onbeforeunload = async e => {
e.returnValue = false
// 没有未保存内容直接关闭
if (!this.isUnSave) {
window.electronAPI.destroy()
} else {
try {
// 否则询问用户是否关闭
await this.checkIsClose()
window.electronAPI.destroy()
} catch (error) {}
}
}
},
methods: {
...mapMutations(['setFileName']),
// 重命名文件
rename() {
let id = this.$route.params.id
window.electronAPI.rename(id, this.name.trim())
},
// 询问是否关闭页面
checkIsClose() {
return new Promise((resolve, reject) => {
this.$confirm('有操作尚未保存,是否确认关闭?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
resolve()
})
.catch(() => {
reject()
})
})
}
}
}
</script>
<style lang="less" scoped>
.workbencheEditContainer {
.workbencheEditHeader {
position: relative;
width: 100%;
height: 40px;
background-color: #ebeef1;
-webkit-app-region: drag;
display: flex;
align-items: center;
flex-shrink: 0;
.inputBox {
-webkit-app-region: no-drag;
position: absolute;
height: 100%;
left: 50%;
transform: translateX(-50%);
top: 0;
display: flex;
align-items: center;
.modifyDotBox {
width: 10px;
height: 10px;
margin-left: 10px;
.modifyDot {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #409eff;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<div
class="workbencheHomeContainer"
@drop="onDrop"
@dragenter="onDragenter"
@dragover="onDragover"
@dragleave="onDragleave"
>
<div class="workbencheHomeHeader">
<MacControl></MacControl>
<WinControl></WinControl>
<div class="rightBar">
<el-dropdown @command="handleCommand">
<span class="settingBtn el-icon-setting"></span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="about">关于软件</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<div class="workbencheHomeContent">
<Sidebar></Sidebar>
<FileList></FileList>
</div>
<AboutDialog v-model="showAboutDialog"></AboutDialog>
</div>
</template>
<script>
import WinControl from '../components/WinControl.vue'
import MacControl from '../components/MacControl.vue'
import Sidebar from '../components/Sidebar.vue'
import FileList from '../components/FileList.vue'
import AboutDialog from '../components/AboutDialog.vue'
export default {
components: {
WinControl,
MacControl,
Sidebar,
FileList,
AboutDialog
},
data() {
return {
showAboutDialog: false
}
},
methods: {
handleCommand(command) {
switch (command) {
case 'about':
this.showAboutDialog = true
break
default:
break
}
},
onDrop(e) {
e.preventDefault()
e.stopPropagation()
let df = e.dataTransfer
let dropFiles = []
if (df.items !== undefined) {
for (let i = 0; i < df.items.length; i++) {
let item = df.items[i]
if (item.kind === 'file' && item.webkitGetAsEntry().isFile) {
let file = item.getAsFile()
if (/\.smm$/.test(file.name)) {
dropFiles.push(file)
}
}
}
}
if (dropFiles.length === 1) {
// 如果只有一个文件,直接打开编辑
window.electronAPI.openFile(dropFiles[0].path)
} else if (dropFiles.length > 1) {
// 否则添加到最近文件列表
window.electronAPI.addRecentFileList(
dropFiles.map(file => {
return file.path
})
)
}
},
onDragenter(e) {
e.preventDefault()
e.stopPropagation()
},
onDragover(e) {
e.preventDefault()
e.stopPropagation()
},
onDragleave(e) {
e.preventDefault()
e.stopPropagation()
}
}
}
</script>
<style lang="less" scoped>
.workbencheHomeContainer {
background-color: #f6f8f9;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.workbencheHomeHeader {
position: relative;
width: 100%;
height: 40px;
background-color: #ebeef1;
-webkit-app-region: drag;
display: flex;
align-items: center;
flex-shrink: 0;
.rightBar {
-webkit-app-region: no-drag;
position: absolute;
right: 20px;
top: 0;
height: 100%;
display: flex;
align-items: center;
.settingBtn {
font-size: 20px;
cursor: pointer;
}
}
}
.workbencheHomeContent {
flex-grow: 1;
padding: 20px;
display: flex;
overflow: hidden;
}
}
</style>

View File

@@ -3,29 +3,69 @@ import VueRouter from 'vue-router'
import EditPage from '@/pages/Edit/Index'
import DocPage from '@/pages/Doc/Index'
import routerList from '@/pages/Doc/routerList'
import WorkbenchePage from '@/pages/Workbenche/Index'
import WorkbencheHomePage from '@/pages/Workbenche/views/Home'
import WorkbencheEditPage from '@/pages/Workbenche/views/Edit'
// 处理没有翻译的章节路由
const handleRouterList = () => {
let zhList = routerList[0].children
for(let i = 1; i < routerList.length; i++) {
let list = routerList[i].children
zhList.forEach(item => {
if (!list.find((item2) => {
return item2.path === item.path
})) {
list.push({
...item,
lang: 'zh'
})
}
})
}
}
handleRouterList()
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Edit',
component: EditPage
{
path: '/',
name: 'Edit',
component: EditPage
},
{
path: '/workbenche',
name: 'Workbenche',
component: WorkbenchePage,
redirect: '/workbenche/home',
children: [
{
path: 'home',
name: 'WorkbencheHome',
component: WorkbencheHomePage,
},
{
path: 'edit/:id',
name: 'WorkbencheEdit',
component: WorkbencheEditPage,
}
]
},
...routerList.map((item) => {
return {
path: `/doc/${item.lang}/`,
return {
path: `/doc/${item.lang}/`,
redirect: `/doc/${item.lang}/introduction/`
}
}),
...routerList.map((item) => {
return {
path: `/doc/${item.lang}/`,
component: DocPage,
return {
path: `/doc/${item.lang}/`,
component: DocPage,
children: item.children.map((child) => {
return {
path: `${child.path}/:h?`,
component: () => import(`./pages/Doc/${item.lang}/${child.path}/index.vue`)
component: () => import(`./pages/Doc/${child.lang || item.lang}/${child.path}/index.vue`)
}
})
}

View File

@@ -7,6 +7,8 @@ Vue.use(Vuex)
const store = new Vuex.Store({
state: {
fileName: '',// 本地的文件名
isUnSave: false,// 当前操作是否未保存
mindMapData: null, // 思维导图数据
isHandleLocalFile: false, // 是否操作的是本地文件
localConfig: {
@@ -15,9 +17,20 @@ const store = new Vuex.Store({
// 是否开启节点富文本
openNodeRichText: true
},
activeSidebar: '' // 当前显示的侧边栏
activeSidebar: '', // 当前显示的侧边栏
localEditList: []// 客户端中正在编辑的思维导图列表
},
mutations: {
// 设置本地文件名
setFileName(state, data) {
state.fileName = data
},
// 设置当前操作是否未保存
setIsUnSave(state, data) {
state.isUnSave = data
},
/**
* @Author: 王林
* @Date: 2021-04-10 14:50:01
@@ -59,6 +72,11 @@ const store = new Vuex.Store({
*/
setActiveSidebar(state, data) {
state.activeSidebar = data
},
// 设置客户端中当前正在编辑的思维导图列表
setLocalEditList(state, list) {
state.localEditList = list
}
},
actions: {

View File

@@ -1,16 +1,119 @@
const path = require('path');
const path = require('path')
const isDev = process.env.NODE_ENV === 'development'
module.exports = {
publicPath: isDev ? '' : './dist',
outputDir: '../dist',
lintOnSave: false,
productionSourceMap: false,
configureWebpack: {
resolve: {
alias: {
'@': path.resolve(__dirname, './src/')
}
}
publicPath: isDev ? '' : './dist',
outputDir: '../dist',
lintOnSave: false,
productionSourceMap: false,
configureWebpack: {
resolve: {
alias: {
'@': path.resolve(__dirname, './src/')
}
}
}
},
pluginOptions: {
electronBuilder: {
preload: 'src/electron/preload.js',
builderOptions: {
productName: '思绪思维导图',
copyright: 'Copyright © 思绪思维导图',
// compression: "maximum", // 机器好的可以打开,配置压缩,开启后会让 .AppImage 格式的客户端启动缓慢
asar: true,
fileAssociations: [
{
ext: 'smm',
name: 'mind map file',
role: 'Editor',
icon: './build/icons/icon.ico'
}
],
publish: [
{
provider: 'github',
owner: 'wanglin2',
repo: 'mind-map',
vPrefixedTagName: true,
releaseType: 'draft'
}
],
directories: {
output: 'dist_electron'
},
mac: {
target: [
{
target: 'dmg',
arch: ['x64', 'arm64', 'universal']
}
],
artifactName: '${productName}-${os}-${version}-${arch}.${ext}',
category: 'public.app-category.utilities',
darkModeSupport: true
},
win: {
target: [
{
target: 'portable',
arch: ['x64']
},
{
target: 'nsis',
arch: ['x64']
}
],
publisherName: '思绪思维导图',
icon: 'build/icons/icon.ico',
publish: ['github']
},
linux: {
target: [
{
target: 'AppImage',
arch: ['x64']
},
{
target: 'tar.gz',
arch: ['x64', 'arm64']
},
{
target: 'deb',
arch: ['x64', 'armv7l', 'arm64']
},
{
target: 'rpm',
arch: ['x64']
},
{
target: 'snap',
arch: ['x64']
},
{
target: 'pacman',
arch: ['x64']
}
],
category: 'Utilities',
icon: './build/icon.icns'
},
dmg: {
icon: 'build/icons/icon.icns'
},
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true,
perMachine: true,
deleteAppDataOnUninstall: true
}
},
// 渲染线程的配置文件
chainWebpackRendererProcess: config => {
config.plugin('define').tap(args => {
args[0]['IS_ELECTRON'] = true
return args
})
}
}
}
}