1. 程式人生 > 其它 >jsonarray 的put功能失效_sortblejs 將 excel 表格透視部分功能搬運到 web 上面(最初版本)...

jsonarray 的put功能失效_sortblejs 將 excel 表格透視部分功能搬運到 web 上面(最初版本)...

技術標籤:jsonarray 的put功能失效

這個功能其實嘗試了很多周了,磕磕絆絆的,知道昨天 12-27 週五才完成,為什麼呢,因為之前的需求,設計稿都不是很明確,之前寫的例子好像報廢了,為什麼呢?接下來我們看看碰到什麼樣的問題,如果還對 sortablejs 不是很瞭解的話,可以看我之前的文章

子由風:sortable.js 的研究​zhuanlan.zhihu.com zhihu-card-default.svg
  1. sortablejs 的拖拽 new Sortble(el, {}),只能是在原本的區域中,超出這個區域貌似 onMove 事件會失效,沒法獲取到 evt.originalEvent.clientX, evt.originalEvent.clientY

2. sortblejs 如果某個區域一旦設定了 pull="clone", 這個表示可以 clone 你當前拖拽會導致事件失效,按照圖上所示的話,如果將紅色方框的內容統一佈局在 一個 li 標籤的話,一旦拖動 clone 某一項,會導致 checkbox 的change 事件失效,也不知道什麼原因,找不到解決方法,只能重新改掉佈局方式,讓 checkbox 單獨成為一列,使用 flex 佈局方式,讓左右成為一行,這樣就能解決拖動 clone問題

d64ea87baeed57565580916e906de669.png
<div class="create-dialysis-table-columns-content">
    <ul class="create-dialysis-table-columns-ul" data-id="columns">
        <li
            :data-alias="item.name"
            :data-field="item.field"
            :data-id="item.id"
            :data-type="item.type"
            :data-index="index"
            :key="index"
             v-for="(item, index) in columns"
         >
            <div class="left">
                 <i class="drag-icon"></i>
                 <span class="column">{{ item.name }}</span>
            </div>
       </li>
     </ul>
     <ul class="create-dialysis-table-columns-checkbox" data-id="checkbox">
         <li
            :data-alias="item.name"
            :data-field="item.field"
            :data-id="item.id"
            :data-type="item.type"
            :data-index="index"
            :class="`columns-${item.id}`"
            :key="index"
            v-for="(item, index) in columns"
          >
             <el-checkbox :key="index" @change.native="columnsUlChange($event, item, index)"></el-checkbox>
         </li>
      </ul>
</div>

4. 之前 demo 的是採用直接拖拽放置的方式,是直接把 DOM 放進某個區域的,現在由於設計稿變化,我們無法採用之前 demo 中的例子,只能重新思考採用別的方式,經過我們的討論,以及我在實施的時候,發現我們必須把資料都定義在 data- 屬性裡面,採用假的拖動方式,然後獲取到 data- 屬性資料,存進陣列,再進行渲染

<li
   :data-alias="item.name"
   :data-field="item.field"
   :data-id="item.id"
   :data-type="item.type"
   :key="index"
   v-for="(item, index) in rows"
   >
                  <div class="left">
                    <i class="drag-icon"></i>
                    <span class="column">{{ item.name }}</span>
                  </div>
</li>

5. 之前的 demo 中在拖拽項溢位某個區域的時候,是啟動了 removeSpill 屬性,發現也是有很多問題,這個屬性還必須結合 onSpill 方法,但是實在是太不靈活了,一旦 removeSpill 被禁用這個 onSpill 方法就沒法使用了,所以經過我的思考,我只能使用脫離原本的區域,left,top,right,bottom,

    dragColumnsUl() {
      // 這裡的 this 指向 vue 例項
      const _this = this
      const oColumnsUl = document.querySelector(
        'ul.create-dialysis-table-columns-ul',
      ) // 好像這一步也是非同步的
      const oRowsContent = document.querySelector(
        '.create-dialysis-table-row-content',
      )
      const oValuesContent = document.querySelector(
        '.create-dialysis-table-value-content',
      )
      const sortable = new Sortable(oColumnsUl, {
        animation: 150,
        group: {
          name: 'shared',
          pull: 'clone',
          put: false,
        },
        sort: false,
        onStart: function(evt) {
          // 這裡的 this 指向 sortable 例項
          this.start = {
            columnsObj: {
              top: evt.item.offsetTop,
              right:
                evt.item.offsetLeft +
                evt.item.parentNode.parentNode.offsetWidth,
              bottom:
                evt.item.offsetTop +
                evt.item.parentNode.parentNode.offsetHeight,
              left: evt.item.offsetLeft,
            },
            rowsObj: {
              top: oRowsContent.offsetTop,
              left: oRowsContent.offsetLeft,
              right: oRowsContent.offsetWidth + oRowsContent.offsetLeft,
              bottom: oRowsContent.offsetHeight + oRowsContent.offsetTop,
            },
            valuesObj: {
              top: oValuesContent.offsetTop,
              left: oValuesContent.offsetLeft,
              right: oValuesContent.offsetWidth + oValuesContent.offsetLeft,
              bottom: oValuesContent.offsetHeight + oValuesContent.offsetTop,
            },
          }
          console.log('this.start===>', this.start)
        },
        onMove: function(evt) {
          console.log(
            'evt.item',
            evt.originalEvent.clientX,
            evt.originalEvent.clientY,
          )
        },
        onEnd: function(evt) {
          // 這裡的 this 物件指向 sortable 物件
          const from = evt.from
          const to = evt.to
          const item = evt.item
          const oldIndex = evt.oldIndex
          this.end = {
            top: item.offsetTop,
            left: item.offsetLeft,
            right: item.offsetWidth + item.offsetLeft,
            bottom: item.offsetHeight + item.offsetTop,
          }
          // 處理拖拽進入 row 行分類
          if (
            this.end.top > this.start.rowsObj.top &&
            this.end.top < this.start.rowsObj.bottom &&
            this.end.top < this.start.valuesObj.top
          ) {
            to.removeChild(item)
            _this.rows.map((item, index) => {
              if (item.id === evt.item.dataset.id) {
                _this.rows.splice(index, 1)
              }
            })
            _this.rows.push({
              name: item.dataset.alias,
              field: item.dataset.field,
              type: item.dataset.type,
              id: item.dataset.id,
            })
            // 設定 checkbox 選中
            _this.setDomChecked(evt.item.dataset.id, true)
            console.log('evt.item.dataset====>', evt.item.dataset, _this.rows)
            console.log('進入行分組')
          } else {
            console.log('沒有進入行分組')
          }
          // 處理拖拽進入 value 值分類
          if (
            this.end.top > this.start.valuesObj.top &&
            this.end.top < this.start.valuesObj.bottom
          ) {
            to.removeChild(item)
            _this.values.map((item, index) => {
              if (item.id === evt.item.dataset.id) {
                _this.values.splice(index, 1)
              }
            })
            _this.values.push({
              name: item.dataset.alias,
              field: item.dataset.field,
              type: item.dataset.type,
              id: item.dataset.id,
            })

            _this.setDomChecked(evt.item.dataset.id, true)
            console.log('進入值分組')
          } else {
            console.log('沒有進入值分組')
          }
          console.log('this.end===>', this.end)
        },
      })
      console.log('oColumnsUl==>', oColumnsUl, sortable)
    },

6. 想要讓 element-ui 中 el-checkbox 元件觸發原生事件,獲取 event 事件物件,需要給 change事件加上 .native,否則無法獲取到 原生的 event 事件物件

<el-checkbox :key="index" @change.native="columnsUlChange($event, item, index)"></el-checkbox>

7. 拖拽超出區域的處理步驟,第一步是超出原有的區域,沒進入別的可拖拽區域,第二是進入了其他可拖拽區域,

    dragRowsUl() {
      // 這裡的 this 指向 vue 例項
      const _this = this
      const oRowsUl = document.querySelector('ul.create-dialysis-table-row-ul') // 好像這一步也是非同步的
      const oRowsContent = document.querySelector('.create-dialysis-table-row-content')
      const sortable = new Sortable(oRowsUl, {
        animation: 150,
        group: {
          name: 'shared',
          put: true,
        },
        sort: true,
        onStart: function(evt) {
          // 這裡的 this 指向 sortable 例項
          this.start = {
            rowsObj: {
              top: oRowsContent.offsetTop,
              left: oRowsContent.offsetLeft,
              right: oRowsContent.offsetWidth + oRowsContent.offsetLeft,
              bottom: oRowsContent.offsetHeight + oRowsContent.offsetTop,
            },
          }
          console.log("this.start===>", this.start)
        },
        onEnd: function(evt) {
          // 這裡的 this 物件指向 sortable 物件
          console.log("evt.from, evt.to", evt.from, evt.to)
          this.end = {
            x: evt.originalEvent.clientX,
            y: evt.originalEvent.clientY,
          }
          if (
            evt.to.dataset.id === evt.from.dataset.id &&
            (this.end.x < this.start.rowsObj.left ||
              this.end.x > this.start.rowsObj.right ||
              (this.end.y < this.start.rowsObj.top || this.end.y > this.start.rowsObj.bottom))
          ) {
            evt.to.removeChild(evt.item)
            _this.rows.map((item, index) => {
              if(item.id === evt.item.dataset.id) {
                _this.rows.splice(index, 1)
              }
            })
            // 設定 checkbox
            _this.setDomChecked(evt.item.dataset.id, false)
            console.log("true")
          }
          // 處理 rows 行分類 to values 值分類
          if(evt.to.dataset.id === 'values' && evt.from.dataset.id === 'rows') {
            // 先移除當前拖動項
            evt.to.removeChild(evt.item)
            // 遍歷刪除重複項
            _this.values.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.values.splice(index, 1)
              }
            })
            // 遍歷刪除重複項
            _this.rows.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.rows.splice(index, 1)
              }
            })
            // 再添加當前項
            _this.values.push({
              name: evt.item.dataset.alias,
              field: evt.item.dataset.field,
              type: evt.item.dataset.type,
              id: evt.item.dataset.id,
            })
            // 設定 checkbox
            _this.setDomChecked(evt.item.dataset.id, true)
          }

          // 處理 values 值分類 to rows 行分類
          if(evt.to.dataset.id === 'rows' && evt.from.dataset.id === 'values') { 
            evt.to.removeChild(evt.item)
            _this.rows.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.rows.splice(index, 1)
              }
            })
            _this.values.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.values.splice(index, 1)
              }
            })
            _this.rows.push({
              name: evt.item.dataset.alias,
              field: evt.item.dataset.field,
              type: evt.item.dataset.type,
              id: evt.item.dataset.id,
            })
            _this.setDomChecked(evt.item.dataset.id, true)
          }
          console.log('this.end===>', this.end)
        },
      })
    },

最終附上原本的程式碼,程式碼實在是太長了,應該還有狠毒地方可以優化,由於是昨天一天衝忙之中完成的,所以好多重複的程式碼塊,下週準備提取一下,然後再做其他功能模組,可以拿過去執行一下,如果沒有圖片檔案,就把相應的樣式給註釋掉就好了

<template>
  <div class="opt-apartment-in-come-wrap">
    <div class="tree-wrap">
      <div class="tree-parent-wrap">
        <div
          :key="index"
          @click="treeParentClick(item, index)"
          class="tree-parent-content"
          v-for="(item, index) in dataList"
        >
          <div class="tree-parent">
            <div class="tree-parent-left">
              <i
                :class="index===dropDownIndex ? 'drop-down-icon-active drop-down-icon' : 'drop-down-icon'"
                @click.stop="dropDown(index)"
                v-show="item.show"
              ></i>
              <i class="file-icon"></i>
              <span class="tree-parent-text">{{item.title}}</span>
            </div>
            <div class="tree-parent-right">
              <span class="date">{{item.date}}</span>
              <span class="name">{{item.name}}</span>
            </div>
          </div>
          <el-collapse-transition>
            <div class="tree-child-wrap" v-if="index===dropDownIndex">
              <div
                @click.stop="treeChildClick(c,i)"
                class="tree-child-content"
                v-for="(c, i) in item.child"
              >
                <div class="tree-child">
                  <div class="tree-parent-left">
                    <i class="file-icon"></i>
                    <span class="tree-parent-text">{{c.title}}</span>
                  </div>
                  <div class="tree-parent-right">
                    <span class="date">{{c.date}}</span>
                    <span class="name">{{c.name}}</span>
                  </div>
                </div>
              </div>
            </div>
          </el-collapse-transition>
        </div>
      </div>
    </div>
    <div class="dialysis-table-wrap">
      <div class="chart-table">
        <div class="chart-table-tab">
          <span
            :class="index===curIndex ? 'active' : ''"
            :key="index"
            @click="tabClick(index)"
            class="chart-table-text"
            v-for="(item, index) in tabs"
          >{{ item }}</span>
        </div>
        <div class="chart-table-content" v-if="!showTabContent">chart</div>
        <div class="chart-table-content" v-if="showTabContent">table</div>
      </div>
      <div class="create-dialysis-table">
        <div class="create-dialysis-table-title">建立資料透析表</div>
        <div class="create-dialysis-table-content">
          <div class="create-dialysis-table-tab">
            <span
              :class="index === columnFilterIndex ? 'columns' : ''"
              :key="index"
              @click="columnFilterClick(index)"
              v-for="(item, index) in columnFilter"
            >{{ item }}</span>
          </div>
          <!-- 列分類 -->
          <div class="create-dialysis-table-columns">
            <div class="create-dialysis-table-columns-select">
              銷售
              <!-- 有可能放置一個下拉框 -->
            </div>
            <div class="create-dialysis-table-columns-content">
              <ul class="create-dialysis-table-columns-ul" data-id="columns">
                <li
                  :data-alias="item.name"
                  :data-field="item.field"
                  :data-id="item.id"
                  :data-type="item.type"
                  :data-index="index"
                  :key="index"
                  v-for="(item, index) in columns"
                >
                  <div class="left">
                    <i class="drag-icon"></i>
                    <span class="column">{{ item.name }}</span>
                  </div>
                </li>
              </ul>
              <ul class="create-dialysis-table-columns-checkbox" data-id="checkbox">
                <li
                  :data-alias="item.name"
                  :data-field="item.field"
                  :data-id="item.id"
                  :data-type="item.type"
                  :data-index="index"
                  :class="`columns-${item.id}`"
                  :key="index"
                  v-for="(item, index) in columns"
                >
                  <el-checkbox :key="index" @change.native="columnsUlChange($event, item, index)"></el-checkbox>
                </li>
              </ul>
            </div>
          </div>
          <!-- 行分類 -->
          <div class="create-dialysis-table-row">
            <div class="create-dialysis-table-row-title">行分組</div>
            <div class="create-dialysis-table-row-content">
              <ul class="create-dialysis-table-row-ul" data-id="rows">
                <li
                  :data-alias="item.name"
                  :data-field="item.field"
                  :data-id="item.id"
                  :data-type="item.type"
                  :key="index"
                  v-for="(item, index) in rows"
                >
                  <div class="left">
                    <i class="drag-icon"></i>
                    <span class="column">{{ item.name }}</span>
                  </div>
                </li>
              </ul>
            </div>
          </div>
          <!-- 值分類 -->
          <div class="create-dialysis-table-value">
            <div class="create-dialysis-table-value-title">值</div>
            <div class="create-dialysis-table-value-content">
              <ul class="create-dialysis-table-value-ul" data-id="values">
                <li
                  :data-alias="item.name"
                  :data-field="item.field"
                  :data-id="item.id"
                  :data-type="item.type"
                  :key="index"
                  v-for="(item, index) in values"
                >
                  <div class="left">
                    <i class="drag-icon"></i>
                    <!-- 給下劃線可以給加個 div -->
                    <div class="select">
                      <fy-select
                        :options="defaultArr"
                        :selectunderline="true"
                        :width="'100px'"
                        label-key="keyName"
                        value-key="keyValue"
                      ></fy-select>
                    </div>
                    <span class="column">{{ item.name }}</span>
                  </div>
                </li>
              </ul>
            </div>
          </div>
          <!-- 報表名字 -->
          <div class="create-dialysis-table-name">
            <div class="create-dialysis-table-name-title">報表名稱</div>
            <div class="create-dialysis-table-name-input">
              <fy-input v-model="tableName"></fy-input>
            </div>
          </div>
          <!-- 按鈕組 -->
          <div class="create-dialysis-table-button">
            <fy-ripple-large-button value="取消"></fy-ripple-large-button>
            <fy-ripple-button value="儲存"></fy-ripple-button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import Sortable from 'sortablejs'
export default {
  name: 'optApartmentInCome',
  data() {
    return {
      tabs: ['圖表', '列表'],
      columnFilter: ['列', '篩選'],
      curIndex: 1, // 預設選中列表
      showTabContent: true, // 預設列表顯示
      dropDownIndex: -1,
      showDropDown: false,
      columnFilterIndex: 0, // 預設選中列 columns
      showColumnFilter: true, // 預設列顯示
      dataList: [
        {
          title: '操作部門收入成本一覽表',
          date: '2019-12-12 12:12',
          name: 'Administrators',
          show: true,

          child: [
            {
              title: '操作部門收入成本一覽表1',
              date: '2019-12-12 12:12',
              name: 'Administrators1',
            },
            {
              title: '操作部門收入成本一覽表2',
              date: '2019-12-13 12:12',
              name: 'Administrators2',
            },
            {
              title: '操作部門收入成本一覽表3',
              date: '2019-12-14 12:12',
              name: 'Administrators2',
            },
            {
              title: '操作部門收入成本一覽表4',
              date: '2019-12-15 12:12',
              name: 'Administrators2',
            },
          ],
        },
        {
          title: '操作部門收入成本一覽表',
          date: '2019-12-12 12:12',
          name: 'Administrators',
          show: true,
          child: [
            {
              title: '操作部門收入成本一覽表1',
              date: '2019-12-12 12:12',
              name: 'Administrators1',
            },
            {
              title: '操作部門收入成本一覽表2',
              date: '2019-12-13 12:12',
              name: 'Administrators2',
            },
            {
              title: '操作部門收入成本一覽表3',
              date: '2019-12-14 12:12',
              name: 'Administrators2',
            },
            {
              title: '操作部門收入成本一覽表4',
              date: '2019-12-15 12:12',
              name: 'Administrators2',
            },
          ],
        },
        {
          title: '操作部門收入成本一覽表',
          date: '2019-12-12 12:12',
          name: 'Administrators',
          show: false,
          child: [],
        },
      ],
      defaultArr: [
        {
          keyName: '計數項',
          keyValue: 'count',
        },
        {
          keyName: '求和項',
          keyValue: 'sum',
        },
        {
          keyName: '乘積項',
          keyValue: 'product',
        },
        {
          keyName: '最大項',
          keyValue: 'max',
        },
        {
          keyName: '最小項',
          keyValue: 'min',
        },
      ],
      columns: [
        {
          name: '訂購時間',
          field: 'orderTime',
          type: 'date',
          id: 1,
        },
        {
          name: '訂單號',
          field: 'orderNum',
          type: 'string',
          id: 2,
        },
        {
          name: '訂單人數',
          field: 'orderPNum',
          type: 'number',
          id: 3,
        },
        {
          name: '訂單應收',
          field: 'ordePricd',
          type: 'number',
          id: 4,
        },
        {
          name: '銷售部門',
          field: 'salesPart',
          type: 'string',
          id: 5,
        },
        {
          name: '訂單列表',
          field: 'orderList',
          type: 'string',
          id: 6,
        },
      ],
      rows: [],
      values: [],
      tableName: '操作部門收入成本一覽表',
      elCheckboxIndex: -1,
      value: '',
    }
  },
  methods: {
    tabClick(index) {
      this.curIndex = index
      if (index === 1) {
        this.showTabContent = true
      } else {
        this.showTabContent = false
      }
    },
    dropDown(index) {
      if (this.dropDownIndex === index) {
        this.dropDownIndex = -1
      } else {
        this.dropDownIndex = index
      }
      this.showDropDown = !this.showDropDown
    },
    treeChildClick(c, i) {
      console.log('c==>', c, 'i===>', i)
    },
    treeParentClick(item, index) {
      console.log('item===>', item, 'index===>', index)
    },
    columnFilterClick(index) {
      this.columnFilterIndex = index
      console.log('index===>', index)
    },
    dragColumnsUl() {
      // 這裡的 this 指向 vue 例項
      const _this = this
      const oColumnsUl = document.querySelector(
        'ul.create-dialysis-table-columns-ul',
      ) // 好像這一步也是非同步的
      const oRowsContent = document.querySelector(
        '.create-dialysis-table-row-content',
      )
      const oValuesContent = document.querySelector(
        '.create-dialysis-table-value-content',
      )
      const sortable = new Sortable(oColumnsUl, {
        animation: 150,
        group: {
          name: 'shared',
          pull: 'clone',
          put: false,
        },
        sort: false,
        onStart: function(evt) {
          // 這裡的 this 指向 sortable 例項
          this.start = {
            columnsObj: {
              top: evt.item.offsetTop,
              right:
                evt.item.offsetLeft +
                evt.item.parentNode.parentNode.offsetWidth,
              bottom:
                evt.item.offsetTop +
                evt.item.parentNode.parentNode.offsetHeight,
              left: evt.item.offsetLeft,
            },
            rowsObj: {
              top: oRowsContent.offsetTop,
              left: oRowsContent.offsetLeft,
              right: oRowsContent.offsetWidth + oRowsContent.offsetLeft,
              bottom: oRowsContent.offsetHeight + oRowsContent.offsetTop,
            },
            valuesObj: {
              top: oValuesContent.offsetTop,
              left: oValuesContent.offsetLeft,
              right: oValuesContent.offsetWidth + oValuesContent.offsetLeft,
              bottom: oValuesContent.offsetHeight + oValuesContent.offsetTop,
            },
          }
          console.log('this.start===>', this.start)
        },
        onMove: function(evt) {
          console.log(
            'evt.item',
            evt.originalEvent.clientX,
            evt.originalEvent.clientY,
          )
        },
        onEnd: function(evt) {
          // 這裡的 this 物件指向 sortable 物件
          const from = evt.from
          const to = evt.to
          const item = evt.item
          const oldIndex = evt.oldIndex
          this.end = {
            top: item.offsetTop,
            left: item.offsetLeft,
            right: item.offsetWidth + item.offsetLeft,
            bottom: item.offsetHeight + item.offsetTop,
          }
          // 處理拖拽進入 row 行分類
          if (
            this.end.top > this.start.rowsObj.top &&
            this.end.top < this.start.rowsObj.bottom &&
            this.end.top < this.start.valuesObj.top
          ) {
            to.removeChild(item)
            _this.rows.map((item, index) => {
              if (item.id === evt.item.dataset.id) {
                _this.rows.splice(index, 1)
              }
            })
            _this.rows.push({
              name: item.dataset.alias,
              field: item.dataset.field,
              type: item.dataset.type,
              id: item.dataset.id,
            })
            // 設定 checkbox 選中
            _this.setDomChecked(evt.item.dataset.id, true)
            console.log('evt.item.dataset====>', evt.item.dataset, _this.rows)
            console.log('進入行分組')
          } else {
            console.log('沒有進入行分組')
          }
          // 處理拖拽進入 value 值分類
          if (
            this.end.top > this.start.valuesObj.top &&
            this.end.top < this.start.valuesObj.bottom
          ) {
            to.removeChild(item)
            _this.values.map((item, index) => {
              if (item.id === evt.item.dataset.id) {
                _this.values.splice(index, 1)
              }
            })
            _this.values.push({
              name: item.dataset.alias,
              field: item.dataset.field,
              type: item.dataset.type,
              id: item.dataset.id,
            })

            _this.setDomChecked(evt.item.dataset.id, true)
            console.log('進入值分組')
          } else {
            console.log('沒有進入值分組')
          }
          console.log('this.end===>', this.end)
        },
      })
      console.log('oColumnsUl==>', oColumnsUl, sortable)
    },
    dragRowsUl() {
      // 這裡的 this 指向 vue 例項
      const _this = this
      const oRowsUl = document.querySelector('ul.create-dialysis-table-row-ul') // 好像這一步也是非同步的
      const oRowsContent = document.querySelector('.create-dialysis-table-row-content')
      const sortable = new Sortable(oRowsUl, {
        animation: 150,
        group: {
          name: 'shared',
          put: true,
        },
        sort: true,
        onStart: function(evt) {
          // 這裡的 this 指向 sortable 例項
          this.start = {
            rowsObj: {
              top: oRowsContent.offsetTop,
              left: oRowsContent.offsetLeft,
              right: oRowsContent.offsetWidth + oRowsContent.offsetLeft,
              bottom: oRowsContent.offsetHeight + oRowsContent.offsetTop,
            },
          }
          console.log("this.start===>", this.start)
        },
        onEnd: function(evt) {
          // 這裡的 this 物件指向 sortable 物件
          console.log("evt.from, evt.to", evt.from, evt.to)
          this.end = {
            x: evt.originalEvent.clientX,
            y: evt.originalEvent.clientY,
          }
          if (
            evt.to.dataset.id === evt.from.dataset.id &&
            (this.end.x < this.start.rowsObj.left ||
              this.end.x > this.start.rowsObj.right ||
              (this.end.y < this.start.rowsObj.top || this.end.y > this.start.rowsObj.bottom))
          ) {
            evt.to.removeChild(evt.item)
            _this.rows.map((item, index) => {
              if(item.id === evt.item.dataset.id) {
                _this.rows.splice(index, 1)
              }
            })
            // 設定 checkbox
            _this.setDomChecked(evt.item.dataset.id, false)
            console.log("true")
          }
          // 處理 rows 行分類 to values 值分類
          if(evt.to.dataset.id === 'values' && evt.from.dataset.id === 'rows') {
            // 先移除當前拖動項
            evt.to.removeChild(evt.item)
            // 遍歷刪除重複項
            _this.values.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.values.splice(index, 1)
              }
            })
            // 遍歷刪除重複項
            _this.rows.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.rows.splice(index, 1)
              }
            })
            // 再添加當前項
            _this.values.push({
              name: evt.item.dataset.alias,
              field: evt.item.dataset.field,
              type: evt.item.dataset.type,
              id: evt.item.dataset.id,
            })
            // 設定 checkbox
            _this.setDomChecked(evt.item.dataset.id, true)
          }

          // 處理 values 值分類 to rows 行分類
          if(evt.to.dataset.id === 'rows' && evt.from.dataset.id === 'values') { 
            evt.to.removeChild(evt.item)
            _this.rows.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.rows.splice(index, 1)
              }
            })
            _this.values.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.values.splice(index, 1)
              }
            })
            _this.rows.push({
              name: evt.item.dataset.alias,
              field: evt.item.dataset.field,
              type: evt.item.dataset.type,
              id: evt.item.dataset.id,
            })
            _this.setDomChecked(evt.item.dataset.id, true)
          }
          console.log('this.end===>', this.end)
        },
      })
    },
    dragValuesUl() {
      // 這裡的 this 指向 vue 例項
      const _this = this
      const oValuesUl = document.querySelector(
        'ul.create-dialysis-table-value-ul',
      ) // 好像這一步也是非同步的
      const oValuesContent = document.querySelector('.create-dialysis-table-value-content')
      const sortable = new Sortable(oValuesUl, {
        animation: 150,
        group: {
          name: 'shared',
          put: true,
        },
        sort: true,
        onStart: function(evt) {
          // 這裡的 this 指向 sortable 例項
          // 儲存邊界值 top, left, right, bottom
          this.start = {
            valuesObj: {
              top: oValuesContent.offsetTop,
              left: oValuesContent.offsetLeft,
              right: oValuesContent.offsetWidth + oValuesContent.offsetLeft,
              bottom: oValuesContent.offsetHeight + oValuesContent.offsetTop,
            },
          }
          console.log("this.start===>", this.start)
        },
        onEnd: function(evt) {
          // 這裡的 this 物件指向 sortable 物件
          console.log("evt.from, evt.to", evt.from, evt.to)
          this.end = {
            x: evt.originalEvent.clientX,
            y: evt.originalEvent.clientY,
          }
          // 溢位邊界的時候,刪除資料,同時設定 checkbox
          if (
            evt.to.dataset.id === evt.from.dataset.id &&
            (this.end.x < this.start.valuesObj.left ||
              this.end.x > this.start.valuesObj.right ||
              (this.end.y < this.start.valuesObj.top || this.end.y > this.start.valuesObj.bottom))
          ) {
            evt.to.removeChild(evt.item)
            _this.values.map((item, index) => {
              if(item.id === evt.item.dataset.id) {
                _this.values.splice(index, 1)
              }
            })
            // 設定 checkbox
            _this.setDomChecked(evt.item.dataset.id, false)
            console.log("true")
          }

          // 處理 rows 行分類 to values 值分類
          if(evt.to.dataset.id === 'values' && evt.from.dataset.id === 'rows') {
            // 先移除 evt.item
            evt.to.removeChild(evt.item)
            // 刪除當前移動項對應的陣列元素(主要是去除重複項)
            _this.values.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.values.splice(index, 1)
              }
            })
            // 刪除當前移動項對應的陣列元素(主要是去除重複項)
            _this.rows.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.rows.splice(index, 1)
              }
            })
            // 再新增資料
            _this.values.push({
              name: evt.item.dataset.alias,
              field: evt.item.dataset.field,
              type: evt.item.dataset.type,
              id: evt.item.dataset.id,
            })
            // 設定 checkbox
            _this.setDomChecked(evt.item.dataset.id, true)
          }

          // 處理 values 值分類 to rows 行分類
          if(evt.to.dataset.id === 'rows' && evt.from.dataset.id === 'values') { 
            evt.to.removeChild(evt.item)
            _this.rows.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.rows.splice(index, 1)
              }
            })
            _this.values.map((item, index)=>{
              if(item.id === evt.item.dataset.id) {
                _this.values.splice(index, 1)
              }
            })
            _this.rows.push({
              name: evt.item.dataset.alias,
              field: evt.item.dataset.field,
              type: evt.item.dataset.type,
              id: evt.item.dataset.id,
            })
            _this.setDomChecked(evt.item.dataset.id, true)
          }
          console.log('this.end===>', this.end)
        },
      })
    },
    columnsUlChange(e, item, index) {
      // 1. 根據型別判斷放置再哪個區域
      if(e.currentTarget.classList.contains("is-checked")) {
        switch(item.type) {
          case 'date':
            this.values.map((v, i)=>{
              if(v.id === item.id) {
                this.values.splice(i, 1)
              }
            })
            this.$set(this.values, this.values.length, item)
            // this.values.push(item)
            break;
          case 'string':
            this.rows.map((v, i)=>{
              if(v.id === item.id) {
                this.rows.splice(i, 1)
              }
            })
            this.$set(this.rows, this.rows.length, item)
            // this.rows.push(item)
            break;
          case 'number':
            this.values.map((v, i)=>{
              if(v.id === item.id) {
                this.values.splice(i, 1)
              }
            })
            this.$set(this.values, this.values.length, item)
            // this.values.push(item)
          default:

            break;
        }
      } else {
        switch(item.type) {
          case 'date':
            this.values.map((v, i)=>{
              if(v.id === item.id) {
                this.values.splice(i, 1)
              }
            })
            break;
          case 'string':
            this.rows.map((v, i)=>{
              if(v.id === item.id) {
                this.rows.splice(i, 1)
              }
            })
            break;
          case 'number':
            this.values.map((v, i)=>{
              if(v.id === item.id) {
                this.values.splice(i, 1)
              }
            })
          default:

            break;
        }
      }

      // 2. 如果已選中,則刪除
      if(e.currentTarget.classList.contains("is-checked"))

      // 


      console.log('e===>', e.currentTarget.children[0])
      console.log('item===>', item)
      // if(e.currentTarget.classList.contains("is-checked")) {
      //   e.currentTarget.classList.remove('is-checked')
      //   e.currentTarget.children[0].classList.remove('is-checked')
      //   console.log("true")
      // } else {
      //   e.currentTarget.classList.add('is-checked')
      //   e.currentTarget.children[0].classList.add('is-checked')
      // }
    },
    setChecked(from, oldIndex, bool) {
      // 設定 checkbox 選中
      if(bool) {
        from.children[oldIndex].children[1].classList.add('is-checked')
        from.children[oldIndex].children[1].children[0].classList.add(
          'is-checked',
        )
      } else {
        from.children[oldIndex].children[1].classList.remove('is-checked')
        from.children[oldIndex].children[1].children[0].classList.remove(
          'is-checked',
        )
      }
      
    },
    setDomChecked(id, bool) {
      const oLi = document.querySelector(`.create-dialysis-table-columns-checkbox li.columns-${id}`)
      console.log("oLi===>", oLi)
      if(bool) {
        oLi.children[0].classList.add('is-checked')
        oLi.children[0].children[0].classList.add(
          'is-checked',
        )
      } else {
        oLi.children[0].classList.remove('is-checked')
        oLi.children[0].children[0].classList.remove(
          'is-checked',
        )
      }
    }
  },
  mounted() {
    this.dragColumnsUl()
    this.dragRowsUl()
    this.dragValuesUl()
  },
}
</script>

<style lang="scss" scoped>
.opt-apartment-in-come-wrap {
  position: relative;
  display: flex;
  box-sizing: border-box;
  width: 100%;
  height: 100vh;
  overflow-x: scroll;
  .tree-wrap {
    box-sizing: border-box;
    width: 654px;
    height: 100vh;
    margin-right: 20px;
    border-radius: 8px;
    background-color: $color-fff;
    box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.08);
    .tree-parent-wrap {
      border-bottom: 1px solid rgba(230, 230, 230, 1);
      .tree-parent-content {
        border-bottom: 1px solid rgba(230, 230, 230, 1);
        &:last-child {
          border-bottom: none;
        }
        .tree-parent {
          display: flex;
          justify-content: space-between;
          box-sizing: border-box;
          width: 100%;
          padding: 10px 40px 10px 20px;
          cursor: pointer;
          .tree-parent-left {
            display: flex;
            align-items: center;
            min-height: 44px;
            .drop-down-icon {
              display: block;
              width: 0;
              height: 0;
              margin-right: 8px;
              border-bottom: 10px solid transparent;
              border-top: 10px solid transparent;
              border-left: 10px solid #666666;
              border-radius: 10px;
            }
            .drop-down-icon-active {
              transform: rotate(90deg);
              -webkit-transform: rotate(90deg);
              -moz-transform: rotate(90deg);
              -ms-transform: rotate(90deg);
              -o-transform: rotate(90deg);
            }
            .file-icon {
              display: block;
              width: 24px;
              height: 24px;
              margin-right: 8px;
              background-image: url('[email protected]/dropDown/set.png');
              background-repeat: no-repeat;
              background-size: 100%;
            }
            .tree-parent-text {
              font-size: 16px;
              font-family: PingFangSC-Regular, PingFang SC;
              font-weight: 400;
              color: rgba(51, 51, 51, 1);
            }
          }
          .tree-parent-right {
            display: flex;
            justify-content: space-between;
            align-items: center;
            min-width: 273px;
            .date {
              font-size: 14px;
              font-family: PingFangSC-Regular, PingFang SC;
              font-weight: 400;
              color: rgba(51, 51, 51, 1);
            }
            .name {
              font-size: 14px;
              font-family: PingFangSC-Regular, PingFang SC;
              font-weight: 400;
              color: rgba(51, 51, 51, 1);
            }
          }
        }
        .tree-child-content {
          width: 100%;
        }
      }
    }
  }
  .dialysis-table-wrap {
    display: flex;
    width: calc(100% - 654px - 20px);
    height: 100vh;
    border-radius: 8px;
    background-color: $color-fff;
    box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.08);
    .chart-table {
      box-sizing: border-box;
      width: calc(100% - 307px);
      min-width: 852px;
      height: 100%;
      border-right: 1px solid rgba(230, 230, 230, 1);
      border-top-left-radius: 8px;
      border-bottom-left-radius: 8px;
      background-color: bisque;
      .chart-table-tab {
        box-sizing: border-box;
        width: 100%;
        padding: 11px 0 11px 19px;
        border-bottom: 1px solid rgba(230, 230, 230, 1);
        font-size: 16px;
        font-family: PingFangSC-Semibold, PingFang SC;
        font-weight: 600;
        color: rgba(102, 102, 102, 1);
        .chart-table-text {
          margin-right: 40px;
          cursor: pointer;
          &:last-child {
            margin-right: 0;
          }
          &.active {
            color: rgba(51, 51, 51, 1);
          }
        }
      }
      .chart-table-content {
        width: 100%;
        background-color: blueviolet;
      }
    }
    .create-dialysis-table {
      box-sizing: border-box;
      width: 307px;
      height: 100%;
      border-top-right-radius: 8px;
      border-bottom-right-radius: 8px;
      .create-dialysis-table-title {
        box-sizing: border-box;
        padding: 11px 0 11px 20px;
        border-bottom: 1px solid rgba(230, 230, 230, 1);
        font-size: 16px;
        font-family: PingFangSC-Semibold, PingFang SC;
        font-weight: 600;
        color: rgba(51, 51, 51, 1);
      }
      .create-dialysis-table-content {
        box-sizing: border-box;
        padding: 20px 32px;
        .create-dialysis-table-tab {
          span {
            margin-right: 20px;
            font-size: 14px;
            font-family: PingFangSC-Regular, PingFang SC;
            font-weight: 400;
            color: rgba(102, 102, 102, 1);
            cursor: pointer;
            &.columns {
              font-weight: 600;
              color: rgba(51, 51, 51, 1);
            }
          }
        }
        // 列分組
        .create-dialysis-table-columns {
          box-sizing: border-box;
          height: 268px;
          margin-top: 8px;
          padding: 12px 12px 0 12px;
          border-radius: 2px;
          border: 1px dashed rgba(204, 204, 204, 1);
          overflow-y: scroll;
          .create-dialysis-table-columns-select {
            width: 100%;
            height: 32px;
            line-height: 32px;
            border-bottom: 1px solid rgba(220, 220, 220, 1);
          }
          .create-dialysis-table-columns-content {
            display: flex;
            flex-flow: row nowrap;
            align-items: center;
            justify-content: space-between;
            margin-top: 12px;
            ul {
              li {
                height: 40px;
                display: flex;
                flex-flow: row nowrap;
                align-items: center;
                justify-content: space-between;
                .left {
                  display: flex;
                  align-items: center;
                  .drag-icon {
                    width: 24px;
                    height: 24px;
                    background-image: url('[email protected]/dropDown/ic_drop-down.png');
                    background-size: 100%;
                    background-repeat: no-repeat;
                  }
                }
              }
            }
          }
        }
        // 行分組
        .create-dialysis-table-row {
          margin-top: 20px;
          .create-dialysis-table-row-title {
            font-size: 14px;
            font-family: PingFangSC-Semibold, PingFang SC;
            font-weight: 600;
            color: rgba(51, 51, 51, 1);
          }
          .create-dialysis-table-row-content {
            height: 100px;
            padding: 12px;
            margin-top: 8px;
            border: 1px dashed rgba(204, 204, 204, 1);
            overflow-y: scroll;
          }
        }
        // 值分組
        .create-dialysis-table-value {
          margin-top: 20px;
          .create-dialysis-table-value-title {
            font-size: 14px;
            font-family: PingFangSC-Semibold, PingFang SC;
            font-weight: 600;
            color: rgba(51, 51, 51, 1);
          }
          .create-dialysis-table-value-content {
            height: 100px;
            padding: 12px;
            margin-top: 8px;
            border: 1px dashed rgba(204, 204, 204, 1);
            overflow-y: scroll;
            .create-dialysis-table-value-ul {
              li {
                div.left {
                  .select {
                    border-bottom: 1px solid rgba(204, 204, 204, 1);
                  }
                  ::v-deep.el-input__suffix-inner .el-input__icon::before {
                    position: absolute;
                    top: 19px;
                  }
                }
              }
            }
          }
        }
        // 報表名字
        .create-dialysis-table-name {
          margin-top: 20px;
          .create-dialysis-table-name-title {
            font-size: 14px;
            font-family: PingFangSC-Regular, PingFang SC;
            font-weight: 400;
            color: rgba(102, 102, 102, 1);
          }
          .create-dialysis-table-name-input {
            border-bottom: 1px solid rgba(230, 230, 230, 1);
          }
        }
        // 按鈕組
        .create-dialysis-table-button {
          position: absolute;
          right: 0;
          bottom: 0px;
        }
      }
    }
  }
}
.tree-child {
  display: flex;
  justify-content: space-between;
  box-sizing: border-box;
  width: 100%;
  padding: 10px 40px 10px 20px;
  border-top: 1px solid rgba(230, 230, 230, 1);
  cursor: pointer;
  .tree-parent-left {
    box-sizing: border-box;
    display: flex;
    align-items: center;
    min-height: 44px;
    padding-left: 78px;
    .file-icon {
      display: block;
      width: 24px;
      height: 24px;
      margin-right: 8px;
      background-image: url('[email protected]/dropDown/box.png');
      background-repeat: no-repeat;
      background-size: 100%;
    }
    .tree-parent-text {
      font-size: 16px;
      font-family: PingFangSC-Regular, PingFang SC;
      font-weight: 400;
      color: rgba(51, 51, 51, 1);
    }
  }
  .tree-parent-right {
    display: flex;
    justify-content: space-between;
    align-items: center;
    min-width: 273px;
    .date {
      font-size: 14px;
      font-family: PingFangSC-Regular, PingFang SC;
      font-weight: 400;
      color: rgba(51, 51, 51, 1);
    }
    .name {
      font-size: 14px;
      font-family: PingFangSC-Regular, PingFang SC;
      font-weight: 400;
      color: rgba(51, 51, 51, 1);
    }
  }
}
ul {
  li {
    height: 40px;
    display: flex;
    flex-flow: row nowrap;
    justify-items: center;
    justify-content: space-between;
    .left {
      display: flex;
      align-items: center;
      .drag-icon {
        width: 24px;
        height: 24px;
        background-image: url('[email protected]/dropDown/ic_drop-down.png');
        background-size: 100%;
        background-repeat: no-repeat;
      }
    }
  }
}
</style>

最終效果

ed06f6f1ad2d1e4de380ddce56950f0f.gif