275 lines
6.5 KiB
Vue
275 lines
6.5 KiB
Vue
<script setup lang="ts">
|
||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||
|
||
import { useVModel } from '@vueuse/core';
|
||
import { Empty, Input, Popover, Spin } from 'ant-design-vue';
|
||
|
||
import { matchExpressByTrackingNo } from '#/utils/matchExpressByTrackingNo';
|
||
import { getExpressCompaniesOption } from '#/views/business/express/express-company/api';
|
||
|
||
defineOptions({
|
||
name: 'ExpressCompanySelect',
|
||
inheritAttrs: false,
|
||
});
|
||
|
||
const props = defineProps<{
|
||
disabled?: boolean;
|
||
placeholder?: string;
|
||
/** 运单号:变化时按前缀自动匹配快递公司(用户手动改选后不再覆盖) */
|
||
trackingNo?: string;
|
||
value?: string;
|
||
}>();
|
||
|
||
const emits = defineEmits<{
|
||
'update:value': [value: string | undefined];
|
||
}>();
|
||
|
||
const mValue = useVModel(props, 'value', emits, { passive: true });
|
||
|
||
type ExpressCompanyOption = {
|
||
code: string;
|
||
id: number;
|
||
name: string;
|
||
};
|
||
|
||
const open = ref(false);
|
||
const loading = ref(false);
|
||
const searchKeyword = ref('');
|
||
const options = ref<ExpressCompanyOption[]>([]);
|
||
const searchInputRef = ref<InstanceType<typeof Input> | null>(null);
|
||
/** 用户是否已手动选择/清空;为 true 时不再按单号自动覆盖 */
|
||
const userPicked = ref(false);
|
||
|
||
function filterExpressCompanyOptions(
|
||
keyword: string,
|
||
list: ExpressCompanyOption[],
|
||
) {
|
||
const q = keyword.trim().toLowerCase();
|
||
if (!q) return list;
|
||
return list.filter((item) => {
|
||
const name = (item.name || '').toLowerCase();
|
||
const code = (item.code || '').toLowerCase();
|
||
return name.includes(q) || code.includes(q);
|
||
});
|
||
}
|
||
|
||
const filteredOptions = computed(() =>
|
||
filterExpressCompanyOptions(searchKeyword.value, options.value),
|
||
);
|
||
|
||
const selectedOption = computed(() =>
|
||
options.value.find((item) => item.code === mValue.value),
|
||
);
|
||
|
||
const displayText = computed(() => {
|
||
const item = selectedOption.value;
|
||
if (!item) return '';
|
||
return item.code ? `${item.name || '-'}(${item.code})` : item.name || '-';
|
||
});
|
||
|
||
/**
|
||
* 根据 trackingNo 自动选中快递公司
|
||
* 规则:尚未手动改选,或当前选中仍与规则结果一致时才覆盖
|
||
*/
|
||
function tryAutoMatchByTrackingNo() {
|
||
if (options.value.length === 0) return;
|
||
const matched = matchExpressByTrackingNo(props.trackingNo, options.value);
|
||
if (!matched?.code) return;
|
||
const matchedCode = matched.code;
|
||
if (userPicked.value && mValue.value && mValue.value !== matchedCode) {
|
||
// 用户已选其他公司,不覆盖
|
||
return;
|
||
}
|
||
if (mValue.value === matchedCode) return;
|
||
mValue.value = matchedCode;
|
||
}
|
||
|
||
async function loadOptions() {
|
||
loading.value = true;
|
||
try {
|
||
const res = await getExpressCompaniesOption({});
|
||
options.value = Array.isArray(res) ? res : [];
|
||
tryAutoMatchByTrackingNo();
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function selectOption(item: ExpressCompanyOption) {
|
||
userPicked.value = true;
|
||
mValue.value = item.code;
|
||
open.value = false;
|
||
searchKeyword.value = '';
|
||
}
|
||
|
||
function clearSelection(e: Event) {
|
||
e.stopPropagation();
|
||
userPicked.value = true;
|
||
mValue.value = undefined;
|
||
}
|
||
|
||
function onOpenChange(next: boolean) {
|
||
if (props.disabled) return;
|
||
open.value = next;
|
||
if (next) {
|
||
searchKeyword.value = '';
|
||
if (options.value.length === 0) {
|
||
loadOptions();
|
||
}
|
||
nextTick(() => searchInputRef.value?.focus?.());
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => props.disabled,
|
||
(disabled) => {
|
||
if (disabled) open.value = false;
|
||
},
|
||
);
|
||
|
||
watch(
|
||
() => props.trackingNo,
|
||
() => {
|
||
// 单号变化时允许再自动匹配(除非用户已改选为不一致公司)
|
||
tryAutoMatchByTrackingNo();
|
||
},
|
||
);
|
||
|
||
onMounted(() => {
|
||
loadOptions();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<Popover
|
||
:open="open"
|
||
overlay-class-name="express-company-select-popover"
|
||
placement="bottomLeft"
|
||
trigger="click"
|
||
@open-change="onOpenChange"
|
||
>
|
||
<template #content>
|
||
<div class="express-panel">
|
||
<Input
|
||
ref="searchInputRef"
|
||
v-model:value="searchKeyword"
|
||
allow-clear
|
||
class="express-search"
|
||
placeholder="搜索公司名称或编码"
|
||
/>
|
||
<Spin :spinning="loading">
|
||
<div v-if="filteredOptions.length > 0" class="express-list">
|
||
<button
|
||
v-for="item in filteredOptions"
|
||
:key="item.id"
|
||
:class="{ active: item.code === mValue }"
|
||
class="express-item"
|
||
type="button"
|
||
@click="selectOption(item)"
|
||
>
|
||
<div class="express-meta">
|
||
<div class="express-name">{{ item.name || '-' }}</div>
|
||
<div class="express-sub">{{ item.code }}</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<Empty v-else class="express-empty" description="无匹配快递公司" />
|
||
</Spin>
|
||
</div>
|
||
</template>
|
||
<div
|
||
:class="{ disabled, placeholder: !displayText }"
|
||
class="express-trigger"
|
||
>
|
||
<Input
|
||
:disabled="disabled"
|
||
:placeholder="placeholder || '请选择快递公司'"
|
||
:value="displayText"
|
||
class="express-trigger-input"
|
||
readonly
|
||
>
|
||
<template v-if="displayText && !disabled" #suffix>
|
||
<span class="clear-btn" @click="clearSelection">×</span>
|
||
</template>
|
||
</Input>
|
||
</div>
|
||
</Popover>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.express-trigger {
|
||
width: 100%;
|
||
/* min-width: fit-content; */
|
||
min-width: 250px;
|
||
cursor: pointer;
|
||
}
|
||
.express-trigger.disabled {
|
||
cursor: not-allowed;
|
||
}
|
||
.express-trigger-input {
|
||
pointer-events: none;
|
||
}
|
||
.express-trigger:not(.disabled) :deep(.ant-input) {
|
||
cursor: pointer;
|
||
}
|
||
.clear-btn {
|
||
pointer-events: auto;
|
||
cursor: pointer;
|
||
color: hsl(var(--muted-foreground));
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
padding: 0 4px;
|
||
}
|
||
.express-panel {
|
||
width: 360px;
|
||
max-width: 80vw;
|
||
}
|
||
.express-search {
|
||
margin-bottom: 8px;
|
||
}
|
||
.express-list {
|
||
max-height: 280px;
|
||
overflow-y: auto;
|
||
}
|
||
.express-item {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
padding: 8px 10px;
|
||
border: none;
|
||
background: transparent;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
border-radius: 6px;
|
||
color: hsl(var(--foreground));
|
||
}
|
||
.express-item:hover,
|
||
.express-item.active {
|
||
background: hsl(var(--accent));
|
||
}
|
||
.express-meta {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
.express-name {
|
||
font-size: 14px;
|
||
color: hsl(var(--foreground));
|
||
}
|
||
.express-sub {
|
||
font-size: 12px;
|
||
color: hsl(var(--muted-foreground));
|
||
}
|
||
.express-empty {
|
||
margin: 12px 0;
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
.express-company-select-popover .ant-popover-inner {
|
||
padding: 12px;
|
||
background: hsl(var(--card));
|
||
color: hsl(var(--foreground));
|
||
border: 1px solid hsl(var(--border));
|
||
}
|
||
</style>
|