From 5690645bd1874462a78be6caaafa51a8b615afab Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 18 Nov 2025 21:55:30 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E3=80=90ele=E3=80=91=E3=80=90crm?= =?UTF-8?q?=E3=80=91statistics=20=E7=9A=84=E8=BF=81=E7=A7=BB=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crm/statistics/customer/chartOptions.ts | 530 ++++++++++++++++++ .../src/views/crm/statistics/customer/data.ts | 398 +++++++++++++ .../views/crm/statistics/customer/index.vue | 100 ++++ .../crm/statistics/funnel/chartOptions.ts | 271 +++++++++ .../src/views/crm/statistics/funnel/data.ts | 268 +++++++++ .../src/views/crm/statistics/funnel/index.vue | 146 +++++ .../statistics/performance/chartOptions.ts | 394 +++++++++++++ .../views/crm/statistics/performance/data.ts | 73 +++ .../crm/statistics/performance/index.vue | 176 ++++++ .../crm/statistics/portrait/chartOptions.ts | 440 +++++++++++++++ .../src/views/crm/statistics/portrait/data.ts | 201 +++++++ .../views/crm/statistics/portrait/index.vue | 105 ++++ .../views/crm/statistics/rank/chartOptions.ts | 394 +++++++++++++ .../src/views/crm/statistics/rank/data.ts | 276 +++++++++ .../src/views/crm/statistics/rank/index.vue | 109 ++++ 15 files changed, 3881 insertions(+) create mode 100644 apps/web-ele/src/views/crm/statistics/customer/chartOptions.ts create mode 100644 apps/web-ele/src/views/crm/statistics/customer/data.ts create mode 100644 apps/web-ele/src/views/crm/statistics/customer/index.vue create mode 100644 apps/web-ele/src/views/crm/statistics/funnel/chartOptions.ts create mode 100644 apps/web-ele/src/views/crm/statistics/funnel/data.ts create mode 100644 apps/web-ele/src/views/crm/statistics/funnel/index.vue create mode 100644 apps/web-ele/src/views/crm/statistics/performance/chartOptions.ts create mode 100644 apps/web-ele/src/views/crm/statistics/performance/data.ts create mode 100644 apps/web-ele/src/views/crm/statistics/performance/index.vue create mode 100644 apps/web-ele/src/views/crm/statistics/portrait/chartOptions.ts create mode 100644 apps/web-ele/src/views/crm/statistics/portrait/data.ts create mode 100644 apps/web-ele/src/views/crm/statistics/portrait/index.vue create mode 100644 apps/web-ele/src/views/crm/statistics/rank/chartOptions.ts create mode 100644 apps/web-ele/src/views/crm/statistics/rank/data.ts create mode 100644 apps/web-ele/src/views/crm/statistics/rank/index.vue diff --git a/apps/web-ele/src/views/crm/statistics/customer/chartOptions.ts b/apps/web-ele/src/views/crm/statistics/customer/chartOptions.ts new file mode 100644 index 000000000..deecbd370 --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/customer/chartOptions.ts @@ -0,0 +1,530 @@ +import { DICT_TYPE } from '@vben/constants'; +import { getDictLabel } from '@vben/hooks'; + +export function getChartOptions(activeTabName: any, res: any): any { + switch (activeTabName) { + case 'conversionStat': { + return { + grid: { + left: 20, + right: 40, // 让 X 轴右侧显示完整 + bottom: 20, + containLabel: true, + }, + legend: {}, + series: [ + { + name: '客户转化率', + type: 'line', + data: res.map((item: any) => { + return { + name: item.time, + value: item.customerCreateCount + ? ( + (item.customerDealCount / item.customerCreateCount) * + 100 + ).toFixed(2) + : 0, + }; + }), + }, + ], + toolbox: { + feature: { + dataZoom: { + xAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '客户转化率分析' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + yAxis: { + type: 'value', + name: '转化率(%)', + }, + xAxis: { + type: 'category', + name: '日期', + data: res.map((s: any) => s.time), + }, + }; + } + case 'customerSummary': { + return { + grid: { + bottom: '5%', + containLabel: true, + left: '5%', + right: '5%', + top: '5 %', + }, + legend: {}, + series: [ + { + name: '新增客户数', + type: 'bar', + yAxisIndex: 0, + data: res.map((item: any) => item.customerCreateCount), + }, + { + name: '成交客户数', + type: 'bar', + yAxisIndex: 1, + data: res.map((item: any) => item.customerDealCount), + }, + ], + 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((item: any) => item.time), + }, + }; + } + case 'dealCycleByArea': { + const data = res.map((s: any) => { + return { + areaName: s.areaName, + customerDealCycle: s.customerDealCycle, + customerDealCount: s.customerDealCount, + }; + }); + return { + grid: { + left: 20, + right: 40, // 让 X 轴右侧显示完整 + bottom: 20, + containLabel: true, + }, + legend: {}, + series: [ + { + name: '成交周期(天)', + type: 'bar', + data: data.map((s: any) => s.customerDealCycle), + yAxisIndex: 0, + }, + { + name: '成交客户数', + type: 'bar', + data: data.map((s: any) => s.customerDealCount), + yAxisIndex: 1, + }, + ], + 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: data.map((s: any) => s.areaName), + }, + }; + } + case 'dealCycleByProduct': { + const data = res.map((s: any) => { + return { + productName: s.productName ?? '未知', + customerDealCycle: s.customerDealCount, + customerDealCount: s.customerDealCount, + }; + }); + return { + grid: { + left: 20, + right: 40, // 让 X 轴右侧显示完整 + bottom: 20, + containLabel: true, + }, + legend: {}, + series: [ + { + name: '成交周期(天)', + type: 'bar', + data: data.map((s: any) => s.customerDealCycle), + yAxisIndex: 0, + }, + { + name: '成交客户数', + type: 'bar', + data: data.map((s: any) => s.customerDealCount), + yAxisIndex: 1, + }, + ], + 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: data.map((s: any) => s.productName), + }, + }; + } + case 'dealCycleByUser': { + const customerDealCycleByDate = res.customerDealCycleByDate; + const customerDealCycleByUser = res.customerDealCycleByUser; + return { + grid: { + left: 20, + right: 40, // 让 X 轴右侧显示完整 + bottom: 20, + containLabel: true, + }, + legend: {}, + series: [ + { + name: '成交周期(天)', + type: 'bar', + data: customerDealCycleByDate.map((s: any) => s.customerDealCycle), + yAxisIndex: 0, + }, + { + name: '成交客户数', + type: 'bar', + data: customerDealCycleByUser.map((s: any) => s.customerDealCount), + yAxisIndex: 1, + }, + ], + 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: customerDealCycleByDate.map((s: any) => s.time), + }, + }; + } + case 'followUpSummary': { + return { + grid: { + left: 20, + right: 30, // 让 X 轴右侧显示完整 + bottom: 20, + containLabel: true, + }, + legend: {}, + series: [ + { + name: '跟进客户数', + type: 'bar', + yAxisIndex: 0, + data: res.map((s: any) => s.followUpCustomerCount), + }, + { + name: '跟进次数', + type: 'bar', + yAxisIndex: 1, + data: res.map((s: any) => s.followUpRecordCount), + }, + ], + 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: '日期', + axisTick: { + alignWithLabel: true, + }, + data: res.map((s: any) => s.time), + }, + }; + } + case 'followUpType': { + return { + title: { + text: '客户跟进方式分析', + left: 'center', + }, + legend: { + orient: 'vertical', + left: 'left', + }, + tooltip: { + trigger: 'item', + formatter: '{b} : {c}% ', + }, + toolbox: { + feature: { + saveAsImage: { show: true, name: '客户跟进方式分析' }, // 保存为图片 + }, + }, + series: [ + { + name: '跟进方式', + type: 'pie', + radius: '50%', + data: res.map((s: any) => { + return { + name: getDictLabel( + DICT_TYPE.CRM_FOLLOW_UP_TYPE, + s.followUpType, + ), + value: s.followUpRecordCount, + }; + }), + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)', + }, + }, + }, + ], + }; + } + case 'poolSummary': { + return { + grid: { + left: 20, + right: 40, // 让 X 轴右侧显示完整 + bottom: 20, + containLabel: true, + }, + legend: {}, + series: [ + { + name: '进入公海客户数', + type: 'bar', + yAxisIndex: 0, + data: res.map((s: any) => s.customerPutCount), + }, + { + name: '公海领取客户数', + type: 'bar', + yAxisIndex: 1, + data: res.map((s: any) => s.customerTakeCount), + }, + ], + 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), + }, + }; + } + default: { + return {}; + } + } +} diff --git a/apps/web-ele/src/views/crm/statistics/customer/data.ts b/apps/web-ele/src/views/crm/statistics/customer/data.ts new file mode 100644 index 000000000..cc091e5ec --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/customer/data.ts @@ -0,0 +1,398 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE } from '@vben/constants'; +import { getDictOptions } from '@vben/hooks'; +import { useUserStore } from '@vben/stores'; +import { + beginOfDay, + endOfDay, + erpCalculatePercentage, + handleTree, +} from '@vben/utils'; + +import { getSimpleDeptList } from '#/api/system/dept'; +import { getSimpleUserList } from '#/api/system/user'; +import { getRangePickerDefaultProps } from '#/utils'; + +const userStore = useUserStore(); + +export const customerSummaryTabs = [ + { + tab: '客户总量分析', + key: 'customerSummary', + }, + { + tab: '客户跟进次数分析', + key: 'followUpSummary', + }, + { + tab: '客户跟进方式分析', + key: 'followUpType', + }, + { + tab: '客户转化率分析', + key: 'conversionStat', + }, + { + tab: '公海客户分析', + key: 'poolSummary', + }, + { + tab: '员工客户成交周期分析', + key: 'dealCycleByUser', + }, + { + tab: '地区客户成交周期分析', + key: 'dealCycleByArea', + }, + { + tab: '产品客户成交周期分析', + key: 'dealCycleByProduct', + }, +]; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'times', + label: '时间范围', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + }, + defaultValue: [ + beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7)), + endOfDay(new Date(Date.now() - 3600 * 1000 * 24)), + ], + }, + { + 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, + labelField: 'nickname', + valueField: 'id', + placeholder: '请选择员工', + allowClear: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns( + activeTabName: any, +): VxeTableGridOptions['columns'] { + switch (activeTabName) { + case 'conversionStat': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'customerName', + title: '客户名称', + minWidth: 100, + }, + { + field: 'contractName', + title: '合同名称', + minWidth: 200, + }, + { + field: 'totalPrice', + title: '合同总金额', + minWidth: 200, + formatter: 'formatAmount2', + }, + { + field: 'receivablePrice', + title: '回款金额', + minWidth: 200, + formatter: 'formatAmount2', + }, + { + field: 'source', + title: '客户来源', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE }, + }, + }, + { + field: 'industryId', + title: '客户行业', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY }, + }, + }, + { + field: 'ownerUserName', + title: '负责人', + minWidth: 200, + }, + { + field: 'creatorUserName', + title: '创建人', + minWidth: 200, + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 200, + formatter: 'formatDateTime', + }, + { + field: 'orderDate', + title: '下单日期', + minWidth: 200, + formatter: 'formatDateTime', + }, + ]; + } + case 'customerSummary': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'ownerUserName', + title: '员工姓名', + minWidth: 100, + }, + { + field: 'customerCreateCount', + title: '新增客户数', + minWidth: 200, + }, + { + field: 'customerDealCount', + title: '成交客户数', + minWidth: 200, + }, + { + field: 'customerDealRate', + title: '客户成交率(%)', + minWidth: 200, + formatter: ({ row }) => { + return erpCalculatePercentage( + row.customerDealCount, + row.customerCreateCount, + ); + }, + }, + { + field: 'contractPrice', + title: '合同总金额', + minWidth: 200, + formatter: 'formatAmount2', + }, + { + field: 'receivablePrice', + title: '回款金额', + minWidth: 200, + formatter: 'formatAmount2', + }, + { + field: 'creceivablePrice', + title: '未回款金额', + minWidth: 200, + formatter: ({ row }) => { + return erpCalculatePercentage( + row.receivablePrice, + row.contractPrice, + ); + }, + }, + { + field: 'ccreceivablePrice', + title: '回款完成率(%)', + formatter: ({ row }) => { + return erpCalculatePercentage( + row.receivablePrice, + row.contractPrice, + ); + }, + }, + ]; + } + case 'dealCycleByArea': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'areaName', + title: '区域', + minWidth: 200, + }, + { + field: 'customerDealCycle', + title: '成交周期(天)', + minWidth: 200, + }, + { + field: 'customerDealCount', + title: '成交客户数', + minWidth: 200, + }, + ]; + } + case 'dealCycleByProduct': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'productName', + title: '产品名称', + minWidth: 200, + }, + { + field: 'customerDealCycle', + title: '成交周期(天)', + minWidth: 200, + }, + { + field: 'customerDealCount', + title: '成交客户数', + minWidth: 200, + }, + ]; + } + case 'dealCycleByUser': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'ownerUserName', + title: '日期', + minWidth: 200, + }, + { + field: 'customerDealCycle', + title: '成交周期(天)', + minWidth: 200, + }, + { + field: 'customerDealCount', + title: '成交客户数', + minWidth: 200, + }, + ]; + } + case 'followUpSummary': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'ownerUserName', + title: '员工姓名', + minWidth: 200, + }, + { + field: 'followUpRecordCount', + title: '跟进次数', + minWidth: 200, + }, + { + field: 'followUpCustomerCount', + title: '跟进客户数', + minWidth: 200, + }, + ]; + } + case 'followUpType': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'followUpType', + title: '跟进方式', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.CRM_FOLLOW_UP_TYPE }, + }, + }, + { + field: 'followUpRecordCount', + title: '个数', + minWidth: 200, + }, + { + field: 'portion', + title: '占比(%)', + minWidth: 200, + }, + ]; + } + case 'poolSummary': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'ownerUserName', + title: '员工姓名', + minWidth: 200, + }, + { + field: 'customerPutCount', + title: '进入公海客户数', + minWidth: 200, + }, + { + field: 'customerTakeCount', + title: '公海领取客户数', + minWidth: 200, + }, + ]; + } + default: { + return []; + } + } +} diff --git a/apps/web-ele/src/views/crm/statistics/customer/index.vue b/apps/web-ele/src/views/crm/statistics/customer/index.vue new file mode 100644 index 000000000..34c5f5ec2 --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/customer/index.vue @@ -0,0 +1,100 @@ + + + diff --git a/apps/web-ele/src/views/crm/statistics/funnel/chartOptions.ts b/apps/web-ele/src/views/crm/statistics/funnel/chartOptions.ts new file mode 100644 index 000000000..e86953f4c --- /dev/null +++ b/apps/web-ele/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-ele/src/views/crm/statistics/funnel/data.ts b/apps/web-ele/src/views/crm/statistics/funnel/data.ts new file mode 100644 index 000000000..f1d4aa388 --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/funnel/data.ts @@ -0,0 +1,268 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE } from '@vben/constants'; +import { getDictOptions } from '@vben/hooks'; +import { useUserStore } from '@vben/stores'; +import { beginOfDay, endOfDay, handleTree } from '@vben/utils'; + +import { getSimpleDeptList } from '#/api/system/dept'; +import { getSimpleUserList } from '#/api/system/user'; +import { 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: [ + beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7)), + endOfDay(new Date(Date.now() - 3600 * 1000 * 24)), + ], + }, + { + 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-ele/src/views/crm/statistics/funnel/index.vue b/apps/web-ele/src/views/crm/statistics/funnel/index.vue new file mode 100644 index 000000000..f4812a2fa --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/funnel/index.vue @@ -0,0 +1,146 @@ + + + diff --git a/apps/web-ele/src/views/crm/statistics/performance/chartOptions.ts b/apps/web-ele/src/views/crm/statistics/performance/chartOptions.ts new file mode 100644 index 000000000..329b4d7ff --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/performance/chartOptions.ts @@ -0,0 +1,394 @@ +export function getChartOptions(activeTabName: any, res: any): any { + switch (activeTabName) { + case 'ContractCountPerformance': { + return { + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: {}, + series: [ + { + name: '当月合同数量(个)', + type: 'line', + data: res.map((s: any) => s.currentMonthCount), + }, + { + name: '上月合同数量(个)', + type: 'line', + data: res.map((s: any) => s.lastMonthCount), + }, + { + name: '去年同月合同数量(个)', + type: 'line', + data: res.map((s: any) => s.lastYearCount), + }, + { + name: '环比增长率(%)', + type: 'line', + yAxisIndex: 1, + data: res.map((s: any) => + s.lastMonthCount === 0 + ? 'NULL' + : ( + ((s.currentMonthCount - s.lastMonthCount) / + s.lastMonthCount) * + 100 + ).toFixed(2), + ), + }, + { + name: '同比增长率(%)', + type: 'line', + yAxisIndex: 1, + data: res.map((s: any) => + s.lastYearCount === 0 + ? 'NULL' + : ( + ((s.currentMonthCount - s.lastYearCount) / + s.lastYearCount) * + 100 + ).toFixed(2), + ), + }, + ], + toolbox: { + feature: { + dataZoom: { + xAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + yAxis: [ + { + type: 'value', + name: '数量(个)', + axisTick: { + show: false, + }, + axisLabel: { + color: '#BDBDBD', + formatter: '{value}', + }, + /** 坐标轴轴线相关设置 */ + axisLine: { + lineStyle: { + color: '#BDBDBD', + }, + }, + splitLine: { + show: true, + lineStyle: { + color: '#e6e6e6', + }, + }, + }, + { + type: 'value', + name: '', + axisTick: { + alignWithLabel: true, + lineStyle: { + width: 0, + }, + }, + axisLabel: { + color: '#BDBDBD', + formatter: '{value}%', + }, + /** 坐标轴轴线相关设置 */ + axisLine: { + lineStyle: { + color: '#BDBDBD', + }, + }, + splitLine: { + show: true, + lineStyle: { + color: '#e6e6e6', + }, + }, + }, + ], + xAxis: { + type: 'category', + name: '日期', + data: res.map((s: any) => s.time), + }, + }; + } + case 'ContractPricePerformance': { + return { + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: {}, + series: [ + { + name: '当月合同金额(元)', + type: 'line', + data: res.map((s: any) => s.currentMonthCount), + }, + { + name: '上月合同金额(元)', + type: 'line', + data: res.map((s: any) => s.lastMonthCount), + }, + { + name: '去年同月合同金额(元)', + type: 'line', + data: res.map((s: any) => s.lastYearCount), + }, + { + name: '环比增长率(%)', + type: 'line', + yAxisIndex: 1, + data: res.map((s: any) => + s.lastMonthCount === 0 + ? 'NULL' + : ( + ((s.currentMonthCount - s.lastMonthCount) / + s.lastMonthCount) * + 100 + ).toFixed(2), + ), + }, + { + name: '同比增长率(%)', + type: 'line', + yAxisIndex: 1, + data: res.map((s: any) => + s.lastYearCount === 0 + ? 'NULL' + : ( + ((s.currentMonthCount - s.lastYearCount) / + s.lastYearCount) * + 100 + ).toFixed(2), + ), + }, + ], + toolbox: { + feature: { + dataZoom: { + xAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + yAxis: [ + { + type: 'value', + name: '金额(元)', + axisTick: { + show: false, + }, + axisLabel: { + color: '#BDBDBD', + formatter: '{value}', + }, + /** 坐标轴轴线相关设置 */ + axisLine: { + lineStyle: { + color: '#BDBDBD', + }, + }, + splitLine: { + show: true, + lineStyle: { + color: '#e6e6e6', + }, + }, + }, + { + type: 'value', + name: '', + axisTick: { + alignWithLabel: true, + lineStyle: { + width: 0, + }, + }, + axisLabel: { + color: '#BDBDBD', + formatter: '{value}%', + }, + /** 坐标轴轴线相关设置 */ + axisLine: { + lineStyle: { + color: '#BDBDBD', + }, + }, + splitLine: { + show: true, + lineStyle: { + color: '#e6e6e6', + }, + }, + }, + ], + xAxis: { + type: 'category', + name: '日期', + data: res.map((s: any) => s.time), + }, + }; + } + case 'ReceivablePricePerformance': { + return { + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: {}, + series: [ + { + name: '当月回款金额(元)', + type: 'line', + data: res.map((s: any) => s.currentMonthCount), + }, + { + name: '上月回款金额(元)', + type: 'line', + data: res.map((s: any) => s.lastMonthCount), + }, + { + name: '去年同月回款金额(元)', + type: 'line', + data: res.map((s: any) => s.lastYearCount), + }, + { + name: '环比增长率(%)', + type: 'line', + yAxisIndex: 1, + data: res.map((s: any) => + s.lastMonthCount === 0 + ? 'NULL' + : ( + ((s.currentMonthCount - s.lastMonthCount) / + s.lastMonthCount) * + 100 + ).toFixed(2), + ), + }, + { + name: '同比增长率(%)', + type: 'line', + yAxisIndex: 1, + data: res.map((s: any) => + s.lastYearCount === 0 + ? 'NULL' + : ( + ((s.currentMonthCount - s.lastYearCount) / + s.lastYearCount) * + 100 + ).toFixed(2), + ), + }, + ], + toolbox: { + feature: { + dataZoom: { + xAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + yAxis: [ + { + type: 'value', + name: '金额(元)', + axisTick: { + show: false, + }, + axisLabel: { + color: '#BDBDBD', + formatter: '{value}', + }, + /** 坐标轴轴线相关设置 */ + axisLine: { + lineStyle: { + color: '#BDBDBD', + }, + }, + splitLine: { + show: true, + lineStyle: { + color: '#e6e6e6', + }, + }, + }, + { + type: 'value', + name: '', + axisTick: { + alignWithLabel: true, + lineStyle: { + width: 0, + }, + }, + axisLabel: { + color: '#BDBDBD', + formatter: '{value}%', + }, + /** 坐标轴轴线相关设置 */ + axisLine: { + lineStyle: { + color: '#BDBDBD', + }, + }, + splitLine: { + show: true, + lineStyle: { + color: '#e6e6e6', + }, + }, + }, + ], + xAxis: { + type: 'category', + name: '日期', + data: res.map((s: any) => s.time), + }, + }; + } + default: { + return {}; + } + } +} diff --git a/apps/web-ele/src/views/crm/statistics/performance/data.ts b/apps/web-ele/src/views/crm/statistics/performance/data.ts new file mode 100644 index 000000000..10e951665 --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/performance/data.ts @@ -0,0 +1,73 @@ +import type { VbenFormSchema } from '#/adapter/form'; + +import { useUserStore } from '@vben/stores'; +import { beginOfDay, endOfDay, handleTree } from '@vben/utils'; + +import { getSimpleDeptList } from '#/api/system/dept'; +import { getSimpleUserList } from '#/api/system/user'; +import { getRangePickerDefaultProps } from '#/utils'; + +const userStore = useUserStore(); + +export const customerSummaryTabs = [ + { + tab: '员工合同数量统计', + key: 'ContractCountPerformance', + }, + { + tab: '员工合同金额统计', + key: 'ContractPricePerformance', + }, + { + tab: '员工回款金额统计', + key: 'ReceivablePricePerformance', + }, +]; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'times', + label: '时间范围', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + type: 'year', + format: 'YYYY', + }, + defaultValue: [ + beginOfDay(new Date(new Date().getFullYear(), 0, 1)), + endOfDay(new Date(new Date().getFullYear(), 11, 31)), + ], + }, + { + 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, + labelField: 'nickname', + valueField: 'id', + placeholder: '请选择员工', + allowClear: true, + }, + }, + ]; +} diff --git a/apps/web-ele/src/views/crm/statistics/performance/index.vue b/apps/web-ele/src/views/crm/statistics/performance/index.vue new file mode 100644 index 000000000..137dcebad --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/performance/index.vue @@ -0,0 +1,176 @@ + + + diff --git a/apps/web-ele/src/views/crm/statistics/portrait/chartOptions.ts b/apps/web-ele/src/views/crm/statistics/portrait/chartOptions.ts new file mode 100644 index 000000000..b63ea2513 --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/portrait/chartOptions.ts @@ -0,0 +1,440 @@ +import { DICT_TYPE } from '@vben/constants'; +import { getDictLabel } from '@vben/hooks'; + +function areaReplace(areaName: string) { + if (!areaName) { + return areaName; + } + return areaName + .replace('维吾尔自治区', '') + .replace('壮族自治区', '') + .replace('回族自治区', '') + .replace('自治区', '') + .replace('省', ''); +} + +export function getChartOptions(activeTabName: any, res: any): any { + switch (activeTabName) { + case 'area': { + const data = res.map((item: any) => { + return { + ...item, + areaName: areaReplace(item.areaName), + }; + }); + let leftMin = 0; + let leftMax = 0; + let rightMin = 0; + let rightMax = 0; + data.forEach((item: any) => { + leftMin = Math.min(leftMin, item.customerCount || 0); + leftMax = Math.max(leftMax, item.customerCount || 0); + rightMin = Math.min(rightMin, item.dealCount || 0); + rightMax = Math.max(rightMax, item.dealCount || 0); + }); + return { + left: { + title: { + text: '全部客户', + left: 'center', + }, + tooltip: { + trigger: 'item', + showDelay: 0, + transitionDuration: 0.2, + }, + visualMap: { + text: ['高', '低'], + realtime: false, + calculable: true, + top: 'middle', + inRange: { + color: ['yellow', 'lightskyblue', 'orangered'], + }, + min: leftMin, + max: leftMax, + }, + series: [ + { + name: '客户地域分布', + type: 'map', + map: 'china', + roam: false, + selectedMode: false, + data: data.map((item: any) => { + return { + name: item.areaName, + value: item.customerCount || 0, + }; + }), + }, + ], + }, + right: { + title: { + text: '成交客户', + left: 'center', + }, + tooltip: { + trigger: 'item', + showDelay: 0, + transitionDuration: 0.2, + }, + visualMap: { + text: ['高', '低'], + realtime: false, + calculable: true, + top: 'middle', + inRange: { + color: ['yellow', 'lightskyblue', 'orangered'], + }, + min: rightMin, + max: rightMax, + }, + series: [ + { + name: '客户地域分布', + type: 'map', + map: 'china', + roam: false, + selectedMode: false, + data: data.map((item: any) => { + return { + name: item.areaName, + value: item.dealCount || 0, + }; + }), + }, + ], + }, + }; + } + case 'industry': { + return { + left: { + title: { + text: '全部客户', + left: 'center', + }, + tooltip: { + trigger: 'item', + }, + legend: { + orient: 'vertical', + left: 'left', + }, + toolbox: { + feature: { + saveAsImage: { show: true, name: '全部客户' }, // 保存为图片 + }, + }, + series: [ + { + name: '全部客户', + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2, + }, + label: { + show: false, + position: 'center', + }, + emphasis: { + label: { + show: true, + fontSize: 40, + fontWeight: 'bold', + }, + }, + labelLine: { + show: false, + }, + data: res.map((r: any) => { + return { + name: getDictLabel( + DICT_TYPE.CRM_CUSTOMER_INDUSTRY, + r.industryId, + ), + value: r.customerCount, + }; + }), + }, + ], + }, + right: { + title: { + text: '成交客户', + left: 'center', + }, + tooltip: { + trigger: 'item', + }, + legend: { + orient: 'vertical', + left: 'left', + }, + toolbox: { + feature: { + saveAsImage: { show: true, name: '成交客户' }, // 保存为图片 + }, + }, + series: [ + { + name: '成交客户', + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2, + }, + label: { + show: false, + position: 'center', + }, + emphasis: { + label: { + show: true, + fontSize: 40, + fontWeight: 'bold', + }, + }, + labelLine: { + show: false, + }, + data: res.map((r: any) => { + return { + name: getDictLabel( + DICT_TYPE.CRM_CUSTOMER_INDUSTRY, + r.industryId, + ), + value: r.dealCount, + }; + }), + }, + ], + }, + }; + } + case 'level': { + return { + left: { + title: { + text: '全部客户', + left: 'center', + }, + tooltip: { + trigger: 'item', + }, + legend: { + orient: 'vertical', + left: 'left', + }, + toolbox: { + feature: { + saveAsImage: { show: true, name: '全部客户' }, // 保存为图片 + }, + }, + series: [ + { + name: '全部客户', + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2, + }, + label: { + show: false, + position: 'center', + }, + emphasis: { + label: { + show: true, + fontSize: 40, + fontWeight: 'bold', + }, + }, + labelLine: { + show: false, + }, + data: res.map((r: any) => { + return { + name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level), + value: r.customerCount, + }; + }), + }, + ], + }, + right: { + title: { + text: '成交客户', + left: 'center', + }, + tooltip: { + trigger: 'item', + }, + legend: { + orient: 'vertical', + left: 'left', + }, + toolbox: { + feature: { + saveAsImage: { show: true, name: '成交客户' }, // 保存为图片 + }, + }, + series: [ + { + name: '成交客户', + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2, + }, + label: { + show: false, + position: 'center', + }, + emphasis: { + label: { + show: true, + fontSize: 40, + fontWeight: 'bold', + }, + }, + labelLine: { + show: false, + }, + data: res.map((r: any) => { + return { + name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level), + value: r.dealCount, + }; + }), + }, + ], + }, + }; + } + case 'source': { + return { + left: { + title: { + text: '全部客户', + left: 'center', + }, + tooltip: { + trigger: 'item', + }, + legend: { + orient: 'vertical', + left: 'left', + }, + toolbox: { + feature: { + saveAsImage: { show: true, name: '全部客户' }, // 保存为图片 + }, + }, + series: [ + { + name: '全部客户', + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2, + }, + label: { + show: false, + position: 'center', + }, + emphasis: { + label: { + show: true, + fontSize: 40, + fontWeight: 'bold', + }, + }, + labelLine: { + show: false, + }, + data: res.map((r: any) => { + return { + name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source), + value: r.customerCount, + }; + }), + }, + ], + }, + right: { + title: { + text: '成交客户', + left: 'center', + }, + tooltip: { + trigger: 'item', + }, + legend: { + orient: 'vertical', + left: 'left', + }, + toolbox: { + feature: { + saveAsImage: { show: true, name: '成交客户' }, // 保存为图片 + }, + }, + series: [ + { + name: '成交客户', + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2, + }, + label: { + show: false, + position: 'center', + }, + emphasis: { + label: { + show: true, + fontSize: 40, + fontWeight: 'bold', + }, + }, + labelLine: { + show: false, + }, + data: res.map((r: any) => { + return { + name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source), + value: r.dealCount, + }; + }), + }, + ], + }, + }; + } + default: { + return {}; + } + } +} diff --git a/apps/web-ele/src/views/crm/statistics/portrait/data.ts b/apps/web-ele/src/views/crm/statistics/portrait/data.ts new file mode 100644 index 000000000..519edecd6 --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/portrait/data.ts @@ -0,0 +1,201 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE } from '@vben/constants'; +import { useUserStore } from '@vben/stores'; +import { beginOfDay, endOfDay, handleTree } from '@vben/utils'; + +import { getSimpleDeptList } from '#/api/system/dept'; +import { getSimpleUserList } from '#/api/system/user'; +import { getRangePickerDefaultProps } from '#/utils'; + +const userStore = useUserStore(); + +export const customerSummaryTabs = [ + { + tab: '城市分布分析', + key: 'area', + }, + { + tab: '客户级别分析', + key: 'level', + }, + { + tab: '客户来源分析', + key: 'source', + }, + { + tab: '客户行业分析', + key: 'industry', + }, +]; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'times', + label: '时间范围', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + format: 'YYYY-MM-DD', + type: 'year', + }, + defaultValue: [ + beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7)), + endOfDay(new Date(Date.now() - 3600 * 1000 * 24)), + ], + }, + { + 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, + labelField: 'nickname', + valueField: 'id', + placeholder: '请选择员工', + allowClear: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns( + activeTabName: any, +): VxeTableGridOptions['columns'] { + switch (activeTabName) { + case 'industry': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'industryId', + title: '客户行业', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY }, + }, + }, + { + field: 'customerCount', + title: '客户个数', + minWidth: 200, + }, + { + field: 'dealCount', + title: '成交个数', + minWidth: 200, + }, + { + field: 'industryPortion', + title: '行业占比(%)', + minWidth: 200, + }, + { + field: 'dealPortion', + title: '成交占比(%)', + minWidth: 200, + }, + ]; + } + case 'level': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'level', + title: '客户级别', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL }, + }, + }, + { + field: 'customerCount', + title: '客户个数', + minWidth: 200, + }, + { + field: 'dealCount', + title: '成交个数', + minWidth: 200, + }, + { + field: 'industryPortion', + title: '行业占比(%)', + minWidth: 200, + }, + { + field: 'dealPortion', + title: '成交占比(%)', + minWidth: 200, + }, + ]; + } + case 'source': { + return [ + { + type: 'seq', + title: '序号', + }, + { + field: 'source', + title: '客户来源', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE }, + }, + }, + { + field: 'customerCount', + title: '客户个数', + minWidth: 200, + }, + { + field: 'dealCount', + title: '成交个数', + minWidth: 200, + }, + { + field: 'industryPortion', + title: '行业占比(%)', + minWidth: 200, + }, + { + field: 'dealPortion', + title: '成交占比(%)', + minWidth: 200, + }, + ]; + } + default: { + return []; + } + } +} diff --git a/apps/web-ele/src/views/crm/statistics/portrait/index.vue b/apps/web-ele/src/views/crm/statistics/portrait/index.vue new file mode 100644 index 000000000..5e09b1f66 --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/portrait/index.vue @@ -0,0 +1,105 @@ + + + diff --git a/apps/web-ele/src/views/crm/statistics/rank/chartOptions.ts b/apps/web-ele/src/views/crm/statistics/rank/chartOptions.ts new file mode 100644 index 000000000..7234687c5 --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/rank/chartOptions.ts @@ -0,0 +1,394 @@ +import { cloneDeep } from '@vben/utils'; + +export function getChartOptions(activeTabName: any, res: any): any { + switch (activeTabName) { + case 'contactCountRank': { + return { + dataset: { + dimensions: ['nickname', 'count'], + source: cloneDeep(res).reverse(), + }, + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: { + top: 50, + }, + series: [ + { + name: '新增联系人数排行', + type: 'bar', + }, + ], + toolbox: { + feature: { + dataZoom: { + yAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '新增联系人数排行' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + xAxis: { + type: 'value', + name: '新增联系人数(个)', + }, + yAxis: { + type: 'category', + name: '创建人', + }, + }; + } + case 'contractCountRank': { + return { + dataset: { + dimensions: ['nickname', 'count'], + source: cloneDeep(res).reverse(), + }, + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: { + top: 50, + }, + series: [ + { + name: '签约合同排行', + type: 'bar', + }, + ], + toolbox: { + feature: { + dataZoom: { + yAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '签约合同排行' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + xAxis: { + type: 'value', + name: '签约合同数(个)', + }, + yAxis: { + type: 'category', + name: '签订人', + }, + }; + } + case 'contractPriceRank': { + return { + dataset: { + dimensions: ['nickname', 'count'], + source: cloneDeep(res).reverse(), + }, + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: { + top: 50, + }, + series: [ + { + name: '合同金额排行', + type: 'bar', + }, + ], + toolbox: { + feature: { + dataZoom: { + yAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '合同金额排行' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + xAxis: { + type: 'value', + name: '合同金额(元)', + }, + yAxis: { + type: 'category', + name: '签订人', + }, + }; + } + case 'customerCountRank': { + return { + dataset: { + dimensions: ['nickname', 'count'], + source: cloneDeep(res).reverse(), + }, + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: { + top: 50, + }, + series: [ + { + name: '新增客户数排行', + type: 'bar', + }, + ], + toolbox: { + feature: { + dataZoom: { + yAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '新增客户数排行' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + xAxis: { + type: 'value', + name: '新增客户数(个)', + }, + yAxis: { + type: 'category', + name: '创建人', + }, + }; + } + case 'followCountRank': { + return { + dataset: { + dimensions: ['nickname', 'count'], + source: cloneDeep(res).reverse(), + }, + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: { + top: 50, + }, + series: [ + { + name: '跟进次数排行', + type: 'bar', + }, + ], + toolbox: { + feature: { + dataZoom: { + yAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '跟进次数排行' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + xAxis: { + type: 'value', + name: '跟进次数(次)', + }, + yAxis: { + type: 'category', + name: '员工', + }, + }; + } + case 'followCustomerCountRank': { + return { + dataset: { + dimensions: ['nickname', 'count'], + source: cloneDeep(res).reverse(), + }, + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: { + top: 50, + }, + series: [ + { + name: '跟进客户数排行', + type: 'bar', + }, + ], + toolbox: { + feature: { + dataZoom: { + yAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '跟进客户数排行' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + xAxis: { + type: 'value', + name: '跟进客户数(个)', + }, + yAxis: { + type: 'category', + name: '员工', + }, + }; + } + case 'productSalesRank': { + return { + dataset: { + dimensions: ['nickname', 'count'], + source: cloneDeep(res).reverse(), + }, + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: { + top: 50, + }, + series: [ + { + name: '产品销量排行', + type: 'bar', + }, + ], + toolbox: { + feature: { + dataZoom: { + yAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '产品销量排行' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + xAxis: { + type: 'value', + name: '产品销量', + }, + yAxis: { + type: 'category', + name: '员工', + }, + }; + } + case 'receivablePriceRank': { + return { + dataset: { + dimensions: ['nickname', 'count'], + source: cloneDeep(res).reverse(), + }, + grid: { + left: 20, + right: 20, + bottom: 20, + containLabel: true, + }, + legend: { + top: 50, + }, + series: [ + { + name: '回款金额排行', + type: 'bar', + }, + ], + toolbox: { + feature: { + dataZoom: { + yAxisIndex: false, // 数据区域缩放:Y 轴不缩放 + }, + brush: { + type: ['lineX', 'clear'], // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '回款金额排行' }, // 保存为图片 + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + xAxis: { + type: 'value', + name: '回款金额(元)', + }, + yAxis: { + type: 'category', + name: '签订人', + nameGap: 30, + }, + }; + } + default: { + return {}; + } + } +} diff --git a/apps/web-ele/src/views/crm/statistics/rank/data.ts b/apps/web-ele/src/views/crm/statistics/rank/data.ts new file mode 100644 index 000000000..e07f6042f --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/rank/data.ts @@ -0,0 +1,276 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { useUserStore } from '@vben/stores'; +import { beginOfDay, endOfDay, handleTree } from '@vben/utils'; + +import { getSimpleDeptList } from '#/api/system/dept'; +import { getRangePickerDefaultProps } from '#/utils'; + +const userStore = useUserStore(); + +export const customerSummaryTabs = [ + { + tab: '合同金额排行', + key: 'contractPriceRank', + }, + { + tab: '回款金额排行', + key: 'receivablePriceRank', + }, + { + tab: '签约合同排行', + key: 'contractCountRank', + }, + { + tab: '产品销量排行', + key: 'productSalesRank', + }, + { + tab: '新增客户数排行', + key: 'customerCountRank', + }, + { + tab: '新增联系人数排行', + key: 'contactCountRank', + }, + { + tab: '跟进次数排行', + key: 'followCountRank', + }, + { + tab: '跟进客户数排行', + key: 'followCustomerCountRank', + }, +]; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'times', + label: '时间范围', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + }, + defaultValue: [ + beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7)), + endOfDay(new Date(Date.now() - 3600 * 1000 * 24)), + ], + }, + { + 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, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns( + activeTabName: any, +): VxeTableGridOptions['columns'] { + switch (activeTabName) { + case 'contactCountRank': { + return [ + { + type: 'seq', + title: '公司排名', + }, + { + field: 'nickname', + title: '创建人', + minWidth: 200, + }, + { + field: 'deptName', + title: '部门', + minWidth: 200, + }, + { + field: 'count', + title: '新增联系人数(个)', + minWidth: 200, + }, + ]; + } + case 'contractCountRank': { + return [ + { + type: 'seq', + title: '公司排名', + }, + { + field: 'nickname', + title: '签订人', + minWidth: 200, + }, + { + field: 'deptName', + title: '部门', + minWidth: 200, + }, + { + field: 'count', + title: '签约合同数(个)', + minWidth: 200, + }, + ]; + } + case 'contractPriceRank': { + return [ + { + type: 'seq', + title: '公司排名', + }, + { + field: 'nickname', + title: '签订人', + minWidth: 200, + }, + { + field: 'deptName', + title: '部门', + minWidth: 200, + }, + { + field: 'count', + title: '合同金额(元)', + minWidth: 200, + formatter: 'formatAmount2', + }, + ]; + } + case 'customerCountRank': { + return [ + { + type: 'seq', + title: '公司排名', + }, + { + field: 'nickname', + title: '签订人', + minWidth: 200, + }, + { + field: 'deptName', + title: '部门', + minWidth: 200, + }, + { + field: 'count', + title: '新增客户数(个)', + minWidth: 200, + }, + ]; + } + case 'followCountRank': { + return [ + { + type: 'seq', + title: '公司排名', + }, + { + field: 'nickname', + title: '签订人', + minWidth: 200, + }, + { + field: 'deptName', + title: '部门', + minWidth: 200, + }, + { + field: 'count', + title: '跟进次数(次)', + minWidth: 200, + }, + ]; + } + case 'followCustomerCountRank': { + return [ + { + type: 'seq', + title: '公司排名', + }, + { + field: 'nickname', + title: '签订人', + minWidth: 200, + }, + { + field: 'deptName', + title: '部门', + minWidth: 200, + }, + { + field: 'count', + title: '跟进客户数(个)', + minWidth: 200, + }, + ]; + } + case 'productSalesRank': { + return [ + { + type: 'seq', + title: '公司排名', + }, + { + field: 'nickname', + title: '签订人', + minWidth: 200, + }, + { + field: 'deptName', + title: '部门', + minWidth: 200, + }, + { + field: 'count', + title: '产品销量', + minWidth: 200, + }, + ]; + } + case 'receivablePriceRank': { + return [ + { + type: 'seq', + title: '公司排名', + }, + { + field: 'nickname', + title: '签订人', + minWidth: 200, + }, + { + field: 'deptName', + title: '部门', + minWidth: 200, + }, + { + field: 'count', + title: '回款金额(元)', + minWidth: 200, + formatter: 'formatAmount2', + }, + ]; + } + default: { + return []; + } + } +} diff --git a/apps/web-ele/src/views/crm/statistics/rank/index.vue b/apps/web-ele/src/views/crm/statistics/rank/index.vue new file mode 100644 index 000000000..964b1db41 --- /dev/null +++ b/apps/web-ele/src/views/crm/statistics/rank/index.vue @@ -0,0 +1,109 @@ + + +