Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3
commit
ab1c506226
@ -0,0 +1,33 @@
|
|||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
export interface StatisticsPerformanceRespVO {
|
||||||
|
time: string
|
||||||
|
currentMonthCount: number
|
||||||
|
lastMonthCount: number
|
||||||
|
lastYearCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排行 API
|
||||||
|
export const StatisticsPerformanceApi = {
|
||||||
|
// 员工获得合同金额统计
|
||||||
|
getContractPricePerformance: (params: any) => {
|
||||||
|
return request.get({
|
||||||
|
url: '/crm/statistics-performance/get-contract-price-performance',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 员工获得回款统计
|
||||||
|
getReceivablePricePerformance: (params: any) => {
|
||||||
|
return request.get({
|
||||||
|
url: '/crm/statistics-performance/get-receivable-price-performance',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
},
|
||||||
|
//员工获得签约合同数量统计
|
||||||
|
getContractCountPerformance: (params: any) => {
|
||||||
|
return request.get({
|
||||||
|
url: '/crm/statistics-performance/get-contract-count-performance',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
export interface CrmStatisticCustomerBaseRespVO {
|
||||||
|
customerCount: number
|
||||||
|
dealCount: number
|
||||||
|
dealPortion: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrmStatisticCustomerIndustryRespVO extends CrmStatisticCustomerBaseRespVO {
|
||||||
|
industryId: number
|
||||||
|
industryPortion: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrmStatisticCustomerSourceRespVO extends CrmStatisticCustomerBaseRespVO {
|
||||||
|
source: number
|
||||||
|
sourcePortion: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrmStatisticCustomerLevelRespVO extends CrmStatisticCustomerBaseRespVO {
|
||||||
|
level: number
|
||||||
|
levelPortion: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrmStatisticCustomerAreaRespVO extends CrmStatisticCustomerBaseRespVO {
|
||||||
|
areaId: number
|
||||||
|
areaName: string
|
||||||
|
areaPortion: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户分析 API
|
||||||
|
export const StatisticsPortraitApi = {
|
||||||
|
// 1. 获取客户行业统计数据
|
||||||
|
getCustomerIndustry: (params: any) => {
|
||||||
|
return request.get({
|
||||||
|
url: '/crm/statistics-portrait/get-customer-industry-summary',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 2. 获取客户来源统计数据
|
||||||
|
getCustomerSource: (params: any) => {
|
||||||
|
return request.get({
|
||||||
|
url: '/crm/statistics-portrait/get-customer-source-summary',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 3. 获取客户级别统计数据
|
||||||
|
getCustomerLevel: (params: any) => {
|
||||||
|
return request.get({
|
||||||
|
url: '/crm/statistics-portrait/get-customer-level-summary',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 4. 获取客户地区统计数据
|
||||||
|
getCustomerArea: (params: any) => {
|
||||||
|
return request.get({
|
||||||
|
url: '/crm/statistics-portrait/get-customer-area-summary',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@ -0,0 +1,3 @@
|
|||||||
|
import DictSelect from './src/DictSelect.vue'
|
||||||
|
|
||||||
|
export { DictSelect }
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
<!-- 数据字典 Select 选择器 -->
|
||||||
|
<template>
|
||||||
|
<el-select class="w-1/1" v-bind="attrs">
|
||||||
|
<template v-if="valueType === 'int'">
|
||||||
|
<el-option
|
||||||
|
v-for="(dict, index) in getIntDictOptions(dictType)"
|
||||||
|
:key="index"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="valueType === 'str'">
|
||||||
|
<el-option
|
||||||
|
v-for="(dict, index) in getStrDictOptions(dictType)"
|
||||||
|
:key="index"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="valueType === 'bool'">
|
||||||
|
<el-option
|
||||||
|
v-for="(dict, index) in getBoolDictOptions(dictType)"
|
||||||
|
:key="index"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||||
|
|
||||||
|
// 接受父组件参数
|
||||||
|
interface Props {
|
||||||
|
modelValue?: any // 值
|
||||||
|
dictType: string // 字典类型
|
||||||
|
valueType: string // 字典值类型
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
dictType: '',
|
||||||
|
valueType: 'str'
|
||||||
|
})
|
||||||
|
const attrs = useAttrs()
|
||||||
|
defineOptions({ name: 'DictSelect' })
|
||||||
|
</script>
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import MyFormCreateDesigner from './src/MyFormCreateDesigner.vue'
|
||||||
|
import { useFormCreateDesigner } from './src/useFormCreateDesigner'
|
||||||
|
|
||||||
|
export { MyFormCreateDesigner, useFormCreateDesigner }
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<!-- TODO puhui999: 没啥问题的话准备移除 -->
|
||||||
|
<template>
|
||||||
|
<FcDesigner ref="designer" height="780px" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useUploadFileRule, useUploadImgRule, useUploadImgsRule } from './config'
|
||||||
|
|
||||||
|
defineOptions({ name: 'MyFormCreateDesigner' })
|
||||||
|
|
||||||
|
const designer = ref() // 表单设计器
|
||||||
|
const uploadFileRule = useUploadFileRule()
|
||||||
|
const uploadImgRule = useUploadImgRule()
|
||||||
|
const uploadImgsRule = useUploadImgsRule()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 移除自带的上传组件规则
|
||||||
|
designer.value?.removeMenuItem('upload')
|
||||||
|
const components = [uploadFileRule, uploadImgRule, uploadImgsRule]
|
||||||
|
components.forEach((component) => {
|
||||||
|
//插入组件规则
|
||||||
|
designer.value?.addComponent(component)
|
||||||
|
//插入拖拽按钮到`main`分类下
|
||||||
|
designer.value?.appendMenuItem('main', {
|
||||||
|
icon: component.icon,
|
||||||
|
name: component.name,
|
||||||
|
label: component.label
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { useUploadFileRule } from './useUploadFileRule'
|
||||||
|
import { useUploadImgRule } from './useUploadImgRule'
|
||||||
|
import { useUploadImgsRule } from './useUploadImgsRule'
|
||||||
|
import { useDictSelectRule } from './useDictSelectRule'
|
||||||
|
import { useUserSelectRule } from './useUserSelectRule'
|
||||||
|
|
||||||
|
export {
|
||||||
|
useUploadFileRule,
|
||||||
|
useUploadImgRule,
|
||||||
|
useUploadImgsRule,
|
||||||
|
useDictSelectRule,
|
||||||
|
useUserSelectRule
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
import { generateUUID } from '@/utils'
|
||||||
|
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
|
||||||
|
|
||||||
|
export const useUploadFileRule = () => {
|
||||||
|
const label = '文件上传'
|
||||||
|
const name = 'UploadFile'
|
||||||
|
return {
|
||||||
|
icon: 'icon-upload',
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
rule() {
|
||||||
|
return {
|
||||||
|
type: name,
|
||||||
|
field: generateUUID(),
|
||||||
|
title: label,
|
||||||
|
info: '',
|
||||||
|
$required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props(_, { t }) {
|
||||||
|
return localeProps(t, name + '.props', [
|
||||||
|
makeRequiredRule(),
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
field: 'fileType',
|
||||||
|
title: '文件类型',
|
||||||
|
value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
|
||||||
|
options: [
|
||||||
|
{ label: 'doc', value: 'doc' },
|
||||||
|
{ label: 'xls', value: 'xls' },
|
||||||
|
{ label: 'ppt', value: 'ppt' },
|
||||||
|
{ label: 'txt', value: 'txt' },
|
||||||
|
{ label: 'pdf', value: 'pdf' }
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
multiple: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'autoUpload',
|
||||||
|
title: '是否在选取文件后立即进行上传',
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'drag',
|
||||||
|
title: '拖拽上传',
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'isShowTip',
|
||||||
|
title: '是否显示提示',
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'inputNumber',
|
||||||
|
field: 'fileSize',
|
||||||
|
title: '大小限制(MB)',
|
||||||
|
value: 5,
|
||||||
|
props: { min: 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'inputNumber',
|
||||||
|
field: 'limit',
|
||||||
|
title: '数量限制',
|
||||||
|
value: 5,
|
||||||
|
props: { min: 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'disabled',
|
||||||
|
title: '是否禁用',
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
import { generateUUID } from '@/utils'
|
||||||
|
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
|
||||||
|
|
||||||
|
export const useUploadImgRule = () => {
|
||||||
|
const label = '单图上传'
|
||||||
|
const name = 'UploadImg'
|
||||||
|
return {
|
||||||
|
icon: 'icon-upload',
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
rule() {
|
||||||
|
return {
|
||||||
|
type: name,
|
||||||
|
field: generateUUID(),
|
||||||
|
title: label,
|
||||||
|
info: '',
|
||||||
|
$required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props(_, { t }) {
|
||||||
|
return localeProps(t, name + '.props', [
|
||||||
|
makeRequiredRule(),
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'drag',
|
||||||
|
title: '拖拽上传',
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
field: 'fileType',
|
||||||
|
title: '图片类型限制',
|
||||||
|
value: ['image/jpeg', 'image/png', 'image/gif'],
|
||||||
|
options: [
|
||||||
|
{ label: 'image/apng', value: 'image/apng' },
|
||||||
|
{ label: 'image/bmp', value: 'image/bmp' },
|
||||||
|
{ label: 'image/gif', value: 'image/gif' },
|
||||||
|
{ label: 'image/jpeg', value: 'image/jpeg' },
|
||||||
|
{ label: 'image/pjpeg', value: 'image/pjpeg' },
|
||||||
|
{ label: 'image/svg+xml', value: 'image/svg+xml' },
|
||||||
|
{ label: 'image/tiff', value: 'image/tiff' },
|
||||||
|
{ label: 'image/webp', value: 'image/webp' },
|
||||||
|
{ label: 'image/x-icon', value: 'image/x-icon' }
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
multiple: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'inputNumber',
|
||||||
|
field: 'fileSize',
|
||||||
|
title: '大小限制(MB)',
|
||||||
|
value: 5,
|
||||||
|
props: { min: 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'height',
|
||||||
|
title: '组件高度',
|
||||||
|
value: '150px'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'width',
|
||||||
|
title: '组件宽度',
|
||||||
|
value: '150px'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'borderradius',
|
||||||
|
title: '组件边框圆角',
|
||||||
|
value: '8px'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'disabled',
|
||||||
|
title: '是否显示删除按钮',
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'showBtnText',
|
||||||
|
title: '是否显示按钮文字',
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
import { generateUUID } from '@/utils'
|
||||||
|
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
|
||||||
|
|
||||||
|
export const useUploadImgsRule = () => {
|
||||||
|
const label = '多图上传'
|
||||||
|
const name = 'UploadImgs'
|
||||||
|
return {
|
||||||
|
icon: 'icon-upload',
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
rule() {
|
||||||
|
return {
|
||||||
|
type: name,
|
||||||
|
field: generateUUID(),
|
||||||
|
title: label,
|
||||||
|
info: '',
|
||||||
|
$required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props(_, { t }) {
|
||||||
|
return localeProps(t, name + '.props', [
|
||||||
|
makeRequiredRule(),
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
field: 'drag',
|
||||||
|
title: '拖拽上传',
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
field: 'fileType',
|
||||||
|
title: '图片类型限制',
|
||||||
|
value: ['image/jpeg', 'image/png', 'image/gif'],
|
||||||
|
options: [
|
||||||
|
{ label: 'image/apng', value: 'image/apng' },
|
||||||
|
{ label: 'image/bmp', value: 'image/bmp' },
|
||||||
|
{ label: 'image/gif', value: 'image/gif' },
|
||||||
|
{ label: 'image/jpeg', value: 'image/jpeg' },
|
||||||
|
{ label: 'image/pjpeg', value: 'image/pjpeg' },
|
||||||
|
{ label: 'image/svg+xml', value: 'image/svg+xml' },
|
||||||
|
{ label: 'image/tiff', value: 'image/tiff' },
|
||||||
|
{ label: 'image/webp', value: 'image/webp' },
|
||||||
|
{ label: 'image/x-icon', value: 'image/x-icon' }
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
multiple: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'inputNumber',
|
||||||
|
field: 'fileSize',
|
||||||
|
title: '大小限制(MB)',
|
||||||
|
value: 5,
|
||||||
|
props: { min: 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'inputNumber',
|
||||||
|
field: 'limit',
|
||||||
|
title: '数量限制',
|
||||||
|
value: 5,
|
||||||
|
props: { min: 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'height',
|
||||||
|
title: '组件高度',
|
||||||
|
value: '150px'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'width',
|
||||||
|
title: '组件宽度',
|
||||||
|
value: '150px'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'borderradius',
|
||||||
|
title: '组件边框圆角',
|
||||||
|
value: '8px'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
useDictSelectRule,
|
||||||
|
useUploadFileRule,
|
||||||
|
useUploadImgRule,
|
||||||
|
useUploadImgsRule,
|
||||||
|
useUserSelectRule
|
||||||
|
} from './config'
|
||||||
|
import { Ref } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单设计器增强 hook
|
||||||
|
* 新增
|
||||||
|
* - 文件上传
|
||||||
|
* - 单图上传
|
||||||
|
* - 多图上传
|
||||||
|
*/
|
||||||
|
export const useFormCreateDesigner = (designer: Ref) => {
|
||||||
|
const uploadFileRule = useUploadFileRule()
|
||||||
|
const uploadImgRule = useUploadImgRule()
|
||||||
|
const uploadImgsRule = useUploadImgsRule()
|
||||||
|
const dictSelectRule = useDictSelectRule()
|
||||||
|
const userSelectRule = useUserSelectRule()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代
|
||||||
|
designer.value?.removeMenuItem('upload')
|
||||||
|
const components = [
|
||||||
|
uploadFileRule,
|
||||||
|
uploadImgRule,
|
||||||
|
uploadImgsRule,
|
||||||
|
dictSelectRule,
|
||||||
|
userSelectRule
|
||||||
|
]
|
||||||
|
components.forEach((component) => {
|
||||||
|
// 插入组件规则
|
||||||
|
designer.value?.addComponent(component)
|
||||||
|
// 插入拖拽按钮到 `main` 分类下
|
||||||
|
designer.value?.appendMenuItem('main', {
|
||||||
|
icon: component.icon,
|
||||||
|
name: component.name,
|
||||||
|
label: component.label
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
// TODO puhui999: 借鉴一下 form-create-designer utils 方法 🤣 (导入不了只能先 copy 过来用下)
|
||||||
|
export function makeRequiredRule() {
|
||||||
|
return {
|
||||||
|
type: 'Required',
|
||||||
|
field: 'formCreate$required',
|
||||||
|
title: '是否必填'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const localeProps = (t, prefix, rules) => {
|
||||||
|
return rules.map((rule) => {
|
||||||
|
if (rule.field === 'formCreate$required') {
|
||||||
|
rule.title = t('props.required') || rule.title
|
||||||
|
} else if (rule.field && rule.field !== '_optionType') {
|
||||||
|
rule.title = t('components.' + prefix + '.' + rule.field) || rule.title
|
||||||
|
}
|
||||||
|
return rule
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upper(str) {
|
||||||
|
return str.replace(str[0], str[0].toLocaleUpperCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeOptionsRule(t, to, userOptions) {
|
||||||
|
console.log(userOptions[0])
|
||||||
|
const options = [
|
||||||
|
{ label: t('props.optionsType.struct'), value: 0 },
|
||||||
|
{ label: t('props.optionsType.json'), value: 1 },
|
||||||
|
{ label: '用户数据', value: 2 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const control = [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
rule: [
|
||||||
|
{
|
||||||
|
type: 'TableOptions',
|
||||||
|
field: 'formCreate' + upper(to).replace('.', '>'),
|
||||||
|
props: { defaultValue: [] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
rule: [
|
||||||
|
{
|
||||||
|
type: 'Struct',
|
||||||
|
field: 'formCreate' + upper(to).replace('.', '>'),
|
||||||
|
props: { defaultValue: [] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
rule: [
|
||||||
|
{
|
||||||
|
type: 'TableOptions',
|
||||||
|
field: 'formCreate' + upper(to).replace('.', '>'),
|
||||||
|
props: { modelValue: [] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
options.splice(0, 0)
|
||||||
|
control.push()
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'radio',
|
||||||
|
title: t('props.options'),
|
||||||
|
field: '_optionType',
|
||||||
|
value: 0,
|
||||||
|
options,
|
||||||
|
props: {
|
||||||
|
type: 'button'
|
||||||
|
},
|
||||||
|
control
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,197 @@
|
|||||||
|
<!-- 客户行业分析 -->
|
||||||
|
<template>
|
||||||
|
<!-- Echarts图 -->
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-skeleton :loading="loading" animated>
|
||||||
|
<Echart :height="500" :options="echartsOption" />
|
||||||
|
</el-skeleton>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-skeleton :loading="loading" animated>
|
||||||
|
<Echart :height="500" :options="echartsOption2" />
|
||||||
|
</el-skeleton>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 统计列表 -->
|
||||||
|
<el-card class="mt-16px" shadow="never">
|
||||||
|
<el-table v-loading="loading" :data="list">
|
||||||
|
<el-table-column align="center" label="序号" type="index" width="80" />
|
||||||
|
<el-table-column align="center" label="客户行业" prop="industryId" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" />
|
||||||
|
<el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" />
|
||||||
|
<el-table-column align="center" label="行业占比(%)" min-width="200" prop="industryPortion" />
|
||||||
|
<el-table-column align="center" label="成交占比(%)" min-width="200" prop="dealPortion" />
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
CrmStatisticCustomerIndustryRespVO,
|
||||||
|
StatisticsPortraitApi
|
||||||
|
} from '@/api/crm/statistics/portrait'
|
||||||
|
import { EChartsOption } from 'echarts'
|
||||||
|
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||||
|
import { getSumValue } from '@/utils'
|
||||||
|
import { isEmpty } from '@/utils/is'
|
||||||
|
|
||||||
|
defineOptions({ name: 'CustomerIndustry' })
|
||||||
|
const props = defineProps<{ queryParams: any }>() // 搜索参数
|
||||||
|
|
||||||
|
const loading = ref(false) // 加载中
|
||||||
|
const list = ref<CrmStatisticCustomerIndustryRespVO[]>([]) // 列表的数据
|
||||||
|
|
||||||
|
/** 饼图配置(全部客户) */
|
||||||
|
const echartsOption = reactive<EChartsOption>({
|
||||||
|
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: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}) as EChartsOption
|
||||||
|
|
||||||
|
/** 饼图配置(成交客户) */
|
||||||
|
const echartsOption2 = reactive<EChartsOption>({
|
||||||
|
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: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}) as EChartsOption
|
||||||
|
|
||||||
|
/** 获取统计数据 */
|
||||||
|
const loadData = async () => {
|
||||||
|
// 1. 加载统计数据
|
||||||
|
loading.value = true
|
||||||
|
const industryList = await StatisticsPortraitApi.getCustomerIndustry(props.queryParams)
|
||||||
|
// 2.1 更新 Echarts 数据
|
||||||
|
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
|
||||||
|
echartsOption.series[0]['data'] = industryList.map((r: CrmStatisticCustomerIndustryRespVO) => {
|
||||||
|
return {
|
||||||
|
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
|
||||||
|
value: r.customerCount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 2.2 更新 Echarts2 数据
|
||||||
|
if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
|
||||||
|
echartsOption2.series[0]['data'] = industryList.map((r: CrmStatisticCustomerIndustryRespVO) => {
|
||||||
|
return {
|
||||||
|
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
|
||||||
|
value: r.dealCount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 3. 计算比例
|
||||||
|
calculateProportion(industryList)
|
||||||
|
list.value = industryList
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
defineExpose({ loadData })
|
||||||
|
|
||||||
|
/** 计算比例 */
|
||||||
|
const calculateProportion = (sourceList: CrmStatisticCustomerIndustryRespVO[]) => {
|
||||||
|
if (isEmpty(sourceList)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 这里类型丢失了所以重新搞个变量
|
||||||
|
const list = sourceList as unknown as CrmStatisticCustomerIndustryRespVO[]
|
||||||
|
const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -0,0 +1,197 @@
|
|||||||
|
<!-- 客户来源分析 -->
|
||||||
|
<template>
|
||||||
|
<!-- Echarts图 -->
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-skeleton :loading="loading" animated>
|
||||||
|
<Echart :height="500" :options="echartsOption" />
|
||||||
|
</el-skeleton>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-skeleton :loading="loading" animated>
|
||||||
|
<Echart :height="500" :options="echartsOption2" />
|
||||||
|
</el-skeleton>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 统计列表 -->
|
||||||
|
<el-card class="mt-16px" shadow="never">
|
||||||
|
<el-table v-loading="loading" :data="list">
|
||||||
|
<el-table-column align="center" label="序号" type="index" width="80" />
|
||||||
|
<el-table-column align="center" label="客户来源" prop="source" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="客户个数" min-width="200" prop="customerCount" />
|
||||||
|
<el-table-column align="center" label="成交个数" min-width="200" prop="dealCount" />
|
||||||
|
<el-table-column align="center" label="来源占比(%)" min-width="200" prop="sourcePortion" />
|
||||||
|
<el-table-column align="center" label="成交占比(%)" min-width="200" prop="dealPortion" />
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
CrmStatisticCustomerSourceRespVO,
|
||||||
|
StatisticsPortraitApi
|
||||||
|
} from '@/api/crm/statistics/portrait'
|
||||||
|
import { EChartsOption } from 'echarts'
|
||||||
|
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||||
|
import { isEmpty } from '@/utils/is'
|
||||||
|
import { getSumValue } from '@/utils'
|
||||||
|
|
||||||
|
defineOptions({ name: 'CustomerSource' })
|
||||||
|
const props = defineProps<{ queryParams: any }>() // 搜索参数
|
||||||
|
|
||||||
|
const loading = ref(false) // 加载中
|
||||||
|
const list = ref<CrmStatisticCustomerSourceRespVO[]>([]) // 列表的数据
|
||||||
|
|
||||||
|
/** 饼图配置(全部客户) */
|
||||||
|
const echartsOption = reactive<EChartsOption>({
|
||||||
|
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: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}) as EChartsOption
|
||||||
|
|
||||||
|
/** 饼图配置(成交客户) */
|
||||||
|
const echartsOption2 = reactive<EChartsOption>({
|
||||||
|
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: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}) as EChartsOption
|
||||||
|
|
||||||
|
/** 获取统计数据 */
|
||||||
|
const loadData = async () => {
|
||||||
|
// 1. 加载统计数据
|
||||||
|
loading.value = true
|
||||||
|
const sourceList = await StatisticsPortraitApi.getCustomerSource(props.queryParams)
|
||||||
|
// 2.1 更新 Echarts 数据
|
||||||
|
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
|
||||||
|
echartsOption.series[0]['data'] = sourceList.map((r: CrmStatisticCustomerSourceRespVO) => {
|
||||||
|
return {
|
||||||
|
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||||
|
value: r.customerCount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 2.2 更新 Echarts2 数据
|
||||||
|
if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
|
||||||
|
echartsOption2.series[0]['data'] = sourceList.map((r: CrmStatisticCustomerSourceRespVO) => {
|
||||||
|
return {
|
||||||
|
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||||
|
value: r.dealCount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 3. 计算比例
|
||||||
|
calculateProportion(sourceList)
|
||||||
|
list.value = sourceList
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
defineExpose({ loadData })
|
||||||
|
|
||||||
|
/** 计算比例 */
|
||||||
|
const calculateProportion = (sourceList: CrmStatisticCustomerSourceRespVO[]) => {
|
||||||
|
if (isEmpty(sourceList)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 这里类型丢失了所以重新搞个变量
|
||||||
|
const list = sourceList as unknown as CrmStatisticCustomerSourceRespVO[]
|
||||||
|
const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<!-- TODO puhui999: 先单独一个后面封装成通用选择组件 -->
|
||||||
|
<template>
|
||||||
|
<el-select class="w-1/1" v-bind="attrs">
|
||||||
|
<el-option
|
||||||
|
v-for="(dict, index) in userOptions"
|
||||||
|
:key="index"
|
||||||
|
:label="dict.nickname"
|
||||||
|
:value="dict.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import * as UserApi from '@/api/system/user'
|
||||||
|
|
||||||
|
defineOptions({ name: 'UserSelect' })
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
const userOptions = ref<UserApi.UserVO[]>([]) // 用户下拉数据
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const data = await UserApi.getSimpleUserList()
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userOptions.value = data
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue