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 58460358..8fa4e871 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 00000000..0ba322b8
--- /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 54279cec..2d59e23c 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 6b06aa4c..631a40b0 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 793f187c..84e447c0 100644
--- a/src/views/crm/business/index.vue
+++ b/src/views/crm/business/index.vue
@@ -5,35 +5,43 @@
- 搜索
- 重置
-
- 新增
+
+
+ 搜索
+
+
+
+ 重置
+
+
+
+ 新增
- 导出
+
+ 导出
@@ -46,8 +54,8 @@
-
-
+
+
{{ scope.row.name }}
@@ -66,17 +74,17 @@
@@ -97,49 +105,49 @@
width="180px"
/>
-
+
编辑
删除
@@ -148,9 +156,9 @@
@@ -159,7 +167,7 @@
-
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 00000000..7579cb64
--- /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 00000000..b8cddf8d
--- /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 f31c7963..8ccd52c8 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 d1f3c173..d4269932 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 2f81c0fc..653feef8 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 af1708fc..ade6445d 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 88793837..71807e17 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?.()