362 lines
10 KiB
Vue
362 lines
10 KiB
Vue
|
|
<!-- 库区下拉选择-->
|
|||
|
|
<template>
|
|||
|
|
<a-select
|
|||
|
|
v-model:value="selectedValue"
|
|||
|
|
showSearch
|
|||
|
|
:placeholder="placeholder"
|
|||
|
|
:loading="loading"
|
|||
|
|
:allowClear="true"
|
|||
|
|
:filterOption="filterOption"
|
|||
|
|
:notFoundContent="notFoundContent"
|
|||
|
|
:mode="multiple ? 'multiple' : 'default'"
|
|||
|
|
@change="handleChange"
|
|||
|
|
@search="handleSearch"
|
|||
|
|
@focus="handleFocus"
|
|||
|
|
@popupScroll="handlePopupScroll"
|
|||
|
|
:getPopupContainer="getParentContainer"
|
|||
|
|
v-bind="attrs"
|
|||
|
|
>
|
|||
|
|
<template #notFoundContent>
|
|||
|
|
<a-spin v-if="loading" size="small" />
|
|||
|
|
<span v-else>暂无库区数据</span>
|
|||
|
|
</template>
|
|||
|
|
<a-select-option v-for="option in areaOptions" :key="option.id" :value="getOptionValue(option)">
|
|||
|
|
{{ getOptionLabel(option) }}
|
|||
|
|
</a-select-option>
|
|||
|
|
</a-select>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script lang="ts">
|
|||
|
|
import { defineComponent, ref, watch, computed, onMounted } from 'vue';
|
|||
|
|
import { useAttrs } from '/@/hooks/core/useAttrs';
|
|||
|
|
import { propTypes } from '/@/utils/propTypes';
|
|||
|
|
import { defHttp } from '/@/utils/http/axios';
|
|||
|
|
import { setPopContainer } from '/@/utils';
|
|||
|
|
import { debounce } from 'lodash-es';
|
|||
|
|
|
|||
|
|
// 库区数据接口
|
|||
|
|
interface Area {
|
|||
|
|
id: string;
|
|||
|
|
areaCode: string;
|
|||
|
|
areaName: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
//响应数据接口
|
|||
|
|
interface ResponseData {
|
|||
|
|
records: Area[];
|
|||
|
|
total: number;
|
|||
|
|
size: number;
|
|||
|
|
current: number;
|
|||
|
|
page: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default defineComponent({
|
|||
|
|
name: 'AreaSelect',
|
|||
|
|
inheritAttrs: false,
|
|||
|
|
props: {
|
|||
|
|
// 选中值,支持v-model
|
|||
|
|
value: propTypes.oneOfType([propTypes.string, propTypes.array, propTypes.object]),
|
|||
|
|
// 占位符
|
|||
|
|
placeholder: propTypes.string.def('请选择库区'),
|
|||
|
|
// 是否多选
|
|||
|
|
multiple: propTypes.bool.def(false),
|
|||
|
|
// 是否异步加载数据
|
|||
|
|
async: propTypes.bool.def(true),
|
|||
|
|
// 分页大小
|
|||
|
|
pageSize: propTypes.number.def(20),
|
|||
|
|
// 弹出层容器
|
|||
|
|
popContainer: propTypes.string,
|
|||
|
|
// 自定义弹出层容器函数
|
|||
|
|
getPopupContainer: {
|
|||
|
|
type: Function,
|
|||
|
|
default: (node: HTMLElement) => node?.parentNode,
|
|||
|
|
},
|
|||
|
|
// 是否立即触发change事件
|
|||
|
|
immediateChange: propTypes.bool.def(false),
|
|||
|
|
// 返回值类型: 'id'(默认) | 'object' | 其他字段名
|
|||
|
|
returnValue: propTypes.string.def('id'),
|
|||
|
|
//默认启用
|
|||
|
|
izActive: propTypes.number.def(1),
|
|||
|
|
},
|
|||
|
|
emits: ['change', 'update:value', 'optionsLoaded'],
|
|||
|
|
setup(props, { emit }) {
|
|||
|
|
const areaOptions = ref<Area[]>([]);
|
|||
|
|
const loading = ref<boolean>(false);
|
|||
|
|
const allAreas = ref<Area[]>([]);
|
|||
|
|
const attrs = useAttrs({ excludeDefaultKeys: false });
|
|||
|
|
|
|||
|
|
// 分页相关
|
|||
|
|
const pageNo = ref(1);
|
|||
|
|
const isHasData = ref(true);
|
|||
|
|
const scrollLoading = ref(false);
|
|||
|
|
const searchKeyword = ref('');
|
|||
|
|
|
|||
|
|
// 选中值
|
|||
|
|
const selectedValue = ref<string | string[] | undefined>(undefined);
|
|||
|
|
|
|||
|
|
// 未找到内容
|
|||
|
|
const notFoundContent = computed(() => {
|
|||
|
|
return loading.value ? undefined : null;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取选项显示文本 - 始终显示完整格式
|
|||
|
|
*/
|
|||
|
|
function getOptionLabel(option: Area) {
|
|||
|
|
return `${option.areaCode} - ${option.areaName}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取选项值 - 根据returnValue确定实际存储的值
|
|||
|
|
*/
|
|||
|
|
function getOptionValue(option: Area) {
|
|||
|
|
if (props.returnValue === 'object') {
|
|||
|
|
return option.id; // 对于object类型,仍然使用id作为选项值,但在change事件中返回完整对象
|
|||
|
|
} else if (props.returnValue === 'id') {
|
|||
|
|
return option.id;
|
|||
|
|
} else {
|
|||
|
|
return option[props.returnValue as keyof Area] as string;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取弹出层容器
|
|||
|
|
*/
|
|||
|
|
function getParentContainer(node: HTMLElement) {
|
|||
|
|
if (props.popContainer) {
|
|||
|
|
return setPopContainer(node, props.popContainer);
|
|||
|
|
} else {
|
|||
|
|
if (typeof props.getPopupContainer === 'function') {
|
|||
|
|
return props.getPopupContainer(node);
|
|||
|
|
} else {
|
|||
|
|
return node?.parentNode;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 过滤选项 - 禁用前端过滤,使用后端搜索
|
|||
|
|
*/
|
|||
|
|
function filterOption(_input: string, _option: any) {
|
|||
|
|
return true; // 禁用前端过滤,完全依赖后端搜索
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取库区数据
|
|||
|
|
*/
|
|||
|
|
const fetchAreas = async (page = 1, keyword = '', isSearch = false) => {
|
|||
|
|
try {
|
|||
|
|
loading.value = true;
|
|||
|
|
|
|||
|
|
const res = await defHttp.get<ResponseData>({
|
|||
|
|
url: '/base/area/list',
|
|||
|
|
params: {
|
|||
|
|
pageSize: props.pageSize,
|
|||
|
|
pageNo: page,
|
|||
|
|
keyword: keyword,
|
|||
|
|
izActive: props.izActive,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('获取库区数据成功:', res);
|
|||
|
|
const records = res.records || [];
|
|||
|
|
|
|||
|
|
if (page === 1 || isSearch) {
|
|||
|
|
// 第一页或搜索时,重置数据
|
|||
|
|
allAreas.value = records;
|
|||
|
|
areaOptions.value = records;
|
|||
|
|
} else {
|
|||
|
|
// 滚动加载时,追加数据
|
|||
|
|
allAreas.value = [...allAreas.value, ...records];
|
|||
|
|
areaOptions.value = [...areaOptions.value, ...records];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 修正分页判断逻辑
|
|||
|
|
isHasData.value = records.length >= props.pageSize;
|
|||
|
|
console.log('是否还有更多数据:', records.length);
|
|||
|
|
|
|||
|
|
emit('optionsLoaded', allAreas.value);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取库区数据失败:', error);
|
|||
|
|
if (page === 1) {
|
|||
|
|
allAreas.value = [];
|
|||
|
|
areaOptions.value = [];
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false;
|
|||
|
|
scrollLoading.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 根据选项值找到对应的选项对象
|
|||
|
|
*/
|
|||
|
|
function findOptionByValue(value: string): Area | undefined {
|
|||
|
|
if (props.returnValue === 'object' || props.returnValue === 'id') {
|
|||
|
|
return allAreas.value.find((item) => item.id === value);
|
|||
|
|
} else {
|
|||
|
|
return allAreas.value.find((item) => item[props.returnValue as keyof Area] === value);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取需要返回的值
|
|||
|
|
*/
|
|||
|
|
function getReturnValue(value: string | string[]) {
|
|||
|
|
if (!value) {
|
|||
|
|
return props.multiple ? [] : undefined;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果返回整个对象
|
|||
|
|
if (props.returnValue === 'object') {
|
|||
|
|
if (Array.isArray(value)) {
|
|||
|
|
return value.map((v) => findOptionByValue(v)).filter(Boolean);
|
|||
|
|
} else {
|
|||
|
|
return findOptionByValue(value);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 如果返回ID(默认情况)
|
|||
|
|
else if (props.returnValue === 'id') {
|
|||
|
|
return value;
|
|||
|
|
}
|
|||
|
|
// 如果返回对象中的某个字段
|
|||
|
|
else {
|
|||
|
|
if (Array.isArray(value)) {
|
|||
|
|
return value.map((v) => {
|
|||
|
|
const option = findOptionByValue(v);
|
|||
|
|
return option ? option[props.returnValue as keyof Area] : v;
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
const option = findOptionByValue(value);
|
|||
|
|
return option ? option[props.returnValue as keyof Area] : value;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 搜索处理(防抖)
|
|||
|
|
*/
|
|||
|
|
const handleSearch = debounce(function (value: string) {
|
|||
|
|
searchKeyword.value = value;
|
|||
|
|
pageNo.value = 1;
|
|||
|
|
isHasData.value = true;
|
|||
|
|
|
|||
|
|
// 直接调用API进行搜索
|
|||
|
|
fetchAreas(1, value, true);
|
|||
|
|
}, 300);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理焦点事件
|
|||
|
|
*/
|
|||
|
|
function handleFocus() {
|
|||
|
|
// 如果还没有数据,加载数据
|
|||
|
|
if (allAreas.value.length === 0 && props.async) {
|
|||
|
|
pageNo.value = 1;
|
|||
|
|
isHasData.value = true;
|
|||
|
|
fetchAreas(1, '');
|
|||
|
|
}
|
|||
|
|
attrs.onFocus?.();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理值变化
|
|||
|
|
*/
|
|||
|
|
function handleChange(value: string | string[]) {
|
|||
|
|
selectedValue.value = value;
|
|||
|
|
|
|||
|
|
// 根据配置返回相应的值
|
|||
|
|
const returnValue = getReturnValue(value);
|
|||
|
|
console.log('值变化:', returnValue);
|
|||
|
|
emit('update:value', returnValue);
|
|||
|
|
emit('change', returnValue);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 滚动加载处理
|
|||
|
|
*/
|
|||
|
|
function handlePopupScroll(e: Event) {
|
|||
|
|
const target = e.target as HTMLElement;
|
|||
|
|
const { scrollTop, scrollHeight, clientHeight } = target;
|
|||
|
|
|
|||
|
|
if (!scrollLoading.value && isHasData.value && scrollTop + clientHeight >= scrollHeight - 10) {
|
|||
|
|
console.log('滚动加载更多');
|
|||
|
|
scrollLoading.value = true;
|
|||
|
|
pageNo.value++;
|
|||
|
|
|
|||
|
|
fetchAreas(pageNo.value, searchKeyword.value)
|
|||
|
|
.finally(() => {
|
|||
|
|
scrollLoading.value = false;
|
|||
|
|
})
|
|||
|
|
.catch(() => {
|
|||
|
|
pageNo.value--;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 根据选中值初始化显示文本
|
|||
|
|
*/
|
|||
|
|
const initSelectValue = async () => {
|
|||
|
|
if (!props.value) {
|
|||
|
|
selectedValue.value = props.multiple ? [] : undefined;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果是异步模式且还没有加载数据,则先加载数据
|
|||
|
|
if (props.async && allAreas.value.length === 0) {
|
|||
|
|
await fetchAreas();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据不同的returnValue设置选中的值
|
|||
|
|
if (props.returnValue === 'object') {
|
|||
|
|
// 如果返回的是对象,value可能是对象或对象数组
|
|||
|
|
if (Array.isArray(props.value)) {
|
|||
|
|
selectedValue.value = props.value.map((item: any) => item.id);
|
|||
|
|
} else {
|
|||
|
|
selectedValue.value = (props.value as any).id;
|
|||
|
|
}
|
|||
|
|
} else if (props.returnValue === 'id') {
|
|||
|
|
selectedValue.value = props.value as string | string[];
|
|||
|
|
} else {
|
|||
|
|
// 对于其他字段类型,直接使用传入的值
|
|||
|
|
selectedValue.value = props.value as string | string[];
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 监听value变化
|
|||
|
|
watch(
|
|||
|
|
() => props.value,
|
|||
|
|
() => {
|
|||
|
|
initSelectValue();
|
|||
|
|
},
|
|||
|
|
{ immediate: true }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 组件挂载时初始化
|
|||
|
|
onMounted(() => {
|
|||
|
|
if (!props.async) {
|
|||
|
|
fetchAreas();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
attrs,
|
|||
|
|
areaOptions,
|
|||
|
|
loading,
|
|||
|
|
selectedValue,
|
|||
|
|
notFoundContent,
|
|||
|
|
getParentContainer,
|
|||
|
|
filterOption,
|
|||
|
|
handleChange,
|
|||
|
|
handleSearch,
|
|||
|
|
handleFocus,
|
|||
|
|
getOptionLabel,
|
|||
|
|
getOptionValue,
|
|||
|
|
handlePopupScroll,
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="less" scoped></style>
|