YunaiV 2024-04-12 20:37:50 +08:00
commit ab1c506226
65 changed files with 3450 additions and 305 deletions

View File

@ -102,6 +102,7 @@
"postcss-html": "^1.6.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.2.5",
"prettier-eslint": "^16.3.0",
"rimraf": "^5.0.5",
"rollup": "^4.12.0",
"sass": "^1.69.5",

View File

@ -15,9 +15,9 @@ export interface PermissionVO {
}
export interface TransferReqVO {
bizId: number // 模块编号
id: number // 模块编号
newOwnerUserId: number // 新负责人的用户编号
oldOwnerPermissionLevel: number // 老负责人加入团队后的权限级别
oldOwnerPermissionLevel?: number // 老负责人加入团队后的权限级别
toBizTypes?: number[] // 转移客户时,需要额外有【联系人】【商机】【合同】的 checkbox 选择
}

View File

@ -14,21 +14,21 @@ export interface CrmStatisticsCustomerSummaryByUserRespVO {
receivablePrice: number
}
export interface CrmStatisticsFollowupSummaryByDateRespVO {
export interface CrmStatisticsFollowUpSummaryByDateRespVO {
time: string
followupRecordCount: number
followupCustomerCount: number
followUpRecordCount: number
followUpCustomerCount: number
}
export interface CrmStatisticsFollowupSummaryByUserRespVO {
export interface CrmStatisticsFollowUpSummaryByUserRespVO {
ownerUserName: string
followupRecordCount: number
followupCustomerCount: number
}
export interface CrmStatisticsFollowupSummaryByTypeRespVO {
followupType: string
followupRecordCount: number
export interface CrmStatisticsFollowUpSummaryByTypeRespVO {
followUpType: string
followUpRecordCount: number
}
export interface CrmStatisticsCustomerContractSummaryRespVO {
@ -44,6 +44,18 @@ export interface CrmStatisticsCustomerContractSummaryRespVO {
orderDate: Date
}
export interface CrmStatisticsPoolSummaryByDateRespVO {
time: string
customerPutCount: number
customerTakeCount: number
}
export interface CrmStatisticsPoolSummaryByUserRespVO {
ownerUserName: string
customerPutCount: number
customerTakeCount: number
}
export interface CrmStatisticsCustomerDealCycleByDateRespVO {
time: string
customerDealCycle: number
@ -72,23 +84,23 @@ export const StatisticsCustomerApi = {
})
},
// 2.1 客户跟进次数分析(按日期)
getFollowupSummaryByDate: (params: any) => {
getFollowUpSummaryByDate: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-followup-summary-by-date',
url: '/crm/statistics-customer/get-follow-up-summary-by-date',
params
})
},
// 2.2 客户跟进次数分析(按用户)
getFollowupSummaryByUser: (params: any) => {
getFollowUpSummaryByUser: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-followup-summary-by-user',
url: '/crm/statistics-customer/get-follow-up-summary-by-user',
params
})
},
// 3.1 获取客户跟进方式统计数
getFollowupSummaryByType: (params: any) => {
getFollowUpSummaryByType: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-followup-summary-by-type',
url: '/crm/statistics-customer/get-follow-up-summary-by-type',
params
})
},
@ -99,14 +111,28 @@ export const StatisticsCustomerApi = {
params
})
},
// 5.1 获取客户成交周期(按日期)
// 5.1 获取客户公海分析(按日期)
getPoolSummaryByDate: (param: any) => {
return request.get({
url: '/crm/statistics-customer/get-pool-summary-by-date',
params: param
})
},
// 5.2 获取客户公海分析(按用户)
getPoolSummaryByUser: (param: any) => {
return request.get({
url: '/crm/statistics-customer/get-pool-summary-by-user',
params: param
})
},
// 6.1 获取客户成交周期(按日期)
getCustomerDealCycleByDate: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-deal-cycle-by-date',
params
})
},
// 5.2 获取客户成交周期(按用户)
// 6.2 获取客户成交周期(按用户)
getCustomerDealCycleByUser: (params: any) => {
return request.get({
url: '/crm/statistics-customer/get-customer-deal-cycle-by-user',

View File

@ -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
})
}
}

View File

@ -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
})
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -503,9 +503,13 @@ const submit = () => {
emit('update:modelValue', defaultValue.value)
dialogVisible.value = false
}
const inputChange = () => {
emit('update:modelValue', defaultValue.value)
}
</script>
<template>
<el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs">
<el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs" @input="inputChange">
<template #append>
<el-select v-model="select" placeholder="生成器" style="width: 115px">
<el-option label="每分钟" value="0 * * * * ?" />

View File

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

View File

@ -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>

View File

@ -0,0 +1,90 @@
<template>
<div class="h-40px flex items-center justify-center">
<MagicCubeEditor
v-model="cellList"
class="m-b-16px"
:rows="1"
:cols="cellCount"
:cube-size="38"
@hot-area-selected="handleHotAreaSelected"
/>
<img src="@/assets/imgs/diy/app-nav-bar-mp.png" alt="" class="h-30px w-76px" v-if="isMp" />
</div>
<template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
<template v-if="selectedHotAreaIndex === cellIndex">
<el-form-item label="类型" :prop="`cell[${cellIndex}].type`">
<el-radio-group v-model="cell.type">
<el-radio label="text">文字</el-radio>
<el-radio label="image">图片</el-radio>
<el-radio label="search">搜索框</el-radio>
</el-radio-group>
</el-form-item>
<!-- 1. 文字 -->
<template v-if="cell.type === 'text'">
<el-form-item label="内容" :prop="`cell[${cellIndex}].text`">
<el-input v-model="cell!.text" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item label="颜色" :prop="`cell[${cellIndex}].text`">
<ColorInput v-model="cell!.textColor" />
</el-form-item>
</template>
<!-- 2. 图片 -->
<template v-else-if="cell.type === 'image'">
<el-form-item label="图片" :prop="`cell[${cellIndex}].imgUrl`">
<UploadImg v-model="cell.imgUrl" :limit="1" height="56px" width="56px">
<template #tip>建议尺寸 56*56</template>
</UploadImg>
</el-form-item>
<el-form-item label="链接" :prop="`cell[${cellIndex}].url`">
<AppLinkInput v-model="cell.url" />
</el-form-item>
</template>
<!-- 3. 搜索框 -->
<template v-else>
<el-form-item label="提示文字" :prop="`cell[${cellIndex}].placeholder`">
<el-input v-model="cell.placeholder" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item label="圆角" :prop="`cell[${cellIndex}].borderRadius`">
<el-slider
v-model="cell.borderRadius"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</template>
</template>
</template>
</template>
<script setup lang="ts">
import { NavigationBarCellProperty } from '../config'
import { usePropertyForm } from '@/components/DiyEditor/util'
//
defineOptions({ name: 'NavigationBarCellProperty' })
const props = defineProps<{
modelValue: NavigationBarCellProperty[]
isMp: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const { formData: cellList } = usePropertyForm(props.modelValue, emit)
if (!cellList.value) cellList.value = []
// 628
const cellCount = computed(() => (props.isMp ? 6 : 8))
//
const selectedHotAreaIndex = ref(0)
const handleHotAreaSelected = (cellValue: NavigationBarCellProperty, index: number) => {
selectedHotAreaIndex.value = index
if (!cellValue.type) {
cellValue.type = 'text'
cellValue.textColor = '#111111'
}
}
</script>
<style scoped lang="scss"></style>

View File

@ -2,22 +2,53 @@ import { DiyComponent } from '@/components/DiyEditor/util'
/** 顶部导航栏属性 */
export interface NavigationBarProperty {
// 页面标题
title: string
// 页面描述
description: string
// 顶部导航高度
navBarHeight: number
// 页面背景颜色
backgroundColor: string
// 页面背景图片
backgroundImage: string
// 背景类型
bgType: 'color' | 'img'
// 背景颜色
bgColor: string
// 图片链接
bgImg: string
// 样式类型:默认 | 沉浸式
styleType: 'default' | 'immersion'
styleType: 'normal' | 'inner'
// 常驻显示
alwaysShow: boolean
// 是否显示返回按钮
showGoBack: boolean
// 小程序单元格列表
mpCells: NavigationBarCellProperty[]
// 其它平台单元格列表
otherCells: NavigationBarCellProperty[]
// 本地变量
_local: {
// 预览顶部导航(小程序)
previewMp: boolean
// 预览顶部导航(非小程序)
previewOther: boolean
}
}
/** 顶部导航栏 - 单元格 属性 */
export interface NavigationBarCellProperty {
// 类型:文字 | 图片 | 搜索框
type: 'text' | 'image' | 'search'
// 宽度
width: number
// 高度
height: number
// 顶部位置
top: number
// 左侧位置
left: number
// 文字内容
text: string
// 文字颜色
textColor: string
// 图片地址
imgUrl: string
// 图片链接
url: string
// 搜索框:提示文字
placeholder: string
// 搜索框:边框圆角半径
borderRadius: number
}
// 定义组件
@ -26,13 +57,26 @@ export const component = {
name: '顶部导航栏',
icon: 'tabler:layout-navbar',
property: {
title: '页面标题',
description: '',
navBarHeight: 35,
backgroundColor: '#fff',
backgroundImage: '',
styleType: 'default',
bgType: 'color',
bgColor: '#fff',
bgImg: '',
styleType: 'normal',
alwaysShow: true,
showGoBack: true
mpCells: [
{
type: 'text',
textColor: '#111111'
}
],
otherCells: [
{
type: 'text',
textColor: '#111111'
}
],
_local: {
previewMp: true,
previewOther: false
}
}
} as DiyComponent<NavigationBarProperty>

View File

@ -1,45 +1,73 @@
<template>
<div
class="navigation-bar"
:style="{
height: `${property.navBarHeight}px`,
backgroundColor: property.backgroundColor,
backgroundImage: `url(${property.backgroundImage})`
}"
>
<!-- 左侧 -->
<div class="left">
<Icon icon="ep:arrow-left" v-show="property.showGoBack" />
<div class="navigation-bar" :style="bgStyle">
<div class="h-full w-full flex items-center">
<div v-for="(cell, cellIndex) in cellList" :key="cellIndex" :style="getCellStyle(cell)">
<span v-if="cell.type === 'text'">{{ cell.text }}</span>
<img v-else-if="cell.type === 'image'" :src="cell.imgUrl" alt="" class="h-full w-full" />
<SearchBar v-else :property="getSearchProp" />
</div>
</div>
<!-- 中间 -->
<div
class="center"
:style="{
height: `${property.navBarHeight}px`,
lineHeight: `${property.navBarHeight}px`
}"
>
{{ property.title }}
</div>
<!-- 右侧 -->
<div class="right"></div>
<img
v-if="property._local?.previewMp"
src="@/assets/imgs/diy/app-nav-bar-mp.png"
alt=""
class="h-30px w-86px"
/>
</div>
</template>
<script setup lang="ts">
import { NavigationBarProperty } from './config'
import { NavigationBarCellProperty, NavigationBarProperty } from './config'
import SearchBar from '@/components/DiyEditor/components/mobile/SearchBar/index.vue'
import { StyleValue } from 'vue'
import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
/** 页面顶部导航栏 */
defineOptions({ name: 'NavigationBar' })
defineProps<{ property: NavigationBarProperty }>()
const props = defineProps<{ property: NavigationBarProperty }>()
//
const bgStyle = computed(() => {
const background =
props.property.bgType === 'img' && props.property.bgImg
? `url(${props.property.bgImg}) no-repeat top center / 100% 100%`
: props.property.bgColor
return { background }
})
//
const cellList = computed(() =>
props.property._local?.previewMp ? props.property.mpCells : props.property.otherCells
)
//
const cellWidth = computed(() => {
return props.property._local?.previewMp ? (375 - 80 - 86) / 6 : (375 - 90) / 8
})
//
const getCellStyle = (cell: NavigationBarCellProperty) => {
return {
width: cell.width * cellWidth.value + (cell.width - 1) * 10 + 'px',
left: cell.left * cellWidth.value + (cell.left + 1) * 10 + 'px',
position: 'absolute'
} as StyleValue
}
//
const getSearchProp = (cell: NavigationBarCellProperty) => {
return {
height: 30,
showScan: false,
placeholder: cell.placeholder,
borderRadius: cell.borderRadius
} as SearchProperty
}
</script>
<style lang="scss" scoped>
.navigation-bar {
display: flex;
height: 35px;
height: 50px;
background: #fff;
justify-content: space-between;
align-items: center;
padding: 0 6px;
/* 左边 */
.left {

View File

@ -1,53 +1,73 @@
<template>
<el-form label-width="80px" :model="formData" :rules="rules">
<el-form-item label="页面标题" prop="title">
<el-input v-model="formData!.title" placeholder="页面标题" maxlength="25" show-word-limit />
</el-form-item>
<el-form-item label="页面描述" prop="description">
<el-input
type="textarea"
v-model="formData!.description"
placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
/>
</el-form-item>
<el-form-item label="样式" prop="styleType">
<el-radio-group v-model="formData!.styleType">
<el-radio label="default">默认</el-radio>
<el-radio label="immersion">沉浸式</el-radio>
<el-radio label="normal">标准</el-radio>
<el-tooltip
content="沉侵式头部仅支持微信小程序、APP建议页面第一个组件为图片展示类组件"
placement="top"
>
<el-radio label="inner">沉浸式</el-radio>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'immersion'">
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'inner'">
<el-radio-group v-model="formData!.alwaysShow">
<el-radio :label="false">关闭</el-radio>
<el-radio :label="true">开启</el-radio>
<el-tooltip content="常驻显示关闭后,头部小组件将在页面滑动时淡入" placement="top">
<el-radio :label="true">开启</el-radio>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="高度" prop="navBarHeight">
<el-slider
v-model="formData!.navBarHeight"
:max="100"
:min="35"
show-input
input-size="small"
/>
<el-form-item label="背景类型" prop="bgType">
<el-radio-group v-model="formData.bgType">
<el-radio label="color">纯色</el-radio>
<el-radio label="img">图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="返回按钮" prop="showGoBack">
<el-switch v-model="formData!.showGoBack" />
<el-form-item label="背景颜色" prop="bgColor" v-if="formData.bgType === 'color'">
<ColorInput v-model="formData.bgColor" />
</el-form-item>
<el-form-item label="背景颜色" prop="backgroundColor">
<ColorInput v-model="formData!.backgroundColor" />
</el-form-item>
<el-form-item label="背景图片" prop="backgroundImage">
<UploadImg v-model="formData!.backgroundImage" :limit="1">
<template #tip>建议宽度 750px</template>
</UploadImg>
<el-form-item label="背景图片" prop="bgImg" v-else>
<UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
</el-form-item>
<el-card class="property-group" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<span>内容小程序</span>
<el-form-item prop="_local.previewMp" class="m-b-0!">
<el-checkbox
v-model="formData._local.previewMp"
@change="formData._local.previewOther = !formData._local.previewMp"
>预览</el-checkbox
>
</el-form-item>
</div>
</template>
<NavigationBarCellProperty v-model="formData.mpCells" is-mp />
</el-card>
<el-card class="property-group" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<span>内容非小程序</span>
<el-form-item prop="_local.previewOther" class="m-b-0!">
<el-checkbox
v-model="formData._local.previewOther"
@change="formData._local.previewMp = !formData._local.previewOther"
>预览</el-checkbox
>
</el-form-item>
</div>
</template>
<NavigationBarCellProperty v-model="formData.otherCells" :is-mp="false" />
</el-card>
</el-form>
</template>
<script setup lang="ts">
import { NavigationBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import NavigationBarCellProperty from '@/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue'
//
defineOptions({ name: 'NavigationBarProperty' })
//
@ -58,6 +78,9 @@ const rules = {
const props = defineProps<{ modelValue: NavigationBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
if (!formData.value._local) {
formData.value._local = { previewMp: true, previewOther: false }
}
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,4 @@
import MyFormCreateDesigner from './src/MyFormCreateDesigner.vue'
import { useFormCreateDesigner } from './src/useFormCreateDesigner'
export { MyFormCreateDesigner, useFormCreateDesigner }

View File

@ -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>

View File

@ -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
}

View File

@ -0,0 +1,124 @@
import { generateUUID } from '@/utils'
import * as DictDataApi from '@/api/system/dict/dict.type'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useDictSelectRule = () => {
const label = '字典选择器'
const name = 'DictSelect'
const dictOptions = ref<{ label: string; value: string }[]>([]) // 字典类型下拉数据
onMounted(async () => {
const data = await DictDataApi.getSimpleDictTypeList()
if (!data || data.length === 0) {
return
}
dictOptions.value =
data?.map((item: DictDataApi.DictTypeVO) => ({
label: item.name,
value: item.type
})) ?? []
})
return {
icon: 'icon-select',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
props(_, { t }) {
return localeProps(t, name + '.props', [
makeRequiredRule(),
{
type: 'select',
field: 'dictType',
title: '字典类型',
value: '',
options: dictOptions.value
},
{
type: 'select',
field: 'valueType',
title: '字典值类型',
value: 'str',
options: [
{ label: '数字', value: 'int' },
{ label: '字符串', value: 'str' },
{ 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是否在输入框获得焦点后自动弹出选项菜单'
}
])
}
}
}

View File

@ -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
}
])
}
}
}

View File

@ -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
}
])
}
}
}

View File

@ -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'
}
])
}
}
}

View File

@ -0,0 +1,93 @@
import { generateUUID } from '@/utils'
import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
export const useUserSelectRule = () => {
const label = '用户选择器'
const name = 'UserSelect'
return {
icon: 'icon-select',
label,
name,
rule() {
return {
type: name,
field: generateUUID(),
title: label,
info: '',
$required: false
}
},
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是否在输入框获得焦点后自动弹出选项菜单'
}
])
}
}
}

View File

@ -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
})
})
})
}

View File

@ -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
}
}

View File

@ -189,7 +189,7 @@ const emit = defineEmits(['update:modelValue', 'hotAreaSelected'])
const emitUpdateModelValue = () => emit('update:modelValue', hotAreas)
//
const selectedHotAreaIndex = ref(-1)
const selectedHotAreaIndex = ref(0)
const handleHotAreaSelected = (hotArea: Rect, index: number) => {
selectedHotAreaIndex.value = index
emit('hotAreaSelected', hotArea, index)

View File

@ -6,7 +6,9 @@
:action="uploadUrl"
:auto-upload="autoUpload"
:before-upload="beforeUpload"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:limit="props.limit"
:multiple="props.limit > 1"
:on-error="excelUploadError"
@ -15,15 +17,14 @@
:on-remove="handleRemove"
:on-success="handleFileSuccess"
:show-file-list="true"
:http-request="httpRequest"
class="upload-file-uploader"
name="file"
>
<el-button type="primary">
<el-button v-if="!disabled" type="primary">
<Icon icon="ep:upload-filled" />
选取文件
</el-button>
<template v-if="isShowTip" #tip>
<template v-if="isShowTip && !disabled" #tip>
<div style="font-size: 8px">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</div>
@ -48,13 +49,13 @@ const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
title: propTypes.string.def('文件上传'),
fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // , ['png', 'jpg', 'jpeg']
fileSize: propTypes.number.def(5), // (MB)
limit: propTypes.number.def(5), //
autoUpload: propTypes.bool.def(true), //
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"
:before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:multiple="false"
:on-error="uploadError"
:on-success="uploadSuccess"
:show-file-list="false"
:http-request="httpRequest"
>
<template v-if="modelValue">
<img :src="modelValue" class="upload-image" />
<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" />
<span v-if="showBtnText">{{ t('action.edit') }}</span>
</div>
@ -77,10 +78,8 @@ const props = defineProps({
height: propTypes.string.def('150px'), // ==> 150px
width: propTypes.string.def('150px'), // ==> 150px
borderradius: propTypes.string.def('8px'), // ==> 8px
//
showDelete: propTypes.bool.def(true),
//
showBtnText: propTypes.bool.def(true)
showDelete: propTypes.bool.def(true), //
showBtnText: propTypes.bool.def(true) //
})
const { t } = useI18n() //
const message = useMessage() //

View File

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

View File

@ -1,22 +1,26 @@
import type { App } from 'vue'
// 👇使用 form-create 需额外全局引入 element plus 组件
import {
ElAlert,
ElAside,
ElPopconfirm,
ElHeader,
ElMain,
ElContainer,
ElDivider,
ElTransfer,
ElAlert,
ElTabs,
ElHeader,
ElMain,
ElPopconfirm,
ElTable,
ElTableColumn,
ElTabPane
ElTabPane,
ElTabs,
ElTransfer
} from 'element-plus'
import FcDesigner from '@form-create/designer'
import formCreate from '@form-create/element-ui'
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'
const components = [
ElAside,
@ -30,7 +34,12 @@ const components = [
ElTabs,
ElTable,
ElTableColumn,
ElTabPane
ElTabPane,
UploadImg,
UploadImgs,
UploadFile,
DictSelect,
UserSelect
]
// 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档
@ -40,4 +49,5 @@ export const setupFormCreate = (app: App<Element>) => {
})
formCreate.use(install)
app.use(formCreate)
app.use(FcDesigner)
}

View File

@ -248,15 +248,15 @@ export const CouponTemplateTakeTypeEnum = {
*/
export const PromotionProductScopeEnum = {
ALL: {
scope: 10,
scope: 1,
name: '通用劵'
},
SPU: {
scope: 20,
scope: 2,
name: '商品劵'
},
CATEGORY: {
scope: 30,
scope: 3,
name: '品类劵'
}
}

View File

@ -1,8 +1,8 @@
/**
*
*/
import { useDictStoreWithOut } from '@/store/modules/dict'
import { ElementPlusInfoType } from '@/types/elementPlus'
import {useDictStoreWithOut} from '@/store/modules/dict'
import {ElementPlusInfoType} from '@/types/elementPlus'
const dictStore = useDictStoreWithOut()
@ -104,6 +104,7 @@ export enum DICT_TYPE {
USER_TYPE = 'user_type',
COMMON_STATUS = 'common_status',
TERMINAL = 'terminal', // 终端
DATE_INTERVAL = 'date_interval', // 数据间隔
// ========== SYSTEM 模块 ==========
SYSTEM_USER_SEX = 'system_user_sex',

View File

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

View File

@ -329,10 +329,11 @@ const ERP_PRICE_DIGIT = 2
*
*
* @param num
* @package digit
* @return
*/
export const erpNumberFormatter = (num: number | string | undefined, digit: number) => {
if (num === null) {
if (num == null) {
return ''
}
if (typeof num === 'string') {
@ -404,3 +405,16 @@ export const erpPriceMultiply = (price: number, count: number) => {
}
return parseFloat((price * count).toFixed(ERP_PRICE_DIGIT))
}
/**
* ERP
*
* total 0 0
*
* @param value
* @param total
*/
export const erpCalculatePercentage = (value: number, total: number) => {
if (total === 0) return 0
return ((value / total) * 100).toFixed(2)
}

View File

@ -45,6 +45,7 @@ import * as FormApi from '@/api/bpm/form'
import FcDesigner from '@form-create/designer'
import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useFormCreateDesigner } from '@/components/FormCreate'
defineOptions({ name: 'BpmFormEditor' })
@ -55,6 +56,7 @@ const { query } = useRoute() // 路由信息
const { delView } = useTagsViewStore() //
const designer = ref() //
useFormCreateDesigner(designer) //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const formData = ref({

View File

@ -24,15 +24,15 @@
{{ processInstance?.startUser.nickname }}
<el-tag size="small" type="info">{{ processInstance?.startUser.deptName }}</el-tag>
</el-form-item>
<el-card class="mb-15px !-mt-10px" v-if="runningTasks[index].formId > 0">
<el-card v-if="runningTasks[index].formId > 0" class="mb-15px !-mt-10px">
<template #header>
<span class="el-icon-picture-outline">
填写表单{{ runningTasks[index]?.formName }}
</span>
</template>
<form-create
v-model:api="approveFormFApis[index]"
v-model="approveForms[index].value"
v-model:api="approveFormFApis[index]"
:option="approveForms[index].option"
:rule="approveForms[index].rule"
/>
@ -92,8 +92,8 @@
<!-- 情况一流程表单 -->
<el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16">
<form-create
ref="fApi"
v-model="detailForm.value"
v-model:api="fApi"
:option="detailForm.option"
:rule="detailForm.rule"
/>
@ -280,9 +280,9 @@ const getProcessInstance = async () => {
data.formVariables
)
nextTick().then(() => {
fApi.value?.fapi?.btn.show(false)
fApi.value?.fapi?.resetBtn.show(false)
fApi.value?.fapi?.disabled(true)
fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false)
fApi.value?.disabled(true)
})
} else {
// data.processDefinition.formCustomViewPath /crm/contract/detail/index.vue

View File

@ -19,7 +19,7 @@
</el-select>
</el-form-item>
<el-form-item label="老负责人">
<el-radio-group v-model="oldOwnerHandler" @change="formData.oldOwnerPermissionLevel">
<el-radio-group v-model="oldOwnerHandler" @change="handleOwnerChange">
<el-radio :label="false" size="large">移除</el-radio>
<el-radio :label="true" size="large">加入团队</el-radio>
</el-radio-group>
@ -86,10 +86,16 @@ const open = async (bizId: number) => {
dialogVisible.value = true
dialogTitle.value = getDialogTitle()
resetForm()
formData.value.bizId = bizId
formData.value.id = bizId
}
defineExpose({ open }) // open
//
const handleOwnerChange = (val: boolean) => {
if (!val) {
// oldOwnerPermissionLevel
formData.value.oldOwnerPermissionLevel = undefined
}
}
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {

View File

@ -10,11 +10,39 @@
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="客户名称" align="center" prop="customerName" min-width="200" />
<el-table-column label="序号" align="center" type="index" width="80" fixed="left" />
<el-table-column
label="客户名称"
align="center"
prop="customerName"
min-width="200"
fixed="left"
/>
<el-table-column label="合同名称" align="center" prop="contractName" min-width="200" />
<el-table-column label="合同总金额" align="center" prop="totalPrice" min-width="200" />
<el-table-column label="回款金额" align="center" prop="receivablePrice" min-width="200" />
<el-table-column
label="合同总金额"
align="center"
prop="totalPrice"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column
label="回款金额"
align="center"
prop="receivablePrice"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
<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="客户行业" 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 label="负责人" align="center" prop="ownerUserName" min-width="200" />
<el-table-column label="创建人" align="center" prop="creatorUserName" min-width="200" />
<el-table-column
@ -28,8 +56,9 @@
label="下单日期"
align="center"
prop="orderDate"
:formatter="dateFormatter2"
:formatter="dateFormatter"
min-width="200"
fixed="right"
/>
</el-table>
</el-card>
@ -40,10 +69,12 @@ import {
CrmStatisticsCustomerSummaryByDateRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
import { round } from 'lodash-es'
import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
import { dateFormatter } from '@/utils/formatTime'
import { erpPriceTableColumnFormatter } from '@/utils'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({ name: 'CustomerConversionStat' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //
@ -53,7 +84,7 @@ const list = ref<CrmStatisticsCustomerSummaryByDateRespVO[]>([]) // 列表的数
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
right: 40, // X
bottom: 20,
containLabel: true
},
@ -93,10 +124,9 @@ const echartsOption = reactive<EChartsOption>({
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1.
loading.value = true
const customerCount = await StatisticsCustomerApi.getCustomerSummaryByDate(props.queryParams)
const contractSummary = await StatisticsCustomerApi.getContractSummary(props.queryParams)
// 2.1 Echarts
@ -111,7 +141,7 @@ const loadData = async () => {
return {
name: item.time,
value: item.customerCreateCount
? round((item.customerDealCount / item.customerCreateCount) * 100, 2)
? ((item.customerDealCount / item.customerCreateCount) * 100).toFixed(2)
: 0
}
}
@ -119,8 +149,18 @@ const loadData = async () => {
}
// 2.2
list.value = contractSummary
loading.value = false
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */

View File

@ -26,11 +26,12 @@
import {
StatisticsCustomerApi,
CrmStatisticsCustomerDealCycleByDateRespVO,
CrmStatisticsCustomerSummaryByDateRespVO,
CrmStatisticsCustomerSummaryByDateRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
defineOptions({ name: 'CustomerDealCycle' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //
@ -40,7 +41,7 @@ const list = ref<CrmStatisticsCustomerDealCycleByDateRespVO[]>([]) // 列表的
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
right: 40, // X
bottom: 20,
containLabel: true
},
@ -49,12 +50,14 @@ const echartsOption = reactive<EChartsOption>({
{
name: '成交周期(天)',
type: 'bar',
data: []
data: [],
yAxisIndex: 0
},
{
name: '成交客户数',
type: 'bar',
data: []
data: [],
yAxisIndex: 1
}
],
toolbox: {
@ -74,10 +77,26 @@ const echartsOption = reactive<EChartsOption>({
type: 'shadow'
}
},
yAxis: {
type: 'value',
name: '数量(个)'
},
yAxis: [
{
type: 'value',
name: '成交周期(天)',
min: 0,
minInterval: 1 //
},
{
type: 'value',
name: '成交客户数',
min: 0,
minInterval: 1, //
splitLine: {
lineStyle: {
type: 'dotted', // 线,
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
@ -85,14 +104,13 @@ const echartsOption = reactive<EChartsOption>({
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1.
loading.value = true
const customerDealCycleByDate = await StatisticsCustomerApi.getCustomerDealCycleByDate(
props.queryParams
)
const customerSummaryByDate = await StatisticsCustomerApi.getCustomerSummaryByDate(
const customerSummaryByDate = await StatisticsCustomerApi.getCustomerSummaryByDate(
props.queryParams
)
const customerDealCycleByUser = await StatisticsCustomerApi.getCustomerDealCycleByUser(
@ -116,7 +134,16 @@ const loadData = async () => {
}
// 2.2
list.value = customerDealCycleByUser
loading.value = false
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })

View File

@ -12,11 +12,11 @@
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="员工姓名" align="center" prop="ownerUserName" min-width="200" />
<el-table-column label="跟进次数" align="right" prop="followupRecordCount" min-width="200" />
<el-table-column label="跟进次数" align="right" prop="followUpRecordCount" min-width="200" />
<el-table-column
label="跟进客户数"
align="right"
prop="followupCustomerCount"
prop="followUpCustomerCount"
min-width="200"
/>
</el-table>
@ -25,22 +25,24 @@
<script setup lang="ts">
import {
StatisticsCustomerApi,
CrmStatisticsFollowupSummaryByDateRespVO,
CrmStatisticsFollowupSummaryByUserRespVO
CrmStatisticsFollowUpSummaryByDateRespVO,
CrmStatisticsFollowUpSummaryByUserRespVO
} from '@/api/crm/statistics/customer'
import Echart from '@/components/Echart/src/Echart.vue'
import { EChartsOption } from 'echarts'
defineOptions({ name: 'CustomerFollowupSummary' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //
const list = ref<CrmStatisticsFollowupSummaryByUserRespVO[]>([]) //
const list = ref<CrmStatisticsFollowUpSummaryByUserRespVO[]>([]) //
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
right: 30, // X
bottom: 20,
containLabel: true
},
@ -49,11 +51,13 @@ const echartsOption = reactive<EChartsOption>({
{
name: '跟进客户数',
type: 'bar',
yAxisIndex: 0,
data: []
},
{
name: '跟进次数',
type: 'bar',
yAxisIndex: 1,
data: []
}
],
@ -74,46 +78,74 @@ const echartsOption = reactive<EChartsOption>({
type: 'shadow'
}
},
yAxis: {
type: 'value',
name: '数量(个)'
},
yAxis: [
{
type: 'value',
name: '跟进客户数',
min: 0,
minInterval: 1 //
},
{
type: 'value',
name: '跟进次数',
min: 0,
minInterval: 1, //
splitLine: {
lineStyle: {
type: 'dotted', // 线,
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
axisTick: {
alignWithLabel: true
},
data: []
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1.
loading.value = true
const followupSummaryByDate = await StatisticsCustomerApi.getFollowupSummaryByDate(
const followUpSummaryByDate = await StatisticsCustomerApi.getFollowUpSummaryByDate(
props.queryParams
)
const followupSummaryByUser = await StatisticsCustomerApi.getFollowupSummaryByUser(
const followUpSummaryByUser = await StatisticsCustomerApi.getFollowUpSummaryByUser(
props.queryParams
)
// 2.1 Echarts
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = followupSummaryByDate.map(
(s: CrmStatisticsFollowupSummaryByDateRespVO) => s.time
echartsOption.xAxis['data'] = followUpSummaryByDate.map(
(s: CrmStatisticsFollowUpSummaryByDateRespVO) => s.time
)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = followupSummaryByDate.map(
(s: CrmStatisticsFollowupSummaryByDateRespVO) => s.followupCustomerCount
echartsOption.series[0]['data'] = followUpSummaryByDate.map(
(s: CrmStatisticsFollowUpSummaryByDateRespVO) => s.followUpCustomerCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = followupSummaryByDate.map(
(s: CrmStatisticsFollowupSummaryByDateRespVO) => s.followupRecordCount
echartsOption.series[1]['data'] = followUpSummaryByDate.map(
(s: CrmStatisticsFollowUpSummaryByDateRespVO) => s.followUpRecordCount
)
}
// 2.2
list.value = followupSummaryByUser
loading.value = false
list.value = followUpSummaryByUser
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })

View File

@ -11,8 +11,12 @@
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="跟进方式" align="center" prop="followupType" min-width="200" />
<el-table-column label="个数" align="center" prop="followupRecordCount" min-width="200" />
<el-table-column label="跟进方式" align="center" prop="followUpType" min-width="200">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_FOLLOW_UP_TYPE" :value="scope.row.followUpType" />
</template>
</el-table-column>
<el-table-column label="个数" align="center" prop="followUpRecordCount" min-width="200" />
<el-table-column label="占比(%)" align="center" prop="portion" min-width="200" />
</el-table>
</el-card>
@ -20,16 +24,19 @@
<script setup lang="ts">
import {
StatisticsCustomerApi,
CrmStatisticsFollowupSummaryByTypeRespVO
CrmStatisticsFollowUpSummaryByTypeRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
import { round, sumBy } from 'lodash-es'
import { sumBy } from 'lodash-es'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { erpCalculatePercentage } from '@/utils'
defineOptions({ name: 'CustomerFollowupType' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //
const list = ref<CrmStatisticsFollowupSummaryByTypeRespVO[]>([]) //
const list = ref<CrmStatisticsFollowUpSummaryByTypeRespVO[]>([]) //
/** 饼图配置 */
const echartsOption = reactive<EChartsOption>({
@ -67,35 +74,43 @@ const echartsOption = reactive<EChartsOption>({
]
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1.
loading.value = true
const followupSummaryByType = await StatisticsCustomerApi.getFollowupSummaryByType(
const followUpSummaryByType = await StatisticsCustomerApi.getFollowUpSummaryByType(
props.queryParams
)
// 2.1 Echarts
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = followupSummaryByType.map(
(r: CrmStatisticsFollowupSummaryByTypeRespVO) => {
echartsOption.series[0]['data'] = followUpSummaryByType.map(
(row: CrmStatisticsFollowUpSummaryByTypeRespVO) => {
return {
name: r.followupType,
value: r.followupRecordCount
name: getDictLabel(DICT_TYPE.CRM_FOLLOW_UP_TYPE, row.followUpType),
value: row.followUpRecordCount
}
}
)
}
// 2.2
const totalCount = sumBy(followupSummaryByType, 'followupRecordCount')
list.value = followupSummaryByType.map((r: CrmStatisticsFollowupSummaryByTypeRespVO) => {
const totalCount = sumBy(followUpSummaryByType, 'followUpRecordCount')
list.value = followUpSummaryByType.map((row: CrmStatisticsFollowUpSummaryByTypeRespVO) => {
return {
followupType: r.followupType,
followupRecordCount: r.followupRecordCount,
portion: round((r.followupRecordCount / totalCount) * 100, 2)
...row,
portion: erpCalculatePercentage(row.followUpRecordCount, totalCount)
}
})
loading.value = false
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */

View File

@ -0,0 +1,154 @@
<!-- 客户总量统计 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" fixed="left" />
<el-table-column label="员工姓名" prop="ownerUserName" min-width="100" fixed="left" />
<el-table-column
label="进入公海客户数"
align="right"
prop="customerPutCount"
min-width="200"
/>
<el-table-column
label="公海领取客户数"
align="right"
prop="customerTakeCount"
min-width="200"
/>
</el-table>
</el-card>
</template>
<script setup lang="ts">
import {
StatisticsCustomerApi,
CrmStatisticsPoolSummaryByDateRespVO,
CrmStatisticsPoolSummaryByUserRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
defineOptions({ name: 'CustomerPoolSummary' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //
const list = ref<CrmStatisticsPoolSummaryByUserRespVO[]>([]) //
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 40, // X
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '进入公海客户数',
type: 'bar',
yAxisIndex: 0,
data: []
},
{
name: '公海领取客户数',
type: 'bar',
yAxisIndex: 1,
data: []
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // Y
},
brush: {
type: ['lineX', 'clear'] //
},
saveAsImage: { show: true, name: '公海客户分析' } //
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '进入公海客户数',
min: 0,
minInterval: 1 //
},
{
type: 'value',
name: '公海领取客户数',
min: 0,
minInterval: 1, //
splitLine: {
lineStyle: {
type: 'dotted', // 线,
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
data: []
}
}) as EChartsOption
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1.
const poolSummaryByDate = await StatisticsCustomerApi.getPoolSummaryByDate(props.queryParams)
const poolSummaryByUser = await StatisticsCustomerApi.getPoolSummaryByUser(props.queryParams)
// 2.1 Echarts
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = poolSummaryByDate.map(
(s: CrmStatisticsPoolSummaryByDateRespVO) => s.time
)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = poolSummaryByDate.map(
(s: CrmStatisticsPoolSummaryByDateRespVO) => s.customerPutCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = poolSummaryByDate.map(
(s: CrmStatisticsPoolSummaryByDateRespVO) => s.customerTakeCount
)
}
// 2.2
list.value = poolSummaryByUser
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */
onMounted(() => {
loadData()
})
</script>

View File

@ -10,8 +10,8 @@
<!-- 统计列表 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="list">
<el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="员工姓名" prop="ownerUserName" min-width="100" />
<el-table-column label="序号" align="center" type="index" width="80" fixed="left" />
<el-table-column label="员工姓名" prop="ownerUserName" min-width="100" fixed="left" />
<el-table-column
label="新增客户数"
align="right"
@ -21,28 +21,31 @@
<el-table-column label="成交客户数" align="right" prop="customerDealCount" min-width="200" />
<el-table-column label="客户成交率(%)" align="right" min-width="200">
<template #default="scope">
{{
scope.row.customerCreateCount !== 0
? round((scope.row.customerDealCount / scope.row.customerCreateCount) * 100, 2)
: 0
}}
{{ erpCalculatePercentage(scope.row.customerDealCount, scope.row.customerCreateCount) }}
</template>
</el-table-column>
<el-table-column label="合同总金额" align="right" prop="contractPrice" min-width="200" />
<el-table-column label="回款金额" align="right" prop="receivablePrice" min-width="200" />
<el-table-column
label="合同总金额"
align="right"
prop="contractPrice"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column
label="回款金额"
align="right"
prop="receivablePrice"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
<el-table-column label="未回款金额" align="right" min-width="200">
<!-- TODO @dhb52参考 util/index.ts // ========== ERP ========== -->
<template #default="scope">
{{ round(scope.row.contractPrice - scope.row.receivablePrice, 2) }}
{{ erpCalculatePercentage(scope.row.receivablePrice, scope.row.contractPrice) }}
</template>
</el-table-column>
<el-table-column label="回款完成率(%)" align="right" min-width="200">
<el-table-column label="回款完成率(%)" align="right" min-width="200" fixed="right">
<template #default="scope">
{{
scope.row.contractPrice !== 0
? round((scope.row.receivablePrice / scope.row.contractPrice) * 100, 2)
: 0
}}
{{ erpCalculatePercentage(scope.row.receivablePrice, scope.row.contractPrice) }}
</template>
</el-table-column>
</el-table>
@ -55,9 +58,10 @@ import {
CrmStatisticsCustomerSummaryByUserRespVO
} from '@/api/crm/statistics/customer'
import { EChartsOption } from 'echarts'
import { round } from 'lodash-es'
import { erpCalculatePercentage, erpPriceTableColumnFormatter } from '@/utils'
defineOptions({ name: 'CustomerSummary' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //
@ -67,7 +71,7 @@ const list = ref<CrmStatisticsCustomerSummaryByUserRespVO[]>([]) // 列表的数
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
right: 30, // X
bottom: 20,
containLabel: true
},
@ -76,11 +80,13 @@ const echartsOption = reactive<EChartsOption>({
{
name: '新增客户数',
type: 'bar',
yAxisIndex: 0,
data: []
},
{
name: '成交客户数',
type: 'bar',
yAxisIndex: 1,
data: []
}
],
@ -101,10 +107,26 @@ const echartsOption = reactive<EChartsOption>({
type: 'shadow'
}
},
yAxis: {
type: 'value',
name: '数量(个)'
},
yAxis: [
{
type: 'value',
name: '新增客户数',
min: 0,
minInterval: 1 //
},
{
type: 'value',
name: '成交客户数',
min: 0,
minInterval: 1, //
splitLine: {
lineStyle: {
type: 'dotted', // 线,
opacity: 0.7
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
@ -112,10 +134,9 @@ const echartsOption = reactive<EChartsOption>({
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
/** 获取数据并填充图表 */
const fetchAndFill = async () => {
// 1.
loading.value = true
const customerSummaryByDate = await StatisticsCustomerApi.getCustomerSummaryByDate(
props.queryParams
)
@ -138,10 +159,21 @@ const loadData = async () => {
(s: CrmStatisticsCustomerSummaryByDateRespVO) => s.customerDealCount
)
}
// 2.2
list.value = customerSummaryByUser
loading.value = false
}
/** 获取统计数据 */
const loadData = async () => {
loading.value = true
try {
await fetchAndFill()
} finally {
loading.value = false
}
}
defineExpose({ loadData })
/** 初始化 */

View File

@ -3,49 +3,77 @@
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="时间范围" prop="orderDate">
<el-date-picker
v-model="queryParams.times"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
:shortcuts="defaultShortcuts"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
@change="handleQuery"
/>
</el-form-item>
<el-form-item label="时间间隔" prop="interval">
<el-select
v-model="queryParams.interval"
class="!w-240px"
placeholder="间隔类型"
@change="handleQuery"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.DATE_INTERVAL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="queryParams.deptId"
class="!w-240px"
:data="deptList"
:props="defaultProps"
check-strictly
class="!w-240px"
node-key="id"
placeholder="请选择归属部门"
@change="queryParams.userId = undefined"
@change="(queryParams.userId = undefined), handleQuery()"
/>
</el-form-item>
<el-form-item label="员工" prop="userId">
<el-select v-model="queryParams.userId" class="!w-240px" placeholder="员工" clearable>
<el-select
v-model="queryParams.userId"
class="!w-240px"
clearable
placeholder="员工"
@change="handleQuery"
>
<el-option
v-for="(user, index) in userListByDeptId"
:key="index"
:label="user.nickname"
:value="user.id"
:key="index"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" /> 搜索 </el-button>
<el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 重置 </el-button>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
查询
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
@ -54,24 +82,28 @@
<el-col>
<el-tabs v-model="activeTab">
<!-- 客户总量分析 -->
<el-tab-pane label="客户总量分析" name="customerSummary" lazy>
<CustomerSummary :query-params="queryParams" ref="customerSummaryRef" />
<el-tab-pane label="客户总量分析" lazy name="customerSummary">
<CustomerSummary ref="customerSummaryRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户跟进次数分析 -->
<el-tab-pane label="客户跟进次数分析" name="followupSummary" lazy>
<CustomerFollowupSummary :query-params="queryParams" ref="followupSummaryRef" />
<el-tab-pane label="客户跟进次数分析" lazy name="followUpSummary">
<CustomerFollowUpSummary ref="followUpSummaryRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户跟进方式分析 -->
<el-tab-pane label="客户跟进方式分析" name="followupType" lazy>
<CustomerFollowupType :query-params="queryParams" ref="followupTypeRef" />
<el-tab-pane label="客户跟进方式分析" lazy name="followUpType">
<CustomerFollowUpType ref="followUpTypeRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 客户转化率分析 -->
<el-tab-pane label="客户转化率分析" name="conversionStat" lazy>
<CustomerConversionStat :query-params="queryParams" ref="conversionStatRef" />
<el-tab-pane label="客户转化率分析" lazy name="conversionStat">
<CustomerConversionStat ref="conversionStatRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 公海客户分析 -->
<el-tab-pane label="公海客户分析" lazy name="poolSummary">
<CustomerPoolSummary ref="customerPoolSummaryRef" :query-params="queryParams" />
</el-tab-pane>
<!-- 成交周期分析 -->
<el-tab-pane label="成交周期分析" name="dealCycle" lazy>
<CustomerDealCycle :query-params="queryParams" ref="dealCycleRef" />
<el-tab-pane label="成交周期分析" lazy name="dealCycle">
<CustomerDealCycle ref="dealCycleRef" :query-params="queryParams" />
</el-tab-pane>
</el-tabs>
</el-col>
@ -81,17 +113,20 @@
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
import { defaultProps, handleTree } from '@/utils/tree'
import CustomerSummary from './components/CustomerSummary.vue'
import CustomerFollowupSummary from './components/CustomerFollowupSummary.vue'
import CustomerFollowupType from './components/CustomerFollowupType.vue'
import CustomerConversionStat from './components/CustomerConversionStat.vue'
import CustomerDealCycle from './components/CustomerDealCycle.vue'
import CustomerFollowUpSummary from './components/CustomerFollowUpSummary.vue'
import CustomerFollowUpType from './components/CustomerFollowUpType.vue'
import CustomerSummary from './components/CustomerSummary.vue'
import CustomerPoolSummary from './components/CustomerPoolSummary.vue'
defineOptions({ name: 'CrmStatisticsCustomer' })
const queryParams = reactive({
interval: 2, // WEEK,
deptId: useUserStore().getUser.deptId,
userId: undefined,
times: [
@ -104,50 +139,47 @@ const queryParams = reactive({
const queryFormRef = ref() //
const deptList = ref<Tree[]>([]) //
const userList = ref<UserApi.UserVO[]>([]) //
//
/** 根据选择的部门筛选员工清单 */
const userListByDeptId = computed(() =>
queryParams.deptId
? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
: []
)
//
const activeTab = ref('customerSummary')
// 1.
const customerSummaryRef = ref()
// 2.
const followupSummaryRef = ref()
// 3.
const followupTypeRef = ref()
// 4.
const conversionStatRef = ref()
// 5.
// crm_owner_record
// 6.
const dealCycleRef = ref()
const activeTab = ref('customerSummary') //
const customerSummaryRef = ref() // 1.
const followUpSummaryRef = ref() // 2.
const followUpTypeRef = ref() // 3.
const conversionStatRef = ref() // 4.
const customerPoolSummaryRef = ref() // 5.
const dealCycleRef = ref() // 6.
/** 搜索按钮操作 */
const handleQuery = () => {
switch (activeTab.value) {
case 'customerSummary':
case 'customerSummary': //
customerSummaryRef.value?.loadData?.()
break
case 'followupSummary':
followupSummaryRef.value?.loadData?.()
case 'followUpSummary': //
followUpSummaryRef.value?.loadData?.()
break
case 'followupType':
followupTypeRef.value?.loadData?.()
case 'followUpType': //
followUpTypeRef.value?.loadData?.()
break
case 'conversionStat':
case 'conversionStat': //
conversionStatRef.value?.loadData?.()
break
case 'dealCycle':
case 'poolSummary': //
customerPoolSummaryRef.value?.loadData?.()
break
case 'dealCycle': //
dealCycleRef.value?.loadData?.()
break
}
}
// activeTab tab
/** 当 activeTab 改变时,刷新当前活动的 tab */
watch(activeTab, () => {
handleQuery()
})
@ -158,7 +190,7 @@ const resetQuery = () => {
handleQuery()
}
//
/** 初始化 */
onMounted(async () => {
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
userList.value = handleTree(await UserApi.getSimpleUserList())

View File

@ -0,0 +1,227 @@
<!-- 客户总量统计 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 TODO @scholar统计列表的展示不对 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="tableData">
<el-table-column
v-for="item in columnsData"
:key="item.prop"
:label="item.label"
:prop="item.prop"
align="center"
>
<template #default="scope">
{{ scope.row[item.prop] }}
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script setup lang="ts">
import { EChartsOption } from 'echarts'
import {
StatisticsPerformanceApi,
StatisticsPerformanceRespVO
} from '@/api/crm/statistics/performance'
defineOptions({ name: 'ContractCountPerformance' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //
const list = ref<StatisticsPerformanceRespVO[]>([]) //
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '当月合同数量(个)',
type: 'line',
data: []
},
{
name: '上月合同数量(个)',
type: 'line',
data: []
},
{
name: '去年同月合同数量(个)',
type: 'line',
data: []
},
{
name: '同比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
},
{
name: '环比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // Y
},
brush: {
type: ['lineX', 'clear'] //
},
saveAsImage: { show: true, name: '客户总量分析' } //
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '数量(个)',
axisTick: {
show: false
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
},
{
type: 'value',
name: '',
axisTick: {
alignWithLabel: true,
lineStyle: {
width: 0
}
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}%'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
data: []
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1.
loading.value = true
const performanceList = await StatisticsPerformanceApi.getContractCountPerformance(
props.queryParams
)
// 2.1 Echarts
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = performanceList.map((s: StatisticsPerformanceRespVO) => s.time)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.currentMonthCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
)
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
)
}
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
echartsOption.series[2]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastYearCount
)
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
)
}
// 2.2
list.value = performanceList
loading.value = false
}
//
const columnsData = reactive([])
const tableData = reactive([
{ title: '当月合同数量统计(个)' },
{ title: '上月合同数量统计(个)' },
{ title: '去年当月合同数量统计(个)' },
{ title: '同比增长率(%' },
{ title: '环比增长率(%' }
])
// init
const init = () => {
const columnObj = { label: '日期', prop: 'title' }
columnsData.push(columnObj)
list.value.forEach((item, index) => {
const columnObj = { label: item.time, prop: 'prop' + index }
columnsData.push(columnObj)
tableData[0]['prop' + index] = item.currentMonthCount
tableData[1]['prop' + index] = item.lastMonthCount
tableData[2]['prop' + index] = item.lastYearCount
tableData[3]['prop' + index] =
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
tableData[4]['prop' + index] =
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
})
}
defineExpose({ loadData })
/** 初始化 */
onMounted(async () => {
await loadData()
init()
})
</script>

View File

@ -0,0 +1,227 @@
<!-- 客户总量统计 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 TODO @scholar统计列表的展示不对 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="tableData">
<el-table-column
v-for="item in columnsData"
:key="item.prop"
:label="item.label"
:prop="item.prop"
align="center"
>
<template #default="scope">
{{ scope.row[item.prop] }}
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script setup lang="ts">
import { EChartsOption } from 'echarts'
import {
StatisticsPerformanceApi,
StatisticsPerformanceRespVO
} from '@/api/crm/statistics/performance'
defineOptions({ name: 'ContractPricePerformance' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //
const list = ref<StatisticsPerformanceRespVO[]>([]) //
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '当月合同金额(元)',
type: 'line',
data: []
},
{
name: '上月合同金额(元)',
type: 'line',
data: []
},
{
name: '去年同月合同金额(元)',
type: 'line',
data: []
},
{
name: '同比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
},
{
name: '环比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // Y
},
brush: {
type: ['lineX', 'clear'] //
},
saveAsImage: { show: true, name: '客户总量分析' } //
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '金额(元)',
axisTick: {
show: false
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
},
{
type: 'value',
name: '',
axisTick: {
alignWithLabel: true,
lineStyle: {
width: 0
}
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}%'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
data: []
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1.
loading.value = true
const performanceList = await StatisticsPerformanceApi.getContractPricePerformance(
props.queryParams
)
// 2.1 Echarts
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = performanceList.map((s: StatisticsPerformanceRespVO) => s.time)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.currentMonthCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
)
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
)
}
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
echartsOption.series[2]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastYearCount
)
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
)
}
// 2.2
list.value = performanceList
loading.value = false
}
//
const columnsData = reactive([])
const tableData = reactive([
{ title: '当月合同金额统计(元)' },
{ title: '上月合同金额统计(元)' },
{ title: '去年当月合同金额统计(元)' },
{ title: '同比增长率(%' },
{ title: '环比增长率(%' }
])
// init
const init = () => {
const columnObj = { label: '日期', prop: 'title' }
columnsData.push(columnObj)
list.value.forEach((item, index) => {
const columnObj = { label: item.time, prop: 'prop' + index }
columnsData.push(columnObj)
tableData[0]['prop' + index] = item.currentMonthCount
tableData[1]['prop' + index] = item.lastMonthCount
tableData[2]['prop' + index] = item.lastYearCount
tableData[3]['prop' + index] =
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
tableData[4]['prop' + index] =
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
})
}
defineExpose({ loadData })
/** 初始化 */
onMounted(async () => {
await loadData()
init()
})
</script>

View File

@ -0,0 +1,227 @@
<!-- 客户总量统计 -->
<template>
<!-- Echarts图 -->
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<Echart :height="500" :options="echartsOption" />
</el-skeleton>
</el-card>
<!-- 统计列表 TODO @scholar统计列表的展示不对 -->
<el-card shadow="never" class="mt-16px">
<el-table v-loading="loading" :data="tableData">
<el-table-column
v-for="item in columnsData"
:key="item.prop"
:label="item.label"
:prop="item.prop"
align="center"
>
<template #default="scope">
{{ scope.row[item.prop] }}
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script setup lang="ts">
import { EChartsOption } from 'echarts'
import {
StatisticsPerformanceApi,
StatisticsPerformanceRespVO
} from '@/api/crm/statistics/performance'
defineOptions({ name: 'ContractPricePerformance' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //
const list = ref<StatisticsPerformanceRespVO[]>([]) //
/** 柱状图配置:纵向 */
const echartsOption = reactive<EChartsOption>({
grid: {
left: 20,
right: 20,
bottom: 20,
containLabel: true
},
legend: {},
series: [
{
name: '当月回款金额(元)',
type: 'line',
data: []
},
{
name: '上月回款金额(元)',
type: 'line',
data: []
},
{
name: '去年同月回款金额(元)',
type: 'line',
data: []
},
{
name: '同比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
},
{
name: '环比增长率(%',
type: 'line',
yAxisIndex: 1,
data: []
}
],
toolbox: {
feature: {
dataZoom: {
xAxisIndex: false // Y
},
brush: {
type: ['lineX', 'clear'] //
},
saveAsImage: { show: true, name: '客户总量分析' } //
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
yAxis: [
{
type: 'value',
name: '金额(元)',
axisTick: {
show: false
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
},
{
type: 'value',
name: '',
axisTick: {
alignWithLabel: true,
lineStyle: {
width: 0
}
},
axisLabel: {
color: '#BDBDBD',
formatter: '{value}%'
},
/** 坐标轴轴线相关设置 */
axisLine: {
lineStyle: {
color: '#BDBDBD'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e6e6e6'
}
}
}
],
xAxis: {
type: 'category',
name: '日期',
data: []
}
}) as EChartsOption
/** 获取统计数据 */
const loadData = async () => {
// 1.
loading.value = true
const performanceList = await StatisticsPerformanceApi.getReceivablePricePerformance(
props.queryParams
)
// 2.1 Echarts
if (echartsOption.xAxis && echartsOption.xAxis['data']) {
echartsOption.xAxis['data'] = performanceList.map((s: StatisticsPerformanceRespVO) => s.time)
}
if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
echartsOption.series[0]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.currentMonthCount
)
}
if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
echartsOption.series[1]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
)
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
)
}
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[1]['data']) {
echartsOption.series[2]['data'] = performanceList.map(
(s: StatisticsPerformanceRespVO) => s.lastYearCount
)
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
)
}
// 2.2
list.value = performanceList
loading.value = false
}
//
const columnsData = reactive([])
const tableData = reactive([
{ title: '当月回款金额统计(元)' },
{ title: '上月回款金额统计(元)' },
{ title: '去年当月回款金额统计(元)' },
{ title: '同比增长率(%' },
{ title: '环比增长率(%' }
])
// init
const init = () => {
const columnObj = { label: '日期', prop: 'title' }
columnsData.push(columnObj)
list.value.forEach((item, index) => {
const columnObj = { label: item.time, prop: 'prop' + index }
columnsData.push(columnObj)
tableData[0]['prop' + index] = item.currentMonthCount
tableData[1]['prop' + index] = item.lastMonthCount
tableData[2]['prop' + index] = item.lastYearCount
tableData[3]['prop' + index] =
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
tableData[4]['prop' + index] =
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
})
}
defineExpose({ loadData })
/** 初始化 */
onMounted(async () => {
await loadData()
init()
})
</script>

View File

@ -0,0 +1,147 @@
<!-- 数据统计 - 员工客户分析 -->
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="时间范围" prop="orderDate">
<el-date-picker
v-model="queryParams.times"
:shortcuts="defaultShortcuts"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
/>
</el-form-item>
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="queryParams.deptId"
class="!w-240px"
:data="deptList"
:props="defaultProps"
check-strictly
node-key="id"
placeholder="请选择归属部门"
@change="queryParams.userId = undefined"
/>
</el-form-item>
<el-form-item label="员工" prop="userId">
<el-select v-model="queryParams.userId" class="!w-240px" placeholder="员工" clearable>
<el-option
v-for="(user, index) in userListByDeptId"
:label="user.nickname"
:value="user.id"
:key="index"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" /> 搜索 </el-button>
<el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 重置 </el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 员工业绩统计 -->
<el-col>
<el-tabs v-model="activeTab">
<!-- 员工合同统计 -->
<el-tab-pane label="员工合同数量统计" name="ContractCountPerformance" lazy>
<ContractCountPerformance :query-params="queryParams" ref="ContractCountPerformanceRef" />
</el-tab-pane>
<!-- 员工合同金额统计 -->
<el-tab-pane label="员工合同金额统计" name="ContractPricePerformance" lazy>
<ContractPricePerformance :query-params="queryParams" ref="ContractPricePerformanceRef" />
</el-tab-pane>
<!-- 员工回款金额统计 -->
<el-tab-pane label="员工回款金额统计" name="followupType" lazy>
<ReceivablePricePerformance
:query-params="queryParams"
ref="ReceivablePricePerformanceRef"
/>
</el-tab-pane>
</el-tabs>
</el-col>
</template>
<script lang="ts" setup>
import * as DeptApi from '@/api/system/dept'
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'
import ContractCountPerformance from './components/ContractCountPerformance.vue'
import ContractPricePerformance from './components/ContractPricePerformance.vue'
import ReceivablePricePerformance from './components/ReceivablePricePerformance.vue'
defineOptions({ name: 'CrmStatisticsPerformance' })
const queryParams = reactive({
deptId: useUserStore().getUser.deptId,
userId: undefined,
times: [
//
formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
]
})
const queryFormRef = ref() //
const deptList = ref<Tree[]>([]) //
const userList = ref<UserApi.UserVO[]>([]) //
//
const userListByDeptId = computed(() =>
queryParams.deptId
? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
: []
)
//
const activeTab = ref('ContractCountPerformance')
// 1.
const ContractCountPerformanceRef = ref()
// 2.
const ContractPricePerformanceRef = ref()
// 3.
const ReceivablePricePerformanceRef = ref()
/** 搜索按钮操作 */
const handleQuery = () => {
switch (activeTab.value) {
case 'ContractCountPerformance':
ContractCountPerformanceRef.value?.loadData?.()
break
case 'ContractPricePerformance':
ContractPricePerformanceRef.value?.loadData?.()
break
case 'ReceivablePricePerformance':
ReceivablePricePerformanceRef.value?.loadData?.()
break
}
}
// activeTab tab
watch(activeTab, () => {
handleQuery()
})
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
//
onMounted(async () => {
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
userList.value = handleTree(await UserApi.getSimpleUserList())
})
</script>

View File

@ -0,0 +1,153 @@
<!-- 客户城市分布 -->
<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,
StatisticsPortraitApi
} from '@/api/crm/statistics/portrait'
// TODO @puhui999address area
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 StatisticsPortraitApi.getCustomerArea(props.queryParams)
areaStatisticsList.value = areaList.map((item: CrmStatisticCustomerAreaRespVO) => {
return {
...item,
areaName: item.areaName // TODO @puhui999
.replace('维吾尔自治区', '')
.replace('壮族自治区', '')
.replace('回族自治区', '')
.replace('自治区', '')
.replace('省', '')
}
})
builderLeftMap()
builderRightMap()
loading.value = false
}
defineExpose({ loadData })
// TODO @puhui999builder build
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,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>

View File

@ -0,0 +1,198 @@
<!-- 客户来源分析 -->
<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="level" width="200">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
</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="levelPortion" />
<el-table-column align="center" label="成交占比(%)" min-width="200" prop="dealPortion" />
</el-table>
</el-card>
</template>
<script lang="ts" setup>
import {
CrmStatisticCustomerLevelRespVO,
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: '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 StatisticsPortraitApi.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: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
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: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
value: r.dealCount
}
})
}
// 3.
calculateProportion(levelList)
list.value = levelList
loading.value = false
}
defineExpose({ loadData })
/** 计算比例 */
const calculateProportion = (levelList: CrmStatisticCustomerLevelRespVO[]) => {
if (isEmpty(levelList)) {
return
}
//
const list = levelList as unknown as 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)
})
}
/** 初始化 */
onMounted(() => {
loadData()
})
</script>

View File

@ -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>

View File

@ -0,0 +1,157 @@
<!-- 数据统计 - 客户画像 -->
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="时间范围" prop="orderDate">
<el-date-picker
v-model="queryParams.times"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
:shortcuts="defaultShortcuts"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="queryParams.deptId"
:data="deptList"
:props="defaultProps"
check-strictly
class="!w-240px"
node-key="id"
placeholder="请选择归属部门"
@change="queryParams.userId = undefined"
/>
</el-form-item>
<el-form-item label="员工" prop="userId">
<el-select v-model="queryParams.userId" class="!w-240px" clearable placeholder="员工">
<el-option
v-for="(user, index) in userListByDeptId"
:key="index"
:label="user.nickname"
:value="user.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 客户统计 -->
<el-col>
<el-tabs v-model="activeTab">
<!-- 城市分布分析 -->
<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-tabs>
</el-col>
</template>
<script lang="ts" setup>
import * as DeptApi from '@/api/system/dept'
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 @puhui999CrmStatisticsPortrait
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: 'CrmStatisticsPortrait' })
const queryParams = reactive({
deptId: useUserStore().getUser.deptId,
userId: undefined,
times: [
//
formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
]
})
const queryFormRef = ref() //
const deptList = ref<Tree[]>([]) //
const userList = ref<UserApi.UserVO[]>([]) //
/** 根据选择的部门筛选员工清单 */
const userListByDeptId = computed(() =>
queryParams.deptId
? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
: []
)
const activeTab = ref('addressRef') //
const addressRef = ref() //
const levelRef = ref() //
const sourceRef = ref() //
const industryRef = ref() //
/** 搜索按钮操作 */
const handleQuery = () => {
switch (activeTab.value) {
case 'addressRef':
addressRef.value?.loadData?.()
break
case 'levelRef':
levelRef.value?.loadData?.()
break
case 'sourceRef':
sourceRef.value?.loadData?.()
break
case 'industryRef':
industryRef.value?.loadData?.()
break
}
}
/** 当 activeTab 改变时,刷新当前活动的 tab */
watch(activeTab, () => {
handleQuery()
})
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 初始化 */
onMounted(async () => {
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
userList.value = handleTree(await UserApi.getSimpleUserList())
})
</script>

View File

@ -22,7 +22,7 @@ import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/ra
import { EChartsOption } from 'echarts'
import { clone } from 'lodash-es'
defineOptions({ name: 'ContactsCountRank' })
defineOptions({ name: 'ContactCountRank' })
const props = defineProps<{ queryParams: any }>() //
const loading = ref(false) //

View File

@ -13,7 +13,13 @@
<el-table-column label="公司排名" align="center" type="index" width="80" />
<el-table-column label="签订人" align="center" prop="nickname" min-width="200" />
<el-table-column label="部门" align="center" prop="deptName" min-width="200" />
<el-table-column label="合同金额(元)" align="center" prop="count" min-width="200" />
<el-table-column
label="合同金额(元)"
align="center"
prop="count"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
</el-table>
</el-card>
</template>
@ -21,6 +27,7 @@
import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
import { EChartsOption } from 'echarts'
import { clone } from 'lodash-es'
import { erpPriceTableColumnFormatter } from '@/utils'
defineOptions({ name: 'ContractPriceRank' })
const props = defineProps<{ queryParams: any }>() //

View File

@ -13,7 +13,13 @@
<el-table-column label="公司排名" align="center" type="index" width="80" />
<el-table-column label="签订人" align="center" prop="nickname" min-width="200" />
<el-table-column label="部门" align="center" prop="deptName" min-width="200" />
<el-table-column label="回款金额(元)" align="center" prop="count" min-width="200" />
<el-table-column
label="回款金额(元)"
align="center"
prop="count"
min-width="200"
:formatter="erpPriceTableColumnFormatter"
/>
</el-table>
</el-card>
</template>
@ -21,6 +27,7 @@
import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
import { EChartsOption } from 'echarts'
import { clone } from 'lodash-es'
import { erpPriceTableColumnFormatter } from '@/utils'
defineOptions({ name: 'ReceivablePriceRank' })
const props = defineProps<{ queryParams: any }>() //

View File

@ -29,6 +29,7 @@
check-strictly
node-key="id"
placeholder="请选择归属部门"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
@ -62,8 +63,8 @@
<CustomerCountRank :query-params="queryParams" ref="customerCountRankRef" />
</el-tab-pane>
<!-- 新增联系人数排行 -->
<el-tab-pane label="新增联系人数排行" name="contactsCountRank" lazy>
<ContactsCountRank :query-params="queryParams" ref="contactsCountRankRef" />
<el-tab-pane label="新增联系人数排行" name="contactCountRank" lazy>
<ContactCountRank :query-params="queryParams" ref="contactCountRankRef" />
</el-tab-pane>
<!-- 跟进次数排行 -->
<el-tab-pane label="跟进次数排行" name="followCountRank" lazy>
@ -77,14 +78,14 @@
</el-col>
</template>
<script lang="ts" setup>
import ContractPriceRank from './ContractPriceRank.vue'
import ReceivablePriceRank from './ReceivablePriceRank.vue'
import ContractCountRank from './ContractCountRank.vue'
import ProductSalesRank from './ProductSalesRank.vue'
import CustomerCountRank from './CustomerCountRank.vue'
import ContactsCountRank from './ContactsCountRank.vue'
import FollowCountRank from './FollowCountRank.vue'
import FollowCustomerCountRank from './FollowCustomerCountRank.vue'
import ContractPriceRank from './components/ContractPriceRank.vue'
import ReceivablePriceRank from './components/ReceivablePriceRank.vue'
import ContractCountRank from './components/ContractCountRank.vue'
import ProductSalesRank from './components/ProductSalesRank.vue'
import CustomerCountRank from './components/CustomerCountRank.vue'
import ContactCountRank from './components/ContactCountRank.vue'
import FollowCountRank from './components/FollowCountRank.vue'
import FollowCustomerCountRank from './components/FollowCustomerCountRank.vue'
import { defaultProps, handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept'
import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
@ -109,35 +110,35 @@ const receivablePriceRankRef = ref() // ReceivablePriceRank 组件的引用
const contractCountRankRef = ref() // ContractCountRank
const productSalesRankRef = ref() // ProductSalesRank
const customerCountRankRef = ref() // CustomerCountRank
const contactsCountRankRef = ref() // ContactsCountRank
const contactCountRankRef = ref() // ContactCountRank
const followCountRankRef = ref() // FollowCountRank
const followCustomerCountRankRef = ref() // FollowCustomerCountRank
/** 搜索按钮操作 */
const handleQuery = () => {
switch (activeTab.value) {
case 'contractPriceRank':
case 'contractPriceRank': //
contractPriceRankRef.value?.loadData?.()
break
case 'receivablePriceRank':
case 'receivablePriceRank': //
receivablePriceRankRef.value?.loadData?.()
break
case 'contractCountRank':
case 'contractCountRank': //
contractCountRankRef.value?.loadData?.()
break
case 'productSalesRank':
case 'productSalesRank': //
productSalesRankRef.value?.loadData?.()
break
case 'customerCountRank':
case 'customerCountRank': //
customerCountRankRef.value?.loadData?.()
break
case 'contactsCountRank':
contactsCountRankRef.value?.loadData?.()
case 'contactCountRank': //
contactCountRankRef.value?.loadData?.()
break
case 'followCountRank':
case 'followCountRank': //
followCountRankRef.value?.loadData?.()
break
case 'followCustomerCountRank':
case 'followCustomerCountRank': //
followCustomerCountRankRef.value?.loadData?.()
break
}

View File

@ -23,15 +23,14 @@
</el-button>
<el-scrollbar height="580">
<div>
<pre><code class="hljs" v-dompurify-html="highlightedCode(formData)"></code></pre>
<pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre>
</div>
</el-scrollbar>
</div>
</Dialog>
</template>
<script lang="ts" setup>
defineOptions({ name: 'InfraBuild' })
import FcDesigner from '@form-create/designer'
import { useFormCreateDesigner } from '@/components/FormCreate'
import { useClipboard } from '@vueuse/core'
import { isString } from '@/utils/is'
@ -41,6 +40,8 @@ import xml from 'highlight.js/lib/languages/java'
import json from 'highlight.js/lib/languages/json'
import formCreate from '@form-create/element-ui'
defineOptions({ name: 'InfraBuild' })
const { t } = useI18n() //
const message = useMessage() //
@ -49,6 +50,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') //
const formType = ref(-1) // 0 - JSON1 - Options2 -
const formData = ref('') //
useFormCreateDesigner(designer) //
/** 打开弹窗 */
const openModel = (title: string) => {
@ -82,14 +84,13 @@ const makeTemplate = () => {
const opt = designer.value.getOption()
return `<template>
<form-create
v-model="fapi"
v-model:api="fApi"
:rule="rule"
:option="option"
@submit="onSubmit"
></form-create>
</template>
<script setup lang=ts>
import formCreate from "@form-create/element-ui";
const faps = ref(null)
const rule = ref('')
const option = ref('')

View File

@ -18,10 +18,10 @@
<template #append>%</template>
</el-input>
</el-form-item>
<el-form-item label-width="180px" label="公众号 APPID" prop="config.appId">
<el-form-item label-width="180px" label="微信 APPID" prop="config.appId">
<el-input
v-model="formData.config.appId"
placeholder="请输入公众号 APPID"
placeholder="请输入微信 APPID"
clearable
:style="{ width: '100%' }"
/>

View File

@ -102,10 +102,12 @@ const open = async (row: RoleApi.RoleVO) => {
formData.name = row.name
formData.code = row.code
formData.dataScope = row.dataScope
await nextTick()
row.dataScopeDeptIds?.forEach((deptId: number): void => {
await nextTick()
// DOM
row.dataScopeDeptIds?.forEach((deptId: number) => {
row.dataScopeDeptIds?.forEach((deptId: number): void => {
treeRef.value.setChecked(deptId, true, false)
})
}

View File

@ -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>