【招生用户端】 新增# 工作台:对接数据简报、线索转化情况统计接口
parent
a8cc9b437b
commit
c57cc282fb
|
@ -0,0 +1,26 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 数据简报
|
||||
export function dataOverviewApi(params?: any) {
|
||||
return request.get({ url: '/control/dataPresentation', params })
|
||||
}
|
||||
// 线索转客户统计
|
||||
export function convertProcessApi(params?: any) {
|
||||
return request.get({ url: '/control/leadToCustomerStatistics', params })
|
||||
}
|
||||
// 线索转化情况统计
|
||||
export function clueStatusApi(params?: any) {
|
||||
return request.get({ url: '/control/clueStatistics', params })
|
||||
}
|
||||
// 获取所有组织以及人员信息
|
||||
export function allUserListApi(params?: any) {
|
||||
return request.get({ url: '/organization/getAllChildOrgInfo', params })
|
||||
}
|
||||
// 获取所有团队
|
||||
export function allTeamListApi(params?: any) {
|
||||
return request.get({ url: '/organization/getAllGroup', params })
|
||||
}
|
||||
// 获取所有组织
|
||||
export function allOrgListApi(params?: any) {
|
||||
return request.get({ url: '/organization/getAllOrg', params })
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { isObject } from '@vue/shared'
|
||||
import { ElMessage, type messageType } from 'element-plus'
|
||||
import { ElMessage, dayjs, type messageType } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { isArray } from './is'
|
||||
import type { FieldNamesProps } from '@/components/ProTable/interface'
|
||||
|
@ -326,3 +326,74 @@ export function findItemNested(enumData: any, callValue: any, value: string, chi
|
|||
if (current[children]) return findItemNested(current[children], callValue, value, children)
|
||||
}, null)
|
||||
}
|
||||
export const shortcuts = [
|
||||
{
|
||||
text: '今天',
|
||||
value: () => {
|
||||
const start = new Date()
|
||||
const end = new Date()
|
||||
start.setHours(0, 0, 0, 0)
|
||||
end.setHours(23, 59, 59, 999)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '一周后',
|
||||
value: () => {
|
||||
const start = new Date()
|
||||
const end = new Date()
|
||||
end.setDate(end.getDate() + 7)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
end.setHours(23, 59, 59, 999)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '一个月后',
|
||||
value: () => {
|
||||
const start = new Date()
|
||||
const end = new Date()
|
||||
end.setMonth(end.getMonth() + 1)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
end.setHours(23, 59, 59, 999)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '三个月后',
|
||||
value: () => {
|
||||
const start = new Date()
|
||||
const end = new Date()
|
||||
end.setMonth(end.getMonth() + 3)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
end.setHours(23, 59, 59, 999)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '半年后',
|
||||
value: () => {
|
||||
const start = new Date()
|
||||
const end = new Date()
|
||||
end.setMonth(end.getMonth() + 6)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
end.setHours(23, 59, 59, 999)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '一年后',
|
||||
value: () => {
|
||||
const start = new Date()
|
||||
const end = new Date()
|
||||
end.setFullYear(end.getFullYear() + 1)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
end.setHours(23, 59, 59, 999)
|
||||
return [start, end]
|
||||
}
|
||||
}
|
||||
]
|
||||
// 今天
|
||||
export const getCurDate = (type: string, format = 'YYYY-MM-DD HH:mm:ss') => {
|
||||
return type == 'start' ? dayjs().startOf('day').format(format) : dayjs().endOf('day').format(format)
|
||||
}
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
<template>
|
||||
<div class="flex-1 w-full">
|
||||
<card title="线索转化情况统计">
|
||||
<v-charts style="height: 350px" :option="option" :autoresize="true" />
|
||||
<v-charts ref="chartRef" v-loading="loading" style="height: 350px" :option="option" :autoresize="true" />
|
||||
</card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { clueStatusApi } from '@/api/workbench'
|
||||
import card from './card.vue'
|
||||
import vCharts from 'vue-echarts'
|
||||
import type { IForm } from '../index.vue'
|
||||
|
||||
const data = [
|
||||
{ value: 1048, name: '有意向' },
|
||||
{ value: 735, name: '待领取' },
|
||||
{ value: 580, name: '转化中' },
|
||||
{ value: 484, name: '已添加' },
|
||||
{ value: 300, name: '异常待处理' },
|
||||
{ value: 300, name: '已成交' },
|
||||
{ value: 300, name: '已战败' }
|
||||
]
|
||||
const chartRef = ref()
|
||||
const data = ref<any[]>([])
|
||||
const option = ref({
|
||||
color: ['#73DDFF', '#73ACFF', '#FDD56A', '#FDB36A', '#FD866A', '#9E87FF', '#58D5FF'],
|
||||
tooltip: {
|
||||
|
@ -47,5 +42,45 @@ const option = ref({
|
|||
}
|
||||
]
|
||||
})
|
||||
const loading = ref(false)
|
||||
const dataMap: Record<string, string> = {
|
||||
unclaimedCount: '待领取',
|
||||
conversionCount: '转化中',
|
||||
addedCount: '已添加',
|
||||
abnormalCount: '异常待处理',
|
||||
tradedCount: '已成交',
|
||||
failCount: '已战败'
|
||||
}
|
||||
const fetchData = async (payload: IForm) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const result = await clueStatusApi(payload)
|
||||
data.value = Object.keys(dataMap).map(item => {
|
||||
return {
|
||||
name: dataMap[item],
|
||||
value: result[item] || 0
|
||||
}
|
||||
})
|
||||
// 检查数组中的每一项是否都为 0
|
||||
const allZero = data.value.every(item => item.value === 0)
|
||||
if (allZero) {
|
||||
chartRef.value.setOption({
|
||||
title: {
|
||||
text: '暂无数据',
|
||||
x: 'center',
|
||||
y: 'center'
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
series: []
|
||||
})
|
||||
}
|
||||
} catch (error) {}
|
||||
loading.value = false
|
||||
}
|
||||
defineExpose({
|
||||
fetchData
|
||||
})
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
<template>
|
||||
<div class="flex-1 w-full chart-card">
|
||||
<card title="线索转客户统计">
|
||||
<v-charts style="height: 350px" :autoresize="true" :option="option" />
|
||||
<v-charts ref="chartRef" v-loading="loading" style="height: 350px" :autoresize="true" :option="option" />
|
||||
</card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { convertProcessApi } from '@/api/workbench'
|
||||
import card from './card.vue'
|
||||
import vCharts from 'vue-echarts'
|
||||
import type { IForm } from '../index.vue'
|
||||
|
||||
const data = [
|
||||
{ name: '湛江团队', clueNumber: 52, client: 52, rate: 100 },
|
||||
{ name: '广州团队', clueNumber: 8, client: 6, rate: 15 }
|
||||
]
|
||||
function createBarSeries(data, name, field) {
|
||||
return {
|
||||
name,
|
||||
|
@ -27,18 +25,21 @@ function createLineSeries(data, name) {
|
|||
type: 'line',
|
||||
tooltip: {
|
||||
valueFormatter: function (value) {
|
||||
return (value as number) + '%'
|
||||
return value as number
|
||||
}
|
||||
},
|
||||
data: data.map(item => item.client),
|
||||
yAxisIndex: 1
|
||||
}
|
||||
}
|
||||
const xAxisData = () => data.map(item => item.name)
|
||||
const loading = ref(false)
|
||||
const data = ref([])
|
||||
const xAxisData = ref([])
|
||||
|
||||
const series = [
|
||||
createBarSeries(data, '线索数', 'clueNumber'),
|
||||
createBarSeries(data, '线索转客户数', 'client'),
|
||||
createLineSeries(data, '线索转客户率')
|
||||
createBarSeries(data.value, '线索数', 'clueNumber'),
|
||||
createBarSeries(data.value, '线索转客户数', 'client'),
|
||||
createLineSeries(data.value, '线索转客户率')
|
||||
]
|
||||
|
||||
const option = ref({
|
||||
|
@ -57,7 +58,7 @@ const option = ref({
|
|||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: xAxisData()
|
||||
data: xAxisData.value
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
|
@ -70,12 +71,60 @@ const option = ref({
|
|||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value} %'
|
||||
formatter: '{value}'
|
||||
}
|
||||
}
|
||||
],
|
||||
series
|
||||
})
|
||||
|
||||
const chartRef = ref()
|
||||
const fetchData = async (payload: IForm) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await convertProcessApi(payload)
|
||||
if (result.length > 0) {
|
||||
data.value = result.map((item: any) => {
|
||||
const { clueCount, transactionClient, percentConversion } = item.leadToCustomerStatisticsVo
|
||||
return {
|
||||
name: item.organizationName,
|
||||
clueNumber: clueCount,
|
||||
client: transactionClient,
|
||||
rate: percentConversion
|
||||
}
|
||||
})
|
||||
xAxisData.value = result.map((item: any) => item.organizationName)
|
||||
if (series[2].data.length === 0) {
|
||||
chartRef.value.setOption({
|
||||
title: {
|
||||
text: '暂无数据',
|
||||
x: 'center',
|
||||
y: 'center'
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: [],
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
],
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
loading.value = false
|
||||
}
|
||||
defineExpose({
|
||||
fetchData
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@media (max-width: 1000px) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<card title="数据简报">
|
||||
<div class="data-overview">
|
||||
<div class="data-overview" v-loading="loading">
|
||||
<div
|
||||
class="flex-1 bg-[#F7F8FA] flex flex-col gap-[12px] p-[20px] rounded-[8px]"
|
||||
v-for="(item, index) in dataOverview"
|
||||
|
@ -15,19 +15,41 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import card from './card.vue'
|
||||
const dataOverview = ref([
|
||||
{ label: '新增跟进记录(个)', value: 60 },
|
||||
{ label: '新增客户(个)', value: 60 },
|
||||
{ label: '成交客户(个)', value: 60 },
|
||||
{ label: '转化中客户(个)', value: 60 },
|
||||
{ label: '异常待处理(个)', value: 60 },
|
||||
{ label: '战败客户(个)', value: 60 }
|
||||
])
|
||||
import type { IForm } from '../index.vue'
|
||||
import { dataOverviewApi } from '@/api/workbench'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataOverview = ref<any[]>([])
|
||||
const dataMap: Record<string, string> = {
|
||||
followUpRecord: '新增跟进记录(个)',
|
||||
newCustomer: '新增客户(个)',
|
||||
transactionClient: '成交客户(个)',
|
||||
convertingClient: '转化中客户(个)',
|
||||
exceptionPending: '异常待处理(个)',
|
||||
defeatedClient: '战败客户(个)'
|
||||
}
|
||||
const fetchData = async (payload: IForm) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await dataOverviewApi(payload)
|
||||
dataOverview.value = Object.keys(dataMap).map(key => {
|
||||
return {
|
||||
label: dataMap[key],
|
||||
value: result[key] || 0
|
||||
}
|
||||
})
|
||||
} catch (error) {}
|
||||
loading.value = false
|
||||
}
|
||||
defineExpose({
|
||||
fetchData
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.data-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
min-height: 100px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<div class="flex gap-[30px]">
|
||||
<div class="flex gap-[30px]">
|
||||
<el-tree-select
|
||||
v-model="queryParams.organizationId"
|
||||
:props="defaultProps"
|
||||
default-expand-all
|
||||
:data="organizationList"
|
||||
:render-after-expand="false"
|
||||
/>
|
||||
<div>
|
||||
<daterange-picker
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
isDisabledDate
|
||||
isDisabledHours
|
||||
:shortcuts="shortcuts"
|
||||
v-model:startTime="queryParams.createTimeStart"
|
||||
v-model:endTime="queryParams.createTimeEnd"
|
||||
/>
|
||||
</div>
|
||||
<el-cascader v-model="selectedUserId" :props="CascaderProps" :options="userList" @change="handleCascaderChange" />
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { organzationLists } from '@/api/account_center/organization'
|
||||
import { shortcuts } from '@/utils/util'
|
||||
import type { PropType } from 'vue'
|
||||
import type { IForm } from '../index.vue'
|
||||
import { allUserListApi } from '@/api/workbench'
|
||||
|
||||
const defaultProps = {
|
||||
label: 'name',
|
||||
value: 'id'
|
||||
}
|
||||
const CascaderProps = {
|
||||
label: 'name',
|
||||
value: 'id'
|
||||
}
|
||||
const props = defineProps({
|
||||
queryParams: {
|
||||
type: Object as PropType<IForm>,
|
||||
default: () => ({
|
||||
organizationId: '',
|
||||
createTimeStart: '',
|
||||
createTimeEnd: '',
|
||||
userId: []
|
||||
})
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:queryParams', 'resetParams', 'resetPage', 'fectAllData'])
|
||||
const localValue = computed({
|
||||
get() {
|
||||
return props.queryParams
|
||||
},
|
||||
set(newValue) {
|
||||
emit('update:queryParams', newValue)
|
||||
}
|
||||
})
|
||||
const selectedUserId = ref([])
|
||||
|
||||
const resetParams = () => {
|
||||
selectedUserId.value = []
|
||||
emit('resetParams')
|
||||
}
|
||||
const resetPage = () => {
|
||||
emit('resetPage')
|
||||
}
|
||||
const organizationList = ref([])
|
||||
const userList = ref<any[]>([])
|
||||
const fetchOrganizationList = async () => {
|
||||
try {
|
||||
const result = await organzationLists()
|
||||
organizationList.value = result ?? []
|
||||
if (result.length > 0) {
|
||||
localValue.value = {
|
||||
...localValue.value,
|
||||
organizationId: result[0].id
|
||||
}
|
||||
emit('fectAllData', result[0].id)
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
const fetchAllUserList = async () => {
|
||||
try {
|
||||
const result = await allUserListApi()
|
||||
const renamedData = renameFields(result)
|
||||
userList.value = renamedData
|
||||
} catch (error) {}
|
||||
}
|
||||
const renameFields = (data: any[]): any[] => {
|
||||
return data.map(item => {
|
||||
const newItem = { ...item }
|
||||
if (newItem.organizationVoList) {
|
||||
newItem.children = renameFields(newItem.organizationVoList)
|
||||
delete newItem.organizationVoList
|
||||
}
|
||||
if (newItem.userVos) {
|
||||
newItem.userVos.forEach(item => {
|
||||
item.name = item.username
|
||||
})
|
||||
newItem.children = newItem.children ? [...newItem.children, ...newItem.userVos] : newItem.userVos
|
||||
delete newItem.userVos
|
||||
}
|
||||
if (!newItem.children.length) {
|
||||
newItem.disabled = true
|
||||
}
|
||||
return newItem
|
||||
})
|
||||
}
|
||||
const handleCascaderChange = value => {
|
||||
const lastVal = value[value.length - 1]
|
||||
localValue.value = {
|
||||
...localValue.value,
|
||||
userId: lastVal.toString()
|
||||
}
|
||||
}
|
||||
fetchOrganizationList()
|
||||
fetchAllUserList()
|
||||
</script>
|
||||
<style scoped></style>
|
|
@ -1,13 +1,14 @@
|
|||
<template>
|
||||
<div class="workbench flex flex-col gap-[16px]">
|
||||
<data-overview />
|
||||
<search-form v-model:queryParams="form" @reset-page="resetPage" @reset-params="resetParams" @fect-all-data="fetchAllData" />
|
||||
<data-overview ref="dataOverviewRef" />
|
||||
<div class="flex gap-[16px] flex-wrap">
|
||||
<conversion-process-chart />
|
||||
<conversion-process-chart ref="coversionProcessChartRef" />
|
||||
<converted-chart />
|
||||
</div>
|
||||
<div class="flex gap-[16px] flex-wrap">
|
||||
<rank />
|
||||
<clue-status-pie />
|
||||
<clue-status-pie ref="clueStatusPieRef" />
|
||||
<!-- <converted-line-chart /> -->
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,6 +21,43 @@ import convertedChart from './components/converted-chart.vue'
|
|||
import rank from './components/rank.vue'
|
||||
import clueStatusPie from './components/clue-status-pie.vue'
|
||||
// import convertedLineChart from './components/converted-line-chart.vue'
|
||||
import searchForm from './components/search-form.vue'
|
||||
import { getCurDate } from '@/utils/util'
|
||||
|
||||
export interface IForm {
|
||||
organizationId: number | null
|
||||
createTimeStart: string
|
||||
createTimeEnd: string
|
||||
userId: string
|
||||
}
|
||||
const initialVal = ref()
|
||||
const form = ref<IForm>({
|
||||
organizationId: null,
|
||||
createTimeStart: getCurDate('start'),
|
||||
createTimeEnd: getCurDate('end'),
|
||||
userId: ''
|
||||
})
|
||||
const clueStatusPieRef = ref<InstanceType<typeof clueStatusPie>>()
|
||||
const coversionProcessChartRef = ref<InstanceType<typeof conversionProcessChart>>()
|
||||
const dataOverviewRef = ref<InstanceType<typeof dataOverview>>()
|
||||
const resetPage = () => {
|
||||
fetchAllData(initialVal.value)
|
||||
}
|
||||
const resetParams = () => {
|
||||
form.value = {
|
||||
organizationId: initialVal.value,
|
||||
createTimeStart: getCurDate('start').toString(),
|
||||
createTimeEnd: getCurDate('end').toString(),
|
||||
userId: ''
|
||||
}
|
||||
fetchAllData(initialVal.value)
|
||||
}
|
||||
const fetchAllData = (defaultValue?: number) => {
|
||||
initialVal.value = defaultValue ?? null
|
||||
dataOverviewRef.value?.fetchData(form.value)
|
||||
coversionProcessChartRef.value?.fetchData(form.value)
|
||||
clueStatusPieRef.value?.fetchData(form.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
Loading…
Reference in New Issue