This is an automated email from the ASF dual-hosted git repository. xxyu pushed a commit to branch kylin5 in repository https://gitbox.apache.org/repos/asf/kylin.git
commit 9572924c450db39661027233ac7de1224e039407 Author: Qian Xia <lauraxiaq...@gmail.com> AuthorDate: Fri Jul 28 18:45:30 2023 +0800 KYLIN-5675 fix usergroup assign large user bug --- .../src/components/common/GroupEditModal/index.vue | 318 ++++++++++++++++----- .../src/components/common/GroupEditModal/store.js | 15 +- kystudio/src/service/user.js | 3 + kystudio/src/store/types.js | 1 + kystudio/src/store/user.js | 3 + 5 files changed, 262 insertions(+), 78 deletions(-) diff --git a/kystudio/src/components/common/GroupEditModal/index.vue b/kystudio/src/components/common/GroupEditModal/index.vue index ae26752d1a..03d762fe6d 100644 --- a/kystudio/src/components/common/GroupEditModal/index.vue +++ b/kystudio/src/components/common/GroupEditModal/index.vue @@ -22,8 +22,9 @@ :total-elements="totalSizes" :show-overflow-tip="true" :titles="[$t('willCheckGroup'), $t('checkedGroup')]" - @change="value => transferInputHandler('selected_users', value)"> + @change="(value, dir, arr) => transferInputHandler('selected_users', value, dir, arr)"> <div class="load-more-uers" slot="left-remote-load-more" v-if="isShowLoadMore" @click="loadMoreUsers(searchValueLeft)">{{$t('kylinLang.common.loadMore')}}</div> + <div class="load-more-uers" slot="right-remote-load-more" v-if="isShowRightLoadMore" @click="loadMoreSelectedUsers(searchValueRight)">{{$t('kylinLang.common.loadMore')}}</div> </el-transfer> </el-form-item> </el-form> @@ -59,8 +60,7 @@ vuex.registerModule(['modals', 'GroupEditModal'], store) form: state => state.form, isShow: state => state.isShow, editType: state => state.editType, - callback: state => state.callback, - totalUsers: state => state.totalUsers + callback: state => state.callback }) }, methods: { @@ -74,13 +74,20 @@ vuex.registerModule(['modals', 'GroupEditModal'], store) // 后台接口请求 ...mapActions({ saveGroup: 'ADD_GROUP', - loadUsersList: 'LOAD_USERS_LIST', + loadUsersList: 'GET_UNASSIGNED_USERS', addUserToGroup: 'ADD_USERS_TO_GROUP' }) }, locales }) export default class GroupEditModal extends Vue { + /** + * 穿梭框组件的几个字段重点说明 + * data: 左右列表数据的总集合 + * value: 右侧数据列表集合 + * 所以:左侧列表 = data - value + * total-elements: 是两个元素的数组,分别对应左右两侧的总数值 + */ // Data: 用来销毁el-form isFormShow = false // Data: el-form表单验证规则 @@ -90,20 +97,26 @@ export default class GroupEditModal extends Vue { }] } - // 获取user分页页码 - page_offset = 0 - // 每页请求数量 - pageSize = 100 + // 表单数据提交防重复提交标记 + submitLoading = false + // 用于记录穿梭框左右列表总数的 + totalSizes = [0, 0] // 第一个值是左侧的总数,第二个值是右侧的总数 + timer = null // 左侧搜索框输入触发过滤的防抖标记(因为左侧是 ajax 的,需要防抖下) - totalUsersSize = 0 - // 返回的数据总数 - totalSizes = [0, 10] + // 穿梭框左侧相关字段 + page_offset = 0 // 获取 user 分页页码 + pageSize = 100 // 左侧每页显示条数 searchValueLeft = '' + clickLoadMore = false // 左侧加载更多按钮的防重复提交标记 + leftAjaxTotalUsers = [] // 左侧接口回来的数据列表 + leftAjaxTotalSize = 0 + // 穿梭框右侧相关字段 searchValueRight = '' - clickLoadMore = false - submitLoading = false - autoLoadLimit = 100 - timer = null + rightPageOffset = 0 + rightPageSize = 100 + reduceTemp = [] // 临时从右挪向左的数据 + addTemp = [] // 临时从左挪向右的数据 + realSelectedUsers = [] // 实际在右侧的数据(综合各种临时挪动后的结果数据) // Computed: Modal宽度 get modalWidth () { @@ -117,12 +130,48 @@ export default class GroupEditModal extends Vue { return titleMaps[this.editType] } + /** + * 左侧的当前页码 < ceil(接口总数 / size) - 1 显示加载更多按钮 + * TODO 这里的计算可能有问题 + */ get isShowLoadMore () { - return this.page_offset < Math.ceil(this.totalUsersSize / this.pageSize) - 1 + return this.page_offset < Math.ceil(this.leftAjaxTotalSize / this.pageSize) - 1 + } + /** + * 右侧的当前页码 < ceil(右侧列表总数 / size) - 1 显示加载更多按钮 + */ + get isShowRightLoadMore () { + const filterArr = this.searchValueRight ? this.realSelectedUsers.filter(user => user.toLowerCase().indexOf(this.searchValueRight.toString().toLowerCase()) >= 0) : this.realSelectedUsers + const notShowList = filterArr.filter((user) => { + return !this.form.selected_users.includes(user) + }) + return notShowList.length > 0 } + /** + * 穿梭框 data 字段绑定的数据 + * 它的值 = ajax 回来的数据 + 临时从右侧移入的非远程数据 + 右侧实际展现的数据 - 临时加入到右侧,但没显示出来的数据 + */ get totalUserData () { - return this.totalUsers.length ? this.totalUsers : [] + // 当前接口取回来的数据 + const leftRemoteData = this.leftAjaxTotalUsers.map((user) => { + return user.username + }) + // 临时移出的数据 + const filterReduceTempArr = this.searchValueLeft ? this.reduceTemp.filter((user) => { + return user.toLowerCase().indexOf(this.searchValueLeft.toString().toLowerCase()) >= 0 + }) : this.reduceTemp + // 临时加入到用户组,但因为分页,没显示出来的数据,要从总数据中踢掉 + const filterAddTempArr = this.addTemp.filter((user) => { + return !this.form.selected_users.includes(user) + }) + const arrTemp = [...new Set([...leftRemoteData, ...filterReduceTempArr, ...this.form.selected_users])] + const arr = arrTemp.filter((user) => { + return !filterAddTempArr.includes(user) + }) + return arr.map((user) => { + return { key: user, label: user } + }) } // Computed Method: 计算每个Form的field是否显示 @@ -130,14 +179,34 @@ export default class GroupEditModal extends Vue { return fieldVisiableMaps[this.editType].includes(fieldName) } + resetDialogInfo () { + this.timer = null + // 左侧相关字段重置 + this.page_offset = 0 + this.searchValueLeft = '' + this.clickLoadMore = false + this.leftAjaxTotalUsers = [] + this.leftAjaxTotalSize = 0 + // 右侧字段重置 + this.searchValueRight = '' + this.rightPageOffset = 0 + this.reduceTemp = [] + this.addTemp = [] + // 初始等于接口返回的列表数据 + this.realSelectedUsers = this.form.origin_selected_users + } + // Watcher: 监视销毁上一次elForm @Watch('isShow') onModalShow (newVal, oldVal) { if (newVal) { + // 初始变量的重置 this.isFormShow = true - this.page_offset = 0 - this.setModal({totalUsers: []}) - this.editType === 'assign' && this.fetchUsers('') + this.resetDialogInfo() + if (this.editType === 'assign') { + this.$set(this.totalSizes, 1, this.form.origin_selected_users.length) + this.fetchUsers('') + } } else { setTimeout(() => { this.isFormShow = false @@ -155,21 +224,31 @@ export default class GroupEditModal extends Vue { }, 200) } + // 穿梭框两侧的搜索框回调 queryHandler (title, query) { const that = this return new Promise(async (resolve, reject) => { - if (title === that.$t('willCheckGroup')) { - this.page_offset = 0 - clearTimeout(this.timer) - this.timer = setTimeout(async function () { - await that.setModal({totalUsers: []}) + if (title === that.$t('willCheckGroup')) { // 左侧的搜索,走 ajax 搜索 + that.page_offset = 0 + clearTimeout(that.timer) + that.timer = setTimeout(async function () { + // 先置空接口总数据 + that.leftAjaxTotalUsers = [] + // 然后获取接口数据 await that.fetchUsers(query) resolve() }, 500) - } else if (title === that.$t('checkedGroup')) { + } else if (title === that.$t('checkedGroup')) { // 右侧的搜索,走前端已有数据的搜索 try { that.searchValueRight = query - that.$set(that.totalSizes, 1, that.searchResults(query).length) + // 进行前端数据的搜索 + const result = that.rightSearchResults(query) + // 重置穿梭框右侧总数 + that.$set(that.totalSizes, 1, result.length) + // 针对搜索结果处理分页 + const arr = result.length > that.rightPageSize ? result.slice(0, that.rightPageSize) : result + // 重置右侧显示数据 + that.setModalForm({selected_users: arr}) resolve() } catch (e) { console.error(e) @@ -179,9 +258,16 @@ export default class GroupEditModal extends Vue { }) } - // 匹配搜索结果的用户 - searchResults (content) { - return this.form.selected_users.filter(user => user.toLowerCase().indexOf(content.toString().toLowerCase()) >= 0) + // 右侧前端搜索 + rightSearchResults (content) { + // 触发搜索时,页码置回 0 + this.rightPageOffset = 0 + // 因为前端分页了,这里的搜索要基于原始所有选中项进行搜索 + if (content) { + return this.realSelectedUsers.filter(user => user.toLowerCase().indexOf(content.toString().toLowerCase()) >= 0) + } else { + return this.realSelectedUsers + } } // Action: 修改Form函数 @@ -189,11 +275,67 @@ export default class GroupEditModal extends Vue { this.setModalForm({[key]: value}) } - transferInputHandler (key, value) { - this.setModalForm({[key]: value}) - this.totalSizes[0] = this.totalUsersSize - (!this.searchValueLeft.length ? value.length : this.searchResults(this.searchValueLeft).length) - const surplusUsers = this.totalUsers.filter(user => !value.includes(user.key)) - surplusUsers.length < this.autoLoadLimit && (!this.searchValueLeft.length ? this.loadMoreUsers() : this.loadMoreUsers(this.searchValueLeft)) + /** + * 穿梭框左右数据变化时的回调 + * 左右移动时,临时存放数据的字段,要进行处理 + * 同时要修正实际右侧数据的字段 + */ + transferInputHandler (key, value, dir, arr) { + const leftOld = this.totalSizes[0] + const rightOld = this.totalSizes[1] + // arr 是被移动的用户数组,格式为用户名字符串数组 + // 向左移动,临时减用户,向右移动,临时加用户 + if (dir === 'left') { + this.reduceTemp = [...new Set(this.reduceTemp.concat(arr))] + // 同时要把临时加用户的数组中,对应的这些用户去掉 + this.addTemp = this.addTemp.filter((auser) => { + return !arr.includes(auser) + }) + // 右侧的实际选中数据,要将这几个数据减掉 + this.realSelectedUsers = this.realSelectedUsers.filter((auser) => { + return !arr.includes(auser) + }) + const filterTemp = this.searchValueLeft ? arr.filter((user) => { + return user.toLowerCase().indexOf(this.searchValueLeft.toString().toLowerCase()) >= 0 + }) : arr + // 修改两边右上角的总数 + this.$set(this.totalSizes, 0, leftOld + filterTemp.length) + this.$set(this.totalSizes, 1, rightOld - arr.length) + // 如果移动后数据为空了,而下一页数据还有,那自动取下一页数据 + if (value.length === 0 && this.isShowRightLoadMore) { + const filterArr = this.searchValueRight ? this.realSelectedUsers.filter(user => user.toLowerCase().indexOf(this.searchValueRight.toString().toLowerCase()) >= 0) : this.realSelectedUsers + const notShowList = filterArr.filter((user) => { + return !this.form.selected_users.includes(user) + }) + const temp = notShowList.slice(0, this.rightPageSize) + this.setModalForm({[key]: temp}) + } else { + this.setModalForm({[key]: value}) + } + } else { + this.addTemp = [...new Set(this.addTemp.concat(arr))] + // 同时要把临时减用户的数组中,对应的这些用户去掉 + this.reduceTemp = this.reduceTemp.filter((auser) => { + return !arr.includes(auser) + }) + // 右侧的实际选中数据,要加上这几个数据 + this.realSelectedUsers = [...new Set(this.realSelectedUsers.concat(arr))] + const filterTemp = this.searchValueRight ? arr.filter((user) => { + return user.toLowerCase().indexOf(this.searchValueRight.toString().toLowerCase()) >= 0 + }) : arr + // 修改两边右上角的总数 + this.$set(this.totalSizes, 0, leftOld - arr.length) + this.$set(this.totalSizes, 1, rightOld + filterTemp.length) + this.setModalForm({[key]: value}) + /** + * 如果移动的数据超过 100 条,就自动再取 100 条; + * 但有概率自动取的 100 条,已经在之前操作中,临时挪入右侧了; + * 这时候触发 fetchUsers 中的自动获取的逻辑 + * */ + if (arr.length >= this.pageSize && this.isShowLoadMore) { + this.loadMoreUsers(this.searchValueLeft) + } + } } // Action: Form递交函数 @@ -201,7 +343,14 @@ export default class GroupEditModal extends Vue { try { this.submitLoading = true // 获取Form格式化后的递交数据 - const data = getSubmitData(this) + // 传给后端的数据是背后存的字段,不是直接的 form 下的 selected_users + const data = getSubmitData({ + editType: this.editType, + form: { + group_name: this.form.group_name, + selected_users: this.realSelectedUsers + } + }) // 验证表单 await this.$refs['form'].validate() // 针对不同的模式,发送不同的请求 @@ -228,54 +377,74 @@ export default class GroupEditModal extends Vue { return validate[type].bind(this) } - // Helper: 从后台获取用户组 + // Helper: 左侧列表从后台获取用户列表 async fetchUsers (value) { this.searchValueLeft = typeof value === 'undefined' ? '' : value - - const { data: { data } } = await this.loadUsersList({ - page_size: this.pageSize, - page_offset: this.page_offset, - // project: this.currentSelectedProject, // 处理资源组时,发现这个接口不用传 project 参数 - name: value + // 临时移入 + const filterReduceTempArr = this.searchValueLeft ? this.reduceTemp.filter((user) => { + return user.toLowerCase().indexOf(this.searchValueLeft.toString().toLowerCase()) >= 0 + }) : this.reduceTemp + // 本身在右侧,然后临时从右侧移入左侧的数据 + const realReduceTempArr = filterReduceTempArr.filter((user) => { + return this.form.origin_selected_users.includes(user) }) - - const remoteUsers = data.value - .map(user => ({ key: user.username, label: user.username })) - - // const filterNotSelected = [...this.totalUsers, ...remoteUsers].filter(item => !this.form.selected_users.includes(item.key)) - - const selectedUsersNotInRemote = this.form.selected_users - .map(sItem => ({key: sItem, label: sItem})) - .filter(sItem => ![...(this.page_offset === 0 ? [] : this.totalUsers), ...remoteUsers].some(user => user.key === sItem.key)) - - const searchUserIsSelected = (typeof value !== 'undefined' && value) ? this.form.selected_users.filter(user => user.toLowerCase().indexOf(value.toString().toLowerCase()) >= 0) : [...this.totalUsers, ...remoteUsers].filter(user => this.form.selected_users.includes(user.key)) - - this.totalUsersSize = data.total_size - - typeof value !== 'undefined' && value ? (this.totalSizes = [this.totalUsersSize - searchUserIsSelected.length]) : (this.totalSizes = [data.total_size - this.form.selected_users.length]) - - const users = [...new Set([ ...(this.page_offset === 0 ? [] : this.totalUsers), ...remoteUsers, ...selectedUsersNotInRemote ].map(it => it.key))].map(item => ({key: item, label: item})) - - this.autoLoadMoreData(users, value) - - this.setModal({totalUsers: users}) - } - - // 判断是否自动加载更多的数据 - autoLoadMoreData (users, value) { - this.clickLoadMore = false - const len = users.filter(user => this.form.selected_users.includes(user.key)).length - if (users.length - len < this.autoLoadLimit && this.isShowLoadMore) { - typeof value !== 'undefined' && !value.length ? this.loadMoreUsers() : this.loadMoreUsers(value) - return + // 本身就是左侧的数据,临时加到右侧,还未提交的数据 + const filterAddTempArr = this.searchValueLeft ? this.addTemp.filter((user) => { + return user.toLowerCase().indexOf(this.searchValueLeft.toString().toLowerCase()) >= 0 + }) : this.addTemp + const realAddTempArr = filterAddTempArr.filter((user) => { + return !this.form.origin_selected_users.includes(user) + }) + try { + const { data: { data } } = await this.loadUsersList({ + page_size: this.pageSize, + page_offset: this.page_offset, + // project: this.currentSelectedProject, // 处理资源组时,发现这个接口不用传 project 参数 + name: this.searchValueLeft, + group_name: this.form.group_name + }) + this.leftAjaxTotalSize = data.total_size + this.leftAjaxTotalUsers = this.page_offset === 0 ? data.value : this.leftAjaxTotalUsers.concat(data.value) + // 左侧的总数 = 接口的 total_size + 本身在右侧,然后临时从右侧移入左侧的数据的个数 + const leftTotalCount = data.total_size + realReduceTempArr.length - realAddTempArr.length + this.$set(this.totalSizes, 0, leftTotalCount) + // 实际会显示在左侧的列表个数 + const leftListLen = this.totalUserData.length - this.form.selected_users.length + // 左侧总个数 > 0,但实际显示的列表个数为 0 ,这时候需要再次自动获取数据 + if (leftTotalCount > 0 && leftListLen < this.pageSize) { + this.clickLoadMore = false + this.loadMoreUsers(this.searchValueLeft) + } + } catch (e) { + this.leftAjaxTotalSize = 0 + this.leftAjaxTotalUsers = [] + this.$set(this.totalSizes, 0, realReduceTempArr.length) + } finally { + this.clickLoadMore = false } } + // 左侧点击加载更多 loadMoreUsers (value) { if (this.clickLoadMore) return this.clickLoadMore = true this.isShowLoadMore && (this.page_offset += 1, this.fetchUsers(value)) } + + // 右侧是前端分页,加载更多,是基于原始数据进行追加 + loadMoreSelectedUsers (value) { + const size = this.rightPageSize + this.rightPageOffset += 1 + // 先过滤 + const filterArr = value ? this.realSelectedUsers.filter(user => user.toLowerCase().indexOf(value.toString().toLowerCase()) >= 0) : this.realSelectedUsers + const notShowList = filterArr.filter((user) => { + return !this.form.selected_users.includes(user) + }) + // 从未展现列表里,再取两个出来 + const arr = [...new Set(this.form.selected_users.concat(notShowList.slice(0, size)))] + // 右侧实际展现数据变了后,会自动变化总数据 + this.setModalForm({selected_users: arr}) + } } </script> @@ -312,5 +481,8 @@ export default class GroupEditModal extends Vue { .option-items { white-space: pre; } + .el-transfer-panel__checkbox:hover .el-transfer-panel__item-hover{ + display: none!important; + } } </style> diff --git a/kystudio/src/components/common/GroupEditModal/store.js b/kystudio/src/components/common/GroupEditModal/store.js index 64dbca1be7..913af556f4 100644 --- a/kystudio/src/components/common/GroupEditModal/store.js +++ b/kystudio/src/components/common/GroupEditModal/store.js @@ -11,10 +11,10 @@ const initialState = JSON.stringify({ isShow: false, editType: 'new', callback: null, - totalUsers: [], form: { group_name: '', - selected_users: [] + selected_users: [], + origin_selected_users: [] } }) @@ -45,10 +45,15 @@ export default { [types.SET_MODAL]: (state, payload) => { for (const key of Object.keys(state)) { switch (key) { - case 'form': - payload.group && (state.form.group_name = payload.group.group_name) - payload.group && (state.form.selected_users = payload.group.users) + case 'form': { + if (payload.group) { + state.form.group_name = payload.group.group_name + state.form.origin_selected_users = payload.group.users + const size = 100 + state.form.selected_users = payload.group.users.length <= size ? payload.group.users : payload.group.users.slice(0, size) + } break + } default: { payload[key] && (state[key] = payload[key]) break diff --git a/kystudio/src/service/user.js b/kystudio/src/service/user.js index 8d8cd15461..4dcd603114 100644 --- a/kystudio/src/service/user.js +++ b/kystudio/src/service/user.js @@ -66,5 +66,8 @@ export default { }, updataUserDataPermission: (para) => { return Vue.resource(apiUrl + 'access/global/permission/data_query').update(para) + }, + getUnassignedUsers: (para) => { + return Vue.resource(apiUrl + 'user/unassigned_users').get(para) } } diff --git a/kystudio/src/store/types.js b/kystudio/src/store/types.js index a0e53351bd..cbd7e45136 100644 --- a/kystudio/src/store/types.js +++ b/kystudio/src/store/types.js @@ -369,6 +369,7 @@ export const INIT_SPEC = 'INIT_SPEC' export const COLLECT_MESSAGE_DIRECTIVES = 'COLLECT_MESSAGE_DIRECTIVES' export const GET_CURRENT_USER_DATA_PERMISSION = 'GET_CURRENT_USER_DATA_PERMISSION' export const UPDATE_USER_DATA_PERMISSION = 'UPDATE_USER_DATA_PERMISSION' +export const GET_UNASSIGNED_USERS = 'GET_UNASSIGNED_USERS' // monitor actions mutations export const SUGGEST_MODEL = 'SUGGEST_MODEL' export const SAVE_SUGGEST_MODELS = 'SAVE_SUGGEST_MODELS' diff --git a/kystudio/src/store/user.js b/kystudio/src/store/user.js index 45993af7b6..a2fcc2b534 100644 --- a/kystudio/src/store/user.js +++ b/kystudio/src/store/user.js @@ -128,6 +128,9 @@ export default { }, [types.UPDATE_USER_DATA_PERMISSION]: function ({ commit }, para) { return api.user.updataUserDataPermission(para) + }, + [types.GET_UNASSIGNED_USERS]: function ({ commit }, para) { + return api.user.getUnassignedUsers(para) } }, getters: {