From 83ddc05cf59f76a99fe78329ac2756354893650a Mon Sep 17 00:00:00 2001 From: xingyu4j Date: Tue, 24 Jun 2025 10:09:16 +0800 Subject: [PATCH] feat: crm statistics funnel --- .../web-antd/src/api/crm/statistics/funnel.ts | 46 ++- .../crm/statistics/funnel/chartOptions.ts | 271 ++++++++++++++++++ .../src/views/crm/statistics/funnel/data.ts | 266 +++++++++++++++++ .../src/views/crm/statistics/funnel/index.vue | 131 +++++++-- 4 files changed, 687 insertions(+), 27 deletions(-) create mode 100644 apps/web-antd/src/views/crm/statistics/funnel/chartOptions.ts create mode 100644 apps/web-antd/src/views/crm/statistics/funnel/data.ts diff --git a/apps/web-antd/src/api/crm/statistics/funnel.ts b/apps/web-antd/src/api/crm/statistics/funnel.ts index a4948e60b..8e023d1b7 100644 --- a/apps/web-antd/src/api/crm/statistics/funnel.ts +++ b/apps/web-antd/src/api/crm/statistics/funnel.ts @@ -1,4 +1,4 @@ -import type { PageParam, PageResult } from '@vben/request'; +import type { PageResult } from '@vben/request'; import { requestClient } from '#/api/request'; @@ -25,8 +25,42 @@ export namespace CrmStatisticsFunnelApi { } } +export function getDatas(activeTabName: any, params: any) { + switch (activeTabName) { + case 'businessInversionRateSummary': { + return getBusinessPageByDate(params); + } + case 'businessSummary': { + return getBusinessPageByDate(params); + } + case 'funnel': { + return getBusinessSummaryByEndStatus(params); + } + default: { + return []; + } + } +} + +export function getChartDatas(activeTabName: any, params: any) { + switch (activeTabName) { + case 'businessInversionRateSummary': { + return getBusinessInversionRateSummaryByDate(params); + } + case 'businessSummary': { + return getBusinessSummaryByDate(params); + } + case 'funnel': { + return getFunnelSummary(params); + } + default: { + return []; + } + } +} + /** 获取销售漏斗统计数据 */ -export function getFunnelSummary(params: PageParam) { +export function getFunnelSummary(params: any) { return requestClient.get( '/crm/statistics-funnel/get-funnel-summary', { params }, @@ -34,7 +68,7 @@ export function getFunnelSummary(params: PageParam) { } /** 获取商机结束状态统计 */ -export function getBusinessSummaryByEndStatus(params: PageParam) { +export function getBusinessSummaryByEndStatus(params: any) { return requestClient.get>( '/crm/statistics-funnel/get-business-summary-by-end-status', { params }, @@ -42,7 +76,7 @@ export function getBusinessSummaryByEndStatus(params: PageParam) { } /** 获取新增商机分析(按日期) */ -export function getBusinessSummaryByDate(params: PageParam) { +export function getBusinessSummaryByDate(params: any) { return requestClient.get( '/crm/statistics-funnel/get-business-summary-by-date', { params }, @@ -50,7 +84,7 @@ export function getBusinessSummaryByDate(params: PageParam) { } /** 获取商机转化率分析(按日期) */ -export function getBusinessInversionRateSummaryByDate(params: PageParam) { +export function getBusinessInversionRateSummaryByDate(params: any) { return requestClient.get< CrmStatisticsFunnelApi.BusinessInversionRateSummaryByDate[] >('/crm/statistics-funnel/get-business-inversion-rate-summary-by-date', { @@ -59,7 +93,7 @@ export function getBusinessInversionRateSummaryByDate(params: PageParam) { } /** 获取商机列表(按日期) */ -export function getBusinessPageByDate(params: PageParam) { +export function getBusinessPageByDate(params: any) { return requestClient.get>( '/crm/statistics-funnel/get-business-page-by-date', { params }, diff --git a/apps/web-antd/src/views/crm/statistics/funnel/chartOptions.ts b/apps/web-antd/src/views/crm/statistics/funnel/chartOptions.ts new file mode 100644 index 000000000..e86953f4c --- /dev/null +++ b/apps/web-antd/src/views/crm/statistics/funnel/chartOptions.ts @@ -0,0 +1,271 @@ +import { erpCalculatePercentage } from '@vben/utils'; + +export function getChartOptions( + activeTabName: any, + active: boolean, + res: any, +): any { + switch (activeTabName) { + case 'businessInversionRateSummary': { + return { + color: ['#6ca2ff', '#6ac9d7', '#ff7474'], + tooltip: { + trigger: 'axis', + axisPointer: { + // 坐标轴指示器,坐标轴触发有效 + type: 'shadow', // 默认为直线,可选为:'line' | 'shadow' + }, + }, + legend: { + data: ['赢单转化率', '商机总数', '赢单商机数'], + bottom: '0px', + itemWidth: 14, + }, + grid: { + top: '40px', + left: '40px', + right: '40px', + bottom: '40px', + containLabel: true, + borderColor: '#fff', + }, + xAxis: [ + { + type: 'category', + data: res.map((s: any) => s.time), + axisTick: { + alignWithLabel: true, + lineStyle: { width: 0 }, + }, + axisLabel: { + color: '#BDBDBD', + }, + /** 坐标轴轴线相关设置 */ + axisLine: { + lineStyle: { color: '#BDBDBD' }, + }, + splitLine: { + show: false, + }, + }, + ], + yAxis: [ + { + type: 'value', + name: '赢单转化率', + axisTick: { + alignWithLabel: true, + lineStyle: { width: 0 }, + }, + axisLabel: { + color: '#BDBDBD', + formatter: '{value}%', + }, + /** 坐标轴轴线相关设置 */ + axisLine: { + lineStyle: { color: '#BDBDBD' }, + }, + splitLine: { + show: false, + }, + }, + { + type: 'value', + name: '商机数', + axisTick: { + alignWithLabel: true, + lineStyle: { width: 0 }, + }, + axisLabel: { + color: '#BDBDBD', + formatter: '{value}个', + }, + /** 坐标轴轴线相关设置 */ + axisLine: { + lineStyle: { color: '#BDBDBD' }, + }, + splitLine: { + show: false, + }, + }, + ], + series: [ + { + name: '赢单转化率', + type: 'line', + yAxisIndex: 0, + data: res.map((s: any) => + erpCalculatePercentage(s.businessWinCount, s.businessCount), + ), + }, + { + name: '商机总数', + type: 'bar', + yAxisIndex: 1, + barWidth: 15, + data: res.map((s: any) => s.businessCount), + }, + { + name: '赢单商机数', + type: 'bar', + yAxisIndex: 1, + barWidth: 15, + data: res.map((s: any) => s.businessWinCount), + }, + ], + }; + } + case 'businessSummary': { + return { + grid: { + left: 30, + right: 30, // 让 X 轴右侧显示完整 + bottom: 20, + containLabel: true, + }, + legend: {}, + series: [ + { + name: '新增商机数量', + type: 'bar', + yAxisIndex: 0, + data: res.map((s: any) => s.businessCreateCount), + }, + { + name: '新增商机金额', + type: 'bar', + yAxisIndex: 1, + data: res.map((s: any) => s.totalPrice), + }, + ], + toolbox: { + feature: { + dataZoom: { + xAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '新增商机分析' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + yAxis: [ + { + type: 'value', + name: '新增商机数量', + min: 0, + minInterval: 1, // 显示整数刻度 + }, + { + type: 'value', + name: '新增商机金额', + min: 0, + minInterval: 1, // 显示整数刻度 + splitLine: { + lineStyle: { + type: 'dotted', // 右侧网格线虚化, 减少混乱 + opacity: 0.7, + }, + }, + }, + ], + xAxis: { + type: 'category', + name: '日期', + data: res.map((s: any) => s.time), + }, + }; + } + case 'funnel': { + // tips:写死 value 值是为了保持漏斗顺序不变 + const list: { name: string; value: number }[] = []; + if (active) { + list.push( + { value: 60, name: `客户-${res.customerCount || 0}个` }, + { value: 40, name: `商机-${res.businessCount || 0}个` }, + { value: 20, name: `赢单-${res.businessWinCount || 0}个` }, + ); + } else { + list.push( + { + value: res.customerCount || 0, + name: `客户-${res.customerCount || 0}个`, + }, + { + value: res.businessCount || 0, + name: `商机-${res.businessCount || 0}个`, + }, + { + value: res.businessWinCount || 0, + name: `赢单-${res.businessWinCount || 0}个`, + }, + ); + } + return { + title: { + text: '销售漏斗', + }, + tooltip: { + trigger: 'item', + formatter: '{a}
{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: list, + }, + ], + }; + } + default: { + return {}; + } + } +} diff --git a/apps/web-antd/src/views/crm/statistics/funnel/data.ts b/apps/web-antd/src/views/crm/statistics/funnel/data.ts new file mode 100644 index 000000000..4b083599d --- /dev/null +++ b/apps/web-antd/src/views/crm/statistics/funnel/data.ts @@ -0,0 +1,266 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { useUserStore } from '@vben/stores'; +import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils'; + +import { getSimpleDeptList } from '#/api/system/dept'; +import { getSimpleUserList } from '#/api/system/user'; +import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; + +const userStore = useUserStore(); + +export const customerSummaryTabs = [ + { + tab: '销售漏斗分析', + key: 'funnel', + }, + { + tab: '新增商机分析', + key: 'businessSummary', + }, + { + tab: '商机转化率分析', + key: 'businessInversionRateSummary', + }, +]; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'times', + label: '时间范围', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + }, + defaultValue: [ + formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))), + formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))), + ] as [Date, Date], + }, + { + fieldName: 'interval', + label: '时间间隔', + component: 'Select', + componentProps: { + allowClear: true, + options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'), + }, + defaultValue: 2, + }, + { + fieldName: 'deptId', + label: '归属部门', + component: 'ApiTreeSelect', + componentProps: { + api: async () => { + const data = await getSimpleDeptList(); + return handleTree(data); + }, + labelField: 'name', + valueField: 'id', + childrenField: 'children', + treeDefaultExpandAll: true, + }, + defaultValue: userStore.userInfo?.deptId, + }, + { + fieldName: 'userId', + label: '员工', + component: 'ApiSelect', + componentProps: { + api: getSimpleUserList, + allowClear: true, + labelField: 'nickname', + valueField: 'id', + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns( + activeTabName: any, +): VxeTableGridOptions['columns'] { + switch (activeTabName) { + case 'businessInversionRateSummary': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'name', + title: '商机名称', + minWidth: 100, + }, + { + field: 'customerName', + title: '客户名称', + minWidth: 200, + }, + { + field: 'totalPrice', + title: '商机金额(元)', + minWidth: 200, + formatter: 'formatAmount2', + }, + { + field: 'dealTime', + title: '预计成交日期', + minWidth: 200, + formatter: 'formatDateTime', + }, + { + field: 'ownerUserName', + title: '负责人', + minWidth: 200, + }, + { + field: 'ownerUserDeptName', + title: '所属部门', + minWidth: 200, + }, + { + field: 'contactLastTime', + title: '最后跟进时间', + minWidth: 200, + formatter: 'formatDateTime', + }, + { + field: 'updateTime', + title: '更新时间', + minWidth: 200, + formatter: 'formatDateTime', + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 200, + formatter: 'formatDateTime', + }, + { + field: 'creatorName', + title: '创建人', + minWidth: 100, + }, + { + field: 'statusTypeName', + title: '商机状态组', + minWidth: 100, + }, + { + field: 'statusName', + title: '商机阶段', + minWidth: 100, + }, + ]; + } + case 'businessSummary': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'name', + title: '商机名称', + minWidth: 100, + }, + { + field: 'customerName', + title: '客户名称', + minWidth: 200, + }, + { + field: 'totalPrice', + title: '商机金额(元)', + minWidth: 200, + formatter: 'formatAmount2', + }, + { + field: 'dealTime', + title: '预计成交日期', + minWidth: 200, + formatter: 'formatDateTime', + }, + { + field: 'ownerUserName', + title: '负责人', + minWidth: 200, + }, + { + field: 'ownerUserDeptName', + title: '所属部门', + minWidth: 200, + }, + { + field: 'contactLastTime', + title: '最后跟进时间', + minWidth: 200, + formatter: 'formatDateTime', + }, + { + field: 'updateTime', + title: '更新时间', + minWidth: 200, + formatter: 'formatDateTime', + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 200, + formatter: 'formatDateTime', + }, + { + field: 'creatorName', + title: '创建人', + minWidth: 100, + }, + { + field: 'statusTypeName', + title: '商机状态组', + minWidth: 100, + }, + { + field: 'statusName', + title: '商机阶段', + minWidth: 100, + }, + ]; + } + case 'funnel': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'endStatus', + title: '阶段', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.CRM_BUSINESS_END_STATUS_TYPE }, + }, + }, + { + field: 'businessCount', + title: '商机数', + minWidth: 200, + }, + { + field: 'totalPrice', + title: '商机总金额(元)', + minWidth: 200, + formatter: 'formatAmount2', + }, + ]; + } + default: { + return []; + } + } +} diff --git a/apps/web-antd/src/views/crm/statistics/funnel/index.vue b/apps/web-antd/src/views/crm/statistics/funnel/index.vue index 3d944187e..f4754b1f9 100644 --- a/apps/web-antd/src/views/crm/statistics/funnel/index.vue +++ b/apps/web-antd/src/views/crm/statistics/funnel/index.vue @@ -1,28 +1,117 @@