From 4217fdb71b360321cff1c143f5bbba64b6df648d Mon Sep 17 00:00:00 2001 From: Timi Date: Sat, 29 Aug 2026 12:06:32 +0800 Subject: [PATCH] disable form for bound customer code --- src/components/BirthdatePicker.vue | 15 ++- src/layout/MainLayout.vue | 25 +++- src/pages/customer/CustomerEdit.vue | 179 ++++++++++++++++++++++++++-- src/store/navBarStore.ts | 11 ++ 4 files changed, 213 insertions(+), 17 deletions(-) diff --git a/src/components/BirthdatePicker.vue b/src/components/BirthdatePicker.vue index 1ee4505..7f3715d 100644 --- a/src/components/BirthdatePicker.vue +++ b/src/components/BirthdatePicker.vue @@ -1,18 +1,19 @@ @@ -257,6 +263,11 @@ const tabBarHeight = computed(() => { justify-content: center; color: var(--td-text-color-primary, #333); background: transparent; + + &:disabled { + color: var(--td-text-color-disabled); + cursor: not-allowed; + } } .nav-title-slot { @@ -288,6 +299,10 @@ const tabBarHeight = computed(() => { &.clickable { cursor: pointer; } + + &.disabled { + cursor: not-allowed; + } } } diff --git a/src/pages/customer/CustomerEdit.vue b/src/pages/customer/CustomerEdit.vue index 76f865c..0de3920 100644 --- a/src/pages/customer/CustomerEdit.vue +++ b/src/pages/customer/CustomerEdit.vue @@ -6,6 +6,7 @@ v-model.trim="formData.code" placeholder="客户编码" borderless + @blur="handleCodeBlur" > @@ -180,6 +206,7 @@ import CustomerAPI from "@/api/CustomerAPI"; import TagAPI from "@/api/TagAPI"; import BodyAreaSelector from "@/components/BodyAreaSelector.vue"; import CodeScanner from "@/components/CodeScanner.vue"; +import CustomerProfileCard from "@/components/CustomerProfileCard.vue"; import CustomerSelector from "@/components/CustomerSelector.vue"; import PhotoScroller from "@/components/PhotoScroller.vue"; import TagSelector from "@/components/TagSelector.vue"; @@ -219,8 +246,17 @@ const scannerVisible = ref(false); const scannerTarget = ref("code"); const scanStatus = ref("点击扫码开始识别"); const introducerCustomerId = ref(""); +const boundCodeCustomer = ref(); +const checkedCode = ref(""); +let codeCheckToken = 0; +let pendingCodeCheck: Promise | undefined; +let pendingCode = ""; +const isCustomerBound = computed(() => isCreateMode.value && !!boundCodeCustomer.value?.id); async function toggleScanner(target: ScanTarget): Promise { + if (isCustomerBound.value && target !== "code") { + return; + } if (scannerVisible.value && scannerTarget.value === target) { scannerVisible.value = false; return; @@ -240,8 +276,9 @@ async function handleDecodedText(rawText: string): Promise { if (scannerTarget.value === "code") { // 选择客户扫码 formData.code = normalizedText; - scanStatus.value = `已识别客户编码 ${normalizedText}`; scannerVisible.value = false; + await checkBoundCustomerCode(); + scanStatus.value = `已识别客户编码 ${normalizedText}`; return; } else { // 选择介绍人扫码 @@ -253,6 +290,56 @@ function handleScanStatusChange(value: string): void { scanStatus.value = value; } +async function handleCodeBlur(): Promise { + if (!isCreateMode.value) { + return; + } + await checkBoundCustomerCode(); +} + +async function checkBoundCustomerCode(): Promise { + if (!isCreateMode.value) { + return; + } + + const normalizedCode = formData.code?.trim() || ""; + if (!normalizedCode) { + codeCheckToken++; + checkedCode.value = ""; + boundCodeCustomer.value = undefined; + pendingCodeCheck = undefined; + pendingCode = ""; + return; + } + if (checkedCode.value === normalizedCode) { + return; + } + if (pendingCodeCheck && pendingCode === normalizedCode) { + await pendingCodeCheck; + return; + } + + const requestToken = ++codeCheckToken; + boundCodeCustomer.value = undefined; + pendingCode = normalizedCode; + pendingCodeCheck = (async () => { + const matched = await CustomerAPI.findByCode(normalizedCode); + if (requestToken !== codeCheckToken || normalizedCode !== (formData.code?.trim() || "")) { + return; + } + checkedCode.value = normalizedCode; + boundCodeCustomer.value = matched?.id ? matched : undefined; + })(); + try { + await pendingCodeCheck; + } finally { + if (pendingCode === normalizedCode) { + pendingCodeCheck = undefined; + pendingCode = ""; + } + } +} + // ---------- 介绍人 ---------- const selectorVisible = ref(false); @@ -291,6 +378,18 @@ async function handleIntroducerChange(id: string): Promise { const formData = reactive({ }); +watch(() => formData.code, (value) => { + if (!isCreateMode.value) { + return; + } + const normalizedCode = value?.trim() || ""; + if (normalizedCode !== checkedCode.value && normalizedCode !== pendingCode) { + codeCheckToken++; + checkedCode.value = ""; + boundCodeCustomer.value = undefined; + } +}); + // ---------- 标签 ---------- const canReadTag = computed(() => userStore.hasPermissions(corePermissionCodes.tag.read)); @@ -312,7 +411,7 @@ async function loadCustomerTags(customerId: string): Promise { } function openTagSelector(): void { - if (!canEditTag.value) { + if (!canEditTag.value || isCustomerBound.value) { return; } selectedTagIdList.value = customerTagList.value.map(item => item.id || "").filter(Boolean); @@ -339,6 +438,9 @@ const selectedAreaText = computed(() => { }); function openBodyAreaPicker(): void { + if (isCustomerBound.value) { + return; + } bodyAreaDraft.value = { selectedCodeList: [...selectedAreas.value.selectedCodeList] }; @@ -350,6 +452,10 @@ function cancelBodyAreaPicker(): void { } function confirmBodyAreaPicker(): void { + if (isCustomerBound.value) { + bodyAreaPickerVisible.value = false; + return; + } selectedAreas.value = { selectedCodeList: [...bodyAreaDraft.value.selectedCodeList] }; @@ -357,10 +463,24 @@ function confirmBodyAreaPicker(): void { } function openPhotoPicker(target: CustomerAttachmentType): void { + if (isCustomerBound.value) { + return; + } photoPickerTarget.value = target; photoInputRef.value?.click(); } +watch(isCustomerBound, (value) => { + navBarStore.setRightDisabled(value); + if (!value) { + return; + } + scannerVisible.value = false; + selectorVisible.value = false; + tagSelectorVisible.value = false; + bodyAreaPickerVisible.value = false; +}); + const canSubmit = computed(() => formData.name?.trim() && currentStoreStore.currentStoreId); async function prepareStoreScope(): Promise { @@ -408,6 +528,7 @@ onMounted(() => { navBarStore.clearRight(); if (userStore.hasPermissions(savePermissionCode.value)) { navBarStore.setRightText("保存", "var(--td-brand-color)", doSubmit); + navBarStore.setRightDisabled(isCustomerBound.value); } }); @@ -526,6 +647,9 @@ async function doSubmit(): Promise { if (isSubmitting.value) { return; } + if (!await ensureCustomerCodeAvailable()) { + return; + } if (!canSubmit.value) { if (!formData.name?.trim()) { Message.warning("请填写姓名"); @@ -576,6 +700,26 @@ async function doSubmit(): Promise { } } +async function ensureCustomerCodeAvailable(): Promise { + if (!isCreateMode.value || !formData.code?.trim()) { + return true; + } + await checkBoundCustomerCode(); + if (!boundCodeCustomer.value?.id) { + return true; + } + Message.warning("该客户编码已绑定客户,不可新录入,请编辑已有客户"); + return false; +} + +async function toBoundCustomerEdit(): Promise { + const customerId = boundCodeCustomer.value?.id; + if (!customerId) { + return; + } + await router.push(`/management/customers/${customerId}/edit`); +} + onUnmounted(() => { navBarStore.clearRight(); }); @@ -616,6 +760,21 @@ function buildAttachmentList(): Attachment[] { padding: 1rem; } + .bound-customer { + gap: .75rem; + padding: 1rem; + display: flex; + flex-direction: column; + border-radius: 1rem; + + .tips { + gap: .25rem; + display: flex; + font-size: .875rem; + align-items: center; + } + } + .textarea { width: 100%; } diff --git a/src/store/navBarStore.ts b/src/store/navBarStore.ts index c44c113..87190a2 100644 --- a/src/store/navBarStore.ts +++ b/src/store/navBarStore.ts @@ -9,6 +9,7 @@ export const useNavBarStore = defineStore("nav-bar", () => { const rightBadge = ref(); const rightText = ref(""); const rightTextColor = ref(""); + const rightDisabled = ref(false); const rightAction = shallowRef<(() => void | Promise) | undefined>(); const customTitleVisible = ref(false); const customRightVisible = ref(false); @@ -35,6 +36,7 @@ export const useNavBarStore = defineStore("nav-bar", () => { function setRightIcon(value?: string, action?: () => void | Promise): void { rightIcon.value = value || ""; rightBadge.value = undefined; + rightDisabled.value = false; rightAction.value = action; if (value) { rightText.value = ""; @@ -50,6 +52,7 @@ export const useNavBarStore = defineStore("nav-bar", () => { function setRightText(value?: string, color?: string, action?: () => void | Promise): void { rightText.value = value || ""; rightTextColor.value = color || ""; + rightDisabled.value = false; rightAction.value = action; if (value) { rightBadge.value = undefined; @@ -58,6 +61,10 @@ export const useNavBarStore = defineStore("nav-bar", () => { } } + function setRightDisabled(value: boolean): void { + rightDisabled.value = value; + } + function setCustomTitleVisible(value: boolean): void { customTitleVisible.value = value; } @@ -69,6 +76,7 @@ export const useNavBarStore = defineStore("nav-bar", () => { rightBadge.value = undefined; rightText.value = ""; rightTextColor.value = ""; + rightDisabled.value = false; rightAction.value = undefined; } } @@ -78,6 +86,7 @@ export const useNavBarStore = defineStore("nav-bar", () => { rightBadge.value = undefined; rightText.value = ""; rightTextColor.value = ""; + rightDisabled.value = false; rightAction.value = undefined; customRightVisible.value = false; } @@ -90,6 +99,7 @@ export const useNavBarStore = defineStore("nav-bar", () => { rightBadge, rightText, rightTextColor, + rightDisabled, rightAction, customTitleVisible, customRightVisible, @@ -100,6 +110,7 @@ export const useNavBarStore = defineStore("nav-bar", () => { setRightIcon, setRightBadge, setRightText, + setRightDisabled, setCustomTitleVisible, setCustomRightVisible, clearRight