修改搜索功能

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
@@ -20,7 +20,7 @@ public class RuoYiApplication
{
// System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(RuoYiApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ \n" +
System.out.println("(♥◠‿◠)ノ゙ 启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \n" +
" | ( ' ) | \\ _. / ' \n" +
@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -78,6 +79,16 @@ public class BillingController extends BaseController
return getDataTable(list);
}
/**
* Order no options for remote-search dropdown (bill list multi search).
*/
@PreAuthorize("@ss.hasPermi('price:billing:bill')")
@GetMapping("/bill/orderNos")
public AjaxResult orderNoOptions(@RequestParam(value = "keyword", required = false) String keyword)
{
return success(prcOrderBillService.listOrderNoOptions(keyword));
}
@PreAuthorize("@ss.hasPermi('price:billing:bill')")
@GetMapping("/bill/{billId}")
public AjaxResult getBill(@PathVariable String billId)
@@ -18,10 +18,14 @@ public class PrcOrderBill extends BaseEntity
private String billId;
private String billKey;
private String orderNo;
/** 批量查询:多个订单号精确匹配(仅查询用,非表字段) */
private List<String> orderNos;
private String trackingNo;
private Long customerId;
private String customerCode;
private String customerName;
/** 批量查询:多个客户代码精确匹配(仅查询用,非表字段) */
private List<String> customerCodes;
private Integer pieces;
private BigDecimal actualWeight;
private BigDecimal volume;
@@ -38,6 +42,8 @@ public class PrcOrderBill extends BaseEntity
private String orderRemark;
private String productCode;
private String productName;
/** 批量查询:多个产品代码精确匹配(仅查询用,非表字段) */
private List<String> productCodes;
private String countryCode;
private String countryName;
private BigDecimal weight;
@@ -15,6 +15,9 @@ public interface PrcOrderBillMapper
public List<PrcOrderBill> selectPrcOrderBillList(PrcOrderBill bill);
/** 订单号下拉搜索:按关键词模糊匹配,返回去重后的订单号(按最近创建排序,限量返回) */
public List<String> selectOrderNoOptions(@Param("keyword") String keyword, @Param("limit") int limit);
/** 应收账单生成读取指定客户截止日期前的未出账订单计费快照。 */
public List<PrcOrderBill> selectPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("cutoffDate") Date cutoffDate,
@@ -12,6 +12,9 @@ public interface IPrcOrderBillService
List<PrcOrderBill> selectBillList(PrcOrderBill query);
/** 订单号下拉搜索选项(模糊匹配,去重限量) */
List<String> listOrderNoOptions(String keyword);
TmsPriceQuoteResult getBill(String billId);
TmsPriceQuoteResult getBillByOrderNo(String orderNo);
@@ -4,9 +4,12 @@ import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -88,9 +91,41 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
@Override
public List<PrcOrderBill> selectBillList(PrcOrderBill query)
{
query.setOrderNos(normalizeCodeList(query.getOrderNos()));
query.setCustomerCodes(normalizeCodeList(query.getCustomerCodes()));
query.setProductCodes(normalizeCodeList(query.getProductCodes()));
return orderBillMapper.selectPrcOrderBillList(query);
}
@Override
public List<String> listOrderNoOptions(String keyword)
{
if (StringUtils.isBlank(keyword))
{
return new ArrayList<>();
}
return orderBillMapper.selectOrderNoOptions(keyword.trim(), 20);
}
/**
* 批量编码归一化:逐个 trim、拆开误传的逗号/分号/空白分隔串、去空去重
*/
private List<String> normalizeCodeList(List<String> codes)
{
if (codes == null || codes.isEmpty())
{
return null;
}
List<String> cleaned = codes.stream()
.filter(StringUtils::isNotBlank)
.map(s -> s.split("[,,;;\\s]+"))
.flatMap(Arrays::stream)
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.collectingAndThen(Collectors.toList(), list -> new ArrayList<>(new LinkedHashSet<>(list))));
return cleaned.isEmpty() ? null : cleaned;
}
@Override
public TmsPriceQuoteResult getBill(String billId)
{
@@ -87,24 +87,55 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where bill_id = #{billId}
</select>
<select id="selectOrderNoOptions" resultType="String">
select order_no
from prc_order_bill
<where>
<if test="keyword != null and keyword != ''">
order_no like concat('%', #{keyword}, '%')
</if>
</where>
group by order_no
order by max(create_time) desc
limit #{limit}
</select>
<select id="selectPrcOrderBillList" parameterType="PrcOrderBill" resultMap="PrcOrderBillResult">
<include refid="selectPrcOrderBillVo"/>
<where>
<if test="orderNo != null and orderNo != ''">
AND order_no like concat('%', #{orderNo}, '%')
</if>
<if test="orderNos != null and orderNos.size() > 0">
AND order_no IN
<foreach collection="orderNos" item="ono" open="(" separator="," close=")">
#{ono}
</foreach>
</if>
<if test="trackingNo != null and trackingNo != ''">
AND tracking_no like concat('%', #{trackingNo}, '%')
</if>
<if test="customerCode != null and customerCode != ''">
AND customer_code like concat('%', #{customerCode}, '%')
</if>
<if test="customerCodes != null and customerCodes.size() > 0">
AND customer_code IN
<foreach collection="customerCodes" item="ccode" open="(" separator="," close=")">
#{ccode}
</foreach>
</if>
<if test="customerName != null and customerName != ''">
AND customer_name like concat('%', #{customerName}, '%')
</if>
<if test="productCode != null and productCode != ''">
AND product_code like concat('%', #{productCode}, '%')
</if>
<if test="productCodes != null and productCodes.size() > 0">
AND product_code IN
<foreach collection="productCodes" item="pcode" open="(" separator="," close=")">
#{pcode}
</foreach>
</if>
<if test="countryCode != null and countryCode != ''">
AND country_code = #{countryCode}
</if>
+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>