Files
BMS/bms-front/src/components/CustomerSelect/index.vue
T

230 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-select
v-model="innerValue"
:placeholder="placeholder"
:clearable="clearable"
:filterable="filterable"
:multiple="multiple"
:collapse-tags="multiple"
:collapse-tags-tooltip="multiple"
:disabled="disabled"
:loading="loading"
:size="size"
:remote="remoteSearch"
:remote-method="remoteSearch ? handleRemoteSearch : undefined"
:reserve-keyword="remoteSearch"
style="width: 100%;"
@change="handleChange"
@visible-change="handleVisibleChange"
>
<el-option
v-for="item in displayOptions"
:key="item[realValueKey]"
:label="getLabel(item)"
:value="item[realValueKey]"
>
<span>{{ getLabel(item) }}</span>
<span class="option-sub" style="float: right; color: var(--el-text-color-secondary); font-size: 12px;">
{{ item[realCodeKey] }}
</span>
</el-option>
</el-select>
</template>
<script setup name="CustomerSelect">
import { listCustomer, listCustomerOption } from '@/api/customersuppliermanagement/customerinfo/customer.js'
import { listSupplier } from '@/api/customersuppliermanagement/supplierinfo/supplier.js'
const props = defineProps({
// 业务页面可传入专用选项接口,默认行为保持不变。
fetchOptions: {
type: Function,
default: undefined
},
// 单选时为 String/Number;multiple 时为数组
modelValue: {
type: [String, Number, Array],
default: ''
},
// 多选模式
multiple: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'customer',
validator: v => ['customer', 'supplier'].includes(v)
},
placeholder: {
type: String,
default: '请选择'
},
clearable: {
type: Boolean,
default: true
},
filterable: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
},
size: {
type: String,
default: ''
},
valueKey: {
type: String,
default: ''
},
labelKey: {
type: String,
default: ''
},
codeKey: {
type: String,
default: ''
},
params: {
type: Object,
default: () => ({})
},
pageSize: {
type: Number,
default: 1000
},
// 远程搜索时按客户名称/代码查询,避免本地只加载固定数量选项。
remoteSearch: {
type: Boolean,
default: false
},
// 为 true 时客户下拉走专用选项接口,默认仍走客户列表接口
optionSelect: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const innerValue = ref(props.modelValue)
const options = ref([])
const selectedOptions = ref([])
const loading = ref(false)
const loaded = ref(false)
const remoteTimer = ref(null)
let requestSerial = 0
const isCustomer = computed(() => props.type === 'customer')
const realValueKey = computed(() => props.valueKey || (isCustomer.value ? 'customerId' : 'supplierId'))
const realLabelKey = computed(() => props.labelKey || (isCustomer.value ? 'customerName' : 'supplierName'))
const realCodeKey = computed(() => props.codeKey || (isCustomer.value ? 'customerCode' : 'supplierCode'))
watch(() => props.modelValue, (val) => {
// 多选模式下空值归一化为数组,避免 el-select 告警
innerValue.value = props.multiple && (val === undefined || val === null) ? [] : val
const empty = props.multiple ? !Array.isArray(val) || val.length === 0 : val === undefined || val === null || val === ''
if (empty) selectedOptions.value = []
}, { immediate: true })
function getLabel(item) {
return item[realLabelKey.value] || ''
}
const displayOptions = computed(() => {
const merged = [...selectedOptions.value, ...options.value]
const seen = new Set()
return merged.filter(item => {
const key = String(item[realValueKey.value])
if (seen.has(key)) return false
seen.add(key)
return true
})
})
function fetchList(keyword = '') {
if (!props.remoteSearch && loaded.value) return Promise.resolve(options.value)
loading.value = true
const api = props.fetchOptions || (!isCustomer.value
? listSupplier
: (props.optionSelect ? listCustomerOption : listCustomer))
const currentRequest = ++requestSerial
const query = String(keyword || '').trim()
return api({
pageNum: 1,
pageSize: props.pageSize,
...props.params,
...(props.remoteSearch ? { keywords: query || undefined } : {})
})
.then(res => {
if (currentRequest !== requestSerial) return options.value
options.value = res.rows || []
loaded.value = true
return displayOptions.value
})
.finally(() => {
if (currentRequest === requestSerial) loading.value = false
})
}
function handleRemoteSearch(keyword) {
if (!props.remoteSearch) return
if (remoteTimer.value) clearTimeout(remoteTimer.value)
remoteTimer.value = setTimeout(() => {
remoteTimer.value = null
fetchList(keyword)
}, 300)
}
function handleChange(val) {
emit('update:modelValue', val)
if (props.multiple) {
const selected = displayOptions.value.filter(item =>
Array.isArray(val) && val.some(v => String(v) === String(item[realValueKey.value]))
)
selectedOptions.value = selected
emit('change', val, selected)
} else {
const selected = displayOptions.value.find(item => String(item[realValueKey.value]) === String(val))
selectedOptions.value = selected ? [selected] : []
emit('change', val, selected)
}
}
function handleVisibleChange(visible) {
if (visible && !loaded.value && !loading.value) {
fetchList()
}
}
onMounted(() => {
fetchList()
})
onBeforeUnmount(() => {
if (remoteTimer.value) clearTimeout(remoteTimer.value)
requestSerial++
})
defineExpose({
reload: () => {
loaded.value = false
options.value = []
return fetchList()
},
getOptions: () => displayOptions.value
})
</script>
<style scoped>
.option-sub {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>