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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+