ElementUI 穿梭框

children.vue
<template>
  <div class="transfer-with-loadmore">
    <div class="transfer-panel">
      <div class="panel-header left-panel-header">
        <span>候选列表</span>  
        <el-input
          v-model="leftSearch"
          size="small"
          placeholder="搜索..."
          clearable
          class="search-input"
          @change="searchInpChange"
        />
      </div>
      <div
        ref="leftPanel"
        v-infinite-scroll="handleScroll"
        :infinite-scroll-distance="50"
        :infinite-scroll-immediate="false"
        :infinite-scroll-disabled="disabled"
        class="panel-body"
        style="overflow:auto"
      >
        <el-checkbox-group v-model="leftChecked" @change="onLeftCheckChange">
          <div
            v-for="item in leftData"
            :key="item[keyProp]"
            class="transfer-item"
          >
            <el-checkbox :label="item" :disabled="isItemDisabled(item)" class="transfer-checkbox">
              {{ item[labelProp] }}
            </el-checkbox>
          </div>
        </el-checkbox-group>
        <div v-if="loading" class="loading-more">
          加载中...
        </div>
        <div v-else-if="noMore" class="no-more">
          没有更多了
        </div>
      </div>
    </div>

    <div class="transfer-buttons">
      <el-button
        class="moveToRight"
        type="primary"
        icon="el-icon-arrow-right"
        :disabled="leftChecked.length === 0"
        size="mini"
        circle
        @click="moveToRight"
      />
      <el-button
        class="moveToLeft"
        type="primary"
        icon="el-icon-arrow-left"
        :disabled="rightChecked.length === 0"
        size="mini"
        circle
        @click="moveToLeft"
      />
    </div>

    <div class="transfer-panel">
      <div class="panel-header">
        <span>已选列表</span>
      </div>
      <div class="panel-body">
        <el-checkbox-group v-model="rightChecked" @change="onRightCheckChange">
          <div
            v-for="item in rightData"
            :key="item[keyProp]"
            class="transfer-item"
          >
            <el-checkbox :label="item" :disabled="isItemDisabled(item)" class="transfer-checkbox">
              {{ item[labelProp] }}
              <span v-if="isItemDisabled(item)" class="disabled-tag">(默认)</span>
            </el-checkbox>
          </div>
        </el-checkbox-group>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'TransferWithLoadMore',
  model: {
    prop: 'value',
    event: 'input',
  },
  props: {
    value: {
      type: Array,
      default: () => [],
    },
    // 禁用且默认选中的 key 数组
    disabledKeys: {
      type: Array,
      default: () => [],
    },
    // 获取左侧数据的函数,需返回 Promise<{ list: [], total: number }>
    loadLeftData: {
      type: Function,
      required: true,
    },
    // 每页数量
    pageSize: {
      type: Number,
      default: 20,
    },
    // 显示字段名
    labelProp: {
      type: String,
      default: 'label',
    },
    // 唯一键字段名
    keyProp: {
      type: String,
      default: 'id',
    },
  },
  data() {
    return {
      leftData: [],
      rightData: [],
      leftChecked: [],
      rightChecked: [],
      leftSearch: '',
      currentPage: 1,
      total: 0,
      loading: false,
      noMore: false,
    }
  },
  computed: {
    disabled() {
      return this.loading || this.noMore
    },
  },
  watch: {
    value: {
      handler(newVal) {
        // this.syncRightData(newVal)
        this.rightData = newVal
      },
      immediate: true,
    },
  },
  mounted() {
    this.loadMore()
  },
  methods: {
    isItemDisabled(item) {
      return this.disabledKeys.includes(item[this.keyProp])
    },
    searchInpChange() {
      this.currentPage = 1
      this.leftData = []
      this.noMore = false
      this.loadMore()
    },
    async loadMore() {
      if (this.loading || this.noMore)
        return false

      this.loading = true
      await new Promise((resolve) => setTimeout(resolve, 1000))

      try {
        const { list, total } = await this.loadLeftData({
          pageNum: this.currentPage,
          pageSize: this.pageSize,
          keyword: this.leftSearch,
        })

        if (list.length === 0) {
          this.noMore = true
        }
        else {
          this.leftData = [...this.leftData, ...list]
          this.total = total
          this.currentPage++
          this.noMore = this.leftData.length >= total
        }

        // 如果是第一页加载完成,通知父组件, 用于初始化时,获取已赋权用户
        if (this.currentPage === 2 && !this.leftSearch) {
          this.$emit('first-load-complete')
        }
      }
      catch (error) {
        console.error('加载数据失败', error)
      }
      finally {
        this.loading = false
      }
    },

    handleScroll(e) {
      this.loadMore()
    },

    moveToRight() {
      if (this.leftChecked.length === 0)
        return

      // 获取移动的项(取到在左侧中没有右侧的项)
      const movedItems = this.leftChecked.filter((i) => !this.rightData.some((k) => i[this.keyProp] === k[this.keyProp]))
      // 更新右侧数据
      this.rightData = [
        ...this.rightData,
        ...movedItems,
      ]

      // 触发 v-model 更新
      this.$emit(
        'input',
        this.rightData,
      )

      // 清空左侧选中
      this.leftChecked = []
    },

    moveToLeft() {
      if (this.rightChecked.length === 0)
        return

      // 删除右侧选中的节点
      const checkedSet = new Set(this.rightChecked.map((i) => i[this.keyProp]))
      this.rightData = this.rightData.filter((item) => !checkedSet.has(item[this.keyProp]))

      // 触发 v-model 更新
      this.$emit(
        'input',
        this.rightData,
      )

      // 清空右侧选中
      this.rightChecked = []
    },

    onLeftCheckChange(val) {
      this.leftChecked = val
    },

    onRightCheckChange(val) {
      this.rightChecked = val
    },
  },
}
</script>

<style scoped>
.transfer-with-loadmore {
  display: flex;
  align-items: stretch;
  gap: 16px;
}

.transfer-panel {
  flex: 1;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  display: flex;
  flex-direction: column;
  height: 400px;
}

.panel-header {
  padding: 12px;
  background-color: #f5f7fa;
  border-bottom: 1px solid #dcdfe6;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.panel-header.left-panel-header{
  padding: 6px;
}

.search-input {
  width: 180px;
}

.panel-body {
  flex: 1;
  overflow-y: auto;
  padding: 8px;
}

.transfer-item {
  padding: 6px 0;
}

.transfer-checkbox ::v-deep .el-checkbox__label {
  width: 100%;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.transfer-buttons {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 8px;
}

.loading-more,
.no-more {
  text-align: center;
  color: #999;
  padding: 8px;
  font-size: 12px;
}

.moveToRight,.moveToLeft{
  width: 40px;
  height: 40px;
  margin-left: 0;
}
</style>

parent.vue
<template>
  <el-dialog
    :visible.sync="visible"
    :title="title"
    :close-on-click-modal="false"
    :before-close="beforeClose"
    :width="width"
    :destroy-on-close="true"
  >
    <TransferWithLoadMore
      v-model="checkedNodes"
      :load-left-data="getUsers"
      :disabled-keys="defaultDisabledNodes"
      label-prop="fullName"
      key-prop="id"
      :page-size="100"
      @first-load-complete="onFirstLoadComplete"
    />
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="handleCancel">取 消</el-button>
        <el-button type="primary" :loading="loading" @click="handleConfirm">确 定</el-button>
      </span>
    </template>
  </el-dialog>
</template>

<script>
import { batchTableAdd, permissionUserList, singleTableAdd } from '@/api/approval-flow.js'
import TransferWithLoadMore from '@/page-components/search/components/TransferWithLoadMore.vue'

export default {
  name: 'BaseDialog',
  components: {
    // Transfer,
    TransferWithLoadMore,
  },
  props: {
    title: {
      type: String,
      default: '对话框',
    },
    width: {
      type: String,
      default: '50%',
    },
    value: {
      type: Boolean,
      default: false,
    },
    type: {
      type: String,
      default: 'single',
    },
    tableIds: {
      type: Array,
      default: () => [],
    },
    isOpenCustomConfirmEvent: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      checkedNodes: [],
      ruleList: [],
      defaultDisabledNodes: [],
      loading: false,
      leftData: [],
      hasFetchedPermission: false, // 防止重复调用,
    }
  },
  computed: {
    visible: {
      set() {
        this.$emit('input', false)
      },
      get() {
        return this.value
      },
    },
  },
  created() {
    this.getUsers()
  },
  methods: {
    async getUsers({ pageNum, pageSize, keyword }) {
      try {
        const { payload } = await this.$axios.$get('/deepexi-console/api/v1/user', {
          params: {
            pageNum,
            pageSize,
            nickname: keyword,
          },
        })

        return {
          list: payload.rows.map((x) => {
            return {
              id: x.userId,
              label: x.nickname, // 昵称
              userName: x.username, // 用户名、账号
              fullName: `${x.nickname}(${x.username})`,
            }
          }),
          total: payload.total,
        }
      }
      catch (e) {
        console.log(e)
      }
    },
    onFirstLoadComplete() {
      if (this.type === 'single' && !this.hasFetchedPermission) {
        this.getPermissionUserList()
        this.hasFetchedPermission = true
      }
    },
    getPermissionUserList() {
      this.$axios.$get(permissionUserList, {
        params: {
          assetId: this.$route.query.id,
        },
      }).then((y) => {
        this.checkedNodes = y.payload.map((item) => ({ ...item, fullName: `${item.nickname}(${item.username})` }))
      }).catch((e) => {
        console.error('获取已赋权用户失败', e)
      })
    },
    async postBatchTableAdd() {
      try {
        return await this.$axios.$post(batchTableAdd, {
          assetIdList: this.tableIds,
          userIdList: this.checkedNodes.map((item) => item.id),
        })
      }
      catch (error) {}
    },
    async postSingleTableAdd() {
      try {
        return await this.$axios.$post(singleTableAdd, {
          assetId: this.$route.query.id,
          userIdList: this.checkedNodes.map((item) => item.id),
        })
      }
      catch (error) {}
    },
    handleCancel() {
      this.$emit('input', false)
    },
    handleConfirm() {
      if (!this.checkedNodes.length) {
        this.$message.error('请正确选择要赋权的用户')
        return false
      }
      this.loading = true
      if (this.isOpenCustomConfirmEvent) {
        this.$emit('confirm', this.checkedNodes.map((item) => item.id), () => {
          this.loading = false
        })
        return
      }
      const request = this.type === 'single' ? this.postSingleTableAdd : this.postBatchTableAdd
      request().then((res) => {
        if (res.payload) {
          this.$emit('input', false)
          this.$message.success('赋权成功')
          this.checkedNodes = []
        }
        else {
          this.$message.error('赋权失败')
        }
        this.loading = false
      }).catch(() => {
        this.loading = false
      })
    },
    beforeClose(done) {
      this.$emit('input', false)
      done()
    },
  },
}
</script>

<style scoped>
.dialog-footer {
  text-align: right;
}
</style>

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容