vue2 手写思维导图编辑器,支持图片和节点拖拽(2)

弹框模块

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>

  

posted @ 2024-01-25 15:13  小凡的天空  阅读(70)  评论(0编辑  收藏  举报