Sfoglia il codice sorgente

feat: CRM/数据统计/员工客户分析 初稿

dhb52 1 anno fa
parent
commit
d0501ee0a6

+ 29 - 0
src/api/crm/statistics/customer.ts

@@ -2,6 +2,7 @@ import request from '@/config/axios'
 
 export interface StatisticsCustomerRespVO {
   count: number
+  cycle: number
   category: string
 }
 
@@ -21,4 +22,32 @@ export const StatisticsCustomerApi = {
       params
     })
   },
+  // 获取客户跟进次数
+  getRecordCount: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-record-count',
+      params
+    })
+  },
+  // 获取客户跟进次数
+  getDistinctRecordCount: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-distinct-record-count',
+      params
+    })
+  },
+  // 获取客户跟进方式统计数
+  getRecordTypeCount: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-record-type-count',
+      params
+    })
+  },
+  // 获取客户成交周期
+  getCustomerCycle: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-customer/get-customer-cycle',
+      params
+    })
+  },
 }

+ 112 - 0
src/views/crm/statistics/customer/components/ConversionRate.vue

@@ -0,0 +1,112 @@
+<!-- 客户转化率分析 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="序号" align="center" type="index" width="80" />
+      <el-table-column label="日期" align="center" prop="category" min-width="200" />
+      <el-table-column label="新增客户数" align="center" prop="customerCount" min-width="200" />
+      <el-table-column label="成交客户数" align="center" prop="dealCustomerCount" min-width="200" />
+      <el-table-column label="转化率(%)" align="center" prop="conversionRate" min-width="200" />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsCustomerApi, StatisticsCustomerRespVO } from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+import { round } from 'lodash-es';
+
+defineOptions({ name: 'ConversionRate' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<StatisticsCustomerRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:纵向 */
+const echartsOption = reactive<EChartsOption>({
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: { },
+  series: [
+    {
+      name: '客户转化率',
+      type: 'line',
+      data: []
+    },
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        xAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '客户转化率分析' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  yAxis: {
+    type: 'value',
+    name: '转化率(%)'
+  },
+  xAxis: {
+    type: 'category',
+    name: '日期',
+    data: []
+  }
+}) as EChartsOption
+
+/** 获取统计数据 */
+const loadData = async () => {
+  // 1. 加载统计数据
+  loading.value = true
+  const customerCount = await StatisticsCustomerApi.getTotalCustomerCount(props.queryParams)
+  const dealCustomerCount = await StatisticsCustomerApi.getDealTotalCustomerCount(props.queryParams)
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+    echartsOption.xAxis['data'] = customerCount.map((s: StatisticsCustomerRespVO) => s.category)
+  }
+  if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+    echartsOption.series[0]['data'] = customerCount.map((item: StatisticsCustomerRespVO, index: number) => {
+    return {
+      name: item.category,
+      value: item.count ? round(dealCustomerCount[index].count / item.count * 100, 2) : 0,
+    }
+  })
+  }
+  // 2.2 更新列表数据
+  const tableData = customerCount.map((item: StatisticsCustomerRespVO, index: number) => {
+    return {
+      category: item.category,
+      dealCustomerCount: dealCustomerCount[index].count,
+      customerCount: item.count,
+      conversionRate: item.count ? round(dealCustomerCount[index].count / item.count * 100, 2) : 0,
+    }
+  })
+  list.value = tableData
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 112 - 0
src/views/crm/statistics/customer/components/CustomerCycle.vue

@@ -0,0 +1,112 @@
+<!-- 成交周期分析 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="序号" align="center" type="index" width="80" />
+      <el-table-column label="日期" align="center" prop="category" min-width="200" />
+      <el-table-column label="成交周期(天)" align="center" prop="customerCycle" min-width="200" />
+      <el-table-column label="成交客户数" align="center" prop="dealCustomerCount" min-width="200" />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsCustomerApi, StatisticsCustomerRespVO } from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+
+defineOptions({ name: 'TotalCustomerCount' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<StatisticsCustomerRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:纵向 */
+const echartsOption = reactive<EChartsOption>({
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: { },
+  series: [
+    {
+      name: '成交周期(天)',
+      type: 'bar',
+      data: []
+    },
+    {
+      name: '成交客户数',
+      type: 'bar',
+      data: []
+    },
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        xAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '成交周期分析' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  yAxis: {
+    type: 'value',
+    name: '数量(个)'
+  },
+  xAxis: {
+    type: 'category',
+    name: '日期',
+    data: []
+  }
+}) as EChartsOption
+
+/** 获取统计数据 */
+const loadData = async () => {
+  // 1. 加载统计数据
+  loading.value = true
+  const customerCycle = await StatisticsCustomerApi.getCustomerCycle(props.queryParams)
+  const dealCustomerCount = await StatisticsCustomerApi.getDealTotalCustomerCount(props.queryParams)
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+    echartsOption.xAxis['data'] = customerCycle.map((s: StatisticsCustomerRespVO) => s.category)
+  }
+  if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+    echartsOption.series[0]['data'] = customerCycle.map((s: StatisticsCustomerRespVO) => s['cycle'])
+  }
+  if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+    echartsOption.series[1]['data'] = dealCustomerCount.map((s: StatisticsCustomerRespVO) => s.count)
+  }
+  // 2.2 更新列表数据
+  const tableData = customerCycle.map((item: StatisticsCustomerRespVO, index: number) => {
+    return {
+      category: item.category,
+      customerCycle: item.cycle,
+      dealCustomerCount: dealCustomerCount[index].count,
+    }
+  })
+  list.value = tableData
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 112 - 0
src/views/crm/statistics/customer/components/FollowupCount.vue

@@ -0,0 +1,112 @@
+<!-- 客户跟进次数分析 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="序号" align="center" type="index" width="80" />
+      <el-table-column label="日期" align="center" prop="category" min-width="200" />
+      <el-table-column label="跟进客户数" align="center" prop="distinctRecordCount" min-width="200" />
+      <el-table-column label="跟进次数" align="center" prop="recordCount" min-width="200" />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsCustomerApi, StatisticsCustomerRespVO } from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+
+defineOptions({ name: 'FollowupCount' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<StatisticsCustomerRespVO[]>([]) // 列表的数据
+
+/** 柱状图配置:纵向 */
+const echartsOption = reactive<EChartsOption>({
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    containLabel: true
+  },
+  legend: { },
+  series: [
+    {
+      name: '跟进客户数',
+      type: 'bar',
+      data: []
+    },
+    {
+      name: '跟进次数',
+      type: 'bar',
+      data: []
+    },
+  ],
+  toolbox: {
+    feature: {
+      dataZoom: {
+        xAxisIndex: false // 数据区域缩放:Y 轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '客户跟进次数分析' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  yAxis: {
+    type: 'value',
+    name: '数量(个)'
+  },
+  xAxis: {
+    type: 'category',
+    name: '日期',
+    data: []
+  }
+}) as EChartsOption
+
+/** 获取统计数据 */
+const loadData = async () => {
+  // 1. 加载统计数据
+  loading.value = true
+  const recordCount = await StatisticsCustomerApi.getRecordCount(props.queryParams)
+  const distinctRecordCount = await StatisticsCustomerApi.getDistinctRecordCount(props.queryParams)
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+    echartsOption.xAxis['data'] = recordCount.map((s: StatisticsCustomerRespVO) => s.category)
+  }
+  if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+    echartsOption.series[0]['data'] = distinctRecordCount.map((s: StatisticsCustomerRespVO) => s.count)
+  }
+  if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+    echartsOption.series[1]['data'] = recordCount.map((s: StatisticsCustomerRespVO) => s.count)
+  }
+  // 2.2 更新列表数据
+  const tableData = recordCount.map((item: StatisticsCustomerRespVO, index: number) => {
+    return {
+      category: item.category,
+      recordCount: item.count,
+      distinctRecordCount: distinctRecordCount[index].count,
+    }
+  })
+  list.value = tableData
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 98 - 0
src/views/crm/statistics/customer/components/FollowupType.vue

@@ -0,0 +1,98 @@
+<!-- 客户跟进方式分析 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-skeleton :loading="loading" animated>
+      <Echart :height="500" :options="echartsOption" />
+    </el-skeleton>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card shadow="never" class="mt-16px">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="序号" align="center" type="index" width="80" />
+      <el-table-column label="跟进方式" align="center" prop="category" min-width="200" />
+      <el-table-column label="个数" align="center" prop="count" min-width="200" />
+      <el-table-column label="占比(%)" align="center" prop="portion" min-width="200" />
+    </el-table>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsCustomerApi, StatisticsCustomerRespVO } from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+import { round, sumBy } from 'lodash-es'
+
+defineOptions({ name: 'FollowupType' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<StatisticsCustomerRespVO[]>([]) // 列表的数据
+
+/** 饼图配置 */
+const echartsOption = reactive<EChartsOption>({
+  title: {
+    text: '客户跟进方式分析',
+    left: 'center'
+  },
+  legend: {
+    orient: 'vertical',
+    left: 'left'
+  },
+  tooltip: {
+    trigger: 'item',
+    formatter: '{b} : {c}% '
+  },
+  toolbox: {
+    feature: {
+      saveAsImage: { show: true, name: '客户跟进方式分析' } // 保存为图片
+    }
+  },
+  series: [
+    {
+      name: '跟进方式',
+      type: 'pie',
+      radius: '50%',
+      data: [],
+      emphasis: {
+        itemStyle: {
+          shadowBlur: 10,
+          shadowOffsetX: 0,
+          shadowColor: 'rgba(0, 0, 0, 0.5)'
+        }
+      }
+    }
+  ]
+}) as EChartsOption
+
+/** 获取统计数据 */
+const loadData = async () => {
+  // 1. 加载统计数据
+  loading.value = true
+  const recordTypeCount = await StatisticsCustomerApi.getRecordTypeCount(props.queryParams)
+  // 2.1 更新 Echarts 数据
+  if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+    echartsOption.series[0]['data'] = recordTypeCount.map((r: StatisticsCustomerRespVO) => {
+      return {
+        name: r.category,
+        value: r.count
+      }
+    })
+  }
+  // 2.2 更新列表数据
+  const totalCount = sumBy(recordTypeCount, 'count')
+  list.value = recordTypeCount.map((r: StatisticsCustomerRespVO) => {
+    return {
+      category: r.category,
+      count: r.count,
+      portion: round((r.count / totalCount) * 100, 2)
+    }
+  })
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 8 - 9
src/views/crm/statistics/customer/components/TotalCustomerCount.vue

@@ -1,6 +1,6 @@
 <!-- 客户总量分析 -->
 <template>
-  <!-- 柱状图 -->
+  <!-- Echarts图 -->
   <el-card shadow="never">
     <el-skeleton :loading="loading" animated>
       <Echart :height="500" :options="echartsOption" />
@@ -20,7 +20,6 @@
 <script setup lang="ts">
 import { StatisticsCustomerApi, StatisticsCustomerRespVO } from '@/api/crm/statistics/customer'
 import { EChartsOption } from 'echarts'
-import { clone } from 'lodash-es'
 
 defineOptions({ name: 'TotalCustomerCount' })
 const props = defineProps<{ queryParams: any }>() // 搜索参数
@@ -85,20 +84,20 @@ const loadData = async () => {
   const dealCustomerCount = await StatisticsCustomerApi.getDealTotalCustomerCount(props.queryParams)
   // 2.1 更新 Echarts 数据
   if (echartsOption.xAxis && echartsOption.xAxis['data']) {
-    echartsOption.xAxis['data'] = clone(customerCount.map(s => s['category']))
+    echartsOption.xAxis['data'] = customerCount.map((s: StatisticsCustomerRespVO) => s.category)
   }
   if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
-    echartsOption.series[0]['data'] = clone(customerCount.map(s => s['count']))
+    echartsOption.series[0]['data'] = customerCount.map((s: StatisticsCustomerRespVO) => s.count)
   }
   if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
-    echartsOption.series[1]['data'] = clone(dealCustomerCount.map(s => s['count']))
+    echartsOption.series[1]['data'] = dealCustomerCount.map((s: StatisticsCustomerRespVO) => s.count)
   }
   // 2.2 更新列表数据
-  const tableData = customerCount.map((item, index) => {
+  const tableData = customerCount.map((item: StatisticsCustomerRespVO, index: number) => {
     return {
-      category: item['category'],
-      customerCount: item['count'],
-      dealCustomerCount: dealCustomerCount[index]['count'],
+      category: item.category,
+      customerCount: item.count,
+      dealCustomerCount: dealCustomerCount[index].count,
     }
   })
   list.value = tableData

+ 39 - 1
src/views/crm/statistics/customer/index.vue

@@ -56,6 +56,22 @@
       <el-tab-pane label="客户总量分析" name="totalCustomerCount" lazy>
         <TotalCustomerCount :query-params="queryParams" ref="totalCustomerCountRef" />
       </el-tab-pane>
+      <!-- 客户跟进次数分析 -->
+      <el-tab-pane label="客户跟进次数分析" name="followupCount" lazy>
+        <FollowupCount :query-params="queryParams" ref="followupCountRef" />
+      </el-tab-pane>
+      <!-- 客户跟进方式分析 -->
+      <el-tab-pane label="客户跟进方式分析" name="followupType" lazy>
+        <FollowupType :query-params="queryParams" ref="followupTypeRef" />
+      </el-tab-pane>
+      <!-- 客户转化率分析 -->
+      <el-tab-pane label="客户转化率分析" name="conversionRate" lazy>
+        <ConversionRate :query-params="queryParams" ref="conversionRateRef" />
+      </el-tab-pane>
+      <!-- 成交周期分析 -->
+      <el-tab-pane label="成交周期分析" name="customerCycle" lazy>
+        <CustomerCycle :query-params="queryParams" ref="customerCycleRef" />
+      </el-tab-pane>
     </el-tabs>
   </el-col>
 </template>
@@ -66,6 +82,11 @@ import { useUserStore } from '@/store/modules/user'
 import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
 import { defaultProps, handleTree } from '@/utils/tree'
 import TotalCustomerCount from './components/TotalCustomerCount.vue'
+import FollowupCount from './components/FollowupCount.vue'
+import FollowupType from './components/FollowupType.vue'
+import ConversionRate from './components/ConversionRate.vue'
+import CustomerCycle from './components/CustomerCycle.vue' 
+
 
 defineOptions({ name: 'CrmStatisticsCustomer' })
 
@@ -84,7 +105,7 @@ const deptList = ref<Tree[]>([]) // 部门树形结构
 const userList = ref<UserApi.UserVO[]>([]) // 全量用户清单
 // 根据选择的部门筛选员工清单
 const userListByDeptId = computed(() =>
-  queryParams.deptId ? userList.value.filter((u) => u.deptId === queryParams.deptId) : []
+  queryParams.deptId ? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId) : []
 )
 
 // 活跃标签
@@ -92,10 +113,15 @@ const activeTab = ref('totalCustomerCount')
 // 1.客户总量分析
 const totalCustomerCountRef = ref()
 // 2.客户跟进次数分析
+const followupCountRef = ref()
 // 3.客户跟进方式分析
+const followupTypeRef = ref()
 // 4.客户转化率分析
+const conversionRateRef = ref()
 // 5.公海客户分析
+// 缺 crm_owner_record 表
 // 6.成交周期分析
+const customerCycleRef = ref()
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
@@ -103,6 +129,18 @@ const handleQuery = () => {
     case 'totalCustomerCount':
       totalCustomerCountRef.value?.loadData?.()
       break
+    case 'followupCount':
+      followupCountRef.value?.loadData?.()
+      break
+    case 'followupType':
+      followupTypeRef.value?.loadData?.()
+      break
+    case 'conversionRate':
+      conversionRateRef.value?.loadData?.()
+      break
+    case 'customerCycle':
+      customerCycleRef.value?.loadData?.()
+      break
   }
 }