commit
						f911b0f65c
					
				| 
						 | 
				
			
			@ -24,6 +24,7 @@ import {
 | 
			
		|||
  ImagePreviewGroup,
 | 
			
		||||
  Popconfirm,
 | 
			
		||||
  Switch,
 | 
			
		||||
  Tag,
 | 
			
		||||
} from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { DictTag } from '#/components/dict-tag';
 | 
			
		||||
| 
						 | 
				
			
			@ -113,6 +114,35 @@ setupVbenVxeTable({
 | 
			
		|||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 表格配置项可以用 cellRender: { name: 'CellTag' },
 | 
			
		||||
    vxeUI.renderer.add('CellTag', {
 | 
			
		||||
      renderTableDefault(renderOpts, params) {
 | 
			
		||||
        const { props } = renderOpts;
 | 
			
		||||
        const { column, row } = params;
 | 
			
		||||
        return h(Tag, { color: props?.color }, () => row[column.field]);
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    vxeUI.renderer.add('CellTags', {
 | 
			
		||||
      renderTableDefault(renderOpts, params) {
 | 
			
		||||
        const { props } = renderOpts;
 | 
			
		||||
        const { column, row } = params;
 | 
			
		||||
        if (!row[column.field] || row[column.field].length === 0) {
 | 
			
		||||
          return '';
 | 
			
		||||
        }
 | 
			
		||||
        return h(
 | 
			
		||||
          'div',
 | 
			
		||||
          { class: 'flex items-center justify-center' },
 | 
			
		||||
          {
 | 
			
		||||
            default: () =>
 | 
			
		||||
              row[column.field].map((item: any) =>
 | 
			
		||||
                h(Tag, { color: props?.color }, { default: () => item }),
 | 
			
		||||
              ),
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 表格配置项可以用 cellRender: { name: 'CellDict', props:{dictType: ''} },
 | 
			
		||||
    vxeUI.renderer.add('CellDict', {
 | 
			
		||||
      renderTableDefault(renderOpts, params) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,3 @@
 | 
			
		|||
import type { PageParam } from '@vben/request';
 | 
			
		||||
 | 
			
		||||
import { requestClient } from '#/api/request';
 | 
			
		||||
 | 
			
		||||
export namespace CrmStatisticsCustomerApi {
 | 
			
		||||
| 
						 | 
				
			
			@ -93,10 +91,84 @@ export namespace CrmStatisticsCustomerApi {
 | 
			
		|||
    customerDealCycle: number;
 | 
			
		||||
    customerDealCount: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface CustomerSummaryParams {
 | 
			
		||||
    times: string[];
 | 
			
		||||
    interval: number;
 | 
			
		||||
    deptId: number;
 | 
			
		||||
    userId: number;
 | 
			
		||||
    userIds: number[];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getDatas(activeTabName: any, params: any) {
 | 
			
		||||
  switch (activeTabName) {
 | 
			
		||||
    case 'conversionStat': {
 | 
			
		||||
      return getContractSummary(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'customerSummary': {
 | 
			
		||||
      return getCustomerSummaryByUser(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'dealCycleByArea': {
 | 
			
		||||
      return getCustomerDealCycleByArea(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'dealCycleByProduct': {
 | 
			
		||||
      return getCustomerDealCycleByProduct(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'dealCycleByUser': {
 | 
			
		||||
      return getCustomerDealCycleByUser(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'followUpSummary': {
 | 
			
		||||
      return getFollowUpSummaryByUser(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'followUpType': {
 | 
			
		||||
      return getFollowUpSummaryByType(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'poolSummary': {
 | 
			
		||||
      return getPoolSummaryByUser(params);
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getChartDatas(activeTabName: any, params: any) {
 | 
			
		||||
  switch (activeTabName) {
 | 
			
		||||
    case 'conversionStat': {
 | 
			
		||||
      return getCustomerSummaryByDate(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'customerSummary': {
 | 
			
		||||
      return getCustomerSummaryByDate(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'dealCycleByArea': {
 | 
			
		||||
      return getCustomerDealCycleByArea(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'dealCycleByProduct': {
 | 
			
		||||
      return getCustomerDealCycleByProduct(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'dealCycleByUser': {
 | 
			
		||||
      return getCustomerDealCycleByUser(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'followUpSummary': {
 | 
			
		||||
      return getFollowUpSummaryByDate(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'followUpType': {
 | 
			
		||||
      return getFollowUpSummaryByType(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'poolSummary': {
 | 
			
		||||
      return getPoolSummaryByDate(params);
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 客户总量分析(按日期) */
 | 
			
		||||
export function getCustomerSummaryByDate(params: PageParam) {
 | 
			
		||||
export function getCustomerSummaryByDate(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.CustomerSummaryByDate[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-customer-summary-by-date',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +176,9 @@ export function getCustomerSummaryByDate(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 客户总量分析(按用户) */
 | 
			
		||||
export function getCustomerSummaryByUser(params: PageParam) {
 | 
			
		||||
export function getCustomerSummaryByUser(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.CustomerSummaryByUser[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-customer-summary-by-user',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +186,9 @@ export function getCustomerSummaryByUser(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 客户跟进次数分析(按日期) */
 | 
			
		||||
export function getFollowUpSummaryByDate(params: PageParam) {
 | 
			
		||||
export function getFollowUpSummaryByDate(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.FollowUpSummaryByDate[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-follow-up-summary-by-date',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -120,7 +196,9 @@ export function getFollowUpSummaryByDate(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 客户跟进次数分析(按用户) */
 | 
			
		||||
export function getFollowUpSummaryByUser(params: PageParam) {
 | 
			
		||||
export function getFollowUpSummaryByUser(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.FollowUpSummaryByUser[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-follow-up-summary-by-user',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -128,7 +206,9 @@ export function getFollowUpSummaryByUser(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取客户跟进方式统计数 */
 | 
			
		||||
export function getFollowUpSummaryByType(params: PageParam) {
 | 
			
		||||
export function getFollowUpSummaryByType(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.FollowUpSummaryByType[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-follow-up-summary-by-type',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -136,7 +216,9 @@ export function getFollowUpSummaryByType(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 合同摘要信息(客户转化率页面) */
 | 
			
		||||
export function getContractSummary(params: PageParam) {
 | 
			
		||||
export function getContractSummary(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.CustomerContractSummary[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-contract-summary',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -144,7 +226,9 @@ export function getContractSummary(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取客户公海分析(按日期) */
 | 
			
		||||
export function getPoolSummaryByDate(params: PageParam) {
 | 
			
		||||
export function getPoolSummaryByDate(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.PoolSummaryByDate[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-pool-summary-by-date',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -152,7 +236,9 @@ export function getPoolSummaryByDate(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取客户公海分析(按用户) */
 | 
			
		||||
export function getPoolSummaryByUser(params: PageParam) {
 | 
			
		||||
export function getPoolSummaryByUser(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.PoolSummaryByUser[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-pool-summary-by-user',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -160,7 +246,9 @@ export function getPoolSummaryByUser(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取客户成交周期(按日期) */
 | 
			
		||||
export function getCustomerDealCycleByDate(params: PageParam) {
 | 
			
		||||
export function getCustomerDealCycleByDate(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.CustomerDealCycleByDate[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-customer-deal-cycle-by-date',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -168,7 +256,9 @@ export function getCustomerDealCycleByDate(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取客户成交周期(按用户) */
 | 
			
		||||
export function getCustomerDealCycleByUser(params: PageParam) {
 | 
			
		||||
export function getCustomerDealCycleByUser(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.CustomerDealCycleByUser[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-customer-deal-cycle-by-user',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -176,7 +266,9 @@ export function getCustomerDealCycleByUser(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取客户成交周期(按地区) */
 | 
			
		||||
export function getCustomerDealCycleByArea(params: PageParam) {
 | 
			
		||||
export function getCustomerDealCycleByArea(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsCustomerApi.CustomerDealCycleByArea[]>(
 | 
			
		||||
    '/crm/statistics-customer/get-customer-deal-cycle-by-area',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -184,7 +276,9 @@ export function getCustomerDealCycleByArea(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取客户成交周期(按产品) */
 | 
			
		||||
export function getCustomerDealCycleByProduct(params: PageParam) {
 | 
			
		||||
export function getCustomerDealCycleByProduct(
 | 
			
		||||
  params: CrmStatisticsCustomerApi.CustomerSummaryParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<
 | 
			
		||||
    CrmStatisticsCustomerApi.CustomerDealCycleByProduct[]
 | 
			
		||||
  >('/crm/statistics-customer/get-customer-deal-cycle-by-product', { params });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import type { PageParam, PageResult } from '@vben/request';
 | 
			
		||||
import type { PageResult } from '@vben/request';
 | 
			
		||||
 | 
			
		||||
import { requestClient } from '#/api/request';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -25,8 +25,42 @@ export namespace CrmStatisticsFunnelApi {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getDatas(activeTabName: any, params: any) {
 | 
			
		||||
  switch (activeTabName) {
 | 
			
		||||
    case 'businessInversionRateSummary': {
 | 
			
		||||
      return getBusinessPageByDate(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'businessSummary': {
 | 
			
		||||
      return getBusinessPageByDate(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'funnel': {
 | 
			
		||||
      return getBusinessSummaryByEndStatus(params);
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getChartDatas(activeTabName: any, params: any) {
 | 
			
		||||
  switch (activeTabName) {
 | 
			
		||||
    case 'businessInversionRateSummary': {
 | 
			
		||||
      return getBusinessInversionRateSummaryByDate(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'businessSummary': {
 | 
			
		||||
      return getBusinessSummaryByDate(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'funnel': {
 | 
			
		||||
      return getFunnelSummary(params);
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 获取销售漏斗统计数据 */
 | 
			
		||||
export function getFunnelSummary(params: PageParam) {
 | 
			
		||||
export function getFunnelSummary(params: any) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsFunnelApi.FunnelSummary>(
 | 
			
		||||
    '/crm/statistics-funnel/get-funnel-summary',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +68,7 @@ export function getFunnelSummary(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取商机结束状态统计 */
 | 
			
		||||
export function getBusinessSummaryByEndStatus(params: PageParam) {
 | 
			
		||||
export function getBusinessSummaryByEndStatus(params: any) {
 | 
			
		||||
  return requestClient.get<Record<string, number>>(
 | 
			
		||||
    '/crm/statistics-funnel/get-business-summary-by-end-status',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +76,7 @@ export function getBusinessSummaryByEndStatus(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取新增商机分析(按日期) */
 | 
			
		||||
export function getBusinessSummaryByDate(params: PageParam) {
 | 
			
		||||
export function getBusinessSummaryByDate(params: any) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsFunnelApi.BusinessSummaryByDate[]>(
 | 
			
		||||
    '/crm/statistics-funnel/get-business-summary-by-date',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +84,7 @@ export function getBusinessSummaryByDate(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取商机转化率分析(按日期) */
 | 
			
		||||
export function getBusinessInversionRateSummaryByDate(params: PageParam) {
 | 
			
		||||
export function getBusinessInversionRateSummaryByDate(params: any) {
 | 
			
		||||
  return requestClient.get<
 | 
			
		||||
    CrmStatisticsFunnelApi.BusinessInversionRateSummaryByDate[]
 | 
			
		||||
  >('/crm/statistics-funnel/get-business-inversion-rate-summary-by-date', {
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +93,7 @@ export function getBusinessInversionRateSummaryByDate(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 获取商机列表(按日期) */
 | 
			
		||||
export function getBusinessPageByDate(params: PageParam) {
 | 
			
		||||
export function getBusinessPageByDate(params: any) {
 | 
			
		||||
  return requestClient.get<PageResult<any>>(
 | 
			
		||||
    '/crm/statistics-funnel/get-business-page-by-date',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,3 @@
 | 
			
		|||
import type { PageParam } from '@vben/request';
 | 
			
		||||
 | 
			
		||||
import { requestClient } from '#/api/request';
 | 
			
		||||
 | 
			
		||||
export namespace CrmStatisticsPerformanceApi {
 | 
			
		||||
| 
						 | 
				
			
			@ -10,10 +8,17 @@ export namespace CrmStatisticsPerformanceApi {
 | 
			
		|||
    lastMonthCount: number;
 | 
			
		||||
    lastYearCount: number;
 | 
			
		||||
  }
 | 
			
		||||
  export interface PerformanceParams {
 | 
			
		||||
    times: string[];
 | 
			
		||||
    deptId: number;
 | 
			
		||||
    userId: number;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 员工获得合同金额统计 */
 | 
			
		||||
export function getContractPricePerformance(params: PageParam) {
 | 
			
		||||
export function getContractPricePerformance(
 | 
			
		||||
  params: CrmStatisticsPerformanceApi.PerformanceParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsPerformanceApi.Performance[]>(
 | 
			
		||||
    '/crm/statistics-performance/get-contract-price-performance',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +26,9 @@ export function getContractPricePerformance(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 员工获得回款统计 */
 | 
			
		||||
export function getReceivablePricePerformance(params: PageParam) {
 | 
			
		||||
export function getReceivablePricePerformance(
 | 
			
		||||
  params: CrmStatisticsPerformanceApi.PerformanceParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsPerformanceApi.Performance[]>(
 | 
			
		||||
    '/crm/statistics-performance/get-receivable-price-performance',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +36,9 @@ export function getReceivablePricePerformance(params: PageParam) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/** 员工获得签约合同数量统计 */
 | 
			
		||||
export function getContractCountPerformance(params: PageParam) {
 | 
			
		||||
export function getContractCountPerformance(
 | 
			
		||||
  params: CrmStatisticsPerformanceApi.PerformanceParams,
 | 
			
		||||
) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsPerformanceApi.Performance[]>(
 | 
			
		||||
    '/crm/statistics-performance/get-contract-count-performance',
 | 
			
		||||
    { params },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<CrmStatisticsPortraitApi.CustomerIndustry[]>(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,38 @@ export namespace CrmStatisticsRankApi {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getDatas(activeTabName: any, params: any) {
 | 
			
		||||
  switch (activeTabName) {
 | 
			
		||||
    case 'contactCountRank': {
 | 
			
		||||
      return getContactsCountRank(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'contractCountRank': {
 | 
			
		||||
      return getContractCountRank(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'contractPriceRank': {
 | 
			
		||||
      return getContractPriceRank(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'customerCountRank': {
 | 
			
		||||
      return getCustomerCountRank(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'followCountRank': {
 | 
			
		||||
      return getFollowCountRank(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'followCustomerCountRank': {
 | 
			
		||||
      return getFollowCustomerCountRank(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'productSalesRank': {
 | 
			
		||||
      return getProductSalesRank(params);
 | 
			
		||||
    }
 | 
			
		||||
    case 'receivablePriceRank': {
 | 
			
		||||
      return getReceivablePriceRank(params);
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 获得合同排行榜 */
 | 
			
		||||
export function getContractPriceRank(params: PageParam) {
 | 
			
		||||
  return requestClient.get<CrmStatisticsRankApi.Rank[]>(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -121,7 +121,7 @@ const apiSelectRule = [
 | 
			
		|||
            field: 'data',
 | 
			
		||||
            title: '请求参数 JSON 格式',
 | 
			
		||||
            props: {
 | 
			
		||||
              autosize: true,
 | 
			
		||||
              autoSize: true,
 | 
			
		||||
              type: 'textarea',
 | 
			
		||||
              placeholder: '{"type": 1}',
 | 
			
		||||
            },
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +155,7 @@ const apiSelectRule = [
 | 
			
		|||
    info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
 | 
			
		||||
    (data: any)=>{ label: string; value: any }[]`,
 | 
			
		||||
    props: {
 | 
			
		||||
      autosize: true,
 | 
			
		||||
      autoSize: true,
 | 
			
		||||
      rows: { minRows: 2, maxRows: 6 },
 | 
			
		||||
      type: 'textarea',
 | 
			
		||||
      placeholder: `
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,6 +63,7 @@ const [Modal, modalApi] = useVbenModal({
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
// TODO xingyu 暴露 modalApi 给父组件是否合适? trigger-node-config.vue 会有多个 conditionDialog 实例
 | 
			
		||||
// 不用暴露啊,用 useVbenModal 就可以了
 | 
			
		||||
defineExpose({ modalApi });
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,10 @@ import type { SimpleFlowNode } from '../../consts';
 | 
			
		|||
 | 
			
		||||
import { inject, ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenModal } from '@vben/common-ui';
 | 
			
		||||
 | 
			
		||||
import { useTaskStatusClass, useWatchNode } from '../../helpers';
 | 
			
		||||
import ProcessInstanceModal from './modules/process-instance-modal.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({ name: 'EndEventNode' });
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
| 
						 | 
				
			
			@ -20,15 +23,26 @@ const currentNode = useWatchNode(props);
 | 
			
		|||
const readonly = inject<Boolean>('readonly');
 | 
			
		||||
const processInstance = inject<Ref<any>>('processInstance', ref({}));
 | 
			
		||||
 | 
			
		||||
const processInstanceInfos = ref<any[]>([]); // 流程的审批信息
 | 
			
		||||
const [Modal, modalApi] = useVbenModal({
 | 
			
		||||
  connectedComponent: ProcessInstanceModal,
 | 
			
		||||
  destroyOnClose: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function nodeClick() {
 | 
			
		||||
  if (readonly && processInstance && processInstance.value) {
 | 
			
		||||
    console.warn(
 | 
			
		||||
      'TODO 只读模式,弹窗显示审批信息',
 | 
			
		||||
      processInstance.value,
 | 
			
		||||
      processInstanceInfos.value,
 | 
			
		||||
    );
 | 
			
		||||
    const processInstanceInfo = [
 | 
			
		||||
      {
 | 
			
		||||
        startUser: processInstance.value.startUser,
 | 
			
		||||
        createTime: processInstance.value.startTime,
 | 
			
		||||
        endTime: processInstance.value.endTime,
 | 
			
		||||
        status: processInstance.value.status,
 | 
			
		||||
        durationInMillis: processInstance.value.durationInMillis,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
    modalApi
 | 
			
		||||
      .setData(processInstanceInfo)
 | 
			
		||||
      .setState({ title: '流程信息' })
 | 
			
		||||
      .open();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -42,5 +56,6 @@ function nodeClick() {
 | 
			
		|||
      <span class="node-fixed-name" title="结束">结束</span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <!-- TODO 审批信息 -->
 | 
			
		||||
  <!-- 流程信息弹窗 -->
 | 
			
		||||
  <Modal />
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
 | 
			
		||||
 | 
			
		||||
import { DICT_TYPE } from '#/utils';
 | 
			
		||||
 | 
			
		||||
/** 流程实例列表字段 */
 | 
			
		||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      field: 'startUser',
 | 
			
		||||
      title: '发起人',
 | 
			
		||||
      slots: {
 | 
			
		||||
        default: ({ row }: { row: any }) => {
 | 
			
		||||
          return row.startUser?.nickname;
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      minWidth: 100,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'deptName',
 | 
			
		||||
      title: '部门',
 | 
			
		||||
      slots: {
 | 
			
		||||
        default: ({ row }: { row: any }) => {
 | 
			
		||||
          return row.startUser?.deptName;
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      minWidth: 100,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'createTime',
 | 
			
		||||
      title: '开始时间',
 | 
			
		||||
      formatter: 'formatDateTime',
 | 
			
		||||
      minWidth: 140,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'endTime',
 | 
			
		||||
      title: '结束时间',
 | 
			
		||||
      formatter: 'formatDateTime',
 | 
			
		||||
      minWidth: 140,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'status',
 | 
			
		||||
      title: '流程状态',
 | 
			
		||||
      minWidth: 90,
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        name: 'CellDict',
 | 
			
		||||
        props: { type: DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'durationInMillis',
 | 
			
		||||
      title: '耗时',
 | 
			
		||||
      minWidth: 100,
 | 
			
		||||
      formatter: 'formatPast2',
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { useVbenModal } from '@vben/common-ui';
 | 
			
		||||
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
 | 
			
		||||
 | 
			
		||||
import { useGridColumns } from './process-instance-data';
 | 
			
		||||
 | 
			
		||||
const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		||||
  gridOptions: {
 | 
			
		||||
    columns: useGridColumns(),
 | 
			
		||||
    border: true,
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
    pagerConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
    toolbarConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const [Modal, modalApi] = useVbenModal({
 | 
			
		||||
  footer: false,
 | 
			
		||||
  async onOpenChange(isOpen: boolean) {
 | 
			
		||||
    if (!isOpen) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    modalApi.lock();
 | 
			
		||||
    try {
 | 
			
		||||
      const data = modalApi.getData<any[]>();
 | 
			
		||||
      // 填充列表数据
 | 
			
		||||
      await gridApi.setGridOptions({ data });
 | 
			
		||||
    } finally {
 | 
			
		||||
      modalApi.unlock();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Modal class="w-3/4">
 | 
			
		||||
    <Grid />
 | 
			
		||||
  </Modal>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
 | 
			
		||||
 | 
			
		||||
import { DICT_TYPE } from '#/utils';
 | 
			
		||||
 | 
			
		||||
/** 审批记录列表字段 */
 | 
			
		||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      field: 'assigneeUser',
 | 
			
		||||
      title: '审批人',
 | 
			
		||||
      slots: {
 | 
			
		||||
        default: ({ row }: { row: any }) => {
 | 
			
		||||
          return row.assigneeUser?.nickname || row.ownerUser?.nickname;
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      minWidth: 100,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'deptName',
 | 
			
		||||
      title: '部门',
 | 
			
		||||
      slots: {
 | 
			
		||||
        default: ({ row }: { row: any }) => {
 | 
			
		||||
          return row.assigneeUser?.deptName || row.ownerUser?.deptName;
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      minWidth: 100,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'createTime',
 | 
			
		||||
      title: '开始时间',
 | 
			
		||||
      formatter: 'formatDateTime',
 | 
			
		||||
      minWidth: 140,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'endTime',
 | 
			
		||||
      title: '结束时间',
 | 
			
		||||
      formatter: 'formatDateTime',
 | 
			
		||||
      minWidth: 140,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'status',
 | 
			
		||||
      title: '审批状态',
 | 
			
		||||
      minWidth: 90,
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        name: 'CellDict',
 | 
			
		||||
        props: { type: DICT_TYPE.BPM_TASK_STATUS },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'reason',
 | 
			
		||||
      title: '审批建议',
 | 
			
		||||
      minWidth: 160,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'durationInMillis',
 | 
			
		||||
      title: '耗时',
 | 
			
		||||
      minWidth: 100,
 | 
			
		||||
      formatter: 'formatPast2',
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { useVbenModal } from '@vben/common-ui';
 | 
			
		||||
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
 | 
			
		||||
 | 
			
		||||
import { useGridColumns } from './task-list-data';
 | 
			
		||||
 | 
			
		||||
const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		||||
  gridOptions: {
 | 
			
		||||
    columns: useGridColumns(),
 | 
			
		||||
    border: true,
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
    rowConfig: {
 | 
			
		||||
      keyField: 'id',
 | 
			
		||||
    },
 | 
			
		||||
    pagerConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
    toolbarConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const [Modal, modalApi] = useVbenModal({
 | 
			
		||||
  footer: false,
 | 
			
		||||
  async onOpenChange(isOpen: boolean) {
 | 
			
		||||
    if (!isOpen) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    modalApi.lock();
 | 
			
		||||
    try {
 | 
			
		||||
      const data = modalApi.getData<any[]>();
 | 
			
		||||
      // 填充列表数据
 | 
			
		||||
      await gridApi.setGridOptions({ data });
 | 
			
		||||
    } finally {
 | 
			
		||||
      modalApi.unlock();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Modal class="w-3/4">
 | 
			
		||||
    <Grid />
 | 
			
		||||
  </Modal>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import type { SimpleFlowNode } from '../../consts';
 | 
			
		|||
 | 
			
		||||
import { inject, ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenModal } from '@vben/common-ui';
 | 
			
		||||
import { IconifyIcon } from '@vben/icons';
 | 
			
		||||
 | 
			
		||||
import { Input } from 'ant-design-vue';
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +16,7 @@ import { BpmNodeTypeEnum } from '#/utils';
 | 
			
		|||
import { NODE_DEFAULT_TEXT } from '../../consts';
 | 
			
		||||
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
 | 
			
		||||
import StartUserNodeConfig from '../nodes-config/start-user-node-config.vue';
 | 
			
		||||
import TaskListModal from './modules/task-list-modal.vue';
 | 
			
		||||
import NodeHandler from './node-handler.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({ name: 'StartUserNode' });
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +29,6 @@ const props = defineProps({
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
// 定义事件,更新父组件。
 | 
			
		||||
// const emits = defineEmits<{
 | 
			
		||||
defineEmits<{
 | 
			
		||||
  'update:modelValue': [node: SimpleFlowNode | undefined];
 | 
			
		||||
}>();
 | 
			
		||||
| 
						 | 
				
			
			@ -44,24 +45,25 @@ const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
 | 
			
		|||
 | 
			
		||||
const nodeSetting = ref();
 | 
			
		||||
 | 
			
		||||
// 任务的弹窗显示,用于只读模式
 | 
			
		||||
const selectTasks = ref<any[] | undefined>([]); // 选中的任务数组
 | 
			
		||||
 | 
			
		||||
const [Modal, modalApi] = useVbenModal({
 | 
			
		||||
  connectedComponent: TaskListModal,
 | 
			
		||||
  destroyOnClose: true,
 | 
			
		||||
});
 | 
			
		||||
function nodeClick() {
 | 
			
		||||
  if (readonly) {
 | 
			
		||||
    // 只读模式,弹窗显示任务信息
 | 
			
		||||
    if (tasks && tasks.value) {
 | 
			
		||||
      console.warn(
 | 
			
		||||
        'TODO 只读模式,弹窗显示任务信息',
 | 
			
		||||
        tasks.value,
 | 
			
		||||
        selectTasks.value,
 | 
			
		||||
      // 过滤出当前节点的任务
 | 
			
		||||
      const nodeTasks = tasks.value.filter(
 | 
			
		||||
        (task) => task.taskDefinitionKey === currentNode.value.id,
 | 
			
		||||
      );
 | 
			
		||||
      // 弹窗显示任务信息
 | 
			
		||||
      modalApi
 | 
			
		||||
        .setData(nodeTasks)
 | 
			
		||||
        .setState({ title: currentNode.value.name })
 | 
			
		||||
        .open();
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    console.warn(
 | 
			
		||||
      'TODO 编辑模式,打开节点配置、把当前节点传递给配置组件',
 | 
			
		||||
      nodeSetting.value,
 | 
			
		||||
    );
 | 
			
		||||
    nodeSetting.value.showStartUserNodeConfig(currentNode.value);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -122,5 +124,6 @@ function nodeClick() {
 | 
			
		|||
    ref="nodeSetting"
 | 
			
		||||
    :flow-node="currentNode"
 | 
			
		||||
  />
 | 
			
		||||
  <!-- 审批记录  TODO -->
 | 
			
		||||
  <!-- 审批记录弹窗 -->
 | 
			
		||||
  <Modal />
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import type { SimpleFlowNode } from '../../consts';
 | 
			
		|||
 | 
			
		||||
import { inject, ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenModal } from '@vben/common-ui';
 | 
			
		||||
import { IconifyIcon } from '@vben/icons';
 | 
			
		||||
 | 
			
		||||
import { Input } from 'ant-design-vue';
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +15,26 @@ import { BpmNodeTypeEnum } from '#/utils';
 | 
			
		|||
import { NODE_DEFAULT_TEXT } from '../../consts';
 | 
			
		||||
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
 | 
			
		||||
import UserTaskNodeConfig from '../nodes-config/user-task-node-config.vue';
 | 
			
		||||
import TaskListModal from './modules/task-list-modal.vue';
 | 
			
		||||
// // 使用useVbenVxeGrid
 | 
			
		||||
// const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		||||
//   gridOptions: {
 | 
			
		||||
//     columns: columns.value,
 | 
			
		||||
//     keepSource: true,
 | 
			
		||||
//     border: true,
 | 
			
		||||
//     height: 'auto',
 | 
			
		||||
//     data: selectTasks.value,
 | 
			
		||||
//     rowConfig: {
 | 
			
		||||
//       keyField: 'id',
 | 
			
		||||
//     },
 | 
			
		||||
//     pagerConfig: {
 | 
			
		||||
//       enabled: false,
 | 
			
		||||
//     },
 | 
			
		||||
//     toolbarConfig: {
 | 
			
		||||
//       enabled: false,
 | 
			
		||||
//     },
 | 
			
		||||
//   } as VxeTableGridOptions<any>,
 | 
			
		||||
// });
 | 
			
		||||
import NodeHandler from './node-handler.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({ name: 'UserTaskNode' });
 | 
			
		||||
| 
						 | 
				
			
			@ -42,11 +63,23 @@ const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
 | 
			
		|||
);
 | 
			
		||||
const nodeSetting = ref();
 | 
			
		||||
 | 
			
		||||
const [Modal, modalApi] = useVbenModal({
 | 
			
		||||
  connectedComponent: TaskListModal,
 | 
			
		||||
  destroyOnClose: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function nodeClick() {
 | 
			
		||||
  if (readonly) {
 | 
			
		||||
    if (tasks && tasks.value) {
 | 
			
		||||
      // 只读模式,弹窗显示任务信息 TODO 待实现
 | 
			
		||||
      console.warn('只读模式,弹窗显示任务信息待实现');
 | 
			
		||||
      // 过滤出当前节点的任务
 | 
			
		||||
      const nodeTasks = tasks.value.filter(
 | 
			
		||||
        (task) => task.taskDefinitionKey === currentNode.value.id,
 | 
			
		||||
      );
 | 
			
		||||
      // 弹窗显示任务信息
 | 
			
		||||
      modalApi
 | 
			
		||||
        .setData(nodeTasks)
 | 
			
		||||
        .setState({ title: currentNode.value.name })
 | 
			
		||||
        .open();
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    // 编辑模式,打开节点配置、把当前节点传递给配置组件
 | 
			
		||||
| 
						 | 
				
			
			@ -64,8 +97,6 @@ function findReturnTaskNodes(
 | 
			
		|||
  // 从父节点查找
 | 
			
		||||
  emits('findParentNode', matchNodeList, BpmNodeTypeEnum.USER_TASK_NODE);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// const selectTasks = ref<any[] | undefined>([]); // 选中的任务数组
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="node-wrapper">
 | 
			
		||||
| 
						 | 
				
			
			@ -138,5 +169,6 @@ function findReturnTaskNodes(
 | 
			
		|||
    :flow-node="currentNode"
 | 
			
		||||
    @find-return-task-nodes="findReturnTaskNodes"
 | 
			
		||||
  />
 | 
			
		||||
  <!--  TODO 审批记录 -->
 | 
			
		||||
  <!--  审批记录弹窗 -->
 | 
			
		||||
  <Modal />
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -249,7 +249,7 @@ onMounted(() => {
 | 
			
		|||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <!-- TODO 这个好像暂时没有用到。保存失败弹窗 -->
 | 
			
		||||
 | 
			
		||||
  <Modal
 | 
			
		||||
    v-model:open="errorDialogVisible"
 | 
			
		||||
    title="保存失败"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import type { SimpleFlowNode } from '../consts';
 | 
			
		||||
 | 
			
		||||
import { provide, ref, watch } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { useWatchNode } from '../helpers';
 | 
			
		||||
import SimpleProcessModel from './simple-process-model.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({ name: 'SimpleProcessViewer' });
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(
 | 
			
		||||
  defineProps<{
 | 
			
		||||
    flowNode: SimpleFlowNode;
 | 
			
		||||
    // 流程实例
 | 
			
		||||
    processInstance?: any;
 | 
			
		||||
    // 流程任务
 | 
			
		||||
    tasks?: any[];
 | 
			
		||||
  }>(),
 | 
			
		||||
  {
 | 
			
		||||
    processInstance: undefined,
 | 
			
		||||
    tasks: () => [] as any[],
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
const approveTasks = ref<any[]>(props.tasks);
 | 
			
		||||
const currentProcessInstance = ref(props.processInstance);
 | 
			
		||||
const simpleModel = useWatchNode(props);
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.tasks,
 | 
			
		||||
  (newValue) => {
 | 
			
		||||
    approveTasks.value = newValue;
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.processInstance,
 | 
			
		||||
  (newValue) => {
 | 
			
		||||
    currentProcessInstance.value = newValue;
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
// 提供给后代组件使用
 | 
			
		||||
provide('tasks', approveTasks);
 | 
			
		||||
provide('processInstance', currentProcessInstance);
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <SimpleProcessModel :flow-node="simpleModel" :readonly="true" />
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -4,4 +4,8 @@ export { default as HttpRequestSetting } from './components/nodes-config/modules
 | 
			
		|||
 | 
			
		||||
export { default as SimpleProcessDesigner } from './components/simple-process-designer.vue';
 | 
			
		||||
 | 
			
		||||
export { default as SimpleProcessViewer } from './components/simple-process-viewer.vue';
 | 
			
		||||
 | 
			
		||||
export type { SimpleFlowNode } from './consts';
 | 
			
		||||
 | 
			
		||||
export { parseFormFields } from './helpers';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,13 +41,13 @@ const props = defineProps({
 | 
			
		|||
 | 
			
		||||
const { hasAccessByCodes } = useAccess();
 | 
			
		||||
 | 
			
		||||
/** 缓存处理后的actions */
 | 
			
		||||
/** 缓存处理后的 actions */
 | 
			
		||||
const processedActions = ref<any[]>([]);
 | 
			
		||||
const processedDropdownActions = ref<any[]>([]);
 | 
			
		||||
 | 
			
		||||
/** 用于比较的字符串化版本 */
 | 
			
		||||
const actionsStringified = ref('');
 | 
			
		||||
const dropdownActionsStringified = ref('');
 | 
			
		||||
const actionsStringField = ref('');
 | 
			
		||||
const dropdownActionsStringField = ref('');
 | 
			
		||||
 | 
			
		||||
function isIfShow(action: ActionItem): boolean {
 | 
			
		||||
  const ifShow = action.ifShow;
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +65,7 @@ function isIfShow(action: ActionItem): boolean {
 | 
			
		|||
  return isIfShow;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 处理actions的纯函数 */
 | 
			
		||||
/** 处理 actions 的纯函数 */
 | 
			
		||||
function processActions(actions: ActionItem[]): any[] {
 | 
			
		||||
  return actions
 | 
			
		||||
    .filter((action: ActionItem) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +84,7 @@ function processActions(actions: ActionItem[]): any[] {
 | 
			
		|||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 处理下拉菜单actions的纯函数 */
 | 
			
		||||
/** 处理下拉菜单 actions 的纯函数 */
 | 
			
		||||
function processDropdownActions(
 | 
			
		||||
  dropDownActions: ActionItem[],
 | 
			
		||||
  divider: boolean,
 | 
			
		||||
| 
						 | 
				
			
			@ -108,10 +108,10 @@ function processDropdownActions(
 | 
			
		|||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 监听actions变化并更新缓存 */
 | 
			
		||||
/** 监听 actions 变化并更新缓存 */
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
  const rawActions = toRaw(props.actions) || [];
 | 
			
		||||
  const currentStringified = JSON.stringify(
 | 
			
		||||
  const currentStringField = JSON.stringify(
 | 
			
		||||
    rawActions.map((a) => ({
 | 
			
		||||
      ...a,
 | 
			
		||||
      onClick: undefined, // 排除函数以便比较
 | 
			
		||||
| 
						 | 
				
			
			@ -121,16 +121,16 @@ watchEffect(() => {
 | 
			
		|||
    })),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (currentStringified !== actionsStringified.value) {
 | 
			
		||||
    actionsStringified.value = currentStringified;
 | 
			
		||||
  if (currentStringField !== actionsStringField.value) {
 | 
			
		||||
    actionsStringField.value = currentStringField;
 | 
			
		||||
    processedActions.value = processActions(rawActions);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/** 监听dropDownActions变化并更新缓存 */
 | 
			
		||||
/** 监听 dropDownActions 变化并更新缓存 */
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
  const rawDropDownActions = toRaw(props.dropDownActions) || [];
 | 
			
		||||
  const currentStringified = JSON.stringify({
 | 
			
		||||
  const currentStringField = JSON.stringify({
 | 
			
		||||
    actions: rawDropDownActions.map((a) => ({
 | 
			
		||||
      ...a,
 | 
			
		||||
      onClick: undefined, // 排除函数以便比较
 | 
			
		||||
| 
						 | 
				
			
			@ -141,8 +141,8 @@ watchEffect(() => {
 | 
			
		|||
    divider: props.divider,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (currentStringified !== dropdownActionsStringified.value) {
 | 
			
		||||
    dropdownActionsStringified.value = currentStringified;
 | 
			
		||||
  if (currentStringField !== dropdownActionsStringField.value) {
 | 
			
		||||
    dropdownActionsStringField.value = currentStringField;
 | 
			
		||||
    processedDropdownActions.value = processDropdownActions(
 | 
			
		||||
      rawDropDownActions,
 | 
			
		||||
      props.divider,
 | 
			
		||||
| 
						 | 
				
			
			@ -154,14 +154,14 @@ const getActions = computed(() => processedActions.value);
 | 
			
		|||
 | 
			
		||||
const getDropdownList = computed(() => processedDropdownActions.value);
 | 
			
		||||
 | 
			
		||||
/** 缓存Space组件的size计算结果 */
 | 
			
		||||
/** 缓存 Space 组件的 size 计算结果 */
 | 
			
		||||
const spaceSize = computed(() => {
 | 
			
		||||
  return unref(getActions)?.some((item: ActionItem) => item.type === 'link')
 | 
			
		||||
    ? 0
 | 
			
		||||
    : 8;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/** 缓存PopConfirm属性 */
 | 
			
		||||
/** 缓存 PopConfirm 属性 */
 | 
			
		||||
const popConfirmPropsMap = new Map<string, any>();
 | 
			
		||||
 | 
			
		||||
function getPopConfirmProps(attrs: PopConfirm) {
 | 
			
		||||
| 
						 | 
				
			
			@ -191,7 +191,7 @@ function getPopConfirmProps(attrs: PopConfirm) {
 | 
			
		|||
  return originAttrs;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 缓存Button属性 */
 | 
			
		||||
/** 缓存 Button 属性 */
 | 
			
		||||
const buttonPropsMap = new Map<string, any>();
 | 
			
		||||
 | 
			
		||||
function getButtonProps(action: ActionItem) {
 | 
			
		||||
| 
						 | 
				
			
			@ -217,7 +217,7 @@ function getButtonProps(action: ActionItem) {
 | 
			
		|||
  return res;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 缓存Tooltip属性 */
 | 
			
		||||
/** 缓存 Tooltip 属性 */
 | 
			
		||||
const tooltipPropsMap = new Map<string, any>();
 | 
			
		||||
 | 
			
		||||
function getTooltipProps(tooltip: any | string) {
 | 
			
		||||
| 
						 | 
				
			
			@ -243,7 +243,7 @@ function handleMenuClick(e: any) {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 生成稳定的key */
 | 
			
		||||
/** 生成稳定的 key */
 | 
			
		||||
function getActionKey(action: ActionItem, index: number) {
 | 
			
		||||
  return `${action.label || ''}-${action.type || ''}-${index}`;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,7 @@ defineExpose({
 | 
			
		|||
    >
 | 
			
		||||
      <Textarea
 | 
			
		||||
        v-model:value="formData.desc"
 | 
			
		||||
        :autosize="{ minRows: 6, maxRows: 6 }"
 | 
			
		||||
        :auto-size="{ minRows: 6, maxRows: 6 }"
 | 
			
		||||
        :maxlength="1200"
 | 
			
		||||
        :show-count="true"
 | 
			
		||||
        placeholder="一首关于糟糕分手的欢快歌曲"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ defineExpose({
 | 
			
		|||
    <Title title="歌词" desc="自己编写歌词或使用Ai生成歌词,两节/8行效果最佳">
 | 
			
		||||
      <Textarea
 | 
			
		||||
        v-model:value="formData.lyric"
 | 
			
		||||
        :autosize="{ minRows: 6, maxRows: 6 }"
 | 
			
		||||
        :auto-size="{ minRows: 6, maxRows: 6 }"
 | 
			
		||||
        :maxlength="1200"
 | 
			
		||||
        :show-count="true"
 | 
			
		||||
        placeholder="请输入您自己的歌词"
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +60,7 @@ defineExpose({
 | 
			
		|||
    >
 | 
			
		||||
      <Textarea
 | 
			
		||||
        v-model="formData.style"
 | 
			
		||||
        :autosize="{ minRows: 4, maxRows: 4 }"
 | 
			
		||||
        :auto-size="{ minRows: 4, maxRows: 4 }"
 | 
			
		||||
        :maxlength="256"
 | 
			
		||||
        show-count
 | 
			
		||||
        placeholder="输入音乐风格(英文)"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -100,15 +100,6 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
 | 
			
		|||
      minWidth: 180,
 | 
			
		||||
      slots: { default: 'content' },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'status',
 | 
			
		||||
      title: '绘画状态',
 | 
			
		||||
      minWidth: 100,
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        name: 'CellDict',
 | 
			
		||||
        props: { type: DICT_TYPE.AI_IMAGE_STATUS },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'duration',
 | 
			
		||||
      title: '时长(秒)',
 | 
			
		||||
| 
						 | 
				
			
			@ -139,9 +130,12 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
 | 
			
		|||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'tags',
 | 
			
		||||
      title: '风格标签',
 | 
			
		||||
      minWidth: 180,
 | 
			
		||||
      slots: { default: 'tags' },
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        name: 'CellTags',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      minWidth: 100,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ import { onMounted, ref } from 'vue';
 | 
			
		|||
 | 
			
		||||
import { confirm, DocAlert, Page } from '@vben/common-ui';
 | 
			
		||||
 | 
			
		||||
import { Button, message, Switch, Tag } from 'ant-design-vue';
 | 
			
		||||
import { Button, message, Switch } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import { deleteMusic, getMusicPage, updateMusic } from '#/api/ai/music';
 | 
			
		||||
| 
						 | 
				
			
			@ -101,9 +101,9 @@ onMounted(async () => {
 | 
			
		|||
      </template>
 | 
			
		||||
 | 
			
		||||
      <template #userId="{ row }">
 | 
			
		||||
        <span>{{
 | 
			
		||||
          userList.find((item) => item.id === row.userId)?.nickname
 | 
			
		||||
        }}</span>
 | 
			
		||||
        <span>
 | 
			
		||||
          {{ userList.find((item) => item.id === row.userId)?.nickname }}
 | 
			
		||||
        </span>
 | 
			
		||||
      </template>
 | 
			
		||||
      <template #content="{ row }">
 | 
			
		||||
        <Button
 | 
			
		||||
| 
						 | 
				
			
			@ -141,11 +141,6 @@ onMounted(async () => {
 | 
			
		|||
          :disabled="row.status !== AiMusicStatusEnum.SUCCESS"
 | 
			
		||||
        />
 | 
			
		||||
      </template>
 | 
			
		||||
      <template #tags="{ row }">
 | 
			
		||||
        <Tag v-for="tag in row.tags" :key="tag" class="ml-1">
 | 
			
		||||
          {{ tag }}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      </template>
 | 
			
		||||
      <template #actions="{ row }">
 | 
			
		||||
        <TableAction
 | 
			
		||||
          :actions="[
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -98,7 +98,7 @@ watch(copied, (val) => {
 | 
			
		|||
        <Textarea
 | 
			
		||||
          id="inputId"
 | 
			
		||||
          v-model:value="compContent"
 | 
			
		||||
          autosize
 | 
			
		||||
          auto-size
 | 
			
		||||
          :bordered="false"
 | 
			
		||||
          placeholder="生成的内容……"
 | 
			
		||||
        />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ const props = defineProps<{
 | 
			
		|||
  type: 'copy' | 'create' | 'edit';
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
// 流程表单详情
 | 
			
		||||
/** 流程表单详情 */
 | 
			
		||||
const flowFormConfig = ref();
 | 
			
		||||
 | 
			
		||||
const [FormModal, formModalApi] = useVbenModal({
 | 
			
		||||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ const [FormModal, formModalApi] = useVbenModal({
 | 
			
		|||
 | 
			
		||||
const designerRef = ref<InstanceType<typeof FcDesigner>>();
 | 
			
		||||
 | 
			
		||||
// 表单设计器配置
 | 
			
		||||
/** 表单设计器配置 */
 | 
			
		||||
const designerConfig = ref({
 | 
			
		||||
  switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
 | 
			
		||||
  autoActive: true, // 是否自动选中拖入的组件
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +80,7 @@ const currentFormId = computed(() => {
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
// 加载表单配置
 | 
			
		||||
async function loadFormConfig(id: number | string) {
 | 
			
		||||
async function loadFormConfig(id: number) {
 | 
			
		||||
  try {
 | 
			
		||||
    const formDetail = await getFormDetail(id);
 | 
			
		||||
    flowFormConfig.value = formDetail;
 | 
			
		||||
| 
						 | 
				
			
			@ -106,8 +106,7 @@ async function initializeDesigner() {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO @ziye:注释使用 /** */ 风格,高亮更明显哈,方法注释;
 | 
			
		||||
// 保存表单
 | 
			
		||||
/** 保存表单 */
 | 
			
		||||
function handleSave() {
 | 
			
		||||
  formModalApi
 | 
			
		||||
    .setData({
 | 
			
		||||
| 
						 | 
				
			
			@ -118,7 +117,7 @@ function handleSave() {
 | 
			
		|||
    .open();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 返回列表页
 | 
			
		||||
/** 返回列表页 */
 | 
			
		||||
function onBack() {
 | 
			
		||||
  router.push({
 | 
			
		||||
    path: '/bpm/manager/form',
 | 
			
		||||
| 
						 | 
				
			
			@ -137,7 +136,11 @@ onMounted(() => {
 | 
			
		|||
  <Page auto-content-height>
 | 
			
		||||
    <FormModal @success="onBack" />
 | 
			
		||||
 | 
			
		||||
    <FcDesigner class="my-designer" ref="designerRef" :config="designerConfig">
 | 
			
		||||
    <FcDesigner
 | 
			
		||||
      class="h-full min-h-[500px]"
 | 
			
		||||
      ref="designerRef"
 | 
			
		||||
      :config="designerConfig"
 | 
			
		||||
    >
 | 
			
		||||
      <template #handle>
 | 
			
		||||
        <Button size="small" type="primary" @click="handleSave">
 | 
			
		||||
          <IconifyIcon icon="mdi:content-save" />
 | 
			
		||||
| 
						 | 
				
			
			@ -147,10 +150,3 @@ onMounted(() => {
 | 
			
		|||
    </FcDesigner>
 | 
			
		||||
  </Page>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.my-designer {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  min-height: 500px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,13 @@ export function useGridColumns(): VxeTableGridOptions<BpmProcessDefinitionApi.Pr
 | 
			
		|||
      field: 'icon',
 | 
			
		||||
      title: '流程图标',
 | 
			
		||||
      minWidth: 100,
 | 
			
		||||
      slots: { default: 'icon' },
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        name: 'CellImage',
 | 
			
		||||
        props: {
 | 
			
		||||
          width: 24,
 | 
			
		||||
          height: 24,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'startUsers',
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +53,9 @@ export function useGridColumns(): VxeTableGridOptions<BpmProcessDefinitionApi.Pr
 | 
			
		|||
      field: 'version',
 | 
			
		||||
      title: '流程版本',
 | 
			
		||||
      minWidth: 80,
 | 
			
		||||
      slots: { default: 'version' },
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        name: 'CellTag',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'deploymentTime',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ import { useRoute, useRouter } from 'vue-router';
 | 
			
		|||
 | 
			
		||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
 | 
			
		||||
 | 
			
		||||
import { Button, Image, Tag, Tooltip } from 'ant-design-vue';
 | 
			
		||||
import { Button, Tooltip } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import { getProcessDefinitionPage } from '#/api/bpm/definition';
 | 
			
		||||
| 
						 | 
				
			
			@ -93,16 +93,6 @@ onMounted(() => {
 | 
			
		|||
      <DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
 | 
			
		||||
    </template>
 | 
			
		||||
    <Grid table-title="流程定义列表">
 | 
			
		||||
      <template #icon="{ row }">
 | 
			
		||||
        <Image
 | 
			
		||||
          v-if="row.icon"
 | 
			
		||||
          :src="row.icon"
 | 
			
		||||
          :width="24"
 | 
			
		||||
          :height="24"
 | 
			
		||||
          class="rounded"
 | 
			
		||||
        />
 | 
			
		||||
        <span v-else> 无图标 </span>
 | 
			
		||||
      </template>
 | 
			
		||||
      <template #startUsers="{ row }">
 | 
			
		||||
        <template v-if="!row.startUsers?.length">全部可见</template>
 | 
			
		||||
        <template v-else-if="row.startUsers.length === 1">
 | 
			
		||||
| 
						 | 
				
			
			@ -135,9 +125,6 @@ onMounted(() => {
 | 
			
		|||
        </Button>
 | 
			
		||||
        <span v-else>暂无表单</span>
 | 
			
		||||
      </template>
 | 
			
		||||
      <template #version="{ row }">
 | 
			
		||||
        <Tag>v{{ row.version }}</Tag>
 | 
			
		||||
      </template>
 | 
			
		||||
      <template #actions="{ row }">
 | 
			
		||||
        <TableAction
 | 
			
		||||
          :actions="[
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ import type { DescriptionItemSchema } from '#/components/description';
 | 
			
		|||
 | 
			
		||||
import { h } from 'vue';
 | 
			
		||||
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
import { formatDateTime } from '@vben/utils';
 | 
			
		||||
 | 
			
		||||
import { DictTag } from '#/components/dict-tag';
 | 
			
		||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
 | 
			
		||||
| 
						 | 
				
			
			@ -186,12 +186,12 @@ export function useDetailFormSchema(): DescriptionItemSchema[] {
 | 
			
		|||
    {
 | 
			
		||||
      label: '开始时间',
 | 
			
		||||
      field: 'startTime',
 | 
			
		||||
      content: (data) => dayjs(data?.startTime).format('YYYY-MM-DD HH:mm:ss'),
 | 
			
		||||
      content: (data) => formatDateTime(data?.startTime) as string,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: '结束时间',
 | 
			
		||||
      field: 'endTime',
 | 
			
		||||
      content: (data) => dayjs(data?.endTime).format('YYYY-MM-DD HH:mm:ss'),
 | 
			
		||||
      content: (data) => formatDateTime(data?.endTime) as string,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: '原因',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -346,7 +346,12 @@ onMounted(async () => {
 | 
			
		|||
              </Row>
 | 
			
		||||
            </TabPane>
 | 
			
		||||
 | 
			
		||||
            <TabPane tab="流程图" key="diagram" class="tab-pane-content">
 | 
			
		||||
            <TabPane
 | 
			
		||||
              tab="流程图"
 | 
			
		||||
              key="diagram"
 | 
			
		||||
              class="tab-pane-content"
 | 
			
		||||
              :force-render="true"
 | 
			
		||||
            >
 | 
			
		||||
              <div class="h-full">
 | 
			
		||||
                <ProcessInstanceSimpleViewer
 | 
			
		||||
                  v-show="
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,180 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
defineOptions({ name: 'ProcessInstanceSimpleViewer' });
 | 
			
		||||
</script>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import type { SimpleFlowNode } from '#/components/simple-process-design';
 | 
			
		||||
 | 
			
		||||
import { ref, watch } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { SimpleProcessViewer } from '#/components/simple-process-design';
 | 
			
		||||
import { BpmNodeTypeEnum, BpmTaskStatusEnum } from '#/utils';
 | 
			
		||||
 | 
			
		||||
defineOptions({ name: 'BpmProcessInstanceSimpleViewer' });
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(
 | 
			
		||||
  defineProps<{
 | 
			
		||||
    loading?: boolean; // 是否加载中
 | 
			
		||||
    modelView?: any;
 | 
			
		||||
    simpleJson?: string; // Simple 模型结构数据 (json 格式)
 | 
			
		||||
  }>(),
 | 
			
		||||
  {
 | 
			
		||||
    loading: false,
 | 
			
		||||
    modelView: () => ({}),
 | 
			
		||||
    simpleJson: '',
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const simpleModel = ref<any>({});
 | 
			
		||||
// 用户任务
 | 
			
		||||
const tasks = ref([]);
 | 
			
		||||
// 流程实例
 | 
			
		||||
const processInstance = ref();
 | 
			
		||||
 | 
			
		||||
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.modelView,
 | 
			
		||||
  async (newModelView) => {
 | 
			
		||||
    if (newModelView) {
 | 
			
		||||
      tasks.value = newModelView.tasks;
 | 
			
		||||
      processInstance.value = newModelView.processInstance;
 | 
			
		||||
      // 已经拒绝的活动节点编号集合,只包括 UserTask
 | 
			
		||||
      const rejectedTaskActivityIds: string[] =
 | 
			
		||||
        newModelView.rejectedTaskActivityIds;
 | 
			
		||||
      // 进行中的活动节点编号集合, 只包括 UserTask
 | 
			
		||||
      const unfinishedTaskActivityIds: string[] =
 | 
			
		||||
        newModelView.unfinishedTaskActivityIds;
 | 
			
		||||
      // 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
 | 
			
		||||
      const finishedActivityIds: string[] =
 | 
			
		||||
        newModelView.finishedTaskActivityIds;
 | 
			
		||||
      // 已经完成的连线节点编号集合,只包括 SequenceFlow
 | 
			
		||||
      const finishedSequenceFlowActivityIds: string[] =
 | 
			
		||||
        newModelView.finishedSequenceFlowActivityIds;
 | 
			
		||||
      setSimpleModelNodeTaskStatus(
 | 
			
		||||
        newModelView.simpleModel,
 | 
			
		||||
        newModelView.processInstance?.status,
 | 
			
		||||
        rejectedTaskActivityIds,
 | 
			
		||||
        unfinishedTaskActivityIds,
 | 
			
		||||
        finishedActivityIds,
 | 
			
		||||
        finishedSequenceFlowActivityIds,
 | 
			
		||||
      );
 | 
			
		||||
      simpleModel.value = newModelView.simpleModel || {};
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
/** 监控模型结构数据 */
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.simpleJson,
 | 
			
		||||
  async (value) => {
 | 
			
		||||
    if (value) {
 | 
			
		||||
      simpleModel.value = JSON.parse(value);
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
const setSimpleModelNodeTaskStatus = (
 | 
			
		||||
  simpleModel: SimpleFlowNode | undefined,
 | 
			
		||||
  processStatus: number,
 | 
			
		||||
  rejectedTaskActivityIds: string[],
 | 
			
		||||
  unfinishedTaskActivityIds: string[],
 | 
			
		||||
  finishedActivityIds: string[],
 | 
			
		||||
  finishedSequenceFlowActivityIds: string[],
 | 
			
		||||
) => {
 | 
			
		||||
  if (!simpleModel) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // 结束节点
 | 
			
		||||
  if (simpleModel.type === BpmNodeTypeEnum.END_EVENT_NODE) {
 | 
			
		||||
    simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
 | 
			
		||||
      ? processStatus
 | 
			
		||||
      : BpmTaskStatusEnum.NOT_START;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // 审批节点
 | 
			
		||||
  if (
 | 
			
		||||
    simpleModel.type === BpmNodeTypeEnum.START_USER_NODE ||
 | 
			
		||||
    simpleModel.type === BpmNodeTypeEnum.USER_TASK_NODE ||
 | 
			
		||||
    simpleModel.type === BpmNodeTypeEnum.TRANSACTOR_NODE ||
 | 
			
		||||
    simpleModel.type === BpmNodeTypeEnum.CHILD_PROCESS_NODE
 | 
			
		||||
  ) {
 | 
			
		||||
    simpleModel.activityStatus = BpmTaskStatusEnum.NOT_START;
 | 
			
		||||
    if (rejectedTaskActivityIds.includes(simpleModel.id)) {
 | 
			
		||||
      simpleModel.activityStatus = BpmTaskStatusEnum.REJECT;
 | 
			
		||||
    } else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
 | 
			
		||||
      simpleModel.activityStatus = BpmTaskStatusEnum.RUNNING;
 | 
			
		||||
    } else if (finishedActivityIds.includes(simpleModel.id)) {
 | 
			
		||||
      simpleModel.activityStatus = BpmTaskStatusEnum.APPROVE;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO 是不是还缺一个 cancel 的状态
 | 
			
		||||
  }
 | 
			
		||||
  // 抄送节点
 | 
			
		||||
  if (simpleModel.type === BpmNodeTypeEnum.COPY_TASK_NODE) {
 | 
			
		||||
    // 抄送节点,只有通过和未执行状态
 | 
			
		||||
    simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
 | 
			
		||||
      ? BpmTaskStatusEnum.APPROVE
 | 
			
		||||
      : BpmTaskStatusEnum.NOT_START;
 | 
			
		||||
  }
 | 
			
		||||
  // 延迟器节点
 | 
			
		||||
  if (simpleModel.type === BpmNodeTypeEnum.DELAY_TIMER_NODE) {
 | 
			
		||||
    // 延迟器节点,只有通过和未执行状态
 | 
			
		||||
    simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
 | 
			
		||||
      ? BpmTaskStatusEnum.APPROVE
 | 
			
		||||
      : BpmTaskStatusEnum.NOT_START;
 | 
			
		||||
  }
 | 
			
		||||
  // 触发器节点
 | 
			
		||||
  if (simpleModel.type === BpmNodeTypeEnum.TRIGGER_NODE) {
 | 
			
		||||
    // 触发器节点,只有通过和未执行状态
 | 
			
		||||
    simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
 | 
			
		||||
      ? BpmTaskStatusEnum.APPROVE
 | 
			
		||||
      : BpmTaskStatusEnum.NOT_START;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 条件节点对应 SequenceFlow
 | 
			
		||||
  if (simpleModel.type === BpmNodeTypeEnum.CONDITION_NODE) {
 | 
			
		||||
    // 条件节点,只有通过和未执行状态
 | 
			
		||||
    simpleModel.activityStatus = finishedSequenceFlowActivityIds.includes(
 | 
			
		||||
      simpleModel.id,
 | 
			
		||||
    )
 | 
			
		||||
      ? BpmTaskStatusEnum.APPROVE
 | 
			
		||||
      : BpmTaskStatusEnum.NOT_START;
 | 
			
		||||
  }
 | 
			
		||||
  // 网关节点
 | 
			
		||||
  if (
 | 
			
		||||
    simpleModel.type === BpmNodeTypeEnum.CONDITION_BRANCH_NODE ||
 | 
			
		||||
    simpleModel.type === BpmNodeTypeEnum.PARALLEL_BRANCH_NODE ||
 | 
			
		||||
    simpleModel.type === BpmNodeTypeEnum.INCLUSIVE_BRANCH_NODE ||
 | 
			
		||||
    simpleModel.type === BpmNodeTypeEnum.ROUTER_BRANCH_NODE
 | 
			
		||||
  ) {
 | 
			
		||||
    // 网关节点。只有通过和未执行状态
 | 
			
		||||
    simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
 | 
			
		||||
      ? BpmTaskStatusEnum.APPROVE
 | 
			
		||||
      : BpmTaskStatusEnum.NOT_START;
 | 
			
		||||
    simpleModel.conditionNodes?.forEach((node) => {
 | 
			
		||||
      setSimpleModelNodeTaskStatus(
 | 
			
		||||
        node,
 | 
			
		||||
        processStatus,
 | 
			
		||||
        rejectedTaskActivityIds,
 | 
			
		||||
        unfinishedTaskActivityIds,
 | 
			
		||||
        finishedActivityIds,
 | 
			
		||||
        finishedSequenceFlowActivityIds,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setSimpleModelNodeTaskStatus(
 | 
			
		||||
    simpleModel.childNode,
 | 
			
		||||
    processStatus,
 | 
			
		||||
    rejectedTaskActivityIds,
 | 
			
		||||
    unfinishedTaskActivityIds,
 | 
			
		||||
    finishedActivityIds,
 | 
			
		||||
    finishedSequenceFlowActivityIds,
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <h1>Simple BPM Viewer</h1>
 | 
			
		||||
  <div v-loading="loading">
 | 
			
		||||
    <SimpleProcessViewer
 | 
			
		||||
      :flow-node="simpleModel"
 | 
			
		||||
      :tasks="tasks"
 | 
			
		||||
      :process-instance="processInstance"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,10 +2,10 @@
 | 
			
		|||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
 | 
			
		||||
 | 
			
		||||
import { h, nextTick, onMounted, ref } from 'vue';
 | 
			
		||||
import { nextTick, onMounted, ref } from 'vue';
 | 
			
		||||
import { useRoute, useRouter } from 'vue-router';
 | 
			
		||||
 | 
			
		||||
import { confirm, Page } from '@vben/common-ui';
 | 
			
		||||
import { Page, prompt } from '@vben/common-ui';
 | 
			
		||||
 | 
			
		||||
import { Input, message } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +29,6 @@ const processDefinitionId = query.processDefinitionId as string;
 | 
			
		|||
const formFields = ref<any[]>([]);
 | 
			
		||||
const userList = ref<any[]>([]); // 用户列表
 | 
			
		||||
const gridReady = ref(false); // 表格是否准备好
 | 
			
		||||
const cancelReason = ref(''); // 取消原因
 | 
			
		||||
 | 
			
		||||
// 表格的列需要解析表单字段,这里定义成变量,解析表单字段后再渲染
 | 
			
		||||
let Grid: any = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -81,26 +80,19 @@ const handleDetail = (row: BpmProcessInstanceApi.ProcessInstance) => {
 | 
			
		|||
 | 
			
		||||
/** 取消按钮操作 */
 | 
			
		||||
const handleCancel = async (row: BpmProcessInstanceApi.ProcessInstance) => {
 | 
			
		||||
  cancelReason.value = ''; // 重置取消原因
 | 
			
		||||
  confirm({
 | 
			
		||||
  prompt({
 | 
			
		||||
    content: '请输入取消原因:',
 | 
			
		||||
    title: '取消流程',
 | 
			
		||||
    content: h('div', [
 | 
			
		||||
      h('p', '请输入取消原因:'),
 | 
			
		||||
      h(Input, {
 | 
			
		||||
        value: cancelReason.value,
 | 
			
		||||
        'onUpdate:value': (val: string) => {
 | 
			
		||||
          cancelReason.value = val;
 | 
			
		||||
        },
 | 
			
		||||
        placeholder: '请输入取消原因',
 | 
			
		||||
      }),
 | 
			
		||||
    ]),
 | 
			
		||||
    beforeClose: async ({ isConfirm }) => {
 | 
			
		||||
      if (!isConfirm) return;
 | 
			
		||||
      if (!cancelReason.value.trim()) {
 | 
			
		||||
    icon: 'question',
 | 
			
		||||
    component: Input,
 | 
			
		||||
    modelPropName: 'value',
 | 
			
		||||
    async beforeClose(scope) {
 | 
			
		||||
      if (!scope.isConfirm) return;
 | 
			
		||||
      if (!scope.value) {
 | 
			
		||||
        message.warning('请输入取消原因');
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      await cancelProcessInstanceByAdmin(row.id, cancelReason.value);
 | 
			
		||||
      await cancelProcessInstanceByAdmin(row.id, scope.value);
 | 
			
		||||
      return true;
 | 
			
		||||
    },
 | 
			
		||||
  }).then(() => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,529 @@
 | 
			
		|||
import { DICT_TYPE, getDictLabel } from '#/utils';
 | 
			
		||||
 | 
			
		||||
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 {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,396 @@
 | 
			
		|||
import type { VbenFormSchema } from '#/adapter/form';
 | 
			
		||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
 | 
			
		||||
import { useUserStore } from '@vben/stores';
 | 
			
		||||
import {
 | 
			
		||||
  beginOfDay,
 | 
			
		||||
  endOfDay,
 | 
			
		||||
  erpCalculatePercentage,
 | 
			
		||||
  formatDateTime,
 | 
			
		||||
  handleTree,
 | 
			
		||||
} from '@vben/utils';
 | 
			
		||||
 | 
			
		||||
import { getSimpleDeptList } from '#/api/system/dept';
 | 
			
		||||
import { getSimpleUserList } from '#/api/system/user';
 | 
			
		||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
 | 
			
		||||
 | 
			
		||||
const userStore = useUserStore();
 | 
			
		||||
 | 
			
		||||
export const customerSummaryTabs = [
 | 
			
		||||
  {
 | 
			
		||||
    tab: '客户总量分析',
 | 
			
		||||
    key: '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: [
 | 
			
		||||
        formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
 | 
			
		||||
        formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
 | 
			
		||||
      ] as [Date, Date],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      fieldName: 'interval',
 | 
			
		||||
      label: '时间间隔',
 | 
			
		||||
      component: 'Select',
 | 
			
		||||
      componentProps: {
 | 
			
		||||
        allowClear: true,
 | 
			
		||||
        options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
 | 
			
		||||
      },
 | 
			
		||||
      defaultValue: 2,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      fieldName: 'deptId',
 | 
			
		||||
      label: '归属部门',
 | 
			
		||||
      component: 'ApiTreeSelect',
 | 
			
		||||
      componentProps: {
 | 
			
		||||
        api: async () => {
 | 
			
		||||
          const data = await getSimpleDeptList();
 | 
			
		||||
          return handleTree(data);
 | 
			
		||||
        },
 | 
			
		||||
        labelField: 'name',
 | 
			
		||||
        valueField: 'id',
 | 
			
		||||
        childrenField: 'children',
 | 
			
		||||
        treeDefaultExpandAll: true,
 | 
			
		||||
      },
 | 
			
		||||
      defaultValue: userStore.userInfo?.deptId,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      fieldName: 'userId',
 | 
			
		||||
      label: '员工',
 | 
			
		||||
      component: 'ApiSelect',
 | 
			
		||||
      componentProps: {
 | 
			
		||||
        api: getSimpleUserList,
 | 
			
		||||
        allowClear: true,
 | 
			
		||||
        labelField: 'nickname',
 | 
			
		||||
        valueField: 'id',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 列表的字段 */
 | 
			
		||||
export function useGridColumns(
 | 
			
		||||
  activeTabName: any,
 | 
			
		||||
): VxeTableGridOptions['columns'] {
 | 
			
		||||
  switch (activeTabName) {
 | 
			
		||||
    case '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 [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +1,79 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { Page } from '@vben/common-ui';
 | 
			
		||||
import type { EchartsUIType } from '@vben/plugins/echarts';
 | 
			
		||||
 | 
			
		||||
import { Button } from 'ant-design-vue';
 | 
			
		||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
import type { CrmStatisticsCustomerApi } from '#/api/crm/statistics/customer';
 | 
			
		||||
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { Page } from '@vben/common-ui';
 | 
			
		||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 | 
			
		||||
 | 
			
		||||
import { Tabs } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import { getChartDatas, getDatas } from '#/api/crm/statistics/customer';
 | 
			
		||||
 | 
			
		||||
import { getChartOptions } from './chartOptions';
 | 
			
		||||
import { customerSummaryTabs, useGridColumns, useGridFormSchema } from './data';
 | 
			
		||||
 | 
			
		||||
const activeTabName = ref('customerSummary');
 | 
			
		||||
const chartRef = ref<EchartsUIType>();
 | 
			
		||||
const { renderEcharts } = useEcharts(chartRef);
 | 
			
		||||
 | 
			
		||||
const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		||||
  formOptions: {
 | 
			
		||||
    schema: useGridFormSchema(),
 | 
			
		||||
  },
 | 
			
		||||
  gridOptions: {
 | 
			
		||||
    columns: useGridColumns(activeTabName.value),
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
    keepSource: true,
 | 
			
		||||
    pagerConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
    proxyConfig: {
 | 
			
		||||
      ajax: {
 | 
			
		||||
        query: async (_, formValues) => {
 | 
			
		||||
          const res = await getChartDatas(activeTabName.value, formValues);
 | 
			
		||||
          renderEcharts(getChartOptions(activeTabName.value, res));
 | 
			
		||||
          return await getDatas(activeTabName.value, formValues);
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    rowConfig: {
 | 
			
		||||
      keyField: 'id',
 | 
			
		||||
      isHover: true,
 | 
			
		||||
    },
 | 
			
		||||
    toolbarConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
  } as VxeTableGridOptions<CrmStatisticsCustomerApi.CustomerSummaryByUser>,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function handleTabChange(key: any) {
 | 
			
		||||
  activeTabName.value = key;
 | 
			
		||||
  gridApi.setGridOptions({
 | 
			
		||||
    columns: useGridColumns(key),
 | 
			
		||||
  });
 | 
			
		||||
  gridApi.reload();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Page>
 | 
			
		||||
    <Button
 | 
			
		||||
      danger
 | 
			
		||||
      type="link"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/yudaocode/yudao-ui-admin-vue3"
 | 
			
		||||
    >
 | 
			
		||||
      该功能支持 Vue3 + element-plus 版本!
 | 
			
		||||
    </Button>
 | 
			
		||||
    <br />
 | 
			
		||||
    <Button
 | 
			
		||||
      type="link"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/customer/index.vue"
 | 
			
		||||
    >
 | 
			
		||||
      可参考
 | 
			
		||||
      https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/customer/index.vue
 | 
			
		||||
      代码,pull request 贡献给我们!
 | 
			
		||||
    </Button>
 | 
			
		||||
  <Page auto-content-height>
 | 
			
		||||
    <Grid>
 | 
			
		||||
      <template #top>
 | 
			
		||||
        <Tabs v-model:active-key="activeTabName" @change="handleTabChange">
 | 
			
		||||
          <Tabs.TabPane
 | 
			
		||||
            v-for="item in customerSummaryTabs"
 | 
			
		||||
            :key="item.key"
 | 
			
		||||
            :tab="item.tab"
 | 
			
		||||
            :force-render="true"
 | 
			
		||||
          />
 | 
			
		||||
        </Tabs>
 | 
			
		||||
        <EchartsUI class="mb-20 h-full w-full" ref="chartRef" />
 | 
			
		||||
      </template>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  </Page>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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} <br/>{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 {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,266 @@
 | 
			
		|||
import type { VbenFormSchema } from '#/adapter/form';
 | 
			
		||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
 | 
			
		||||
import { useUserStore } from '@vben/stores';
 | 
			
		||||
import { beginOfDay, endOfDay, formatDateTime, handleTree } from '@vben/utils';
 | 
			
		||||
 | 
			
		||||
import { getSimpleDeptList } from '#/api/system/dept';
 | 
			
		||||
import { getSimpleUserList } from '#/api/system/user';
 | 
			
		||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
 | 
			
		||||
 | 
			
		||||
const userStore = useUserStore();
 | 
			
		||||
 | 
			
		||||
export const customerSummaryTabs = [
 | 
			
		||||
  {
 | 
			
		||||
    tab: '销售漏斗分析',
 | 
			
		||||
    key: 'funnel',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    tab: '新增商机分析',
 | 
			
		||||
    key: 'businessSummary',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    tab: '商机转化率分析',
 | 
			
		||||
    key: 'businessInversionRateSummary',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
/** 列表的搜索表单 */
 | 
			
		||||
export function useGridFormSchema(): VbenFormSchema[] {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      fieldName: 'times',
 | 
			
		||||
      label: '时间范围',
 | 
			
		||||
      component: 'RangePicker',
 | 
			
		||||
      componentProps: {
 | 
			
		||||
        ...getRangePickerDefaultProps(),
 | 
			
		||||
      },
 | 
			
		||||
      defaultValue: [
 | 
			
		||||
        formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
 | 
			
		||||
        formatDateTime(endOfDay(new Date(Date.now() - 3600 * 1000 * 24))),
 | 
			
		||||
      ] as [Date, Date],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      fieldName: 'interval',
 | 
			
		||||
      label: '时间间隔',
 | 
			
		||||
      component: 'Select',
 | 
			
		||||
      componentProps: {
 | 
			
		||||
        allowClear: true,
 | 
			
		||||
        options: getDictOptions(DICT_TYPE.DATE_INTERVAL, 'number'),
 | 
			
		||||
      },
 | 
			
		||||
      defaultValue: 2,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      fieldName: 'deptId',
 | 
			
		||||
      label: '归属部门',
 | 
			
		||||
      component: 'ApiTreeSelect',
 | 
			
		||||
      componentProps: {
 | 
			
		||||
        api: async () => {
 | 
			
		||||
          const data = await getSimpleDeptList();
 | 
			
		||||
          return handleTree(data);
 | 
			
		||||
        },
 | 
			
		||||
        labelField: 'name',
 | 
			
		||||
        valueField: 'id',
 | 
			
		||||
        childrenField: 'children',
 | 
			
		||||
        treeDefaultExpandAll: true,
 | 
			
		||||
      },
 | 
			
		||||
      defaultValue: userStore.userInfo?.deptId,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      fieldName: 'userId',
 | 
			
		||||
      label: '员工',
 | 
			
		||||
      component: 'ApiSelect',
 | 
			
		||||
      componentProps: {
 | 
			
		||||
        api: getSimpleUserList,
 | 
			
		||||
        allowClear: true,
 | 
			
		||||
        labelField: 'nickname',
 | 
			
		||||
        valueField: 'id',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 列表的字段 */
 | 
			
		||||
export function useGridColumns(
 | 
			
		||||
  activeTabName: any,
 | 
			
		||||
): VxeTableGridOptions['columns'] {
 | 
			
		||||
  switch (activeTabName) {
 | 
			
		||||
    case 'businessInversionRateSummary': {
 | 
			
		||||
      return [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'seq',
 | 
			
		||||
          title: '序号',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'name',
 | 
			
		||||
          title: '商机名称',
 | 
			
		||||
          minWidth: 100,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'customerName',
 | 
			
		||||
          title: '客户名称',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'totalPrice',
 | 
			
		||||
          title: '商机金额(元)',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatAmount2',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'dealTime',
 | 
			
		||||
          title: '预计成交日期',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatDateTime',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'ownerUserName',
 | 
			
		||||
          title: '负责人',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'ownerUserDeptName',
 | 
			
		||||
          title: '所属部门',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'contactLastTime',
 | 
			
		||||
          title: '最后跟进时间',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatDateTime',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'updateTime',
 | 
			
		||||
          title: '更新时间',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatDateTime',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'createTime',
 | 
			
		||||
          title: '创建时间',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatDateTime',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'creatorName',
 | 
			
		||||
          title: '创建人',
 | 
			
		||||
          minWidth: 100,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'statusTypeName',
 | 
			
		||||
          title: '商机状态组',
 | 
			
		||||
          minWidth: 100,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'statusName',
 | 
			
		||||
          title: '商机阶段',
 | 
			
		||||
          minWidth: 100,
 | 
			
		||||
        },
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
    case 'businessSummary': {
 | 
			
		||||
      return [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'seq',
 | 
			
		||||
          title: '序号',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'name',
 | 
			
		||||
          title: '商机名称',
 | 
			
		||||
          minWidth: 100,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'customerName',
 | 
			
		||||
          title: '客户名称',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'totalPrice',
 | 
			
		||||
          title: '商机金额(元)',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatAmount2',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'dealTime',
 | 
			
		||||
          title: '预计成交日期',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatDateTime',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'ownerUserName',
 | 
			
		||||
          title: '负责人',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'ownerUserDeptName',
 | 
			
		||||
          title: '所属部门',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'contactLastTime',
 | 
			
		||||
          title: '最后跟进时间',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatDateTime',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'updateTime',
 | 
			
		||||
          title: '更新时间',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatDateTime',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'createTime',
 | 
			
		||||
          title: '创建时间',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatDateTime',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'creatorName',
 | 
			
		||||
          title: '创建人',
 | 
			
		||||
          minWidth: 100,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'statusTypeName',
 | 
			
		||||
          title: '商机状态组',
 | 
			
		||||
          minWidth: 100,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'statusName',
 | 
			
		||||
          title: '商机阶段',
 | 
			
		||||
          minWidth: 100,
 | 
			
		||||
        },
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
    case 'funnel': {
 | 
			
		||||
      return [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'seq',
 | 
			
		||||
          title: '序号',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'endStatus',
 | 
			
		||||
          title: '阶段',
 | 
			
		||||
          minWidth: 100,
 | 
			
		||||
          cellRender: {
 | 
			
		||||
            name: 'CellDict',
 | 
			
		||||
            props: { type: DICT_TYPE.CRM_BUSINESS_END_STATUS_TYPE },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'businessCount',
 | 
			
		||||
          title: '商机数',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          field: 'totalPrice',
 | 
			
		||||
          title: '商机总金额(元)',
 | 
			
		||||
          minWidth: 200,
 | 
			
		||||
          formatter: 'formatAmount2',
 | 
			
		||||
        },
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +1,117 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { Page } from '@vben/common-ui';
 | 
			
		||||
import type { EchartsUIType } from '@vben/plugins/echarts';
 | 
			
		||||
 | 
			
		||||
import { Button } from 'ant-design-vue';
 | 
			
		||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
import type { CrmStatisticsFunnelApi } from '#/api/crm/statistics/funnel';
 | 
			
		||||
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { Page } from '@vben/common-ui';
 | 
			
		||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 | 
			
		||||
 | 
			
		||||
import { Button, ButtonGroup, Tabs } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import { getChartDatas, getDatas } from '#/api/crm/statistics/funnel';
 | 
			
		||||
 | 
			
		||||
import { getChartOptions } from './chartOptions';
 | 
			
		||||
import { customerSummaryTabs, useGridColumns, useGridFormSchema } from './data';
 | 
			
		||||
 | 
			
		||||
const activeTabName = ref('funnel');
 | 
			
		||||
const chartRef = ref<EchartsUIType>();
 | 
			
		||||
const { renderEcharts } = useEcharts(chartRef);
 | 
			
		||||
 | 
			
		||||
const active = ref(true);
 | 
			
		||||
 | 
			
		||||
const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		||||
  formOptions: {
 | 
			
		||||
    schema: useGridFormSchema(),
 | 
			
		||||
  },
 | 
			
		||||
  gridOptions: {
 | 
			
		||||
    columns: useGridColumns(activeTabName.value),
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
    keepSource: true,
 | 
			
		||||
    pagerConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
    proxyConfig: {
 | 
			
		||||
      ajax: {
 | 
			
		||||
        query: async ({ page }, formValues) => {
 | 
			
		||||
          const res = await getChartDatas(activeTabName.value, formValues);
 | 
			
		||||
          renderEcharts(
 | 
			
		||||
            getChartOptions(activeTabName.value, active.value, res),
 | 
			
		||||
          );
 | 
			
		||||
          return await getDatas(activeTabName.value, {
 | 
			
		||||
            page: page.currentPage,
 | 
			
		||||
            pageSize: page.pageSize,
 | 
			
		||||
            ...formValues,
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    rowConfig: {
 | 
			
		||||
      keyField: 'id',
 | 
			
		||||
      isHover: true,
 | 
			
		||||
    },
 | 
			
		||||
    toolbarConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
  } as VxeTableGridOptions<CrmStatisticsFunnelApi.BusinessSummaryByDate>,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function handleTabChange(key: any) {
 | 
			
		||||
  activeTabName.value = key;
 | 
			
		||||
  gridApi.setGridOptions({
 | 
			
		||||
    columns: useGridColumns(key),
 | 
			
		||||
    pagerConfig: {
 | 
			
		||||
      enabled: activeTabName.value !== 'funnelRef',
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  gridApi.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleActive(value: boolean) {
 | 
			
		||||
  active.value = value;
 | 
			
		||||
  renderEcharts(
 | 
			
		||||
    getChartOptions(
 | 
			
		||||
      activeTabName.value,
 | 
			
		||||
      active.value,
 | 
			
		||||
      gridApi.formApi.getValues(),
 | 
			
		||||
    ),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Page>
 | 
			
		||||
    <Button
 | 
			
		||||
      danger
 | 
			
		||||
      type="link"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/yudaocode/yudao-ui-admin-vue3"
 | 
			
		||||
    >
 | 
			
		||||
      该功能支持 Vue3 + element-plus 版本!
 | 
			
		||||
    </Button>
 | 
			
		||||
    <br />
 | 
			
		||||
    <Button
 | 
			
		||||
      type="link"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/funnel/index"
 | 
			
		||||
    >
 | 
			
		||||
      可参考
 | 
			
		||||
      https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/funnel/index
 | 
			
		||||
      代码,pull request 贡献给我们!
 | 
			
		||||
    </Button>
 | 
			
		||||
  <Page auto-content-height>
 | 
			
		||||
    <Grid>
 | 
			
		||||
      <template #top>
 | 
			
		||||
        <Tabs v-model:active-key="activeTabName" @change="handleTabChange">
 | 
			
		||||
          <Tabs.TabPane
 | 
			
		||||
            v-for="item in customerSummaryTabs"
 | 
			
		||||
            :key="item.key"
 | 
			
		||||
            :tab="item.tab"
 | 
			
		||||
            :force-render="true"
 | 
			
		||||
          />
 | 
			
		||||
        </Tabs>
 | 
			
		||||
        <ButtonGroup>
 | 
			
		||||
          <Button
 | 
			
		||||
            :type="active ? 'primary' : 'default'"
 | 
			
		||||
            v-if="activeTabName === 'funnel'"
 | 
			
		||||
            @click="handleActive(true)"
 | 
			
		||||
          >
 | 
			
		||||
            客户视角
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            :type="active ? 'default' : 'primary'"
 | 
			
		||||
            v-if="activeTabName === 'funnel'"
 | 
			
		||||
            @click="handleActive(false)"
 | 
			
		||||
          >
 | 
			
		||||
            动态视角
 | 
			
		||||
          </Button>
 | 
			
		||||
        </ButtonGroup>
 | 
			
		||||
        <EchartsUI class="mb-20 h-2/5 w-full" ref="chartRef" />
 | 
			
		||||
      </template>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  </Page>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
import type { VbenFormSchema } from '#/adapter/form';
 | 
			
		||||
 | 
			
		||||
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 { 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(),
 | 
			
		||||
        picker: 'year',
 | 
			
		||||
        showTime: false,
 | 
			
		||||
        format: 'YYYY',
 | 
			
		||||
        ranges: {},
 | 
			
		||||
      },
 | 
			
		||||
      defaultValue: [
 | 
			
		||||
        formatDateTime(beginOfDay(new Date(new Date().getFullYear(), 0, 1))),
 | 
			
		||||
        formatDateTime(endOfDay(new Date(new Date().getFullYear(), 11, 31))),
 | 
			
		||||
      ] 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',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +1,156 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { Page } from '@vben/common-ui';
 | 
			
		||||
import type { EchartsUIType } from '@vben/plugins/echarts';
 | 
			
		||||
 | 
			
		||||
import { Button } from 'ant-design-vue';
 | 
			
		||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
import type { CrmStatisticsCustomerApi } from '#/api/crm/statistics/customer';
 | 
			
		||||
 | 
			
		||||
import { onMounted, ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { Page } from '@vben/common-ui';
 | 
			
		||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 | 
			
		||||
 | 
			
		||||
import { Tabs } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import {
 | 
			
		||||
  getContractCountPerformance,
 | 
			
		||||
  getContractPricePerformance,
 | 
			
		||||
  getReceivablePricePerformance,
 | 
			
		||||
} from '#/api/crm/statistics/performance';
 | 
			
		||||
 | 
			
		||||
import { getChartOptions } from './chartOptions';
 | 
			
		||||
import { customerSummaryTabs, useGridFormSchema } from './data';
 | 
			
		||||
 | 
			
		||||
const activeTabName = ref('ContractCountPerformance');
 | 
			
		||||
const chartRef = ref<EchartsUIType>();
 | 
			
		||||
const { renderEcharts } = useEcharts(chartRef);
 | 
			
		||||
 | 
			
		||||
const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		||||
  formOptions: {
 | 
			
		||||
    schema: useGridFormSchema(),
 | 
			
		||||
    handleSubmit: async () => {
 | 
			
		||||
      await handleTabChange(activeTabName.value);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  gridOptions: {
 | 
			
		||||
    columns: [],
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
    keepSource: true,
 | 
			
		||||
    pagerConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
    proxyConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
    data: [],
 | 
			
		||||
    rowConfig: {
 | 
			
		||||
      keyField: 'id',
 | 
			
		||||
      isHover: true,
 | 
			
		||||
    },
 | 
			
		||||
    toolbarConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
  } as VxeTableGridOptions<CrmStatisticsCustomerApi.CustomerSummaryByUser>,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function handleTabChange(key: any) {
 | 
			
		||||
  activeTabName.value = key;
 | 
			
		||||
  const params = (await gridApi.formApi.getValues()) as any;
 | 
			
		||||
  let data: any[] = [];
 | 
			
		||||
  const columnsData: any[] = [];
 | 
			
		||||
  let tableData: any[] = [];
 | 
			
		||||
  switch (key) {
 | 
			
		||||
    case 'ContractCountPerformance': {
 | 
			
		||||
      tableData = [
 | 
			
		||||
        { title: '当月合同数量统计(个)' },
 | 
			
		||||
        { title: '上月合同数量统计(个)' },
 | 
			
		||||
        { title: '去年当月合同数量统计(个)' },
 | 
			
		||||
        { title: '环比增长率(%)' },
 | 
			
		||||
        { title: '同比增长率(%)' },
 | 
			
		||||
      ];
 | 
			
		||||
      data = await getContractCountPerformance(params);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 'ContractPricePerformance': {
 | 
			
		||||
      tableData = [
 | 
			
		||||
        { title: '当月合同金额统计(元)' },
 | 
			
		||||
        { title: '上月合同金额统计(元)' },
 | 
			
		||||
        { title: '去年当月合同金额统计(元)' },
 | 
			
		||||
        { title: '环比增长率(%)' },
 | 
			
		||||
        { title: '同比增长率(%)' },
 | 
			
		||||
      ];
 | 
			
		||||
      data = await getContractPricePerformance(params);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 'ReceivablePricePerformance': {
 | 
			
		||||
      tableData = [
 | 
			
		||||
        { title: '当月回款金额统计(元)' },
 | 
			
		||||
        { title: '上月回款金额统计(元)' },
 | 
			
		||||
        { title: '去年当月回款金额统计(元)' },
 | 
			
		||||
        { title: '环比增长率(%)' },
 | 
			
		||||
        { title: '同比增长率(%)' },
 | 
			
		||||
      ];
 | 
			
		||||
      data = await getReceivablePricePerformance(params);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const columnObj = {
 | 
			
		||||
    title: '日期',
 | 
			
		||||
    field: 'title',
 | 
			
		||||
    minWidth: 200,
 | 
			
		||||
    align: 'left',
 | 
			
		||||
  };
 | 
			
		||||
  columnsData.splice(0); // 清空数组
 | 
			
		||||
  columnsData.push(columnObj);
 | 
			
		||||
  data.forEach((item: any, index: number) => {
 | 
			
		||||
    const columnObj = { title: item.time, field: `field${index}` };
 | 
			
		||||
    columnsData.push(columnObj);
 | 
			
		||||
    tableData[0][`field${index}`] = item.currentMonthCount;
 | 
			
		||||
    tableData[1][`field${index}`] = item.lastMonthCount;
 | 
			
		||||
    tableData[2][`field${index}`] = item.lastYearCount;
 | 
			
		||||
    tableData[3][`field${index}`] =
 | 
			
		||||
      item.lastMonthCount === 0
 | 
			
		||||
        ? 'NULL'
 | 
			
		||||
        : (
 | 
			
		||||
            ((item.currentMonthCount - item.lastMonthCount) /
 | 
			
		||||
              item.lastMonthCount) *
 | 
			
		||||
            100
 | 
			
		||||
          ).toFixed(2);
 | 
			
		||||
    tableData[4][`field${index}`] =
 | 
			
		||||
      item.lastYearCount === 0
 | 
			
		||||
        ? 'NULL'
 | 
			
		||||
        : (
 | 
			
		||||
            ((item.currentMonthCount - item.lastYearCount) /
 | 
			
		||||
              item.lastYearCount) *
 | 
			
		||||
            100
 | 
			
		||||
          ).toFixed(2);
 | 
			
		||||
  });
 | 
			
		||||
  renderEcharts(getChartOptions(key, data), true);
 | 
			
		||||
  gridApi.grid.reloadColumn(columnsData);
 | 
			
		||||
  gridApi.grid.reloadData(tableData);
 | 
			
		||||
}
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  handleTabChange(activeTabName.value);
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Page>
 | 
			
		||||
    <Button
 | 
			
		||||
      danger
 | 
			
		||||
      type="link"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/yudaocode/yudao-ui-admin-vue3"
 | 
			
		||||
    >
 | 
			
		||||
      该功能支持 Vue3 + element-plus 版本!
 | 
			
		||||
    </Button>
 | 
			
		||||
    <br />
 | 
			
		||||
    <Button
 | 
			
		||||
      type="link"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/performance/index"
 | 
			
		||||
    >
 | 
			
		||||
      可参考
 | 
			
		||||
      https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/performance/index
 | 
			
		||||
      代码,pull request 贡献给我们!
 | 
			
		||||
    </Button>
 | 
			
		||||
  <Page auto-content-height>
 | 
			
		||||
    <Grid>
 | 
			
		||||
      <template #top>
 | 
			
		||||
        <Tabs v-model:active-key="activeTabName" @change="handleTabChange">
 | 
			
		||||
          <Tabs.TabPane
 | 
			
		||||
            v-for="item in customerSummaryTabs"
 | 
			
		||||
            :key="item.key"
 | 
			
		||||
            :tab="item.tab"
 | 
			
		||||
            :force-render="true"
 | 
			
		||||
          />
 | 
			
		||||
        </Tabs>
 | 
			
		||||
        <EchartsUI class="mb-20 h-full w-full" ref="chartRef" />
 | 
			
		||||
      </template>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  </Page>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +1,85 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { Page } from '@vben/common-ui';
 | 
			
		||||
import type { EchartsUIType } from '@vben/plugins/echarts';
 | 
			
		||||
 | 
			
		||||
import { Button } from 'ant-design-vue';
 | 
			
		||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
import type { CrmStatisticsCustomerApi } from '#/api/crm/statistics/customer';
 | 
			
		||||
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { Page } from '@vben/common-ui';
 | 
			
		||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 | 
			
		||||
 | 
			
		||||
import { Tabs } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import { getDatas } from '#/api/crm/statistics/portrait';
 | 
			
		||||
 | 
			
		||||
import { getChartOptions } from './chartOptions';
 | 
			
		||||
import { customerSummaryTabs, useGridColumns, useGridFormSchema } from './data';
 | 
			
		||||
 | 
			
		||||
const activeTabName = ref('area');
 | 
			
		||||
const leftChartRef = ref<EchartsUIType>();
 | 
			
		||||
const rightChartRef = ref<EchartsUIType>();
 | 
			
		||||
const { renderEcharts: renderLeftEcharts } = useEcharts(leftChartRef);
 | 
			
		||||
const { renderEcharts: renderRightEcharts } = useEcharts(rightChartRef);
 | 
			
		||||
 | 
			
		||||
const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		||||
  formOptions: {
 | 
			
		||||
    schema: useGridFormSchema(),
 | 
			
		||||
  },
 | 
			
		||||
  gridOptions: {
 | 
			
		||||
    columns: useGridColumns(activeTabName.value),
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
    keepSource: true,
 | 
			
		||||
    pagerConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
    proxyConfig: {
 | 
			
		||||
      ajax: {
 | 
			
		||||
        query: async (_, formValues) => {
 | 
			
		||||
          const res = await getDatas(activeTabName.value, formValues);
 | 
			
		||||
          renderLeftEcharts(getChartOptions(activeTabName.value, res).left);
 | 
			
		||||
          renderRightEcharts(getChartOptions(activeTabName.value, res).right);
 | 
			
		||||
          return res;
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    rowConfig: {
 | 
			
		||||
      keyField: 'id',
 | 
			
		||||
      isHover: true,
 | 
			
		||||
    },
 | 
			
		||||
    toolbarConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
  } as VxeTableGridOptions<CrmStatisticsCustomerApi.CustomerSummaryByUser>,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function handleTabChange(key: any) {
 | 
			
		||||
  activeTabName.value = key;
 | 
			
		||||
  gridApi.setGridOptions({
 | 
			
		||||
    columns: useGridColumns(key),
 | 
			
		||||
  });
 | 
			
		||||
  gridApi.reload();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Page>
 | 
			
		||||
    <Button
 | 
			
		||||
      danger
 | 
			
		||||
      type="link"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/yudaocode/yudao-ui-admin-vue3"
 | 
			
		||||
    >
 | 
			
		||||
      该功能支持 Vue3 + element-plus 版本!
 | 
			
		||||
    </Button>
 | 
			
		||||
    <br />
 | 
			
		||||
    <Button
 | 
			
		||||
      type="link"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/portrait/index"
 | 
			
		||||
    >
 | 
			
		||||
      可参考
 | 
			
		||||
      https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/portrait/index
 | 
			
		||||
      代码,pull request 贡献给我们!
 | 
			
		||||
    </Button>
 | 
			
		||||
  <Page auto-content-height>
 | 
			
		||||
    <Grid>
 | 
			
		||||
      <template #top>
 | 
			
		||||
        <Tabs v-model:active-key="activeTabName" @change="handleTabChange">
 | 
			
		||||
          <Tabs.TabPane
 | 
			
		||||
            v-for="item in customerSummaryTabs"
 | 
			
		||||
            :key="item.key"
 | 
			
		||||
            :tab="item.tab"
 | 
			
		||||
            :force-render="true"
 | 
			
		||||
          />
 | 
			
		||||
        </Tabs>
 | 
			
		||||
        <div class="mt-5 flex">
 | 
			
		||||
          <EchartsUI class="m-4 w-1/2" ref="leftChartRef" />
 | 
			
		||||
          <EchartsUI class="m-4 w-1/2" ref="rightChartRef" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  </Page>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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, formatDateTime, 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: [
 | 
			
		||||
        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,
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 列表的字段 */
 | 
			
		||||
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 [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +1,79 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { Page } from '@vben/common-ui';
 | 
			
		||||
import type { EchartsUIType } from '@vben/plugins/echarts';
 | 
			
		||||
 | 
			
		||||
import { Button } from 'ant-design-vue';
 | 
			
		||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
import type { CrmStatisticsCustomerApi } from '#/api/crm/statistics/customer';
 | 
			
		||||
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { Page } from '@vben/common-ui';
 | 
			
		||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
 | 
			
		||||
 | 
			
		||||
import { Tabs } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import { getDatas } from '#/api/crm/statistics/customer';
 | 
			
		||||
 | 
			
		||||
import { getChartOptions } from './chartOptions';
 | 
			
		||||
import { customerSummaryTabs, useGridColumns, useGridFormSchema } from './data';
 | 
			
		||||
 | 
			
		||||
const activeTabName = ref('contractPriceRank');
 | 
			
		||||
const chartRef = ref<EchartsUIType>();
 | 
			
		||||
const { renderEcharts } = useEcharts(chartRef);
 | 
			
		||||
 | 
			
		||||
const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		||||
  formOptions: {
 | 
			
		||||
    schema: useGridFormSchema(),
 | 
			
		||||
  },
 | 
			
		||||
  gridOptions: {
 | 
			
		||||
    columns: useGridColumns(activeTabName.value),
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
    keepSource: true,
 | 
			
		||||
    pagerConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
    proxyConfig: {
 | 
			
		||||
      ajax: {
 | 
			
		||||
        query: async (_, formValues) => {
 | 
			
		||||
          const res = await getDatas(activeTabName.value, formValues);
 | 
			
		||||
          renderEcharts(getChartOptions(activeTabName.value, res));
 | 
			
		||||
          return res;
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    rowConfig: {
 | 
			
		||||
      keyField: 'id',
 | 
			
		||||
      isHover: true,
 | 
			
		||||
    },
 | 
			
		||||
    toolbarConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
  } as VxeTableGridOptions<CrmStatisticsCustomerApi.CustomerSummaryByUser>,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function handleTabChange(key: any) {
 | 
			
		||||
  activeTabName.value = key;
 | 
			
		||||
  gridApi.setGridOptions({
 | 
			
		||||
    columns: useGridColumns(key),
 | 
			
		||||
  });
 | 
			
		||||
  gridApi.reload();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Page>
 | 
			
		||||
    <Button
 | 
			
		||||
      danger
 | 
			
		||||
      type="link"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/yudaocode/yudao-ui-admin-vue3"
 | 
			
		||||
    >
 | 
			
		||||
      该功能支持 Vue3 + element-plus 版本!
 | 
			
		||||
    </Button>
 | 
			
		||||
    <br />
 | 
			
		||||
    <Button
 | 
			
		||||
      type="link"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/rank/index"
 | 
			
		||||
    >
 | 
			
		||||
      可参考
 | 
			
		||||
      https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/statistics/rank/index
 | 
			
		||||
      代码,pull request 贡献给我们!
 | 
			
		||||
    </Button>
 | 
			
		||||
  <Page auto-content-height>
 | 
			
		||||
    <Grid>
 | 
			
		||||
      <template #top>
 | 
			
		||||
        <Tabs v-model:active-key="activeTabName" @change="handleTabChange">
 | 
			
		||||
          <Tabs.TabPane
 | 
			
		||||
            v-for="item in customerSummaryTabs"
 | 
			
		||||
            :key="item.key"
 | 
			
		||||
            :tab="item.tab"
 | 
			
		||||
            :force-render="true"
 | 
			
		||||
          />
 | 
			
		||||
        </Tabs>
 | 
			
		||||
        <EchartsUI class="mb-20 h-full w-full" ref="chartRef" />
 | 
			
		||||
      </template>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  </Page>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,7 +54,14 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
 | 
			
		|||
      field: 'avatar',
 | 
			
		||||
      title: '头像',
 | 
			
		||||
      width: 70,
 | 
			
		||||
      slots: { default: 'avatar' },
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        name: 'CellImage',
 | 
			
		||||
        props: {
 | 
			
		||||
          width: 24,
 | 
			
		||||
          height: 24,
 | 
			
		||||
          shape: 'circle',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'nickname',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ import { useAccess } from '@vben/access';
 | 
			
		|||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
 | 
			
		||||
import { $t } from '@vben/locales';
 | 
			
		||||
 | 
			
		||||
import { Avatar, message, Switch } from 'ant-design-vue';
 | 
			
		||||
import { message, Switch } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -167,10 +167,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		|||
        />
 | 
			
		||||
      </template>
 | 
			
		||||
 | 
			
		||||
      <template #avatar="{ row }">
 | 
			
		||||
        <Avatar :src="row.avatar" />
 | 
			
		||||
      </template>
 | 
			
		||||
 | 
			
		||||
      <template #brokerageEnabled="{ row }">
 | 
			
		||||
        <Switch
 | 
			
		||||
          v-model:checked="row.brokerageEnabled"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,8 +9,6 @@ import { ref } from 'vue';
 | 
			
		|||
import { useVbenModal } from '@vben/common-ui';
 | 
			
		||||
import { fenToYuan } from '@vben/utils';
 | 
			
		||||
 | 
			
		||||
import { Avatar, Tag } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import { getBrokerageRecordPage } from '#/api/mall/trade/brokerage/record';
 | 
			
		||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +100,13 @@ function useColumns(): VxeTableGridOptions['columns'] {
 | 
			
		|||
      field: 'sourceUserAvatar',
 | 
			
		||||
      title: '头像',
 | 
			
		||||
      width: 70,
 | 
			
		||||
      slots: { default: 'avatar' },
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        name: 'CellImage',
 | 
			
		||||
        props: {
 | 
			
		||||
          width: 24,
 | 
			
		||||
          height: 24,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'sourceUserNickname',
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +123,10 @@ function useColumns(): VxeTableGridOptions['columns'] {
 | 
			
		|||
      field: 'status',
 | 
			
		||||
      title: '状态',
 | 
			
		||||
      minWidth: 85,
 | 
			
		||||
      slots: { default: 'status' },
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        name: 'CellDict',
 | 
			
		||||
        props: { type: DICT_TYPE.BROKERAGE_RECORD_STATUS },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'createTime',
 | 
			
		||||
| 
						 | 
				
			
			@ -173,21 +180,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		|||
 | 
			
		||||
<template>
 | 
			
		||||
  <Modal title="推广订单列表" class="w-3/5">
 | 
			
		||||
    <Grid table-title="推广订单列表">
 | 
			
		||||
      <template #avatar="{ row }">
 | 
			
		||||
        <Avatar :src="row.sourceUserAvatar" />
 | 
			
		||||
      </template>
 | 
			
		||||
 | 
			
		||||
      <template #status="{ row }">
 | 
			
		||||
        <template
 | 
			
		||||
          v-for="dict in getDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS)"
 | 
			
		||||
          :key="dict.value"
 | 
			
		||||
        >
 | 
			
		||||
          <Tag v-if="dict.value === row.status" :color="dict.colorType">
 | 
			
		||||
            {{ dict.label }}
 | 
			
		||||
          </Tag>
 | 
			
		||||
        </template>
 | 
			
		||||
      </template>
 | 
			
		||||
    </Grid>
 | 
			
		||||
    <Grid table-title="推广订单列表" />
 | 
			
		||||
  </Modal>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ import { ref } from 'vue';
 | 
			
		|||
 | 
			
		||||
import { useVbenModal } from '@vben/common-ui';
 | 
			
		||||
 | 
			
		||||
import { Avatar, Tag } from 'ant-design-vue';
 | 
			
		||||
import { Tag } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import { getBrokerageUserPage } from '#/api/mall/trade/brokerage/user';
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +76,14 @@ function useColumns(): VxeTableGridOptions['columns'] {
 | 
			
		|||
      field: 'avatar',
 | 
			
		||||
      title: '头像',
 | 
			
		||||
      width: 70,
 | 
			
		||||
      slots: { default: 'avatar' },
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        name: 'CellImage',
 | 
			
		||||
        props: {
 | 
			
		||||
          width: 24,
 | 
			
		||||
          height: 24,
 | 
			
		||||
          shape: 'circle',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'nickname',
 | 
			
		||||
| 
						 | 
				
			
			@ -144,10 +151,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		|||
<template>
 | 
			
		||||
  <Modal title="推广人列表" class="w-3/5">
 | 
			
		||||
    <Grid table-title="推广人列表">
 | 
			
		||||
      <template #avatar="{ row }">
 | 
			
		||||
        <Avatar :src="row.avatar" />
 | 
			
		||||
      </template>
 | 
			
		||||
 | 
			
		||||
      <template #brokerageEnabled="{ row }">
 | 
			
		||||
        <Tag v-if="row.brokerageEnabled" color="success">有</Tag>
 | 
			
		||||
        <Tag v-else>无</Tag>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,4 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
// TODO @xingyu:是不是不引入 @form-create/ant-design-vue 组件哈;保持和 vben 一致~;
 | 
			
		||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
import type { PayAppApi } from '#/api/pay/app';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,15 @@
 | 
			
		|||
import type { VbenFormSchema } from '#/adapter/form';
 | 
			
		||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
import type { DescriptionItemSchema } from '#/components/description';
 | 
			
		||||
 | 
			
		||||
import { h } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { fenToYuan, formatDateTime } from '@vben/utils';
 | 
			
		||||
 | 
			
		||||
import { Tag } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { getAppList } from '#/api/pay/app';
 | 
			
		||||
import { DictTag } from '#/components/dict-tag';
 | 
			
		||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
 | 
			
		||||
 | 
			
		||||
/** 列表的搜索表单 */
 | 
			
		||||
| 
						 | 
				
			
			@ -119,3 +127,128 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
 | 
			
		|||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 详情页的字段 */
 | 
			
		||||
export function useBaseDetailSchema(): DescriptionItemSchema[] {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      field: 'merchantRefundId',
 | 
			
		||||
      label: '商户退款单号',
 | 
			
		||||
      content: (data) =>
 | 
			
		||||
        h(Tag, {}, () => {
 | 
			
		||||
          return data?.merchantRefundId || '-';
 | 
			
		||||
        }),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'channelRefundNo',
 | 
			
		||||
      label: '渠道退款单号',
 | 
			
		||||
      content: (data) =>
 | 
			
		||||
        h(Tag, {}, () => {
 | 
			
		||||
          return data?.channelRefundNo || '-';
 | 
			
		||||
        }),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'merchantOrderId',
 | 
			
		||||
      label: '商户支付单号',
 | 
			
		||||
      content: (data) =>
 | 
			
		||||
        h(Tag, {}, () => {
 | 
			
		||||
          return data?.merchantOrderId || '-';
 | 
			
		||||
        }),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'channelOrderNo',
 | 
			
		||||
      label: '渠道支付单号',
 | 
			
		||||
      content: (data) =>
 | 
			
		||||
        h(Tag, {}, () => {
 | 
			
		||||
          return data?.channelOrderNo || '-';
 | 
			
		||||
        }),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'appId',
 | 
			
		||||
      label: '应用编号',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'appName',
 | 
			
		||||
      label: '应用名称',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'payPrice',
 | 
			
		||||
      label: '支付金额',
 | 
			
		||||
      content: (data) =>
 | 
			
		||||
        h(Tag, { color: 'success' }, () => {
 | 
			
		||||
          return fenToYuan(data.payPrice || 0);
 | 
			
		||||
        }),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'refundPrice',
 | 
			
		||||
      label: '退款金额',
 | 
			
		||||
      content: (data) =>
 | 
			
		||||
        h(Tag, { color: 'red' }, () => {
 | 
			
		||||
          return fenToYuan(data.refundPrice || 0);
 | 
			
		||||
        }),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'status',
 | 
			
		||||
      label: '退款状态',
 | 
			
		||||
      content: (data) =>
 | 
			
		||||
        h(DictTag, {
 | 
			
		||||
          type: DICT_TYPE.PAY_REFUND_STATUS,
 | 
			
		||||
          value: data?.status,
 | 
			
		||||
        }),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'successTime',
 | 
			
		||||
      label: '退款时间',
 | 
			
		||||
      content: (data) => formatDateTime(data.successTime) as string,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'createTime',
 | 
			
		||||
      label: '创建时间',
 | 
			
		||||
      content: (data) => formatDateTime(data.createTime) as string,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'updateTime',
 | 
			
		||||
      label: '更新时间',
 | 
			
		||||
      content: (data) => formatDateTime(data.updateTime) as string,
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 详情页的字段 */
 | 
			
		||||
export function useChannelDetailSchema(): DescriptionItemSchema[] {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      field: 'channelCode',
 | 
			
		||||
      label: '退款渠道',
 | 
			
		||||
      content: (data) =>
 | 
			
		||||
        h(DictTag, {
 | 
			
		||||
          type: DICT_TYPE.PAY_CHANNEL_CODE,
 | 
			
		||||
          value: data?.channelCode,
 | 
			
		||||
        }),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'reason',
 | 
			
		||||
      label: '退款原因',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'userIp',
 | 
			
		||||
      label: '退款 IP',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'notifyUrl',
 | 
			
		||||
      label: '通知 URL',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'channelErrorCode',
 | 
			
		||||
      label: '渠道错误码',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'channelErrorMsg',
 | 
			
		||||
      label: '渠道错误码描述',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      field: 'channelNotifyData',
 | 
			
		||||
      label: '支付通道异步回调内容',
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,16 +4,34 @@ import type { PayRefundApi } from '#/api/pay/refund';
 | 
			
		|||
import { ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenModal } from '@vben/common-ui';
 | 
			
		||||
import { formatDateTime } from '@vben/utils';
 | 
			
		||||
 | 
			
		||||
import { Descriptions, Divider, Tag } from 'ant-design-vue';
 | 
			
		||||
import { Divider } from 'ant-design-vue';
 | 
			
		||||
 | 
			
		||||
import { getRefund } from '#/api/pay/refund';
 | 
			
		||||
import { DictTag } from '#/components/dict-tag';
 | 
			
		||||
import { DICT_TYPE } from '#/utils';
 | 
			
		||||
import { useDescription } from '#/components/description';
 | 
			
		||||
 | 
			
		||||
import { useBaseDetailSchema, useChannelDetailSchema } from '../data';
 | 
			
		||||
 | 
			
		||||
const formData = ref<PayRefundApi.Refund>();
 | 
			
		||||
 | 
			
		||||
const [BaseDescription] = useDescription({
 | 
			
		||||
  componentProps: {
 | 
			
		||||
    bordered: false,
 | 
			
		||||
    column: 2,
 | 
			
		||||
    class: 'mx-4',
 | 
			
		||||
  },
 | 
			
		||||
  schema: useBaseDetailSchema(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const [ChannelDescription] = useDescription({
 | 
			
		||||
  componentProps: {
 | 
			
		||||
    bordered: false,
 | 
			
		||||
    column: 2,
 | 
			
		||||
    class: 'mx-4',
 | 
			
		||||
  },
 | 
			
		||||
  schema: useChannelDetailSchema(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const [Modal, modalApi] = useVbenModal({
 | 
			
		||||
  async onOpenChange(isOpen: boolean) {
 | 
			
		||||
    if (!isOpen) {
 | 
			
		||||
| 
						 | 
				
			
			@ -42,89 +60,8 @@ const [Modal, modalApi] = useVbenModal({
 | 
			
		|||
    :show-cancel-button="false"
 | 
			
		||||
    :show-confirm-button="false"
 | 
			
		||||
  >
 | 
			
		||||
    <Descriptions bordered :column="2" size="middle" class="mx-4">
 | 
			
		||||
      <Descriptions.Item label="商户退款单号">
 | 
			
		||||
        <Tag size="small">{{ formData?.merchantRefundId }}</Tag>
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="渠道退款单号">
 | 
			
		||||
        <Tag type="success" size="small" v-if="formData?.channelRefundNo">
 | 
			
		||||
          {{ formData?.channelRefundNo }}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="商户支付单号">
 | 
			
		||||
        <Tag size="small">{{ formData?.merchantOrderId }}</Tag>
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="渠道支付单号">
 | 
			
		||||
        <Tag type="success" size="small">
 | 
			
		||||
          {{ formData?.channelOrderNo }}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="应用编号">
 | 
			
		||||
        {{ formData?.appId }}
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="应用名称">
 | 
			
		||||
        {{ formData?.appName }}
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="支付金额">
 | 
			
		||||
        <Tag type="success" size="small">
 | 
			
		||||
          ¥{{ (formData?.payPrice || 0) / 100.0 }}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="退款金额">
 | 
			
		||||
        <Tag size="mini" type="danger">
 | 
			
		||||
          ¥{{ (formData?.refundPrice || 0) / 100.0 }}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="退款状态">
 | 
			
		||||
        <DictTag
 | 
			
		||||
          :type="DICT_TYPE.PAY_REFUND_STATUS"
 | 
			
		||||
          :value="formData?.status"
 | 
			
		||||
        />
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="退款时间">
 | 
			
		||||
        {{ formatDateTime(formData?.successTime || '') }}
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="创建时间">
 | 
			
		||||
        {{ formatDateTime(formData?.createTime || '') }}
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="更新时间">
 | 
			
		||||
        {{ formatDateTime(formData?.updateTime || '') }}
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
    </Descriptions>
 | 
			
		||||
    <BaseDescription :data="formData" />
 | 
			
		||||
    <Divider />
 | 
			
		||||
    <Descriptions bordered :column="2" size="middle" class="mx-4">
 | 
			
		||||
      <Descriptions.Item label="退款渠道">
 | 
			
		||||
        <DictTag
 | 
			
		||||
          :type="DICT_TYPE.PAY_CHANNEL_CODE"
 | 
			
		||||
          :value="formData?.channelCode"
 | 
			
		||||
        />
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="退款原因">
 | 
			
		||||
        {{ formData?.reason }}
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="退款 IP">
 | 
			
		||||
        {{ formData?.userIp }}
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="通知 URL">
 | 
			
		||||
        {{ formData?.notifyUrl }}
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
    </Descriptions>
 | 
			
		||||
    <Divider />
 | 
			
		||||
    <Descriptions bordered :column="2" size="middle" class="mx-4">
 | 
			
		||||
      <Descriptions.Item label="渠道错误码">
 | 
			
		||||
        {{ formData?.channelErrorCode }}
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
      <Descriptions.Item label="渠道错误码描述">
 | 
			
		||||
        {{ formData?.channelErrorMsg }}
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
    </Descriptions>
 | 
			
		||||
 | 
			
		||||
    <Descriptions bordered :column="1" size="middle" class="mx-4">
 | 
			
		||||
      <Descriptions.Item label="支付通道异步回调内容">
 | 
			
		||||
        <p class="whitespace-pre-wrap break-words">
 | 
			
		||||
          {{ formData?.channelNotifyData }}
 | 
			
		||||
        </p>
 | 
			
		||||
      </Descriptions.Item>
 | 
			
		||||
    </Descriptions>
 | 
			
		||||
    <ChannelDescription :data="formData" />
 | 
			
		||||
  </Modal>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,47 +1,67 @@
 | 
			
		|||
import dayjs from 'dayjs';
 | 
			
		||||
 | 
			
		||||
import { isEmpty } from '.';
 | 
			
		||||
import { formatDate } from './date';
 | 
			
		||||
 | 
			
		||||
/** 时间段选择器拓展  */
 | 
			
		||||
export function rangePickerExtend() {
 | 
			
		||||
  return {
 | 
			
		||||
    // 显示格式
 | 
			
		||||
    format: 'YYYY-MM-DD HH:mm:ss',
 | 
			
		||||
    placeholder: ['开始时间', '结束时间'],
 | 
			
		||||
    ranges: {
 | 
			
		||||
      今天: [dayjs().startOf('day'), dayjs().endOf('day')],
 | 
			
		||||
      最近7天: [
 | 
			
		||||
        dayjs().subtract(7, 'day').startOf('day'),
 | 
			
		||||
        dayjs().endOf('day'),
 | 
			
		||||
      ],
 | 
			
		||||
      最近30天: [
 | 
			
		||||
        dayjs().subtract(30, 'day').startOf('day'),
 | 
			
		||||
        dayjs().endOf('day'),
 | 
			
		||||
      ],
 | 
			
		||||
      昨天: [
 | 
			
		||||
        dayjs().subtract(1, 'day').startOf('day'),
 | 
			
		||||
        dayjs().subtract(1, 'day').endOf('day'),
 | 
			
		||||
      ],
 | 
			
		||||
      本周: [dayjs().startOf('week'), dayjs().endOf('day')],
 | 
			
		||||
      本月: [dayjs().startOf('month'), dayjs().endOf('day')],
 | 
			
		||||
    },
 | 
			
		||||
    showTime: {
 | 
			
		||||
      defaultValue: [
 | 
			
		||||
        dayjs('00:00:00', 'HH:mm:ss'),
 | 
			
		||||
        dayjs('23:59:59', 'HH:mm:ss'),
 | 
			
		||||
      ],
 | 
			
		||||
      format: 'HH:mm:ss',
 | 
			
		||||
    },
 | 
			
		||||
    transformDateFunc: (dates: any) => {
 | 
			
		||||
      if (dates && dates.length === 2) {
 | 
			
		||||
        // 格式化为后台支持的时间格式
 | 
			
		||||
        return [dates.createTime[0], dates.createTime[1]].join(',');
 | 
			
		||||
/**
 | 
			
		||||
 * @param {Date | number | string} time 需要转换的时间
 | 
			
		||||
 * @param {string} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
 | 
			
		||||
 */
 | 
			
		||||
export function formatTime(time: Date | number | string, fmt: string) {
 | 
			
		||||
  if (time) {
 | 
			
		||||
    const date = new Date(time);
 | 
			
		||||
    const o = {
 | 
			
		||||
      'M+': date.getMonth() + 1,
 | 
			
		||||
      'd+': date.getDate(),
 | 
			
		||||
      'H+': date.getHours(),
 | 
			
		||||
      'm+': date.getMinutes(),
 | 
			
		||||
      's+': date.getSeconds(),
 | 
			
		||||
      'q+': Math.floor((date.getMonth() + 3) / 3),
 | 
			
		||||
      S: date.getMilliseconds(),
 | 
			
		||||
    };
 | 
			
		||||
    const yearMatch = fmt.match(/y+/);
 | 
			
		||||
    if (yearMatch) {
 | 
			
		||||
      fmt = fmt.replace(
 | 
			
		||||
        yearMatch[0],
 | 
			
		||||
        `${date.getFullYear()}`.slice(4 - yearMatch[0].length),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    for (const k in o) {
 | 
			
		||||
      const match = fmt.match(new RegExp(`(${k})`));
 | 
			
		||||
      if (match) {
 | 
			
		||||
        fmt = fmt.replace(
 | 
			
		||||
          match[0],
 | 
			
		||||
          match[0].length === 1
 | 
			
		||||
            ? (o[k as keyof typeof o] as any)
 | 
			
		||||
            : `00${o[k as keyof typeof o]}`.slice(
 | 
			
		||||
                `${o[k as keyof typeof o]}`.length,
 | 
			
		||||
              ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return {};
 | 
			
		||||
    },
 | 
			
		||||
    // 如果需要10位时间戳(秒级)可以使用 valueFormat: 'X'
 | 
			
		||||
    valueFormat: 'YYYY-MM-DD HH:mm:ss',
 | 
			
		||||
  };
 | 
			
		||||
    }
 | 
			
		||||
    return fmt;
 | 
			
		||||
  } else {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取当前日期是第几周
 | 
			
		||||
 * @param dateTime 当前传入的日期值
 | 
			
		||||
 * @returns 返回第几周数字值
 | 
			
		||||
 */
 | 
			
		||||
export function getWeek(dateTime: Date): number {
 | 
			
		||||
  const temptTime = new Date(dateTime);
 | 
			
		||||
  // 周几
 | 
			
		||||
  const weekday = temptTime.getDay() || 7;
 | 
			
		||||
  // 周1+5天=周六
 | 
			
		||||
  temptTime.setDate(temptTime.getDate() - weekday + 1 + 5);
 | 
			
		||||
  let firstDay = new Date(temptTime.getFullYear(), 0, 1);
 | 
			
		||||
  const dayOfWeek = firstDay.getDay();
 | 
			
		||||
  let spendDay = 1;
 | 
			
		||||
  if (dayOfWeek !== 0) spendDay = 7 - dayOfWeek + 1;
 | 
			
		||||
  firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay);
 | 
			
		||||
  const d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86_400_000);
 | 
			
		||||
  return Math.ceil(d / 7);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -94,10 +114,28 @@ export function formatPast(
 | 
			
		|||
      typeof param === 'string' || typeof param === 'object'
 | 
			
		||||
        ? new Date(param)
 | 
			
		||||
        : param;
 | 
			
		||||
    return dayjs(date).format(format);
 | 
			
		||||
    return formatDate(date, format) as string;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 时间问候语
 | 
			
		||||
 * @param param 当前时间,new Date() 格式
 | 
			
		||||
 * @description param 调用 `formatAxis(new Date())` 输出 `上午好`
 | 
			
		||||
 * @returns 返回拼接后的时间字符串
 | 
			
		||||
 */
 | 
			
		||||
export function formatAxis(param: Date): string {
 | 
			
		||||
  const hour: number = new Date(param).getHours();
 | 
			
		||||
  if (hour < 6) return '凌晨好';
 | 
			
		||||
  else if (hour < 9) return '早上好';
 | 
			
		||||
  else if (hour < 12) return '上午好';
 | 
			
		||||
  else if (hour < 14) return '中午好';
 | 
			
		||||
  else if (hour < 17) return '下午好';
 | 
			
		||||
  else if (hour < 19) return '傍晚好';
 | 
			
		||||
  else if (hour < 22) return '晚上好';
 | 
			
		||||
  else return '夜里好';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 将毫秒,转换成时间字符串。例如说,xx 分钟
 | 
			
		||||
 *
 | 
			
		||||
| 
						 | 
				
			
			@ -105,22 +143,12 @@ export function formatPast(
 | 
			
		|||
 * @returns {string} 字符串
 | 
			
		||||
 */
 | 
			
		||||
export function formatPast2(ms: number): string {
 | 
			
		||||
  if (isEmpty(ms)) {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
  // 定义时间单位常量,便于维护
 | 
			
		||||
  const SECOND = 1000;
 | 
			
		||||
  const MINUTE = 60 * SECOND;
 | 
			
		||||
  const HOUR = 60 * MINUTE;
 | 
			
		||||
  const DAY = 24 * HOUR;
 | 
			
		||||
 | 
			
		||||
  // 计算各时间单位
 | 
			
		||||
  const day = Math.floor(ms / DAY);
 | 
			
		||||
  const hour = Math.floor((ms % DAY) / HOUR);
 | 
			
		||||
  const minute = Math.floor((ms % HOUR) / MINUTE);
 | 
			
		||||
  const second = Math.floor((ms % MINUTE) / SECOND);
 | 
			
		||||
 | 
			
		||||
  // 根据时间长短返回不同格式
 | 
			
		||||
  const day = Math.floor(ms / (24 * 60 * 60 * 1000));
 | 
			
		||||
  const hour = Math.floor(ms / (60 * 60 * 1000) - day * 24);
 | 
			
		||||
  const minute = Math.floor(ms / (60 * 1000) - day * 24 * 60 - hour * 60);
 | 
			
		||||
  const second = Math.floor(
 | 
			
		||||
    ms / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60,
 | 
			
		||||
  );
 | 
			
		||||
  if (day > 0) {
 | 
			
		||||
    return `${day} 天${hour} 小时 ${minute} 分钟`;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -134,43 +162,138 @@ export function formatPast2(ms: number): string {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {Date | number | string} time 需要转换的时间
 | 
			
		||||
 * @param {string} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
 | 
			
		||||
 * 设置起始日期,时间为00:00:00
 | 
			
		||||
 * @param param 传入日期
 | 
			
		||||
 * @returns 带时间00:00:00的日期
 | 
			
		||||
 */
 | 
			
		||||
export function formatTime(time: Date | number | string, fmt: string) {
 | 
			
		||||
  if (time) {
 | 
			
		||||
    const date = new Date(time);
 | 
			
		||||
    const o = {
 | 
			
		||||
      'M+': date.getMonth() + 1,
 | 
			
		||||
      'd+': date.getDate(),
 | 
			
		||||
      'H+': date.getHours(),
 | 
			
		||||
      'm+': date.getMinutes(),
 | 
			
		||||
      's+': date.getSeconds(),
 | 
			
		||||
      'q+': Math.floor((date.getMonth() + 3) / 3),
 | 
			
		||||
      S: date.getMilliseconds(),
 | 
			
		||||
    };
 | 
			
		||||
    const yearMatch = fmt.match(/y+/);
 | 
			
		||||
    if (yearMatch) {
 | 
			
		||||
      fmt = fmt.replace(
 | 
			
		||||
        yearMatch[0],
 | 
			
		||||
        `${date.getFullYear()}`.slice(4 - yearMatch[0].length),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    for (const k in o) {
 | 
			
		||||
      const match = fmt.match(new RegExp(`(${k})`));
 | 
			
		||||
      if (match) {
 | 
			
		||||
        fmt = fmt.replace(
 | 
			
		||||
          match[0],
 | 
			
		||||
          match[0].length === 1
 | 
			
		||||
            ? (o[k as keyof typeof o] as any)
 | 
			
		||||
            : `00${o[k as keyof typeof o]}`.slice(
 | 
			
		||||
                `${o[k as keyof typeof o]}`.length,
 | 
			
		||||
              ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return fmt;
 | 
			
		||||
  } else {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
export function beginOfDay(param: Date): Date {
 | 
			
		||||
  return new Date(
 | 
			
		||||
    param.getFullYear(),
 | 
			
		||||
    param.getMonth(),
 | 
			
		||||
    param.getDate(),
 | 
			
		||||
    0,
 | 
			
		||||
    0,
 | 
			
		||||
    0,
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 设置结束日期,时间为23:59:59
 | 
			
		||||
 * @param param 传入日期
 | 
			
		||||
 * @returns 带时间23:59:59的日期
 | 
			
		||||
 */
 | 
			
		||||
export function endOfDay(param: Date): Date {
 | 
			
		||||
  return new Date(
 | 
			
		||||
    param.getFullYear(),
 | 
			
		||||
    param.getMonth(),
 | 
			
		||||
    param.getDate(),
 | 
			
		||||
    23,
 | 
			
		||||
    59,
 | 
			
		||||
    59,
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 计算两个日期间隔天数
 | 
			
		||||
 * @param param1 日期1
 | 
			
		||||
 * @param param2 日期2
 | 
			
		||||
 */
 | 
			
		||||
export function betweenDay(param1: Date, param2: Date): number {
 | 
			
		||||
  param1 = convertDate(param1);
 | 
			
		||||
  param2 = convertDate(param2);
 | 
			
		||||
  // 计算差值
 | 
			
		||||
  return Math.floor((param2.getTime() - param1.getTime()) / (24 * 3600 * 1000));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 日期计算
 | 
			
		||||
 * @param param1 日期
 | 
			
		||||
 * @param param2 添加的时间
 | 
			
		||||
 */
 | 
			
		||||
export function addTime(param1: Date, param2: number): Date {
 | 
			
		||||
  param1 = convertDate(param1);
 | 
			
		||||
  return new Date(param1.getTime() + param2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 日期转换
 | 
			
		||||
 * @param param 日期
 | 
			
		||||
 */
 | 
			
		||||
export function convertDate(param: Date | string): Date {
 | 
			
		||||
  if (typeof param === 'string') {
 | 
			
		||||
    return new Date(param);
 | 
			
		||||
  }
 | 
			
		||||
  return param;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 指定的两个日期, 是否为同一天
 | 
			
		||||
 * @param a 日期 A
 | 
			
		||||
 * @param b 日期 B
 | 
			
		||||
 */
 | 
			
		||||
export function isSameDay(a: dayjs.ConfigType, b: dayjs.ConfigType): boolean {
 | 
			
		||||
  if (!a || !b) return false;
 | 
			
		||||
 | 
			
		||||
  const aa = dayjs(a);
 | 
			
		||||
  const bb = dayjs(b);
 | 
			
		||||
  return (
 | 
			
		||||
    aa.year() === bb.year() &&
 | 
			
		||||
    aa.month() === bb.month() &&
 | 
			
		||||
    aa.day() === bb.day()
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取一天的开始时间、截止时间
 | 
			
		||||
 * @param date 日期
 | 
			
		||||
 * @param days 天数
 | 
			
		||||
 */
 | 
			
		||||
export function getDayRange(
 | 
			
		||||
  date: dayjs.ConfigType,
 | 
			
		||||
  days: number,
 | 
			
		||||
): [dayjs.ConfigType, dayjs.ConfigType] {
 | 
			
		||||
  const day = dayjs(date).add(days, 'd');
 | 
			
		||||
  return getDateRange(day, day);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取最近7天的开始时间、截止时间
 | 
			
		||||
 */
 | 
			
		||||
export function getLast7Days(): [dayjs.ConfigType, dayjs.ConfigType] {
 | 
			
		||||
  const lastWeekDay = dayjs().subtract(7, 'd');
 | 
			
		||||
  const yesterday = dayjs().subtract(1, 'd');
 | 
			
		||||
  return getDateRange(lastWeekDay, yesterday);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取最近30天的开始时间、截止时间
 | 
			
		||||
 */
 | 
			
		||||
export function getLast30Days(): [dayjs.ConfigType, dayjs.ConfigType] {
 | 
			
		||||
  const lastMonthDay = dayjs().subtract(30, 'd');
 | 
			
		||||
  const yesterday = dayjs().subtract(1, 'd');
 | 
			
		||||
  return getDateRange(lastMonthDay, yesterday);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取最近1年的开始时间、截止时间
 | 
			
		||||
 */
 | 
			
		||||
export function getLast1Year(): [dayjs.ConfigType, dayjs.ConfigType] {
 | 
			
		||||
  const lastYearDay = dayjs().subtract(1, 'y');
 | 
			
		||||
  const yesterday = dayjs().subtract(1, 'd');
 | 
			
		||||
  return getDateRange(lastYearDay, yesterday);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取指定日期的开始时间、截止时间
 | 
			
		||||
 * @param beginDate 开始日期
 | 
			
		||||
 * @param endDate 截止日期
 | 
			
		||||
 */
 | 
			
		||||
export function getDateRange(
 | 
			
		||||
  beginDate: dayjs.ConfigType,
 | 
			
		||||
  endDate: dayjs.ConfigType,
 | 
			
		||||
): [string, string] {
 | 
			
		||||
  return [
 | 
			
		||||
    dayjs(beginDate).startOf('d').format('YYYY-MM-DD HH:mm:ss'),
 | 
			
		||||
    dayjs(endDate).endOf('d').format('YYYY-MM-DD HH:mm:ss'),
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,11 +26,11 @@ const [Modal, modalApi] = useVbenModal({
 | 
			
		|||
});
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <Modal class="w-2/5" :title="$t('ui.widgets.qa')">
 | 
			
		||||
  <Modal class="w-1/3" :title="$t('ui.widgets.qa')">
 | 
			
		||||
    <div class="mt-2 flex flex-col">
 | 
			
		||||
      <div class="mt-2 flex flex-row">
 | 
			
		||||
      <div class="mt-2 flex flex-col">
 | 
			
		||||
        <VbenButtonGroup class="basis-1/3" :gap="2" border size="large">
 | 
			
		||||
          <p class="p-2">项目地址:</p>
 | 
			
		||||
          <p class="w-24 p-2">项目地址:</p>
 | 
			
		||||
          <VbenButton
 | 
			
		||||
            variant="link"
 | 
			
		||||
            @click="
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +50,7 @@ const [Modal, modalApi] = useVbenModal({
 | 
			
		|||
        </VbenButtonGroup>
 | 
			
		||||
 | 
			
		||||
        <VbenButtonGroup class="basis-1/3" :gap="2" border size="large">
 | 
			
		||||
          <p class="p-2">issues:</p>
 | 
			
		||||
          <p class="w-24 p-2">issues:</p>
 | 
			
		||||
          <VbenButton
 | 
			
		||||
            variant="link"
 | 
			
		||||
            @click="
 | 
			
		||||
| 
						 | 
				
			
			@ -74,7 +74,7 @@ const [Modal, modalApi] = useVbenModal({
 | 
			
		|||
        </VbenButtonGroup>
 | 
			
		||||
 | 
			
		||||
        <VbenButtonGroup class="basis-1/3" :gap="2" border size="large">
 | 
			
		||||
          <p class="p-2">开发文档:</p>
 | 
			
		||||
          <p class="w-24 p-2">开发文档:</p>
 | 
			
		||||
          <VbenButton
 | 
			
		||||
            variant="link"
 | 
			
		||||
            @click="openWindow('https://doc.iocoder.cn/quick-start/')"
 | 
			
		||||
| 
						 | 
				
			
			@ -86,13 +86,17 @@ const [Modal, modalApi] = useVbenModal({
 | 
			
		|||
          </VbenButton>
 | 
			
		||||
        </VbenButtonGroup>
 | 
			
		||||
      </div>
 | 
			
		||||
      <p class="mt-2 flex justify-center">
 | 
			
		||||
        <span>
 | 
			
		||||
          <img src="/wx-xingyu.png" alt="数舵科技" />
 | 
			
		||||
        </span>
 | 
			
		||||
      </p>
 | 
			
		||||
      <div class="mt-2 flex justify-start">
 | 
			
		||||
        <p class="w-24 p-2">软件外包:</p>
 | 
			
		||||
        <img
 | 
			
		||||
          src="/wx-xingyu.png"
 | 
			
		||||
          alt="数舵科技"
 | 
			
		||||
          class="cursor-pointer"
 | 
			
		||||
          @click="openWindow('https://shuduokeji.com')"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <p class="mt-2 flex justify-center pt-4 text-sm italic">
 | 
			
		||||
        本项目采用<Badge variant="destructive">MIT</Badge>
 | 
			
		||||
        本项目采用 <Badge class="mx-2" variant="destructive">MIT</Badge>
 | 
			
		||||
        开源协议,个人与企业可100% 免费使用。
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,9 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { IconifyIcon } from '@vben/icons';
 | 
			
		||||
import { $t } from '@vben/locales';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  DropdownMenu,
 | 
			
		||||
| 
						 | 
				
			
			@ -47,15 +50,17 @@ async function handleChange(id: number | undefined) {
 | 
			
		|||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <DropdownMenu>
 | 
			
		||||
    <DropdownMenuTrigger as-child>
 | 
			
		||||
    <DropdownMenuTrigger>
 | 
			
		||||
      <Button
 | 
			
		||||
        variant="outline"
 | 
			
		||||
        class="hover:bg-accent ml-1 mr-2 h-8 w-24 cursor-pointer rounded-full p-1.5"
 | 
			
		||||
        class="hover:bg-accent ml-1 mr-2 h-8 w-32 cursor-pointer rounded-full p-1.5"
 | 
			
		||||
      >
 | 
			
		||||
        {{ tenants.find((item) => item.id === visitTenantId)?.name }}
 | 
			
		||||
        <IconifyIcon icon="lucide:align-justify" class="mr-4" />
 | 
			
		||||
        {{ $t('page.tenant.placeholder') }}
 | 
			
		||||
        <!-- {{ tenants.find((item) => item.id === visitTenantId)?.name }} -->
 | 
			
		||||
      </Button>
 | 
			
		||||
    </DropdownMenuTrigger>
 | 
			
		||||
    <DropdownMenuContent class="w-56 p-0 pb-1">
 | 
			
		||||
    <DropdownMenuContent class="w-40 p-0 pb-1">
 | 
			
		||||
      <DropdownMenuGroup>
 | 
			
		||||
        <DropdownMenuItem
 | 
			
		||||
          v-for="tenant in tenants"
 | 
			
		||||
| 
						 | 
				
			
			@ -64,7 +69,13 @@ async function handleChange(id: number | undefined) {
 | 
			
		|||
          class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
 | 
			
		||||
          @click="handleChange(tenant.id)"
 | 
			
		||||
        >
 | 
			
		||||
          {{ tenant.name }}
 | 
			
		||||
          <template v-if="tenant.id === visitTenantId">
 | 
			
		||||
            <IconifyIcon icon="lucide:check" class="mr-2" />
 | 
			
		||||
            {{ tenant.name }}
 | 
			
		||||
          </template>
 | 
			
		||||
          <template v-else>
 | 
			
		||||
            {{ tenant.name }}
 | 
			
		||||
          </template>
 | 
			
		||||
        </DropdownMenuItem>
 | 
			
		||||
      </DropdownMenuGroup>
 | 
			
		||||
    </DropdownMenuContent>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,13 +3,16 @@ import type {
 | 
			
		|||
  BarSeriesOption,
 | 
			
		||||
  GaugeSeriesOption,
 | 
			
		||||
  LineSeriesOption,
 | 
			
		||||
  MapSeriesOption,
 | 
			
		||||
} from 'echarts/charts';
 | 
			
		||||
import type {
 | 
			
		||||
  DatasetComponentOption,
 | 
			
		||||
  GeoComponentOption,
 | 
			
		||||
  GridComponentOption,
 | 
			
		||||
  // 组件类型的定义后缀都为 ComponentOption
 | 
			
		||||
  TitleComponentOption,
 | 
			
		||||
  TooltipComponentOption,
 | 
			
		||||
  VisualMapComponentOption,
 | 
			
		||||
} from 'echarts/components';
 | 
			
		||||
import type { ComposeOption } from 'echarts/core';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -17,12 +20,14 @@ import {
 | 
			
		|||
  BarChart,
 | 
			
		||||
  GaugeChart,
 | 
			
		||||
  LineChart,
 | 
			
		||||
  MapChart,
 | 
			
		||||
  PieChart,
 | 
			
		||||
  RadarChart,
 | 
			
		||||
} from 'echarts/charts';
 | 
			
		||||
import {
 | 
			
		||||
  // 数据集组件
 | 
			
		||||
  DatasetComponent,
 | 
			
		||||
  GeoComponent,
 | 
			
		||||
  GridComponent,
 | 
			
		||||
  LegendComponent,
 | 
			
		||||
  TitleComponent,
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +35,7 @@ import {
 | 
			
		|||
  TooltipComponent,
 | 
			
		||||
  // 内置数据转换器组件 (filter, sort)
 | 
			
		||||
  TransformComponent,
 | 
			
		||||
  VisualMapComponent,
 | 
			
		||||
} from 'echarts/components';
 | 
			
		||||
import * as echarts from 'echarts/core';
 | 
			
		||||
import { LabelLayout, UniversalTransition } from 'echarts/features';
 | 
			
		||||
| 
						 | 
				
			
			@ -40,10 +46,13 @@ export type ECOption = ComposeOption<
 | 
			
		|||
  | BarSeriesOption
 | 
			
		||||
  | DatasetComponentOption
 | 
			
		||||
  | GaugeSeriesOption
 | 
			
		||||
  | GeoComponentOption
 | 
			
		||||
  | GridComponentOption
 | 
			
		||||
  | LineSeriesOption
 | 
			
		||||
  | MapSeriesOption
 | 
			
		||||
  | TitleComponentOption
 | 
			
		||||
  | TooltipComponentOption
 | 
			
		||||
  | VisualMapComponentOption
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
// 注册必须的组件
 | 
			
		||||
| 
						 | 
				
			
			@ -63,6 +72,9 @@ echarts.use([
 | 
			
		|||
  CanvasRenderer,
 | 
			
		||||
  LegendComponent,
 | 
			
		||||
  ToolboxComponent,
 | 
			
		||||
  VisualMapComponent,
 | 
			
		||||
  MapChart,
 | 
			
		||||
  GeoComponent,
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
export default echarts;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
				
			
			@ -19,6 +19,7 @@ import {
 | 
			
		|||
} from '@vueuse/core';
 | 
			
		||||
 | 
			
		||||
import echarts from './echarts';
 | 
			
		||||
import chinaMap from './map/china.json';
 | 
			
		||||
 | 
			
		||||
type EchartsUIType = typeof EchartsUI | undefined;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +33,18 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
 | 
			
		|||
  const { height, width } = useWindowSize();
 | 
			
		||||
  const resizeHandler: () => void = useDebounceFn(resize, 200);
 | 
			
		||||
 | 
			
		||||
  echarts.registerMap('china', {
 | 
			
		||||
    geoJSON: chinaMap as any,
 | 
			
		||||
    specialAreas: {
 | 
			
		||||
      china: {
 | 
			
		||||
        left: 500,
 | 
			
		||||
        top: 500,
 | 
			
		||||
        width: 1000,
 | 
			
		||||
        height: 1000,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const getOptions = computed((): EChartsOption => {
 | 
			
		||||
    if (!isDark.value) {
 | 
			
		||||
      return {};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue