【招生小程序】 新增# 主账号-个人:电销以及招生的线索数据和线索详情

master
kaeery 2025-03-07 17:55:31 +08:00
parent 56aa0125b8
commit d06d3f7c78
12 changed files with 284 additions and 128 deletions

View File

@ -18,7 +18,7 @@ export function apiOrganizationAllList() {
}
// 岗位列表
export function postLists(params?: any) {
return request.get({ url: '/system/post/list', data: params })
return request.get({ url: '/system/post/list', data: params }, { ignoreCancel: true })
}
// 数据简报
@ -45,3 +45,12 @@ export function rankListApi(params?: any) {
export function rankMoreListApi(params?: any) {
return request.get({ url: '/control/topAll', data: params })
}
// 具体某个成员的数据
export function personalListApi(params?: any) {
return request.get({ url: '/personageControl/personageControlList', data: params })
}
// 具体某个成员的详情
export function personalDetailApi(params?: any) {
return request.get({ url: '/personageControl/detail', data: params })
}

View File

@ -11,18 +11,25 @@
</u-copy>
</view>
</view>
<text v-if="item.situation == converStatusEnum.UN_RECEIVED" class="text-[#ED6D41]">
未领取
</text>
</view>
</template>
<template #content>
<view class="flex gap-[20rpx] mb-[16rpx]">
<text class="text-muted w-[128rpx]">基本情况</text>
<text class="flex-1">
{{ ellipsisDesc(item) }}
{{ item.basicInformation }}
</text>
</view>
<view class="flex gap-[20rpx] mb-[16rpx]">
<text class="text-muted w-[128rpx]">电销老师</text>
<text class="flex-1">{{ item.telemarketingTeacherName }}</text>
</view>
<view class="flex gap-[20rpx] mb-[16rpx]">
<text class="text-muted w-[128rpx]">跟进时间</text>
<text class="flex-1">{{ item.createTime }}</text>
<text class="flex-1">{{ item.followUpTime }}</text>
</view>
<view class="flex gap-[20rpx] mb-[16rpx]">
<text class="text-muted w-[128rpx]">招生老师</text>
@ -30,21 +37,21 @@
</view>
<view class="flex gap-[20rpx] mb-[16rpx]">
<text class="text-muted w-[128rpx]">领取时间</text>
<text class="flex-1">{{ item.createTime }}</text>
<text class="flex-1">{{ item.getTime }}</text>
</view>
<view class="flex gap-[20rpx]">
<view class="flex gap-[20rpx] mb-[16rpx]">
<text class="text-muted w-[128rpx]">状态</text>
<text class="flex-1 text-orage">{{ parseStateText }}</text>
</view>
<view class="flex gap-[20rpx]">
<view class="flex gap-[20rpx] mb-[16rpx]">
<text class="text-muted w-[128rpx]">备注</text>
<view class="flex gap-[12rpx] flex-1">
<text class="text-error">{{ item.remark }}</text>
</view>
</view>
<view class="flex gap-[20rpx]">
<view class="flex gap-[20rpx] mb-[16rpx]">
<text class="text-muted w-[128rpx]">成交时间</text>
<text class="flex-1">{{ item.updateTime }}</text>
<text class="flex-1">{{ item.accomplishTime }}</text>
</view>
</template>
</w-card>
@ -70,6 +77,9 @@ export interface IClue {
recruitTeacherId: number
telemarketingTeacherId: number
updateTime: string
followUpTime: string //
accomplishTime: string //
getTime: string //
}
const props = defineProps({
item: {
@ -77,12 +87,6 @@ const props = defineProps({
default: () => ({})
}
})
const ellipsisDesc = computed(
() => (item: IClue) =>
item.basicInformation?.length >= 14
? item.basicInformation?.slice(0, 14) + '...'
: item.basicInformation
)
const stateMap: Record<stateEnum, string> = {
[stateEnum.ADD_RELATION]: '账号已添加',
[stateEnum.NO_EXIST]: '账号不存在',

View File

@ -47,64 +47,91 @@ import { ref, shallowRef } from 'vue'
import clueCard from './components/clue-card.vue'
import datePopup from './components/date-popup.vue'
import { useZPaging } from '@/hooks/useZPaging'
import { apiTeleClueList } from '@/api/clue'
import { computed } from 'vue'
import { debounce } from 'lodash-es'
import { nextTick } from 'vue'
import { formatDate, formateDate, getCurDate } from '@/utils/util'
import { personalDetailApi } from '@/api/admin'
import { converStatusEnum } from '@/enums'
const queryParams = ref({
likeWork: ''
likeWork: '',
userId: '',
teachedType: null,
status: null,
startTime: '',
endTime: ''
})
const dataList = ref([])
const { paging, queryList, refresh, changeApi, setParams } = useZPaging(
queryParams.value,
apiTeleClueList
)
const activeTab = ref(0)
const tabOptions: Record<string, any[]> = {
telesale: [
{ name: '全部', value: 0 },
{ name: '未领取', value: 1 },
{ name: '已领取', value: 2 },
{ name: '异常待处理', value: 3 }
{ name: '未领取', value: converStatusEnum.UN_RECEIVED },
{ name: '已领取', value: converStatusEnum.CONVERTED_PROCESS },
{ name: '异常待处理', value: converStatusEnum.EXCEPTION }
],
recruitsale: [
{ name: '转化中', value: 0 },
{ name: '已添加', value: 1 },
{ name: '异常待处理', value: 2 },
{ name: '已成交', value: 3 },
{ name: '已战败', value: 4 }
{ name: '转化中', value: converStatusEnum.CONVERTED_PROCESS },
{ name: '已添加', value: converStatusEnum.ADD_RELATION },
{ name: '异常待处理', value: converStatusEnum.EXCEPTION },
{ name: '已成交', value: converStatusEnum.CONVERTED },
{ name: '已战败', value: converStatusEnum.FAILED }
]
}
const tabs = computed(() => tabOptions[type.value])
const isScrollable = computed(() => tabs.value?.length >= 5)
const handleChangeTab = item => {
activeTab.value = item.value
queryParams.value.status = activeTab.value
queryList()
}
const searchChange = debounce(() => {
refresh(queryParams.value)
queryList()
}, 300)
const type = ref('')
const dateTagFlag = ref('')
const paging = ref()
const queryList = async (pageNo = 1, pageSize = 10) => {
try {
uni.showLoading({
title: '加载中...'
})
const params = {
...queryParams.value,
pageNo,
pageSize
}
const result = await personalDetailApi(params)
paging.value.complete(result.lists)
} catch (e) {
paging.value.complete(false)
}
uni.hideLoading()
}
// const dateTagFlag = ref('')
onLoad(async options => {
setFormData(options)
dateTagFlag.value = options?.dateTag ?? ''
await nextTick()
datePopupRef.value?.setDefaultValue({ ...form.value, dateTag: dateTagFlag.value })
// dateTagFlag.value = options?.dateTag ?? ''
// await nextTick()
// datePopupRef.value?.setDefaultValue({ ...form.value, dateTag: dateTagFlag.value })
// datePopupRef.value?.setDefaultValue({ ...form.value })
// if (option.id) {
// uni.setNavigationBarTitle({
// title: ''
// })
type.value = options.type ?? ''
uni.setNavigationBarTitle({
title:
options.postIds == 5 ? '电销老师:' + options.username : '招生老师:' + options.username
})
type.value = options.postIds == 5 ? 'telesale' : 'recruitsale'
// }
})
const form = ref()
//
const setFormData = options => {
form.value = Object.keys(options).reduce((acc, key) => {
if (key == 'id' || key == 'dateTag' || key == 'type') {
queryParams.value = Object.keys(options).reduce((acc, key) => {
if (key == 'username') {
return acc
}
acc[key] = decodeURIComponent(options[key])
@ -118,16 +145,16 @@ const handleShowPopup = () => {
}
const handleConfirmDate = (date: { [key: string]: string }) => {
const { start, end } = date
form.value.createTimeStart = formateDate(start, 'start', 'YYYY-MM-DD')
form.value.createTimeEnd = formateDate(end, 'end', 'YYYY-MM-DD')
queryParams.value.startTime = formateDate(start, 'start')
queryParams.value.endTime = formateDate(end, 'end')
datePopupRef.value?.handleToggle(true)
queryList()
}
const handleResetDate = () => {
form.value.createTimeStart = getCurDate('start', 'YYYY-MM-DD')
form.value.createTimeEnd = getCurDate('end', 'YYYY-MM-DD')
console.log(form.value)
queryParams.value.startTime = getCurDate('start')
queryParams.value.endTime = getCurDate('end')
datePopupRef.value?.handleToggle(true)
queryList()
}
</script>
<style scoped></style>

View File

@ -239,7 +239,7 @@ async function loginHandle(data: any) {
cache.set(ROLEINDEX, 2)
setNextRoute()
} else {
if (userInfo.roles?.length > 1) {
if (userInfo.roles?.length >= 1) {
uni.redirectTo({
url: '/bundle/pages/select-role/index'
})

View File

@ -11,11 +11,21 @@
{{ form.createTimeStart }} - {{ form.createTimeEnd }}
</text>
</template>
<template v-else-if="key === 'timeRange'">
<text class="text-[#1E40AF]">{{ parseLabel(key) }}:</text>
<text class="text-primary">{{ form.startTime }} - {{ form.endTime }}</text>
</template>
<template v-else>
<text class="text-[#1E40AF]">{{ parseLabel(key) }}:</text>
<text class="text-primary" v-if="key === 'organizationId'">
{{ findTargetNode(value) }}
</text>
<text class="text-primary" v-else-if="key === 'userId'">
{{ findTargetMember(value) }}
</text>
<text class="text-primary" v-else-if="key === 'postId'">
{{ findTargetPosition(value) }}
</text>
<text class="text-primary" v-else>{{ value }}</text>
</template>
</view>
@ -27,6 +37,7 @@ import { PropType } from 'vue'
import { IForm } from './team/index.vue'
import { computed } from 'vue'
import { AdminTabEnum } from '@/enums'
import { usePositions } from '@/hooks/useCommon'
const formMap: Record<AdminTabEnum, any> = {
[AdminTabEnum.TEAM]: {
@ -36,7 +47,7 @@ const formMap: Record<AdminTabEnum, any> = {
[AdminTabEnum.PERSONALLY]: {
postId: '岗位',
userId: '用户',
createTimeRange: '日期'
timeRange: '日期'
}
}
const props = defineProps({
@ -51,10 +62,18 @@ const props = defineProps({
organizationList: {
type: Array as PropType<any[]>,
default: () => []
},
positionList: {
type: Array as PropType<any[]>,
default: () => []
},
memberList: {
type: Array as PropType<any[]>,
default: () => []
}
})
const filteredForm = computed(() => {
const { createTimeStart, createTimeEnd, ...rest } = props.form
const { createTimeStart, createTimeEnd, startTime = '', endTime = '', ...rest } = props.form
const activeTabMap = formMap[props.activeTab]
const filtered = Object.keys(rest).reduce((acc, key) => {
if (activeTabMap[key]) {
@ -66,6 +85,10 @@ const filteredForm = computed(() => {
if (createTimeStart && createTimeEnd) {
filtered['createTimeRange'] = 'createTimeRange'
}
if (startTime && endTime) {
filtered['timeRange'] = 'timeRange'
}
console.log(filtered)
return filtered
})
@ -98,5 +121,14 @@ const findTargetNode = computed(() => (id: number) => {
}
return nodeInfo?.name || ''
})
const findTargetPosition = computed(() => (id: number) => {
if (!id) return
return props.positionList.find(item => item.id == id).label ?? ''
})
const findTargetMember = computed(() => (id: number) => {
if (!id) return
const nodeInfo = findNodeById(props.memberList, id)
if (nodeInfo && nodeInfo.id) return nodeInfo.username
})
</script>
<style scoped></style>

View File

@ -198,7 +198,7 @@ export default defineComponent({
&-item {
display: flex;
align-items: center;
// align-items: center;
justify-content: space-between;
padding: 24rpx;
// font-size: 24rpx;

View File

@ -48,7 +48,7 @@ const props = defineProps({
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue', 'refreshPage', 'confirm', 'reset'])
const emit = defineEmits(['update:modelValue', 'refreshPage', 'confirm', 'reset', 'getData'])
const localValue = computed({
get() {
@ -81,8 +81,8 @@ const handleConfirm = (type: string, item, dateTag?: string) => {
case 'date':
localValue.value = {
...localValue.value,
createTimeStart: formateDate(item.start, 'start'),
createTimeEnd: formateDate(item.end, 'end')
startTime: formateDate(item.start, 'start'),
endTime: formateDate(item.end, 'end')
}
break
case 'position':
@ -94,7 +94,6 @@ const handleConfirm = (type: string, item, dateTag?: string) => {
default:
break
}
emit('refreshPage')
emit('confirm', dateTag)
closeDropDown()
}
@ -110,21 +109,20 @@ const handleReset = (type: string) => {
case 'date':
localValue.value = {
...localValue.value,
createTimeStart: getCurDate('start'),
createTimeEnd: getCurDate('end')
startTime: getCurDate('start'),
endTime: getCurDate('end')
}
break
case 'position':
localValue.value = {
...localValue.value,
postId: 0
postId: ''
}
break
default:
break
}
emit('refreshPage')
emit('reset')
closeDropDown()
}
@ -137,6 +135,7 @@ const fetchAllOrganizationList = async () => {
try {
const result = await apiOrganizationAllList()
dropdownMenuOrgList.value = renameFields(result)
emit('getData', dropdownMenuOrgList.value)
} catch (error) {}
}
const renameFields = (data: any[]): any[] => {

View File

@ -5,11 +5,13 @@
<view
class="bg-primary rounded-full text-white w-[120rpx] h-[120rpx] flex items-center justify-center"
>
张三
{{ item.username }}
</view>
<view class="flex-1 flex flex-col gap-[12rpx]">
<text>张三</text>
<text class="text-muted">湛江团队-A组 . {{ parsePostionText }}</text>
<text>{{ item.username }}</text>
<text class="text-muted">
{{ item.organizationName }} . {{ parsePostionText }}
</text>
</view>
</view>
<view class="flex gap-[12rpx] flex-wrap" v-if="!containPostIds">
@ -25,7 +27,7 @@
<view v-else class="flex gap-[12rpx] flex-wrap">
<view
class="w-full flex flex-col gap-[24rpx] mb-[24rpx]"
v-for="(value, key) in cardMap"
v-for="(value, key) in data"
:key="key"
>
<view class="flex justify-between items-center px-[24rpx]">
@ -53,25 +55,37 @@
<script setup lang="ts">
import { computed } from 'vue'
import card from '../../card.vue'
import { PositionEnum } from '@/enums'
import { usePositions } from '@/hooks/useCommon'
import { watch } from 'vue'
const props = defineProps({
item: {
type: Object,
default: () => {}
},
positionList: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['handleCardClick'])
const parsePostionText = computed(() => {
const { postId } = props.item
const ids = postId?.split(',')
const positionsList = [
{ name: '电销老师', id: 5 },
{ name: '招生老师', id: 6 }
]
const findItems = positionsList.filter(item => ids.includes(String(item.id)))
return findItems.map(item => item.name).join('/')
// ids
const parsePostIds = computed(() => {
const { postIds } = props.item
return postIds?.split(',').map(Number)
})
// +
const containPostIds = computed(() => {
return parsePostIds.value?.includes(5) && parsePostIds.value?.includes(6)
})
const parsePostionText = computed(() => {
const findItems = props.positionList?.filter(item => parsePostIds.value?.includes(item.id))
return findItems.map(item => item.label).join('/')
})
const parseText = computed(() => (key: number) => key == 5 ? '电销' : '招生')
const cardMap: Record<number, Array<{ label: string; value: number }>> = {
5: [
@ -88,30 +102,67 @@ const cardMap: Record<number, Array<{ label: string; value: number }>> = {
{ label: '战败客户', value: 368 }
]
}
const parseText = computed(() => (key: number) => key == 5 ? '电销' : '招生')
const data = computed(() => {
const { postId } = props.item
return cardMap[postId]
})
const parsePostIds = computed(() => {
const { postId } = props.item
return postId?.split(',').map(Number)
})
const containPostIds = computed(() => {
return parsePostIds.value.includes(5) && parsePostIds.value.includes(6)
if (!props.item) return []
const { postIds } = props.item
const dataMap: Record<string, any> = {
telemarketingVo: [
{ field: 'newAddCount', label: '新增跟进' },
{ field: 'unclaimedCount', label: '未领取' },
{ field: 'alreadyReceivedCount', label: '已领取' },
{ field: 'exceptionCount', label: '异常待处理' }
],
recruitVo: [
{ field: 'conversionCount', label: '转化中' },
{ field: 'addedCount', label: '已添加' },
{ field: 'abnormalCount', label: '异常待处理' },
{ field: 'tradedCount', label: '成交客户' },
{ field: 'failCount', label: '战败客户' }
]
}
const ids = postIds?.split(',').map(Number)
const field =
ids?.length == 1 && ids?.includes(5)
? 'telemarketingVo'
: ids?.length == 1 && ids?.includes(6)
? 'recruitVo'
: ''
// and
if (field == '' && ids.length >= 2) {
const obj: Record<number, Array<{ label: string; value: number }>> = {}
Object.keys(dataMap).map(key => {
const id = key === 'telemarketingVo' ? 5 : 6
obj[id] = dataMap[key].map(item => {
return {
label: item.label,
value: props.item[key][item.field]
}
})
})
return obj
// or
} else {
return dataMap[field].map(item => {
return {
label: item.label,
value: props.item[field][item.field]
}
})
}
})
const handleCardClick = () => {
if (containPostIds.value) return
const type =
parsePostIds.value.length == 1 && parsePostIds.value.includes(5)
? 'telesale'
: 'recruitsale'
emit('handleCardClick', type, props.item)
// const type =
// parsePostIds.value.length == 1 && parsePostIds.value.includes(5)
// ? 'telesale'
// : 'recruitsale'
emit('handleCardClick', props.item)
}
const handleMore = (key: number) => {
const type = key == 5 ? 'telesale' : 'recruitsale'
emit('handleCardClick', type, props.item)
// const type = key == 5 ? 'telesale' : 'recruitsale'
emit('handleCardClick', { ...props.item, postIds: key.toString() })
}
</script>
<style scoped></style>

View File

@ -1,77 +1,108 @@
<template>
<view class="flex-1 h-full flex flex-col">
<dropdownPicker v-model="form" @confirm="confirm" @reset-page="resetPage" />
<dropdownPicker
v-model="form"
@confirm="confirm"
@reset="refreshPage"
@get-data="getPickerData"
/>
<filter-value
:form="form"
:activeTab="activeTab"
:positionList="positionList"
:memberList="memberList"
/>
<view class="flex-1 mt-3 px-[32rpx] overflow-auto bg-[#FAFAFE]">
<!-- <z-paging
<z-paging
ref="paging"
v-model="dataList"
@query="queryList"
:fixed="false"
height="100%"
> -->
<template v-for="(item, index) in dataList" :key="`unique_${index}`">
<telesale-card :item="item" @handleCardClick="handleCardClick" />
</template>
<!-- </z-paging> -->
>
<template v-for="(item, index) in dataList" :key="`unique_${index}`">
<telesale-card
:item="item"
:positionList="positionList"
@handleCardClick="handleCardClick"
/>
</template>
</z-paging>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { PropType, computed, onMounted, ref } from 'vue'
import telesaleCard from './components/telesale-card.vue'
import filterValue from '../filter-value.vue'
import { useZPaging } from '@/hooks/useZPaging'
import { apiTeleClueList } from '@/api/clue'
import { getCurDate } from '@/utils/util'
import dropdownPicker from './components/dropdown-picker.vue'
import { personalListApi } from '@/api/admin'
import { usePositions } from '@/hooks/useCommon'
import { AdminTabEnum } from '@/enums'
import { nextTick } from 'vue'
defineProps({
activeTab: {
type: Number as PropType<AdminTabEnum>,
default: AdminTabEnum.TEAM
}
})
const form = ref({
postId: 0,
createTimeStart: getCurDate('start'),
createTimeEnd: getCurDate('end'),
postId: '',
startTime: getCurDate('start'),
endTime: getCurDate('end'),
userId: ''
})
const queryParams = ref({
likeWork: ''
})
const dataList = ref([{ id: 1, postId: '5,6', studentName: '张三' }])
const dataList = ref([])
const { paging, queryList, refresh, changeApi, setParams } = useZPaging(
queryParams.value,
apiTeleClueList,
form.value,
personalListApi,
() => {}
)
const dateTagFlag = ref() //
const confirm = (dateTag?: string) => {
console.log(form.value, dateTag)
dateTagFlag.value = dateTag
refresh(form.value)
}
const refreshPage = () => {
refresh(form.value)
}
const splitpostIds = (postIds: string) => {
return postIds?.split(',').map(Number)[0]
}
const resetPage = () => {}
//
const handleCardClick = (type: string, item: any) => {
const { id } = item
console.log(form.value)
const handleCardClick = (item: any) => {
const { id, postIds, username } = item
const params = {
...form.value,
dateTag: encodeURIComponent(dateTagFlag.value),
id,
type
userId: id,
postIds: splitpostIds(postIds),
teacherType: splitpostIds(postIds) == 5 ? 0 : 1,
username
// dateTag: encodeURIComponent(dateTagFlag.value),
}
const queryString = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&')
switch (type) {
case 'telesale':
case 'recruitsale':
uni.navigateTo({
url: '/bundle/pages/clue-list/index?id=' + queryString
})
break
default:
break
}
uni.navigateTo({
url: '/bundle/pages/clue-list/index?' + queryString
})
}
const { positionList, fetchPositions } = usePositions()
const memberList = ref<any[]>([])
const getPickerData = (data: any[]) => {
memberList.value = data
}
onMounted(() => {
fetchPositions()
})
</script>
<style scoped lang="scss"></style>

View File

@ -1,5 +1,5 @@
<template>
<view class="px-[32rpx]">
<view class="px-[32rpx] min-h-[250rpx] bg-white">
<template v-if="loading && !data.length">
<view class="min-h-[200rpx] flex justify-center items-center">
<u-loading-icon></u-loading-icon>

View File

@ -26,11 +26,13 @@ import { getCurDate } from '@/utils/util'
import { AdminTabEnum } from '@/enums'
export interface IForm {
organizationId: number | null
createTimeStart: string
createTimeEnd: string
organizationId?: number | null
createTimeStart?: string
createTimeEnd?: string
startTime?: string
endTime?: string
userId: string
postId?: number
postId?: number | string
}
defineProps({

View File

@ -161,6 +161,7 @@ const judgeShow = () => {
onMounted(() => {
judgeShow()
userStore.getUser()
})
</script>
<style lang="scss" scoped>