【招生小程序】 优化# 团队:线索转化情况分析布局

master
kaeery 2025-03-05 23:10:53 +08:00
parent c8ace90b43
commit 4226411775
20 changed files with 232 additions and 115 deletions

View File

@ -16,6 +16,10 @@ export function apiOrganizationByIdList() {
export function apiOrganizationAllList() { export function apiOrganizationAllList() {
return request.get({ url: '/organization/getAllChildOrgInfo' }) return request.get({ url: '/organization/getAllChildOrgInfo' })
} }
// 岗位列表
export function postLists(params?: any) {
return request.get({ url: '/system/post/list', data: params })
}
// 数据简报 // 数据简报
export function apiDataOverview(params: any) { export function apiDataOverview(params: any) {
@ -29,3 +33,11 @@ export function convertProcessApi(params?: any) {
export function clueStatusApi(params?: any) { export function clueStatusApi(params?: any) {
return request.get({ url: '/control/clueStatistics', data: params }) return request.get({ url: '/control/clueStatistics', data: params })
} }
// 成交客户转化统计
export function convertedSuccessApi(params?: any) {
return request.get({ url: '/control/clueTransactionCustomerStatistics', data: params })
}
// Top5
export function rankListApi(params?: any) {
return request.get({ url: '/control/top', data: params })
}

View File

@ -25,7 +25,7 @@
<template v-if="!loading && data.length > 0"> <template v-if="!loading && data.length > 0">
<TTable :columns="columns" :data="data"> <TTable :columns="columns" :data="data">
<template #index="scope"> <template #index="scope">
<text class="px-[16rpx] py-[6rpx] rounded-[4px]" :style="rankStyle(scope.row)"> <text class="px-[20rpx] py-[6rpx] rounded-[4px]" :style="rankStyle(scope.row)">
{{ scope.row }} {{ scope.row }}
</text> </text>
</template> </template>

View File

@ -157,7 +157,7 @@ export default defineComponent({
flex-grow: 1; flex-grow: 1;
height: 66rpx; height: 66rpx;
padding: 0 24rpx; padding: 0 24rpx;
font-size: 26rpx; // font-size: 26rpx;
line-height: 66rpx; line-height: 66rpx;
color: var(--dropdown-text-color); color: var(--dropdown-text-color);
text-align: center; text-align: center;
@ -190,7 +190,7 @@ export default defineComponent({
margin-right: 20rpx; margin-right: 20rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
overflow: hidden; overflow: hidden;
font-size: 28rpx; // font-size: 28rpx;
color: var(--dropdown-text-color); color: var(--dropdown-text-color);
background-color: #f5f5f5; background-color: #f5f5f5;
border-radius: 999rpx; border-radius: 999rpx;

View File

@ -119,7 +119,7 @@ export default defineComponent({
height: var(--cell-height); height: var(--cell-height);
padding: 0 24rpx; padding: 0 24rpx;
overflow: hidden; overflow: hidden;
font-size: 28rpx; // font-size: 28rpx;
color: var(--dropdown-text-color); color: var(--dropdown-text-color);
white-space: nowrap; white-space: nowrap;
border-bottom: 1rpx solid #dedede; border-bottom: 1rpx solid #dedede;

View File

@ -0,0 +1,70 @@
<template>
<view class="flex flex-wrap p-[32rpx] gap-[16rpx] text-[28rpx] bg-white">
<view
class="bg-[#F0F7FF] rounded-[8rpx] px-[16rpx] py-[8rpx]"
v-for="(value, key) in filteredForm"
:key="key"
>
<template v-if="key === 'createTimeRange'">
<text class="text-[#1E40AF]">{{ parseLabel(key) }}:</text>
<text class="text-primary">
{{ form.createTimeStart }} - {{ form.createTimeEnd }}
</text>
</template>
<template v-else>
<text class="text-[#1E40AF]">{{ parseLabel(key) }}:</text>
<text class="text-primary">{{ value }}</text>
</template>
</view>
</view>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { IForm } from './team/index.vue'
import { computed } from 'vue'
import { AdminTabEnum } from '@/enums'
const formMap: Record<AdminTabEnum, any> = {
[AdminTabEnum.TEAM]: {
organizationId: '组织',
createTimeRange: '日期'
},
[AdminTabEnum.PERSONALLY]: {
postId: '组织',
userId: '用户',
createTimeRange: '日期'
}
}
const props = defineProps({
form: {
type: Object as PropType<IForm>,
default: () => ({})
},
activeTab: {
type: Number as PropType<AdminTabEnum>,
default: AdminTabEnum.TEAM
}
})
const filteredForm = computed(() => {
const { createTimeStart, createTimeEnd, ...rest } = props.form
const activeTabMap = formMap[props.activeTab]
const filtered = Object.keys(rest).reduce((acc, key) => {
if (activeTabMap[key]) {
acc[key] = rest[key]
}
return acc
}, {} as Record<string, any>)
if (createTimeStart && createTimeEnd) {
filtered['createTimeRange'] = 'createTimeRange'
}
return filtered
})
const parseLabel = computed(() => (key: string) => {
const { activeTab } = props
return formMap[activeTab][key]
})
</script>
<style scoped></style>

View File

@ -201,7 +201,7 @@ export default defineComponent({
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 24rpx; padding: 24rpx;
font-size: 24rpx; // font-size: 24rpx;
color: var(--dropdown-text-color); color: var(--dropdown-text-color);
text-align: left; text-align: left;

View File

@ -9,7 +9,7 @@
> >
<text>{{ item.label }}</text> <text>{{ item.label }}</text>
<template v-if="postId == item.id"> <template v-if="postId == item.id">
<TIcon name="icon-check" color="#0E66FB" /> <TIcon name="icon-check" color="#0E66FB" :size="24" />
</template> </template>
</view> </view>
<dropdownFooter @reset="handleReset" @confirm="handleConfirm" /> <dropdownFooter @reset="handleReset" @confirm="handleConfirm" />
@ -35,6 +35,7 @@ const handleCellClick = (item: any) => {
const { id } = item const { id } = item
postId.value = id postId.value = id
emit('update:modelValue', id) emit('update:modelValue', id)
emit('confirm', item)
} }
const handleReset = () => { const handleReset = () => {
postId.value = 0 postId.value = 0

View File

@ -7,7 +7,7 @@
<position-tabs <position-tabs
v-model="postId" v-model="postId"
@reset="handleReset('position')" @reset="handleReset('position')"
@confirm="handleConfirm('position', value)" @confirm="value => handleConfirm('position', value)"
/> />
</view> </view>
</u-dropdown-item> </u-dropdown-item>
@ -32,7 +32,7 @@
</u-dropdown> </u-dropdown>
</view> </view>
<view class="flex-1 mt-3 px-[32rpx] overflow-auto bg-[#FAFAFE]"> <view class="flex-1 mt-3 px-[32rpx] overflow-auto bg-[#FAFAFE]">
<z-paging <!-- <z-paging
ref="paging" ref="paging"
v-model="dataList" v-model="dataList"
@query="queryList" @query="queryList"
@ -42,7 +42,7 @@
<template v-for="(item, index) in dataList" :key="`unique_${index}`"> <template v-for="(item, index) in dataList" :key="`unique_${index}`">
<telesale-card :item="item" @handleCardClick="handleCardClick" /> <telesale-card :item="item" @handleCardClick="handleCardClick" />
</template> </template>
</z-paging> </z-paging> -->
</view> </view>
</view> </view>
</template> </template>
@ -90,6 +90,7 @@ const handleCardClick = (type: string, item: any) => {
break break
} }
} }
//
const handleConfirm = (type: string, item) => { const handleConfirm = (type: string, item) => {
switch (type) { switch (type) {
case 'organization': case 'organization':
@ -100,11 +101,14 @@ const handleConfirm = (type: string, item) => {
break break
case 'position': case 'position':
console.log(item)
break break
default: default:
break break
} }
} }
//
const handleReset = (type: string) => { const handleReset = (type: string) => {
switch (type) { switch (type) {
case 'organization': case 'organization':

View File

@ -14,11 +14,17 @@
:key="`unique_${index}`" :key="`unique_${index}`"
> >
<text class="text-gray5">{{ item.label }}</text> <text class="text-gray5">{{ item.label }}</text>
<u-line-progress <view class="flex items-center gap-[12rpx]">
:percentage="item.value" <view class="flex-1">
activeColor="#5783FE" <u-line-progress
shape="square" :percentage="item.value"
></u-line-progress> activeColor="#5783FE"
shape="square"
:showText="false"
></u-line-progress>
</view>
<text>{{ item.value + '%' }}</text>
</view>
</view> </view>
</view> </view>
</template> </template>
@ -56,6 +62,8 @@ const fetchData = async (payload: IForm) => {
value: result[item] || 0 value: result[item] || 0
} }
}) })
console.log(data.value)
loading.value = false loading.value = false
} catch (error) {} } catch (error) {}
} }

View File

@ -26,6 +26,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import card from '../../card.vue' import card from '../../card.vue'
import { IForm } from '../index.vue' import { IForm } from '../index.vue'
import { convertedSuccessApi } from '@/api/admin'
interface IConvertedData { interface IConvertedData {
name: string name: string
@ -33,29 +34,24 @@ interface IConvertedData {
} }
const data = ref<IConvertedData[]>([]) const data = ref<IConvertedData[]>([])
const loading = ref(false) const loading = ref(false)
const fetchData = (payload: IForm) => { const fetchData = async (payload: IForm) => {
loading.value = true loading.value = true
setTimeout(() => { try {
data.value = [ const result = await convertedSuccessApi(payload)
{ console.log(result)
name: '湛江团队', data.value = result.map(item => {
const { clientCount, clueCount, percentConversion } = item.leadToCustomerStatisticsVo
return {
name: item.organizationName,
children: [ children: [
{ label: '线索', value: '52个' }, { label: '线索', value: clueCount ?? 0 + '个' },
{ label: '成交客户', value: '52个' }, { label: '成交客户', value: clientCount ?? 0 + '个' },
{ label: '转化率', value: '15%' } { label: '转化率', value: percentConversion }
]
},
{
name: '广州团队',
children: [
{ label: '线索', value: '52个' },
{ label: '成交客户', value: '52个' },
{ label: '转化率', value: '15%' }
] ]
} }
] })
loading.value = false } catch (error) {}
}, 300) loading.value = false
} }
defineExpose({ defineExpose({
fetchData fetchData

View File

@ -1,6 +1,6 @@
<template> <template>
<view class="px-[32rpx]"> <view class="px-[32rpx] mt-[12rpx]">
<card> <card className="pt-2 pb-3">
<u-tabs <u-tabs
:list="tabs" :list="tabs"
:scrollable="false" :scrollable="false"
@ -23,11 +23,11 @@
<u-loading-icon></u-loading-icon> <u-loading-icon></u-loading-icon>
</view> </view>
</template> </template>
<template v-if="!loading && data.length > 0"> <template v-else-if="!loading && data.length > 0">
<TTable :columns="columns" :data="data"> <TTable :columns="columns" :data="data">
<template #index="scope"> <template #index="scope">
<text <text
class="px-[16rpx] py-[6rpx] rounded-[4px]" class="px-[20rpx] py-[6rpx] rounded-[4px]"
:style="rankStyle(scope.row)" :style="rankStyle(scope.row)"
> >
{{ scope.row }} {{ scope.row }}
@ -41,25 +41,26 @@
<text>查看更多</text> <text>查看更多</text>
</view> </view>
</template> </template>
<template v-else>
<w-empty />
</template>
</card> </card>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { watch } from 'vue'
import card from '../../card.vue' import card from '../../card.vue'
import { useRank } from '@/hooks/useCommon' import { useRank } from '@/hooks/useCommon'
const emit = defineEmits(['refresh'])
const { tabs, activeTab, columns, data, loading, rankStyle, handleChangeTab, fetchData } = useRank({ const { tabs, activeTab, columns, data, loading, rankStyle, handleChangeTab, fetchData } = useRank({
width: 120 width: 120,
callback: () => {
emit('refresh')
}
}) })
watch(
() => activeTab.value,
(val: number) => {
console.log(val)
}
)
const handleMore = () => { const handleMore = () => {
uni.navigateTo({ uni.navigateTo({
url: '/bundle/pages/rank-list/index' url: '/bundle/pages/rank-list/index'

View File

@ -31,9 +31,10 @@
</u-dropdown-item> </u-dropdown-item>
</u-dropdown> </u-dropdown>
</view> </view>
<filter-value :form="form" :activeTab="activeTab" />
<data-overview ref="dataOverviewRef" /> <data-overview ref="dataOverviewRef" />
<converted-overview ref="convertedOverviewRef" /> <converted-overview ref="convertedOverviewRef" />
<rank ref="rankRef" /> <rank ref="rankRef" @refresh="refreshData" />
<clue-status ref="clueStatusRef" /> <clue-status ref="clueStatusRef" />
</view> </view>
</template> </template>
@ -46,8 +47,11 @@ import rank from './components/rank.vue'
import clueStatus from './components/clue-status.vue' import clueStatus from './components/clue-status.vue'
import dateDropdownPicker from '@/components/date-dropdown/daterange.vue' import dateDropdownPicker from '@/components/date-dropdown/daterange.vue'
import selectDropdown from '@/components/select-dropdown/index.vue' import selectDropdown from '@/components/select-dropdown/index.vue'
import filterValue from '../filter-value.vue'
import { apiOrganizationByIdList, apiOrganizationList, apiOrganizationTeamList } from '@/api/admin' import { apiOrganizationByIdList, apiOrganizationList, apiOrganizationTeamList } from '@/api/admin'
import { formateDate, getCurDate } from '@/utils/util' import { formateDate, getCurDate } from '@/utils/util'
import { PropType } from 'vue'
import { AdminTabEnum } from '@/enums'
export interface IForm { export interface IForm {
organizationId: number | null organizationId: number | null
@ -55,6 +59,13 @@ export interface IForm {
createTimeEnd: string createTimeEnd: string
userId: string userId: string
} }
defineProps({
activeTab: {
type: Number as PropType<AdminTabEnum>,
default: AdminTabEnum.TEAM
}
})
const clueStatusRef = ref<InstanceType<typeof clueStatus>>() const clueStatusRef = ref<InstanceType<typeof clueStatus>>()
const rankRef = ref<InstanceType<typeof rank>>() const rankRef = ref<InstanceType<typeof rank>>()
const convertedOverviewRef = ref<InstanceType<typeof convertedOverview>>() const convertedOverviewRef = ref<InstanceType<typeof convertedOverview>>()
@ -131,7 +142,9 @@ const handleReset = (type: string) => {
const closeDropDown = () => { const closeDropDown = () => {
uDropdownRef.value.close() uDropdownRef.value.close()
} }
const refreshData = () => {
rankRef.value?.fetchData(form.value)
}
onMounted(async () => { onMounted(async () => {
await fetchOrganizationList() await fetchOrganizationList()
fetchAllData() fetchAllData()

View File

@ -1,7 +1,7 @@
<template> <template>
<view class="empty-comp text-center text-[#969799]"> <view class="empty-comp text-center text-[#969799]">
<template v-if="type == 'data'"> <template v-if="type == 'data'">
<image :src="dataImage" mode="aspectFit" class="w-[240px]" /> <image :src="dataImage" mode="aspectFit" class="w-[240px] h-[200px]" />
<view>暂无数据</view> <view>暂无数据</view>
</template> </template>
<template v-else-if="type == 'location'"> <template v-else-if="type == 'location'">

View File

@ -21,3 +21,7 @@ export enum AdminTabEnum {
TEAM = 1, TEAM = 1,
PERSONALLY = 2 PERSONALLY = 2
} }
export enum PositionEnum {
TELESALE = 5, //电销
RECRUITSALE = 6 //招生
}

View File

@ -1,5 +1,7 @@
import { postLists, rankListApi } from '@/api/admin'
import { apiCluseDetail } from '@/api/clue' import { apiCluseDetail } from '@/api/clue'
import { IForm } from '@/components/widgets/admin/team/index.vue' import { IForm } from '@/components/widgets/admin/team/index.vue'
import { PositionEnum } from '@/enums'
import { computed, ref, shallowRef } from 'vue' import { computed, ref, shallowRef } from 'vue'
// 获取线索详情 // 获取线索详情
@ -32,12 +34,12 @@ interface IRankTab {
id: number id: number
} }
// 主账号排行榜 // 主账号排行榜
export function useRank({ width }: { width: number }) { export function useRank({ width, callback }: { width: number; callback?: () => void }) {
const tabs = ref<IRankTab[]>([]) const tabs = ref<IRankTab[]>([])
const activeTab = ref() const activeTab = ref()
const loading = ref(false) const loading = ref(false)
const data = ref<IRank[]>([]) const data = ref<IRank[]>([])
const totalLabel = computed(() => (activeTab.value == 5 ? '意向' : '成交')) const totalLabel = computed(() => (activeTab.value == PositionEnum.TELESALE ? '意向' : '成交'))
const rankStyle = computed(() => row => { const rankStyle = computed(() => row => {
const styleMap: Record<number, string> = { const styleMap: Record<number, string> = {
'1': '#FCAE3C', '1': '#FCAE3C',
@ -52,56 +54,46 @@ export function useRank({ width }: { width: number }) {
const handleChangeTab = item => { const handleChangeTab = item => {
activeTab.value = item.id activeTab.value = item.id
generateColumns() columns.value = generateColumns()
callback && callback()
} }
const generateColumns = () => { const generateColumns = () => {
return [ return [
{ name: 'index', label: '排名', slot: true }, { name: 'rank', label: '排名', slot: true },
{ name: 'name', label: '姓名' }, { name: 'username', label: '姓名' },
{ name: 'total', label: totalLabel.value + '客户数', width, align: 'right' } { name: 'clueCount', label: totalLabel.value + '客户数', width, align: 'right' }
] ]
} }
const columns = ref(generateColumns()) const columns = ref()
const fetchTabs = () => { // 获取岗位列表
return new Promise(resolve => { const fetchTabs = async () => {
tabs.value = [ try {
{ name: '电销业绩排行榜', value: 1, id: 5 }, const result = await postLists()
{ name: '招生业绩排行榜', value: 2, id: 6 } tabs.value = result.lists.map(item => {
] const { name } = item
if (tabs.value.length > 0) activeTab.value = tabs.value[0] return {
resolve(tabs.value) id: item.id,
}) name: name + '业绩排行榜'
}
const fetchData = async (payload: IForm) => {
await fetchTabs()
console.log(activeTab.value)
loading.value = true
setTimeout(() => {
data.value = [
{
index: 1,
name: '王小虎1789789789789789',
total: 100
},
{
index: 2,
name: '王小虎2',
total: 20
},
{
index: 3,
name: '王小虎2',
total: 20
},
{
index: 4,
name: '王小虎2',
total: 20
} }
] })
loading.value = false if (tabs.value.length > 0) activeTab.value = tabs.value[0].id
}, 500) columns.value = generateColumns()
} catch (error) {}
}
// 获取每一个岗位前5名
const fetchData = async (payload: IForm) => {
if (!tabs.value.length) await fetchTabs()
try {
loading.value = true
const newPayload = {
...payload,
post: activeTab.value
}
const result = await rankListApi(newPayload)
data.value = result ?? []
} catch (error) {}
loading.value = false
} }
return { return {
@ -120,21 +112,17 @@ export function useRank({ width }: { width: number }) {
export function usePositions() { export function usePositions() {
const positionList = ref() const positionList = ref()
const postId = ref() const postId = ref()
const fetchPositions = () => { const fetchPositions = async () => {
try { try {
positionList.value = [ const result = await postLists()
{ positionList.value = result.lists.map(item => {
label: '电销老师', return {
id: 5, label: item.name,
value: 5 id: item.id,
}, value: item.id
{
label: '招生老师',
id: 6,
value: 6
} }
] })
if (positionList.value.length > 0) postId.value = positionList.value if (positionList.value.length > 0) postId.value = positionList.value[0].id
} catch (error) {} } catch (error) {}
} }

View File

@ -60,7 +60,8 @@
{ {
"path": "pages/admin/my/index", "path": "pages/admin/my/index",
"style": { "style": {
"navigationBarTitleText": "" "navigationBarTitleText": "",
"navigationStyle": "custom"
}, },
"auth": true "auth": true
}, },

View File

@ -10,8 +10,8 @@
@change="handleChangeTab" @change="handleChangeTab"
></u-tabs> ></u-tabs>
<view class="flex-1 bg-gray3"> <view class="flex-1 bg-gray3">
<admin-team v-if="activeTab === AdminTabEnum.TEAM" /> <admin-team v-if="activeTab === AdminTabEnum.TEAM" :activeTab="activeTab" />
<personally v-if="activeTab === AdminTabEnum.PERSONALLY" /> <personally v-if="activeTab === AdminTabEnum.PERSONALLY" :activeTab="activeTab" />
</view> </view>
</TContainer> </TContainer>
</template> </template>

View File

@ -15,6 +15,7 @@ export default {
percentage: 0, percentage: 0,
showText: true, showText: true,
height: 12, height: 12,
shape: 'circle' shape: 'circle',
thresholdValue: 10
} }
} }

View File

@ -30,6 +30,10 @@ export const props = defineMixin({
shape: { shape: {
type: String, type: String,
default: () => defProps.lineProgress.shape default: () => defProps.lineProgress.shape
},
thresholdValue: {
type: Number,
default: () => defProps.lineProgress.thresholdValue
} }
} }
}) })

View File

@ -13,7 +13,11 @@
></view> ></view>
<view class="u-line-progress__line" :style="[progressStyle]"> <view class="u-line-progress__line" :style="[progressStyle]">
<slot> <slot>
<text v-if="showText && percentage >= 10" class="u-line-progress__text"> <text
v-if="showText && percentage >= thresholdValue"
class="u-line-progress__text"
:style="{ right: textGap }"
>
{{ innserPercentage + '%' }} {{ innserPercentage + '%' }}
</text> </text>
</slot> </slot>
@ -66,6 +70,14 @@ export default {
innserPercentage() { innserPercentage() {
// 0-100 // 0-100
return range(0, 100, this.percentage) return range(0, 100, this.percentage)
},
textGap() {
let value =
typeof this.lineWidth == 'string'
? Math.ceil(parseFloat(this.lineWidth))
: Math.ceil(this.lineWidth)
const newVal = value + 17
return '-' + newVal + 'px'
} }
}, },
mounted() { mounted() {
@ -128,18 +140,20 @@ export default {
bottom: 0; bottom: 0;
align-items: center; align-items: center;
@include flex(row); @include flex(row);
color: #ffffff; // color: #ffffff;
// border-radius: 100px; // border-radius: 100px;
transition: width 0.5s ease; transition: width 0.5s ease;
justify-content: flex-end; justify-content: flex-end;
} }
&__text { &__text {
position: absolute;
bottom: 0;
font-size: 10px; font-size: 10px;
align-items: center; align-items: center;
text-align: right; // text-align: right;
color: #ffffff; // color: #ffffff;
margin-right: 5px; // margin-right: 5px;
transform: scale(0.9); transform: scale(0.9);
} }
} }