<script lang="ts" setup>
|
import type { ScrollActionType } from '@jnpf/ui';
|
|
import { computed, nextTick, reactive, ref, toRefs, unref, watch } from 'vue';
|
|
import { useAttrs, useGlobSetting, useMessage } from '@jnpf/hooks';
|
import { ScrollContainer } from '@jnpf/ui';
|
import { ModalClose, ModalFooter } from '@jnpf/ui/modal';
|
|
import { $t } from '@vben/locales';
|
import { useUserStore } from '@vben/stores';
|
|
import { SearchOutlined } from '@ant-design/icons-vue';
|
import { useDebounceFn } from '@vueuse/core';
|
import { Modal as AModal, Avatar, Checkbox, Form, Radio, Select, Tag } from 'ant-design-vue';
|
import { cloneDeep, pick } from 'lodash-es';
|
|
import { getListBySystem, getShareInfo } from '#/api/onlineDev/visualDev';
|
import { getSystemlist } from '#/api/system/system';
|
|
interface State {
|
finish: boolean;
|
innerValue: any[] | string | undefined;
|
lastList: any[];
|
lastLoading: boolean;
|
loading: boolean;
|
options: any[];
|
query: any;
|
selectedData: any[];
|
selectedIds: any[];
|
treeData: any[];
|
cacheList: any[];
|
visible: boolean;
|
systemItem: any;
|
keywordShare: string;
|
}
|
|
defineOptions({ inheritAttrs: false, name: 'FormSelectModal' });
|
|
const props = defineProps({
|
allowClear: { default: true, type: Boolean },
|
disabled: { default: false, type: Boolean },
|
multiple: { default: false, type: Boolean },
|
placeholder: { default: '请选择', type: String },
|
required: { default: false, type: Boolean },
|
size: String,
|
value: { type: [String, Array] as PropType<string | string[]> },
|
});
|
const emit = defineEmits(['update:value', 'change', 'labelChange']);
|
const attrs: any = useAttrs({ excludeDefaultKeys: false });
|
const globSetting = useGlobSetting();
|
const apiUrl = ref(globSetting.apiURL);
|
const infiniteBody = ref<Nullable<ScrollActionType>>(null);
|
const state = reactive<State>({
|
finish: false,
|
innerValue: '',
|
lastList: [],
|
lastLoading: false,
|
loading: false,
|
options: [],
|
query: {
|
currentPage: 1,
|
hasPage: 0,
|
keyword: '',
|
pageSize: 20,
|
},
|
selectedData: [],
|
selectedIds: [],
|
treeData: [],
|
cacheList: [],
|
visible: false,
|
systemItem: {},
|
keywordShare: '',
|
});
|
const { innerValue, lastList, lastLoading, options, query, selectedData, selectedIds, treeData, visible, keywordShare } = toRefs(state);
|
const formItemContext = Form.useInjectFormItemContext();
|
const { createMessage } = useMessage();
|
const userStore = useUserStore();
|
const debounceHandleSearch = useDebounceFn(handleSearch, 300);
|
const debounceHandleShareSearch = useDebounceFn(handleSearchShare, 300);
|
|
const getSelectBindValue: any = computed(() => ({
|
...pick(props, ['placeholder', 'disabled', 'size', 'allowClear']),
|
class: unref(attrs).class ? `w-full ${unref(attrs).class}` : 'w-full',
|
fieldNames: { label: 'showName', value: 'id' },
|
mode: 'multiple',
|
open: false,
|
showArrow: true,
|
showSearch: false,
|
}));
|
const getUserInfo: any = computed(() => userStore.getUserInfo || {});
|
|
watch(
|
() => unref(props.value),
|
() => {
|
setValue();
|
},
|
{ immediate: true },
|
);
|
watch(
|
() => state.selectedData,
|
(val) => {
|
state.selectedIds = val.map((o) => o.id);
|
},
|
{ deep: true },
|
);
|
|
function setValue() {
|
if (!props.value || props.value.length === 0) return setNullValue();
|
const ids = props.multiple ? (props.value as any[]) : [props.value];
|
if (!ids.length) return setOptions([]);
|
getShareInfo(ids).then((res) => {
|
setOptions(res.data);
|
});
|
}
|
function setOptions(data) {
|
if (!props.value || props.value.length === 0) return setNullValue();
|
const selectedList: any[] = data;
|
const innerIds = selectedList.map((o) => o.id);
|
state.innerValue = props.multiple ? innerIds : innerIds[0];
|
state.options = cloneDeep(selectedList);
|
state.selectedData = cloneDeep(selectedList);
|
const labels = selectedData.value.map((o) => o.showName).join(',');
|
emit('labelChange', labels);
|
}
|
function setNullValue() {
|
state.innerValue = props.multiple ? [] : undefined;
|
state.options = [];
|
state.selectedData = [];
|
}
|
function onChange(_val, option) {
|
state.selectedData = option || [];
|
handleSubmit();
|
}
|
function openSelectModal() {
|
if (props.disabled) return;
|
state.visible = true;
|
initData();
|
setValue();
|
}
|
function handleCancel() {
|
state.visible = false;
|
}
|
function handleSearch(e) {
|
const value = e.target.value;
|
if (state.lastLoading) return;
|
state.lastList = [];
|
state.query.keyword = value || '';
|
state.query.hasPage = state.query.keyword ? 1 : 0;
|
if (state.query.hasPage) {
|
nextTick(() => {
|
bindScroll();
|
});
|
}
|
getLastList();
|
}
|
function handleSearchShare(e) {
|
const value = e.target.value;
|
state.keywordShare = value || '';
|
state.treeData = state.cacheList.filter((o) => o.fullName.includes(state.keywordShare));
|
}
|
function handleListNodeClick(data) {
|
if (state.systemItem.id === data.id) return;
|
state.systemItem = data;
|
state.lastList = [];
|
state.finish = false;
|
state.query.currentPage = 1;
|
getLastList();
|
}
|
function handleNodeClick(data) {
|
const index = state.selectedData.findIndex((o) => o.id === data.id);
|
if (index !== -1) return state.selectedData.splice(index, 1);
|
props.multiple ? state.selectedData.push(data) : (state.selectedData = [data]);
|
}
|
function removeAll() {
|
state.selectedData = [];
|
}
|
function removeData(index: number) {
|
state.selectedData.splice(index, 1);
|
}
|
function handleSubmit() {
|
const ids = state.selectedData.map((o) => o.id);
|
if (props.required && !ids.length) {
|
createMessage.warning('至少选择一个表单');
|
return;
|
}
|
state.options = state.selectedData;
|
state.innerValue = props.multiple ? ids : ids[0];
|
if (props.multiple) {
|
emit('update:value', ids);
|
emit('change', ids, state.options);
|
} else {
|
emit('update:value', ids[0] || '');
|
emit('change', ids[0] || '', state.options[0]);
|
}
|
formItemContext.onFieldChange();
|
handleCancel();
|
}
|
function bindScroll() {
|
const bodyRef = infiniteBody.value;
|
const vBody = bodyRef?.getScrollWrap();
|
vBody?.addEventListener('scroll', () => {
|
if (vBody.scrollTop > 0 && vBody.scrollHeight - vBody.clientHeight - vBody.scrollTop <= 200 && !state.lastLoading && !state.finish) {
|
state.query.currentPage += 1;
|
getLastList();
|
}
|
});
|
}
|
function getLastList() {
|
state.lastLoading = true;
|
const query = {
|
...state.query,
|
systemId: state.systemItem.id,
|
};
|
getListBySystem(query)
|
.then((res) => {
|
state.lastLoading = false;
|
state.finish = res.data.list.length < state.query.pageSize;
|
state.lastList = [...state.lastList, ...res.data.list];
|
})
|
.catch(() => {
|
state.lastLoading = false;
|
});
|
}
|
async function initData() {
|
state.keywordShare = '';
|
state.query.keyword = '';
|
state.query.currentPage = 1;
|
state.finish = false;
|
state.treeData = [];
|
state.lastList = [];
|
getSystemlist().then((res) => {
|
const list = res.data.filter((o) => o.id !== unref(getUserInfo)?.systemId);
|
state.cacheList = list;
|
state.treeData = list;
|
if (state.treeData.length) {
|
state.systemItem = state.treeData[0];
|
getLastList();
|
}
|
});
|
}
|
</script>
|
|
<template>
|
<div @click="openSelectModal" v-if="$slots.action">
|
<slot name="action"></slot>
|
</div>
|
<template v-else>
|
<Select v-bind="getSelectBindValue" v-model:value="innerValue" :options="options" @change="onChange" @click="openSelectModal" />
|
</template>
|
<AModal v-model:open="visible" :keyboard="false" :mask-closable="false" title="选择表单" :width="800" centered class="common-select-modal">
|
<template #closeIcon>
|
<ModalClose :can-fullscreen="false" @cancel="handleCancel" />
|
</template>
|
<template #footer>
|
<ModalFooter @cancel="handleCancel" @ok="handleSubmit">
|
<template #insertFooter v-if="multiple">
|
<div class="float-left">
|
<span class="mr-[10px]">{{ $t('component.jnpf.common.selected') }}({{ selectedData.length }})</span>
|
<span class="remove-all-btn" @click="removeAll">{{ $t('component.jnpf.common.clearAll') }}</span>
|
</div>
|
</template>
|
</ModalFooter>
|
</template>
|
<div class="common-select-modal-main">
|
<div class="selected-pane">
|
<Tag v-for="(item, i) in selectedData" :key="item.id" :closable="true" class="selected-pane-tag" @close="removeData(i)">
|
{{ item.showName }}
|
</Tag>
|
</div>
|
<div class="select-pane">
|
<div class="select-pane-tool">
|
<div class="select-pane-main">
|
<div class="select-pane-list select-pane-item">
|
<a-input class="mb-[10px]" v-model:value="keywordShare" placeholder="搜索应用" allow-clear @change="debounceHandleShareSearch">
|
<template #suffix>
|
<SearchOutlined style="color: rgb(0 0 0 / 45%)" />
|
</template>
|
</a-input>
|
<ScrollContainer>
|
<div
|
v-for="(item, i) in treeData"
|
:key="i"
|
class="select-pane-list__item"
|
:class="{ 'select-pane-list__item-active': state.systemItem.id === item.id }"
|
@click="handleListNodeClick(item)">
|
<div class="select-pane-list__item-title">
|
<i :class="item.icon" v-if="item.icon" class="mr-[6px]"></i>
|
<span :title="item.fullName">{{ item.fullName }}</span>
|
</div>
|
</div>
|
<jnpf-empty v-if="treeData.length === 0" />
|
</ScrollContainer>
|
</div>
|
<div class="select-pane-list select-pane-item">
|
<a-input class="mb-[10px]" v-model:value="query.keyword" placeholder="搜索表单" allow-clear @change="debounceHandleSearch">
|
<template #suffix>
|
<SearchOutlined style="color: rgb(0 0 0 / 45%)" />
|
</template>
|
</a-input>
|
<ScrollContainer ref="infiniteBody" v-loading="lastLoading && query.currentPage === 1">
|
<div v-for="(item, i) in lastList" :key="i" class="select-pane-list__item" @click="handleNodeClick(item)">
|
<div class="select-pane-list__item-title">
|
<Avatar :size="26" :src="apiUrl + item.headIcon" class="select-pane-list__item--headIcon" v-if="item.headIcon" />
|
<i :class="item.icon" class="mr-[6px]" v-if="item.icon && !item.headIcon"></i>
|
<span :title="item.fullName">{{ item.fullName }}</span>
|
</div>
|
<div class="select-pane-list__item-action">
|
<Checkbox :checked="selectedIds.includes(item.id)" v-if="multiple" />
|
<Radio :checked="selectedIds.includes(item.id)" v-else />
|
</div>
|
</div>
|
<jnpf-empty v-if="lastList.length === 0" />
|
</ScrollContainer>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</AModal>
|
</template>
|
<style lang="scss" scoped>
|
.select-pane-item {
|
padding: 10px;
|
}
|
|
.scroll-container {
|
height: calc(100% - 42px);
|
}
|
</style>
|