Vue 实现高级穿梭框 Transfer 封装

文章目录

  • 01 基础信息
    • 1.1. 技术栈
    • 1.2. 组件设计
      • a. 竖版设计稿
      • b. 横版设计稿
  • 02 技术方案
    • (1)初定义数据
    • (2)注意事项
    • (3)逻辑草图
  • 03 代码示例
    • 3.1. 组件使用
    • 3.2. 组件源码
      • ./TransferPlus/index.vue
      • ./TransferPlus/TransferTable.vue

01 基础信息

1.1. 技术栈

Element-UIVue2lodash

1.2. 组件设计

需求描述:

  1. 【待选择列表】 接收业务的表格数据,支持选择多项并将其添加到【已添加列表】 (勾选或删除操作,两边的列表是同步的);
  2. 【已添加列表】支持本地分页和本地简易搜索功能(已添加的列表数据需要实时同步给业务);

a. 竖版设计稿

穿梭框竖版设计稿

b. 横版设计稿

穿梭框横版设计稿

02 技术方案

(1)初定义数据

// 【待选择列表】外部传输源数据
// 【已添加列表】组件内部控制数据的分页、搜索和展示

const props = {
    sourceList: [], // 源数据
    columnList: [], // 表格列配置(注:字段类型均为字符串)
    searchList: [], // 【已添加列表】搜索项(注:与表头对应)
    refreshTableData: (param)=>{}, // 回调函数
    total: 0, // 用于控制分页器
}

const state = {
    targetList: [], // 目标数据
    searchList: [], // 【已添加列表】搜索项
}

(2)注意事项

  1. 【待选择列表】翻页选择时需要记录并回显已选择的行
  2. 【已添加列表】删除后需要继续留在当前页,即要判断删除的是否是最后一页中只有一条的数据
  3. 【待选择列表】更改选择后,【已添加列表】的筛选项或是状态项是否重置?或是维持不变?

(3)逻辑草图

逻辑草图

03 代码示例

3.1. 组件使用

外部可通过 ref 调用的方法:

  1. clearSelection():清空所有选择项;
  2. setPaginationParam({pageNum,pageSize},isFetch):设置源表格分页器参数,若 isFetch 为 true 则会自动调用 fetchSourceListisFetch 默认为 false );
  3. initializeComponent(isFetch):初始化组件,若 isFetch 为 true 则初始化后自动请求源表格数据( isFetch 默认为 false );
  4. this.$refs['transferPlus'].selectList:若要初始化 selectList 可以使用 ref 设置(记得外面包裹 this.$nextTick);

注意事项:

  1. 使用插槽自定义表格列时,是同时应用到两个列表中的;
  2. 组件会通过 selectionChange 事件告知您选择的列表结果;
  3. 特别地,组件一开始不会默认请求源表格数据,所以您需要在使用前自行调用 fetchSourceList 获取 sourceList 等来渲染组件的数据,组件只会在内部的分页状态等有更改的情况下自动调用 fetchSourceList 为您刷新渲染数据;
  4. usePaginationtrue,则组件自动为您控制分页器,但您必须设置好变量(sourceTotal)和源表格数据请求方法(fetchSourceList),并且为了防止您初始请求的分页参数和组件内部定义的默认初始分页参数不同,您可以设置 initSourcePageNuminitSourcePageSize 来同步内外初始化参数;
<template>  
  <TransferPlus 
    ref="transferPlusRef"
    :sourceList="sourceList" 
    :tableColumnList="tableColumnList" 
    usePagination 
    tableHeight="240" 
    :sourceTotal="sourceTotal" 
    :tableLoading="tableLoading" 
    @fetchSourceList="fetchSourceList" 
  >
    <!-- "table_"后拼接的是你定义该列的prop -->
    <template #table_tag="{ row, rowIndex }">{{ rowIndex + 1 }}{{row.tag}}</template>
    <!-- 自定义源表格的搜索区域 -->
    <template #source_search>
      <el-input placeholder="请输入课程名称" v-model="queryInfo.title" class="search-input" clearable>
        <el-button slot="append" icon="el-icon-search" @click="searchSourceList"></el-button>
      </el-input>
    </template>
  </TransferPlus>
</template>

<script>
  import TransferPlus from '@/components/TransferPlus'
  
  export default {
    components: { TransferPlus },
    data() {
      sourceList: [],    
      tableColumnList: [
        { label: '课程id', prop: 'id' },
        { label: '课程名称', prop: 'title' },
        { label: '课程类型', prop: 'tag' },
      ],
      tableLoading: false,
      sourceTotal: 0,
      queryInfo: {
        pageNum: 1,
        pageSize: 10,
        title: '',
        tag: '',
      },
    }
    method:{
      async fetchSourceList (params={pageNum,pageSize}) {
        this.tableLoading = true
        const { pageNum, pageSize } = this.queryInfo
        this.queryInfo = {
          ...this.queryInfo,
          pageNum: params?.pageNum || pageNum,
          pageSize: params?.pageSize || pageSize,
        }
        const res = await getList(this.queryInfo)
        this.sourceList = res.data.list || []
        this.sourceTotal = res.data.total || 0
        this.tableLoading = false
      },
      searchSourceList() {
        // 每次查询时只需要重置穿梭框的页码到 1,并配置自动调用搜索函数
        this.$refs['transferPlusRef'].setPaginationParam({ pageNum: 1 }, true)
      },
    }
  }
 </script>
 
 <style scoped>
   .search-input {
      margin-bottom: 12px;
      width: 100%;
      height: 32px;
  }
 </style>

实现效果图:
实现效果图

3.2. 组件源码

./TransferPlus/index.vue

<!--  
组件使用方式如下:
  <TransferPlus :sourceList="sourceList" :tableColumnList="tableColumnList" usePagination tableHeight="240" :sourceTotal="sourceTotal" :tableLoading="tableLoading" @fetchSourceList="fetchSourceList" >
    <template #table_你定义该列的prop="{ columnProps }">{{ columnProps.$index + 1 }}{{columnProps.row.xxx}}</template>
  </TransferPlus>
  method:{
    async fetchSourceList (params={pageNum,pageSize}) {
      this.tableLoading = true
      const res = await getList({ ...this.queryInfo, ...params })
      this.sourceList = res.data.list
      this.sourceTotal = res.data.total
      this.tableLoading = false
    }
  }
外部可通过 ref 调用的方法:
1. clearSelection():清空所有选择项;
2. setPaginationParam({pageNum,pageSize},isFetch):设置源表格分页器参数,若 isFetch 为 true 则会自动调用 fetchSourceList( isFetch 默认为 false );
3. initializeComponent(isFetch):初始化组件,若 isFetch 为 true 则初始化后自动请求源表格数据( isFetch 默认为 false );
4. this.$refs['transferPlusRef'].selectList:若要初始化 selectList 可以使用 ref 设置(记得外面包裹 this.$nextTick);
注意事项:
1. 使用插槽自定义表格列时,是同时应用到两个列表中的;
2. 组件会通过 selectionChange 事件告知您选择的列表结果;
3. 特别地,组件一开始不会默认请求源表格数据,所以您需要在使用前自行调用 fetchSourceList 获取 sourceList 等来渲染组件的数据,组件只会在内部的分页状态等有更改的情况下自动调用 fetchSourceList 为您刷新渲染数据;
4. 若 usePagination 为 true,则组件自动为您控制分页器,但您必须设置好变量(sourceTotal)和源表格数据请求方法(fetchSourceList),并且为了防止您初始请求的分页参数和组件内部定义的默认初始分页参数不同,您可以设置 initSourcePageNum 和 initSourcePageSize 来同步内外初始化参数;
 -->
<template>
  <div :class="direction === 'horizontal' ? 'transfer-horizontal' : ''">
    <!-- 【待选择列表】 -->
    <div :class="['list-wrapping', { horizontal: direction === 'horizontal' }]">
      <div class="wrapping-header">
        <span>待选择列表</span>
        <span>{{ selectLength }}/{{ sourceTotal || sourceList.length }}</span>
      </div>
      <div class="wrapping-content">
        <!-- 自定义搜索 -->
        <slot name="source_search" />
        <TransferTable
          ref="sourceTransferTableRef"
          v-model="selectList"
          :tableList="sourceList"
          :tableColumnList="tableColumnList"
          :tableHeight="tableHeight"
          :total="sourceTotal"
          :initPageNum="initSourcePageNum"
          :initPageSize="initSourcePageSize"
          :usePagination="usePagination"
          :tableLoading="tableLoading"
          :uniqueKey="uniqueKey"
          :selectable="selectable"
          :pagerCount="pagerCount"
          @fetchTableList="handleFetchTableList"
        >
          <!-- 使用穿梭表格的自定义列插槽 -->
          <template v-for="(item, index) in tableColumnList" :slot="`inner_table_${item.prop}`" slot-scope="slotData">
            <span :key="index">
              <!-- 设置新的插槽提供给消费端自定义列 -->
              <slot :name="`table_${item.prop}`" :columnProps="slotData.columnProps" :row="slotData.columnProps.row" :rowIndex="slotData.columnProps.$index">
                {{ slotData.columnProps.row[item.prop] || '-' }}
              </slot>
            </span>
          </template>
        </TransferTable>
      </div>
    </div>
    <!-- 【已添加列表】 -->
    <div :class="['list-wrapping', { horizontal: direction === 'horizontal' }]">
      <div class="wrapping-header">
        <span>已添加列表</span>
        <span>{{ selectLength }}</span>
      </div>
      <div class="wrapping-content">
        <template v-if="selectLength">
          <el-input placeholder="请输入内容" v-model="searchStr" class="search-input" clearable>
            <el-select slot="prepend" v-model="searchKey" placeholder="请选择" class="search-select" @change="handleSearchKeyChange" value-key="prop">
              <el-option v-for="item in targetSearchList" :key="item.prop" :label="item.label" :value="item.prop"></el-option>
            </el-select>
            <el-button slot="append" icon="el-icon-search" @click="handleSearchStrChange"></el-button>
          </el-input>
          <TransferTable
            ref="targetTransferTableRef"
            :tableList="targetList"
            :tableColumnList="tableColumnList"
            :tableHeight="tableHeight"
            tableType="target"
            :uniqueKey="uniqueKey"
            :total="targetTotal"
            :usePagination="usePagination"
            :pagerCount="pagerCount"
            @removeSelectRow="handleRemoveSelectRow"
            @fetchTableList="getTargetTableList"
          >
            <!-- 使用穿梭表格的自定义列插槽 -->
            <template v-for="(item, index) in tableColumnList" :slot="`inner_table_${item.prop}`" slot-scope="slotData">
              <span :key="index">
                <!-- 设置新的插槽提供给消费端自定义列 -->
                <slot :name="`table_${item.prop}`" :columnProps="slotData.columnProps" :row="slotData.columnProps.row" :rowIndex="slotData.columnProps.$index">
                  {{ slotData.columnProps.row[item.prop] || '-' }}
                </slot>
              </span>
            </template>
          </TransferTable>
        </template>
        <div class="empty-box" v-else>
          <el-image class="empty-image" :src="require('@/assets/empty_images/data_empty.png')" />
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  import TransferTable from './TransferTable.vue'
  import { throttle, differenceBy, filter, isNil, noop } from 'lodash'
  export default {
    components: { TransferTable },
    props: {
      // 源数据
      sourceList: {
        type: Array,
        default: () => [],
      },
      // 表格列配置列表
      tableColumnList: {
        type: Array,
        default: () => [], // {label,prop,align}[]
      },
      // 表格数据是否加载中
      tableLoading: {
        type: Boolean,
        default: false,
      },
      // 表格高度
      tableHeight: {
        type: String | Number,
        default: 240,
      },
      // 【已添加列表】搜索项(注:与表格列配置对应,且仅能搜索字段类型为 String)
      searchList: {
        type: Array,
        default: () => [], // {label,prop}[]
      },
      // 源表格总数据的条数
      sourceTotal: {
        type: Number,
        default: 0,
      },
      // 源表格初始 pageNum(用于同步消费端初始化请求时的分页参数,进而帮助控制分页器)
      initSourcePageNum: {
        type: Number,
        default: 1,
      },
      // 源表格初始 pageSize(用于同步消费端初始化请求时的分页参数,进而帮助控制分页器)
      initSourcePageSize: {
        type: Number,
        default: 10,
      },
      // 使用分页器
      usePagination: {
        type: Boolean,
        default: false,
      },
      // 唯一标识符(便于定位到某条数据进行添加和移除操作)
      uniqueKey: {
        type: String,
        default: 'id',
      },
      // 穿梭框展示方式
      direction: {
        type: String,
        default: 'vertical', // horizontal 左右布局, vertical 上下布局
      },
      selectable: {
        type: Function,
        default: noop(),
      },
      // 页码按钮的数量,当总页数超过该值时会折叠(element规定:大于等于 5 且小于等于 21 的奇数)
      pagerCount: {
        type: Number,
        default: 7,
      },
    },
    data() {
      return {
        selectList: [], // 已选择的列表
        targetList: [], // 已添加列表的回显数据
        searchKey: '',
        searchStr: '',
        targetPageNum: 1,
        targetPageSize: 10,
        targetTotal: 10,
      }
    },
    computed: {
      targetSearchList() {
        return this.searchList.length ? this.searchList : this.tableColumnList
      },
      selectLength() {
        return this.selectList?.length || 0
      },
    },
    watch: {
      selectList(newVal) {
        this.getTargetTableList()
        this.$emit('selectionChange', newVal)
      },
    },
    mounted() {
      this.searchKey = this.targetSearchList[0].prop
      this.targetPageNum = 1
      this.targetPageSize = 10
    },
    methods: {
      handleFetchTableList(params) {
        this.$emit('fetchSourceList', params)
      },
      handleRemoveSelectRow(rowItem) {
        this.selectList = differenceBy(this.selectList, [rowItem], this.uniqueKey)
      },
      handleSearchStrChange() {
        // 每次查询时只需要重置穿梭框的页码到 1,并配置自动调用搜索函数
        this.$refs['targetTransferTableRef'].setPaginationParam({ pageNum: 1 }, true)
      },
      handleSearchKeyChange() {
        // 更新搜索 Key 之后,需要清空搜索字符串
        this.searchStr = ''
        this.$refs['targetTransferTableRef'].setPaginationParam({ pageNum: 1 }, true)
      },
      getTargetTableList(params = null) {
        const targetTableList = filter(this.selectList, (item) => {
          if (this.searchStr) {
            const itemValueToString = isNil(item[this.searchKey]) ? '' : JSON.stringify(item[this.searchKey])
            return itemValueToString.includes(this.searchStr)
          } else {
            return true
          }
        })
        this.targetTotal = targetTableList.length
        if (params) {
          this.targetPageNum = params.pageNum
          this.targetPageSize = params.pageSize
        }
        // 前端分页
        const startIndex = (this.targetPageNum - 1) * this.targetPageSize
        const endIndex = this.targetPageNum * this.targetPageSize
        this.targetList = targetTableList.slice(startIndex, endIndex)
      },
      clearSelection() {
        // 清空所有选择项(用于消费端设置的 ref 调用)
        this.selectList = []
        this.targetPageNum = 1
        this.targetPageSize = 10
        this.searchKey = this.targetSearchList[0].prop
      },
      setPaginationParam({ pageNum, pageSize }, isFetch) {
        // 设置源表格分页器参数(用于消费端设置的 ref 调用)
        //  若 isFetch 为 true,则自动调用消费端传进来的回调搜索方法
        this.$refs['sourceTransferTableRef'].setPaginationParam({ pageNum, pageSize }, isFetch)
      },
      initializeComponent(isFetch) {
        // 初始化组件(用于消费端设置的 ref 调用)
        //  若 isFetch 为 true,则自动调用消费端传进来的回调搜索方法
        this.clearSelection()
        this.setPaginationParam({ pageNum: this.initSourcePageNum || 1, pageSize: this.initSourcePageSize || 10 }, isFetch)
      },
    },
  }
</script>

<style lang="scss" scoped>
  .transfer-horizontal {
    display: flex;
  }

  .list-wrapping {
    margin-bottom: 12px;
    border-radius: 2px;
    border: 1px solid #d9d9d9;
    background: #fff;
    overflow: hidden;
  }

  .horizontal {
    flex: 1;
    margin-right: 20px;
    margin-bottom: 0px;
    &:last-child {
      margin: 0px;
    }
  }

  .wrapping-header {
    width: 100%;
    padding: 10px 20px;
    height: 40px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #d9d9d9;
    background: #f5f5f5;
    color: #333;
    font-size: 14px;
    line-height: 20px;
  }

  .wrapping-content {
    padding: 12px;
    width: 100%;
  }

  .search-input {
    margin-bottom: 12px;
    max-width: 500px;
    height: 32px;
  }

  .search-select {
    width: 120px;
  }

  .empty-box {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    margin: 40px 0px;
  }

  .empty-image {
    width: 150px;
    height: 150px;
  }

  :deep(.search-input .el-input-group__prepend) {
    background-color: #fff;
  }

  :deep(.el-select .el-input .el-select__caret) {
    color: #3564ff;
  }
</style>

./TransferPlus/TransferTable.vue

<template>
  <div>
    <!-- 表格区域 -->
    <el-table
      ref="transferTable"
      v-loading="tableLoading"
      :border="true"
      :data="tableList"
      size="mini"
      :stripe="true"
      :height="tableHeight || 'auto'"
      :row-class-name="getTableRowClassName"
      :header-cell-style="{
        background: '#F1F1F1',
      }"
      @select="handleSelect"
      @select-all="handleSelectAll"
    >
      <el-table-column type="index" align="center"></el-table-column>
      <el-table-column type="selection" width="50" v-if="tableType === 'source'" :selectable="selectable"></el-table-column>
      <el-table-column v-for="(item, index) in tableColumnList" :key="item.prop || index" :label="item.label" :prop="item.prop" :align="item.align || 'left'" :width="item.width || 'auto'" show-overflow-tooltip>
        <template #default="columnProps">
          <slot :name="`inner_table_${item.prop}`" :columnProps="columnProps">
            <span>{{ columnProps.row[item.prop] }}</span>
          </slot>
        </template>
      </el-table-column>
      <el-table-column fixed="right" label="操作" width="70" align="center" v-if="tableType === 'target'">
        <template slot-scope="scope">
          <el-button @click="handleRemoveRowItem(scope.row, scope.$index)" type="text" icon="el-icon-delete" size="medium"></el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页器区域 -->
    <div v-if="usePagination" class="pagination-box">
      <!-- 实现两侧分布的分页器布局:使用两个分页器组件 + 不同 layout 组成 -->
      <el-pagination background :current-page="pageNum" :layout="layoutLeft" :page-size="pageSize" :pager-count="pagerCount" :total="total" @current-change="handleCurrentChange" @size-change="handleSizeChange" />
      <el-pagination background :current-page="pageNum" :layout="layoutRight" :page-size="pageSize" :pager-count="pagerCount" :total="total" @current-change="handleCurrentChange" @size-change="handleSizeChange" />
    </div>
  </div>
</template>

<script>
  import { differenceBy, uniqBy, noop } from 'lodash'

  export default {
    props: {
      // 已勾选的数组
      value: {
        type: Array,
        default: () => [],
        require: true,
      },
      // 表格数据
      tableList: {
        type: Array,
        default: () => [],
      },
      // 表格列配置列表
      tableColumnList: {
        type: Array,
        default: () => [], // {label,prop,align}[]
      },
      // 表格数据是否加载中
      tableLoading: {
        type: Boolean,
        default: false,
      },
      // 表格高度
      tableHeight: {
        type: String | Number,
        default: 240,
      },
      // 表格数据类型
      tableType: {
        type: String,
        default: 'source', // source 源列表,target 目标列表
      },
      // 【已添加列表】搜索项(注:与表格列配置对应,且仅能字段类型为 String)
      searchList: {
        type: Array,
        default: () => [], // {label,prop,align}[]
      },
      // 分页后表格总数据的条数
      total: {
        type: Number,
        default: 0,
      },
      // 初始 pageNum
      initPageNum: {
        type: Number,
        default: 1,
      },
      // 初始 pageSize
      initPageSize: {
        type: Number,
        default: 10,
      },
      // 使用分页器
      usePagination: {
        type: Boolean,
        default: false,
      },
      // 唯一标识符(便于定位到某条数据进行添加和移除操作)
      uniqueKey: {
        type: String,
        default: 'id',
      },
      // Function 的返回值用来决定这一行的 CheckBox 是否可以勾选
      selectable: {
        type: Function,
        default: noop(),
      },
      // 页码按钮的数量,当总页数超过该值时会折叠(element规定:大于等于 5 且小于等于 21 的奇数)
      pagerCount: {
        type: Number,
        default: 7,
      },
    },
    data() {
      return {
        layoutLeft: 'total',
        layoutRight: 'sizes, prev, pager, next',
        pageNum: 1,
        pageSize: 10,
        preSelectList: [], // 上一次选择的数据(点击分页器就清空)
        stashSelectList: [], // 暂存数据,便于点击页码后,还能保存前一页的数据
        isNeedToggle: true, // 是否需要勾选该页已选择项(用于换页后的回显选择项)
        isTableChangeData: false, // 是否是当前表格造成选择项的变化(用于同步【待选择列表】的勾选项)
      }
    },
    computed: {
      currentPageSelectList() {
        const currentSelectList = []
        this.stashSelectList?.forEach((item) => {
          const currentRow = this.tableList?.find((row) => row[this.uniqueKey] === item[this.uniqueKey])
          if (currentRow) {
            currentSelectList.push(currentRow)
          }
        })
        return currentSelectList
      },
    },
    watch: {
      value(newVal) {
        this.stashSelectList = newVal || []
        // 只有在其他地方修改了选择表格数据后,才刷新覆盖勾选项(当前表格修改选择项是双向绑定的,所以不需要刷新覆盖勾选项),实现精准回显和两表格的联动
        if (!this.isTableChangeData) {
          this.handleToggleSelection()
        }
        // 当暂存的选择列表为空时,需要同步更新 preSelect 为空数组,以便下次选择时进行判断是增加选择项还是减少选择项
        if (!this.stashSelectList.length) {
          this.preSelectList = []
        }
        this.isTableChangeData = false
      },
      tableList() {
        if (this.isNeedToggle) {
          this.preSelectList = this.currentPageSelectList
          this.handleToggleSelection()
          this.isNeedToggle = false
        }
      },
    },
    mounted() {
      this.pageNum = this.initPageNum || 1
      this.pageSize = this.initPageSize || 110
      // 解决右侧固定操作栏错位问题
      this.$nextTick(() => {
        this.$refs.transferTable.doLayout()
      })
      this.$emit('selectionChange', [])
    },
    methods: {
      getTableRowClassName({ rowIndex }) {
        if (rowIndex % 2 == 0) {
          return ''
        } else {
          return 'stripe-row'
        }
      },
      fetchTableList(pageNum = 1) {
        if (this.usePagination) {
          // 若不是页码更改触发,则默认将 pageNum 重置为 1
          this.pageNum = pageNum
          const params = {
            pageNum: this.pageNum,
            pageSize: this.pageSize,
          }
          this.$emit('fetchTableList', params)
        } else {
          this.$emit('fetchTableList')
        }
      },
      setPaginationParam({ pageNum, pageSize }, isFetch = false) {
        // 设置分页器参数(用于消费端设置的 ref 调用)
        this.pageNum = pageNum || this.pageNum
        this.pageSize = pageSize || this.pageSize
        this.isNeedToggle = true
        if (isFetch) {
          this.fetchTableList()
        }
      },
      handleSizeChange(val) {
        this.pageSize = val
        this.isNeedToggle = true
        this.fetchTableList()
      },
      handleCurrentChange(val) {
        this.isNeedToggle = true
        this.fetchTableList(val)
      },
      handleStashSelectList(isAdd = true, list = []) {
        if (isAdd) {
          // 暂存数组中增加,并兜底去重
          this.stashSelectList = uniqBy([...this.stashSelectList, ...list], this.uniqueKey)
        } else {
          // 暂存数组中移除
          this.stashSelectList = differenceBy(this.stashSelectList, list, this.uniqueKey)
        }
        this.isTableChangeData = true
        this.$emit('input', this.stashSelectList)
        this.$emit('selectionChange', this.stashSelectList)
      },
      handleSelect(selectList, row) {
        // 判断是否是增加选择项
        const isAddRow = this.preSelectList.length < selectList.length
        this.handleStashSelectList(isAddRow, [row])
        // 更新当前页记录的上次数据
        this.preSelectList = [...selectList]
      },
      handleSelectAll(selectList) {
        // 判断是否是全选(需要考虑两个数组长度相等的情况)
        const isAddAll = this.preSelectList.length <= selectList.length
        // 更新当前页记录的上次数据
        this.handleStashSelectList(isAddAll, isAddAll ? selectList : this.preSelectList)
        this.preSelectList = [...selectList]
      },
      handleRemoveRowItem(rowItem, rowIndex) {
        const remainderPage = this.total % this.pageSize ? 1 : 0
        const pageNumTotal = parseInt(this.total / this.pageSize) + remainderPage
        const isLastPageOnlyOne = rowIndex === 0 && this.pageNum === pageNumTotal
        // 判断删除的是否是最后一页中只有一条的数据
        if (isLastPageOnlyOne && this.pageNum > 1) {
          // 若是,则 pageNum 需要往前调整一页,因为删除后最后一页不存在
          this.handleCurrentChange(this.pageNum - 1)
        }
        this.$emit('removeSelectRow', rowItem)
      },
      handleToggleSelection() {
        this.$nextTick(() => {
          // 先清除所有勾选状态
          this.$refs.transferTable.clearSelection()
          if (this.currentPageSelectList.length) {
            // 再依次勾选当前页存在的行
            this.currentPageSelectList.forEach((item) => {
              this.$refs.transferTable.toggleRowSelection(item, true)
            })
          }
        })
      },
    },
  }
</script>

<style scoped>
  /* 表格斑马自定义颜色 */
  :deep(.el-table__row.stripe-row) {
    background: #f9f9f9;
  }

  /* 表格操作栏按钮取消间距 */
  :deep(.el-button) {
    padding: 0px;
  }

  /* 表格操作栏按钮固定大小 */
  :deep(.el-icon-delete::before) {
    font-size: 14px !important;
  }

  .pagination-box {
    display: flex;
    justify-content: space-between;
  }
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/881058.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《史上最简单的 SpringCloud 教程》

Finchley版本 Spring Cloud Finchley; Spring Boot 2.0.3 史上最简单的 SpringCloud 教程 | 第一篇: 服务的注册与发现&#xff08;Eureka&#xff09;(Finchley版本)史上最简单的SpringCloud教程 | 第二篇: 服务消费者&#xff08;restribbon&#xff09;(Finchley版本)史上最…

有没有自带财务管理功能的海外仓系统?

在全球化的商业环境中&#xff0c;海外仓作为连接国际市场的物流枢纽&#xff0c;其重要性日益凸显。然而&#xff0c;随着业务范围的扩展和费用类型的多样化&#xff0c;海外仓在财务管理上面临着诸多挑战。传统的手工计费和对账方式不仅耗时费力&#xff0c;而且容易出错&…

网关登录校验(2)----网关如何将用户信息传递给微服务

1.微服务获取用户信息 现在&#xff0c;网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时&#xff0c;微服务又该如何获取用户身份呢&#xff1f; 由于网关发送请求到微服务依然采用的是Http请求&#xff0c;因此我们可以将用户信息以请求头…

Zabbix 部署----安装Zabbix(业务主机)

目录 1、另外准备一台虚拟机(192.xx.xx.20) 设置主机名 关闭防火墙、selinux 准备zabbix-repo 安装zabbix-agent 配置主服务器地址 启动zabbix-agent&#xff1a;10050 1、另外准备一台虚拟机(192.xx.xx.20) 设置主机名 hostname web1 关闭防火墙、selinux syst…

【HTTP】请求“报头”(Host、Content-Length/Content-Type、User-Agent(简称 UA))

Host 表示服务器主机的地址和端口号 URL 里面不是已经有 Host 了吗&#xff0c;为什么还要写一次&#xff1f; 这里的 Host 和 URL 中的 IP 地址、端口什么的&#xff0c;绝大部分情况下是一样的&#xff0c;少数情况下可能不同当前我们经过某个代理进行转发。过程中&#xf…

『功能项目』QFrameWork道具栏物品生成【64】

我们打开上一篇63QFrameWork框架重构OnGUI的项目&#xff0c; OnGUI优点&#xff1a; 简单易用&#xff1a;OnGUI是基于代码的UI系统&#xff0c;对于简单的调试界面或者小型项目来说&#xff0c;可以快速实现UI需求。即时更新&#xff1a;OnGUI的UI元素是即时更新的&#xff…

STM32外设之LTDC/DMA2D—液晶显示(野火)

文章目录 显示屏有几种?基本参数控制?显存 LTDC 液晶控制器LTDC 结构框图LTDC 初始化结构体 LTDC_InitTypeDefLTDC 层级初始化结构体 DMA2D 图形加速器DMA2D 初始化结构体 要了解什么 屏幕是什么&#xff0c;有几种屏&#xff0c;有什么组成。 怎么控制&#xff0c;不同屏幕控…

vue3 透传 Attributes

前言 Vue 3 现在正式支持了多根节点的组件&#xff0c;也就是片段&#xff01; Vue 2.x 遵循单根节点组件的规则&#xff0c;即一个组件的模板必须有且仅有一个根元素。 为了满足单根节点的要求&#xff0c;开发者会将原本多根节点的内容包裹在一个<div>元素中&#x…

如何联系真正的开发者而非公司??

&#x1f3c6;本文收录于《全栈Bug调优(实战版)》专栏&#xff0c;主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&am…

【2024/09/20更新】植物大战僵尸杂交版V2.5下载

植物大战僵尸杂交版V2.5 2.5版本更新公告&#xff1a; 增加新关卡– 益智模式–两面夹击模式关卡 挑战模式关卡 增加新铲子–骷髅铲子 --银币购买-挖掉植物触发亡语或召唤骷髅僵尸 增加新植物– 4张白卡植物-通过两面夹击获得 2张金卡植物和4张星卡植物 游戏调整– 调整…

无人机之控制距离篇

无人机的控制距离是一个复杂且多变的概念&#xff0c;它受到多种因素的共同影响。以下是对无人机控制距离及其影响因素的详细分析&#xff1a; 一、无人机控制距离的定义 无人机控制距离指的是遥控器和接收机之间的最远传输距离。这个距离决定了无人机在操作者控制下能够飞行的…

three.js shader 实现天空中白云

three.js shader 实现天空中白云 预览&#xff1a; https://threehub.cn/#/codeMirror?navigationThreeJS&classifyshader&idwhiteCloud 更多案例 可见 预览&#xff1a; https://threehub.cn import * as THREE from "three"; import { OrbitControls …

【二级C语言考试】自定义数据类型

C语言二级考试——自定义数据类型 十、结构体&#xff08;即“结构”&#xff09;与共同体&#xff08;即“联合”&#xff09; 用 typedef说明一个新类型。结构体和共用体类型数据的定义和成员的引用。通过结构体构成链表&#xff0c;单向链表的建立&#xff0c;结点数据的输出…

Windows安全日志分析(事件ID详解)

目录 如何查看Windows安全日志 常见事件ID列表 事件ID 1116 - 防病毒软件检测到恶意软件 事件ID 4624 - 账户登录成功 事件ID 4625 - 账户登录失败 事件ID 4672 - 为新登录分配特殊权限 事件ID 4688 - 新进程创建 事件ID 4689 - 进程终止 事件ID 4720 - 用户账户创建 …

Git常用指令大全详解

Git常用指令大全详解 Git&#xff0c;作为目前最流行的分布式版本控制系统&#xff0c;其强大的功能和灵活性为开发者提供了极大的便利。无论是个人项目还是团队协作&#xff0c;Git都扮演着不可或缺的角色。本文将详细总结Git的常用指令&#xff0c;帮助大家更好地掌握这一工…

Linux-mysql5.7-mysql8.0安装包下载及安装教程,二合一

一、安装包下载 1、手动下载 MySQL :: Download MySQL Community Server 2、wegt下载 wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.24-linux-glibc2.12-x86_64.tar.gz 登录自己的liunx &#xff0c;复制上面的命令下载。 二、手动安装 1、上传压缩包到…

【数据结构与算法 | 灵神题单 | 自底向上DFS篇】力扣965, 2331, 100, 1379

1. 力扣965&#xff1a;单值二叉树 1.1 题目&#xff1a; 如果二叉树每个节点都具有相同的值&#xff0c;那么该二叉树就是单值二叉树。 只有给定的树是单值二叉树时&#xff0c;才返回 true&#xff1b;否则返回 false。 示例 1&#xff1a; 输入&#xff1a;[1,1,1,1,1,n…

EI-BISYNCH协议,欧陆2000系列设备读取数据

EI-Bisynch是一种基于ANSI X3.28-2.5 A4标准的专有协议&#xff0c;用于消息框架。尽管其名称中包含“Bisynch”&#xff0c;但它实际上是一种基于ASCII的异步协议。数据通过7位数据位、偶校验和1个停止位进行传输。 4.1 术语解释 4.1.1 地址 每个仪器都有一个可配置的地址&…

数据结构之二叉树(1)

数据结构之二叉树&#xff08;1&#xff09; 一、树 1、树的概念与结构 &#xff08;1&#xff09;树是一种非线性的数据结构&#xff0c;由n(n>0)个有限结点组成一个具有层次关系的集合。 &#xff08;2&#xff09;树有一个特殊的结点&#xff0c;叫做根结点&#xff…

shell指令及笔试题

一&#xff1a;linux基本指令考察 创建文件&#xff0c;直接在本目录的上级目录下创建一个名为dir1的文件夹&#xff0c;并在dir1文件夹下创建一个名为file1的文件 答&#xff1a;本目录的上级目录下创建一个名为dir1的文件:mkdir ../dir1 在dir1文件夹下创建一个名为file1的…