From 4cad0e6523ffaaa0f468ec5f8496cdde590809f1 Mon Sep 17 00:00:00 2001 From: xingyu4j Date: Tue, 24 Jun 2025 12:52:05 +0800 Subject: [PATCH] feat: crm statistics portrait --- .../src/api/crm/statistics/portrait.ts | 20 + .../crm/statistics/portrait/chartOptions.ts | 439 ++++++++++++++++++ .../src/views/crm/statistics/portrait/data.ts | 199 ++++++++ .../views/crm/statistics/portrait/index.vue | 99 +++- 4 files changed, 736 insertions(+), 21 deletions(-) create mode 100644 apps/web-antd/src/views/crm/statistics/portrait/chartOptions.ts create mode 100644 apps/web-antd/src/views/crm/statistics/portrait/data.ts diff --git a/apps/web-antd/src/api/crm/statistics/portrait.ts b/apps/web-antd/src/api/crm/statistics/portrait.ts index 88ff518de..ecbe9c9dc 100644 --- a/apps/web-antd/src/api/crm/statistics/portrait.ts +++ b/apps/web-antd/src/api/crm/statistics/portrait.ts @@ -36,6 +36,26 @@ export namespace CrmStatisticsPortraitApi { } } +export function getDatas(activeTabName: any, params: any) { + switch (activeTabName) { + case 'area': { + return getCustomerArea(params); + } + case 'industry': { + return getCustomerIndustry(params); + } + case 'level': { + return getCustomerLevel(params); + } + case 'source': { + return getCustomerSource(params); + } + default: { + return []; + } + } +} + /** 获取客户行业统计数据 */ export function getCustomerIndustry(params: PageParam) { return requestClient.get( diff --git a/apps/web-antd/src/views/crm/statistics/portrait/chartOptions.ts b/apps/web-antd/src/views/crm/statistics/portrait/chartOptions.ts new file mode 100644 index 000000000..fe6aa507f --- /dev/null +++ b/apps/web-antd/src/views/crm/statistics/portrait/chartOptions.ts @@ -0,0 +1,439 @@ +import { DICT_TYPE, getDictLabel } from '#/utils'; + +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-antd/src/views/crm/statistics/portrait/data.ts b/apps/web-antd/src/views/crm/statistics/portrait/data.ts new file mode 100644 index 000000000..8c0e69440 --- /dev/null +++ b/apps/web-antd/src/views/crm/statistics/portrait/data.ts @@ -0,0 +1,199 @@ +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, 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', + picker: 'year', + }, + defaultValue: [ + formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))), + formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))), + ] as [Date, Date], + }, + { + 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 '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-antd/src/views/crm/statistics/portrait/index.vue b/apps/web-antd/src/views/crm/statistics/portrait/index.vue index 857fc95e9..d8ceea9d3 100644 --- a/apps/web-antd/src/views/crm/statistics/portrait/index.vue +++ b/apps/web-antd/src/views/crm/statistics/portrait/index.vue @@ -1,28 +1,85 @@