feat(manager): 实现团队管理页面的数据对接和功能优化
- 新增团队异常预警API接口和数据展示 - 完善销售漏斗组件,对接实际数据 - 优化业绩排名组件,支持多种数据源 - 更新成员详情组件,适配新数据结构 - 重构管理页面,整合多个API调用
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import https from '../utils/https'
|
||||
|
||||
// 团队异常预警 /api/v1/manager/group_abnormal_response
|
||||
export const getGroupAbnormalResponse = (params) => {
|
||||
return https.post('/api/v1/manager/group_abnormal_response', params)
|
||||
}
|
||||
|
||||
// 团队总通话 /api/v1/manager/week_total_call
|
||||
export const getWeekTotalCall = (params) => {
|
||||
return https.post('/api/v1/manager/week_total_call', params)
|
||||
@@ -20,7 +25,10 @@ export const getWeekAddDealTotal = (params) => {
|
||||
export const getWeekAddFeeTotal = (params) => {
|
||||
return https.post('/api/v1/manager/week_add_fee_total', params)
|
||||
}
|
||||
// 定金转化率 /api/v1/manager/week_add_fee_total
|
||||
// 定金转化率 /api/v1/manager/get_pay_deposit_to_money_rate
|
||||
export const getPayDepositToMoneyRate = (params) => {
|
||||
return https.post('/api/v1/manager/get_pay_deposit_to_money_rate', params)
|
||||
}
|
||||
|
||||
// 团队漏斗 /api/v1/group_funnel/get_group_funnel
|
||||
export const getGroupFunnel = (params) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="member-details">
|
||||
<div class="details-header" @click="toggleDetailsCollapse">
|
||||
<h2>{{ selectedMember.name }} 的详细数据</h2>
|
||||
<h2>{{ selectedMember.user_name || selectedMember.name }} 的详细数据</h2>
|
||||
<div class="collapse-toggle" :class="{ 'collapsed': isDetailsCollapsed }">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 4l4 4H4l4-4z"/>
|
||||
@@ -12,27 +12,27 @@
|
||||
<div class="details-grid" v-show="!isDetailsCollapsed" :class="{ 'collapsing': isDetailsCollapsed }">
|
||||
<div class="detail-card">
|
||||
<div class="detail-label">总通话次数</div>
|
||||
<div class="detail-value">{{ selectedMember.calls }} 次</div>
|
||||
<div class="detail-value">{{ selectedMember.calls || 0 }} 次</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-label">通话时长</div>
|
||||
<div class="detail-value">{{ selectedMember.callTime }} 小时</div>
|
||||
<div class="detail-value">{{ selectedMember.callTime || 0 }} 小时</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-label">新增客户</div>
|
||||
<div class="detail-value">{{ selectedMember.newClients }} 人</div>
|
||||
<div class="detail-value">{{ selectedMember.newClients || 0 }} 人</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-label">成交单数</div>
|
||||
<div class="detail-value">{{ selectedMember.deals }} 单</div>
|
||||
<div class="detail-value">{{ selectedMember.deals || 0 }} 单</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-label">总业绩</div>
|
||||
<div class="detail-value">¥{{ selectedMember.performance.toLocaleString() }}</div>
|
||||
<div class="detail-value">¥{{ formatAmount(selectedMember.week_amount || selectedMember.performance) }}</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-label">平均单价</div>
|
||||
<div class="detail-value">¥{{ selectedMember.avgDealValue.toLocaleString() }}</div>
|
||||
<div class="detail-label">转化率</div>
|
||||
<div class="detail-value">{{ selectedMember.conversion_rate || selectedMember.conversion || '0%' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="no-guidance" v-else>
|
||||
<div class="celebration-icon">🎉</div>
|
||||
<h4>表现优秀!</h4>
|
||||
<p>{{ selectedMember.name }} 的各项指标都很不错,继续保持这种状态!</p>
|
||||
<p>{{ selectedMember.user_name || selectedMember.name }} 的各项指标都很不错,继续保持这种状态!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,7 +104,7 @@
|
||||
<div class="no-recordings" v-else>
|
||||
<div class="no-data-icon">📞</div>
|
||||
<h4>暂无录音</h4>
|
||||
<p>{{ selectedMember.name }} 还没有通话录音记录</p>
|
||||
<p>{{ selectedMember.user_name || selectedMember.name }} 还没有通话录音记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,6 +146,14 @@ const toggleRecordingsCollapse = () => {
|
||||
isRecordingsCollapsed.value = !isRecordingsCollapsed.value
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount) => {
|
||||
if (typeof amount === 'number') {
|
||||
return amount.toLocaleString()
|
||||
}
|
||||
return amount || '0'
|
||||
}
|
||||
|
||||
// 获取成员录音列表
|
||||
const getRecordingsForMember = (member) => {
|
||||
// 模拟录音数据,实际项目中应该从API获取
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
<span>入群率</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="member in teamMembers"
|
||||
:key="member.id"
|
||||
v-for="(member, index) in displayMembers"
|
||||
:key="member.user_name || member.id"
|
||||
class="table-row"
|
||||
:class="{ active: selectedMember.id === member.id }"
|
||||
:class="{ active: selectedMember && (selectedMember.user_name === member.user_name || selectedMember.id === member.id) }"
|
||||
@click="selectMember(member)"
|
||||
>
|
||||
<span class="rank">{{ member.rank }}</span>
|
||||
<span class="name">{{ member.name }}</span>
|
||||
<span class="performance">¥{{ member.performance.toLocaleString() }}</span>
|
||||
<span class="conversion">{{ member.conversion }}%</span>
|
||||
<span class="wechat-rate">{{ member.wechatRate || 0 }}%</span>
|
||||
<span class="group-rate">{{ member.groupRate || 0 }}%</span>
|
||||
<span class="rank">{{ index + 1 }}</span>
|
||||
<span class="name">{{ member.user_name || member.name }}</span>
|
||||
<span class="performance">¥{{ formatAmount(member.week_amount || member.performance) }}</span>
|
||||
<span class="conversion">{{ member.conversion_rate || member.conversion || '0%' }}</span>
|
||||
<span class="wechat-rate">{{ member.plus_v_rate || member.wechatRate || '0%' }}</span>
|
||||
<span class="group-rate">{{ member.group_rate || member.groupRate || '0%' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,23 +31,45 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
import { defineProps, defineEmits, computed } from 'vue'
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
teamMembers: {
|
||||
type: Array,
|
||||
required: true
|
||||
default: () => []
|
||||
},
|
||||
selectedMember: {
|
||||
type: Object,
|
||||
required: true
|
||||
default: () => null
|
||||
},
|
||||
groupRanking: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
// 定义emits
|
||||
const emit = defineEmits(['select-member'])
|
||||
|
||||
// 计算显示的成员数据
|
||||
const displayMembers = computed(() => {
|
||||
// 如果有groupRanking数据,优先使用
|
||||
if (props.groupRanking && props.groupRanking.team_data && props.groupRanking.team_data.group_list) {
|
||||
return props.groupRanking.team_data.group_list
|
||||
}
|
||||
// 否则使用teamMembers数据
|
||||
return props.teamMembers
|
||||
})
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount) => {
|
||||
if (typeof amount === 'number') {
|
||||
return amount.toLocaleString()
|
||||
}
|
||||
return amount || '0'
|
||||
}
|
||||
|
||||
// 选择成员函数
|
||||
const selectMember = (member) => {
|
||||
emit('select-member', member)
|
||||
|
||||
@@ -5,23 +5,23 @@
|
||||
<div class="funnel-chart">
|
||||
<div class="funnel-stage" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<span class="stage-label">线索总数</span>
|
||||
<span class="stage-value">1000</span>
|
||||
<span class="stage-value">{{ funnelData.customers_count?.['线索总数'] || 0 }}</span>
|
||||
</div>
|
||||
<div class="funnel-stage" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||
<span class="stage-label">有效沟通</span>
|
||||
<span class="stage-value">450</span>
|
||||
<span class="stage-value">{{ funnelData.customers_count?.['有效沟通'] || 0 }}</span>
|
||||
</div>
|
||||
<div class="funnel-stage" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
||||
<span class="stage-label">到课数据</span>
|
||||
<span class="stage-value">180</span>
|
||||
<span class="stage-value">{{ funnelData.customers_count?.['到课数据'] || 0 }}</span>
|
||||
</div>
|
||||
<div class="funnel-stage" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
|
||||
<span class="stage-label">预付定金</span>
|
||||
<span class="stage-value">50</span>
|
||||
<span class="stage-value">{{ funnelData.customers_count?.['预付定金'] || 0 }}</span>
|
||||
</div>
|
||||
<div class="funnel-stage" style="background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);">
|
||||
<span class="stage-label">成功签单</span>
|
||||
<span class="stage-value">12</span>
|
||||
<span class="stage-value">{{ funnelData.customers_count?.['成功签单'] || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,6 +29,22 @@
|
||||
|
||||
<script setup>
|
||||
// 团队销售漏斗组件
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
funnelData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
customers_count: {
|
||||
'线索总数': 0,
|
||||
'有效沟通': 0,
|
||||
'到课数据': 0,
|
||||
'预付定金': 0,
|
||||
'成功签单': 0
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -2,24 +2,80 @@
|
||||
<div class="team-alerts">
|
||||
<h2>团队异常预警</h2>
|
||||
<div class="alert-list">
|
||||
<div class="alert-item warning">
|
||||
<span class="alert-icon">⚠</span>
|
||||
<span>钱鑫有102人(预计)需今日跟进通话</span>
|
||||
<div
|
||||
v-for="alert in aggregatedAlerts"
|
||||
:key="alert.id"
|
||||
class="alert-item"
|
||||
:class="alert.type"
|
||||
>
|
||||
<span class="alert-icon">{{ alert.icon }}</span>
|
||||
<span>{{ alert.message }}</span>
|
||||
</div>
|
||||
<div class="alert-item danger">
|
||||
<span class="alert-icon">🔺</span>
|
||||
<span>李娜今日预计电话工作量达30%</span>
|
||||
</div>
|
||||
<div class="alert-item info">
|
||||
<div v-if="aggregatedAlerts.length === 0" class="alert-item info">
|
||||
<span class="alert-icon">ℹ</span>
|
||||
<span>高明明客户"王先生"下次未来电话记录</span>
|
||||
<span>暂无异常预警</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 团队异常预警组件
|
||||
import { computed } from 'vue'
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
abnormalData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
// 聚合异常数据,按人员分组
|
||||
const aggregatedAlerts = computed(() => {
|
||||
const alerts = []
|
||||
const data = props.abnormalData
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
return alerts
|
||||
}
|
||||
|
||||
// 收集所有异常人员
|
||||
const personAbnormalities = new Map()
|
||||
|
||||
// 处理严重超时率异常
|
||||
if (data.erious_timeout_rate_abnorma && Array.isArray(data.erious_timeout_rate_abnorma)) {
|
||||
data.erious_timeout_rate_abnorma.forEach(person => {
|
||||
if (!personAbnormalities.has(person)) {
|
||||
personAbnormalities.set(person, [])
|
||||
}
|
||||
personAbnormalities.get(person).push('严重超时率异常')
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格填写异常
|
||||
if (data.table_filling_abnormal && Array.isArray(data.table_filling_abnormal)) {
|
||||
data.table_filling_abnormal.forEach(person => {
|
||||
if (!personAbnormalities.has(person)) {
|
||||
personAbnormalities.set(person, [])
|
||||
}
|
||||
personAbnormalities.get(person).push('表格填写异常')
|
||||
})
|
||||
}
|
||||
|
||||
// 生成聚合后的预警信息
|
||||
let alertId = 1
|
||||
personAbnormalities.forEach((abnormalities, person) => {
|
||||
const abnormalityText = abnormalities.join('、')
|
||||
alerts.push({
|
||||
id: alertId++,
|
||||
type: abnormalities.length > 1 ? 'danger' : 'warning',
|
||||
icon: abnormalities.length > 1 ? '🔺' : '⚠',
|
||||
message: `${person}存在${abnormalityText}`
|
||||
})
|
||||
})
|
||||
|
||||
return alerts
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -43,6 +99,27 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
||||
// 自定义滚动条样式
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
|
||||
@@ -32,17 +32,17 @@
|
||||
</div>
|
||||
<div class="report-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">总业绩</span>
|
||||
<span class="card-trend positive">+8% vs 上期</span>
|
||||
<span class="card-title">月度总业绩</span>
|
||||
<span class="card-trend positive">+8% vs 上月</span>
|
||||
</div>
|
||||
<div class="card-value">{{ formatCurrency(weekTotalData.week_add_fee_total?.total_add_fee || 0) }} 元</div>
|
||||
</div>
|
||||
<div class="report-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">定金转化率</span>
|
||||
<span class="card-trend positive">+9% vs 上期</span>
|
||||
<span class="card-trend positive">{{ weekTotalData.pay_deposit_to_money_rate?.team_data?.week_vs_last_week || '0%' }} vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">{{ formatCurrency(weekTotalData.week_add_deal_total?.total_add_deal_fee || 0) }} 元</div>
|
||||
<div class="card-value">{{ weekTotalData.pay_deposit_to_money_rate?.team_data?.week_pay_deposit_to_money_rate || '0%' }} </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,6 +60,7 @@ const props = defineProps({
|
||||
week_add_customer_total: {},
|
||||
week_add_deal_total: {},
|
||||
week_add_fee_total: {},
|
||||
pay_deposit_to_money_rate: {}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
<!-- Top Section - Team Alerts and Today's Report -->
|
||||
<div class="top-section">
|
||||
<!-- Team Alerts -->
|
||||
<TeamAlerts />
|
||||
<TeamAlerts :abnormalData="groupAbnormalResponse" />
|
||||
<!-- Today's Team Report -->
|
||||
<TeamReport :weekTotalData="weekTotalData" />
|
||||
|
||||
</div>
|
||||
<!-- Sales Funnel Section -->
|
||||
<SalesFunnel />
|
||||
<SalesFunnel :funnelData="weekTotalData.group_funnel" />
|
||||
|
||||
<!-- Bottom Section -->
|
||||
<div class="bottom-section">
|
||||
@@ -34,6 +34,7 @@
|
||||
<PerformanceRanking
|
||||
:team-members="teamMembers"
|
||||
:selected-member="selectedMember"
|
||||
:group-ranking="groupRanking"
|
||||
@select-member="selectMember"
|
||||
/>
|
||||
</div>
|
||||
@@ -61,9 +62,7 @@ import RawDataCards from "../person/components/RawDataCards.vue";
|
||||
import CustomerDetail from "../person/components/CustomerDetail.vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getWeekTotalCall, getWeekAddCustomerTotal, getWeekAddDealTotal, getWeekAddFeeTotal, getGroupFunnel } from "@/api/manager.js";
|
||||
|
||||
|
||||
import {getGroupAbnormalResponse, getWeekTotalCall, getWeekAddCustomerTotal, getWeekAddDealTotal, getWeekAddFeeTotal, getGroupFunnel,getPayDepositToMoneyRate,getGroupRanking } from "@/api/manager.js";
|
||||
|
||||
// 团队成员数据
|
||||
const teamMembers = [
|
||||
@@ -214,8 +213,21 @@ const weekTotalData = ref({
|
||||
week_add_customer_total: {},
|
||||
week_add_deal_total: {},
|
||||
week_add_fee_total: {},
|
||||
pay_deposit_to_money_rate: {},
|
||||
group_funnel: {},
|
||||
week_add_fee_total: {},
|
||||
});
|
||||
|
||||
// 团队异常预警
|
||||
const groupAbnormalResponse = ref({})
|
||||
async function TeamGetGroupAbnormalResponse() {
|
||||
const params = getRequestParams()
|
||||
const hasParams = params.user_name
|
||||
const res = await getGroupAbnormalResponse(hasParams ? params : undefined)
|
||||
console.log(res)
|
||||
if (res.code === 200) {
|
||||
groupAbnormalResponse.value = res.data
|
||||
}
|
||||
}
|
||||
// 团队总通话
|
||||
async function TeamGetWeekTotalCall() {
|
||||
const params = getRequestParams()
|
||||
@@ -246,9 +258,19 @@ async function TeamGetWeekAddDealTotal() {
|
||||
weekTotalData.value.week_add_deal_total = res.data
|
||||
}
|
||||
}
|
||||
// 总业绩
|
||||
// 月度总业绩
|
||||
|
||||
|
||||
// 定金转化
|
||||
async function TeamGetWeekAddFeeTotal() {
|
||||
const params = getRequestParams()
|
||||
const hasParams = params.user_name
|
||||
const res = await getPayDepositToMoneyRate(hasParams ? params : undefined)
|
||||
console.log(res)
|
||||
if (res.code === 200) {
|
||||
weekTotalData.value.pay_deposit_to_money_rate = res.data
|
||||
}
|
||||
}
|
||||
// 销售漏斗
|
||||
async function TeamGetGroupFunnel() {
|
||||
const params = getRequestParams()
|
||||
@@ -256,9 +278,74 @@ async function TeamGetGroupFunnel() {
|
||||
const res = await getGroupFunnel(hasParams ? params : undefined)
|
||||
console.log(res)
|
||||
if (res.code === 200) {
|
||||
weekTotalData.value.week_add_fee_total = res.data
|
||||
weekTotalData.value.group_funnel = res.data
|
||||
/**
|
||||
* "data": {
|
||||
"user_name": "马然",
|
||||
"user_level": 2,
|
||||
"customers_count": {
|
||||
"线索总数": 132,
|
||||
"有效沟通": 33,
|
||||
"到课数据": 59,
|
||||
"预付定金": 7,
|
||||
"成功签单": 2
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
// 团队业绩排名
|
||||
const groupRanking = ref({})
|
||||
|
||||
async function TeamGetGroupRanking() {
|
||||
const params = getRequestParams()
|
||||
const hasParams = params.user_name
|
||||
const res = await getGroupRanking(hasParams ? params : undefined)
|
||||
console.log(res)
|
||||
if (res.code === 200) {
|
||||
groupRanking.value = res.data
|
||||
/**
|
||||
* "data": {
|
||||
"user_name": "马然",
|
||||
"user_level": 2,
|
||||
"team_data": {
|
||||
"group_list": [
|
||||
{
|
||||
"user_name": "马然",
|
||||
"week_amount": 0,
|
||||
"conversion_rate": "0%",
|
||||
"plus_v_rate": "0%",
|
||||
"group_rate": "0%"
|
||||
},
|
||||
{
|
||||
"user_name": "程慧仟",
|
||||
"week_amount": 7100.0,
|
||||
"conversion_rate": "0.00%",
|
||||
"plus_v_rate": "0.00%",
|
||||
"group_rate": "0.00%"
|
||||
},
|
||||
{
|
||||
"user_name": "常琳",
|
||||
"week_amount": 14500.0,
|
||||
"conversion_rate": "3.51%",
|
||||
"plus_v_rate": "54.39%",
|
||||
"group_rate": "49.12%"
|
||||
},
|
||||
{
|
||||
"user_name": "王娟娟",
|
||||
"week_amount": 600.0,
|
||||
"conversion_rate": "0.00%",
|
||||
"plus_v_rate": "3.08%",
|
||||
"group_rate": "0.00%"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
// 成员详细数据
|
||||
const memberDetails = ref({})
|
||||
|
||||
|
||||
// 当前选中的成员,默认为第一名
|
||||
@@ -269,11 +356,13 @@ const selectMember = (member) => {
|
||||
selectedMember.value = member;
|
||||
};
|
||||
onMounted(async () => {
|
||||
await TeamGetGroupAbnormalResponse()
|
||||
await TeamGetWeekTotalCall()
|
||||
await TeamGetWeekAddCustomerTotal()
|
||||
await TeamGetWeekAddDealTotal()
|
||||
// await TeamGetWeekAddFeeTotal()
|
||||
await TeamGetWeekAddFeeTotal()
|
||||
await TeamGetGroupFunnel()
|
||||
await TeamGetGroupRanking()
|
||||
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user