修改搜索功能
This commit is contained in:
@@ -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 }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user