batch customer event

This commit is contained in:
Timi
2026-08-03 17:28:19 +08:00
parent 337700cfa7
commit 386d5e0613
5 changed files with 362 additions and 135 deletions
@@ -8,6 +8,7 @@
readonly
borderless
placeholder="选择或扫码客户"
@click="openSelectedCustomerList"
>
<template #suffix-icon>
<div class="actions">
@@ -15,12 +16,12 @@
theme="primary"
variant="outline"
size="extra-small"
@click="selectorVisible = true"
@click.stop="selectorVisible = true"
>选择</t-button>
<t-button
theme="primary"
size="extra-small"
@click="toggleScanner"
@click.stop="toggleScanner"
>
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
</t-button>
@@ -37,11 +38,6 @@
@status-change="handleScanStatusChange"
/>
</div>
<customer-profile-card
v-if="selectedCustomer"
class="customer-profile"
:customer="selectedCustomer"
/>
<registration-event-selector
v-model="selectedRegistrationEventIdList"
class="registration-event-selector"
@@ -92,10 +88,27 @@
</t-cell-group>
<div v-else class="empty">当前客户暂无事件记录</div>
<customer-selector
v-model="selectedCustomerId"
v-model="selectedCustomerIdList"
v-model:visible="selectorVisible"
:selection-mode="isEditMode ? 'single' : 'multiple'"
@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
ref="recordPhotoInputRef"
class="file-input"
@@ -128,8 +141,8 @@ import CustomerAPI from "@/api/CustomerAPI";
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
import RegistrationEventRecordAPI from "@/api/RegistrationEventRecordAPI";
import CodeScanner from "@/components/CodeScanner.vue";
import CustomerList from "@/components/CustomerList.vue";
import CustomerSelector from "@/components/CustomerSelector.vue";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import PhotoScroller from "@/components/PhotoScroller.vue";
import RegistrationEventSelector from "@/components/RegistrationEventSelector.vue";
import { useSettingStore } from "@/store/settingStore";
@@ -150,8 +163,9 @@ const settingStore = useSettingStore();
const customerCode = ref("");
const scanCode = ref(customerCode.value);
const selectorVisible = ref(false);
const selectedCustomerId = ref("");
const selectedCustomer = ref<Customer>();
const selectedCustomerListVisible = ref(false);
const selectedCustomerIdList = ref<string[]>([]);
const selectedCustomerList = ref<Customer[]>([]);
const registrationEventList = ref<RegistrationEvent[]>([]);
const selectedRegistrationEventIdList = ref<string[]>([]);
const recentRecordList = ref<RegistrationEventRecord[]>([]);
@@ -178,8 +192,15 @@ const routeId = computed(() => String(route.params.id || ""));
const isEditMode = computed(() => !!routeId.value);
const pageAccess = computed(() => isEditMode.value ? gaoPermissionCodes.registrationEventRecord.update : gaoPermissionCodes.registrationEventRecord.create);
const saveButtonText = computed(() => isEditMode.value ? "保存记录" : "提交");
const canSubmit = computed(() => !!selectedCustomer.value?.id && 0 < selectedRegistrationEventIdList.value.length);
const customerDisplayText = computed(() => selectedCustomer.value?.name || selectedCustomer.value?.code || customerCode.value);
const selectedCustomer = computed(() => selectedCustomerList.value[0]);
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[]>({
get() {
return photoItemList.value.map(item => item.url);
@@ -215,9 +236,9 @@ async function loadRecordForEdit(): Promise<void> {
const record = await RegistrationEventRecordAPI.detail(routeId.value);
editingRecord.value = record;
appendRecordRegistrationEvent(record);
selectedCustomer.value = record.customer;
selectedCustomerList.value = record.customer ? [record.customer] : [];
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] : [];
registeredAtText.value = record.registeredAt ? Time.formatDateTime(new Date(record.registeredAt)) : "";
note.value = record.remark || "";
@@ -278,12 +299,36 @@ function handleScanStatusChange(value: string): void {
// ---------- 客户 ----------
async function handleCustomerChange(id: string): Promise<void> {
const customer = await CustomerAPI.detail(id);
selectedCustomer.value = customer;
selectedCustomerId.value = customer.id || "";
customerCode.value = customer.code || "";
scanCode.value = customer.code || "";
async function handleCustomerChange(value: string | string[]): Promise<void> {
selectedCustomerIdList.value = Array.isArray(value) ? value : [value];
await loadSelectedCustomerList();
await loadRecentRecordList();
}
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();
}
@@ -297,39 +342,32 @@ async function goRecordDetail(id: string): Promise<void> {
async function loadSelectedCustomer(): Promise<void> {
const code = customerCode.value.trim();
if (!code) {
selectedCustomer.value = undefined;
selectedCustomerId.value = "";
selectedCustomerList.value = [];
selectedCustomerIdList.value = [];
recentRecordList.value = [];
return;
}
const customer = await CustomerAPI.nullableDetailByCode(code);
if (!customer) {
selectedCustomer.value = undefined;
selectedCustomerId.value = "";
selectedCustomerList.value = [];
selectedCustomerIdList.value = [];
recentRecordList.value = [];
scanStatus.value = `客户 ${code} 不存在,请先建档。`;
return;
}
selectedCustomer.value = customer;
selectedCustomerId.value = customer.id || "";
if (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();
}
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> {
const customerId = selectedCustomer.value?.id;
if (!customerId) {
@@ -399,8 +437,7 @@ function confirmRegisteredAt(): void {
}
async function submitRecord(): Promise<void> {
const submitCustomer = await resolveSubmitCustomer();
if (!submitCustomer?.id) {
if (!validateForm()) {
return;
}
@@ -411,15 +448,18 @@ async function submitRecord(): Promise<void> {
return;
}
const customerIdList = selectedCustomerList.value.map(customer => customer.id).filter((item): item is string => !!item);
for (const registrationEventId of selectedRegistrationEventIdList.value) {
await RegistrationEventRecordAPI.create({
registrationEventId,
customerId: submitCustomer.id,
customerIdList,
remark: note.value || "客户现场登记",
registeredAt: buildRegisteredAt(),
attachmentIdList: buildTempFileIdList()
});
}
selectedCustomerList.value = [];
selectedCustomerIdList.value = [];
note.value = "";
photoItemList.value = [];
await loadRecentRecordList();
@@ -432,7 +472,7 @@ async function submitRecord(): Promise<void> {
}
function validateForm(): boolean {
if (!selectedCustomer.value?.id) {
if (!selectedCustomerList.value.some(item => !!item.id)) {
showToast("请先选择或扫码客户", "warning");
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) {
.registration-record-edit {
:deep(.form-item .t-form__label),
@@ -609,7 +671,8 @@ watch(customerCode, async (value) => {
}
.form,
.record-panel {
.record-panel,
.selected-customer-popup {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
}