Skip to content

Commit 3c49ed5

Browse files
author
puhui999
committed
CRM: 完善销售漏斗分析
1 parent 2d7bab8 commit 3c49ed5

File tree

4 files changed

+305
-6
lines changed

4 files changed

+305
-6
lines changed

src/api/crm/statistics/funnel.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import request from '@/config/axios'
2+
3+
export interface CrmStatisticFunnelRespVO {
4+
customerCount: number // 客户数
5+
businessCount: number // 商机数
6+
winCount: number // 赢单数
7+
}
8+
9+
// 客户分析 API
10+
export const StatisticFunnelApi = {
11+
// 1. 获取销售漏斗统计数据
12+
getFunnelSummary: (params: any) => {
13+
return request.get({
14+
url: '/crm/statistics-funnel/get-funnel-summary',
15+
params
16+
})
17+
},
18+
// 2. 获取商机结束状态统计
19+
getBusinessEndStatusSummary: (params: any) => {
20+
return request.get({
21+
url: '/crm/statistics-funnel/get-business-end-status-summary',
22+
params
23+
})
24+
}
25+
}

src/utils/dict.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,15 @@ export enum DICT_TYPE {
197197
// ========== CRM - 客户管理模块 ==========
198198
CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态
199199
CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型
200+
CRM_BUSINESS_END_STATUS_TYPE = 'crm_business_end_status_type', // CRM 商机结束状态类型
200201
CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式
201-
CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry',
202-
CRM_CUSTOMER_LEVEL = 'crm_customer_level',
203-
CRM_CUSTOMER_SOURCE = 'crm_customer_source',
204-
CRM_PRODUCT_STATUS = 'crm_product_status',
202+
CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', // CRM 客户所属行业
203+
CRM_CUSTOMER_LEVEL = 'crm_customer_level', // CRM 客户级别
204+
CRM_CUSTOMER_SOURCE = 'crm_customer_source', // CRM 客户来源
205+
CRM_PRODUCT_STATUS = 'crm_product_status', // CRM 商品状态
205206
CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 数据权限的级别
206-
CRM_PRODUCT_UNIT = 'crm_product_unit', // 产品单位
207-
CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // 跟进方式
207+
CRM_PRODUCT_UNIT = 'crm_product_unit', // CRM 产品单位
208+
CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // CRM 跟进方式
208209

209210
// ========== ERP - 企业资源计划模块 ==========
210211
ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<!-- 销售漏斗分析 -->
2+
<template>
3+
<!-- Echarts图 -->
4+
<el-card shadow="never">
5+
<el-row>
6+
<el-col :span="24">
7+
<el-skeleton :loading="loading" animated>
8+
<Echart :height="500" :options="echartsOption" />
9+
</el-skeleton>
10+
</el-col>
11+
</el-row>
12+
</el-card>
13+
14+
<!-- 统计列表 -->
15+
<el-card class="mt-16px" shadow="never">
16+
<el-table v-loading="loading" :data="list">
17+
<el-table-column align="center" label="序号" type="index" width="80" />
18+
<el-table-column align="center" label="阶段" prop="endStatus" width="200">
19+
<template #default="scope">
20+
<dict-tag :type="DICT_TYPE.CRM_BUSINESS_END_STATUS_TYPE" :value="scope.row.endStatus" />
21+
</template>
22+
</el-table-column>
23+
<el-table-column align="center" label="商机数" min-width="200" prop="businessCount" />
24+
<el-table-column align="center" label="商机总金额(元)" min-width="200" prop="totalPrice" />
25+
</el-table>
26+
</el-card>
27+
</template>
28+
<script lang="ts" setup>
29+
import { CrmStatisticFunnelRespVO, StatisticFunnelApi } from '@/api/crm/statistics/funnel'
30+
import { EChartsOption } from 'echarts'
31+
import { DICT_TYPE } from '@/utils/dict'
32+
import echarts from '@/plugins/echarts'
33+
import { FunnelChart } from 'echarts/charts'
34+
35+
echarts?.use([FunnelChart])
36+
defineOptions({ name: 'FunnelBusiness' })
37+
const props = defineProps<{ queryParams: any }>() // 搜索参数
38+
39+
const loading = ref(false) // 加载中
40+
const list = ref<CrmStatisticFunnelRespVO[]>([]) // 列表的数据
41+
42+
/** 销售漏斗 */
43+
const echartsOption = reactive<EChartsOption>({
44+
title: {
45+
text: '销售漏斗'
46+
},
47+
tooltip: {
48+
trigger: 'item',
49+
formatter: '{a} <br/>{b}'
50+
},
51+
toolbox: {
52+
feature: {
53+
dataView: { readOnly: false },
54+
restore: {},
55+
saveAsImage: {}
56+
}
57+
},
58+
legend: {
59+
data: ['客户', '商机', '赢单']
60+
},
61+
series: [
62+
{
63+
name: '销售漏斗',
64+
type: 'funnel',
65+
left: '10%',
66+
top: 60,
67+
bottom: 60,
68+
width: '80%',
69+
min: 0,
70+
max: 100,
71+
minSize: '0%',
72+
maxSize: '100%',
73+
sort: 'descending',
74+
gap: 2,
75+
label: {
76+
show: true,
77+
position: 'inside'
78+
},
79+
labelLine: {
80+
length: 10,
81+
lineStyle: {
82+
width: 1,
83+
type: 'solid'
84+
}
85+
},
86+
itemStyle: {
87+
borderColor: '#fff',
88+
borderWidth: 1
89+
},
90+
emphasis: {
91+
label: {
92+
fontSize: 20
93+
}
94+
},
95+
data: [
96+
{ value: 60, name: '客户-0个' },
97+
{ value: 40, name: '商机-0个' },
98+
{ value: 20, name: '赢单-0个' }
99+
]
100+
}
101+
]
102+
}) as EChartsOption
103+
104+
/** 获取统计数据 */
105+
const loadData = async () => {
106+
loading.value = true
107+
// 1. 加载漏斗数据
108+
const data = (await StatisticFunnelApi.getFunnelSummary(
109+
props.queryParams
110+
)) as CrmStatisticFunnelRespVO
111+
// 2.1 更新 Echarts 数据
112+
if (
113+
!!data &&
114+
echartsOption.series &&
115+
echartsOption.series[0] &&
116+
echartsOption.series[0]['data']
117+
) {
118+
// tips:写死 value 值是为了保持漏斗顺序不变
119+
const list: { value: number; name: string }[] = []
120+
list.push({ value: 60, name: `客户-${data.customerCount || 0}个` })
121+
list.push({ value: 40, name: `商机-${data.businessCount || 0}个` })
122+
list.push({ value: 20, name: `赢单-${data.winCount || 0}个` })
123+
echartsOption.series[0]['data'] = list
124+
}
125+
// 2.2 获取商机结束状态统计
126+
list.value = await StatisticFunnelApi.getBusinessEndStatusSummary(props.queryParams)
127+
loading.value = false
128+
}
129+
defineExpose({ loadData })
130+
131+
/** 初始化 */
132+
onMounted(() => {
133+
loadData()
134+
})
135+
</script>
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<!-- 数据统计 - 客户画像 -->
2+
<template>
3+
<ContentWrap>
4+
<!-- 搜索工作栏 -->
5+
<el-form
6+
ref="queryFormRef"
7+
:inline="true"
8+
:model="queryParams"
9+
class="-mb-15px"
10+
label-width="68px"
11+
>
12+
<el-form-item label="时间范围" prop="orderDate">
13+
<el-date-picker
14+
v-model="queryParams.times"
15+
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
16+
:shortcuts="defaultShortcuts"
17+
class="!w-240px"
18+
end-placeholder="结束日期"
19+
start-placeholder="开始日期"
20+
type="daterange"
21+
value-format="YYYY-MM-DD HH:mm:ss"
22+
/>
23+
</el-form-item>
24+
<el-form-item label="归属部门" prop="deptId">
25+
<el-tree-select
26+
v-model="queryParams.deptId"
27+
:data="deptList"
28+
:props="defaultProps"
29+
check-strictly
30+
class="!w-240px"
31+
node-key="id"
32+
placeholder="请选择归属部门"
33+
@change="queryParams.userId = undefined"
34+
/>
35+
</el-form-item>
36+
<el-form-item label="员工" prop="userId">
37+
<el-select v-model="queryParams.userId" class="!w-240px" clearable placeholder="员工">
38+
<el-option
39+
v-for="(user, index) in userListByDeptId"
40+
:key="index"
41+
:label="user.nickname"
42+
:value="user.id"
43+
/>
44+
</el-select>
45+
</el-form-item>
46+
<el-form-item>
47+
<el-button @click="handleQuery">
48+
<Icon class="mr-5px" icon="ep:search" />
49+
搜索
50+
</el-button>
51+
<el-button @click="resetQuery">
52+
<Icon class="mr-5px" icon="ep:refresh" />
53+
重置
54+
</el-button>
55+
</el-form-item>
56+
</el-form>
57+
</ContentWrap>
58+
59+
<!-- 客户统计 -->
60+
<el-col>
61+
<el-tabs v-model="activeTab">
62+
<el-tab-pane label="销售漏斗分析" lazy name="funnelRef">
63+
<FunnelBusiness ref="funnelRef" :query-params="queryParams" />
64+
</el-tab-pane>
65+
<el-tab-pane label="新增商机分析" lazy name="levelRef" />
66+
<el-tab-pane label="商机转化率分析" lazy name="sourceRef" />
67+
</el-tabs>
68+
</el-col>
69+
</template>
70+
71+
<script lang="ts" setup>
72+
import * as DeptApi from '@/api/system/dept'
73+
import * as UserApi from '@/api/system/user'
74+
import { useUserStore } from '@/store/modules/user'
75+
import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
76+
import { defaultProps, handleTree } from '@/utils/tree'
77+
import FunnelBusiness from './components/FunnelBusiness.vue'
78+
79+
defineOptions({ name: 'CrmStatisticsFunnel' })
80+
81+
const queryParams = reactive({
82+
deptId: useUserStore().getUser.deptId,
83+
userId: undefined,
84+
times: [
85+
// 默认显示最近一周的数据
86+
formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
87+
formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
88+
]
89+
})
90+
91+
const queryFormRef = ref() // 搜索的表单
92+
const deptList = ref<Tree[]>([]) // 部门树形结构
93+
const userList = ref<UserApi.UserVO[]>([]) // 全量用户清单
94+
95+
/** 根据选择的部门筛选员工清单 */
96+
const userListByDeptId = computed(() =>
97+
queryParams.deptId
98+
? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
99+
: []
100+
)
101+
102+
const activeTab = ref('funnelRef') // 活跃标签
103+
const funnelRef = ref() // 客户地区分布
104+
const levelRef = ref() // 客户级别
105+
const sourceRef = ref() // 客户来源
106+
107+
/** 搜索按钮操作 */
108+
const handleQuery = () => {
109+
switch (activeTab.value) {
110+
case 'funnelRef':
111+
funnelRef.value?.loadData?.()
112+
break
113+
case 'levelRef':
114+
levelRef.value?.loadData?.()
115+
break
116+
case 'sourceRef':
117+
sourceRef.value?.loadData?.()
118+
break
119+
}
120+
}
121+
122+
/** 当 activeTab 改变时,刷新当前活动的 tab */
123+
watch(activeTab, () => {
124+
handleQuery()
125+
})
126+
127+
/** 重置按钮操作 */
128+
const resetQuery = () => {
129+
queryFormRef.value.resetFields()
130+
handleQuery()
131+
}
132+
133+
/** 初始化 */
134+
onMounted(async () => {
135+
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
136+
userList.value = handleTree(await UserApi.getSimpleUserList())
137+
})
138+
</script>

0 commit comments

Comments
 (0)