# Conflicts:
#	src/views/bpm/definition/index.vue
#	src/views/bpm/model/index.vue
#	src/views/bpm/processInstance/create/index.vue
#	src/views/crm/statistics/customer/index.vue
This commit is contained in:
YunaiV 2024-03-30 12:54:51 +08:00
commit 34b788cb88
16 changed files with 844 additions and 26 deletions

View File

@ -49,6 +49,36 @@ export interface CrmStatisticsCustomerDealCycleByDateRespVO {
customerDealCycle: number customerDealCycle: number
} }
export interface CrmStatisticCustomerBaseRespVO {
customerCount: number
dealCount: number
dealPortion: number
}
export interface CrmStatisticCustomerIndustryRespVO extends CrmStatisticCustomerBaseRespVO {
industryId: number
industryName: string
industryPortion: number
}
export interface CrmStatisticCustomerSourceRespVO extends CrmStatisticCustomerBaseRespVO {
source: number
sourceName: string
sourcePortion: number
}
export interface CrmStatisticCustomerLevelRespVO extends CrmStatisticCustomerBaseRespVO {
level: number
levelName: string
levelPortion: number
}
export interface CrmStatisticCustomerAreaRespVO extends CrmStatisticCustomerBaseRespVO {
areaId: number
areaName: string
areaPortion: number
}
export interface CrmStatisticsCustomerDealCycleByUserRespVO { export interface CrmStatisticsCustomerDealCycleByUserRespVO {
ownerUserName: string ownerUserName: string
customerDealCycle: number customerDealCycle: number
@ -112,5 +142,33 @@ export const StatisticsCustomerApi = {
url: '/crm/statistics-customer/get-customer-deal-cycle-by-user', url: '/crm/statistics-customer/get-customer-deal-cycle-by-user',
params params
}) })
},
// 6.1 获取客户行业统计数据
getCustomerIndustry: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-industry-summary',
params
})
},
// 6.1 获取客户来源统计数据
getCustomerSource: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-source-summary',
params
})
},
// 6.1 获取客户行业统计数据
getCustomerLevel: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-level-summary',
params
})
},
// 6.1 获取客户行业统计数据
getCustomerArea: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-area-summary',
params
})
} }
} }

View File

@ -5,7 +5,7 @@ import { formatDate } from '@/utils/formatTime'
/** 会员分析 Request VO */ /** 会员分析 Request VO */
export interface MemberAnalyseReqVO { export interface MemberAnalyseReqVO {
times: [dayjs.ConfigType, dayjs.ConfigType] times: dayjs.ConfigType[]
} }
/** 会员分析 Response VO */ /** 会员分析 Response VO */

View File

@ -0,0 +1,3 @@
import MyFormCreate from './src/MyFormCreate.vue'
export { MyFormCreate }

View File

@ -0,0 +1,54 @@
<template>
<form-create v-bind="attrs">
<!-- 保障 form-create 的原始插槽 -->
<template v-for="(_, name) in slots" #[name]="slotData">
<slot :name="name" v-bind="slotData || {}"></slot>
</template>
<!-- 使用项目重新封装的文件上传组件实现文件上载 -->
<template #type-upload="scope">
<!-- {{ logC(scope) }}-->
<template v-if="scope.prop.props.uploadType === 'file'">
<!-- TODO puhui999: 考虑是否使用属性透传直接把整个 scope.prop.props 传递给组件 -->
<UploadFile
:disabled="scope.prop.props.disabled"
:limit="scope.prop.props.limit"
:modelValue="scope.model.value || scope.prop.value"
@update:modelValue="(val) => setValue(scope, val)"
/>
</template>
<template v-if="scope.prop.props.uploadType === 'image' && scope.prop.props.limit === 1">
<UploadImg
:disabled="scope.prop.props.disabled"
:modelValue="scope.model.value || scope.prop.value"
@update:modelValue="(val) => setValue(scope, val)"
/>
</template>
<template v-if="scope.prop.props.uploadType === 'image' && scope.prop.props.limit > 1">
<UploadImgs
:disabled="scope.prop.props.disabled"
:limit="scope.prop.props.limit"
:modelValue="scope.model.value || scope.prop.value"
@update:modelValue="(val) => setValue(scope, val)"
/>
</template>
</template>
</form-create>
</template>
<script lang="ts" setup>
defineOptions({ name: 'MyFormCreate' })
const attrs = useAttrs()
const slots = useSlots()
// 使 scope
// const logC = (s) => {
// console.log(s)
// }
//
const setValue = (scope: any, value: any) => {
const obj = {}
obj[scope.prop.field] = value
scope.api.setValue(obj)
}
</script>

View File

@ -6,7 +6,9 @@
:action="uploadUrl" :action="uploadUrl"
:auto-upload="autoUpload" :auto-upload="autoUpload"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:disabled="disabled"
:drag="drag" :drag="drag"
:http-request="httpRequest"
:limit="props.limit" :limit="props.limit"
:multiple="props.limit > 1" :multiple="props.limit > 1"
:on-error="excelUploadError" :on-error="excelUploadError"
@ -15,15 +17,14 @@
:on-remove="handleRemove" :on-remove="handleRemove"
:on-success="handleFileSuccess" :on-success="handleFileSuccess"
:show-file-list="true" :show-file-list="true"
:http-request="httpRequest"
class="upload-file-uploader" class="upload-file-uploader"
name="file" name="file"
> >
<el-button type="primary"> <el-button v-if="!disabled" type="primary">
<Icon icon="ep:upload-filled" /> <Icon icon="ep:upload-filled" />
选取文件 选取文件
</el-button> </el-button>
<template v-if="isShowTip" #tip> <template v-if="isShowTip && !disabled" #tip>
<div style="font-size: 8px"> <div style="font-size: 8px">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</div> </div>
@ -54,7 +55,8 @@ const props = defineProps({
limit: propTypes.number.def(5), // limit: propTypes.number.def(5), //
autoUpload: propTypes.bool.def(true), // autoUpload: propTypes.bool.def(true), //
drag: propTypes.bool.def(false), // drag: propTypes.bool.def(false), //
isShowTip: propTypes.bool.def(true) // isShowTip: propTypes.bool.def(true), //
disabled: propTypes.bool.def(false) // ==> false
}) })
// ========== ========== // ========== ==========

View File

@ -6,17 +6,18 @@
:action="uploadUrl" :action="uploadUrl"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']" :class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag" :drag="drag"
:http-request="httpRequest"
:multiple="false" :multiple="false"
:on-error="uploadError" :on-error="uploadError"
:on-success="uploadSuccess" :on-success="uploadSuccess"
:show-file-list="false" :show-file-list="false"
:http-request="httpRequest"
> >
<template v-if="modelValue"> <template v-if="modelValue">
<img :src="modelValue" class="upload-image" /> <img :src="modelValue" class="upload-image" />
<div class="upload-handle" @click.stop> <div class="upload-handle" @click.stop>
<div class="handle-icon" @click="editImg" v-if="!disabled"> <div v-if="!disabled" class="handle-icon" @click="editImg">
<Icon icon="ep:edit" /> <Icon icon="ep:edit" />
<span v-if="showBtnText">{{ t('action.edit') }}</span> <span v-if="showBtnText">{{ t('action.edit') }}</span>
</div> </div>
@ -77,10 +78,8 @@ const props = defineProps({
height: propTypes.string.def('150px'), // ==> 150px height: propTypes.string.def('150px'), // ==> 150px
width: propTypes.string.def('150px'), // ==> 150px width: propTypes.string.def('150px'), // ==> 150px
borderradius: propTypes.string.def('8px'), // ==> 8px borderradius: propTypes.string.def('8px'), // ==> 8px
// showDelete: propTypes.bool.def(true), //
showDelete: propTypes.bool.def(true), showBtnText: propTypes.bool.def(true) //
//
showBtnText: propTypes.bool.def(true)
}) })
const { t } = useI18n() // const { t } = useI18n() //
const message = useMessage() // const message = useMessage() //

View File

@ -6,13 +6,14 @@
:action="uploadUrl" :action="uploadUrl"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']" :class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag" :drag="drag"
:http-request="httpRequest"
:limit="limit" :limit="limit"
:multiple="true" :multiple="true"
:on-error="uploadError" :on-error="uploadError"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:on-success="uploadSuccess" :on-success="uploadSuccess"
:http-request="httpRequest"
list-type="picture-card" list-type="picture-card"
> >
<div class="upload-empty"> <div class="upload-empty">

View File

@ -40,7 +40,7 @@ export const setConfAndFields = (designerRef: object, conf: string, fields: stri
export const setConfAndFields2 = ( export const setConfAndFields2 = (
detailPreview: object, detailPreview: object,
conf: string, conf: string,
fields: string, fields: string[],
value?: object value?: object
) => { ) => {
if (isRef(detailPreview)) { if (isRef(detailPreview)) {

View File

@ -88,7 +88,7 @@
<!-- 表单详情的弹窗 --> <!-- 表单详情的弹窗 -->
<Dialog v-model="detailVisible" title="表单详情" width="800"> <Dialog v-model="detailVisible" title="表单详情" width="800">
<form-create :option="detailData.option" :rule="detailData.rule" /> <my-form-create :option="detailData.option" :rule="detailData.rule" />
</Dialog> </Dialog>
</template> </template>

View File

@ -91,9 +91,9 @@
</template> </template>
<!-- 情况一流程表单 --> <!-- 情况一流程表单 -->
<el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16"> <el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16">
<form-create <my-form-create
ref="fApi"
v-model="detailForm.value" v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option" :option="detailForm.option"
:rule="detailForm.rule" :rule="detailForm.rule"
/> />
@ -280,9 +280,9 @@ const getProcessInstance = async () => {
data.formVariables data.formVariables
) )
nextTick().then(() => { nextTick().then(() => {
fApi.value?.fapi?.btn.show(false) fApi.value?.btn.show(false)
fApi.value?.fapi?.resetBtn.show(false) fApi.value?.resetBtn.show(false)
fApi.value?.fapi?.disabled(true) fApi.value?.disabled(true)
}) })
} else { } else {
// data.processDefinition.formCustomViewPath /crm/contract/detail/index.vue // data.processDefinition.formCustomViewPath /crm/contract/detail/index.vue

View File

@ -0,0 +1,150 @@
<!-- 客户城市分布 -->
<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>
</template>
<script lang="ts" setup>
import { EChartsOption } from 'echarts'
import china from '@/assets/map/json/china.json'
import echarts from '@/plugins/echarts'
import {
CrmStatisticCustomerAreaRespVO,
StatisticsCustomerApi
} from '@/api/crm/statistics/customer'
defineOptions({ name: 'CustomerAddress' })
const props = defineProps<{ queryParams: any }>() //
//
echarts?.registerMap('china', china as any)
const loading = ref(false) //
const areaStatisticsList = ref<CrmStatisticCustomerAreaRespVO[]>([]) //
/** 地图配置 */
const echartsOption = reactive<EChartsOption>({
title: {
text: '全部客户',
left: 'center'
},
tooltip: {
trigger: 'item',
showDelay: 0,
transitionDuration: 0.2
},
visualMap: {
text: ['高', '低'],
realtime: false,
calculable: true,
top: 'middle',
inRange: {
color: ['#fff', '#3b82f6']
}
},
series: [
{
name: '客户地域分布',
type: 'map',
map: 'china',
roam: false,
selectedMode: false,
data: []
}
]
}) as EChartsOption
/** 地图配置 */
const echartsOption2 = reactive<EChartsOption>({
title: {
text: '成交客户',
left: 'center'
},
tooltip: {
trigger: 'item',
showDelay: 0,
transitionDuration: 0.2
},
visualMap: {
text: ['高', '低'],
realtime: false,
calculable: true,
top: 'middle',
inRange: {
color: ['#fff', '#3b82f6']
}
},
series: [
{
name: '客户地域分布',
type: 'map',
map: 'china',
roam: false,
selectedMode: false,
data: []
}
]
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1.
loading.value = true
const areaList = await StatisticsCustomerApi.getCustomerArea(props.queryParams)
areaStatisticsList.value = areaList.map((item: CrmStatisticCustomerAreaRespVO) => {
return {
...item,
areaName: item.areaName
.replace('维吾尔自治区', '')
.replace('壮族自治区', '')
.replace('回族自治区', '')
.replace('自治区', '')
.replace('省', '')
}
})
builderLeftMap()
builderRightMap()
loading.value = false
}
defineExpose({ loadData })
const builderLeftMap = () => {
let min = 0
let max = 0
echartsOption.series![0].data = areaStatisticsList.value.map((item) => {
min = Math.min(min, item.customerCount || 0)
max = Math.max(max, item.customerCount || 0)
return { ...item, name: item.areaName, value: item.customerCount || 0 }
})
echartsOption.visualMap!['min'] = min
echartsOption.visualMap!['max'] = max
}
const builderRightMap = () => {
let min = 0
let max = 0
echartsOption2.series![0].data = areaStatisticsList.value.map((item) => {
min = Math.min(min, item.dealCount || 0)
max = Math.max(max, item.dealCount || 0)
return { ...item, name: item.areaName, value: item.dealCount || 0 }
})
echartsOption2.visualMap!['min'] = min
echartsOption2.visualMap!['max'] = max
}
/** 初始化 */
onMounted(() => {
loadData()
})
</script>

View File

@ -0,0 +1,171 @@
<!-- 客户行业分析 -->
<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="客户行业" min-width="200" prop="industryName" />
<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,
StatisticsCustomerApi
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
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 StatisticsCustomerApi.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: r.industryName,
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: r.industryName,
value: r.dealCount
}
})
}
list.value = industryList
loading.value = false
}
defineExpose({ loadData })
/** 初始化 */
onMounted(() => {
loadData()
})
</script>

View File

@ -0,0 +1,171 @@
<!-- 客户来源分析 -->
<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="客户来源" min-width="200" prop="levelName" />
<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="levelPortion" />
<el-table-column align="center" label="成交占比(%)" min-width="200" prop="dealPortion" />
</el-table>
</el-card>
</template>
<script lang="ts" setup>
import {
CrmStatisticCustomerLevelRespVO,
StatisticsCustomerApi
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
defineOptions({ name: 'CustomerSource' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //
const list = ref<CrmStatisticCustomerLevelRespVO[]>([]) //
/** 饼图配置 */
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 levelList = await StatisticsCustomerApi.getCustomerLevel(props.queryParams)
// 2.1 Echarts
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = levelList.map((r: CrmStatisticCustomerLevelRespVO) => {
return {
name: r.levelName,
value: r.customerCount
}
})
}
// 2.2 Echarts2
if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
echartsOption2.series[0]['data'] = levelList.map((r: CrmStatisticCustomerLevelRespVO) => {
return {
name: r.levelName,
value: r.dealCount
}
})
}
list.value = levelList
loading.value = false
}
defineExpose({ loadData })
/** 初始化 */
onMounted(() => {
loadData()
})
</script>

View File

@ -0,0 +1,171 @@
<!-- 客户来源分析 -->
<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="客户来源" min-width="200" prop="sourceName" />
<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,
StatisticsCustomerApi
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
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 StatisticsCustomerApi.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: r.sourceName,
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: r.sourceName,
value: r.dealCount
}
})
}
list.value = sourceList
loading.value = false
}
defineExpose({ loadData })
/** 初始化 */
onMounted(() => {
loadData()
})
</script>

View File

@ -80,8 +80,24 @@
<CustomerConversionStat :query-params="queryParams" ref="conversionStatRef" /> <CustomerConversionStat :query-params="queryParams" ref="conversionStatRef" />
</el-tab-pane> </el-tab-pane>
<!-- 成交周期分析 --> <!-- 成交周期分析 -->
<el-tab-pane label="成交周期分析" name="dealCycle" lazy> <el-tab-pane label="成交周期分析" lazy name="dealCycle">
<CustomerDealCycle :query-params="queryParams" ref="dealCycleRef" /> <CustomerDealCycle ref="dealCycleRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 城市分布分析 -->
<el-tab-pane label="城市分布分析" lazy name="addressRef">
<CustomerAddress ref="addressRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户级别分析 -->
<el-tab-pane label="客户级别分析" lazy name="levelRef">
<CustomerLevel ref="levelRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户来源分析 -->
<el-tab-pane label="客户来源分析" lazy name="sourceRef">
<CustomerSource ref="sourceRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户行业分析 -->
<el-tab-pane label="客户行业分析" lazy name="industryRef">
<CustomerIndustry ref="industryRef" :query-params="queryParams" />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</el-col> </el-col>
@ -99,6 +115,10 @@ import CustomerFollowUpType from './components/CustomerFollowUpType.vue'
import CustomerConversionStat from './components/CustomerConversionStat.vue' import CustomerConversionStat from './components/CustomerConversionStat.vue'
import CustomerDealCycle from './components/CustomerDealCycle.vue' import CustomerDealCycle from './components/CustomerDealCycle.vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import CustomerAddress from './components/CustomerAddress.vue'
import CustomerIndustry from './components/CustomerIndustry.vue'
import CustomerSource from './components/CustomerSource.vue'
import CustomerLevel from './components/CustomerLevel.vue'
defineOptions({ name: 'CrmStatisticsCustomer' }) defineOptions({ name: 'CrmStatisticsCustomer' })
@ -132,6 +152,13 @@ const conversionStatRef = ref() // 4. 客户转化率分析
// 5. TODO // 5. TODO
// crm_owner_record TODO @dhb52 + mock // crm_owner_record TODO @dhb52 + mock
const dealCycleRef = ref() // 6. const dealCycleRef = ref() // 6.
const addressRef = ref()
//
const levelRef = ref()
//
const sourceRef = ref()
//
const industryRef = ref()
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { const handleQuery = () => {
@ -151,6 +178,18 @@ const handleQuery = () => {
case 'dealCycle': // case 'dealCycle': //
dealCycleRef.value?.loadData?.() dealCycleRef.value?.loadData?.()
break break
case 'addressRef':
addressRef.value?.loadData?.()
break
case 'levelRef':
levelRef.value?.loadData?.()
break
case 'sourceRef':
sourceRef.value?.loadData?.()
break
case 'industryRef':
industryRef.value?.loadData?.()
break
} }
} }

View File

@ -23,7 +23,7 @@
</el-button> </el-button>
<el-scrollbar height="580"> <el-scrollbar height="580">
<div> <div>
<pre><code class="hljs" v-dompurify-html="highlightedCode(formData)"></code></pre> <pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre>
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
@ -81,15 +81,14 @@ const makeTemplate = () => {
const rule = designer.value.getRule() const rule = designer.value.getRule()
const opt = designer.value.getOption() const opt = designer.value.getOption()
return `<template> return `<template>
<form-create <my-form-create
v-model="fapi" v-model:api="fApi"
:rule="rule" :rule="rule"
:option="option" :option="option"
@submit="onSubmit" @submit="onSubmit"
></form-create> ></my-form-create>
</template> </template>
<script setup lang=ts> <script setup lang=ts>
import formCreate from "@form-create/element-ui";
const faps = ref(null) const faps = ref(null)
const rule = ref('') const rule = ref('')
const option = ref('') const option = ref('')