1. 简介
个性化脑图工具是一款具有图形化界面的工具,用户可以通过拖拽节点的形式,创建自己的思维导图并进行存储、编辑、分享等操作。本文将介绍一个利用PHP和Vue结合开发的个性化脑图工具,在实现基本功能的基础上,还包含了一些自定义和高级功能。
2. 技术选型
2.1 PHP后端
本文使用PHP作为后端语言,主要原因是PHP易于学习上手,具有良好的扩展性和跨平台性。
/**
* 获取节点信息
* @param int $id 节点id
* @access public
* @return array 节点信息
*/
public function getNode(int $id): array
{
//todo
}
上述代码为PHP中获取节点信息的函数,使用了type hinting增强了类型约束,增加了代码的健壮性。
2.2 Vue前端
本文使用Vue.js作为前端框架,主要原因是Vue.js轻量且易于学习,采用了组件化的思想,方便维护和扩展。
// 定义节点组件
Vue.component('node', {
props: {
title: {
type: String,
required: true,
default: 'New Node'
},
x: {
type: Number,
required: true,
default: 0
},
y: {
type: Number,
required: true,
default: 0
}
},
template: `
{{ title }}
`
})
上述代码为Vue中定义节点组件的代码,使用了props接收父组件传递的数据,在template中使用了绑定语法将组件绑定到节点上。
3. 基本功能
3.1 创建节点
用户可以通过鼠标左键点击空白处来创建新节点,节点会自动定位到鼠标位置,并且可以自定义节点的名称。
用户添加新节点的方法为单击空白处——>输入名称——>按下回车键,下面是实现此功能的代码:
// 添加鼠标左键点击事件
container.addEventListener('click', event => {
if (event.button !== 0) return // 只处理鼠标左键事件
const node = new Node({
x: event.clientX,
y: event.clientY,
title: prompt('Please input node title')
})
nodes.push(node)
})
3.2 连接节点
用户可以通过拖拽鼠标左键连接两个节点,并且可以自定义连线的类型和样式。
用户连接两个节点的方法为单击起点节点——>拖拽到终点节点——>松开鼠标左键,下面是实现此功能的代码:
// 添加起点节点选中事件
container.addEventListener('mousedown', event => {
if (event.button !== 0) return // 只处理鼠标左键事件
const node = getClickedNode(event)
if (node) {
startLinkNode = node
isLinking = true
event.preventDefault() // 防止浏览器默认选中
}
})
// 添加鼠标移动事件
container.addEventListener('mousemove', event => {
if (isLinking) {
context.beginPath()
context.moveTo(startLinkNode.x, startLinkNode.y)
context.lineTo(event.clientX, event.clientY)
context.stroke()
}
})
// 添加终点节点选中事件
container.addEventListener('mouseup', event => {
if (event.button !== 0) return // 只处理鼠标左键事件
if (!isLinking || event.target.tagName.toUpperCase() !== 'CANVAS') return
const endNode = getClickedNode(event)
if (endNode) {
const promptResult = prompt('Please input the link type (default: link)', 'link') || 'link'
const link = new Link({
start: startLinkNode,
end: endNode,
type: promptResult,
style: {
strokeStyle: 'blue',
lineWidth: 2
}
})
}
})
上述代码中主要使用了Canvas画布来绘制连线的效果。
3.3 存储图谱
用户可以在本地保存已经创建好的图谱,下次打开后可以直接加载到已经保存的图谱。
用户存储图谱的方法为单击顶部工具栏中的保存按钮,下面是实现此功能的代码:
// 添加保存按钮事件
const saveButton = document.getElementById('saveButton')
saveButton.addEventListener('click', event => {
const data = {
nodes: nodes,
links: links
}
localStorage.setItem('mindmap', JSON.stringify(data))
})
上述代码中,使用了localStorage将图谱信息存储到本地,下次打开时可以在localStorage中查找是否有对应信息,如果有则加载。
4. 自定义功能
4.1 鼠标滚轮缩放
用户可以通过鼠标滚轮来缩放图谱,方便查看和编辑。同时,用户可以通过顶部工具栏中的放大、缩小按钮来实现同样效果。
实现鼠标滚轮缩放的代码如下:
// 添加鼠标滚轮事件
container.addEventListener('wheel', event => {
event.preventDefault() // 防止页面滚动
const scaleDelta = event.wheelDelta > 0 ? zoomScale : 1 / zoomScale
scale *= scaleDelta
})
上述代码中,使用了scale变量记录当前缩放比例,zoomScale表示存储的缩放倍数,防止继续缩放或放大超过常规使用范围。
4.2 节点隐藏/显示
用户可以选择特定的节点或节点分支,隐藏其下方的节点;在需要时再次显示。
实现节点隐藏/显示的代码如下:
// 添加节点选中事件
container.addEventListener('click', event => {
if (event.button !== 0) return // 只处理鼠标左键事件
const node = getClickedNode(event)
if (node) {
node.toggle()
}
})
// 定义节点类的toggle函数
class Node {
// ...
toggle() {
this.children.forEach(child => child.toggle())
this.isHidden = !this.isHidden
}
}
上述代码中,toggle函数可以递归隐藏/显示节点,isHidden是存储该节点是否隐藏的标记。
5. 高级功能
5.1 打印图谱
用户可以通过顶部工具栏中的“打印”按钮将图谱内容输出为纸质版。
实现打印图谱功能的代码如下:
// 添加打印按钮事件
const printButton = document.getElementById('printButton')
printButton.addEventListener('click', event => {
const dataUrl = canvas.toDataURL()
const printWindow = window.open('', '_blank')
})
上述代码中,使用了Canvas的toDataURL方法将画布内容转为图片并输出到打印窗口中,同时通过window.print()方法实现打印机打印。使用了window.open()打开了一个新窗口,显示打印内容。
5.2 复制/剪切/粘贴节点、分支
用户可以将特定节点或节点分支进行复制、剪切、粘贴功能。
实现此功能的代码如下:
// 定义复制节点类
class ClipboardNode {
constructor(source) {
this.source = source
this.target = null
this.nodes = []
this.links = []
}
// 复制节点(禁止递归遍历)
prepare() {
const nodesIdMap = {}
// 收集子节点和父节点
const getNodes = node => {
nodesIdMap[node.id] = true
this.nodes.push(node.clone())
node.links.forEach(link => {
if (!nodesIdMap[link.end.id]) {
this.links.push(link.clone())
getNodes(link.end)
}
})
}
getNodes(this.source)
}
// 粘贴节点
paste(position) {
if (!this.target) {
// 新增节点
this.nodes.forEach(node => {
node.offset(position.x, position.y)
nodes.push(node)
})
this.links.forEach(link => {
link.relation = nodes
links.push(link)
})
} else {
// 替换节点
let idMap = {}
let newNodes = []
let newLinks = []
const getNodes = (node, parent) => {
const newNode = node.clone()
idMap[node.id] = newNode.id
newNode.offset(position.x, position.y)
newNode.parent = parent
newNodes.push(newNode)
node.links.forEach(link => {
if (!idMap[link.end.id]) {
newLinks.push(link.clone())
link.end.parent = newNode
link.end.links = []
getNodes(link.end, newNode)
} else {
const targetId = idMap[link.end.id]
const newLink = link.clone()
newLink.start = newNode
newLink.end = newNodes.find(node => node.id === targetId)
newLinks.push(newLink)
}
})
}
getNodes(this.source, this.target.parent)
const replaceData = {
nodes: newNodes,
links: newLinks
}
nodes.splice(nodes.findIndex(node => node.id === this.target.id), 1, ...newNodes)
links.splice(links.findIndex(link => link.end.id === this.target.id), 1, ...newLinks)
}
}
}
// 添加剪切、复制、粘贴事件
container.addEventListener('keydown', event => {
const isCtrlKey = event.ctrlKey || event.metaKey // Mac键盘是command
if (isCtrlKey && event.key === 'c') {
const nodesIdMap = {}
const getNodes = node => {
nodesIdMap[node.id] = true
node.links.forEach(link => {
if (!nodesIdMap[link.end.id]) {
getNodes(link.end)
}
})
}
const selectedNode = getSelectedNode()
if (selectedNode) {
const clipboard = new ClipboardNode(selectedNode)
getNodes(selectedNode)
clipboard.prepare()
localStorage.setItem('clipboard', JSON.stringify(clipboard))
}
} else if (isCtrlKey && event.key === 'x') {
const nodesIdMap = {}
const getNodes = node => {
nodesIdMap[node.id] = true
this.nodes.push(node.clone())
node.links.forEach(link => {
if (!nodesIdMap[link.end.id]) {
this.links.push(link.clone())
getNodes(link.end)
}
})
}
const selectedNode = getSelectedNode()
if (selectedNode) {
const clipboard = new ClipboardNode(selectedNode)
getNodes(selectedNode)
clipboard.prepare()
localStorage.setItem('clipboard', JSON.stringify(clipboard))
deleteNode(selectedNode)
}
} else if (isCtrlKey && event.key === 'v') {
const clipboard = JSON.parse(localStorage.getItem('clipboard'))
if (clipboard) {
clipboard.paste(getMousePos(event))
}
}
})
上述代码中,使用了一个剪贴板类,将需要复制、剪切和粘贴的节点相关信息保存起来,并使用localStorage将保存节点信息。
5.3 快捷键
用户可以快速响应常用功能,如节点复制、快速隐藏所有节点等快捷键功能。
实现快捷键功能的代码如下:
// 添加快捷键
document.body.addEventListener('keydown', event => {
const isCtrlKey = event.ctrlKey || event.metaKey // Mac键盘是command
if (isCtrlKey && event.key === 'c') {
// ...
} else if (isCtrlKey && event.key === 'x') {
// ...
} else if (isCtrlKey && event.key === 'v') {
// ...
} else if (event.key === 'h') {
nodes.forEach(node => {
node.hide()
})
} else if (event.key === 's') {
const data = {
nodes: nodes,
links: links
}
localStorage.setItem('mindmap', JSON.stringify(data))
} else if (event.key === '+') {
scale *= zoomScale
} else if (event.key === '-') {
scale *= 1 / zoomScale
}
})
上述代码中,使用了document.body作为事件监听对象,并判断了键盘事件类型和使用的快捷键。
6. 总结
该篇文章介绍了一个PHP和Vue.js结合的个性化脑图工具,其中实现了基本功能如节点创建、连接、存储、编辑和图谱缩放;同时增加了自定义功能如节点隐藏/显示、分支复制/剪切/粘贴和节点删除等;高级功能方面则增加了打印图谱和快捷键等便捷功能。该工具结合PHP后端和Vue.js前端技术,具有较好的可扩展性和跨平台性,适用于大多数用户创建和编辑自己的脑图工具。