|
|
@@ -0,0 +1,414 @@
|
|
|
+<template>
|
|
|
+ <div class="org-user-list" v-loading="isLoading">
|
|
|
+ <bs-empty v-if="searchData.length === 0" />
|
|
|
+ <div v-else class="org-user-content">
|
|
|
+ <div class="top">
|
|
|
+ <div class="top-left">
|
|
|
+ <a-space>
|
|
|
+ <a-checkbox
|
|
|
+ @change="handleSelectAllChange"
|
|
|
+ :indeterminate="isIndeterminate"
|
|
|
+ v-model:checked="isSelectAll"
|
|
|
+ v-if="multiple === SELECT_MULTIPLE.MORE"
|
|
|
+ :disabled="disabled"
|
|
|
+ >
|
|
|
+ 全选
|
|
|
+ </a-checkbox>
|
|
|
+ <a-checkbox v-model:checked="isInclude" :disabled="disabled"> 包含下级 </a-checkbox>
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+ <div class="top-right">
|
|
|
+ <a-space>
|
|
|
+ <span style="font-size: 12px; color: #1677ff">
|
|
|
+ <span>全部</span>
|
|
|
+ <span>({{ searchData.length }})</span>
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <span style="font-size: 12px" v-if="sceneType === SCENE_TYPE.USER">
|
|
|
+ <span>人员</span>
|
|
|
+ <span>({{ searchData.length }})</span>
|
|
|
+ </span>
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="bottom">
|
|
|
+ <div class="bot-i-item" v-for="(item, index) in searchData" :key="item[idKey]" @click="selectNode(item)">
|
|
|
+ <a-checkbox
|
|
|
+ v-if="multiple === SELECT_MULTIPLE.MORE"
|
|
|
+ :checked="allSelectedKeys.findIndex((v) => v === item[idKey]) > -1"
|
|
|
+ :disabled="disabled"
|
|
|
+ ></a-checkbox>
|
|
|
+ <a-radio
|
|
|
+ v-if="multiple === SELECT_MULTIPLE.ONE"
|
|
|
+ :checked="allSelectedKeys.findIndex((v) => v === item[idKey]) > -1"
|
|
|
+ :disabled="disabled"
|
|
|
+ ></a-radio>
|
|
|
+ <a-avatar style="color: #fff; background-color: #1677ff">{{ item[labelKey] && item[labelKey][0] }}</a-avatar>
|
|
|
+ <div class="bot-item">
|
|
|
+ <span class="bot-item-top">{{ item[labelKey] }}</span>
|
|
|
+ <span class="bot-item-bottom">{{ item.parentName }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="pager-config" v-if="false">
|
|
|
+ <a-pagination
|
|
|
+ size="small"
|
|
|
+ v-model:current="pageInfo.pageNum"
|
|
|
+ :total="pageInfo.total"
|
|
|
+ v-model:pageSize="pageInfo.pageSize"
|
|
|
+ @change="handlePagerChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+ import { cloneDeep, isEmpty } from 'lodash';
|
|
|
+ import { queryListByParams } from '/@/api/system/table-api.js';
|
|
|
+ import BsEmpty from '/@/components/BsUi/Empty/index.vue';
|
|
|
+ import { computed, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
|
|
+ import { SCENE_TYPE, SELECT_MULTIPLE } from '/@/components/BsUi/constant.js';
|
|
|
+
|
|
|
+ const emit = defineEmits(['change']);
|
|
|
+
|
|
|
+ const isLoaded = ref(false);
|
|
|
+
|
|
|
+ const props = defineProps({
|
|
|
+ multiple: {
|
|
|
+ required: false,
|
|
|
+ // default: SELECT_MULTIPLE.ONE,
|
|
|
+ default: SELECT_MULTIPLE.MORE,
|
|
|
+ },
|
|
|
+ disabled: {
|
|
|
+ required: false,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ idKey: {
|
|
|
+ required: false,
|
|
|
+ default: 'id',
|
|
|
+ },
|
|
|
+ labelKey: {
|
|
|
+ required: false,
|
|
|
+ default: 'name',
|
|
|
+ },
|
|
|
+ keyWord: {
|
|
|
+ required: false,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ sceneType: {
|
|
|
+ required: false,
|
|
|
+ default: SCENE_TYPE.USER,
|
|
|
+ },
|
|
|
+ oldSelectData: {
|
|
|
+ required: false,
|
|
|
+ default: [],
|
|
|
+ },
|
|
|
+ selectedTreeId: {
|
|
|
+ required: false,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const isLoading = ref(false);
|
|
|
+ const isSelectAll = ref(false);
|
|
|
+ const isInclude = ref(false);
|
|
|
+ const isIndeterminate = ref(false);
|
|
|
+
|
|
|
+ const pageInfo = reactive({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ total: 100,
|
|
|
+ });
|
|
|
+
|
|
|
+ const searchData = ref([]);
|
|
|
+ const currentSelectedKeys = ref([]);
|
|
|
+ const currentSelectedData = ref([]);
|
|
|
+
|
|
|
+ const uniqueByIdKey = (arr) => {
|
|
|
+ return [...new Map(arr.map((item) => [item[props.idKey], item])).values()];
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleRemove = (node) => {
|
|
|
+ searchData.value.forEach((v) => {
|
|
|
+ if (node[props.idKey] === v[props.idKey]) {
|
|
|
+ v.isSelected = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ props.oldSelectData.forEach((v) => {
|
|
|
+ if (node[props.idKey] === v[props.idKey]) {
|
|
|
+ v.isSelected = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleReset = () => {
|
|
|
+ searchData.value.forEach((v) => {
|
|
|
+ v.isSelected = false;
|
|
|
+ });
|
|
|
+
|
|
|
+ props.oldSelectData.forEach((v) => {
|
|
|
+ v.isSelected = false;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handlePagerChange = (pageNum, pageSize) => {
|
|
|
+ pageInfo.pageNum = pageNum;
|
|
|
+ pageInfo.pageSize = pageSize;
|
|
|
+ init(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ const selectNode = (node) => {
|
|
|
+ if (props.disabled) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (props.multiple === SELECT_MULTIPLE.ONE) {
|
|
|
+ handleReset();
|
|
|
+ node.isSelected = true;
|
|
|
+ } else {
|
|
|
+ props.oldSelectData.forEach((v) => {
|
|
|
+ if (v[props.idKey] === node[props.idKey]) {
|
|
|
+ v.isSelected = !v.isSelected;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ node.isSelected = !node.isSelected;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSelectAllChange = (event) => {
|
|
|
+ const checked = event.target.checked;
|
|
|
+ searchData.value.forEach((v) => {
|
|
|
+ v.isSelected = checked;
|
|
|
+ });
|
|
|
+
|
|
|
+ currentSelectedData.value.forEach((v) => {
|
|
|
+ v.isSelected = checked;
|
|
|
+ });
|
|
|
+
|
|
|
+ props.oldSelectData.forEach((v) => {
|
|
|
+ if (searchData.value.findIndex((vv) => vv[props.idKey] === v[props.idKey]) > -1) {
|
|
|
+ v.isSelected = checked;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const fetchData = (data) => {
|
|
|
+ isLoading.value = true;
|
|
|
+
|
|
|
+ const params = {
|
|
|
+ include: false,
|
|
|
+ nodeType: [props.sceneType],
|
|
|
+ ...data,
|
|
|
+ };
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ queryListByParams(params).then((res) => {
|
|
|
+ isLoading.value = false;
|
|
|
+ if (res?.code === 0) {
|
|
|
+ searchData.value = res?.data;
|
|
|
+ pageInfo.total = searchData.value.length;
|
|
|
+ resolve();
|
|
|
+ } else {
|
|
|
+ reject("数据获取失败")
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(
|
|
|
+ [currentSelectedData],
|
|
|
+ ([cur]) => {
|
|
|
+ currentSelectedKeys.value = cur.map((v) => v[props.idKey]);
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+ );
|
|
|
+
|
|
|
+ watch(
|
|
|
+ [searchData],
|
|
|
+ ([value]) => {
|
|
|
+ const cloneDeepCurSelData = cloneDeep(currentSelectedData.value);
|
|
|
+ const currentSelData = cloneDeep(value.filter((v) => v.isSelected));
|
|
|
+ let newArr = [];
|
|
|
+ cloneDeepCurSelData.forEach((v) => {
|
|
|
+ const it = value.find((vv) => vv[props.idKey] === v[props.idKey]);
|
|
|
+ if (isEmpty(it)) {
|
|
|
+ newArr.push(v);
|
|
|
+ } else if (it?.isSelected) {
|
|
|
+ newArr.push(it);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (props.multiple === SELECT_MULTIPLE.MORE) {
|
|
|
+ currentSelectedData.value = uniqueByIdKey([...currentSelData, ...newArr]);
|
|
|
+ } else {
|
|
|
+ currentSelectedData.value = currentSelData;
|
|
|
+ }
|
|
|
+ // 全选,反选逻辑
|
|
|
+ if (value.every((v) => v?.isSelected)) {
|
|
|
+ isSelectAll.value = true;
|
|
|
+ isIndeterminate.value = false;
|
|
|
+ } else if (value.some((v) => v?.isSelected)) {
|
|
|
+ isSelectAll.value = false;
|
|
|
+ isIndeterminate.value = true;
|
|
|
+ } else {
|
|
|
+ isSelectAll.value = false;
|
|
|
+ isIndeterminate.value = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+ );
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.keyWord,
|
|
|
+ (value) => {
|
|
|
+ // 监听关键字变化
|
|
|
+ if (!isEmpty(value)) {
|
|
|
+ init(false, {
|
|
|
+ keyword: value,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ watch(
|
|
|
+ () => props.selectedTreeId,
|
|
|
+ (val) => {
|
|
|
+ init(false, {
|
|
|
+ parentId: val
|
|
|
+ });
|
|
|
+ },
|
|
|
+ {
|
|
|
+ immediate: false,
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ const init = (isInit = true, params = {}) => {
|
|
|
+ isLoaded.value = false;
|
|
|
+ searchData.value = [];
|
|
|
+ const curSelData = cloneDeep(currentSelectedData.value);
|
|
|
+ const oldSelectData = cloneDeep(props.oldSelectData);
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ fetchData(params).then((res) => {
|
|
|
+ currentSelectedKeys.value = Array.from(new Set([...curSelData.map((v) => v[props.idKey]), ...oldSelectData.map((v) => v[props.idKey])]));
|
|
|
+ // 数据的回显
|
|
|
+ searchData.value.forEach((a) => {
|
|
|
+ if (currentSelectedKeys.value.findIndex((v) => v === a[props.idKey]) > -1) {
|
|
|
+ a.isSelected = true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ isLoaded.value = true;
|
|
|
+ resolve();
|
|
|
+ });
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const getOldSelectData = () => {
|
|
|
+ if (props.oldSelectData.length === 0) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return cloneDeep(props.oldSelectData).filter((v) => v.isSelected);
|
|
|
+ };
|
|
|
+
|
|
|
+ const allSelectedData = computed(() => {
|
|
|
+ return uniqueByIdKey([...currentSelectedData.value, ...getOldSelectData()]);
|
|
|
+ });
|
|
|
+
|
|
|
+ const allSelectedKeys = computed(() => {
|
|
|
+ return allSelectedData.value.map((v) => v[props.idKey]);
|
|
|
+ });
|
|
|
+
|
|
|
+ watch(allSelectedKeys, (val) => {
|
|
|
+ isLoaded.value &&
|
|
|
+ emit('change', {
|
|
|
+ selectedKeys: val,
|
|
|
+ selectedData: allSelectedData.value,
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 是否开启下一级,监听
|
|
|
+ watch(isInclude, (val) => {
|
|
|
+ init(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ defineExpose({
|
|
|
+ fetchData,
|
|
|
+ init,
|
|
|
+ allSelectedData,
|
|
|
+ allSelectedKeys,
|
|
|
+ handleReset,
|
|
|
+ handleRemove,
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+ .org-user-list {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ .org-user-content {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ .top {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pager-config {
|
|
|
+ width: 100%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bottom {
|
|
|
+ margin-top: 10px;
|
|
|
+ display: grid;
|
|
|
+ gap: 10px;
|
|
|
+ flex: 1;
|
|
|
+ overflow: scroll;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); /* 自动填充列 */
|
|
|
+ grid-template-rows: repeat(auto-fill, minmax(50px, 1fr)); /* 自动填充列 */
|
|
|
+ .bot-i-item {
|
|
|
+ width: 270px;
|
|
|
+ height: 50px;
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ align-items: center;
|
|
|
+ cursor: pointer;
|
|
|
+ .bot-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+
|
|
|
+ .bot-item-top {
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bot-item-bottom {
|
|
|
+ color: rgba(#000, 0.6);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .bb-bottom {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ .select-num {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 5px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|