Vue 实现高级穿梭框 Transfer 封装

news2024/11/16 6:49:03

文章目录

  • 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>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

爬虫----webpack

目录 一. 什么是webpack 出现的原因&#xff1a;同名函数 概念: 特征&#xff1a;大量缩进 webpack的格式 简单的webpack格式&#xff1a; 详细的webpack格式&#xff1a; 几个参数的运用 1. webpack数组形式 2. webpack对象格式 3.多个js文件打包 打印要扣的代码 …

【STM32 Blue Pill编程实例】-手机通过HC-05串口蓝牙控制LED

手机通过HC-05串口蓝牙控制LED 文章目录 手机通过HC-05串口蓝牙控制LED1、HC-05串口蓝牙模块介绍2、硬件准备和接线3、模块配置4、代码实现5、手机控制在本文中,我们介绍如何使用 STM32CubeIDE 和 HAL 库将 HC-05 蓝牙模块与 STM32 Blue Pill 开发板连接。 我们将使用 Android…

HarmonyOS 应用获取公钥和 MD5 指纹签名信息

鸿蒙版本获取 MD5 指纹和公钥可参考如下方式; 首先,通过 AGC 官网 将所需证书下载至本地; 其次,通过记事本或者文本编译器的方式将其正式打开,将其内容中前两项 BEGIN CERTIFICATE 和 END CERTIFICATE 的段落删除,仅保留最后一段中的内容(包括 BEGIN CERTIFICATE 和 END CERTI…

Jboss CVE-2017-12149 靶场攻略

漏洞简述 该漏洞为 Java反序列化错误类型&#xff0c;存在于 Jboss 的 HttpInvoker 组件中的 ReadOnlyAccessFilter过滤器中。该过滤器在没有进⾏任何安全检查的情况下尝试将来⾃客户端的数据流进⾏反序列化&#xff0c;从⽽导 致了漏洞 漏洞范围 JBoss 5.x/6.x 环境搭建 …

Xinstall全链路解决方案,让社交App推广效果倍增!

随着互联网的飞速发展&#xff0c;社交App如雨后春笋般涌现&#xff0c;然而&#xff0c;如何在激烈的市场竞争中脱颖而出&#xff0c;成为每个推广者亟待解决的问题。今天&#xff0c;就让我们来揭秘Xinstall如何助力社交App打破运营推广瓶颈&#xff0c;让你的推广之路更加顺…

前端vue-父传值给儿子或者孙子(依赖注入),只能从上层传给下层组件

不是响应式的 加上computed包裹就是响应式的&#xff0c;但是发明这个传值方法的目的就是不让他成为响应式的&#xff0c;所以推荐使用上面的写法。

对商品分类系统的若干问题的思考

科学研究的目的就是研究事物的特征&#xff0c;并根据共同的特征加以分类 商品分类是商业&#xff0c;制造业中最普遍的活动&#xff0c;几乎所有的企业&#xff0c;电商平台都要对销售的商品&#xff0c;使用的原材料&#xff08;BOM&#xff09;进行分类和编号。 商品分类貌似…

OpenHarmony(鸿蒙南向开发)——小型系统内核(LiteOS-A)【内核通信机制】下

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ 子系统开发内核 轻量系统内核&#xff08;LiteOS-M&#xff09; 轻量系统内核&#…

前端自动化测试框架:如何选择最适合你的方案

前端自动化测试是指使用代码或工具来模拟用户在浏览器上的操作&#xff0c;以检验网页或应用程序的功能和性能是否符合预期。前端自动化测试可以提高开发效率&#xff0c;减少人工错误&#xff0c;保证软件质量和用户体验。 但是&#xff0c;在众多的前端自动化测试框架中&…

鸿蒙应用开发:视频播放

AVPlayer API同样适用于视频播放&#xff0c;与音频播放的区别仅在于是否提供surfaceId&#xff08;相当于给视频播放选择一个容器&#xff09;。鸿蒙官网描述&#xff0c;surfaceId指视频窗口ID&#xff0c;只允许在initialized状态下设置&#xff0c;在视频播放时要指定窗口进…

破损shp文件修复

大家中秋节快乐&#xff01; 介绍 半年前写过破损shp文件修复的内容&#xff0c;当时写了一个简陋的小工具。 现在重新讲讲这方面的内容&#xff0c;这篇文章也主要围绕以下两个方面进行展开&#xff1a; 1、当时是怎么实现破损shp文件修复 2、现在怎么把这个功能集成到rs…

LINUX网络编程:TCP(1)

目录 1.认识Tcp的报头 2.确认应答机制&#xff08;ACK&#xff09; 序号与确认序号 捎带应答 3.超时重传机制 4.Tcp连接管理 三次握手 为什是三次握手 四次挥手 理解TIMEWAIT 1.认识Tcp的报头 源端口和目的端口号没什么说的 32位的序号和确认序号&#xff0c;之后会介…

如何使用ssm实现毕业生学历证明系统+vue

TOC ssm651毕业生学历证明系统vue 绪论 1.1 课题背景 二十一世纪互联网的出现&#xff0c;改变了几千年以来人们的生活&#xff0c;不仅仅是生活物资的丰富&#xff0c;还有精神层次的丰富。在互联网诞生之前&#xff0c;地域位置往往是人们思想上不可跨域的鸿沟&#xff0…

Linux C# DAY3

作业 1、 #!/bin/bash mkdir -p ~/dir/dir1 mkdir ~/dir/dir2 cp ./* ~/dir/dir1 cp ./*.sh ~/dir/dir2 cd ~/dir/ tar -cvJf dir2.tar.xz ./dir2 mv dir2.tar.xz ~/dir/dir1/ cd ~/dir/dir1/ tar -xvf dir2.tar.xz 2、 #!/bin/bash head -5 /etc/group | tail -1 sudo mkdi…

Docker安装mysql并配置主从,超详细

简介&#xff1a; 本文使用docker安装mysql&#xff0c;并创建master节点&#xff0c;slave节点用于实现主从。废话不多说&#xff0c;直接开始。 1.docker下载镜像&#xff0c;这里我以5.7版本为例。 docker pull mysql:5.7 2.在宿主机上新建如下目录&#xff0c;进行文件挂…

Lichee NanoKVM基本使用环境

Lichee NanoKVM基本使用环境 本文章主要记录一些自己在初期的使用&#xff0c;以及自己的一些经验 &#xff0c;非常感谢sipeed NanoKVM官方使用教程 外观&#xff08;博主自己的是lite版本&#xff0c;非常感谢sipeed&#xff09; Lichee NanoKVM 是基于 LicheeRV Nano 的 I…

专为GOA TFT-LCD面板设计的16ch水平移位器-iML7272A

GOA是Gate on Array的简写&#xff0c;简单可以理解为gate IC集成在玻璃上了&#xff0c;面板就可以不用gate ic了&#xff0c;窄边框面板大多数都用了GOA技术。随着窄边框设计的日益流行&#xff0c;面板设计的周边空间被逐渐压缩&#xff0c;在传统的GOA电路设计中&#xff0…

Python学习——【3.1】函数

文章目录 【3.1】函数一、函数的定义二、函数的参数三、函数的返回值&#xff08;一&#xff09;函数返回值的定义&#xff08;二&#xff09;None类型 四、函数的说明文档五、函数的嵌套调用六、函数中变量的作用域&#xff08;一&#xff09;局部变量&#xff08;二&#xff…

(数组) LeetCode 1184. 公交站间的距离

原题链接 一. 题目描述 环形公交路线上有 n 个站&#xff0c;按次序从 0 到 n - 1 进行编号。我们已知每一对相邻公交站之间的距离&#xff0c;distance[i] 表示编号为 i 的车站和编号为 (i 1) % n 的车站之间的距离。 环线上的公交车都可以按顺时针和逆时针的方向行驶。 …

解决:The play() request was interrupted by a call to pause().报错

前言&#xff1a; 最近在公司中实现进入页面之后点击单词直接播放音频的时候&#xff0c;发现音频并不会播放声音&#xff0c;并且控制台报错&#xff1a; 研究之后找到了解决方案&#xff0c;与小伙伴们进行分享 原因&#xff1a; 首先看这句话的意思&#xff1a; 在调用 …