Browse Source

CRM: 完善销售漏斗分析

puhui999 1 year ago
parent
commit
3c49ed570a

+ 25 - 0
src/api/crm/statistics/funnel.ts

@@ -0,0 +1,25 @@
+import request from '@/config/axios'
+
+export interface CrmStatisticFunnelRespVO {
+  customerCount: number // 客户数
+  businessCount: number // 商机数
+  winCount: number // 赢单数
+}
+
+// 客户分析 API
+export const StatisticFunnelApi = {
+  // 1. 获取销售漏斗统计数据
+  getFunnelSummary: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-funnel/get-funnel-summary',
+      params
+    })
+  },
+  // 2. 获取商机结束状态统计
+  getBusinessEndStatusSummary: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-funnel/get-business-end-status-summary',
+      params
+    })
+  }
+}

+ 7 - 6
src/utils/dict.ts

@@ -197,14 +197,15 @@ export enum DICT_TYPE {
   // ========== CRM - 客户管理模块 ==========
   CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态
   CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型
+  CRM_BUSINESS_END_STATUS_TYPE = 'crm_business_end_status_type', // CRM 商机结束状态类型
   CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式
-  CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry',
-  CRM_CUSTOMER_LEVEL = 'crm_customer_level',
-  CRM_CUSTOMER_SOURCE = 'crm_customer_source',
-  CRM_PRODUCT_STATUS = 'crm_product_status',
+  CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', // CRM 客户所属行业
+  CRM_CUSTOMER_LEVEL = 'crm_customer_level', // CRM 客户级别
+  CRM_CUSTOMER_SOURCE = 'crm_customer_source', // CRM 客户来源
+  CRM_PRODUCT_STATUS = 'crm_product_status', // CRM 商品状态
   CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 数据权限的级别
-  CRM_PRODUCT_UNIT = 'crm_product_unit', // 产品单位
-  CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // 跟进方式
+  CRM_PRODUCT_UNIT = 'crm_product_unit', // CRM 产品单位
+  CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // CRM 跟进方式
 
   // ========== ERP - 企业资源计划模块  ==========
   ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态

+ 135 - 0
src/views/crm/statistics/funnel/components/FunnelBusiness.vue

@@ -0,0 +1,135 @@
+<!-- 销售漏斗分析 -->
+<template>
+  <!-- Echarts图 -->
+  <el-card shadow="never">
+    <el-row>
+      <el-col :span="24">
+        <el-skeleton :loading="loading" animated>
+          <Echart :height="500" :options="echartsOption" />
+        </el-skeleton>
+      </el-col>
+    </el-row>
+  </el-card>
+
+  <!-- 统计列表 -->
+  <el-card class="mt-16px" shadow="never">
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="序号" type="index" width="80" />
+      <el-table-column align="center" label="阶段" prop="endStatus" width="200">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.CRM_BUSINESS_END_STATUS_TYPE" :value="scope.row.endStatus" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="商机数" min-width="200" prop="businessCount" />
+      <el-table-column align="center" label="商机总金额(元)" min-width="200" prop="totalPrice" />
+    </el-table>
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { CrmStatisticFunnelRespVO, StatisticFunnelApi } from '@/api/crm/statistics/funnel'
+import { EChartsOption } from 'echarts'
+import { DICT_TYPE } from '@/utils/dict'
+import echarts from '@/plugins/echarts'
+import { FunnelChart } from 'echarts/charts'
+
+echarts?.use([FunnelChart])
+defineOptions({ name: 'FunnelBusiness' })
+const props = defineProps<{ queryParams: any }>() // 搜索参数
+
+const loading = ref(false) // 加载中
+const list = ref<CrmStatisticFunnelRespVO[]>([]) // 列表的数据
+
+/** 销售漏斗 */
+const echartsOption = reactive<EChartsOption>({
+  title: {
+    text: '销售漏斗'
+  },
+  tooltip: {
+    trigger: 'item',
+    formatter: '{a} <br/>{b}'
+  },
+  toolbox: {
+    feature: {
+      dataView: { readOnly: false },
+      restore: {},
+      saveAsImage: {}
+    }
+  },
+  legend: {
+    data: ['客户', '商机', '赢单']
+  },
+  series: [
+    {
+      name: '销售漏斗',
+      type: 'funnel',
+      left: '10%',
+      top: 60,
+      bottom: 60,
+      width: '80%',
+      min: 0,
+      max: 100,
+      minSize: '0%',
+      maxSize: '100%',
+      sort: 'descending',
+      gap: 2,
+      label: {
+        show: true,
+        position: 'inside'
+      },
+      labelLine: {
+        length: 10,
+        lineStyle: {
+          width: 1,
+          type: 'solid'
+        }
+      },
+      itemStyle: {
+        borderColor: '#fff',
+        borderWidth: 1
+      },
+      emphasis: {
+        label: {
+          fontSize: 20
+        }
+      },
+      data: [
+        { value: 60, name: '客户-0个' },
+        { value: 40, name: '商机-0个' },
+        { value: 20, name: '赢单-0个' }
+      ]
+    }
+  ]
+}) as EChartsOption
+
+/** 获取统计数据 */
+const loadData = async () => {
+  loading.value = true
+  // 1. 加载漏斗数据
+  const data = (await StatisticFunnelApi.getFunnelSummary(
+    props.queryParams
+  )) as CrmStatisticFunnelRespVO
+  // 2.1 更新 Echarts 数据
+  if (
+    !!data &&
+    echartsOption.series &&
+    echartsOption.series[0] &&
+    echartsOption.series[0]['data']
+  ) {
+    // tips:写死 value 值是为了保持漏斗顺序不变
+    const list: { value: number; name: string }[] = []
+    list.push({ value: 60, name: `客户-${data.customerCount || 0}个` })
+    list.push({ value: 40, name: `商机-${data.businessCount || 0}个` })
+    list.push({ value: 20, name: `赢单-${data.winCount || 0}个` })
+    echartsOption.series[0]['data'] = list
+  }
+  // 2.2 获取商机结束状态统计
+  list.value = await StatisticFunnelApi.getBusinessEndStatusSummary(props.queryParams)
+  loading.value = false
+}
+defineExpose({ loadData })
+
+/** 初始化 */
+onMounted(() => {
+  loadData()
+})
+</script>

+ 138 - 0
src/views/crm/statistics/funnel/index.vue

@@ -0,0 +1,138 @@
+<!-- 数据统计 - 客户画像 -->
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="时间范围" prop="orderDate">
+        <el-date-picker
+          v-model="queryParams.times"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          :shortcuts="defaultShortcuts"
+          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="deptId">
+        <el-tree-select
+          v-model="queryParams.deptId"
+          :data="deptList"
+          :props="defaultProps"
+          check-strictly
+          class="!w-240px"
+          node-key="id"
+          placeholder="请选择归属部门"
+          @change="queryParams.userId = undefined"
+        />
+      </el-form-item>
+      <el-form-item label="员工" prop="userId">
+        <el-select v-model="queryParams.userId" class="!w-240px" clearable placeholder="员工">
+          <el-option
+            v-for="(user, index) in userListByDeptId"
+            :key="index"
+            :label="user.nickname"
+            :value="user.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <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-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 客户统计 -->
+  <el-col>
+    <el-tabs v-model="activeTab">
+      <el-tab-pane label="销售漏斗分析" lazy name="funnelRef">
+        <FunnelBusiness ref="funnelRef" :query-params="queryParams" />
+      </el-tab-pane>
+      <el-tab-pane label="新增商机分析" lazy name="levelRef" />
+      <el-tab-pane label="商机转化率分析" lazy name="sourceRef" />
+    </el-tabs>
+  </el-col>
+</template>
+
+<script lang="ts" setup>
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
+import { defaultProps, handleTree } from '@/utils/tree'
+import FunnelBusiness from './components/FunnelBusiness.vue'
+
+defineOptions({ name: 'CrmStatisticsFunnel' })
+
+const queryParams = reactive({
+  deptId: useUserStore().getUser.deptId,
+  userId: undefined,
+  times: [
+    // 默认显示最近一周的数据
+    formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+    formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
+  ]
+})
+
+const queryFormRef = ref() // 搜索的表单
+const deptList = ref<Tree[]>([]) // 部门树形结构
+const userList = ref<UserApi.UserVO[]>([]) // 全量用户清单
+
+/** 根据选择的部门筛选员工清单 */
+const userListByDeptId = computed(() =>
+  queryParams.deptId
+    ? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
+    : []
+)
+
+const activeTab = ref('funnelRef') // 活跃标签
+const funnelRef = ref() // 客户地区分布
+const levelRef = ref() // 客户级别
+const sourceRef = ref() // 客户来源
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  switch (activeTab.value) {
+    case 'funnelRef':
+      funnelRef.value?.loadData?.()
+      break
+    case 'levelRef':
+      levelRef.value?.loadData?.()
+      break
+    case 'sourceRef':
+      sourceRef.value?.loadData?.()
+      break
+  }
+}
+
+/** 当 activeTab 改变时,刷新当前活动的 tab */
+watch(activeTab, () => {
+  handleQuery()
+})
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 初始化 */
+onMounted(async () => {
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+  userList.value = handleTree(await UserApi.getSimpleUserList())
+})
+</script>