291 lines
6.1 KiB
Vue
291 lines
6.1 KiB
Vue
<template>
|
|
<t-popup v-model="popupVisible" placement="bottom">
|
|
<div class="customer-selector bg-white">
|
|
<h3 class="title">选择客户</h3>
|
|
<div class="search-wrapper">
|
|
<t-input
|
|
v-model.trim="keyword"
|
|
class="search"
|
|
borderless
|
|
clearable
|
|
placeholder="姓名 / 编码 / 手机号"
|
|
/>
|
|
</div>
|
|
<div v-if="customerOptions.length" ref="resultListRef" class="result-list">
|
|
<t-cell-group>
|
|
<t-cell
|
|
v-for="(item, index) in customerOptions"
|
|
:key="item.id || item.code || index"
|
|
:note="item.code"
|
|
hover
|
|
@click="selectCustomer(item)"
|
|
>
|
|
<template #title>
|
|
<div class="cell-title">
|
|
<span v-text="item.name || item.code || '未命名客户'" />
|
|
<gender-icon :value="item.gender" />
|
|
</div>
|
|
</template>
|
|
<template #description>
|
|
<telephone-link :value="item.telephone" placeholder="未填写电话" />
|
|
</template>
|
|
<template #leftIcon>
|
|
<t-image v-if="getCustomerPhotoURL(item)" :src="getCustomerPhotoURL(item)" :alt="item.name" fit="cover" class="avatar" />
|
|
<div v-else class="avatar fallback" v-text="(item.name || item.code || '客').slice(0, 1)" />
|
|
</template>
|
|
</t-cell>
|
|
</t-cell-group>
|
|
<div class="load-state" v-text="loadStateText" />
|
|
</div>
|
|
<div v-else class="empty-block" v-text="loading ? '正在加载客户' : '未找到匹配客户'" />
|
|
</div>
|
|
</t-popup>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Scroller } from "timi-web";
|
|
import CustomerAPI from "@/api/CustomerAPI";
|
|
import type { Customer } from "@/types/Customer";
|
|
import { getCustomerPhotoURL } from "@/utils/customer";
|
|
|
|
const props = defineProps<{
|
|
modelValue: string;
|
|
visible: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
"update:modelValue": [value: string];
|
|
"update:visible": [value: boolean];
|
|
change: [value: string];
|
|
}>();
|
|
|
|
const pageSize = 20;
|
|
const scrollListenerName = "CustomerSelector";
|
|
const keyword = ref("");
|
|
const loading = ref(false);
|
|
const noMore = ref(false);
|
|
const pageIndex = ref(0);
|
|
const totalCount = ref(0);
|
|
const customerOptions = ref<Customer[]>([]);
|
|
const resultListRef = ref<HTMLElement>();
|
|
let searchToken = 0;
|
|
let keywordTimer: number | undefined;
|
|
|
|
const loadStateText = computed(() => {
|
|
if (loading.value) {
|
|
return "正在加载更多";
|
|
}
|
|
|
|
return noMore.value ? `共 ${totalCount.value} 位客户` : "上拉加载更多";
|
|
});
|
|
|
|
const popupVisible = computed({
|
|
get() {
|
|
return props.visible;
|
|
},
|
|
set(value: boolean) {
|
|
emit("update:visible", value);
|
|
}
|
|
});
|
|
|
|
async function reloadCustomers(): Promise<void> {
|
|
pageIndex.value = 0;
|
|
totalCount.value = 0;
|
|
customerOptions.value = [];
|
|
noMore.value = false;
|
|
await loadCustomers();
|
|
}
|
|
|
|
async function loadCustomers(): Promise<void> {
|
|
if (loading.value || noMore.value) {
|
|
return;
|
|
}
|
|
|
|
const token = searchToken + 1;
|
|
searchToken = token;
|
|
loading.value = true;
|
|
try {
|
|
const result = await CustomerAPI.searchPage({
|
|
index: pageIndex.value,
|
|
size: pageSize,
|
|
equalsExample: {
|
|
keyword: keyword.value || undefined
|
|
}
|
|
});
|
|
if (token !== searchToken) {
|
|
return;
|
|
}
|
|
const list = result.list || [];
|
|
customerOptions.value = customerOptions.value.concat(list);
|
|
totalCount.value = result.total || 0;
|
|
pageIndex.value += 1;
|
|
noMore.value = totalCount.value <= customerOptions.value.length || list.length < pageSize;
|
|
} finally {
|
|
if (token === searchToken) {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function bindScrollListener(): Promise<void> {
|
|
if (!props.visible || customerOptions.value.length <= 0) {
|
|
Scroller.removeListener(scrollListenerName);
|
|
return;
|
|
}
|
|
await nextTick();
|
|
Scroller.addBottomListener(scrollListenerName, loadCustomers, {
|
|
distance: 48,
|
|
canLoad: () => props.visible && !loading.value && !noMore.value,
|
|
getElement: () => resultListRef.value
|
|
});
|
|
}
|
|
|
|
function selectCustomer(item: Customer): void {
|
|
const id = item.id;
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
emit("update:modelValue", id);
|
|
emit("change", id);
|
|
emit("update:visible", false);
|
|
}
|
|
|
|
watch(() => props.visible, async (value) => {
|
|
if (!value) {
|
|
keyword.value = "";
|
|
window.clearTimeout(keywordTimer);
|
|
Scroller.removeListener(scrollListenerName);
|
|
return;
|
|
}
|
|
await reloadCustomers();
|
|
await bindScrollListener();
|
|
});
|
|
|
|
watch(keyword, async () => {
|
|
if (!props.visible) {
|
|
return;
|
|
}
|
|
window.clearTimeout(keywordTimer);
|
|
keywordTimer = window.setTimeout(async () => {
|
|
await reloadCustomers();
|
|
await bindScrollListener();
|
|
}, 300);
|
|
});
|
|
|
|
watch(() => customerOptions.value.length, async () => {
|
|
await bindScrollListener();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.clearTimeout(keywordTimer);
|
|
Scroller.removeListener(scrollListenerName);
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.customer-selector {
|
|
display: flex;
|
|
height: 80vh;
|
|
padding-top: 1rem;
|
|
border-radius: 1.25rem 1.25rem 0 0;
|
|
flex-direction: column;
|
|
|
|
.title {
|
|
margin: 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.search-wrapper {
|
|
z-index: 1;
|
|
padding: 1rem 0 .5rem 0;
|
|
box-shadow: var(--td-shadow-1);
|
|
|
|
.search {
|
|
--td-input-vertical-padding: 8px;
|
|
|
|
flex: 0;
|
|
margin: 0 1rem;
|
|
border: 1px solid var(--td-component-border);
|
|
border-radius: .875rem;
|
|
}
|
|
}
|
|
|
|
.result-list {
|
|
flex: 1;
|
|
overflow: auto;
|
|
min-height: 0;
|
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
|
|
.cell-title {
|
|
gap: .5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
}
|
|
|
|
.load-state {
|
|
padding: .75rem;
|
|
color: #52607a;
|
|
text-align: center;
|
|
font-size: .875rem;
|
|
}
|
|
}
|
|
|
|
.avatar {
|
|
width: 3.5rem;
|
|
height: 3.5rem;
|
|
border-radius: .5rem;
|
|
|
|
&.fallback {
|
|
color: #2563eb;
|
|
display: flex;
|
|
font-size: 1.25rem;
|
|
background: #dbeafe;
|
|
align-items: center;
|
|
font-weight: 600;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
:deep(.avatar .t-image__img) {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
border-radius: inherit;
|
|
}
|
|
|
|
.empty-block {
|
|
color: #52607a;
|
|
margin: 1rem;
|
|
padding: 1rem;
|
|
text-align: center;
|
|
background: #f8fbff;
|
|
border-radius: 1rem;
|
|
}
|
|
|
|
:global(html.theme-dark) {
|
|
.customer-selector {
|
|
background: rgba(15, 23, 42, .96);
|
|
}
|
|
|
|
.empty-block {
|
|
color: #e2e8f0;
|
|
background: rgba(30, 41, 59, .9);
|
|
border-color: rgba(148, 163, 184, .16);
|
|
}
|
|
|
|
.load-state {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.avatar {
|
|
|
|
&.fallback {
|
|
color: #93c5fd;
|
|
background: rgba(30, 64, 175, .32);
|
|
}
|
|
}
|
|
}
|
|
</style>
|