修改搜索功能

This commit is contained in:
wuweihua
2026-09-17 17:58:00 +08:00
parent 5dcea947a7
commit 63988b798e
14 changed files with 353 additions and 12 deletions
+9
View File
@@ -51,3 +51,12 @@ export function getBill(billId) {
method: 'get'
})
}
/** Order no options for remote search (bill list multi search) */
export function listOrderNoOptions(keyword) {
return request({
url: '/price/billing/bill/orderNos',
method: 'get',
params: { keyword }
})
}
+18 -2
View File
@@ -24,7 +24,7 @@
</slot>
</el-form-item>
</template>
<el-form-item v-if="$slots.default">
<el-form-item v-if="$slots.default" :label-width="actionLabelWidth" :class="actionAlign === 'center' ? 'basic-form-action--center' : ''">
<slot />
</el-form-item>
</template>
@@ -50,7 +50,7 @@
</el-form-item>
</el-col>
<el-col :span="actionSpan" v-if="$slots.default">
<el-form-item :label-width="actionLabelWidth">
<el-form-item :label-width="actionLabelWidth" :class="actionAlign === 'center' ? 'basic-form-action--center' : ''">
<slot />
</el-form-item>
</el-col>
@@ -151,6 +151,11 @@ const props = defineProps({
actionLabelWidth: {
type: [String, Number],
default: '0'
},
// 操作区(默认插槽)对齐方式:left / center(center 时独占一行并水平居中)
actionAlign: {
type: String,
default: 'left'
}
})
@@ -233,4 +238,15 @@ defineExpose({
.basic-form--grid :deep(.el-form-item) {
margin-bottom: 18px;
}
/* 操作区居中:占满整行、内容水平居中(搜索按钮换行后居中显示的场景) */
.basic-form .basic-form-action--center {
width: 100%;
flex: 0 0 100%;
margin-right: 0;
}
.basic-form-action--center :deep(.el-form-item__content) {
display: flex;
justify-content: center;
}
</style>
@@ -8,6 +8,7 @@
:schemas="searchColumns"
:rules="searchRules"
:label-width="searchLabelWidth"
:action-align="searchActionAlign"
:size="size"
inline
v-show="showSearch"
@@ -178,6 +179,11 @@ const props = defineProps({
type: [String, Number],
default: 'auto'
},
// 搜索按钮区对齐:left / center(搜索项较多按钮换行时可用 center 让按钮居中)
searchActionAlign: {
type: String,
default: 'left'
},
// 列表加载函数:(params) => Promise<{rows, total}> | {rows, total}
query: {
type: Function,
@@ -4,6 +4,9 @@
:placeholder="placeholder"
:clearable="clearable"
:filterable="filterable"
:multiple="multiple"
:collapse-tags="multiple"
:collapse-tags-tooltip="multiple"
:disabled="disabled"
:loading="loading"
:size="size"
@@ -30,10 +33,16 @@ import { listCustomer } from '@/api/customersuppliermanagement/customerinfo/cust
import { listSupplier } from '@/api/customersuppliermanagement/supplierinfo/supplier.js'
const props = defineProps({
// 单选时为 String/Number;multiple 时为数组
modelValue: {
type: [String, Number],
type: [String, Number, Array],
default: ''
},
// 多选模式
multiple: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'customer',
@@ -94,7 +103,8 @@ const realLabelKey = computed(() => props.labelKey || (isCustomer.value ? 'custo
const realCodeKey = computed(() => props.codeKey || (isCustomer.value ? 'customerCode' : 'supplierCode'))
watch(() => props.modelValue, (val) => {
innerValue.value = val
// 多选模式下空值归一化为数组,避免 el-select 告警
innerValue.value = props.multiple && (val === undefined || val === null) ? [] : val
}, { immediate: true })
function getLabel(item) {
@@ -118,8 +128,15 @@ function fetchList() {
function handleChange(val) {
emit('update:modelValue', val)
const selected = options.value.find(item => String(item[realValueKey.value]) === String(val))
emit('change', val, selected)
if (props.multiple) {
const selected = options.value.filter(item =>
Array.isArray(val) && val.some(v => String(v) === String(item[realValueKey.value]))
)
emit('change', val, selected)
} else {
const selected = options.value.find(item => String(item[realValueKey.value]) === String(val))
emit('change', val, selected)
}
}
function handleVisibleChange(visible) {
@@ -0,0 +1,140 @@
<template>
<el-select
:model-value="tags"
multiple
filterable
remote
allow-create
default-first-option
clearable
:reserve-keyword="false"
collapse-tags
collapse-tags-tooltip
:remote-method="onRemoteSearch"
:loading="loading"
:placeholder="placeholder"
:style="{ width }"
@update:model-value="onUpdate"
@paste="onPaste"
>
<el-option v-for="v in mergedOptions" :key="v" :label="v" :value="v" />
</el-select>
</template>
<script setup name="MultiValueInput">
/**
* 公共多值输入组件 MultiValueInput
* 用于"一个字段搜索多个值"的场景(如订单号批量搜索)。
*
* 两种模式:
* 1. 手动模式(默认):输入后回车生成一个标签;支持直接粘贴多行/逗号/分号分隔的文本,自动拆成多个标签。
* 2. 远程搜索模式(传 :search-method):输入关键词时调用接口拉取候选项,下拉选择;回车/粘贴仍可强制生成标签。
*
* v-model 绑定字符串数组;空数组时视为未输入(不参与查询)。
*
* 用法示例(配合 BasicTable 搜索插槽):
* <basic-table ...>
* <template #search-orderNos="{ model }">
* <multi-value-input
* v-model="model.orderNos"
* placeholder="订单号,可多个"
* :search-method="keyword => listOrderNoOptions(keyword).then(res => res.data || [])"
* />
* </template>
* </basic-table>
*/
defineOptions({ name: 'MultiValueInput' })
const props = defineProps({
// 标签数组(v-model)
modelValue: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: '输入后回车,可粘贴多个'
},
width: {
type: String,
default: '220px'
},
// 最多标签数,超出忽略
max: {
type: Number,
default: 100
},
// 远程搜索方法:(keyword) => Promise<string[]>;不传则为手动模式
searchMethod: {
type: Function,
default: null
}
})
const emit = defineEmits(['update:modelValue'])
const loading = ref(false)
// 远程搜索返回的候选项
const options = ref([])
// 请求序号,防止慢响应覆盖新结果
let remoteSeq = 0
const tags = computed(() => (Array.isArray(props.modelValue) ? props.modelValue : []))
// 下拉展示 = 已选标签 + 远程候选项(保证已选值有对应 option 渲染)
const mergedOptions = computed(() => [...new Set([...tags.value, ...options.value])])
// 粘贴文本按 空白/逗号/分号(含中文标点、顿号)拆分
function splitTokens(text) {
return String(text || '')
.split(/[\s,;,;、]+/)
.map(t => t.trim())
.filter(Boolean)
}
function mergeTokens(text) {
if (!text) return
const merged = [...tags.value]
splitTokens(text).forEach(t => {
if (!merged.includes(t) && merged.length < props.max) merged.push(t)
})
emit('update:modelValue', merged)
}
function onUpdate(val) {
// el-select 多选返回数组(clearable 清空时为 [])
emit('update:modelValue', Array.isArray(val) ? val : [])
}
async function onRemoteSearch(query) {
if (!props.searchMethod) {
options.value = []
return
}
const keyword = String(query || '').trim()
if (!keyword) {
options.value = []
return
}
loading.value = true
const seq = ++remoteSeq
try {
const list = await props.searchMethod(keyword)
if (seq !== remoteSeq) return // 丢弃过期响应
options.value = (Array.isArray(list) ? list : []).map(v => String(v))
} catch (e) {
if (seq === remoteSeq) options.value = []
} finally {
if (seq === remoteSeq) loading.value = false
}
}
function onPaste(e) {
const text = e.clipboardData?.getData('text') || ''
// 无分隔符的单值粘贴交给 el-select 自身处理(allow-create / 远程搜索)
if (splitTokens(text).length <= 1) return
e.preventDefault()
e.stopPropagation()
mergeTokens(text)
}
</script>
+3
View File
@@ -55,6 +55,8 @@ import UserSelect from '@/components/UserSelect'
import UserName from '@/components/UserName'
// 产品多选绑定组件
import ProductSelect from '@/components/ProductSelect'
// 多值输入组件(一个字段搜索多个值,如订单号批量搜索)
import MultiValueInput from '@/components/MultiValueInput'
const app = createApp(App)
@@ -84,6 +86,7 @@ app.component('CustomerSelect', CustomerSelect)
app.component('UserSelect', UserSelect)
app.component('UserName', UserName)
app.component('ProductSelect', ProductSelect)
app.component('MultiValueInput', MultiValueInput)
app.use(router)
app.use(store)
+66 -5
View File
@@ -5,8 +5,40 @@
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
search-action-align="center"
row-key="billId"
>
<template #search-orderNos="{ model }">
<multi-value-input v-model="model.orderNos" placeholder="订单号" width="100%" :search-method="searchOrderNoOptions" />
</template>
<template #search-customerCodes="{ model }">
<customer-select
v-model="model.customerCodes"
type="customer"
value-key="customerCode"
label-key="customerName"
code-key="customerCode"
placeholder="请输入"
multiple
/>
</template>
<template #search-productCodes="{ model }">
<el-select
v-model="model.productCodes"
multiple
filterable
clearable
collapse-tags
collapse-tags-tooltip
placeholder="请输入"
style="width: 100%;"
>
<el-option v-for="p in productOptions" :key="p.code" :label="p.name" :value="p.code">
<span>{{ p.name }}</span>
<span style="float: right; color: var(--el-text-color-secondary); font-size: 12px;">{{ p.code }}</span>
</el-option>
</el-select>
</template>
<template #col-product="{ row }">
{{ row.productName || '-' }} ({{ row.productCode }})
</template>
@@ -32,9 +64,24 @@
</template>
<script setup name="BillingBill">
import { listBill, getBill } from '@/api/price/billing'
import { listBill, getBill, listOrderNoOptions } from '@/api/price/billing'
import { useProducts } from '@/components/ProductSelect/useProducts'
import BillDetail from './components/BillDetail.vue'
// 订单号下拉远程搜索:返回匹配的订单号列表(最多 20 条)
function searchOrderNoOptions(keyword) {
return listOrderNoOptions(keyword).then(res => res.data || [])
}
// 产品下拉选项:复用 ProductSelect 的模块级产品缓存(全应用只拉一次)
const { productList, ensureProducts } = useProducts()
ensureProducts()
const productOptions = computed(() =>
productList.value
.filter(p => p.productCode)
.map(p => ({ code: p.productCode, name: p.productName || p.productCode }))
)
const tableRef = ref()
const open = ref(false)
const title = ref('计费详情')
@@ -58,10 +105,10 @@ const columns = [
]
const searchColumns = [
{ label: '订单号', prop: 'orderNo', component: 'input', placeholder: '订单号', width: '160px' },
{ label: '运单号', prop: 'trackingNo', component: 'input', placeholder: '运单号', width: '160px' },
{ label: '客户代码', prop: 'customerCode', component: 'input', placeholder: '客户代码', width: '140px' },
{ label: '产品代码', prop: 'productCode', component: 'input', placeholder: '产品代码', width: '140px' }
{ label: '订单号', prop: 'orderNos', placeholder: '订单号' },
{ label: '运单号', prop: 'trackingNo', component: 'input', placeholder: '运单号' },
{ label: '客户', prop: 'customerCodes', placeholder: '客户代码' },
{ label: '产品代码', prop: 'productCodes', placeholder: '产品代码' }
]
function queryList(params) {
@@ -112,4 +159,18 @@ function sourceTagType(source) {
font-weight: 600;
font-variant-numeric: tabular-nums;
}
/* 搜索栏铺满整行:搜索项 flex 等分拉伸,控件占满所在项 */
.billing-bill :deep(.basic-form.el-form--inline) {
display: flex;
flex-wrap: wrap;
}
.billing-bill :deep(.basic-form.el-form--inline > .el-form-item:not(.basic-form-action--center)) {
flex: 1 1 0;
margin-right: 12px;
}
.billing-bill :deep(.basic-form.el-form--inline .el-form-item .el-select),
.billing-bill :deep(.basic-form.el-form--inline .el-form-item .el-input) {
width: 100%;
}
</style>