419 lines
13 KiB
Vue
419 lines
13 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"
|
||
@popup-scroll="handlePopupScroll"
|
||
:getPopupContainer="getParentContainer"
|
||
v-bind="attrs"
|
||
>
|
||
<template #notFoundContent>
|
||
|
||
</template>
|
||
<a-select-option v-for="option in Options" :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 { queryById } from '@/views/base/item/Item.api';
|
||
import { setPopContainer } from '/@/utils';
|
||
import { debounce } from 'lodash-es';
|
||
|
||
// 物料数据接口
|
||
interface Item {
|
||
id: string;
|
||
itemCode: string;
|
||
itemName: string;
|
||
}
|
||
|
||
//响应数据接口
|
||
interface ResponseData {
|
||
records: Item[];
|
||
total: number;
|
||
size: number;
|
||
current: number;
|
||
page: number;
|
||
}
|
||
|
||
export default defineComponent({
|
||
name: 'ItemSelect',
|
||
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(10),
|
||
// 弹出层容器
|
||
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),
|
||
//默认未删除
|
||
delFlag: propTypes.number.def(0),
|
||
},
|
||
emits: ['change', 'update:value', 'optionsLoaded'],
|
||
setup(props, { emit }) {
|
||
const Options = ref<Item[]>([]);
|
||
const loading = ref<boolean>(false);
|
||
const allItems = ref<Item[]>([]);
|
||
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 : '暂无数据';
|
||
});
|
||
|
||
/**
|
||
* 获取选项显示文本 - 始终显示完整格式
|
||
*/
|
||
function getOptionLabel(option: Item) {
|
||
return `${option.itemCode} - ${option.itemName}`;
|
||
}
|
||
|
||
/**
|
||
* 获取选项值 - 根据returnValue确定实际存储的值
|
||
*/
|
||
function getOptionValue(option: Item) {
|
||
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 Item] 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; // 禁用前端过滤,完全依赖后端搜索
|
||
}
|
||
|
||
/**
|
||
* 确保物料唯一性的辅助函数
|
||
*/
|
||
function ensureUnique(items: Item[], newItem: Item): Item[] {
|
||
// 如果已经存在相同id的物料,先移除旧的再添加新的
|
||
const filtered = items.filter(item => item.id !== newItem.id);
|
||
return [newItem, ...filtered];
|
||
}
|
||
|
||
/**
|
||
* 获取物料数据
|
||
*/
|
||
const queryData = async (page = 1, keyword = '', isSearch = false) => {
|
||
try {
|
||
loading.value = true;
|
||
|
||
const res = await defHttp.get<ResponseData>({
|
||
url: '/base/item/list',
|
||
params: {
|
||
pageSize: props.pageSize,
|
||
pageNo: page,
|
||
keyword: keyword,
|
||
izActive: props.izActive,
|
||
delFlag: props.delFlag,
|
||
},
|
||
});
|
||
|
||
const records = res.records || [];
|
||
|
||
if (page === 1 || isSearch) {
|
||
// 第一页或搜索时,重置数据
|
||
allItems.value = records;
|
||
Options.value = records;
|
||
} else {
|
||
// 滚动加载时,追加数据(确保不重复)
|
||
const newRecords = records.filter(record =>
|
||
!allItems.value.some(item => item.id === record.id)
|
||
);
|
||
allItems.value = [...allItems.value, ...newRecords];
|
||
Options.value = [...Options.value, ...newRecords];
|
||
}
|
||
|
||
// 修正分页判断逻辑
|
||
isHasData.value = records.length >= props.pageSize;
|
||
|
||
emit('optionsLoaded', allItems.value);
|
||
} catch (error) {
|
||
if (page === 1) {
|
||
allItems.value = [];
|
||
Options.value = [];
|
||
}
|
||
} finally {
|
||
loading.value = false;
|
||
scrollLoading.value = false;
|
||
}
|
||
};
|
||
|
||
async function queryDataById(value: string) {
|
||
try {
|
||
const res = await queryById({ id: value });
|
||
if (res) {
|
||
// 将查询到的单个物料信息添加到列表中,确保唯一性
|
||
const item = Array.isArray(res) ? res[0] : res;
|
||
if (item) {
|
||
// 使用ensureUnique确保物料不重复
|
||
allItems.value = ensureUnique(allItems.value, item);
|
||
Options.value = ensureUnique(Options.value, item);
|
||
}
|
||
return item;
|
||
}
|
||
} catch (error) {
|
||
console.error('查询物料失败:', error);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 根据选项值找到对应的选项对象
|
||
*/
|
||
function findOptionByValue(value: string): Item | undefined {
|
||
if (props.returnValue === 'object' || props.returnValue === 'id') {
|
||
return allItems.value.find((item) => item.id === value);
|
||
} else {
|
||
return allItems.value.find((item) => item[props.returnValue as keyof Item] === 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 Item] : v;
|
||
});
|
||
} else {
|
||
const option = findOptionByValue(value);
|
||
return option ? option[props.returnValue as keyof Item] : value;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 搜索处理(防抖)
|
||
*/
|
||
const handleSearch = debounce(function (value: string) {
|
||
searchKeyword.value = value;
|
||
pageNo.value = 1;
|
||
isHasData.value = true;
|
||
|
||
// 直接调用API进行搜索
|
||
queryData(1, value, true);
|
||
}, 300);
|
||
|
||
/**
|
||
* 处理焦点事件
|
||
*/
|
||
function handleFocus() {
|
||
// 如果还没有数据,加载数据
|
||
if (allItems.value.length === 0 && props.async) {
|
||
pageNo.value = 1;
|
||
isHasData.value = true;
|
||
queryData(1, '');
|
||
}
|
||
attrs.onFocus?.();
|
||
}
|
||
|
||
/**
|
||
* 处理值变化
|
||
*/
|
||
function handleChange(value: string | string[]) {
|
||
selectedValue.value = value;
|
||
|
||
// 根据配置返回相应的值
|
||
const returnValue = getReturnValue(value);
|
||
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) {
|
||
scrollLoading.value = true;
|
||
pageNo.value++;
|
||
|
||
queryData(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 && allItems.value.length === 0) {
|
||
await queryData();
|
||
}
|
||
|
||
// 根据不同的returnValue设置选中的值
|
||
let valueIds: string[] = [];
|
||
|
||
if (props.returnValue === 'object') {
|
||
// 如果返回的是对象,value可能是对象或对象数组
|
||
if (Array.isArray(props.value)) {
|
||
valueIds = props.value.map((item: any) => item.id);
|
||
selectedValue.value = valueIds;
|
||
} else {
|
||
valueIds = [(props.value as any).id];
|
||
selectedValue.value = valueIds[0];
|
||
}
|
||
} else if (props.returnValue === 'id') {
|
||
if (Array.isArray(props.value)) {
|
||
valueIds = props.value as string[];
|
||
selectedValue.value = valueIds;
|
||
} else {
|
||
valueIds = [props.value as string];
|
||
selectedValue.value = valueIds[0];
|
||
}
|
||
} else {
|
||
// 对于其他字段类型,直接使用传入的值
|
||
selectedValue.value = props.value as string | string[];
|
||
// 这里需要根据字段值查找对应的ID
|
||
if (Array.isArray(props.value)) {
|
||
valueIds = props.value.map((v) => {
|
||
const option = allItems.value.find((item) => item[props.returnValue as keyof Item] === v);
|
||
return option ? option.id : v;
|
||
});
|
||
} else {
|
||
const option = allItems.value.find((item) => item[props.returnValue as keyof Item] === props.value);
|
||
valueIds = option ? [option.id] : [props.value as string];
|
||
}
|
||
}
|
||
|
||
// 检查是否有值不在当前列表中,如果有则查询
|
||
const missingIds = valueIds.filter((id) => !allItems.value.some((item) => item.id === id));
|
||
if (missingIds.length > 0) {
|
||
// 对每个不在列表中的ID进行查询
|
||
const queryPromises = missingIds.map((id) => queryDataById(id));
|
||
await Promise.all(queryPromises);
|
||
}
|
||
};
|
||
|
||
// 监听value变化
|
||
watch(
|
||
() => props.value,
|
||
() => {
|
||
initSelectValue();
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
// 组件挂载时初始化
|
||
onMounted(() => {
|
||
if (!props.async) {
|
||
queryData();
|
||
}
|
||
});
|
||
|
||
return {
|
||
attrs,
|
||
Options,
|
||
loading,
|
||
selectedValue,
|
||
notFoundContent,
|
||
getParentContainer,
|
||
filterOption,
|
||
handleChange,
|
||
handleSearch,
|
||
handleFocus,
|
||
getOptionLabel,
|
||
getOptionValue,
|
||
handlePopupScroll,
|
||
};
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped></style>
|