节点支持多种形状开发完成

This commit is contained in:
wanglin2
2022-09-12 23:07:01 +08:00
parent 13a4b12ad4
commit 5b5aab1c9e
9 changed files with 472 additions and 17 deletions

View File

@@ -18,6 +18,8 @@
- [x] 支持节点自由拖拽、拖拽调整
- [x] 支持多种节点形状
## 目录介绍
1.`simple-mind-map`
@@ -86,7 +88,7 @@ npm run build
# 安装
> 当然仓库版本0.2.3当前npm版本0.2.3
> 当然仓库版本0.2.4当前npm版本0.2.3
```bash
npm i simple-mind-map

View File

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

View File

@@ -1,4 +1,5 @@
import Style from './Style'
import Shape from './Shape'
import {
resizeImgSize,
asyncRun
@@ -43,6 +44,12 @@ class Node {
this.themeConfig = this.mindMap.themeConfig
// 样式实例
this.style = new Style(this, this.themeConfig)
// 形状实例
this.shapeInstance = new Shape(this)
this.shapePadding = {
paddingX: 0,
paddingY: 0
}
// 是否是根节点
this.isRoot = opt.isRoot === undefined ? false : opt.isRoot
// 是否是概要节点
@@ -331,9 +338,16 @@ class Node {
// 间距
let margin = imgContentHeight > 0 && textContentHeight > 0 ? this.blockContentMargin : 0
let { paddingX, paddingY } = this.getPaddingVale()
// 纯内容宽高
let _width = Math.max(imgContentWidth, textContentWidth)
let _height = imgContentHeight + textContentHeight
// 计算节点形状需要的附加内边距
let { paddingX: shapePaddingX, paddingY: shapePaddingY } = this.shapeInstance.getShapePadding(_width, _height, paddingX, paddingY)
this.shapePadding.paddingX = shapePaddingX
this.shapePadding.paddingY = shapePaddingY
return {
width: Math.max(imgContentWidth, textContentWidth) + paddingX * 2,
height: imgContentHeight + textContentHeight + paddingY * 2 + margin
width: _width + paddingX * 2 + shapePaddingX * 2,
height: _height + paddingY * 2 + margin + shapePaddingY * 2
}
}
@@ -542,6 +556,16 @@ class Node {
}
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 22:02:07
* @Desc: 获取节点形状
*/
getShape() {
return this.style.getStyle('shape', false, false)
}
/**
* javascript comment
* @Author: 王林25
@@ -555,6 +579,7 @@ class Node {
textContentItemMargin
} = this
let { paddingY } = this.getPaddingVale()
paddingY += this.shapePadding.paddingY
// 创建组
this.group = new G()
// 概要节点添加一个带所属节点id的类名
@@ -563,8 +588,9 @@ class Node {
}
this.draw.add(this.group)
this.update(true)
// 节点
this.style.rect(this.group.rect(width, height))
// 节点形
const shape = this.getShape()
this.style[shape === 'rectangle' ? 'rect' : 'shape'](this.shapeInstance.createShape())
// 图片节点
let imgHeight = 0
if (this._imgData) {
@@ -1206,6 +1232,16 @@ class Node {
setTag(tag) {
this.mindMap.execCommand('SET_NODE_TAG', this, tag)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:47:45
* @Desc: 设置形状
*/
setShape(shape) {
this.mindMap.execCommand('SET_NODE_SHAPE', this, shape)
}
}
export default Node

View File

@@ -5,6 +5,7 @@ import CatalogOrganization from './layouts/CatalogOrganization'
import OrganizationStructure from './layouts/OrganizationStructure'
import TextEdit from './TextEdit'
import { copyNodeTree, simpleDeepClone, walk } from './utils'
import { shapeList } from './Shape';
// 布局列表
const layouts = {
@@ -175,6 +176,9 @@ class Render {
// 一键整理布局
this.resetLayout = this.resetLayout.bind(this)
this.mindMap.command.add('RESET_LAYOUT', this.resetLayout)
// 设置节点形状
this.setNodeShape = this.setNodeShape.bind(this)
this.mindMap.command.add('SET_NODE_SHAPE', this.setNodeShape)
}
/**
@@ -977,6 +981,22 @@ class Render {
}, null, true, 0, 0)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:44:01
* @Desc: 设置节点形状
*/
setNodeShape(node, shape) {
if (!shape || !shapeList.includes(shape)) {
return
}
let nodeList = [node] || this.activeNodeList
nodeList.forEach((item) => {
this.setNodeStyle(item, 'shape', shape)
})
}
/**
* @Author: 王林
* @Date: 2021-05-04 14:19:48

View File

@@ -0,0 +1,265 @@
/**
* @Author: 王林
* @Date: 2022-08-22 21:32:50
* @Desc: 节点形状类
*/
export default class Shape {
constructor(node) {
this.node = node
}
/**
* @Author: 王林
* @Date: 2022-08-17 22:32:32
* @Desc: 形状需要的padding
*/
getShapePadding(width, height, paddingX, paddingY) {
const shape = this.node.getShape()
const defaultPaddingX = 15
const defaultPaddingY = 5
const actWidth = width + paddingX * 2
const actHeight = height + paddingY * 2
const actOffset = Math.abs(actWidth - actHeight)
switch (shape) {
case 'roundedRectangle':
return {
paddingX: height > width ? (height - width) / 2 : 0,
paddingY: 0
}
case 'diamond':
return {
paddingX: width / 2,
paddingY: height / 2
}
case 'parallelogram':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'outerTriangularRectangle':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'innerTriangularRectangle':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: 0
}
case 'ellipse':
return {
paddingX: paddingX <= 0 ? defaultPaddingX : 0,
paddingY: paddingY <= 0 ? defaultPaddingY : 0
}
case 'circle':
return {
paddingX: actHeight > actWidth ? actOffset / 2 : 0,
paddingY: actHeight < actWidth ? actOffset / 2 : 0,
}
default:
return {
paddingX: 0,
paddingY: 0
}
}
}
/**
* @Author: 王林
* @Date: 2022-08-17 22:22:53
* @Desc: 创建形状节点
*/
createShape() {
const shape = this.node.getShape()
let { width, height } = this.node
let node = null
// 矩形
if (shape === 'rectangle') {
node = this.node.group.rect(width, height)
} else if (shape === 'diamond') {
// 菱形
node = this.createDiamond()
} else if (shape === 'parallelogram') {
// 平行四边形
node = this.createParallelogram()
} else if (shape === 'roundedRectangle') {
// 圆角矩形
node = this.createRoundedRectangle()
} else if (shape === 'octagonalRectangle') {
// 八角矩形
node = this.createOctagonalRectangle()
} else if (shape === 'outerTriangularRectangle') {
// 外三角矩形
node = this.createOuterTriangularRectangle()
} else if (shape === 'innerTriangularRectangle') {
// 内三角矩形
node = this.createInnerTriangularRectangle()
} else if (shape === 'ellipse') {
// 椭圆
node = this.createEllipse()
} else if (shape === 'circle') {
// 圆
node = this.createCircle()
}
return node
}
/**
* @Author: 王林
* @Date: 2022-09-04 09:08:54
* @Desc: 创建菱形
*/
createDiamond() {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
let topX = halfWidth
let topY = 0
let rightX = width
let rightY = halfHeight
let bottomX = halfWidth
let bottomY = height
let leftX = 0
let leftY = halfHeight
return this.node.group.polygon(`
${topX}, ${topY}
${rightX}, ${rightY}
${bottomX}, ${bottomY}
${leftX}, ${leftY}
`)
}
/**
* @Author: 王林
* @Date: 2022-09-03 16:14:12
* @Desc: 创建平行四边形
*/
createParallelogram() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${paddingX}, ${0}
${width}, ${0}
${width - paddingX}, ${height}
${0}, ${height}
`)
}
/**
* @Author: 王林
* @Date: 2022-09-03 16:50:23
* @Desc: 创建圆角矩形
*/
createRoundedRectangle() {
let { width, height } = this.node
let halfHeight = height / 2
return this.node.group.path(`
M${halfHeight},0
L${width - halfHeight},0
A${height / 2},${height / 2} 0 0,1 ${width - halfHeight},${height}
L${halfHeight},${height}
A${height / 2},${height / 2} 0 0,1 ${halfHeight},${0}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 16:14:08
* @Desc: 创建八角矩形
*/
createOctagonalRectangle() {
let w = 5
let { width, height } = this.node
return this.node.group.polygon(`
${0}, ${w}
${w}, ${0}
${width - w}, ${0}
${width}, ${w}
${width}, ${height - w}
${width - w}, ${height}
${w}, ${height}
${0}, ${height - w}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 20:55:50
* @Desc: 创建外三角矩形
*/
createOuterTriangularRectangle() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${paddingX}, ${0}
${width - paddingX}, ${0}
${width}, ${height / 2}
${width - paddingX}, ${height}
${paddingX}, ${height}
${0}, ${height / 2}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 20:59:37
* @Desc: 创建内三角矩形
*/
createInnerTriangularRectangle() {
let { paddingX } = this.node.getPaddingVale()
paddingX = paddingX || this.node.shapePadding.paddingX
let { width, height } = this.node
return this.node.group.polygon(`
${0}, ${0}
${width}, ${0}
${width - paddingX / 2}, ${height / 2}
${width}, ${height}
${0}, ${height}
${paddingX / 2}, ${height / 2}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:06:31
* @Desc: 创建椭圆
*/
createEllipse() {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return this.node.group.path(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:14:04
* @Desc: 创建圆
*/
createCircle() {
let { width, height } = this.node
let halfWidth = width / 2
let halfHeight = height / 2
return this.node.group.path(`
M${halfWidth},0
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${height}
M${halfWidth},${height}
A${halfWidth},${halfHeight} 0 0,1 ${halfWidth},${0}
`)
}
}
// 形状列表
export const shapeList = ['rectangle', 'diamond', 'parallelogram', 'roundedRectangle', 'octagonalRectangle', 'outerTriangularRectangle', 'innerTriangularRectangle', 'ellipse', 'circle']

View File

@@ -69,19 +69,40 @@ class Style {
return this.ctx.nodeData.data[prop] !== undefined ? this.ctx.nodeData.data[prop] : defaultConfig[prop]
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 21:55:57
* @Desc: 获取某个样式值
*/
getStyle(prop, root, isActive) {
return this.merge(prop, root, isActive)
}
/**
* @Author: 王林
* @Date: 2021-04-11 10:12:56
* @Desc: 矩形
*/
rect(node) {
this.shape(node)
node.radius(this.merge('borderRadius'))
}
/**
* javascript comment
* @Author: 王林
* @Date: 2022-09-12 15:04:28
* @Desc: 矩形外的其他形状
*/
shape(node) {
node.fill({
color: this.merge('fillColor')
}).stroke({
color: this.merge('borderColor'),
width: this.merge('borderWidth'),
dasharray: this.merge('borderDasharray')
}).radius(this.merge('borderRadius'))
})
}
/**

View File

@@ -33,6 +33,7 @@ export default {
backgroundRepeat: 'no-repeat',
// 根节点样式
root: {
shape: 'rectangle',
fillColor: '#549688',
fontFamily: '微软雅黑, Microsoft YaHei',
color: '#fff',
@@ -53,6 +54,7 @@ export default {
},
// 二级节点样式
second: {
shape: 'rectangle',
marginX: 100,
marginY: 40,
fillColor: '#fff',
@@ -75,6 +77,7 @@ export default {
},
// 三级及以下节点样式
node: {
shape: 'rectangle',
marginX: 50,
marginY: 0,
fillColor: 'transparent',
@@ -97,6 +100,7 @@ export default {
},
// 概要节点样式
generalization: {
shape: 'rectangle',
marginX: 100,
marginY: 40,
fillColor: '#fff',
@@ -117,4 +121,8 @@ export default {
borderDasharray: 'none',
}
}
}
}
// 支持激活样式的属性
// 简单来说,会改变节点大小的都不支持在激活时设置,为了性能考虑,节点切换激活态时不会重新计算节点大小
export const supportActiveStyle = ['fillColor', 'color', 'fontWeight', 'fontStyle', 'borderColor', 'borderWidth', 'borderDasharray', 'borderRadius', 'textDecoration']

View File

@@ -311,4 +311,44 @@ export const shortcutKeyList = [
}
]
}
]
// 形状列表
export const shapeList = [
{
name: '矩形',
value: 'rectangle'
},
{
name: '菱形',
value: 'diamond'
},
{
name: '平行四边形',
value: 'parallelogram'
},
{
name: '圆角矩形',
value: 'roundedRectangle'
},
{
name: '八角矩形',
value: 'octagonalRectangle'
},
{
name: '外三角矩形',
value: 'outerTriangularRectangle'
},
{
name: '内三角矩形',
value: 'innerTriangularRectangle'
},
{
name: '椭圆',
value: 'ellipse'
},
{
name: '圆',
value: 'circle'
}
]

View File

@@ -15,6 +15,7 @@
size="mini"
v-model="style.fontFamily"
placeholder=""
:disabled="checkDisabled('fontFamily')"
@change="update('fontFamily')"
>
<el-option
@@ -36,6 +37,7 @@
style="width: 80px"
v-model="style.fontSize"
placeholder=""
:disabled="checkDisabled('fontSize')"
@change="update('fontSize')"
>
<el-option
@@ -54,6 +56,7 @@
style="width: 80px"
v-model="style.lineHeight"
placeholder=""
:disabled="checkDisabled('lineHeight')"
@change="update('lineHeight')"
>
<el-option
@@ -69,7 +72,7 @@
<div class="row">
<div class="btnGroup">
<el-tooltip content="颜色" placement="bottom">
<div class="styleBtn" v-popover:popover>
<div class="styleBtn" v-popover:popover :class="{ disabled: checkDisabled('color') }">
A
<span
class="colorShow"
@@ -80,7 +83,7 @@
<el-tooltip content="加粗" placement="bottom">
<div
class="styleBtn"
:class="{ actived: style.fontWeight === 'bold' }"
:class="{ actived: style.fontWeight === 'bold', disabled: checkDisabled('fontWeight') }"
@click="toggleFontWeight"
>
B
@@ -89,7 +92,7 @@
<el-tooltip content="斜体" placement="bottom">
<div
class="styleBtn i"
:class="{ actived: style.fontStyle === 'italic' }"
:class="{ actived: style.fontStyle === 'italic', disabled: checkDisabled('fontStyle') }"
@click="toggleFontStyle"
>
I
@@ -99,16 +102,17 @@
<div
class="styleBtn u"
:style="{ textDecoration: style.textDecoration || 'none' }"
:class="{ disabled: checkDisabled('textDecoration') }"
v-popover:popover2
>
U
</div>
</el-tooltip>
</div>
<el-popover ref="popover" placement="bottom" trigger="click">
<el-popover ref="popover" placement="bottom" trigger="click" :disabled="checkDisabled('color')">
<Color :color="style.color" @change="changeFontColor"></Color>
</el-popover>
<el-popover ref="popover2" placement="bottom" trigger="click">
<el-popover ref="popover2" placement="bottom" trigger="click" :disabled="checkDisabled('textDecoration')">
<el-radio-group
size="mini"
v-model="style.textDecoration"
@@ -129,8 +133,9 @@
class="block"
v-popover:popover3
:style="{ width: '80px', backgroundColor: style.borderColor }"
:class="{ disabled: checkDisabled('borderColor') }"
></span>
<el-popover ref="popover3" placement="bottom" trigger="click">
<el-popover ref="popover3" placement="bottom" trigger="click" :disabled="checkDisabled('borderColor')">
<Color
:color="style.borderColor"
@change="changeBorderColor"
@@ -144,6 +149,7 @@
style="width: 80px"
v-model="style.borderDasharray"
placeholder=""
:disabled="checkDisabled('borderDasharray')"
@change="update('borderDasharray')"
>
<el-option
@@ -164,6 +170,7 @@
style="width: 80px"
v-model="style.borderWidth"
placeholder=""
:disabled="checkDisabled('borderWidth')"
@change="update('borderWidth')"
>
<el-option
@@ -182,6 +189,7 @@
style="width: 80px"
v-model="style.borderRadius"
placeholder=""
:disabled="checkDisabled('borderRadius')"
@change="update('borderRadius')"
>
<el-option
@@ -203,12 +211,36 @@
class="block"
v-popover:popover4
:style="{ backgroundColor: style.fillColor }"
:class="{ disabled: checkDisabled('fillColor') }"
></span>
<el-popover ref="popover4" placement="bottom" trigger="click">
<el-popover ref="popover4" placement="bottom" trigger="click" :disabled="checkDisabled('fillColor')">
<Color :color="style.fillColor" @change="changeFillColor"></Color>
</el-popover>
</div>
</div>
<!-- 形状 -->
<div class="title">形状</div>
<div class="row">
<div class="rowItem">
<span class="name">形状</span>
<el-select
size="mini"
style="width: 120px"
v-model="style.shape"
placeholder=""
:disabled="checkDisabled('shape')"
@change="update('shape')"
>
<el-option
v-for="item in shapeList"
:key="item"
:label="item.name"
:value="item.value"
>
</el-option>
</el-select>
</div>
</div>
<!-- 节点内边距 -->
<div class="title noTop">节点内边距</div>
<div class="row">
@@ -217,6 +249,7 @@
<el-slider
style="width: 200px"
v-model="style.paddingX"
:disabled="checkDisabled('paddingX')"
@change="update('paddingX')"
></el-slider>
</div>
@@ -227,6 +260,7 @@
<el-slider
style="width: 200px"
v-model="style.paddingY"
:disabled="checkDisabled('paddingY')"
@change="update('paddingY')"
></el-slider>
</div>
@@ -246,7 +280,9 @@ import {
borderDasharrayList,
borderRadiusList,
lineHeightList,
shapeList,
} from "@/config";
import { supportActiveStyle } from 'simple-mind-map/src/themes/default';
/**
* @Author: 王林
@@ -261,6 +297,8 @@ export default {
},
data() {
return {
supportActiveStyle,
shapeList,
fontFamilyList,
fontSizeList,
borderWidthList,
@@ -270,6 +308,7 @@ export default {
activeNodes: [],
activeTab: "normal",
style: {
shape: '',
paddingX: 0,
paddingY: 0,
color: "",
@@ -289,11 +328,11 @@ export default {
},
created() {
this.$bus.$on("node_active", (...args) => {
this.$refs.sidebar.show = false;
if (this.$refs.sidebar) this.$refs.sidebar.show = false;
this.$nextTick(() => {
this.activeTab = "normal";
this.activeNodes = args[1];
this.$refs.sidebar.show = this.activeNodes.length > 0;
if (this.$refs.sidebar) this.$refs.sidebar.show = this.activeNodes.length > 0;
this.initNodeStyle();
});
});
@@ -308,6 +347,15 @@ export default {
this.initNodeStyle();
},
/**
* @Author: 王林
* @Date: 2022-09-12 22:16:56
* @Desc: 检查是否禁用
*/
checkDisabled(prop) {
return this.activeTab === 'active' && !this.supportActiveStyle.includes(prop)
},
/**
* @Author: 王林
* @Date: 2021-05-05 09:48:52
@@ -319,6 +367,7 @@ export default {
return;
}
[
"shape",
"paddingX",
"paddingY",
"color",
@@ -472,6 +521,13 @@ export default {
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
&.disabled {
background-color: #F5F7FA !important;
border-color: #E4E7ED !important;
color: #C0C4CC !important;
cursor: not-allowed !important;
}
}
}
@@ -492,6 +548,13 @@ export default {
background-color: #eee;
}
&.disabled {
background-color: #F5F7FA !important;
border-color: #E4E7ED !important;
color: #C0C4CC !important;
cursor: not-allowed !important;
}
&.i {
font-style: italic;
}