front/src/views/order/priceCheck/components/ShippingMethodTabSelect.vue
zzh 67cf30b6e8 feat: 自动触发公式引擎、查价分组运输方式与销售产品预览
新增自动触发中文公式编辑/测试、查价运输方式 Tab 分组选择,并完善销售产品字段预览与菜单展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 18:36:15 +08:00

279 lines
6.0 KiB
Vue

<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { IconDown, IconSearch } from '@arco-design/web-vue/es/icon'
import type { PriceCheckShippingGroup } from '@/api/order'
const props = defineProps<{
modelValue: string
groups: PriceCheckShippingGroup[]
loading?: boolean
placeholder?: string
disabled?: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [string]
}>()
const popupVisible = ref(false)
const activeGroupCode = ref('')
const keyword = ref('')
const flatOptions = computed(() =>
props.groups.flatMap((group) =>
group.products.map((product) => ({
...product,
groupCode: group.groupCode,
groupName: group.groupName,
}))
)
)
const selectedLabel = computed(() => {
const hit = flatOptions.value.find((item) => item.productCode === props.modelValue)
return hit?.label ?? ''
})
const filteredGroups = computed(() => {
const kw = keyword.value.trim().toLowerCase()
if (!kw) return props.groups
return props.groups
.map((group) => ({
...group,
products: group.products.filter((product) => {
const text = `${product.label} ${product.productCode} ${product.productName}`.toLowerCase()
return text.includes(kw)
}),
}))
.filter((group) => group.products.length > 0)
})
watch(
() => props.groups,
(groups) => {
if (!groups.length) {
activeGroupCode.value = ''
return
}
if (!groups.some((group) => group.groupCode === activeGroupCode.value)) {
activeGroupCode.value = groups[0].groupCode
}
},
{ immediate: true }
)
watch(filteredGroups, (groups) => {
if (!groups.length) {
activeGroupCode.value = ''
return
}
if (!groups.some((group) => group.groupCode === activeGroupCode.value)) {
activeGroupCode.value = groups[0].groupCode
}
})
function openPopup() {
if (props.disabled || props.loading) return
popupVisible.value = true
}
function closePopup() {
popupVisible.value = false
keyword.value = ''
}
function selectProduct(productCode: string) {
emit('update:modelValue', productCode)
closePopup()
}
</script>
<template>
<a-dropdown
v-model:popup-visible="popupVisible"
trigger="click"
position="bl"
:disabled="disabled || loading"
>
<div
class="shipping-tab-select-trigger"
:class="{ disabled: disabled || loading, open: popupVisible }"
@click="openPopup"
>
<span class="trigger-text" :class="{ placeholder: !selectedLabel }">
{{ selectedLabel || placeholder || '请选择运输方式' }}
</span>
<icon-down class="trigger-icon" />
</div>
<template #content>
<div class="shipping-tab-select-panel" @click.stop>
<div class="panel-search">
<a-input
v-model="keyword"
allow-clear
size="small"
:placeholder="placeholder || '搜索产品名称或代码'"
>
<template #prefix>
<icon-search />
</template>
</a-input>
</div>
<a-tabs
v-if="filteredGroups.length"
v-model:active-key="activeGroupCode"
type="rounded"
size="small"
class="group-tabs"
>
<a-tab-pane
v-for="group in filteredGroups"
:key="group.groupCode"
:title="group.groupName"
>
<div class="product-list">
<button
v-for="product in group.products"
:key="product.productCode"
type="button"
class="product-item"
:class="{ active: modelValue === product.productCode }"
@click="selectProduct(product.productCode)"
>
<span class="product-name">{{ product.productName || product.productCode }}</span>
<span class="product-code">{{ product.productCode }}</span>
</button>
</div>
</a-tab-pane>
</a-tabs>
<a-empty v-else description="暂无运输方式" />
</div>
</template>
</a-dropdown>
</template>
<style lang="less" scoped>
.shipping-tab-select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 32px;
padding: 4px 12px;
border: 1px solid #e5e6eb;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
&:hover:not(.disabled) {
border-color: #c9cdd4;
}
&.open:not(.disabled) {
border-color: var(--app-primary-color, #2d8c8c);
box-shadow: 0 0 0 2px rgba(45, 140, 140, 0.12);
}
&.disabled {
cursor: not-allowed;
background: #f7f8fa;
color: #c9cdd4;
}
}
.trigger-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
color: #1d2129;
&.placeholder {
color: #c9cdd4;
}
}
.trigger-icon {
flex-shrink: 0;
margin-left: 8px;
color: #86909c;
font-size: 12px;
}
.shipping-tab-select-panel {
width: 420px;
max-width: 90vw;
padding: 8px 8px 4px;
box-sizing: border-box;
}
.panel-search {
margin-bottom: 8px;
}
.group-tabs {
:deep(.arco-tabs-nav) {
margin-bottom: 0;
}
:deep(.arco-tabs-content) {
padding-top: 8px;
}
}
.product-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 260px;
overflow-y: auto;
}
.product-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
padding: 8px 10px;
border: none;
border-radius: 4px;
background: transparent;
text-align: left;
cursor: pointer;
transition: background-color 0.15s;
&:hover {
background: rgba(45, 140, 140, 0.06);
}
&.active {
background: rgba(45, 140, 140, 0.12);
.product-name {
color: var(--app-primary-color, #2d8c8c);
font-weight: 600;
}
}
}
.product-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
color: #1d2129;
}
.product-code {
flex-shrink: 0;
font-size: 12px;
color: #86909c;
}
</style>