diff --git a/package.json b/package.json index 58460358..c7ef3d40 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,6 @@ "url": "https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues" }, "homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3", - "packageManager": "pnpm@8.6.0", "engines": { "node": ">= 16.0.0", "pnpm": ">=8.6.0" diff --git a/src/api/crm/statistics/customer.ts b/src/api/crm/statistics/customer.ts index 815fe653..c2092e48 100644 --- a/src/api/crm/statistics/customer.ts +++ b/src/api/crm/statistics/customer.ts @@ -67,6 +67,18 @@ export interface CrmStatisticsCustomerDealCycleByUserRespVO { customerDealCount: number } +export interface CrmStatisticsCustomerDealCycleByAreaRespVO { + areaName: string + customerDealCycle: number + customerDealCount: number +} + +export interface CrmStatisticsCustomerDealCycleByProductRespVO { + productName: string + customerDealCycle: number + customerDealCount: number +} + // 客户分析 API export const StatisticsCustomerApi = { // 1.1 客户总量分析(按日期) @@ -138,5 +150,19 @@ export const StatisticsCustomerApi = { url: '/crm/statistics-customer/get-customer-deal-cycle-by-user', params }) + }, + // 6.2 获取客户成交周期(按用户) + getCustomerDealCycleByArea: (params: any) => { + return request.get({ + url: '/crm/statistics-customer/get-customer-deal-cycle-by-area', + params + }) + }, + // 6.2 获取客户成交周期(按用户) + getCustomerDealCycleByProduct: (params: any) => { + return request.get({ + url: '/crm/statistics-customer/get-customer-deal-cycle-by-product', + params + }) } } diff --git a/src/api/crm/statistics/funnel.ts b/src/api/crm/statistics/funnel.ts new file mode 100644 index 00000000..574a5f4f --- /dev/null +++ b/src/api/crm/statistics/funnel.ts @@ -0,0 +1,58 @@ +import request from '@/config/axios' + +export interface CrmStatisticFunnelRespVO { + customerCount: number // 客户数 + businessCount: number // 商机数 + businessWinCount: number // 赢单数 +} + +export interface CrmStatisticsBusinessSummaryByDateRespVO { + time: string // 时间 + businessCreateCount: number // 商机数 + totalPrice: number | string // 商机金额 +} + +export interface CrmStatisticsBusinessInversionRateSummaryByDateRespVO { + time: string // 时间 + businessCount: number // 商机数量 + businessWinCount: number // 赢单商机数 +} + +// 客户分析 API +export const StatisticFunnelApi = { + // 1. 获取销售漏斗统计数据 + getFunnelSummary: (params: any) => { + return request.get({ + url: '/crm/statistics-funnel/get-funnel-summary', + params + }) + }, + // 2. 获取商机结束状态统计 + getBusinessSummaryByEndStatus: (params: any) => { + return request.get({ + url: '/crm/statistics-funnel/get-business-summary-by-end-status', + params + }) + }, + // 3. 获取新增商机分析(按日期) + getBusinessSummaryByDate: (params: any) => { + return request.get({ + url: '/crm/statistics-funnel/get-business-summary-by-date', + params + }) + }, + // 4. 获取商机转化率分析(按日期) + getBusinessInversionRateSummaryByDate: (params: any) => { + return request.get({ + url: '/crm/statistics-funnel/get-business-inversion-rate-summary-by-date', + params + }) + }, + // 5. 获取商机列表(按日期) + getBusinessPageByDate: (params: any) => { + return request.get({ + url: '/crm/statistics-funnel/get-business-page-by-date', + params + }) + } +} diff --git a/src/api/infra/codegen/index.ts b/src/api/infra/codegen/index.ts new file mode 100644 index 00000000..441ca83b --- /dev/null +++ b/src/api/infra/codegen/index.ts @@ -0,0 +1,122 @@ +import request from '@/config/axios' + +export type CodegenTableVO = { + id: number + tableId: number + isParentMenuIdValid: boolean + dataSourceConfigId: number + scene: number + tableName: string + tableComment: string + remark: string + moduleName: string + businessName: string + className: string + classComment: string + author: string + createTime: Date + updateTime: Date + templateType: number + parentMenuId: number +} + +export type CodegenColumnVO = { + id: number + tableId: number + columnName: string + dataType: string + columnComment: string + nullable: number + primaryKey: number + ordinalPosition: number + javaType: string + javaField: string + dictType: string + example: string + createOperation: number + updateOperation: number + listOperation: number + listOperationCondition: string + listOperationResult: number + htmlType: string +} + +export type DatabaseTableVO = { + name: string + comment: string +} + +export type CodegenDetailVO = { + table: CodegenTableVO + columns: CodegenColumnVO[] +} + +export type CodegenPreviewVO = { + filePath: string + code: string +} + +export type CodegenUpdateReqVO = { + table: CodegenTableVO | any + columns: CodegenColumnVO[] +} + +export type CodegenCreateListReqVO = { + dataSourceConfigId: number + tableNames: string[] +} + +// 查询列表代码生成表定义 +export const getCodegenTableList = (dataSourceConfigId: number) => { + return request.get({ url: '/infra/codegen/table/list?dataSourceConfigId=' + dataSourceConfigId }) +} + +// 查询列表代码生成表定义 +export const getCodegenTablePage = (params: PageParam) => { + return request.get({ url: '/infra/codegen/table/page', params }) +} + +// 查询详情代码生成表定义 +export const getCodegenTable = (id: number) => { + return request.get({ url: '/infra/codegen/detail?tableId=' + id }) +} + +// 新增代码生成表定义 +export const createCodegenTable = (data: CodegenCreateListReqVO) => { + return request.post({ url: '/infra/codegen/create', data }) +} + +// 修改代码生成表定义 +export const updateCodegenTable = (data: CodegenUpdateReqVO) => { + return request.put({ url: '/infra/codegen/update', data }) +} + +// 基于数据库的表结构,同步数据库的表和字段定义 +export const syncCodegenFromDB = (id: number) => { + return request.put({ url: '/infra/codegen/sync-from-db?tableId=' + id }) +} + +// 预览生成代码 +export const previewCodegen = (id: number) => { + return request.get({ url: '/infra/codegen/preview?tableId=' + id }) +} + +// 下载生成代码 +export const downloadCodegen = (id: number) => { + return request.download({ url: '/infra/codegen/download?tableId=' + id }) +} + +// 获得表定义 +export const getSchemaTableList = (params) => { + return request.get({ url: '/infra/codegen/db/table/list', params }) +} + +// 基于数据库的表结构,创建代码生成器的表定义 +export const createCodegenList = (data) => { + return request.post({ url: '/infra/codegen/create-list', data }) +} + +// 删除代码生成表定义 +export const deleteCodegenTable = (id: number) => { + return request.delete({ url: '/infra/codegen/delete?tableId=' + id }) +} 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/components/FormCreate/index.ts b/src/components/FormCreate/index.ts index 51b52603..d50ed3c6 100644 --- a/src/components/FormCreate/index.ts +++ b/src/components/FormCreate/index.ts @@ -1,4 +1,3 @@ -import MyFormCreateDesigner from './src/MyFormCreateDesigner.vue' import { useFormCreateDesigner } from './src/useFormCreateDesigner' -export { MyFormCreateDesigner, useFormCreateDesigner } +export { useFormCreateDesigner } diff --git a/src/components/FormCreate/src/MyFormCreateDesigner.vue b/src/components/FormCreate/src/MyFormCreateDesigner.vue deleted file mode 100644 index f202b9cb..00000000 --- a/src/components/FormCreate/src/MyFormCreateDesigner.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - diff --git a/src/components/FormCreate/src/config/index.ts b/src/components/FormCreate/src/config/index.ts index 5403ce0e..c3939159 100644 --- a/src/components/FormCreate/src/config/index.ts +++ b/src/components/FormCreate/src/config/index.ts @@ -3,11 +3,13 @@ import { useUploadImgRule } from './useUploadImgRule' import { useUploadImgsRule } from './useUploadImgsRule' import { useDictSelectRule } from './useDictSelectRule' import { useUserSelectRule } from './useUserSelectRule' +import { useEditorRule } from './useEditorRule' export { useUploadFileRule, useUploadImgRule, useUploadImgsRule, useDictSelectRule, - useUserSelectRule + useUserSelectRule, + useEditorRule } diff --git a/src/components/FormCreate/src/config/selectRule.ts b/src/components/FormCreate/src/config/selectRule.ts new file mode 100644 index 00000000..0974139e --- /dev/null +++ b/src/components/FormCreate/src/config/selectRule.ts @@ -0,0 +1,71 @@ +const selectRule = [ + { type: 'switch', field: 'multiple', title: '是否多选' }, + { + type: 'switch', + field: 'disabled', + title: '是否禁用' + }, + { type: 'switch', field: 'clearable', title: '是否可以清空选项' }, + { + type: 'switch', + field: 'collapseTags', + title: '多选时是否将选中值按文字的形式展示' + }, + { + type: 'inputNumber', + field: 'multipleLimit', + title: '多选时用户最多可以选择的项目数,为 0 则不限制', + props: { min: 0 } + }, + { + type: 'input', + field: 'autocomplete', + title: 'autocomplete 属性' + }, + { type: 'input', field: 'placeholder', title: '占位符' }, + { + type: 'switch', + field: 'filterable', + title: '是否可搜索' + }, + { type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' }, + { + type: 'input', + field: 'noMatchText', + title: '搜索条件无匹配时显示的文字' + }, + { + type: 'switch', + field: 'remote', + title: '其中的选项是否从服务器远程加载' + }, + { + type: 'Struct', + field: 'remoteMethod', + title: '自定义远程搜索方法' + }, + { type: 'input', field: 'noDataText', title: '选项为空时显示的文字' }, + { + type: 'switch', + field: 'reserveKeyword', + title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词' + }, + { + type: 'switch', + field: 'defaultFirstOption', + title: '在输入框按下回车,选择第一个匹配项' + }, + { + type: 'switch', + field: 'popperAppendToBody', + title: '是否将弹出框插入至 body 元素', + value: true + }, + { + type: 'switch', + field: 'automaticDropdown', + title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单' + } +] + +export default selectRule diff --git a/src/components/FormCreate/src/config/useDictSelectRule.ts b/src/components/FormCreate/src/config/useDictSelectRule.ts index 51cd33b7..7a1afc60 100644 --- a/src/components/FormCreate/src/config/useDictSelectRule.ts +++ b/src/components/FormCreate/src/config/useDictSelectRule.ts @@ -1,6 +1,7 @@ import { generateUUID } from '@/utils' import * as DictDataApi from '@/api/system/dict/dict.type' import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' +import selectRule from '@/components/FormCreate/src/config/selectRule' export const useDictSelectRule = () => { const label = '字典选择器' @@ -51,73 +52,7 @@ export const useDictSelectRule = () => { { label: '布尔值', value: 'bool' } ] }, - { type: 'switch', field: 'multiple', title: '是否多选' }, - { - type: 'switch', - field: 'disabled', - title: '是否禁用' - }, - { type: 'switch', field: 'clearable', title: '是否可以清空选项' }, - { - type: 'switch', - field: 'collapseTags', - title: '多选时是否将选中值按文字的形式展示' - }, - { - type: 'inputNumber', - field: 'multipleLimit', - title: '多选时用户最多可以选择的项目数,为 0 则不限制', - props: { min: 0 } - }, - { - type: 'input', - field: 'autocomplete', - title: 'autocomplete 属性' - }, - { type: 'input', field: 'placeholder', title: '占位符' }, - { - type: 'switch', - field: 'filterable', - title: '是否可搜索' - }, - { type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' }, - { - type: 'input', - field: 'noMatchText', - title: '搜索条件无匹配时显示的文字' - }, - { - type: 'switch', - field: 'remote', - title: '其中的选项是否从服务器远程加载' - }, - { - type: 'Struct', - field: 'remoteMethod', - title: '自定义远程搜索方法' - }, - { type: 'input', field: 'noDataText', title: '选项为空时显示的文字' }, - { - type: 'switch', - field: 'reserveKeyword', - title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词' - }, - { - type: 'switch', - field: 'defaultFirstOption', - title: '在输入框按下回车,选择第一个匹配项' - }, - { - type: 'switch', - field: 'popperAppendToBody', - title: '是否将弹出框插入至 body 元素', - value: true - }, - { - type: 'switch', - field: 'automaticDropdown', - title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单' - } + ...selectRule ]) } } diff --git a/src/components/FormCreate/src/config/useEditorRule.ts b/src/components/FormCreate/src/config/useEditorRule.ts new file mode 100644 index 00000000..ac6d9ac0 --- /dev/null +++ b/src/components/FormCreate/src/config/useEditorRule.ts @@ -0,0 +1,32 @@ +import { generateUUID } from '@/utils' +import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' + +export const useEditorRule = () => { + const label = '富文本' + const name = 'Editor' + return { + icon: 'icon-editor', + label, + name, + rule() { + return { + type: name, + field: generateUUID(), + title: label, + info: '', + $required: false + } + }, + props(_, { t }) { + return localeProps(t, name + '.props', [ + makeRequiredRule(), + { + type: 'input', + field: 'height', + title: '高度' + }, + { type: 'switch', field: 'readonly', title: '是否只读' } + ]) + } + } +} diff --git a/src/components/FormCreate/src/config/useUserSelectRule.ts b/src/components/FormCreate/src/config/useUserSelectRule.ts index 07f70836..dd5e51c3 100644 --- a/src/components/FormCreate/src/config/useUserSelectRule.ts +++ b/src/components/FormCreate/src/config/useUserSelectRule.ts @@ -1,5 +1,6 @@ import { generateUUID } from '@/utils' import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' +import selectRule from '@/components/FormCreate/src/config/selectRule' export const useUserSelectRule = () => { const label = '用户选择器' @@ -18,76 +19,7 @@ export const useUserSelectRule = () => { } }, props(_, { t }) { - return localeProps(t, name + '.props', [ - makeRequiredRule(), - { type: 'switch', field: 'multiple', title: '是否多选' }, - { - type: 'switch', - field: 'disabled', - title: '是否禁用' - }, - { type: 'switch', field: 'clearable', title: '是否可以清空选项' }, - { - type: 'switch', - field: 'collapseTags', - title: '多选时是否将选中值按文字的形式展示' - }, - { - type: 'inputNumber', - field: 'multipleLimit', - title: '多选时用户最多可以选择的项目数,为 0 则不限制', - props: { min: 0 } - }, - { - type: 'input', - field: 'autocomplete', - title: 'autocomplete 属性' - }, - { type: 'input', field: 'placeholder', title: '占位符' }, - { - type: 'switch', - field: 'filterable', - title: '是否可搜索' - }, - { type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' }, - { - type: 'input', - field: 'noMatchText', - title: '搜索条件无匹配时显示的文字' - }, - { - type: 'switch', - field: 'remote', - title: '其中的选项是否从服务器远程加载' - }, - { - type: 'Struct', - field: 'remoteMethod', - title: '自定义远程搜索方法' - }, - { type: 'input', field: 'noDataText', title: '选项为空时显示的文字' }, - { - type: 'switch', - field: 'reserveKeyword', - title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词' - }, - { - type: 'switch', - field: 'defaultFirstOption', - title: '在输入框按下回车,选择第一个匹配项' - }, - { - type: 'switch', - field: 'popperAppendToBody', - title: '是否将弹出框插入至 body 元素', - value: true - }, - { - type: 'switch', - field: 'automaticDropdown', - title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单' - } - ]) + return localeProps(t, name + '.props', [makeRequiredRule(), ...selectRule]) } } } diff --git a/src/components/FormCreate/src/useFormCreateDesigner.ts b/src/components/FormCreate/src/useFormCreateDesigner.ts index e142970d..fe42b24f 100644 --- a/src/components/FormCreate/src/useFormCreateDesigner.ts +++ b/src/components/FormCreate/src/useFormCreateDesigner.ts @@ -1,5 +1,6 @@ import { useDictSelectRule, + useEditorRule, useUploadFileRule, useUploadImgRule, useUploadImgsRule, @@ -13,8 +14,12 @@ import { Ref } from 'vue' * - 文件上传 * - 单图上传 * - 多图上传 + * - 字典选择器 + * - 系统用户选择器 + * - 富文本 */ export const useFormCreateDesigner = (designer: Ref) => { + const editorRule = useEditorRule() const uploadFileRule = useUploadFileRule() const uploadImgRule = useUploadImgRule() const uploadImgsRule = useUploadImgsRule() @@ -24,7 +29,10 @@ export const useFormCreateDesigner = (designer: Ref) => { onMounted(() => { // 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代 designer.value?.removeMenuItem('upload') + // 移除自带的富文本组件规则,使用 editorRule 替代 + designer.value?.removeMenuItem('fc-editor') const components = [ + editorRule, uploadFileRule, uploadImgRule, uploadImgsRule, diff --git a/src/plugins/formCreate/index.ts b/src/plugins/formCreate/index.ts index 2e4fc5d8..5f8428e6 100644 --- a/src/plugins/formCreate/index.ts +++ b/src/plugins/formCreate/index.ts @@ -21,6 +21,7 @@ import install from '@form-create/element-ui/auto-import' import { UploadFile, UploadImg, UploadImgs } from '@/components/UploadFile' import { DictSelect } from '@/components/DictSelect' import UserSelect from '@/views/system/user/components/UserSelect.vue' +import { Editor } from '@/components/Editor' const components = [ ElAside, @@ -39,7 +40,8 @@ const components = [ UploadImgs, UploadFile, DictSelect, - UserSelect + UserSelect, + Editor ] // 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档 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/utils/index.ts b/src/utils/index.ts index 412a8b62..274ab7ca 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -import { toNumber } from 'lodash-es' +import {toNumber} from 'lodash-es' /** * @@ -418,3 +418,20 @@ export const erpCalculatePercentage = (value: number, total: number) => { if (total === 0) return 0 return ((value / total) * 100).toFixed(2) } + +/** + * 适配 echarts map 的地名 + * + * @param areaName 地区名称 + */ +export const areaReplace = (areaName: string) => { + if (!areaName) { + return areaName + } + return areaName + .replace('维吾尔自治区', '') + .replace('壮族自治区', '') + .replace('回族自治区', '') + .replace('自治区', '') + .replace('省', '') +} 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 @@ - - + + @@ -97,49 +105,49 @@ width="180px" /> - + - diff --git a/src/views/crm/statistics/customer/components/CustomerDealCycleByProduct.vue b/src/views/crm/statistics/customer/components/CustomerDealCycleByProduct.vue new file mode 100644 index 00000000..74558d15 --- /dev/null +++ b/src/views/crm/statistics/customer/components/CustomerDealCycleByProduct.vue @@ -0,0 +1,153 @@ + + + diff --git a/src/views/crm/statistics/customer/components/CustomerDealCycle.vue b/src/views/crm/statistics/customer/components/CustomerDealCycleByUser.vue similarity index 98% rename from src/views/crm/statistics/customer/components/CustomerDealCycle.vue rename to src/views/crm/statistics/customer/components/CustomerDealCycleByUser.vue index 23ce1448..e3d877e5 100644 --- a/src/views/crm/statistics/customer/components/CustomerDealCycle.vue +++ b/src/views/crm/statistics/customer/components/CustomerDealCycleByUser.vue @@ -30,7 +30,7 @@ import { } from '@/api/crm/statistics/customer' import { EChartsOption } from 'echarts' -defineOptions({ name: 'CustomerDealCycle' }) +defineOptions({ name: 'CustomerDealCycleByUser' }) const props = defineProps<{ queryParams: any }>() // 搜索参数 diff --git a/src/views/crm/statistics/customer/index.vue b/src/views/crm/statistics/customer/index.vue index 88a324e5..207dc350 100644 --- a/src/views/crm/statistics/customer/index.vue +++ b/src/views/crm/statistics/customer/index.vue @@ -102,8 +102,14 @@ - - + + + + + + + + @@ -117,7 +123,9 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime' import { defaultProps, handleTree } from '@/utils/tree' import CustomerConversionStat from './components/CustomerConversionStat.vue' -import CustomerDealCycle from './components/CustomerDealCycle.vue' +import CustomerDealCycleByUser from './components/CustomerDealCycleByUser.vue' +import CustomerDealCycleByArea from './components/CustomerDealCycleByArea.vue' +import CustomerDealCycleByProduct from './components/CustomerDealCycleByProduct.vue' import CustomerFollowUpSummary from './components/CustomerFollowUpSummary.vue' import CustomerFollowUpType from './components/CustomerFollowUpType.vue' import CustomerSummary from './components/CustomerSummary.vue' @@ -153,7 +161,9 @@ const followUpSummaryRef = ref() // 2. 客户跟进次数分析 const followUpTypeRef = ref() // 3. 客户跟进方式分析 const conversionStatRef = ref() // 4. 客户转化率分析 const customerPoolSummaryRef = ref() // 5. 客户公海分析 -const dealCycleRef = ref() // 6. 成交周期分析 +const dealCycleByUserRef = ref() // 6. 成交周期分析(按员工) +const dealCycleByAreaRef = ref() // 7. 成交周期分析(按地区) +const dealCycleByProductRef = ref() // 8. 成交周期分析(按产品) /** 搜索按钮操作 */ const handleQuery = () => { @@ -173,8 +183,14 @@ const handleQuery = () => { case 'poolSummary': // 公海客户分析 customerPoolSummaryRef.value?.loadData?.() break - case 'dealCycle': // 成交周期分析 - dealCycleRef.value?.loadData?.() + case 'dealCycleByUser': // 成交周期分析 + dealCycleByUserRef.value?.loadData?.() + break + case 'dealCycleByArea': // 成交周期分析 + dealCycleByAreaRef.value?.loadData?.() + break + case 'dealCycleByProduct': // 成交周期分析 + dealCycleByProductRef.value?.loadData?.() break } } diff --git a/src/views/crm/statistics/funnel/components/BusinessInversionRateSummary.vue b/src/views/crm/statistics/funnel/components/BusinessInversionRateSummary.vue new file mode 100644 index 00000000..541d6fc4 --- /dev/null +++ b/src/views/crm/statistics/funnel/components/BusinessInversionRateSummary.vue @@ -0,0 +1,307 @@ + + + diff --git a/src/views/crm/statistics/funnel/components/BusinessSummary.vue b/src/views/crm/statistics/funnel/components/BusinessSummary.vue new file mode 100644 index 00000000..942a7128 --- /dev/null +++ b/src/views/crm/statistics/funnel/components/BusinessSummary.vue @@ -0,0 +1,259 @@ + + + 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..9b7b08ae --- /dev/null +++ b/src/views/crm/statistics/funnel/components/FunnelBusiness.vue @@ -0,0 +1,152 @@ + + + diff --git a/src/views/crm/statistics/funnel/index.vue b/src/views/crm/statistics/funnel/index.vue new file mode 100644 index 00000000..804cb49b --- /dev/null +++ b/src/views/crm/statistics/funnel/index.vue @@ -0,0 +1,171 @@ + + + + diff --git a/src/views/crm/statistics/portrait/components/CustomerAddress.vue b/src/views/crm/statistics/portrait/components/PortraitCustomerArea.vue similarity index 86% rename from src/views/crm/statistics/portrait/components/CustomerAddress.vue rename to src/views/crm/statistics/portrait/components/PortraitCustomerArea.vue index f31c7963..513936c9 100644 --- a/src/views/crm/statistics/portrait/components/CustomerAddress.vue +++ b/src/views/crm/statistics/portrait/components/PortraitCustomerArea.vue @@ -24,9 +24,9 @@ import { CrmStatisticCustomerAreaRespVO, StatisticsPortraitApi } from '@/api/crm/statistics/portrait' +import { areaReplace } from '@/utils' -// TODO @puhui999:address 换成 area 会更合适哈, -defineOptions({ name: 'CustomerAddress' }) +defineOptions({ name: 'PortraitCustomerArea' }) const props = defineProps<{ queryParams: any }>() // 搜索参数 // 注册地图 @@ -107,22 +107,16 @@ const loadData = async () => { areaStatisticsList.value = areaList.map((item: CrmStatisticCustomerAreaRespVO) => { return { ...item, - areaName: item.areaName // TODO @puhui999:这里最好注释下原因哈 - .replace('维吾尔自治区', '') - .replace('壮族自治区', '') - .replace('回族自治区', '') - .replace('自治区', '') - .replace('省', '') + areaName: areaReplace(item.areaName) } }) - 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 +128,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?.() diff --git a/src/views/infra/apiAccessLog/index.vue b/src/views/infra/apiAccessLog/index.vue index 45110c41..570f579e 100644 --- a/src/views/infra/apiAccessLog/index.vue +++ b/src/views/infra/apiAccessLog/index.vue @@ -80,7 +80,7 @@ plain @click="handleExport" :loading="exportLoading" - v-hasPermi="['infra:api-error-log:export']" + v-hasPermi="['infra:api-access-log:export']" > 导出 diff --git a/src/views/infra/file/index.vue b/src/views/infra/file/index.vue index 17967312..2ff33641 100644 --- a/src/views/infra/file/index.vue +++ b/src/views/infra/file/index.vue @@ -96,7 +96,7 @@ link type="danger" @click="handleDelete(scope.row.id)" - v-hasPermi="['infra:config:delete']" + v-hasPermi="['infra:file:delete']" > 删除 diff --git a/src/views/infra/fileConfig/index.vue b/src/views/infra/fileConfig/index.vue index 3e8f38fb..a9c001a9 100644 --- a/src/views/infra/fileConfig/index.vue +++ b/src/views/infra/fileConfig/index.vue @@ -107,7 +107,7 @@ link type="danger" @click="handleDelete(scope.row.id)" - v-hasPermi="['infra:config:delete']" + v-hasPermi="['infra:file-config:delete']" > 删除 diff --git a/src/views/mall/statistics/member/index.vue b/src/views/mall/statistics/member/index.vue index a075810d..0e1bbaf6 100644 --- a/src/views/mall/statistics/member/index.vue +++ b/src/views/mall/statistics/member/index.vue @@ -3,44 +3,44 @@
- + - + - + - + @@ -67,42 +67,42 @@ @@ -110,7 +110,7 @@ - + @@ -122,16 +122,16 @@