From c96cf0ead98ee9f3975388c019e876ec445ef12d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 6 Jul 2026 08:50:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=81=E7=A7=BB=20CRM=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=BB=9F=E8=AE=A1=E4=B8=8E=E4=B8=9A=E7=BB=A9=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 迁移 CRM 销售漏斗分析,补充漏斗图、阶段统计和商机转化率分析 - 迁移 CRM 员工业绩分析,补充合同汇总统计 - 迁移 CRM 产品分析,新增产品销售情况和产品分类销售分析 - 新增 CRM 业绩达成统计,支持销售目标和回款目标达成情况展示 - 新增 CRM 业绩目标设置,支持部门和员工维度的目标配置 - 补充 web-antd、web-antdv-next、web-ele 对应 API 和页面实现 - 调整统计图表配置、筛选项和表格展示,统一多端 CRM 统计交互 --- .../web-antd/src/api/crm/statistics/funnel.ts | 21 +- .../src/api/crm/statistics/performance.ts | 20 +- .../crm/statistics/funnel/chartOptions.ts | 56 +- .../src/views/crm/statistics/funnel/data.ts | 27 +- .../src/views/crm/statistics/funnel/index.vue | 24 +- .../statistics/performance/chartOptions.ts | 44 ++ .../views/crm/statistics/performance/data.ts | 4 + .../crm/statistics/performance/index.vue | 45 +- .../src/api/crm/performance/config/index.ts | 67 ++ .../src/api/crm/statistics/funnel.ts | 21 +- .../src/api/crm/statistics/performance.ts | 20 +- .../api/crm/statistics/performanceTarget.ts | 30 + .../src/api/crm/statistics/product.ts | 46 ++ .../src/views/crm/performance/config/data.ts | 263 ++++++++ .../views/crm/performance/config/index.vue | 150 +++++ .../crm/performance/config/modules/form.vue | 132 ++++ .../crm/statistics/funnel/chartOptions.ts | 56 +- .../src/views/crm/statistics/funnel/data.ts | 27 +- .../src/views/crm/statistics/funnel/index.vue | 24 +- .../statistics/performance/chartOptions.ts | 44 ++ .../views/crm/statistics/performance/data.ts | 4 + .../crm/statistics/performance/index.vue | 45 +- .../statistics/performanceTarget/index.vue | 285 +++++++++ .../views/crm/statistics/product/index.vue | 585 +++++++++++++++++ .../src/api/crm/performance/config/index.ts | 67 ++ apps/web-ele/src/api/crm/statistics/funnel.ts | 21 +- .../src/api/crm/statistics/performance.ts | 20 +- .../api/crm/statistics/performanceTarget.ts | 30 + .../web-ele/src/api/crm/statistics/product.ts | 46 ++ .../src/views/crm/performance/config/data.ts | 169 +++++ .../views/crm/performance/config/index.vue | 150 +++++ .../crm/performance/config/modules/form.vue | 247 ++++++++ .../crm/statistics/funnel/chartOptions.ts | 56 +- .../src/views/crm/statistics/funnel/data.ts | 27 +- .../src/views/crm/statistics/funnel/index.vue | 24 +- .../statistics/performance/chartOptions.ts | 44 ++ .../views/crm/statistics/performance/data.ts | 4 + .../crm/statistics/performance/index.vue | 33 +- .../statistics/performanceTarget/index.vue | 285 +++++++++ .../views/crm/statistics/product/index.vue | 588 ++++++++++++++++++ 40 files changed, 3698 insertions(+), 153 deletions(-) create mode 100644 apps/web-antdv-next/src/api/crm/performance/config/index.ts create mode 100644 apps/web-antdv-next/src/api/crm/statistics/performanceTarget.ts create mode 100644 apps/web-antdv-next/src/api/crm/statistics/product.ts create mode 100644 apps/web-antdv-next/src/views/crm/performance/config/data.ts create mode 100644 apps/web-antdv-next/src/views/crm/performance/config/index.vue create mode 100644 apps/web-antdv-next/src/views/crm/performance/config/modules/form.vue create mode 100644 apps/web-antdv-next/src/views/crm/statistics/performanceTarget/index.vue create mode 100644 apps/web-antdv-next/src/views/crm/statistics/product/index.vue create mode 100644 apps/web-ele/src/api/crm/performance/config/index.ts create mode 100644 apps/web-ele/src/api/crm/statistics/performanceTarget.ts create mode 100644 apps/web-ele/src/api/crm/statistics/product.ts create mode 100644 apps/web-ele/src/views/crm/performance/config/data.ts create mode 100644 apps/web-ele/src/views/crm/performance/config/index.vue create mode 100644 apps/web-ele/src/views/crm/performance/config/modules/form.vue create mode 100644 apps/web-ele/src/views/crm/statistics/performanceTarget/index.vue create mode 100644 apps/web-ele/src/views/crm/statistics/product/index.vue diff --git a/apps/web-antd/src/api/crm/statistics/funnel.ts b/apps/web-antd/src/api/crm/statistics/funnel.ts index eb7cf8ca8..a4227d8a8 100644 --- a/apps/web-antd/src/api/crm/statistics/funnel.ts +++ b/apps/web-antd/src/api/crm/statistics/funnel.ts @@ -17,6 +17,16 @@ export namespace CrmStatisticsFunnelApi { totalPrice: number | string; // 商机金额 } + /** 商机阶段统计响应 */ + export interface BusinessSummaryByStatusRespVO { + statusId: number; // 商机阶段编号 + statusName: string; // 商机阶段名称 + statusPercent: number; // 赢单率 + sort: number; // 排序 + businessCount: number; // 商机数 + totalPrice: number | string; // 商机金额 + } + /** 商机转化率分析(按日期)响应 */ export interface BusinessInversionRateSummaryByDateRespVO { time: string; // 时间 @@ -34,7 +44,7 @@ export function getDatas(activeTabName: any, params: any) { return getBusinessPageByDate(params); } case 'funnel': { - return getBusinessSummaryByEndStatus(params); + return getBusinessSummaryByStatus(params); } default: { return []; @@ -51,7 +61,7 @@ export function getChartDatas(activeTabName: any, params: any) { return getBusinessSummaryByDate(params); } case 'funnel': { - return getFunnelSummary(params); + return getBusinessSummaryByStatus(params); } default: { return []; @@ -75,6 +85,13 @@ export function getBusinessSummaryByEndStatus(params: any) { ); } +/** 获取商机阶段统计 */ +export function getBusinessSummaryByStatus(params: any) { + return requestClient.get< + CrmStatisticsFunnelApi.BusinessSummaryByStatusRespVO[] + >('/crm/statistics-funnel/get-business-summary-by-status', { params }); +} + /** 获取新增商机分析(按日期) */ export function getBusinessSummaryByDate(params: any) { return requestClient.get< diff --git a/apps/web-antd/src/api/crm/statistics/performance.ts b/apps/web-antd/src/api/crm/statistics/performance.ts index 28048a314..fa3af8311 100644 --- a/apps/web-antd/src/api/crm/statistics/performance.ts +++ b/apps/web-antd/src/api/crm/statistics/performance.ts @@ -5,7 +5,7 @@ export namespace CrmStatisticsPerformanceApi { export interface PerformanceReqVO { times: string[]; deptId: number; - userId: number; + userId?: number; } /** 员工业绩统计响应 */ @@ -15,6 +15,15 @@ export namespace CrmStatisticsPerformanceApi { lastMonthCount: number; lastYearCount: number; } + + /** 员工业绩合同汇总响应 */ + export interface PerformanceSummaryRespVO { + time: string; + contractCount: number; + contractPrice: number; + receivablePrice: number; + unreceivedPrice: number; + } } /** 员工获得合同金额统计 */ @@ -46,3 +55,12 @@ export function getContractCountPerformance( { params }, ); } + +/** 获得合同汇总表 */ +export function getContractSummary( + params: CrmStatisticsPerformanceApi.PerformanceReqVO, +) { + return requestClient.get< + CrmStatisticsPerformanceApi.PerformanceSummaryRespVO[] + >('/crm/statistics-performance/get-contract-summary', { 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 index e7ce47511..24eeda769 100644 --- a/apps/web-antd/src/views/crm/statistics/funnel/chartOptions.ts +++ b/apps/web-antd/src/views/crm/statistics/funnel/chartOptions.ts @@ -1,4 +1,4 @@ -import { erpCalculatePercentage } from '@vben/utils'; +import { erpCalculatePercentage, erpPriceInputFormatter } from '@vben/utils'; const getLegend = (extra: Record = {}) => ({ top: 10, @@ -190,30 +190,20 @@ export function getChartOptions( }; } 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}个`, - }, - ); - } + const list = res.map((item: any) => ({ + value: active + ? Number(item.businessCount || 0) + : Number(item.totalPrice || 0), + name: `${item.statusName}-${item.businessCount || 0}个`, + statusName: item.statusName, + statusPercent: item.statusPercent, + businessCount: item.businessCount, + totalPrice: item.totalPrice, + })); + const maxValue = Math.max( + ...list.map((item: any) => Number(item.value || 0)), + 1, + ); return { title: { text: '销售漏斗', @@ -221,7 +211,15 @@ export function getChartOptions( tooltip: getTooltip({ trigger: 'item', axisPointer: undefined, - formatter: '{a}
{b}', + formatter: (params: any) => { + const data = params.data || {}; + return [ + data.statusName || params.name, + `商机数:${data.businessCount || 0} 个`, + `商机金额:${erpPriceInputFormatter(data.totalPrice || 0)} 元`, + `赢单率:${data.statusPercent || 0}%`, + ].join('
'); + }, }), toolbox: { feature: { @@ -231,7 +229,7 @@ export function getChartOptions( }, }, legend: getLegend({ - data: ['客户', '商机', '赢单'], + data: list.map((item: any) => item.name), }), series: [ { @@ -242,10 +240,10 @@ export function getChartOptions( bottom: 60, width: '80%', min: 0, - max: 100, + max: maxValue, minSize: '0%', maxSize: '100%', - sort: 'descending', + sort: 'none', gap: 2, label: { show: true, diff --git a/apps/web-antd/src/views/crm/statistics/funnel/data.ts b/apps/web-antd/src/views/crm/statistics/funnel/data.ts index 5dd93db19..73728bfcd 100644 --- a/apps/web-antd/src/views/crm/statistics/funnel/data.ts +++ b/apps/web-antd/src/views/crm/statistics/funnel/data.ts @@ -6,6 +6,7 @@ import { getDictOptions } from '@vben/hooks'; import { useUserStore } from '@vben/stores'; import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils'; +import { getBusinessStatusTypeSimpleList } from '#/api/crm/business/status'; import { getSimpleDeptList } from '#/api/system/dept'; import { getSimpleUserList } from '#/api/system/user'; import { getRangePickerDefaultProps } from '#/utils'; @@ -53,6 +54,18 @@ export function useGridFormSchema(): VbenFormSchema[] { }, defaultValue: 2, }, + { + fieldName: 'statusTypeId', + label: '商机组', + component: 'ApiSelect', + componentProps: { + api: getBusinessStatusTypeSimpleList, + allowClear: true, + labelField: 'name', + valueField: 'id', + placeholder: '请选择商机组', + }, + }, { fieldName: 'deptId', label: '归属部门', @@ -243,13 +256,15 @@ export function useGridColumns( title: '序号', }, { - field: 'endStatus', + field: 'statusName', title: '阶段', - minWidth: 100, - cellRender: { - name: 'CellDict', - props: { type: DICT_TYPE.CRM_BUSINESS_END_STATUS_TYPE }, - }, + minWidth: 160, + }, + { + field: 'statusPercent', + title: '赢单率', + minWidth: 120, + formatter: ({ row }) => `${row.statusPercent || 0}%`, }, { field: 'businessCount', 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 72035f28f..ae70e2778 100644 --- a/apps/web-antd/src/views/crm/statistics/funnel/index.vue +++ b/apps/web-antd/src/views/crm/statistics/funnel/index.vue @@ -5,9 +5,8 @@ import type { VxeGridListeners, VxeTableGridOptions, } from '#/adapter/vxe-table'; -import type { CrmStatisticsFunnelApi } from '#/api/crm/statistics/funnel'; -import { reactive, ref } from 'vue'; +import { onMounted, reactive, ref } from 'vue'; import { ContentWrap, Page } from '@vben/common-ui'; import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; @@ -72,7 +71,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ toolbarConfig: { enabled: false, }, - } as VxeTableGridOptions, + } as VxeTableGridOptions, }); /** tab 切换 */ @@ -89,7 +88,10 @@ async function handleTabChange(key: any) { const queryParams = await formApi.getValues(); const res = await getChartDatas(activeTabName.value, queryParams); await renderEcharts(getChartOptions(activeTabName.value, active.value, res)); - const data: any = await getDatas(activeTabName.value, queryParams); + const data: any = + activeTabName.value === 'funnel' + ? res + : await getDatas(activeTabName.value, queryParams); await gridApi.grid.reloadData( activeTabName.value === 'funnel' ? data : data.list, ); @@ -99,10 +101,14 @@ async function handleTabChange(key: any) { async function handleActive(value: boolean) { active.value = value; const queryParams = await formApi.getValues(); - renderEcharts( - getChartOptions(activeTabName.value, active.value, queryParams), - ); + const res = await getChartDatas(activeTabName.value, queryParams); + renderEcharts(getChartOptions(activeTabName.value, active.value, res)); } + +/** 初始化加载 */ +onMounted(() => { + handleTabChange(activeTabName.value); +});