Pre Merge pull request !155 from xingyu/dev
commit
edc4b5ca4d
|
|
@ -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