batch customer event
This commit is contained in:
@@ -41,7 +41,8 @@ async function rangeExport(req: RegistrationEventRecordRangeReq): Promise<Blob>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(req: RegistrationEventRecordCreateReq): Promise<RegistrationEventRecord> {
|
|
||||||
|
async function create(req: RegistrationEventRecordCreateReq): Promise<RegistrationEventRecord[]> {
|
||||||
return await axios.post(`${BASE_URI}/create`, req);
|
return await axios.post(`${BASE_URI}/create`, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<div class="customer-list">
|
||||||
|
<t-cell-group v-if="customerList.length">
|
||||||
|
<t-cell
|
||||||
|
v-for="(item, index) in customerList"
|
||||||
|
:key="item.id || item.code || index"
|
||||||
|
:note="item.code"
|
||||||
|
hover
|
||||||
|
@click="handleSelect(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>
|
||||||
|
<div class="left-icon">
|
||||||
|
<span v-if="selectable" class="check" :class="{selected: isSelected(item)}">
|
||||||
|
<t-icon v-if="isSelected(item)" name="check" />
|
||||||
|
</span>
|
||||||
|
<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)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="removable" #rightIcon>
|
||||||
|
<t-button theme="danger" size="extra-small" @click.stop="handleRemove(item)">移除</t-button>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
|
<div v-else class="empty-block" v-text="emptyText" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Customer } from "@/types/Customer";
|
||||||
|
import { getCustomerPhotoURL } from "@/utils/customer";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "CustomerList"
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
customerList: Customer[];
|
||||||
|
selectedIdList?: string[];
|
||||||
|
selectable?: boolean;
|
||||||
|
removable?: boolean;
|
||||||
|
emptyText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
selectedIdList: () => [],
|
||||||
|
selectable: false,
|
||||||
|
removable: false,
|
||||||
|
emptyText: "暂无客户"
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
select: [customer: Customer];
|
||||||
|
remove: [customer: Customer];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedIdSet = computed(() => new Set(props.selectedIdList));
|
||||||
|
|
||||||
|
function isSelected(customer: Customer): boolean {
|
||||||
|
return !!customer.id && selectedIdSet.value.has(customer.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(customer: Customer): void {
|
||||||
|
emit("select", customer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemove(customer: Customer): void {
|
||||||
|
emit("remove", customer);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.customer-list {
|
||||||
|
.cell-title {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-icon {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
border: 1px solid var(--td-component-border);
|
||||||
|
border-radius: 50%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--td-brand-color);
|
||||||
|
background: var(--td-brand-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-block {
|
||||||
|
color: #52607a;
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
background: #f8fbff;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.avatar .t-image__img) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) {
|
||||||
|
.customer-list {
|
||||||
|
.check {
|
||||||
|
border-color: rgba(148, 163, 184, .4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-block {
|
||||||
|
color: #e2e8f0;
|
||||||
|
background: rgba(30, 41, 59, .9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar.fallback {
|
||||||
|
color: #93c5fd;
|
||||||
|
background: rgba(30, 64, 175, .32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-popup v-model="popupVisible" placement="bottom">
|
<t-popup v-model="popupVisible" placement="bottom">
|
||||||
<div class="customer-selector bg-white">
|
<div class="customer-selector bg-white">
|
||||||
|
<div class="title-bar">
|
||||||
|
<t-button variant="text" size="small" @click="cancelSelection">取消</t-button>
|
||||||
<h3 class="title">选择客户</h3>
|
<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">
|
<div class="search-wrapper">
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="keyword"
|
v-model.trim="keyword"
|
||||||
@@ -11,33 +22,16 @@
|
|||||||
placeholder="姓名 / 编码 / 手机号"
|
placeholder="姓名 / 编码 / 手机号"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="customerOptions.length" ref="resultListRef" class="result-list">
|
<div ref="resultListRef" class="result-list">
|
||||||
<t-cell-group>
|
<customer-list
|
||||||
<t-cell
|
:customer-list="customerOptions"
|
||||||
v-for="(item, index) in customerOptions"
|
:selected-id-list="draftSelectedIdList"
|
||||||
:key="item.id || item.code || index"
|
:selectable="isMultiple"
|
||||||
:note="item.code"
|
:empty-text="loading ? '正在加载客户' : '未找到匹配客户'"
|
||||||
hover
|
@select="toggleCustomer"
|
||||||
@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 class="load-state" v-text="loadStateText" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-block" v-text="loading ? '正在加载客户' : '未找到匹配客户'" />
|
|
||||||
</div>
|
</div>
|
||||||
</t-popup>
|
</t-popup>
|
||||||
</template>
|
</template>
|
||||||
@@ -45,16 +39,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Scroller } from "timi-web";
|
import { Scroller } from "timi-web";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
|
import CustomerList from "@/components/CustomerList.vue";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import { getCustomerPhotoURL } from "@/utils/customer";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
type SelectionMode = "single" | "multiple";
|
||||||
modelValue: string;
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: string | string[];
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
}>();
|
selectionMode?: SelectionMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
selectionMode: "single"
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
"update:modelValue": [value: string];
|
"update:modelValue": [value: string | string[]];
|
||||||
"update:visible": [value: boolean];
|
"update:visible": [value: boolean];
|
||||||
change: [value: string];
|
change: [value: string];
|
||||||
}>();
|
}>();
|
||||||
@@ -68,9 +69,12 @@ const pageIndex = ref(0);
|
|||||||
const totalCount = ref(0);
|
const totalCount = ref(0);
|
||||||
const customerOptions = ref<Customer[]>([]);
|
const customerOptions = ref<Customer[]>([]);
|
||||||
const resultListRef = ref<HTMLElement>();
|
const resultListRef = ref<HTMLElement>();
|
||||||
|
const draftSelectedIdList = ref<string[]>([]);
|
||||||
|
const selectedCustomerCache = new Map<string, Customer>();
|
||||||
let searchToken = 0;
|
let searchToken = 0;
|
||||||
let keywordTimer: number | undefined;
|
let keywordTimer: number | undefined;
|
||||||
|
|
||||||
|
const isMultiple = computed(() => props.selectionMode === "multiple");
|
||||||
const loadStateText = computed(() => {
|
const loadStateText = computed(() => {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
return "正在加载更多";
|
return "正在加载更多";
|
||||||
@@ -116,6 +120,11 @@ async function loadCustomers(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const list = result.list || [];
|
const list = result.list || [];
|
||||||
|
for (const item of list) {
|
||||||
|
if (item.id) {
|
||||||
|
selectedCustomerCache.set(item.id, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
customerOptions.value = customerOptions.value.concat(list);
|
customerOptions.value = customerOptions.value.concat(list);
|
||||||
totalCount.value = result.total || 0;
|
totalCount.value = result.total || 0;
|
||||||
pageIndex.value += 1;
|
pageIndex.value += 1;
|
||||||
@@ -128,7 +137,7 @@ async function loadCustomers(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function bindScrollListener(): Promise<void> {
|
async function bindScrollListener(): Promise<void> {
|
||||||
if (!props.visible || customerOptions.value.length <= 0) {
|
if (!props.visible || 0 === customerOptions.value.length) {
|
||||||
Scroller.removeListener(scrollListenerName);
|
Scroller.removeListener(scrollListenerName);
|
||||||
return;
|
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;
|
const id = item.id;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedCustomerCache.set(id, item);
|
||||||
|
if (!isMultiple.value) {
|
||||||
emit("update:modelValue", id);
|
emit("update:modelValue", id);
|
||||||
emit("change", id);
|
emit("change", id);
|
||||||
emit("update:visible", false);
|
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) => {
|
watch(() => props.visible, async (value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
syncDraftSelection();
|
||||||
keyword.value = "";
|
keyword.value = "";
|
||||||
window.clearTimeout(keywordTimer);
|
window.clearTimeout(keywordTimer);
|
||||||
Scroller.removeListener(scrollListenerName);
|
Scroller.removeListener(scrollListenerName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
syncDraftSelection();
|
||||||
await reloadCustomers();
|
await reloadCustomers();
|
||||||
await bindScrollListener();
|
await bindScrollListener();
|
||||||
});
|
});
|
||||||
@@ -191,11 +234,22 @@ onUnmounted(() => {
|
|||||||
border-radius: 1.25rem 1.25rem 0 0;
|
border-radius: 1.25rem 1.25rem 0 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4rem 1fr 4rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title-placeholder {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.search-wrapper {
|
.search-wrapper {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: 1rem 0 .5rem 0;
|
padding: 1rem 0 .5rem 0;
|
||||||
@@ -216,12 +270,6 @@ onUnmounted(() => {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
|
|
||||||
.cell-title {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-state {
|
.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) {
|
:global(html.theme-dark) {
|
||||||
.customer-selector {
|
.customer-selector {
|
||||||
background: rgba(15, 23, 42, .96);
|
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 {
|
.load-state {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
|
||||||
|
|
||||||
&.fallback {
|
|
||||||
color: #93c5fd;
|
|
||||||
background: rgba(30, 64, 175, .32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
readonly
|
readonly
|
||||||
borderless
|
borderless
|
||||||
placeholder="选择或扫码客户"
|
placeholder="选择或扫码客户"
|
||||||
|
@click="openSelectedCustomerList"
|
||||||
>
|
>
|
||||||
<template #suffix-icon>
|
<template #suffix-icon>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@@ -15,12 +16,12 @@
|
|||||||
theme="primary"
|
theme="primary"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
@click="selectorVisible = true"
|
@click.stop="selectorVisible = true"
|
||||||
>选择</t-button>
|
>选择</t-button>
|
||||||
<t-button
|
<t-button
|
||||||
theme="primary"
|
theme="primary"
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
@click="toggleScanner"
|
@click.stop="toggleScanner"
|
||||||
>
|
>
|
||||||
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
||||||
</t-button>
|
</t-button>
|
||||||
@@ -37,11 +38,6 @@
|
|||||||
@status-change="handleScanStatusChange"
|
@status-change="handleScanStatusChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<customer-profile-card
|
|
||||||
v-if="selectedCustomer"
|
|
||||||
class="customer-profile"
|
|
||||||
:customer="selectedCustomer"
|
|
||||||
/>
|
|
||||||
<registration-event-selector
|
<registration-event-selector
|
||||||
v-model="selectedRegistrationEventIdList"
|
v-model="selectedRegistrationEventIdList"
|
||||||
class="registration-event-selector"
|
class="registration-event-selector"
|
||||||
@@ -92,10 +88,27 @@
|
|||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-else class="empty">当前客户暂无事件记录</div>
|
<div v-else class="empty">当前客户暂无事件记录</div>
|
||||||
<customer-selector
|
<customer-selector
|
||||||
v-model="selectedCustomerId"
|
v-model="selectedCustomerIdList"
|
||||||
v-model:visible="selectorVisible"
|
v-model:visible="selectorVisible"
|
||||||
|
:selection-mode="isEditMode ? 'single' : 'multiple'"
|
||||||
@change="handleCustomerChange"
|
@change="handleCustomerChange"
|
||||||
/>
|
/>
|
||||||
|
<t-popup v-model="selectedCustomerListVisible" placement="bottom">
|
||||||
|
<div class="selected-customer-popup bg-white">
|
||||||
|
<div class="popup-header">
|
||||||
|
<t-button variant="text" size="small" @click="selectedCustomerListVisible = false">关闭</t-button>
|
||||||
|
<h3>已选客户</h3>
|
||||||
|
<t-button theme="primary" variant="text" size="small" @click="openCustomerSelector">选择</t-button>
|
||||||
|
</div>
|
||||||
|
<customer-list
|
||||||
|
class="list"
|
||||||
|
:customer-list="selectedCustomerList"
|
||||||
|
:removable="!isEditMode"
|
||||||
|
empty-text="暂未选择客户"
|
||||||
|
@remove="removeSelectedCustomer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</t-popup>
|
||||||
<input
|
<input
|
||||||
ref="recordPhotoInputRef"
|
ref="recordPhotoInputRef"
|
||||||
class="file-input"
|
class="file-input"
|
||||||
@@ -128,8 +141,8 @@ import CustomerAPI from "@/api/CustomerAPI";
|
|||||||
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
|
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
|
||||||
import RegistrationEventRecordAPI from "@/api/RegistrationEventRecordAPI";
|
import RegistrationEventRecordAPI from "@/api/RegistrationEventRecordAPI";
|
||||||
import CodeScanner from "@/components/CodeScanner.vue";
|
import CodeScanner from "@/components/CodeScanner.vue";
|
||||||
|
import CustomerList from "@/components/CustomerList.vue";
|
||||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
|
||||||
import PhotoScroller from "@/components/PhotoScroller.vue";
|
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||||
import RegistrationEventSelector from "@/components/RegistrationEventSelector.vue";
|
import RegistrationEventSelector from "@/components/RegistrationEventSelector.vue";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
@@ -150,8 +163,9 @@ const settingStore = useSettingStore();
|
|||||||
const customerCode = ref("");
|
const customerCode = ref("");
|
||||||
const scanCode = ref(customerCode.value);
|
const scanCode = ref(customerCode.value);
|
||||||
const selectorVisible = ref(false);
|
const selectorVisible = ref(false);
|
||||||
const selectedCustomerId = ref("");
|
const selectedCustomerListVisible = ref(false);
|
||||||
const selectedCustomer = ref<Customer>();
|
const selectedCustomerIdList = ref<string[]>([]);
|
||||||
|
const selectedCustomerList = ref<Customer[]>([]);
|
||||||
const registrationEventList = ref<RegistrationEvent[]>([]);
|
const registrationEventList = ref<RegistrationEvent[]>([]);
|
||||||
const selectedRegistrationEventIdList = ref<string[]>([]);
|
const selectedRegistrationEventIdList = ref<string[]>([]);
|
||||||
const recentRecordList = ref<RegistrationEventRecord[]>([]);
|
const recentRecordList = ref<RegistrationEventRecord[]>([]);
|
||||||
@@ -178,8 +192,15 @@ const routeId = computed(() => String(route.params.id || ""));
|
|||||||
const isEditMode = computed(() => !!routeId.value);
|
const isEditMode = computed(() => !!routeId.value);
|
||||||
const pageAccess = computed(() => isEditMode.value ? gaoPermissionCodes.registrationEventRecord.update : gaoPermissionCodes.registrationEventRecord.create);
|
const pageAccess = computed(() => isEditMode.value ? gaoPermissionCodes.registrationEventRecord.update : gaoPermissionCodes.registrationEventRecord.create);
|
||||||
const saveButtonText = computed(() => isEditMode.value ? "保存记录" : "提交");
|
const saveButtonText = computed(() => isEditMode.value ? "保存记录" : "提交");
|
||||||
const canSubmit = computed(() => !!selectedCustomer.value?.id && 0 < selectedRegistrationEventIdList.value.length);
|
const selectedCustomer = computed(() => selectedCustomerList.value[0]);
|
||||||
const customerDisplayText = computed(() => selectedCustomer.value?.name || selectedCustomer.value?.code || customerCode.value);
|
const canSubmit = computed(() => selectedCustomerList.value.some(item => !!item.id) && 0 < selectedRegistrationEventIdList.value.length);
|
||||||
|
const customerDisplayText = computed(() => {
|
||||||
|
if (1 < selectedCustomerList.value.length) {
|
||||||
|
return `已选择 ${selectedCustomerList.value.length} 位客户`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedCustomer.value?.name || selectedCustomer.value?.code || customerCode.value;
|
||||||
|
});
|
||||||
const photoList = computed<string[]>({
|
const photoList = computed<string[]>({
|
||||||
get() {
|
get() {
|
||||||
return photoItemList.value.map(item => item.url);
|
return photoItemList.value.map(item => item.url);
|
||||||
@@ -215,9 +236,9 @@ async function loadRecordForEdit(): Promise<void> {
|
|||||||
const record = await RegistrationEventRecordAPI.detail(routeId.value);
|
const record = await RegistrationEventRecordAPI.detail(routeId.value);
|
||||||
editingRecord.value = record;
|
editingRecord.value = record;
|
||||||
appendRecordRegistrationEvent(record);
|
appendRecordRegistrationEvent(record);
|
||||||
selectedCustomer.value = record.customer;
|
selectedCustomerList.value = record.customer ? [record.customer] : [];
|
||||||
customerCode.value = record.customer?.code || record.customerId || "";
|
customerCode.value = record.customer?.code || record.customerId || "";
|
||||||
selectedCustomerId.value = record.customer?.id || "";
|
selectedCustomerIdList.value = record.customer?.id ? [record.customer.id] : [];
|
||||||
selectedRegistrationEventIdList.value = record.registrationEventId ? [record.registrationEventId] : [];
|
selectedRegistrationEventIdList.value = record.registrationEventId ? [record.registrationEventId] : [];
|
||||||
registeredAtText.value = record.registeredAt ? Time.formatDateTime(new Date(record.registeredAt)) : "";
|
registeredAtText.value = record.registeredAt ? Time.formatDateTime(new Date(record.registeredAt)) : "";
|
||||||
note.value = record.remark || "";
|
note.value = record.remark || "";
|
||||||
@@ -278,12 +299,36 @@ function handleScanStatusChange(value: string): void {
|
|||||||
|
|
||||||
// ---------- 客户 ----------
|
// ---------- 客户 ----------
|
||||||
|
|
||||||
async function handleCustomerChange(id: string): Promise<void> {
|
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
||||||
const customer = await CustomerAPI.detail(id);
|
selectedCustomerIdList.value = Array.isArray(value) ? value : [value];
|
||||||
selectedCustomer.value = customer;
|
await loadSelectedCustomerList();
|
||||||
selectedCustomerId.value = customer.id || "";
|
await loadRecentRecordList();
|
||||||
customerCode.value = customer.code || "";
|
}
|
||||||
scanCode.value = customer.code || "";
|
|
||||||
|
async function loadSelectedCustomerList(): Promise<void> {
|
||||||
|
const customerList = await Promise.all(selectedCustomerIdList.value.map(id => CustomerAPI.detail(id)));
|
||||||
|
selectedCustomerList.value = customerList.filter(item => !!item.id);
|
||||||
|
const firstCustomer = selectedCustomer.value;
|
||||||
|
customerCode.value = firstCustomer?.code || "";
|
||||||
|
scanCode.value = firstCustomer?.code || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSelectedCustomerList(): void {
|
||||||
|
selectedCustomerListVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openCustomerSelector(): Promise<void> {
|
||||||
|
selectedCustomerListVisible.value = false;
|
||||||
|
await nextTick();
|
||||||
|
selectorVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSelectedCustomer(customer: Customer): Promise<void> {
|
||||||
|
if (!customer.id || isEditMode.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedCustomerIdList.value = selectedCustomerIdList.value.filter(id => id !== customer.id);
|
||||||
|
selectedCustomerList.value = selectedCustomerList.value.filter(item => item.id !== customer.id);
|
||||||
await loadRecentRecordList();
|
await loadRecentRecordList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,39 +342,32 @@ async function goRecordDetail(id: string): Promise<void> {
|
|||||||
async function loadSelectedCustomer(): Promise<void> {
|
async function loadSelectedCustomer(): Promise<void> {
|
||||||
const code = customerCode.value.trim();
|
const code = customerCode.value.trim();
|
||||||
if (!code) {
|
if (!code) {
|
||||||
selectedCustomer.value = undefined;
|
selectedCustomerList.value = [];
|
||||||
selectedCustomerId.value = "";
|
selectedCustomerIdList.value = [];
|
||||||
recentRecordList.value = [];
|
recentRecordList.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customer = await CustomerAPI.nullableDetailByCode(code);
|
const customer = await CustomerAPI.nullableDetailByCode(code);
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
selectedCustomer.value = undefined;
|
selectedCustomerList.value = [];
|
||||||
selectedCustomerId.value = "";
|
selectedCustomerIdList.value = [];
|
||||||
recentRecordList.value = [];
|
recentRecordList.value = [];
|
||||||
scanStatus.value = `客户 ${code} 不存在,请先建档。`;
|
scanStatus.value = `客户 ${code} 不存在,请先建档。`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedCustomer.value = customer;
|
if (customer.id) {
|
||||||
selectedCustomerId.value = customer.id || "";
|
selectedCustomerIdList.value = isEditMode.value
|
||||||
|
? [customer.id]
|
||||||
|
: [...new Set([...selectedCustomerIdList.value, customer.id])];
|
||||||
|
selectedCustomerList.value = isEditMode.value
|
||||||
|
? [customer]
|
||||||
|
: [...selectedCustomerList.value.filter(item => item.id !== customer.id), customer];
|
||||||
|
}
|
||||||
await loadRecentRecordList();
|
await loadRecentRecordList();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveSubmitCustomer(): Promise<Customer | undefined> {
|
|
||||||
if (!validateForm()) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCustomer.value?.id) {
|
|
||||||
return selectedCustomer.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast("请先选择或扫码已有客户", "warning");
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRecentRecordList(): Promise<void> {
|
async function loadRecentRecordList(): Promise<void> {
|
||||||
const customerId = selectedCustomer.value?.id;
|
const customerId = selectedCustomer.value?.id;
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
@@ -399,8 +437,7 @@ function confirmRegisteredAt(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitRecord(): Promise<void> {
|
async function submitRecord(): Promise<void> {
|
||||||
const submitCustomer = await resolveSubmitCustomer();
|
if (!validateForm()) {
|
||||||
if (!submitCustomer?.id) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,15 +448,18 @@ async function submitRecord(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customerIdList = selectedCustomerList.value.map(customer => customer.id).filter((item): item is string => !!item);
|
||||||
for (const registrationEventId of selectedRegistrationEventIdList.value) {
|
for (const registrationEventId of selectedRegistrationEventIdList.value) {
|
||||||
await RegistrationEventRecordAPI.create({
|
await RegistrationEventRecordAPI.create({
|
||||||
registrationEventId,
|
registrationEventId,
|
||||||
customerId: submitCustomer.id,
|
customerIdList,
|
||||||
remark: note.value || "客户现场登记",
|
remark: note.value || "客户现场登记",
|
||||||
registeredAt: buildRegisteredAt(),
|
registeredAt: buildRegisteredAt(),
|
||||||
attachmentIdList: buildTempFileIdList()
|
attachmentIdList: buildTempFileIdList()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
selectedCustomerList.value = [];
|
||||||
|
selectedCustomerIdList.value = [];
|
||||||
note.value = "";
|
note.value = "";
|
||||||
photoItemList.value = [];
|
photoItemList.value = [];
|
||||||
await loadRecentRecordList();
|
await loadRecentRecordList();
|
||||||
@@ -432,7 +472,7 @@ async function submitRecord(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
if (!selectedCustomer.value?.id) {
|
if (!selectedCustomerList.value.some(item => !!item.id)) {
|
||||||
showToast("请先选择或扫码客户", "warning");
|
showToast("请先选择或扫码客户", "warning");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -600,6 +640,28 @@ watch(customerCode, async (value) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-customer-popup {
|
||||||
|
height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-radius: 1.25rem 1.25rem 0 0;
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
.registration-record-edit {
|
.registration-record-edit {
|
||||||
:deep(.form-item .t-form__label),
|
:deep(.form-item .t-form__label),
|
||||||
@@ -609,7 +671,8 @@ watch(customerCode, async (value) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form,
|
.form,
|
||||||
.record-panel {
|
.record-panel,
|
||||||
|
.selected-customer-popup {
|
||||||
background: rgba(15, 23, 42, .88);
|
background: rgba(15, 23, 42, .88);
|
||||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,7 @@ export type RegistrationEventRecordCalendarView = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type RegistrationEventRecordCreateReq = {
|
export type RegistrationEventRecordCreateReq = {
|
||||||
customerId?: string;
|
customerIdList: string[];
|
||||||
customerCode?: string;
|
|
||||||
registrationEventId: string;
|
registrationEventId: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
registeredAt?: number;
|
registeredAt?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user