✨ 支持账单客户名称和编码远程搜索

This commit is contained in:
caojia
2026-10-09 10:33:41 +08:00
parent 5b4cbaffd6
commit 94e7a18e0b
3 changed files with 61 additions and 12 deletions
@@ -10,12 +10,15 @@
:disabled="disabled" :disabled="disabled"
:loading="loading" :loading="loading"
:size="size" :size="size"
:remote="remoteSearch"
:remote-method="remoteSearch ? handleRemoteSearch : undefined"
:reserve-keyword="remoteSearch"
style="width: 100%;" style="width: 100%;"
@change="handleChange" @change="handleChange"
@visible-change="handleVisibleChange" @visible-change="handleVisibleChange"
> >
<el-option <el-option
v-for="item in options" v-for="item in displayOptions"
:key="item[realValueKey]" :key="item[realValueKey]"
:label="getLabel(item)" :label="getLabel(item)"
:value="item[realValueKey]" :value="item[realValueKey]"
@@ -87,6 +90,11 @@ const props = defineProps({
pageSize: { pageSize: {
type: Number, type: Number,
default: 1000 default: 1000
},
// 远程搜索时按客户名称/代码查询,避免本地只加载固定数量选项。
remoteSearch: {
type: Boolean,
default: false
} }
}) })
@@ -94,8 +102,11 @@ const emit = defineEmits(['update:modelValue', 'change'])
const innerValue = ref(props.modelValue) const innerValue = ref(props.modelValue)
const options = ref([]) const options = ref([])
const selectedOptions = ref([])
const loading = ref(false) const loading = ref(false)
const loaded = ref(false) const loaded = ref(false)
const remoteTimer = ref(null)
let requestSerial = 0
const isCustomer = computed(() => props.type === 'customer') const isCustomer = computed(() => props.type === 'customer')
const realValueKey = computed(() => props.valueKey || (isCustomer.value ? 'customerId' : 'supplierId')) const realValueKey = computed(() => props.valueKey || (isCustomer.value ? 'customerId' : 'supplierId'))
@@ -105,36 +116,68 @@ const realCodeKey = computed(() => props.codeKey || (isCustomer.value ? 'custome
watch(() => props.modelValue, (val) => { watch(() => props.modelValue, (val) => {
// 多选模式下空值归一化为数组,避免 el-select 告警 // 多选模式下空值归一化为数组,避免 el-select 告警
innerValue.value = props.multiple && (val === undefined || val === null) ? [] : val 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 }) }, { immediate: true })
function getLabel(item) { function getLabel(item) {
return item[realLabelKey.value] || '' return item[realLabelKey.value] || ''
} }
function fetchList() { const displayOptions = computed(() => {
if (loaded.value) return Promise.resolve(options.value) 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 loading.value = true
const api = isCustomer.value ? listCustomer : listSupplier const api = isCustomer.value ? listCustomer : listSupplier
return api({ pageNum: 1, pageSize: props.pageSize, ...props.params }) const currentRequest = ++requestSerial
const query = String(keyword || '').trim()
return api({
pageNum: 1,
pageSize: props.pageSize,
...props.params,
...(props.remoteSearch ? { keywords: query || undefined } : {})
})
.then(res => { .then(res => {
if (currentRequest !== requestSerial) return options.value
options.value = res.rows || [] options.value = res.rows || []
loaded.value = true loaded.value = true
return options.value return displayOptions.value
}) })
.finally(() => { .finally(() => {
loading.value = false 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) { function handleChange(val) {
emit('update:modelValue', val) emit('update:modelValue', val)
if (props.multiple) { if (props.multiple) {
const selected = options.value.filter(item => const selected = displayOptions.value.filter(item =>
Array.isArray(val) && val.some(v => String(v) === String(item[realValueKey.value])) Array.isArray(val) && val.some(v => String(v) === String(item[realValueKey.value]))
) )
selectedOptions.value = selected
emit('change', val, selected) emit('change', val, selected)
} else { } else {
const selected = options.value.find(item => String(item[realValueKey.value]) === String(val)) const selected = displayOptions.value.find(item => String(item[realValueKey.value]) === String(val))
selectedOptions.value = selected ? [selected] : []
emit('change', val, selected) emit('change', val, selected)
} }
} }
@@ -149,12 +192,18 @@ onMounted(() => {
fetchList() fetchList()
}) })
onBeforeUnmount(() => {
if (remoteTimer.value) clearTimeout(remoteTimer.value)
requestSerial++
})
defineExpose({ defineExpose({
reload: () => { reload: () => {
loaded.value = false loaded.value = false
options.value = []
return fetchList() return fetchList()
}, },
getOptions: () => options.value getOptions: () => displayOptions.value
}) })
</script> </script>
@@ -3,7 +3,7 @@
<el-dialog v-model="dialogOpen" class="generate-dialog" title="生成应收账单" width="1200px" append-to-body destroy-on-close> <el-dialog v-model="dialogOpen" class="generate-dialog" title="生成应收账单" width="1200px" append-to-body destroy-on-close>
<el-form ref="generateFormRef" :model="generateForm" :rules="generateRules" label-width="110px" class="generate-form"> <el-form ref="generateFormRef" :model="generateForm" :rules="generateRules" label-width="110px" class="generate-form">
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="8"><el-form-item label="客户" prop="customerId"><customer-select v-model="generateForm.customerId" placeholder="请选择客户" @change="handleCustomerChange" /></el-form-item></el-col> <el-col :span="8"><el-form-item label="客户" prop="customerId"><customer-select v-model="generateForm.customerId" placeholder="请输入客户名称或代码搜索" :remote-search="true" :page-size="20" @change="handleCustomerChange" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="币种" prop="currency"><el-select v-model="generateForm.currency" placeholder="请选择币种" style="width: 100%;" @change="handleCurrencyChange"><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /></el-select></el-form-item></el-col> <el-col :span="8"><el-form-item label="币种" prop="currency"><el-select v-model="generateForm.currency" placeholder="请选择币种" style="width: 100%;" @change="handleCurrencyChange"><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="账单截止日" prop="cutoffDate"><el-date-picker v-model="generateForm.cutoffDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择账单截止日" style="width: 100%;" @change="resetPreview" /></el-form-item></el-col> <el-col :span="8"><el-form-item label="账单截止日" prop="cutoffDate"><el-date-picker v-model="generateForm.cutoffDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择账单截止日" style="width: 100%;" @change="resetPreview" /></el-form-item></el-col>
<el-col :span="8"> <el-col :span="8">
+2 -2
View File
@@ -33,7 +33,7 @@
<el-col v-if="customerScoped" :span="1.5"><el-button type="info" plain icon="Close" @click="clearCustomerFilter">清除客户条件</el-button></el-col> <el-col v-if="customerScoped" :span="1.5"><el-button type="info" plain icon="Close" @click="clearCustomerFilter">清除客户条件</el-button></el-col>
</template> </template>
<template #search-customerId="{ model }"> <template #search-customerId="{ model }">
<customer-select v-model="model.customerId" placeholder="请选择客户" :page-size="1000" /> <customer-select v-model="model.customerId" placeholder="请输入客户名称或代码搜索" :remote-search="true" :page-size="20" />
</template> </template>
<template #col-status="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template> <template #col-status="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template>
<template #col-totalAmount="{ row }"><span class="amount">{{ money(row.totalAmount) }}</span></template> <template #col-totalAmount="{ row }"><span class="amount">{{ money(row.totalAmount) }}</span></template>
@@ -153,7 +153,7 @@ const columns = [
{ label: '账单日期', prop: 'billDate', width: 120, formatter: row => date(row.billDate) }, { label: '操作', prop: 'action', width: 330, fixed: 'right', slot: 'action', showInToolbar: false } { label: '账单日期', prop: 'billDate', width: 120, formatter: row => date(row.billDate) }, { label: '操作', prop: 'action', width: 330, fixed: 'right', slot: 'action', showInToolbar: false }
] ]
const searchColumns = [ const searchColumns = [
{ label: '账单号', prop: 'billNo', component: 'input', placeholder: '账单号', width: '180px' }, { label: '账单名称', prop: 'billName', component: 'input', placeholder: '账单名称', width: '180px' }, { label: '客户名称', prop: 'customerId', component: 'input', placeholder: '请选择客户', width: '220px' }, { label: '客户编码', prop: 'customerCode', component: 'input', placeholder: '客户编码', width: '140px' }, { label: '账单号', prop: 'billNo', component: 'input', placeholder: '账单号', width: '180px' }, { label: '账单名称', prop: 'billName', component: 'input', placeholder: '账单名称', width: '180px' }, { label: '客户名称', prop: 'customerId', component: 'input', placeholder: '请选择客户', width: '220px' },
{ label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '全部状态', width: '150px' }, { label: '账单日期', prop: 'billDateRange', component: 'daterange', width: '300px' }, { label: '账单截止日', prop: 'billPeriodEnd', component: 'date', width: '160px' } { label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '全部状态', width: '150px' }, { label: '账单日期', prop: 'billDateRange', component: 'daterange', width: '300px' }, { label: '账单截止日', prop: 'billPeriodEnd', component: 'date', width: '160px' }
] ]