Pārlūkot izejas kodu

Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3

liuhongfeng 1 gadu atpakaļ
vecāks
revīzija
d9ed3d1a6b
100 mainītis faili ar 3440 papildinājumiem un 1042 dzēšanām
  1. 57 0
      src/api/crm/business/index.ts
  2. 48 0
      src/api/crm/businessStatusType/index.ts
  3. 19 17
      src/api/crm/contact/index.ts
  4. 11 6
      src/api/crm/contract/index.ts
  5. 5 0
      src/api/crm/customer/index.ts
  6. 14 0
      src/api/crm/customerLimitConfig/index.ts
  7. 3 2
      src/api/crm/customerPoolConfig/index.ts
  8. 31 7
      src/api/crm/permission/index.ts
  9. 12 11
      src/api/crm/receivable/index.ts
  10. 5 0
      src/api/crm/receivable/plan/index.ts
  11. 1 1
      src/api/infra/demo/demo01/index.ts
  12. 1 1
      src/api/infra/demo/demo02/index.ts
  13. 1 1
      src/api/infra/demo/demo03/erp/index.ts
  14. 7 3
      src/api/infra/demo/demo03/inner/index.ts
  15. 7 3
      src/api/infra/demo/demo03/normal/index.ts
  16. 1 1
      src/api/infra/fileConfig/index.ts
  17. 2 2
      src/api/infra/jobLog/index.ts
  18. 1 1
      src/api/mall/promotion/article/index.ts
  19. 1 1
      src/api/mp/user/index.ts
  20. 1 1
      src/api/system/dept/index.ts
  21. 3 3
      src/api/system/dict/dict.data.ts
  22. 1 1
      src/api/system/dict/dict.type.ts
  23. 1 0
      src/api/system/loginLog/index.ts
  24. 1 1
      src/api/system/mail/account/index.ts
  25. 1 1
      src/api/system/menu/index.ts
  26. 1 0
      src/api/system/notify/message/index.ts
  27. 1 1
      src/api/system/notify/template/index.ts
  28. 1 1
      src/api/system/post/index.ts
  29. 1 1
      src/api/system/role/index.ts
  30. 1 1
      src/api/system/sms/smsChannel/index.ts
  31. 3 3
      src/api/system/sms/smsTemplate/index.ts
  32. 1 1
      src/api/system/tenantPackage/index.ts
  33. 1 1
      src/api/system/user/index.ts
  34. 16 28
      src/api/system/user/profile.ts
  35. 198 0
      src/components/AppLinkInput/AppLinkSelectDialog.vue
  36. 246 0
      src/components/AppLinkInput/data.ts
  37. 43 0
      src/components/AppLinkInput/index.vue
  38. 1 1
      src/components/DiyEditor/components/ComponentContainerProperty.vue
  39. 1 1
      src/components/DiyEditor/components/mobile/Carousel/property.vue
  40. 1 1
      src/components/DiyEditor/components/mobile/ImageBar/property.vue
  41. 1 1
      src/components/DiyEditor/components/mobile/MagicCube/property.vue
  42. 1 1
      src/components/DiyEditor/components/mobile/MenuGrid/property.vue
  43. 1 1
      src/components/DiyEditor/components/mobile/MenuList/property.vue
  44. 1 1
      src/components/DiyEditor/components/mobile/MenuSwiper/property.vue
  45. 1 1
      src/components/DiyEditor/components/mobile/NoticeBar/property.vue
  46. 1 1
      src/components/DiyEditor/components/mobile/ProductList/config.ts
  47. 1 1
      src/components/DiyEditor/components/mobile/ProductList/index.vue
  48. 1 1
      src/components/DiyEditor/components/mobile/ProductList/property.vue
  49. 25 0
      src/components/DiyEditor/components/mobile/PromotionArticle/config.ts
  50. 27 0
      src/components/DiyEditor/components/mobile/PromotionArticle/index.vue
  51. 56 0
      src/components/DiyEditor/components/mobile/PromotionArticle/property.vue
  52. 64 0
      src/components/DiyEditor/components/mobile/PromotionCombination/config.ts
  53. 125 0
      src/components/DiyEditor/components/mobile/PromotionCombination/index.vue
  54. 112 0
      src/components/DiyEditor/components/mobile/PromotionCombination/property.vue
  55. 64 0
      src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts
  56. 125 0
      src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue
  57. 112 0
      src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue
  58. 1 1
      src/components/DiyEditor/components/mobile/TabBar/property.vue
  59. 1 1
      src/components/DiyEditor/components/mobile/TitleBar/property.vue
  60. 21 0
      src/components/DiyEditor/components/mobile/UserCard/config.ts
  61. 29 0
      src/components/DiyEditor/components/mobile/UserCard/index.vue
  62. 17 0
      src/components/DiyEditor/components/mobile/UserCard/property.vue
  63. 23 0
      src/components/DiyEditor/components/mobile/UserCoupon/config.ts
  64. 15 0
      src/components/DiyEditor/components/mobile/UserCoupon/index.vue
  65. 17 0
      src/components/DiyEditor/components/mobile/UserCoupon/property.vue
  66. 23 0
      src/components/DiyEditor/components/mobile/UserOrder/config.ts
  67. 13 0
      src/components/DiyEditor/components/mobile/UserOrder/index.vue
  68. 17 0
      src/components/DiyEditor/components/mobile/UserOrder/property.vue
  69. 23 0
      src/components/DiyEditor/components/mobile/UserWallet/config.ts
  70. 15 0
      src/components/DiyEditor/components/mobile/UserWallet/index.vue
  71. 17 0
      src/components/DiyEditor/components/mobile/UserWallet/property.vue
  72. 8 2
      src/components/DiyEditor/util.ts
  73. 36 1
      src/components/RouterSearch/index.vue
  74. 17 16
      src/components/UploadFile/src/UploadImg.vue
  75. 5 0
      src/layout/components/ToolHeader.vue
  76. 2 0
      src/store/modules/app.ts
  77. 3 3
      src/store/modules/dict.ts
  78. 157 152
      src/utils/dict.ts
  79. 3 3
      src/utils/formatTime.ts
  80. 22 0
      src/utils/index.ts
  81. 2 2
      src/views/Profile/components/ProfileUser.vue
  82. 279 0
      src/views/crm/business/BusinessForm.vue
  83. 107 0
      src/views/crm/business/components/BusinessList.vue
  84. 207 0
      src/views/crm/business/index.vue
  85. 167 0
      src/views/crm/businessStatusType/BusinessStatusTypeForm.vue
  86. 171 0
      src/views/crm/businessStatusType/index.vue
  87. 17 2
      src/views/crm/clue/ClueForm.vue
  88. 0 138
      src/views/crm/components/CrmPermissionList.vue
  89. 0 15
      src/views/crm/components/index.ts
  90. 3 3
      src/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue
  91. 13 5
      src/views/crm/config/customerLimitConfig/CustomerLimitConfigList.vue
  92. 19 0
      src/views/crm/config/customerLimitConfig/index.vue
  93. 25 27
      src/views/crm/config/customerPoolConfig/index.vue
  94. 183 232
      src/views/crm/contact/ContactForm.vue
  95. 112 0
      src/views/crm/contact/components/ContactList.vue
  96. 9 26
      src/views/crm/contact/detail/ContactDetails.vue
  97. 10 25
      src/views/crm/contact/detail/index.vue
  98. 30 48
      src/views/crm/contact/index.vue
  99. 132 0
      src/views/crm/contract/components/ContractList.vue
  100. 0 228
      src/views/crm/contract/contract.data.ts

+ 57 - 0
src/api/crm/business/index.ts

@@ -0,0 +1,57 @@
+import request from '@/config/axios'
+
+export interface BusinessVO {
+  id: number
+  name: string
+  statusTypeId: number
+  statusId: number
+  contactNextTime: Date
+  customerId: number
+  dealTime: Date
+  price: number
+  discountPercent: number
+  productPrice: number
+  remark: string
+  ownerUserId: number
+  roUserIds: string
+  rwUserIds: string
+  endStatus: number
+  endRemark: string
+  contactLastTime: Date
+  followUpStatus: number
+}
+
+// 查询 CRM 商机列表
+export const getBusinessPage = async (params) => {
+  return await request.get({ url: `/crm/business/page`, params })
+}
+
+// 查询 CRM 商机列表,基于指定客户
+export const getBusinessPageByCustomer = async (params) => {
+  return await request.get({ url: `/crm/business/page-by-customer`, params })
+}
+
+// 查询 CRM 商机详情
+export const getBusiness = async (id: number) => {
+  return await request.get({ url: `/crm/business/get?id=` + id })
+}
+
+// 新增 CRM 商机
+export const createBusiness = async (data: BusinessVO) => {
+  return await request.post({ url: `/crm/business/create`, data })
+}
+
+// 修改 CRM 商机
+export const updateBusiness = async (data: BusinessVO) => {
+  return await request.put({ url: `/crm/business/update`, data })
+}
+
+// 删除 CRM 商机
+export const deleteBusiness = async (id: number) => {
+  return await request.delete({ url: `/crm/business/delete?id=` + id })
+}
+
+// 导出 CRM 商机 Excel
+export const exportBusiness = async (params) => {
+  return await request.download({ url: `/crm/business/export-excel`, params })
+}

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

@@ -0,0 +1,48 @@
+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 })
+}

+ 19 - 17
src/api/crm/contact/index.ts

@@ -1,10 +1,3 @@
-/*
- * @Author: zyna
- * @Date: 2023-11-05 13:34:41
- * @LastEditTime: 2023-11-11 16:20:19
- * @FilePath: \yudao-ui-admin-vue3\src\api\crm\contact\index.ts
- * @Description:
- */
 import request from '@/config/axios'
 
 export interface ContactVO {
@@ -22,44 +15,53 @@ export interface ContactVO {
   id: number
   parentId: number
   qq: number
-  webchat: string
+  wechat: string
   sex: number
-  policyMakers: boolean
+  master: boolean
   creatorName: string
   updateTime?: Date
   createTime?: Date
   customerName: string
+  areaName: string
+  ownerUserName: string
 }
 
-// 查询crm联系人列表
+// 查询 CRM 联系人列表
 export const getContactPage = async (params) => {
   return await request.get({ url: `/crm/contact/page`, params })
 }
 
-// 查询crm联系人详情
+// 查询 CRM 联系人列表,基于指定客户
+export const getContactPageByCustomer = async (params: any) => {
+  return await request.get({ url: `/crm/contact/page-by-customer`, params })
+}
+
+// 查询 CRM 联系人详情
 export const getContact = async (id: number) => {
   return await request.get({ url: `/crm/contact/get?id=` + id })
 }
 
-// 新增crm联系人
+// 新增 CRM 联系人
 export const createContact = async (data: ContactVO) => {
   return await request.post({ url: `/crm/contact/create`, data })
 }
 
-// 修改crm联系人
+// 修改 CRM 联系人
 export const updateContact = async (data: ContactVO) => {
   return await request.put({ url: `/crm/contact/update`, data })
 }
 
-// 删除crm联系人
+// 删除 CRM 联系人
 export const deleteContact = async (id: number) => {
   return await request.delete({ url: `/crm/contact/delete?id=` + id })
 }
 
-// 导出crm联系人 Excel
+// 导出 CRM 联系人 Excel
 export const exportContact = async (params) => {
   return await request.download({ url: `/crm/contact/export-excel`, params })
 }
-export const simpleAlllist = async () => {
-  return await request.get({ url: `/crm/contact/simpleAlllist` })
+
+// 获得 CRM 联系人列表(精简)
+export const getSimpleContactList = async () => {
+  return await request.get({ url: `/crm/contact/simple-all-list` })
 }

+ 11 - 6
src/api/crm/contract/index.ts

@@ -22,32 +22,37 @@ export interface ContractVO {
   remark: string
 }
 
-// 查询合同列表
+// 查询 CRM 合同列表
 export const getContractPage = async (params) => {
   return await request.get({ url: `/crm/contract/page`, params })
 }
 
-// 查询合同详情
+// 查询 CRM 联系人列表,基于指定客户
+export const getContractPageByCustomer = async (params: any) => {
+  return await request.get({ url: `/crm/contract/page-by-customer`, params })
+}
+
+// 查询 CRM 合同详情
 export const getContract = async (id: number) => {
   return await request.get({ url: `/crm/contract/get?id=` + id })
 }
 
-// 新增合同
+// 新增 CRM 合同
 export const createContract = async (data: ContractVO) => {
   return await request.post({ url: `/crm/contract/create`, data })
 }
 
-// 修改合同
+// 修改 CRM 合同
 export const updateContract = async (data: ContractVO) => {
   return await request.put({ url: `/crm/contract/update`, data })
 }
 
-// 删除合同
+// 删除 CRM 合同
 export const deleteContract = async (id: number) => {
   return await request.delete({ url: `/crm/contract/delete?id=` + id })
 }
 
-// 导出合同 Excel
+// 导出 CRM 合同 Excel
 export const exportContract = async (params) => {
   return await request.download({ url: `/crm/contract/export-excel`, params })
 }

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

@@ -62,3 +62,8 @@ export const deleteCustomer = async (id: number) => {
 export const exportCustomer = async (params) => {
   return await request.download({ url: `/crm/customer/export-excel`, params })
 }
+
+// 客户列表
+export const queryAllList = async () => {
+  return await request.get({ url: `/crm/customer/query-all-list` })
+}

+ 14 - 0
src/api/crm/customerLimitConfig/index.ts

@@ -9,6 +9,20 @@ export interface CustomerLimitConfigVO {
   dealCountEnabled?: boolean
 }
 
+/**
+ * 客户限制配置类型
+ */
+export enum LimitConfType {
+  /**
+   * 拥有客户数限制
+   */
+  CUSTOMER_QUANTITY_LIMIT = 1,
+  /**
+   * 锁定客户数限制
+   */
+  CUSTOMER_LOCK_LIMIT = 2
+}
+
 // 查询客户限制配置列表
 export const getCustomerLimitConfigPage = async (params) => {
   return await request.get({ url: `/crm/customer-limit-config/page`, params })

+ 3 - 2
src/api/crm/customerPoolConf/index.ts → src/api/crm/customerPoolConfig/index.ts

@@ -1,4 +1,5 @@
 import request from '@/config/axios'
+import { ConfigVO } from '@/api/infra/config'
 
 export interface CustomerPoolConfigVO {
   enabled?: boolean
@@ -14,6 +15,6 @@ export const getCustomerPoolConfig = async () => {
 }
 
 // 更新客户公海规则设置
-export const updateCustomerPoolConfig = async (data: ConfigVO) => {
-  return await request.put({ url: `/crm/customer-pool-config/update`, data })
+export const saveCustomerPoolConfig = async (data: ConfigVO) => {
+  return await request.put({ url: `/crm/customer-pool-config/save`, data })
 }

+ 31 - 7
src/api/crm/permission/index.ts

@@ -12,36 +12,60 @@ export interface PermissionVO {
   createTime?: Date
 }
 
-// 查询团队成员列表
+/**
+ * CRM 业务类型枚举
+ *
+ * @author HUIHUI
+ */
+export enum BizTypeEnum {
+  CRM_LEADS = 1, // 线索
+  CRM_CUSTOMER = 2, // 客户
+  CRM_CONTACT = 3, // 联系人
+  CRM_BUSINESS = 5, // 商机
+  CRM_CONTRACT = 6 // 合同
+}
+
+/**
+ * CRM 数据权限级别枚举
+ */
+export enum PermissionLevelEnum {
+  OWNER = 1, // 负责人
+  READ = 2, // 只读
+  WRITE = 3 // 读写
+}
+
+// 获得数据权限列表(查询团队成员列表)
 export const getPermissionList = async (params) => {
   return await request.get({ url: `/crm/permission/list`, params })
 }
 
-// 新增团队成员
+// 创建数据权限(新增团队成员
 export const createPermission = async (data: PermissionVO) => {
   return await request.post({ url: `/crm/permission/create`, data })
 }
 
-// 修改团队成员权限级别
+// 编辑数据权限(修改团队成员权限级别
 export const updatePermission = async (data) => {
   return await request.put({ url: `/crm/permission/update`, data })
 }
 
-// 删除团队成员
-export const deletePermission = async (params) => {
+// 删除数据权限(删除团队成员
+export const deletePermissionBatch = async (params) => {
   return await request.delete({ url: '/crm/permission/delete', params })
 }
 
-// 退出团队
-export const quitTeam = async (id) => {
+// 删除自己的数据权限(退出团队
+export const deleteSelfPermission = async (id) => {
   return await request.delete({ url: '/crm/permission/quit-team?id=' + id })
 }
 
+// TODO @puhui999:调整下位置
 // 领取公海数据
 export const receive = async (data: { bizType: number; bizId: number }) => {
   return await request.put({ url: `/crm/permission/receive`, data })
 }
 
+// TODO @puhui999:调整下位置
 // 数据放入公海
 export const putPool = async (data: { bizType: number; bizId: number }) => {
   return await request.put({ url: `/crm/permission/put-pool`, data })

+ 12 - 11
src/api/crm/receivable/index.ts

@@ -6,46 +6,47 @@ export interface ReceivableVO {
   planId: number
   customerId: number
   contractId: number
-  checkStatus: number
+  auditStatus: number
   processInstanceId: number
   returnTime: Date
   returnType: string
   price: number
   ownerUserId: number
-  batchId: number
   sort: number
-  dataScope: number
-  dataScopeDeptIds: string
-  status: number
   remark: string
 }
 
-// 查询回款管理列表
+// 查询回款列表
 export const getReceivablePage = async (params) => {
   return await request.get({ url: `/crm/receivable/page`, params })
 }
 
-// 查询回款管理详情
+// 查询回款列表
+export const getReceivablePageByCustomer = async (params) => {
+  return await request.get({ url: `/crm/receivable/page-by-customer`, params })
+}
+
+// 查询回款详情
 export const getReceivable = async (id: number) => {
   return await request.get({ url: `/crm/receivable/get?id=` + id })
 }
 
-// 新增回款管理
+// 新增回款
 export const createReceivable = async (data: ReceivableVO) => {
   return await request.post({ url: `/crm/receivable/create`, data })
 }
 
-// 修改回款管理
+// 修改回款
 export const updateReceivable = async (data: ReceivableVO) => {
   return await request.put({ url: `/crm/receivable/update`, data })
 }
 
-// 删除回款管理
+// 删除回款
 export const deleteReceivable = async (id: number) => {
   return await request.delete({ url: `/crm/receivable/delete?id=` + id })
 }
 
-// 导出回款管理 Excel
+// 导出回款 Excel
 export const exportReceivable = async (params) => {
   return await request.download({ url: `/crm/receivable/export-excel`, params })
 }

+ 5 - 0
src/api/crm/receivablePlan/index.ts → src/api/crm/receivable/plan/index.ts

@@ -23,6 +23,11 @@ export const getReceivablePlanPage = async (params) => {
   return await request.get({ url: `/crm/receivable-plan/page`, params })
 }
 
+// 查询回款计划列表
+export const getReceivablePlanPageByCustomer = async (params) => {
+  return await request.get({ url: `/crm/receivable-plan/page-by-customer`, params })
+}
+
 // 查询回款计划详情
 export const getReceivablePlan = async (id: number) => {
   return await request.get({ url: `/crm/receivable-plan/get?id=` + id })

+ 1 - 1
src/api/infra/demo/demo01/index.ts

@@ -37,4 +37,4 @@ export const deleteDemo01Contact = async (id: number) => {
 // 导出示例联系人 Excel
 export const exportDemo01Contact = async (params) => {
   return await request.download({ url: `/infra/demo01-contact/export-excel`, params })
-}
+}

+ 1 - 1
src/api/infra/demo/demo02/index.ts

@@ -34,4 +34,4 @@ export const deleteDemo02Category = async (id: number) => {
 // 导出示例分类 Excel
 export const exportDemo02Category = async (params) => {
   return await request.download({ url: `/infra/demo02-category/export-excel`, params })
-}
+}

+ 1 - 1
src/api/infra/demo/demo03/erp/index.ts

@@ -88,4 +88,4 @@ export const deleteDemo03Grade = async (id: number) => {
 // 获得学生班级
 export const getDemo03Grade = async (id: number) => {
   return await request.get({ url: `/infra/demo03-student/demo03-grade/get?id=` + id })
-}
+}

+ 7 - 3
src/api/infra/demo/demo03/inner/index.ts

@@ -42,12 +42,16 @@ export const exportDemo03Student = async (params) => {
 
 // 获得学生课程列表
 export const getDemo03CourseListByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId })
+  return await request.get({
+    url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId
+  })
 }
 
 // ==================== 子表(学生班级) ====================
 
 // 获得学生班级
 export const getDemo03GradeByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId })
-}
+  return await request.get({
+    url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId
+  })
+}

+ 7 - 3
src/api/infra/demo/demo03/normal/index.ts

@@ -42,12 +42,16 @@ export const exportDemo03Student = async (params) => {
 
 // 获得学生课程列表
 export const getDemo03CourseListByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId })
+  return await request.get({
+    url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId
+  })
 }
 
 // ==================== 子表(学生班级) ====================
 
 // 获得学生班级
 export const getDemo03GradeByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId })
-}
+  return await request.get({
+    url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId
+  })
+}

+ 1 - 1
src/api/infra/fileConfig/index.ts

@@ -17,7 +17,7 @@ export interface FileClientConfig {
 export interface FileConfigVO {
   id: number
   name: string
-  storage: any
+  storage?: number
   master: boolean
   visible: boolean
   config: FileClientConfig

+ 2 - 2
src/api/infra/jobLog/index.ts

@@ -7,8 +7,8 @@ export interface JobLogVO {
   handlerParam: string
   cronExpression: string
   executeIndex: string
-  beginTime: string
-  endTime: string
+  beginTime: Date
+  endTime: Date
   duration: string
   status: number
   createTime: string

+ 1 - 1
src/api/mall/promotion/article/index.ts

@@ -17,7 +17,7 @@ export interface ArticleVO {
 }
 
 // 查询文章管理列表
-export const getArticlePage = async (params) => {
+export const getArticlePage = async (params: any) => {
   return await request.get({ url: `/promotion/article/page`, params })
 }
 

+ 1 - 1
src/api/mp/user/index.ts

@@ -26,6 +26,6 @@ export const getUserPage = (query) => {
 // 同步公众号粉丝
 export const syncUser = (accountId) => {
   return request.post({
-    url: '/mp/tag/sync?accountId=' + accountId
+    url: '/mp/user/sync?accountId=' + accountId
   })
 }

+ 1 - 1
src/api/system/dept/index.ts

@@ -14,7 +14,7 @@ export interface DeptVO {
 
 // 查询部门(精简)列表
 export const getSimpleDeptList = async (): Promise<DeptVO[]> => {
-  return await request.get({ url: '/system/dept/list-all-simple' })
+  return await request.get({ url: '/system/dept/simple-list' })
 }
 
 // 查询部门列表

+ 3 - 3
src/api/system/dict/dict.data.ts

@@ -14,8 +14,8 @@ export type DictDataVO = {
 }
 
 // 查询字典数据(精简)列表
-export const listSimpleDictData = () => {
-  return request.get({ url: '/system/dict-data/list-all-simple' })
+export const getSimpleDictDataList = () => {
+  return request.get({ url: '/system/dict-data/simple-list' })
 }
 
 // 查询字典数据列表
@@ -45,5 +45,5 @@ export const deleteDictData = (id: number) => {
 
 // 导出字典类型数据
 export const exportDictData = (params) => {
-  return request.get({ url: '/system/dict-data/export', params })
+  return request.download({ url: '/system/dict-data/export', params })
 }

+ 1 - 1
src/api/system/dict/dict.type.ts

@@ -40,5 +40,5 @@ export const deleteDictType = (id: number) => {
 }
 // 导出字典类型
 export const exportDictType = (params) => {
-  return request.get({ url: '/system/dict-type/export', params })
+  return request.download({ url: '/system/dict-type/export', params })
 }

+ 1 - 0
src/api/system/loginLog/index.ts

@@ -7,6 +7,7 @@ export interface LoginLogVO {
   userId: number
   userType: number
   username: string
+  result: number
   status: number
   userIp: string
   userAgent: string

+ 1 - 1
src/api/system/mail/account/index.ts

@@ -37,5 +37,5 @@ export const deleteMailAccount = async (id: number) => {
 
 // 获得邮箱账号精简列表
 export const getSimpleMailAccountList = async () => {
-  return request.get({ url: '/system/mail-account/list-all-simple' })
+  return request.get({ url: '/system/mail-account/simple-list' })
 }

+ 1 - 1
src/api/system/menu/index.ts

@@ -20,7 +20,7 @@ export interface MenuVO {
 
 // 查询菜单(精简)列表
 export const getSimpleMenusList = () => {
-  return request.get({ url: '/system/menu/list-all-simple' })
+  return request.get({ url: '/system/menu/simple-list' })
 }
 
 // 查询菜单列表

+ 1 - 0
src/api/system/notify/message/index.ts

@@ -13,6 +13,7 @@ export interface NotifyMessageVO {
   templateParams: string
   readStatus: boolean
   readTime: Date
+  createTime: Date
 }
 
 // 查询站内信消息列表

+ 1 - 1
src/api/system/notify/template/index.ts

@@ -6,7 +6,7 @@ export interface NotifyTemplateVO {
   nickname: string
   code: string
   content: string
-  type: number
+  type?: number
   params: string
   status: number
   remark: string

+ 1 - 1
src/api/system/post/index.ts

@@ -17,7 +17,7 @@ export const getPostPage = async (params: PageParam) => {
 
 // 获取岗位精简信息列表
 export const getSimplePostList = async (): Promise<PostVO[]> => {
-  return await request.get({ url: '/system/post/list-all-simple' })
+  return await request.get({ url: '/system/post/simple-list' })
 }
 
 // 查询岗位详情

+ 1 - 1
src/api/system/role/index.ts

@@ -24,7 +24,7 @@ export const getRolePage = async (params: PageParam) => {
 
 // 查询角色(精简)列表
 export const getSimpleRoleList = async (): Promise<RoleVO[]> => {
-  return await request.get({ url: '/system/role/list-all-simple' })
+  return await request.get({ url: '/system/role/simple-list' })
 }
 
 // 查询角色详情

+ 1 - 1
src/api/system/sms/smsChannel/index.ts

@@ -19,7 +19,7 @@ export const getSmsChannelPage = (params: PageParam) => {
 
 // 获得短信渠道精简列表
 export function getSimpleSmsChannelList() {
-  return request.get({ url: '/system/sms-channel/list-all-simple' })
+  return request.get({ url: '/system/sms-channel/simple-list' })
 }
 
 // 查询短信渠道详情

+ 3 - 3
src/api/system/sms/smsTemplate/index.ts

@@ -1,15 +1,15 @@
 import request from '@/config/axios'
 
 export interface SmsTemplateVO {
-  id: number | null
-  type: number | null
+  id?: number
+  type?: number
   status: number
   code: string
   name: string
   content: string
   remark: string
   apiTemplateId: string
-  channelId: number | null
+  channelId?: number
   channelCode?: string
   params?: string[]
   createTime?: Date

+ 1 - 1
src/api/system/tenantPackage/index.ts

@@ -38,5 +38,5 @@ export const deleteTenantPackage = (id: number) => {
 }
 // 获取租户套餐精简信息列表
 export const getTenantPackageList = () => {
-  return request.get({ url: '/system/tenant-package/get-simple-list' })
+  return request.get({ url: '/system/tenant-package/simple-list' })
 }

+ 1 - 1
src/api/system/user/index.ts

@@ -77,5 +77,5 @@ export const updateUserStatus = (id: number, status: number) => {
 
 // 获取用户精简信息列表
 export const getSimpleUserList = (): Promise<UserVO[]> => {
-  return request.get({ url: '/system/user/list-all-simple' })
+  return request.get({ url: '/system/user/simple-list' })
 }

+ 16 - 28
src/api/system/user/profile.ts

@@ -1,37 +1,25 @@
 import request from '@/config/axios'
 
-export interface ProfileDept {
-  id: number
-  name: string
-}
-export interface ProfileRole {
-  id: number
-  name: string
-}
-export interface ProfilePost {
-  id: number
-  name: string
-}
-export interface SocialUser {
-  id: number
-  type: number
-  openid: string
-  token: string
-  rawTokenInfo: string
-  nickname: string
-  avatar: string
-  rawUserInfo: string
-  code: string
-  state: string
-}
 export interface ProfileVO {
   id: number
   username: string
   nickname: string
-  dept: ProfileDept
-  roles: ProfileRole[]
-  posts: ProfilePost[]
-  socialUsers: SocialUser[]
+  dept: {
+    id: number
+    name: string
+  }
+  roles: {
+    id: number
+    name: string
+  }[]
+  posts: {
+    id: number
+    name: string
+  }[]
+  socialUsers: {
+    type: number
+    openid: string
+  }[]
   email: string
   mobile: string
   sex: number

+ 198 - 0
src/components/AppLinkInput/AppLinkSelectDialog.vue

@@ -0,0 +1,198 @@
+<template>
+  <Dialog v-model="dialogVisible" title="选择链接" width="65%">
+    <div class="h-500px flex gap-8px">
+      <!-- 左侧分组列表 -->
+      <el-scrollbar wrap-class="h-full" ref="groupScrollbar" view-class="flex flex-col">
+        <el-button
+          v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
+          :key="groupIndex"
+          :class="[
+            'm-r-16px m-l-0px! justify-start! w-90px',
+            { active: activeGroup === group.name }
+          ]"
+          ref="groupBtnRefs"
+          :text="activeGroup !== group.name"
+          :type="activeGroup === group.name ? 'primary' : 'default'"
+          @click="handleGroupSelected(group.name)"
+        >
+          {{ group.name }}
+        </el-button>
+      </el-scrollbar>
+      <!-- 右侧链接列表 -->
+      <el-scrollbar class="h-full flex-1" @scroll="handleScroll" ref="linkScrollbar">
+        <div v-for="(group, groupIndex) in APP_LINK_GROUP_LIST" :key="groupIndex">
+          <!-- 分组标题 -->
+          <div class="font-bold" ref="groupTitleRefs">{{ group.name }}</div>
+          <!-- 链接列表 -->
+          <el-tooltip
+            v-for="(appLink, appLinkIndex) in group.links"
+            :key="appLinkIndex"
+            :content="appLink.path"
+            placement="bottom"
+          >
+            <el-button
+              class="m-b-8px m-r-8px m-l-0px!"
+              :type="isSameLink(appLink.path, activeAppLink) ? 'primary' : 'default'"
+              @click="handleAppLinkSelected(appLink)"
+            >
+              {{ appLink.name }}
+            </el-button>
+          </el-tooltip>
+        </div>
+      </el-scrollbar>
+    </div>
+    <!-- 底部对话框操作按钮 -->
+    <template #footer>
+      <el-button type="primary" @click="handleSubmit">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+  <Dialog v-model="detailSelectDialog.visible" title="" width="50%">
+    <el-form class="min-h-200px">
+      <el-form-item
+        label="选择分类"
+        v-if="detailSelectDialog.type === APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST"
+      >
+        <ProductCategorySelect
+          v-model="detailSelectDialog.id"
+          :parent-id="0"
+          @update:model-value="handleProductCategorySelected"
+        />
+      </el-form-item>
+    </el-form>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM } from './data'
+import { ButtonInstance, ScrollbarInstance } from 'element-plus'
+import { split } from 'lodash-es'
+import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
+import { getUrlNumberValue } from '@/utils'
+
+// APP 链接选择弹框
+defineOptions({ name: 'AppLinkSelectDialog' })
+// 选中的分组,默认选中第一个
+const activeGroup = ref(APP_LINK_GROUP_LIST[0].name)
+// 选中的 APP 链接
+const activeAppLink = ref('')
+
+/** 打开弹窗 */
+const dialogVisible = ref(false)
+const open = (link: string) => {
+  activeAppLink.value = link
+  dialogVisible.value = true
+
+  // 滚动到当前的链接
+  const group = APP_LINK_GROUP_LIST.find((group) =>
+    group.links.some((linkItem) => isSameLink(linkItem.path, link))
+  )
+  if (group) {
+    // 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败
+    nextTick(() => handleGroupSelected(group.name))
+  }
+}
+defineExpose({ open })
+
+// 处理 APP 链接选中
+const handleAppLinkSelected = (appLink: any) => {
+  if (!isSameLink(appLink.path, activeAppLink.value)) {
+    activeAppLink.value = appLink.path
+  }
+  switch (appLink.type) {
+    case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST:
+      detailSelectDialog.value.visible = true
+      detailSelectDialog.value.type = appLink.type
+      // 返显
+      detailSelectDialog.value.id =
+        getUrlNumberValue('id', 'http://127.0.0.1' + activeAppLink.value) || undefined
+      break
+    default:
+      break
+  }
+}
+
+// 处理绑定值更新
+const emit = defineEmits<{
+  change: [link: string]
+}>()
+const handleSubmit = () => {
+  dialogVisible.value = false
+  emit('change', activeAppLink.value)
+}
+
+// 分组标题引用列表
+const groupTitleRefs = ref<HTMLInputElement[]>([])
+/**
+ * 处理右侧链接列表滚动
+ * @param scrollTop 滚动条的位置
+ */
+const handleScroll = ({ scrollTop }: { scrollTop: number }) => {
+  const titleEl = groupTitleRefs.value.find((titleEl) => {
+    // 获取标题的位置信息
+    const { offsetHeight, offsetTop } = titleEl
+    // 判断标题是否在可视范围内
+    return scrollTop >= offsetTop && scrollTop < offsetTop + offsetHeight
+  })
+  // 只需处理一次
+  if (titleEl && activeGroup.value !== titleEl.textContent) {
+    activeGroup.value = titleEl.textContent || ''
+    // 同步左侧的滚动条位置
+    scrollToGroupBtn(activeGroup.value)
+  }
+}
+
+// 右侧滚动条
+const linkScrollbar = ref<ScrollbarInstance>()
+// 处理分组选中
+const handleGroupSelected = (group: string) => {
+  activeGroup.value = group
+  const titleRef = groupTitleRefs.value.find((item) => item.textContent === group)
+  if (titleRef) {
+    // 滚动分组标题
+    linkScrollbar.value?.setScrollTop(titleRef.offsetTop)
+  }
+}
+
+// 分组滚动条
+const groupScrollbar = ref<ScrollbarInstance>()
+// 分组引用列表
+const groupBtnRefs = ref<ButtonInstance[]>([])
+// 自动滚动分组按钮,确保分组按钮保持在可视区域内
+const scrollToGroupBtn = (group: string) => {
+  const groupBtn = groupBtnRefs.value
+    .map((btn) => btn['ref'])
+    .find((ref) => ref.textContent === group)
+  if (groupBtn) {
+    groupScrollbar.value?.setScrollTop(groupBtn.offsetTop)
+  }
+}
+
+// 是否为相同的链接(不比较参数,只比较链接)
+const isSameLink = (link1: string, link2: string) => {
+  return split(link1, '?', 1)[0] === split(link2, '?', 1)[0]
+}
+
+// 详情选择对话框
+const detailSelectDialog = ref<{
+  visible: boolean
+  id?: number
+  type?: APP_LINK_TYPE_ENUM
+}>({
+  visible: false,
+  id: undefined,
+  type: undefined
+})
+// 处理详情选择
+const handleProductCategorySelected = (id: number) => {
+  const url = new URL(activeAppLink.value, 'http://127.0.0.1')
+  // 修改 id 参数
+  url.searchParams.set('id', `${id}`)
+  // 排除域名
+  activeAppLink.value = `${url.pathname}${url.search}`
+  // 关闭对话框
+  detailSelectDialog.value.visible = false
+  // 重置 id
+  detailSelectDialog.value.id = undefined
+}
+</script>
+<style lang="scss" scoped></style>

+ 246 - 0
src/components/AppLinkInput/data.ts

@@ -0,0 +1,246 @@
+// APP 链接类型(需要特殊处理,例如商品详情)
+export const enum APP_LINK_TYPE_ENUM {
+  // 拼团活动
+  ACTIVITY_COMBINATION,
+  // 秒杀活动
+  ACTIVITY_SECKILL,
+  // 文章详情
+  ARTICLE_DETAIL,
+  // 优惠券详情
+  COUPON_DETAIL,
+  // 自定义页面详情
+  DIY_PAGE_DETAIL,
+  // 品类列表
+  PRODUCT_CATEGORY_LIST,
+  // 商品列表
+  PRODUCT_LIST,
+  // 商品详情
+  PRODUCT_DETAIL_NORMAL,
+  // 拼团商品详情
+  PRODUCT_DETAIL_COMBINATION,
+  // 积分商品详情
+  PRODUCT_DETAIL_POINT,
+  // 秒杀商品详情
+  PRODUCT_DETAIL_SECKILL
+}
+
+// APP 链接列表(做一下持久化?)
+export const APP_LINK_GROUP_LIST = [
+  {
+    name: '商城',
+    links: [
+      {
+        name: '首页',
+        path: '/pages/index/index'
+      },
+      {
+        name: '商品分类',
+        path: '/pages/index/category',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST
+      },
+      {
+        name: '购物车',
+        path: '/pages/index/cart'
+      },
+      {
+        name: '个人中心',
+        path: '/pages/index/user'
+      },
+      {
+        name: '商品搜索',
+        path: '/pages/index/search'
+      },
+      {
+        name: '自定义页面',
+        path: '/pages/index/page',
+        type: APP_LINK_TYPE_ENUM.DIY_PAGE_DETAIL
+      },
+      {
+        name: '客服',
+        path: '/pages/chat/index'
+      },
+      {
+        name: '系统设置',
+        path: '/pages/public/setting'
+      },
+      {
+        name: '问题反馈',
+        path: '/pages/public/feedback'
+      },
+      {
+        name: '常见问题',
+        path: '/pages/public/faq'
+      }
+    ]
+  },
+  {
+    name: '商品',
+    links: [
+      {
+        name: '商品列表',
+        path: '/pages/goods/list',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_LIST
+      },
+      {
+        name: '商品详情',
+        path: '/pages/goods/index',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_NORMAL
+      },
+      {
+        name: '拼团商品详情',
+        path: '/pages/goods/groupon',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_COMBINATION
+      },
+      {
+        name: '秒杀商品详情',
+        path: '/pages/goods/seckill',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL
+      },
+      {
+        name: '积分商品详情',
+        path: '/pages/goods/score',
+        type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_POINT
+      }
+    ]
+  },
+  {
+    name: '营销活动',
+    links: [
+      {
+        name: '拼团订单',
+        path: '/pages/activity/groupon/order'
+      },
+      {
+        name: '营销商品',
+        path: '/pages/activity/index'
+      },
+      {
+        name: '拼团活动',
+        path: '/pages/activity/groupon/list',
+        type: APP_LINK_TYPE_ENUM.ACTIVITY_COMBINATION
+      },
+      {
+        name: '秒杀活动',
+        path: '/pages/activity/seckill/list',
+        type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL
+      },
+      {
+        name: '签到中心',
+        path: '/pages/app/sign'
+      },
+      {
+        name: '积分商城',
+        path: '/pages/app/score-shop'
+      },
+      {
+        name: '优惠券中心',
+        path: '/pages/coupon/list'
+      },
+      {
+        name: '优惠券详情',
+        path: '/pages/coupon/detail',
+        type: APP_LINK_TYPE_ENUM.COUPON_DETAIL
+      },
+      {
+        name: '文章详情',
+        path: '/pages/public/richtext',
+        type: APP_LINK_TYPE_ENUM.ARTICLE_DETAIL
+      }
+    ]
+  },
+  {
+    name: '分销商城',
+    links: [
+      {
+        name: '分销中心',
+        path: '/pages/commission/index'
+      },
+      {
+        name: '申请分销商',
+        path: '/pages/commission/apply'
+      },
+      {
+        name: '推广商品',
+        path: '/pages/commission/goods'
+      },
+      {
+        name: '分销订单',
+        path: '/pages/commission/order'
+      },
+      {
+        name: '分享记录',
+        path: '/pages/commission/share-log'
+      },
+      {
+        name: '我的团队',
+        path: '/pages/commission/team'
+      }
+    ]
+  },
+  {
+    name: '支付',
+    links: [
+      {
+        name: '充值余额',
+        path: '/pages/pay/recharge'
+      },
+      {
+        name: '充值记录',
+        path: '/pages/pay/recharge-log'
+      },
+      {
+        name: '申请提现',
+        path: '/pages/pay/withdraw'
+      },
+      {
+        name: '提现记录',
+        path: '/pages/pay/withdraw-log'
+      }
+    ]
+  },
+  {
+    name: '用户中心',
+    links: [
+      {
+        name: '用户信息',
+        path: '/pages/user/info'
+      },
+      {
+        name: '用户订单',
+        path: '/pages/order/list'
+      },
+      {
+        name: '售后订单',
+        path: '/pages/order/aftersale/list'
+      },
+      {
+        name: '商品收藏',
+        path: '/pages/user/goods-collect'
+      },
+      {
+        name: '浏览记录',
+        path: '/pages/user/goods-log'
+      },
+      {
+        name: '地址管理',
+        path: '/pages/user/address/list'
+      },
+      {
+        name: '发票管理',
+        path: '/pages/user/invoice/list'
+      },
+      {
+        name: '用户佣金',
+        path: '/pages/user/wallet/commission'
+      },
+      {
+        name: '用户余额',
+        path: '/pages/user/wallet/money'
+      },
+      {
+        name: '用户积分',
+        path: '/pages/user/wallet/score'
+      }
+    ]
+  }
+]

+ 43 - 0
src/components/AppLinkInput/index.vue

@@ -0,0 +1,43 @@
+<template>
+  <el-input v-model="appLink" placeholder="输入或选择链接">
+    <template #append>
+      <el-button @click="handleOpenDialog">选择</el-button>
+    </template>
+  </el-input>
+  <AppLinkSelectDialog ref="dialogRef" @change="handleLinkSelected" />
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+
+// APP 链接输入框
+defineOptions({ name: 'AppLinkInput' })
+// 定义属性
+const props = defineProps({
+  // 当前选中的链接
+  modelValue: propTypes.string.def('')
+})
+// 当前的链接
+const appLink = ref('')
+// 选择对话框
+const dialogRef = ref()
+// 处理打开对话框
+const handleOpenDialog = () => dialogRef.value?.open(appLink.value)
+// 处理 APP 链接选中
+const handleLinkSelected = (link: string) => (appLink.value = link)
+
+// getter
+watch(
+  () => props.modelValue,
+  () => (appLink.value = props.modelValue),
+  { immediate: true }
+)
+
+// setter
+const emit = defineEmits<{
+  'update:modelValue': [link: string]
+}>()
+watch(
+  () => appLink,
+  () => emit('update:modelValue', appLink.value)
+)
+</script>

+ 1 - 1
src/components/DiyEditor/components/ComponentContainerProperty.vue

@@ -1,6 +1,6 @@
 <template>
   <el-tabs stretch>
-    <el-tab-pane label="内容">
+    <el-tab-pane label="内容" v-if="$slots.default">
       <slot></slot>
     </el-tab-pane>
     <el-tab-pane label="样式" lazy>

+ 1 - 1
src/components/DiyEditor/components/mobile/Carousel/property.vue

@@ -103,7 +103,7 @@
                   </el-form-item>
                 </template>
                 <el-form-item label="链接" class="m-b-8px!" label-width="50px">
-                  <el-input placeholder="链接" v-model="element.url" />
+                  <AppLinkInput v-model="element.url" />
                 </el-form-item>
               </div>
             </template>

+ 1 - 1
src/components/DiyEditor/components/mobile/ImageBar/property.vue

@@ -13,7 +13,7 @@
         </UploadImg>
       </el-form-item>
       <el-form-item label="链接" prop="url">
-        <el-input placeholder="链接" v-model="formData.url" />
+        <AppLinkInput v-model="formData.url" />
       </el-form-item>
     </el-form>
   </ComponentContainerProperty>

+ 1 - 1
src/components/DiyEditor/components/mobile/MagicCube/property.vue

@@ -17,7 +17,7 @@
             <UploadImg v-model="hotArea.imgUrl" height="80px" width="80px" />
           </el-form-item>
           <el-form-item label="链接" :prop="`list[${index}].url`">
-            <el-input v-model="hotArea.url" placeholder="请输入链接" />
+            <AppLinkInput v-model="hotArea.url" />
           </el-form-item>
         </template>
       </template>

+ 1 - 1
src/components/DiyEditor/components/mobile/MenuGrid/property.vue

@@ -38,7 +38,7 @@
                 <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
               </el-form-item>
               <el-form-item label="链接" prop="url">
-                <el-input v-model="element.url" />
+                <AppLinkInput v-model="element.url" />
               </el-form-item>
               <el-form-item label="显示角标" prop="badge.show">
                 <el-switch v-model="element.badge.show" />

+ 1 - 1
src/components/DiyEditor/components/mobile/MenuList/property.vue

@@ -31,7 +31,7 @@
                 <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
               </el-form-item>
               <el-form-item label="链接" prop="url">
-                <el-input v-model="element.url" />
+                <AppLinkInput v-model="element.url" />
               </el-form-item>
             </div>
           </template>

+ 1 - 1
src/components/DiyEditor/components/mobile/MenuSwiper/property.vue

@@ -48,7 +48,7 @@
                 <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
               </el-form-item>
               <el-form-item label="链接" prop="url">
-                <el-input v-model="element.url" />
+                <AppLinkInput v-model="element.url" />
               </el-form-item>
               <el-form-item label="显示角标" prop="badge.show">
                 <el-switch v-model="element.badge.show" />

+ 1 - 1
src/components/DiyEditor/components/mobile/NoticeBar/property.vue

@@ -35,7 +35,7 @@
             </div>
             <div class="w-full flex flex-col gap-8px">
               <el-input v-model="element.text" placeholder="请输入公告" />
-              <el-input v-model="element.url" placeholder="请输入链接" />
+              <AppLinkInput v-model="element.url" />
             </div>
           </div>
         </template>

+ 1 - 1
src/components/DiyEditor/components/mobile/ProductList/config.ts

@@ -1,6 +1,6 @@
 import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
 
-/** 商品卡片属性 */
+/** 商品属性 */
 export interface ProductListProperty {
   // 布局类型:双列 | 三列 | 水平滑动
   layoutType: 'twoCol' | 'threeCol' | 'horizSwiper'

+ 1 - 1
src/components/DiyEditor/components/mobile/ProductList/index.vue

@@ -66,7 +66,7 @@
 import { ProductListProperty } from './config'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 
-/** 商品卡片 */
+/** 商品 */
 defineOptions({ name: 'ProductList' })
 // 定义属性
 const props = defineProps<{ property: ProductListProperty }>()

+ 1 - 1
src/components/DiyEditor/components/mobile/ProductList/property.vue

@@ -88,7 +88,7 @@ import { ProductListProperty } from './config'
 import { usePropertyForm } from '@/components/DiyEditor/util'
 import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
 
-// 商品卡片属性面板
+// 商品属性面板
 defineOptions({ name: 'ProductListProperty' })
 
 const props = defineProps<{ modelValue: ProductListProperty }>()

+ 25 - 0
src/components/DiyEditor/components/mobile/PromotionArticle/config.ts

@@ -0,0 +1,25 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 营销文章属性 */
+export interface PromotionArticleProperty {
+  // 文章编号
+  id: number
+  // 组件样式
+  style: ComponentStyle
+}
+
+// 定义组件
+export const component = {
+  id: 'PromotionArticle',
+  name: '营销文章',
+  icon: 'ph:article-medium',
+  property: {
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<PromotionArticleProperty>

+ 27 - 0
src/components/DiyEditor/components/mobile/PromotionArticle/index.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="min-h-30px" v-html="article.content"></div>
+</template>
+<script setup lang="ts">
+import { PromotionArticleProperty } from './config'
+import * as ArticleApi from '@/api/mall/promotion/article/index'
+
+/** 营销文章 */
+defineOptions({ name: 'PromotionArticle' })
+// 定义属性
+const props = defineProps<{ property: PromotionArticleProperty }>()
+// 商品列表
+const article = ref<ArticleApi.ArticleVO[]>({})
+watch(
+  () => props.property.id,
+  async () => {
+    if (props.property.id) {
+      article.value = await ArticleApi.getArticle(props.property.id)
+    }
+  },
+  {
+    immediate: true
+  }
+)
+</script>
+
+<style scoped lang="scss"></style>

+ 56 - 0
src/components/DiyEditor/components/mobile/PromotionArticle/property.vue

@@ -0,0 +1,56 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style">
+    <el-form label-width="40px" :model="formData">
+      <el-form-item label="文章" prop="id">
+        <el-select
+          v-model="formData.id"
+          placeholder="请选择文章"
+          class="w-full"
+          filterable
+          remote
+          :remote-method="queryArticleList"
+          :loading="loading"
+        >
+          <el-option
+            v-for="article in articles"
+            :key="article.id"
+            :label="article.title"
+            :value="article.id"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+  </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { PromotionArticleProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import * as ArticleApi from '@/api/mall/promotion/article/index'
+
+// 营销文章属性面板
+defineOptions({ name: 'PromotionArticleProperty' })
+
+const props = defineProps<{ modelValue: PromotionArticleProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+// 文章列表
+const articles = ref<ArticleApi.ArticleVO>([])
+
+// 加载中
+const loading = ref(false)
+// 查询文章列表
+const queryArticleList = async (title?: string) => {
+  loading.value = true
+  const { list } = await ArticleApi.getArticlePage({ title, pageSize: 10 })
+  articles.value = list
+  loading.value = false
+}
+
+// 初始化
+onMounted(() => {
+  queryArticleList()
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 64 - 0
src/components/DiyEditor/components/mobile/PromotionCombination/config.ts

@@ -0,0 +1,64 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 拼团属性 */
+export interface PromotionCombinationProperty {
+  // 布局类型:单列 | 三列
+  layoutType: 'oneCol' | 'threeCol'
+  // 商品字段
+  fields: {
+    // 商品名称
+    name: PromotionCombinationFieldProperty
+    // 商品价格
+    price: PromotionCombinationFieldProperty
+  }
+  // 角标
+  badge: {
+    // 是否显示
+    show: boolean
+    // 角标图片
+    imgUrl: string
+  }
+  // 上圆角
+  borderRadiusTop: number
+  // 下圆角
+  borderRadiusBottom: number
+  // 间距
+  space: number
+  // 拼团活动编号
+  activityId: number
+  // 组件样式
+  style: ComponentStyle
+}
+// 商品字段
+export interface PromotionCombinationFieldProperty {
+  // 是否显示
+  show: boolean
+  // 颜色
+  color: string
+}
+
+// 定义组件
+export const component = {
+  id: 'PromotionCombination',
+  name: '拼团',
+  icon: 'mdi:account-group',
+  property: {
+    activityId: undefined,
+    layoutType: 'oneCol',
+    fields: {
+      name: { show: true, color: '#000' },
+      price: { show: true, color: '#ff3000' }
+    },
+    badge: { show: false, imgUrl: '' },
+    borderRadiusTop: 8,
+    borderRadiusBottom: 8,
+    space: 8,
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<PromotionCombinationProperty>

+ 125 - 0
src/components/DiyEditor/components/mobile/PromotionCombination/index.vue

@@ -0,0 +1,125 @@
+<template>
+  <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
+    <!-- 商品网格 -->
+    <div
+      class="grid overflow-x-auto"
+      :style="{
+        gridGap: `${property.space}px`,
+        gridTemplateColumns,
+        width: scrollbarWidth
+      }"
+    >
+      <!-- 商品 -->
+      <div
+        class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+        :style="{
+          borderTopLeftRadius: `${property.borderRadiusTop}px`,
+          borderTopRightRadius: `${property.borderRadiusTop}px`,
+          borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
+          borderBottomRightRadius: `${property.borderRadiusBottom}px`
+        }"
+        v-for="(spu, index) in spuList"
+        :key="index"
+      >
+        <!-- 角标 -->
+        <div
+          v-if="property.badge.show"
+          class="absolute left-0 top-0 z-1 items-center justify-center"
+        >
+          <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
+        </div>
+        <!-- 商品封面图 -->
+        <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
+        <div
+          :class="[
+            'flex flex-col gap-8px p-8px box-border',
+            {
+              'w-[calc(100%-64px)]': columns === 2,
+              'w-full': columns === 3
+            }
+          ]"
+        >
+          <!-- 商品名称 -->
+          <div
+            v-if="property.fields.name.show"
+            class="truncate text-12px"
+            :style="{ color: property.fields.name.color }"
+          >
+            {{ spu.name }}
+          </div>
+          <div>
+            <!-- 商品价格 -->
+            <span
+              v-if="property.fields.price.show"
+              class="text-12px"
+              :style="{ color: property.fields.price.color }"
+            >
+              ¥{{ spu.price }}
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </el-scrollbar>
+</template>
+<script setup lang="ts">
+import { PromotionCombinationProperty } from './config'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+
+/** 拼团 */
+defineOptions({ name: 'PromotionCombination' })
+// 定义属性
+const props = defineProps<{ property: PromotionCombinationProperty }>()
+// 商品列表
+const spuList = ref<ProductSpuApi.Spu[]>([])
+watch(
+  () => props.property.activityId,
+  async () => {
+    if (!props.property.activityId) return
+    const activity = await CombinationActivityApi.getCombinationActivity(props.property.activityId)
+    if (!activity?.spuId) return
+    spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+// 手机宽度
+const phoneWidth = ref(375)
+// 容器
+const containerRef = ref()
+// 商品的列数
+const columns = ref(2)
+// 滚动条宽度
+const scrollbarWidth = ref('100%')
+// 商品图大小
+const imageSize = ref('0')
+// 商品网络列数
+const gridTemplateColumns = ref('')
+// 计算布局参数
+watch(
+  () => [props.property, phoneWidth, spuList.value.length],
+  () => {
+    // 计算列数
+    columns.value = props.property.layoutType === 'oneCol' ? 1 : 3
+    // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
+    const productWidth =
+      (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
+    // 商品图布局:2列时,左右布局 3列时,上下布局
+    imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`
+    // 指定列数
+    gridTemplateColumns.value = `repeat(${columns.value}, auto)`
+    // 不滚动
+    scrollbarWidth.value = '100%'
+  },
+  { immediate: true, deep: true }
+)
+onMounted(() => {
+  // 提取手机宽度
+  phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 112 - 0
src/components/DiyEditor/components/mobile/PromotionCombination/property.vue

@@ -0,0 +1,112 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style">
+    <el-form label-width="80px" :model="formData">
+      <el-card header="拼团活动" class="property-group" shadow="never">
+        <el-form-item label="拼团活动" prop="activityId">
+          <el-select v-model="formData.activityId">
+            <el-option
+              v-for="activity in activityList"
+              :key="activity.id"
+              :label="activity.name"
+              :value="activity.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-card>
+      <el-card header="商品样式" class="property-group" shadow="never">
+        <el-form-item label="布局" prop="type">
+          <el-radio-group v-model="formData.layoutType">
+            <el-tooltip class="item" content="单列" placement="bottom">
+              <el-radio-button label="oneCol">
+                <Icon icon="fluent:text-column-one-24-filled" />
+              </el-radio-button>
+            </el-tooltip>
+            <el-tooltip class="item" content="三列" placement="bottom">
+              <el-radio-button label="threeCol">
+                <Icon icon="fluent:text-column-three-24-filled" />
+              </el-radio-button>
+            </el-tooltip>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="商品名称" prop="fields.name.show">
+          <div class="flex gap-8px">
+            <ColorInput v-model="formData.fields.name.color" />
+            <el-checkbox v-model="formData.fields.name.show" />
+          </div>
+        </el-form-item>
+        <el-form-item label="商品价格" prop="fields.price.show">
+          <div class="flex gap-8px">
+            <ColorInput v-model="formData.fields.price.color" />
+            <el-checkbox v-model="formData.fields.price.show" />
+          </div>
+        </el-form-item>
+      </el-card>
+      <el-card header="角标" class="property-group" shadow="never">
+        <el-form-item label="角标" prop="badge.show">
+          <el-switch v-model="formData.badge.show" />
+        </el-form-item>
+        <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
+          <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
+            <template #tip> 建议尺寸:36 * 22 </template>
+          </UploadImg>
+        </el-form-item>
+      </el-card>
+      <el-card header="商品样式" class="property-group" shadow="never">
+        <el-form-item label="上圆角" prop="borderRadiusTop">
+          <el-slider
+            v-model="formData.borderRadiusTop"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+        <el-form-item label="下圆角" prop="borderRadiusBottom">
+          <el-slider
+            v-model="formData.borderRadiusBottom"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+        <el-form-item label="间隔" prop="space">
+          <el-slider
+            v-model="formData.space"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+      </el-card>
+    </el-form>
+  </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { PromotionCombinationProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+import { CommonStatusEnum } from '@/utils/constants'
+
+// 拼团属性面板
+defineOptions({ name: 'PromotionCombinationProperty' })
+
+const props = defineProps<{ modelValue: PromotionCombinationProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+// 活动列表
+const activityList = ref<CombinationActivityApi.CombinationActivityVO>([])
+onMounted(async () => {
+  const { list } = await CombinationActivityApi.getCombinationActivityPage({
+    status: CommonStatusEnum.ENABLE
+  })
+  activityList.value = list
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 64 - 0
src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts

@@ -0,0 +1,64 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 秒杀属性 */
+export interface PromotionSeckillProperty {
+  // 布局类型:单列 | 三列
+  layoutType: 'oneCol' | 'threeCol'
+  // 商品字段
+  fields: {
+    // 商品名称
+    name: PromotionSeckillFieldProperty
+    // 商品价格
+    price: PromotionSeckillFieldProperty
+  }
+  // 角标
+  badge: {
+    // 是否显示
+    show: boolean
+    // 角标图片
+    imgUrl: string
+  }
+  // 上圆角
+  borderRadiusTop: number
+  // 下圆角
+  borderRadiusBottom: number
+  // 间距
+  space: number
+  // 秒杀活动编号
+  activityId: number
+  // 组件样式
+  style: ComponentStyle
+}
+// 商品字段
+export interface PromotionSeckillFieldProperty {
+  // 是否显示
+  show: boolean
+  // 颜色
+  color: string
+}
+
+// 定义组件
+export const component = {
+  id: 'PromotionSeckill',
+  name: '秒杀',
+  icon: 'mdi:calendar-time',
+  property: {
+    activityId: undefined,
+    layoutType: 'oneCol',
+    fields: {
+      name: { show: true, color: '#000' },
+      price: { show: true, color: '#ff3000' }
+    },
+    badge: { show: false, imgUrl: '' },
+    borderRadiusTop: 8,
+    borderRadiusBottom: 8,
+    space: 8,
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<PromotionSeckillProperty>

+ 125 - 0
src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue

@@ -0,0 +1,125 @@
+<template>
+  <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
+    <!-- 商品网格 -->
+    <div
+      class="grid overflow-x-auto"
+      :style="{
+        gridGap: `${property.space}px`,
+        gridTemplateColumns,
+        width: scrollbarWidth
+      }"
+    >
+      <!-- 商品 -->
+      <div
+        class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+        :style="{
+          borderTopLeftRadius: `${property.borderRadiusTop}px`,
+          borderTopRightRadius: `${property.borderRadiusTop}px`,
+          borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
+          borderBottomRightRadius: `${property.borderRadiusBottom}px`
+        }"
+        v-for="(spu, index) in spuList"
+        :key="index"
+      >
+        <!-- 角标 -->
+        <div
+          v-if="property.badge.show"
+          class="absolute left-0 top-0 z-1 items-center justify-center"
+        >
+          <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
+        </div>
+        <!-- 商品封面图 -->
+        <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
+        <div
+          :class="[
+            'flex flex-col gap-8px p-8px box-border',
+            {
+              'w-[calc(100%-64px)]': columns === 2,
+              'w-full': columns === 3
+            }
+          ]"
+        >
+          <!-- 商品名称 -->
+          <div
+            v-if="property.fields.name.show"
+            class="truncate text-12px"
+            :style="{ color: property.fields.name.color }"
+          >
+            {{ spu.name }}
+          </div>
+          <div>
+            <!-- 商品价格 -->
+            <span
+              v-if="property.fields.price.show"
+              class="text-12px"
+              :style="{ color: property.fields.price.color }"
+            >
+              ¥{{ spu.price }}
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </el-scrollbar>
+</template>
+<script setup lang="ts">
+import { PromotionSeckillProperty } from './config'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+
+/** 秒杀 */
+defineOptions({ name: 'PromotionSeckill' })
+// 定义属性
+const props = defineProps<{ property: PromotionSeckillProperty }>()
+// 商品列表
+const spuList = ref<ProductSpuApi.Spu[]>([])
+watch(
+  () => props.property.activityId,
+  async () => {
+    if (!props.property.activityId) return
+    const activity = await SeckillActivityApi.getSeckillActivity(props.property.activityId)
+    if (!activity?.spuId) return
+    spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+// 手机宽度
+const phoneWidth = ref(375)
+// 容器
+const containerRef = ref()
+// 商品的列数
+const columns = ref(2)
+// 滚动条宽度
+const scrollbarWidth = ref('100%')
+// 商品图大小
+const imageSize = ref('0')
+// 商品网络列数
+const gridTemplateColumns = ref('')
+// 计算布局参数
+watch(
+  () => [props.property, phoneWidth, spuList.value.length],
+  () => {
+    // 计算列数
+    columns.value = props.property.layoutType === 'oneCol' ? 1 : 3
+    // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
+    const productWidth =
+      (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
+    // 商品图布局:2列时,左右布局 3列时,上下布局
+    imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`
+    // 指定列数
+    gridTemplateColumns.value = `repeat(${columns.value}, auto)`
+    // 不滚动
+    scrollbarWidth.value = '100%'
+  },
+  { immediate: true, deep: true }
+)
+onMounted(() => {
+  // 提取手机宽度
+  phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 112 - 0
src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue

@@ -0,0 +1,112 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style">
+    <el-form label-width="80px" :model="formData">
+      <el-card header="秒杀活动" class="property-group" shadow="never">
+        <el-form-item label="秒杀活动" prop="activityId">
+          <el-select v-model="formData.activityId">
+            <el-option
+              v-for="activity in activityList"
+              :key="activity.id"
+              :label="activity.name"
+              :value="activity.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-card>
+      <el-card header="商品样式" class="property-group" shadow="never">
+        <el-form-item label="布局" prop="type">
+          <el-radio-group v-model="formData.layoutType">
+            <el-tooltip class="item" content="单列" placement="bottom">
+              <el-radio-button label="oneCol">
+                <Icon icon="fluent:text-column-one-24-filled" />
+              </el-radio-button>
+            </el-tooltip>
+            <el-tooltip class="item" content="三列" placement="bottom">
+              <el-radio-button label="threeCol">
+                <Icon icon="fluent:text-column-three-24-filled" />
+              </el-radio-button>
+            </el-tooltip>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="商品名称" prop="fields.name.show">
+          <div class="flex gap-8px">
+            <ColorInput v-model="formData.fields.name.color" />
+            <el-checkbox v-model="formData.fields.name.show" />
+          </div>
+        </el-form-item>
+        <el-form-item label="商品价格" prop="fields.price.show">
+          <div class="flex gap-8px">
+            <ColorInput v-model="formData.fields.price.color" />
+            <el-checkbox v-model="formData.fields.price.show" />
+          </div>
+        </el-form-item>
+      </el-card>
+      <el-card header="角标" class="property-group" shadow="never">
+        <el-form-item label="角标" prop="badge.show">
+          <el-switch v-model="formData.badge.show" />
+        </el-form-item>
+        <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
+          <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
+            <template #tip> 建议尺寸:36 * 22 </template>
+          </UploadImg>
+        </el-form-item>
+      </el-card>
+      <el-card header="商品样式" class="property-group" shadow="never">
+        <el-form-item label="上圆角" prop="borderRadiusTop">
+          <el-slider
+            v-model="formData.borderRadiusTop"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+        <el-form-item label="下圆角" prop="borderRadiusBottom">
+          <el-slider
+            v-model="formData.borderRadiusBottom"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+        <el-form-item label="间隔" prop="space">
+          <el-slider
+            v-model="formData.space"
+            :max="100"
+            :min="0"
+            show-input
+            input-size="small"
+            :show-input-controls="false"
+          />
+        </el-form-item>
+      </el-card>
+    </el-form>
+  </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { PromotionSeckillProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import { CommonStatusEnum } from '@/utils/constants'
+
+// 秒杀属性面板
+defineOptions({ name: 'PromotionSeckillProperty' })
+
+const props = defineProps<{ modelValue: PromotionSeckillProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+// 活动列表
+const activityList = ref<SeckillActivityApi.SeckillActivityVO>([])
+onMounted(async () => {
+  const { list } = await SeckillActivityApi.getSeckillActivityPage({
+    status: CommonStatusEnum.ENABLE
+  })
+  activityList.value = list
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 1 - 1
src/components/DiyEditor/components/mobile/TabBar/property.vue

@@ -88,7 +88,7 @@
                 <el-input v-model="element.text" placeholder="请输入文字" />
               </el-form-item>
               <el-form-item prop="url" label-width="0" class="m-b-0!">
-                <el-input v-model="element.url" placeholder="请选择链接" />
+                <AppLinkInput v-model="element.url" />
               </el-form-item>
             </div>
           </div>

+ 1 - 1
src/components/DiyEditor/components/mobile/TitleBar/property.vue

@@ -92,7 +92,7 @@
           <el-input v-model="formData.more.text" />
         </el-form-item>
         <el-form-item label="跳转链接" prop="more.url">
-          <el-input v-model="formData.more.url" placeholder="请输入跳转链接" />
+          <AppLinkInput v-model="formData.more.url" />
         </el-form-item>
       </template>
     </el-form>

+ 21 - 0
src/components/DiyEditor/components/mobile/UserCard/config.ts

@@ -0,0 +1,21 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 用户卡片属性 */
+export interface UserCardProperty {
+  // 组件样式
+  style: ComponentStyle
+}
+
+// 定义组件
+export const component = {
+  id: 'UserCard',
+  name: '用户卡片',
+  icon: 'mdi:user-card-details',
+  property: {
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<UserCardProperty>

+ 29 - 0
src/components/DiyEditor/components/mobile/UserCard/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="flex flex-col">
+    <div class="flex items-center justify-between p-x-18px p-y-24px">
+      <div class="flex flex-1 items-center gap-16px">
+        <el-avatar :size="60">
+          <Icon icon="ep:avatar" :size="60" />
+        </el-avatar>
+        <span class="text-18px font-bold">芋道源码</span>
+      </div>
+      <Icon icon="tdesign:qrcode" :size="20" />
+    </div>
+    <div
+      class="flex items-center justify-between justify-between bg-white p-x-20px p-y-8px text-12px"
+    >
+      <span class="color-#ff690d">点击绑定手机号</span>
+      <span class="rounded-26px bg-#ff6100 p-x-8px p-y-5px color-white">去绑定</span>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { UserCardProperty } from './config'
+
+/** 用户卡片 */
+defineOptions({ name: 'UserCard' })
+// 定义属性
+defineProps<{ property: UserCardProperty }>()
+</script>
+
+<style scoped lang="scss"></style>

+ 17 - 0
src/components/DiyEditor/components/mobile/UserCard/property.vue

@@ -0,0 +1,17 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserCardProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+
+// 用户卡片属性面板
+defineOptions({ name: 'UserCardProperty' })
+
+const props = defineProps<{ modelValue: UserCardProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+</script>
+
+<style scoped lang="scss"></style>

+ 23 - 0
src/components/DiyEditor/components/mobile/UserCoupon/config.ts

@@ -0,0 +1,23 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 用户卡券属性 */
+export interface UserCouponProperty {
+  // 组件样式
+  style: ComponentStyle
+}
+
+// 定义组件
+export const component = {
+  id: 'UserCoupon',
+  name: '用户卡券',
+  icon: 'ep:ticket',
+  property: {
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<UserCouponProperty>

+ 15 - 0
src/components/DiyEditor/components/mobile/UserCoupon/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <el-image
+    src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/couponCardStyle.png"
+  />
+</template>
+<script setup lang="ts">
+import { UserCouponProperty } from './config'
+
+/** 用户卡券 */
+defineOptions({ name: 'UserCoupon' })
+// 定义属性
+defineProps<{ property: UserCouponProperty }>()
+</script>
+
+<style scoped lang="scss"></style>

+ 17 - 0
src/components/DiyEditor/components/mobile/UserCoupon/property.vue

@@ -0,0 +1,17 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserCouponProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+
+// 用户卡券属性面板
+defineOptions({ name: 'UserCouponProperty' })
+
+const props = defineProps<{ modelValue: UserCouponProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+</script>
+
+<style scoped lang="scss"></style>

+ 23 - 0
src/components/DiyEditor/components/mobile/UserOrder/config.ts

@@ -0,0 +1,23 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 用户订单属性 */
+export interface UserOrderProperty {
+  // 组件样式
+  style: ComponentStyle
+}
+
+// 定义组件
+export const component = {
+  id: 'UserOrder',
+  name: '用户订单',
+  icon: 'ep:list',
+  property: {
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<UserOrderProperty>

+ 13 - 0
src/components/DiyEditor/components/mobile/UserOrder/index.vue

@@ -0,0 +1,13 @@
+<template>
+  <el-image src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/orderCardStyle.png" />
+</template>
+<script setup lang="ts">
+import { UserOrderProperty } from './config'
+
+/** 用户订单 */
+defineOptions({ name: 'UserOrder' })
+// 定义属性
+defineProps<{ property: UserOrderProperty }>()
+</script>
+
+<style scoped lang="scss"></style>

+ 17 - 0
src/components/DiyEditor/components/mobile/UserOrder/property.vue

@@ -0,0 +1,17 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserOrderProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+
+// 用户订单属性面板
+defineOptions({ name: 'UserOrderProperty' })
+
+const props = defineProps<{ modelValue: UserOrderProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+</script>
+
+<style scoped lang="scss"></style>

+ 23 - 0
src/components/DiyEditor/components/mobile/UserWallet/config.ts

@@ -0,0 +1,23 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 用户资产属性 */
+export interface UserWalletProperty {
+  // 组件样式
+  style: ComponentStyle
+}
+
+// 定义组件
+export const component = {
+  id: 'UserWallet',
+  name: '用户资产',
+  icon: 'ep:wallet-filled',
+  property: {
+    style: {
+      bgType: 'color',
+      bgColor: '',
+      marginLeft: 8,
+      marginRight: 8,
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<UserWalletProperty>

+ 15 - 0
src/components/DiyEditor/components/mobile/UserWallet/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <el-image
+    src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/walletCardStyle.png"
+  />
+</template>
+<script setup lang="ts">
+import { UserWalletProperty } from './config'
+
+/** 用户资产 */
+defineOptions({ name: 'UserWallet' })
+// 定义属性
+defineProps<{ property: UserWalletProperty }>()
+</script>
+
+<style scoped lang="scss"></style>

+ 17 - 0
src/components/DiyEditor/components/mobile/UserWallet/property.vue

@@ -0,0 +1,17 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserWalletProperty } from './config'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+
+// 用户资产属性面板
+defineOptions({ name: 'UserWalletProperty' })
+
+const props = defineProps<{ modelValue: UserWalletProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+</script>
+
+<style scoped lang="scss"></style>

+ 8 - 2
src/components/DiyEditor/util.ts

@@ -109,13 +109,19 @@ export const PAGE_LIBS = [
   },
   { name: '商品组件', extended: true, components: ['ProductCard', 'ProductList'] },
   {
-    name: '会员组件',
+    name: '用户组件',
     extended: true,
     components: ['UserCard', 'UserOrder', 'UserWallet', 'UserCoupon']
   },
   {
     name: '营销组件',
     extended: true,
-    components: ['CombinationCard', 'SeckillCard', 'PointCard', 'CouponCard']
+    components: [
+      'PromotionCombination',
+      'PromotionSeckill',
+      'PromotionPoint',
+      'CouponCard',
+      'PromotionArticle'
+    ]
   }
 ] as DiyComponentLibrary[]

+ 36 - 1
src/components/RouterSearch/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <ElDialog v-model="showSearch" :show-close="false" title="菜单搜索">
+  <ElDialog v-if="isModal" v-model="showSearch" :show-close="false" title="菜单搜索">
     <el-select
       filterable
       :reserve-keyword="false"
@@ -17,11 +17,39 @@
       />
     </el-select>
   </ElDialog>
+  <div v-else class="custom-hover" @click.stop="showTopSearch = !showTopSearch">
+    <Icon icon="ep:search" />
+    <el-select
+      filterable
+      :reserve-keyword="false"
+      remote
+      placeholder="请输入菜单内容"
+      :remote-method="remoteMethod"
+      class="overflow-hidden transition-all-600"
+      :class="showTopSearch ? 'w-220px ml2' : 'w-0'"
+      @change="handleChange"
+    >
+      <el-option
+        v-for="item in options"
+        :key="item.value"
+        :label="item.label"
+        :value="item.value"
+      />
+    </el-select>
+  </div>
 </template>
 
 <script lang="ts" setup>
+defineProps({
+  isModal: {
+    type: Boolean,
+    default: true
+  }
+})
+
 const router = useRouter() // 路由对象
 const showSearch = ref(false) // 是否显示弹框
+const showTopSearch = ref(false) // 是否显示顶部搜索框
 const value: Ref = ref('') // 用户输入的值
 
 const routers = router.getRoutes() // 路由对象
@@ -50,14 +78,21 @@ function remoteMethod(data) {
 
 function handleChange(path) {
   router.push({ path })
+  hiddenTopSearch()
+}
+
+function hiddenTopSearch() {
+  showTopSearch.value = false
 }
 
 onMounted(() => {
   window.addEventListener('keydown', listenKey)
+  window.addEventListener('click', hiddenTopSearch)
 })
 
 onUnmounted(() => {
   window.removeEventListener('keydown', listenKey)
+  window.removeEventListener('click', hiddenTopSearch)
 })
 
 // 监听 ctrl + k

+ 17 - 16
src/components/UploadFile/src/UploadImg.vue

@@ -1,17 +1,17 @@
 <template>
   <div class="upload-box">
     <el-upload
-      :action="updateUrl"
       :id="uuid"
+      :accept="fileType.join(',')"
+      :action="updateUrl"
+      :before-upload="beforeUpload"
       :class="['upload', drag ? 'no-border' : '']"
-      :multiple="false"
-      :show-file-list="false"
+      :drag="drag"
       :headers="uploadHeaders"
-      :before-upload="beforeUpload"
-      :on-success="uploadSuccess"
+      :multiple="false"
       :on-error="uploadError"
-      :drag="drag"
-      :accept="fileType.join(',')"
+      :on-success="uploadSuccess"
+      :show-file-list="false"
     >
       <template v-if="modelValue">
         <img :src="modelValue" class="upload-image" />
@@ -20,11 +20,11 @@
             <Icon icon="ep:edit" />
             <span v-if="showBtnText">{{ t('action.edit') }}</span>
           </div>
-          <div class="handle-icon" @click="imgViewVisible = true">
+          <div class="handle-icon" @click="imagePreview(modelValue)">
             <Icon icon="ep:zoom-in" />
             <span v-if="showBtnText">{{ t('action.detail') }}</span>
           </div>
-          <div class="handle-icon" @click="deleteImg" v-if="showDelete">
+          <div v-if="showDelete" class="handle-icon" @click="deleteImg">
             <Icon icon="ep:delete" />
             <span v-if="showBtnText">{{ t('action.del') }}</span>
           </div>
@@ -42,11 +42,6 @@
     <div class="el-upload__tip">
       <slot name="tip"></slot>
     </div>
-    <el-image-viewer
-      v-if="imgViewVisible"
-      @close="imgViewVisible = false"
-      :url-list="[modelValue]"
-    />
   </div>
 </template>
 
@@ -56,6 +51,7 @@ import type { UploadProps } from 'element-plus'
 import { generateUUID } from '@/utils'
 import { propTypes } from '@/utils/propTypes'
 import { getAccessToken, getTenantId } from '@/utils/auth'
+import { createImageViewer } from '@/components/ImageViewer'
 
 defineOptions({ name: 'UploadImg' })
 
@@ -92,7 +88,12 @@ const message = useMessage() // 消息弹窗
 // 生成组件唯一id
 const uuid = ref('id-' + generateUUID())
 // 查看图片
-const imgViewVisible = ref(false)
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    zIndex: 9999999,
+    urlList: [imgUrl]
+  })
+}
 
 const emit = defineEmits(['update:modelValue'])
 
@@ -130,7 +131,7 @@ const uploadError = () => {
   message.notifyError('图片上传失败,请您重新上传!')
 }
 </script>
-<style scoped lang="scss">
+<style lang="scss" scoped>
 .is-error {
   .upload {
     :deep(.el-upload),

+ 5 - 0
src/layout/components/ToolHeader.vue

@@ -7,6 +7,7 @@ import { Screenfull } from '@/layout/components/Screenfull'
 import { Breadcrumb } from '@/layout/components/Breadcrumb'
 import { SizeDropdown } from '@/layout/components/SizeDropdown'
 import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
+import RouterSearch from '@/components/RouterSearch/index.vue'
 import { useAppStore } from '@/store/modules/app'
 import { useDesign } from '@/hooks/web/useDesign'
 
@@ -25,6 +26,9 @@ const hamburger = computed(() => appStore.getHamburger)
 // 全屏图标
 const screenfull = computed(() => appStore.getScreenfull)
 
+// 搜索图片
+const search = computed(() => appStore.search)
+
 // 尺寸图标
 const size = computed(() => appStore.getSize)
 
@@ -61,6 +65,7 @@ export default defineComponent({
           {screenfull.value ? (
             <Screenfull class="custom-hover" color="var(--top-header-text-color)"></Screenfull>
           ) : undefined}
+          {search.value ? <RouterSearch isModal={false} /> : undefined}
           {size.value ? (
             <SizeDropdown class="custom-hover" color="var(--top-header-text-color)"></SizeDropdown>
           ) : undefined}

+ 2 - 0
src/store/modules/app.ts

@@ -16,6 +16,7 @@ interface AppState {
   uniqueOpened: boolean
   hamburger: boolean
   screenfull: boolean
+  search: boolean
   size: boolean
   locale: boolean
   message: boolean
@@ -52,6 +53,7 @@ export const useAppStore = defineStore('app', {
       uniqueOpened: true, // 是否只保持一个子菜单的展开
       hamburger: true, // 折叠图标
       screenfull: true, // 全屏图标
+      search: true, // 搜索图标
       size: true, // 尺寸图标
       locale: true, // 多语言图标
       message: true, // 消息图标

+ 3 - 3
src/store/modules/dict.ts

@@ -4,7 +4,7 @@ import { store } from '../index'
 import { DictDataVO } from '@/api/system/dict/types'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
 const { wsCache } = useCache('sessionStorage')
-import { listSimpleDictData } from '@/api/system/dict/dict.data'
+import { getSimpleDictDataList } from '@/api/system/dict/dict.data'
 
 export interface DictValueType {
   value: any
@@ -45,7 +45,7 @@ export const useDictStore = defineStore('dict', {
         this.dictMap = dictMap
         this.isSetDict = true
       } else {
-        const res = await listSimpleDictData()
+        const res = await getSimpleDictDataList()
         // 设置数据
         const dictDataMap = new Map<string, any>()
         res.forEach((dictData: DictDataVO) => {
@@ -75,7 +75,7 @@ export const useDictStore = defineStore('dict', {
     },
     async resetDict() {
       wsCache.delete(CACHE_KEY.DICT_CACHE)
-      const res = await listSimpleDictData()
+      const res = await getSimpleDictDataList()
       // 设置数据
       const dictDataMap = new Map<string, any>()
       res.forEach((dictData: DictDataVO) => {

+ 157 - 152
src/utils/dict.ts

@@ -1,8 +1,8 @@
 /**
  * 数据字典工具类
  */
-import {useDictStoreWithOut} from '@/store/modules/dict'
-import {ElementPlusInfoType} from '@/types/elementPlus'
+import { useDictStoreWithOut } from '@/store/modules/dict'
+import { ElementPlusInfoType } from '@/types/elementPlus'
 
 const dictStore = useDictStoreWithOut()
 
@@ -13,51 +13,58 @@ const dictStore = useDictStoreWithOut()
  * @returns {*|Array} 数据字典数组
  */
 export interface DictDataType {
-    dictType: string
-    label: string
-    value: string | number | boolean
-    colorType: ElementPlusInfoType | ''
-    cssClass: string
+  dictType: string
+  label: string
+  value: string | number | boolean
+  colorType: ElementPlusInfoType | ''
+  cssClass: string
+}
+
+export interface NumberDictDataType extends DictDataType {
+  value: number
 }
 
 export const getDictOptions = (dictType: string) => {
-    return dictStore.getDictByType(dictType) || []
+  return dictStore.getDictByType(dictType) || []
 }
 
-export const getIntDictOptions = (dictType: string): DictDataType[] => {
-    const dictOption: DictDataType[] = []
-    const dictOptions: DictDataType[] = getDictOptions(dictType)
-    dictOptions.forEach((dict: DictDataType) => {
-        dictOption.push({
-            ...dict,
-            value: parseInt(dict.value + '')
-        })
+export const getIntDictOptions = (dictType: string): NumberDictDataType[] => {
+  // 获得通用的 DictDataType 列表
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  // 转换成 number 类型的 NumberDictDataType 类型
+  // why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警
+  const dictOption: NumberDictDataType[] = []
+  dictOptions.forEach((dict: DictDataType) => {
+    dictOption.push({
+      ...dict,
+      value: parseInt(dict.value + '')
     })
-    return dictOption
+  })
+  return dictOption
 }
 
 export const getStrDictOptions = (dictType: string) => {
-    const dictOption: DictDataType[] = []
-    const dictOptions: DictDataType[] = getDictOptions(dictType)
-    dictOptions.forEach((dict: DictDataType) => {
-        dictOption.push({
-            ...dict,
-            value: dict.value + ''
-        })
+  const dictOption: DictDataType[] = []
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  dictOptions.forEach((dict: DictDataType) => {
+    dictOption.push({
+      ...dict,
+      value: dict.value + ''
     })
-    return dictOption
+  })
+  return dictOption
 }
 
 export const getBoolDictOptions = (dictType: string) => {
-    const dictOption: DictDataType[] = []
-    const dictOptions: DictDataType[] = getDictOptions(dictType)
-    dictOptions.forEach((dict: DictDataType) => {
-        dictOption.push({
-            ...dict,
-            value: dict.value + '' === 'true'
-        })
+  const dictOption: DictDataType[] = []
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  dictOptions.forEach((dict: DictDataType) => {
+    dictOption.push({
+      ...dict,
+      value: dict.value + '' === 'true'
     })
-    return dictOption
+  })
+  return dictOption
 }
 
 /**
@@ -67,12 +74,12 @@ export const getBoolDictOptions = (dictType: string) => {
  * @return DictDataType 字典对象
  */
 export const getDictObj = (dictType: string, value: any): DictDataType | undefined => {
-    const dictOptions: DictDataType[] = getDictOptions(dictType)
-    for (const dict of dictOptions) {
-        if (dict.value === value + '') {
-            return dict
-        }
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  for (const dict of dictOptions) {
+    if (dict.value === value + '') {
+      return dict
     }
+  }
 }
 
 /**
@@ -83,121 +90,119 @@ export const getDictObj = (dictType: string, value: any): DictDataType | undefin
  * @return 字典名称
  */
 export const getDictLabel = (dictType: string, value: any): string => {
-    const dictOptions: DictDataType[] = getDictOptions(dictType)
-    const dictLabel = ref('')
-    dictOptions.forEach((dict: DictDataType) => {
-        if (dict.value === value + '') {
-            dictLabel.value = dict.label
-        }
-    })
-    return dictLabel.value
+  const dictOptions: DictDataType[] = getDictOptions(dictType)
+  const dictLabel = ref('')
+  dictOptions.forEach((dict: DictDataType) => {
+    if (dict.value === value + '') {
+      dictLabel.value = dict.label
+    }
+  })
+  return dictLabel.value
 }
 
 export enum DICT_TYPE {
-    USER_TYPE = 'user_type',
-    COMMON_STATUS = 'common_status',
-    SYSTEM_TENANT_PACKAGE_ID = 'system_tenant_package_id',
-    TERMINAL = 'terminal', // 终端
-
-    // ========== SYSTEM 模块 ==========
-    SYSTEM_USER_SEX = 'system_user_sex',
-    SYSTEM_MENU_TYPE = 'system_menu_type',
-    SYSTEM_ROLE_TYPE = 'system_role_type',
-    SYSTEM_DATA_SCOPE = 'system_data_scope',
-    SYSTEM_NOTICE_TYPE = 'system_notice_type',
-    SYSTEM_OPERATE_TYPE = 'system_operate_type',
-    SYSTEM_LOGIN_TYPE = 'system_login_type',
-    SYSTEM_LOGIN_RESULT = 'system_login_result',
-    SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code',
-    SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type',
-    SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status',
-    SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status',
-    SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type',
-    SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
-    SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status',
-    SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type',
-    SYSTEM_SOCIAL_TYPE = 'system_social_type',
-
-    // ========== INFRA 模块 ==========
-    INFRA_BOOLEAN_STRING = 'infra_boolean_string',
-    INFRA_JOB_STATUS = 'infra_job_status',
-    INFRA_JOB_LOG_STATUS = 'infra_job_log_status',
-    INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status',
-    INFRA_CONFIG_TYPE = 'infra_config_type',
-    INFRA_CODEGEN_TEMPLATE_TYPE = 'infra_codegen_template_type',
-    INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type',
-    INFRA_CODEGEN_SCENE = 'infra_codegen_scene',
-    INFRA_FILE_STORAGE = 'infra_file_storage',
-
-    // ========== BPM 模块 ==========
-    BPM_MODEL_CATEGORY = 'bpm_model_category',
-    BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
-    BPM_TASK_ASSIGN_RULE_TYPE = 'bpm_task_assign_rule_type',
-    BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
-    BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result',
-    BPM_TASK_ASSIGN_SCRIPT = 'bpm_task_assign_script',
-    BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
-
-    // ========== PAY 模块 ==========
-    PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型
-    PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态
-    PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态
-    PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态
-    PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态
-    PAY_TRANSFER_STATUS = 'pay_transfer_status', // 转账订单状态
-    PAY_TRANSFER_TYPE = 'pay_transfer_type', // 转账订单状态
-
-    // ========== MP 模块 ==========
-    MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
-    MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型
-
-    // ========== MALL - 会员模块 ==========
-    MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型
-    MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型
-
-    // ========== MALL - 商品模块 ==========
-    PRODUCT_UNIT = 'product_unit', // 商品单位
-    PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态
-    PROMOTION_TYPE_ENUM = 'promotion_type_enum', // 营销类型枚举
-
-    // ========== MALL - 交易模块 ==========
-    EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //快递的计费方式
-    TRADE_AFTER_SALE_STATUS = 'trade_after_sale_status', // 售后 - 状态
-    TRADE_AFTER_SALE_WAY = 'trade_after_sale_way', // 售后 - 方式
-    TRADE_AFTER_SALE_TYPE = 'trade_after_sale_type', // 售后 - 类型
-    TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型
-    TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态
-    TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态
-    TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 配送方式
-    BROKERAGE_ENABLED_CONDITION = 'brokerage_enabled_condition', // 分佣模式
-    BROKERAGE_BIND_MODE = 'brokerage_bind_mode', // 分销关系绑定模式
-    BROKERAGE_BANK_NAME = 'brokerage_bank_name', // 佣金提现银行
-    BROKERAGE_WITHDRAW_TYPE = 'brokerage_withdraw_type', // 佣金提现类型
-    BROKERAGE_RECORD_BIZ_TYPE = 'brokerage_record_biz_type', // 佣金业务类型
-    BROKERAGE_RECORD_STATUS = 'brokerage_record_status', // 佣金状态
-    BROKERAGE_WITHDRAW_STATUS = 'brokerage_withdraw_status', // 佣金提现状态
-
-    // ========== MALL - 营销模块 ==========
-    PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型
-    PROMOTION_PRODUCT_SCOPE = 'promotion_product_scope', // 营销的商品范围
-    PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型
-    PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态
-    PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式
-    PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态
-    PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举
-    PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态
-    PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 拼团记录的状态
-    PROMOTION_BANNER_POSITION = 'promotion_banner_position', // banner 定位
-
-    // ========== CRM - 客户管理模块 ==========
-    CRM_RECEIVABLE_CHECK_STATUS = 'crm_receivable_check_status',
-    CRM_RETURN_TYPE = 'crm_return_type',
-    CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry',
-    CRM_CUSTOMER_LEVEL = 'crm_customer_level',
-    CRM_CUSTOMER_SOURCE = 'crm_customer_source',
-    CRM_PRODUCT_STATUS = 'crm_product_status',
-
-    // ========== CRM - 数据权限模块 ==========
-    CRM_BIZ_TYPE = 'crm_biz_type', // 数据模块类型
-    CRM_PERMISSION_LEVEL = 'crm_permission_level' // 用户数据权限类型
+  USER_TYPE = 'user_type',
+  COMMON_STATUS = 'common_status',
+  SYSTEM_TENANT_PACKAGE_ID = 'system_tenant_package_id',
+  TERMINAL = 'terminal', // 终端
+
+  // ========== SYSTEM 模块 ==========
+  SYSTEM_USER_SEX = 'system_user_sex',
+  SYSTEM_MENU_TYPE = 'system_menu_type',
+  SYSTEM_ROLE_TYPE = 'system_role_type',
+  SYSTEM_DATA_SCOPE = 'system_data_scope',
+  SYSTEM_NOTICE_TYPE = 'system_notice_type',
+  SYSTEM_OPERATE_TYPE = 'system_operate_type',
+  SYSTEM_LOGIN_TYPE = 'system_login_type',
+  SYSTEM_LOGIN_RESULT = 'system_login_result',
+  SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code',
+  SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type',
+  SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status',
+  SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status',
+  SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type',
+  SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
+  SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status',
+  SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type',
+  SYSTEM_SOCIAL_TYPE = 'system_social_type',
+
+  // ========== INFRA 模块 ==========
+  INFRA_BOOLEAN_STRING = 'infra_boolean_string',
+  INFRA_JOB_STATUS = 'infra_job_status',
+  INFRA_JOB_LOG_STATUS = 'infra_job_log_status',
+  INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status',
+  INFRA_CONFIG_TYPE = 'infra_config_type',
+  INFRA_CODEGEN_TEMPLATE_TYPE = 'infra_codegen_template_type',
+  INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type',
+  INFRA_CODEGEN_SCENE = 'infra_codegen_scene',
+  INFRA_FILE_STORAGE = 'infra_file_storage',
+
+  // ========== BPM 模块 ==========
+  BPM_MODEL_CATEGORY = 'bpm_model_category',
+  BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
+  BPM_TASK_ASSIGN_RULE_TYPE = 'bpm_task_assign_rule_type',
+  BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
+  BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result',
+  BPM_TASK_ASSIGN_SCRIPT = 'bpm_task_assign_script',
+  BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
+
+  // ========== PAY 模块 ==========
+  PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型
+  PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态
+  PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态
+  PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态
+  PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态
+  PAY_TRANSFER_STATUS = 'pay_transfer_status', // 转账订单状态
+  PAY_TRANSFER_TYPE = 'pay_transfer_type', // 转账订单状态
+
+  // ========== MP 模块 ==========
+  MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
+  MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型
+
+  // ========== MALL - 会员模块 ==========
+  MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型
+  MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型
+
+  // ========== MALL - 商品模块 ==========
+  PRODUCT_UNIT = 'product_unit', // 商品单位
+  PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态
+  PROMOTION_TYPE_ENUM = 'promotion_type_enum', // 营销类型枚举
+
+  // ========== MALL - 交易模块 ==========
+  EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //快递的计费方式
+  TRADE_AFTER_SALE_STATUS = 'trade_after_sale_status', // 售后 - 状态
+  TRADE_AFTER_SALE_WAY = 'trade_after_sale_way', // 售后 - 方式
+  TRADE_AFTER_SALE_TYPE = 'trade_after_sale_type', // 售后 - 类型
+  TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型
+  TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态
+  TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态
+  TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 配送方式
+  BROKERAGE_ENABLED_CONDITION = 'brokerage_enabled_condition', // 分佣模式
+  BROKERAGE_BIND_MODE = 'brokerage_bind_mode', // 分销关系绑定模式
+  BROKERAGE_BANK_NAME = 'brokerage_bank_name', // 佣金提现银行
+  BROKERAGE_WITHDRAW_TYPE = 'brokerage_withdraw_type', // 佣金提现类型
+  BROKERAGE_RECORD_BIZ_TYPE = 'brokerage_record_biz_type', // 佣金业务类型
+  BROKERAGE_RECORD_STATUS = 'brokerage_record_status', // 佣金状态
+  BROKERAGE_WITHDRAW_STATUS = 'brokerage_withdraw_status', // 佣金提现状态
+
+  // ========== MALL - 营销模块 ==========
+  PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型
+  PROMOTION_PRODUCT_SCOPE = 'promotion_product_scope', // 营销的商品范围
+  PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型
+  PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态
+  PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式
+  PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态
+  PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举
+  PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态
+  PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 拼团记录的状态
+  PROMOTION_BANNER_POSITION = 'promotion_banner_position', // banner 定位
+
+  // ========== CRM - 客户管理模块 ==========
+  CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态
+  CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型
+  CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式
+  CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry',
+  CRM_CUSTOMER_LEVEL = 'crm_customer_level',
+  CRM_CUSTOMER_SOURCE = 'crm_customer_source',
+  CRM_PRODUCT_STATUS = 'crm_product_status',
+  CRM_PERMISSION_LEVEL = 'crm_permission_level' // CRM 数据权限的级别
 }

+ 3 - 3
src/utils/formatTime.ts

@@ -62,7 +62,7 @@ export const defaultShortcuts = [
  * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
  * @returns 返回拼接后的时间字符串
  */
-export function formatDate(date: dayjs.ConfigType, format?: string): string {
+export function formatDate(date: Date, format?: string): string {
   // 日期不存在,则返回空
   if (!date) {
     return ''
@@ -200,9 +200,9 @@ export function formatPast2(ms) {
  * @param cellValue 字段值
  */
 // @ts-ignore
-export const dateFormatter = (row, column, cellValue) => {
+export const dateFormatter = (row, column, cellValue): string => {
   if (!cellValue) {
-    return
+    return ''
   }
   return formatDate(cellValue)
 }

+ 22 - 0
src/utils/index.ts

@@ -1,3 +1,5 @@
+import { toNumber } from 'lodash-es'
+
 /**
  *
  * @param component 需要注册的组件
@@ -263,3 +265,23 @@ export const calculateRelativeRate = (value?: number, reference?: number) => {
 
   return ((100 * ((value || 0) - reference)) / reference).toFixed(0)
 }
+
+/**
+ * 获取链接的参数值
+ * @param key 参数键名
+ * @param urlStr 链接地址,默认为当前浏览器的地址
+ */
+export const getUrlValue = (key: string, urlStr: string = location.href): string => {
+  if (!urlStr || !key) return ''
+  const url = new URL(decodeURIComponent(urlStr))
+  return url.searchParams.get(key) ?? ''
+}
+
+/**
+ * 获取链接的参数值(值类型)
+ * @param key 参数键名
+ * @param urlStr 链接地址,默认为当前浏览器的地址
+ */
+export const getUrlNumberValue = (key: string, urlStr: string = location.href): number => {
+  return toNumber(getUrlValue(key, urlStr))
+}

+ 2 - 2
src/views/Profile/components/ProfileUser.vue

@@ -41,7 +41,7 @@
       <li class="list-group-item">
         <Icon class="mr-5px" icon="ep:calendar" />
         {{ t('profile.user.createTime') }}
-        <div class="pull-right">{{ formatDate(userInfo?.createTime) }}</div>
+        <div class="pull-right">{{ formatDate(userInfo.createTime) }}</div>
       </li>
     </ul>
   </div>
@@ -55,7 +55,7 @@ import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
 defineOptions({ name: 'ProfileUser' })
 
 const { t } = useI18n()
-const userInfo = ref<ProfileVO>()
+const userInfo = ref({} as ProfileVO)
 const getUserInfo = async () => {
   const users = await getUserProfile()
   userInfo.value = users

+ 279 - 0
src/views/crm/business/BusinessForm.vue

@@ -0,0 +1,279 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      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
+              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"
+              />
+            </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>
+      <el-form-item label="商机状态类型" prop="statusTypeId">
+        <el-select
+          v-model="formData.statusTypeId"
+          placeholder="请选择商机状态类型"
+          clearable
+          size="small"
+          @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 size="small">
+          <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-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 BusinessStatusTypeApi from '@/api/crm/businessStatusType'
+import * as CustomerApi from '@/api/crm/customer'
+import { DICT_TYPE } from '@/utils/dict'
+import { ElTable } from 'element-plus'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  statusTypeId: undefined,
+  statusId: undefined,
+  contactNextTime: undefined,
+  customerId: undefined,
+  dealTime: undefined,
+  price: undefined,
+  discountPercent: undefined,
+  productPrice: undefined,
+  remark: undefined,
+  ownerUserId: undefined,
+  roUserIds: undefined,
+  rwUserIds: undefined,
+  endStatus: undefined,
+  endRemark: undefined,
+  contactLastTime: undefined,
+  followUpStatus: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '商机名称不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const businessStatusList = ref([]) // 商机状态列表
+const businessStatusTypeList = ref([]) //商机状态类型列表
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const customerList = ref([]) // 客户列表的数据
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await BusinessApi.getBusiness(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 加载商机状态类型列表
+  businessStatusTypeList.value = await BusinessStatusTypeApi.getBusinessStatusTypeList()
+}
+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 {
+    const data = formData.value as unknown as BusinessApi.BusinessVO
+    if (formType.value === 'create') {
+      await BusinessApi.createBusiness(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await BusinessApi.updateBusiness(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    statusTypeId: undefined,
+    statusId: undefined,
+    contactNextTime: undefined,
+    customerId: undefined,
+    dealTime: undefined,
+    price: undefined,
+    discountPercent: undefined,
+    productPrice: undefined,
+    remark: undefined,
+    ownerUserId: undefined,
+    roUserIds: undefined,
+    rwUserIds: undefined,
+    endStatus: undefined,
+    endRemark: undefined,
+    contactLastTime: undefined,
+    followUpStatus: undefined
+  }
+  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
+})
+// 选择客户
+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)
+    console.log(JSON.stringify(data))
+    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>

+ 107 - 0
src/views/crm/business/components/BusinessList.vue

@@ -0,0 +1,107 @@
+<template>
+  <!-- 操作栏 -->
+  <el-row justify="end">
+    <el-button @click="openForm">
+      <Icon class="mr-5px" icon="ep:opportunity" />
+      创建商机
+    </el-button>
+  </el-row>
+
+  <!-- 列表 -->
+  <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="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="price" :formatter="fenToYuanFormat" />
+      <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>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加 -->
+  <BusinessForm ref="formRef" @success="getList" />
+</template>
+<script setup lang="ts">
+import * as BusinessApi from '@/api/crm/business'
+import BusinessForm from './../BusinessForm.vue'
+import { BizTypeEnum } from '@/api/crm/permission'
+import { fenToYuanFormat } from '@/utils/formatter'
+
+defineOptions({ name: 'CrmBusinessList' })
+const props = defineProps<{
+  bizType: number // 业务类型
+  bizId: number // 业务编号
+}>()
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  customerId: 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 BusinessApi.getBusinessPageByCustomer(queryParams)
+        break
+      default:
+        return
+    }
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 添加操作 */
+const formRef = ref()
+const openForm = () => {
+  formRef.value.open('create')
+}
+
+/** 打开联系人详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 监听打开的 bizId + bizType,从而加载最新的列表 */
+watch(
+  () => [props.bizId, props.bizType],
+  () => {
+    handleQuery()
+  },
+  { immediate: true, deep: true }
+)
+</script>

+ 207 - 0
src/views/crm/business/index.vue

@@ -0,0 +1,207 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <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('create')" v-hasPermi="['crm:business:create']">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['crm:business:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <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="customerName" />
+      <el-table-column label="商机金额" align="center" prop="price" />
+      <el-table-column
+        label="预计成交日期"
+        align="center"
+        prop="dealTime"
+        :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
+        label="更新时间"
+        align="center"
+        prop="updateTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :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 label="操作" align="center" fixed="right" width="130px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['crm:business:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['crm:business:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <BusinessForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as BusinessApi from '@/api/crm/business'
+import BusinessForm from './BusinessForm.vue'
+
+defineOptions({ name: 'CrmBusiness' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+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
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await BusinessApi.getBusinessPage(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 = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await BusinessApi.deleteBusiness(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await BusinessApi.exportBusiness(queryParams)
+    download.excel(data, '商机.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 167 - 0
src/views/crm/businessStatusType/BusinessStatusTypeForm.vue

@@ -0,0 +1,167 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="状态类型名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入状态类型名" />
+      </el-form-item>
+      <el-form-item label="应用部门" prop="deptIds">
+        <el-tree
+          ref="treeRef"
+          :data="deptList"
+          :props="defaultProps"
+          :check-strictly="!checkStrictly"
+          node-key="id"
+          placeholder="请选择归属部门"
+          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">
+            <template #default="scope">
+              <el-text>状态{{ scope.$index + 1 }}</el-text>
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="状态名称" width="120" prop="name">
+            <template #default="{ row }">
+              <el-input v-model="row.name" placeholder="请输入状态名称" />
+            </template>
+          </el-table-column>
+          <el-table-column width="120" align="center" label="赢单率" prop="percent">
+            <template #default="{ row }">
+              <el-input v-model="row.percent" placeholder="请输入赢单率" />
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center">
+            <template #default="scope">
+              <el-button link type="primary" @click="addStatusArea(scope.$index)"> 添加 </el-button>
+              <el-button
+                link
+                type="danger"
+                @click="deleteStatusArea(scope.$index)"
+                v-show="scope.$index > 0"
+              >
+                删除
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </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 BusinessStatusTypeApi from '@/api/crm/businessStatusType'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: 0,
+  name: '',
+  deptIds: [],
+  statusList: []
+})
+const formRules = reactive({
+  name: [{ required: true, message: '状态类型名不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const deptList = ref<Tree[]>([]) // 树形结构
+const treeRef = ref() // 菜单树组件 Ref
+const checkStrictly = ref(true) // 是否严格模式,即父子不关联
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await BusinessStatusTypeApi.getBusinessStatusType(id)
+      treeRef.value.setCheckedKeys(formData.value.deptIds)
+      if (formData.value.statusList.length == 0) {
+        addStatusArea(0)
+      }
+    } finally {
+      formLoading.value = false
+    }
+  } else {
+    addStatusArea(0)
+  }
+  // 加载部门树
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as BusinessStatusTypeApi.BusinessStatusTypeVO
+    data.deptIds = treeRef.value.getCheckedKeys(false)
+    if (formType.value === 'create') {
+      await BusinessStatusTypeApi.createBusinessStatusType(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await BusinessStatusTypeApi.updateBusinessStatusType(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  checkStrictly.value = true
+  formData.value = {
+    id: 0,
+    name: '',
+    deptIds: [],
+    statusList: []
+  }
+  treeRef.value?.setCheckedNodes([])
+  formRef.value?.resetFields()
+}
+
+/** 添加状态 */
+const addStatusArea = () => {
+  const data = formData.value
+  data.statusList.push({
+    name: '',
+    percent: ''
+  })
+}
+
+/** 删除状态 */
+const deleteStatusArea = (index: number) => {
+  const data = formData.value
+  data.statusList.splice(index, 1)
+}
+</script>

+ 171 - 0
src/views/crm/businessStatusType/index.vue

@@ -0,0 +1,171 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      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']"
+        >
+          <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>
+
+  <!-- 列表 -->
+  <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="creator" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['crm:business-status-type:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['crm:business-status-type:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <BusinessStatusTypeForm 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'
+
+defineOptions({ name: 'BusinessStatusType' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await BusinessStatusTypeApi.getBusinessStatusTypePage(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 = (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)
+    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()
+})
+</script>

+ 17 - 2
src/views/crm/clue/ClueForm.vue

@@ -10,9 +10,16 @@
       <el-form-item label="线索名称" prop="name">
         <el-input v-model="formData.name" placeholder="请输入线索名称" />
       </el-form-item>
-      <!-- TODO wanwan 客户选择 -->
+      <!-- TODO 芋艿:后续客户的选择 -->
       <el-form-item label="客户" prop="customerId">
-        <el-input v-model="formData.customerId" placeholder="请选择客户" />
+        <el-select v-model="formData.customerId" clearable placeholder="请选择客户">
+          <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="contactNextTime">
         <el-date-picker
@@ -47,6 +54,7 @@
 </template>
 <script setup lang="ts">
 import * as ClueApi from '@/api/crm/clue'
+import * as CustomerApi from '@/api/crm/customer'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -55,6 +63,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const customerList = ref([]) // 客户列表
 const formData = ref({
   id: undefined,
   name: undefined,
@@ -79,6 +88,12 @@ const open = async (type: string, id?: number) => {
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
+  const customerData = await CustomerApi.getCustomerPage({
+    pageNo: 1,
+    pageSize: 100,
+    pool: false
+  })
+  customerList.value = customerData.list
   // 修改时,设置数据
   if (id) {
     formLoading.value = true

+ 0 - 138
src/views/crm/components/CrmPermissionList.vue

@@ -1,138 +0,0 @@
-<template>
-  <!-- 操作栏 -->
-  <el-row justify="end">
-    <el-button type="primary" @click="openForm">
-      <Icon class="mr-5px" icon="ep:plus" />
-      新增
-    </el-button>
-    <el-button @click="handleEdit">
-      <Icon class="mr-5px" icon="ep:edit" />
-      编辑
-    </el-button>
-    <el-button @click="handleDelete">
-      <Icon class="mr-5px" icon="ep:delete" />
-      移除
-    </el-button>
-    <el-button type="danger" @click="handleQuit"> 退出团队</el-button>
-  </el-row>
-  <!--  团队成员展示 -->
-  <el-table
-    v-loading="loading"
-    :data="list"
-    :show-overflow-tooltip="true"
-    :stripe="true"
-    class="mt-20px"
-    @selection-change="handleSelectionChange"
-  >
-    <el-table-column type="selection" width="55" />
-    <el-table-column align="center" label="姓名" prop="nickname" />
-    <el-table-column align="center" label="部门" prop="deptName" />
-    <el-table-column align="center" label="岗位" prop="postNames" />
-    <el-table-column align="center" label="权限级别" prop="level">
-      <template #default="{ row }">
-        <dict-tag :type="DICT_TYPE.CRM_PERMISSION_LEVEL" :value="row.level" />
-      </template>
-    </el-table-column>
-    <el-table-column :formatter="dateFormatter" align="center" label="加入时间" prop="createTime" />
-  </el-table>
-  <CrmPermissionForm ref="permissionFormRef" @success="getList" />
-</template>
-<script lang="ts" setup>
-import { dateFormatter } from '@/utils/formatTime'
-import { ElTable } from 'element-plus'
-import * as PermissionApi from '@/api/crm/permission'
-import { useUserStoreWithOut } from '@/store/modules/user'
-import CrmPermissionForm from './CrmPermissionForm.vue'
-import { CrmPermissionLevelEnum } from './index'
-import { DICT_TYPE } from '@/utils/dict'
-
-defineOptions({ name: 'CrmPermissionList' })
-
-const message = useMessage() // 消息
-
-const props = defineProps<{
-  bizType: number // 模块类型
-  bizId: number // 模块数据编号
-}>()
-const loading = ref(true) // 列表的加载中
-const list = ref<PermissionApi.PermissionVO[]>([]) // 列表的数据
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await PermissionApi.getPermissionList({
-      bizType: props.bizType,
-      bizId: props.bizId
-    })
-    list.value = data
-  } finally {
-    loading.value = false
-  }
-}
-const multipleSelection = ref<PermissionApi.PermissionVO[]>([]) // 选择的团队成员
-const handleSelectionChange = (val: PermissionApi.PermissionVO[]) => {
-  multipleSelection.value = val
-}
-
-const permissionFormRef = ref<InstanceType<typeof CrmPermissionForm>>() // 权限表单 Ref
-/**
- * 编辑团队成员
- */
-const handleEdit = () => {
-  if (multipleSelection.value?.length === 0) {
-    message.warning('请先选择团队成员后操作!')
-    return
-  }
-  const ids = multipleSelection.value?.map((item) => item.id)
-  permissionFormRef.value?.open('update', props.bizType, props.bizId, ids)
-}
-
-/**
- * 移除团队成员
- */
-const handleDelete = async () => {
-  if (multipleSelection.value?.length === 0) {
-    message.warning('请先选择团队成员后操作!')
-    return
-  }
-  await message.delConfirm()
-  const ids = multipleSelection.value?.map((item) => item.id)
-  await PermissionApi.deletePermission({
-    bizType: props.bizType,
-    bizId: props.bizId,
-    ids
-  })
-}
-
-/**
- * 添加团队成员
- */
-const openForm = () => {
-  permissionFormRef.value?.open('create', props.bizType, props.bizId)
-}
-
-const userStore = useUserStoreWithOut() // 用户信息缓存
-/**
- * 退出团队
- */
-const handleQuit = async () => {
-  const permission = list.value.find(
-    (item) => item.userId === userStore.getUser.id && item.level === CrmPermissionLevelEnum.OWNER
-  )
-  if (permission) {
-    message.warning('负责人不能退出团队!')
-    return
-  }
-  const userPermission = list.value.find((item) => item.userId === userStore.getUser.id)
-  await PermissionApi.quitTeam(userPermission?.id)
-}
-
-watch(
-  () => props.bizId,
-  () => {
-    getList()
-  },
-  { immediate: true, deep: true }
-)
-</script>

+ 0 - 15
src/views/crm/components/index.ts

@@ -1,15 +0,0 @@
-import CrmPermissionList from './CrmPermissionList.vue'
-
-enum CrmBizTypeEnum {
-  CRM_LEADS = 1, // 线索
-  CRM_CUSTOMER = 2, // 客户
-  CRM_CONTACTS = 3, // 联系人
-  CRM_BUSINESS = 5, // 商机
-  CRM_CONTRACT = 6 // 合同
-}
-
-enum CrmPermissionLevelEnum {
-  OWNER = 1 // 负责人
-}
-
-export { CrmPermissionList, CrmBizTypeEnum, CrmPermissionLevelEnum }

+ 3 - 3
src/views/crm/customerLimitConfig/CustomerLimitConfigForm.vue → src/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue

@@ -12,9 +12,9 @@
           v-model="formData.userIds"
           :data="userTree"
           :props="defaultProps"
-          check-on-click-node
           multiple
           filterable
+          check-on-click-node
           node-key="id"
           placeholder="请选择规则适用人群"
         />
@@ -25,8 +25,8 @@
           :data="deptTree"
           :props="defaultProps"
           multiple
-          check-strictly
           filterable
+          check-strictly
           node-key="id"
           placeholder="请选择规则适用部门"
         />
@@ -57,11 +57,11 @@
 </template>
 <script setup lang="ts">
 import * as CustomerLimitConfigApi from '@/api/crm/customerLimitConfig'
-import { LimitConfType } from '@/views/crm/customerLimitConfig/customerLimitConf'
 import * as DeptApi from '@/api/system/dept'
 import { defaultProps, handleTree } from '@/utils/tree'
 import * as UserApi from '@/api/system/user'
 import { cloneDeep } from 'lodash-es'
+import { LimitConfType } from '@/api/crm/customerLimitConfig'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗

+ 13 - 5
src/views/crm/customerLimitConfig/CustomerLimitConfDetails.vue → src/views/crm/config/customerLimitConfig/CustomerLimitConfigList.vue

@@ -19,8 +19,16 @@
   >
     <el-table-column label="编号" align="center" prop="id" />
     <el-table-column label="规则类型" align="center" prop="type" />
-    <el-table-column label="规则适用人群" align="center" prop="userNames" />
-    <el-table-column label="规则适用部门" align="center" prop="deptNames" />
+    <el-table-column
+      label="规则适用人群"
+      align="center"
+      :formatter="(row) => row.users?.map((user: any) => user.nickname).join(',')"
+    />
+    <el-table-column
+      label="规则适用部门"
+      align="center"
+      :formatter="(row) => row.depts?.map((dept: any) => dept.name).join(',')"
+    />
     <el-table-column
       :label="
         confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT ? '拥有客户数上限' : '锁定客户数上限'
@@ -80,11 +88,11 @@
 <script setup lang="ts">
 import { dateFormatter } from '@/utils/formatTime'
 import * as CustomerLimitConfigApi from '@/api/crm/customerLimitConfig'
-import CustomerLimitConfigForm from '@/views/crm/customerLimitConfig/CustomerLimitConfigForm.vue'
-import { LimitConfType } from '@/views/crm/customerLimitConfig/customerLimitConf'
+import CustomerLimitConfigForm from '@/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
+import { LimitConfType } from '@/api/crm/customerLimitConfig'
 
-defineOptions({ name: 'CustomerLimitConfDetails' })
+defineOptions({ name: 'CustomerLimitConfigList' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化

+ 19 - 0
src/views/crm/config/customerLimitConfig/index.vue

@@ -0,0 +1,19 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-tabs>
+      <el-tab-pane label="拥有客户数限制">
+        <CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_QUANTITY_LIMIT" />
+      </el-tab-pane>
+      <el-tab-pane label="锁定客户数限制">
+        <CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_LOCK_LIMIT" />
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import CustomerLimitConfigList from '@/views/crm/config/customerLimitConfig/CustomerLimitConfigList.vue'
+import { LimitConfType } from '@/api/crm/customerLimitConfig'
+
+defineOptions({ name: 'CrmCustomerLimitConfig' })
+</script>

+ 25 - 27
src/views/crm/customerPoolConf/index.vue → src/views/crm/config/customerPoolConfig/index.vue

@@ -23,7 +23,7 @@
         </template>
         <!-- 表单 -->
         <el-form-item label="客户公海规则设置" prop="enabled">
-          <el-radio-group v-model="formData.enabled" class="ml-4">
+          <el-radio-group v-model="formData.enabled" @change="changeEnable" class="ml-4">
             <el-radio :label="false" size="large">不启用</el-radio>
             <el-radio :label="true" size="large">启用</el-radio>
           </el-radio-group>
@@ -36,7 +36,11 @@
             天未成交
           </el-form-item>
           <el-form-item label="提前提醒设置" prop="notifyEnabled">
-            <el-radio-group v-model="formData.notifyEnabled" class="ml-4">
+            <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>
@@ -52,11 +56,10 @@
   </ContentWrap>
 </template>
 <script setup lang="ts">
-import * as CustomerPoolConfApi from '@/api/crm/customerPoolConf'
+import * as CustomerPoolConfigApi from '@/api/crm/customerPoolConfig'
 import { CardTitle } from '@/components/Card'
 
-// TODO @wanwan:CustomerPoolConf =》 CustomerPoolConfig;另外,我们在 crm 目录下,新建一个 config 目录,然后把 customerPoolConfig 和 customerLimitConfig 都挪进
-defineOptions({ name: 'CustomerPoolConf' })
+defineOptions({ name: 'CrmCustomerPoolConfig' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
@@ -78,7 +81,7 @@ const formRef = ref() // 表单 Ref
 const getConfig = async () => {
   try {
     formLoading.value = true
-    const data = await CustomerPoolConfApi.getCustomerPoolConfig()
+    const data = await CustomerPoolConfigApi.getCustomerPoolConfig()
     if (data === null) {
       return
     }
@@ -97,8 +100,8 @@ const onSubmit = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as CustomerPoolConfApi.CustomerPoolConfigVO
-    await CustomerPoolConfApi.updateCustomerPoolConfig(data)
+    const data = formData.value as unknown as CustomerPoolConfigApi.CustomerPoolConfigVO
+    await CustomerPoolConfigApi.saveCustomerPoolConfig(data)
     message.success(t('common.updateSuccess'))
     await getConfig()
     formLoading.value = false
@@ -107,27 +110,22 @@ const onSubmit = async () => {
   }
 }
 
-// TODO @wanwan:el-radio-group 选择后,触发会不会更好哈;
-watch(
-  () => formData.value.enabled,
-  (val: boolean) => {
-    if (!val) {
-      formData.value.contactExpireDays = undefined
-      formData.value.dealExpireDays = undefined
-      formData.value.notifyEnabled = false
-      formData.value.notifyDays = undefined
-    }
+/** 更改客户公海规则设置 */
+const changeEnable = () => {
+  if (!formData.value.enabled) {
+    formData.value.contactExpireDays = undefined
+    formData.value.dealExpireDays = undefined
+    formData.value.notifyEnabled = false
+    formData.value.notifyDays = undefined
   }
-)
-// TODO @wanwan:el-radio-group 选择后,触发会不会更好哈;
-watch(
-  () => formData.value.notifyEnabled,
-  (val: boolean) => {
-    if (!val) {
-      formData.value.notifyDays = undefined
-    }
+}
+
+/** 更改提前提醒设置 */
+const changeNotifyEnable = () => {
+  if (!formData.value.notifyEnabled) {
+    formData.value.notifyDays = undefined
   }
-)
+}
 
 onMounted(() => {
   getConfig()

+ 183 - 232
src/views/crm/contact/ContactForm.vue

@@ -1,170 +1,194 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible" :width="800">
+  <Dialog :title="dialogTitle" v-model="dialogVisible" :width="820">
     <el-form
       ref="formRef"
       :model="formData"
       :rules="formRules"
-      label-width="130px"
+      label-width="110px"
       v-loading="formLoading"
-      :inline="true"
     >
-      <el-form-item label="姓名" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入姓名" />
-      </el-form-item>
-      <el-form-item label="负责人" prop="ownerUserId">
-        <el-select
-          v-model="ownerUserList"
-          placeholder="请选择负责人"
-          multiple
-          value-key="id"
-          lable-key="nickname"
-          @click="openOwerForm('open')"
-        >
-          <el-option
-            v-for="item in ownerUserList"
-            :key="item.id"
-            :label="item.nickname"
-            :value="item"
-          />
-        </el-select>
-      </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-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="姓名" prop="name">
+            <el-input input-style="width:190px;" 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"
+              placeholder="请选择负责人"
+              value-key="id"
+              lable-key="nickname"
+            >
+              <el-option
+                v-for="item in userList"
+                :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="customerName">
+            <el-select
+              v-model="formData.customerId"
+              placeholder="请选择客户"
+              value-key="id"
+              lable-key="name"
+            >
+              <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="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
+        ></el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="手机号" prop="mobile">
+            <el-input
+              input-style="width:190px;"
+              v-model="formData.mobile"
+              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>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="邮箱" prop="email">
+            <el-input
+              input-style="width:190px;"
+              v-model="formData.email"
+              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-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="微信" prop="wechat">
+            <el-input
+              input-style="width:190px;"
+              v-model="formData.wechat"
+              placeholder="请输入微信"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="下次联系时间" prop="nextTime">
+            <el-date-picker
+              v-model="formData.nextTime"
+              type="date"
+              value-format="x"
+              placeholder="选择下次联系时间"
+            />
+          </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="address">
             <el-input
-              placeholder="请选择"
-              @click="openCustomerSelect"
-              v-model="formData.customerName"
+              input-style="width:190px;"
+              v-model="formData.address"
+              placeholder="请输入地址"
             />
-          </template>
-          <el-table :data="list" ref="multipleTableRef" @select="handleSelectionChange">
-            <el-table-column label="选择" type="selection" width="55" />
-            <el-table-column width="100" property="id" label="编号" />
-            <el-table-column width="150" property="name" label="客户名称" />
-            <el-table-column label="客户来源" align="center" 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 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="getList"
-                layout="sizes, prev, pager, next"
+          </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-option
+                v-for="item in allContactList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+                :disabled="item.id == formData.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>
-      <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>
-      <el-form-item label="手机号" prop="mobile">
-        <el-input v-model="formData.mobile" placeholder="请输入手机号" />
-      </el-form-item>
-      <el-form-item label="座机" prop="telephone">
-        <el-input v-model="formData.telephone" placeholder="请输入座机" style="width: 215px" />
-      </el-form-item>
-      <el-form-item label="邮箱" prop="email">
-        <el-input v-model="formData.email" placeholder="请输入邮箱" />
-      </el-form-item>
-      <el-form-item label="QQ" prop="qq">
-        <el-input v-model="formData.qq" placeholder="请输入QQ" style="width: 215px" />
-      </el-form-item>
-      <el-form-item label="微信" prop="webchat">
-        <el-input v-model="formData.webchat" placeholder="请输入微信" />
-      </el-form-item>
-      <el-form-item label="下次联系时间" prop="nextTime">
-        <el-date-picker
-          v-model="formData.nextTime"
-          type="date"
-          value-format="x"
-          placeholder="选择下次联系时间"
-        />
-      </el-form-item>
-      <el-form-item label="地址" prop="address">
-        <el-input v-model="formData.address" placeholder="请输入地址" />
-      </el-form-item>
-      <el-form-item label="直属上级" prop="parentId">
-        <el-select v-model="formData.parentId" placeholder="请选择">
-          <el-option
-            v-for="item in allContactList"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-            :disabled="item.id == formData.id"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item label="职位" prop="post">
-        <el-input v-model="formData.post" placeholder="请输入职位" />
-      </el-form-item>
-
-      <el-form-item label="是否关键决策人" prop="policyMakers" style="width: 400px">
-        <el-radio-group v-model="formData.policyMakers">
-          <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-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入备注" />
-      </el-form-item>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="职位" prop="post">
+            <el-input input-style="width:190px;" v-model="formData.post" 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>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="24"
+          ><el-form-item label="备注" prop="remark">
+            <el-input v-model="formData.remark" placeholder="请输入备注" />
+          </el-form-item>
+        </el-col>
+      </el-row>
     </el-form>
     <template #footer>
       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
-  <OwerSelect
-    ref="owerRef"
-    @confirmOwerSelect="owerSelectValue"
-    :initOwerUser="formData.ownerUserId"
-  />
 </template>
 <script setup lang="ts">
 import * as ContactApi from '@/api/crm/contact'
 import { DICT_TYPE, getIntDictOptions, getBoolDictOptions } from '@/utils/dict'
-import OwerSelect from './OwerSelect.vue'
 import * as UserApi from '@/api/system/user'
 import * as CustomerApi from '@/api/crm/customer'
-import { ElTable } from 'element-plus'
+import * as AreaApi from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -172,6 +196,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const areaList = ref([]) // 地区列表
 const formData = ref({
   nextTime: undefined,
   mobile: undefined,
@@ -188,21 +213,10 @@ const formData = ref({
   name: undefined,
   post: undefined,
   qq: undefined,
-  webchat: undefined,
+  wechat: undefined,
   sex: undefined,
-  policyMakers: undefined
-})
-const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  name: null,
-  mobile: null,
-  industryId: null,
-  level: null,
-  source: null
+  master: false,
+  areaId: undefined
 })
 const formRules = reactive({
   name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
@@ -212,56 +226,34 @@ const formRules = reactive({
 const formRef = ref() // 表单 Ref
 const ownerUserList = ref<any[]>([])
 const userList = ref<UserApi.UserVO[]>([]) // 用户列表
-
+// TODO 芋艿:统一的客户选择面板
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
+const allContactList = ref([]) // 所有联系人列表
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
-  allContactList.value = await ContactApi.simpleAlllist()
   resetForm()
+  allContactList.value = await ContactApi.getSimpleContactList()
+  userList.value = await UserApi.getSimpleUserList()
+  customerList.value = await CustomerApi.queryAllList()
+  areaList.value = await AreaApi.getAreaTree()
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
     try {
       formData.value = await ContactApi.getContact(id)
-      userList.value = await UserApi.getSimpleUserList()
-      await gotOwnerUser(formData.value.ownerUserId)
     } finally {
       formLoading.value = false
     }
   }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await CustomerApi.getCustomerPage(queryParams)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-
-const gotOwnerUser = (owerUserId: any) => {
-  if (owerUserId !== null) {
-    owerUserId.split(',').forEach((item: string) => {
-      userList.value.find((user: { id: any }) => {
-        if (user.id == item) {
-          ownerUserList.value.push(user)
-        }
-      })
-    })
-  }
-}
-
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
-  owerSelectValue(ownerUserList)
+  // owerSelectValue(ownerUserList)
   // 校验表单
   if (!formRef) return
   const valid = await formRef.value.validate()
@@ -302,52 +294,11 @@ const resetForm = () => {
     name: undefined,
     post: undefined,
     qq: undefined,
-    webchat: undefined,
+    wechat: undefined,
     sex: undefined,
-    policyMakers: undefined
+    master: false
   }
   formRef.value?.resetFields()
   ownerUserList.value = []
 }
-
-/** 添加/修改操作 */
-// TODO @zyna:owner?拼写要注意哈;
-const owerRef = ref()
-const openOwerForm = (type: string) => {
-  owerRef.value.open(type, ownerUserList.value)
-}
-const owerSelectValue = (value) => {
-  ownerUserList.value = value.value
-  formData.value.ownerUserId = undefined
-  value.value.forEach((item, index) => {
-    if (index != 0) {
-      formData.value.ownerUserId = formData.value.ownerUserId + ',' + item.id
-    } else {
-      formData.value.ownerUserId = item.id
-    }
-  })
-}
-// 选择客户
-const showCustomer = ref(false)
-const openCustomerSelect = () => {
-  showCustomer.value = !showCustomer.value
-  queryParams.pageNo = 1
-  getList()
-}
-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
-}
-const allContactList = ref([]) // 所有联系人列表
-onMounted(async () => {
-  allContactList.value = await ContactApi.simpleAlllist()
-})
 </script>

+ 112 - 0
src/views/crm/contact/components/ContactList.vue

@@ -0,0 +1,112 @@
+<template>
+  <!-- 操作栏 -->
+  <el-row justify="end">
+    <el-button @click="openForm">
+      <Icon class="mr-5px" icon="system-uicons:contacts" />
+      创建联系人
+    </el-button>
+  </el-row>
+
+  <!-- 列表 -->
+  <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="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">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+        </template>
+      </el-table-column>
+      <!-- TODO 芋艿:【操作:设为首要联系人】 -->
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加 -->
+  <ContactForm ref="formRef" @success="getList" />
+</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'
+
+defineOptions({ name: 'CrmContactList' })
+const props = defineProps<{
+  bizType: number // 业务类型
+  bizId: number // 业务编号
+}>()
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  customerId: 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 ContactApi.getContactPageByCustomer(queryParams)
+        break
+      default:
+        return
+    }
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 添加操作 */
+const formRef = ref()
+const openForm = () => {
+  formRef.value.open('create')
+}
+
+/** 打开联系人详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'CrmContactDetail', params: { id } })
+}
+
+/** 监听打开的 bizId + bizType,从而加载最新的列表 */
+watch(
+  () => [props.bizId, props.bizType],
+  () => {
+    handleQuery()
+  },
+  { immediate: true, deep: true }
+)
+</script>

+ 9 - 26
src/views/crm/contact/detail/ContactDetails.vue

@@ -8,7 +8,7 @@
         <el-descriptions-item label="姓名">
           {{ contact.name }}
         </el-descriptions-item>
-        <el-descriptions-item label="客户名称">
+        <el-descriptions-item label="客户">
           {{ contact.customerName }}
         </el-descriptions-item>
         <el-descriptions-item label="手机">
@@ -24,14 +24,17 @@
           {{ contact.qq }}
         </el-descriptions-item>
         <el-descriptions-item label="微信">
-          {{ contact.webchat }}
-        </el-descriptions-item>
-        <el-descriptions-item label="详细地址">
-          {{ contact.address }}
+          {{ 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.address }}
+        </el-descriptions-item>
         <el-descriptions-item label="性别">
           <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="contact.sex" />
         </el-descriptions-item>
@@ -46,7 +49,7 @@
       </template>
       <el-descriptions :column="2">
         <el-descriptions-item label="负责人">
-          {{ gotOwnerUser(contact.ownerUserId) }}
+          {{ contact.ownerUserName }}
         </el-descriptions-item>
         <el-descriptions-item label="创建人">
           {{ contact.creatorName }}
@@ -66,29 +69,9 @@
 import * as ContactApi from '@/api/crm/contact'
 import { DICT_TYPE } from '@/utils/dict'
 import { formatDate } from '@/utils/formatTime'
-import * as UserApi from '@/api/system/user'
 const { contact } = defineProps<{ contact: ContactApi.ContactVO }>()
 
 // 展示的折叠面板
 const activeNames = ref(['basicInfo', 'systemInfo'])
-const gotOwnerUser = (owerUserId: string) => {
-  let ownerName = ''
-  if (owerUserId !== null && owerUserId != undefined) {
-    owerUserId.split(',').forEach((item: string, index: number) => {
-      if (index != 0) {
-        ownerName =
-          ownerName + ',' + userList.value.find((user: { id: any }) => user.id == item)?.nickname
-      } else {
-        ownerName = userList.value.find((user: { id: any }) => user.id == item)?.nickname || ''
-      }
-    })
-  }
-  return ownerName
-}
-const userList = ref<UserApi.UserVO[]>([]) // 用户列表
-/** 初始化 **/
-onMounted(async () => {
-  userList.value = await UserApi.getSimpleUserList()
-})
 </script>
 <style scoped lang="scss"></style>

+ 10 - 25
src/views/crm/contact/detail/index.vue

@@ -46,7 +46,7 @@
   </div>
   <ContentWrap class="mt-10px">
     <el-descriptions :column="5" direction="vertical">
-      <el-descriptions-item label="客户名称">
+      <el-descriptions-item label="客户">
         {{ contact.customerName }}
       </el-descriptions-item>
       <el-descriptions-item label="职务">
@@ -63,33 +63,18 @@
   <!-- TODO wanwan:这个 tab 拉满哈,可以更好看; -->
   <el-col :span="18">
     <el-tabs>
-      <el-tab-pane label="详细资料">
+      <el-tab-pane label="基本信息">
         <!-- TODO wanwan:这个 ml-2 是不是可以优化下,不要整个左移,而是里面的内容有个几 px 的偏移,不顶在框里 -->
         <ContactDetails class="ml-2" :contact="contact" />
       </el-tab-pane>
-      <el-tab-pane label="活动" lazy> 活动</el-tab-pane>
-      <el-tab-pane label="邮件" lazy> 邮件</el-tab-pane>
-      <el-tab-pane label="工商信息" lazy> 工商信息</el-tab-pane>
-      <!-- TODO wanwan 以下标签上的数量需要接口统计返回 -->
-      <el-tab-pane label="客户" lazy>
-        <template #label> 客户<el-badge :value="12" class="item" type="primary" /> </template>
-        客户
-      </el-tab-pane>
-      <el-tab-pane label="团队成员" lazy>
-        <template #label> 团队成员<el-badge :value="2" class="item" type="primary" /> </template>
-        团队成员
-      </el-tab-pane>
+      <el-tab-pane label="跟进记录" lazy> 跟进记录</el-tab-pane>
       <el-tab-pane label="商机" lazy> 商机</el-tab-pane>
-      <el-tab-pane label="合同" lazy>
-        <template #label> 合同<el-badge :value="3" class="item" type="primary" /> </template>
-        合同
-      </el-tab-pane>
-      <el-tab-pane label="回款" lazy>
-        <template #label> 回款<el-badge :value="4" class="item" type="primary" /> </template>
-        回款
+      <el-tab-pane label="附件" lazy> 附件</el-tab-pane>
+      <!-- TODO wanwan 以下标签上的数量需要接口统计返回 -->
+      <el-tab-pane label="操作记录" lazy>
+        <template #label> 操作记录<el-badge :value="12" class="item" type="primary" /> </template>
+        操作记录
       </el-tab-pane>
-      <el-tab-pane label="回访" lazy> 回访</el-tab-pane>
-      <el-tab-pane label="发票" lazy> 发票</el-tab-pane>
     </el-tabs>
   </el-col>
 
@@ -105,10 +90,10 @@ import ContactBasicInfo from '@/views/crm/contact/detail/ContactBasicInfo.vue'
 import ContactDetails from '@/views/crm/contact/detail/ContactDetails.vue'
 import ContactForm from '@/views/crm/contact/ContactForm.vue'
 import { formatDate } from '@/utils/formatTime'
-import * as CustomerApi from '@/api/crm/customer'
 // TODO 芋艿:后面在 review 么?
 
-defineOptions({ name: 'ContactDetail' })
+defineOptions({ name: 'CrmContactDetail' })
+
 const { delView } = useTagsViewStore() // 视图操作
 const route = useRoute()
 const { currentRoute } = useRouter() // 路由

+ 30 - 48
src/views/crm/contact/index.vue

@@ -1,7 +1,6 @@
 <template>
   <ContentWrap>
     <!-- 搜索工作栏 -->
-    <!-- TODO zyna:筛选项,按照需求简化下 -->
     <el-form
       class="-mb-15px"
       :model="queryParams"
@@ -9,14 +8,22 @@
       :inline="true"
       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
+          placeholder="请选择客户"
+          value-key="id"
+          lable-key="name"
           @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+          clearable
+        >
+          <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="name">
         <el-input
@@ -55,9 +62,9 @@
           class="!w-240px"
         />
       </el-form-item>
-      <el-form-item label="微信" prop="webchat">
+      <el-form-item label="微信" prop="wechat">
         <el-input
-          v-model="queryParams.webchat"
+          v-model="queryParams.wechat"
           placeholder="请输入微信"
           clearable
           @keyup.enter="handleQuery"
@@ -74,8 +81,8 @@
         />
       </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 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('create')" v-hasPermi="['crm:contact:create']">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
@@ -97,32 +104,28 @@
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <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>
+          <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+            {{ scope.row.name }}
+          </el-link>
         </template>
       </el-table-column>
-      <el-table-column label="客户名称" fixed="left" align="center" prop="customerName" />
+      <el-table-column label="客户" fixed="left" align="center" prop="customerName" />
       <el-table-column label="性别" align="center" prop="sex">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
         </template>
       </el-table-column>
       <el-table-column label="职位" align="center" prop="post" />
-      <el-table-column label="是否关键决策人" align="center" prop="policyMakers">
+      <el-table-column label="是否关键决策人" align="center" prop="master">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.policyMakers" />
-        </template>
-      </el-table-column>
-      <el-table-column label="直属上级" align="center" prop="parentId">
-        <template #default="scope">
-          {{ allContactList.find((contact) => contact.id === scope.row.parentId)?.name }}
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
         </template>
       </el-table-column>
+      <el-table-column label="直属上级" align="center" prop="parentName" />
       <el-table-column label="手机号" align="center" prop="mobile" />
       <el-table-column label="座机" align="center" prop="telephone" />
       <el-table-column label="QQ" align="center" prop="qq" />
-      <el-table-column label="微信" align="center" prop="webchat" />
+      <el-table-column label="微信" align="center" prop="wechat" />
       <el-table-column label="邮箱" align="center" prop="email" />
       <el-table-column label="地址" align="center" prop="address" />
       <el-table-column
@@ -142,7 +145,7 @@
       />
       <el-table-column label="负责人" align="center" prop="ownerUserId">
         <template #default="scope">
-          {{ gotOwnerUser(scope.row.ownerUserId) }}
+          {{ scope.row.ownerUserName }}
         </template>
       </el-table-column>
       <!-- <el-table-column label="所属部门" align="center" prop="ownerUserId" /> -->
@@ -211,7 +214,6 @@ import download from '@/utils/download'
 import * as ContactApi from '@/api/crm/contact'
 import ContactForm from './ContactForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
-import * as UserApi from '@/api/system/user'
 import * as CustomerApi from '@/api/crm/customer'
 
 defineOptions({ name: 'CrmContact' })
@@ -222,6 +224,7 @@ 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,
@@ -239,13 +242,12 @@ const queryParams = reactive({
   name: null,
   post: null,
   qq: null,
-  webchat: null,
+  wechat: null,
   sex: null,
   policyMakers: null
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
-const userList = ref<UserApi.UserVO[]>([]) // 用户列表
 
 /** 查询列表 */
 const getList = async () => {
@@ -305,35 +307,15 @@ const handleExport = async () => {
   }
 }
 
-// TODO @zyna:这个负责人的读取,放在后端好点
-const gotOwnerUser = (owerUserId: string) => {
-  let ownerName = ''
-  if (owerUserId !== null) {
-    owerUserId.split(',').forEach((item: string, index: number) => {
-      if (index != 0) {
-        ownerName =
-          ownerName + ',' + userList.value.find((user: { id: any }) => user.id == item)?.nickname
-      } else {
-        ownerName = userList.value.find((user: { id: any }) => user.id == item)?.nickname || ''
-      }
-    })
-  }
-  return ownerName
-}
-
 /** 打开客户详情 */
 const { push } = useRouter()
 const openDetail = (id: number) => {
   push({ name: 'CrmContactDetail', params: { id } })
 }
 
-// TODO @zyna:这个上级的读取,放在后端读取,更合适;因为可能数据量比较大
-const allContactList = ref([]) //所有联系人列表
-const allCustomerList = ref([]) //客户列表
 /** 初始化 **/
 onMounted(async () => {
   await getList()
-  userList.value = await UserApi.getSimpleUserList()
-  allContactList.value = await ContactApi.simpleAlllist()
+  customerList.value = await CustomerApi.queryAllList()
 })
 </script>

+ 132 - 0
src/views/crm/contract/components/ContractList.vue

@@ -0,0 +1,132 @@
+<template>
+  <!-- 操作栏 -->
+  <el-row justify="end">
+    <el-button @click="openForm">
+      <Icon class="mr-5px" icon="clarity:contract-line" />
+      创建合同
+    </el-button>
+  </el-row>
+
+  <!-- 列表 -->
+  <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="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="no" />
+      <el-table-column label="客户名称" align="center" prop="customerName" />
+      <el-table-column
+        label="合同金额(元)"
+        align="center"
+        prop="price"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column
+        label="开始时间"
+        align="center"
+        prop="startTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column
+        label="结束时间"
+        align="center"
+        prop="endTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column align="center" label="状态" prop="auditStatus">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加 -->
+  <ContractForm ref="formRef" @success="getList" />
+</template>
+<script setup lang="ts">
+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'
+
+defineOptions({ name: 'CrmContractList' })
+const props = defineProps<{
+  bizType: number // 业务类型
+  bizId: number // 业务编号
+}>()
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  customerId: 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 ContractApi.getContractPageByCustomer(queryParams)
+        break
+      default:
+        return
+    }
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 添加 */
+const formRef = ref()
+const openForm = () => {
+  formRef.value.open('create')
+}
+
+/** 打开合同详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'CrmContractDetail', params: { id } })
+}
+
+/** 监听打开的 bizId + bizType,从而加载最新的列表 */
+watch(
+  () => [props.bizId, props.bizType],
+  () => {
+    handleQuery()
+  },
+  { immediate: true, deep: true }
+)
+</script>

+ 0 - 228
src/views/crm/contract/contract.data.ts

@@ -1,228 +0,0 @@
-import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
-import { dateFormatter } from '@/utils/formatTime'
-
-// 表单校验
-export const rules = reactive({
-  name: [required]
-})
-
-// TODO @dbh52:不使用 crud 模式哈,使用标准的 ep 代码哈;主要后续 crud schema 可能会改
-// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
-const crudSchemas = reactive<CrudSchema[]>([
-  {
-    label: '合同编号',
-    field: 'id',
-    isForm: false
-  },
-  {
-    label: '合同名称',
-    field: 'name',
-    isSearch: true
-  },
-  {
-    label: '客户编号',
-    field: 'customerId',
-    isSearch: true,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    }
-  },
-  {
-    label: '商机编号',
-    field: 'businessId',
-    isSearch: true,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    }
-  },
-  {
-    label: '工作流编号',
-    field: 'processInstanceId',
-    isSearch: true,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    }
-  },
-  {
-    label: '下单日期',
-    field: 'orderDate',
-    formatter: dateFormatter,
-    isSearch: true,
-    search: {
-      component: 'DatePicker',
-      componentProps: {
-        valueFormat: 'YYYY-MM-DD HH:mm:ss',
-        type: 'daterange',
-        defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
-      }
-    },
-    form: {
-      component: 'DatePicker',
-      componentProps: {
-        type: 'datetime',
-        valueFormat: 'x'
-      }
-    }
-  },
-  {
-    label: '负责人的用户编号',
-    field: 'ownerUserId',
-    isSearch: true,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    }
-  },
-  {
-    label: '创建时间',
-    field: 'createTime',
-    formatter: dateFormatter,
-    isSearch: true,
-    search: {
-      component: 'DatePicker',
-      componentProps: {
-        valueFormat: 'YYYY-MM-DD HH:mm:ss',
-        type: 'daterange',
-        defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
-      }
-    },
-    isForm: false
-  },
-  {
-    label: '合同编号',
-    field: 'no',
-    isSearch: true
-  },
-  {
-    label: '开始时间',
-    field: 'startTime',
-    formatter: dateFormatter,
-    isSearch: true,
-    search: {
-      component: 'DatePicker',
-      componentProps: {
-        valueFormat: 'YYYY-MM-DD HH:mm:ss',
-        type: 'daterange',
-        defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
-      }
-    },
-    form: {
-      component: 'DatePicker',
-      componentProps: {
-        type: 'datetime',
-        valueFormat: 'x'
-      }
-    }
-  },
-  {
-    label: '结束时间',
-    field: 'endTime',
-    formatter: dateFormatter,
-    isSearch: true,
-    search: {
-      component: 'DatePicker',
-      componentProps: {
-        valueFormat: 'YYYY-MM-DD HH:mm:ss',
-        type: 'daterange',
-        defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
-      }
-    },
-    form: {
-      component: 'DatePicker',
-      componentProps: {
-        type: 'datetime',
-        valueFormat: 'x'
-      }
-    }
-  },
-  {
-    label: '合同金额',
-    field: 'price',
-    isSearch: true,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    }
-  },
-  {
-    label: '整单折扣',
-    field: 'discountPercent',
-    isSearch: true,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    }
-  },
-  {
-    label: '产品总金额',
-    field: 'productPrice',
-    isSearch: true,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    }
-  },
-  {
-    label: '只读权限的用户编号数组',
-    field: 'roUserIds',
-    isSearch: true
-  },
-  {
-    label: '读写权限的用户编号数组',
-    field: 'rwUserIds',
-    isSearch: true
-  },
-  {
-    label: '联系人编号',
-    field: 'contactId',
-    isSearch: true,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    }
-  },
-  {
-    label: '备注',
-    field: 'remark',
-    isSearch: true
-  },
-  {
-    label: '公司签约人',
-    field: 'signUserId',
-    isSearch: true,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    }
-  },
-  {
-    label: '最后跟进时间',
-    field: 'contactLastTime',
-    formatter: dateFormatter,
-    isSearch: true,
-    search: {
-      component: 'DatePicker',
-      componentProps: {
-        valueFormat: 'YYYY-MM-DD HH:mm:ss',
-        type: 'daterange',
-        defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
-      }
-    },
-    form: {
-      component: 'DatePicker',
-      componentProps: {
-        type: 'datetime',
-        valueFormat: 'x'
-      }
-    }
-  },
-  {
-    label: '操作',
-    field: 'action',
-    isForm: false
-  }
-])
-export const { allSchemas } = useCrudSchemas(crudSchemas)

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels