288 lines
6.4 KiB
Vue
288 lines
6.4 KiB
Vue
<template>
|
|
<t-popup v-model="popupVisible" placement="bottom">
|
|
<div class="customer-selector bg-white">
|
|
<div class="title-bar">
|
|
<t-button variant="text" size="small" @click="cancelSelection">取消</t-button>
|
|
<h3 class="title">选择客户</h3>
|
|
<t-button
|
|
v-if="isMultiple"
|
|
theme="primary"
|
|
variant="text"
|
|
size="small"
|
|
@click="confirmSelection"
|
|
>确定</t-button>
|
|
<span v-else class="title-placeholder" />
|
|
</div>
|
|
<div class="search-wrapper">
|
|
<t-input
|
|
v-model.trim="keyword"
|
|
class="search"
|
|
borderless
|
|
clearable
|
|
placeholder="姓名 / 编码 / 手机号"
|
|
/>
|
|
</div>
|
|
<div ref="resultListRef" class="result-list">
|
|
<customer-list
|
|
:customer-list="customerOptions"
|
|
:selected-id-list="draftSelectedIdList"
|
|
:selectable="isMultiple"
|
|
:empty-text="loading ? '正在加载客户' : '未找到匹配客户'"
|
|
@select="toggleCustomer"
|
|
/>
|
|
<div class="load-state" v-text="loadStateText" />
|
|
</div>
|
|
</div>
|
|
</t-popup>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Scroller } from "timi-web";
|
|
import CustomerAPI from "@/api/CustomerAPI";
|
|
import CustomerList from "@/components/CustomerList.vue";
|
|
import type { Customer } from "@/types/Customer";
|
|
|
|
type SelectionMode = "single" | "multiple";
|
|
|
|
interface Props {
|
|
modelValue: string | string[];
|
|
visible: boolean;
|
|
selectionMode?: SelectionMode;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
selectionMode: "single"
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
"update:modelValue": [value: string | 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>();
|
|
const draftSelectedIdList = ref<string[]>([]);
|
|
const selectedCustomerCache = new Map<string, Customer>();
|
|
let searchToken = 0;
|
|
let keywordTimer: number | undefined;
|
|
|
|
const isMultiple = computed(() => props.selectionMode === "multiple");
|
|
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.list({
|
|
index: pageIndex.value,
|
|
size: pageSize,
|
|
likesExample: {
|
|
code: keyword.value || undefined,
|
|
name: keyword.value || undefined,
|
|
telephone: keyword.value || undefined
|
|
}
|
|
});
|
|
if (token !== searchToken) {
|
|
return;
|
|
}
|
|
const list = result.list || [];
|
|
for (const item of list) {
|
|
if (item.id) {
|
|
selectedCustomerCache.set(item.id, item);
|
|
}
|
|
}
|
|
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 || 0 === customerOptions.value.length) {
|
|
Scroller.removeListener(scrollListenerName);
|
|
return;
|
|
}
|
|
await nextTick();
|
|
Scroller.addBottomListener(scrollListenerName, loadCustomers, {
|
|
distance: 48,
|
|
canLoad: () => props.visible && !loading.value && !noMore.value,
|
|
getElement: () => resultListRef.value
|
|
});
|
|
}
|
|
|
|
function getModelValueIdList(): string[] {
|
|
const value = props.modelValue;
|
|
return (Array.isArray(value) ? value : [value]).filter((item): item is string => Boolean(item));
|
|
}
|
|
|
|
function syncDraftSelection(): void {
|
|
draftSelectedIdList.value = getModelValueIdList();
|
|
}
|
|
|
|
function toggleCustomer(item: Customer): void {
|
|
const id = item.id;
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
selectedCustomerCache.set(id, item);
|
|
if (!isMultiple.value) {
|
|
emit("update:modelValue", id);
|
|
emit("change", id);
|
|
emit("update:visible", false);
|
|
return;
|
|
}
|
|
|
|
if (draftSelectedIdList.value.includes(id)) {
|
|
draftSelectedIdList.value = draftSelectedIdList.value.filter(itemId => itemId !== id);
|
|
return;
|
|
}
|
|
|
|
draftSelectedIdList.value = [...draftSelectedIdList.value, id];
|
|
}
|
|
|
|
function cancelSelection(): void {
|
|
syncDraftSelection();
|
|
emit("update:visible", false);
|
|
}
|
|
|
|
function confirmSelection(): void {
|
|
const value = [...draftSelectedIdList.value];
|
|
emit("update:modelValue", value);
|
|
emit("change", value as unknown as string);
|
|
emit("update:visible", false);
|
|
}
|
|
|
|
watch(() => props.visible, async (value) => {
|
|
if (!value) {
|
|
syncDraftSelection();
|
|
keyword.value = "";
|
|
window.clearTimeout(keywordTimer);
|
|
Scroller.removeListener(scrollListenerName);
|
|
return;
|
|
}
|
|
syncDraftSelection();
|
|
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-bar {
|
|
padding: 0 1rem;
|
|
display: grid;
|
|
grid-template-columns: 4rem 1fr 4rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.title {
|
|
margin: 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.title-placeholder {
|
|
height: 1px;
|
|
}
|
|
|
|
.search-wrapper {
|
|
z-index: 1;
|
|
padding: 1rem 0 .5rem 0;
|
|
box-shadow: 0 4px 4px rgba(0, 0, 0, .05);
|
|
|
|
.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);
|
|
}
|
|
}
|
|
|
|
:global(html.theme-dark) {
|
|
.customer-selector {
|
|
background: rgba(15, 23, 42, .96);
|
|
}
|
|
|
|
.load-state {
|
|
color: #94a3b8;
|
|
}
|
|
}
|
|
</style>
|