瀏覽代碼

Merge remote-tracking branch 'yudao/dev' into dev

dhb52 1 年之前
父節點
當前提交
783f6283ca
共有 94 個文件被更改,包括 4258 次插入2361 次删除
  1. 0 5
      src/api/crm/backlog/index.ts
  2. 36 16
      src/api/crm/business/index.ts
  3. 68 0
      src/api/crm/business/status/index.ts
  4. 0 48
      src/api/crm/businessStatusType/index.ts
  5. 48 28
      src/api/crm/contact/index.ts
  6. 16 0
      src/api/crm/contract/config/index.ts
  7. 49 13
      src/api/crm/contract/index.ts
  8. 5 0
      src/api/crm/customer/index.ts
  9. 10 15
      src/api/crm/followup/index.ts
  10. 0 0
      src/api/crm/product/category/index.ts
  11. 6 6
      src/api/crm/product/index.ts
  12. 15 1
      src/api/crm/receivable/index.ts
  13. 19 4
      src/api/crm/receivable/plan/index.ts
  14. 10 10
      src/api/crm/statistics/rank.ts
  15. 33 1
      src/router/modules/remaining.ts
  16. 1 0
      src/views/bpm/processInstance/detail/index.vue
  17. 92 19
      src/views/crm/backlog/components/ContractAuditList.vue
  18. 91 20
      src/views/crm/backlog/components/ContractRemindList.vue
  19. 7 4
      src/views/crm/backlog/components/CustomerFollowList.vue
  20. 21 13
      src/views/crm/backlog/components/CustomerPutPoolRemindList.vue
  21. 3 2
      src/views/crm/backlog/components/common.ts
  22. 15 14
      src/views/crm/backlog/index.vue
  23. 193 185
      src/views/crm/business/BusinessForm.vue
  24. 108 0
      src/views/crm/business/BusinessUpdateStatusForm.vue
  25. 9 3
      src/views/crm/business/components/BusinessList.vue
  26. 6 5
      src/views/crm/business/components/BusinessListModal.vue
  27. 183 0
      src/views/crm/business/components/BusinessProductForm.vue
  28. 37 0
      src/views/crm/business/detail/BusinessDetailsHeader.vue
  29. 61 0
      src/views/crm/business/detail/BusinessDetailsInfo.vue
  30. 66 0
      src/views/crm/business/detail/BusinessProductList.vue
  31. 146 0
      src/views/crm/business/detail/index.vue
  32. 84 27
      src/views/crm/business/index.vue
  33. 56 29
      src/views/crm/business/status/BusinessStatusForm.vue
  34. 19 43
      src/views/crm/business/status/index.vue
  35. 1 1
      src/views/crm/clue/detail/index.vue
  36. 119 119
      src/views/crm/contact/ContactForm.vue
  37. 75 5
      src/views/crm/contact/components/ContactList.vue
  38. 154 0
      src/views/crm/contact/components/ContactListModal.vue
  39. 4 10
      src/views/crm/contact/detail/ContactDetailsHeader.vue
  40. 27 38
      src/views/crm/contact/detail/ContactDetailsInfo.vue
  41. 19 12
      src/views/crm/contact/detail/index.vue
  42. 15 11
      src/views/crm/contact/index.vue
  43. 186 99
      src/views/crm/contract/ContractForm.vue
  44. 7 3
      src/views/crm/contract/components/ContractList.vue
  45. 183 0
      src/views/crm/contract/components/ContractProductForm.vue
  46. 0 171
      src/views/crm/contract/components/ProductList.vue
  47. 100 0
      src/views/crm/contract/config/index.vue
  48. 4 4
      src/views/crm/contract/detail/ContractDetailsHeader.vue
  49. 22 27
      src/views/crm/contract/detail/ContractDetailsInfo.vue
  50. 61 62
      src/views/crm/contract/detail/ContractProductList.vue
  51. 31 10
      src/views/crm/contract/detail/index.vue
  52. 116 24
      src/views/crm/contract/index.vue
  53. 0 221
      src/views/crm/contract/oa/ContractDetail/index.vue
  54. 9 6
      src/views/crm/customer/CustomerImportForm.vue
  55. 12 4
      src/views/crm/customer/detail/index.vue
  56. 12 85
      src/views/crm/customer/limitConfig/CustomerLimitConfigForm.vue
  57. 1 1
      src/views/crm/customer/limitConfig/CustomerLimitConfigList.vue
  58. 68 41
      src/views/crm/followup/FollowUpRecordForm.vue
  59. 0 71
      src/views/crm/followup/components/BusinessList.vue
  60. 0 88
      src/views/crm/followup/components/BusinessTableSelect.vue
  61. 0 97
      src/views/crm/followup/components/ContactList.vue
  62. 0 87
      src/views/crm/followup/components/ContactTableSelect.vue
  63. 42 0
      src/views/crm/followup/components/FollowUpRecordBusinessForm.vue
  64. 47 0
      src/views/crm/followup/components/FollowUpRecordContactForm.vue
  65. 0 6
      src/views/crm/followup/components/index.ts
  66. 57 28
      src/views/crm/followup/index.vue
  67. 1 1
      src/views/crm/permission/components/PermissionForm.vue
  68. 2 7
      src/views/crm/product/ProductForm.vue
  69. 1 1
      src/views/crm/product/category/ProductCategoryForm.vue
  70. 1 1
      src/views/crm/product/category/index.vue
  71. 5 14
      src/views/crm/product/detail/ProductDetailsHeader.vue
  72. 5 12
      src/views/crm/product/detail/ProductDetailsInfo.vue
  73. 2 2
      src/views/crm/product/index.vue
  74. 216 123
      src/views/crm/receivable/ReceivableForm.vue
  75. 88 49
      src/views/crm/receivable/components/ReceivableList.vue
  76. 43 0
      src/views/crm/receivable/detail/ReceivableDetailsHeader.vue
  77. 62 0
      src/views/crm/receivable/detail/ReceivableDetailsInfo.vue
  78. 99 0
      src/views/crm/receivable/detail/index.vue
  79. 170 58
      src/views/crm/receivable/index.vue
  80. 163 117
      src/views/crm/receivable/plan/ReceivablePlanForm.vue
  81. 96 51
      src/views/crm/receivable/plan/components/ReceivablePlanList.vue
  82. 44 0
      src/views/crm/receivable/plan/detail/ReceivablePlanDetailsHeader.vue
  83. 83 0
      src/views/crm/receivable/plan/detail/ReceivablePlanDetailsInfo.vue
  84. 103 0
      src/views/crm/receivable/plan/detail/index.vue
  85. 164 58
      src/views/crm/receivable/plan/index.vue
  86. 3 3
      src/views/crm/statistics/rank/ContactsCountRank.vue
  87. 3 3
      src/views/crm/statistics/rank/ContractCountRank.vue
  88. 3 3
      src/views/crm/statistics/rank/ContractPriceRank.vue
  89. 3 3
      src/views/crm/statistics/rank/CustomerCountRank.vue
  90. 3 3
      src/views/crm/statistics/rank/FollowCountRank.vue
  91. 3 3
      src/views/crm/statistics/rank/FollowCustomerCountRank.vue
  92. 3 3
      src/views/crm/statistics/rank/ProductSalesRank.vue
  93. 3 3
      src/views/crm/statistics/rank/ReceivablePriceRank.vue
  94. 1 1
      src/views/crm/statistics/rank/index.vue

+ 0 - 5
src/api/crm/backlog/index.ts

@@ -1,11 +1,6 @@
 import request from '@/config/axios'
 // TODO 芋艿:融合下
 
-// 3. 获得分配给我的客户数量
-export const getFollowCustomerCount = async () => {
-  return await request.get({ url: '/crm/customer/follow-customer-count' })
-}
-
 // 5. 获得待审核合同数量
 export const getCheckContractCount = async () => {
   return await request.get({ url: '/crm/contract/check-contract-count' })

+ 36 - 16
src/api/crm/business/index.ts

@@ -4,22 +4,42 @@ import { TransferReqVO } from '@/api/crm/customer'
 export interface BusinessVO {
   id: number
   name: string
+  customerId: number
+  customerName?: string
+  followUpStatus: boolean
+  contactLastTime: Date
+  contactNextTime: Date
+  ownerUserId: number
+  ownerUserName?: string // 负责人的用户名称
+  ownerUserDept?: string // 负责人的部门名称
   statusTypeId: number
+  statusTypeName?: string
   statusId: number
-  contactNextTime: Date
-  customerId: number
+  statusName?: string
+  endStatus: number
+  endRemark: string
   dealTime: Date
-  price: number
+  totalProductPrice: number
+  totalPrice: number
   discountPercent: number
-  productPrice: number
   remark: string
-  ownerUserId: number
-  roUserIds: string
-  rwUserIds: string
-  endStatus: number
-  endRemark: string
-  contactLastTime: Date
-  followUpStatus: number
+  creator: string // 创建人
+  creatorName?: string // 创建人名称
+  createTime: Date // 创建时间
+  updateTime: Date // 更新时间
+  products?: [
+    {
+      id: number
+      productId: number
+      productName: string
+      productNo: string
+      productUnit: number
+      productPrice: number
+      businessPrice: number
+      count: number
+      totalPrice: number
+    }
+  ]
 }
 
 // 查询 CRM 商机列表
@@ -52,6 +72,11 @@ export const updateBusiness = async (data: BusinessVO) => {
   return await request.put({ url: `/crm/business/update`, data })
 }
 
+// 修改 CRM 商机状态
+export const updateBusinessStatus = async (data: BusinessVO) => {
+  return await request.put({ url: `/crm/business/update-status`, data })
+}
+
 // 删除 CRM 商机
 export const deleteBusiness = async (id: number) => {
   return await request.delete({ url: `/crm/business/delete?id=` + id })
@@ -67,11 +92,6 @@ export const getBusinessPageByContact = async (params) => {
   return await request.get({ url: `/crm/business/page-by-contact`, params })
 }
 
-// 获得 CRM 商机列表
-export const getBusinessListByIds = async (val: number[]) => {
-  return await request.get({ url: '/crm/business/list-by-ids', params: { ids: val.join(',') } })
-}
-
 // 商机转移
 export const transferBusiness = async (data: TransferReqVO) => {
   return await request.put({ url: '/crm/business/transfer', data })

+ 68 - 0
src/api/crm/business/status/index.ts

@@ -0,0 +1,68 @@
+import request from '@/config/axios'
+
+export interface BusinessStatusTypeVO {
+  id: number
+  name: string
+  deptIds: number[]
+  statuses?: {
+    id: number
+    name: string
+    percent: number
+  }
+}
+
+export const DEFAULT_STATUSES = [
+  {
+    endStatus: 1,
+    key: '结束',
+    name: '赢单',
+    percent: 100
+  },
+  {
+    endStatus: 2,
+    key: '结束',
+    name: '输单',
+    percent: 0
+  },
+  {
+    endStatus: 3,
+    key: '结束',
+    name: '无效',
+    percent: 0
+  }
+]
+
+// 查询商机状态组列表
+export const getBusinessStatusPage = async (params: any) => {
+  return await request.get({ url: `/crm/business-status/page`, params })
+}
+
+// 新增商机状态组
+export const createBusinessStatus = async (data: BusinessStatusTypeVO) => {
+  return await request.post({ url: `/crm/business-status/create`, data })
+}
+
+// 修改商机状态组
+export const updateBusinessStatus = async (data: BusinessStatusTypeVO) => {
+  return await request.put({ url: `/crm/business-status/update`, data })
+}
+
+// 查询商机状态类型详情
+export const getBusinessStatus = async (id: number) => {
+  return await request.get({ url: `/crm/business-status/get?id=` + id })
+}
+
+// 删除商机状态
+export const deleteBusinessStatus = async (id: number) => {
+  return await request.delete({ url: `/crm/business-status/delete?id=` + id })
+}
+
+// 获得商机状态组列表
+export const getBusinessStatusTypeSimpleList = async () => {
+  return await request.get({ url: `/crm/business-status/type-simple-list` })
+}
+
+// 获得商机阶段列表
+export const getBusinessStatusSimpleList = async (typeId: number) => {
+  return await request.get({ url: `/crm/business-status/status-simple-list`, params: { typeId } })
+}

+ 0 - 48
src/api/crm/businessStatusType/index.ts

@@ -1,48 +0,0 @@
-import request from '@/config/axios'
-
-export interface BusinessStatusTypeVO {
-  id: number
-  name: string
-  deptIds: number[]
-  status: boolean
-}
-
-// 查询商机状态类型列表
-export const getBusinessStatusTypePage = async (params) => {
-  return await request.get({ url: `/crm/business-status-type/page`, params })
-}
-
-// 查询商机状态类型详情
-export const getBusinessStatusType = async (id: number) => {
-  return await request.get({ url: `/crm/business-status-type/get?id=` + id })
-}
-
-// 新增商机状态类型
-export const createBusinessStatusType = async (data: BusinessStatusTypeVO) => {
-  return await request.post({ url: `/crm/business-status-type/create`, data })
-}
-
-// 修改商机状态类型
-export const updateBusinessStatusType = async (data: BusinessStatusTypeVO) => {
-  return await request.put({ url: `/crm/business-status-type/update`, data })
-}
-
-// 删除商机状态类型
-export const deleteBusinessStatusType = async (id: number) => {
-  return await request.delete({ url: `/crm/business-status-type/delete?id=` + id })
-}
-
-// 导出商机状态类型 Excel
-export const exportBusinessStatusType = async (params) => {
-  return await request.download({ url: `/crm/business-status-type/export-excel`, params })
-}
-
-// 获取商机状态类型信息列表
-export const getBusinessStatusTypeList = async () => {
-  return await request.get({ url: `/crm/business-status-type/get-simple-list` })
-}
-
-// 根据类型ID获取商机状态信息列表
-export const getBusinessStatusListByTypeId = async (typeId: number) => {
-  return await request.get({ url: `/crm/business-status-type/get-status-list?typeId=` + typeId })
-}

+ 48 - 28
src/api/crm/contact/index.ts

@@ -2,29 +2,34 @@ import request from '@/config/axios'
 import { TransferReqVO } from '@/api/crm/customer'
 
 export interface ContactVO {
-  name: string
-  nextTime: Date
-  mobile: string
-  telephone: string
-  email: string
-  post: string
-  customerId: number
-  detailAddress: string
-  remark: string
-  ownerUserId: string
-  lastTime: Date
-  id: number
-  parentId: number
-  qq: number
-  wechat: string
-  sex: number
-  master: boolean
-  creatorName: string
-  updateTime?: Date
-  createTime?: Date
-  customerName: string
-  areaName: string
-  ownerUserName: string
+  id: number // 编号
+  name: string // 联系人名称
+  customerId: number // 客户编号
+  customerName?: string // 客户名称
+  contactLastTime: Date // 最后跟进时间
+  contactLastContent: string // 最后跟进内容
+  contactNextTime: Date // 下次联系时间
+  ownerUserId: number // 负责人的用户编号
+  ownerUserName?: string // 负责人的用户名称
+  ownerUserDept?: string // 负责人的部门名称
+  mobile: string // 手机号
+  telephone: string // 电话
+  qq: string // QQ
+  wechat: string // wechat
+  email: string // email
+  areaId: number // 所在地
+  areaName?: string // 所在地名称
+  detailAddress: string // 详细地址
+  sex: number // 性别
+  master: boolean // 是否主联系人
+  post: string // 职务
+  parentId: number // 上级联系人编号
+  parentName?: string // 上级联系人名称
+  remark: string // 备注
+  creator: string // 创建人
+  creatorName?: string // 创建人名称
+  createTime: Date // 创建时间
+  updateTime: Date // 更新时间
 }
 
 export interface ContactBusinessReqVO {
@@ -32,6 +37,11 @@ export interface ContactBusinessReqVO {
   businessIds: number[]
 }
 
+export interface ContactBusiness2ReqVO {
+  businessId: number
+  contactIds: number[]
+}
+
 // 查询 CRM 联系人列表
 export const getContactPage = async (params) => {
   return await request.get({ url: `/crm/contact/page`, params })
@@ -42,6 +52,11 @@ export const getContactPageByCustomer = async (params: any) => {
   return await request.get({ url: `/crm/contact/page-by-customer`, params })
 }
 
+// 查询 CRM 联系人列表,基于指定商机
+export const getContactPageByBusiness = async (params: any) => {
+  return await request.get({ url: `/crm/contact/page-by-business`, params })
+}
+
 // 查询 CRM 联系人详情
 export const getContact = async (id: number) => {
   return await request.get({ url: `/crm/contact/get?id=` + id })
@@ -72,21 +87,26 @@ export const getSimpleContactList = async () => {
   return await request.get({ url: `/crm/contact/simple-all-list` })
 }
 
-// 获得 CRM 联系人列表
-export const getContactListByIds = async (val: number[]) => {
-  return await request.get({ url: '/crm/contact/list-by-ids', params: { ids: val.join(',') } })
-}
-
 // 批量新增联系人商机关联
 export const createContactBusinessList = async (data: ContactBusinessReqVO) => {
   return await request.post({ url: `/crm/contact/create-business-list`, data })
 }
 
+// 批量新增联系人商机关联
+export const createContactBusinessList2 = async (data: ContactBusiness2ReqVO) => {
+  return await request.post({ url: `/crm/contact/create-business-list2`, data })
+}
+
 // 解除联系人商机关联
 export const deleteContactBusinessList = async (data: ContactBusinessReqVO) => {
   return await request.delete({ url: `/crm/contact/delete-business-list`, data })
 }
 
+// 解除联系人商机关联
+export const deleteContactBusinessList2 = async (data: ContactBusiness2ReqVO) => {
+  return await request.delete({ url: `/crm/contact/delete-business-list2`, data })
+}
+
 // 联系人转移
 export const transferContact = async (data: TransferReqVO) => {
   return await request.put({ url: '/crm/contact/transfer', data })

+ 16 - 0
src/api/crm/contract/config/index.ts

@@ -0,0 +1,16 @@
+import request from '@/config/axios'
+
+export interface ContractConfigVO {
+  notifyEnabled?: boolean
+  notifyDays?: number
+}
+
+// 获取合同配置
+export const getContractConfig = async () => {
+  return await request.get({ url: `/crm/contract-config/get` })
+}
+
+// 更新合同配置
+export const saveContractConfig = async (data: ContractConfigVO) => {
+  return await request.put({ url: `/crm/contract-config/save`, data })
+}

+ 49 - 13
src/api/crm/contract/index.ts

@@ -1,35 +1,49 @@
 import request from '@/config/axios'
-import { ProductExpandVO } from '@/api/crm/product'
 import { TransferReqVO } from '@/api/crm/customer'
 
 export interface ContractVO {
   id: number
   name: string
+  no: string
   customerId: number
+  customerName?: string
   businessId: number
   businessName: string
+  contactLastTime: Date
+  ownerUserId: number
+  ownerUserName?: string
+  ownerUserDeptName?: string
   processInstanceId: number
+  auditStatus: number
   orderDate: Date
-  ownerUserId: number
-  no: string
   startTime: Date
   endTime: Date
-  price: number
+  totalProductPrice: number
   discountPercent: number
-  productPrice: number
-  contactId: number
+  totalPrice: number
+  totalReceivablePrice: number
+  signContactId: number
+  signContactName?: string
   signUserId: number
   signUserName: string
-  contactLastTime: Date
-  auditStatus: number
   remark: string
-  productItems: ProductExpandVO[]
+  createTime?: Date
+  creator: string
   creatorName: string
   updateTime?: Date
-  createTime?: Date
-  customerName: string
-  contactName: string
-  ownerUserName: string
+  products?: [
+    {
+      id: number
+      productId: number
+      productName: string
+      productNo: string
+      productUnit: number
+      productPrice: number
+      contractPrice: number
+      count: number
+      totalPrice: number
+    }
+  ]
 }
 
 // 查询 CRM 合同列表
@@ -42,11 +56,23 @@ export const getContractPageByCustomer = async (params: any) => {
   return await request.get({ url: `/crm/contract/page-by-customer`, params })
 }
 
+// 查询 CRM 联系人列表,基于指定商机
+export const getContractPageByBusiness = async (params: any) => {
+  return await request.get({ url: `/crm/contract/page-by-business`, params })
+}
+
 // 查询 CRM 合同详情
 export const getContract = async (id: number) => {
   return await request.get({ url: `/crm/contract/get?id=` + id })
 }
 
+// 查询 CRM 合同下拉列表
+export const getContractSimpleList = async (customerId: number) => {
+  return await request.get({
+    url: `/crm/contract/simple-list?customerId=${customerId}`
+  })
+}
+
 // 新增 CRM 合同
 export const createContract = async (data: ContractVO) => {
   return await request.post({ url: `/crm/contract/create`, data })
@@ -76,3 +102,13 @@ export const submitContract = async (id: number) => {
 export const transferContract = async (data: TransferReqVO) => {
   return await request.put({ url: '/crm/contract/transfer', data })
 }
+
+// 获得待审核合同数量
+export const getAuditContractCount = async () => {
+  return await request.get({ url: '/crm/contract/audit-count' })
+}
+
+// 获得即将到期(提醒)的合同数量
+export const getRemindContractCount = async () => {
+  return await request.get({ url: '/crm/contract/remind-count' })
+}

+ 5 - 0
src/api/crm/customer/index.ts

@@ -90,6 +90,11 @@ export const importCustomerTemplate = () => {
   return request.download({ url: '/crm/customer/get-import-template' })
 }
 
+// 导入客户
+export const handleImport = async (formData) => {
+  return await request.upload({ url: `/crm/customer/import`, data: formData })
+}
+
 // 客户列表
 export const getCustomerSimpleList = async () => {
   return await request.get({ url: `/crm/customer/simple-list` })

+ 10 - 15
src/api/crm/followup/index.ts

@@ -11,7 +11,17 @@ export interface FollowUpRecordVO {
   fileUrls: string[] // 附件
   nextTime: Date // 下次联系时间
   businessIds: number[] // 关联的商机编号数组
+  businesses: {
+    id: number
+    name: string
+  }[] // 关联的商机数组
   contactIds: number[] // 关联的联系人编号数组
+  contacts: {
+    id: number
+    name: string
+  }[] // 关联的联系人数组
+  creator: string
+  creatorName?: string
 }
 
 // 跟进记录 API
@@ -21,28 +31,13 @@ export const FollowUpRecordApi = {
     return await request.get({ url: `/crm/follow-up-record/page`, params })
   },
 
-  // 查询跟进记录详情
-  getFollowUpRecord: async (id: number) => {
-    return await request.get({ url: `/crm/follow-up-record/get?id=` + id })
-  },
-
   // 新增跟进记录
   createFollowUpRecord: async (data: FollowUpRecordVO) => {
     return await request.post({ url: `/crm/follow-up-record/create`, data })
   },
 
-  // 修改跟进记录
-  updateFollowUpRecord: async (data: FollowUpRecordVO) => {
-    return await request.put({ url: `/crm/follow-up-record/update`, data })
-  },
-
   // 删除跟进记录
   deleteFollowUpRecord: async (id: number) => {
     return await request.delete({ url: `/crm/follow-up-record/delete?id=` + id })
-  },
-
-  // 导出跟进记录 Excel
-  exportFollowUpRecord: async (params) => {
-    return await request.download({ url: `/crm/follow-up-record/export-excel`, params })
   }
 }

+ 0 - 0
src/api/crm/product/productCategory/index.ts → src/api/crm/product/category/index.ts


+ 6 - 6
src/api/crm/product/index.ts

@@ -8,21 +8,21 @@ export interface ProductVO {
   price: number
   status: number
   categoryId: number
+  categoryName?: string
   description: string
   ownerUserId: number
 }
 
-export interface ProductExpandVO extends ProductVO {
-  count: number
-  discountPercent: number
-  totalPrice: number
-}
-
 // 查询产品列表
 export const getProductPage = async (params) => {
   return await request.get({ url: `/crm/product/page`, params })
 }
 
+// 获得产品精简列表
+export const getProductSimpleList = async () => {
+  return await request.get({ url: `/crm/product/simple-list` })
+}
+
 // 查询产品详情
 export const getProduct = async (id: number) => {
   return await request.get({ url: `/crm/product/get?id=` + id })

+ 15 - 1
src/api/crm/receivable/index.ts

@@ -5,15 +5,24 @@ export interface ReceivableVO {
   no: string
   planId: number
   customerId: number
+  customerName?: string
   contractId: number
+  contract?: {
+    no: string
+    totalPrice: number
+  }
   auditStatus: number
   processInstanceId: number
   returnTime: Date
   returnType: string
   price: number
   ownerUserId: number
-  sort: number
+  ownerUserName?: string
   remark: string
+  creator: string // 创建人
+  creatorName?: string // 创建人名称
+  createTime: Date // 创建时间
+  updateTime: Date // 更新时间
 }
 
 // 查询回款列表
@@ -50,3 +59,8 @@ export const deleteReceivable = async (id: number) => {
 export const exportReceivable = async (params) => {
   return await request.download({ url: `/crm/receivable/export-excel`, params })
 }
+
+// 提交审核
+export const submitReceivable = async (id: number) => {
+  return await request.put({ url: `/crm/receivable/submit?id=${id}` })
+}

+ 19 - 4
src/api/crm/receivable/plan/index.ts

@@ -4,18 +4,26 @@ export interface ReceivablePlanVO {
   id: number
   period: number
   receivableId: number
-  status: number
-  checkStatus: string
-  processInstanceId: number
   price: number
   returnTime: Date
   remindDays: number
+  returnType: number
   remindTime: Date
   customerId: number
+  customerName?: string
   contractId: number
+  contractNo?: string
   ownerUserId: number
-  sort: number
+  ownerUserName?: string
   remark: string
+  creator: string // 创建人
+  creatorName?: string // 创建人名称
+  createTime: Date // 创建时间
+  updateTime: Date // 更新时间
+  receivable?: {
+    price: number
+    returnTime: Date
+  }
 }
 
 // 查询回款计划列表
@@ -33,6 +41,13 @@ export const getReceivablePlan = async (id: number) => {
   return await request.get({ url: `/crm/receivable-plan/get?id=` + id })
 }
 
+// 查询回款计划下拉数据
+export const getReceivablePlanSimpleList = async (customerId: number, contractId: number) => {
+  return await request.get({
+    url: `/crm/receivable-plan/simple-list?customerId=${customerId}&contractId=${contractId}`
+  })
+}
+
 // 新增回款计划
 export const createReceivablePlan = async (data: ReceivablePlanVO) => {
   return await request.post({ url: `/crm/receivable-plan/create`, data })

+ 10 - 10
src/api/crm/bi/rank.ts → src/api/crm/statistics/rank.ts

@@ -1,66 +1,66 @@
 import request from '@/config/axios'
 
-export interface BiRankRespVO {
+export interface StatisticsRankRespVO {
   count: number
   nickname: string
   deptName: string
 }
 
 // 排行 API
-export const RankApi = {
+export const StatisticsRankApi = {
   // 获得合同排行榜
   getContractPriceRank: (params: any) => {
     return request.get({
-      url: '/crm/bi-rank/get-contract-price-rank',
+      url: '/crm/statistics-rank/get-contract-price-rank',
       params
     })
   },
   // 获得回款排行榜
   getReceivablePriceRank: (params: any) => {
     return request.get({
-      url: '/crm/bi-rank/get-receivable-price-rank',
+      url: '/crm/statistics-rank/get-receivable-price-rank',
       params
     })
   },
   // 签约合同排行
   getContractCountRank: (params: any) => {
     return request.get({
-      url: '/crm/bi-rank/get-contract-count-rank',
+      url: '/crm/statistics-rank/get-contract-count-rank',
       params
     })
   },
   // 产品销量排行
   getProductSalesRank: (params: any) => {
     return request.get({
-      url: '/crm/bi-rank/get-product-sales-rank',
+      url: '/crm/statistics-rank/get-product-sales-rank',
       params
     })
   },
   // 新增客户数排行
   getCustomerCountRank: (params: any) => {
     return request.get({
-      url: '/crm/bi-rank/get-customer-count-rank',
+      url: '/crm/statistics-rank/get-customer-count-rank',
       params
     })
   },
   // 新增联系人数排行
   getContactsCountRank: (params: any) => {
     return request.get({
-      url: '/crm/bi-rank/get-contacts-count-rank',
+      url: '/crm/statistics-rank/get-contacts-count-rank',
       params
     })
   },
   // 跟进次数排行
   getFollowCountRank: (params: any) => {
     return request.get({
-      url: '/crm/bi-rank/get-follow-count-rank',
+      url: '/crm/statistics-rank/get-follow-count-rank',
       params
     })
   },
   // 跟进客户数排行
   getFollowCustomerCountRank: (params: any) => {
     return request.get({
-      url: '/crm/bi-rank/get-follow-customer-count-rank',
+      url: '/crm/statistics-rank/get-follow-customer-count-rank',
       params
     })
   }

+ 33 - 1
src/router/modules/remaining.ts

@@ -104,7 +104,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
-
   {
     path: '/dict',
     component: Layout,
@@ -518,6 +517,17 @@ const remainingRouter: AppRouteRecordRaw[] = [
         },
         component: () => import('@/views/crm/customer/detail/index.vue')
       },
+      {
+        path: 'business/detail/:id',
+        name: 'CrmBusinessDetail',
+        meta: {
+          title: '商机详情',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/crm/business'
+        },
+        component: () => import('@/views/crm/business/detail/index.vue')
+      },
       {
         path: 'contract/detail/:id',
         name: 'CrmContractDetail',
@@ -529,6 +539,28 @@ const remainingRouter: AppRouteRecordRaw[] = [
         },
         component: () => import('@/views/crm/contract/detail/index.vue')
       },
+      {
+        path: 'receivable-plan/detail/:id',
+        name: 'CrmReceivablePlanDetail',
+        meta: {
+          title: '回款计划详情',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/crm/receivable-plan'
+        },
+        component: () => import('@/views/crm/receivable/plan/detail/index.vue')
+      },
+      {
+        path: 'receivable/detail/:id',
+        name: 'CrmReceivableDetail',
+        meta: {
+          title: '回款详情',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/crm/receivable'
+        },
+        component: () => import('@/views/crm/receivable/detail/index.vue')
+      },
       {
         path: 'contact/detail/:id',
         name: 'CrmContactDetail',

+ 1 - 0
src/views/bpm/processInstance/detail/index.vue

@@ -234,6 +234,7 @@ const getProcessInstance = async () => {
         fApi.value?.fapi?.disabled(true)
       })
     } else {
+      // 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
       BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
     }
 

+ 92 - 19
src/views/crm/backlog/tables/CheckContract.vue → src/views/crm/backlog/components/ContractAuditList.vue

@@ -30,8 +30,14 @@
 
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" fixed="left" label="合同编号" prop="no" width="130" />
-      <el-table-column align="center" label="合同名称" prop="name" width="130" />
+      <el-table-column align="center" fixed="left" label="合同编号" prop="no" width="180" />
+      <el-table-column align="center" fixed="left" label="合同名称" prop="name" width="160">
+        <template #default="scope">
+          <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+            {{ scope.row.name }}
+          </el-link>
+        </template>
+      </el-table-column>
       <el-table-column align="center" label="客户名称" prop="customerName" width="120">
         <template #default="scope">
           <el-link
@@ -43,8 +49,24 @@
           </el-link>
         </template>
       </el-table-column>
-      <!-- TODO @puhui999:做了商机详情后,可以把这个超链接加上 -->
-      <el-table-column align="center" label="商机名称" prop="businessName" width="130" />
+      <el-table-column align="center" label="商机名称" prop="businessName" width="130">
+        <template #default="scope">
+          <el-link
+            :underline="false"
+            type="primary"
+            @click="openBusinessDetail(scope.row.businessId)"
+          >
+            {{ scope.row.businessName }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="合同金额(元)"
+        prop="totalPrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
       <el-table-column
         align="center"
         label="下单时间"
@@ -52,13 +74,6 @@
         width="120"
         :formatter="dateFormatter2"
       />
-      <el-table-column
-        align="center"
-        label="合同金额"
-        prop="price"
-        width="130"
-        :formatter="fenToYuanFormat"
-      />
       <el-table-column
         align="center"
         label="合同开始时间"
@@ -78,17 +93,41 @@
           <el-link
             :underline="false"
             type="primary"
-            @click="openContactDetail(scope.row.contactId)"
+            @click="openContactDetail(scope.row.signContactId)"
           >
-            {{ scope.row.contactName }}
+            {{ scope.row.signContactName }}
           </el-link>
         </template>
       </el-table-column>
       <el-table-column align="center" label="公司签约人" prop="signUserName" width="130" />
-      <el-table-column align="center" label="备注" prop="remark" width="130" />
-      <!-- TODO @puhui999:后续可加 【已收款金额】、【未收款金额】 -->
+      <el-table-column align="center" label="备注" prop="remark" width="200" />
+      <el-table-column
+        align="center"
+        label="已回款金额(元)"
+        prop="totalReceivablePrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column
+        align="center"
+        label="未回款金额(元)"
+        prop="totalReceivablePrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      >
+        <template #default="scope">
+          {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.totalReceivablePrice) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="最后跟进时间"
+        prop="contactLastTime"
+        width="180px"
+      />
       <el-table-column align="center" label="负责人" prop="ownerUserName" width="120" />
-      <el-table-column align="center" label="创建人" prop="creatorName" width="120" />
+      <el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
@@ -103,11 +142,24 @@
         prop="createTime"
         width="180px"
       />
+      <el-table-column align="center" label="创建人" prop="creatorName" width="120" />
       <el-table-column align="center" fixed="right" label="合同状态" prop="auditStatus" width="120">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
         </template>
       </el-table-column>
+      <el-table-column fixed="right" label="操作" width="90">
+        <template #default="scope">
+          <el-button
+            link
+            v-hasPermi="['crm:contract:update']"
+            type="primary"
+            @click="handleProcessDetail(scope.row)"
+          >
+            查看审批
+          </el-button>
+        </template>
+      </el-table-column>
     </el-table>
     <!-- 分页 -->
     <Pagination
@@ -122,9 +174,9 @@
 <script setup lang="ts" name="CheckContract">
 import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
 import * as ContractApi from '@/api/crm/contract'
-import { fenToYuanFormat } from '@/utils/formatter'
 import { DICT_TYPE } from '@/utils/dict'
 import { AUDIT_STATUS } from './common'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
@@ -132,7 +184,8 @@ const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  auditStatus: 20
+  sceneType: 1, // 我负责的
+  auditStatus: 10
 })
 const queryFormRef = ref() // 搜索的表单
 
@@ -154,8 +207,18 @@ const handleQuery = () => {
   getList()
 }
 
+/** 查看审批 */
+const handleProcessDetail = (row: ContractApi.ContractVO) => {
+  push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } })
+}
+
+/** 打开合同详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'CrmContractDetail', params: { id } })
+}
+
 /** 打开客户详情 */
-const { push } = useRouter() // 路由
 const openCustomerDetail = (id: number) => {
   push({ name: 'CrmCustomerDetail', params: { id } })
 }
@@ -165,6 +228,16 @@ const openContactDetail = (id: number) => {
   push({ name: 'CrmContactDetail', params: { id } })
 }
 
+/** 打开商机详情 */
+const openBusinessDetail = (id: number) => {
+  push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 激活时 */
+onActivated(async () => {
+  await getList()
+})
+
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 91 - 20
src/views/crm/backlog/tables/EndContract.vue → src/views/crm/backlog/components/ContractRemindList.vue

@@ -30,8 +30,14 @@
 
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" fixed="left" label="合同编号" prop="no" width="130" />
-      <el-table-column align="center" label="合同名称" prop="name" width="130" />
+      <el-table-column align="center" fixed="left" label="合同编号" prop="no" width="180" />
+      <el-table-column align="center" fixed="left" label="合同名称" prop="name" width="160">
+        <template #default="scope">
+          <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+            {{ scope.row.name }}
+          </el-link>
+        </template>
+      </el-table-column>
       <el-table-column align="center" label="客户名称" prop="customerName" width="120">
         <template #default="scope">
           <el-link
@@ -43,8 +49,24 @@
           </el-link>
         </template>
       </el-table-column>
-      <!-- TODO @puhui999:做了商机详情后,可以把这个超链接加上 -->
-      <el-table-column align="center" label="商机名称" prop="businessName" width="130" />
+      <el-table-column align="center" label="商机名称" prop="businessName" width="130">
+        <template #default="scope">
+          <el-link
+            :underline="false"
+            type="primary"
+            @click="openBusinessDetail(scope.row.businessId)"
+          >
+            {{ scope.row.businessName }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="合同金额(元)"
+        prop="totalPrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
       <el-table-column
         align="center"
         label="下单时间"
@@ -52,13 +74,6 @@
         width="120"
         :formatter="dateFormatter2"
       />
-      <el-table-column
-        align="center"
-        label="合同金额"
-        prop="price"
-        width="130"
-        :formatter="fenToYuanFormat"
-      />
       <el-table-column
         align="center"
         label="合同开始时间"
@@ -78,17 +93,41 @@
           <el-link
             :underline="false"
             type="primary"
-            @click="openContactDetail(scope.row.contactId)"
+            @click="openContactDetail(scope.row.signContactId)"
           >
-            {{ scope.row.contactName }}
+            {{ scope.row.signContactName }}
           </el-link>
         </template>
       </el-table-column>
       <el-table-column align="center" label="公司签约人" prop="signUserName" width="130" />
-      <el-table-column align="center" label="备注" prop="remark" width="130" />
-      <!-- TODO @puhui999:后续可加 【已收款金额】、【未收款金额】 -->
+      <el-table-column align="center" label="备注" prop="remark" width="200" />
+      <el-table-column
+        align="center"
+        label="已回款金额(元)"
+        prop="totalReceivablePrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column
+        align="center"
+        label="未回款金额(元)"
+        prop="totalReceivablePrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      >
+        <template #default="scope">
+          {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.totalReceivablePrice) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="最后跟进时间"
+        prop="contactLastTime"
+        width="180px"
+      />
       <el-table-column align="center" label="负责人" prop="ownerUserName" width="120" />
-      <el-table-column align="center" label="创建人" prop="creatorName" width="120" />
+      <el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
@@ -103,11 +142,24 @@
         prop="createTime"
         width="180px"
       />
+      <el-table-column align="center" label="创建人" prop="creatorName" width="120" />
       <el-table-column align="center" fixed="right" label="合同状态" prop="auditStatus" width="120">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
         </template>
       </el-table-column>
+      <el-table-column fixed="right" label="操作" width="90">
+        <template #default="scope">
+          <el-button
+            link
+            v-hasPermi="['crm:contract:update']"
+            type="primary"
+            @click="handleProcessDetail(scope.row)"
+          >
+            查看审批
+          </el-button>
+        </template>
+      </el-table-column>
     </el-table>
     <!-- 分页 -->
     <Pagination
@@ -125,8 +177,7 @@ import * as ContractApi from '@/api/crm/contract'
 import { fenToYuanFormat } from '@/utils/formatter'
 import { DICT_TYPE } from '@/utils/dict'
 import { CONTRACT_EXPIRY_TYPE } from './common'
-
-const { push } = useRouter() // 路由
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
@@ -134,6 +185,7 @@ const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
+  sceneType: '1', // 自己负责的
   expiryType: 1
 })
 const queryFormRef = ref() // 搜索的表单
@@ -156,6 +208,17 @@ const handleQuery = () => {
   getList()
 }
 
+/** 查看审批 */
+const handleProcessDetail = (row: ContractApi.ContractVO) => {
+  push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } })
+}
+
+/** 打开合同详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'CrmContractDetail', params: { id } })
+}
+
 /** 打开客户详情 */
 const openCustomerDetail = (id: number) => {
   push({ name: 'CrmCustomerDetail', params: { id } })
@@ -166,10 +229,18 @@ const openContactDetail = (id: number) => {
   push({ name: 'CrmContactDetail', params: { id } })
 }
 
+/** 打开商机详情 */
+const openBusinessDetail = (id: number) => {
+  push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 激活时 */
+onActivated(async () => {
+  await getList()
+})
+
 /** 初始化 **/
 onMounted(() => {
   getList()
 })
 </script>
-
-<style scoped></style>

+ 7 - 4
src/views/crm/backlog/components/CustomerFollowList.vue

@@ -130,8 +130,8 @@ const list = ref([]) // 列表的数据
 const queryParams = ref({
   pageNo: 1,
   pageSize: 10,
-  followUpStatus: false,
-  sceneType: 1
+  sceneType: 1,
+  followUpStatus: false
 })
 const queryFormRef = ref() // 搜索的表单
 
@@ -158,10 +158,13 @@ const openDetail = (id: number) => {
   push({ name: 'CrmCustomerDetail', params: { id } })
 }
 
+/** 激活时 */
+onActivated(async () => {
+  await getList()
+})
+
 /** 初始化 **/
 onMounted(() => {
   getList()
 })
 </script>
-
-<style scoped></style>

+ 21 - 13
src/views/crm/backlog/components/CustomerPutPoolRemindList.vue

@@ -29,32 +29,31 @@
   </ContentWrap>
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" label="编号" prop="id" />
-      <el-table-column align="center" label="客户名称" prop="name" width="160">
+      <el-table-column align="center" label="客户名称" fixed="left" prop="name" width="160">
         <template #default="scope">
           <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
             {{ scope.row.name }}
           </el-link>
         </template>
       </el-table-column>
-      <el-table-column align="center" label="手机" prop="mobile" width="120" />
-      <el-table-column align="center" label="电话" prop="telephone" width="120" />
       <el-table-column align="center" label="客户来源" prop="source" width="100">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
         </template>
       </el-table-column>
-      <el-table-column align="center" label="所属行业" prop="industryId" width="120">
+      <el-table-column label="手机" align="center" prop="mobile" width="120" />
+      <el-table-column label="电话" align="center" prop="telephone" width="130" />
+      <el-table-column label="邮箱" align="center" prop="email" width="180" />
+      <el-table-column align="center" label="客户级别" prop="level" width="135">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
         </template>
       </el-table-column>
-      <el-table-column align="center" label="客户级别" prop="level" width="120">
+      <el-table-column align="center" label="客户行业" prop="industryId" width="100">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
+          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
         </template>
       </el-table-column>
-      <el-table-column align="center" label="网址" prop="website" width="200" />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
@@ -63,12 +62,16 @@
         width="180px"
       />
       <el-table-column align="center" label="备注" prop="remark" width="200" />
+      <el-table-column align="center" label="锁定状态" prop="lockStatus">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" />
+        </template>
+      </el-table-column>
       <el-table-column align="center" label="成交状态" prop="dealStatus">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
         </template>
       </el-table-column>
-      <el-table-column align="center" label="距进入公海天数" prop="poolDay" width="100px" />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
@@ -76,10 +79,17 @@
         prop="contactLastTime"
         width="180px"
       />
+      <el-table-column align="center" label="最后跟进记录" prop="contactLastContent" width="200" />
+      <el-table-column label="地址" align="center" prop="detailAddress" width="180" />
+      <el-table-column align="center" label="距离进入公海天数" prop="poolDay" width="140">
+        <template #default="scope"> {{ scope.row.poolDay }} 天</template>
+      </el-table-column>
+      <el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" />
+      <el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
-        label="创建时间"
+        label="更新时间"
         prop="updateTime"
         width="180px"
       />
@@ -90,8 +100,6 @@
         prop="createTime"
         width="180px"
       />
-      <el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" />
-      <el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
       <el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
     </el-table>
     <!-- 分页 -->

+ 3 - 2
src/views/crm/backlog/components/common.ts

@@ -20,8 +20,9 @@ export const CONTACT_STATUS = [
 
 /** 审批状态 */
 export const AUDIT_STATUS = [
-  { label: '已审批', value: 20 },
-  { label: '待审批', value: 10 }
+  { label: '待审批', value: 10 },
+  { label: '审核通过', value: 20 },
+  { label: '审核不通过', value: 30 }
 ]
 
 /** 回款提醒类型 */

+ 15 - 14
src/views/crm/backlog/index.vue

@@ -17,9 +17,9 @@
     <el-col :span="20" :xs="24">
       <CustomerTodayContactList v-if="leftMenu === 'customerTodayContact'" />
       <ClueFollowList v-if="leftMenu === 'clueFollow'" />
-      <CheckContract v-if="leftMenu === 'checkContract'" />
+      <ContractAuditList v-if="leftMenu === 'contractAudit'" />
       <CheckReceivables v-if="leftMenu === 'checkReceivables'" />
-      <EndContract v-if="leftMenu === 'endContract'" />
+      <ContractRemindList v-if="leftMenu === 'contractRemind'" />
       <CustomerFollowList v-if="leftMenu === 'customerFollow'" />
       <CustomerPutPoolRemindList v-if="leftMenu === 'customerPutPoolRemind'" />
       <RemindReceivables v-if="leftMenu === 'remindReceivables'" />
@@ -33,25 +33,26 @@ import CustomerFollowList from './components/CustomerFollowList.vue'
 import CustomerTodayContactList from './components/CustomerTodayContactList.vue'
 import CustomerPutPoolRemindList from './components/CustomerPutPoolRemindList.vue'
 import ClueFollowList from './components/ClueFollowList.vue'
-import CheckContract from './tables/CheckContract.vue'
-import CheckReceivables from './tables/CheckReceivables.vue'
-import EndContract from './tables/EndContract.vue'
+import ContractAuditList from './components/ContractAuditList.vue'
+import ContractRemindList from './components/ContractRemindList.vue'
 import RemindReceivables from './tables/RemindReceivables.vue'
+import CheckReceivables from './tables/CheckReceivables.vue'
 import * as CustomerApi from '@/api/crm/customer'
 import * as ClueApi from '@/api/crm/clue'
+import * as ContractApi from '@/api/crm/contract'
 
 defineOptions({ name: 'CrmBacklog' })
 
 const leftMenu = ref('customerTodayContact')
 
-const customerTodayContactCount = ref(0)
 const clueFollowCount = ref(0)
 const customerFollowCount = ref(0)
 const customerPutPoolRemindCount = ref(0)
-const checkContractCount = ref(0)
+const customerTodayContactCount = ref(0)
+const contractAuditCount = ref(0)
+const contractRemindCount = ref(0)
 const checkReceivablesCount = ref(0)
 const remindReceivablesCount = ref(0)
-const endContractCount = ref(0)
 
 const leftSides = ref([
   {
@@ -76,8 +77,8 @@ const leftSides = ref([
   },
   {
     name: '待审核合同',
-    menu: 'checkContract',
-    count: checkContractCount
+    menu: 'contractAudit',
+    count: contractAuditCount
   },
   {
     name: '待审核回款',
@@ -91,8 +92,8 @@ const leftSides = ref([
   },
   {
     name: '即将到期的合同',
-    menu: 'endContract',
-    count: endContractCount
+    menu: 'contractRemind',
+    count: contractRemindCount
   }
 ])
 
@@ -110,10 +111,10 @@ const getCount = () => {
   )
   CustomerApi.getFollowCustomerCount().then((count) => (customerFollowCount.value = count))
   ClueApi.getFollowClueCount().then((count) => (clueFollowCount.value = count))
-  BacklogApi.getCheckContractCount().then((count) => (checkContractCount.value = count))
+  ContractApi.getAuditContractCount().then((count) => (contractAuditCount.value = count))
+  ContractApi.getRemindContractCount().then((count) => (contractRemindCount.value = count))
   BacklogApi.getCheckReceivablesCount().then((count) => (checkReceivablesCount.value = count))
   BacklogApi.getRemindReceivablePlanCount().then((count) => (remindReceivablesCount.value = count))
-  BacklogApi.getEndContractCount().then((count) => (endContractCount.value = count))
 }
 
 /** 激活时 */

+ 193 - 185
src/views/crm/business/BusinessForm.vue

@@ -1,113 +1,133 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="1280">
     <el-form
       ref="formRef"
       :model="formData"
       :rules="formRules"
-      label-width="100px"
+      label-width="120px"
       v-loading="formLoading"
     >
-      <el-form-item label="商机名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入商机名称" />
-      </el-form-item>
-      <!-- TODO 芋艿:客户列表的组件 -->
-      <el-form-item label="客户名称" prop="customerName">
-        <el-popover
-          placement="bottom"
-          :width="600"
-          trigger="click"
-          :teleported="false"
-          :visible="showCustomer"
-          :offset="10"
-        >
-          <template #reference>
-            <el-input
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="商机名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入商机名称" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="负责人" prop="ownerUserId">
+            <el-select
+              v-model="formData.ownerUserId"
+              :disabled="formType !== 'create'"
+              class="w-1/1"
+            >
+              <el-option
+                v-for="item in userOptions"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="客户名称" prop="customerId">
+            <el-select
+              :disabled="formData.customerDefault"
+              v-model="formData.customerId"
               placeholder="请选择客户"
-              @click="openCustomerSelect"
-              v-model="formData.customerName"
-            />
-          </template>
-          <el-table :data="customerList" ref="multipleTableRef" @select="handleSelectionChange">
-            <el-table-column width="55" label="选择" type="selection" />
-            <el-table-column width="100" label="编号" property="id" />
-            <el-table-column width="150" label="客户名称" property="name" />
-            <el-table-column width="100" label="客户来源" prop="source" align="center">
-              <template #default="scope">
-                <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
-              </template>
-            </el-table-column>
-            <el-table-column label="客户级别" align="center" prop="level" width="120">
-              <template #default="scope">
-                <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
-              </template>
-            </el-table-column>
-          </el-table>
-          <!-- 分页 -->
-          <el-row :gutter="20">
-            <el-col>
-              <Pagination
-                :total="total"
-                v-model:page="queryParams.pageNo"
-                v-model:limit="queryParams.pageSize"
-                @pagination="getCustomerList"
-                layout="sizes, prev, pager, next"
+              class="w-1/1"
+            >
+              <el-option
+                v-for="item in customerList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
               />
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="10" :offset="13">
-              <el-button @click="selectCustomer">确认</el-button>
-              <el-button @click="showCustomer = false">取消</el-button>
-            </el-col>
-          </el-row>
-        </el-popover>
-      </el-form-item>
-      <!-- TODO @ljlleo:idea 红色的报错,可以解决下 -->
-      <el-form-item label="商机状态类型" prop="statusTypeId">
-        <el-select
-          v-model="formData.statusTypeId"
-          placeholder="请选择商机状态类型"
-          clearable
-          @change="changeBusinessStatusType"
-        >
-          <el-option
-            v-for="item in businessStatusTypeList"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="商机状态" prop="statusId">
-        <el-select v-model="formData.statusId" placeholder="请选择商机状态" clearable>
-          <el-option
-            v-for="item in businessStatusList"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="预计成交日期" prop="dealTime">
-        <el-date-picker
-          v-model="formData.dealTime"
-          type="date"
-          value-format="x"
-          placeholder="选择预计成交日期"
-        />
-      </el-form-item>
-      <el-form-item label="商机金额" prop="price">
-        <el-input v-model="formData.price" placeholder="请输入商机金额" />
-      </el-form-item>
-      <el-form-item label="整单折扣" prop="discountPercent">
-        <el-input v-model="formData.discountPercent" placeholder="请输入整单折扣" />
-      </el-form-item>
-      <el-form-item label="产品总金额" prop="productPrice">
-        <el-input v-model="formData.productPrice" placeholder="请输入产品总金额" />
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入备注" />
-      </el-form-item>
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="商机状态组" prop="statusTypeId">
+            <el-select
+              v-model="formData.statusTypeId"
+              placeholder="请选择商机状态组"
+              clearable
+              class="w-1/1"
+              :disabled="formType !== 'create'"
+            >
+              <el-option
+                v-for="item in statusTypeList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="预计成交日期" prop="dealTime">
+            <el-date-picker
+              v-model="formData.dealTime"
+              type="date"
+              value-format="x"
+              placeholder="选择预计成交日期"
+              class="!w-1/1"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="备注" prop="remark">
+            <el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <!-- 子表的表单 -->
+      <ContentWrap>
+        <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+          <el-tab-pane label="产品清单" name="product">
+            <BusinessProductForm
+              ref="productFormRef"
+              :products="formData.products"
+              :disabled="disabled"
+            />
+          </el-tab-pane>
+        </el-tabs>
+      </ContentWrap>
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="产品总金额" prop="totalProductPrice">
+            <el-input
+              disabled
+              v-model="formData.totalProductPrice"
+              :formatter="erpPriceTableColumnFormatter"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="整单折扣(%)" prop="discountPercent">
+            <el-input-number
+              v-model="formData.discountPercent"
+              placeholder="请输入整单折扣"
+              controls-position="right"
+              :min="0"
+              :precision="2"
+              class="!w-1/1"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="折扣后金额" prop="price">
+            <el-input
+              disabled
+              v-model="formData.totalPrice"
+              placeholder="请输入商机金额"
+              :formatter="erpPriceTableColumnFormatter"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
     </el-form>
     <template #footer>
       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
@@ -117,10 +137,12 @@
 </template>
 <script setup lang="ts">
 import * as BusinessApi from '@/api/crm/business'
-import * as BusinessStatusTypeApi from '@/api/crm/businessStatusType'
+import * as BusinessStatusApi from '@/api/crm/business/status'
 import * as CustomerApi from '@/api/crm/customer'
-import { DICT_TYPE } from '@/utils/dict'
-import { ElTable } from 'element-plus'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import BusinessProductForm from './components/BusinessProductForm.vue'
+import { erpPriceMultiply, erpPriceTableColumnFormatter } from '@/utils'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -132,35 +154,56 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined,
   name: undefined,
-  statusTypeId: undefined,
-  statusId: undefined,
-  contactNextTime: undefined,
   customerId: undefined,
+  ownerUserId: undefined,
+  statusTypeId: undefined,
   dealTime: undefined,
-  price: undefined,
-  discountPercent: undefined,
-  productPrice: undefined,
+  discountPercent: 0,
+  totalProductPrice: undefined,
+  totalPrice: undefined,
   remark: undefined,
-  ownerUserId: undefined,
-  roUserIds: undefined,
-  rwUserIds: undefined,
-  endStatus: undefined,
-  endRemark: undefined,
-  contactLastTime: undefined,
-  followUpStatus: undefined
+  products: [],
+  contactId: undefined,
+  customerDefault: false
 })
 const formRules = reactive({
-  name: [{ required: true, message: '商机名称不能为空', trigger: 'blur' }]
+  name: [{ required: true, message: '商机名称不能为空', trigger: 'blur' }],
+  customerId: [{ required: true, message: '客户不能为空', trigger: 'blur' }],
+  ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'blur' }],
+  statusTypeId: [{ required: true, message: '商机状态组不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const businessStatusList = ref([]) // 商机状态列表
-const businessStatusTypeList = ref([]) //商机状态类型列表
-const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+const statusTypeList = ref([]) // 商机状态类型列表
+// TODO 芋艿:统一的客户选择面板
 const customerList = ref([]) // 客户列表的数据
 
+/** 子表的表单 */
+const subTabsName = ref('product')
+const productFormRef = ref()
+
+/** 计算 discountPrice、totalPrice 价格 */
+watch(
+  () => formData.value,
+  (val) => {
+    if (!val) {
+      return
+    }
+    const totalProductPrice = val.products.reduce((prev, curr) => prev + curr.totalPrice, 0)
+    const discountPrice =
+      val.discountPercent != null
+        ? erpPriceMultiply(totalProductPrice, val.discountPercent / 100.0)
+        : 0
+    const totalPrice = totalProductPrice - discountPrice
+    // 赋值
+    formData.value.totalProductPrice = totalProductPrice
+    formData.value.totalPrice = totalPrice
+  },
+  { deep: true }
+)
+
 /** 打开弹窗 */
-const open = async (type: string, id?: number) => {
+const open = async (type: string, id?: number, customerId?: number, contactId?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
@@ -173,9 +216,26 @@ const open = async (type: string, id?: number) => {
     } finally {
       formLoading.value = false
     }
+  } else {
+    if (customerId) {
+      formData.value.customerId = customerId
+      formData.value.customerDefault = true // 默认客户的选择,不允许变
+    }
+    // 自动关联 contactId 联系人编号
+    if (contactId) {
+      formData.value.contactId = contactId
+    }
   }
+  // 获得客户列表
+  customerList.value = await CustomerApi.getCustomerSimpleList()
   // 加载商机状态类型列表
-  businessStatusTypeList.value = await BusinessStatusTypeApi.getBusinessStatusTypeList()
+  statusTypeList.value = await BusinessStatusApi.getBusinessStatusTypeSimpleList()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+  // 默认新建时选中自己
+  if (formType.value === 'create') {
+    formData.value.ownerUserId = useUserStore().getUser.id
+  }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -186,6 +246,7 @@ const submitForm = async () => {
   if (!formRef) return
   const valid = await formRef.value.validate()
   if (!valid) return
+  await productFormRef.value.validate()
   // 提交请求
   formLoading.value = true
   try {
@@ -210,71 +271,18 @@ const resetForm = () => {
   formData.value = {
     id: undefined,
     name: undefined,
-    statusTypeId: undefined,
-    statusId: undefined,
-    contactNextTime: undefined,
     customerId: undefined,
+    ownerUserId: undefined,
+    statusTypeId: undefined,
     dealTime: undefined,
-    price: undefined,
-    discountPercent: undefined,
-    productPrice: undefined,
+    discountPercent: 0,
+    totalProductPrice: undefined,
+    totalPrice: undefined,
     remark: undefined,
-    ownerUserId: undefined,
-    roUserIds: undefined,
-    rwUserIds: undefined,
-    endStatus: undefined,
-    endRemark: undefined,
-    contactLastTime: undefined,
-    followUpStatus: undefined
+    products: [],
+    contactId: undefined,
+    customerDefault: false
   }
   formRef.value?.resetFields()
 }
-
-/** 加载商机状态列表 */
-const changeBusinessStatusType = async (typeId: number) => {
-  businessStatusList.value = await BusinessStatusTypeApi.getBusinessStatusListByTypeId(typeId)
-}
-
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  name: null,
-  mobile: null,
-  industryId: null,
-  level: null,
-  source: null,
-  pool: false
-})
-// 选择客户
-const showCustomer = ref(false)
-const openCustomerSelect = () => {
-  showCustomer.value = !showCustomer.value
-  queryParams.pageNo = 1
-  getCustomerList()
-}
-
-/** 查询客户列表 */
-const getCustomerList = async () => {
-  loading.value = true
-  try {
-    const data = await CustomerApi.getCustomerPage(queryParams)
-    customerList.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-const multipleTableRef = ref<InstanceType<typeof ElTable>>()
-const multipleSelection = ref()
-const handleSelectionChange = ({}, row) => {
-  multipleSelection.value = row
-  multipleTableRef.value!.clearSelection()
-  multipleTableRef.value!.toggleRowSelection(row, undefined)
-}
-
-const selectCustomer = () => {
-  formData.value.customerId = multipleSelection.value.id
-  formData.value.customerName = multipleSelection.value.name
-  showCustomer.value = !showCustomer.value
-}
 </script>

+ 108 - 0
src/views/crm/business/BusinessUpdateStatusForm.vue

@@ -0,0 +1,108 @@
+<template>
+  <Dialog title="变更商机状态" v-model="dialogVisible" width="400">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="商机阶段" prop="status">
+        <el-select v-model="formData.status" placeholder="请选择商机阶段" class="w-1/1">
+          <el-option
+            v-for="item in statusList"
+            :key="item.id"
+            :label="item.name + '(赢单率:' + item.percent + '%)'"
+            :value="item.id"
+          />
+          <el-option
+            v-for="item in BusinessStatusApi.DEFAULT_STATUSES"
+            :key="item.endStatus"
+            :label="item.name + '(赢单率:' + item.percent + '%)'"
+            :value="-item.endStatus"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as BusinessApi from '@/api/crm/business'
+import * as BusinessStatusApi from '@/api/crm/business/status'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中
+const formData = ref({
+  id: undefined,
+  statusId: undefined,
+  endStatus: undefined,
+  status: undefined
+})
+const formRules = reactive({
+  status: [{ required: true, message: '商机阶段不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const statusList = ref([]) // 商机状态列表
+
+/** 打开弹窗 */
+const open = async (business: BusinessApi.BusinessVO) => {
+  dialogVisible.value = true
+  resetForm()
+  formData.value = {
+    id: business.id,
+    statusId: business.statusId,
+    endStatus: business.endStatus,
+    status: business.endStatus != null ? -business.endStatus : business.statusId
+  }
+  // 加载状态列表
+  formLoading.value = true
+  try {
+    statusList.value = await BusinessStatusApi.getBusinessStatusSimpleList(business.statusTypeId)
+  } finally {
+    formLoading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    await BusinessApi.updateBusinessStatus({
+      id: formData.value.id,
+      statusId: formData.value.status > 0 ? formData.value.status : undefined,
+      endStatus: formData.value.status < 0 ? -formData.value.status : undefined
+    })
+    message.success('更新商机状态成功')
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    statusId: undefined,
+    endStatus: undefined,
+    status: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 9 - 3
src/views/crm/business/components/BusinessList.vue

@@ -38,7 +38,12 @@
           </el-link>
         </template>
       </el-table-column>
-      <el-table-column label="商机金额" align="center" prop="price" :formatter="fenToYuanFormat" />
+      <el-table-column
+        label="商机金额"
+        align="center"
+        prop="price"
+        :formatter="erpPriceTableColumnFormatter"
+      />
       <el-table-column label="客户名称" align="center" prop="customerName" />
       <el-table-column label="商机组" align="center" prop="statusTypeName" />
       <el-table-column label="商机阶段" align="center" prop="statusName" />
@@ -66,8 +71,8 @@ import * as BusinessApi from '@/api/crm/business'
 import * as ContactApi from '@/api/crm/contact'
 import BusinessForm from './../BusinessForm.vue'
 import { BizTypeEnum } from '@/api/crm/permission'
-import { fenToYuanFormat } from '@/utils/formatter'
 import BusinessListModal from './BusinessListModal.vue'
+import { erpPriceTableColumnFormatter } from '@/utils'
 
 const message = useMessage() // 消息
 
@@ -76,6 +81,7 @@ const props = defineProps<{
   bizType: number // 业务类型
   bizId: number // 业务编号
   customerId?: number // 关联联系人与商机时,需要传入 customerId 进行筛选
+  contactId?: number // 特殊:联系人编号;在【联系人】详情中,可以传递联系人编号,默认新建的商机关联到该联系人
 }>()
 
 const loading = ref(true) // 列表的加载中
@@ -125,7 +131,7 @@ const handleQuery = () => {
 /** 添加操作 */
 const formRef = ref()
 const openForm = () => {
-  formRef.value.open('create')
+  formRef.value.open('create', null, props.customerId, props.contactId)
 }
 
 /** 打开联系人详情 */

+ 6 - 5
src/views/crm/business/components/BusinessListModal.vue

@@ -48,8 +48,8 @@
         <el-table-column
           label="商机金额"
           align="center"
-          prop="price"
-          :formatter="fenToYuanFormat"
+          prop="totalPrice"
+          :formatter="erpPriceTableColumnFormatter"
         />
         <el-table-column label="客户名称" align="center" prop="customerName" />
         <el-table-column label="商机组" align="center" prop="statusTypeName" />
@@ -75,7 +75,7 @@
 <script setup lang="ts">
 import * as BusinessApi from '@/api/crm/business'
 import BusinessForm from '../BusinessForm.vue'
-import { fenToYuanFormat } from '@/utils/formatter'
+import { erpPriceTableColumnFormatter } from '@/utils'
 
 const message = useMessage() // 消息弹窗
 const props = defineProps<{
@@ -99,6 +99,7 @@ const queryParams = reactive({
 /** 打开弹窗 */
 const open = async () => {
   dialogVisible.value = true
+  queryParams.customerId = props.customerId // 解决 props.customerId 没更新到 queryParams 上的问题
   await getList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
@@ -144,10 +145,10 @@ const submitForm = async () => {
     return message.error('未选择商机')
   }
   dialogVisible.value = false
-  emit('success', businessIds)
+  emit('success', businessIds, businessRef.value.getSelectionRows())
 }
 
-/** 打开联系人详情 */
+/** 打开商机详情 */
 const { push } = useRouter()
 const openDetail = (id: number) => {
   push({ name: 'CrmBusinessDetail', params: { id } })

+ 183 - 0
src/views/crm/business/components/BusinessProductForm.vue

@@ -0,0 +1,183 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    v-loading="formLoading"
+    label-width="0px"
+    :inline-message="true"
+    :disabled="disabled"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="序号" type="index" align="center" width="60" />
+      <el-table-column label="产品名称" min-width="180">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+            <el-select
+              v-model="row.productId"
+              clearable
+              filterable
+              @change="onChangeProduct($event, row)"
+              placeholder="请选择产品"
+            >
+              <el-option
+                v-for="item in productList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="条码" min-width="150">
+        <template #default="{ row }">
+          <el-form-item class="mb-0px!">
+            <el-input disabled v-model="row.productNo" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="单位" min-width="80">
+        <template #default="{ row }">
+          <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="row.productUnit" />
+        </template>
+      </el-table-column>
+      <el-table-column label="价格(元)" min-width="120">
+        <template #default="{ row }">
+          <el-form-item class="mb-0px!">
+            <el-input disabled v-model="row.productPrice" :formatter="erpPriceInputFormatter" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="售价(元)" fixed="right" min-width="140">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.businessPrice`" class="mb-0px!">
+            <el-input-number
+              v-model="row.businessPrice"
+              controls-position="right"
+              :min="0.001"
+              :precision="2"
+              class="!w-100%"
+            />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="数量" prop="count" fixed="right" min-width="120">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+            <el-input-number
+              v-model="row.count"
+              controls-position="right"
+              :min="0.001"
+              :precision="3"
+              class="!w-100%"
+            />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="合计" prop="totalPrice" fixed="right" min-width="140">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+            <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link>—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-form>
+  <el-row justify="center" class="mt-3" v-if="!disabled">
+    <el-button @click="handleAdd" round>+ 添加产品</el-button>
+  </el-row>
+</template>
+<script setup lang="ts">
+import * as ProductApi from '@/api/crm/product'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import { DICT_TYPE } from '@/utils/dict'
+
+const props = defineProps<{
+  products: undefined
+  disabled: false
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
+  businessPrice: [{ required: true, message: '合同价格不能为空', trigger: 'blur' }],
+  count: [{ required: true, message: '产品数量不能为空', trigger: 'blur' }]
+})
+const formRef = ref([]) // 表单 Ref
+const productList = ref<ProductApi.ProductVO[]>([]) // 产品列表
+
+/** 初始化设置产品项 */
+watch(
+  () => props.products,
+  async (val) => {
+    formData.value = val
+  },
+  { immediate: true }
+)
+
+/** 监听合同产品变化,计算合同产品总价 */
+watch(
+  () => formData.value,
+  (val) => {
+    if (!val || val.length === 0) {
+      return
+    }
+    // 循环处理
+    val.forEach((item) => {
+      if (item.businessPrice != null && item.count != null) {
+        item.totalPrice = erpPriceMultiply(item.businessPrice, item.count)
+      } else {
+        item.totalPrice = undefined
+      }
+    })
+  },
+  { deep: true }
+)
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  const row = {
+    id: undefined,
+    productId: undefined,
+    productUnit: undefined, // 产品单位
+    productNo: undefined, // 产品条码
+    productPrice: undefined, // 产品价格
+    businessPrice: undefined,
+    count: 1
+  }
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index: number) => {
+  formData.value.splice(index, 1)
+}
+
+/** 处理产品变更 */
+const onChangeProduct = (productId, row) => {
+  const product = productList.value.find((item) => item.id === productId)
+  if (product) {
+    row.productUnit = product.unit
+    row.productNo = product.no
+    row.productPrice = product.price
+    row.businessPrice = product.price
+  }
+}
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 初始化 */
+onMounted(async () => {
+  productList.value = await ProductApi.getProductSimpleList()
+})
+</script>

+ 37 - 0
src/views/crm/business/detail/BusinessDetailsHeader.vue

@@ -0,0 +1,37 @@
+<template>
+  <div>
+    <div class="flex items-start justify-between">
+      <div>
+        <el-col>
+          <el-row>
+            <span class="text-xl font-bold">{{ business.name }}</span>
+          </el-row>
+        </el-col>
+      </div>
+      <div>
+        <!-- 右上:按钮 -->
+        <slot></slot>
+      </div>
+    </div>
+  </div>
+  <ContentWrap class="mt-10px">
+    <el-descriptions :column="5" direction="vertical">
+      <el-descriptions-item label="客户名称">{{ business.customerName }}</el-descriptions-item>
+      <el-descriptions-item label="商机金额(元)">
+        {{ erpPriceInputFormatter(business.totalPrice) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="商机组">{{ business.statusTypeName }}</el-descriptions-item>
+      <el-descriptions-item label="负责人">{{ business.ownerUserName }}</el-descriptions-item>
+      <el-descriptions-item label="创建时间">
+        {{ formatDate(business.createTime) }}
+      </el-descriptions-item>
+    </el-descriptions>
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as BusinessApi from '@/api/crm/business'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { business } = defineProps<{ business: BusinessApi.BusinessVO }>()
+</script>

+ 61 - 0
src/views/crm/business/detail/BusinessDetailsInfo.vue

@@ -0,0 +1,61 @@
+<template>
+  <ContentWrap>
+    <el-collapse v-model="activeNames">
+      <el-collapse-item name="basicInfo">
+        <template #title>
+          <span class="text-base font-bold">基本信息</span>
+        </template>
+        <el-descriptions :column="4">
+          <el-descriptions-item label="商机姓名">{{ business.name }}</el-descriptions-item>
+          <el-descriptions-item label="客户名称">{{ business.customerName }}</el-descriptions-item>
+          <el-descriptions-item label="商机金额(元)">
+            {{ erpPriceInputFormatter(business.totalPrice) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="预计成交日期">
+            {{ formatDate(business.dealTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="下次联系时间">
+            {{ formatDate(business.contactNextTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="商机状态组">
+            {{ business.statusTypeName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="商机阶段">{{ business.statusName }}</el-descriptions-item>
+          <el-descriptions-item label="备注">{{ business.remark }}</el-descriptions-item>
+        </el-descriptions>
+      </el-collapse-item>
+      <el-collapse-item name="systemInfo">
+        <template #title>
+          <span class="text-base font-bold">系统信息</span>
+        </template>
+        <el-descriptions :column="4">
+          <el-descriptions-item label="负责人">{{ business.ownerUserName }}</el-descriptions-item>
+          <el-descriptions-item label="最后跟进时间">
+            {{ formatDate(business.contactLastTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="">&nbsp;</el-descriptions-item>
+          <el-descriptions-item label="">&nbsp;</el-descriptions-item>
+          <el-descriptions-item label="创建人">{{ business.creatorName }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间">
+            {{ formatDate(business.createTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="更新时间">
+            {{ formatDate(business.updateTime) }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-collapse-item>
+    </el-collapse>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as BusinessApi from '@/api/crm/business'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { business } = defineProps<{
+  business: BusinessApi.BusinessVO
+}>()
+
+// 展示的折叠面板
+const activeNames = ref(['basicInfo', 'systemInfo'])
+</script>

+ 66 - 0
src/views/crm/business/detail/BusinessProductList.vue

@@ -0,0 +1,66 @@
+<template>
+  <ContentWrap>
+    <el-table :data="business.products" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column
+        align="center"
+        label="产品名称"
+        fixed="left"
+        prop="productName"
+        min-width="160"
+      >
+        <template #default="scope">
+          {{ scope.row.productName }}
+        </template>
+      </el-table-column>
+      <el-table-column label="产品条码" align="center" prop="productNo" min-width="120" />
+      <el-table-column align="center" label="产品单位" prop="productUnit" min-width="160">
+        <template #default="{ row }">
+          <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="row.productUnit" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="产品价格(元)"
+        align="center"
+        prop="productPrice"
+        min-width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column
+        label="商机价格(元)"
+        align="center"
+        prop="businessPrice"
+        min-width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column
+        align="center"
+        label="数量"
+        prop="count"
+        min-width="100px"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column
+        label="合计金额(元)"
+        align="center"
+        prop="totalPrice"
+        min-width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+    </el-table>
+    <el-row class="mt-10px" justify="end">
+      <el-col :span="3"> 整单折扣:{{ erpPriceInputFormatter(business.discountPercent) }}% </el-col>
+      <el-col :span="4">
+        产品总金额:{{ erpPriceInputFormatter(business.totalProductPrice) }} 元
+      </el-col>
+    </el-row>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as BusinessApi from '@/api/crm/business'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { DICT_TYPE } from '@/utils/dict'
+
+const { business } = defineProps<{
+  business: BusinessApi.BusinessVO
+}>()
+</script>

+ 146 - 0
src/views/crm/business/detail/index.vue

@@ -0,0 +1,146 @@
+<template>
+  <BusinessDetailsHeader v-loading="loading" :business="business">
+    <el-button v-if="permissionListRef?.validateWrite" @click="openForm('update', business.id)">
+      编辑
+    </el-button>
+    <el-button
+      :disabled="business.endStatus"
+      v-if="permissionListRef?.validateWrite"
+      type="success"
+      @click="openStatusForm()"
+    >
+      变更商机状态
+    </el-button>
+    <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer">
+      转移
+    </el-button>
+  </BusinessDetailsHeader>
+  <el-col>
+    <el-tabs>
+      <el-tab-pane label="跟进记录">
+        <FollowUpList :biz-id="businessId" :biz-type="BizTypeEnum.CRM_BUSINESS" />
+      </el-tab-pane>
+      <el-tab-pane label="详细资料">
+        <BusinessDetailsInfo :business="business" />
+      </el-tab-pane>
+      <el-tab-pane label="联系人" lazy>
+        <ContactList
+          :biz-id="business.id!"
+          :biz-type="BizTypeEnum.CRM_BUSINESS"
+          :business-id="business.id"
+          :customer-id="business.customerId"
+        />
+      </el-tab-pane>
+      <el-tab-pane label="产品">
+        <BusinessProductList :business="business" />
+      </el-tab-pane>
+      <el-tab-pane label="合同" lazy>
+        <ContractList :biz-id="business.id!" :biz-type="BizTypeEnum.CRM_BUSINESS" />
+      </el-tab-pane>
+      <el-tab-pane label="操作日志">
+        <OperateLogV2 :log-list="logList" />
+      </el-tab-pane>
+      <el-tab-pane label="团队成员">
+        <PermissionList
+          ref="permissionListRef"
+          :biz-id="business.id!"
+          :biz-type="BizTypeEnum.CRM_BUSINESS"
+          :show-action="true"
+          @quit-team="close"
+        />
+      </el-tab-pane>
+    </el-tabs>
+  </el-col>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <BusinessForm ref="formRef" @success="getBusiness(business.id)" />
+  <BusinessUpdateStatusForm ref="statusFormRef" @success="getBusiness(business.id)" />
+  <CrmTransferForm ref="transferFormRef" @success="close" />
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ContactApi from '@/api/crm/contact'
+import * as BusinessApi from '@/api/crm/business'
+import BusinessDetailsHeader from './BusinessDetailsHeader.vue'
+import BusinessDetailsInfo from './BusinessDetailsInfo.vue'
+import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
+import { BizTypeEnum } from '@/api/crm/permission'
+import { OperateLogV2VO } from '@/api/system/operatelog'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+import BusinessForm from '@/views/crm/business/BusinessForm.vue'
+import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
+import FollowUpList from '@/views/crm/followup/index.vue'
+import ContactList from '@/views/crm/contact/components/ContactList.vue'
+import BusinessUpdateStatusForm from '@/views/crm/business/BusinessUpdateStatusForm.vue'
+import ContractList from '@/views/crm/contract/components/ContractList.vue'
+
+defineOptions({ name: 'CrmBusinessDetail' })
+
+const message = useMessage()
+
+const businessId = ref(0) // 线索编号
+const loading = ref(true) // 加载中
+const business = ref<ContactApi.ContactVO>({} as ContactApi.ContactVO) // 联系人详情
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
+
+/** 获取详情 */
+const getBusiness = async (id: number) => {
+  loading.value = true
+  try {
+    business.value = await BusinessApi.getBusiness(id)
+    await getOperateLog(id)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 编辑 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 变更商机状态 */
+const statusFormRef = ref()
+const openStatusForm = () => {
+  statusFormRef.value.open(business.value)
+}
+
+/** 联系人转移 */
+const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 联系人转移表单 ref
+const transfer = () => {
+  transferFormRef.value?.open('商机转移', business.value.id, BusinessApi.transferBusiness)
+}
+
+/** 获取操作日志 */
+const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
+const getOperateLog = async (contactId: number) => {
+  if (!contactId) {
+    return
+  }
+  const data = await getOperateLogPage({
+    bizType: BizTypeEnum.CRM_BUSINESS,
+    bizId: contactId
+  })
+  logList.value = data.list
+}
+
+/** 关闭窗口 */
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute } = useRouter() // 路由
+const close = () => {
+  delView(unref(currentRoute))
+}
+
+/** 初始化 */
+const { params } = useRoute()
+onMounted(async () => {
+  if (!params.id) {
+    message.warning('参数错误,商机不能为空!')
+    close()
+    return
+  }
+  businessId.value = params.id as unknown as number
+  await getBusiness(businessId.value)
+})
+</script>

+ 84 - 27
src/views/crm/business/index.vue

@@ -38,10 +38,37 @@
 
   <!-- 列表 -->
   <ContentWrap>
+    <el-tabs v-model="activeName" @tab-click="handleTabClick">
+      <el-tab-pane label="我负责的" name="1" />
+      <el-tab-pane label="我参与的" name="2" />
+      <el-tab-pane label="下属负责的" name="3" />
+    </el-tabs>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="商机名称" align="center" prop="name" />
-      <el-table-column label="客户名称" align="center" prop="customerName" />
-      <el-table-column label="商机金额" align="center" prop="price" />
+      <el-table-column align="center" label="商机名称" fixed="left" prop="name" width="160">
+        <template #default="scope">
+          <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+            {{ scope.row.name }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="left" label="客户名称" prop="customerName" width="120">
+        <template #default="scope">
+          <el-link
+            :underline="false"
+            type="primary"
+            @click="openCustomerDetail(scope.row.customerId)"
+          >
+            {{ scope.row.customerName }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="商机金额(元)"
+        align="center"
+        prop="totalPrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
       <el-table-column
         label="预计成交日期"
         align="center"
@@ -49,9 +76,23 @@
         :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column label="备注" align="center" prop="remark" />
-      <el-table-column label="商机状态类型" align="center" prop="statusTypeName" />
-      <el-table-column label="商机状态" align="center" prop="statusName" />
+      <el-table-column align="center" label="备注" prop="remark" width="200" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="下次联系时间"
+        prop="contactNextTime"
+        width="180px"
+      />
+      <el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" />
+      <el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="最后跟进时间"
+        prop="contactLastTime"
+        width="180px"
+      />
       <el-table-column
         label="更新时间"
         align="center"
@@ -66,9 +107,21 @@
         :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column label="负责人" align="center" prop="ownerUserId" />
-      <el-table-column label="创建人" align="center" prop="creator" />
-      <el-table-column label="跟进状态" align="center" prop="followUpStatus" />
+      <el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
+      <el-table-column
+        label="商机状态组"
+        align="center"
+        prop="statusTypeName"
+        fixed="right"
+        width="140"
+      />
+      <el-table-column
+        label="商机阶段"
+        align="center"
+        prop="statusName"
+        fixed="right"
+        width="120"
+      />
       <el-table-column label="操作" align="center" fixed="right" width="130px">
         <template #default="scope">
           <el-button
@@ -108,6 +161,8 @@ import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as BusinessApi from '@/api/crm/business'
 import BusinessForm from './BusinessForm.vue'
+import { erpPriceTableColumnFormatter } from '@/utils'
+import { TabsPaneContext } from 'element-plus'
 
 defineOptions({ name: 'CrmBusiness' })
 
@@ -120,27 +175,12 @@ const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  name: null,
-  statusTypeId: null,
-  statusId: null,
-  contactNextTime: [],
-  customerId: null,
-  dealTime: [],
-  price: null,
-  discountPercent: null,
-  productPrice: null,
-  remark: null,
-  ownerUserId: null,
-  createTime: [],
-  roUserIds: null,
-  rwUserIds: null,
-  endStatus: null,
-  endRemark: null,
-  contactLastTime: [],
-  followUpStatus: null
+  sceneType: '1', // 默认和 activeName 相等
+  name: null
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
+const activeName = ref('1') // 列表 tab
 
 /** 查询列表 */
 const getList = async () => {
@@ -166,6 +206,23 @@ const resetQuery = () => {
   handleQuery()
 }
 
+/** tab 切换 */
+const handleTabClick = (tab: TabsPaneContext) => {
+  queryParams.sceneType = tab.paneName
+  handleQuery()
+}
+
+/** 打开客户详情 */
+const { currentRoute, push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 打开客户详情 */
+const openCustomerDetail = (id: number) => {
+  push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {

+ 56 - 29
src/views/crm/businessStatusType/BusinessStatusTypeForm.vue → src/views/crm/business/status/BusinessStatusForm.vue

@@ -7,10 +7,13 @@
       label-width="100px"
       v-loading="formLoading"
     >
-      <el-form-item label="状态类型名" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入状态类型名" />
+      <el-form-item label="状态名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入状态名" />
       </el-form-item>
       <el-form-item label="应用部门" prop="deptIds">
+        <template #label>
+          <Tooltip message="不选择部门时,默认全公司生效" title="应用部门" />
+        </template>
         <el-tree
           ref="treeRef"
           :data="deptList"
@@ -21,31 +24,55 @@
           show-checkbox
         />
       </el-form-item>
-      <el-form-item label="状态设置" prop="statusList">
-        <el-table border style="width: 100%" :data="formData.statusList">
-          <el-table-column align="center" label="状态" width="120" prop="star">
+      <el-form-item label="阶段设置" prop="statuses">
+        <el-table
+          border
+          style="width: 100%"
+          :data="formData.statuses.concat(BusinessStatusApi.DEFAULT_STATUSES)"
+        >
+          <el-table-column align="center" label="阶段" width="70">
             <template #default="scope">
-              <el-text>状态{{ scope.$index + 1 }}</el-text>
+              <el-text v-if="!scope.row.defaultStatus">阶段 {{ scope.$index + 1 }}</el-text>
+              <el-text v-else>结束</el-text>
             </template>
           </el-table-column>
-          <el-table-column align="center" label="状态名称" width="120" prop="name">
+          <el-table-column align="center" label="阶段名称" width="160" prop="name">
             <template #default="{ row }">
-              <el-input v-model="row.name" placeholder="请输入状态名称" />
+              <el-input v-if="!row.endStatus" v-model="row.name" placeholder="请输入状态名称" />
+              <el-text v-else>{{ row.name }}</el-text>
             </template>
           </el-table-column>
-          <el-table-column width="120" align="center" label="赢单率" prop="percent">
+          <el-table-column width="140" align="center" label="赢单率(%)" prop="percent">
             <template #default="{ row }">
-              <el-input v-model="row.percent" placeholder="请输入赢单率" />
+              <el-input-number
+                v-if="!row.endStatus"
+                v-model="row.percent"
+                placeholder="请输入赢单率"
+                controls-position="right"
+                :min="0"
+                :max="100"
+                :precision="2"
+                class="!w-1/1"
+              />
+              <el-text v-else>{{ row.percent }}</el-text>
             </template>
           </el-table-column>
-          <el-table-column label="操作" align="center">
+          <el-table-column label="操作" width="110" align="center">
             <template #default="scope">
-              <el-button link type="primary" @click="addStatusArea(scope.$index)"> 添加 </el-button>
               <el-button
+                v-if="!scope.row.endStatus"
+                link
+                type="primary"
+                @click="addStatus(scope.$index)"
+              >
+                添加
+              </el-button>
+              <el-button
+                v-if="!scope.row.endStatus"
                 link
                 type="danger"
                 @click="deleteStatusArea(scope.$index)"
-                v-show="scope.$index > 0"
+                :disabled="formData.statuses.length <= 1"
               >
                 删除
               </el-button>
@@ -61,7 +88,7 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import * as BusinessStatusTypeApi from '@/api/crm/businessStatusType'
+import * as BusinessStatusApi from '@/api/crm/business/status'
 import { defaultProps, handleTree } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 
@@ -71,15 +98,15 @@ const message = useMessage() // 消息弹窗
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formType = ref('') // 表单的:create - 新增;update - 修改
 const formData = ref({
   id: 0,
   name: '',
   deptIds: [],
-  statusList: []
+  statuses: []
 })
 const formRules = reactive({
-  name: [{ required: true, message: '状态类型名不能为空', trigger: 'blur' }]
+  name: [{ required: true, message: '状态名不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 const deptList = ref<Tree[]>([]) // 树形结构
@@ -96,16 +123,16 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await BusinessStatusTypeApi.getBusinessStatusType(id)
+      formData.value = await BusinessStatusApi.getBusinessStatus(id)
       treeRef.value.setCheckedKeys(formData.value.deptIds)
-      if (formData.value.statusList.length == 0) {
-        addStatusArea(0)
+      if (formData.value.statuses.length == 0) {
+        addStatus()
       }
     } finally {
       formLoading.value = false
     }
   } else {
-    addStatusArea(0)
+    addStatus()
   }
   // 加载部门树
   deptList.value = handleTree(await DeptApi.getSimpleDeptList())
@@ -120,13 +147,13 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as BusinessStatusTypeApi.BusinessStatusTypeVO
+    const data = formData.value as unknown as BusinessStatusApi.BusinessStatusTypeVO
     data.deptIds = treeRef.value.getCheckedKeys(false)
     if (formType.value === 'create') {
-      await BusinessStatusTypeApi.createBusinessStatusType(data)
+      await BusinessStatusApi.createBusinessStatus(data)
       message.success(t('common.createSuccess'))
     } else {
-      await BusinessStatusTypeApi.updateBusinessStatusType(data)
+      await BusinessStatusApi.updateBusinessStatus(data)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false
@@ -144,24 +171,24 @@ const resetForm = () => {
     id: 0,
     name: '',
     deptIds: [],
-    statusList: []
+    statuses: []
   }
   treeRef.value?.setCheckedNodes([])
   formRef.value?.resetFields()
 }
 
 /** 添加状态 */
-const addStatusArea = () => {
+const addStatus = () => {
   const data = formData.value
-  data.statusList.push({
+  data.statuses.push({
     name: '',
-    percent: ''
+    percent: undefined
   })
 }
 
 /** 删除状态 */
 const deleteStatusArea = (index: number) => {
   const data = formData.value
-  data.statusList.splice(index, 1)
+  data.statuses.splice(index, 1)
 }
 </script>

+ 19 - 43
src/views/crm/businessStatusType/index.vue → src/views/crm/business/status/index.vue

@@ -9,25 +9,14 @@
       label-width="68px"
     >
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
         <el-button
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['crm:business-status-type:create']"
+          v-hasPermi="['crm:business-status:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['crm:business-status-type:export']"
-        >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
-        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -35,8 +24,15 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="状态类型名" align="center" prop="name" />
-      <el-table-column label="使用的部门" align="center" prop="deptNames" />
+      <el-table-column label="状态组名" align="center" prop="name" />
+      <el-table-column label="应用部门" align="center" prop="deptNames">
+        <template #default="scope">
+          <span v-if="scope.row?.deptNames?.length > 0">
+            {{ scope.row.deptNames.join(' ') }}
+          </span>
+          <span v-else>全公司</span>
+        </template>
+      </el-table-column>
       <el-table-column label="创建人" align="center" prop="creator" />
       <el-table-column
         label="创建时间"
@@ -51,7 +47,7 @@
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['crm:business-status-type:update']"
+            v-hasPermi="['crm:business-status:update']"
           >
             编辑
           </el-button>
@@ -59,7 +55,7 @@
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['crm:business-status-type:delete']"
+            v-hasPermi="['crm:business-status:delete']"
           >
             删除
           </el-button>
@@ -76,16 +72,17 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <BusinessStatusTypeForm ref="formRef" @success="getList" />
+  <BusinessStatusForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
-import * as BusinessStatusTypeApi from '@/api/crm/businessStatusType'
-import BusinessStatusTypeForm from './BusinessStatusTypeForm.vue'
+import * as BusinessStatusApi from '@/api/crm/business/status'
+import BusinessStatusForm from './BusinessStatusForm.vue'
+import { deleteBusinessStatus } from '@/api/crm/business/status'
 
-defineOptions({ name: 'BusinessStatusType' })
+defineOptions({ name: 'CrmBusinessStatus' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
@@ -104,7 +101,7 @@ const exportLoading = ref(false) // 导出的加载中
 const getList = async () => {
   loading.value = true
   try {
-    const data = await BusinessStatusTypeApi.getBusinessStatusTypePage(queryParams)
+    const data = await BusinessStatusApi.getBusinessStatusPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -130,40 +127,19 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
-/** 选择客户操作 */
-const formCustomerRef = ref()
-const openCustomerForm = (id?: number) => {
-  formCustomerRef.value.open(id)
-}
-
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await BusinessStatusTypeApi.deleteBusinessStatusType(id)
+    await BusinessStatusApi.deleteBusinessStatus(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
   } catch {}
 }
 
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await BusinessStatusTypeApi.exportBusinessStatusType(queryParams)
-    download.excel(data, '商机状态类型.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
-
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 1 - 1
src/views/crm/clue/detail/index.vue

@@ -33,7 +33,7 @@
           ref="permissionListRef"
           :biz-id="clue.id!"
           :biz-type="BizTypeEnum.CRM_CLUE"
-          :show-action="!permissionListRef?.isPool || false"
+          :show-action="true"
           @quit-team="close"
         />
       </el-tab-pane>

+ 119 - 119
src/views/crm/contact/ContactForm.vue

@@ -1,28 +1,27 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" :width="820">
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
     <el-form
       ref="formRef"
       v-loading="formLoading"
       :model="formData"
       :rules="formRules"
-      label-width="110px"
+      label-width="100px"
     >
-      <el-row :gutter="20">
+      <el-row>
         <el-col :span="12">
-          <el-form-item label="姓名" prop="name">
-            <el-input v-model="formData.name" input-style="width:190px;" placeholder="请输入姓名" />
+          <el-form-item label="联系人姓名" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入姓名" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="负责人" prop="ownerUserId">
             <el-select
               v-model="formData.ownerUserId"
-              lable-key="nickname"
-              placeholder="请选择负责人"
-              value-key="id"
+              :disabled="formType !== 'create'"
+              class="w-1/1"
             >
               <el-option
-                v-for="item in userList"
+                v-for="item in userOptions"
                 :key="item.id"
                 :label="item.nickname"
                 :value="item.id"
@@ -33,12 +32,12 @@
       </el-row>
       <el-row>
         <el-col :span="12">
-          <el-form-item label="客户名称" prop="customerName">
+          <el-form-item label="客户名称" prop="customerId">
             <el-select
+              :disabled="formData.customerDefault"
               v-model="formData.customerId"
-              lable-key="name"
               placeholder="请选择客户"
-              value-key="id"
+              class="w-1/1"
             >
               <el-option
                 v-for="item in customerList"
@@ -50,98 +49,73 @@
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="性别" prop="sex">
-            <el-select v-model="formData.sex" placeholder="请选择">
-              <el-option
-                v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
+          <el-form-item label="手机" prop="mobile">
+            <el-input v-model="formData.mobile" placeholder="请输入手机" />
           </el-form-item>
         </el-col>
       </el-row>
       <el-row>
         <el-col :span="12">
-          <el-form-item label="手机号" prop="mobile">
-            <el-input
-              v-model="formData.mobile"
-              input-style="width:190px;"
-              placeholder="请输入手机号"
-            />
+          <el-form-item label="电话" prop="telephone">
+            <el-input v-model="formData.telephone" placeholder="请输入电话" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="电话" prop="telephone">
-            <el-input v-model="formData.telephone" placeholder="请输入电话" style="width: 215px" />
+          <el-form-item label="邮箱" prop="email">
+            <el-input v-model="formData.email" placeholder="请输入邮箱" />
           </el-form-item>
         </el-col>
       </el-row>
       <el-row>
         <el-col :span="12">
-          <el-form-item label="邮箱" prop="email">
-            <el-input
-              v-model="formData.email"
-              input-style="width:190px;"
-              placeholder="请输入邮箱"
-            />
+          <el-form-item label="微信" prop="wechat">
+            <el-input v-model="formData.wechat" placeholder="请输入微信" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="QQ" prop="qq">
-            <el-input v-model="formData.qq" placeholder="请输入QQ" style="width: 215px" />
+            <el-input v-model="formData.qq" placeholder="请输入 QQ" />
           </el-form-item>
         </el-col>
       </el-row>
       <el-row>
         <el-col :span="12">
-          <el-form-item label="微信" prop="wechat">
-            <el-input
-              v-model="formData.wechat"
-              input-style="width:190px;"
-              placeholder="请输入微信"
-            />
+          <el-form-item label="职位" prop="post">
+            <el-input v-model="formData.post" placeholder="请输入职位" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="下次联系时间" prop="contactNextTime">
-            <el-date-picker
-              v-model="formData.contactNextTime"
-              placeholder="选择下次联系时间"
-              type="datetime"
-              value-format="x"
-            />
+          <el-form-item label="关键决策人" prop="master" style="width: 400px">
+            <el-radio-group v-model="formData.master">
+              <el-radio
+                v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                :key="dict.value"
+                :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-radio>
+            </el-radio-group>
           </el-form-item>
         </el-col>
       </el-row>
       <el-row>
         <el-col :span="12">
-          <el-form-item label="所在地" prop="areaId">
-            <el-tree-select
-              v-model="formData.areaId"
-              :data="areaList"
-              :props="defaultProps"
-              :render-after-expand="true"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="地址" prop="detailAddress">
-            <el-input
-              v-model="formData.detailAddress"
-              input-style="width:190px;"
-              placeholder="请输入地址"
-            />
+          <el-form-item label="性别" prop="sex">
+            <el-select v-model="formData.sex" placeholder="请选择" class="w-1/1">
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
           </el-form-item>
         </el-col>
-      </el-row>
-      <el-row>
         <el-col :span="12">
           <el-form-item label="直属上级" prop="parentId">
-            <el-select v-model="formData.parentId" placeholder="请选择">
+            <el-select v-model="formData.parentId" placeholder="请选择直属上级" class="w-1/1">
               <el-option
-                v-for="item in allContactList"
+                v-for="item in contactList"
                 :key="item.id"
                 :disabled="item.id == formData.id"
                 :label="item.name"
@@ -150,31 +124,41 @@
             </el-select>
           </el-form-item>
         </el-col>
+      </el-row>
+      <el-row>
         <el-col :span="12">
-          <el-form-item label="职位" prop="post">
-            <el-input v-model="formData.post" input-style="width:190px;" placeholder="请输入职位" />
+          <el-form-item label="地址" prop="areaId">
+            <el-cascader
+              v-model="formData.areaId"
+              :options="areaList"
+              :props="defaultProps"
+              class="w-1/1"
+              clearable
+              filterable
+              placeholder="请选择城市"
+            />
           </el-form-item>
         </el-col>
-      </el-row>
-      <el-row>
         <el-col :span="12">
-          <el-form-item label="关键决策人" prop="master" style="width: 400px">
-            <el-radio-group v-model="formData.master">
-              <el-radio
-                v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-                :key="dict.value"
-                :label="dict.value"
-              >
-                {{ dict.label }}
-              </el-radio>
-            </el-radio-group>
+          <el-form-item label="详细地址" prop="detailAddress">
+            <el-input v-model="formData.detailAddress" placeholder="请输入详细地址" />
           </el-form-item>
         </el-col>
       </el-row>
       <el-row>
-        <el-col :span="24">
+        <el-col :span="12">
+          <el-form-item label="下次联系时间" prop="contactNextTime">
+            <el-date-picker
+              v-model="formData.contactNextTime"
+              placeholder="选择下次联系时间"
+              type="datetime"
+              value-format="x"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
           <el-form-item label="备注" prop="remark">
-            <el-input v-model="formData.remark" placeholder="请输入备注" />
+            <el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" />
           </el-form-item>
         </el-col>
       </el-row>
@@ -192,6 +176,7 @@ import * as UserApi from '@/api/system/user'
 import * as CustomerApi from '@/api/crm/customer'
 import * as AreaApi from '@/api/system/area'
 import { defaultProps } from '@/utils/tree'
+import { useUserStore } from '@/store/modules/user'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -202,25 +187,25 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const areaList = ref([]) // 地区列表
 const formData = ref({
+  id: undefined,
+  name: undefined,
+  customerId: undefined,
   contactNextTime: undefined,
+  ownerUserId: 0,
   mobile: undefined,
   telephone: undefined,
-  email: undefined,
-  customerId: undefined,
-  customerName: undefined,
-  detailAddress: undefined,
-  remark: undefined,
-  ownerUserId: undefined,
-  lastTime: undefined,
-  id: undefined,
-  parentId: undefined,
-  name: undefined,
-  post: undefined,
   qq: undefined,
   wechat: undefined,
+  email: undefined,
+  areaId: undefined,
+  detailAddress: undefined,
   sex: undefined,
   master: false,
-  areaId: undefined
+  post: undefined,
+  parentId: undefined,
+  remark: undefined,
+  businessId: undefined,
+  customerDefault: false
 })
 const formRules = reactive({
   name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
@@ -228,22 +213,17 @@ const formRules = reactive({
   ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const ownerUserList = ref<any[]>([])
-const userList = ref<UserApi.UserVO[]>([]) // 用户列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 // TODO 芋艿:统一的客户选择面板
 const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
-const allContactList = ref<ContactApi.ContactVO[]>([]) // 所有联系人列表
+const contactList = ref<ContactApi.ContactVO[]>([]) // 联系人列表
 
 /** 打开弹窗 */
-const open = async (type: string, id?: number) => {
+const open = async (type: string, id?: number, customerId?: number, businessId?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
-  allContactList.value = await ContactApi.getSimpleContactList()
-  userList.value = await UserApi.getSimpleUserList()
-  customerList.value = await CustomerApi.getCustomerSimpleList()
-  areaList.value = await AreaApi.getAreaTree()
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
@@ -252,6 +232,27 @@ const open = async (type: string, id?: number) => {
     } finally {
       formLoading.value = false
     }
+  } else {
+    if (customerId) {
+      formData.value.customerId = customerId
+      formData.value.customerDefault = true // 默认客户的选择,不允许变
+    }
+    // 自动关联 businessId 商机编号
+    if (businessId) {
+      formData.value.businessId = businessId
+    }
+  }
+  // 获得联系人列表
+  contactList.value = await ContactApi.getSimpleContactList()
+  // 获得客户列表
+  customerList.value = await CustomerApi.getCustomerSimpleList()
+  // 获得地区列表
+  areaList.value = await AreaApi.getAreaTree()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+  // 默认新建时选中自己
+  if (formType.value === 'create') {
+    formData.value.ownerUserId = useUserStore().getUser.id
   }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
@@ -259,7 +260,6 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
-  // owerSelectValue(ownerUserList)
   // 校验表单
   if (!formRef) return
   const valid = await formRef.value.validate()
@@ -285,27 +285,27 @@ const submitForm = async () => {
 
 /** 重置表单 */
 const resetForm = () => {
-  // TODO zyna:ide 告警,看看怎么去掉哈;
   formData.value = {
+    id: undefined,
+    name: undefined,
+    customerId: undefined,
     contactNextTime: undefined,
+    ownerUserId: 0,
     mobile: undefined,
     telephone: undefined,
-    email: undefined,
-    customerId: undefined,
-    detailAddress: undefined,
-    remark: undefined,
-    ownerUserId: undefined,
-    lastTime: undefined,
-    id: undefined,
-    parentId: undefined,
-    name: undefined,
-    post: undefined,
     qq: undefined,
     wechat: undefined,
+    email: undefined,
+    areaId: undefined,
+    detailAddress: undefined,
     sex: undefined,
-    master: false
+    master: false,
+    post: undefined,
+    parentId: undefined,
+    remark: undefined,
+    businessId: undefined,
+    customerDefault: false
   }
   formRef.value?.resetFields()
-  ownerUserList.value = []
 }
 </script>

+ 75 - 5
src/views/crm/contact/components/ContactList.vue

@@ -5,11 +5,32 @@
       <Icon class="mr-5px" icon="system-uicons:contacts" />
       创建联系人
     </el-button>
+    <el-button
+      @click="openBusinessModal"
+      v-hasPermi="['crm:contact:create-business']"
+      v-if="queryParams.businessId"
+    >
+      <Icon class="mr-5px" icon="ep:circle-plus" />关联
+    </el-button>
+    <el-button
+      @click="deleteContactBusinessList"
+      v-hasPermi="['crm:contact:delete-business']"
+      v-if="queryParams.businessId"
+    >
+      <Icon class="mr-5px" icon="ep:remove" />解除关联
+    </el-button>
   </el-row>
 
   <!-- 列表 -->
   <ContentWrap class="mt-10px">
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+    <el-table
+      ref="contactRef"
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+    >
+      <el-table-column type="selection" width="55" v-if="queryParams.businessId" />
       <el-table-column label="姓名" fixed="left" align="center" prop="name">
         <template #default="scope">
           <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
@@ -20,12 +41,11 @@
       <el-table-column label="手机号" align="center" prop="mobile" />
       <el-table-column label="职位" align="center" prop="post" />
       <el-table-column label="直属上级" align="center" prop="parentName" />
-      <el-table-column label="是否关键决策人" align="center" prop="master">
+      <el-table-column label="是否关键决策人" align="center" prop="master" min-width="100">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
         </template>
       </el-table-column>
-      <!-- TODO 芋艿:【操作:设为首要联系人】 -->
     </el-table>
     <!-- 分页 -->
     <Pagination
@@ -38,17 +58,26 @@
 
   <!-- 表单弹窗:添加 -->
   <ContactForm ref="formRef" @success="getList" />
+  <!-- 关联商机选择弹框 -->
+  <ContactListModal
+    ref="contactModalRef"
+    :customer-id="props.customerId"
+    @success="createContactBusinessList"
+  />
 </template>
 <script setup lang="ts">
 import * as ContactApi from '@/api/crm/contact'
 import ContactForm from './../ContactForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
 import { BizTypeEnum } from '@/api/crm/permission'
+import ContactListModal from './ContactListModal.vue'
 
 defineOptions({ name: 'CrmContactList' })
 const props = defineProps<{
   bizType: number // 业务类型
   bizId: number // 业务编号
+  customerId: number // 特殊:客户编号;在【商机】详情中,可以传递客户编号,默认新建的联系人关联到该客户
+  businessId: number // 特殊:商机编号;在【商机】详情中,可以传递商机编号,默认新建的联系人关联到该商机
 }>()
 
 const loading = ref(true) // 列表的加载中
@@ -57,8 +86,10 @@ const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  customerId: undefined as unknown // 允许 undefined + number
+  customerId: undefined as unknown, // 允许 undefined + number
+  businessId: undefined as unknown // 允许 undefined + number
 })
+const message = useMessage()
 
 /** 查询列表 */
 const getList = async () => {
@@ -73,6 +104,10 @@ const getList = async () => {
         queryParams.customerId = props.bizId
         data = await ContactApi.getContactPageByCustomer(queryParams)
         break
+      case BizTypeEnum.CRM_BUSINESS:
+        queryParams.businessId = props.bizId
+        data = await ContactApi.getContactPageByBusiness(queryParams)
+        break
       default:
         return
     }
@@ -92,7 +127,7 @@ const handleQuery = () => {
 /** 添加操作 */
 const formRef = ref()
 const openForm = () => {
-  formRef.value.open('create')
+  formRef.value.open('create', undefined, props.customerId, props.businessId)
 }
 
 /** 打开联系人详情 */
@@ -101,6 +136,41 @@ const openDetail = (id: number) => {
   push({ name: 'CrmContactDetail', params: { id } })
 }
 
+/** 打开联系人与商机的关联弹窗 */
+const contactModalRef = ref()
+const openBusinessModal = () => {
+  contactModalRef.value.open()
+}
+const createContactBusinessList = async (contactIds: number[]) => {
+  const data = {
+    businessId: props.bizId,
+    contactIds: contactIds
+  } as ContactApi.ContactBusiness2ReqVO
+  contactRef.value.getSelectionRows().forEach((row: ContactApi.ContactVO) => {
+    data.businessIds.push(row.id)
+  })
+  await ContactApi.createContactBusinessList2(data)
+  // 刷新列表
+  message.success('关联联系人成功')
+  handleQuery()
+}
+
+/** 解除联系人与商机的关联 */
+const contactRef = ref()
+const deleteContactBusinessList = async () => {
+  const data = {
+    businessId: props.bizId,
+    contactIds: contactRef.value.getSelectionRows().map((row: ContactApi.ContactVO) => row.id)
+  } as ContactApi.ContactBusiness2ReqVO
+  if (data.contactIds.length === 0) {
+    return message.error('未选择联系人')
+  }
+  await ContactApi.deleteContactBusinessList2(data)
+  // 刷新列表
+  message.success('取关联系人成功')
+  handleQuery()
+}
+
 /** 监听打开的 bizId + bizType,从而加载最新的列表 */
 watch(
   () => [props.bizId, props.bizType],

+ 154 - 0
src/views/crm/contact/components/ContactListModal.vue

@@ -0,0 +1,154 @@
+<template>
+  <Dialog title="关联联系人" v-model="dialogVisible">
+    <!-- 搜索工作栏 -->
+    <ContentWrap>
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="90px"
+      >
+        <el-form-item label="联系人名称" prop="name">
+          <el-input
+            v-model="queryParams.name"
+            placeholder="请输入联系人名称"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+          <el-button type="primary" @click="openForm()" v-hasPermi="['crm:business:create']">
+            <Icon icon="ep:plus" class="mr-5px" /> 新增
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <!-- 列表 -->
+    <ContentWrap class="mt-10px">
+      <el-table
+        v-loading="loading"
+        ref="contactRef"
+        :data="list"
+        :stripe="true"
+        :show-overflow-tooltip="true"
+      >
+        <el-table-column type="selection" width="55" />
+        <el-table-column label="姓名" fixed="left" align="center" prop="name">
+          <template #default="scope">
+            <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+              {{ scope.row.name }}
+            </el-link>
+          </template>
+        </el-table-column>
+        <el-table-column label="手机号" align="center" prop="mobile" />
+        <el-table-column label="职位" align="center" prop="post" />
+        <el-table-column label="直属上级" align="center" prop="parentName" />
+        <el-table-column label="是否关键决策人" align="center" prop="master" min-width="100">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+
+    <!-- 表单弹窗:添加 -->
+    <ContactForm ref="formRef" @success="getList" />
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as ContactApi from '@/api/crm/contact'
+import ContactForm from '../ContactForm.vue'
+import { erpPriceTableColumnFormatter } from '@/utils'
+import { DICT_TYPE } from '@/utils/dict'
+
+const message = useMessage() // 消息弹窗
+const props = defineProps<{
+  customerId: number
+}>()
+defineOptions({ name: 'ContactListModal' })
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryFormRef = ref() // 搜索的表单
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  customerId: props.customerId
+})
+
+/** 打开弹窗 */
+const open = async () => {
+  dialogVisible.value = true
+  queryParams.customerId = props.customerId // 解决 props.customerId 没更新到 queryParams 上的问题
+  await getList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ContactApi.getContactPageByCustomer(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加操作 */
+const formRef = ref()
+const openForm = () => {
+  formRef.value.open('create')
+}
+
+/** 关联联系人提交 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const contactRef = ref()
+const submitForm = async () => {
+  const contactIds = contactRef.value.getSelectionRows().map((row: ContactApi.ContactVO) => row.id)
+  if (contactIds.length === 0) {
+    return message.error('未选择联系人')
+  }
+  dialogVisible.value = false
+  emit('success', contactIds, contactRef.value.getSelectionRows())
+}
+
+/** 打开联系人详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'CrmContactDetail', params: { id } })
+}
+</script>

+ 4 - 10
src/views/crm/contact/detail/ContactDetailsHeader.vue

@@ -16,17 +16,11 @@
   </div>
   <ContentWrap class="mt-10px">
     <el-descriptions :column="5" direction="vertical">
-      <el-descriptions-item label="客户">
-        {{ contact.customerName }}
-      </el-descriptions-item>
-      <el-descriptions-item label="职务">
-        {{ contact.post }}
-      </el-descriptions-item>
-      <el-descriptions-item label="手机">
-        {{ contact.mobile }}
-      </el-descriptions-item>
+      <el-descriptions-item label="客户名称">{{ contact.customerName }}</el-descriptions-item>
+      <el-descriptions-item label="职务">{{ contact.post }}</el-descriptions-item>
+      <el-descriptions-item label="手机">{{ contact.mobile }}</el-descriptions-item>
       <el-descriptions-item label="创建时间">
-        {{ contact.createTime ? formatDate(contact.createTime) : '空' }}
+        {{ formatDate(contact.createTime) }}
       </el-descriptions-item>
     </el-descriptions>
   </ContentWrap>

+ 27 - 38
src/views/crm/contact/detail/ContactDetailsInfo.vue

@@ -6,60 +6,49 @@
           <span class="text-base font-bold">基本信息</span>
         </template>
         <el-descriptions :column="4">
-          <el-descriptions-item label="姓名">
-            {{ contact.name }}
-          </el-descriptions-item>
-          <el-descriptions-item label="客户">
-            {{ contact.customerName }}
-          </el-descriptions-item>
-          <el-descriptions-item label="手机">
-            {{ contact.mobile }}
-          </el-descriptions-item>
-          <el-descriptions-item label="座机">
-            {{ contact.telephone }}
-          </el-descriptions-item>
-          <el-descriptions-item label="邮箱">
-            {{ contact.email }}
-          </el-descriptions-item>
-          <el-descriptions-item label="QQ">
-            {{ contact.qq }}
-          </el-descriptions-item>
-          <el-descriptions-item label="微信">
-            {{ contact.wechat }}
-          </el-descriptions-item>
-          <el-descriptions-item label="下次联系时间">
-            {{ contact.nextTime ? formatDate(contact.nextTime) : '空' }}
-          </el-descriptions-item>
-          <el-descriptions-item label="所在地">
-            {{ contact.areaName }}
-          </el-descriptions-item>
-          <el-descriptions-item label="详细地址">
-            {{ contact.detailAddress }}
+          <el-descriptions-item label="姓名">{{ contact.name }}</el-descriptions-item>
+          <el-descriptions-item label="客户名称">{{ contact.customerName }}</el-descriptions-item>
+          <el-descriptions-item label="手机">{{ contact.mobile }}</el-descriptions-item>
+          <el-descriptions-item label="电话">{{ contact.telephone }}</el-descriptions-item>
+          <el-descriptions-item label="邮箱">{{ contact.email }}</el-descriptions-item>
+          <el-descriptions-item label="QQ">{{ contact.qq }}</el-descriptions-item>
+          <el-descriptions-item label="微信">{{ contact.wechat }}</el-descriptions-item>
+          <el-descriptions-item label="地址">
+            {{ contact.areaName }} {{ contact.detailAddress }}
+          </el-descriptions-item>
+          <el-descriptions-item label="职务">{{ contact.post }}</el-descriptions-item>
+          <el-descriptions-item label="直属上级">{{ contact.parentName }}</el-descriptions-item>
+          <el-descriptions-item label="关键决策人">
+            <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="contact.master" />
           </el-descriptions-item>
           <el-descriptions-item label="性别">
             <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="contact.sex" />
           </el-descriptions-item>
-          <el-descriptions-item label="备注">
-            {{ contact.remark }}
+          <el-descriptions-item label="下次联系时间">
+            {{ formatDate(contact.contactNextTime) }}
           </el-descriptions-item>
+          <el-descriptions-item label="备注">{{ contact.remark }}</el-descriptions-item>
         </el-descriptions>
       </el-collapse-item>
       <el-collapse-item name="systemInfo">
         <template #title>
           <span class="text-base font-bold">系统信息</span>
         </template>
-        <el-descriptions :column="2">
-          <el-descriptions-item label="负责人">
-            {{ contact.ownerUserName }}
+        <el-descriptions :column="4">
+          <el-descriptions-item label="负责人">{{ contact.ownerUserName }}</el-descriptions-item>
+          <el-descriptions-item label="最后跟进记录">
+            {{ contact.contactLastContent }}
           </el-descriptions-item>
-          <el-descriptions-item label="创建人">
-            {{ contact.creatorName }}
+          <el-descriptions-item label="最后跟进时间">
+            {{ formatDate(contact.contactLastTime) }}
           </el-descriptions-item>
+          <el-descriptions-item label="">&nbsp;</el-descriptions-item>
+          <el-descriptions-item label="创建人">{{ contact.creatorName }}</el-descriptions-item>
           <el-descriptions-item label="创建时间">
-            {{ contact.createTime ? formatDate(contact.createTime) : '空' }}
+            {{ formatDate(contact.createTime) }}
           </el-descriptions-item>
           <el-descriptions-item label="更新时间">
-            {{ contact.updateTime ? formatDate(contact.updateTime) : '空' }}
+            {{ formatDate(contact.updateTime) }}
           </el-descriptions-item>
         </el-descriptions>
       </el-collapse-item>

+ 19 - 12
src/views/crm/contact/detail/index.vue

@@ -9,6 +9,9 @@
   </ContactDetailsHeader>
   <el-col>
     <el-tabs>
+      <el-tab-pane label="跟进记录">
+        <FollowUpList :biz-id="contactId" :biz-type="BizTypeEnum.CRM_CONTACT" />
+      </el-tab-pane>
       <el-tab-pane label="详细资料">
         <ContactDetailsInfo :contact="contact" />
       </el-tab-pane>
@@ -20,7 +23,7 @@
           ref="permissionListRef"
           :biz-id="contact.id!"
           :biz-type="BizTypeEnum.CRM_CONTACT"
-          :show-action="!permissionListRef?.isPool || false"
+          :show-action="true"
           @quit-team="close"
         />
       </el-tab-pane>
@@ -29,13 +32,14 @@
           :biz-id="contact.id!"
           :biz-type="BizTypeEnum.CRM_CONTACT"
           :customer-id="contact.customerId"
+          :contact-id="contact.id"
         />
       </el-tab-pane>
     </el-tabs>
   </el-col>
   <!-- 表单弹窗:添加/修改 -->
-  <ContactForm ref="formRef" @success="getContactData(contact.id)" />
-  <CrmTransferForm ref="crmTransferFormRef" @success="close" />
+  <ContactForm ref="formRef" @success="getContact(contact.id)" />
+  <CrmTransferForm ref="transferFormRef" @success="close" />
 </template>
 <script lang="ts" setup>
 import { useTagsViewStore } from '@/store/modules/tagsView'
@@ -49,18 +53,19 @@ import { OperateLogV2VO } from '@/api/system/operatelog'
 import { getOperateLogPage } from '@/api/crm/operateLog'
 import ContactForm from '@/views/crm/contact/ContactForm.vue'
 import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
+import FollowUpList from '@/views/crm/followup/index.vue'
 
 defineOptions({ name: 'CrmContactDetail' })
 
-const route = useRoute()
 const message = useMessage()
-const id = Number(route.params.id) // 联系人编号
+
+const contactId = ref(0) // 线索编号
 const loading = ref(true) // 加载中
 const contact = ref<ContactApi.ContactVO>({} as ContactApi.ContactVO) // 联系人详情
 const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
 
 /** 获取详情 */
-const getContactData = async (id: number) => {
+const getContact = async (id: number) => {
   loading.value = true
   try {
     contact.value = await ContactApi.getContact(id)
@@ -77,9 +82,9 @@ const openForm = (type: string, id?: number) => {
 }
 
 /** 联系人转移 */
-const crmTransferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 联系人转移表单 ref
+const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 联系人转移表单 ref
 const transfer = () => {
-  crmTransferFormRef.value?.open('联系人转移', contact.value.id, ContactApi.transferContact)
+  transferFormRef.value?.open('联系人转移', contact.value.id, ContactApi.transferContact)
 }
 
 /** 获取操作日志 */
@@ -96,19 +101,21 @@ const getOperateLog = async (contactId: number) => {
 }
 
 /** 关闭窗口 */
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute } = useRouter() // 路由
 const close = () => {
   delView(unref(currentRoute))
 }
 
 /** 初始化 */
-const { delView } = useTagsViewStore() // 视图操作
-const { currentRoute } = useRouter() // 路由
+const { params } = useRoute()
 onMounted(async () => {
-  if (!id) {
+  if (!params.id) {
     message.warning('参数错误,联系人不能为空!')
     close()
     return
   }
-  await getContactData(id)
+  contactId.value = params.id as unknown as number
+  await getContact(contactId.value)
 })
 </script>

+ 15 - 11
src/views/crm/contact/index.vue

@@ -53,15 +53,6 @@
           @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="QQ" prop="qq">
-        <el-input
-          v-model="queryParams.qq"
-          class="!w-240px"
-          clearable
-          placeholder="请输入QQ"
-          @keyup.enter="handleQuery"
-        />
-      </el-form-item>
       <el-form-item label="微信" prop="wechat">
         <el-input
           v-model="queryParams.wechat"
@@ -109,6 +100,11 @@
 
   <!-- 列表 -->
   <ContentWrap>
+    <el-tabs v-model="activeName" @tab-click="handleTabClick">
+      <el-tab-pane label="我负责的" name="1" />
+      <el-tab-pane label="我参与的" name="2" />
+      <el-tab-pane label="下属负责的" name="3" />
+    </el-tabs>
     <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
       <el-table-column align="center" fixed="left" label="联系人姓名" prop="name" width="160">
         <template #default="scope">
@@ -224,6 +220,7 @@ import * as ContactApi from '@/api/crm/contact'
 import ContactForm from './ContactForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
 import * as CustomerApi from '@/api/crm/customer'
+import { TabsPaneContext } from 'element-plus'
 
 defineOptions({ name: 'CrmContact' })
 
@@ -233,20 +230,21 @@ const { t } = useI18n() // 国际化
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
-const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
+  sceneType: '1', // 默认和 activeName 相等
   mobile: undefined,
   telephone: undefined,
   email: undefined,
   customerId: undefined,
   name: undefined,
-  qq: undefined,
   wechat: undefined
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
+const activeName = ref('1') // 列表 tab
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
 
 /** 查询列表 */
 const getList = async () => {
@@ -272,6 +270,12 @@ const resetQuery = () => {
   handleQuery()
 }
 
+/** tab 切换 */
+const handleTabClick = (tab: TabsPaneContext) => {
+  queryParams.sceneType = tab.paneName
+  handleQuery()
+}
+
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {

+ 186 - 99
src/views/crm/contract/ContractForm.vue

@@ -1,40 +1,68 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" width="820">
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="1280">
     <el-form
       ref="formRef"
       v-loading="formLoading"
       :model="formData"
       :rules="formRules"
-      label-width="110px"
+      label-width="120px"
     >
-      <el-row :gutter="20">
-        <el-col :span="12">
+      <el-row>
+        <el-col :span="8">
           <el-form-item label="合同编号" prop="no">
-            <el-input v-model="formData.no" placeholder="请输入合同编号" />
+            <el-input disabled v-model="formData.no" placeholder="保存时自动生成" />
           </el-form-item>
         </el-col>
-        <el-col :span="12">
+        <el-col :span="8">
           <el-form-item label="合同名称" prop="name">
             <el-input v-model="formData.name" placeholder="请输入合同名称" />
           </el-form-item>
         </el-col>
-        <el-col :span="12">
+        <el-col :span="8">
+          <el-form-item label="负责人" prop="ownerUserId">
+            <el-select
+              v-model="formData.ownerUserId"
+              :disabled="formType !== 'create'"
+              class="w-1/1"
+            >
+              <el-option
+                v-for="item in userOptions"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="8">
           <el-form-item label="客户名称" prop="customerId">
-            <el-select v-model="formData.customerId">
+            <el-select
+              v-model="formData.customerId"
+              placeholder="请选择客户"
+              class="w-1/1"
+              @change="handleCustomerChange"
+            >
               <el-option
                 v-for="item in customerList"
                 :key="item.id"
                 :label="item.name"
-                :value="item.id!"
+                :value="item.id"
               />
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="12">
+        <el-col :span="8">
           <el-form-item label="商机名称" prop="businessId">
-            <el-select v-model="formData.businessId">
+            <el-select
+              @change="handleBusinessChange"
+              :disabled="!formData.customerId"
+              v-model="formData.businessId"
+              class="w-1/1"
+            >
               <el-option
-                v-for="item in businessList"
+                v-for="item in getBusinessOptions"
                 :key="item.id"
                 :label="item.name"
                 :value="item.id!"
@@ -42,46 +70,48 @@
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="12">
+      </el-row>
+      <el-row>
+        <el-col :span="8">
           <el-form-item label="下单日期" prop="orderDate">
             <el-date-picker
               v-model="formData.orderDate"
               placeholder="选择下单日期"
               type="date"
               value-format="x"
+              class="!w-1/1"
             />
           </el-form-item>
         </el-col>
-        <el-col :span="12">
-          <el-form-item label="合同金额" prop="price">
-            <el-input v-model="formData.price" placeholder="请输入合同金额" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
+        <el-col :span="8">
           <el-form-item label="开始时间" prop="startTime">
             <el-date-picker
               v-model="formData.startTime"
               placeholder="选择开始时间"
               type="date"
               value-format="x"
+              class="!w-1/1"
             />
           </el-form-item>
         </el-col>
-        <el-col :span="12">
+        <el-col :span="8">
           <el-form-item label="结束时间" prop="endTime">
             <el-date-picker
               v-model="formData.endTime"
               placeholder="选择结束时间"
               type="date"
               value-format="x"
+              class="!w-1/1"
             />
           </el-form-item>
         </el-col>
-        <el-col :span="12">
+      </el-row>
+      <el-row>
+        <el-col :span="8">
           <el-form-item label="公司签约人" prop="signUserId">
-            <el-select v-model="formData.signUserId">
+            <el-select v-model="formData.signUserId" class="w-1/1">
               <el-option
-                v-for="item in userList"
+                v-for="item in userOptions"
                 :key="item.id"
                 :label="item.nickname"
                 :value="item.id!"
@@ -89,61 +119,70 @@
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="12">
-          <el-form-item label="客户签约人" prop="contactId">
-            <el-select v-model="formData.contactId" :disabled="!formData.customerId">
+        <el-col :span="8">
+          <el-form-item label="客户签约人" prop="signContactId">
+            <el-select
+              v-model="formData.signContactId"
+              :disabled="!formData.customerId"
+              class="w-1/1"
+            >
               <el-option
                 v-for="item in getContactOptions"
                 :key="item.id"
                 :label="item.name"
-                :value="item.id!"
+                :value="item.id"
               />
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="12">
-          <el-form-item label="负责人" prop="ownerUserId">
-            <el-select v-model="formData.ownerUserId">
-              <el-option
-                v-for="item in userList"
-                :key="item.id"
-                :label="item.nickname"
-                :value="item.id!"
-              />
-            </el-select>
+        <el-col :span="8">
+          <el-form-item label="备注" prop="remark">
+            <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
           </el-form-item>
         </el-col>
-        <el-col :span="24">
-          <el-form-item label="备注" prop="remark">
+      </el-row>
+      <!-- 子表的表单 -->
+      <ContentWrap>
+        <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+          <el-tab-pane label="产品清单" name="product">
+            <ContractProductForm
+              ref="productFormRef"
+              :products="formData.products"
+              :disabled="disabled"
+            />
+          </el-tab-pane>
+        </el-tabs>
+      </ContentWrap>
+      <el-row>
+        <el-col :span="8">
+          <el-form-item label="产品总金额" prop="totalProductPrice">
             <el-input
-              v-model="formData.remark"
-              :rows="3"
-              placeholder="请输入备注"
-              type="textarea"
+              disabled
+              v-model="formData.totalProductPrice"
+              :formatter="erpPriceTableColumnFormatter"
             />
           </el-form-item>
         </el-col>
-        <!-- TODO @puhui999:productItems 改成 products 会更好点;因为它不是 item 哈 -->
-        <el-col :span="24">
-          <el-form-item label="产品列表" prop="productList">
-            <ProductList v-model="formData.productItems" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="整单折扣(%)" prop="discountPercent">
+        <el-col :span="8">
+          <el-form-item label="整单折扣(%)" prop="discountPercent">
             <el-input-number
               v-model="formData.discountPercent"
-              :min="0"
-              :max="100"
-              :precision="0"
               placeholder="请输入整单折扣"
-              class="!w-100%"
+              controls-position="right"
+              :min="0"
+              :precision="2"
+              class="!w-1/1"
             />
           </el-form-item>
         </el-col>
-        <el-col :span="12">
-          <el-form-item label="产品总金额(元)" prop="productPrice">
-            {{ fenToYuan(formData.productPrice) }}
+        <el-col :span="8">
+          <el-form-item label="折扣后金额" prop="totalPrice">
+            <el-input
+              disabled
+              v-model="formData.totalPrice"
+              placeholder="请输入商机金额"
+              :formatter="erpPriceTableColumnFormattere"
+            />
           </el-form-item>
         </el-col>
       </el-row>
@@ -160,8 +199,9 @@ import * as ContractApi from '@/api/crm/contract'
 import * as UserApi from '@/api/system/user'
 import * as ContactApi from '@/api/crm/contact'
 import * as BusinessApi from '@/api/crm/business'
-import ProductList from './components/ProductList.vue'
-import { fenToYuan } from '@/utils'
+import { erpPriceMultiply, erpPriceTableColumnFormatter } from '@/utils'
+import { useUserStore } from '@/store/modules/user'
+import ContractProductForm from '@/views/crm/contract/components/ContractProductForm.vue'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -170,30 +210,56 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref<ContractApi.ContractVO>({} as ContractApi.ContractVO)
+const formData = ref({
+  id: undefined,
+  no: undefined,
+  name: undefined,
+  customerId: undefined,
+  businessId: undefined,
+  orderDate: undefined,
+  startTime: undefined,
+  endTime: undefined,
+  signUserId: undefined,
+  signContactId: undefined,
+  ownerUserId: undefined,
+  discountPercent: 0,
+  totalProductPrice: undefined,
+  remark: undefined,
+  products: []
+})
 const formRules = reactive({
   name: [{ required: true, message: '合同名称不能为空', trigger: 'blur' }],
   customerId: [{ required: true, message: '客户不能为空', trigger: 'blur' }],
   orderDate: [{ required: true, message: '下单日期不能为空', trigger: 'blur' }],
-  ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'blur' }],
-  no: [{ required: true, message: '合同编号不能为空', trigger: 'blur' }]
+  ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+// TODO 芋艿:统一的客户选择面板
+const customerList = ref([]) // 客户列表的数据
+const businessList = ref<BusinessApi.BusinessVO[]>([])
+const contactList = ref<ContactApi.ContactVO[]>([])
+
+/** 子表的表单 */
+const subTabsName = ref('product')
+const productFormRef = ref()
 
-/** 监听合同产品变化,计算合同产品总价 */
+/** 计算 discountPrice、totalPrice 价格 */
 watch(
-  () => formData.value.productItems,
+  () => formData.value,
   (val) => {
-    if (!val || val.length === 0) {
-      formData.value.productPrice = 0
+    if (!val) {
       return
     }
-    // 使用 reduce 函数进行累加
-    formData.value.productPrice = val.reduce(
-      (accumulator, currentValue) =>
-        isNaN(accumulator + currentValue.totalPrice) ? 0 : accumulator + currentValue.totalPrice,
-      0
-    )
+    const totalProductPrice = val.products.reduce((prev, curr) => prev + curr.totalPrice, 0)
+    const discountPrice =
+      val.discountPercent != null
+        ? erpPriceMultiply(totalProductPrice, val.discountPercent / 100.0)
+        : 0
+    const totalPrice = totalProductPrice - discountPrice
+    // 赋值
+    formData.value.totalProductPrice = totalProductPrice
+    formData.value.totalPrice = totalPrice
   },
   { deep: true }
 )
@@ -213,7 +279,18 @@ const open = async (type: string, id?: number) => {
       formLoading.value = false
     }
   }
-  await getAllApi()
+  // 获得客户列表
+  customerList.value = await CustomerApi.getCustomerSimpleList()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+  // 默认新建时选中自己
+  if (formType.value === 'create') {
+    formData.value.ownerUserId = useUserStore().getUser.id
+  }
+  // 获取联系人
+  contactList.value = await ContactApi.getSimpleContactList()
+  // 获得商机列表
+  businessList.value = await BusinessApi.getSimpleBusinessList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -226,6 +303,7 @@ const submitForm = async () => {
   if (!valid) return
   // 提交请求
   formLoading.value = true
+  productFormRef.value.validate()
   try {
     const data = unref(formData.value) as unknown as ContractApi.ContractVO
     if (formType.value === 'create') {
@@ -245,39 +323,48 @@ const submitForm = async () => {
 
 /** 重置表单 */
 const resetForm = () => {
-  formData.value = {} as ContractApi.ContractVO
+  formData.value = {
+    id: undefined,
+    no: undefined,
+    name: undefined,
+    customerId: undefined,
+    businessId: undefined,
+    orderDate: undefined,
+    startTime: undefined,
+    endTime: undefined,
+    signUserId: undefined,
+    signContactId: undefined,
+    ownerUserId: undefined,
+    discountPercent: 0,
+    totalProductPrice: undefined,
+    remark: undefined,
+    products: []
+  }
   formRef.value?.resetFields()
 }
 
-/** 获取其它相关数据 */
-const getAllApi = async () => {
-  await Promise.all([getCustomerList(), getUserList(), getContactListList(), getBusinessList()])
+/** 处理切换客户 */
+const handleCustomerChange = () => {
+  formData.value.businessId = undefined
+  formData.value.signContactId = undefined
+  formData.value.products = []
 }
 
-/** 获取客户 */
-const customerList = ref<CustomerApi.CustomerVO[]>([])
-const getCustomerList = async () => {
-  customerList.value = await CustomerApi.getCustomerSimpleList()
+/** 处理商机变化 */
+const handleBusinessChange = async (businessId: number) => {
+  const business = await BusinessApi.getBusiness(businessId)
+  business.products.forEach((item) => {
+    item.contractPrice = item.businessPrice
+  })
+  formData.value.products = business.products
 }
 
 /** 动态获取客户联系人 */
-const contactList = ref<ContactApi.ContactVO[]>([])
 const getContactOptions = computed(() =>
-  contactList.value.filter((item) => item.customerId === formData.value.customerId)
+  contactList.value.filter((item) => item.customerId == formData.value.customerId)
+)
+/** 动态获取商机 */
+const getBusinessOptions = computed(() =>
+  businessList.value.filter((item) => item.customerId == formData.value.customerId)
 )
-const getContactListList = async () => {
-  contactList.value = await ContactApi.getSimpleContactList()
-}
-
-/** 获取用户列表 */
-const userList = ref<UserApi.UserVO[]>([])
-const getUserList = async () => {
-  userList.value = await UserApi.getSimpleUserList()
-}
-
-/** 获取商机 */
-const businessList = ref<BusinessApi.BusinessVO[]>([])
-const getBusinessList = async () => {
-  businessList.value = await BusinessApi.getSimpleBusinessList()
-}
 </script>

+ 7 - 3
src/views/crm/contract/components/ContractList.vue

@@ -22,8 +22,8 @@
       <el-table-column
         label="合同金额(元)"
         align="center"
-        prop="price"
-        :formatter="fenToYuanFormat"
+        prop="totalPrice"
+        :formatter="erpPriceTableColumnFormatter"
       />
       <el-table-column
         label="开始时间"
@@ -61,9 +61,9 @@
 import * as ContractApi from '@/api/crm/contract'
 import ContractForm from './../ContractForm.vue'
 import { BizTypeEnum } from '@/api/crm/permission'
-import { fenToYuanFormat } from '@/utils/formatter'
 import { dateFormatter } from '@/utils/formatTime'
 import { DICT_TYPE } from '@/utils/dict'
+import { erpPriceTableColumnFormatter } from '@/utils'
 
 defineOptions({ name: 'CrmContractList' })
 const props = defineProps<{
@@ -93,6 +93,10 @@ const getList = async () => {
         queryParams.customerId = props.bizId
         data = await ContractApi.getContractPageByCustomer(queryParams)
         break
+      case BizTypeEnum.CRM_BUSINESS:
+        queryParams.businessId = props.bizId
+        data = await ContractApi.getContractPageByBusiness(queryParams)
+        break
       default:
         return
     }

+ 183 - 0
src/views/crm/contract/components/ContractProductForm.vue

@@ -0,0 +1,183 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    v-loading="formLoading"
+    label-width="0px"
+    :inline-message="true"
+    :disabled="disabled"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="序号" type="index" align="center" width="60" />
+      <el-table-column label="产品名称" min-width="180">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+            <el-select
+              v-model="row.productId"
+              clearable
+              filterable
+              @change="onChangeProduct($event, row)"
+              placeholder="请选择产品"
+            >
+              <el-option
+                v-for="item in productList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="条码" min-width="150">
+        <template #default="{ row }">
+          <el-form-item class="mb-0px!">
+            <el-input disabled v-model="row.productNo" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="单位" min-width="80">
+        <template #default="{ row }">
+          <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="row.productUnit" />
+        </template>
+      </el-table-column>
+      <el-table-column label="价格(元)" min-width="120">
+        <template #default="{ row }">
+          <el-form-item class="mb-0px!">
+            <el-input disabled v-model="row.productPrice" :formatter="erpPriceInputFormatter" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="售价(元)" fixed="right" min-width="140">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.contractPrice`" class="mb-0px!">
+            <el-input-number
+              v-model="row.contractPrice"
+              controls-position="right"
+              :min="0.001"
+              :precision="2"
+              class="!w-100%"
+            />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="数量" prop="count" fixed="right" min-width="120">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+            <el-input-number
+              v-model="row.count"
+              controls-position="right"
+              :min="0.001"
+              :precision="3"
+              class="!w-100%"
+            />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="合计" prop="totalPrice" fixed="right" min-width="140">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+            <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link>—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-form>
+  <el-row justify="center" class="mt-3" v-if="!disabled">
+    <el-button @click="handleAdd" round>+ 添加产品</el-button>
+  </el-row>
+</template>
+<script setup lang="ts">
+import * as ProductApi from '@/api/crm/product'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import { DICT_TYPE } from '@/utils/dict'
+
+const props = defineProps<{
+  products: undefined
+  disabled: false
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
+  contractPrice: [{ required: true, message: '合同价格不能为空', trigger: 'blur' }],
+  count: [{ required: true, message: '产品数量不能为空', trigger: 'blur' }]
+})
+const formRef = ref([]) // 表单 Ref
+const productList = ref<ProductApi.ProductVO[]>([]) // 产品列表
+
+/** 初始化设置产品项 */
+watch(
+  () => props.products,
+  async (val) => {
+    formData.value = val
+  },
+  { immediate: true }
+)
+
+/** 监听合同产品变化,计算合同产品总价 */
+watch(
+  () => formData.value,
+  (val) => {
+    if (!val || val.length === 0) {
+      return
+    }
+    // 循环处理
+    val.forEach((item) => {
+      if (item.contractPrice != null && item.count != null) {
+        item.totalPrice = erpPriceMultiply(item.contractPrice, item.count)
+      } else {
+        item.totalPrice = undefined
+      }
+    })
+  },
+  { deep: true }
+)
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  const row = {
+    id: undefined,
+    productId: undefined,
+    productUnit: undefined, // 产品单位
+    productNo: undefined, // 产品条码
+    productPrice: undefined, // 产品价格
+    contractPrice: undefined,
+    count: 1
+  }
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index: number) => {
+  formData.value.splice(index, 1)
+}
+
+/** 处理产品变更 */
+const onChangeProduct = (productId, row) => {
+  const product = productList.value.find((item) => item.id === productId)
+  if (product) {
+    row.productUnit = product.unit
+    row.productNo = product.no
+    row.productPrice = product.price
+    row.contractPrice = product.price
+  }
+}
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 初始化 */
+onMounted(async () => {
+  productList.value = await ProductApi.getProductSimpleList()
+})
+</script>

+ 0 - 171
src/views/crm/contract/components/ProductList.vue

@@ -1,171 +0,0 @@
-<!-- 合同 Form 表单下的 Product 列表 -->
-<template>
-  <el-row justify="end">
-    <el-button plain type="primary" @click="openForm">添加产品</el-button>
-  </el-row>
-  <el-table :data="list" :show-overflow-tooltip="true" :stripe="true">
-    <el-table-column align="center" label="产品名称" prop="name" width="120" />
-    <el-table-column
-      :formatter="fenToYuanFormat"
-      align="center"
-      label="价格"
-      prop="price"
-      width="100"
-    />
-    <el-table-column align="center" label="产品类型" prop="categoryName" width="100" />
-    <el-table-column align="center" label="产品单位" prop="unit">
-      <template #default="scope">
-        <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" />
-      </template>
-    </el-table-column>
-    <el-table-column align="center" label="产品编码" prop="no" />
-    <el-table-column align="center" fixed="right" label="数量" prop="count" width="100">
-      <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
-        <el-input-number
-          v-model="row.count"
-          controls-position="right"
-          :min="0"
-          :precision="0"
-          class="!w-100%"
-        />
-      </template>
-    </el-table-column>
-    <el-table-column
-      align="center"
-      fixed="right"
-      label="折扣(%)"
-      prop="discountPercent"
-      width="120"
-    >
-      <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
-        <el-input-number
-          v-model="row.discountPercent"
-          controls-position="right"
-          :min="0"
-          :max="100"
-          :precision="0"
-          class="!w-100%"
-        />
-      </template>
-    </el-table-column>
-    <el-table-column align="center" fixed="right" label="合计" prop="totalPrice" width="100">
-      <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
-        {{ fenToYuan(getTotalPrice(row)) }}
-      </template>
-    </el-table-column>
-    <el-table-column align="center" fixed="right" label="操作" width="60">
-      <template #default="scope">
-        <el-button link type="danger" @click="handleDelete(scope.row.id)"> 移除</el-button>
-      </template>
-    </el-table-column>
-  </el-table>
-
-  <!-- table 选择表单 -->
-  <TableSelectForm ref="tableSelectFormRef" v-model="multipleSelection" title="选择产品">
-    <el-table-column align="center" label="产品名称" prop="name" width="160" />
-    <el-table-column align="center" label="产品类型" prop="categoryName" width="160" />
-    <el-table-column align="center" label="产品单位" prop="unit">
-      <template #default="scope">
-        <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" />
-      </template>
-    </el-table-column>
-    <el-table-column align="center" label="产品编码" prop="no" />
-    <el-table-column
-      :formatter="fenToYuanFormat"
-      align="center"
-      label="价格(元)"
-      prop="price"
-      width="100"
-    />
-  </TableSelectForm>
-</template>
-
-<script lang="ts" setup>
-import * as ProductApi from '@/api/crm/product'
-import { DICT_TYPE } from '@/utils/dict'
-import { fenToYuanFormat } from '@/utils/formatter'
-import { TableSelectForm } from '@/components/Table/index'
-import { fenToYuan, floatToFixed2, yuanToFen } from '@/utils'
-
-defineOptions({ name: 'ProductList' })
-const props = withDefaults(defineProps<{ modelValue: ProductApi.ProductExpandVO[] }>(), {
-  modelValue: () => []
-})
-const emits = defineEmits<{
-  (e: 'update:modelValue', v: any[]): void
-}>()
-
-const list = ref<ProductApi.ProductExpandVO[]>([]) // 已添加的产品列表
-const multipleSelection = ref<ProductApi.ProductExpandVO[]>([]) // 多选
-
-/** 处理删除 */
-const handleDelete = (id: number) => {
-  const index = list.value.findIndex((item) => item.id === id)
-  if (index !== -1) {
-    list.value.splice(index, 1)
-  }
-}
-
-/** 打开 Product 弹窗  */
-const tableSelectFormRef = ref<InstanceType<typeof TableSelectForm>>()
-const openForm = () => {
-  tableSelectFormRef.value?.open(ProductApi.getProductPage)
-}
-
-/** 计算 totalPrice */
-const getTotalPrice = computed(() => (row: ProductApi.ProductExpandVO) => {
-  const totalPrice =
-    (Number(row.price) / 100) * Number(row.count) * (1 - Number(row.discountPercent) / 100)
-  row.totalPrice = isNaN(totalPrice) ? 0 : yuanToFen(totalPrice)
-  return isNaN(totalPrice) ? 0 : totalPrice.toFixed(2)
-})
-
-/** 编辑时合同产品回显 */
-const isSetListValue = ref(false) // 判断是否已经给 list 赋值过,用于编辑表单产品回显
-watch(
-  () => props.modelValue,
-  (val) => {
-    if (!val || val.length === 0 || isSetListValue.value) {
-      return
-    }
-    list.value = [
-      ...props.modelValue.map((item) => {
-        item.totalPrice = floatToFixed2(item.totalPrice) as unknown as number
-        return item
-      })
-    ]
-    isSetListValue.value = true
-  },
-  { immediate: true, deep: true }
-)
-
-/** 监听列表变化,动态更新合同产品列表 */
-watch(
-  list,
-  (val) => {
-    if (!val || val.length === 0) {
-      return
-    }
-    emits('update:modelValue', list.value)
-  },
-  { deep: true }
-)
-
-// 监听产品选择结果动态添加产品到列表中,如果产品存在则不放入列表中
-watch(
-  multipleSelection,
-  (val) => {
-    if (!val || val.length === 0) {
-      return
-    }
-    // 过滤出不在列表中的产品
-    const ids = list.value.map((item) => item.id)
-    const productList = multipleSelection.value.filter((item) => ids.indexOf(item.id) === -1)
-    if (!productList || productList.length === 0) {
-      return
-    }
-    list.value.push(...productList)
-  },
-  { deep: true }
-)
-</script>

+ 100 - 0
src/views/crm/contract/config/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <ContentWrap>
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="160px"
+      v-loading="formLoading"
+    >
+      <el-card shadow="never">
+        <!-- 操作 -->
+        <template #header>
+          <div class="flex items-center justify-between">
+            <CardTitle title="合同配置设置" />
+            <el-button type="primary" @click="onSubmit" v-hasPermi="['crm:contract-config:update']">
+              保存
+            </el-button>
+          </div>
+        </template>
+        <!-- 表单 -->
+        <el-form-item label="提前提醒设置" prop="notifyEnabled">
+          <el-radio-group
+            v-model="formData.notifyEnabled"
+            @change="changeNotifyEnable"
+            class="ml-4"
+          >
+            <el-radio :label="false" size="large">不提醒</el-radio>
+            <el-radio :label="true" size="large">提醒</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <div v-if="formData.notifyEnabled">
+          <el-form-item>
+            提前 <el-input-number class="mx-2" v-model="formData.notifyDays" /> 天提醒
+          </el-form-item>
+        </div>
+      </el-card>
+    </el-form>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as ContractConfigApi from '@/api/crm/contract/config'
+import { CardTitle } from '@/components/Card'
+
+defineOptions({ name: 'CrmContractConfig' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const formLoading = ref(false)
+const formData = ref({
+  notifyEnabled: false,
+  notifyDays: undefined
+})
+const formRules = reactive({})
+const formRef = ref() // 表单 Ref
+
+/** 获取配置 */
+const getConfig = async () => {
+  try {
+    formLoading.value = true
+    const data = await ContractConfigApi.getContractConfig()
+    if (data === null) {
+      return
+    }
+    formData.value = data
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 提交配置 */
+const onSubmit = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as ContractConfigApi.ContractConfigVO
+    await ContractConfigApi.saveContractConfig(data)
+    message.success(t('common.updateSuccess'))
+    await getConfig()
+    formLoading.value = false
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 更改提前提醒设置 */
+const changeNotifyEnable = () => {
+  if (!formData.value.notifyEnabled) {
+    formData.value.notifyDays = undefined
+  }
+}
+
+onMounted(() => {
+  getConfig()
+})
+</script>

+ 4 - 4
src/views/crm/contract/detail/ContractDetailsHeader.vue

@@ -21,13 +21,13 @@
         {{ contract.customerName }}
       </el-descriptions-item>
       <el-descriptions-item label="合同金额(元)">
-        {{ floatToFixed2(contract.price) }}
+        {{ erpPriceInputFormatter(contract.totalPrice) }}
       </el-descriptions-item>
       <el-descriptions-item label="下单时间">
-        {{ contract.orderDate ? formatDate(contract.orderDate) : '空' }}
+        {{ formatDate(contract.orderDate) }}
       </el-descriptions-item>
       <el-descriptions-item label="回款金额(元)">
-        {{ floatToFixed2(contract.price) }}
+        {{ erpPriceInputFormatter(contract.totalReceivablePrice) }}
       </el-descriptions-item>
       <el-descriptions-item label="负责人">
         {{ contract.ownerUserName }}
@@ -38,7 +38,7 @@
 <script lang="ts" setup>
 import * as ContractApi from '@/api/crm/contract'
 import { formatDate } from '@/utils/formatTime'
-import { floatToFixed2 } from '@/utils'
+import { erpPriceInputFormatter } from '@/utils'
 
 defineOptions({ name: 'ContractDetailsHeader' })
 defineProps<{ contract: ContractApi.ContractVO }>()

+ 22 - 27
src/views/crm/contract/detail/ContractDetailsInfo.vue

@@ -6,33 +6,25 @@
         <template #title>
           <span class="text-base font-bold">基本信息</span>
         </template>
-        <el-descriptions :column="3">
-          <el-descriptions-item label="合同编号">
-            {{ contract.no }}
-          </el-descriptions-item>
-          <el-descriptions-item label="合同名称">
-            {{ contract.name }}
-          </el-descriptions-item>
-          <el-descriptions-item label="客户名称">
-            {{ contract.customerName }}
-          </el-descriptions-item>
-          <el-descriptions-item label="商机名称">
-            {{ contract.businessName }}
-          </el-descriptions-item>
+        <el-descriptions :column="4">
+          <el-descriptions-item label="合同编号">{{ contract.no }}</el-descriptions-item>
+          <el-descriptions-item label="合同名称">{{ contract.name }}</el-descriptions-item>
+          <el-descriptions-item label="客户名称">{{ contract.customerName }}</el-descriptions-item>
+          <el-descriptions-item label="商机名称">{{ contract.businessName }}</el-descriptions-item>
           <el-descriptions-item label="合同金额(元)">
-            {{ contract.price }}
+            {{ erpPriceInputFormatter(contract.totalPrice) }}
           </el-descriptions-item>
           <el-descriptions-item label="下单时间">
             {{ formatDate(contract.orderDate) }}
           </el-descriptions-item>
-          <el-descriptions-item label="开始时间">
+          <el-descriptions-item label="合同开始时间">
             {{ formatDate(contract.startTime) }}
           </el-descriptions-item>
-          <el-descriptions-item label="结束时间">
+          <el-descriptions-item label="合同结束时间">
             {{ formatDate(contract.endTime) }}
           </el-descriptions-item>
           <el-descriptions-item label="客户签约人">
-            {{ contract.contactName }}
+            {{ contract.signContactName }}
           </el-descriptions-item>
           <el-descriptions-item label="公司签约人">
             {{ contract.signUserName }}
@@ -41,7 +33,7 @@
             {{ contract.remark }}
           </el-descriptions-item>
           <el-descriptions-item label="合同状态">
-            {{ contract.auditStatus }}
+            <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="contract.auditStatus" />
           </el-descriptions-item>
         </el-descriptions>
       </el-collapse-item>
@@ -49,18 +41,19 @@
         <template #title>
           <span class="text-base font-bold">系统信息</span>
         </template>
-        <el-descriptions :column="2">
-          <el-descriptions-item label="负责人">
-            {{ contract.ownerUserName }}
-          </el-descriptions-item>
-          <el-descriptions-item label="创建人">
-            {{ contract.creatorName }}
-          </el-descriptions-item>
+        <el-descriptions :column="4">
+          <el-descriptions-item label="负责人">{{ contract.ownerUserName }}</el-descriptions-item>
+          <el-descriptions-item label="最后跟进时间">
+            {{ formatDate(contract.contactLastTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="">&nbsp;</el-descriptions-item>
+          <el-descriptions-item label="">&nbsp;</el-descriptions-item>
+          <el-descriptions-item label="创建人">{{ contract.creatorName }}</el-descriptions-item>
           <el-descriptions-item label="创建时间">
-            {{ contract.createTime ? formatDate(contract.createTime) : '空' }}
+            {{ formatDate(contract.createTime) }}
           </el-descriptions-item>
           <el-descriptions-item label="更新时间">
-            {{ contract.updateTime ? formatDate(contract.updateTime) : '空' }}
+            {{ formatDate(contract.updateTime) }}
           </el-descriptions-item>
         </el-descriptions>
       </el-collapse-item>
@@ -70,6 +63,8 @@
 <script lang="ts" setup>
 import * as ContractApi from '@/api/crm/contract'
 import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+import { erpPriceInputFormatter } from '@/utils'
 
 defineOptions({ name: 'ContractDetailsInfo' })
 defineProps<{

+ 61 - 62
src/views/crm/contract/detail/ContractProductList.vue

@@ -1,67 +1,66 @@
-<!-- 合同详情:产品列表 -->
 <template>
-  <el-table :data="list" :show-overflow-tooltip="true" :stripe="true">
-    <el-table-column align="center" label="产品名称" prop="name" width="160" />
-    <el-table-column align="center" label="产品类型" prop="categoryName" width="160" />
-    <el-table-column align="center" label="产品单位" prop="unit">
-      <template #default="scope">
-        <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" />
-      </template>
-    </el-table-column>
-    <el-table-column align="center" label="产品编码" prop="no" />
-    <el-table-column
-      :formatter="fenToYuanFormat"
-      align="center"
-      label="价格(元)"
-      prop="price"
-      width="100"
-    />
-    <el-table-column align="center" label="数量" prop="count" width="200" />
-    <el-table-column align="center" label="折扣(%)" prop="discountPercent" width="200" />
-    <el-table-column align="center" label="合计" prop="totalPrice" width="100">
-      <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
-        {{ getTotalPrice(row) }}
-      </template>
-    </el-table-column>
-  </el-table>
+  <ContentWrap>
+    <el-table :data="contract.products" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column
+        align="center"
+        label="产品名称"
+        fixed="left"
+        prop="productName"
+        min-width="160"
+      >
+        <template #default="scope">
+          {{ scope.row.productName }}
+        </template>
+      </el-table-column>
+      <el-table-column label="产品条码" align="center" prop="productNo" min-width="120" />
+      <el-table-column align="center" label="产品单位" prop="productUnit" min-width="160">
+        <template #default="{ row }">
+          <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="row.productUnit" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="产品价格(元)"
+        align="center"
+        prop="productPrice"
+        min-width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column
+        label="合同价格(元)"
+        align="center"
+        prop="contractPrice"
+        min-width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column
+        align="center"
+        label="数量"
+        prop="count"
+        min-width="100px"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column
+        label="合计金额(元)"
+        align="center"
+        prop="totalPrice"
+        min-width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+    </el-table>
+    <el-row class="mt-10px" justify="end">
+      <el-col :span="3"> 整单折扣:{{ erpPriceInputFormatter(contract.discountPercent) }}% </el-col>
+      <el-col :span="4">
+        产品总金额:{{ erpPriceInputFormatter(contract.totalProductPrice) }} 元
+      </el-col>
+    </el-row>
+  </ContentWrap>
 </template>
-
-<script lang="ts" setup>
+<script setup lang="ts">
+import * as ContractApi from '@/api/crm/contract'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
 import { DICT_TYPE } from '@/utils/dict'
-import { fenToYuanFormat } from '@/utils/formatter'
-import * as ProductApi from '@/api/crm/product'
-import { floatToFixed2, yuanToFen } from '@/utils'
-
-defineOptions({ name: 'ContractProductList' })
-const props = withDefaults(defineProps<{ modelValue: ProductApi.ProductExpandVO[] }>(), {
-  modelValue: () => []
-})
-const list = ref<ProductApi.ProductExpandVO[]>([]) // 产品列表
-
-/** 计算 totalPrice */
-const getTotalPrice = computed(() => (row: ProductApi.ProductExpandVO) => {
-  const totalPrice =
-    (Number(row.price) / 100) * Number(row.count) * (1 - Number(row.discountPercent) / 100)
-  row.totalPrice = isNaN(totalPrice) ? 0 : yuanToFen(totalPrice)
-  return isNaN(totalPrice) ? 0 : totalPrice.toFixed(2)
-})
 
-/** 编辑时合同产品回显 */
-const isSetListValue = ref(false) // 判断是否已经给 list 赋值过,用于编辑表单产品回显
-watch(
-  () => props.modelValue,
-  (val) => {
-    if (!val || val.length === 0 || isSetListValue.value) {
-      return
-    }
-    list.value = [
-      ...props.modelValue.map((item) => {
-        item.totalPrice = floatToFixed2(item.totalPrice) as unknown as number
-        return item
-      })
-    ]
-    isSetListValue.value = true
-  },
-  { immediate: true, deep: true }
-)
+const { contract } = defineProps<{
+  contract: ContractApi.ContractVO
+}>()
 </script>

+ 31 - 10
src/views/crm/contract/detail/index.vue

@@ -10,22 +10,33 @@
   </ContractDetailsHeader>
   <el-col>
     <el-tabs>
-      <!-- TODO @puhui999:跟进记录 -->
+      <el-tab-pane label="跟进记录">
+        <FollowUpList :biz-id="contract.id" :biz-type="BizTypeEnum.CRM_CONTRACT" />
+      </el-tab-pane>
       <el-tab-pane label="基本信息">
         <ContractDetailsInfo :contract="contract" />
       </el-tab-pane>
-      <!-- TODO @puhui999:products 更合适哈 -->
       <el-tab-pane label="产品">
-        <ContractProductList v-model="contract.productItems" />
+        <ContractProductList :contract="contract" />
+      </el-tab-pane>
+      <el-tab-pane label="回款">
+        <ReceivablePlanList
+          :contract-id="contract.id!"
+          :customer-id="contract.customerId"
+          @create-receivable="createReceivable"
+        />
+        <ReceivableList
+          ref="receivableListRef"
+          :contract-id="contract.id!"
+          :customer-id="contract.customerId"
+        />
       </el-tab-pane>
-      <!-- TODO @puhui999:回款信息 -->
-      <!-- TODO @puhui999:这里是不是不用 isPool 哈 -->
       <el-tab-pane label="团队成员">
         <PermissionList
           ref="permissionListRef"
           :biz-id="contract.id!"
           :biz-type="BizTypeEnum.CRM_CONTRACT"
-          :show-action="!permissionListRef?.isPool || false"
+          :show-action="false"
           @quit-team="close"
         />
       </el-tab-pane>
@@ -43,16 +54,20 @@
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { OperateLogV2VO } from '@/api/system/operatelog'
 import * as ContractApi from '@/api/crm/contract'
-import ContractDetailsHeader from './ContractDetailsHeader.vue'
 import ContractDetailsInfo from './ContractDetailsInfo.vue'
+import ContractDetailsHeader from './ContractDetailsHeader.vue'
 import ContractProductList from './ContractProductList.vue'
 import { BizTypeEnum } from '@/api/crm/permission'
 import { getOperateLogPage } from '@/api/crm/operateLog'
 import ContractForm from '@/views/crm/contract/ContractForm.vue'
 import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
 import PermissionList from '@/views/crm/permission/components/PermissionList.vue'
+import FollowUpList from '@/views/crm/followup/index.vue'
+import ReceivableList from '@/views/crm/receivable/components/ReceivableList.vue'
+import ReceivablePlanList from '@/views/crm/receivable/plan/components/ReceivablePlanList.vue'
 
 defineOptions({ name: 'CrmContractDetail' })
+const props = defineProps<{ id?: number }>()
 
 const route = useRoute()
 const message = useMessage()
@@ -71,8 +86,8 @@ const openForm = (type: string, id?: number) => {
 const getContractData = async () => {
   loading.value = true
   try {
-    await getOperateLog(contractId.value)
     contract.value = await ContractApi.getContract(contractId.value)
+    await getOperateLog(contractId.value)
   } finally {
     loading.value = false
   }
@@ -91,8 +106,14 @@ const getOperateLog = async (contractId: number) => {
   logList.value = data.list
 }
 
+/** 从回款计划创建回款 */
+const receivableListRef = ref<InstanceType<typeof ReceivableList>>() // 回款列表 Ref
+const createReceivable = (planData: any) => {
+  receivableListRef.value?.createReceivable(planData)
+}
+
 /** 转移 */
-// TODO @puhui999:这个组件,要不传递业务类型,然后组件里判断 title 和 api 能调用哪个;整体治理掉;
+// TODO @puhui999:这个组件,要不传递业务类型,然后组件里判断 title 和 api 能调用哪个;整体治理掉;好呢
 const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 合同转移表单 ref
 const transferContract = () => {
   transferFormRef.value?.open('合同转移', contract.value.id, ContractApi.transferContract)
@@ -107,7 +128,7 @@ const close = () => {
 
 /** 初始化 */
 onMounted(async () => {
-  const id = route.params.id
+  const id = props.id || route.params.id
   if (!id) {
     message.warning('参数错误,合同不能为空!')
     close()

+ 116 - 24
src/views/crm/contract/index.vue

@@ -25,6 +25,24 @@
           placeholder="请输入合同名称"
           @keyup.enter="handleQuery"
         />
+        <el-form-item label="客户" prop="customerId">
+          <el-select
+            v-model="queryParams.customerId"
+            class="!w-240px"
+            clearable
+            lable-key="name"
+            placeholder="请选择客户"
+            value-key="id"
+            @keyup.enter="handleQuery"
+          >
+            <el-option
+              v-for="item in customerList"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id!"
+            />
+          </el-select>
+        </el-form-item>
       </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery">
@@ -55,9 +73,20 @@
 
   <!-- 列表 -->
   <ContentWrap>
+    <el-tabs v-model="activeName" @tab-click="handleTabClick">
+      <el-tab-pane label="我负责的" name="1" />
+      <el-tab-pane label="我参与的" name="2" />
+      <el-tab-pane label="下属负责的" name="3" />
+    </el-tabs>
     <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" fixed="left" label="合同编号" prop="no" width="130" />
-      <el-table-column align="center" label="合同名称" prop="name" width="130" />
+      <el-table-column align="center" fixed="left" label="合同编号" prop="no" width="180" />
+      <el-table-column align="center" fixed="left" label="合同名称" prop="name" width="160">
+        <template #default="scope">
+          <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+            {{ scope.row.name }}
+          </el-link>
+        </template>
+      </el-table-column>
       <el-table-column align="center" label="客户名称" prop="customerName" width="120">
         <template #default="scope">
           <el-link
@@ -69,8 +98,24 @@
           </el-link>
         </template>
       </el-table-column>
-      <!-- TODO @puhui999:做了商机详情后,可以把这个超链接加上 -->
-      <el-table-column align="center" label="商机名称" prop="businessName" width="130" />
+      <el-table-column align="center" label="商机名称" prop="businessName" width="130">
+        <template #default="scope">
+          <el-link
+            :underline="false"
+            type="primary"
+            @click="openBusinessDetail(scope.row.businessId)"
+          >
+            {{ scope.row.businessName }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="合同金额(元)"
+        prop="totalPrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
       <el-table-column
         align="center"
         label="下单时间"
@@ -78,13 +123,6 @@
         width="120"
         :formatter="dateFormatter2"
       />
-      <el-table-column
-        align="center"
-        label="合同金额"
-        prop="price"
-        width="130"
-        :formatter="fenToYuanFormat"
-      />
       <el-table-column
         align="center"
         label="合同开始时间"
@@ -104,17 +142,41 @@
           <el-link
             :underline="false"
             type="primary"
-            @click="openContactDetail(scope.row.contactId)"
+            @click="openContactDetail(scope.row.signContactId)"
           >
-            {{ scope.row.contactName }}
+            {{ scope.row.signContactName }}
           </el-link>
         </template>
       </el-table-column>
       <el-table-column align="center" label="公司签约人" prop="signUserName" width="130" />
-      <el-table-column align="center" label="备注" prop="remark" width="130" />
-      <!-- TODO @puhui999:后续可加 【已收款金额】、【未收款金额】 -->
+      <el-table-column align="center" label="备注" prop="remark" width="200" />
+      <el-table-column
+        align="center"
+        label="已回款金额(元)"
+        prop="totalReceivablePrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column
+        align="center"
+        label="未回款金额(元)"
+        prop="totalReceivablePrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      >
+        <template #default="scope">
+          {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.totalReceivablePrice) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="最后跟进时间"
+        prop="contactLastTime"
+        width="180px"
+      />
       <el-table-column align="center" label="负责人" prop="ownerUserName" width="120" />
-      <el-table-column align="center" label="创建人" prop="creatorName" width="120" />
+      <el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
@@ -129,6 +191,7 @@
         prop="createTime"
         width="180px"
       />
+      <el-table-column align="center" label="创建人" prop="creatorName" width="120" />
       <el-table-column align="center" fixed="right" label="合同状态" prop="auditStatus" width="120">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
@@ -137,6 +200,7 @@
       <el-table-column fixed="right" label="操作" width="250">
         <template #default="scope">
           <el-button
+            v-if="scope.row.auditStatus === 0"
             v-hasPermi="['crm:contract:update']"
             link
             type="primary"
@@ -144,8 +208,8 @@
           >
             编辑
           </el-button>
-          <!-- TODO @puhui999:可以加下判断,什么情况下,可以审批;然后加个【查看审批】按钮 -->
           <el-button
+            v-if="scope.row.auditStatus === 0"
             v-hasPermi="['crm:contract:update']"
             link
             type="primary"
@@ -153,6 +217,15 @@
           >
             提交审核
           </el-button>
+          <el-button
+            v-else
+            link
+            v-hasPermi="['crm:contract:update']"
+            type="primary"
+            @click="handleProcessDetail(scope.row)"
+          >
+            查看审批
+          </el-button>
           <el-button
             v-hasPermi="['crm:contract:query']"
             link
@@ -189,8 +262,10 @@ import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as ContractApi from '@/api/crm/contract'
 import ContractForm from './ContractForm.vue'
-import { fenToYuanFormat } from '@/utils/formatter'
 import { DICT_TYPE } from '@/utils/dict'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import * as CustomerApi from '@/api/crm/customer'
+import { TabsPaneContext } from 'element-plus'
 
 defineOptions({ name: 'CrmContract' })
 
@@ -203,16 +278,22 @@ const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
+  sceneType: '1', // 默认和 activeName 相等
   name: null,
   customerId: null,
-  businessId: null,
   orderDate: [],
-  no: null,
-  discountPercent: null,
-  productPrice: null
+  no: null
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
+const activeName = ref('1') // 列表 tab
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
+
+/** tab 切换 */
+const handleTabClick = (tab: TabsPaneContext) => {
+  queryParams.sceneType = tab.paneName
+  handleQuery()
+}
 
 /** 查询列表 */
 const getList = async () => {
@@ -280,6 +361,11 @@ const handleSubmit = async (row: ContractApi.ContractVO) => {
   await getList()
 }
 
+/** 查看审批 */
+const handleProcessDetail = (row: ContractApi.ContractVO) => {
+  push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } })
+}
+
 /** 打开合同详情 */
 const { push } = useRouter()
 const openDetail = (id: number) => {
@@ -296,8 +382,14 @@ const openContactDetail = (id: number) => {
   push({ name: 'CrmContactDetail', params: { id } })
 }
 
+/** 打开商机详情 */
+const openBusinessDetail = (id: number) => {
+  push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
 /** 初始化 **/
-onMounted(() => {
-  getList()
+onMounted(async () => {
+  await getList()
+  customerList.value = await CustomerApi.getCustomerSimpleList()
 })
 </script>

+ 0 - 221
src/views/crm/contract/oa/ContractDetail/index.vue

@@ -1,221 +0,0 @@
-<!-- TODO @puhui999:这个好像和 detail 重复了???能不能复用 detail 哈? -->
-<template>
-  <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="110px">
-    <el-row>
-      <el-col :span="24" class="mb-10px">
-        <CardTitle title="基本信息" />
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="合同名称" prop="name">
-          <el-input v-model="formData.name" placeholder="请输入合同名称" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="合同编号" prop="no">
-          <el-input v-model="formData.no" placeholder="请输入合同编号" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="客户" prop="customerId">
-          <el-select v-model="formData.customerId">
-            <el-option
-              v-for="item in customerList"
-              :key="item.id"
-              :label="item.name"
-              :value="item.id!"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="客户签约人" prop="contactId">
-          <el-select v-model="formData.contactId" :disabled="!formData.customerId">
-            <el-option
-              v-for="item in getContactOptions"
-              :key="item.id"
-              :label="item.name"
-              :value="item.id!"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="公司签约人" prop="signUserId">
-          <el-select v-model="formData.signUserId">
-            <el-option
-              v-for="item in userList"
-              :key="item.id"
-              :label="item.nickname"
-              :value="item.id!"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="负责人" prop="ownerUserId">
-          <el-select v-model="formData.ownerUserId">
-            <el-option
-              v-for="item in userList"
-              :key="item.id"
-              :label="item.nickname"
-              :value="item.id!"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商机名称" prop="businessId">
-          <el-select v-model="formData.businessId">
-            <el-option
-              v-for="item in businessList"
-              :key="item.id"
-              :label="item.name"
-              :value="item.id!"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="合同金额(元)" prop="price">
-          <el-input v-model="formData.price" placeholder="请输入合同金额" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="下单日期" prop="orderDate">
-          <el-date-picker
-            v-model="formData.orderDate"
-            placeholder="选择下单日期"
-            type="date"
-            value-format="x"
-          />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="开始时间" prop="startTime">
-          <el-date-picker
-            v-model="formData.startTime"
-            placeholder="选择开始时间"
-            type="date"
-            value-format="x"
-          />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="结束时间" prop="endTime">
-          <el-date-picker
-            v-model="formData.endTime"
-            placeholder="选择结束时间"
-            type="date"
-            value-format="x"
-          />
-        </el-form-item>
-      </el-col>
-      <el-col :span="24">
-        <el-form-item label="备注" prop="remark">
-          <el-input v-model="formData.remark" :rows="3" placeholder="请输入备注" type="textarea" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="24">
-        <el-form-item label="产品列表" prop="productList">
-          <ProductList v-model="formData.productItems" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="整单折扣(%)" prop="discountPercent">
-          <el-input v-model="formData.discountPercent" placeholder="请输入整单折扣" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="产品总金额(元)" prop="productPrice">
-          <el-input v-model="formData.productPrice" placeholder="请输入产品总金额" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="24">
-        <CardTitle class="mb-10px" title="审批信息" />
-      </el-col>
-      <el-col :span="12">
-        <el-button
-          class="m-20px"
-          link
-          type="primary"
-          @click="BPMLModelRef?.handleBpmnDetail('contract-approve')"
-        >
-          查看工作流
-        </el-button>
-      </el-col>
-    </el-row>
-  </el-form>
-  <BPMLModel ref="BPMLModelRef" />
-</template>
-<script lang="ts" setup>
-import * as CustomerApi from '@/api/crm/customer'
-import * as ContractApi from '@/api/crm/contract'
-import * as UserApi from '@/api/system/user'
-import * as ContactApi from '@/api/crm/contact'
-import * as BusinessApi from '@/api/crm/business'
-import ProductList from '@/views/crm/contract/components/ProductList.vue'
-// import BPMLModel from '@/views/crm/contract/components/BPMLModel.vue'
-
-defineOptions({ name: 'ContractDetailOA' })
-const props = defineProps<{ id?: number }>()
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formData = ref<ContractApi.ContractVO>({} as ContractApi.ContractVO)
-const formRef = ref() // 表单 Ref
-const BPMLModelRef = ref<InstanceType<typeof BPMLModel>>()
-watch(
-  () => formData.value.productItems,
-  (val) => {
-    if (!val || val.length === 0) {
-      formData.value.productPrice = 0
-      return
-    }
-    // 使用reduce函数进行累加
-    formData.value.productPrice = val.reduce(
-      (accumulator, currentValue) =>
-        isNaN(accumulator + currentValue.totalPrice) ? 0 : accumulator + currentValue.totalPrice,
-      0
-    )
-  },
-  { deep: true }
-)
-/** 打开弹窗 */
-const getFormData = async () => {
-  await getAllApi()
-  formLoading.value = true
-  try {
-    formData.value = await ContractApi.getContract(props.id!)
-  } finally {
-    formLoading.value = false
-  }
-}
-const getAllApi = async () => {
-  await Promise.all([getCustomerList(), getUserList(), getContactListList(), getBusinessList()])
-}
-const customerList = ref<CustomerApi.CustomerVO[]>([])
-/** 获取客户 */
-const getCustomerList = async () => {
-  customerList.value = await CustomerApi.getCustomerSimpleList()
-}
-const contactList = ref<ContactApi.ContactVO[]>([])
-/** 动态获取客户联系人 */
-const getContactOptions = computed(() =>
-  contactList.value.filter((item) => item.customerId === formData.value.customerId)
-)
-const getContactListList = async () => {
-  contactList.value = await ContactApi.getSimpleContactList()
-}
-const userList = ref<UserApi.UserVO[]>([])
-/** 获取用户列表 */
-const getUserList = async () => {
-  userList.value = await UserApi.getSimpleUserList()
-}
-const businessList = ref<BusinessApi.BusinessVO[]>([])
-/** 获取商机 */
-const getBusinessList = async () => {
-  businessList.value = await BusinessApi.getSimpleBusinessList()
-}
-
-onMounted(() => {
-  getFormData()
-})
-</script>

+ 9 - 6
src/views/crm/customer/CustomerImportForm.vue

@@ -4,7 +4,6 @@
     <el-upload
       ref="uploadRef"
       v-model:file-list="fileList"
-      :action="importUrl + '?updateSupport=' + updateSupport"
       :auto-upload="false"
       :disabled="formLoading"
       :headers="uploadHeaders"
@@ -13,6 +12,7 @@
       :on-exceed="handleExceed"
       :on-success="submitFormSuccess"
       accept=".xlsx, .xls"
+      action="none"
       drag
     >
       <Icon icon="ep:upload" />
@@ -45,6 +45,7 @@
 import * as CustomerApi from '@/api/crm/customer'
 import { getAccessToken, getTenantId } from '@/utils/auth'
 import download from '@/utils/download'
+import type { UploadUserFile } from 'element-plus'
 
 defineOptions({ name: 'SystemUserImportForm' })
 
@@ -53,11 +54,9 @@ const message = useMessage() // 消息弹窗
 const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
 const uploadRef = ref()
-const importUrl =
-  import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/crm/customer/import'
 const uploadHeaders = ref() // 上传 Header 头
-const fileList = ref([]) // 文件列表
-const updateSupport = ref(0) // 是否更新已经存在的客户数据
+const fileList = ref<UploadUserFile[]>([]) // 文件列表
+const updateSupport = ref(false) // 是否更新已经存在的客户数据
 
 /** 打开弹窗 */
 const open = () => {
@@ -79,7 +78,11 @@ const submitForm = async () => {
     'tenant-id': getTenantId()
   }
   formLoading.value = true
-  uploadRef.value!.submit()
+  const formData = new FormData()
+  formData.append('updateSupport', updateSupport.value)
+  formData.append('file', fileList.value[0].raw)
+  // TODO @芋艿:后面是不是可以采用这种形式,去掉 uploadHeaders
+  await CustomerApi.handleImport(formData)
 }
 
 /** 文件上传成功 */

+ 12 - 4
src/views/crm/customer/detail/index.vue

@@ -26,7 +26,7 @@
     >
       锁定
     </el-button>
-    <el-button v-if="!customer.ownerUserId" type="primary" @click="handleReceive"> 领取 </el-button>
+    <el-button v-if="!customer.ownerUserId" type="primary" @click="handleReceive"> 领取</el-button>
     <el-button v-if="!customer.ownerUserId" type="primary" @click="handleDistributeForm">
       分配
     </el-button>
@@ -64,8 +64,8 @@
         <ContractList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
       </el-tab-pane>
       <el-tab-pane label="回款" lazy>
-        <ReceivablePlanList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
-        <ReceivableList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
+        <ReceivablePlanList :customer-id="customer.id!" @create-receivable="createReceivable" />
+        <ReceivableList ref="receivableListRef" :customer-id="customer.id!" />
       </el-tab-pane>
       <el-tab-pane label="操作日志">
         <OperateLogV2 :log-list="logList" />
@@ -103,7 +103,7 @@ const customerId = ref(0) // 客户编号
 const loading = ref(true) // 加载中
 const message = useMessage() // 消息弹窗
 const { delView } = useTagsViewStore() // 视图操作
-const { currentRoute } = useRouter() // 路由
+const { push, currentRoute } = useRouter() // 路由
 
 const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
 
@@ -180,6 +180,7 @@ const handlePutPool = async () => {
   await message.confirm(`确定将客户【${customer.value.name}】放入公海吗?`)
   await CustomerApi.putCustomerPool(unref(customerId.value))
   message.success(`客户【${customer.value.name}】放入公海成功`)
+  // 加载
   close()
 }
 
@@ -196,8 +197,15 @@ const getOperateLog = async () => {
   logList.value = data.list
 }
 
+/** 从回款计划创建回款 */
+const receivableListRef = ref<InstanceType<typeof ReceivableList>>() // 回款列表 Ref
+const createReceivable = (planData: any) => {
+  receivableListRef.value?.createReceivable(planData)
+}
+
 const close = () => {
   delView(unref(currentRoute))
+  push({ name: 'CrmCustomer' })
 }
 
 /** 初始化 */

+ 12 - 85
src/views/crm/customer/limitConfig/CustomerLimitConfigForm.vue

@@ -8,16 +8,14 @@
       v-loading="formLoading"
     >
       <el-form-item label="规则适用人群" prop="userIds">
-        <el-tree-select
-          v-model="formData.userIds"
-          :data="userTree"
-          :props="defaultProps"
-          multiple
-          filterable
-          check-on-click-node
-          node-key="id"
-          placeholder="请选择规则适用人群"
-        />
+        <el-select multiple filterable v-model="formData.userIds">
+          <el-option
+            v-for="item in userOptions"
+            :key="item.id"
+            :label="item.nickname"
+            :value="item.id"
+          />
+        </el-select>
       </el-form-item>
       <el-form-item label="规则适用部门" prop="deptIds">
         <el-tree-select
@@ -62,6 +60,7 @@ import { defaultProps, handleTree } from '@/utils/tree'
 import * as UserApi from '@/api/system/user'
 import { cloneDeep } from 'lodash-es'
 import { LimitConfType } from '@/api/crm/customer/limitConfig'
+import { aw } from '../../../../../dist-prod/assets/index-9eac537b'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -85,7 +84,7 @@ const formRules = reactive({
 const formRef = ref() // 表单 Ref
 // TODO @芋艿:看看怎么搞个部门选择组件
 const deptTree = ref() // 部门树形结构
-const userTree = ref() // 用户树形结构
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 
 /** 打开弹窗 */
 const open = async (type: string, limitConfType: LimitConfType, id?: number) => {
@@ -105,9 +104,9 @@ const open = async (type: string, limitConfType: LimitConfType, id?: number) =>
     formData.value.type = limitConfType
   }
   // 获得部门树
-  await getDeptTree()
+  deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
   // 获得用户
-  await getUserTree()
+  userOptions.value = await UserApi.getSimpleUserList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -149,76 +148,4 @@ const resetForm = () => {
   }
   formRef.value?.resetFields()
 }
-
-/**
- * 获取部门树
- */
-const getDeptTree = async () => {
-  const res = await DeptApi.getSimpleDeptList()
-  deptTree.value = []
-  deptTree.value.push(...handleTree(res))
-}
-
-/**
- * 获取用户树
- */
-const getUserTree = async () => {
-  const res = await UserApi.getAllUser()
-  userTree.value = []
-  userTree.value = cloneDeep(unref(deptTree))
-
-  const deptUserMap = {}
-  res.forEach((user) => {
-    if (user.dept) {
-      if (!deptUserMap[user.deptId]) {
-        deptUserMap[user.deptId] = []
-      }
-      deptUserMap[user.deptId].push(user)
-    }
-  })
-
-  handleUserData(userTree.value, deptUserMap)
-}
-
-// TODO @芋艿:看看怎么搞个用户选择的组件
-/**
- * 处理用户树
- *
- * @param deptTree
- * @param deptUserMap
- */
-const handleUserData = (deptTree, deptUserMap) => {
-  for (let i = 0; i < deptTree.length; i++) {
-    // 如果是用户,就不用继续找部门下的用户
-    if (deptTree[i].isUser) {
-      continue
-    }
-    const users = deptUserMap[deptTree[i].id]
-    if (users) {
-      if (!deptTree[i].children) {
-        deptTree[i].children = []
-      }
-      deptTree[i].children.push(
-        ...users.map((user) => {
-          return {
-            id: user.id,
-            name: user.username + '-' + user.nickname,
-            isUser: true,
-            // 用户状态为关闭
-            disabled: user.status === 1
-          }
-        })
-      )
-    }
-
-    if (deptTree[i].children && deptTree[i].children.length !== 0) {
-      handleUserData(deptTree[i].children, deptUserMap)
-    }
-
-    // 非人员选项禁用
-    deptTree[i].disabled = true
-    // 将非人员的 id 置为空
-    deptTree[i].id = 'null'
-  }
-}
 </script>

+ 1 - 1
src/views/crm/customer/limitConfig/CustomerLimitConfigList.vue

@@ -16,7 +16,6 @@
     class="mt-4"
   >
     <el-table-column label="编号" align="center" prop="id" />
-    <el-table-column label="规则类型" align="center" prop="type" />
     <el-table-column
       label="规则适用人群"
       align="center"
@@ -39,6 +38,7 @@
       label="成交客户是否占用拥有客户数"
       align="center"
       prop="dealCountEnabled"
+      min-width="100"
     >
       <template #default="scope">
         <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealCountEnabled" />

+ 68 - 41
src/views/crm/followup/FollowUpRecordForm.vue

@@ -1,6 +1,6 @@
 <!-- 跟进记录的添加表单弹窗 -->
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
+  <Dialog v-model="dialogVisible" title="添加跟进记录" width="50%">
     <el-form
       ref="formRef"
       v-loading="formLoading"
@@ -36,32 +36,32 @@
             <el-input v-model="formData.content" :rows="3" type="textarea" />
           </el-form-item>
         </el-col>
-        <el-col :span="24">
-          <el-form-item label="图片" prop="content">
+        <el-col :span="12">
+          <el-form-item label="图片" prop="picUrls">
             <UploadImgs v-model="formData.picUrls" class="min-w-80px" />
           </el-form-item>
         </el-col>
-        <el-col :span="24">
-          <el-form-item label="附件" prop="content">
+        <el-col :span="12">
+          <el-form-item label="附件" prop="fileUrls">
             <UploadFile v-model="formData.fileUrls" class="min-w-80px" />
           </el-form-item>
         </el-col>
-        <el-col :span="24">
+        <el-col :span="24" v-if="formData.bizType == BizTypeEnum.CRM_CUSTOMER">
           <el-form-item label="关联联系人" prop="contactIds">
-            <el-button @click="handleAddContact">
+            <el-button @click="handleOpenContact">
               <Icon class="mr-5px" icon="ep:plus" />
               添加联系人
             </el-button>
-            <contact-list v-model:contactIds="formData.contactIds" />
+            <FollowUpRecordContactForm :contacts="formData.contacts" />
           </el-form-item>
         </el-col>
-        <el-col :span="24">
+        <el-col :span="24" v-if="formData.bizType == BizTypeEnum.CRM_CUSTOMER">
           <el-form-item label="关联商机" prop="businessIds">
-            <el-button @click="handleAddBusiness">
+            <el-button @click="handleOpenBusiness">
               <Icon class="mr-5px" icon="ep:plus" />
               添加商机
             </el-button>
-            <business-list v-model:businessIds="formData.businessIds" />
+            <FollowUpRecordBusinessForm :businesses="formData.businesses" />
           </el-form-item>
         </el-col>
       </el-row>
@@ -71,13 +71,29 @@
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
-  <ContactTableSelect ref="contactTableSelectRef" v-model="formData.contactIds" />
-  <BusinessTableSelect ref="businessTableSelectRef" v-model="formData.businessIds" />
+
+  <!-- 弹窗 -->
+  <ContactListModal
+    ref="contactTableSelectRef"
+    :customer-id="formData.bizId"
+    @success="handleAddContact"
+  />
+  <BusinessListModal
+    ref="businessTableSelectRef"
+    :customer-id="formData.bizId"
+    @success="handleAddBusiness"
+  />
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { FollowUpRecordApi, FollowUpRecordVO } from '@/api/crm/followup'
-import { BusinessList, BusinessTableSelect, ContactList, ContactTableSelect } from './components'
+import { BizTypeEnum } from '@/api/crm/permission'
+import FollowUpRecordBusinessForm from './components/FollowUpRecordBusinessForm.vue'
+import FollowUpRecordContactForm from './components/FollowUpRecordContactForm.vue'
+import BusinessListModal from '@/views/crm/business/components/BusinessListModal.vue'
+import * as BusinessApi from '@/api/crm/business'
+import ContactListModal from '@/views/crm/contact/components/ContactListModal.vue'
+import * as ContactApi from '@/api/crm/contact'
 
 defineOptions({ name: 'FollowUpRecordForm' })
 
@@ -87,8 +103,12 @@ const message = useMessage() // 消息弹窗
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref<FollowUpRecordVO>({} as FollowUpRecordVO)
+const formData = ref({
+  bizType: undefined,
+  bizId: undefined,
+  businesses: [],
+  contacts: []
+})
 const formRules = reactive({
   type: [{ required: true, message: '跟进类型不能为空', trigger: 'change' }],
   content: [{ required: true, message: '跟进内容不能为空', trigger: 'blur' }],
@@ -98,22 +118,11 @@ const formRules = reactive({
 const formRef = ref() // 表单 Ref
 
 /** 打开弹窗 */
-const open = async (bizType: number, bizId: number, type: string, id?: number) => {
+const open = async (bizType: number, bizId: number) => {
   dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
   resetForm()
   formData.value.bizType = bizType
   formData.value.bizId = bizId
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await FollowUpRecordApi.getFollowUpRecord(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -125,14 +134,13 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as FollowUpRecordVO
-    if (formType.value === 'create') {
-      await FollowUpRecordApi.createFollowUpRecord(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await FollowUpRecordApi.updateFollowUpRecord(data)
-      message.success(t('common.updateSuccess'))
-    }
+    const data = {
+      ...formData.value,
+      contactIds: formData.value.contacts.map((item) => item.id),
+      businessIds: formData.value.businesses.map((item) => item.id)
+    } as unknown as FollowUpRecordVO
+    await FollowUpRecordApi.createFollowUpRecord(data)
+    message.success(t('common.createSuccess'))
     dialogVisible.value = false
     // 发送操作成功的事件
     emit('success')
@@ -142,20 +150,39 @@ const submitForm = async () => {
 }
 
 /** 关联联系人 */
-const contactTableSelectRef = ref<InstanceType<typeof ContactTableSelect>>()
-const handleAddContact = () => {
+const contactTableSelectRef = ref<InstanceType<typeof ContactListModal>>()
+const handleOpenContact = () => {
   contactTableSelectRef.value?.open()
 }
+const handleAddContact = (contactId: [], newContacts: ContactApi.ContactVO[]) => {
+  newContacts.forEach((contact) => {
+    if (!formData.value.contacts.some((item) => item.id === contact.id)) {
+      formData.value.contacts.push(contact)
+    }
+  })
+}
 
 /** 关联商机 */
-const businessTableSelectRef = ref<InstanceType<typeof BusinessTableSelect>>()
-const handleAddBusiness = () => {
+const businessTableSelectRef = ref<InstanceType<typeof BusinessListModal>>()
+const handleOpenBusiness = () => {
   businessTableSelectRef.value?.open()
 }
+const handleAddBusiness = (businessId: [], newBusinesses: BusinessApi.BusinessVO[]) => {
+  newBusinesses.forEach((business) => {
+    if (!formData.value.businesses.some((item) => item.id === business.id)) {
+      formData.value.businesses.push(business)
+    }
+  })
+}
 
 /** 重置表单 */
 const resetForm = () => {
   formRef.value?.resetFields()
-  formData.value = {} as FollowUpRecordVO
+  formData.value = {
+    bizId: undefined,
+    bizType: undefined,
+    businesses: [],
+    contacts: []
+  }
 }
 </script>

+ 0 - 71
src/views/crm/followup/components/BusinessList.vue

@@ -1,71 +0,0 @@
-<template>
-  <el-table :data="list" :show-overflow-tooltip="true" :stripe="true" height="200">
-    <el-table-column align="center" label="商机名称" prop="name" />
-    <el-table-column align="center" label="客户名称" prop="customerName" />
-    <el-table-column align="center" label="商机金额" prop="price" />
-    <el-table-column
-      :formatter="dateFormatter"
-      align="center"
-      label="预计成交日期"
-      prop="dealTime"
-      width="120px"
-    />
-    <el-table-column align="center" label="商机状态类型" prop="statusTypeName" width="120" />
-    <el-table-column align="center" label="商机状态" prop="statusName" />
-    <el-table-column
-      :formatter="dateFormatter"
-      align="center"
-      label="更新时间"
-      prop="updateTime"
-      width="180px"
-    />
-    <el-table-column
-      :formatter="dateFormatter"
-      align="center"
-      label="创建时间"
-      prop="createTime"
-      width="180px"
-    />
-    <el-table-column align="center" label="负责人" prop="ownerUserName" width="120" />
-    <el-table-column align="center" label="创建人" prop="creatorName" width="120" />
-    <el-table-column align="center" label="备注" prop="remark" />
-    <el-table-column align="center" fixed="right" label="操作" width="130">
-      <template #default="scope">
-        <el-button link type="danger" @click="handleDelete(scope.row.id)"> 移除</el-button>
-      </template>
-    </el-table-column>
-  </el-table>
-</template>
-
-<script lang="ts" setup>
-import { dateFormatter } from '@/utils/formatTime'
-import * as BusinessApi from '@/api/crm/business'
-
-defineOptions({ name: 'BusinessList' })
-const props = withDefaults(defineProps<{ businessIds: number[] }>(), {
-  businessIds: () => []
-})
-const list = ref<BusinessApi.BusinessVO[]>([] as BusinessApi.BusinessVO[])
-watch(
-  () => props.businessIds,
-  (val) => {
-    if (!val || val.length === 0) {
-      return
-    }
-    list.value = BusinessApi.getBusinessListByIds(unref(val)) as unknown as BusinessApi.BusinessVO[]
-  }
-)
-const emits = defineEmits<{
-  (e: 'update:businessIds', businessIds: number[]): void
-}>()
-const handleDelete = (id: number) => {
-  const index = list.value.findIndex((item) => item.id === id)
-  if (index !== -1) {
-    list.value.splice(index, 1)
-  }
-  emits(
-    'update:businessIds',
-    list.value.map((item) => item.id)
-  )
-}
-</script>

+ 0 - 88
src/views/crm/followup/components/BusinessTableSelect.vue

@@ -1,88 +0,0 @@
-<!-- 商机的选择列表 TODO 芋艿:后面看看要不要搞到统一封装里 -->
-<template>
-  <Dialog v-model="dialogVisible" :appendToBody="true" title="选择商机" width="700">
-    <el-table
-      ref="multipleTableRef"
-      v-loading="loading"
-      :data="list"
-      :show-overflow-tooltip="true"
-      :stripe="true"
-      @selection-change="handleSelectionChange"
-    >
-      <el-table-column type="selection" width="55" />
-      <el-table-column align="center" label="商机名称" prop="name" />
-      <el-table-column align="center" label="客户名称" prop="customerName" />
-      <el-table-column align="center" label="商机金额" prop="price" />
-      <el-table-column
-        :formatter="dateFormatter"
-        align="center"
-        label="预计成交日期"
-        prop="dealTime"
-        width="180px"
-      />
-      <el-table-column align="center" label="备注" prop="remark" />
-    </el-table>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-
-<script lang="ts" setup>
-import * as BusinessApi from '@/api/crm/business'
-import { dateFormatter } from '@/utils/formatTime'
-import { ElTable } from 'element-plus'
-
-defineOptions({ name: 'BusinessTableSelect' })
-withDefaults(defineProps<{ modelValue: number[] }>(), { modelValue: () => [] })
-
-const list = ref<BusinessApi.BusinessVO[]>([]) // 列表的数据
-const loading = ref(false) // 列表的加载中
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false)
-
-// 确认选择时的触发事件
-const emits = defineEmits<{
-  (e: 'update:modelValue', v: number[]): void
-}>()
-const multipleTableRef = ref<InstanceType<typeof ElTable>>()
-const multipleSelection = ref<BusinessApi.BusinessVO[]>([])
-const handleSelectionChange = (val: BusinessApi.BusinessVO[]) => {
-  multipleSelection.value = val
-}
-/** 触发 */
-const submitForm = () => {
-  formLoading.value = true
-  try {
-    emits(
-      'update:modelValue',
-      multipleSelection.value.map((item) => item.id)
-    )
-  } finally {
-    formLoading.value = false
-    // 关闭弹窗
-    dialogVisible.value = false
-  }
-}
-
-const getList = async () => {
-  loading.value = true
-  try {
-    list.value = await BusinessApi.getSimpleBusinessList()
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 打开弹窗 */
-const open = async () => {
-  dialogVisible.value = true
-  await nextTick()
-  if (multipleSelection.value.length > 0) {
-    multipleTableRef.value!.clearSelection()
-  }
-  await getList()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-</script>

+ 0 - 97
src/views/crm/followup/components/ContactList.vue

@@ -1,97 +0,0 @@
-<template>
-  <el-table :data="list" :show-overflow-tooltip="true" :stripe="true" height="200">
-    <el-table-column align="center" fixed="left" label="姓名" prop="name" width="140" />
-    <el-table-column align="center" fixed="left" label="客户名称" prop="customerName" width="120" />
-    <el-table-column align="center" label="手机" prop="mobile" width="120" />
-    <el-table-column align="center" label="电话" prop="telephone" width="120" />
-    <el-table-column align="center" label="邮箱" prop="email" width="120" />
-    <el-table-column align="center" label="职位" prop="post" width="120" />
-    <el-table-column align="center" label="地址" prop="detailAddress" width="120" />
-    <el-table-column
-      :formatter="dateFormatter"
-      align="center"
-      label="下次联系时间"
-      prop="contactNextTime"
-      width="180px"
-    />
-    <el-table-column align="center" label="关键决策人" prop="master" width="100">
-      <template #default="scope">
-        <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
-      </template>
-    </el-table-column>
-    <el-table-column align="center" label="直属上级" prop="parentName" width="140" />
-    <el-table-column
-      :formatter="dateFormatter"
-      align="center"
-      label="最后跟进时间"
-      prop="contactLastTime"
-      width="180px"
-    />
-    <el-table-column align="center" label="性别" prop="sex">
-      <template #default="scope">
-        <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
-      </template>
-    </el-table-column>
-    <el-table-column align="center" label="负责人" prop="ownerUserName" width="120" />
-    <el-table-column align="center" label="创建人" prop="creatorName" width="120" />
-    <el-table-column
-      :formatter="dateFormatter"
-      align="center"
-      label="更新时间"
-      prop="updateTime"
-      width="180px"
-    />
-    <el-table-column
-      :formatter="dateFormatter"
-      align="center"
-      label="创建时间"
-      prop="createTime"
-      width="180px"
-    />
-    <el-table-column align="center" label="备注" prop="remark" />
-    <el-table-column align="center" fixed="right" label="操作" width="130">
-      <template #default="scope">
-        <el-button link type="danger" @click="handleDelete(scope.row.id)"> 移除</el-button>
-      </template>
-    </el-table-column>
-  </el-table>
-</template>
-
-<script lang="ts" setup>
-import { dateFormatter } from '@/utils/formatTime'
-import { DICT_TYPE } from '@/utils/dict'
-import * as ContactApi from '@/api/crm/contact'
-
-defineOptions({ name: 'ContactList' })
-const props = withDefaults(defineProps<{ contactIds: number[] }>(), {
-  contactIds: () => []
-})
-const list = ref<ContactApi.ContactVO[]>([] as ContactApi.ContactVO[])
-const getContactList = async () => {
-  list.value = (await ContactApi.getContactListByIds(
-    unref(props.contactIds)
-  )) as unknown as ContactApi.ContactVO[]
-}
-watch(
-  () => props.contactIds,
-  (val) => {
-    if (!val || val.length === 0) {
-      return
-    }
-    getContactList()
-  }
-)
-const emits = defineEmits<{
-  (e: 'update:contactIds', contactIds: number[]): void
-}>()
-const handleDelete = (id: number) => {
-  const index = list.value.findIndex((item) => item.id === id)
-  if (index !== -1) {
-    list.value.splice(index, 1)
-  }
-  emits(
-    'update:contactIds',
-    list.value.map((item) => item.id)
-  )
-}
-</script>

+ 0 - 87
src/views/crm/followup/components/ContactTableSelect.vue

@@ -1,87 +0,0 @@
-<!-- 联系人的选择列表 TODO 芋艿:后面看看要不要搞到统一封装里 -->
-<template>
-  <Dialog v-model="dialogVisible" :appendToBody="true" title="选择联系人" width="700">
-    <el-table
-      ref="multipleTableRef"
-      v-loading="loading"
-      :data="list"
-      :show-overflow-tooltip="true"
-      :stripe="true"
-      @selection-change="handleSelectionChange"
-    >
-      <el-table-column type="selection" width="55" />
-      <el-table-column align="center" fixed="left" label="姓名" prop="name" width="140" />
-      <el-table-column
-        align="center"
-        fixed="left"
-        label="客户名称"
-        prop="customerName"
-        width="120"
-      />
-      <el-table-column align="center" label="手机" prop="mobile" width="120" />
-      <el-table-column align="center" label="电话" prop="telephone" width="120" />
-      <el-table-column align="center" label="邮箱" prop="email" width="120" />
-      <el-table-column align="center" label="职位" prop="post" width="120" />
-    </el-table>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-
-<script lang="ts" setup>
-import * as ContactApi from '@/api/crm/contact'
-import { ElTable } from 'element-plus'
-
-defineOptions({ name: 'ContactTableSelect' })
-withDefaults(defineProps<{ modelValue: number[] }>(), { modelValue: () => [] })
-
-const list = ref<ContactApi.ContactVO[]>([]) // 列表的数据
-const loading = ref(false) // 列表的加载中
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false)
-
-// 确认选择时的触发事件
-const emits = defineEmits<{
-  (e: 'update:modelValue', v: number[]): void
-}>()
-const multipleTableRef = ref<InstanceType<typeof ElTable>>()
-const multipleSelection = ref<ContactApi.ContactVO[]>([])
-const handleSelectionChange = (val: ContactApi.ContactVO[]) => {
-  multipleSelection.value = val
-}
-/** 触发 */
-const submitForm = () => {
-  formLoading.value = true
-  try {
-    emits(
-      'update:modelValue',
-      multipleSelection.value.map((item) => item.id)
-    )
-  } finally {
-    formLoading.value = false
-    // 关闭弹窗
-    dialogVisible.value = false
-  }
-}
-const getList = async () => {
-  loading.value = true
-  try {
-    list.value = await ContactApi.getSimpleContactList()
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 打开弹窗 */
-const open = async () => {
-  dialogVisible.value = true
-  await nextTick()
-  if (multipleSelection.value.length > 0) {
-    multipleTableRef.value!.clearSelection()
-  }
-  await getList()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-</script>

+ 42 - 0
src/views/crm/followup/components/FollowUpRecordBusinessForm.vue

@@ -0,0 +1,42 @@
+<template>
+  <el-table :data="formData" :show-overflow-tooltip="true" :stripe="true" height="120">
+    <el-table-column label="商机名称" fixed="left" align="center" prop="name" />
+    <el-table-column
+      label="商机金额"
+      align="center"
+      prop="totalPrice"
+      :formatter="erpPriceTableColumnFormatter"
+    />
+    <el-table-column label="客户名称" align="center" prop="customerName" />
+    <el-table-column label="商机组" align="center" prop="statusTypeName" />
+    <el-table-column label="商机阶段" align="center" prop="statusName" />
+    <el-table-column align="center" fixed="right" label="操作" width="80">
+      <template #default="{ $index }">
+        <el-button link type="danger" @click="handleDelete($index)"> 移除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script lang="ts" setup>
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+const props = defineProps<{
+  businesses: undefined
+}>()
+const formData = ref([])
+
+/** 初始化商机列表 */
+watch(
+  () => props.businesses,
+  async (val) => {
+    formData.value = val
+  },
+  { immediate: true }
+)
+
+/** 删除按钮操作 */
+const handleDelete = (index: number) => {
+  formData.value.splice(index, 1)
+}
+</script>

+ 47 - 0
src/views/crm/followup/components/FollowUpRecordContactForm.vue

@@ -0,0 +1,47 @@
+<template>
+  <el-table :data="contacts" :show-overflow-tooltip="true" :stripe="true" height="150">
+    <el-table-column label="姓名" fixed="left" align="center" prop="name">
+      <template #default="scope">
+        <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+          {{ scope.row.name }}
+        </el-link>
+      </template>
+    </el-table-column>
+    <el-table-column label="手机号" align="center" prop="mobile" />
+    <el-table-column label="职位" align="center" prop="post" />
+    <el-table-column label="直属上级" align="center" prop="parentName" />
+    <el-table-column label="是否关键决策人" align="center" prop="master" min-width="100">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" fixed="right" label="操作" width="130">
+      <template #default="scope">
+        <el-button link type="danger" @click="handleDelete(scope.row.id)"> 移除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+
+const props = defineProps<{
+  contacts: undefined
+}>()
+const formData = ref([])
+
+/** 初始化联系人列表 */
+watch(
+  () => props.contacts,
+  async (val) => {
+    formData.value = val
+  },
+  { immediate: true }
+)
+
+/** 删除按钮操作 */
+const handleDelete = (index: number) => {
+  formData.value.splice(index, 1)
+}
+</script>

+ 0 - 6
src/views/crm/followup/components/index.ts

@@ -1,6 +0,0 @@
-import BusinessList from './BusinessList.vue'
-import BusinessTableSelect from './BusinessTableSelect.vue'
-import ContactList from './ContactList.vue'
-import ContactTableSelect from './ContactTableSelect.vue'
-
-export { BusinessList, BusinessTableSelect, ContactList, ContactTableSelect }

+ 57 - 28
src/views/crm/followup/index.vue

@@ -2,7 +2,7 @@
 <template>
   <!-- 操作栏 -->
   <el-row class="mb-10px" justify="end">
-    <el-button @click="openForm('create')">
+    <el-button @click="openForm">
       <Icon class="mr-5px" icon="ep:edit" />
       写跟进
     </el-button>
@@ -10,8 +10,13 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-      <el-table-column align="center" label="编号" prop="id" />
-      <!-- TODO @puhui999:展示不出来 -->
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="创建时间"
+        prop="createTime"
+        width="180px"
+      />
       <el-table-column align="center" label="跟进人" prop="creatorName" />
       <el-table-column align="center" label="跟进类型" prop="type">
         <template #default="scope">
@@ -26,35 +31,47 @@
         prop="nextTime"
         width="180px"
       />
-      <!-- TODO @puhui999:点击后,查看关联联系人 -->
-      <el-table-column align="center" label="关联联系人" prop="contactIds" />
-      <!-- TODO @puhui999:点击后,查看关联商机 -->
-      <el-table-column align="center" label="关联商机" prop="businessIds" />
       <el-table-column
-        :formatter="dateFormatter"
         align="center"
-        label="创建时间"
-        prop="createTime"
-        width="180px"
-      />
-      <el-table-column align="center" label="操作">
+        label="关联联系人"
+        prop="contactIds"
+        v-if="bizType === BizTypeEnum.CRM_CUSTOMER"
+      >
         <template #default="scope">
-          <el-button
-            v-hasPermi="['crm:follow-up-record:update']"
-            link
+          <el-link
+            v-for="contact in scope.row.contacts"
+            :key="`key-${contact.id}`"
+            :underline="false"
             type="primary"
-            @click="openForm('update', scope.row.id)"
+            @click="openContactDetail(contact.id)"
+            class="ml-5px"
           >
-            编辑
-          </el-button>
-          <el-button
-            v-hasPermi="['crm:follow-up-record:delete']"
-            link
-            type="danger"
-            @click="handleDelete(scope.row.id)"
+            {{ contact.name }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        label="关联商机"
+        prop="businessIds"
+        v-if="bizType === BizTypeEnum.CRM_CUSTOMER"
+      >
+        <template #default="scope">
+          <el-link
+            v-for="business in scope.row.businesses"
+            :key="`key-${business.id}`"
+            :underline="false"
+            type="primary"
+            @click="openBusinessDetail(business.id)"
+            class="ml-5px"
           >
-            删除
-          </el-button>
+            {{ business.name }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="操作">
+        <template #default="scope">
+          <el-button link type="danger" @click="handleDelete(scope.row.id)"> 删除 </el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -76,6 +93,7 @@ import { dateFormatter } from '@/utils/formatTime'
 import { DICT_TYPE } from '@/utils/dict'
 import { FollowUpRecordApi, FollowUpRecordVO } from '@/api/crm/followup'
 import FollowUpRecordForm from './FollowUpRecordForm.vue'
+import { BizTypeEnum } from '@/api/crm/permission'
 
 /** 跟进记录列表 */
 defineOptions({ name: 'FollowUpRecord' })
@@ -110,8 +128,8 @@ const getList = async () => {
 
 /** 添加/修改操作 */
 const formRef = ref<InstanceType<typeof FollowUpRecordForm>>()
-const openForm = (type: string, id?: number) => {
-  formRef.value?.open(props.bizType, props.bizId, type, id)
+const openForm = () => {
+  formRef.value?.open(props.bizType, props.bizId)
 }
 
 /** 删除按钮操作 */
@@ -127,6 +145,17 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+/** 打开联系人详情 */
+const { push } = useRouter()
+const openContactDetail = (id: number) => {
+  push({ name: 'CrmContactDetail', params: { id } })
+}
+
+/** 打开商机详情 */
+const openBusinessDetail = (id: number) => {
+  push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
 watch(
   () => props.bizId,
   () => {

+ 1 - 1
src/views/crm/permission/components/PermissionForm.vue

@@ -30,7 +30,7 @@
         </el-radio-group>
       </el-form-item>
       <!-- TODO @puhui999:同时添加至,还没想好下次搞 -->
-      <el-form-item v-if="formType === 'create'" label="同时添加至" prop="toBizType">
+      <el-form-item v-if="false && formType === 'create'" label="同时添加至" prop="toBizType">
         <el-select v-model="formData.userId">
           <el-option :value="1" label="联系人" />
           <el-option :value="1" label="商机" />

+ 2 - 7
src/views/crm/product/ProductForm.vue

@@ -100,11 +100,10 @@
 <script setup lang="ts">
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as ProductApi from '@/api/crm/product'
-import * as ProductCategoryApi from '@/api/crm/product/productCategory'
+import * as ProductCategoryApi from '@/api/crm/product/category'
 import { defaultProps, handleTree } from '@/utils/tree'
 import { getSimpleUserList, UserVO } from '@/api/system/user'
 import { useUserStore } from '@/store/modules/user'
-import { fenToYuan, yuanToFen } from '@/utils'
 
 defineOptions({ name: 'CrmProductForm' })
 
@@ -149,7 +148,6 @@ const open = async (type: string, id?: number) => {
     formLoading.value = true
     try {
       formData.value = await ProductApi.getProduct(id)
-      formData.value.price = Number(fenToYuan(formData.value.price))
     } finally {
       formLoading.value = false
     }
@@ -169,10 +167,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = {
-      ...formData.value,
-      price: yuanToFen(formData.value.price)
-    } as unknown as ProductApi.ProductVO
+    const data = formData.value as unknown as ProductApi.ProductVO
     if (formType.value === 'create') {
       await ProductApi.createProduct(data)
       message.success(t('common.createSuccess'))

+ 1 - 1
src/views/crm/product/category/ProductCategoryForm.vue

@@ -29,7 +29,7 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import * as ProductCategoryApi from '@/api/crm/product/productCategory'
+import * as ProductCategoryApi from '@/api/crm/product/category'
 
 defineOptions({ name: 'CrmProductCategoryForm' })
 

+ 1 - 1
src/views/crm/product/category/index.vue

@@ -73,7 +73,7 @@
 
 <script setup lang="ts">
 import { dateFormatter } from '@/utils/formatTime'
-import * as ProductCategoryApi from '@/api/crm/product/productCategory'
+import * as ProductCategoryApi from '@/api/crm/product/category'
 import ProductCategoryForm from './ProductCategoryForm.vue'
 import { handleTree } from '@/utils/tree'
 

+ 5 - 14
src/views/crm/product/detail/ProductDetailsHeader.vue

@@ -18,13 +18,13 @@
   </div>
   <ContentWrap class="mt-10px">
     <el-descriptions :column="5" direction="vertical">
-      <el-descriptions-item label="产品类别">
-        {{ productCategoryList?.find((c) => c.id === product.categoryId)?.name }}
-      </el-descriptions-item>
+      <el-descriptions-item label="产品类别">{{ product.categoryName }}</el-descriptions-item>
       <el-descriptions-item label="产品单位">
         <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="product.unit" />
       </el-descriptions-item>
-      <el-descriptions-item label="产品价格">{{ fenToYuan(product.price) }}元</el-descriptions-item>
+      <el-descriptions-item label="产品价格">
+        {{ erpPriceInputFormatter(product.price) }} 元
+      </el-descriptions-item>
       <el-descriptions-item label="产品编码">{{ product.no }}</el-descriptions-item>
     </el-descriptions>
   </ContentWrap>
@@ -34,9 +34,8 @@
 <script setup lang="ts">
 import ProductForm from '@/views/crm/product/ProductForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
-import { fenToYuan } from '@/utils'
+import { erpPriceInputFormatter } from '@/utils'
 import * as ProductApi from '@/api/crm/product'
-import * as ProductCategoryApi from '@/api/crm/product/productCategory'
 
 // 操作修改
 const formRef = ref()
@@ -44,12 +43,4 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 const { product } = defineProps<{ product: ProductApi.ProductVO }>()
-const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调
-
-/** 初始化 */
-const productCategoryList = ref([]) // 产品分类树
-
-onMounted(async () => {
-  productCategoryList.value = await ProductCategoryApi.getProductCategoryList({})
-})
 </script>

+ 5 - 12
src/views/crm/product/detail/ProductDetailsInfo.vue

@@ -8,11 +8,11 @@
         <el-descriptions :column="4">
           <el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
           <el-descriptions-item label="产品编码">{{ product.no }}</el-descriptions-item>
-          <el-descriptions-item label="价格">{{ fenToYuan(product.price) }}元</el-descriptions-item>
-          <el-descriptions-item label="产品描述">{{ product.description }}</el-descriptions-item>
-          <el-descriptions-item label="产品类型">
-            {{ productCategoryList?.find((c) => c.id === product.categoryId)?.name }}
+          <el-descriptions-item label="价格">
+            {{ erpPriceInputFormatter(product.price) }} 元
           </el-descriptions-item>
+          <el-descriptions-item label="产品描述">{{ product.description }}</el-descriptions-item>
+          <el-descriptions-item label="产品类型">{{ product.categoryName }}</el-descriptions-item>
           <el-descriptions-item label="是否上下架">
             <dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="product.status" />
           </el-descriptions-item>
@@ -27,8 +27,7 @@
 <script setup lang="ts">
 import { DICT_TYPE } from '@/utils/dict'
 import * as ProductApi from '@/api/crm/product'
-import { fenToYuan } from '@/utils'
-import * as ProductCategoryApi from '@/api/crm/product/productCategory'
+import { erpPriceInputFormatter } from '@/utils'
 
 const { product } = defineProps<{
   product: ProductApi.ProductVO
@@ -36,10 +35,4 @@ const { product } = defineProps<{
 
 // 展示的折叠面板
 const activeNames = ref(['basicInfo'])
-
-/** 初始化 */
-const productCategoryList = ref([]) // 产品分类树
-onMounted(async () => {
-  productCategoryList.value = await ProductCategoryApi.getProductCategoryList({})
-})
 </script>

+ 2 - 2
src/views/crm/product/index.vue

@@ -68,7 +68,7 @@
         label="价格(元)"
         align="center"
         prop="price"
-        :formatter="fenToYuanFormat"
+        :formatter="erpPriceTableColumnFormatter"
         width="100"
       />
       <el-table-column label="产品描述" align="center" prop="description" width="150" />
@@ -133,7 +133,7 @@ import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as ProductApi from '@/api/crm/product'
 import ProductForm from './ProductForm.vue'
-import { fenToYuanFormat } from '@/utils/formatter'
+import { erpPriceTableColumnFormatter } from '@/utils'
 
 defineOptions({ name: 'CrmProduct' })
 

+ 216 - 123
src/views/crm/receivable/ReceivableForm.vue

@@ -1,132 +1,182 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
       label-width="100px"
-      v-loading="formLoading"
     >
-      <el-form-item label="回款编号" prop="no">
-        <el-input v-model="formData.no" placeholder="请输入回款编号" />
-      </el-form-item>
-      <el-form-item label="回款计划" prop="planId">
-        <el-input v-model="formData.planId" placeholder="请输入回款计划" />
-      </el-form-item>
-      <el-form-item label="客户名称" prop="customerId">
-        <el-input v-model="formData.customerId" placeholder="请输入客户名称" />
-      </el-form-item>
-      <el-form-item label="合同名称" prop="contractId">
-        <el-input v-model="formData.contractId" placeholder="请输入合同名称" />
-      </el-form-item>
-      <!--<el-form-item label="审批状态" prop="checkStatus">
-        <el-select v-model="formData.checkStatus" placeholder="请选择审批状态">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.CRM_RECEIVABLE_CHECK_STATUS)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>-->
-      <!--<el-form-item label="工作流编号" prop="processInstanceId">
-        <el-input v-model="formData.processInstanceId" placeholder="请输入工作流编号" />
-      </el-form-item>-->
-      <el-form-item label="回款日期" prop="returnTime">
-        <el-date-picker
-          v-model="formData.returnTime"
-          type="date"
-          value-format="x"
-          placeholder="选择回款日期"
-        />
-      </el-form-item>
-      <el-form-item label="回款方式" prop="returnType">
-        <el-select v-model="formData.returnType" placeholder="请选择回款方式">
-          <el-option
-            v-for="dict in getStrDictOptions(DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="回款金额" prop="price">
-        <el-input-number v-model="formData.price" placeholder="请输入回款金额" />
-      </el-form-item>
-      <el-form-item label="负责人" prop="ownerUserId">
-        <el-select v-model="formData.ownerUserId" clearable placeholder="请输入负责人">
-          <el-option
-            v-for="item in userList"
-            :key="item.id"
-            :label="item.nickname"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="批次" prop="batchId">
-        <el-input-number v-model="formData.batchId" placeholder="请输入批次" />
-      </el-form-item>
-      <el-form-item label="显示排序" prop="sort">
-        <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
-      </el-form-item>
-      <!--<el-form-item label="状态" prop="status">
-        <el-select v-model="formData.status" placeholder="请选择状态">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>-->
-      <el-form-item label="备注" prop="remark">
-        <el-input type="textarea" :rows="3" v-model="formData.remark" placeholder="请输入备注" />
-      </el-form-item>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="回款编号" prop="no">
+            <el-input disabled v-model="formData.no" placeholder="保存时自动生成" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="负责人" prop="ownerUserId">
+            <el-select
+              v-model="formData.ownerUserId"
+              :disabled="formType !== 'create'"
+              class="w-1/1"
+            >
+              <el-option
+                v-for="item in userOptions"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="客户名称" prop="customerId">
+            <el-select
+              v-model="formData.customerId"
+              :disabled="formType !== 'create'"
+              class="w-1/1"
+              filterable
+              @change="handleCustomerChange"
+              placeholder="请选择客户"
+            >
+              <el-option
+                v-for="item in customerList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="合同名称" prop="contractId">
+            <el-select
+              v-model="formData.contractId"
+              :disabled="formType !== 'create' || !formData.customerId"
+              class="w-1/1"
+              filterable
+              @change="handleContractChange"
+              placeholder="请选择合同"
+            >
+              <el-option
+                v-for="data in contractList"
+                :key="data.id"
+                :label="data.name"
+                :value="data.id!"
+                :disabled="data.auditStatus !== 20"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="回款期数" prop="planId">
+            <el-select
+              v-model="formData.planId"
+              :disabled="formType !== 'create' || !formData.contractId"
+              class="!w-1/1"
+              @change="handleReceivablePlanChange"
+              placeholder="请选择回款期数"
+            >
+              <el-option
+                v-for="data in receivablePlanList"
+                :key="data.id"
+                :label="'第 ' + data.period + ' 期'"
+                :value="data.id!"
+                :disabled="data.receivableId"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="回款方式" prop="returnType">
+            <el-select v-model="formData.returnType" class="w-1/1" placeholder="请选择回款方式">
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="回款金额" prop="price">
+            <el-input-number
+              v-model="formData.price"
+              class="!w-100%"
+              controls-position="right"
+              placeholder="请输入回款金额"
+              :min="0.01"
+              :precision="2"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="回款日期" prop="returnTime">
+            <el-date-picker
+              v-model="formData.returnTime"
+              placeholder="选择回款日期"
+              type="date"
+              value-format="x"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remark">
+            <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
+          </el-form-item>
+        </el-col>
+      </el-row>
     </el-form>
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
 </template>
-<script setup lang="ts">
-import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+<script lang="ts" setup>
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
 import * as ReceivableApi from '@/api/crm/receivable'
 import * as UserApi from '@/api/system/user'
+import * as CustomerApi from '@/api/crm/customer'
+import * as ContractApi from '@/api/crm/contract'
+import { useUserStore } from '@/store/modules/user'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import form from '@/components/Form/src/Form.vue'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-const userList = ref<UserApi.UserVO[]>([]) // 用户列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  no: undefined,
-  planId: undefined,
-  customerId: undefined,
-  contractId: undefined,
-  checkStatus: undefined,
-  processInstanceId: undefined,
-  returnTime: undefined,
-  returnType: undefined,
-  price: undefined,
-  ownerUserId: undefined,
-  batchId: undefined,
-  sort: undefined,
-  dataScope: undefined,
-  dataScopeDeptIds: undefined,
-  status: undefined,
-  remark: undefined
+const formData = ref<ReceivableApi.ReceivableVO>({} as ReceivableApi.ReceivableVO)
+const formRules = reactive({
+  customerId: [{ required: true, message: '客户不能为空', trigger: 'blur' }],
+  contractId: [{ required: true, message: '合同不能为空', trigger: 'blur' }],
+  returnTime: [{ required: true, message: '回款日期不能为空', trigger: 'blur' }],
+  price: [{ required: true, message: '回款金额不能为空', trigger: 'blur' }]
 })
-// const formRules = reactive({
-//   status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
-// })
 const formRef = ref() // 表单 Ref
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
+const contractList = ref<ContractApi.ContractVO[]>([]) // 合同列表
+const receivablePlanList = ref<ReceivablePlanApi.ReceivablePlanVO[]>([]) // 回款计划列表
 
 /** 打开弹窗 */
-const open = async (type: string, id?: number) => {
+const open = async (
+  type: string,
+  id?: number,
+  receivablePlan?: ReceivablePlanApi.ReceivablePlanVO
+) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
@@ -141,7 +191,25 @@ const open = async (type: string, id?: number) => {
     }
   }
   // 获得用户列表
-  userList.value = await UserApi.getSimpleUserList()
+  userOptions.value = await UserApi.getSimpleUserList()
+  // 获得客户列表
+  customerList.value = await CustomerApi.getCustomerSimpleList()
+  // 默认新建时选中自己
+  if (formType.value === 'create') {
+    formData.value.ownerUserId = useUserStore().getUser.id
+  }
+  // 从回款计划创建回款
+  if (receivablePlan) {
+    formData.value.customerId = receivablePlan.customerId
+    await handleCustomerChange(receivablePlan.customerId)
+    formData.value.contractId = receivablePlan.contractId
+    await handleContractChange(receivablePlan.contractId)
+    if (receivablePlan.id) {
+      formData.value.planId = receivablePlan.id
+      formData.value.price = receivablePlan.price
+      formData.value.returnType = receivablePlan.returnType
+    }
+  }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -173,25 +241,50 @@ const submitForm = async () => {
 
 /** 重置表单 */
 const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    no: undefined,
-    planId: undefined,
-    customerId: undefined,
-    contractId: undefined,
-    checkStatus: undefined,
-    processInstanceId: undefined,
-    returnTime: undefined,
-    returnType: undefined,
-    price: undefined,
-    ownerUserId: undefined,
-    batchId: undefined,
-    sort: undefined,
-    dataScope: undefined,
-    dataScopeDeptIds: undefined,
-    status: undefined,
-    remark: undefined
-  }
+  formData.value = {} as ReceivableApi.ReceivableVO
   formRef.value?.resetFields()
 }
+
+/** 处理切换客户 */
+const handleCustomerChange = async (customerId: number) => {
+  // 重置合同编号
+  formData.value.contractId = undefined
+  // 获得合同列表
+  if (customerId) {
+    contractList.value = []
+    contractList.value = await ContractApi.getContractSimpleList(customerId)
+  }
+}
+
+/** 处理切换合同 */
+const handleContractChange = async (contractId: number) => {
+  // 重置回款计划编号
+  formData.value.planId = undefined
+  if (contractId) {
+    // 获得回款计划列表
+    receivablePlanList.value = []
+    receivablePlanList.value = await ReceivablePlanApi.getReceivablePlanSimpleList(
+      formData.value.customerId,
+      contractId
+    )
+    // 设置金额
+    const contract = contractList.value.find((item) => item.id === contractId)
+    if (contract) {
+      formData.value.price = contract.totalPrice - contract.totalReceivablePrice
+    }
+  }
+}
+
+/** 处理切换回款计划 */
+const handleReceivablePlanChange = (planId: number) => {
+  if (!planId) {
+    return
+  }
+  const receivablePlan = receivablePlanList.value.find((item) => item.id === planId)
+  if (!receivablePlan) {
+    return
+  }
+  formData.value.price = receivablePlan.price
+  formData.value.returnType = receivablePlan.returnType
+}
 </script>

+ 88 - 49
src/views/crm/receivable/components/ReceivableList.vue

@@ -1,7 +1,7 @@
 <template>
   <!-- 操作栏 -->
   <el-row justify="end">
-    <el-button @click="openForm">
+    <el-button @click="openForm('create')">
       <Icon class="mr-5px" icon="icon-park:income-one" />
       创建回款
     </el-button>
@@ -9,40 +9,56 @@
 
   <!-- 列表 -->
   <ContentWrap class="mt-10px">
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="回款编号" fixed="left" align="center" prop="no">
+    <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+      <el-table-column align="center" label="回款编号" prop="no" />
+      <el-table-column align="center" label="客户" prop="customerName" />
+      <el-table-column align="center" label="合同" prop="contract.no" />
+      <el-table-column
+        :formatter="dateFormatter2"
+        align="center"
+        label="回款日期"
+        prop="returnTime"
+        width="150px"
+      />
+      <el-table-column align="center" label="回款方式" prop="returnType" width="130px">
         <template #default="scope">
-          <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
-            {{ scope.row.no }}
-          </el-link>
+          <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="scope.row.returnType" />
         </template>
       </el-table-column>
-      <el-table-column label="合同编号" align="center" prop="contractNo" />
       <el-table-column
-        label="回款金额(元)"
         align="center"
+        label="回款金额(元)"
         prop="price"
-        :formatter="fenToYuanFormat"
+        :formatter="erpPriceTableColumnFormatter"
       />
-      <el-table-column label="负责人" align="center" prop="ownerUserName" />
-      <el-table-column align="center" label="状态" prop="auditStatus">
+      <el-table-column align="center" label="负责人" prop="ownerUserName" />
+      <el-table-column align="center" label="备注" prop="remark" />
+      <el-table-column align="center" fixed="right" label="操作" width="130px">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+          <el-button
+            v-hasPermi="['crm:receivable:update']"
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+          >
+            编辑
+          </el-button>
+          <el-button
+            v-hasPermi="['crm:receivable:delete']"
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+          >
+            删除
+          </el-button>
         </template>
       </el-table-column>
-      <el-table-column
-        label="回款日期"
-        align="center"
-        prop="returnTime"
-        :formatter="dateFormatter"
-        width="180px"
-      />
     </el-table>
     <!-- 分页 -->
     <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
       @pagination="getList"
     />
   </ContentWrap>
@@ -50,45 +66,44 @@
   <!-- 表单弹窗:添加 -->
   <ReceivableForm ref="formRef" @success="getList" />
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
 import * as ReceivableApi from '@/api/crm/receivable'
 import ReceivableForm from './../ReceivableForm.vue'
-import { BizTypeEnum } from '@/api/crm/permission'
-import { dateFormatter } from '@/utils/formatTime'
-import { fenToYuanFormat } from '@/utils/formatter'
+import { dateFormatter2 } from '@/utils/formatTime'
 import { DICT_TYPE } from '@/utils/dict'
+import { erpPriceTableColumnFormatter } from '@/utils'
 
 defineOptions({ name: 'CrmReceivableList' })
 const props = defineProps<{
-  bizType: number // 业务类型
-  bizId: number // 业务编号
+  customerId?: number // 客户编号
+  contractId?: number // 合同编号
 }>()
 
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  customerId: undefined as unknown // 允许 undefined + number
+  customerId: undefined as unknown, // 允许 undefined + number
+  contractId: undefined as unknown // 允许 undefined + number
 })
 
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    // 置空参数
-    queryParams.customerId = undefined
-    // 执行查询
-    let data = { list: [], total: 0 }
-    switch (props.bizType) {
-      case BizTypeEnum.CRM_CUSTOMER:
-        queryParams.customerId = props.bizId
-        data = await ReceivableApi.getReceivablePageByCustomer(queryParams)
-        break
-      default:
-        return
+    if (props.customerId && !props.contractId) {
+      queryParams.customerId = props.customerId
+    } else if (props.customerId && props.contractId) {
+      // 如果是合同的话客户编号也需要带上因为权限基于客户
+      queryParams.customerId = props.customerId
+      queryParams.contractId = props.contractId
     }
+    const data = await ReceivableApi.getReceivablePageByCustomer(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -99,25 +114,49 @@ const getList = async () => {
 /** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
+  // 置空参数
+  queryParams.customerId = undefined
+  queryParams.contractId = undefined
   getList()
 }
 
-/** 添加 */
+/** 添加/修改操作 */
 const formRef = ref()
-const openForm = () => {
-  formRef.value.open('create')
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id, {
+    customerId: props.customerId,
+    contractId: props.contractId
+  })
 }
 
-/** 打开合同详情 */
-const { push } = useRouter()
-const openDetail = (id: number) => {
-  push({ name: 'CrmReceivableDetail', params: { id } })
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ReceivableApi.deleteReceivable(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 从回款计划创建回款 */
+const createReceivable = (planData: any) => {
+  const data = planData as unknown as ReceivablePlanApi.ReceivablePlanVO
+  formRef.value.open('create', undefined, data)
 }
+defineExpose({ createReceivable })
 
-/** 监听打开的 bizId + bizType,从而加载最新的列表 */
+/** 监听打开的 customerId + contractId,从而加载最新的列表 */
 watch(
-  () => [props.bizId, props.bizType],
-  () => {
+  () => [props.customerId, props.contractId],
+  (newVal) => {
+    // 保证至少客户编号有值
+    if (!newVal[0]) {
+      return
+    }
     handleQuery()
   },
   { immediate: true, deep: true }

+ 43 - 0
src/views/crm/receivable/detail/ReceivableDetailsHeader.vue

@@ -0,0 +1,43 @@
+<template>
+  <div>
+    <div class="flex items-start justify-between">
+      <div>
+        <el-col>
+          <el-row>
+            <span class="text-xl font-bold">{{ receivable.no }}</span>
+          </el-row>
+        </el-col>
+      </div>
+      <div>
+        <!-- 右上:按钮 -->
+        <slot></slot>
+      </div>
+    </div>
+  </div>
+  <ContentWrap class="mt-10px">
+    <el-descriptions :column="5" direction="vertical">
+      <el-descriptions-item label="客户名称">
+        {{ receivable.customerName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="合同金额">
+        {{ erpPriceInputFormatter(receivable.contract?.totalPrice) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="回款日期">
+        {{ formatDate(receivable.returnTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="回款金额">
+        {{ erpPriceInputFormatter(receivable.price) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="负责人">
+        {{ receivable.ownerUserName }}
+      </el-descriptions-item>
+    </el-descriptions>
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ReceivableApi from '@/api/crm/receivable'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { receivable } = defineProps<{ receivable: ReceivableApi.ReceivableVO }>()
+</script>

+ 62 - 0
src/views/crm/receivable/detail/ReceivableDetailsInfo.vue

@@ -0,0 +1,62 @@
+<template>
+  <ContentWrap>
+    <el-collapse v-model="activeNames">
+      <el-collapse-item name="basicInfo">
+        <template #title>
+          <span class="text-base font-bold">基本信息</span>
+        </template>
+        <el-descriptions :column="4">
+          <el-descriptions-item label="回款编号">{{ receivable.no }}</el-descriptions-item>
+          <el-descriptions-item label="客户名称">
+            {{ receivable.customerName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="合同编号">
+            {{ receivable.contract?.no }}
+          </el-descriptions-item>
+          <el-descriptions-item label="回款日期">
+            {{ formatDate(receivable.returnTime, 'YYYY-MM-DD') }}
+          </el-descriptions-item>
+          <el-descriptions-item label="回款金额">
+            {{ erpPriceInputFormatter(receivable.price) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="回款方式">
+            <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="receivable.returnType" />
+          </el-descriptions-item>
+          <el-descriptions-item label="备注">{{ receivable.remark }}</el-descriptions-item>
+        </el-descriptions>
+      </el-collapse-item>
+      <el-collapse-item name="systemInfo">
+        <template #title>
+          <span class="text-base font-bold">系统信息</span>
+        </template>
+        <el-descriptions :column="4">
+          <el-descriptions-item label="负责人">
+            {{ receivable.ownerUserName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="创建人">
+            {{ receivable.creatorName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="创建时间">
+            {{ formatDate(receivable.createTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="更新时间">
+            {{ formatDate(receivable.updateTime) }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-collapse-item>
+    </el-collapse>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as ReceivableApi from '@/api/crm/receivable'
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { receivable } = defineProps<{
+  receivable: ReceivableApi.ReceivableVO
+}>()
+
+// 展示的折叠面板
+const activeNames = ref(['basicInfo', 'systemInfo'])
+</script>

+ 99 - 0
src/views/crm/receivable/detail/index.vue

@@ -0,0 +1,99 @@
+<template>
+  <ReceivableDetailsHeader v-loading="loading" :receivable="receivable">
+    <el-button v-if="permissionListRef?.validateWrite" @click="openForm('update', receivable.id)">
+      编辑
+    </el-button>
+  </ReceivableDetailsHeader>
+  <el-col>
+    <el-tabs>
+      <el-tab-pane label="详细资料">
+        <ReceivableDetailsInfo :receivable="receivable" />
+      </el-tab-pane>
+      <el-tab-pane label="操作日志">
+        <OperateLogV2 :log-list="logList" />
+      </el-tab-pane>
+      <el-tab-pane label="团队成员">
+        <PermissionList
+          ref="permissionListRef"
+          :biz-id="receivable.id!"
+          :biz-type="BizTypeEnum.CRM_RECEIVABLE"
+          :show-action="true"
+          @quit-team="close"
+        />
+      </el-tab-pane>
+    </el-tabs>
+  </el-col>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ReceivableForm ref="formRef" @success="getReceivable(receivable.id)" />
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ReceivableApi from '@/api/crm/receivable'
+import ReceivableDetailsHeader from './ReceivableDetailsHeader.vue'
+import ReceivableDetailsInfo from './ReceivableDetailsInfo.vue'
+import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
+import { BizTypeEnum } from '@/api/crm/permission'
+import { OperateLogV2VO } from '@/api/system/operatelog'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+import ReceivableForm from '@/views/crm/receivable/ReceivableForm.vue'
+
+defineOptions({ name: 'CrmReceivablePlanDetail' })
+
+const message = useMessage()
+
+const receivableId = ref(0) // 回款编号
+const loading = ref(true) // 加载中
+const receivable = ref<ReceivableApi.ReceivableVO>({} as ReceivableApi.ReceivableVO) // 回款详情
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
+
+/** 获取详情 */
+const getReceivable = async (id: number) => {
+  loading.value = true
+  try {
+    receivable.value = await ReceivableApi.getReceivable(id)
+    await getOperateLog(id)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 编辑 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 获取操作日志 */
+const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
+const getOperateLog = async (receivableId: number) => {
+  if (!receivableId) {
+    return
+  }
+  const data = await getOperateLogPage({
+    bizType: BizTypeEnum.CRM_RECEIVABLE,
+    bizId: receivableId
+  })
+  logList.value = data.list
+}
+
+/** 关闭窗口 */
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute } = useRouter() // 路由
+const close = () => {
+  delView(unref(currentRoute))
+}
+
+/** 初始化 */
+const { params } = useRoute()
+onMounted(async () => {
+  const id = props.id || route.params.id
+  if (!id) {
+    message.warning('参数错误,回款不能为空!')
+    close()
+    return
+  }
+  receivableId.value = id
+  await getReceivable(receivableId.value)
+})
+</script>

+ 170 - 58
src/views/crm/receivable/index.vue

@@ -2,49 +2,63 @@
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
-      class="-mb-15px"
-      :model="queryParams"
       ref="queryFormRef"
       :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
       label-width="68px"
     >
       <el-form-item label="回款编号" prop="no">
         <el-input
           v-model="queryParams.no"
-          placeholder="请输入回款编号"
+          class="!w-240px"
           clearable
+          placeholder="请输入回款编号"
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
       <el-form-item label="客户名称" prop="customerId">
-        <el-input
+        <el-select
           v-model="queryParams.customerId"
-          placeholder="请输入客户名称"
-          clearable
-          @keyup.enter="handleQuery"
           class="!w-240px"
-        />
+          placeholder="请选择客户"
+          @keyup.enter="handleQuery"
+        >
+          <el-option
+            v-for="item in customerList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
         <el-button
-          type="primary"
+          v-hasPermi="['crm:receivable:create']"
           plain
+          type="primary"
           @click="openForm('create')"
-          v-hasPermi="['crm:receivable:create']"
         >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
+          <Icon class="mr-5px" icon="ep:plus" />
+          新增
         </el-button>
         <el-button
-          type="success"
+          v-hasPermi="['crm:receivable:export']"
+          :loading="exportLoading"
           plain
+          type="success"
           @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['crm:receivable:export']"
         >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
+          <Icon class="mr-5px" icon="ep:download" />
+          导出
         </el-button>
       </el-form-item>
     </el-form>
@@ -52,66 +66,123 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="ID" align="center" prop="id" />
-      <el-table-column label="回款编号" align="center" prop="no" />
-      <!-- <el-table-column label="回款计划ID" align="center" prop="planId" />-->
-      <el-table-column label="客户" align="center" prop="customerId" />
-      <el-table-column label="合同" align="center" prop="contractId" />
-      <el-table-column label="审批状态" align="center" prop="checkStatus" width="130px">
+    <el-tabs v-model="activeName" @tab-click="handleTabClick">
+      <el-tab-pane label="我负责的" name="1" />
+      <el-tab-pane label="我参与的" name="2" />
+      <el-tab-pane label="下属负责的" name="3" />
+    </el-tabs>
+    <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+      <el-table-column align="center" fixed="left" label="回款编号" prop="no" width="180">
+        <template #default="scope">
+          <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+            {{ scope.row.no }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="客户名称" prop="customerName" width="120">
+        <template #default="scope">
+          <el-link
+            :underline="false"
+            type="primary"
+            @click="openCustomerDetail(scope.row.customerId)"
+          >
+            {{ scope.row.customerName }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="合同编号" prop="contractNo" width="180">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.checkStatus" />
+          <el-link
+            :underline="false"
+            type="primary"
+            @click="openContractDetail(scope.row.contractId)"
+          >
+            {{ scope.row.contract.no }}
+          </el-link>
         </template>
       </el-table-column>
-      <!-- <el-table-column label="工作流编号" align="center" prop="processInstanceId" />-->
       <el-table-column
-        label="回款日期"
+        :formatter="dateFormatter2"
         align="center"
+        label="回款日期"
         prop="returnTime"
-        :formatter="dateFormatter2"
         width="150px"
       />
-      <el-table-column label="回款方式" align="center" prop="returnType" width="130px">
+      <el-table-column
+        align="center"
+        label="回款金额(元)"
+        prop="price"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column align="center" label="回款方式" prop="returnType" width="130px">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="scope.row.returnType" />
         </template>
       </el-table-column>
-      <el-table-column label="回款金额(元)" align="center" prop="price" />
-      <el-table-column label="负责人" align="center" prop="ownerUserId" />
-      <el-table-column label="批次" align="center" prop="batchId" />
-      <!--<el-table-column label="显示顺序" align="center" prop="sort" />-->
-      <el-table-column label="状态" align="center" prop="status">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
-        </template>
-      </el-table-column>
-      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column align="center" label="备注" prop="remark" width="200" />
       <el-table-column
-        label="创建时间"
         align="center"
-        prop="createTime"
+        label="合同金额(元)"
+        prop="contract.totalPrice"
+        width="140"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column align="center" label="负责人" prop="ownerUserName" width="120" />
+      <el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="更新时间"
+        prop="updateTime"
+        width="180px"
+      />
+      <el-table-column
         :formatter="dateFormatter"
+        align="center"
+        label="创建时间"
+        prop="createTime"
         width="180px"
       />
-      <el-table-column label="操作" align="center" width="180px">
+      <el-table-column align="center" label="创建人" prop="creatorName" width="120" />
+      <el-table-column align="center" fixed="right" label="回款状态" prop="auditStatus" width="120">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="180px">
         <template #default="scope">
-          <!-- todo @liuhongfeng:用路径参数哈,receivableId -->
-          <!--<router-link :to="'/crm/receivable-plan?receivableId=' + scope.row.receivableId">
-            <el-button link type="primary">详情</el-button>
-          </router-link>-->
           <el-button
+            v-hasPermi="['crm:receivable:update']"
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['crm:receivable:update']"
           >
             编辑
           </el-button>
           <el-button
+            v-if="scope.row.auditStatus === 0"
+            v-hasPermi="['crm:receivable:update']"
+            link
+            type="primary"
+            @click="handleSubmit(scope.row)"
+          >
+            提交审核
+          </el-button>
+          <el-button
+            v-else
+            v-hasPermi="['crm:receivable:update']"
+            link
+            type="primary"
+            @click="handleProcessDetail(scope.row)"
+          >
+            查看审批
+          </el-button>
+          <el-button
+            v-hasPermi="['crm:receivable:delete']"
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['crm:receivable:delete']"
           >
             删除
           </el-button>
@@ -120,9 +191,9 @@
     </el-table>
     <!-- 分页 -->
     <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
       @pagination="getList"
     />
   </ContentWrap>
@@ -130,30 +201,40 @@
   <!-- 表单弹窗:添加/修改 -->
   <ReceivableForm ref="formRef" @success="getList" />
 </template>
-
-<script setup lang="ts">
+<script lang="ts" setup>
 import { DICT_TYPE } from '@/utils/dict'
 import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as ReceivableApi from '@/api/crm/receivable'
 import ReceivableForm from './ReceivableForm.vue'
+import * as CustomerApi from '@/api/crm/customer'
+import { TabsPaneContext } from 'element-plus'
+import { erpPriceTableColumnFormatter } from '@/utils'
 
 defineOptions({ name: 'Receivable' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
-
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  no: null,
-  customerId: null
+  sceneType: '1', // 默认和 activeName 相等
+  no: undefined,
+  customerId: undefined
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
+const activeName = ref('1') // 列表 tab
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
+
+/** tab 切换 */
+const handleTabClick = (tab: TabsPaneContext) => {
+  queryParams.sceneType = tab.paneName
+  handleQuery()
+}
 
 /** 查询列表 */
 const getList = async () => {
@@ -198,6 +279,35 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+/** 提交审核 **/
+const handleSubmit = async (row: ReceivableApi.ReceivableVO) => {
+  await message.confirm(`您确定提交编号为【${row.no}】的回款审核吗?`)
+  await ReceivableApi.submitReceivable(row.id)
+  message.success('提交审核成功!')
+  await getList()
+}
+
+/** 打开回款详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'CrmReceivableDetail', params: { id } })
+}
+
+/** 打开客户详情 */
+const openCustomerDetail = (id: number) => {
+  push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 打开合同详情 */
+const openContractDetail = (id: number) => {
+  push({ name: 'CrmContractDetail', params: { id } })
+}
+
+/** 查看审批 */
+const handleProcessDetail = (row: ReceivableApi.ReceivableVO) => {
+  push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } })
+}
+
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {
@@ -214,7 +324,9 @@ const handleExport = async () => {
 }
 
 /** 初始化 **/
-onMounted(() => {
-  getList()
+onMounted(async () => {
+  await getList()
+  // 获得客户列表
+  customerList.value = await CustomerApi.getCustomerSimpleList()
 })
 </script>

+ 163 - 117
src/views/crm/receivable/plan/ReceivablePlanForm.vue

@@ -1,126 +1,164 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
+      label-width="110px"
     >
-      <el-form-item label="客户名称" prop="customerId">
-        <el-input v-model="formData.customerId" placeholder="请输入客户名称" />
-      </el-form-item>
-      <el-form-item label="合同名称" prop="contractId">
-        <el-input v-model="formData.contractId" placeholder="请输入合同名称" />
-      </el-form-item>
-      <el-form-item label="负责人" prop="ownerUserId">
-        <el-select v-model="formData.ownerUserId" clearable placeholder="请输入负责人">
-          <el-option
-            v-for="item in userList"
-            :key="item.id"
-            :label="item.nickname"
-            :value="item.id"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="期数" prop="period">
-        <el-input-number v-model="formData.period" placeholder="请输入期数" />
-      </el-form-item>
-      <!--<el-form-item label="回款ID" prop="receivableId">
-        <el-input v-model="formData.receivableId" placeholder="请输入回款ID" />
-      </el-form-item>
-      <el-form-item label="完成状态" prop="status">
-        <el-select v-model="formData.status" placeholder="请选择完成状态">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="审批状态" prop="checkStatus">
-        <el-select v-model="formData.checkStatus" placeholder="请选择审批状态">
-          <el-option
-            v-for="dict in getStrDictOptions(DICT_TYPE.CRM_RECEIVABLE_CHECK_STATUS)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="工作流编号" prop="processInstanceId">
-        <el-input v-model="formData.processInstanceId" placeholder="请输入工作流编号" />
-      </el-form-item>-->
-      <el-form-item label="计划回款金额" prop="price">
-        <el-input-number v-model="formData.price" placeholder="请输入计划回款金额" />
-      </el-form-item>
-      <el-form-item label="计划回款日期" prop="returnTime">
-        <el-date-picker
-          v-model="formData.returnTime"
-          type="date"
-          value-format="x"
-          placeholder="选择计划回款日期"
-        />
-      </el-form-item>
-      <el-form-item label="提前几天提醒" prop="remindDays">
-        <el-input-number v-model="formData.remindDays" placeholder="请输入提前几天提醒" />
-      </el-form-item>
-      <el-form-item label="提醒日期" prop="remindTime">
-        <el-date-picker
-          v-model="formData.remindTime"
-          type="date"
-          value-format="x"
-          placeholder="选择提醒日期"
-        />
-      </el-form-item>
-      <el-form-item label="显示排序" prop="sort">
-        <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input type="textarea" :rows="3" v-model="formData.remark" placeholder="请输入备注" />
-      </el-form-item>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="还款期数" prop="period">
+            <el-input disabled v-model="formData.period" placeholder="保存时自动生成" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="负责人" prop="ownerUserId">
+            <el-select
+              v-model="formData.ownerUserId"
+              :disabled="formType !== 'create'"
+              class="w-1/1"
+            >
+              <el-option
+                v-for="item in userOptions"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="客户名称" prop="customerId">
+            <el-select
+              v-model="formData.customerId"
+              :disabled="formType !== 'create'"
+              class="w-1/1"
+              filterable
+              @change="handleCustomerChange"
+              placeholder="请选择客户"
+            >
+              <el-option
+                v-for="item in customerList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="合同名称" prop="contractId">
+            <el-select
+              v-model="formData.contractId"
+              :disabled="formType !== 'create' || !formData.customerId"
+              class="w-1/1"
+              filterable
+              placeholder="请选择合同"
+            >
+              <el-option
+                v-for="data in contractList"
+                :key="data.id"
+                :label="data.name"
+                :value="data.id!"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="计划回款金额" prop="price">
+            <el-input-number
+              v-model="formData.price"
+              class="!w-100%"
+              controls-position="right"
+              placeholder="请输入计划回款金额"
+              :min="0.01"
+              :precision="2"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="计划回款日期" prop="returnTime">
+            <el-date-picker
+              v-model="formData.returnTime"
+              placeholder="选择计划回款日期"
+              type="date"
+              value-format="x"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="提前几天提醒" prop="remindDays">
+            <el-input-number
+              v-model="formData.remindDays"
+              class="!w-100%"
+              controls-position="right"
+              placeholder="请输入提前几天提醒"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="回款方式" prop="returnType">
+            <el-select v-model="formData.returnType" class="w-1/1" placeholder="请选择回款方式">
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remark">
+            <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
+          </el-form-item>
+        </el-col>
+      </el-row>
     </el-form>
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
 import * as UserApi from '@/api/system/user'
+import * as CustomerApi from '@/api/crm/customer'
+import * as ContractApi from '@/api/crm/contract'
+import { useUserStore } from '@/store/modules/user'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { aw } from '../../../../../dist-prod/assets/index-9eac537b'
+
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-const userList = ref<UserApi.UserVO[]>([]) // 用户列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  period: undefined,
-  receivableId: undefined,
-  status: undefined,
-  checkStatus: undefined,
-  processInstanceId: undefined,
-  price: undefined,
-  returnTime: undefined,
-  remindDays: undefined,
-  remindTime: undefined,
-  customerId: undefined,
-  contractId: undefined,
-  ownerUserId: undefined,
-  sort: undefined,
-  remark: undefined
-})
+const formData = ref<ReceivablePlanApi.ReceivablePlanVO>({} as ReceivablePlanApi.ReceivablePlanVO)
 const formRules = reactive({
-  status: [{ required: true, message: '完成状态不能为空', trigger: 'change' }]
+  price: [{ required: true, message: '计划回款金额不能为空', trigger: 'blur' }],
+  returnTime: [{ required: true, message: '计划回款日期不能为空', trigger: 'blur' }],
+  customerId: [{ required: true, message: '客户编号不能为空', trigger: 'blur' }],
+  contractId: [{ required: true, message: '合同编号不能为空', trigger: 'blur' }],
+  ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
+const contractList = ref<ContractApi.ContractVO[]>([]) // 合同列表
 
 /** 打开弹窗 */
-const open = async (type: string, id?: number) => {
+const open = async (type: string, id?: number, customerId?: number, contractId?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
@@ -134,9 +172,22 @@ const open = async (type: string, id?: number) => {
       formLoading.value = false
     }
   }
-
   // 获得用户列表
-  userList.value = await UserApi.getSimpleUserList()
+  userOptions.value = await UserApi.getSimpleUserList()
+  // 获得客户列表
+  customerList.value = await CustomerApi.getCustomerSimpleList()
+  // 默认新建时选中自己
+  if (formType.value === 'create') {
+    formData.value.ownerUserId = useUserStore().getUser.id
+  }
+  // 设置 customerId 和 contractId 默认值
+  if (customerId) {
+    formData.value.customerId = customerId
+    await handleCustomerChange(customerId)
+  }
+  if (contractId) {
+    formData.value.contractId = contractId
+  }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -168,23 +219,18 @@ const submitForm = async () => {
 
 /** 重置表单 */
 const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    period: undefined,
-    receivableId: undefined,
-    status: undefined,
-    checkStatus: undefined,
-    processInstanceId: undefined,
-    price: undefined,
-    returnTime: undefined,
-    remindDays: undefined,
-    remindTime: undefined,
-    customerId: undefined,
-    contractId: undefined,
-    ownerUserId: undefined,
-    sort: undefined,
-    remark: undefined
-  }
+  formData.value = {} as ReceivablePlanApi.ReceivablePlanVO
   formRef.value?.resetFields()
 }
+
+/** 处理切换客户 */
+const handleCustomerChange = async (customerId: number) => {
+  // 重置合同编号
+  formData.value.contractId = undefined
+  // 获得合同列表
+  if (customerId) {
+    contractList.value = []
+    contractList.value = await ContractApi.getContractSimpleList(customerId)
+  }
+}
 </script>

+ 96 - 51
src/views/crm/receivable/plan/components/ReceivablePlanList.vue

@@ -1,7 +1,7 @@
 <template>
   <!-- 操作栏 -->
   <el-row justify="end">
-    <el-button @click="openForm">
+    <el-button @click="openForm('create', undefined)">
       <Icon class="mr-5px" icon="icon-park:income" />
       创建回款计划
     </el-button>
@@ -9,43 +9,69 @@
 
   <!-- 列表 -->
   <ContentWrap class="mt-10px">
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="期数" fixed="left" align="center" prop="no">
-        <template #default="scope">
-          <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
-            {{ scope.row.period }}
-          </el-link>
-        </template>
-      </el-table-column>
-      <el-table-column label="客户名称" align="center" prop="customerName" />
-      <el-table-column label="合同编号" align="center" prop="contractNo" />
+    <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+      <el-table-column align="center" label="客户名称" prop="customerName" width="150px" />
+      <el-table-column align="center" label="合同编号" prop="contractNo" width="200px" />
+      <el-table-column align="center" label="期数" prop="period" />
       <el-table-column
-        label="计划还款金额(元)"
         align="center"
+        label="计划回款(元)"
         prop="price"
-        :formatter="fenToYuanFormat"
+        width="120"
+        :formatter="erpPriceTableColumnFormatter"
       />
       <el-table-column
-        label="计划还款日期"
+        :formatter="dateFormatter2"
         align="center"
+        label="计划回款日期"
         prop="returnTime"
-        :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column align="center" label="计划还款方式" prop="auditStatus">
+      <el-table-column align="center" label="提前几天提醒" prop="remindDays" width="150" />
+      <el-table-column
+        :formatter="dateFormatter2"
+        align="center"
+        label="提醒日期"
+        prop="remindTime"
+        width="180px"
+      />
+      <el-table-column label="负责人" prop="ownerUserName" width="120" />
+      <el-table-column align="center" label="备注" prop="remark" />
+      <el-table-column align="center" fixed="right" label="操作" width="200px">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="scope.row.returnType" />
+          <el-button
+            v-hasPermi="['crm:receivable:create']"
+            link
+            type="primary"
+            @click="createReceivable(scope.row)"
+            :disabled="scope.row.receivableId"
+          >
+            创建回款
+          </el-button>
+          <el-button
+            v-hasPermi="['crm:receivable-plan:update']"
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+          >
+            编辑
+          </el-button>
+          <el-button
+            v-hasPermi="['crm:receivable-plan:delete']"
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+          >
+            删除
+          </el-button>
         </template>
       </el-table-column>
-      <el-table-column label="提前几日提醒" align="center" prop="remindDays" />
-      <el-table-column label="备注" align="center" prop="remark" />
-      <!-- TODO 芋艿:新建回款、编辑、删除 -->
     </el-table>
     <!-- 分页 -->
     <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
       @pagination="getList"
     />
   </ContentWrap>
@@ -53,45 +79,42 @@
   <!-- 表单弹窗:添加 -->
   <ReceivableForm ref="formRef" @success="getList" />
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
 import ReceivableForm from './../ReceivablePlanForm.vue'
-import { BizTypeEnum } from '@/api/crm/permission'
-import { dateFormatter } from '@/utils/formatTime'
-import { fenToYuanFormat } from '@/utils/formatter'
-import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { erpPriceTableColumnFormatter } from '@/utils'
 
 defineOptions({ name: 'CrmReceivablePlanList' })
 const props = defineProps<{
-  bizType: number // 业务类型
-  bizId: number // 业务编号
+  customerId?: number // 客户编号
+  contractId?: number // 合同编号
 }>()
 
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  customerId: undefined as unknown // 允许 undefined + number
+  customerId: undefined as unknown, // 允许 undefined + number
+  contractId: undefined as unknown // 允许 undefined + number
 })
 
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    // 置空参数
-    queryParams.customerId = undefined
-    // 执行查询
-    let data = { list: [], total: 0 }
-    switch (props.bizType) {
-      case BizTypeEnum.CRM_CUSTOMER:
-        queryParams.customerId = props.bizId
-        data = await ReceivablePlanApi.getReceivablePlanPageByCustomer(queryParams)
-        break
-      default:
-        return
+    if (props.customerId && !props.contractId) {
+      queryParams.customerId = props.customerId
+    } else if (props.customerId && props.contractId) {
+      // 如果是合同的话客户编号也需要带上因为权限基于客户
+      queryParams.customerId = props.customerId
+      queryParams.contractId = props.contractId
     }
+    const data = await ReceivablePlanApi.getReceivablePlanPageByCustomer(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -102,25 +125,47 @@ const getList = async () => {
 /** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
+  // 置空参数
+  queryParams.customerId = undefined
+  queryParams.contractId = undefined
   getList()
 }
 
-/** 添加 */
+/** 添加/修改操作 */
 const formRef = ref()
-const openForm = () => {
-  formRef.value.open('create')
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id, props.customerId, props.contractId)
+}
+
+/** 创建回款 */
+const emits = defineEmits<{
+  (e: 'createReceivable', v: ReceivablePlanApi.ReceivablePlanVO)
+}>()
+const createReceivable = (row: ReceivablePlanApi.ReceivablePlanVO) => {
+  emits('createReceivable', row)
 }
 
-/** 打开合同详情 */
-const { push } = useRouter()
-const openDetail = (id: number) => {
-  push({ name: 'CrmReceivablePlanDetail', params: { id } })
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ReceivablePlanApi.deleteReceivablePlan(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
 }
 
-/** 监听打开的 bizId + bizType,从而加载最新的列表 */
+/** 监听打开的 customerId + contractId,从而加载最新的列表 */
 watch(
-  () => [props.bizId, props.bizType],
-  () => {
+  () => [props.customerId, props.contractId],
+  (newVal) => {
+    // 保证至少客户编号有值
+    if (!newVal[0]) {
+      return
+    }
     handleQuery()
   },
   { immediate: true, deep: true }

+ 44 - 0
src/views/crm/receivable/plan/detail/ReceivablePlanDetailsHeader.vue

@@ -0,0 +1,44 @@
+<template>
+  <div>
+    <div class="flex items-start justify-between">
+      <div>
+        <el-col>
+          <el-row>
+            <span class="text-xl font-bold">第 {{ receivablePlan.period }} 期</span>
+          </el-row>
+        </el-col>
+      </div>
+      <div>
+        <!-- 右上:按钮 -->
+        <slot></slot>
+      </div>
+    </div>
+  </div>
+  <ContentWrap class="mt-10px">
+    <el-descriptions :column="5" direction="vertical">
+      <el-descriptions-item label="客户名称">
+        {{ receivablePlan.customerName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="合同编号">{{ receivablePlan.contractNo }}</el-descriptions-item>
+      <el-descriptions-item label="计划回款金额">
+        {{ erpPriceInputFormatter(receivablePlan.price) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="计划回款日期">
+        {{ formatDate(receivablePlan.returnTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="实际回款金额">
+        <el-text v-if="receivablePlan.receivable">
+          {{ erpPriceInputFormatter(receivablePlan.receivable.price) }}
+        </el-text>
+        <el-text v-else>{{ erpPriceInputFormatter(0) }}</el-text>
+      </el-descriptions-item>
+    </el-descriptions>
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { receivablePlan } = defineProps<{ receivablePlan: ReceivablePlanApi.ReceivablePlanVO }>()
+</script>

+ 83 - 0
src/views/crm/receivable/plan/detail/ReceivablePlanDetailsInfo.vue

@@ -0,0 +1,83 @@
+<template>
+  <ContentWrap>
+    <el-collapse v-model="activeNames">
+      <el-collapse-item name="basicInfo">
+        <template #title>
+          <span class="text-base font-bold">基本信息</span>
+        </template>
+        <el-descriptions :column="4">
+          <el-descriptions-item label="期数">{{ receivablePlan.period }}</el-descriptions-item>
+          <el-descriptions-item label="客户名称">
+            {{ receivablePlan.customerName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="合同编号">
+            {{ receivablePlan.contractNo }}
+          </el-descriptions-item>
+          <el-descriptions-item label="计划回款金额">
+            {{ erpPriceInputFormatter(receivablePlan.price) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="计划回款日期">
+            {{ formatDate(receivablePlan.returnTime, 'YYYY-MM-DD') }}
+          </el-descriptions-item>
+          <el-descriptions-item label="计划回款方式">
+            <dict-tag
+              :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE"
+              :value="receivablePlan.returnType"
+            />
+          </el-descriptions-item>
+          <el-descriptions-item label="提前几天提醒">
+            {{ receivablePlan.remindDays }}
+          </el-descriptions-item>
+          <el-descriptions-item label="备注">{{ receivablePlan.remark }}</el-descriptions-item>
+          <el-descriptions-item label="实际回款金额">
+            <el-text v-if="receivablePlan.receivable">
+              {{ erpPriceInputFormatter(receivablePlan.receivable.price) }}
+            </el-text>
+            <el-text v-else>{{ erpPriceInputFormatter(0) }}</el-text>
+          </el-descriptions-item>
+          <el-descriptions-item label="未回款金额">
+            <el-text v-if="receivablePlan.receivable">
+              {{ erpPriceInputFormatter(receivablePlan.price - receivablePlan.receivable.price) }}
+            </el-text>
+            <el-text v-else>{{ erpPriceInputFormatter(receivablePlan.price) }}</el-text>
+          </el-descriptions-item>
+          <el-descriptions-item label="实际回款日期">
+            {{ formatDate(receivablePlan.receivable?.returnTime, 'YYYY-MM-DD') }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-collapse-item>
+      <el-collapse-item name="systemInfo">
+        <template #title>
+          <span class="text-base font-bold">系统信息</span>
+        </template>
+        <el-descriptions :column="4">
+          <el-descriptions-item label="负责人">
+            {{ receivablePlan.ownerUserName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="创建人">
+            {{ receivablePlan.creatorName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="创建时间">
+            {{ formatDate(receivablePlan.createTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="更新时间">
+            {{ formatDate(receivablePlan.updateTime) }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-collapse-item>
+    </el-collapse>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { receivablePlan } = defineProps<{
+  receivablePlan: ReceivablePlanApi.ReceivablePlanVO
+}>()
+
+// 展示的折叠面板
+const activeNames = ref(['basicInfo', 'systemInfo'])
+</script>

+ 103 - 0
src/views/crm/receivable/plan/detail/index.vue

@@ -0,0 +1,103 @@
+<template>
+  <ReceivablePlanDetailsHeader v-loading="loading" :receivable-plan="receivablePlan">
+    <el-button
+      v-if="permissionListRef?.validateWrite"
+      @click="openForm('update', receivablePlan.id)"
+    >
+      编辑
+    </el-button>
+  </ReceivablePlanDetailsHeader>
+  <el-col>
+    <el-tabs>
+      <el-tab-pane label="详细资料">
+        <ReceivablePlanDetailsInfo :receivable-plan="receivablePlan" />
+      </el-tab-pane>
+      <el-tab-pane label="操作日志">
+        <OperateLogV2 :log-list="logList" />
+      </el-tab-pane>
+      <el-tab-pane label="团队成员">
+        <PermissionList
+          ref="permissionListRef"
+          :biz-id="receivablePlan.id!"
+          :biz-type="BizTypeEnum.CRM_RECEIVABLE_PLAN"
+          :show-action="true"
+          @quit-team="close"
+        />
+      </el-tab-pane>
+    </el-tabs>
+  </el-col>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ReceivablePlanForm ref="formRef" @success="getReceivablePlan(receivablePlan.id)" />
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import ReceivablePlanDetailsHeader from './ReceivablePlanDetailsHeader.vue'
+import ReceivablePlanDetailsInfo from './ReceivablePlanDetailsInfo.vue'
+import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
+import { BizTypeEnum } from '@/api/crm/permission'
+import { OperateLogV2VO } from '@/api/system/operatelog'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+import ReceivablePlanForm from '@/views/crm/receivable/plan/ReceivablePlanForm.vue'
+
+defineOptions({ name: 'CrmReceivablePlanDetail' })
+
+const message = useMessage()
+
+const receivablePlanId = ref(0) // 回款计划编号
+const loading = ref(true) // 加载中
+const receivablePlan = ref<ReceivablePlanApi.ReceivablePlanVO>(
+  {} as ReceivablePlanApi.ReceivablePlanVO
+) // 回款计划详情
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 团队成员列表 Ref
+
+/** 获取详情 */
+const getReceivablePlan = async (id: number) => {
+  loading.value = true
+  try {
+    receivablePlan.value = await ReceivablePlanApi.getReceivablePlan(id)
+    await getOperateLog(id)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 编辑 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 获取操作日志 */
+const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
+const getOperateLog = async (receivablePlanId: number) => {
+  if (!receivablePlanId) {
+    return
+  }
+  const data = await getOperateLogPage({
+    bizType: BizTypeEnum.CRM_RECEIVABLE_PLAN,
+    bizId: receivablePlanId
+  })
+  logList.value = data.list
+}
+
+/** 关闭窗口 */
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute } = useRouter() // 路由
+const close = () => {
+  delView(unref(currentRoute))
+}
+
+/** 初始化 */
+const { params } = useRoute()
+onMounted(async () => {
+  if (!params.id) {
+    message.warning('参数错误,回款计划不能为空!')
+    close()
+    return
+  }
+  receivablePlanId.value = params.id as unknown as number
+  await getReceivablePlan(receivablePlanId.value)
+})
+</script>

+ 164 - 58
src/views/crm/receivable/plan/index.vue

@@ -2,49 +2,63 @@
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
-      class="-mb-15px"
-      :model="queryParams"
       ref="queryFormRef"
       :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
       label-width="68px"
     >
-      <el-form-item label="客户" prop="customerId">
-        <el-input
+      <el-form-item label="客户名称" prop="customerId">
+        <el-select
           v-model="queryParams.customerId"
-          placeholder="请输入客户"
-          clearable
-          @keyup.enter="handleQuery"
           class="!w-240px"
-        />
+          placeholder="请选择客户"
+          @keyup.enter="handleQuery"
+        >
+          <el-option
+            v-for="item in customerList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
       </el-form-item>
-      <el-form-item label="合同" prop="contractId">
+      <el-form-item label="合同编号" prop="contractNo">
         <el-input
-          v-model="queryParams.contractId"
-          placeholder="请输入合同"
+          v-model="queryParams.contractNo"
+          class="!w-240px"
           clearable
+          placeholder="请输入合同编号"
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
         <el-button
-          type="primary"
+          v-hasPermi="['crm:receivable-plan:create']"
           plain
+          type="primary"
           @click="openForm('create')"
-          v-hasPermi="['crm:receivable-plan:create']"
         >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
+          <Icon class="mr-5px" icon="ep:plus" />
+          新增
         </el-button>
         <el-button
-          type="success"
+          v-hasPermi="['crm:receivable-plan:export']"
+          :loading="exportLoading"
           plain
+          type="success"
           @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['crm:receivable-plan:export']"
         >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
+          <Icon class="mr-5px" icon="ep:download" />
+          导出
         </el-button>
       </el-form-item>
     </el-form>
@@ -52,68 +66,131 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <!--<el-table-column label="ID" align="center" prop="id" />-->
-      <el-table-column label="客户名称" align="center" prop="customerId" width="150px" />
-      <el-table-column label="合同名称" align="center" prop="contractId" width="150px" />
-      <el-table-column label="期数" align="center" prop="period" />
-      <el-table-column label="计划回款" align="center" prop="price" />
+    <el-tabs v-model="activeName" @tab-click="handleTabClick">
+      <el-tab-pane label="我负责的" name="1" />
+      <el-tab-pane label="下属负责的" name="3" />
+    </el-tabs>
+    <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+      <el-table-column align="center" fixed="left" label="客户名称" prop="customerName" width="150">
+        <template #default="scope">
+          <el-link
+            :underline="false"
+            type="primary"
+            @click="openCustomerDetail(scope.row.customerId)"
+          >
+            {{ scope.row.customerName }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="合同编号" prop="contractNo" width="200px" />
+      <el-table-column align="center" label="期数" prop="period">
+        <template #default="scope">
+          <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+            {{ scope.row.period }}
+          </el-link>
+        </template>
+      </el-table-column>
       <el-table-column
-        label="计划回款日期"
         align="center"
-        prop="returnTime"
+        label="计划回款金额(元)"
+        prop="price"
+        width="160"
+        :formatter="erpPriceTableColumnFormatter"
+      />
+      <el-table-column
         :formatter="dateFormatter2"
+        align="center"
+        label="计划回款日期"
+        prop="returnTime"
         width="180px"
       />
-      <el-table-column label="提前几天提醒" align="center" prop="remindDays" />
-      <!--<el-table-column
-        label="提醒日期"
+      <el-table-column align="center" label="提前几天提醒" prop="remindDays" width="150" />
+      <el-table-column
         align="center"
+        label="提醒日期"
         prop="remindTime"
-        :formatter="dateFormatter"
         width="180px"
-      />-->
-      <!--<el-table-column label="回款ID" align="center" prop="receivableId" />-->
-      <el-table-column label="完成状态" align="center" prop="status">
+        :formatter="dateFormatter2"
+      />
+      <el-table-column align="center" label="回款方式" prop="returnType" width="130px">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+          <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="scope.row.returnType" />
         </template>
       </el-table-column>
-      <el-table-column label="审批状态" align="center" prop="checkStatus" width="130px">
+      <el-table-column align="center" label="备注" prop="remark" />
+      <el-table-column label="负责人" prop="ownerUserName" width="120" />
+      <el-table-column
+        align="center"
+        label="实际回款金额(元)"
+        prop="receivable.price"
+        width="160"
+      >
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.checkStatus" />
+          <el-text v-if="scope.row.receivable">
+            {{ erpPriceInputFormatter(scope.row.receivable.price) }}
+          </el-text>
+          <el-text v-else>{{ erpPriceInputFormatter(0) }}</el-text>
         </template>
       </el-table-column>
-      <!--<el-table-column label="工作流编号" align="center" prop="processInstanceId" />-->
-      <el-table-column prop="ownerUserId" label="负责人" width="120">
+      <el-table-column
+        align="center"
+        label="实际回款日期"
+        prop="receivable.returnTime"
+        width="180px"
+        :formatter="dateFormatter2"
+      />
+      <el-table-column
+        align="center"
+        label="实际回款金额(元)"
+        prop="receivable.price"
+        width="160"
+      >
         <template #default="scope">
-          {{ userList.find((user) => user.id === scope.row.ownerUserId)?.nickname }}
+          <el-text v-if="scope.row.receivable">
+            {{ erpPriceInputFormatter(scope.row.price - scope.row.receivable.price) }}
+          </el-text>
+          <el-text v-else>{{ erpPriceInputFormatter(scope.row.price) }}</el-text>
         </template>
       </el-table-column>
-      <el-table-column label="显示顺序" align="center" prop="sort" />
-      <el-table-column label="备注" align="center" prop="remark" />
       <el-table-column
-        label="创建时间"
+        :formatter="dateFormatter"
         align="center"
-        prop="createTime"
+        label="更新时间"
+        prop="updateTime"
+        width="180px"
+      />
+      <el-table-column
         :formatter="dateFormatter"
+        align="center"
+        label="创建时间"
+        prop="createTime"
         width="180px"
       />
-      <el-table-column label="操作" align="center" width="130px">
+      <el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
+      <el-table-column align="center" fixed="right" label="操作" width="180px">
         <template #default="scope">
           <el-button
+            v-hasPermi="['crm:receivable:create']"
+            link
+            type="success"
+            @click="openReceivableForm(scope.row)"
+            :disabled="scope.row.receivableId"
+          >
+            创建回款
+          </el-button>
+          <el-button
+            v-hasPermi="['crm:receivable-plan:update']"
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['crm:receivable-plan:update']"
           >
             编辑
           </el-button>
           <el-button
+            v-hasPermi="['crm:receivable-plan:delete']"
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['crm:receivable-plan:delete']"
           >
             删除
           </el-button>
@@ -122,24 +199,28 @@
     </el-table>
     <!-- 分页 -->
     <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
       @pagination="getList"
     />
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
   <ReceivablePlanForm ref="formRef" @success="getList" />
+  <ReceivableForm ref="receivableFormRef" @success="getList" />
 </template>
 
-<script setup lang="ts">
+<script lang="ts" setup>
 import { DICT_TYPE } from '@/utils/dict'
 import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
 import ReceivablePlanForm from './ReceivablePlanForm.vue'
-import * as UserApi from '@/api/system/user'
+import * as CustomerApi from '@/api/crm/customer'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { TabsPaneContext } from 'element-plus'
+import ReceivableForm from '@/views/crm/receivable/ReceivableForm.vue'
 
 defineOptions({ name: 'ReceivablePlan' })
 
@@ -149,15 +230,23 @@ const { t } = useI18n() // 国际化
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
-const userList = ref<UserApi.UserVO[]>([]) // 用户列表
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  customerId: null,
-  contractId: null
+  sceneType: '1', // 默认和 activeName 相等
+  customerId: undefined,
+  contractNo: undefined
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
+const activeName = ref('1') // 列表 tab
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
+
+/** tab 切换 */
+const handleTabClick = (tab: TabsPaneContext) => {
+  queryParams.sceneType = tab.paneName
+  handleQuery()
+}
 
 /** 查询列表 */
 const getList = async () => {
@@ -189,6 +278,12 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
+/** 创建回款操作 */
+const receivableFormRef = ref()
+const openReceivableForm = (row: ReceivablePlanApi.ReceivablePlanVO) => {
+  receivableFormRef.value.open('create', undefined, row)
+}
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
@@ -217,10 +312,21 @@ const handleExport = async () => {
   }
 }
 
+/** 打开详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'CrmReceivablePlanDetail', params: { id } })
+}
+
+/** 打开客户详情 */
+const openCustomerDetail = (id: number) => {
+  push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
 /** 初始化 **/
 onMounted(async () => {
   await getList()
-  // 获取用户列表
-  userList.value = await UserApi.getSimpleUserList()
+  // 获得客户列表
+  customerList.value = await CustomerApi.getCustomerSimpleList()
 })
 </script>

+ 3 - 3
src/views/crm/bi/rank/ContactsCountRank.vue → src/views/crm/statistics/rank/ContactsCountRank.vue

@@ -18,7 +18,7 @@
   </el-card>
 </template>
 <script setup lang="ts">
-import { RankApi, BiRankRespVO } from '@/api/crm/bi/rank'
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
 import { EChartsOption } from 'echarts'
 import { clone } from 'lodash-es'
 
@@ -26,7 +26,7 @@ defineOptions({ name: 'ContactsCountRank' })
 const props = defineProps<{ queryParams: any }>() // 搜索参数
 
 const loading = ref(false) // 加载中
-const list = ref<BiRankRespVO[]>([]) // 列表的数据
+const list = ref<StatisticsRankRespVO[]>([]) // 列表的数据
 
 /** 柱状图配置:横向 */
 const echartsOption = reactive<EChartsOption>({
@@ -80,7 +80,7 @@ const echartsOption = reactive<EChartsOption>({
 const loadData = async () => {
   // 1. 加载排行数据
   loading.value = true
-  const rankingList = await RankApi.getContactsCountRank(props.queryParams)
+  const rankingList = await StatisticsRankApi.getContactsCountRank(props.queryParams)
   // 2.1 更新 Echarts 数据
   if (echartsOption.dataset && echartsOption.dataset['source']) {
     echartsOption.dataset['source'] = clone(rankingList).reverse()

+ 3 - 3
src/views/crm/bi/rank/ContractCountRank.vue → src/views/crm/statistics/rank/ContractCountRank.vue

@@ -18,7 +18,7 @@
   </el-card>
 </template>
 <script setup lang="ts">
-import { RankApi, BiRankRespVO } from '@/api/crm/bi/rank'
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
 import { EChartsOption } from 'echarts'
 import { clone } from 'lodash-es'
 
@@ -26,7 +26,7 @@ defineOptions({ name: 'ContractCountRank' })
 const props = defineProps<{ queryParams: any }>() // 搜索参数
 
 const loading = ref(false) // 加载中
-const list = ref<BiRankRespVO[]>([]) // 列表的数据
+const list = ref<StatisticsRankRespVO[]>([]) // 列表的数据
 
 /** 柱状图配置:横向 */
 const echartsOption = reactive<EChartsOption>({
@@ -80,7 +80,7 @@ const echartsOption = reactive<EChartsOption>({
 const loadData = async () => {
   // 1. 加载排行数据
   loading.value = true
-  const rankingList = await RankApi.getContractCountRank(props.queryParams)
+  const rankingList = await StatisticsRankApi.getContractCountRank(props.queryParams)
   // 2.1 更新 Echarts 数据
   if (echartsOption.dataset && echartsOption.dataset['source']) {
     echartsOption.dataset['source'] = clone(rankingList).reverse()

+ 3 - 3
src/views/crm/bi/rank/ContractPriceRank.vue → src/views/crm/statistics/rank/ContractPriceRank.vue

@@ -18,7 +18,7 @@
   </el-card>
 </template>
 <script setup lang="ts">
-import { RankApi, BiRankRespVO } from '@/api/crm/bi/rank'
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
 import { EChartsOption } from 'echarts'
 import { clone } from 'lodash-es'
 
@@ -26,7 +26,7 @@ defineOptions({ name: 'ContractPriceRank' })
 const props = defineProps<{ queryParams: any }>() // 搜索参数
 
 const loading = ref(false) // 加载中
-const list = ref<BiRankRespVO[]>([]) // 列表的数据
+const list = ref<StatisticsRankRespVO[]>([]) // 列表的数据
 
 /** 柱状图配置:横向 */
 const echartsOption = reactive<EChartsOption>({
@@ -80,7 +80,7 @@ const echartsOption = reactive<EChartsOption>({
 const loadData = async () => {
   // 1. 加载排行数据
   loading.value = true
-  const rankingList = await RankApi.getContractPriceRank(props.queryParams)
+  const rankingList = await StatisticsRankApi.getContractPriceRank(props.queryParams)
   // 2.1 更新 Echarts 数据
   if (echartsOption.dataset && echartsOption.dataset['source']) {
     echartsOption.dataset['source'] = clone(rankingList).reverse()

+ 3 - 3
src/views/crm/bi/rank/CustomerCountRank.vue → src/views/crm/statistics/rank/CustomerCountRank.vue

@@ -18,7 +18,7 @@
   </el-card>
 </template>
 <script setup lang="ts">
-import { RankApi, BiRankRespVO } from '@/api/crm/bi/rank'
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
 import { EChartsOption } from 'echarts'
 import { clone } from 'lodash-es'
 
@@ -26,7 +26,7 @@ defineOptions({ name: 'CustomerCountRank' })
 const props = defineProps<{ queryParams: any }>() // 搜索参数
 
 const loading = ref(false) // 加载中
-const list = ref<BiRankRespVO[]>([]) // 列表的数据
+const list = ref<StatisticsRankRespVO[]>([]) // 列表的数据
 
 /** 柱状图配置:横向 */
 const echartsOption = reactive<EChartsOption>({
@@ -80,7 +80,7 @@ const echartsOption = reactive<EChartsOption>({
 const loadData = async () => {
   // 1. 加载排行数据
   loading.value = true
-  const rankingList = await RankApi.getCustomerCountRank(props.queryParams)
+  const rankingList = await StatisticsRankApi.getCustomerCountRank(props.queryParams)
   // 2.1 更新 Echarts 数据
   if (echartsOption.dataset && echartsOption.dataset['source']) {
     echartsOption.dataset['source'] = clone(rankingList).reverse()

+ 3 - 3
src/views/crm/bi/rank/FollowCountRank.vue → src/views/crm/statistics/rank/FollowCountRank.vue

@@ -18,7 +18,7 @@
   </el-card>
 </template>
 <script setup lang="ts">
-import { RankApi, BiRankRespVO } from '@/api/crm/bi/rank'
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
 import { EChartsOption } from 'echarts'
 import { clone } from 'lodash-es'
 
@@ -26,7 +26,7 @@ defineOptions({ name: 'FollowCountRank' })
 const props = defineProps<{ queryParams: any }>() // 搜索参数
 
 const loading = ref(false) // 加载中
-const list = ref<BiRankRespVO[]>([]) // 列表的数据
+const list = ref<StatisticsRankRespVO[]>([]) // 列表的数据
 
 /** 柱状图配置:横向 */
 const echartsOption = reactive<EChartsOption>({
@@ -80,7 +80,7 @@ const echartsOption = reactive<EChartsOption>({
 const loadData = async () => {
   // 1. 加载排行数据
   loading.value = true
-  const rankingList = await RankApi.getFollowCountRank(props.queryParams)
+  const rankingList = await StatisticsRankApi.getFollowCountRank(props.queryParams)
   // 2.1 更新 Echarts 数据
   if (echartsOption.dataset && echartsOption.dataset['source']) {
     echartsOption.dataset['source'] = clone(rankingList).reverse()

+ 3 - 3
src/views/crm/bi/rank/FollowCustomerCountRank.vue → src/views/crm/statistics/rank/FollowCustomerCountRank.vue

@@ -18,7 +18,7 @@
   </el-card>
 </template>
 <script setup lang="ts">
-import { RankApi, BiRankRespVO } from '@/api/crm/bi/rank'
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
 import { EChartsOption } from 'echarts'
 import { clone } from 'lodash-es'
 
@@ -26,7 +26,7 @@ defineOptions({ name: 'FollowCustomerCountRank' })
 const props = defineProps<{ queryParams: any }>() // 搜索参数
 
 const loading = ref(false) // 加载中
-const list = ref<BiRankRespVO[]>([]) // 列表的数据
+const list = ref<StatisticsRankRespVO[]>([]) // 列表的数据
 
 /** 柱状图配置:横向 */
 const echartsOption = reactive<EChartsOption>({
@@ -80,7 +80,7 @@ const echartsOption = reactive<EChartsOption>({
 const loadData = async () => {
   // 1. 加载排行数据
   loading.value = true
-  const rankingList = await RankApi.getFollowCustomerCountRank(props.queryParams)
+  const rankingList = await StatisticsRankApi.getFollowCustomerCountRank(props.queryParams)
   // 2.1 更新 Echarts 数据
   if (echartsOption.dataset && echartsOption.dataset['source']) {
     echartsOption.dataset['source'] = clone(rankingList).reverse()

+ 3 - 3
src/views/crm/bi/rank/ProductSalesRank.vue → src/views/crm/statistics/rank/ProductSalesRank.vue

@@ -18,7 +18,7 @@
   </el-card>
 </template>
 <script setup lang="ts">
-import { RankApi, BiRankRespVO } from '@/api/crm/bi/rank'
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
 import { EChartsOption } from 'echarts'
 import { clone } from 'lodash-es'
 
@@ -26,7 +26,7 @@ defineOptions({ name: 'ProductSalesRank' })
 const props = defineProps<{ queryParams: any }>() // 搜索参数
 
 const loading = ref(false) // 加载中
-const list = ref<BiRankRespVO[]>([]) // 列表的数据
+const list = ref<StatisticsRankRespVO[]>([]) // 列表的数据
 
 /** 柱状图配置:横向 */
 const echartsOption = reactive<EChartsOption>({
@@ -80,7 +80,7 @@ const echartsOption = reactive<EChartsOption>({
 const loadData = async () => {
   // 1. 加载排行数据
   loading.value = true
-  const rankingList = await RankApi.getProductSalesRank(props.queryParams)
+  const rankingList = await StatisticsRankApi.getProductSalesRank(props.queryParams)
   // 2.1 更新 Echarts 数据
   if (echartsOption.dataset && echartsOption.dataset['source']) {
     echartsOption.dataset['source'] = clone(rankingList).reverse()

+ 3 - 3
src/views/crm/bi/rank/ReceivablePriceRank.vue → src/views/crm/statistics/rank/ReceivablePriceRank.vue

@@ -18,7 +18,7 @@
   </el-card>
 </template>
 <script setup lang="ts">
-import { RankApi, BiRankRespVO } from '@/api/crm/bi/rank'
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
 import { EChartsOption } from 'echarts'
 import { clone } from 'lodash-es'
 
@@ -26,7 +26,7 @@ defineOptions({ name: 'ReceivablePriceRank' })
 const props = defineProps<{ queryParams: any }>() // 搜索参数
 
 const loading = ref(false) // 加载中
-const list = ref<BiRankRespVO[]>([]) // 列表的数据
+const list = ref<StatisticsRankRespVO[]>([]) // 列表的数据
 
 /** 柱状图配置:横向 */
 const echartsOption = reactive<EChartsOption>({
@@ -81,7 +81,7 @@ const echartsOption = reactive<EChartsOption>({
 const loadData = async () => {
   // 1. 加载排行数据
   loading.value = true
-  const rankingList = await RankApi.getReceivablePriceRank(props.queryParams)
+  const rankingList = await StatisticsRankApi.getReceivablePriceRank(props.queryParams)
   // 2.1 更新 Echarts 数据
   if (echartsOption.dataset && echartsOption.dataset['source']) {
     echartsOption.dataset['source'] = clone(rankingList).reverse()

+ 1 - 1
src/views/crm/bi/rank/index.vue → src/views/crm/statistics/rank/index.vue

@@ -90,7 +90,7 @@ import * as DeptApi from '@/api/system/dept'
 import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
 import { useUserStore } from '@/store/modules/user'
 
-defineOptions({ name: 'CrmBiRank' })
+defineOptions({ name: 'CrmStatisticsRank' })
 
 const queryParams = reactive({
   deptId: useUserStore().getUser.deptId,