From 68617335f06697f5d47fae6fb1f8a7b77013ea80 Mon Sep 17 00:00:00 2001 From: lzq <357099073@qq.com> Date: Tue, 3 Nov 2026 17:48:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contract/components/QuickCreateParty.vue | 1 + backend/src/views/apps/crm/customer/index.vue | 124 +++++- .../crm/project/components/detail_contact.vue | 16 +- .../apps/crm/project/components/edit.vue | 386 +++++++++++++----- backend/src/views/apps/crm/project/index.vue | 9 +- backend/src/views/apps/crm/supplier/index.vue | 127 +++++- go/controllers/backend_crm_project.go | 82 ++-- .../alter_crm_project_customer_ref_type.sql | 33 ++ go/models/tenant_crm_project.go | 28 +- 9 files changed, 645 insertions(+), 161 deletions(-) create mode 100644 go/docs/sql/alter_crm_project_customer_ref_type.sql diff --git a/backend/src/views/apps/crm/contract/components/QuickCreateParty.vue b/backend/src/views/apps/crm/contract/components/QuickCreateParty.vue index 2af85e9..ed14aaa 100644 --- a/backend/src/views/apps/crm/contract/components/QuickCreateParty.vue +++ b/backend/src/views/apps/crm/contract/components/QuickCreateParty.vue @@ -166,6 +166,7 @@ async function handleSubmit() { emit("created", { ...row, id: createdID || null, + ref_type: Number(props.refType) === 2 ? 2 : 1, // 1=客户库 2=供应商库,调用方据此回填关联类型 label: form.name, // 一并回传联系人信息,供调用方自动带出对接人 / 电话 contact_person: form.contact_person, diff --git a/backend/src/views/apps/crm/customer/index.vue b/backend/src/views/apps/crm/customer/index.vue index 98354ea..cbcbf23 100644 --- a/backend/src/views/apps/crm/customer/index.vue +++ b/backend/src/views/apps/crm/customer/index.vue @@ -7,14 +7,6 @@
刷新 - - 移入公海 - 新增客户 智能添加
@@ -69,6 +61,26 @@ + +
+ 已选 {{ multipleSelection.length }} 项 + + 批量启用 + + + 批量禁用 + + + 批量转供应商 + + + 批量移入公海 + + + 批量删除 + +
+
action(row))); + const failed = results.filter((item) => item.status === "rejected"); + const okCount = rows.length - failed.length; + if (failed.length === 0) { + ElMessage.success(`${actionText}成功(${okCount} 条)`); + } else if (okCount === 0) { + ElMessage.error(`${actionText}失败:${failed[0].reason?.message || "请稍后重试"}`); + } else { + ElMessage.warning(`${actionText}完成:成功 ${okCount} 条,失败 ${failed.length} 条`); + } + multipleSelection.value = []; + fetchList(); + } finally { + batchLoading.value = false; + } +} + +/** 批量启用 / 禁用 */ +async function handleBatchToggleStatus(status) { + const rows = multipleSelection.value; + if (!rows.length) return; + const action = status === "1" ? "启用" : "禁用"; + try { + await ElMessageBox.confirm(`确定要${action}选中的 ${rows.length} 个客户吗?`, `批量${action}`, { + type: "warning", + }); + } catch (e) { + return; // 取消 + } + await runBatch(rows, (row) => toggleCrmCustomerStatus(row.id, status), `批量${action}`); +} + +/** 批量删除(软删,不可恢复) */ +async function handleBatchDelete() { + const rows = multipleSelection.value; + if (!rows.length) return; + try { + await ElMessageBox.confirm( + `确定要删除选中的 ${rows.length} 个客户吗?删除后不可恢复。`, + "批量删除", + { type: "warning" } + ); + } catch (e) { + return; // 取消 + } + await runBatch(rows, (row) => deleteCrmCustomer(row.id), "批量删除"); +} + +/** 批量转供应商:同名企业后端只补同步联系人,原客户记录保留 */ +async function handleBatchConvertSupplier() { + const rows = multipleSelection.value; + if (!rows.length) return; + try { + await ElMessageBox.confirm( + `将选中的 ${rows.length} 个客户的企业资料与联系人复制到供应商库?原客户记录会保留。`, + "批量转供应商", + { type: "warning" } + ); + } catch (e) { + return; // 取消 + } + await runBatch(rows, (row) => convertCustomerToSupplier(row.id), "批量转供应商"); +} + function openCreate() { currentEditData.value = null; editVisible.value = true; @@ -630,6 +719,25 @@ async function submitMovePool() { border: 1px solid var(--el-border-color-lighter); } +/* 批量操作栏:勾选行后出现 */ +.batch-bar { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 12px; + padding: 10px 16px; + border-radius: 6px; + border: 1px solid var(--el-color-primary-light-7); + background: var(--el-color-primary-light-9); + + .batch-tip { + margin-right: 8px; + font-size: 13px; + color: var(--el-text-color-regular); + } +} + .name-link { cursor: pointer; font-weight: 500; diff --git a/backend/src/views/apps/crm/project/components/detail_contact.vue b/backend/src/views/apps/crm/project/components/detail_contact.vue index 773e2af..abe3295 100644 --- a/backend/src/views/apps/crm/project/components/detail_contact.vue +++ b/backend/src/views/apps/crm/project/components/detail_contact.vue @@ -188,6 +188,12 @@ const downCompany = computed(() => { if (pt.value === 1 || !p?.supplier_id) return null; return { type: Number(p.supplier_ref_type) === 1 ? "customer" : "supplier", id: p.supplier_id }; }); +// 上游主体的联系人来源库:customer_ref_type 2=供应商库,其余(含历史数据)按客户库 +const upCompany = computed(() => { + const p = props.project; + if (!p?.customer_id) return null; + return { type: Number(p.customer_ref_type) === 2 ? "supplier" : "customer", id: p.customer_id }; +}); /** 解析项目上的联系人ID集合(JSON 数组字符串 / 数组) */ function parseIds(raw) { @@ -238,7 +244,7 @@ async function reload() { const p = props.project; pickedUp.value = parseIds(p?.upstream_contact_ids); pickedDown.value = parseIds(p?.downstream_contact_ids); - if (!p?.customer_id && !downCompany.value) { + if (!upCompany.value && !downCompany.value) { upstreamContacts.value = []; downstreamContacts.value = []; emit("count", 0); @@ -246,9 +252,9 @@ async function reload() { } loading.value = true; const jobs = []; - if (p?.customer_id) { + if (upCompany.value) { jobs.push( - loadCompanyContacts("customer", p.customer_id).then((list) => { + loadCompanyContacts(upCompany.value.type, upCompany.value.id).then((list) => { upstreamContacts.value = list; }) ); @@ -306,9 +312,7 @@ async function openPicker(groupKey) { await syncPickerSelection(); return; } - const company = isUp - ? { type: "customer", id: props.project?.customer_id } - : downCompany.value; + const company = isUp ? upCompany.value : downCompany.value; if (!company?.id) return; picker.loading = true; picker.list = await loadCompanyContacts(company.type, company.id); diff --git a/backend/src/views/apps/crm/project/components/edit.vue b/backend/src/views/apps/crm/project/components/edit.vue index 761146e..6a98cc4 100644 --- a/backend/src/views/apps/crm/project/components/edit.vue +++ b/backend/src/views/apps/crm/project/components/edit.vue @@ -75,25 +75,44 @@ {{ hasCounterparty ? "上游信息" : "客户信息" }} - -
- +
+ + - - - - - - + 客户 + 供应商 + +
+ + + {{ c.customer_name }} + {{ customerMeta(c) }} + + + + + + +
@@ -106,14 +125,14 @@ - + @@ -128,7 +147,7 @@ - + @@ -139,33 +158,42 @@ -
- + + - - - - - - + 客户 + 供应商 + +
+ + + + + + + +
@@ -231,21 +259,21 @@ --> - + - + @@ -295,6 +323,8 @@ const defaultForm = () => ({ project_type: 1, customer_id: "", customer_name: "", + // 上游主体来源库:1=客户库(默认) 2=供应商库 + customer_ref_type: 1, supplier_id: 0, supplier_ref_type: 0, supplier_name: "", @@ -356,7 +386,14 @@ const profitAmount = computed( watch( () => form.project_type, (val) => { - if (Number(val) === 2 || Number(val) === 3) return; + if (Number(val) === 2 || Number(val) === 3) { + // 未选对方主体时按项目类型切默认分类:上下游→供应商,上中游→客户 + // 候选留到下拉/失焦时再检索,避免与打开弹窗时的回查互相覆盖 + if (!Number(form.supplier_id)) { + counterpartyCategory.value = defaultCounterpartyCategory(val); + } + return; + } form.supplier_id = 0; form.supplier_ref_type = 0; form.supplier_name = ""; @@ -441,6 +478,14 @@ watch( Object.assign(form, defaultForm(), props.editData); form.status = Number(props.editData.status) || 1; form.project_type = Number(props.editData.project_type) || 1; + // 上游主体来源库:历史数据没有该字段,统一按客户库处理 + form.customer_ref_type = Number(props.editData.customer_ref_type) === 2 ? 2 : 1; + customerCategory.value = form.customer_ref_type === 2 ? "supplier" : "customer"; + // 对方主体分类:优先按已关联的库类型回显(1客户 / 2供应商),未关联时按项目类型给默认分类 + syncCounterpartyCategory(form.supplier_ref_type); + if (!Number(form.supplier_ref_type)) { + counterpartyCategory.value = defaultCounterpartyCategory(form.project_type); + } // 存量项目兼容:只有旧「项目金额 / 对接人」时回填到上游侧 if (!Number(form.upstream_amount) && Number(props.editData.amount)) { form.upstream_amount = props.editData.amount; @@ -462,12 +507,20 @@ watch( Object.assign(form, defaultForm()); form.owner_user_id = authStore.user?.id ? String(authStore.user.id) : ""; form.owner_user_name = authStore.user?.name || ""; + // 新增时上游默认客户库,对方主体按项目类型给默认分类(上下游→供应商,上中游→客户) + customerCategory.value = "customer"; + counterpartyCategory.value = defaultCounterpartyCategory(form.project_type); } loadUsers(); searchCustomers(form.customer_name || ""); // 对接人候选随所选企业加载(编辑回显时也需带上,赋值未变化时不触发 watch) loadUpstreamContacts(); loadDownstreamContacts(); + // 存量项目可能只存了下游主体名称、没有关联库记录:按名称回查一次,命中同名企业则补上关联, + // 否则下游对接人无法带出联系人(详情页的下游联系人也需要库关联) + if (hasCounterparty.value && !Number(form.supplier_id) && String(form.supplier_name || "").trim()) { + applySupplier(null); + } } } ); @@ -484,95 +537,182 @@ async function loadUsers() { } } -async function searchCustomers(keyword) { +/** 上游主体下拉副文案:联系人 · 联系方式(电话/邮箱),方便区分同名企业 */ +function customerMeta(c) { + return [c.contact_person, c.contact_phone || c.contact_email].filter(Boolean).join(" · "); +} + +/** 上游主体分类:customer=客户库 / supplier=供应商库(Tab 切换) */ +const customerCategory = ref("customer"); +const customerCategoryLabel = computed(() => + customerCategory.value === "supplier" ? "供应商" : "客户" +); + +// 检索序号:快速输入时丢弃过期响应,避免旧结果覆盖最新结果 +let customerQuerySeq = 0; + +/** 按分类 + 关键字请求企业库(上游主体来源:客户库 / 供应商库) */ +async function fetchUpstreamParties(keyword, category) { + const kw = String(keyword || "").trim(); + const isSupplier = category === "supplier"; + const res = isSupplier + ? await getCrmSupplierList({ page: 1, pageSize: 20, keyword: kw }) + : await getCrmCustomerList({ page: 1, pageSize: 20, keyword: kw }); + return (res?.data?.list || []).map((row) => ({ + id: row.id, + ref_type: isSupplier ? 2 : 1, + customer_name: (isSupplier ? row.supplier_name : row.customer_name) || "", + contact_person: row.contact_person || "", + contact_phone: row.contact_phone || "", + contact_email: row.contact_email || "", + })); +} + +/** 上游主体候选:随输入实时远程检索当前分类对应的企业库 */ +async function searchCustomers(keyword, category = customerCategory.value) { customerKeyword.value = String(keyword || ""); + const seq = ++customerQuerySeq; customerLoading.value = true; try { - const res = await getCrmCustomerList({ page: 1, pageSize: 20, keyword: keyword || "" }); - const data = res?.data || {}; - customerOptions.value = (data.list || []).map((c) => ({ id: c.id, customer_name: c.customer_name })); + const list = await fetchUpstreamParties(keyword, category); + if (seq !== customerQuerySeq) return; // 过期结果:已有更新的检索在路上 + if (category === customerCategory.value) customerOptions.value = list; } catch (e) { - customerOptions.value = []; + if (seq === customerQuerySeq && category === customerCategory.value) customerOptions.value = []; } finally { - customerLoading.value = false; + if (seq === customerQuerySeq) customerLoading.value = false; } } -/** 选中客户后同步名称;清空时一并清掉,避免把旧名称提交到后端 */ +/** 切换分类:按当前名称重新检索对应库(已选主体保持不变) */ +function onCustomerCategoryChange() { + searchCustomers(form.customer_name || ""); +} + +/** 选中上游主体后同步名称与来源库;清空时一并清掉,避免把旧名称提交到后端 */ function onCustomerPick(id) { const hit = customerOptions.value.find((c) => Number(c.id) === Number(id)); form.customer_name = hit ? hit.customer_name : ""; + form.customer_ref_type = hit ? Number(hit.ref_type) || 1 : 0; } -/* --------------------------- 快速新建客户 --------------------------- */ +/* --------------------------- 快速新建上游主体 --------------------------- */ function openCustomerCreate() { customerCreateVisible.value = true; } /** - * 快速新建客户成功:刷新候选并自动选中新客户, + * 快速新建成功:刷新候选并自动选中新企业, * 同时按新建时填写的信息回填对接人与联系电话(对接人候选随 customer_id 自动加载)。 + * 新建落在哪个库由当前分类决定(客户库 / 供应商库),据此记录上游来源。 */ async function onCustomerCreated(row) { if (!row?.id) return; - const name = row.customer_name || row.label || ""; - await searchCustomers(""); + const name = row.customer_name || row.supplier_name || row.label || ""; + const refType = Number(row.ref_type) || (customerCategory.value === "supplier" ? 2 : 1); + customerCategory.value = refType === 2 ? "supplier" : "customer"; + await searchCustomers(name); if (!customerOptions.value.some((c) => Number(c.id) === Number(row.id))) { - customerOptions.value.unshift({ id: row.id, customer_name: name }); + customerOptions.value.unshift({ + id: row.id, + ref_type: refType, + customer_name: name, + contact_person: row.contact_person || "", + contact_phone: row.contact_phone || "", + contact_email: row.contact_email || "", + }); } form.customer_id = Number(row.id); + form.customer_ref_type = refType; form.customer_name = name; form.upstream_contact = row.contact_person || ""; form.upstream_phone = row.contact_phone || ""; } -/* --------------------------- 下游主体(供应商 / 客户) --------------------------- */ +/* --------------------------- 下游主体(客户 / 供应商) --------------------------- */ const supplierOptions = ref([]); -/** - * 下游主体候选:同时检索供应商库与客户库。 - * 同一家单位在系统里可能以客户身份建档,因此下游既支持选供应商,也支持选客户。 - */ -async function searchSuppliers(keyword) { +// 检索序号:快速输入时丢弃过期响应,避免旧结果覆盖最新结果 +let supplierQuerySeq = 0; + +/** 对方主体分类:customer=客户库 / supplier=供应商库(下游与中游共用,Tab 切换) */ +const counterpartyCategory = ref("supplier"); +const counterpartyCategoryLabel = computed(() => + counterpartyCategory.value === "supplier" ? "供应商" : "客户" +); + +/** 按库类型(1客户 2供应商)同步当前分类;未关联库时保持原分类 */ +function syncCounterpartyCategory(refType) { + const t = Number(refType) || 0; + if (t === 1) counterpartyCategory.value = "customer"; + else if (t === 2) counterpartyCategory.value = "supplier"; +} + +/** 未选对方主体时的默认分类:上下游项目默认供应商,上中游项目默认客户 */ +function defaultCounterpartyCategory(projectType) { + return Number(projectType) === 3 ? "customer" : "supplier"; +} + +/** 切换分类:按当前输入重新检索该分类候选(已关联的企业保持不变) */ +function onCounterpartyCategoryChange() { + searchSuppliers(form.supplier_name || ""); +} + +/** 按分类 + 关键字请求企业库(下游主体来源:客户库 / 供应商库) */ +async function fetchCounterparties(keyword, category) { const kw = String(keyword || "").trim(); + const isSupplier = category === "supplier"; + const res = isSupplier + ? await getCrmSupplierList({ page: 1, pageSize: 50, keyword: kw }) + : await getCrmCustomerList({ page: 1, pageSize: 50, keyword: kw }); + return (res?.data?.list || []).map((row) => ({ + id: row.id, + ref_type: isSupplier ? 2 : 1, + supplier_name: (isSupplier ? row.supplier_name : row.customer_name) || "", + contact_person: row.contact_person || "", + contact_phone: row.contact_phone || "", + })); +} + +/** + * 对方主体候选:按当前分类 + 关键字实时检索对应企业库(服务端搜索,不受「只加载一页」限制)。 + * 返回本次检索结果;若期间有更新的检索发出则返回 null,由最新一次检索负责渲染。 + */ +async function searchSuppliers(keyword, category = counterpartyCategory.value) { + const seq = ++supplierQuerySeq; try { - const [supRes, cusRes] = await Promise.all([ - getCrmSupplierList({ page: 1, pageSize: 50, keyword: kw }), - getCrmCustomerList({ page: 1, pageSize: 50, keyword: kw }), - ]); - const suppliers = (supRes?.data?.list || []).map((s) => ({ - id: s.id, - ref_type: 2, - supplier_name: s.supplier_name || "", - })); - const customers = (cusRes?.data?.list || []).map((c) => ({ - id: c.id, - ref_type: 1, - supplier_name: c.customer_name || "", - })); - supplierOptions.value = [...suppliers, ...customers]; + const list = await fetchCounterparties(keyword, category); + if (seq !== supplierQuerySeq) return null; // 过期结果:已有更新的检索在路上 + if (category === counterpartyCategory.value) supplierOptions.value = list; + return list; } catch { - supplierOptions.value = []; + if (seq === supplierQuerySeq && category === counterpartyCategory.value) supplierOptions.value = []; + return []; } } -/** 供应商名称输入建议:本地模糊过滤已加载候选(自由输入同样生效) */ -function querySuppliers(query, cb) { - const kw = String(query || "").trim().toLowerCase(); - const list = (supplierOptions.value || []).filter( - (s) => !kw || String(s.supplier_name || "").toLowerCase().includes(kw) - ); +/** 下游主体副文案:联系人 · 联系方式,便于确认选中的是哪家企业 */ +function supplierMeta(item) { + return [item?.contact_person, item?.contact_phone].filter(Boolean).join(" · "); +} + +/** 输入建议:随输入实时远程检索当前分类(空关键字展示最新候选,自由输入同样生效) */ +async function querySuppliers(query, cb) { + const list = await searchSuppliers(query); + if (!list) return; // 已有更新的检索在路上,等它回调 // 附加 value 字段:兼容 autocomplete 默认以 value 作为选中值 - cb(list.slice(0, 50).map((s) => ({ ...s, value: s.supplier_name }))); + cb(list.map((s) => ({ ...s, value: s.supplier_name }))); } /** * 选中的库中主体:记录来源类型(1客户 / 2供应商)与 ID,并写入标准名称; * 手工输入且未命中库时清空关联,仅按名称保存。 + * 手工输入的名称不在当前分类候选里时,先按当前分类回查,仍未命中再查另一个分类, + * 精确命中同名企业则自动关联并切到对应分类,确保下游对接人能正常带出联系人。 */ -function applySupplier(item) { +async function applySupplier(item) { if (item && item.id) { form.supplier_id = item.id; form.supplier_ref_type = Number(item.ref_type) || 0; @@ -580,7 +720,30 @@ function applySupplier(item) { return; } const name = String(form.supplier_name || "").trim(); - const hit = supplierOptions.value.find((s) => s.supplier_name === name); + if (!name) { + form.supplier_id = 0; + form.supplier_ref_type = 0; + return; + } + let hit = supplierOptions.value.find((s) => s.supplier_name === name); + if (!hit) { + const current = counterpartyCategory.value; + const list = (await searchSuppliers(name, current)) || supplierOptions.value; + // 输入内容在检索期间被改动:放弃本次绑定,避免关联到旧名称的企业 + if (String(form.supplier_name || "").trim() !== name) return; + hit = list.find((s) => s.supplier_name === name); + if (!hit) { + // 当前分类没有同名企业:再到另一个库查一次(同一家单位可能建在另一侧) + const other = current === "supplier" ? "customer" : "supplier"; + const otherList = (await searchSuppliers(name, other)) || []; + if (String(form.supplier_name || "").trim() !== name) return; + hit = otherList.find((s) => s.supplier_name === name); + if (hit) { + counterpartyCategory.value = other; + supplierOptions.value = otherList; + } + } + } if (hit) { form.supplier_id = hit.id; form.supplier_ref_type = Number(hit.ref_type) || 0; @@ -590,9 +753,9 @@ function applySupplier(item) { } } -/* --------------------------- 快速新建中游企业 --------------------------- */ +/* --------------------------- 快速新建(下游主体 / 中游企业) --------------------------- */ -// 中游企业可能不在客户 / 供应商库中:支持直接新建到客户库后自动关联 +// 企业可能不在客户 / 供应商库中:按当前分类直接新建并自动关联 const counterpartyCreateVisible = ref(false); function openCounterpartyCreate() { @@ -600,15 +763,17 @@ function openCounterpartyCreate() { } /** - * 快速新建中游企业成功:按新建记录回填中游企业(库关联 + 标准名称), - * 并用新建时填写的联系人带出中游对接人 / 电话(联系人候选随 supplier_id 自动加载)。 + * 快速新建成功:按新建记录回填对方主体(库关联 + 标准名称 + 来源分类), + * 并用新建时填写的联系人带出下游 / 中游对接人、电话(联系人候选随 supplier_id 自动加载)。 */ async function onCounterpartyCreated(row) { if (!row?.id) return; const name = row.customer_name || row.supplier_name || row.label || ""; + const refType = Number(row.ref_type) || (counterpartyCategory.value === "supplier" ? 2 : 1); + syncCounterpartyCategory(refType); await searchSuppliers(name); form.supplier_id = Number(row.id); - form.supplier_ref_type = Number(row.ref_type) || 1; // 新建的企业落在客户库 + form.supplier_ref_type = refType; form.supplier_name = name; form.downstream_contact = row.contact_person || ""; form.downstream_phone = row.contact_phone || ""; @@ -636,7 +801,7 @@ async function fetchContactsOf(relatedType, relatedID) { } } -/** 上游对接人候选:所选客户(上游)的联系人 */ +/** 上游对接人候选:所选上游主体(客户 / 供应商)的联系人 */ async function loadUpstreamContacts() { const id = Number(form.customer_id) || 0; if (!id) { @@ -645,7 +810,7 @@ async function loadUpstreamContacts() { } upstreamContactLoading.value = true; try { - upstreamContacts.value = await fetchContactsOf(1, id); + upstreamContacts.value = await fetchContactsOf(Number(form.customer_ref_type) === 2 ? 2 : 1, id); } finally { upstreamContactLoading.value = false; } @@ -678,8 +843,8 @@ function onDownstreamContactPick(name) { if (hit) form.downstream_phone = hit.mobile || hit.phone || ""; } -// 客户 / 下游主体变化时刷新对接人候选(不清空已选值,兼容历史手工数据) -watch(() => form.customer_id, loadUpstreamContacts); +// 上游 / 下游主体变化时刷新对接人候选(不清空已选值,兼容历史手工数据) +watch(() => [form.customer_id, form.customer_ref_type], loadUpstreamContacts); watch(() => [form.supplier_id, form.supplier_ref_type], loadDownstreamContacts); function handleClose() { @@ -708,6 +873,7 @@ async function handleSubmit() { project_no: form.project_no, project_type: Number(form.project_type) || 1, customer_id: Number(form.customer_id) || 0, + customer_ref_type: Number(form.customer_ref_type) || 1, // 1客户库 2供应商库 customer_name: form.customer_name, supplier_id: Number(form.supplier_id) || 0, supplier_ref_type: Number(form.supplier_ref_type) || 0, @@ -778,6 +944,14 @@ async function handleSubmit() { } } +/* 对方主体:客户 / 供应商分类切换 + 搜索框 */ +.counterparty-picker { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; +} + .opt-meta { margin-left: 8px; font-size: 12px; diff --git a/backend/src/views/apps/crm/project/index.vue b/backend/src/views/apps/crm/project/index.vue index c4d783b..68a39c1 100644 --- a/backend/src/views/apps/crm/project/index.vue +++ b/backend/src/views/apps/crm/project/index.vue @@ -128,7 +128,7 @@ @@ -168,8 +168,9 @@ const editVisible = ref(false); const detailVisible = ref(false); const currentRow = ref(null); -// 项目联系人(正式库) +// 项目联系人(正式库):上游主体可能来自客户库或供应商库 const contactBookVisible = ref(false); +const contactBookCompanyType = ref("customer"); const contactBookCompanyId = ref(null); const contactBookCompanyName = ref(""); @@ -246,9 +247,11 @@ function onDetailEdit(row) { function openContactBook(row) { if (!row.customer_id) { - ElMessage.warning("该项目未关联正式客户,暂无正式联系人"); + ElMessage.warning("该项目未关联上游主体,暂无正式联系人"); return; } + // 上游主体可能建在客户库或供应商库(customer_ref_type 2=供应商库) + contactBookCompanyType.value = Number(row.customer_ref_type) === 2 ? "supplier" : "customer"; contactBookCompanyId.value = row.customer_id; contactBookCompanyName.value = row.customer_name || ""; contactBookVisible.value = true; diff --git a/backend/src/views/apps/crm/supplier/index.vue b/backend/src/views/apps/crm/supplier/index.vue index 2d689da..e361199 100644 --- a/backend/src/views/apps/crm/supplier/index.vue +++ b/backend/src/views/apps/crm/supplier/index.vue @@ -61,8 +61,32 @@
+ +
+ 已选 {{ multipleSelection.length }} 项 + + 批量启用 + + + 批量禁用 + + + 批量转客户 + + + 批量删除 + +
+
- + +