batch customer event
This commit is contained in:
@@ -1,7 +1,18 @@
|
||||
<template>
|
||||
<t-popup v-model="popupVisible" placement="bottom">
|
||||
<div class="customer-selector bg-white">
|
||||
<h3 class="title">选择客户</h3>
|
||||
<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"
|
||||
@@ -11,33 +22,16 @@
|
||||
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 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 v-else class="empty-block" v-text="loading ? '正在加载客户' : '未找到匹配客户'" />
|
||||
</div>
|
||||
</t-popup>
|
||||
</template>
|
||||
@@ -45,16 +39,23 @@
|
||||
<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";
|
||||
import { getCustomerPhotoURL } from "@/utils/customer";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
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];
|
||||
"update:modelValue": [value: string | string[]];
|
||||
"update:visible": [value: boolean];
|
||||
change: [value: string];
|
||||
}>();
|
||||
@@ -68,9 +69,12 @@ 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 "正在加载更多";
|
||||
@@ -116,6 +120,11 @@ async function loadCustomers(): Promise<void> {
|
||||
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;
|
||||
@@ -128,7 +137,7 @@ async function loadCustomers(): Promise<void> {
|
||||
}
|
||||
|
||||
async function bindScrollListener(): Promise<void> {
|
||||
if (!props.visible || customerOptions.value.length <= 0) {
|
||||
if (!props.visible || 0 === customerOptions.value.length) {
|
||||
Scroller.removeListener(scrollListenerName);
|
||||
return;
|
||||
}
|
||||
@@ -140,24 +149,58 @@ async function bindScrollListener(): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
function selectCustomer(item: Customer): void {
|
||||
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;
|
||||
}
|
||||
|
||||
emit("update:modelValue", id);
|
||||
emit("change", id);
|
||||
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();
|
||||
});
|
||||
@@ -191,11 +234,22 @@ onUnmounted(() => {
|
||||
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;
|
||||
@@ -216,12 +270,6 @@ onUnmounted(() => {
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
|
||||
.cell-title {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.load-state {
|
||||
@@ -232,59 +280,13 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user