Files
gao-customer-h5/src/components/CustomerSelector.vue
T

377 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<bottom-popup
v-model="popupVisible"
title="选择客户"
height="80vh"
:right-button-text="isMultiple ? '确定' : ''"
@left-button-click="cancelSelection"
@right-button-click="confirmSelection"
>
<div class="customer-selector">
<div class="search-wrap">
<t-input
v-model.trim="keyword"
class="search"
borderless
clearable
placeholder="搜索编码、姓名(拼音)、电话"
/>
<t-button
v-if="props.enableScan"
class="scan"
size="small"
theme="primary"
@click="toggleScanner"
>
<template #icon>
<t-icon name="scan" />
</template>
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
</t-button>
</div>
<div v-if="scannerVisible" class="scanner-wrap">
<code-scanner
:visible="scannerVisible"
:status-text="scanStatus"
active-text="正在识别客户编码,请将条码或二维码移入取景框。"
@decode="handleDecodedText"
@status-change="handleScanStatusChange"
/>
</div>
<t-pull-down-refresh
class="result-list"
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="refresh"
@scrolltolower="loadCustomers"
>
<customer-list
:customer-list="customerOptions"
:selected-id-list="draftSelectedIdList"
:selectable="isMultiple"
:empty-text="loading ? '正在加载客户' : '未找到匹配客户'"
@select="toggleCustomer"
/>
</t-pull-down-refresh>
</div>
</bottom-popup>
</template>
<script setup lang="ts">
import { Toolkit } from "timi-web";
import { BottomPopup } from "timi-tdesign-mobile";
import CustomerAPI from "@/api/CustomerAPI";
import CodeScanner from "@/components/CodeScanner.vue";
import CustomerList from "@/components/CustomerList.vue";
import { useSettingStore } from "@/store/settingStore";
import type { Customer } from "@/types/Customer";
type SelectionMode = "single" | "multiple";
interface Props {
enableScan?: boolean;
initialKeyword?: string;
modelValue: string | string[];
visible: boolean;
selectionMode?: SelectionMode;
storeId?: string;
}
const props = withDefaults(defineProps<Props>(), {
enableScan: true,
initialKeyword: "",
selectionMode: "single",
storeId: ""
});
const settingStore = useSettingStore();
const emit = defineEmits<{
"update:modelValue": [value: string | string[]];
"update:visible": [value: boolean];
change: [value: string];
}>();
const pageSize = 20;
const keyword = ref("");
const loading = ref(false);
const noMore = ref(false);
const pageIndex = ref(0);
const totalCount = ref(0);
const customerOptions = ref<Customer[]>([]);
const isRefreshing = ref(false);
const draftSelectedIdList = ref<string[]>([]);
const selectedCustomerCache = new Map<string, Customer>();
let searchToken = 0;
const scannerVisible = ref(false);
const scanStatus = ref("点击扫码开始识别");
let latestDecodedText = "";
let scanToken = 0;
const isMultiple = computed(() => props.selectionMode === "multiple");
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 refresh(): Promise<void> {
try {
await reloadCustomers();
} finally {
isRefreshing.value = false;
}
}
const reloadByKeyword = Toolkit.debounce(async () => {
await reloadCustomers();
}, false);
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,
storeId: props.storeId || undefined,
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;
}
}
}
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 toggleScanner(): void {
if (!props.enableScan) {
return;
}
scannerVisible.value = !scannerVisible.value;
if (scannerVisible.value) {
latestDecodedText = "";
scanStatus.value = "正在启动扫码预览";
return;
}
scanToken++;
}
async function handleDecodedText(rawText: string): Promise<void> {
const normalizedText = rawText.trim();
if (!normalizedText || normalizedText === latestDecodedText) {
return;
}
latestDecodedText = normalizedText;
const token = ++scanToken;
await settingStore.playScanSuccessSound();
try {
const customer = await CustomerAPI.findByCode(normalizedText, props.storeId || undefined);
if (token !== scanToken) {
return;
}
if (!customer?.id) {
scanStatus.value = `客户 ${normalizedText} 不存在,请先建档。`;
return;
}
selectedCustomerCache.set(customer.id, customer);
if (!customerOptions.value.some(item => item.id === customer.id)) {
customerOptions.value = [customer, ...customerOptions.value];
}
if (isMultiple.value) {
if (!draftSelectedIdList.value.includes(customer.id)) {
draftSelectedIdList.value = [...draftSelectedIdList.value, customer.id];
}
} else {
emit("update:modelValue", customer.id);
emit("change", customer.id);
emit("update:visible", false);
}
scanStatus.value = `已识别 ${customer.code || normalizedText},已选中 ${customer.name || "未命名客户"}。`;
scannerVisible.value = false;
} catch {
if (token === scanToken) {
scanStatus.value = "客户查询失败,请重试。";
}
}
}
function handleScanStatusChange(value: string): void {
scanStatus.value = value;
}
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 = "";
scannerVisible.value = false;
latestDecodedText = "";
scanToken++;
scanStatus.value = "点击扫码开始识别";
reloadByKeyword.cancel();
return;
}
syncDraftSelection();
keyword.value = props.initialKeyword;
await reloadCustomers();
});
watch(() => props.enableScan, (value) => {
if (value) {
return;
}
scannerVisible.value = false;
scanToken++;
});
watch(() => props.storeId, async () => {
if (!props.visible) {
return;
}
await reloadCustomers();
});
watch(keyword, () => {
if (!props.visible) {
return;
}
reloadByKeyword();
});
onUnmounted(() => {
reloadByKeyword.cancel();
});
</script>
<style scoped lang="less">
.customer-selector {
height: 100%;
display: flex;
flex-direction: column;
.search-wrap {
gap: .5rem;
display: flex;
z-index: 1;
padding: .5rem;
box-shadow: 0 4px 4px rgba(0, 0, 0, .05);
align-items: center;
.search {
--td-input-vertical-padding: 8px 14px;
flex: 1;
border: 1px solid var(--td-component-border);
padding: .25rem .5rem;
min-width: 0;
border-radius: .75rem;
}
.scan {
--td-button-border-radius: .75rem;
flex-shrink: 0;
}
}
.scanner-wrap {
padding: .5rem 1rem 0;
}
.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);
}
.center-tips {
color: #94a3b8;
}
}
</style>