可视化栅格布局设计-pc端(类excel操作设计)

185 阅读3分钟

UI还原分析

image_ui.png

从UI图上来看像极了excel,然后大部分操作也沿袭了excel

功能拆分

栅格布局功能 (1).png

如何绘制网格

  1. 采⽤ div 绝对定位的⽅式,通过 translate 移动⽅位进⾏⽹格的绘制
  2. Canvas绘制减少重排

./grid-layout/core/gridCvs.js

  // 初始化基础网格
  drawGrid ({ stepy, stepx, color, lineWidth }) {
    const { ctx, el } = this
    const { width, height } = el
    const gridsy = Object.keys(stepy).length
    const gridsx = Object.keys(stepx).length
    this.clear()
    ctx.beginPath()
    // rows
    for (let i = 0, j = 0; i < gridsy + 1; i++) {
      ctx.moveTo(0, j + 0.5)
      ctx.lineTo(width, j + 0.5)
      if (i === gridsy) {
        ctx.moveTo(0, j - 0.5)
        ctx.lineTo(width, j - 0.5)
      }
      if (i < gridsy) {
        j += stepy[i].height
      }
    }
    // cols
    for (let i = 0, j = 0; i < gridsx + 1; i++) {
      ctx.moveTo(j + 0.5, 0)
      ctx.lineTo(j + 0.5, height)
      if (i === gridsx) {
        ctx.moveTo(j - 0.5, 0)
        ctx.lineTo(j - 0.5, height)
      }
      if (i < gridsx) {
        j += stepx[i].width
      }
    }
    // 设置绘制颜色
    ctx.strokeStyle = color
    // ctx.strokeStyle = '#cccccc'
    // 设置绘制线段的宽度
    ctx.lineWidth = lineWidth
    // 绘制格网
    ctx.stroke()
    // 清除路径
    ctx.closePath()
  }

./grid-layout/core/cvsdraw.js

  // 绘制合并的单元格
  rect (clipFillBox, cb) {
    const { ctx } = this
    const {
      x, y, width, height, bgcolor
    } = clipFillBox
    ctx.save()
    ctx.beginPath()
    ctx.fillStyle = bgcolor || '#fff'
    // // 设置绘制颜色
    // ctx.strokeStyle = 'black'
    // // 设置绘制线段的宽度
    // ctx.lineWidth = 1
    const offsetWidth = (x + 1 + width) > this.cvsWidth ? 2 : 1
    const offsetHeight = (y + 1 + height) > this.cvsHeight ? 2 : 1
    ctx.rect(x + 1, y + 1, width - offsetWidth, height - offsetHeight)
    ctx.stroke()
    ctx.clip()
    ctx.fill()
    cb()
    ctx.restore()
  }

主文件

./grid-layout/LayoutGrid.vue

区域划分

46787020dc412f375e9352d2ed842fb.png

网格区

<div ref="xlayoutContent" class="xlayout-content">
  <!-- 网格绘制区 -->
  <canvas ref="canvasSheet"></canvas>
  <!-- 高亮选中区 -->
  <div class='xlayout-selectors' :style="xlayoutSelectors"></div>
  <!-- 高亮插入选区 -->
  <div class='add-row-selectors' :style="addRowSelectors"></div>
  <!-- 事件操作区 -->
  <div
    class='xlayout-overlay'
    @mousedown.self="overlayerMousedown"
    @mouseup.self="overlayerMouseup"
    @mousemove.self="overlayerMousemove"
  >
  </div>
</div>

左标尺区

./grid-layout/AssistRule

<AssistRule ref="vRuler" :isVertical="true" :cvsView="cvsView" :layoutController="layoutController" @resetSeleced="resetXlayoutSelectors" @updateRowItems='updateRowItems'/>

行操作区

<div class="rows-layout">
  <div class="row-index"
    v-for="(row, key, index) in rowItems" :key="index"
    :class="{'selectedRow': selectRowIndex === index}"
    :style="{height: row.height + 'px'}"
    @click="selectRow(row, index)"
    @contextmenu.prevent="openMenu(row, $event, index)"
  >
    {{index + 1}}
    <div class="drag-item"
      @mousedown.self="dragMousedown(row, $event, index)">
    </div>
  </div>
  <div class='assist-resizer' :style="dragInfo.assistResizerHighlight">
  </div>
  <div
    class='assist-rule-overlay'
    v-show="dragInfo.assistRuleOverlayShow"
    @mouseup.self="dragRuleMouseup"
    @mousemove.self="dragRuleMouseDownMove"
    :style="{width: canvasSheetWidth + 20 + 'px'}"
  ></div>
</div>

列操作区

<div class="cols-layout" :style="{width: canvasSheetWidth + 'px'}">
  <div class="col-index" v-for="index in 12" :key="index" :style="{width: (canvasSheetWidth / 12) + 'px'}">{{index}}</div>
</div>

核心操作类 ./grid-layout/core/controller.js

controller.png

constructor() {
    this.mRanges = new Mergeds(mRanges) // [CellRange, ...]
    this.rows = new Rows(rows)
    this.cols = new Cols({ gridsx, cvsView })
    ......
}

通过⿏标点击x,y坐标获取当前cell选区

controller.js => getCellRectByXY(x, y, isMoving)

// 通过鼠标点击x,y坐标获取当前cell选区
  getCellRectByXY (x, y, isMoving) {
    const { ri, top, height } = this.getCellRowByY.call(this, y)
    const { ci, left, width } = this.getCellColByX.call(this, x)
    let crLTWH = {}
    let cMerged = ''
    if (!isMoving) {
      cMerged = this.mRanges.getFirstIncludes(ri, ci)
      // 点击在已合并的选区上
      if (cMerged) {
        console.log('getCellRectByXY >>>>>> cMerged', cMerged)
        const { sri, sci, eri, eci } = cMerged.getCellRange
        crLTWH = this.getCellRangeLTWH({ sri, sci, eri, eci })
      }
    }
    return {
      ri, ci, left, top, width, height, ...crLTWH, cMerged
    }
  }

选区对象CellRange

class CellRange {
  // sri:开始行坐标  sci:开始列坐标  eri:结束行坐标 eci:结束行坐标
  #sri
  #sci
  #eri
  #eci
  constructor (sri, sci, eri, eci) {
    this.#sri = sri
    this.#sci = sci
    this.#eri = eri
    this.#eci = eci
  }
  
  // cell-index: ri, ci
  // cell-ref: A10
  // 当前cell是否完全包含在cellRange中,用于在已有合并选区中点击时候扩大选区
  isIncludes (...args) {
    let [ri, ci] = [0, 0]
    // 解析两种不同的入参
    if (args.length === 1) {
      [ci, ri] = expr2xy(args[0])
    } else if (args.length === 2) {
      [ri, ci] = args
    }
    return this.#sri <= ri && ri <= this.#eri && this.#sci <= ci && ci <= this.#eci
  }
  
  // 是否有交叉
  isIntersects (cr) {
    const ncr = cr.getCellRange
    return this.#sri <= ncr.eri &&
      this.#sci <= ncr.eci &&
      ncr.sri <= this.#eri &&
      ncr.sci <= this.#eci
  }

功能实现

./grid-layout/LayoutGrid.vue

1.普通点击选中 overlayerMousedown => overlayerMouseup

overlayerMousedown (evt) {
  this.selectRowIndex = -1
  this.dragInfo.isMouseDown = true
  if (!evt) return
  // console.log('overlayerMousedown >>>> evt',evt)
  const { layoutController, selectedRange } = this
  const { offsetX, offsetY } = evt
  const sRect = layoutController.getCellRectByXY(offsetX, offsetY)
  // 当前鼠标落下当前选区的信息
  console.log('overlayerMousedown >>>> sRect', sRect)
  const { ri, ci, width, height, left, top, cMerged } = sRect
  this.xlayoutSelectors = {
    display: 'block',
    width: `${width + 1}px`,
    height: `${height + 1}px`,
    left: `${left}px`,
    top: `${top}px`
  }
  this.cMerged = cMerged
  this.cCell = { ri, ci }
  if (selectedRange && selectedRange.cellRange) {
    this.selectedRange.cellRange = ''
  }
}

2.普通点击选中 overlayerMousedown => overlayerMousemove=> overlayerMouseup

overlayerMousemove (evt, pickRow) {
  let nCell
  let cCell
  const isPickRow = pickRow && pickRow.nCell && pickRow.cCell
  const { layoutController } = this
  if (isPickRow) {
    ({ nCell, cCell } = pickRow)
  } else {
    if (!this.dragInfo.isMouseDown) return
    const { offsetX, offsetY } = evt
    const sRect = layoutController.getCellRectByXY(offsetX, offsetY, true)
    const { ri: nri, ci: nci } = sRect
    // 不在布局容器中移动不执行下一步逻辑
    if (nri === -1 || nci === -1) return
    // 行列索引相同不执行逻辑 同一cell反复移动
    if (this.nCell.ri === nri && this.nCell.ci === nci) return
    this.nCell = { ri: nri, ci: nci }
    nCell = this.nCell
    // 计算当前热选区
    cCell = this.cCell // 点击位cell
  }

  const { ri: nri, ci: nci } = nCell || {}
  const { ri: cri, ci: cci } = cCell || {}

  // 计算当前热选区
  let _sri = ''
  let _sci = ''
  let _eri = ''
  let _eci = ''
  if (nri > cri) [_sri, _eri] = [cri, nri]
  else [_sri, _eri] = [nri, cri]
  if (nci > cci) [_sci, _eci] = [cci, nci]
  else [_sci, _eci] = [nci, cci]

  // 获得选区
  let cellRange = layoutController.mRanges.union(new CellRange(_sri, _sci, _eri, _eci))
  console.log('cellRange >>>>', cellRange)
  cellRange = layoutController.mRanges.union(cellRange)
  console.log('cellRange >>>>', cellRange)
  const { sri, eri, sci, eci } = cellRange.getCellRange
  // 计算划选范围
  const { rows, cols } = layoutController
  const crLeft = cols.getColLeftByCi(sci)
  const crTop = rows.getRowTopByRi(sri)
  const crHeight = rows.sumHeightBetween(sri, eri)
  const crWidth = cols.sumWidthBetweenCi(sci, eci)
  console.log('overlayerMousemove >>>> cellRange crLeft,crTop,crHeight,crWidth', crLeft, crTop, crHeight, crWidth)
  this.selectedRange = { sri, eri, crLeft, crTop, crHeight, crWidth, multi: cellRange.isMultiple(), cellRange }
  this.cMerged = ''
  // 高亮选区
  this.xlayoutSelectors = {
    display: 'block',
    width: `${crWidth + 1}px`,
    height: `${crHeight + 1}px`,
    left: `${crLeft}px`,
    top: `${crTop}px`
  }
}

合并/拆分

./grid-layout/index.vue

// 合并
mergeCRHandler () {
  if (this.actionsDtnDisable.merge) {
    return
  }
  // console.log('mergeCRHandler >>>>',this.$refs.layoutGrid)
  const { selectedRange, layoutController } = this.$refs.layoutGrid
  const { crLeft, crTop, crHeight, crWidth, multi, cellRange } = selectedRange || {}
  const { draw, mRanges } = layoutController || {}

  // 没有划动选区或者单cell不执行合并
  if (!selectedRange || !multi) return
  draw.rect({
    x: crLeft,
    y: crTop,
    width: crWidth,
    height: crHeight,
    bgcolor: '#fff'
  }, () => {
    // 更新之前记录
    this.record()
    // 存入已合并选区
    mRanges.addData(cellRange)
    this.refreshActionsDtnDisable()
  })
}

// 拆分
unMergeCRHandler () {
  if (this.actionsDtnDisable.unMerge) {
    return
  }
  console.log('unMergeCRHandler')
  const { layoutController: lc, cMerged, selectedRange } = this.$refs.layoutGrid
  const { mRanges } = lc
  const { multi, cellRange } = selectedRange
  // 更新之前记录
  this.record()
  if (cMerged || (multi && cellRange)) {
    mRanges.removeData(cellRange || cMerged)
    lc.fresh()
  }
  this.refreshActionsDtnDisable()
}

行操作

./grid-layout/LayoutGrid.vue

insertRowAfterHandler () { // 下增行
  const { layoutController: lc, proxyChildren } = this
  const { vRuler } = proxyChildren()
  // 记录
  // lc.record()
  this.$emit('record')
  // 功能操作
  lc.rows.insertRowBottom(this.selectedRange.eri, { height: 150 })
  // lc.rows.insertRowBottom(this.selectedRange.eri, { height: 100 })
  this.updateMRanges(this.selectedRange.eri, false)
  console.log('this.layoutType')
  console.log(this.layoutType)
  // 刷新
  lc.fresh()
  this.updateRowItems(lc.rows.getRowsHeight)
  // this.$emit('record')
  vRuler.fresh()

  // 插入行start
  // 获得选区
  const { layoutController } = this
  const { rows } = layoutController
  const crTop = rows.getRowTopByRi(this.selectedRange.eri + 1)
  // 高亮插入行选区
  this.addRowSelectors = {
    display: 'block',
    width: `${this.$refs.xlayoutContent.offsetWidth}px`,
    height: '150px',
    left: '0px',
    top: `${crTop}px`
  }
  setTimeout(() => {
    this.addRowSelectors = {
      display: 'none'
    }
  }, 500)
}

insertRowBeforeHandler () { // 上增行
  console.log('insertRowAfterHandler')
  console.log(this.selectedRange.sri)
  console.log(this.selectedRange.eri)
  const { layoutController: lc, proxyChildren } = this
  const { vRuler } = proxyChildren()
  // 记录
  // lc.record()
  this.$emit('record')

  // 功能操作
  lc.rows.insertRowTop(this.selectedRange.sri, { height: 150 })
  // lc.rows.insertRowTop(this.selectedRange.sri, { height: 100 })
  this.updateMRanges(this.selectedRange.sri, true)
  // 刷新
  lc.fresh()
  this.updateRowItems(lc.rows.getRowsHeight)
  vRuler.fresh()

  // 获得选区
  let cellRange = lc.mRanges.union(new CellRange(this.selectedRange.sri + 1, 0, this.selectedRange.eri + 1, 11))
  cellRange = lc.mRanges.union(cellRange)
  console.log('cellRange >>>>', cellRange)
  const { sri, eri, sci, eci } = cellRange.getCellRange
  // 计算划选范围
  const { rows, cols } = lc
  const crLeft = cols.getColLeftByCi(sci)
  const crTop = rows.getRowTopByRi(sri)
  const crHeight = rows.sumHeightBetween(sri, eri)
  const crWidth = cols.sumWidthBetweenCi(sci, eci)
  console.log('overlayerMousemove >>>> cellRange crLeft,crTop,crHeight,crWidth', crLeft, crTop, crHeight, crWidth)
  this.selectedRange = { sri, eri, crLeft, crTop, crHeight, crWidth, multi: cellRange.isMultiple(), cellRange }
  this.cMerged = ''
  // 高亮选区
  this.xlayoutSelectors = {
    display: 'block',
    width: `${crWidth + 1}px`,
    height: `${crHeight + 1}px`,
    left: `${crLeft}px`,
    top: `${crTop}px`
  }
  this.selectRowIndex = this.selectRowIndex + 1

  // 插入行start
  // 获得选区
  const addCrTop = rows.getRowTopByRi(this.selectedRange.sri - 1)
  // 高亮插入行选区
  this.addRowSelectors = {
    display: 'block',
    width: `${this.$refs.xlayoutContent.offsetWidth}px`,
    height: '150px',
    left: '0px',
    top: `${addCrTop}px`
  }
  setTimeout(() => {
    this.addRowSelectors = {
      display: 'none'
    }
  }, 500)
  // 插入行end
}

deleteRow () {
  MessageBox.confirm('您确定要删除行吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    console.log('deleteRow')
    console.log(this.selectedRange.sri)
    console.log(this.selectedRange.eri)
    const { layoutController: lc, resetSelect, proxyChildren } = this
    const { vRuler } = proxyChildren()
    // 记录
    // lc.record()
    this.$emit('record')
    // 功能操作 从后往前删 不然删除合并的最后行有bug
    for (let index = this.selectedRange.eri; index >= this.selectedRange.sri; index--) {
      console.log('delRowByRi')
      lc.rows.delRowByRi(index)
    }
    this.deleteAndUpdateMRanges(this.selectedRange.sri, this.selectedRange.eri)
    // 刷新
    lc.fresh()
    this.updateRowItems(lc.rows.getRowsHeight)
    resetSelect()
    vRuler.fresh()
  }).catch(() => {
    console.log('取消退出模板编辑')
  })
}

拖拉改变行高

./grid-layout/LayoutGrid.vue

<div class="rows-layout">
  <div class="row-index"
    v-for="(row, key, index) in rowItems" :key="index"
    :class="{'selectedRow': selectRowIndex === index}"
    :style="{height: row.height + 'px'}"
    @click="selectRow(row, index)"
    @contextmenu.prevent="openMenu(row, $event, index)"
  >
    {{index + 1}}
    <!-- 拖拉触发区域 -->
    <div class="drag-item"
      @mousedown.self="dragMousedown(row, $event, index)">
    </div>
  </div>
  <div class='assist-resizer' :style="dragInfo.assistResizerHighlight">
  </div>
  <!-- 拖拉移动区域 -->
  <div
    class='assist-rule-overlay'
    v-show="dragInfo.assistRuleOverlayShow"
    @mouseup.self="dragRuleMouseup"
    @mousemove.self="dragRuleMouseDownMove"
    :style="{width: canvasSheetWidth + 20 + 'px'}"
  ></div>
</div>

触发事件顺序 dragMousedown => dragRuleMouseDownMove =>dragRuleMouseup

dragMousedown (row, e, index) {
  const { layoutController } = this

  this.resetXlayoutSelectors()
  this.isMosueDown = true
  this.dragRuleCanMove = true
  this.dragRow = row
  this.cRowIndex = index

  this.dragInfo.offsetHeight = 0

  const top = layoutController.rows.getRowTopByRi(index)

  this.dragInfo.assistResizerHighlight = {
    display: 'block',
    width: `${this.cvsView.width + 20}px`,
    top: `${top + row.height + 20}px`
  }

  this.dragStartY = top + row.height + 20

  // 计算行信息位置
  const menuMinWidth = 105
  const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
  const offsetWidth = this.$el.offsetWidth // container width
  const maxLeft = offsetWidth - menuMinWidth // left boundary
  const left = e.clientX - offsetLeft + 15 // 15: margin right

  if (left > maxLeft) {
    this.dragInfo.rowInfoLeft = maxLeft
  } else {
    this.dragInfo.rowInfoLeft = left
  }

  this.dragInfo.rowInfoTop = e.pageY - 170

  this.rowHeights = layoutController.rows.getRowsHeight
  const cheight = this.rowHeights[this.cRowIndex].height
  this.dragInfo.curRowHeight = cheight
  if (this.dragInfo.curRowHeight === 10) {
    this.dragRuleCanMoveUp = false
  }

  this.dragInfo.rowInfoVisible = true

  this.dragInfo.assistRuleOverlayShow = true
},
dragRuleMouseDownMove: _.throttle(function (e) {
  console.log('dragRuleMouseDownMove')
  if (!this.isMosueDown) return
  if (!this.dragRuleCanMove) return
  const { layoutController } = this
  const top = layoutController.rows.getRowTopByRi(this.cRowIndex)
  const nHeight = e.offsetY
  // 10px拉动单位吸附
  const roundHeight = this.roundHeight = Math.round(nHeight / 10) * 10
  this.dragInfo.curRowHeight = roundHeight - 20 - top
  this.dragInfo.offsetHeight = roundHeight - this.dragStartY
  this.dragInfo.rowInfoTop = e.pageY - 170
  if (this.dragInfo.curRowHeight < 15) {
    this.dragInfo.curRowHeight = 10
    this.dragInfo.offsetHeight = 10 - layoutController.rows.getRowHeightByRi(this.cRowIndex)
    this.dragInfo.assistResizerHighlight = {
      display: 'block',
      width: `${this.cvsView.width + 20}px`,
      top: `${top + 20 + 10}px`
    }
    return
  }
  this.dragInfo.assistResizerHighlight = {
    display: 'block',
    width: `${this.cvsView.width + 20}px`,
    top: `${nHeight - 1}px`
  }
}, 50),
dragRuleMouseup (row, e) {
  this.selectRowIndex = -1
  this.isMosueDown = false
  this.dragRuleCanMove = true
  this.dragInfo.assistRuleOverlayShow = false
  this.dragInfo.assistResizerHighlight = {
    display: 'none'
  }
  const { layoutController } = this
  const nowy = this.roundHeight - 20 || 0
  if (this.dragInfo.offsetHeight === 0) {
    this.dragInfo.rowInfoVisible = false
    return
  } else {
    this.$emit('record')
  }
  this.rowTops = layoutController.rows.getRowsTops
  const crowTop = this.rowTops[this.cRowIndex].top
  // if (nowy - crowTop <= 0) {
  //   this.dragInfo.rowInfoVisible = false
  //   return
  // }
  console.log(this.rows)
  layoutController.rows.setRowByRi(this.cRowIndex, { height: this.dragInfo.curRowHeight })
  // 网格更新
  layoutController.fresh()
  this.updateRowItems(layoutController.rows.getRowsHeight)
  this.$refs.vRuler.fresh()
  this.dragInfo.rowInfoVisible = false
}

前进/后退

class History {
  undoStack
  redoStack
  static hInstance = null
  constructor () {
    // if (History.hInstance) return History.hInstance
    this.undoStack = []
    this.redoStack = []
    History.hInstance = this
  }
  
  // 增加撤销数据
  // 注意这个数据是操作数据进行更新之前的数据
  add (data) {
    if (!data) return
    this.undoStack.push(JSON.stringify(data))
    // 对表格进行操作之后恢复撤销置空 恢复撤销是针对撤销连贯操作 中间穿插其他操作 就不允许恢复撤销了
    this.redoStack = []
  }

  // 是否能撤销
  canUndo () {
    return this.undoStack.length > 0
  }

  // 是否能重做
  canRedo () {
    return this.redoStack.length > 0
  }

  // 后退
  undo (currentd, cb) {
    if (this.canUndo()) {
      // 当前数据压入前进栈
      this.redoStack.push(JSON.stringify(currentd))
      console.log('>>>> this.redoStack', this.redoStack)
      // 弹出撤销栈的最新数据 传入回调函数
      cb(JSON.parse(this.undoStack.pop()))
    } else {
      alert('没有撤销数据了')
    }
  }

  // 前进
  redo (currentd, cb) {
    if (this.canRedo()) {
      // 当前数据压入撤销栈
      this.undoStack.push(JSON.stringify(currentd))
      // 弹出重做栈的最新数据 传入回调函数
      cb(JSON.parse(this.redoStack.pop()))
    } else {
      alert('没有重做数据了')
    }
  }
}

关于输出结果 与 二次编辑思考

输出结果

layoutData: { // canvas 布局数据
    cols: { // 列
      gridsx: 12 // 多少列
    },
    rows: { // 行高和数量不限 超出滚动
      rowItems: {
        0: { height: 150 },
        1: { height: 150 },
        2: { height: 150 },
        3: { height: 150 },
        4: { height: 150 },
        5: { height: 150 },
        6: { height: 150 },
        7: { height: 150 },
        8: { height: 150 },
        9: { height: 150 },
        10: { height: 150 },
        11: { height: 150 }
      }
    },
    mRanges: ['A0:B0', 'C0:D0'] // 合并信息 其中A代表列'第0列' 
},

divAbsoluteLayoutData: { // canvas 转换成绝对定位div数据
    width: 1200, // 编辑布局时的基线宽度
    height: 1800, // 编辑布局时的基线高度
    layoutItems: [ // 容器列表
      {
        top: 100,
        left: 100,
        width: 100
        height: 100,
      }
    ]
}

二次编辑

1.数据存储与转换

reportInfo:

pageList: [
    containerList: {
        '0b598969-abe7-4140-837c-350ccd35fdf8': {
            componentIds: []
            image"base64"
            uuid"0b598969-abe7-4140-837c-350ccd35fdf8"
        }
    },
    // 同上布局模板输出数据
    pageLayoutInfo: {
        layoutData: [...]
        divAbsoluteLayoutData: [...]
    }

]

把containerList元素中的image字段同步到divAbsoluteLayoutData元素中

image.png

2.如何还原图例?

  • canvas上画图片
  • 再增加一个Absolute div层只做展示(目前采用方案)

LayoutGrid.vue 跟'高亮选中区' 等区域一样增加'报表二次编辑布局图例展示区'

image.png

  1. 如何保证操作后containerList元素中已有图例的uuid不变
  • layoutData 转 divAbsoluteLayoutData 需要改造 reportLayoutEdit.vue 中 save() 方法
const { layoutController: lc } = this.$refs.layoutGrid
      const divs = []
      const { cols, mRanges, rows } = lc.getData()

      const rowItemsHeightArry = []
      for (const i in rows.rowItems) {
        rowItemsHeightArry.push(rows.rowItems[i].height)
      }
      const colsItemsWidthArry = []
      for (const i in cols.colItems) {
        colsItemsWidthArry.push(cols.colItems[i].width)
      }
      for (const i in rows.rowItems) {
        const colTemp = []
        for (const j in cols.colItems) {
          colTemp.push({
            width: cols.colItems[j].width,
            height: rows.rowItems[i].height,
            top: _.dropRight(rowItemsHeightArry, rowItemsHeightArry.length - i).reduce((t, i) => { return t + i }, 0),
            left: j * cols.colItems[j].width,
            show: true
          })
        }
        
        // todo  与老的 divAbsoluteLayoutData 比较是否审查新的uuid
        divs.push(colTemp)
      }
      const merges = mRanges.map(item => {
        return mexpr2mxy(item)
      })

      const mergedivs = []
      merges.forEach(item => {
        const mergeInfo = {
          show: true,
          width: 0,
          height: 0
        }
        for (let i = item.sri; i <= item.eri; i++) {
          mergeInfo.top = divs[item.sri][item.sci].top
          mergeInfo.left = divs[item.sri][item.sci].left
          const opRow = divs[i]
          for (let j = item.sci; j <= item.eci; j++) {
            opRow[j].show = false
            mergeInfo.width += opRow[j].width
            mergeInfo.height += opRow[j].height
          }
        }
        mergeInfo.width = mergeInfo.width / (item.eri - item.sri + 1)
        mergeInfo.height = mergeInfo.height / (item.eci - item.sci + 1)
        // todo  与老的 divAbsoluteLayoutData 比较是否审查新的uuid
        mergedivs.push(mergeInfo)
      })
      const final = divs.flat().filter(item => item.show).concat(mergedivs)
      
      // 删除 不能统一生成uuid
      final.forEach(item => {
        item.uuid = uuidv4()
      })

4.哪些操作会影响老的divAbsoluteLayoutData中的元素top和height

  • 增加行
  • 删除行
  • 拖拉行