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>