Cpte-Vue3/src/views/base/item/components/ItemSelect.vue

417 lines
13 KiB
Vue
Raw Normal View History

2025-12-19 18:06:39 +08:00
<!-- 物料选择 -->
<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>
<a-spin v-if="loading" size="small" />
<span v-else></span>
</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),
},
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 : null;
});
/**
* 获取选项显示文本 - 始终显示完整格式
*/
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,
},
});
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>