弹框模块
DigitalXmindDialog.vue
<template> <el-dialog :title="title" width="1200px" class="auth-dialog" top="5%" :append-to-body="true" :lock-scroll="false" :close-on-click-modal="false" :visible.sync="visible" :closed="hide" > <VueXmind v-if="visible" ref="vueXmindDialogRef" :nodeData="xmindData.tree" :styleData="xmindData.theme" ></VueXmind> <div slot="footer" class="center-dialog-footer"> <el-button class="btn btn-gray" @click="visible = false">取消</el-button> <el-button class="btn btn-shadow-pay" @click="onConfirm">确认</el-button> </div> </el-dialog> </template> <script> import VueXmind from '@/components/VueXmind/index' export default { components: { VueXmind }, props: { title: { type: String, default: '在线导图编辑-创建', }, }, data() { return { visible: false, currenNode: null, currentId: '', xmindData: {}, saveLoading: false, } }, methods: { async onConfirm() { if (this.$refs.vueXmindDialogRef && !this.saveLoading) { try { this.saveLoading = true let data = await this.$refs.vueXmindDialogRef.saveXmindData() // 1表格 2图片 3思维导图 let params = { bookExamModuleId: this.currenNode.id, analysisType: 3, analysisDetail: encodeURIComponent(JSON.stringify(data.data)), picture: data.imgUrl, } if (this.currentId) { params.id = this.currentId } this.$emit('updateNodeContent', params) this.saveLoading = false } catch (e) {} this.hide() } else { console.log('在处理中') } }, hide() { this.visible = false }, show(node, snode) { this.currenNode = node this.saveLoading = false // console.log(node, snode) if (snode) { this.currentId = snode.id try { this.xmindData = JSON.parse( decodeURIComponent(snode.analysisDetail), ) this.visible = true } catch (e) { console.log(e) this.xmindData = {} this.visible = true } } else { this.xmindData = {} this.currentId = '' this.visible = true } }, }, } </script> <style scoped lang="scss"></style>
VueXmind/index.vue
<template> <div> <div class="cp-xmind-controller"> <div class="control-left"> <span @click="onClickAddMainNode">添加同级节点</span> <span @click="onClickAddNode">添加子级节点</span> <span @click="deleteSelectNode" title="快捷键 Delete">删除节点</span> <span @click="openLatexEdit">插入/编辑公式</span> </div> <div class="control-right"> <span @click="onSetSceneTheme">主题设置 (配色选择)</span> <span @click="reSizeBox">排版优化</span> <!-- <span @click="savePhoto">保存图片</span> --> </div> </div> <div class="xmind-node-panel-segment"> <div class="xmind-node-box"> <div v-if="treeData" class="xmind-node-content" ref="xmindNodeBoxRef" :style="treeBoxStyle" @dragover="onGragMove" @dblclick="onSelectlayer" > <XmindSvgLine :node="treeData" :dwidth="currentTheme.dwidth" :canvasSize="canvasSize" :lineStyle="currentTheme.lineStyle" :drogDate="drogDate" :startDrag="startDrag" :dragNodeId="dragNodeId" ></XmindSvgLine> <XmindNode :node="treeData" ref="xmindsvgRef" :dwidth="currentTheme.dwidth" :boxStyle="currentTheme.boxStyle" :boxColorStyle="currentTheme.boxColorStyle" :currentId="currentId" :dragNodeId="dragNodeId" @updateNodeEnd="updateNodeEnd" @selectNodeId="changeSelectNode" ></XmindNode> </div> </div> <div class="xmind-node-attribute"> <NodeAttributes :node="currentNode" :currentId="currentId" @selectTheme="onSelectTheme" @updateAttribute="onUpdateAttribute" ></NodeAttributes> </div> </div> <!-- <img v-if="imgUrl" :src="imgUrl" /> --> <latex-edit-dialog ref="latexEditDialogRef" @updateNodeContent="onUpdateNodeContent" ></latex-edit-dialog> </div> </template> <script> import { API } from '@/api/config' import XmindNode from './XmindNode' import XmindSvgLine from './XmindSvgLine' import NodeAttributes from './NodeAttributes/index' import LatexEditDialog from './components/latex-edit-dialog' import XmindStyleMixins from './mixins/XmindStyleMixins' import XmindDateSaveMixins from './mixins/XmindDateSaveMixins' import KeyboardShortcutMixins from './mixins/KeyboardShortcutMixins' import { executeXmindNodePosition, CalculateDragUtil, selectNodeByTreeNode, } from './util' import html2canvas from 'html2canvas' export default { components: { XmindNode, XmindSvgLine, NodeAttributes, LatexEditDialog }, mixins: [XmindStyleMixins, XmindDateSaveMixins, KeyboardShortcutMixins], watch: { treeData() { if (this.treeData) { if (this.currentId) { this.currentNode = selectNodeByTreeNode( this.treeData, this.currentId, ) } } }, }, props: { nodeData: { type: Object, }, styleData: { type: Object, }, }, data() { return { imgUrl: '', DemoData: { data: '中心主题', w: 94, h: 42, children: [ { w: 70, h: 40, data: '分支主题', children: [] }, { w: 70, h: 40, data: '分支主题', children: [] }, ], }, // 防抖 更新 updateThrottle: null, } }, destroyed() { this.updateThrottle.cancel() document.ondragend = null document.ondragstart = null }, mounted() { this.updateThrottle = _.throttle(this.calculateDragExecute, 200) // 监听 鼠标松开的时候 document.ondragend = (e) => { this.onEndDragNode() } document.ondragstart = (ev) => { if (ev.target.id) { this.onStartDragNode(ev.target.id) } } if (this.styleData) { this.initStyleData(this.styleData) } if (this.nodeData) { this.updateTreeList(this.nodeData) } else { this.updateTreeList(this.DemoData) } this.$nextTick(() => { this.reSizeBox() }) }, methods: { savePhoto() { html2canvas(this.$refs.xmindNodeBoxRef, { useCORS: true }).then( (canvas) => { let dataURL = canvas.toDataURL('image/png') this.imgUrl = dataURL if (this.imgUrl !== '') { this.dialogTableVisible = true } }, ) }, getXmindPicture() { return new Promise((resolve, reject) => { html2canvas(this.$refs.xmindNodeBoxRef, { useCORS: true }).then( (canvas) => { let dataURL = canvas.toDataURL('image/png') resolve(dataURL) }, () => { resolve('') }, ) }) }, // 保存数据 async saveXmindData() { this.currentId = '' await this.cpsleep(10) let imgUrl = await this.getXmindPicture() let params = { base64File: imgUrl } imgUrl = await this.apiPost(API.CONFIG_UPLOAD_BASE64FILE, params).then( (res) => { if (this.checkoutRes(res)) { return res.data } else { return '' } }, () => { return '' }, ) // 上传图片 获取 url return { imgUrl: imgUrl, data: { tree: this.treeData, theme: this.currentTheme, }, } }, openLatexEdit(e) { if (this.currentId) { let cnode = selectNodeByTreeNode(this.treeData, this.currentId) if (cnode) { this.$refs.latexEditDialogRef.show(cnode.data) } } else { this.showMessage('请选择要修改的内容') } }, onGragMove(ev) { // 获取 应该加到那个 node 上面 let pos = { x: ev.layerX, y: ev.layerY, id: this.dragNodeId } this.updateThrottle(pos) ev.preventDefault() }, calculateDragExecute(pos) { let dropDate = CalculateDragUtil.calculateDragOverNode( this.treeData, pos, ) if (dropDate) { this.drogDate = dropDate } else { this.currentId = '' this.drogDate = null } }, changeSelectNode(node) { this.currentId = node.id this.currentNode = node }, onSelectlayer() { this.currentId = '' }, updateTreeList(data) { // 渲染除 node 的位置 if (data) { executeXmindNodePosition( data, this.currentTheme.dwidth, this.currentTheme.dheight, this.currentTheme.scaneBox, ) this.treeData = data } }, updateNodeEnd() { this.$nextTick(() => { let datas = JSON.parse(JSON.stringify(this.treeData)) this.updateTreeList(datas) }) }, reSizeBox() { if (this.$refs.xmindsvgRef) { this.$refs.xmindsvgRef.reSizeBox() this.$nextTick(() => { let datas = JSON.parse(JSON.stringify(this.treeData)) this.updateTreeList(datas) }) } }, }, } </script> <style scoped lang="scss"> .xmind-node-box { height: 600px; overflow: auto; background-color: #e4e4e4; } .xmind-node-content { background-color: #fff; position: relative; } .cp-xmind-controller { padding: 10px; display: flex; .control-left { flex: 1; } .control-right { flex: 1; text-align: right; } span { display: inline-block; border: 1px solid $color-theme; padding: 10px; border-radius: 6px; margin-left: 20px; cursor: pointer; user-select: none; } } .xmind-node-panel-segment { display: flex; .xmind-node-box { flex: 1; } .xmind-node-attribute { width: 200px; } } </style>
节点数据
XmindNode.vue<template> <span :style="{ opacity: dragNodeId === node.id ? 0.5 : 1 }"> <div class="vue-xmind-node" :class="{ topnode: currentId === node.id }" :style="nodeBoxStyle" :ref="'xmindbox' + node.id + 'ref'" @click.stop="onSelectCurrent(node)" > <div class="vue-xmind-content"> <div :id="node.id" :draggable="node.level !== 1 && !contenteditable" class="vue-xmind-name" :class="{ 'vue-xmind-edit': contenteditable }" :style="nodeBoxNameStyle" :ref="'xmindboxname' + node.id + 'ref'" :contenteditable="contenteditable" @blur.stop="onBlurSaveDate" @keydown="inputChecked" v-html="node.data" @dblclick.stop="openEditContent" ></div> <div class="mouse-zoom-move" v-if="currentId === node.id"> <mouse-direction-move id="x" height="100%" @translation="onTranslation" @translationEnd="onTranslationEnd" > <div class="mouse-zoom-box"></div> </mouse-direction-move> </div> </div> </div> <span class="vue-xmind-children" v-if="node.children && node.children.length" > <XmindNode v-for="item in node.children" :key="item.id" :level="level + 1" :ref="'xmind' + node.id" :node="item" :dwidth="dwidth" :boxStyle="boxStyle" :boxColorStyle="boxColorStyle" :currentId="currentId" :dragNodeId="dragNodeId" @updateNodeEnd="updateNodeEnd" @selectNodeId="onSelectCurrent" ></XmindNode> </span> </span> </template> <script> import { getLevelNodeDwidth, getLevelListDate } from './util' import MouseDirectionMove from './components/mouse-direction-move' export default { components: { MouseDirectionMove }, props: { node: { type: Object, default: () => { return {} }, }, level: { type: Number, default: 1, }, dragNodeId: String, currentId: String, dwidth: Array, boxStyle: Object, boxColorStyle: Object, }, name: 'XmindNode', watch: { node() { this.customWidth = this.node.ctw || 0 }, }, computed: { lineWidth() { return getLevelNodeDwidth(this.level, this.dwidth) }, nodeBoxNameStyle() { let padding = [this.boxStyle.ptb + 'px', this.boxStyle.plr + 'px'].join( ' ', ) let level = this.node.level let cwidth = 'auto' let lineHeight = getLevelListDate(level, this.boxStyle.lineSize) + 'px' let fontSize = getLevelListDate(level, this.boxStyle.fontSize) + 'px' let fontColor = getLevelListDate(level, this.boxColorStyle.fontColor) let borderColor = getLevelListDate( level, this.boxColorStyle.borderColor, ) let backgroundColor = getLevelListDate( level, this.boxColorStyle.backgroundColor, ) if (this.customWidth) { cwidth = this.customWidth + 'px' } let borderRadius = getLevelListDate(level, this.boxColorStyle.borderRadius) + 'px' if (this.currentId === this.node.id) { return { padding: padding, lineHeight: lineHeight, fontSize: fontSize, color: fontColor, backgroundColor: backgroundColor, borderColor: '#ff0000', borderRadius: borderRadius, width: cwidth, } } else { return { padding: padding, lineHeight: lineHeight, fontSize: fontSize, color: fontColor, backgroundColor: backgroundColor, borderColor: borderColor || backgroundColor, borderRadius: borderRadius, width: cwidth, } } }, nodeBoxStyle() { return { transform: 'translate3d(' + this.node.x + 'px,' + this.node.y + 'px,0)', } }, }, data() { return { contenteditable: false, customWidth: 0, } }, created() { this.customWidth = this.node.ctw || 0 }, methods: { onTranslationEnd() { this.node.ctw = this.customWidth this.$nextTick(() => { this.updateNodeBox() this.updateNodeEnd() }) }, onTranslation(t) { if (!this.customWidth) { this.customWidth = this.node.w } if (t.x + this.customWidth >= 40) { this.customWidth += t.x } }, onSelectCurrent(data) { this.$emit('selectNodeId', data) }, inputChecked(e) { // Backspace键8 F5键116 37~40方向箭头 Del键46 // if (e.keyCode === 8) { // return // } // if (e.keyCode === 13) { // e.preventDefault() // } }, openEditContent(node) { if (this.contenteditable) return let content = this.$refs['xmindboxname' + this.node.id + 'ref'] this.contenteditable = true this.$nextTick(() => { if (content) { if (content[0]) { content[0].focus() this.getInputSelection(content[0]) } else { content.focus() this.getInputSelection(content) } } }) }, /** * 获取输入的光标到字符串最后一位 * @param {obj} obj */ getInputSelection(obj) { // 处理光标问题 if (window.getSelection) { // ie11 10 9 ff safari // obj.focus(); //解决ff不获取焦点无法定位问题 let range = window.getSelection() // 创建range range.selectAllChildren(obj) // range 选择obj下所有子内容 range.collapseToEnd() // 光标移至最后 } else if (document.selection) { // ie10 9 8 7 6 5 let range = document.selection.createRange() // 创建选择对象 // var range = document.body.createTextRange(); range.moveToElementText(obj) // range定位到obj range.collapse(false) // 光标移至最后 range.select() } }, onBlurSaveDate(e) { this.contenteditable = false this.node.data = e.target.innerHTML || '内容' this.$nextTick(() => { this.updateNodeBox() this.updateNodeEnd() }) }, updateNodeEnd() { this.$emit('updateNodeEnd') }, updateNodeBox() { let nodeel = this.$refs['xmindbox' + this.node.id + 'ref'] if (nodeel) { this.node.h = nodeel.offsetHeight this.node.w = nodeel.offsetWidth } }, reSizeBox() { if (this.node.children && this.node.children.length) { let els = this.$refs['xmind' + this.node.id] if (els) { els.forEach((item) => { if (item.reSizeBox) { item.reSizeBox() } }) } } this.updateNodeBox() }, }, } </script> <style scoped lang="scss"> .node-box-edit { display: inline-block; position: absolute; top: 0; left: 0; z-index: 99; } .vue-xmind-node { display: inline-block; position: absolute; top: 0; left: 0; &.topnode { z-index: 999; } } .vue-xmind-name { border-radius: 6px; user-select: none; box-sizing: border-box; word-break: break-word; border: 2px solid #ffffff; &.vue-xmind-edit { box-shadow: 0px 0px 10px 0px rgba(42, 77, 138, 0.6); } /deep/ img { -webkit-user-drag: none; } } .mouse-zoom-move { position: absolute; top: 0; bottom: 0; right: 0px; display: inline-block; cursor: w-resize; .mouse-zoom-box { display: inline-block; // background: #ff0000; width: 10px; height: 100%; border-radius: 5px; } } .vue-xmind-content { display: inline-block; position: relative; } </style>
XmindSvgLine.vue
svg 背景先逻辑
<template> <svg :width="canvasSize.w + 'px'" :height="canvasSize.h + 'px'"> <g> <path v-for="(item, index) in pathList" :key="index" :d="item.path" :stroke="item.stroke" :stroke-width="item.strokeWidth" fill="none" /> </g> <g v-if="drogDate && startDrag"> <path :d="drogLine.path" stroke="$color-theme" stroke-width="1" fill="none" /> <rect width="30" height="15" rx="5" ry="5" :x="drogRect.x" :y="drogRect.y - 7" style="fill: $color-theme" /> </g> </svg> </template> <script> import { executeXmindNodeLineDate, getSpaceNodePath } from './util' export default { props: { node: { type: Object, default: () => { return {} }, }, lineStyle: Object, dragNodeId: String, canvasSize: Object, drogDate: Object, dwidth: Array, startDrag: Boolean, }, data() { return { pathList: [], drogRect: { x: 0, y: 0 }, drogLine: { path: '', }, } }, watch: { node() { this.updateSvgLine() }, lineStyle() { this.updateSvgLine() }, drogDate(v) { if (v && v.dpos) { this.drogRect.x = v.dpos[0] this.drogRect.y = v.dpos[1] let line = { tx: v.dpos[0], ty: v.dpos[1], x: v.dpos[2], y: v.dpos[3], } this.drogLine.path = getSpaceNodePath(line, this.lineStyle.type) } }, }, mounted() { this.updateSvgLine() }, methods: { updateSvgLine() { // console.log('updateSvgLine', this.node) if (this.node.cw) { this.pathList = executeXmindNodeLineDate(this.node, this.lineStyle) } else { this.pathList = [] } }, }, } </script> <style scoped lang="scss"></style>
标签:node,return,导图,currentId,编辑器,let,vue2,import,data From: https://www.cnblogs.com/nfan/p/17987234