diff --git a/web-admin/src/views/m/pl/plan-cert/archive-form.vue b/web-admin/src/views/m/pl/plan-cert/archive-form.vue index c4b4ddac7..28bc10096 100644 --- a/web-admin/src/views/m/pl/plan-cert/archive-form.vue +++ b/web-admin/src/views/m/pl/plan-cert/archive-form.vue @@ -610,6 +610,7 @@ } from '@/views/pl/quota-cert/model' import { bindingsFromPurchaseCerts, + exactCategoryMatches, exactCustomerMatches, matchPurchaseCert, scopeProductsFromLines @@ -1064,6 +1065,24 @@ if (current === epoch) ElMessage.warning('客户匹配失败,请手工选择') } } + + async function matchCertCategory(productName: string, current: number) { + const name = productName.trim() + if (!name || form.value.categoryId || edited.has('categoryId')) return + if (!categoryOptions.value.length) await loadCategories() + if (current !== epoch || form.value.categoryId || edited.has('categoryId')) return + const matches = exactCategoryMatches(categoryOptions.value, name) + if (matches.length === 1) { + form.value.categoryId = matches[0].id + touch('categoryId') + return + } + ElMessage.warning( + matches.length + ? '有多个品类与证面品名同名,请手工选择证用品类' + : '未找到与证面品名完全一致的证用品类,请手工选择' + ) + } function toContractOption(row: ContractVO): ContractOption { return { id: String(row.id), @@ -1524,7 +1543,9 @@ if (current !== epoch || kind.value !== targetKind || recognitionImage.value !== image) return if (targetKind === 'purchase') { const buyer = String(form.value.buyerName || '').trim() + const productName = String(form.value.productNameOnCert || '').trim() if (buyer) await matchBuyerCustomer(buyer, current) + if (productName) await matchCertCategory(productName, current) } if (targetKind === 'transport') await linkRecognizedPurchaseCerts(current, targetKind, image) if (current !== epoch || kind.value !== targetKind || recognitionImage.value !== image) return diff --git a/web-admin/src/views/pl/quota-cert/contract-binder-rules.test.ts b/web-admin/src/views/pl/quota-cert/contract-binder-rules.test.ts index 4ba81480d..740371b56 100644 --- a/web-admin/src/views/pl/quota-cert/contract-binder-rules.test.ts +++ b/web-admin/src/views/pl/quota-cert/contract-binder-rules.test.ts @@ -3,6 +3,7 @@ import { test } from 'node:test' import { bindingsFromPurchaseCerts, contractsForCustomer, + exactCategoryMatches, exactCustomerMatches, matchPurchaseCert, scopeProductsFromLines @@ -22,6 +23,19 @@ test('购买单位只命中名称完全一致的客户', () => { assert.equal(exactCustomerMatches(customers, '腾龙源').length, 0) }) +test('证面品名只命中名称完全一致的证用品类', () => { + const categories = [ + { id: 'acid98', categoryName: '98% 工业硫酸' }, + { id: 'acid', categoryName: ' 硫酸 ' }, + { id: 'other', categoryName: '盐酸' } + ] + assert.deepEqual( + exactCategoryMatches(categories, '硫酸').map((item) => item.id), + ['acid'] + ) + assert.deepEqual(exactCategoryMatches(categories, ' '), []) + assert.equal(exactCategoryMatches(categories, '工业硫酸').length, 0) +}) test('选定客户后只保留该客户的合同', () => { const contracts = [ { id: 'a', customerId: 'c1', customerName: '安庆市腾龙源科技有限公司' }, diff --git a/web-admin/src/views/pl/quota-cert/contract-binder-rules.ts b/web-admin/src/views/pl/quota-cert/contract-binder-rules.ts index d0e8c8f2f..c7b3c65dc 100644 --- a/web-admin/src/views/pl/quota-cert/contract-binder-rules.ts +++ b/web-admin/src/views/pl/quota-cert/contract-binder-rules.ts @@ -22,6 +22,19 @@ export function exactCustomerMatches( return customers.filter((item) => (item.customerName || '').trim() === name) } +export interface NamedCategory { + categoryName?: string | null +} + +/** 证用品类名与证面品名去首尾空白后完全一致才算命中。 */ +export function exactCategoryMatches( + categories: T[], + productNameOnCert: string +): T[] { + const name = productNameOnCert.trim() + if (!name) return [] + return categories.filter((item) => (item.categoryName || '').trim() === name) +} export interface NamedContract { customerId?: string | null customerName?: string | null diff --git a/web-admin/src/views/pl/quota-cert/index.vue b/web-admin/src/views/pl/quota-cert/index.vue index abd62518d..18adc9a87 100644 --- a/web-admin/src/views/pl/quota-cert/index.vue +++ b/web-admin/src/views/pl/quota-cert/index.vue @@ -669,7 +669,12 @@ import { fetchCustomerOptions } from '@/api/crm/customer' import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue' import CertContractBinder from './CertContractBinder.vue' - import { bindingsFromPurchaseCerts, matchPurchaseCert } from './contract-binder-rules' + import { + bindingsFromPurchaseCerts, + exactCategoryMatches, + exactCustomerMatches, + matchPurchaseCert + } from './contract-binder-rules' import { buyPartyName, certKindLabel, @@ -788,6 +793,63 @@ } } + function extractCustomerRows(result: unknown): Array<{ id: string; customerName: string }> { + const list = Array.isArray(result) + ? result + : (result as { list?: unknown[]; data?: unknown[] })?.list || + (result as { data?: unknown[] })?.data || + [] + return (list || []) + .map((row: any) => ({ + id: String(row.id || row.customerId || ''), + customerName: String(row.customerName || row.name || '') + })) + .filter((row: { id: string }) => row.id) + } + + /** 解析后回填【客户与品类】系统客户:名称完全一致且唯一才选中。 */ + async function matchSystemCustomer(name: string, protectedFields: Set) { + const buyer = name.trim() + if (!buyer || form.value.customerId || protectedFields.has('customerId')) return + try { + const result = await fetchCustomerOptions({ keyword: buyer, limit: 200 }) + if (form.value.customerId || protectedFields.has('customerId')) return + const matches = exactCustomerMatches(extractCustomerRows(result), buyer) + if (matches.length === 1) { + await ensureCustomerOption(matches[0].id, matches[0].customerName || buyer) + form.value.customerId = matches[0].id + edited.add('customerId') + return + } + ElMessage.warning( + matches.length + ? '有多个客户与购买单位同名,请手工选择系统客户' + : '未找到与购买单位完全一致的客户,请手工选择系统客户' + ) + } catch { + ElMessage.warning('系统客户匹配失败,请手工选择') + } + } + + /** 解析后回填证用品类:证面品名与品类名完全一致且唯一才选中。 */ + async function matchCertCategory(productName: string, protectedFields: Set) { + const name = productName.trim() + if (!name || form.value.categoryId || protectedFields.has('categoryId')) return + if (!categoryOptions.value.length) await loadCategories() + if (form.value.categoryId || protectedFields.has('categoryId')) return + const matches = exactCategoryMatches(categoryOptions.value, name) + if (matches.length === 1) { + form.value.categoryId = matches[0].id + edited.add('categoryId') + return + } + ElMessage.warning( + matches.length + ? '有多个品类与证面品名同名,请手工选择证用品类' + : '未找到与证面品名完全一致的证用品类,请手工选择' + ) + } + function onCustomerChange() { edited.add('customerId') } @@ -1561,9 +1623,14 @@ form.value.referencesEditable === false ? new Set([...edited, 'purchaseRefs']) : edited mergeCandidates(form.value as any, result.candidates, protectedFields) if (!protectedFields.has('totalQty')) form.value.totalQty = inputQty(form.value.totalQty) - if (targetKind === 'purchase' && form.value.bindingsEditable !== false) { + if (targetKind === 'purchase') { const buyer = String(form.value.buyerName || '').trim() - if (buyer) await purchaseBinder.value?.matchBuyerCustomer(buyer) + const productName = String(form.value.productNameOnCert || '').trim() + if (buyer) await matchSystemCustomer(buyer, protectedFields) + if (productName) await matchCertCategory(productName, protectedFields) + if (form.value.bindingsEditable !== false && buyer) { + await purchaseBinder.value?.matchBuyerCustomer(buyer) + } } if (targetKind === 'transport') await linkRecognizedPurchaseCerts(session, targetKind, targetImage)