diff --git a/backend/src/api/contactOA.js b/backend/src/api/contactOA.js index 04396cc..5217493 100644 --- a/backend/src/api/contactOA.js +++ b/backend/src/api/contactOA.js @@ -72,7 +72,7 @@ export function getContactOrgTree(params) { }) } -// 获取组织架构列表(用于选择器) +// 获取组织架构列表 export function getOrganizationList(params) { return request({ url: '/backend/erp/getOrganization', @@ -81,7 +81,7 @@ export function getOrganizationList(params) { }) } -// 获取职位列表(用于选择器,可按部门筛选) +// 获取职位列表 export function getPositionList(params) { return request({ url: '/backend/erp/getPosition', @@ -90,7 +90,7 @@ export function getPositionList(params) { }) } -// 获取职位详情(编辑联系人时,已选职位若不在当前部门列表内用于回显) +// 获取职位详情 export function getPositionDetail(id) { return request({ url: `/backend/erp/getPositionDetail/${id}`, diff --git a/backend/src/api/crm.js b/backend/src/api/crm.js index e620eec..359194c 100644 --- a/backend/src/api/crm.js +++ b/backend/src/api/crm.js @@ -39,6 +39,14 @@ export function toggleCrmCustomerStatus(id, status) { }); } +/** + * 客户转供应商:把客户档案(含联系人)复制一套到供应商库。 + * 同一家单位可能既是客户又是供应商,因此原客户记录保留;供应商库已有同名企业时不重复建档,只补同步联系人。 + */ +export function convertCustomerToSupplier(id) { + return request({ url: `/backend/erp/customer/${id}/convert-supplier`, method: "post" }); +} + /* -------------------------------- 供应商 -------------------------------- */ export function getCrmSupplierList(params) { @@ -69,6 +77,14 @@ export function toggleCrmSupplierStatus(id, status) { }); } +/** + * 供应商转客户:把供应商档案(含联系人)复制一套到客户库。 + * 同一家单位可能既是供应商又是客户,因此原供应商记录保留;客户库已有同名企业时不重复建档,只补同步联系人。 + */ +export function convertSupplierToCustomer(id) { + return request({ url: `/backend/erp/supplier/${id}/convert-customer`, method: "post" }); +} + /* ------------------------------- 客户公海 ------------------------------- */ /** 公海列表:租户内共享,所有成员可见 */ diff --git a/backend/src/api/crmContract.js b/backend/src/api/crmContract.js index 9a3ee5f..5041ee3 100644 --- a/backend/src/api/crmContract.js +++ b/backend/src/api/crmContract.js @@ -5,7 +5,7 @@ import request from "@/utils/request"; * * 说明: * - 合同绑定项目:project_id 为空即为「无头合同」,否则为「项目合同」; - * - 合同状态 status:1=草稿 2=已完成 3=已作废 4=履约中 5=执行异常; + * - 合同状态 status:1=草稿 2=已完成 3=已作废 4=履约中 5=异常; * - 参与方 parties / 产品清单 products / 金额汇总 summary 随主表一并以 JSON 提交, * 后端按需落库;step 记录进度式创建所到达的步骤,支持每步保存续填。 */ @@ -35,7 +35,7 @@ export function deleteContract(id) { return request({ url: `/backend/crm/contract/${id}`, method: "delete" }); } -/** 合同状态流转:status 1=草稿 2=已完成 3=已作废 4=履约中 5=执行异常 */ +/** 合同状态流转:status 1=草稿 2=已完成 3=已作废 4=履约中 5=异常 */ export function changeContractStatus(id, status) { return request({ url: `/backend/crm/contract/${id}/status`, method: "post", data: { status } }); } diff --git a/backend/src/api/crmPipeline.js b/backend/src/api/crmPipeline.js index 533a39c..897fc55 100644 --- a/backend/src/api/crmPipeline.js +++ b/backend/src/api/crmPipeline.js @@ -82,6 +82,21 @@ export function deleteProject(id) { return request({ url: `/backend/crm/project/${id}`, method: "delete" }); } +/** 项目状态流转:仅更新状态(1未开始 / 2进行中 / 3已完成 / 4已暂停 / 5异常) */ +export function changeProjectStatus(id, status) { + return request({ url: `/backend/crm/project/${id}/status`, method: "post", data: { status } }); +} + +/** + * 保存项目联系人:从对应企业通讯录中挑选的联系人ID集合(多选)。 + * - upstream_contact_ids:上游(客户方)选中的联系人ID数组; + * - downstream_contact_ids:下游/中游(对方主体方)选中的联系人ID数组; + * - 单一项目后端会自动清空下游集合,传入不属于对应企业的ID会被过滤。 + */ +export function saveProjectContacts(id, data) { + return request({ url: `/backend/crm/project/${id}/contacts`, method: "post", data }); +} + /* --------------------------- 项目文档(OA 文档库) --------------------------- */ /** diff --git a/backend/src/api/customer.js b/backend/src/api/customer.js index 7412e24..362e1df 100644 --- a/backend/src/api/customer.js +++ b/backend/src/api/customer.js @@ -45,3 +45,15 @@ export function toggleCustomerStatus(id, status) { data: { status } }) } + +/** + * 客户转供应商:把客户档案(含联系人)复制一套到供应商库。 + * 同一家单位可能既是客户又是供应商,因此原客户记录保留; + * 供应商库已有同名企业时不重复建档,只补同步联系人。 + */ +export function convertCustomerToSupplier(id) { + return request({ + url: `/backend/erp/customer/${id}/convert-supplier`, + method: 'post' + }) +} diff --git a/backend/src/api/supplier.js b/backend/src/api/supplier.js index d9a08a2..87384b5 100644 --- a/backend/src/api/supplier.js +++ b/backend/src/api/supplier.js @@ -45,3 +45,15 @@ export function toggleSupplierStatus(id, status) { data: { status } }) } + +/** + * 供应商转客户:把供应商档案(含联系人)复制一套到客户库。 + * 同一家单位可能既是供应商又是客户,因此原供应商记录保留; + * 客户库已有同名企业时不重复建档,只补同步联系人。 + */ +export function convertSupplierToCustomer(id) { + return request({ + url: `/backend/erp/supplier/${id}/convert-customer`, + method: 'post' + }) +} diff --git a/backend/src/views/apps/crm/business/components/detail_attach.vue b/backend/src/views/apps/crm/business/components/detail_attach.vue index 875af71..2e4de34 100644 --- a/backend/src/views/apps/crm/business/components/detail_attach.vue +++ b/backend/src/views/apps/crm/business/components/detail_attach.vue @@ -1,8 +1,11 @@ - + + + @@ -32,7 +34,17 @@ - + +
+
+ + +
+ + 添加手机号({{ form.mobiles.length }}/5) + +
+
@@ -52,13 +64,14 @@ diff --git a/backend/src/views/apps/crm/contact/components/edit.vue b/backend/src/views/apps/crm/contact/components/edit.vue index 77df56a..6a3bb0d 100644 --- a/backend/src/views/apps/crm/contact/components/edit.vue +++ b/backend/src/views/apps/crm/contact/components/edit.vue @@ -73,9 +73,33 @@ - - - + + + +
+
+ + +
+ + 添加手机号({{ form.mobiles.length }}/5) + +
@@ -163,6 +187,7 @@ + + diff --git a/backend/src/views/apps/crm/contact/index.vue b/backend/src/views/apps/crm/contact/index.vue index 79f3b99..1529507 100644 --- a/backend/src/views/apps/crm/contact/index.vue +++ b/backend/src/views/apps/crm/contact/index.vue @@ -73,7 +73,10 @@ - + + + + @@ -143,6 +148,15 @@ const contactPlaceholder = computed(() => { return party.value?.ref_id ? "选择签约人(也可输入)" : "请先选择客户 / 供应商"; }); +/** 已选企业但通讯录里还没有联系人:提示签约人信息会在保存合同后自动写入通讯录 */ +const showContactTip = computed( + () => + !isOwnCompany.value && + !!party.value?.ref_id && + !contactLoading.value && + contacts.value.length === 0 +); + /** 拉取本公司人员(在职员工),映射为与联系人一致的字段结构,便于复用下拉渲染 */ async function fetchOwnEmployees() { ownLoading.value = true; @@ -338,5 +352,13 @@ const handleRefTypeChange = () => { .select-footer { text-align: center; } + + /* 暂无联系人提示:签约人信息保存后自动写入通讯录 */ + .contact-tip { + margin-top: 8px; + font-size: 12px; + line-height: 1.5; + color: var(--el-text-color-secondary); + } } diff --git a/backend/src/views/apps/crm/contract/components/ProductList.vue b/backend/src/views/apps/crm/contract/components/ProductList.vue index d7c42d8..1bb7c74 100644 --- a/backend/src/views/apps/crm/contract/components/ProductList.vue +++ b/backend/src/views/apps/crm/contract/components/ProductList.vue @@ -10,6 +10,15 @@ + + + diff --git a/backend/src/views/apps/crm/project/components/detail_contract.vue b/backend/src/views/apps/crm/project/components/detail_contract.vue index cb73af8..e318db2 100644 --- a/backend/src/views/apps/crm/project/components/detail_contract.vue +++ b/backend/src/views/apps/crm/project/components/detail_contract.vue @@ -16,8 +16,8 @@ @@ -32,8 +32,8 @@ > {{ c.contract_name }} - - {{ contractSideOf(c) === "down" ? "下游" : "上游" }} + + {{ sideLabel(c) }}
- 合同总利润 - ¥{{ formatMoney(summaryOf(c).total_profit) }} + {{ contractProfitLabel(c.our_role) }} + + {{ contractProfitText(c.our_role, summaryOf(c)) }} +
硬件部分金额 @@ -166,6 +169,8 @@ import { contractCategoryText, productCategoryText, productCategoryTag, + contractProfitLabel, + contractProfitText, } from "../../dict"; const props = defineProps({ @@ -173,6 +178,8 @@ const props = defineProps({ projectId: { type: [Number, String], default: null }, // 关联项目名称(用于新建合同时预设) projectName: { type: String, default: "" }, + // 项目类型:1=单一项目(仅上游) 2=上下游项目 + projectType: { type: [Number, String], default: 1 }, }); const emit = defineEmits(["count"]); @@ -187,25 +194,53 @@ const createVisible = ref(false); const editRow = ref(null); const initStep = ref(1); -/** 新建合同时预设的关联项目 */ +/** 新建合同时预设的关联项目(带项目类型,供新建向导给出上中游提示) */ const presetProject = computed(() => - props.projectId ? { id: props.projectId, project_name: props.projectName } : null + props.projectId + ? { id: props.projectId, project_name: props.projectName, project_type: props.projectType } + : null ); const partyLabel = (role) => PARTY_ROLES.find((r) => r.key === role)?.label || role || "-"; /** - * 合同在项目中的上下游归属: - * 判定以「我方是收款方还是付款方」为准 —— 参与方中出现供应商(我方下游)即下游合同; - * 其余按我方角色判断:我方为甲方(采购 / 付款方)算下游,为乙方(供货 / 收款方)算上游。 - * 注意:不能只看对方是否在客户库建档,客户也可能是我方的上游供应商。 + * 是否展示合同归属分段标签:上下游项目区分「上游 / 下游」, + * 上中游项目区分「上中游 / 中下游」;单一项目只有一段,不展示。 + */ +const showSideTag = computed(() => [2, 3].includes(Number(props.projectType))); + +/** + * 合同在项目中的归属分段(side:up / down / mid): + * - 上下游项目(type=2):判定以「我方是收款方还是付款方」为准 —— 参与方中出现供应商(我方下游) + * 即下游合同;其余按我方角色判断:我方为甲方(采购 / 付款方)算下游,为乙方(供货 / 收款方)算上游。 + * 注意:不能只看对方是否在客户库建档,客户也可能是我方的上游供应商。 + * - 上中游项目(type=3):我方不参与签约(our_role=0 或参与方中无本公司)即「上中游」 + * (上游与中游主体之间的合同,甲乙双方均非本公司);否则为我方与中游主体之间的「中下游」合同。 */ function contractSideOf(c) { const parties = Array.isArray(c?.parties) ? c.parties : []; + if (Number(props.projectType) === 3) { + const ownIn = Number(c?.our_role) !== 0 && parties.some((p) => Number(p?.ref_type) === 0); + return ownIn ? "down" : "mid"; + } if (parties.some((p) => Number(p?.ref_type) === 2)) return "down"; return Number(c?.our_role) === 1 ? "down" : "up"; } +/** 分段标签文案:上下游项目=上游 / 下游;上中游项目=上中游 / 中下游 */ +function sideLabel(c) { + const side = contractSideOf(c); + if (Number(props.projectType) === 3) return side === "down" ? "中下游" : "上中游"; + return side === "down" ? "下游" : "上游"; +} + +/** 分段标签颜色 */ +function sideTagType(c) { + const side = contractSideOf(c); + if (Number(props.projectType) === 3) return side === "down" ? "warning" : "danger"; + return side === "down" ? "warning" : "success"; +} + /** 金额汇总:优先后端 summary,缺失时按产品清单前端重算 */ function summaryOf(c) { const s = c?.summary; diff --git a/backend/src/views/apps/crm/project/components/detail_docs.vue b/backend/src/views/apps/crm/project/components/detail_docs.vue index 9988068..051f4fe 100644 --- a/backend/src/views/apps/crm/project/components/detail_docs.vue +++ b/backend/src/views/apps/crm/project/components/detail_docs.vue @@ -69,10 +69,31 @@ 刷新 + + + + + + + + + + + + +
- + + + + diff --git a/backend/src/views/apps/crm/supplier/index.vue b/backend/src/views/apps/crm/supplier/index.vue index 41db26c..2d689da 100644 --- a/backend/src/views/apps/crm/supplier/index.vue +++ b/backend/src/views/apps/crm/supplier/index.vue @@ -66,6 +66,10 @@ @@ -98,10 +102,11 @@ - + - + + + + + - - + + @@ -31,8 +33,6 @@
- - diff --git a/backend/src/views/apps/erp/customer/index.vue b/backend/src/views/apps/erp/customer/index.vue index 096f25f..e3c0665 100644 --- a/backend/src/views/apps/erp/customer/index.vue +++ b/backend/src/views/apps/erp/customer/index.vue @@ -68,6 +68,8 @@
{{ row.customer_name }} 草稿 + + 也是供应商
@@ -101,10 +103,11 @@ {{ formatDateTime(row.update_time) }}
- + @@ -100,10 +102,11 @@ {{ formatDateTime(row.update_time) }} - + - + + +