From 9b8848ea8cc573f05beb789d74a6d154cac12b2f Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sun, 30 Aug 2026 11:02:58 +0800 Subject: [PATCH] feat(web-admin): gate load products by selected contract Co-authored-by: Cursor --- .../business/pl-plan-form-dialog/index.vue | 107 ++++++++++++++++++ .../business/pl-plan-load-section/index.vue | 66 ++++++++--- 2 files changed, 159 insertions(+), 14 deletions(-) diff --git a/web-admin/src/components/business/pl-plan-form-dialog/index.vue b/web-admin/src/components/business/pl-plan-form-dialog/index.vue index 568c4d54d..d94ccb11e 100644 --- a/web-admin/src/components/business/pl-plan-form-dialog/index.vue +++ b/web-admin/src/components/business/pl-plan-form-dialog/index.vue @@ -284,6 +284,7 @@ /> @@ -340,6 +341,7 @@ usePlRemoteVehicleOptions } from '@/composables/usePlRemoteOptions' import { fetchCustomerOptions } from '@/api/crm/customer' + import { getContract } from '@/api/crm/contract/index' import CustomerSelector from '@/components/CustomerSelector.vue' import ProductSelector from '@/components/business/product-selector/index.vue' import PlPlanLoadSection from '@/components/business/pl-plan-load-section/index.vue' @@ -396,6 +398,11 @@ import { useUserStore } from '@/store/modules/user' import { formatDate } from '@/utils/format' import { pickBxData } from '@/utils/http/bx' + import { isPlInternalCustomerName } from '@/utils/pl-internal-customer' + import { + collectContractProductIds, + filterProductsByAllowedProductIds + } from '@/utils/pl-plan-contract-product' import { getPersonnelAffiliationLabel } from '@/utils/pl-personnel-cert' const props = defineProps<{ @@ -504,6 +511,12 @@ const customerOptions = ref([]) const customerOptionsLoading = ref(false) const productSelectorVisible = ref(false) + /** 当前装货合同明细中的 CRM productId(非内部客户限制选品) */ + const loadContractProductIds = ref([]) + const productSelectorAllowedIds = computed(() => { + if (isPlInternalCustomerName(loadForm.value.customerName)) return undefined + return loadContractProductIds.value + }) const vehicleViewVisible = ref(false) const vehicleLibraryVisible = ref(false) const carrierLibraryVisible = ref(false) @@ -994,6 +1007,40 @@ { deep: true } ) + watch( + () => loadForm.value.contracts, + async (contracts, prev) => { + const id = String(contracts?.[0]?.contractId || '').trim() + const prevId = String(prev?.[0]?.contractId || '').trim() + if (!id) { + loadContractProductIds.value = [] + return + } + try { + const res = await getContract(id) + const detail = pickBxData<{ products?: Array<{ productId?: string | null }> }>(res) + loadContractProductIds.value = collectContractProductIds(detail?.products || []) + } catch { + loadContractProductIds.value = [] + } + // 合同 id 变化时过滤装货产品(详情首次灌入同 id / 加载中不刷) + if ( + prev && + id !== prevId && + !detailLoading.value && + !suppressStopProductSync.value + ) { + const nextProducts = filterProductsByAllowedProductIds( + loadForm.value.products || [], + loadContractProductIds.value + ) + loadForm.value = { ...loadForm.value, products: nextProducts } + syncFromLoadProducts() + } + }, + { deep: true } + ) + function openCustomerSelector(side: CustomerSide, deliveryIndex = 0) { customerSelectorSide.value = side customerSelectorDeliveryIndex.value = deliveryIndex @@ -1665,6 +1712,64 @@ return true } + function stopHasSelectedContract(contracts?: { contractId?: string }[] | null) { + return (contracts || []).some((item) => !!String(item.contractId || '').trim()) + } + + /** 非内部客户:装/送货段合同至多 1 份;已选 1 份时校验装货产品归属合同 */ + function validateSingleContractPerStop(): boolean { + if ( + !isPlInternalCustomerName(loadForm.value.customerName) && + (loadForm.value.contracts || []).length > 1 + ) { + ElMessage.warning('装货合同只能保留 1 份,请移除多余合同后再保存') + return false + } + for (let i = 0; i < deliveryForms.value.length; i += 1) { + const row = deliveryForms.value[i] + if (isPlInternalCustomerName(row.customerName)) continue + if ((row.contracts || []).length > 1) { + ElMessage.warning(`送货段#${i + 1}合同只能保留 1 份,请移除多余合同后再保存`) + return false + } + } + if ( + !isPlInternalCustomerName(loadForm.value.customerName) && + (loadForm.value.contracts || []).length === 1 && + loadContractProductIds.value.length + ) { + const allowed = new Set(loadContractProductIds.value.map((id) => String(id).trim())) + const orphan = (loadForm.value.products || []).some( + (row) => !allowed.has(String(row.productId || '').trim()) + ) + if (orphan) { + ElMessage.warning('装货产品须属于所选合同') + return false + } + } + return true + } + + /** 安排运输前:非内部客户(山峡/云栋)的装/送货段必须至少选一份合同 */ + function validateContractsForArrange() { + if ( + !isPlInternalCustomerName(loadForm.value.customerName) && + !stopHasSelectedContract(loadForm.value.contracts) + ) { + ElMessage.warning('安排运输前请为装货客户选择合同') + return false + } + for (let i = 0; i < deliveryForms.value.length; i += 1) { + const row = deliveryForms.value[i] + if (isPlInternalCustomerName(row.customerName)) continue + if (!stopHasSelectedContract(row.contracts)) { + ElMessage.warning(`安排运输前请为送货段#${i + 1}选择合同`) + return false + } + } + return true + } + /** 安排运输前必填:创建日期、计划日期、归属公司、单据类型、跟进人 */ function getArrangePrerequisiteGaps(): string[] { const missing: string[] = [] @@ -1688,6 +1793,7 @@ try { const valid = await basicFormRef.value?.validate().catch(() => false) if (!valid || !validateBusinessRules()) return + if (!validateSingleContractPerStop()) return if (mode === 'saveAndArrange' || mode === 'rearrange') { const missing = getArrangePrerequisiteGaps() @@ -1699,6 +1805,7 @@ ElMessage.warning('安排运输前请选择车辆') return } + if (!validateContractsForArrange()) return } mirrorPlanProductsFromLoad() diff --git a/web-admin/src/components/business/pl-plan-load-section/index.vue b/web-admin/src/components/business/pl-plan-load-section/index.vue index 3bf714f23..76b2cb3dc 100644 --- a/web-admin/src/components/business/pl-plan-load-section/index.vue +++ b/web-admin/src/components/business/pl-plan-load-section/index.vue @@ -4,8 +4,8 @@