diff --git a/.env.local b/.env.local-dev similarity index 100% rename from .env.local rename to .env.local-dev diff --git a/package.json b/package.json index 58460358aa95a9c19e107813db32c1a187ed3cb5..8fa4e871c2d3634d61d0a65d5f55a86380baaf2c 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,11 @@ "private": false, "scripts": { "i": "pnpm install", - "dev": "vite --mode local-dev", + "local-server": "vite --mode local-dev", "dev-server": "vite --mode dev", "ts:check": "vue-tsc --noEmit", - "build:local-dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev", - "build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev", + "build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev", + "build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev", "build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test", "build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage", "build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod", diff --git a/src/api/crm/statistics/funnel.ts b/src/api/crm/statistics/funnel.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ba322b8585ec38fb7e8d57e1882fdfb519dc6d4 --- /dev/null +++ b/src/api/crm/statistics/funnel.ts @@ -0,0 +1,45 @@ +import request from '@/config/axios' + +export interface CrmStatisticFunnelRespVO { + customerCount: number // 客户数 + businessCount: number // 商机数 + winCount: number // 赢单数 +} + +export interface CrmStatisticsBusinessSummaryByDateRespVO { + time: string // 时间 + businessCreateCount: number // 商机数 + businessDealCount: number // 商机金额 +} + +// 客户分析 API +export const StatisticFunnelApi = { + // 1. 获取销售漏斗统计数据 + getFunnelSummary: (params: any) => { + return request.get({ + url: '/crm/statistics-funnel/get-funnel-summary', + params + }) + }, + // 2. 获取商机结束状态统计 + getBusinessEndStatusSummary: (params: any) => { + return request.get({ + url: '/crm/statistics-funnel/get-business-end-status-summary', + params + }) + }, + // 3. 获取新增商机分析(按日期) + getBusinessSummaryByDate: (params: any) => { + return request.get({ + url: '/crm/statistics-funnel/get-business-summary-by-date', + params + }) + }, + // 4. 获取商机列表(按日期) + getBusinessPageByDate: (params: any) => { + return request.get({ + url: '/crm/statistics-funnel/get-business-page-by-date', + params + }) + } +} diff --git a/src/components/DictSelect/src/DictSelect.vue b/src/components/DictSelect/src/DictSelect.vue index 54279cec7a2dd8358aee7accbc7d0732cdd15708..2d59e23c9722e1c9f4786093e623cc39cdffc017 100644 --- a/src/components/DictSelect/src/DictSelect.vue +++ b/src/components/DictSelect/src/DictSelect.vue @@ -33,7 +33,6 @@ import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/util // 接受父组件参数 interface Props { - modelValue?: any // 值 dictType: string // 字典类型 valueType: string // 字典值类型 } diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 6b06aa4c393a8fd7a0207ab4d368d55eb51dc108..631a40b06e46122ff54f23ec1b58174861460679 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -197,14 +197,15 @@ export enum DICT_TYPE { // ========== CRM - 客户管理模块 ========== CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 审批状态 CRM_BIZ_TYPE = 'crm_biz_type', // CRM 业务类型 + CRM_BUSINESS_END_STATUS_TYPE = 'crm_business_end_status_type', // CRM 商机结束状态类型 CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 回款的还款方式 - CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', - CRM_CUSTOMER_LEVEL = 'crm_customer_level', - CRM_CUSTOMER_SOURCE = 'crm_customer_source', - CRM_PRODUCT_STATUS = 'crm_product_status', + CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', // CRM 客户所属行业 + CRM_CUSTOMER_LEVEL = 'crm_customer_level', // CRM 客户级别 + CRM_CUSTOMER_SOURCE = 'crm_customer_source', // CRM 客户来源 + CRM_PRODUCT_STATUS = 'crm_product_status', // CRM 商品状态 CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 数据权限的级别 - CRM_PRODUCT_UNIT = 'crm_product_unit', // 产品单位 - CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // 跟进方式 + CRM_PRODUCT_UNIT = 'crm_product_unit', // CRM 产品单位 + CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // CRM 跟进方式 // ========== ERP - 企业资源计划模块 ========== ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态 diff --git a/src/views/crm/business/index.vue b/src/views/crm/business/index.vue index 793f187c2e47118afe56cd4d99d46d195082d597..84e447c0044883b2e9f17475f7b012f6179dda82 100644 --- a/src/views/crm/business/index.vue +++ b/src/views/crm/business/index.vue @@ -5,35 +5,43 @@ - 搜索 - 重置 - - 新增 + + + 搜索 + + + + 重置 + + + + 新增 - 导出 + + 导出 @@ -46,8 +54,8 @@ - - + + @@ -97,49 +105,49 @@ width="180px" /> - + - diff --git a/src/views/crm/statistics/funnel/components/FunnelBusiness.vue b/src/views/crm/statistics/funnel/components/FunnelBusiness.vue new file mode 100644 index 0000000000000000000000000000000000000000..7579cb64a0ab325f60a75e1e86a3c943c80b5e9c --- /dev/null +++ b/src/views/crm/statistics/funnel/components/FunnelBusiness.vue @@ -0,0 +1,135 @@ + + + diff --git a/src/views/crm/statistics/funnel/index.vue b/src/views/crm/statistics/funnel/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..b8cddf8dda271d387f6f5a0dc3ad3960e5a4e0f9 --- /dev/null +++ b/src/views/crm/statistics/funnel/index.vue @@ -0,0 +1,165 @@ + + + + diff --git a/src/views/crm/statistics/portrait/components/CustomerAddress.vue b/src/views/crm/statistics/portrait/components/PortraitCustomerArea.vue similarity index 88% rename from src/views/crm/statistics/portrait/components/CustomerAddress.vue rename to src/views/crm/statistics/portrait/components/PortraitCustomerArea.vue index f31c79633ef8b473ad7f5872fc944e2430d00fc0..8ccd52c82dc92c011939f2d594786ac559bfc9d5 100644 --- a/src/views/crm/statistics/portrait/components/CustomerAddress.vue +++ b/src/views/crm/statistics/portrait/components/PortraitCustomerArea.vue @@ -25,8 +25,7 @@ import { StatisticsPortraitApi } from '@/api/crm/statistics/portrait' -// TODO @puhui999:address 换成 area 会更合适哈, -defineOptions({ name: 'CustomerAddress' }) +defineOptions({ name: 'PortraitCustomerArea' }) const props = defineProps<{ queryParams: any }>() // 搜索参数 // 注册地图 @@ -107,22 +106,21 @@ const loadData = async () => { areaStatisticsList.value = areaList.map((item: CrmStatisticCustomerAreaRespVO) => { return { ...item, - areaName: item.areaName // TODO @puhui999:这里最好注释下原因哈 - .replace('维吾尔自治区', '') - .replace('壮族自治区', '') - .replace('回族自治区', '') - .replace('自治区', '') - .replace('省', '') + areaName: item.areaName // TODO @puhui999:这里最好注释下原因哈, 🤣 我从 mall copy 过来的 + // .replace('维吾尔自治区', '') + // .replace('壮族自治区', '') + // .replace('回族自治区', '') + // .replace('自治区', '') + // .replace('省', '') } }) - builderLeftMap() - builderRightMap() + buildLeftMap() + buildRightMap() loading.value = false } defineExpose({ loadData }) -// TODO @puhui999:builder 改成 build 更合理哈 -const builderLeftMap = () => { +const buildLeftMap = () => { let min = 0 let max = 0 echartsOption.series![0].data = areaStatisticsList.value.map((item) => { @@ -134,7 +132,7 @@ const builderLeftMap = () => { echartsOption.visualMap!['max'] = max } -const builderRightMap = () => { +const buildRightMap = () => { let min = 0 let max = 0 echartsOption2.series![0].data = areaStatisticsList.value.map((item) => { diff --git a/src/views/crm/statistics/portrait/components/CustomerIndustry.vue b/src/views/crm/statistics/portrait/components/PortraitCustomerIndustry.vue similarity index 94% rename from src/views/crm/statistics/portrait/components/CustomerIndustry.vue rename to src/views/crm/statistics/portrait/components/PortraitCustomerIndustry.vue index d1f3c173965d61ff8c118403070466c49ebb88e6..d42699322f2183529ec156fecfc804be32586c3d 100644 --- a/src/views/crm/statistics/portrait/components/CustomerIndustry.vue +++ b/src/views/crm/statistics/portrait/components/PortraitCustomerIndustry.vue @@ -39,10 +39,10 @@ import { } from '@/api/crm/statistics/portrait' import { EChartsOption } from 'echarts' import { DICT_TYPE, getDictLabel } from '@/utils/dict' -import { getSumValue } from '@/utils' +import { erpCalculatePercentage, getSumValue } from '@/utils' import { isEmpty } from '@/utils/is' -defineOptions({ name: 'CustomerIndustry' }) +defineOptions({ name: 'PortraitCustomerIndustry' }) const props = defineProps<{ queryParams: any }>() // 搜索参数 const loading = ref(false) // 加载中 @@ -185,8 +185,9 @@ const calculateProportion = (sourceList: CrmStatisticCustomerIndustryRespVO[]) = const sumDealCount = getSumValue(list.map((item) => item.dealCount)) list.forEach((item) => { item.industryPortion = - item.customerCount === 0 ? 0 : ((item.customerCount / sumCustomerCount) * 100).toFixed(2) - item.dealPortion = item.dealCount === 0 ? 0 : ((item.dealCount / sumDealCount) * 100).toFixed(2) + item.customerCount === 0 ? 0 : erpCalculatePercentage(item.customerCount, sumCustomerCount) + item.dealPortion = + item.dealCount === 0 ? 0 : erpCalculatePercentage(item.dealCount, sumDealCount) }) } diff --git a/src/views/crm/statistics/portrait/components/CustomerLevel.vue b/src/views/crm/statistics/portrait/components/PortraitCustomerLevel.vue similarity index 93% rename from src/views/crm/statistics/portrait/components/CustomerLevel.vue rename to src/views/crm/statistics/portrait/components/PortraitCustomerLevel.vue index 2f81c0fc1670867a870d7c221d77b9a859c2e18b..653feef8b99df0bd88dd0f225a11fbc80a90a24d 100644 --- a/src/views/crm/statistics/portrait/components/CustomerLevel.vue +++ b/src/views/crm/statistics/portrait/components/PortraitCustomerLevel.vue @@ -39,10 +39,10 @@ import { } from '@/api/crm/statistics/portrait' import { EChartsOption } from 'echarts' import { DICT_TYPE, getDictLabel } from '@/utils/dict' -import { getSumValue } from '@/utils' +import { erpCalculatePercentage, getSumValue } from '@/utils' import { isEmpty } from '@/utils/is' -defineOptions({ name: 'CustomerSource' }) +defineOptions({ name: 'PortraitCustomerLevel' }) const props = defineProps<{ queryParams: any }>() // 搜索参数 const loading = ref(false) // 加载中 @@ -184,10 +184,10 @@ const calculateProportion = (levelList: CrmStatisticCustomerLevelRespVO[]) => { const sumCustomerCount = getSumValue(list.map((item) => item.customerCount)) const sumDealCount = getSumValue(list.map((item) => item.dealCount)) list.forEach((item) => { - // TODO @puhui999:可以使用 erpCalculatePercentage 方法 item.levelPortion = - item.customerCount === 0 ? 0 : ((item.customerCount / sumCustomerCount) * 100).toFixed(2) - item.dealPortion = item.dealCount === 0 ? 0 : ((item.dealCount / sumDealCount) * 100).toFixed(2) + item.customerCount === 0 ? 0 : erpCalculatePercentage(item.customerCount, sumCustomerCount) + item.dealPortion = + item.dealCount === 0 ? 0 : erpCalculatePercentage(item.dealCount, sumDealCount) }) } diff --git a/src/views/crm/statistics/portrait/components/CustomerSource.vue b/src/views/crm/statistics/portrait/components/PortraitCustomerSource.vue similarity index 94% rename from src/views/crm/statistics/portrait/components/CustomerSource.vue rename to src/views/crm/statistics/portrait/components/PortraitCustomerSource.vue index af1708fcc8f928d182d4001de71f167c8c896266..ade6445de60c672eec7226450a06fb584ec1711d 100644 --- a/src/views/crm/statistics/portrait/components/CustomerSource.vue +++ b/src/views/crm/statistics/portrait/components/PortraitCustomerSource.vue @@ -40,9 +40,9 @@ import { import { EChartsOption } from 'echarts' import { DICT_TYPE, getDictLabel } from '@/utils/dict' import { isEmpty } from '@/utils/is' -import { getSumValue } from '@/utils' +import { erpCalculatePercentage, getSumValue } from '@/utils' -defineOptions({ name: 'CustomerSource' }) +defineOptions({ name: 'PortraitCustomerSource' }) const props = defineProps<{ queryParams: any }>() // 搜索参数 const loading = ref(false) // 加载中 @@ -185,8 +185,9 @@ const calculateProportion = (sourceList: CrmStatisticCustomerSourceRespVO[]) => const sumDealCount = getSumValue(list.map((item) => item.dealCount)) list.forEach((item) => { item.sourcePortion = - item.customerCount === 0 ? 0 : ((item.customerCount / sumCustomerCount) * 100).toFixed(2) - item.dealPortion = item.dealCount === 0 ? 0 : ((item.dealCount / sumDealCount) * 100).toFixed(2) + item.customerCount === 0 ? 0 : erpCalculatePercentage(item.customerCount, sumCustomerCount) + item.dealPortion = + item.dealCount === 0 ? 0 : erpCalculatePercentage(item.dealCount, sumDealCount) }) } diff --git a/src/views/crm/statistics/portrait/index.vue b/src/views/crm/statistics/portrait/index.vue index 88793837196d2451c2651d5136935dc62510034e..71807e1762abe91cc3a9af8ab4ef710a82b15c7d 100644 --- a/src/views/crm/statistics/portrait/index.vue +++ b/src/views/crm/statistics/portrait/index.vue @@ -60,20 +60,20 @@ - - + + - + - + - + @@ -85,11 +85,10 @@ import * as UserApi from '@/api/system/user' import { useUserStore } from '@/store/modules/user' import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime' import { defaultProps, handleTree } from '@/utils/tree' -// TODO @puhui999:最好命名带上模块名,如:CrmStatisticsPortrait -import CustomerAddress from './components/CustomerAddress.vue' -import CustomerIndustry from './components/CustomerIndustry.vue' -import CustomerSource from './components/CustomerSource.vue' -import CustomerLevel from './components/CustomerLevel.vue' +import PortraitCustomerArea from './components/PortraitCustomerArea.vue' +import PortraitCustomerIndustry from './components/PortraitCustomerIndustry.vue' +import PortraitCustomerSource from './components/PortraitCustomerSource.vue' +import PortraitCustomerLevel from './components/PortraitCustomerLevel.vue' defineOptions({ name: 'CrmStatisticsPortrait' }) @@ -114,8 +113,8 @@ const userListByDeptId = computed(() => : [] ) -const activeTab = ref('addressRef') // 活跃标签 -const addressRef = ref() // 客户地区分布 +const activeTab = ref('areaRef') // 活跃标签 +const areaRef = ref() // 客户地区分布 const levelRef = ref() // 客户级别 const sourceRef = ref() // 客户来源 const industryRef = ref() // 客户行业 @@ -123,8 +122,8 @@ const industryRef = ref() // 客户行业 /** 搜索按钮操作 */ const handleQuery = () => { switch (activeTab.value) { - case 'addressRef': - addressRef.value?.loadData?.() + case 'areaRef': + areaRef.value?.loadData?.() break case 'levelRef': levelRef.value?.loadData?.()