anvt / X6 參數(shù)配置

一、需要安裝的依賴
npm i element-ui -S
"@antv/layout": "^0.2.0",
"@antv/x6": "^1.1.1",
"vue": "^2.5.2",

二、代碼

<template>
  <div class='antv'>
    <el-container>
      <el-header>
        <el-button type="info" @click="zoomIn">放大</el-button>
        <el-button type="info" @click="zoomOut">縮小</el-button>
        <el-upload :show-file-list="false" ref="upload" :http-request="importData">
          <el-button type="info">導(dǎo)入</el-button>
        </el-upload>
        <el-button type="info" @click="exportData">導(dǎo)出</el-button>
        <el-button type="info" @click="changeLayout">自動布局</el-button>
      </el-header>
      <el-container>
        <el-aside>
          <div id="left" ref="left"></div>
        </el-aside>
        <el-main>
          <div id="area" ref="area"></div>
        </el-main>
      </el-container>
    </el-container>
    <el-drawer size="30%" title="情節(jié)屬性設(shè)置" :visible.sync="drawer" direction="rtl" :before-close="handleClose">
      <el-form id="form" :model="form" :rules="rules" ref="form">
        <el-form-item label="標(biāo)題" label-width="60px" prop="title">
          <el-input v-model="form.title" maxlength="12" placeholder="請輸入標(biāo)題內(nèi)容"></el-input>
        </el-form-item>
        <el-form-item label="文本" label-width="60px" prop="text">
          <el-input v-model="form.text" :autosize="{ minRows: 3, maxRows: 5}" type="textarea" maxlength="300" placeholder="請輸入文本內(nèi)容"></el-input>
        </el-form-item>
        <el-form-item label="分值" label-width="60px" prop="score">
          <el-input-number v-model="form.score" controls-position="right" :min="0" :max="100"></el-input-number>
        </el-form-item>
        <el-form-item label="評價" label-width="60px" prop="comment">
          <el-input v-model="form.comment" :autosize="{ minRows: 3, maxRows: 5}" type="textarea" maxlength="300" placeholder="請輸入評價內(nèi)容"></el-input>
        </el-form-item>
        <el-form-item label="情節(jié)" label-width="60px" prop="nextPieceMode">
          <el-select v-model="form.nextPieceMode" placeholder="請選擇情節(jié)展開的方式">
            <el-option label="用戶選擇" value="choose"></el-option>
            <el-option label="隨機(jī)進(jìn)行" value="random"></el-option>
          </el-select>
        </el-form-item>
      </el-form>
    </el-drawer>
  </div>
</template>
<script>
import { Addon, Graph, Shape } from '@antv/x6'
import { DagreLayout } from '@antv/layout'
export default {
  name: 'antv',
  components: {},
  data () {
    return {
      graph: null,
      nodeProps: {},//全部有效節(jié)點(非默認(rèn)拖拽的節(jié)點,修改過得節(jié)點)的數(shù)據(jù)對象
      node: null,//雙擊之后,選中的當(dāng)前節(jié)點對象
      drawer: false,
      form: {
        text: "",
        title: "",
        score: 20,
        comment: "",
        nextPieceMode: ""
      },
      rules: {
        title: [
          { required: true, message: '請輸入標(biāo)題', trigger: 'blur' },
          { min: 0, max: 12, message: '長度在1到12個字符', trigger: 'blur' }
        ],
        text: [
          { required: true, message: '請輸入文本', trigger: 'blur' },
          { min: 1, max: 300, message: '長度在1到300個字符', trigger: 'blur' }
        ],
        score: [
          { required: true, message: '得分不能為空' },
          { type: 'number', message: '得分必須是數(shù)字' },
          { pattern: /^(0|[1-9]\d?|100)$/, message: '范圍在0-100', trigger: 'blur' }
        ],
        comment: [
          { min: 1, max: 300, message: '長度在1到300個字符', trigger: 'blur' }
        ],
        nextPieceMode: [
          { required: true, message: '情節(jié)發(fā)展方式不能為空' }
        ]
      },
    }
  },
  created () { },
  mounted () {
    this.initGraph()
  },
  methods: {
    initGraph () {
      const left = this.$refs.left
      const area = this.$refs.area
      // Graph 是圖的載體
      const graph = new Graph({
        container: area,
        history: true,
        grid: true,//顯示坐標(biāo)點
        // 畫布縮放參數(shù)配置
        mousewheel: {
          enabled: true,
          zoomAtMousePosition: true,
          modifiers: 'ctrl',
          minScale: 0.5,
          maxScale: 5,
        },
        connecting: {
          router: {
            name: 'manhattan',
            args: {
              padding: 1,
            },
          },
          connector: {
            name: 'rounded',
            args: {
              radius: 8,
            },
          },
          anchor: 'center',
          connectionPoint: 'anchor',
          allowBlank: false,
          snap: {
            radius: 20,
          },
          createEdge () {
            return new Shape.Edge({
              attrs: {
                line: {
                  stroke: '#A2B1C3',
                  strokeWidth: 2,
                  targetMarker: {
                    name: 'block',
                    width: 12,
                    height: 8,
                  },
                },
              },
              zIndex: 0,
            })
          },
          validateConnection ({ targetMagnet }) {
            return !!targetMagnet
          },
        },
        highlighting: {
          magnetAdsorbed: {
            name: 'stroke',
            args: {
              attrs: {
                fill: '#5F95FF',
                stroke: '#5F95FF',
              },
            },
          },
        },
        resizing: true,
        rotating: true,
        // 是否可拖動
        panning: {
          enabled: true,
          eventTypes: ['leftMouseDown']
        },
        // 選擇器
        selecting: {
          enabled: true,
          rubberband: false,
          showNodeSelectionBox: true,
        },
        snapline: true,
        keyboard: true,
        clipboard: true,
      })
      this.graph = graph;

      // 左側(cè)可被拖拽的圖形節(jié)點所在的 “區(qū)域容器”
      const stencil = new Addon.Stencil({
        title: '流程圖',
        target: graph,
        stencilGraphWidth: 250,
        stencilGraphHeight: 180,
        collapsable: false,
        groups: [
          {
            title: '基礎(chǔ)流程圖',
            name: 'group1',
          },
        ],
        layoutOptions: {
          columns: 1,//區(qū)域容器每行顯示的節(jié)點數(shù)量
          columnWidth: 200,//區(qū)域容器的行寬
          rowHeight: 60,//區(qū)域容器的行高
        },
      })
      left.appendChild(stencil.container)

      // 注冊兩種圖形節(jié)點
      const ports = {
        groups: {
          top: {
            position: 'top',
            attrs: {
              circle: {
                r: 4,
                magnet: true,
                stroke: '#5F95FF',
                strokeWidth: 1,
                fill: '#fff',
                style: {
                  visibility: 'hidden',
                },
              },
            },
          },
          right: {
            position: 'right',
            attrs: {
              circle: {
                r: 4,
                magnet: true,
                stroke: '#5F95FF',
                strokeWidth: 1,
                fill: '#fff',
                style: {
                  visibility: 'hidden',
                },
              },
            },
          },
          bottom: {
            position: 'bottom',
            attrs: {
              circle: {
                r: 4,
                magnet: true,
                stroke: '#5F95FF',
                strokeWidth: 1,
                fill: '#fff',
                style: {
                  visibility: 'hidden',
                },
              },
            },
          },
          left: {
            position: 'left',
            attrs: {
              circle: {
                r: 4,
                magnet: true,
                stroke: '#5F95FF',
                strokeWidth: 1,
                fill: '#fff',
                style: {
                  visibility: 'hidden',
                },
              },
            },
          },
        },
        items: [
          {
            group: 'top',
          },
          {
            group: 'right',
          },
          {
            group: 'bottom',
          },
          {
            group: 'left',
          },
        ],
      }
      Graph.registerNode(
        'RectAi',
        {
          inherit: 'rect',
          width: 160,
          height: 36,
          attrs: {
            body: {
              strokeWidth: 1,
              stroke: '#5F95FF',
              fill: '#CCFFFF',
            },
            text: {
              fontSize: 12,
              fill: '#262626',
            },
          },
          ports: { ...ports },
        },
        true,
      )

      Graph.registerNode(
        'RectMe',
        {
          inherit: 'rect',
          width: 160,
          height: 36,
          attrs: {
            body: {
              strokeWidth: 1,
              stroke: '#5F95FF',
              fill: '#CCFFCC',
            },
            text: {
              fontSize: 12,
              fill: '#262626',
            },
          },
          ports: { ...ports },
        },
        true,
      )
      //  將兩種圖形節(jié)點添加到“區(qū)域容器”中
      const r1 = graph.createNode({
        shape: 'RectAi',
        label: 'AI情節(jié)',
      })
      const r2 = graph.createNode({
        shape: 'RectMe',
        label: '客戶情節(jié)',
      })
      stencil.load([r1, r2], 'group1')


      // 配置鍵盤快捷鍵
      // select all
      graph.bindKey(['meta+a', 'ctrl+a'], () => {
        const nodes = graph.getNodes()
        if (nodes) {
          graph.select(nodes)
        }
      })
      // copy
      graph.bindKey(['meta+c', 'ctrl+c'], () => {
        const cells = graph.getSelectedCells()
        if (cells.length) {
          graph.copy(cells)
        }
        return false
      })
      //   cut
      graph.bindKey(['meta+x', 'ctrl+x'], () => {
        const cells = graph.getSelectedCells()
        if (cells.length) {
          graph.cut(cells)
        }
        return false
      })
      //   paste
      graph.bindKey(['meta+v', 'ctrl+v'], () => {
        if (!graph.isClipboardEmpty()) {
          const cells = graph.paste({ offset: 32 })
          graph.cleanSelection()
          graph.select(cells)
        }
        return false
      })
      //delete
      graph.bindKey('backspace', () => {
        const cells = graph.getSelectedCells()
        if (cells.length) {
          graph.removeCells(cells)
        }
      })
      // 節(jié)點撤銷
      graph.bindKey(['meta+z', 'ctrl+z'], () => {
        if (graph.history.canUndo()) {
          graph.history.undo()
        }
        return false
      })
      // 節(jié)點撤銷后,的恢復(fù)操作
      graph.bindKey(['meta+shift+z', 'ctrl+shift+z'], () => {
        if (graph.history.canRedo()) {
          graph.history.redo()
        }
        return false
      })

      // 監(jiān)聽節(jié)點事件函數(shù)
      graph.on('node:removed', ({ node: curNode }) => {
        //   更新有效節(jié)點數(shù)據(jù)對象
        delete this.nodeProps[curNode.id];
      })
      // 節(jié)點雙擊事件  存儲當(dāng)前節(jié)點對象,把當(dāng)前節(jié)點對象存到nodeProps(有效節(jié)點數(shù)據(jù)對象)里
      graph.on('node:dblclick', ({ node: curNode }) => {
        this.node = curNode;

        if (this.nodeProps[this.node.id]) {
          this.form = this.nodeProps[this.node.id];
        } else {
          this.nodeProps[this.node.id] = {
            title: "",
            text: "",
            score: 20,
            comment: "",
            nextPieceMode: ""
          };
          this.form = this.nodeProps[this.node.id];
        }

        this.drawer = true;

      })

      // 控制全部節(jié)點對象的錨點 顯示/隱藏
      graph.on('node:mouseenter', () => {
        const ports = area.querySelectorAll('.x6-port-body')
        ports.forEach(port => {
          port.style.visibility = 'visible'
        });
      })
      graph.on('node:mouseleave', () => {
        const ports = area.querySelectorAll('.x6-port-body')
        ports.forEach(port => {
          port.style.visibility = 'hidden'
        });
      })
    },
    changeLayout () {
      let edges = []
      let nodes = []
      this.graph.toJSON().cells.forEach(cell => {
        if (cell.shape === "edge") {
          edges.push({
            source: cell.source.cell,
            target: cell.target.cell
          })
        } else {
          nodes.push(cell)
        }
      });
      const dagreLayout = new DagreLayout({
        type: 'dagre',
        rankdir: 'TB',
        align: 'DL',
        controlPoints: true,
      })

      const newModel = dagreLayout.layout({
        nodes: nodes,
        edges: edges
      })

      let nodeMap = {};
      newModel.nodes.forEach(node => {
        nodeMap[node.id] = node
        // 需要注意的是,布局算法返回的 x、y 其實是節(jié)點的中心點坐標(biāo),在 X6 中,節(jié)點的 x、y 其實是左上角坐標(biāo),所以布局之后,我們需要做一次坐標(biāo)轉(zhuǎn)換。
        node.x -= node.size.width / 2;
        node.y -= node.size.height / 2;
        console.log(node)
      })
      // 解決布局之后,節(jié)點之間的連線跑到節(jié)點中間去了,我們需要描點之間的連線
      newModel.edges.forEach(edge => {
        edge.source = {
          cell: edge.source,
          port: nodeMap[edge.source].ports.items[2].id
        }
        edge.target = {
          cell: edge.target,
          port: nodeMap[edge.target].ports.items[0].id
        }
        console.log(edge)
      })

      this.graph.fromJSON(newModel)
    },
    zoomIn () {
      let zoom1 = this.graph.zoom();//獲取當(dāng)前縮放級別
      let zoom2 = Math.min(5, zoom1 + 0.5); // 設(shè)置放大范圍,最大是5
      this.graph.zoom(zoom2 - zoom1);//在原來縮放級別上增加多少
    },
    zoomOut () {
      let zoom1 = this.graph.zoom();// 獲取當(dāng)前縮放級別
      let zoom2 = Math.max(0.5, zoom1 - 0.5);// 設(shè)置縮小范圍,最小是0.5
      this.graph.zoom(zoom2 - zoom1);//在原來縮放級別上減少多少
    },
    importData (file) {
      let that = this
      let reader = new FileReader() //new一個FileReader實例
      reader.readAsText(file.file)
      reader.onload = function (f) {
        //   讀取文件獲得的對象
        let data = JSON.parse(this.result)
        // 這兩行代碼是將獲取到的對象,轉(zhuǎn)化為節(jié)點對象
        that.nodeProps = data.nodeProps
        that.graph.fromJSON(data.cellInfo)
      }
    },
    exportData () {
      let data = {
        nodeProps: this.nodeProps,
        cellInfo: this.graph.toJSON()
      }

      //定義文件內(nèi)容,類型必須為Blob 否則createObjectURL會報錯
      let content = new Blob([JSON.stringify(data)])
      //生成url對象
      let urlObject = window.URL || window.webkitURL || window
      let url = urlObject.createObjectURL(content)
      //生成<a></a>DOM元素
      let el = document.createElement('a')
      //鏈接賦值
      el.href = url
      el.download = "劇情文件.txt"
      //必須點擊否則不會下載
      el.click()
      //移除鏈接釋放資源
      urlObject.revokeObjectURL(url)
      el.remove();
    },
    handleClose (done) {
      this.$refs.form.validate((result) => {
        if (result) {
          this.$message.success('驗證通過')
          this.node.label = this.form.title;
          done();
        } else {
          this.$message.error('表單驗證失敗,請?zhí)顚懻_后關(guān)閉')
        }
      })
    },
  },
}
</script>
<style lang='scss'>
.antv {
  height: 100vh;
  width: 100%;
  background-color: rgba($color: #333, $alpha: 0.1);
  .el-container {
    width: 100%;
    height: 100%;
    .el-header {
      border: 1px solid yellow;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      padding-left: 200px;
      .el-upload {
        margin-left: 10px;
        margin-right: 10px;
      }
    }
    .el-aside {
      width: 250px !important;
      box-sizing: border-box;
      height: 100%;
      border: 1px solid red;
      #left {
        width: 100%;
        height: 100%;
        position: relative;
      }
    }
    .el-main {
      height: 100%;
      width: 100%;
      border: 1px solid blue;
      #area {
        width: 100%;
        height: 100%;
      }
    }
  }
  .el-form {
    padding: 20px;
    .el-form-item__label {
      font-weight: normal;
      white-space: nowrap;
    }
  }
}
</style>

三、預(yù)覽圖


image.png

================================分割線==================================
四、新代碼

<template>
  <div class='antv'>
    <el-container>
      <el-header>
        <p style="color:red">客戶1、用戶1暫時無法演示</p>
        <el-button-group>
          <el-button type="primary" icon="el-icon-zoom-in" @click="zoomIn" plain>放大</el-button>
          <el-button type="primary" icon="el-icon-zoom-out" @click="zoomOut" plain>縮小</el-button>
        </el-button-group>
        <el-upload :show-file-list="false" ref="upload" :http-request="importData">
          <el-button type="primary" icon="el-icon-upload2" plain>上傳</el-button>
        </el-upload>
        <el-button type="primary" icon="el-icon-download" @click="exportData" plain>下載</el-button>
        <el-button type="primary" icon="el-icon-download" @click="exportSVG" plain>SVG</el-button>
        <el-button type="primary" icon="el-icon-s-fold" @click="changeLayout" plain>布局</el-button>
        <el-button type="primary" icon="el-icon-thumb" @click="handleChangePanning" plain>{{pannable?'拖拽':"框選"}}</el-button>
        <el-button type="danger" icon="el-icon-refresh" @click="sync" plain>同步</el-button>
        <el-button type="danger" icon="el-icon-close" @click="$emit('close')" plain>關(guān)閉</el-button>
      </el-header>
      <el-container>
        <el-aside>
          <div id="left" ref="left"></div>
        </el-aside>
        <el-main>
          <div id="area" ref="area"></div>
        </el-main>
      </el-container>
    </el-container>
    <el-drawer size="30%" title="情節(jié)屬性設(shè)置" :visible.sync="drawer" direction="rtl" :modal="false" :before-close="handleClose">
      <el-form id="form" :model="form" :rules="rules" ref="form">
        <el-form-item label="標(biāo)題" label-width="60px" prop="title">
          <el-input v-model="form.title" maxlength="20" placeholder="請輸入標(biāo)題內(nèi)容"></el-input>
        </el-form-item>
        <el-form-item label="文本" label-width="60px" prop="text">
          <el-input v-model="form.text" :autosize="{ minRows: 3, maxRows: 5}" type="textarea" maxlength="300" placeholder="請輸入文本內(nèi)容"></el-input>
        </el-form-item>
        <el-form-item label="分值" label-width="60px" prop="score">
          <el-input-number v-model="form.score" controls-position="right" :min="0" :max="100"></el-input-number>
        </el-form-item>
        <el-form-item label="評價" label-width="60px" prop="comment">
          <el-input v-model="form.comment" :autosize="{ minRows: 3, maxRows: 5}" type="textarea" maxlength="300" placeholder="請輸入評價內(nèi)容"></el-input>
        </el-form-item>
        <el-form-item label="情節(jié)" label-width="60px" prop="nextPieceMode">
          <el-select v-model="form.nextPieceMode" placeholder="請選擇情節(jié)展開的方式">
            <el-option label="用戶選擇" value="choose"></el-option>
            <el-option label="隨機(jī)進(jìn)行" value="random"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="備注" label-width="60px" prop="remark">
          <el-input v-model="form.remark" :autosize="{ minRows: 3, maxRows: 5}" type="textarea" maxlength="300" placeholder="請輸入備注內(nèi)容"></el-input>
        </el-form-item>
      </el-form>
    </el-drawer>
  </div>
</template>
<script>
import { Addon, Graph, Shape, ObjectExt, Vector, DataUri } from '@antv/x6'
import { DagreLayout } from '@antv/layout'
import { generateDramaData, getDramaData } from '@/api/api'

export default {
  name: 'antv',
  components: {},
  data () {
    return {
      graph: null,
      nodeProps: {},//全部有效節(jié)點(非默認(rèn)拖拽的節(jié)點,修改過得節(jié)點)的數(shù)據(jù)對象
      node: null,//雙擊之后,選中的當(dāng)前節(jié)點對象
      drawer: false,
      pannable: false,
      seeTitle: false,
      form: {
        name: "",
        text: "",
        title: "",
        score: 0,
        comment: "",
        speaker: "",
        nextPieceMode: "",
        remark: "",
      },
      rules: {
        title: [
          { required: true, message: '請輸入標(biāo)題', trigger: 'blur' },
          { min: 0, max: 20, message: '長度在1到20個字符', trigger: 'blur' }
        ],
        text: [
          { required: true, message: '請輸入文本', trigger: 'blur' },
          { min: 1, max: 500, message: '長度在1到500個字符', trigger: 'blur' }
        ],
        score: [
          { required: true, message: '得分不能為空' },
          { type: 'number', message: '得分必須是數(shù)字' },
          { pattern: /^(0|[1-9]\d?|100)$/, message: '范圍在0-100', trigger: 'blur' }
        ],
        comment: [
          { min: 1, max: 300, message: '長度在1到300個字符', trigger: 'blur' }
        ],
        nextPieceMode: [
          { required: true, message: '情節(jié)發(fā)展方式不能為空' }
        ]
      },
    }
  },
  created () {
    this.getDramaData()

  },
  mounted () {
    this.initGraph()
  },
  methods: {
    getDramaData () {
      getDramaData(this.$route.query.dramaId).then((res) => {
        if (res.data.jsonObjectData) {
          let cells = res.data.jsonObjectData.cellInfo.cells
          let isOld = false

          for(let index in cells) {
            let cell = cells[index]
            if (cell.shape === "examiner" || cell.shape === "user" || cell.shape === "aside") {
              isOld = true
              break
            }
          }

          if (isOld) {
            this.initFromOldData(res.data)
          } else {
            this.nodeProps = res.data.jsonObjectData.nodeProps
            this.graph.fromJSON(res.data.jsonObjectData.cellInfo)
            // 強(qiáng)制刷新所有內(nèi)容
            this.graph.getNodes().forEach(node => {
              // 設(shè)置新節(jié)點屬性
              this.form = this.nodeProps[node.id]
              this.node = node
              this.seeTitle = node.shape.startsWith("user") ? true : false
              this.handleClose(() => {})
            })
          }
        }
      })
    },
    sync () {
      this.$confirm('你確定保存該劇本下的這些情節(jié)嗎?', '提示', {
        confirmButtonText: '保存',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {   // 確定操作
        //開啟loading
        const load = this.$loading({
          lock: true,
          text: 'Loading',
          spinner: 'el-icon-loading',
          background: 'rgba(0, 0, 0, 0.7)'
        });
        generateDramaData({
          dramaId: this.$route.query.dramaId,
          cellInfo: this.graph.toJSON(),
          nodeProps: this.nodeProps
        }).then((res) => {
          //關(guān)閉loading
          load.close();
          this.$emit('sync')
        })
      })
    },
    initGraph () {
      const left = this.$refs.left
      const area = this.$refs.area
      // Graph 是圖的載體
      const graph = new Graph({
        container: area,

        history: true,
        grid: true,//顯示坐標(biāo)點

        // 畫布滾動設(shè)置
        scroller: {
          enabled: true,
          pannable: this.pannable,
          pageVisible: true,
          pageBreak: false,
        },
        // 畫布縮放參數(shù)配置
        mousewheel: {
          enabled: true,
          zoomAtMousePosition: true,
          modifiers: ['ctrl', 'meta'],
          minScale: 0.5,
          maxScale: 5,
        },
        connecting: {
          router: {
            name: 'manhattan',
            args: {
              padding: 1,
            },
          },
          connector: {
            name: 'rounded',
            args: {
              radius: 8,
            },
          },
          anchor: 'center',
          connectionPoint: 'anchor',
          allowBlank: false,
          snap: {
            radius: 20,
          },
          createEdge () {
            return new Shape.Edge({
              attrs: {
                line: {
                  stroke: '#A2B1C3',
                  strokeWidth: 2,
                  targetMarker: {
                    name: 'block',
                    fill: 'blue',
                    width: 12,
                    height: 8,
                  },
                },
              },
              zIndex: 0,
            })
          },
          validateConnection ({ targetMagnet }) {
            return !!targetMagnet
          },
        },
        highlighting: {
          magnetAdsorbed: {
            name: 'stroke',
            args: {
              attrs: {
                fill: '#5F95FF',
                stroke: '#5F95FF',
              },
            },
          },
        },
        resizing: true,
        rotating: false,
        // 選擇器
        selecting: {
          enabled: true,
          rubberband: !this.pannable, // 啟用框選
          multiple: true,   // 啟用多選
          movable: true,    // 多選可同時移動
          // showNodeSelectionBox: true,
          // showEdgeSelectionBox: true,
          showNodeSelectionBox: false,
          showEdgeSelectionBox: true,
        },
        snapline: true,
        keyboard: true,
        clipboard: true,
      })
      this.graph = graph;

      // 左側(cè)可被拖拽的圖形節(jié)點所在的 “區(qū)域容器”
      const stencil = new Addon.Stencil({
        title: '流程圖',
        target: graph,
        stencilGraphWidth: 250,
        stencilGraphHeight: 500,
        collapsable: false,
        groups: [
          {
            title: '基礎(chǔ)流程圖',
            name: 'group1',
            collapsable: false
          },
        ],
        layoutOptions: {
          columns: 1,//區(qū)域容器每行顯示的節(jié)點數(shù)量
          columnWidth: 220,//區(qū)域容器的行寬
          rowHeight: 100,//區(qū)域容器的行高
        },
      })

      left.appendChild(stencil.container)

      // 構(gòu)建圖形連接樁
      let buildBasePort = function (pos) {
        return {
          position: pos,
          // position: {
          //   name: "absolute"
          // },
          attrs: {
            circle: {
              r: 4,
              magnet: true,
              stroke: '#5F95FF',
              strokeWidth: 1,
              fill: '#fff',
              style: {
                visibility: 'hidden',
              },
            },
          },
        }
      }

      // 注冊兩種圖形節(jié)點
      const ports = {
        groups: {
          top: buildBasePort("top"),
          right: buildBasePort("right"),
          bottom: buildBasePort("bottom"),
          left: buildBasePort("left"),
        },
        items: [
          {
            group: 'top',
          },
          {
            group: 'right',
          },
          {
            group: 'bottom',
          },
          {
            group: 'left',
          },
        ],
      }

      let buildNode = function (color, name, title, content) {
        return {
          inherit: 'rect',
          markup: [
            {
              tagName: 'rect',
              selector: 'body',
            },
            {
              tagName: 'rect',
              selector: 'nameRect',
            },
            {
              tagName: 'rect',
              selector: 'titleRect',
            },
            {
              tagName: 'rect',
              selector: 'contentRect',
            },
            {
              tagName: 'text',
              selector: 'nameText',
            },
            {
              tagName: 'text',
              selector: 'titleText',
            },
            {
              tagName: 'text',
              selector: 'contentText',
            },
          ],
          attrs: {
            rect: {
              width: 220,
            },
            body: {
              stroke: '#fff',
            },
            nameRect: {
              fill: color,
              stroke: '#fff',
              strokeWidth: 0.5,
            },
            titleRect: {
              fill: '#eff4ff',
              stroke: '#fff',
              strokeWidth: 0.5,
            },
            contentRect: {
              fill: '#eff4ff',
              stroke: '#fff',
              strokeWidth: 0.5,
            },
            nameText: {
              ref: 'nameRect',
              refY: 0.5,
              refX: 0.5,
              textAnchor: 'middle',
              fontWeight: 'bold',
              fill: '#fff',
              fontSize: 12,
            },
            titleText: {
              ref: 'titleRect',
              refY: 0.5,
              refX: 5,
              textAnchor: 'left',
              fill: 'black',
              fontSize: 10,
              
            },
            contentText: {
              ref: 'contentRect',
              refY: 0.5,
              refX: 5,
              textAnchor: 'left',
              fill: 'black',
              fontSize: 10,
              textWrap: {
                width: 210,
                height: 54,
                ellipsis: true,
              },
            },
          },
          ports: {...ports},
          propHooks(meta) {
            const { ...others } = meta
            
            let offsetY = 0;
            ObjectExt.setByPath(others, `attrs/nameText/text`, name)
            ObjectExt.setByPath(others, `attrs/nameRect/height`, 28)
            ObjectExt.setByPath(others, `attrs/nameRect/transform`,'translate(0,0)')
            offsetY += 28

            if (meta.shape.startsWith("user")) {
              ObjectExt.setByPath(others, `attrs/titleText/text`, title)
              ObjectExt.setByPath(others, `attrs/titleRect/height`, 28)
              ObjectExt.setByPath(others, `attrs/titleRect/transform`,'translate(0,' + offsetY + ')')
              offsetY += 28
            }
            
            const maxContentLines = 3
            const lineLength = 20
            const lines = Math.min(Math.max(1, Math.ceil(content.length * 1.0 / lineLength)), maxContentLines)
            const contentHeight = 12 * lines + 16

            ObjectExt.setByPath(others, `attrs/contentText/text`, content)
            ObjectExt.setByPath(others, `attrs/contentRect/height`, contentHeight)
            ObjectExt.setByPath(others, `attrs/contentRect/transform`,'translate(0,' + offsetY + ')')

            others.size = { width: 220, height: offsetY + contentHeight }
            return others
          },
        }
      }

      Graph.registerNode('customer0', buildNode('#5f95ff', '客戶_0', "", "內(nèi)容"), true)
      Graph.registerNode('customer1', buildNode('#5f95ff', '客戶_1', "", "內(nèi)容"), true)
      Graph.registerNode('user0', buildNode('#ff8000', '用戶_0', "標(biāo)題", "內(nèi)容"), true)
      Graph.registerNode('user1', buildNode('#ff8000', '用戶_1', "標(biāo)題", "內(nèi)容"), true)
      Graph.registerNode('voiceOver', buildNode('#006600', '旁白', "", "內(nèi)容"), true)


      const r00 = graph.createNode({"shape": "customer0"})
      const r01 = graph.createNode({"shape": "customer1"})
      const r10 = graph.createNode({"shape": "user0"})
      const r11 = graph.createNode({"shape": "user1"})
      const r20 = graph.createNode({"shape": "voiceOver"})

      // 加入模板節(jié)點
      stencil.load([r00, r01, r10, r11, r20], 'group1')


      // 配置鍵盤快捷鍵
      // select all
      graph.bindKey(['meta+a', 'ctrl+a'], () => {
        const nodes = graph.getNodes()
        if (nodes) {
          graph.select(nodes)
        }
      })
      // copy
      graph.bindKey(['meta+c', 'ctrl+c'], () => {
        const cells = graph.getSelectedCells()
        if (cells.length) {
          graph.copy(cells)
        }
        return false
      })
      //   cut
      graph.bindKey(['meta+x', 'ctrl+x'], () => {
        const cells = graph.getSelectedCells()
        if (cells.length) {
          graph.cut(cells)
        }
        return false
      })
      //   paste
      graph.bindKey(['meta+v', 'ctrl+v'], () => {
        if (!graph.isClipboardEmpty()) {
          const cells = graph.paste({ offset: 32 })
          graph.cleanSelection()
          graph.select(cells)
        }
        return false
      })
      //delete
      graph.bindKey('backspace', () => {
        const cells = graph.getSelectedCells()
        if (cells.length) {
          graph.removeCells(cells)
        }
      })
      // 節(jié)點撤銷
      graph.bindKey(['meta+z', 'ctrl+z'], () => {
        if (graph.history.canUndo()) {
          graph.history.undo()
        }
        return false
      })
      // 節(jié)點撤銷后,的恢復(fù)操作
      graph.bindKey(['meta+shift+z', 'ctrl+shift+z'], () => {
        if (graph.history.canRedo()) {
          graph.history.redo()
        }
        return false
      })

      graph.on('node:added', ({ node: curNode }) => {
        //   初始化數(shù)據(jù)
        this.nodeProps[curNode.id] = {
          name: curNode.attrs.nameText.text,
          title: curNode.attrs.titleText.text,
          text: curNode.attrs.contentText.text,
          score: 0,
          comment: "",
          nextPieceMode: "choose",
          speaker: curNode.shape
        };

        this.form = this.nodeProps[curNode.id]
        this.node = curNode
      })
      // 監(jiān)聽節(jié)點事件函數(shù)
      graph.on('node:removed', ({ node: curNode }) => {
        //   更新有效節(jié)點數(shù)據(jù)對象
        delete this.nodeProps[curNode.id];
      })
      // 節(jié)點雙擊事件  存儲當(dāng)前節(jié)點對象,把當(dāng)前節(jié)點對象存到nodeProps(有效節(jié)點數(shù)據(jù)對象)里
      graph.on('node:dblclick', ({ node: curNode }) => {
        this.node = curNode;
        this.seeTitle = this.node.shape.startsWith('user')
        this.form = this.nodeProps[this.node.id];
        this.drawer = true;
      })

      // 單機(jī)節(jié)點時  展示數(shù)據(jù)流向
      graph.on('node:click', ({ node: curNode }) => {
        let outEdges = this.graph.getOutgoingEdges(curNode)
        if (outEdges) {
          outEdges.forEach(edge => {
            let view = this.graph.findViewByCell(edge);
            let token = Vector.create('circle', { r: 6, fill: 'green' })
            let stop = view.sendToken(token.node, 1000, () => { })
            // 2s 后停止該動畫
            setTimeout(stop, 2000)
          })
        }

        let inEdges = this.graph.getIncomingEdges(curNode)
        if (inEdges) {
          inEdges.forEach(edge => {
            let view = this.graph.findViewByCell(edge);
            let token = Vector.create('circle', { r: 6, fill: 'red' })
            let stop = view.sendToken(token.node, 1000, () => { })
            // 2s 后停止該動畫
            setTimeout(stop, 2000)
          })
        }
      })

      // 控制全部節(jié)點對象的錨點 顯示/隱藏
      graph.on('node:mouseenter', () => {
        const ports = area.querySelectorAll('.x6-port-body')
        ports.forEach(port => {
          port.style.visibility = 'visible'
        });
      })
      graph.on('node:mouseleave', () => {
        const ports = area.querySelectorAll('.x6-port-body')
        ports.forEach(port => {
          port.style.visibility = 'hidden'
        });
      })
    },

    // TODO 臨時用
    initFromOldData(data) {
      this.nodeProps = data.jsonObjectData.nodeProps
      let cells = data.jsonObjectData.cellInfo.cells

      let edges = []
      let nodes = []

      for (let index in cells) {
        let cell = cells[index]
        if (cell.shape === "edge") {
          edges.push(cell)
        } else {
          nodes.push(cell)
        }
      }

      let newNodes = []
      let newNodeMap = {}
      nodes.forEach(node => {
        let shape = null
        if (node.shape === "user") {
          this.nodeProps[node.id].name = "用戶_0"
          this.nodeProps[node.id].speaker = "user0"
          shape = "user0"
        }
        if (node.shape === "examiner") {
          this.nodeProps[node.id].name = "客戶_0"
          this.nodeProps[node.id].title = ""
          this.nodeProps[node.id].speaker = "customer0"
          shape = "customer0"
        }
        if (node.shape === "aside") {
          this.nodeProps[node.id].name = "旁白"
          this.nodeProps[node.id].title = ""
          this.nodeProps[node.id].speaker = "voiceOver"
          shape = "voiceOver"
        }


        // 創(chuàng)建新節(jié)點
        let newNode = this.graph.createNode({
          "id": node.id,
          "shape": shape
        })
        
        
        newNode.attrs.nameText.text = this.nodeProps[node.id].name
        newNode.attrs.titleText.text = this.nodeProps[node.id].title
        newNode.attrs.contentText.text = this.nodeProps[node.id].text

        let newNodeJson = newNode.toJSON()
        newNodes.push(newNodeJson)
        newNodeMap[node.id] = newNodeJson
      })

      // 重新規(guī)劃布局的點  source取bottom點, target取top點
      edges.forEach(edge => {
        edge.source.port = newNodeMap[edge.source.cell].ports.items[2].id
        edge.target.port = newNodeMap[edge.target.cell].ports.items[0].id
      })
      
      let dagreLayout = new DagreLayout({
        type: 'dagre',
        rankdir: 'TB',
        align: 'DL',
        controlPoints: true,
      })
      let newModel = dagreLayout.layout({
        nodes: newNodes,
        edges: edges
      })

      // 坐標(biāo)轉(zhuǎn)換
      newModel.nodes.forEach(node => {
        node.x -= node.size.width / 2;
        node.y -= node.size.height / 2;
      })

      this.graph.fromJSON(newModel)

      // 強(qiáng)制刷新所有內(nèi)容
      this.graph.getNodes().forEach(node => {
        // 設(shè)置新節(jié)點屬性
        this.form = this.nodeProps[node.id]
        this.node = node
        this.seeTitle = node.shape.startsWith("user") ? true : false
        this.handleClose(() => {})
      })
    },

    changeLayout () {
      let edges = []
      let nodes = []
      let nodeMap = {}
      this.graph.toJSON().cells.forEach(cell => {
        if (cell.shape === "edge") {
          edges.push(cell)
        } else {
          nodes.push(cell)
          nodeMap[cell.id] = cell
        }
      });

      let dagreLayout = new DagreLayout({
        type: 'dagre',
        rankdir: 'TB',
        align: 'DL',
        controlPoints: true,
      })

      // 重新規(guī)劃布局的點  source取bottom點, target取top點
      edges.forEach(edge => {
        edge.source.port = nodeMap[edge.source.cell].ports.items[2].id
        edge.target.port = nodeMap[edge.target.cell].ports.items[0].id
      })

      let newModel = dagreLayout.layout({
        nodes: nodes,
        edges: edges
      })

      newModel.nodes.forEach(node => {
        // 需要注意的是,布局算法返回的 x、y 其實是節(jié)點的中心點坐標(biāo),在 X6 中,節(jié)點的 x、y 其實是左上角坐標(biāo),所以布局之后,我們需要做一次坐標(biāo)轉(zhuǎn)換。
        node.x -= node.size.width / 2;
        node.y -= node.size.height / 2;
      })

      this.graph.fromJSON(newModel)
    },
    zoomIn () {
      let zoom1 = this.graph.zoom();//獲取當(dāng)前縮放級別
      let zoom2 = Math.min(5, zoom1 + 0.2); // 設(shè)置放大范圍,最大是5
      this.graph.zoom(zoom2 - zoom1);//在原來縮放級別上增加多少
    },
    zoomOut () {
      let zoom1 = this.graph.zoom();// 獲取當(dāng)前縮放級別
      let zoom2 = Math.max(0.2, zoom1 - 0.2);// 設(shè)置縮小范圍,最小是0.2
      this.graph.zoom(zoom2 - zoom1);//在原來縮放級別上減少多少
    },
    importData (file) {
      let that = this
      let reader = new FileReader() //new一個FileReader實例
      reader.readAsText(file.file)
      reader.onload = function (f) {
        //   讀取文件獲得的對象
        let data = JSON.parse(this.result)
        // 這兩行代碼是將獲取到的對象,轉(zhuǎn)化為節(jié)點對象
        that.nodeProps = data.nodeProps
        that.graph.fromJSON(data.cellInfo)
      }
    },
    exportData () {
      let data = {
        nodeProps: this.nodeProps,
        cellInfo: this.graph.toJSON()
      }

      //定義文件內(nèi)容,類型必須為Blob 否則createObjectURL會報錯
      let content = new Blob([JSON.stringify(data)])
      //生成url對象
      let urlObject = window.URL || window.webkitURL || window
      let url = urlObject.createObjectURL(content)
      //生成<a></a>DOM元素
      let el = document.createElement('a')
      //鏈接賦值
      el.href = url
      el.download = "劇情文件.txt"
      //必須點擊否則不會下載
      el.click()
      //移除鏈接釋放資源
      urlObject.revokeObjectURL(url)
      el.remove();
    },

    exportSVG() {
      this.graph.toSVG((dataUri) => {
          // 下載
          DataUri.downloadDataUri(DataUri.svgToDataUrl(dataUri), '劇本圖片.svg')
        }, {
          preserveDimensions: true,
          copyStyles: false
        })
    },

    handleChangePanning () {
      this.pannable = !this.pannable
      if (this.pannable) {
        // 可以拖拽,取消框選
        this.graph.enablePanning()
        this.graph.disableSelection()
      } else {
        // 不可拖拽,打開框選
        this.graph.disablePanning()
        this.graph.enableSelection()
      }
    },
    handleClose (done) {
      let maxContentLines = 3
      let lineLength = 20
      let lines = Math.min(Math.max(1, Math.ceil(this.form.text.length * 1.0 / lineLength)), maxContentLines)
      let contentHeight = 12 * lines + 16

      let newAttrs = {
        "nameText": {
          "text": this.form.name
        },
        "contentText": {
          "text": this.form.text
        },
        "contentRect": {
          "height": contentHeight
        }
      }
      let offsetY = 28
      if (this.seeTitle) {
        newAttrs['titleText'] = {"text" : this.form.title}
        offsetY = 56
      } else {
        this.form.title = ""
      }

      this.node.updateAttrs(ObjectExt.merge({}, this.node.attrs, newAttrs), {silent: false})
      this.node.size(220, offsetY + contentHeight);

      // 強(qiáng)制刷新該節(jié)點相關(guān)的連線
      let outEdges = this.graph.getOutgoingEdges(this.node)
      let incomingEdges = this.graph.getIncomingEdges(this.node)
      if (outEdges) {
          outEdges.forEach(edge => {
            edge.updateAttrs(ObjectExt.merge({}, this.node.attrs, {}), {silent:false})
          })
      }
      if (incomingEdges) {
        incomingEdges.forEach(edge => {
            edge.updateAttrs(ObjectExt.merge({}, this.node.attrs, {}), {silent:false})
          })
      }
      done();
    },
  }
}
</script>
<style lang='scss'>
.antv {
  height: 100vh;
  width: 100%;
  background-color: #fff;
  .x6-widget-stencil {
    background-color: #fff;
  }
  .x6-widget-transform {
    margin: -1px 0 0 -1px;
    padding: 0px;
    border: 1px solid #239edd;
  }
  .x6-widget-transform > div {
    border: 1px solid #239edd;
  }
  .x6-widget-transform > div:hover {
    background-color: #3dafe4;
  }
  .x6-widget-transform-active-handle {
    background-color: #3dafe4;
  }
  .x6-widget-transform-resize {
    border-radius: 0;
    visibility: hidden; // 設(shè)置resize的節(jié)點不可見
  }
  .x6-widget-selection-inner {
    border: 1px solid #239edd;
  }
  .x6-widget-selection-box {
    opacity: 1;
  }
  .el-container {
    width: 100%;
    height: 100%;
    .el-header {
      border: 1px solid yellow;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      padding-left: 200px;
      .el-upload {
        margin-left: 10px;
        margin-right: 10px;
      }
    }
    .el-aside {
      width: 250px !important;
      box-sizing: border-box;
      height: 100%;
      border: 1px solid red;
      #left {
        width: 100%;
        height: 100%;
        position: relative;
      }
    }
    .el-main {
      height: 100%;
      width: 100%;
      border: 1px solid blue;
      #area {
        width: 100%;
        height: 100%;
      }
    }
  }
  .el-form {
    padding: 20px;
    .el-form-item__label {
      font-weight: normal;
      white-space: nowrap;
    }
  }
}
</style>

五、新預(yù)覽圖


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容