CRM: 完善用户画像数据统计

This commit is contained in:
puhui999 2024-04-03 12:19:09 +08:00
parent d16bed4ad7
commit 36385a74ba
9 changed files with 339 additions and 137 deletions

View File

@ -49,36 +49,6 @@ export interface CrmStatisticsCustomerDealCycleByDateRespVO {
customerDealCycle: number customerDealCycle: number
} }
export interface CrmStatisticCustomerBaseRespVO {
customerCount: number
dealCount: number
dealPortion: number
}
export interface CrmStatisticCustomerIndustryRespVO extends CrmStatisticCustomerBaseRespVO {
industryId: number
industryName: string
industryPortion: number
}
export interface CrmStatisticCustomerSourceRespVO extends CrmStatisticCustomerBaseRespVO {
source: number
sourceName: string
sourcePortion: number
}
export interface CrmStatisticCustomerLevelRespVO extends CrmStatisticCustomerBaseRespVO {
level: number
levelName: string
levelPortion: number
}
export interface CrmStatisticCustomerAreaRespVO extends CrmStatisticCustomerBaseRespVO {
areaId: number
areaName: string
areaPortion: number
}
export interface CrmStatisticsCustomerDealCycleByUserRespVO { export interface CrmStatisticsCustomerDealCycleByUserRespVO {
ownerUserName: string ownerUserName: string
customerDealCycle: number customerDealCycle: number
@ -142,34 +112,5 @@ export const StatisticsCustomerApi = {
url: '/crm/statistics-customer/get-customer-deal-cycle-by-user', url: '/crm/statistics-customer/get-customer-deal-cycle-by-user',
params params
}) })
},
// TODO @puhui999下面这些拆出去哈
// 6.1 获取客户行业统计数据
getCustomerIndustry: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-industry-summary',
params
})
},
// 6.1 获取客户来源统计数据
getCustomerSource: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-source-summary',
params
})
},
// 6.1 获取客户行业统计数据
getCustomerLevel: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-level-summary',
params
})
},
// 6.1 获取客户行业统计数据
getCustomerArea: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-area-summary',
params
})
} }
} }

View File

@ -0,0 +1,60 @@
import request from '@/config/axios'
export interface CrmStatisticCustomerBaseRespVO {
customerCount: number
dealCount: number
dealPortion: string | number
}
export interface CrmStatisticCustomerIndustryRespVO extends CrmStatisticCustomerBaseRespVO {
industryId: number
industryPortion: string | number
}
export interface CrmStatisticCustomerSourceRespVO extends CrmStatisticCustomerBaseRespVO {
source: number
sourcePortion: string | number
}
export interface CrmStatisticCustomerLevelRespVO extends CrmStatisticCustomerBaseRespVO {
level: number
levelPortion: string | number
}
export interface CrmStatisticCustomerAreaRespVO extends CrmStatisticCustomerBaseRespVO {
areaId: number
areaName: string
areaPortion: string | number
}
// 客户分析 API
export const StatisticsPortraitApi = {
// 1. 获取客户行业统计数据
getCustomerIndustry: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-industry-summary',
params
})
},
// 2. 获取客户来源统计数据
getCustomerSource: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-source-summary',
params
})
},
// 3. 获取客户级别统计数据
getCustomerLevel: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-level-summary',
params
})
},
// 4. 获取客户地区统计数据
getCustomerArea: (params: any) => {
return request.get({
url: '/crm/statistics-portrait/get-customer-area-summary',
params
})
}
}

View File

@ -137,7 +137,7 @@ export enum DICT_TYPE {
INFRA_FILE_STORAGE = 'infra_file_storage', INFRA_FILE_STORAGE = 'infra_file_storage',
// ========== BPM 模块 ========== // ========== BPM 模块 ==========
BPM_MODEL_FORM_TYPE = 'bpm_model_category', BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy', BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status', BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
BPM_TASK_STATUS = 'bpm_task_status', BPM_TASK_STATUS = 'bpm_task_status',

View File

@ -3,22 +3,22 @@
<ContentWrap> <ContentWrap>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<el-form <el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef" ref="queryFormRef"
:inline="true" :inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px" label-width="68px"
> >
<el-form-item label="时间范围" prop="orderDate"> <el-form-item label="时间范围" prop="orderDate">
<el-date-picker <el-date-picker
v-model="queryParams.times" v-model="queryParams.times"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
:shortcuts="defaultShortcuts" :shortcuts="defaultShortcuts"
class="!w-240px" class="!w-240px"
end-placeholder="结束日期" end-placeholder="结束日期"
start-placeholder="开始日期" start-placeholder="开始日期"
type="daterange" type="daterange"
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
/> />
</el-form-item> </el-form-item>
<el-form-item label="时间间隔" prop="interval"> <el-form-item label="时间间隔" prop="interval">
@ -34,28 +34,34 @@
<el-form-item label="归属部门" prop="deptId"> <el-form-item label="归属部门" prop="deptId">
<el-tree-select <el-tree-select
v-model="queryParams.deptId" v-model="queryParams.deptId"
class="!w-240px"
:data="deptList" :data="deptList"
:props="defaultProps" :props="defaultProps"
check-strictly check-strictly
class="!w-240px"
node-key="id" node-key="id"
placeholder="请选择归属部门" placeholder="请选择归属部门"
@change="queryParams.userId = undefined" @change="queryParams.userId = undefined"
/> />
</el-form-item> </el-form-item>
<el-form-item label="员工" prop="userId"> <el-form-item label="员工" prop="userId">
<el-select v-model="queryParams.userId" class="!w-240px" placeholder="员工" clearable> <el-select v-model="queryParams.userId" class="!w-240px" clearable placeholder="员工">
<el-option <el-option
v-for="(user, index) in userListByDeptId" v-for="(user, index) in userListByDeptId"
:key="index"
:label="user.nickname" :label="user.nickname"
:value="user.id" :value="user.id"
:key="index"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" /> 搜索 </el-button> <el-button @click="handleQuery">
<el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 重置 </el-button> <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-item>
</el-form> </el-form>
</ContentWrap> </ContentWrap>
@ -64,41 +70,25 @@
<el-col> <el-col>
<el-tabs v-model="activeTab"> <el-tabs v-model="activeTab">
<!-- 客户总量分析 --> <!-- 客户总量分析 -->
<el-tab-pane label="客户总量分析" name="customerSummary" lazy> <el-tab-pane label="客户总量分析" lazy name="customerSummary">
<CustomerSummary :query-params="queryParams" ref="customerSummaryRef" /> <CustomerSummary ref="customerSummaryRef" :query-params="queryParams" />
</el-tab-pane> </el-tab-pane>
<!-- 客户跟进次数分析 --> <!-- 客户跟进次数分析 -->
<el-tab-pane label="客户跟进次数分析" name="followUpSummary" lazy> <el-tab-pane label="客户跟进次数分析" lazy name="followUpSummary">
<CustomerFollowUpSummary :query-params="queryParams" ref="followUpSummaryRef" /> <CustomerFollowUpSummary ref="followUpSummaryRef" :query-params="queryParams" />
</el-tab-pane> </el-tab-pane>
<!-- 客户跟进方式分析 --> <!-- 客户跟进方式分析 -->
<el-tab-pane label="客户跟进方式分析" name="followUpType" lazy> <el-tab-pane label="客户跟进方式分析" lazy name="followUpType">
<CustomerFollowUpType :query-params="queryParams" ref="followUpTypeRef" /> <CustomerFollowUpType ref="followUpTypeRef" :query-params="queryParams" />
</el-tab-pane> </el-tab-pane>
<!-- 客户转化率分析 --> <!-- 客户转化率分析 -->
<el-tab-pane label="客户转化率分析" name="conversionStat" lazy> <el-tab-pane label="客户转化率分析" lazy name="conversionStat">
<CustomerConversionStat :query-params="queryParams" ref="conversionStatRef" /> <CustomerConversionStat ref="conversionStatRef" :query-params="queryParams" />
</el-tab-pane> </el-tab-pane>
<!-- 成交周期分析 --> <!-- 成交周期分析 -->
<el-tab-pane label="成交周期分析" lazy name="dealCycle"> <el-tab-pane label="成交周期分析" lazy name="dealCycle">
<CustomerDealCycle ref="dealCycleRef" :query-params="queryParams" /> <CustomerDealCycle ref="dealCycleRef" :query-params="queryParams" />
</el-tab-pane> </el-tab-pane>
<!-- 城市分布分析 -->
<el-tab-pane label="城市分布分析" lazy name="addressRef">
<CustomerAddress ref="addressRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户级别分析 -->
<el-tab-pane label="客户级别分析" lazy name="levelRef">
<CustomerLevel ref="levelRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户来源分析 -->
<el-tab-pane label="客户来源分析" lazy name="sourceRef">
<CustomerSource ref="sourceRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户行业分析 -->
<el-tab-pane label="客户行业分析" lazy name="industryRef">
<CustomerIndustry ref="industryRef" :query-params="queryParams" />
</el-tab-pane>
</el-tabs> </el-tabs>
</el-col> </el-col>
</template> </template>
@ -115,10 +105,6 @@ import CustomerFollowUpType from './components/CustomerFollowUpType.vue'
import CustomerConversionStat from './components/CustomerConversionStat.vue' import CustomerConversionStat from './components/CustomerConversionStat.vue'
import CustomerDealCycle from './components/CustomerDealCycle.vue' import CustomerDealCycle from './components/CustomerDealCycle.vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import CustomerAddress from './components/CustomerAddress.vue'
import CustomerIndustry from './components/CustomerIndustry.vue'
import CustomerSource from './components/CustomerSource.vue'
import CustomerLevel from './components/CustomerLevel.vue'
defineOptions({ name: 'CrmStatisticsCustomer' }) defineOptions({ name: 'CrmStatisticsCustomer' })
@ -152,13 +138,6 @@ const conversionStatRef = ref() // 4. 客户转化率分析
// 5. TODO // 5. TODO
// crm_owner_record TODO @dhb52 + mock // crm_owner_record TODO @dhb52 + mock
const dealCycleRef = ref() // 6. const dealCycleRef = ref() // 6.
const addressRef = ref()
//
const levelRef = ref()
//
const sourceRef = ref()
//
const industryRef = ref()
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { const handleQuery = () => {
@ -178,19 +157,6 @@ const handleQuery = () => {
case 'dealCycle': // case 'dealCycle': //
dealCycleRef.value?.loadData?.() dealCycleRef.value?.loadData?.()
break break
// TODO @puhui999 4
case 'addressRef':
addressRef.value?.loadData?.()
break
case 'levelRef':
levelRef.value?.loadData?.()
break
case 'sourceRef':
sourceRef.value?.loadData?.()
break
case 'industryRef':
industryRef.value?.loadData?.()
break
} }
} }

View File

@ -22,8 +22,8 @@ import china from '@/assets/map/json/china.json'
import echarts from '@/plugins/echarts' import echarts from '@/plugins/echarts'
import { import {
CrmStatisticCustomerAreaRespVO, CrmStatisticCustomerAreaRespVO,
StatisticsCustomerApi StatisticsPortraitApi
} from '@/api/crm/statistics/customer' } from '@/api/crm/statistics/portrait'
defineOptions({ name: 'CustomerAddress' }) defineOptions({ name: 'CustomerAddress' })
const props = defineProps<{ queryParams: any }>() // const props = defineProps<{ queryParams: any }>() //
@ -102,7 +102,7 @@ const echartsOption2 = reactive<EChartsOption>({
const loadData = async () => { const loadData = async () => {
// 1. // 1.
loading.value = true loading.value = true
const areaList = await StatisticsCustomerApi.getCustomerArea(props.queryParams) const areaList = await StatisticsPortraitApi.getCustomerArea(props.queryParams)
areaStatisticsList.value = areaList.map((item: CrmStatisticCustomerAreaRespVO) => { areaStatisticsList.value = areaList.map((item: CrmStatisticCustomerAreaRespVO) => {
return { return {
...item, ...item,

View File

@ -20,7 +20,11 @@
<el-card class="mt-16px" shadow="never"> <el-card class="mt-16px" shadow="never">
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column align="center" label="序号" type="index" width="80" /> <el-table-column align="center" label="序号" type="index" width="80" />
<el-table-column align="center" label="客户行业" min-width="200" prop="industryName" /> <el-table-column align="center" label="客户行业" prop="industryId" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
</template>
</el-table-column>
<el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" /> <el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" />
<el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" /> <el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" />
<el-table-column align="center" label="行业占比(%)" min-width="200" prop="industryPortion" /> <el-table-column align="center" label="行业占比(%)" min-width="200" prop="industryPortion" />
@ -31,9 +35,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
CrmStatisticCustomerIndustryRespVO, CrmStatisticCustomerIndustryRespVO,
StatisticsCustomerApi StatisticsPortraitApi
} from '@/api/crm/statistics/customer' } from '@/api/crm/statistics/portrait'
import { EChartsOption } from 'echarts' import { EChartsOption } from 'echarts'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { getSumValue } from '@/utils'
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'CustomerIndustry' }) defineOptions({ name: 'CustomerIndustry' })
const props = defineProps<{ queryParams: any }>() // const props = defineProps<{ queryParams: any }>() //
@ -140,12 +147,12 @@ const echartsOption2 = reactive<EChartsOption>({
const loadData = async () => { const loadData = async () => {
// 1. // 1.
loading.value = true loading.value = true
const industryList = await StatisticsCustomerApi.getCustomerIndustry(props.queryParams) const industryList = await StatisticsPortraitApi.getCustomerIndustry(props.queryParams)
// 2.1 Echarts // 2.1 Echarts
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) { if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = industryList.map((r: CrmStatisticCustomerIndustryRespVO) => { echartsOption.series[0]['data'] = industryList.map((r: CrmStatisticCustomerIndustryRespVO) => {
return { return {
name: r.industryName, name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
value: r.customerCount value: r.customerCount
} }
}) })
@ -154,15 +161,34 @@ const loadData = async () => {
if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) { if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
echartsOption2.series[0]['data'] = industryList.map((r: CrmStatisticCustomerIndustryRespVO) => { echartsOption2.series[0]['data'] = industryList.map((r: CrmStatisticCustomerIndustryRespVO) => {
return { return {
name: r.industryName, name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
value: r.dealCount value: r.dealCount
} }
}) })
} }
// 3.
calculateProportion(industryList)
list.value = industryList list.value = industryList
loading.value = false loading.value = false
} }
defineExpose({ loadData }) defineExpose({ loadData })
/**
* 计算比例
*/
const calculateProportion = (sourceList: CrmStatisticCustomerIndustryRespVO[]) => {
if (isEmpty(sourceList)) {
return
}
//
const list = sourceList as unknown as CrmStatisticCustomerIndustryRespVO[]
const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
const sumDealCount = getSumValue(list.map((item) => item.dealCount))
list.forEach((item) => {
item.industryPortion =
item.customerCount === 0 ? 0 : ((item.customerCount / sumCustomerCount) * 100).toFixed(2)
item.dealPortion = item.dealCount === 0 ? 0 : ((item.dealCount / sumDealCount) * 100).toFixed(2)
})
}
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {

View File

@ -20,7 +20,11 @@
<el-card class="mt-16px" shadow="never"> <el-card class="mt-16px" shadow="never">
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column align="center" label="序号" type="index" width="80" /> <el-table-column align="center" label="序号" type="index" width="80" />
<el-table-column align="center" label="客户来源" min-width="200" prop="levelName" /> <el-table-column align="center" label="客户级别" prop="level" width="200">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
</template>
</el-table-column>
<el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" /> <el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" />
<el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" /> <el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" />
<el-table-column align="center" label="级别占比(%)" min-width="200" prop="levelPortion" /> <el-table-column align="center" label="级别占比(%)" min-width="200" prop="levelPortion" />
@ -31,9 +35,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
CrmStatisticCustomerLevelRespVO, CrmStatisticCustomerLevelRespVO,
StatisticsCustomerApi StatisticsPortraitApi
} from '@/api/crm/statistics/customer' } from '@/api/crm/statistics/portrait'
import { EChartsOption } from 'echarts' import { EChartsOption } from 'echarts'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { getSumValue } from '@/utils'
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'CustomerSource' }) defineOptions({ name: 'CustomerSource' })
const props = defineProps<{ queryParams: any }>() // const props = defineProps<{ queryParams: any }>() //
@ -140,12 +147,12 @@ const echartsOption2 = reactive<EChartsOption>({
const loadData = async () => { const loadData = async () => {
// 1. // 1.
loading.value = true loading.value = true
const levelList = await StatisticsCustomerApi.getCustomerLevel(props.queryParams) const levelList = await StatisticsPortraitApi.getCustomerLevel(props.queryParams)
// 2.1 Echarts // 2.1 Echarts
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) { if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = levelList.map((r: CrmStatisticCustomerLevelRespVO) => { echartsOption.series[0]['data'] = levelList.map((r: CrmStatisticCustomerLevelRespVO) => {
return { return {
name: r.levelName, name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
value: r.customerCount value: r.customerCount
} }
}) })
@ -154,15 +161,34 @@ const loadData = async () => {
if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) { if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
echartsOption2.series[0]['data'] = levelList.map((r: CrmStatisticCustomerLevelRespVO) => { echartsOption2.series[0]['data'] = levelList.map((r: CrmStatisticCustomerLevelRespVO) => {
return { return {
name: r.levelName, name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
value: r.dealCount value: r.dealCount
} }
}) })
} }
// 3.
calculateProportion(levelList)
list.value = levelList list.value = levelList
loading.value = false loading.value = false
} }
defineExpose({ loadData }) defineExpose({ loadData })
/**
* 计算比例
*/
const calculateProportion = (levelList: CrmStatisticCustomerLevelRespVO[]) => {
if (isEmpty(levelList)) {
return
}
//
const list = levelList as unknown as CrmStatisticCustomerLevelRespVO[]
const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
const sumDealCount = getSumValue(list.map((item) => item.dealCount))
list.forEach((item) => {
item.levelPortion =
item.customerCount === 0 ? 0 : ((item.customerCount / sumCustomerCount) * 100).toFixed(2)
item.dealPortion = item.dealCount === 0 ? 0 : ((item.dealCount / sumDealCount) * 100).toFixed(2)
})
}
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {

View File

@ -20,7 +20,11 @@
<el-card class="mt-16px" shadow="never"> <el-card class="mt-16px" shadow="never">
<el-table v-loading="loading" :data="list"> <el-table v-loading="loading" :data="list">
<el-table-column align="center" label="序号" type="index" width="80" /> <el-table-column align="center" label="序号" type="index" width="80" />
<el-table-column align="center" label="客户来源" min-width="200" prop="sourceName" /> <el-table-column align="center" label="客户来源" prop="source" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
</template>
</el-table-column>
<el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" /> <el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" />
<el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" /> <el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" />
<el-table-column align="center" label="来源占比(%)" min-width="200" prop="sourcePortion" /> <el-table-column align="center" label="来源占比(%)" min-width="200" prop="sourcePortion" />
@ -31,9 +35,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
CrmStatisticCustomerSourceRespVO, CrmStatisticCustomerSourceRespVO,
StatisticsCustomerApi StatisticsPortraitApi
} from '@/api/crm/statistics/customer' } from '@/api/crm/statistics/portrait'
import { EChartsOption } from 'echarts' import { EChartsOption } from 'echarts'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { isEmpty } from '@/utils/is'
import { getSumValue } from '@/utils'
defineOptions({ name: 'CustomerSource' }) defineOptions({ name: 'CustomerSource' })
const props = defineProps<{ queryParams: any }>() // const props = defineProps<{ queryParams: any }>() //
@ -140,12 +147,12 @@ const echartsOption2 = reactive<EChartsOption>({
const loadData = async () => { const loadData = async () => {
// 1. // 1.
loading.value = true loading.value = true
const sourceList = await StatisticsCustomerApi.getCustomerSource(props.queryParams) const sourceList = await StatisticsPortraitApi.getCustomerSource(props.queryParams)
// 2.1 Echarts // 2.1 Echarts
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) { if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = sourceList.map((r: CrmStatisticCustomerSourceRespVO) => { echartsOption.series[0]['data'] = sourceList.map((r: CrmStatisticCustomerSourceRespVO) => {
return { return {
name: r.sourceName, name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
value: r.customerCount value: r.customerCount
} }
}) })
@ -154,16 +161,36 @@ const loadData = async () => {
if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) { if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
echartsOption2.series[0]['data'] = sourceList.map((r: CrmStatisticCustomerSourceRespVO) => { echartsOption2.series[0]['data'] = sourceList.map((r: CrmStatisticCustomerSourceRespVO) => {
return { return {
name: r.sourceName, name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
value: r.dealCount value: r.dealCount
} }
}) })
} }
// 3.
calculateProportion(sourceList)
list.value = sourceList list.value = sourceList
loading.value = false loading.value = false
} }
defineExpose({ loadData }) defineExpose({ loadData })
/**
* 计算比例
*/
const calculateProportion = (sourceList: CrmStatisticCustomerSourceRespVO[]) => {
if (isEmpty(sourceList)) {
return
}
//
const list = sourceList as unknown as CrmStatisticCustomerSourceRespVO[]
const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
const sumDealCount = getSumValue(list.map((item) => item.dealCount))
list.forEach((item) => {
item.sourcePortion =
item.customerCount === 0 ? 0 : ((item.customerCount / sumCustomerCount) * 100).toFixed(2)
item.dealPortion = item.dealCount === 0 ? 0 : ((item.dealCount / sumDealCount) * 100).toFixed(2)
})
}
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {
loadData() loadData()

View File

@ -0,0 +1,156 @@
<!-- 数据统计 - 客户画像 -->
<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="addressRef">
<CustomerAddress ref="addressRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户级别分析 -->
<el-tab-pane label="客户级别分析" lazy name="levelRef">
<CustomerLevel ref="levelRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户来源分析 -->
<el-tab-pane label="客户来源分析" lazy name="sourceRef">
<CustomerSource ref="sourceRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户行业分析 -->
<el-tab-pane label="客户行业分析" lazy name="industryRef">
<CustomerIndustry ref="industryRef" :query-params="queryParams" />
</el-tab-pane>
</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 CustomerAddress from './components/CustomerAddress.vue'
import CustomerIndustry from './components/CustomerIndustry.vue'
import CustomerSource from './components/CustomerSource.vue'
import CustomerLevel from './components/CustomerLevel.vue'
defineOptions({ name: 'CrmStatisticsPortrait' })
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('addressRef') //
const addressRef = ref() //
const levelRef = ref() //
const sourceRef = ref() //
const industryRef = ref() //
/** 搜索按钮操作 */
const handleQuery = () => {
switch (activeTab.value) {
case 'addressRef':
addressRef.value?.loadData?.()
break
case 'levelRef':
levelRef.value?.loadData?.()
break
case 'sourceRef':
sourceRef.value?.loadData?.()
break
case 'industryRef':
industryRef.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>