Browse Source

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

owen 1 năm trước cách đây
mục cha
commit
1fb7701e6e
64 tập tin đã thay đổi với 1100 bổ sung548 xóa
  1. 1 1
      .vscode/settings.json
  2. 1 1
      src/api/bpm/task/index.ts
  3. 12 0
      src/api/crm/business/index.ts
  4. 16 1
      src/api/crm/contact/index.ts
  5. 0 47
      src/api/crm/contactbusinesslink/index.ts
  6. 11 1
      src/api/crm/customer/index.ts
  7. 0 0
      src/api/crm/customer/limitConfig/index.ts
  8. 2 3
      src/api/crm/customer/poolConfig/index.ts
  9. 39 0
      src/api/report/ureport/index.ts
  10. 26 0
      src/api/system/operatelog/index.ts
  11. 0 31
      src/components/AppLinkInput/data.ts
  12. 6 3
      src/components/DiyEditor/components/ComponentContainer.vue
  13. 4 1
      src/components/DiyEditor/components/ComponentContainerProperty.vue
  14. 4 1
      src/components/DiyEditor/components/ComponentLibrary.vue
  15. 1 0
      src/components/DiyEditor/components/mobile/CouponCard/component.tsx
  16. 1 0
      src/components/DiyEditor/components/mobile/CouponCard/config.ts
  17. 1 0
      src/components/DiyEditor/components/mobile/HotZone/config.ts
  18. 2 0
      src/components/DiyEditor/components/mobile/MagicCube/config.ts
  19. 1 0
      src/components/DiyEditor/components/mobile/MenuGrid/config.ts
  20. 1 0
      src/components/DiyEditor/components/mobile/MenuList/config.ts
  21. 0 1
      src/components/DiyEditor/components/mobile/NoticeBar/property.vue
  22. 1 0
      src/components/DiyEditor/components/mobile/ProductCard/config.ts
  23. 1 0
      src/components/DiyEditor/components/mobile/ProductCard/index.vue
  24. 1 0
      src/components/DiyEditor/components/mobile/ProductList/config.ts
  25. 1 0
      src/components/DiyEditor/components/mobile/PromotionArticle/index.vue
  26. 2 0
      src/components/DiyEditor/components/mobile/PromotionCombination/config.ts
  27. 1 0
      src/components/DiyEditor/components/mobile/SearchBar/config.ts
  28. 1 0
      src/components/DiyEditor/components/mobile/VideoPlayer/config.ts
  29. 14 5
      src/components/DiyEditor/index.vue
  30. 1 0
      src/components/DiyEditor/util.ts
  31. 3 0
      src/components/OperateLogV2/index.ts
  32. 175 0
      src/components/OperateLogV2/src/OperateLogV2.vue
  33. 36 39
      src/components/UploadFile/src/UploadFile.vue
  34. 8 4
      src/config/axios/service.ts
  35. 6 2
      src/router/modules/remaining.ts
  36. 11 12
      src/views/bpm/oa/leave/create.vue
  37. 47 32
      src/views/bpm/oa/leave/index.vue
  38. 5 4
      src/views/crm/business/BusinessForm.vue
  39. 75 2
      src/views/crm/business/components/BusinessList.vue
  40. 19 34
      src/views/crm/business/components/BusinessListModal.vue
  41. 29 23
      src/views/crm/contact/ContactForm.vue
  42. 1 11
      src/views/crm/contact/detail/ContactDetailsHeader.vue
  43. 1 0
      src/views/crm/contact/detail/ContactDetailsInfo.vue
  44. 7 3
      src/views/crm/contact/detail/index.vue
  45. 50 63
      src/views/crm/contact/index.vue
  46. 0 143
      src/views/crm/contactBusinessLink/components/BusinessListByContact.vue
  47. 10 10
      src/views/crm/customer/CustomerForm.vue
  48. 31 4
      src/views/crm/customer/detail/CustomerDetailsHeader.vue
  49. 13 12
      src/views/crm/customer/detail/CustomerDetailsInfo.vue
  50. 20 3
      src/views/crm/customer/detail/index.vue
  51. 25 21
      src/views/crm/customer/index.vue
  52. 9 9
      src/views/crm/customer/limitConfig/CustomerLimitConfigForm.vue
  53. 5 9
      src/views/crm/customer/limitConfig/CustomerLimitConfigList.vue
  54. 2 2
      src/views/crm/customer/limitConfig/index.vue
  55. 5 5
      src/views/crm/customer/poolConfig/index.vue
  56. 1 0
      src/views/crm/permission/components/PermissionForm.vue
  57. 1 0
      src/views/crm/permission/components/PermissionList.vue
  58. 2 2
      src/views/mall/product/spu/form/BasicInfoForm.vue
  59. 1 0
      src/views/mall/promotion/diy/page/decorate.vue
  60. 3 2
      src/views/mall/promotion/diy/template/decorate.vue
  61. 1 1
      src/views/mall/promotion/diy/template/index.vue
  62. 113 0
      src/views/report/ureport/UReportDataForm.vue
  63. 13 0
      src/views/report/ureport/index.vue
  64. 220 0
      src/views/report/ureport/index2.vue

+ 1 - 1
.vscode/settings.json

@@ -83,7 +83,7 @@
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "editor.codeActionsOnSave": {
-    "source.fixAll.eslint": true
+    "source.fixAll.eslint": "explicit"
   },
   "[vue]": {
     "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"

+ 1 - 1
src/api/bpm/task/index.ts

@@ -44,7 +44,7 @@ export const exportTask = async (params) => {
 
 // 获取所有可回退的节点
 export const getReturnList = async (params) => {
-  return await request.get({ url: '/bpm/task/get-return-list', params })
+  return await request.get({ url: '/bpm/task/return-list', params })
 }
 
 // 回退

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

@@ -1,3 +1,10 @@
+/*
+ * @Author: zyna
+ * @Date: 2023-12-02 13:08:56
+ * @LastEditTime: 2023-12-17 16:28:20
+ * @FilePath: \yudao-ui-admin-vue3\src\api\crm\business\index.ts
+ * @Description: 
+ */
 import request from '@/config/axios'
 
 export interface BusinessVO {
@@ -55,3 +62,8 @@ export const deleteBusiness = async (id: number) => {
 export const exportBusiness = async (params) => {
   return await request.download({ url: `/crm/business/export-excel`, params })
 }
+
+//联系人关联商机列表
+export const getBusinessPageByContact = async (params) => {
+  return await request.get({ url: `/crm/business/page-by-contact`, params })
+}

+ 16 - 1
src/api/crm/contact/index.ts

@@ -8,7 +8,7 @@ export interface ContactVO {
   email: string
   post: string
   customerId: number
-  address: string
+  detailAddress: string
   remark: string
   ownerUserId: string
   lastTime: Date
@@ -26,6 +26,11 @@ export interface ContactVO {
   ownerUserName: string
 }
 
+export interface ContactBusinessReqVO {
+  contactId: number
+  businessIds: number[]
+}
+
 // 查询 CRM 联系人列表
 export const getContactPage = async (params) => {
   return await request.get({ url: `/crm/contact/page`, params })
@@ -65,3 +70,13 @@ export const exportContact = async (params) => {
 export const getSimpleContactList = async () => {
   return await request.get({ url: `/crm/contact/simple-all-list` })
 }
+
+// 批量新增联系人商机关联
+export const createContactBusinessList = async (data: ContactBusinessReqVO) => {
+  return await request.post({ url: `/crm/contact/create-business-list`, data })
+}
+
+// 解除联系人商机关联
+export const deleteContactBusinessList = async (data: ContactBusinessReqVO) => {
+  return await request.delete({ url: `/crm/contact/delete-business-list`, data })
+}

+ 0 - 47
src/api/crm/contactbusinesslink/index.ts

@@ -1,47 +0,0 @@
-import request from '@/config/axios'
-
-export interface ContactBusinessLinkVO {
-  id: number
-  contactId: number
-  businessId: number
-}
-
-// 查询联系人商机关联分页
-export const getContactBusinessLinkPage = async (params) => {
-  return await request.get({ url: `/crm/contact-business-link/page`, params })
-}
-
-// 查询联系人商机关联详情
-export const getContactBusinessLink = async (id: number) => {
-  return await request.get({ url: `/crm/contact-business-link/get?id=` + id })
-}
-
-// 新增联系人商机关联
-export const createContactBusinessLink = async (data: ContactBusinessLinkVO) => {
-  return await request.post({ url: `/crm/contact-business-link/create`, data })
-}
-
-// 修改联系人商机关联
-export const updateContactBusinessLink = async (data: ContactBusinessLinkVO) => {
-  return await request.put({ url: `/crm/contact-business-link/update`, data })
-}
-
-// 删除联系人商机关联
-export const deleteContactBusinessLink = async (data: ContactBusinessLinkVO) => {
-  return await request.delete({ url: `/crm/contact-business-link/delete-batch`, data })
-}
-
-// 导出联系人商机关联 Excel
-export const exportContactBusinessLink = async (params) => {
-  return await request.download({ url: `/crm/contact-business-link/export-excel`, params })
-}
-
-//批量新增联系人商机关联
-export const createContactBusinessLinkBatch = async (data: ContactBusinessLinkVO[]) => {
-  return await request.post({ url: `/crm/contact-business-link/create-batch`, data })
-}
-// 查询联系人关联商机列表
-export const getBusinessByContactPage = async (params) => {
-  return await request.get({ url: `/crm/contact-business-link/page-by-contact`, params })
-}
-// TODO @zyna:根据后端,调整下请求

+ 11 - 1
src/api/crm/customer/index.ts

@@ -59,7 +59,7 @@ export const deleteCustomer = async (id: number) => {
 }
 
 // 导出客户 Excel
-export const exportCustomer = async (params) => {
+export const exportCustomer = async (params: any) => {
   return await request.download({ url: `/crm/customer/export-excel`, params })
 }
 
@@ -67,3 +67,13 @@ export const exportCustomer = async (params) => {
 export const queryAllList = async () => {
   return await request.get({ url: `/crm/customer/query-all-list` })
 }
+
+// 查询客户操作日志
+export const getOperateLogPage = async (params: any) => {
+  return await request.get({ url: '/crm/customer/operate-log-page', params })
+}
+
+// 锁定/解锁客户
+export const lockCustomer = async (id: number, lockStatus: boolean) => {
+  return await request.put({ url: `/crm/customer/lock`, data: { id, lockStatus } })
+}

+ 0 - 0
src/api/crm/customerLimitConfig/index.ts → src/api/crm/customer/limitConfig/index.ts


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

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

+ 39 - 0
src/api/report/ureport/index.ts

@@ -0,0 +1,39 @@
+import request from '@/config/axios'
+
+export interface UReportDataVO {
+  id: number
+  name: string
+  status: number
+  content: string
+  remark: string
+}
+
+// 查询Ureport2报表分页
+export const getUReportDataPage = async (params) => {
+  return await request.get({ url: `/report/ureport-data/page`, params })
+}
+
+// 查询Ureport2报表详情
+export const getUReportData = async (id: number) => {
+  return await request.get({ url: `/report/ureport-data/get?id=` + id })
+}
+
+// 新增Ureport2报表
+export const createUReportData = async (data: UReportDataVO) => {
+  return await request.post({ url: `/report/ureport-data/create`, data })
+}
+
+// 修改Ureport2报表
+export const updateUReportData = async (data: UReportDataVO) => {
+  return await request.put({ url: `/report/ureport-data/update`, data })
+}
+
+// 删除Ureport2报表
+export const deleteUReportData = async (id: number) => {
+  return await request.delete({ url: `/report/ureport-data/delete?id=` + id })
+}
+
+// 导出Ureport2报表 Excel
+export const exportUReportData = async (params) => {
+  return await request.download({ url: `/report/ureport-data/export-excel`, params })
+}

+ 26 - 0
src/api/system/operatelog/index.ts

@@ -23,6 +23,32 @@ export type OperateLogVO = {
   resultData: string
 }
 
+export type OperateLogV2VO = {
+  id: number
+  userNickname: string
+  traceId: string
+  userType: number
+  userId: number
+  userName: string
+  type: string
+  subType: string
+  bizId: number
+  action: string
+  extra: string
+  requestMethod: string
+  requestUrl: string
+  userIp: string
+  userAgent: string
+  creator: string
+  creatorName: string
+  createTime: Date
+  // 数据扩展,渲染时使用
+  title: string // 操作标题(如果为空则取 name 值)
+  colSize: number // 变更记录行数
+  contentStrList: string[]
+  tagsContentList: string[]
+}
+
 // 查询操作日志列表
 export const getOperateLogPage = (params: PageParam) => {
   return request.get({ url: '/system/operate-log/page', params })

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

@@ -35,8 +35,6 @@ export const enum APP_LINK_TYPE_ENUM {
   PRODUCT_DETAIL_NORMAL,
   // 拼团商品详情
   PRODUCT_DETAIL_COMBINATION,
-  // 积分商品详情
-  PRODUCT_DETAIL_POINT,
   // 秒杀商品详情
   PRODUCT_DETAIL_SECKILL
 }
@@ -80,10 +78,6 @@ export const APP_LINK_GROUP_LIST = [
         name: '系统设置',
         path: '/pages/public/setting'
       },
-      {
-        name: '问题反馈',
-        path: '/pages/public/feedback'
-      },
       {
         name: '常见问题',
         path: '/pages/public/faq'
@@ -112,11 +106,6 @@ export const APP_LINK_GROUP_LIST = [
         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
       }
     ]
   },
@@ -145,10 +134,6 @@ export const APP_LINK_GROUP_LIST = [
         name: '签到中心',
         path: '/pages/app/sign'
       },
-      {
-        name: '积分商城',
-        path: '/pages/app/score-shop'
-      },
       {
         name: '优惠券中心',
         path: '/pages/coupon/list'
@@ -172,10 +157,6 @@ export const APP_LINK_GROUP_LIST = [
         name: '分销中心',
         path: '/pages/commission/index'
       },
-      {
-        name: '申请分销商',
-        path: '/pages/commission/apply'
-      },
       {
         name: '推广商品',
         path: '/pages/commission/goods'
@@ -184,10 +165,6 @@ export const APP_LINK_GROUP_LIST = [
         name: '分销订单',
         path: '/pages/commission/order'
       },
-      {
-        name: '分享记录',
-        path: '/pages/commission/share-log'
-      },
       {
         name: '我的团队',
         path: '/pages/commission/team'
@@ -208,10 +185,6 @@ export const APP_LINK_GROUP_LIST = [
       {
         name: '申请提现',
         path: '/pages/pay/withdraw'
-      },
-      {
-        name: '提现记录',
-        path: '/pages/pay/withdraw-log'
       }
     ]
   },
@@ -242,10 +215,6 @@ export const APP_LINK_GROUP_LIST = [
         name: '地址管理',
         path: '/pages/user/address/list'
       },
-      {
-        name: '发票管理',
-        path: '/pages/user/invoice/list'
-      },
       {
         name: '用户佣金',
         path: '/pages/user/wallet/commission'

+ 6 - 3
src/components/DiyEditor/components/ComponentContainer.vue

@@ -8,11 +8,11 @@
       <component :is="component.id" :property="component.property" />
     </div>
     <div class="component-wrap">
-      <!-- 左侧组件名 -->
+      <!-- 左侧组件名(悬浮的小贴条) -->
       <div class="component-name" v-if="component.name">
         {{ component.name }}
       </div>
-      <!-- 侧:组件操作工具栏 -->
+      <!-- 侧:组件操作工具栏 -->
       <div class="component-toolbar" v-if="showToolbar && component.name && active">
         <VerticalButtonGroup type="primary">
           <el-tooltip content="上移" placement="right">
@@ -54,7 +54,7 @@ import { propTypes } from '@/utils/propTypes'
 import { object } from 'vue-types'
 
 /**
- * 组件容器
+ * 组件容器:目前在中间部分
  * 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式
  */
 defineOptions({ name: 'ComponentContainer' })
@@ -100,6 +100,7 @@ const emits = defineEmits<{
   (e: 'copy'): void
   (e: 'delete'): void
 }>()
+
 /**
  * 移动组件
  * @param direction 移动方向
@@ -107,12 +108,14 @@ const emits = defineEmits<{
 const handleMoveComponent = (direction: number) => {
   emits('move', direction)
 }
+
 /**
  * 复制组件
  */
 const handleCopyComponent = () => {
   emits('copy')
 }
+
 /**
  * 删除组件
  */

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

@@ -1,8 +1,11 @@
 <template>
   <el-tabs stretch>
+    <!-- 每个组件的自定义内容 -->
     <el-tab-pane label="内容" v-if="$slots.default">
       <slot></slot>
     </el-tab-pane>
+
+    <!-- 每个组件的通用内容 -->
     <el-tab-pane label="样式" lazy>
       <el-card header="组件样式" class="property-group">
         <el-form :model="formData" label-width="80px">
@@ -51,7 +54,7 @@
 import { ComponentStyle, usePropertyForm } from '@/components/DiyEditor/util'
 
 /**
- * 组件容器属性
+ * 组件容器属性:目前右边部分
  * 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式
  */
 defineOptions({ name: 'ComponentContainer' })

+ 4 - 1
src/components/DiyEditor/components/ComponentLibrary.vue

@@ -41,16 +41,19 @@ import { componentConfigs } from '../components/mobile/index'
 import { cloneDeep } from 'lodash-es'
 import { DiyComponent, DiyComponentLibrary } from '@/components/DiyEditor/util'
 
-/** 组件库 */
+/** 组件库:目前左侧的【基础组件】、【图文组件】部分 */
 defineOptions({ name: 'ComponentLibrary' })
 
 // 组件列表
 const props = defineProps<{
   list: DiyComponentLibrary[]
 }>()
+// 组件分组
 const groups = reactive<any[]>([])
 // 展开的折叠面板
 const extendGroups = reactive<string[]>([])
+
+// 监听 list 属性,按照 DiyComponentLibrary 的 name 分组
 watch(
   () => props.list,
   () => {

+ 1 - 0
src/components/DiyEditor/components/mobile/CouponCard/component.tsx

@@ -4,6 +4,7 @@ import { floatToFixed2 } from '@/utils'
 import { formatDate } from '@/utils/formatTime'
 
 // 优惠值
+// TODO @疯狂:idea 有告警
 export const CouponDiscount = defineComponent({
   name: 'CouponDiscount',
   props: {

+ 1 - 0
src/components/DiyEditor/components/mobile/CouponCard/config.ts

@@ -24,6 +24,7 @@ export interface CouponCardProperty {
 }
 
 // 定义组件
+// TODO @疯狂:idea 有告警
 export const component = {
   id: 'CouponCard',
   name: '优惠券',

+ 1 - 0
src/components/DiyEditor/components/mobile/HotZone/config.ts

@@ -9,6 +9,7 @@ export interface HotZoneProperty {
   // 组件样式
   style: ComponentStyle
 }
+
 /** 热区项目属性 */
 export interface HotZoneItemProperty {
   // 链接的名称

+ 2 - 0
src/components/DiyEditor/components/mobile/MagicCube/config.ts

@@ -13,6 +13,7 @@ export interface MagicCubeProperty {
   // 组件样式
   style: ComponentStyle
 }
+
 /** 广告魔方项目属性 */
 export interface MagicCubeItemProperty {
   // 图标链接
@@ -30,6 +31,7 @@ export interface MagicCubeItemProperty {
 }
 
 // 定义组件
+// TODO @疯狂:有 idea 爆红告警
 export const component = {
   id: 'MagicCube',
   name: '广告魔方',

+ 1 - 0
src/components/DiyEditor/components/mobile/MenuGrid/config.ts

@@ -10,6 +10,7 @@ export interface MenuGridProperty {
   // 组件样式
   style: ComponentStyle
 }
+
 /** 宫格导航项目属性 */
 export interface MenuGridItemProperty {
   // 图标链接

+ 1 - 0
src/components/DiyEditor/components/mobile/MenuList/config.ts

@@ -8,6 +8,7 @@ export interface MenuListProperty {
   // 组件样式
   style: ComponentStyle
 }
+
 /** 列表导航项目属性 */
 export interface MenuListItemProperty {
   // 图标链接

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

@@ -12,7 +12,6 @@
       <el-form-item label="文字颜色" prop="文字颜色">
         <ColorInput v-model="formData.textColor" />
       </el-form-item>
-
       <el-card header="公告内容" class="property-group" shadow="never">
         <Draggable v-model="formData.contents">
           <template #default="{ element }">

+ 1 - 0
src/components/DiyEditor/components/mobile/ProductCard/config.ts

@@ -59,6 +59,7 @@ export interface ProductCardFieldProperty {
 }
 
 // 定义组件
+// TODO @疯狂:idea 有告警
 export const component = {
   id: 'ProductCard',
   name: '商品卡片',

+ 1 - 0
src/components/DiyEditor/components/mobile/ProductCard/index.vue

@@ -134,6 +134,7 @@ watch(
     deep: true
   }
 )
+
 /**
  * 计算商品的间距
  * @param index 商品索引

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

@@ -38,6 +38,7 @@ export interface ProductListFieldProperty {
 }
 
 // 定义组件
+// TODO @疯狂:idea 有告警
 export const component = {
   id: 'ProductList',
   name: '商品栏',

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

@@ -6,6 +6,7 @@ import { PromotionArticleProperty } from './config'
 import * as ArticleApi from '@/api/mall/promotion/article/index'
 
 /** 营销文章 */
+// TODO @疯狂:idea 有告警
 defineOptions({ name: 'PromotionArticle' })
 // 定义属性
 const props = defineProps<{ property: PromotionArticleProperty }>()

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

@@ -29,6 +29,7 @@ export interface PromotionCombinationProperty {
   // 组件样式
   style: ComponentStyle
 }
+
 // 商品字段
 export interface PromotionCombinationFieldProperty {
   // 是否显示
@@ -38,6 +39,7 @@ export interface PromotionCombinationFieldProperty {
 }
 
 // 定义组件
+// TODO @疯狂:idea 有告警
 export const component = {
   id: 'PromotionCombination',
   name: '拼团',

+ 1 - 0
src/components/DiyEditor/components/mobile/SearchBar/config.ts

@@ -17,6 +17,7 @@ export interface SearchProperty {
 export type PlaceholderPosition = 'left' | 'center'
 
 // 定义组件
+// TODO @疯狂:idea 这里爆红,可以卡看咋优化下哇:is missing the following properties from type DiyComponent<SearchProperty>: uid, position
 export const component = {
   id: 'SearchBar',
   name: '搜索框',

+ 1 - 0
src/components/DiyEditor/components/mobile/VideoPlayer/config.ts

@@ -19,6 +19,7 @@ export interface VideoPlayerStyle extends ComponentStyle {
 }
 
 // 定义组件
+// TODO @疯狂:idea 有告警
 export const component = {
   id: 'VideoPlayer',
   name: '视频播放',

+ 14 - 5
src/components/DiyEditor/index.vue

@@ -27,11 +27,12 @@
         </el-tooltip>
       </el-button-group>
     </el-header>
+
     <!-- 中心区域 -->
     <el-container class="editor-container">
-      <!-- 左侧:组件库 -->
+      <!-- 左侧:组件库(ComponentLibrary) -->
       <ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" />
-      <!-- 中心设计区域 -->
+      <!-- 中心设计区域(ComponentContainer) -->
       <div class="editor-center page-prop-area" @click="handlePageSelected">
         <!-- 手机顶部 -->
         <div class="editor-design-top">
@@ -132,7 +133,7 @@
           </template>
         </div>
       </div>
-      <!-- 右侧属性面板 -->
+      <!-- 右侧属性面板(ComponentContainerProperty) -->
       <el-aside class="editor-right" width="350px" v-if="selectedComponent?.property">
         <el-card
           shadow="never"
@@ -160,6 +161,7 @@
       </el-aside>
     </el-container>
   </el-container>
+
   <!-- 预览弹框 -->
   <Dialog v-model="previewDialogVisible" title="预览" width="700">
     <div class="flex justify-around">
@@ -231,6 +233,7 @@ const props = defineProps({
 })
 
 // 监听传入的页面配置
+// 解析出 pageConfigComponent 页面整体的配置,navigationBarComponent、pageComponents、tabBarComponent 页面上、中、下的配置
 watch(
   () => props.modelValue,
   () => {
@@ -251,6 +254,7 @@ watch(
     immediate: true
   }
 )
+
 // 保存
 const handleSave = () => {
   const pageConfig = {
@@ -303,7 +307,7 @@ const handleTabBarSelected = () => {
   handleComponentSelected(unref(tabBarComponent))
 }
 
-// 组件变动
+// 组件变动(拖拽)
 const handleComponentChange = (dragEvent: any) => {
   // 新增,即从组件库拖拽添加组件
   if (dragEvent.added) {
@@ -327,19 +331,21 @@ const swapComponent = (oldIndex: number, newIndex: number) => {
   selectedComponentIndex.value = newIndex
 }
 
-/** 移动组件 */
+/** 移动组件(上移、下移) */
 const handleMoveComponent = (index: number, direction: number) => {
   const newIndex = index + direction
   if (newIndex < 0 || newIndex >= pageComponents.value.length) return
 
   swapComponent(index, newIndex)
 }
+
 /** 复制组件 */
 const handleCopyComponent = (index: number) => {
   const component = cloneDeep(pageComponents.value[index])
   component.uid = new Date().getTime()
   pageComponents.value.splice(index + 1, 0, component)
 }
+
 /**
  * 删除组件
  * @param index 当前组件index
@@ -371,6 +377,7 @@ const handleReset = () => {
   if (reload) reload()
   emits('reset')
 }
+
 // 预览
 const previewDialogVisible = ref(false)
 const handlePreview = () => {
@@ -388,10 +395,12 @@ const setDefaultSelectedComponent = () => {
     selectedComponent.value = unref(tabBarComponent)
   }
 }
+
 watch(
   () => [props.showPageConfig, props.showNavigationBar, props.showTabBar],
   () => setDefaultSelectedComponent()
 )
+
 onMounted(() => setDefaultSelectedComponent())
 </script>
 <style lang="scss" scoped>

+ 1 - 0
src/components/DiyEditor/util.ts

@@ -103,6 +103,7 @@ export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: R
     }
   )
 
+  // TODO @疯狂:这个 idea 爆红,看看怎么可以解决哈
   return { formData }
 }
 

+ 3 - 0
src/components/OperateLogV2/index.ts

@@ -0,0 +1,3 @@
+import OperateLogV2 from './src/OperateLogV2.vue'
+
+export { OperateLogV2 }

+ 175 - 0
src/components/OperateLogV2/src/OperateLogV2.vue

@@ -0,0 +1,175 @@
+<template>
+  <div class="p-20px">
+    <el-timeline>
+      <el-timeline-item
+        v-for="(log, index) in logDataList"
+        :key="index"
+        :timestamp="formatDate(log.createTime)"
+        placement="top"
+      >
+        <div class="el-timeline-right-content">
+          <el-row>
+            <el-col :span="24" class="mb-10px">
+              =======================
+              <el-tag class="mr-10px" type="success">{{ log.userName }}</el-tag>
+              <span>{{ log.title }}</span>
+              =======================
+            </el-col>
+            <!-- 先处理一下有几行-->
+            <template v-for="colNum in log.colSize" :key="colNum + 'col'">
+              <el-col :span="24" class="mb-10px">
+                <!-- 处理每一行-->
+                <template
+                  v-for="(tagVal, index2) in log.tagsContentList.slice(
+                    (colNum - 1) * 3,
+                    3 * colNum
+                  )"
+                  :key="index2"
+                >
+                  <el-tag class="mx-10px"> {{ tagVal }}</el-tag>
+                  <span>{{ log.contentStrList[index2] }}</span>
+                </template>
+              </el-col>
+            </template>
+          </el-row>
+        </div>
+        <template #dot>
+          <span :style="{ backgroundColor: getUserTypeColor(log.userType) }" class="dot-node-style">
+            {{ getDictLabel(DICT_TYPE.USER_TYPE, log.userType)[0] }}
+          </span>
+        </template>
+      </el-timeline-item>
+    </el-timeline>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { OperateLogV2VO } from '@/api/system/operatelog'
+import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
+import { ElTag } from 'element-plus'
+
+defineOptions({ name: 'OperateLogV2' })
+
+const props = defineProps<{
+  logList: OperateLogV2VO[] // 操作日志列表
+}>()
+
+const logDataList = ref<OperateLogV2VO[]>([]) // 操作日志列表
+
+/** 获得 userType 颜色 */
+const getUserTypeColor = (type: number) => {
+  const dict = getDictObj(DICT_TYPE.USER_TYPE, type)
+  switch (dict?.colorType) {
+    case 'success':
+      return '#67C23A'
+    case 'info':
+      return '#909399'
+    case 'warning':
+      return '#E6A23C'
+    case 'danger':
+      return '#F56C6C'
+  }
+  return '#409EFF'
+}
+
+// 提取 tag 所需内容和位置
+const renderTags = (content: string) => {
+  let newStr = unref(content).slice() // 去掉引用
+  newStr = newStr.replaceAll('【】', '【空】').replaceAll(';', '') // 处理掉分号 特殊:处理一下空的情况
+  const regex = /【([^【】]+)】/g
+  const fg = '|' // 原始位置替换符号
+  let match: any[] | null
+  let matchStr: string[] = []
+  let oldStr: string[] = []
+  while ((match = regex.exec(newStr)) !== null) {
+    matchStr.push(match[1]) // 提取值
+    oldStr.push(match[0]) // 原值
+  }
+  // 为什么重新循环不放在 while 中一起是因为替换重新赋值过后 match 值就不准确了
+  oldStr.forEach((item) => {
+    newStr = newStr.replace(item, fg)
+  })
+  return [newStr.split(fg), matchStr]
+}
+
+const initLog = () => {
+  logDataList.value = props.logList.map((logItem) => {
+    const keyValue = renderTags(logItem.action)
+    // 挂载数据
+    logItem.contentStrList = keyValue[0]
+    if (keyValue[0][0] === '从') {
+      logItem.title = logItem.subType
+    } else {
+      logItem.title = keyValue[0][0]
+      logItem.contentStrList.splice(0, 1)
+    }
+    logItem.colSize = keyValue[0].length / 3 // 变更记录行数
+    logItem.tagsContentList = keyValue[1]
+    return logItem
+  })
+}
+
+watch(
+  () => props.logList.length,
+  (newObj) => {
+    if (newObj) {
+      initLog()
+      console.log(logDataList.value)
+    }
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+</script>
+
+<style lang="scss" scoped>
+// 时间线样式调整
+:deep(.el-timeline) {
+  margin: 10px 0 0 160px;
+
+  .el-timeline-item__wrapper {
+    position: relative;
+    top: -20px;
+
+    .el-timeline-item__timestamp {
+      position: absolute !important;
+      top: 10px;
+      left: -150px;
+    }
+  }
+
+  .el-timeline-right-content {
+    display: flex;
+    align-items: center;
+    min-height: 30px;
+    padding: 10px;
+    background-color: #fff;
+
+    &::before {
+      position: absolute;
+      top: 10px;
+      left: 13px; /* 将伪元素水平居中 */
+      border-color: transparent #fff transparent transparent; /* 尖角颜色,左侧朝向 */
+      border-style: solid;
+      border-width: 8px; /* 调整尖角大小 */
+      content: ''; /* 必须设置 content 属性 */
+    }
+  }
+}
+
+.dot-node-style {
+  position: absolute;
+  left: -5px;
+  display: flex;
+  width: 20px;
+  height: 20px;
+  font-size: 10px;
+  color: #fff;
+  border-radius: 50%;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 36 - 39
src/components/UploadFile/src/UploadFile.vue

@@ -2,25 +2,27 @@
   <div class="upload-file">
     <el-upload
       ref="uploadRef"
-      :multiple="props.limit > 1"
-      name="file"
-      v-model="valueRef"
       v-model:file-list="fileList"
-      :show-file-list="true"
-      :auto-upload="autoUpload"
       :action="updateUrl"
+      :auto-upload="autoUpload"
+      :before-upload="beforeUpload"
+      :drag="drag"
       :headers="uploadHeaders"
       :limit="props.limit"
-      :drag="drag"
-      :before-upload="beforeUpload"
-      :on-exceed="handleExceed"
-      :on-success="handleFileSuccess"
+      :multiple="props.limit > 1"
       :on-error="excelUploadError"
-      :on-remove="handleRemove"
+      :on-exceed="handleExceed"
       :on-preview="handlePreview"
+      :on-remove="handleRemove"
+      :on-success="handleFileSuccess"
+      :show-file-list="true"
       class="upload-file-uploader"
+      name="file"
     >
-      <el-button type="primary"><Icon icon="ep:upload-filled" />选取文件</el-button>
+      <el-button type="primary">
+        <Icon icon="ep:upload-filled" />
+        选取文件
+      </el-button>
       <template v-if="isShowTip" #tip>
         <div style="font-size: 8px">
           大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
@@ -35,8 +37,8 @@
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
 import { getAccessToken, getTenantId } from '@/utils/auth'
-import type { UploadInstance, UploadUserFile, UploadProps, UploadRawFile } from 'element-plus'
-import { isArray, isString } from '@/utils/is'
+import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
+import { isString } from '@/utils/is'
 
 defineOptions({ name: 'UploadFile' })
 
@@ -54,8 +56,8 @@ const props = defineProps({
   drag: propTypes.bool.def(false), // 拖拽上传
   isShowTip: propTypes.bool.def(true) // 是否显示提示
 })
+
 // ========== 上传相关 ==========
-const valueRef = ref(props.modelValue)
 const uploadRef = ref<UploadInstance>()
 const uploadList = ref<UploadUserFile[]>([])
 const fileList = ref<UploadUserFile[]>([])
@@ -64,6 +66,7 @@ const uploadHeaders = ref({
   Authorization: 'Bearer ' + getAccessToken(),
   'tenant-id': getTenantId()
 })
+
 // 文件上传之前判断
 const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
   if (fileList.value.length >= props.limit) {
@@ -97,12 +100,10 @@ const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
 // 文件上传成功
 const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
   message.success('上传成功')
-  const fileListNew = fileList.value
-  fileListNew.pop()
-  fileList.value = fileListNew
+  fileList.value.shift()
   uploadList.value.push({ name: res.data, url: res.data })
   if (uploadList.value.length == uploadNumber.value) {
-    fileList.value = fileList.value.concat(uploadList.value)
+    fileList.value.push(...uploadList.value)
     uploadList.value = []
     uploadNumber.value = 0
     emitUpdateModelValue()
@@ -131,29 +132,25 @@ const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
 // 监听模型绑定值变动
 watch(
   () => props.modelValue,
-  () => {
-    const files: string[] = []
+  (val: string | string[]) => {
+    if (!val) {
+      fileList.value = [] // fix:处理掉缓存,表单重置后上传组件的内容并没有重置
+      return
+    }
+
+    fileList.value = [] // 保障数据为空
     // 情况1:字符串
-    if (isString(props.modelValue)) {
-      // 情况1.1:逗号分隔的多值
-      if (props.modelValue.includes(',')) {
-        files.concat(props.modelValue.split(','))
-      } else if (props.modelValue.length > 0) {
-        files.push(props.modelValue)
-      }
-    } else if (isArray(props.modelValue)) {
-      // 情况2:字符串
-      files.concat(props.modelValue)
-    } else if (props.modelValue == null) {
-      // 情况3:undefined 不处理
-    } else {
-      throw new Error('不支持的 modelValue 类型')
+    if (isString(val)) {
+      fileList.value.push(
+        ...val.split(',').map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
+      )
     }
-    fileList.value = files.map((url: string) => {
-      return { url, name: url.substring(url.lastIndexOf('/') + 1) } as UploadUserFile
-    })
+    // 情况2:数组
+    fileList.value.push(
+      ...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
+    )
   },
-  { immediate: true }
+  { immediate: true, deep: true }
 )
 // 发送文件链接列表更新
 const emitUpdateModelValue = () => {
@@ -166,7 +163,7 @@ const emitUpdateModelValue = () => {
   emit('update:modelValue', result)
 }
 </script>
-<style scoped lang="scss">
+<style lang="scss" scoped>
 .upload-file-uploader {
   margin-bottom: 5px;
 }

+ 8 - 4
src/config/axios/service.ts

@@ -88,7 +88,7 @@ service.interceptors.request.use(
 // response 拦截器
 service.interceptors.response.use(
   async (response: AxiosResponse<any>) => {
-    const { data } = response
+    let { data } = response
     const config = response.config
     if (!data) {
       // 返回“[HTTP]请求没有返回值”;
@@ -96,14 +96,18 @@ service.interceptors.response.use(
     }
     const { t } = useI18n()
     // 未设置状态码则默认成功状态
-    const code = data.code || result_code
-    // 二进制数据则直接返回
+    // 二进制数据则直接返回,例如说 Excel 导出
     if (
       response.request.responseType === 'blob' ||
       response.request.responseType === 'arraybuffer'
     ) {
-      return response.data
+      // 注意:如果导出的响应为 json,说明可能失败了,不直接返回进行下载
+      if (response.data.type !== 'application/json') {
+        return response.data
+      }
+      data = await new Response(response.data).json()
     }
+    const code = data.code || result_code
     // 获取错误信息
     const msg = data.msg || errorCode[code] || errorCode['default']
     if (ignoreMsgs.indexOf(msg) !== -1) {

+ 6 - 2
src/router/modules/remaining.ts

@@ -472,7 +472,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
         meta: {
           title: '模板装修',
           noCache: true,
-          hidden: true
+          hidden: true,
+          // TODO @疯狂:建议 menu 那的 /mall/promotion/diy-template/diy-template 改成 /mall/promotion/diy/template
+          activeMenu: '/mall/promotion/diy-template/diy-template'
         },
         component: () => import('@/views/mall/promotion/diy/template/decorate.vue')
       },
@@ -482,7 +484,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
         meta: {
           title: '页面装修',
           noCache: true,
-          hidden: true
+          hidden: true,
+          // TODO @疯狂:建议 menu 那的 /mall/promotion/diy-template/diy-page 改成 /mall/promotion/diy/page
+          activeMenu: '/mall/promotion/diy-template/diy-page'
         },
         component: () => import('@/views/mall/promotion/diy/page/decorate.vue')
       }

+ 11 - 12
src/views/bpm/oa/leave/create.vue

@@ -1,13 +1,13 @@
 <template>
   <el-form
     ref="formRef"
+    v-loading="formLoading"
     :model="formData"
     :rules="formRules"
     label-width="80px"
-    v-loading="formLoading"
   >
     <el-form-item label="请假类型" prop="type">
-      <el-select v-model="formData.type" placeholder="请选择请假类型" clearable>
+      <el-select v-model="formData.type" clearable placeholder="请选择请假类型">
         <el-option
           v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
           :key="dict.value"
@@ -18,27 +18,27 @@
     </el-form-item>
     <el-form-item label="开始时间" prop="startTime">
       <el-date-picker
-        clearable
         v-model="formData.startTime"
+        clearable
+        placeholder="请选择开始时间"
         type="datetime"
         value-format="x"
-        placeholder="请选择开始时间"
       />
     </el-form-item>
     <el-form-item label="结束时间" prop="endTime">
       <el-date-picker
-        clearable
         v-model="formData.endTime"
+        clearable
+        placeholder="请选择结束时间"
         type="datetime"
         value-format="x"
-        placeholder="请选择结束时间"
       />
     </el-form-item>
     <el-form-item label="原因" prop="reason">
-      <el-input v-model="formData.reason" type="textarea" placeholder="请输请假原因" />
+      <el-input v-model="formData.reason" placeholder="请输请假原因" type="textarea" />
     </el-form-item>
     <el-form-item>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
     </el-form-item>
   </el-form>
 </template>
@@ -50,9 +50,6 @@ import { useTagsViewStore } from '@/store/modules/tagsView'
 defineOptions({ name: 'BpmOALeaveCreate' })
 
 const message = useMessage() // 消息弹窗
-const { delView } = useTagsViewStore() // 视图操作
-const { currentRoute } = useRouter() // 路由
-
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formData = ref({
   type: undefined,
@@ -67,7 +64,8 @@ const formRules = reactive({
   endTime: [{ required: true, message: '请假结束时间不能为空', trigger: 'change' }]
 })
 const formRef = ref() // 表单 Ref
-
+const { delView } = useTagsViewStore() // 视图操作
+const { push, currentRoute } = useRouter() // 路由
 /** 提交表单 */
 const submitForm = async () => {
   // 校验表单
@@ -82,6 +80,7 @@ const submitForm = async () => {
     message.success('发起成功')
     // 关闭当前 Tab
     delView(unref(currentRoute))
+    await push({ name: 'BpmOALeave' })
   } finally {
     formLoading.value = false
   }

+ 47 - 32
src/views/bpm/oa/leave/index.vue

@@ -2,18 +2,18 @@
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
-      class="-mb-15px"
-      :model="queryParams"
       ref="queryFormRef"
       :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
       label-width="68px"
     >
       <el-form-item label="请假类型" prop="type">
         <el-select
           v-model="queryParams.type"
-          placeholder="请选择请假类型"
-          clearable
           class="!w-240px"
+          clearable
+          placeholder="请选择请假类型"
         >
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
@@ -26,16 +26,16 @@
       <el-form-item label="申请时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          type="daterange"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
           :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
           class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
         />
       </el-form-item>
       <el-form-item label="结果" prop="result">
-        <el-select v-model="queryParams.result" placeholder="请选择结果" clearable class="!w-240px">
+        <el-select v-model="queryParams.result" class="!w-240px" clearable placeholder="请选择结果">
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)"
             :key="dict.value"
@@ -47,17 +47,24 @@
       <el-form-item label="原因" prop="reason">
         <el-input
           v-model="queryParams.reason"
-          placeholder="请输入原因"
+          class="!w-240px"
           clearable
+          placeholder="请输入原因"
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button type="primary" plain @click="handleCreate()">
-          <Icon icon="ep:plus" class="mr-5px" /> 发起请假
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+        <el-button plain type="primary" @click="handleCreate()">
+          <Icon class="mr-5px" icon="ep:plus" />
+          发起请假
         </el-button>
       </el-form-item>
     </el-form>
@@ -66,63 +73,63 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="申请编号" align="center" prop="id" />
-      <el-table-column label="状态" align="center" prop="result">
+      <el-table-column align="center" label="申请编号" prop="id" />
+      <el-table-column align="center" label="状态" prop="result">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
         </template>
       </el-table-column>
       <el-table-column
-        label="开始时间"
+        :formatter="dateFormatter"
         align="center"
+        label="开始时间"
         prop="startTime"
         width="180"
-        :formatter="dateFormatter"
       />
       <el-table-column
-        label="结束时间"
+        :formatter="dateFormatter"
         align="center"
+        label="结束时间"
         prop="endTime"
         width="180"
-        :formatter="dateFormatter"
       />
-      <el-table-column label="请假类型" align="center" prop="type">
+      <el-table-column align="center" label="请假类型" prop="type">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="scope.row.type" />
         </template>
       </el-table-column>
-      <el-table-column label="原因" align="center" prop="reason" />
+      <el-table-column align="center" label="原因" prop="reason" />
       <el-table-column
-        label="申请时间"
+        :formatter="dateFormatter"
         align="center"
+        label="申请时间"
         prop="createTime"
         width="180"
-        :formatter="dateFormatter"
       />
-      <el-table-column label="操作" align="center" width="200">
+      <el-table-column align="center" label="操作" width="200">
         <template #default="scope">
           <el-button
+            v-hasPermi="['bpm:oa-leave:query']"
             link
             type="primary"
             @click="handleDetail(scope.row)"
-            v-hasPermi="['bpm:oa-leave:query']"
           >
             详情
           </el-button>
           <el-button
+            v-hasPermi="['bpm:oa-leave:query']"
             link
             type="primary"
             @click="handleProcessDetail(scope.row)"
-            v-hasPermi="['bpm:oa-leave:query']"
           >
             进度
           </el-button>
           <el-button
+            v-if="scope.row.result === 1"
+            v-hasPermi="['bpm:oa-leave:create']"
             link
             type="danger"
             @click="cancelLeave(scope.row)"
-            v-hasPermi="['bpm:oa-leave:create']"
-            v-if="scope.row.result === 1"
           >
             取消
           </el-button>
@@ -131,9 +138,9 @@
     </el-table>
     <!-- 分页 -->
     <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
       @pagination="getList"
     />
   </ContentWrap>
@@ -228,6 +235,14 @@ const handleProcessDetail = (row) => {
   })
 }
 
+// fix: 列表不刷新的问题。
+watch(
+  () => router.currentRoute.value,
+  () => {
+    getList()
+  }
+)
+
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 5 - 4
src/views/crm/business/BusinessForm.vue

@@ -62,12 +62,12 @@
           </el-row>
         </el-popover>
       </el-form-item>
+      <!-- TODO @ljlleo:idea 红色的报错,可以解决下 -->
       <el-form-item label="商机状态类型" prop="statusTypeId">
         <el-select
           v-model="formData.statusTypeId"
           placeholder="请选择商机状态类型"
           clearable
-          size="small"
           @change="changeBusinessStatusType"
         >
           <el-option
@@ -79,7 +79,7 @@
         </el-select>
       </el-form-item>
       <el-form-item label="商机状态" prop="statusId">
-        <el-select v-model="formData.statusId" placeholder="请选择商机状态" clearable size="small">
+        <el-select v-model="formData.statusId" placeholder="请选择商机状态" clearable>
           <el-option
             v-for="item in businessStatusList"
             :key="item.id"
@@ -242,7 +242,8 @@ const queryParams = reactive({
   mobile: null,
   industryId: null,
   level: null,
-  source: null
+  source: null,
+  pool: false
 })
 // 选择客户
 const showCustomer = ref(false)
@@ -251,12 +252,12 @@ const openCustomerSelect = () => {
   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 {

+ 75 - 2
src/views/crm/business/components/BusinessList.vue

@@ -5,11 +5,32 @@
       <Icon class="mr-5px" icon="ep:opportunity" />
       创建商机
     </el-button>
+    <el-button
+      @click="openBusinessModal"
+      v-hasPermi="['crm:contact:create-business']"
+      v-if="queryParams.contactId"
+    >
+      <Icon class="mr-5px" icon="ep:circle-plus" />关联
+    </el-button>
+    <el-button
+      @click="deleteContactBusinessList"
+      v-hasPermi="['crm:contact:delete-business']"
+      v-if="queryParams.contactId"
+    >
+      <Icon class="mr-5px" icon="ep:remove" />解除关联
+    </el-button>
   </el-row>
 
   <!-- 列表 -->
   <ContentWrap class="mt-10px">
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+    <el-table
+      ref="businessRef"
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+    >
+      <el-table-column type="selection" width="55" v-if="queryParams.contactId" />
       <el-table-column label="商机名称" fixed="left" align="center" prop="name">
         <template #default="scope">
           <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
@@ -33,17 +54,28 @@
 
   <!-- 表单弹窗:添加 -->
   <BusinessForm ref="formRef" @success="getList" />
+  <!-- 关联商机选择弹框 -->
+  <BusinessListModal
+    ref="businessModalRef"
+    :customer-id="props.customerId"
+    @success="createContactBusinessList"
+  />
 </template>
 <script setup lang="ts">
 import * as BusinessApi from '@/api/crm/business'
+import * as ContactApi from '@/api/crm/contact'
 import BusinessForm from './../BusinessForm.vue'
 import { BizTypeEnum } from '@/api/crm/permission'
 import { fenToYuanFormat } from '@/utils/formatter'
+import BusinessListModal from './BusinessListModal.vue'
+
+const message = useMessage() // 消息
 
 defineOptions({ name: 'CrmBusinessList' })
 const props = defineProps<{
   bizType: number // 业务类型
   bizId: number // 业务编号
+  customerId?: number // 关联联系人与商机时,需要传入 customerId 进行筛选
 }>()
 
 const loading = ref(true) // 列表的加载中
@@ -52,7 +84,8 @@ const list = ref([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  customerId: undefined as unknown // 允许 undefined + number
+  customerId: undefined as unknown, // 允许 undefined + number
+  contactId: undefined as unknown // 允许 undefined + number
 })
 
 /** 查询列表 */
@@ -61,6 +94,7 @@ const getList = async () => {
   try {
     // 置空参数
     queryParams.customerId = undefined
+    queryParams.contactId = undefined
     // 执行查询
     let data = { list: [], total: 0 }
     switch (props.bizType) {
@@ -68,6 +102,10 @@ const getList = async () => {
         queryParams.customerId = props.bizId
         data = await BusinessApi.getBusinessPageByCustomer(queryParams)
         break
+      case BizTypeEnum.CRM_CONTACT:
+        queryParams.contactId = props.bizId
+        data = await BusinessApi.getBusinessPageByContact(queryParams)
+        break
       default:
         return
     }
@@ -96,6 +134,41 @@ const openDetail = (id: number) => {
   push({ name: 'CrmBusinessDetail', params: { id } })
 }
 
+/** 打开联系人与商机的关联弹窗 */
+const businessModalRef = ref()
+const openBusinessModal = () => {
+  businessModalRef.value.open()
+}
+const createContactBusinessList = async (businessIds: number[]) => {
+  const data = {
+    contactId: props.bizId,
+    businessIds: businessIds
+  } as ContactApi.ContactBusinessReqVO
+  businessRef.value.getSelectionRows().forEach((row: BusinessApi.BusinessVO) => {
+    data.businessIds.push(row.id)
+  })
+  await ContactApi.createContactBusinessList(data)
+  // 刷新列表
+  message.success('关联商机成功')
+  handleQuery()
+}
+
+/** 解除联系人与商机的关联 */
+const businessRef = ref()
+const deleteContactBusinessList = async () => {
+  const data = {
+    contactId: props.bizId,
+    businessIds: businessRef.value.getSelectionRows().map((row: BusinessApi.BusinessVO) => row.id)
+  } as ContactApi.ContactBusinessReqVO
+  if (data.businessIds.length === 0) {
+    return message.error('未选择商机')
+  }
+  await ContactApi.deleteContactBusinessList(data)
+  // 刷新列表
+  message.success('取关商机成功')
+  handleQuery()
+}
+
 /** 监听打开的 bizId + bizType,从而加载最新的列表 */
 watch(
   () => [props.bizId, props.bizType],

+ 19 - 34
src/views/crm/contactBusinessLink/components/BusinessLinkContactList.vue → src/views/crm/business/components/BusinessListModal.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog title="关联商机" v-model="dialogVisible">
     <!-- 搜索工作栏 -->
     <ContentWrap>
       <el-form
@@ -29,7 +29,6 @@
     </ContentWrap>
 
     <!-- 列表 -->
-    <!-- TODO @zyna:字段按照他们对齐下 -->
     <ContentWrap class="mt-10px">
       <el-table
         v-loading="loading"
@@ -75,16 +74,16 @@
 </template>
 <script setup lang="ts">
 import * as BusinessApi from '@/api/crm/business'
-import BusinessForm from '../../business/BusinessForm.vue'
+import BusinessForm from '../BusinessForm.vue'
 import { fenToYuanFormat } from '@/utils/formatter'
-// TODO @zyna:下面这个拼接,要注意大小写哈
-import * as ContactbusinesslinkApi from '@/api/crm/contactbusinesslink'
-const message = useMessage() // 消息弹窗
 
-defineOptions({ name: 'CrmBusinessLinkContactList' })
+const message = useMessage() // 消息弹窗
+const props = defineProps<{
+  customerId: number
+}>()
+defineOptions({ name: 'BusinessListModal' })
 
 const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题 TODO @zyna:是不是搞个标题?
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
@@ -93,17 +92,14 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  // TODO @zyna:是不是要根据 customerId 筛选?
-  name: undefined
+  name: undefined,
+  customerId: props.customerId
 })
-const contactIdProp = ref(0) // 联系人编号
 
 /** 打开弹窗 */
-const open = async (contactId: number) => {
+const open = async () => {
   dialogVisible.value = true
-  contactIdProp.value = contactId
-  // TODO @zyna:下面要 await 下;一般 idea 如果有黄色警告,最好都看看哈
-  getList()
+  await getList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -111,7 +107,7 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 const getList = async () => {
   loading.value = true
   try {
-    const data = await BusinessApi.getBusinessPage(queryParams)
+    const data = await BusinessApi.getBusinessPageByCustomer(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -141,25 +137,14 @@ const openForm = () => {
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const businessRef = ref()
 const submitForm = async () => {
-  // TODO @zyna:可以 if return,这样括号层级简单一点
-  if (businessRef.value.getSelectionRows().length === 0) {
-    message.success('未选择商机')
-  } else {
-    // TODO @zyna:这里 postData 应该不用 ref,搞个 数组就好了?
-    const postData = ref<ContactbusinesslinkApi.ContactBusinessLinkVO[]>([])
-    businessRef.value.getSelectionRows().forEach((element) => {
-      // TODO @zyna:可以直接 push,不用声明 data
-      let data = {
-        id: undefined,
-        businessId: element.id,
-        contactId: contactIdProp.value
-      }
-      postData.value.push(data)
-    })
-    await ContactbusinesslinkApi.createContactBusinessLinkBatch(postData.value)
-    dialogVisible.value = false
-    emit('success')
+  const businessIds = businessRef.value
+    .getSelectionRows()
+    .map((row: BusinessApi.BusinessVO) => row.id)
+  if (businessIds.length === 0) {
+    return message.error('未选择商机')
   }
+  dialogVisible.value = false
+  emit('success', businessIds)
 }
 
 /** 打开联系人详情 */

+ 29 - 23
src/views/crm/contact/ContactForm.vue

@@ -49,8 +49,8 @@
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="12"
-          ><el-form-item label="性别" prop="sex">
+        <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)"
@@ -58,8 +58,9 @@
                 :label="dict.label"
                 :value="dict.value"
               />
-            </el-select> </el-form-item
-        ></el-col>
+            </el-select>
+          </el-form-item>
+        </el-col>
       </el-row>
       <el-row>
         <el-col :span="12">
@@ -72,8 +73,8 @@
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="座机" prop="telephone">
-            <el-input v-model="formData.telephone" placeholder="请输入座机" style="width: 215px" />
+          <el-form-item label="电话" prop="telephone">
+            <el-input v-model="formData.telephone" placeholder="请输入电话" style="width: 215px" />
           </el-form-item>
         </el-col>
       </el-row>
@@ -104,10 +105,10 @@
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="下次联系时间" prop="nextTime">
+          <el-form-item label="下次联系时间" prop="contactNextTime">
             <el-date-picker
-              v-model="formData.nextTime"
-              type="date"
+              v-model="formData.contactNextTime"
+              type="datetime"
               value-format="x"
               placeholder="选择下次联系时间"
             />
@@ -126,15 +127,17 @@
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="地址" prop="address">
+          <el-form-item label="地址" prop="detailAddress">
             <el-input
               input-style="width:190px;"
-              v-model="formData.address"
+              v-model="formData.detailAddress"
               placeholder="请输入地址"
             />
           </el-form-item>
-        </el-col> </el-row
-      ><el-row>
+        </el-col>
+      </el-row>
+      <!-- TODO @zyna:解决下 ide 报错 -->
+      <el-row>
         <el-col :span="12">
           <el-form-item label="直属上级" prop="parentId">
             <el-select v-model="formData.parentId" placeholder="请选择">
@@ -152,10 +155,11 @@
           <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-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)"
@@ -169,8 +173,8 @@
         </el-col>
       </el-row>
       <el-row>
-        <el-col :span="24"
-          ><el-form-item label="备注" prop="remark">
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remark">
             <el-input v-model="formData.remark" placeholder="请输入备注" />
           </el-form-item>
         </el-col>
@@ -192,19 +196,20 @@ import { defaultProps } from '@/utils/tree'
 
 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 areaList = ref([]) // 地区列表
 const formData = ref({
-  nextTime: undefined,
+  contactNextTime: undefined,
   mobile: undefined,
   telephone: undefined,
   email: undefined,
   customerId: undefined,
   customerName: undefined,
-  address: undefined,
+  detailAddress: undefined,
   remark: undefined,
   ownerUserId: undefined,
   lastTime: undefined,
@@ -279,13 +284,14 @@ const submitForm = async () => {
 
 /** 重置表单 */
 const resetForm = () => {
+  // TODO zyna:ide 告警,看看怎么去掉哈;
   formData.value = {
-    nextTime: undefined,
+    contactNextTime: undefined,
     mobile: undefined,
     telephone: undefined,
     email: undefined,
     customerId: undefined,
-    address: undefined,
+    detailAddress: undefined,
     remark: undefined,
     ownerUserId: undefined,
     lastTime: undefined,

+ 1 - 11
src/views/crm/contact/detail/ContactDetailsHeader.vue

@@ -1,14 +1,5 @@
-<!--
- * @Author: zyna
- * @Date: 2023-12-02 13:08:57
- * @LastEditTime: 2023-12-03 13:47:16
- * @FilePath: \yudao-ui-admin-vue3\src\views\crm\contact\detail\ContactDetailsHeader.vue
- * @Description:
--->
-<!-- TODO @zyna:上面这个不加哈 -->
 <template>
-  <!-- TODO @zyna:loading 缺了 -->
-  <div v-loading="loading">
+  <div>
     <div class="flex items-start justify-between">
       <div>
         <el-col>
@@ -48,7 +39,6 @@
 import * as ContactApi from '@/api/crm/contact'
 import ContactForm from '@/views/crm/contact/ContactForm.vue'
 import { formatDate } from '@/utils/formatTime'
-
 //操作修改
 const formRef = ref()
 const openForm = (type: string, id?: number) => {

+ 1 - 0
src/views/crm/contact/detail/ContactDetailsInfo.vue

@@ -1,4 +1,5 @@
 <template>
+  <!-- TODO @zyna:少了一个外边框 -->
   <el-collapse v-model="activeNames">
     <el-collapse-item name="basicInfo">
       <template #title>

+ 7 - 3
src/views/crm/contact/detail/index.vue

@@ -10,18 +10,22 @@
         <PermissionList :biz-id="contact.id!" :biz-type="BizTypeEnum.CRM_CONTACT" />
       </el-tab-pane>
       <el-tab-pane label="商机" lazy>
-        <BusinessList :biz-id="contact.id!" :biz-type="BizTypeEnum.CRM_CONTACT" />
+        <BusinessList
+          :biz-id="contact.id!"
+          :biz-type="BizTypeEnum.CRM_CONTACT"
+          :customer-id="contact.customerId"
+        />
       </el-tab-pane>
     </el-tabs>
   </el-col>
 </template>
 <script setup lang="ts">
-import { ElMessage } from 'element-plus'
+import { ElMessage } from 'element-plus' // TODO @zyna:使用 hook 引入 message
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import * as ContactApi from '@/api/crm/contact'
 import ContactDetailsHeader from '@/views/crm/contact/detail/ContactDetailsHeader.vue'
 import ContactDetailsInfo from '@/views/crm/contact/detail/ContactDetailsInfo.vue'
-import BusinessList from '@/views/crm/contactBusinessLink/components/BusinessListByContact.vue' // 商机列表
+import BusinessList from '@/views/crm/business/components/BusinessList.vue' // 商机列表
 import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
 import { BizTypeEnum } from '@/api/crm/permission'
 

+ 50 - 63
src/views/crm/contact/index.vue

@@ -11,6 +11,7 @@
       <el-form-item label="客户" prop="customerId">
         <el-select
           v-model="queryParams.customerId"
+          class="!w-240px"
           placeholder="请选择客户"
           value-key="id"
           lable-key="name"
@@ -28,56 +29,55 @@
       <el-form-item label="姓名" prop="name">
         <el-input
           v-model="queryParams.name"
+          class="!w-240px"
           placeholder="请输入姓名"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
       <el-form-item label="手机号" prop="mobile">
         <el-input
           v-model="queryParams.mobile"
+          class="!w-240px"
           placeholder="请输入手机号"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
-      <el-form-item label="座机" prop="telephone">
+      <el-form-item label="电话" prop="telephone">
         <el-input
           v-model="queryParams.telephone"
+          class="!w-240px"
           placeholder="请输入电话"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
-
       <el-form-item label="QQ" prop="qq">
         <el-input
           v-model="queryParams.qq"
+          class="!w-240px"
           placeholder="请输入QQ"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
       <el-form-item label="微信" prop="wechat">
         <el-input
           v-model="queryParams.wechat"
+          class="!w-240px"
           placeholder="请输入微信"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
       <el-form-item label="电子邮箱" prop="email">
         <el-input
           v-model="queryParams.email"
+          class="!w-240px"
           placeholder="请输入电子邮箱"
           clearable
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
       <el-form-item>
@@ -102,53 +102,57 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="姓名" fixed="left" align="center" prop="name">
+      <el-table-column label="姓名" fixed="left" align="center" prop="name" width="140">
         <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="客户" 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="master">
+      <el-table-column label="客户名称" fixed="left" align="center" prop="customerName" width="120">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+          <el-link
+            type="primary"
+            :underline="false"
+            @click="openCustomerDetail(scope.row.customerId)"
+          >
+            {{ scope.row.customerName }}
+          </el-link>
         </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="wechat" />
-      <el-table-column label="邮箱" align="center" prop="email" />
-      <el-table-column label="地址" align="center" prop="address" />
+      <el-table-column label="手机" align="center" prop="mobile" width="120" />
+      <el-table-column label="电话" align="center" prop="telephone" width="120" />
+      <el-table-column label="邮箱" align="center" prop="email" width="120" />
+      <el-table-column label="职位" align="center" prop="post" width="120" />
+      <el-table-column label="地址" align="center" prop="detailAddress" width="120" />
       <el-table-column
-        label="下次联系时间"
+        :formatter="dateFormatter"
         align="center"
-        prop="nextTime"
+        label="下次联系时间"
+        prop="contactNextTime"
         width="180px"
-        :formatter="dateFormatter"
       />
       <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="关键决策人" align="center" prop="master" width="100">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+        </template>
+      </el-table-column>
+      <el-table-column label="直属上级" align="center" prop="parentName" width="140" />
       <el-table-column
         label="最后跟进时间"
         align="center"
-        prop="lastTime"
+        prop="contactLastTime"
         :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column label="负责人" align="center" prop="ownerUserId">
+      <el-table-column label="性别" align="center" prop="sex">
         <template #default="scope">
-          {{ scope.row.ownerUserName }}
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
         </template>
       </el-table-column>
-      <!-- <el-table-column label="所属部门" align="center" prop="ownerUserId" /> -->
+      <el-table-column label="负责人" align="center" prop="ownerUserName" width="120" />
+      <el-table-column label="创建人" align="center" prop="creatorName" width="120" />
       <el-table-column
         label="更新时间"
         align="center"
@@ -163,21 +167,10 @@
         :formatter="dateFormatter"
         width="180px"
       />
-      <!-- <el-table-column
-        label="创建人"
-        align="center"
-        prop="creator"
-        :formatter="dateFormatter"
-        width="180px"
-      >
-        <template #default="scope">
-          {{ userList.find((user) => user.id === scope.row.creator)?.nickname }}
-        </template>
-      </el-table-column> -->
       <el-table-column label="操作" align="center" fixed="right" width="200">
         <template #default="scope">
           <el-button
-            plain
+            link
             type="primary"
             @click="openForm('update', scope.row.id)"
             v-hasPermi="['crm:contact:update']"
@@ -185,7 +178,7 @@
             编辑
           </el-button>
           <el-button
-            plain
+            link
             type="danger"
             @click="handleDelete(scope.row.id)"
             v-hasPermi="['crm:contact:delete']"
@@ -228,23 +221,13 @@ const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  nextTime: [],
-  mobile: null,
-  telephone: null,
-  email: null,
-  customerId: null,
-  address: null,
-  remark: null,
-  ownerUserId: null,
-  createTime: [],
-  lastTime: [],
-  parentId: null,
-  name: null,
-  post: null,
-  qq: null,
-  wechat: null,
-  sex: null,
-  policyMakers: null
+  mobile: undefined,
+  telephone: undefined,
+  email: undefined,
+  customerId: undefined,
+  name: undefined,
+  qq: undefined,
+  wechat: undefined
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -307,11 +290,15 @@ const handleExport = async () => {
   }
 }
 
-/** 打开客户详情 */
+/** 打开联系人详情 */
 const { push } = useRouter()
 const openDetail = (id: number) => {
   push({ name: 'CrmContactDetail', params: { id } })
 }
+/** 打开客户详情 */
+const openCustomerDetail = (id: number) => {
+  push({ name: 'CrmCustomerDetail', params: { id } })
+}
 
 /** 初始化 **/
 onMounted(async () => {

+ 0 - 143
src/views/crm/contactBusinessLink/components/BusinessListByContact.vue

@@ -1,143 +0,0 @@
-<template>
-  <!-- 操作栏 -->
-  <el-row justify="end">
-    <el-button @click="openForm">
-      <Icon class="mr-5px" icon="ep:opportunity" />
-      创建商机
-    </el-button>
-    <el-button @click="openBusinessLink"> 关联 </el-button>
-    <el-button @click="deleteBusinessLink"> 解除关联 </el-button>
-  </el-row>
-
-  <!-- 列表 -->
-  <ContentWrap class="mt-10px">
-    <el-table
-      v-loading="loading"
-      ref="businessRef"
-      :data="list"
-      :stripe="true"
-      :show-overflow-tooltip="true"
-    >
-      <el-table-column type="selection" width="55" />
-      <el-table-column label="商机名称" fixed="left" align="center" prop="name">
-        <template #default="scope">
-          <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
-            {{ scope.row.name }}
-          </el-link>
-        </template>
-      </el-table-column>
-      <el-table-column label="商机金额" align="center" prop="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" />
-  <!---->
-  <BusinessLink ref="businessLinkRef" @success="getList" />
-</template>
-<script setup lang="ts">
-import * as ContactBusinessLinkApi from '@/api/crm/contactbusinesslink'
-import BusinessForm from '../../business/BusinessForm.vue'
-import { BizTypeEnum } from '@/api/crm/permission'
-import { fenToYuanFormat } from '@/utils/formatter'
-import BusinessLink from './BusinessLinkContactList.vue'
-const message = useMessage() // 消息弹窗
-// TODO @zyna:这个组件,可以服用 BusinessList,然后根据传入的编号类型,做一些判断?
-defineOptions({ name: 'CrmBusinessContactList' })
-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,
-  contactId: undefined as unknown // 允许 undefined + number
-})
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    // 置空参数
-    queryParams.contactId = undefined
-    // 执行查询
-    let data = { list: [], total: 0 }
-    switch (props.bizType) {
-      case BizTypeEnum.CRM_CONTACT:
-        queryParams.contactId = props.bizId
-        data = await ContactBusinessLinkApi.getBusinessByContactPage(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 businessLinkRef = ref()
-const openBusinessLink = () => {
-  businessLinkRef.value.open(props.bizId)
-}
-/**解除关联 */
-const businessRef = ref()
-const deleteBusinessLink = async () => {
-  if (businessRef.value.getSelectionRows().length === 0) {
-    message.success('未选择商机')
-  } else {
-    const postData = ref<ContactBusinessLinkApi.ContactBusinessLinkVO[]>([])
-    businessRef.value.getSelectionRows().forEach((element) => {
-      let data = {
-        id: undefined,
-        businessId: element.id,
-        contactId: props.bizId
-      }
-      postData.value.push(data)
-    })
-    await ContactBusinessLinkApi.deleteContactBusinessLink(postData.value)
-    handleQuery()
-  }
-}
-/** 打开联系人详情 */
-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>

+ 10 - 10
src/views/crm/customer/CustomerForm.vue

@@ -1,11 +1,11 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
       label-width="100px"
-      v-loading="formLoading"
     >
       <el-row>
         <el-col :span="12">
@@ -17,7 +17,7 @@
           <el-form-item label="所属行业" prop="industryId">
             <el-select v-model="formData.industryId" placeholder="请选择所属行业">
               <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
+                v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
                 :key="dict.value"
                 :label="dict.label"
                 :value="dict.value"
@@ -31,7 +31,7 @@
           <el-form-item label="客户来源" prop="source">
             <el-select v-model="formData.source" placeholder="请选择客户来源">
               <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
+                v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
                 :key="dict.value"
                 :label="dict.label"
                 :value="dict.value"
@@ -43,7 +43,7 @@
           <el-form-item label="客户等级" prop="level">
             <el-select v-model="formData.level" placeholder="请选择客户等级">
               <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
+                v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
                 :key="dict.value"
                 :label="dict.label"
                 :value="dict.value"
@@ -120,9 +120,9 @@
           <el-form-item label="下次联系时间" prop="contactNextTime">
             <el-date-picker
               v-model="formData.contactNextTime"
-              type="date"
-              value-format="x"
               placeholder="选择下次联系时间"
+              type="datetime"
+              value-format="x"
             />
           </el-form-item>
         </el-col>
@@ -139,13 +139,13 @@
       </el-col>
     </el-form>
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
 </template>
-<script setup lang="ts">
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as CustomerApi from '@/api/crm/customer'
 import * as AreaApi from '@/api/system/area'
 import { defaultProps } from '@/utils/tree'

+ 31 - 4
src/views/crm/customer/detail/CustomerDetailsHeader.vue

@@ -11,10 +11,19 @@
       </div>
       <div>
         <!-- 右上:按钮 -->
-        <el-button v-hasPermi="['crm:customer:update']" @click="openForm(customer.id)">
+        <el-button
+          type="primary"
+          v-hasPermi="['crm:customer:update']"
+          @click="openForm(customer.id)"
+        >
           编辑
         </el-button>
+        <!-- TODO @puhui999:转移的操作接入 -->
+        <el-button type="primary" @click="transfer">转移</el-button>
+        <!-- TODO @puhui999:修改成交状态的接入 -->
         <el-button>更改成交状态</el-button>
+        <el-button v-if="customer.lockStatus" @click="handleUnlock(customer.id!)">解锁</el-button>
+        <el-button v-else @click="handleLock(customer.id!)">锁定</el-button>
       </div>
     </div>
   </div>
@@ -26,26 +35,29 @@
       <el-descriptions-item label="成交状态">
         {{ customer.dealStatus ? '已成交' : '未成交' }}
       </el-descriptions-item>
-      <el-descriptions-item label="负责人">{{ customer.ownerUserName }} </el-descriptions-item>
+      <el-descriptions-item label="负责人">{{ customer.ownerUserName }}</el-descriptions-item>
       <!-- TODO wanwan 首要联系人? -->
       <el-descriptions-item label="首要联系人" />
       <!-- TODO wanwan 首要联系人电话? -->
-      <el-descriptions-item label="首要联系人电话">{{ customer.mobile }} </el-descriptions-item>
+      <el-descriptions-item label="首要联系人电话">{{ customer.mobile }}</el-descriptions-item>
     </el-descriptions>
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
   <CustomerForm ref="formRef" @success="emit('refresh')" />
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import { DICT_TYPE } from '@/utils/dict'
 import * as CustomerApi from '@/api/crm/customer'
 import CustomerForm from '../CustomerForm.vue'
 
+defineOptions({ name: 'CustomerDetailsHeader' })
+
 const { customer, loading } = defineProps<{
   customer: CustomerApi.CustomerVO // 客户信息
   loading: boolean // 加载中
 }>()
+const message = useMessage() // 消息弹窗
 
 /** 修改操作 */
 const formRef = ref()
@@ -53,5 +65,20 @@ const openForm = (id?: number) => {
   formRef.value.open('update', id)
 }
 
+/** 锁定操作 */
+const handleLock = async (id: number) => {
+  await CustomerApi.lockCustomer(id, true)
+  message.success('锁定成功')
+  emit('refresh')
+}
+
+/** 解锁操作 */
+const handleUnlock = async (id: number) => {
+  console.log(customer, '=======')
+  await CustomerApi.lockCustomer(id, false)
+  message.success('解锁成功')
+  emit('refresh')
+}
+
 const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调
 </script>

+ 13 - 12
src/views/crm/customer/detail/CustomerDetailsInfo.vue

@@ -1,6 +1,6 @@
 <template>
   <ContentWrap>
-    <el-collapse class="" v-model="activeNames">
+    <el-collapse v-model="activeNames" class="">
       <el-collapse-item name="basicInfo">
         <template #title>
           <span class="text-base font-bold">基本信息</span>
@@ -20,11 +20,11 @@
           </el-descriptions-item>
           <el-descriptions-item label="手机">{{ customer.mobile }}</el-descriptions-item>
           <el-descriptions-item label="电话">{{ customer.telephone }}</el-descriptions-item>
-          <el-descriptions-item label="邮箱">{{ customer.email }} </el-descriptions-item>
-          <el-descriptions-item label="QQ">{{ customer.qq }} </el-descriptions-item>
-          <el-descriptions-item label="微信">{{ customer.wechat }} </el-descriptions-item>
-          <el-descriptions-item label="网址">{{ customer.website }} </el-descriptions-item>
-          <el-descriptions-item label="所在地">{{ customer.areaName }} </el-descriptions-item>
+          <el-descriptions-item label="邮箱">{{ customer.email }}</el-descriptions-item>
+          <el-descriptions-item label="QQ">{{ customer.qq }}</el-descriptions-item>
+          <el-descriptions-item label="微信">{{ customer.wechat }}</el-descriptions-item>
+          <el-descriptions-item label="网址">{{ customer.website }}</el-descriptions-item>
+          <el-descriptions-item label="所在地">{{ customer.areaName }}</el-descriptions-item>
           <el-descriptions-item label="详细地址"
             >{{ customer.detailAddress }}
           </el-descriptions-item>
@@ -38,8 +38,8 @@
           </el-descriptions-item>
         </el-descriptions>
         <el-descriptions :column="1">
-          <el-descriptions-item label="客户描述">{{ customer.description }} </el-descriptions-item>
-          <el-descriptions-item label="备注">{{ customer.remark }} </el-descriptions-item>
+          <el-descriptions-item label="客户描述">{{ customer.description }}</el-descriptions-item>
+          <el-descriptions-item label="备注">{{ customer.remark }}</el-descriptions-item>
         </el-descriptions>
       </el-collapse-item>
       <el-collapse-item name="systemInfo">
@@ -47,8 +47,8 @@
           <span class="text-base font-bold">系统信息</span>
         </template>
         <el-descriptions :column="2">
-          <el-descriptions-item label="负责人">{{ customer.ownerUserName }} </el-descriptions-item>
-          <el-descriptions-item label="创建人">{{ customer.creatorName }} </el-descriptions-item>
+          <el-descriptions-item label="负责人">{{ customer.ownerUserName }}</el-descriptions-item>
+          <el-descriptions-item label="创建人">{{ customer.creatorName }}</el-descriptions-item>
           <el-descriptions-item label="创建时间">
             {{ customer.createTime ? formatDate(customer.createTime) : '空' }}
           </el-descriptions-item>
@@ -60,15 +60,16 @@
     </el-collapse>
   </ContentWrap>
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import * as CustomerApi from '@/api/crm/customer'
 import { DICT_TYPE } from '@/utils/dict'
 import { formatDate } from '@/utils/formatTime'
 
+defineOptions({ name: 'CustomerDetailsInfo' })
 const { customer } = defineProps<{
   customer: CustomerApi.CustomerVO // 客户明细
 }>()
 
 const activeNames = ref(['basicInfo', 'systemInfo']) // 展示的折叠面板
 </script>
-<style scoped lang="scss"></style>
+<style lang="scss" scoped></style>

+ 20 - 3
src/views/crm/customer/detail/index.vue

@@ -5,7 +5,9 @@
       <el-tab-pane label="详细资料">
         <CustomerDetailsInfo :customer="customer" />
       </el-tab-pane>
-      <el-tab-pane label="操作日志" lazy>TODO 待开发</el-tab-pane>
+      <el-tab-pane label="操作日志">
+        <OperateLogV2 :log-list="logList" />
+      </el-tab-pane>
       <el-tab-pane label="联系人" lazy>
         <ContactList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
       </el-tab-pane>
@@ -38,24 +40,39 @@ import ReceivableList from '@/views/crm/receivable/components/ReceivableList.vue
 import ReceivablePlanList from '@/views/crm/receivable/plan/components/ReceivablePlanList.vue' // 回款计划列表
 import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
 import { BizTypeEnum } from '@/api/crm/permission'
+import { OperateLogV2VO } from '@/api/system/operatelog'
 
 defineOptions({ name: 'CrmCustomerDetail' })
 
 const route = useRoute()
 const id = Number(route.params.id) // 客户编号
 const loading = ref(true) // 加载中
-
 /** 获取详情 */
 const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO) // 客户详情
 const getCustomer = async (id: number) => {
   loading.value = true
   try {
     customer.value = await CustomerApi.getCustomer(id)
+    await getOperateLog(id)
   } finally {
     loading.value = false
   }
 }
-
+const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
+/**
+ * 获取操作日志
+ */
+const getOperateLog = async (customerId: number) => {
+  if (!customerId) {
+    return
+  }
+  const data = await CustomerApi.getOperateLogPage({
+    pageNo: 1,
+    pageSize: 10,
+    bizId: customerId
+  })
+  logList.value = data.list
+}
 /** 初始化 */
 const { delView } = useTagsViewStore() // 视图操作
 const { currentRoute } = useRouter() // 路由

+ 25 - 21
src/views/crm/customer/index.vue

@@ -101,14 +101,16 @@
           </el-link>
         </template>
       </el-table-column>
-      <el-table-column align="center" label="所属行业" prop="industryId" width="120">
+      <el-table-column align="center" label="手机" prop="mobile" width="120" />
+      <el-table-column align="center" label="电话" prop="telephone" width="120" />
+      <el-table-column align="center" label="客户来源" prop="source" width="100">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
         </template>
       </el-table-column>
-      <el-table-column align="center" label="客户来源" prop="source" width="100">
+      <el-table-column align="center" label="所属行业" prop="industryId" width="120">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
+          <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
         </template>
       </el-table-column>
       <el-table-column align="center" label="客户等级" prop="level" width="120">
@@ -116,43 +118,45 @@
           <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
         </template>
       </el-table-column>
-      <el-table-column align="center" label="手机" prop="mobile" width="120" />
-      <el-table-column align="center" label="详细地址" prop="detailAddress" width="200" />
-      <el-table-column align="center" label="负责人" prop="ownerUserName" />
-      <el-table-column align="center" label="所属部门" prop="ownerUserDeptName" />
-      <el-table-column align="center" label="创建人" prop="creatorName" />
+      <el-table-column align="center" label="网址" prop="website" width="200" />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
-        label="创建时间"
-        prop="createTime"
+        label="下次联系时间"
+        prop="contactNextTime"
         width="180px"
       />
+      <el-table-column align="center" label="备注" prop="remark" width="200" />
       <el-table-column align="center" label="成交状态" prop="dealStatus">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
         </template>
       </el-table-column>
+      <!-- TODO @puhui999:距进入公海天数 -->
       <el-table-column
         :formatter="dateFormatter"
         align="center"
-        label="下次联系时间"
-        prop="contactNextTime"
+        label="最后跟进时间"
+        prop="contactLastTime"
         width="180px"
       />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
-        label="最后跟进时间"
-        prop="contactLastTime"
+        label="创建时间"
+        prop="updateTime"
         width="180px"
       />
-      <el-table-column align="center" label="锁定状态" prop="lockStatus">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" />
-        </template>
-      </el-table-column>
-      <!--  TODO @wanwan 距进入公海天数    -->
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="创建时间"
+        prop="createTime"
+        width="180px"
+      />
+      <el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" />
+      <el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
+      <el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
       <el-table-column align="center" fixed="right" label="操作" min-width="150">
         <template #default="scope">
           <el-button

+ 9 - 9
src/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue → src/views/crm/customer/limitConfig/CustomerLimitConfigForm.vue

@@ -56,12 +56,12 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import * as CustomerLimitConfigApi from '@/api/crm/customerLimitConfig'
+import * as CustomerLimitConfigApi from '@/api/crm/customer/limitConfig'
 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'
+import { LimitConfType } from '@/api/crm/customer/limitConfig'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -72,7 +72,7 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined,
-  type: undefined,
+  type: LimitConfType.CUSTOMER_LOCK_LIMIT, // 给个默认值,避免 IDE 报错
   userIds: undefined,
   deptIds: undefined,
   maxCount: undefined,
@@ -88,15 +88,11 @@ const deptTree = ref() // 部门树形结构
 const userTree = ref() // 用户树形结构
 
 /** 打开弹窗 */
-const open = async (type: string, id?: number, limitConfType?: LimitConfType) => {
+const open = async (type: string, limitConfType: LimitConfType, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
-  // 获得部门树
-  await getDeptTree()
-  // 获得用户
-  await getUserTree()
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
@@ -108,6 +104,10 @@ const open = async (type: string, id?: number, limitConfType?: LimitConfType) =>
   } else {
     formData.value.type = limitConfType
   }
+  // 获得部门树
+  await getDeptTree()
+  // 获得用户
+  await getUserTree()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -141,7 +141,7 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     id: undefined,
-    type: undefined,
+    type: LimitConfType.CUSTOMER_LOCK_LIMIT,
     userIds: undefined,
     deptIds: undefined,
     maxCount: undefined,

+ 5 - 9
src/views/crm/config/customerLimitConfig/CustomerLimitConfigList.vue → src/views/crm/customer/limitConfig/CustomerLimitConfigList.vue

@@ -1,7 +1,5 @@
 <template>
-  <el-button type="primary" plain @click="handleQuery">
-    <Icon icon="ep:refresh" class="mr-5px" /> 刷新
-  </el-button>
+  <el-button plain @click="handleQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 刷新 </el-button>
   <el-button
     type="primary"
     plain
@@ -87,10 +85,10 @@
 </template>
 <script setup lang="ts">
 import { dateFormatter } from '@/utils/formatTime'
-import * as CustomerLimitConfigApi from '@/api/crm/customerLimitConfig'
-import CustomerLimitConfigForm from '@/views/crm/config/customerLimitConfig/CustomerLimitConfigForm.vue'
+import * as CustomerLimitConfigApi from '@/api/crm/customer/limitConfig'
+import CustomerLimitConfigForm from './CustomerLimitConfigForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
-import { LimitConfType } from '@/api/crm/customerLimitConfig'
+import { LimitConfType } from '@/api/crm/customer/limitConfig'
 
 defineOptions({ name: 'CustomerLimitConfigList' })
 
@@ -107,8 +105,6 @@ const queryParams = reactive({
   pageSize: 10,
   type: confType
 })
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
 
 /** 查询列表 */
 const getList = async () => {
@@ -125,7 +121,7 @@ const getList = async () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id, confType)
+  formRef.value.open(type, confType, id)
 }
 
 /** 删除按钮操作 */

+ 2 - 2
src/views/crm/config/customerLimitConfig/index.vue → src/views/crm/customer/limitConfig/index.vue

@@ -12,8 +12,8 @@
   </ContentWrap>
 </template>
 <script setup lang="ts">
-import CustomerLimitConfigList from '@/views/crm/config/customerLimitConfig/CustomerLimitConfigList.vue'
-import { LimitConfType } from '@/api/crm/customerLimitConfig'
+import CustomerLimitConfigList from './CustomerLimitConfigList.vue'
+import { LimitConfType } from '@/api/crm/customer/limitConfig'
 
 defineOptions({ name: 'CrmCustomerLimitConfig' })
 </script>

+ 5 - 5
src/views/crm/config/customerPoolConfig/index.vue → src/views/crm/customer/poolConfig/index.vue

@@ -56,7 +56,7 @@
   </ContentWrap>
 </template>
 <script setup lang="ts">
-import * as CustomerPoolConfigApi from '@/api/crm/customerPoolConfig'
+import * as CustomerPoolConfigApi from '@/api/crm/customer/poolConfig'
 import { CardTitle } from '@/components/Card'
 
 defineOptions({ name: 'CrmCustomerPoolConfig' })
@@ -67,10 +67,10 @@ const { t } = useI18n() // 国际化
 const formLoading = ref(false)
 const formData = ref({
   enabled: false,
-  contactExpireDays: 0,
-  dealExpireDays: 0,
+  contactExpireDays: undefined,
+  dealExpireDays: undefined,
   notifyEnabled: false,
-  notifyDays: 0
+  notifyDays: undefined
 })
 const formRules = reactive({
   enabled: [{ required: true, message: '是否启用客户公海不能为空', trigger: 'blur' }]
@@ -100,7 +100,7 @@ const onSubmit = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as CustomerPoolConfigApi.CustomerPoolConfigVO
+    const data = formData.value as CustomerPoolConfigApi.CustomerPoolConfigVO
     await CustomerPoolConfigApi.saveCustomerPoolConfig(data)
     message.success(t('common.updateSuccess'))
     await getConfig()

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

@@ -17,6 +17,7 @@
           />
         </el-select>
       </el-form-item>
+      <!-- TODO @puhui999:编辑时,level 没带过来 -->
       <el-form-item label="权限级别" prop="level">
         <el-radio-group v-model="formData.level">
           <template

+ 1 - 0
src/views/crm/permission/components/PermissionList.vue

@@ -24,6 +24,7 @@
       :stripe="true"
       @selection-change="handleSelectionChange"
     >
+      <!-- TODO @puhui999:负责人不允许选中 -->
       <el-table-column type="selection" width="55" />
       <el-table-column align="center" label="姓名" prop="nickname" />
       <el-table-column align="center" label="部门" prop="deptName" />

+ 2 - 2
src/views/mall/product/spu/form/BasicInfoForm.vue

@@ -197,7 +197,7 @@ defineOptions({ name: 'ProductSpuBasicInfoForm' })
 const ruleConfig: RuleConfig[] = [
   {
     name: 'stock',
-    rule: (arg) => arg >= 1,
+    rule: (arg) => arg >= 0,
     message: '商品库存必须大于等于 1 !!!'
   },
   {
@@ -213,7 +213,7 @@ const ruleConfig: RuleConfig[] = [
   {
     name: 'costPrice',
     rule: (arg) => arg >= 0.01,
-    message: '商品成本价格必须大于等于 0.01 元!!!'
+    message: '商品成本价格必须大于等于 0.00 元!!!'
   }
 ]
 

+ 1 - 0
src/views/mall/promotion/diy/page/decorate.vue

@@ -30,6 +30,7 @@ const getPageDetail = async (id: any) => {
     formLoading.value = false
   }
 }
+
 // 提交表单
 const submitForm = async () => {
   // 校验表单

+ 3 - 2
src/views/mall/promotion/diy/template/decorate.vue

@@ -27,10 +27,11 @@
   </DiyEditor>
 </template>
 <script setup lang="ts">
+// TODO @疯狂:要不要建个 decorate 目录,然后挪进去,改成 index.vue,这样可以更明确看到是个独立界面哈,更好找
 import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
 import * as DiyPageApi from '@/api/mall/promotion/diy/page'
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util'
+import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util' // 商城的 DIY 组件,在 DiyEditor 目录下
 import { toNumber } from 'lodash-es'
 
 /** 装修模板表单 */
@@ -51,7 +52,7 @@ const formData = ref<DiyTemplateApi.DiyTemplatePropertyVO>()
 const formRef = ref() // 表单 Ref
 // 当前编辑的属性
 const currentFormData = ref<DiyTemplateApi.DiyTemplatePropertyVO | DiyPageApi.DiyPageVO>()
-// 商城H5预览地址
+// 商城 H5 预览地址
 const previewUrl = ref('')
 
 // 获取详情

+ 1 - 1
src/views/mall/promotion/diy/template/index.vue

@@ -205,7 +205,7 @@ const handleUse = async (row: DiyTemplateApi.DiyTemplateVO) => {
     // 使用模板的二次确认
     await message.confirm(`是否使用模板“${row.name}”?`)
     // 发起删除
-    await DiyTemplateApi.useDiyTemplate(row.id)
+    await DiyTemplateApi.useDiyTemplate(row.id!)
     message.success('使用成功')
     // 刷新列表
     await getList()

+ 113 - 0
src/views/report/ureport/UReportDataForm.vue

@@ -0,0 +1,113 @@
+<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="status">
+        <el-select v-model="formData.status" placeholder="请选择状态">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="文件内容" prop="content">
+        <Editor v-model="formData.content" height="150px" />
+      </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 { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as UReportDataApi from '@/api/report/ureport'
+
+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,
+  status: undefined,
+  content: undefined,
+  remark: undefined,
+})
+const formRules = reactive({
+  name: [{ required: true, message: '文件名称不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
+})
+const formRef = ref() // 表单 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 UReportDataApi.getUReportData(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+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 UReportDataApi.UReportDataVO
+    if (formType.value === 'create') {
+      await UReportDataApi.createUReportData(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await UReportDataApi.updateUReportData(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    status: undefined,
+    content: undefined,
+    remark: undefined,
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 13 - 0
src/views/report/ureport/index.vue

@@ -0,0 +1,13 @@
+<template>
+  <ContentWrap>
+    <IFrame :src="src" />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { getAccessToken } from '@/utils/auth'
+
+defineOptions({ name: 'UReportData' })
+
+const BASE_URL = import.meta.env.VITE_BASE_URL
+const src = ref(BASE_URL + '/ureport/designer?token=' + getAccessToken())
+</script>

+ 220 - 0
src/views/report/ureport/index2.vue

@@ -0,0 +1,220 @@
+<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 label="状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input
+          v-model="queryParams.remark"
+          placeholder="请输入备注"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          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"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['report:ureport-data:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['report:ureport-data: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="ID" align="center" prop="id" />
+      <el-table-column label="文件名称" align="center" prop="name" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="文件内容" align="center" prop="content" />
+      <el-table-column label="备注" align="center" prop="remark" />
+      <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="['report:ureport-data:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['report:ureport-data: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>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <UReportDataForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as UReportDataApi from '@/api/report/ureport'
+import UReportDataForm from './UReportDataForm.vue'
+
+defineOptions({ name: 'UReportData' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  status: null,
+  remark: null,
+  createTime: [],
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await UReportDataApi.getUReportDataPage(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 UReportDataApi.deleteUReportData(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await UReportDataApi.exportUReportData(queryParams)
+    download.excel(data, 'Ureport2报表.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>