From 5b5aab1c9e3ad8d96ecdf6b016b07deb15ff62ab Mon Sep 17 00:00:00 2001 From: wanglin2 <1013335014@qq.com> Date: Mon, 12 Sep 2022 23:07:01 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=94=AF=E6=8C=81=E5=A4=9A?= =?UTF-8?q?=E7=A7=8D=E5=BD=A2=E7=8A=B6=E5=BC=80=E5=8F=91=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- simple-mind-map/package.json | 2 +- simple-mind-map/src/Node.js | 44 +++- simple-mind-map/src/Render.js | 20 ++ simple-mind-map/src/Shape.js | 265 ++++++++++++++++++++++++ simple-mind-map/src/Style.js | 23 +- simple-mind-map/src/themes/default.js | 10 +- web/src/config/index.js | 40 ++++ web/src/pages/Edit/components/Style.vue | 81 +++++++- 9 files changed, 472 insertions(+), 17 deletions(-) create mode 100644 simple-mind-map/src/Shape.js diff --git a/README.md b/README.md index 9160b002..e8e4ad74 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/simple-mind-map/package.json b/simple-mind-map/package.json index 481dffd6..4d1c60a0 100644 --- a/simple-mind-map/package.json +++ b/simple-mind-map/package.json @@ -1,6 +1,6 @@ { "name": "simple-mind-map", - "version": "0.2.3", + "version": "0.2.4", "description": "一个简单的web在线思维导图", "authors": [ { diff --git a/simple-mind-map/src/Node.js b/simple-mind-map/src/Node.js index d00867a3..99add85d 100644 --- a/simple-mind-map/src/Node.js +++ b/simple-mind-map/src/Node.js @@ -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 diff --git a/simple-mind-map/src/Render.js b/simple-mind-map/src/Render.js index 57e97e4f..f8fb0f9b 100644 --- a/simple-mind-map/src/Render.js +++ b/simple-mind-map/src/Render.js @@ -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 diff --git a/simple-mind-map/src/Shape.js b/simple-mind-map/src/Shape.js new file mode 100644 index 00000000..be73db8b --- /dev/null +++ b/simple-mind-map/src/Shape.js @@ -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'] \ No newline at end of file diff --git a/simple-mind-map/src/Style.js b/simple-mind-map/src/Style.js index 31096475..820000dd 100644 --- a/simple-mind-map/src/Style.js +++ b/simple-mind-map/src/Style.js @@ -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')) + }) } /** diff --git a/simple-mind-map/src/themes/default.js b/simple-mind-map/src/themes/default.js index 03d6d65e..13f56816 100644 --- a/simple-mind-map/src/themes/default.js +++ b/simple-mind-map/src/themes/default.js @@ -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', } } -} \ No newline at end of file +} + +// 支持激活样式的属性 +// 简单来说,会改变节点大小的都不支持在激活时设置,为了性能考虑,节点切换激活态时不会重新计算节点大小 +export const supportActiveStyle = ['fillColor', 'color', 'fontWeight', 'fontStyle', 'borderColor', 'borderWidth', 'borderDasharray', 'borderRadius', 'textDecoration'] \ No newline at end of file diff --git a/web/src/config/index.js b/web/src/config/index.js index f374b38a..7966a0e7 100644 --- a/web/src/config/index.js +++ b/web/src/config/index.js @@ -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' + } ] \ No newline at end of file diff --git a/web/src/pages/Edit/components/Style.vue b/web/src/pages/Edit/components/Style.vue index dcfab820..e22d5611 100644 --- a/web/src/pages/Edit/components/Style.vue +++ b/web/src/pages/Edit/components/Style.vue @@ -15,6 +15,7 @@ size="mini" v-model="style.fontFamily" placeholder="" + :disabled="checkDisabled('fontFamily')" @change="update('fontFamily')" >
-
+
A
B @@ -89,7 +92,7 @@
I @@ -99,16 +102,17 @@
U
- + - + - + - +
+ +
形状
+
+
+ 形状 + + + + +
+
节点内边距
@@ -217,6 +249,7 @@
@@ -227,6 +260,7 @@
@@ -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; }