diff --git a/simple-mind-map/src/core/render/Render.js b/simple-mind-map/src/core/render/Render.js index 4bfa6c31..c6be1777 100644 --- a/simple-mind-map/src/core/render/Render.js +++ b/simple-mind-map/src/core/render/Render.js @@ -65,7 +65,9 @@ class Render { this.mindMap = opt.mindMap this.themeConfig = this.mindMap.themeConfig // 渲染树,操作过程中修改的都是这里的数据 - this.renderTree = this.mindMap.opt.data ? merge({}, this.mindMap.opt.data) : null + this.renderTree = this.mindMap.opt.data + ? merge({}, this.mindMap.opt.data) + : null // 是否重新渲染 this.reRender = false // 是否正在渲染中 @@ -246,6 +248,9 @@ class Render { // 设置节点备注 this.setNodeNote = this.setNodeNote.bind(this) this.mindMap.command.add('SET_NODE_NOTE', this.setNodeNote) + // 设置节点附件 + this.setNodeAttachment = this.setNodeAttachment.bind(this) + this.mindMap.command.add('SET_NODE_ATTACHMENT', this.setNodeAttachment) // 设置节点标签 this.setNodeTag = this.setNodeTag.bind(this) this.mindMap.command.add('SET_NODE_TAG', this.setNodeTag) @@ -1600,6 +1605,14 @@ class Render { }) } + // 设置节点附件 + setNodeAttachment(node, url, name = '') { + this.setNodeDataRender(node, { + attachmentUrl: url, + attachmentName: name + }) + } + // 设置节点标签 setNodeTag(node, tag) { this.setNodeDataRender(node, { diff --git a/simple-mind-map/src/core/render/node/Node.js b/simple-mind-map/src/core/render/node/Node.js index 80863b74..8a1cad17 100644 --- a/simple-mind-map/src/core/render/node/Node.js +++ b/simple-mind-map/src/core/render/node/Node.js @@ -75,6 +75,7 @@ class Node { this._noteData = null this.noteEl = null this.noteContentIsShow = false + this._attachmentData = null this._expandBtn = null this._lastExpandBtnType = null this._showExpandBtn = false @@ -199,6 +200,7 @@ class Node { this._hyperlinkData = this.createHyperlinkNode() this._tagData = this.createTagNode() this._noteData = this.createNoteNode() + this._attachmentData = this.createAttachmentNode() } // 计算节点的宽高 @@ -267,6 +269,11 @@ class Node { textContentWidth += this._noteData.width textContentHeight = Math.max(textContentHeight, this._noteData.height) } + // 附件 + if (this._attachmentData) { + textContentWidth += this._attachmentData.width + textContentHeight = Math.max(textContentHeight, this._attachmentData.height) + } // 文字内容部分的尺寸 this._rectInfo.textContentWidth = textContentWidth this._rectInfo.textContentHeight = textContentHeight @@ -399,6 +406,14 @@ class Node { textContentNested.add(this._noteData.node) textContentOffsetX += this._noteData.width } + // 附件 + if (this._attachmentData) { + this._attachmentData.node + .x(textContentOffsetX) + .y((this._rectInfo.textContentHeight - this._attachmentData.height) / 2) + textContentNested.add(this._attachmentData.node) + textContentOffsetX += this._attachmentData.width + } // 文字内容整体 textContentNested.translate( width / 2 - textContentNested.bbox().width / 2, diff --git a/simple-mind-map/src/core/render/node/nodeCommandWraps.js b/simple-mind-map/src/core/render/node/nodeCommandWraps.js index 1a51130e..3f961cfa 100644 --- a/simple-mind-map/src/core/render/node/nodeCommandWraps.js +++ b/simple-mind-map/src/core/render/node/nodeCommandWraps.js @@ -28,6 +28,11 @@ function setNote(note) { this.mindMap.execCommand('SET_NODE_NOTE', this, note) } +// 设置附件 +function setAttachment(url, name) { + this.mindMap.execCommand('SET_NODE_ATTACHMENT', this, url, name) +} + // 设置标签 function setTag(tag) { this.mindMap.execCommand('SET_NODE_TAG', this, tag) @@ -55,6 +60,7 @@ export default { setIcon, setHyperlink, setNote, + setAttachment, setTag, setShape, setStyle, diff --git a/simple-mind-map/src/core/render/node/nodeCreateContents.js b/simple-mind-map/src/core/render/node/nodeCreateContents.js index 8fe5f3c7..20fda808 100644 --- a/simple-mind-map/src/core/render/node/nodeCreateContents.js +++ b/simple-mind-map/src/core/render/node/nodeCreateContents.js @@ -262,7 +262,7 @@ function createHyperlinkNode() { e.stopPropagation() }) if (hyperlinkTitle) { - a.attr('title', hyperlinkTitle) + node.add(SVG(`${hyperlinkTitle}`)) } // 添加一个透明的层,作为鼠标区域 a.rect(iconSize, iconSize).fill({ color: 'transparent' }) @@ -368,6 +368,36 @@ function createNoteNode() { } } +// 创建附件节点 +function createAttachmentNode() { + const { attachmentUrl, attachmentName } = this.getData() + if (!attachmentUrl) { + return + } + const iconSize = this.mindMap.themeConfig.iconSize + const node = new SVG().attr('cursor', 'pointer').size(iconSize, iconSize) + if (attachmentName) { + node.add(SVG(`${attachmentName}`)) + } + // 透明的层,用来作为鼠标区域 + node.add(new Rect().size(iconSize, iconSize).fill({ color: 'transparent' })) + // 备注图标 + const iconNode = SVG(iconsSvg.attachment).size(iconSize, iconSize) + this.style.iconNode(iconNode) + node.add(iconNode) + node.on('click', e => { + this.mindMap.emit('node_attachmentClick', this, e, node) + }) + node.on('contextmenu', e => { + this.mindMap.emit('node_attachmentContextmenu', this, e, node) + }) + return { + node, + width: iconSize, + height: iconSize + } +} + // 获取节点备注显示位置 function getNoteContentPosition() { const iconSize = this.mindMap.themeConfig.iconSize @@ -415,6 +445,7 @@ export default { createHyperlinkNode, createTagNode, createNoteNode, + createAttachmentNode, getNoteContentPosition, measureCustomNodeContentSize, isUseCustomNodeContent diff --git a/simple-mind-map/src/core/view/View.js b/simple-mind-map/src/core/view/View.js index 8def88a3..41ee92ba 100644 --- a/simple-mind-map/src/core/view/View.js +++ b/simple-mind-map/src/core/view/View.js @@ -158,7 +158,8 @@ class View { ...viewData.transform }) this.mindMap.emit('view_data_change', this.getTransformData()) - this.mindMap.emit('scale', this.scale) + this.emitEvent('scale') + this.emitEvent('translate') } } @@ -168,6 +169,7 @@ class View { this.x += x this.y += y this.transform() + this.emitEvent('translate') } // 平移x方向 @@ -175,12 +177,14 @@ class View { if (step === 0) return this.x += step this.transform() + this.emitEvent('translate') } // 平移x方式到 translateXTo(x) { this.x = x this.transform() + this.emitEvent('translate') } // 平移y方向 @@ -188,12 +192,14 @@ class View { if (step === 0) return this.y += step this.transform() + this.emitEvent('translate') } // 平移y方向到 translateYTo(y) { this.y = y this.transform() + this.emitEvent('translate') } // 应用变换 @@ -211,13 +217,17 @@ class View { // 恢复 reset() { - let scaleChange = this.scale !== 1 + const scaleChange = this.scale !== 1 + const translateChange = this.x !== 0 || this.y !== 0 this.scale = 1 this.x = 0 this.y = 0 this.transform() if (scaleChange) { - this.mindMap.emit('scale', this.scale) + this.emitEvent('scale') + } + if (translateChange) { + this.emitEvent('translate') } } @@ -227,7 +237,7 @@ class View { const scale = Math.max(this.scale - scaleRatio, 0.1) this.scaleInCenter(scale, cx, cy) this.transform() - this.mindMap.emit('scale', this.scale) + this.emitEvent('scale') } // 放大 @@ -236,7 +246,7 @@ class View { const scale = this.scale + scaleRatio this.scaleInCenter(scale, cx, cy) this.transform() - this.mindMap.emit('scale', this.scale) + this.emitEvent('scale') } // 基于指定中心进行缩放,cx,cy 可不指定,此时会使用画布中心点 @@ -262,7 +272,7 @@ class View { this.scale = scale } this.transform() - this.mindMap.emit('scale', this.scale) + this.emitEvent('scale') } // 适应画布大小 @@ -394,6 +404,16 @@ class View { bottom } } + + // 派发事件 + emitEvent(type) { + switch (type) { + case 'scale': + this.mindMap.emit('scale', this.scale) + case 'translate': + this.mindMap.emit('translate', this.x, this.y) + } + } } export default View diff --git a/simple-mind-map/src/plugins/Search.js b/simple-mind-map/src/plugins/Search.js index 7b5d69a4..8e7727ac 100644 --- a/simple-mind-map/src/plugins/Search.js +++ b/simple-mind-map/src/plugins/Search.js @@ -86,6 +86,7 @@ class Search { this.matchNodeList = [] this.currentIndex = -1 const { isOnlySearchCurrentRenderNodes } = this.mindMap.opt + // 如果要搜索收起来的节点,那么要遍历渲染树而不是节点树 const tree = isOnlySearchCurrentRenderNodes ? this.mindMap.renderer.root : this.mindMap.renderer.renderTree @@ -103,6 +104,11 @@ class Search { }) } + // 判断对象是否是节点实例 + isNodeInstance(node) { + return node instanceof Node + } + // 搜索下一个,定位到下一个匹配节点 searchNext(callback) { if (!this.isSearching || this.matchNodeList.length <= 0) return @@ -113,13 +119,12 @@ class Search { } const currentNode = this.matchNodeList[this.currentIndex] this.notResetSearchText = true - const uid = - currentNode instanceof Node - ? currentNode.getData('uid') - : currentNode.data.uid + const uid = this.isNodeInstance(currentNode) + ? currentNode.getData('uid') + : currentNode.data.uid const targetNode = this.mindMap.renderer.findNodeByUid(uid) this.mindMap.execCommand('GO_TARGET_NODE', uid, node => { - if (!(currentNode instanceof Node)) { + if (!this.isNodeInstance(currentNode)) { this.matchNodeList[this.currentIndex] = node } callback() @@ -173,15 +178,20 @@ class Search { return replaceText = String(replaceText) this.matchNodeList.forEach(node => { - let text = this.getReplacedText(node, this.searchText, replaceText) - this.mindMap.renderer.setNodeDataRender( - node, - { - text, - resetRichText: !!node.getData('richText') - }, - true - ) + const text = this.getReplacedText(node, this.searchText, replaceText) + if (this.isNodeInstance(node)) { + this.mindMap.renderer.setNodeDataRender( + node, + { + text, + resetRichText: !!node.getData('richText') + }, + true + ) + } else { + node.data.text = text + node.data.resetRichText = !!node.data.richText + } }) this.mindMap.render() this.mindMap.command.addHistory() @@ -190,7 +200,9 @@ class Search { // 获取某个节点替换后的文本 getReplacedText(node, searchText, replaceText) { - let { richText, text } = node.getData() + let { richText, text } = this.isNodeInstance(node) + ? node.getData() + : node.data if (richText) { return replaceHtmlText(text, searchText, replaceText) } else { diff --git a/simple-mind-map/src/svg/icons.js b/simple-mind-map/src/svg/icons.js index cafe49d4..f056545e 100644 --- a/simple-mind-map/src/svg/icons.js +++ b/simple-mind-map/src/svg/icons.js @@ -8,6 +8,10 @@ const hyperlink = const note = '' +// 附件图标 +const attachment = + '' + // 节点icon export const nodeIconList = [ { @@ -303,6 +307,7 @@ const getNodeIconListIcon = (name, extendIconList = []) => { export default { hyperlink, note, + attachment, nodeIconList, getNodeIconListIcon } diff --git a/web/src/assets/icon-font/iconfont.css b/web/src/assets/icon-font/iconfont.css index a7b6d1bc..063864fb 100644 --- a/web/src/assets/icon-font/iconfont.css +++ b/web/src/assets/icon-font/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 2479351 */ - src: url('iconfont.woff2?t=1711536835850') format('woff2'), - url('iconfont.woff?t=1711536835850') format('woff'), - url('iconfont.ttf?t=1711536835850') format('truetype'); + src: url('iconfont.woff2?t=1711935414521') format('woff2'), + url('iconfont.woff?t=1711935414521') format('woff'), + url('iconfont.ttf?t=1711935414521') format('truetype'); } .iconfont { @@ -13,6 +13,10 @@ -moz-osx-font-smoothing: grayscale; } +.iconfujian:before { + content: "\e88a"; +} + .icongeshihua:before { content: "\e7a3"; } diff --git a/web/src/assets/icon-font/iconfont.ttf b/web/src/assets/icon-font/iconfont.ttf index 530a259c..bb0fbedf 100644 Binary files a/web/src/assets/icon-font/iconfont.ttf and b/web/src/assets/icon-font/iconfont.ttf differ diff --git a/web/src/assets/icon-font/iconfont.woff b/web/src/assets/icon-font/iconfont.woff index 670ceb03..fdb5ff4b 100644 Binary files a/web/src/assets/icon-font/iconfont.woff and b/web/src/assets/icon-font/iconfont.woff differ diff --git a/web/src/assets/icon-font/iconfont.woff2 b/web/src/assets/icon-font/iconfont.woff2 index f330a8bb..4225466d 100644 Binary files a/web/src/assets/icon-font/iconfont.woff2 and b/web/src/assets/icon-font/iconfont.woff2 differ diff --git a/web/src/lang/en_us.js b/web/src/lang/en_us.js index 6f01ed98..8eafa1e7 100644 --- a/web/src/lang/en_us.js +++ b/web/src/lang/en_us.js @@ -261,6 +261,7 @@ export default { save: 'Save', painter: 'Painter', formula: 'Formula', + attachment: 'Attachment', more: 'More', selectFileTip: 'Please select a file', notSupportTip: @@ -333,5 +334,8 @@ export default { formatErrorTip: 'The JSON format is incorrect. Please check and try again', copyTip: 'Copied to clipboard', formatTip: 'Format complete' + }, + attachment: { + deleteAttachment: 'Delete attachment' } } diff --git a/web/src/lang/zh_cn.js b/web/src/lang/zh_cn.js index 1eda5518..f0e2a546 100644 --- a/web/src/lang/zh_cn.js +++ b/web/src/lang/zh_cn.js @@ -258,6 +258,7 @@ export default { save: '保存', painter: '格式刷', formula: '公式', + attachment: '附件', more: '更多', selectFileTip: '请选择文件', notSupportTip: '你的浏览器不支持该功能,或者当前页面非https协议', @@ -327,5 +328,8 @@ export default { formatErrorTip: 'JSON格式有误,请检查后再试', copyTip: '已复制到剪贴板', formatTip: '格式化完成' + }, + attachment: { + deleteAttachment: '删除附件' } } diff --git a/web/src/pages/Edit/components/Contextmenu.vue b/web/src/pages/Edit/components/Contextmenu.vue index 325a64cf..6eefe6cb 100644 --- a/web/src/pages/Edit/components/Contextmenu.vue +++ b/web/src/pages/Edit/components/Contextmenu.vue @@ -225,6 +225,7 @@ export default { this.$bus.$on('expand_btn_click', this.hide) this.$bus.$on('svg_mousedown', this.onMousedown) this.$bus.$on('mouseup', this.onMouseup) + this.$bus.$on('translate', this.hide) }, beforeDestroy() { this.$bus.$off('node_contextmenu', this.show) @@ -233,6 +234,7 @@ export default { this.$bus.$off('expand_btn_click', this.hide) this.$bus.$off('svg_mousedown', this.onMousedown) this.$bus.$off('mouseup', this.onMouseup) + this.$bus.$off('translate', this.hide) }, methods: { ...mapMutations(['setLocalConfig']), diff --git a/web/src/pages/Edit/components/Edit.vue b/web/src/pages/Edit/components/Edit.vue index ae86fbb7..6f0a3426 100644 --- a/web/src/pages/Edit/components/Edit.vue +++ b/web/src/pages/Edit/components/Edit.vue @@ -16,6 +16,7 @@ v-if="mindMap" :mindMap="mindMap" > + @@ -87,6 +88,7 @@ import Scrollbar from './Scrollbar.vue' import exampleData from 'simple-mind-map/example/exampleData' import FormulaSidebar from './FormulaSidebar.vue' import SourceCodeEdit from './SourceCodeEdit.vue' +import NodeAttachment from './NodeAttachment.vue' // 注册插件 MindMap.usePlugin(MiniMap) @@ -139,7 +141,8 @@ export default { OutlineEdit, Scrollbar, FormulaSidebar, - SourceCodeEdit + SourceCodeEdit, + NodeAttachment }, data() { return { @@ -532,7 +535,10 @@ export default { 'painter_start', 'painter_end', 'scrollbar_change', - 'scale' + 'scale', + 'translate', + 'node_attachmentClick', + 'node_attachmentContextmenu' ].forEach(event => { this.mindMap.on(event, (...args) => { this.$bus.$emit(event, ...args) diff --git a/web/src/pages/Edit/components/NodeAttachment.vue b/web/src/pages/Edit/components/NodeAttachment.vue new file mode 100644 index 00000000..63d92250 --- /dev/null +++ b/web/src/pages/Edit/components/NodeAttachment.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/web/src/pages/Edit/components/NodeNoteContentShow.vue b/web/src/pages/Edit/components/NodeNoteContentShow.vue index 19a7c9e4..ac5cdf21 100644 --- a/web/src/pages/Edit/components/NodeNoteContentShow.vue +++ b/web/src/pages/Edit/components/NodeNoteContentShow.vue @@ -37,6 +37,7 @@ export default { document.body.addEventListener('click', this.hideNoteContent) this.$bus.$on('node_active', this.hideNoteContent) this.$bus.$on('scale', this.onScale) + this.$bus.$on('translate', this.onScale) this.$bus.$on('svg_mousedown', this.hideNoteContent) }, mounted() { @@ -48,6 +49,7 @@ export default { document.body.removeEventListener('click', this.hideNoteContent) this.$bus.$off('node_active', this.hideNoteContent) this.$bus.$off('scale', this.onScale) + this.$bus.$off('translate', this.onScale) this.$bus.$off('svg_mousedown', this.hideNoteContent) }, methods: { @@ -97,6 +99,8 @@ export default { border-radius: 5px; max-height: 300px; overflow-y: auto; + box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.06); + border: 1px solid rgba(0, 0, 0, 0.06); &::-webkit-scrollbar { width: 7px; diff --git a/web/src/pages/Edit/components/Toolbar.vue b/web/src/pages/Edit/components/Toolbar.vue index e90635b5..6b91638f 100644 --- a/web/src/pages/Edit/components/Toolbar.vue +++ b/web/src/pages/Edit/components/Toolbar.vue @@ -184,6 +184,7 @@ export default { 'summary', 'associativeLine', 'formula' + // 'attachment' ], horizontalList: [], verticalList: [], diff --git a/web/src/pages/Edit/components/ToolbarNodeBtnList.vue b/web/src/pages/Edit/components/ToolbarNodeBtnList.vue index 7e84e0b2..70eedb6f 100644 --- a/web/src/pages/Edit/components/ToolbarNodeBtnList.vue +++ b/web/src/pages/Edit/components/ToolbarNodeBtnList.vue @@ -156,6 +156,17 @@ {{ $t('toolbar.formula') }} +
+ + {{ $t('toolbar.attachment') }} +
@@ -258,6 +269,11 @@ export default { // 打开公式侧边栏 showFormula() { this.setActiveSidebar('formulaSidebar') + }, + + // 选择附件 + selectAttachmentFile() { + this.$bus.$emit('selectAttachment', this.activeNodes) } } }