feat(manager): 实现团队管理页面的数据对接和功能优化

- 新增团队异常预警API接口和数据展示
- 完善销售漏斗组件,对接实际数据
- 优化业绩排名组件,支持多种数据源
- 更新成员详情组件,适配新数据结构
- 重构管理页面,整合多个API调用
This commit is contained in:
2025-08-14 12:00:04 +08:00
parent 366d7b6120
commit 70f44d87a3
7 changed files with 272 additions and 51 deletions

View File

@@ -1,5 +1,10 @@
import https from '../utils/https' 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 // 团队总通话 /api/v1/manager/week_total_call
export const getWeekTotalCall = (params) => { export const getWeekTotalCall = (params) => {
return https.post('/api/v1/manager/week_total_call', params) return https.post('/api/v1/manager/week_total_call', params)
@@ -20,7 +25,10 @@ export const getWeekAddDealTotal = (params) => {
export const getWeekAddFeeTotal = (params) => { export const getWeekAddFeeTotal = (params) => {
return https.post('/api/v1/manager/week_add_fee_total', 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 // 团队漏斗 /api/v1/group_funnel/get_group_funnel
export const getGroupFunnel = (params) => { export const getGroupFunnel = (params) => {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="member-details"> <div class="member-details">
<div class="details-header" @click="toggleDetailsCollapse"> <div class="details-header" @click="toggleDetailsCollapse">
<h2>{{ selectedMember.name }} 的详细数据</h2> <h2>{{ selectedMember.user_name || selectedMember.name }} 的详细数据</h2>
<div class="collapse-toggle" :class="{ 'collapsed': isDetailsCollapsed }"> <div class="collapse-toggle" :class="{ 'collapsed': isDetailsCollapsed }">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4l4 4H4l4-4z"/> <path d="M8 4l4 4H4l4-4z"/>
@@ -12,27 +12,27 @@
<div class="details-grid" v-show="!isDetailsCollapsed" :class="{ 'collapsing': isDetailsCollapsed }"> <div class="details-grid" v-show="!isDetailsCollapsed" :class="{ 'collapsing': isDetailsCollapsed }">
<div class="detail-card"> <div class="detail-card">
<div class="detail-label">总通话次数</div> <div class="detail-label">总通话次数</div>
<div class="detail-value">{{ selectedMember.calls }} </div> <div class="detail-value">{{ selectedMember.calls || 0 }} </div>
</div> </div>
<div class="detail-card"> <div class="detail-card">
<div class="detail-label">通话时长</div> <div class="detail-label">通话时长</div>
<div class="detail-value">{{ selectedMember.callTime }} 小时</div> <div class="detail-value">{{ selectedMember.callTime || 0 }} 小时</div>
</div> </div>
<div class="detail-card"> <div class="detail-card">
<div class="detail-label">新增客户</div> <div class="detail-label">新增客户</div>
<div class="detail-value">{{ selectedMember.newClients }} </div> <div class="detail-value">{{ selectedMember.newClients || 0 }} </div>
</div> </div>
<div class="detail-card"> <div class="detail-card">
<div class="detail-label">成交单数</div> <div class="detail-label">成交单数</div>
<div class="detail-value">{{ selectedMember.deals }} </div> <div class="detail-value">{{ selectedMember.deals || 0 }} </div>
</div> </div>
<div class="detail-card"> <div class="detail-card">
<div class="detail-label">总业绩</div> <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>
<div class="detail-card"> <div class="detail-card">
<div class="detail-label">平均单价</div> <div class="detail-label">转化率</div>
<div class="detail-value">¥{{ selectedMember.avgDealValue.toLocaleString() }}</div> <div class="detail-value">{{ selectedMember.conversion_rate || selectedMember.conversion || '0%' }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -66,7 +66,7 @@
<div class="no-guidance" v-else> <div class="no-guidance" v-else>
<div class="celebration-icon">🎉</div> <div class="celebration-icon">🎉</div>
<h4>表现优秀</h4> <h4>表现优秀</h4>
<p>{{ selectedMember.name }} 的各项指标都很不错继续保持这种状态</p> <p>{{ selectedMember.user_name || selectedMember.name }} 的各项指标都很不错继续保持这种状态</p>
</div> </div>
</div> </div>
</div> </div>
@@ -104,7 +104,7 @@
<div class="no-recordings" v-else> <div class="no-recordings" v-else>
<div class="no-data-icon">📞</div> <div class="no-data-icon">📞</div>
<h4>暂无录音</h4> <h4>暂无录音</h4>
<p>{{ selectedMember.name }} 还没有通话录音记录</p> <p>{{ selectedMember.user_name || selectedMember.name }} 还没有通话录音记录</p>
</div> </div>
</div> </div>
</div> </div>
@@ -146,6 +146,14 @@ const toggleRecordingsCollapse = () => {
isRecordingsCollapsed.value = !isRecordingsCollapsed.value isRecordingsCollapsed.value = !isRecordingsCollapsed.value
} }
// 格式化金额
const formatAmount = (amount) => {
if (typeof amount === 'number') {
return amount.toLocaleString()
}
return amount || '0'
}
// 获取成员录音列表 // 获取成员录音列表
const getRecordingsForMember = (member) => { const getRecordingsForMember = (member) => {
// 模拟录音数据实际项目中应该从API获取 // 模拟录音数据实际项目中应该从API获取

View File

@@ -12,18 +12,18 @@
<span>入群率</span> <span>入群率</span>
</div> </div>
<div <div
v-for="member in teamMembers" v-for="(member, index) in displayMembers"
:key="member.id" :key="member.user_name || member.id"
class="table-row" 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)" @click="selectMember(member)"
> >
<span class="rank">{{ member.rank }}</span> <span class="rank">{{ index + 1 }}</span>
<span class="name">{{ member.name }}</span> <span class="name">{{ member.user_name || member.name }}</span>
<span class="performance">¥{{ member.performance.toLocaleString() }}</span> <span class="performance">¥{{ formatAmount(member.week_amount || member.performance) }}</span>
<span class="conversion">{{ member.conversion }}%</span> <span class="conversion">{{ member.conversion_rate || member.conversion || '0%' }}</span>
<span class="wechat-rate">{{ member.wechatRate || 0 }}%</span> <span class="wechat-rate">{{ member.plus_v_rate || member.wechatRate || '0%' }}</span>
<span class="group-rate">{{ member.groupRate || 0 }}%</span> <span class="group-rate">{{ member.group_rate || member.groupRate || '0%' }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -31,23 +31,45 @@
</template> </template>
<script setup> <script setup>
import { defineProps, defineEmits } from 'vue' import { defineProps, defineEmits, computed } from 'vue'
// 定义props // 定义props
const props = defineProps({ const props = defineProps({
teamMembers: { teamMembers: {
type: Array, type: Array,
required: true default: () => []
}, },
selectedMember: { selectedMember: {
type: Object, type: Object,
required: true default: () => null
},
groupRanking: {
type: Object,
default: () => ({})
} }
}) })
// 定义emits // 定义emits
const emit = defineEmits(['select-member']) 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) => { const selectMember = (member) => {
emit('select-member', member) emit('select-member', member)

View File

@@ -5,23 +5,23 @@
<div class="funnel-chart"> <div class="funnel-chart">
<div class="funnel-stage" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);"> <div class="funnel-stage" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<span class="stage-label">线索总数</span> <span class="stage-label">线索总数</span>
<span class="stage-value">1000</span> <span class="stage-value">{{ funnelData.customers_count?.['线索总数'] || 0 }}</span>
</div> </div>
<div class="funnel-stage" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);"> <div class="funnel-stage" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<span class="stage-label">有效沟通</span> <span class="stage-label">有效沟通</span>
<span class="stage-value">450</span> <span class="stage-value">{{ funnelData.customers_count?.['有效沟通'] || 0 }}</span>
</div> </div>
<div class="funnel-stage" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);"> <div class="funnel-stage" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<span class="stage-label">到课数据</span> <span class="stage-label">到课数据</span>
<span class="stage-value">180</span> <span class="stage-value">{{ funnelData.customers_count?.['到课数据'] || 0 }}</span>
</div> </div>
<div class="funnel-stage" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);"> <div class="funnel-stage" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
<span class="stage-label">预付定金</span> <span class="stage-label">预付定金</span>
<span class="stage-value">50</span> <span class="stage-value">{{ funnelData.customers_count?.['预付定金'] || 0 }}</span>
</div> </div>
<div class="funnel-stage" style="background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);"> <div class="funnel-stage" style="background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);">
<span class="stage-label">成功签单</span> <span class="stage-label">成功签单</span>
<span class="stage-value">12</span> <span class="stage-value">{{ funnelData.customers_count?.['成功签单'] || 0 }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -29,6 +29,22 @@
<script setup> <script setup>
// 团队销售漏斗组件 // 团队销售漏斗组件
// 定义props
const props = defineProps({
funnelData: {
type: Object,
default: () => ({
customers_count: {
'线索总数': 0,
'有效沟通': 0,
'到课数据': 0,
'预付定金': 0,
'成功签单': 0
}
})
}
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -2,24 +2,80 @@
<div class="team-alerts"> <div class="team-alerts">
<h2>团队异常预警</h2> <h2>团队异常预警</h2>
<div class="alert-list"> <div class="alert-list">
<div class="alert-item warning"> <div
<span class="alert-icon"></span> v-for="alert in aggregatedAlerts"
<span>钱鑫有102人(预计)需今日跟进通话</span> :key="alert.id"
class="alert-item"
:class="alert.type"
>
<span class="alert-icon">{{ alert.icon }}</span>
<span>{{ alert.message }}</span>
</div> </div>
<div class="alert-item danger"> <div v-if="aggregatedAlerts.length === 0" class="alert-item info">
<span class="alert-icon">🔺</span>
<span>李娜今日预计电话工作量达30%</span>
</div>
<div class="alert-item info">
<span class="alert-icon"></span> <span class="alert-icon"></span>
<span>高明明客户"王先生"下次未来电话记录</span> <span>暂无异常预警</span>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -43,6 +99,27 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; 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 { .alert-item {

View File

@@ -32,17 +32,17 @@
</div> </div>
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
<span class="card-title">总业绩</span> <span class="card-title">月度总业绩</span>
<span class="card-trend positive">+8% vs </span> <span class="card-trend positive">+8% vs </span>
</div> </div>
<div class="card-value">{{ formatCurrency(weekTotalData.week_add_fee_total?.total_add_fee || 0) }} </div> <div class="card-value">{{ formatCurrency(weekTotalData.week_add_fee_total?.total_add_fee || 0) }} </div>
</div> </div>
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
<span class="card-title">定金转化率</span> <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>
<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> </div>
</div> </div>
@@ -60,6 +60,7 @@ const props = defineProps({
week_add_customer_total: {}, week_add_customer_total: {},
week_add_deal_total: {}, week_add_deal_total: {},
week_add_fee_total: {}, week_add_fee_total: {},
pay_deposit_to_money_rate: {}
}) })
} }
}) })

View File

@@ -19,13 +19,13 @@
<!-- Top Section - Team Alerts and Today's Report --> <!-- Top Section - Team Alerts and Today's Report -->
<div class="top-section"> <div class="top-section">
<!-- Team Alerts --> <!-- Team Alerts -->
<TeamAlerts /> <TeamAlerts :abnormalData="groupAbnormalResponse" />
<!-- Today's Team Report --> <!-- Today's Team Report -->
<TeamReport :weekTotalData="weekTotalData" /> <TeamReport :weekTotalData="weekTotalData" />
</div> </div>
<!-- Sales Funnel Section --> <!-- Sales Funnel Section -->
<SalesFunnel /> <SalesFunnel :funnelData="weekTotalData.group_funnel" />
<!-- Bottom Section --> <!-- Bottom Section -->
<div class="bottom-section"> <div class="bottom-section">
@@ -34,6 +34,7 @@
<PerformanceRanking <PerformanceRanking
:team-members="teamMembers" :team-members="teamMembers"
:selected-member="selectedMember" :selected-member="selectedMember"
:group-ranking="groupRanking"
@select-member="selectMember" @select-member="selectMember"
/> />
</div> </div>
@@ -61,9 +62,7 @@ import RawDataCards from "../person/components/RawDataCards.vue";
import CustomerDetail from "../person/components/CustomerDetail.vue"; import CustomerDetail from "../person/components/CustomerDetail.vue";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { useRouter } from "vue-router"; 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 = [ const teamMembers = [
@@ -214,8 +213,21 @@ const weekTotalData = ref({
week_add_customer_total: {}, week_add_customer_total: {},
week_add_deal_total: {}, week_add_deal_total: {},
week_add_fee_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() { async function TeamGetWeekTotalCall() {
const params = getRequestParams() const params = getRequestParams()
@@ -246,9 +258,19 @@ async function TeamGetWeekAddDealTotal() {
weekTotalData.value.week_add_deal_total = res.data 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() { async function TeamGetGroupFunnel() {
const params = getRequestParams() const params = getRequestParams()
@@ -256,9 +278,74 @@ async function TeamGetGroupFunnel() {
const res = await getGroupFunnel(hasParams ? params : undefined) const res = await getGroupFunnel(hasParams ? params : undefined)
console.log(res) console.log(res)
if (res.code === 200) { 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; selectedMember.value = member;
}; };
onMounted(async () => { onMounted(async () => {
await TeamGetGroupAbnormalResponse()
await TeamGetWeekTotalCall() await TeamGetWeekTotalCall()
await TeamGetWeekAddCustomerTotal() await TeamGetWeekAddCustomerTotal()
await TeamGetWeekAddDealTotal() await TeamGetWeekAddDealTotal()
// await TeamGetWeekAddFeeTotal() await TeamGetWeekAddFeeTotal()
await TeamGetGroupFunnel() await TeamGetGroupFunnel()
await TeamGetGroupRanking()
}) })