Files
DJKB/my-vue-app/src/views/secondTop/secondTop.vue
lbw_9527443 353a3f194d feat(GroupComparison): 添加动态组别列表功能并优化显示
- 新增groupList属性支持动态加载组别数据
- 实现根据高级经理选择过滤组别功能
- 移除静态综合分显示并优化滚动条样式
- 添加数据处理逻辑支持嵌套API响应结构
2025-08-15 16:01:35 +08:00

970 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="senior-manager-dashboard">
<!-- Header -->
<header class="dashboard-header">
<div class="header-content">
<div class="logo-section">
<div class="header-text">
<h1>中心组长指挥台</h1>
<p>统筹多组运营优化资源配置驱动业绩增长实现团队协同发展</p>
</div>
<!-- 营期阶段信息 -->
<div class="stage-info" style="margin-left: 100px;">
<span class="stage-label">营期所属阶段</span>
<span class="stage-value">接数据</span>
</div>
<div>
<!-- 用户下拉菜单 -->
<UserDropdown />
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="dashboard-main">
<!-- Top Section - Center Overview and Action Items -->
<div class="top-section">
<!-- Center Performance Overview -->
<CenterOverview :overall-data="overallCenterPerformance" />
<!-- Action Items (Compact) -->
<div class="action-items-compact">
<ActionItems :selected-group="selectedGroup" />
</div>
</div>
<div class="BB-section">
<!--客户类型占比-->
<CustomerType :customer-data="customerTypeDistribution" @category-change="handleCustomerTypeChange" />
<!-- 优秀录音 -->
<GoodMusic />
<!-- 客户问题排行 -->
<ProblemRanking :ranking-data="formattedUrgentNeedData" />
</div>
<!-- Bottom Section -->
<div class="bottom-section">
<!-- Left Section - Group Performance Ranking -->
<div class="left-section">
<GroupRanking :groups="groups" :selected-group="selectedGroup" @select-group="selectGroup" />
</div>
<!-- Right Section - Group Comparison -->
<div class="right-section">
<GroupComparison :groups="groups" :senior-manager-data="seniorManagerList" :group-list="groupList" @select-group="selectGroup" @manager-change="handleManagerChange" />
</div>
</div>
<!-- Team Members Detail Section -->
<div class="team-detail-section" v-if="selectedGroup">
<div class="team-detail-header">
<h2>{{ selectedGroup.name }} - 团队成员详情</h2>
<div class="team-summary">
<div class="summary-item">
<span class="label">组长:</span>
<span class="value">{{ selectedGroup.leader }}</span>
</div>
<div class="summary-item">
<span class="label">成员数:</span>
<span class="value">{{ selectedGroup.memberCount }}</span>
</div>
<div class="summary-item">
<span class="label">今日业绩:</span>
<span class="value">{{ formatCurrency(selectedGroup.todayPerformance) }}</span>
</div>
<div class="summary-item">
<span class="label">转化率:</span>
<span class="value">{{ selectedGroup.conversionRate }}%</span>
</div>
</div>
</div>
<div class="members-grid">
<div v-for="member in selectedGroup.members" :key="member.id" class="member-card"
:class="getStatusClass(member.status)">
<div class="member-header">
<div class="member-info">
<h3 class="member-name">{{ member.name }}</h3>
<p class="member-position">{{ member.position }}</p>
<p class="member-phone">{{ member.phone }}</p>
</div>
<div class="member-status">
<span class="status-badge" :class="member.status">{{ getStatusText(member.status) }}</span>
<span class="join-date">入职: {{ member.joinDate }}</span>
</div>
</div>
<div class="member-metrics">
<div class="metric-row">
<div class="metric-item">
<span class="metric-label">今日业绩</span>
<span class="metric-value">{{ formatCurrency(member.todayPerformance) }}</span>
</div>
<div class="metric-item">
<span class="metric-label">月度业绩</span>
<span class="metric-value">{{ formatCurrency(member.monthlyPerformance) }}</span>
</div>
</div>
<div class="metric-row">
<div class="metric-item">
<span class="metric-label">转化率</span>
<span class="metric-value">{{ member.conversionRate }}%</span>
</div>
<div class="metric-item">
<span class="metric-label">通话次数</span>
<span class="metric-value">{{ member.callCount }}</span>
</div>
</div>
<div class="metric-row">
<div class="metric-item">
<span class="metric-label">新增客户</span>
<span class="metric-value">{{ member.newClients }}</span>
</div>
<div class="metric-item">
<span class="metric-label">成交订单</span>
<span class="metric-value">{{ member.deals }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import CenterOverview from './components/CenterOverview.vue'
import GroupComparison from './components/GroupComparison.vue'
import GroupRanking from './components/GroupRanking.vue'
import ActionItems from './components/ActionItems.vue'
import CustomerDetail from './components/CustomerDetail.vue'
import CustomerType from './components/CustomerType.vue'
import GoodMusic from './components/GoodMusic.vue'
import ProblemRanking from './components/ProblemRanking.vue'
import seniorManager from './components/seniorManager.vue'
import UserDropdown from '@/components/UserDropdown.vue'
import {
getOverallCenterPerformance, getTotalGroupCount, getCenterConversionRate, getTotalCallCount, getNewCustomer
, getDepositConversionRate, getCustomerTypeDistribution, getUrgentNeedToAddress, getCenterAdvancedManagerList, getTeamRanking, getTeamRankingInfo
} from '@/api/secondTop.js'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user.js'
// 组别数据
const groups = ref([])
// 路由实例
const router = useRouter();
// 用户store实例
const userStore = useUserStore();
// 获取通用请求参数的函数
const getRequestParams = () => {
const params = {}
// 只从路由参数获取
const routeUserLevel = router.currentRoute.value.query.user_level || router.currentRoute.value.params.user_level
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
// 如果路由有参数,使用路由参数
if (routeUserLevel) {
params.user_level = routeUserLevel.toString()
}
if (routeUserName) {
params.user_name = routeUserName
}
return params
}
// 中心整体概览
const overallCenterPerformance = ref({
CenterPerformance: {},
TotalGroupCount: {},
CenterConversionRate: {},
TotalCallCount: {},
NewCustomer: {},
DepositConversionRate: {}
})
// 客户类型
const customerTypeDistribution = ref({})
// 迫切解决的问题
const urgentNeedToAddress = ref({})
// 格式化迫切解决问题数据为组件所需格式
const formattedUrgentNeedData = computed(() => {
if (urgentNeedToAddress.value && urgentNeedToAddress.value.center_urgent_issue_ratio) {
return Object.entries(urgentNeedToAddress.value.center_urgent_issue_ratio).map(([name, value]) => ({
name,
value
}))
}
return []
})
// 中心总业绩
async function CenterOverallCenterPerformance() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const res = await getOverallCenterPerformance(hasParams ? params : undefined)
if (res.code === 200) {
overallCenterPerformance.value.CenterPerformance = res.data
}
} catch (error) {
console.error('获取中心整体概览失败:', error)
}
}
// 活跃组数
async function CenterTotalGroupCount() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const res = await getTotalGroupCount(hasParams ? params : undefined)
if (res.code === 200) {
overallCenterPerformance.value.TotalGroupCount = res.data
}
} catch (error) {
console.error('获取中心整体概览失败:', error)
}
}
// 中心转化率
async function CenterConversionRate() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const res = await getCenterConversionRate(hasParams ? params : undefined)
if (res.code === 200) {
overallCenterPerformance.value.CenterConversionRate = res.data
}
} catch (error) {
console.error('获取中心整体概览失败:', error)
}
}
// 中心通话次数
async function CenterTotalCallCount() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const res = await getTotalCallCount(hasParams ? params : undefined)
if (res.code === 200) {
overallCenterPerformance.value.TotalCallCount = res.data
}
} catch (error) {
console.error('获取中心整体概览失败:', error)
}
}
// 新增客户
async function CenterNewCustomer() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const res = await getNewCustomer(hasParams ? params : undefined)
if (res.code === 200) {
overallCenterPerformance.value.NewCustomer = res.data
}
} catch (error) {
console.error('获取中心整体概览失败:', error)
}
}
// 定金转化率
async function CenterDepositConversionRate() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const res = await getDepositConversionRate(hasParams ? params : undefined)
if (res.code === 200) {
overallCenterPerformance.value.DepositConversionRate = res.data
}
} catch (error) {
console.error('获取中心整体概览失败:', error)
}
}
// 客户类型
async function CenterCustomerType(distributionType = 'occupation') {
const params = getRequestParams()
const hasParams = params.user_name
// 添加distribution_type参数
const requestParams = hasParams ? { ...params, distribution_type: distributionType } : { distribution_type: distributionType }
try {
const res = await getCustomerTypeDistribution(requestParams)
if (res.code === 200) {
customerTypeDistribution.value = res.data
}
} catch (error) {
console.error('获取客户类型分布失败:', error)
}
}
// 处理客户类型选择变化
const handleCustomerTypeChange = (distributionType) => {
CenterCustomerType(distributionType)
}
// 客户迫切解决的问题
async function CenterUrgentNeedToAddress() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const res = await getUrgentNeedToAddress(hasParams ? params : undefined)
if (res.code === 200) {
urgentNeedToAddress.value = res.data
}
} catch (error) {
console.error('获取中心整体概览失败:', error)
}
}
// 综合排名---高级经理列表
const seniorManagerList = ref([])
async function CenterSeniorManagerList() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const res = await getCenterAdvancedManagerList(hasParams ? params : undefined)
if (res.code === 200) {
seniorManagerList.value = res.data
}
} catch (error) {
console.error('获取中心整体概览失败:', error)
}
}
// 综合排名---组别列表
const groupList = ref([])
async function CenterGroupList(selectedManagerId = 'all') {
const params = getRequestParams()
const hasParams = params.user_name
// 根据选择的经理构建请求参数
let requestParams = hasParams ? { ...params } : {}
if (selectedManagerId === 'all') {
requestParams.get_all = "true"
} else {
// 根据manager id找到对应的名字
const selectedManager = seniorManagerList.value?.center_advanced_managers?.find((name, index) => `manager_${index}` === selectedManagerId)
console.log('Found Manager:', selectedManager)
if (selectedManager) {
requestParams.team_leader_name = selectedManager
}
}
try {
const res = await getTeamRanking(requestParams)
console.log('API Response:', res)
if (res.code === 200) {
groupList.value = res.data
console.log('Updated groupList:', groupList.value)
}
} catch (error) {
console.error('获取团队排名失败:', error)
}
}
// 当前选中的组别,默认为第一个
const selectedGroup = ref(groups[0])
// 选择组别函数
const selectGroup = (group) => {
selectedGroup.value = group
}
// 处理高级经理选择变化
const handleManagerChange = (selectedManagerId) => {
CenterGroupList(selectedManagerId)
}
// 格式化货币
const formatCurrency = (value) => {
if (value >= 10000) {
return (value / 10000).toFixed(1) + '万'
}
return value.toLocaleString()
}
// 获取状态样式类
const getStatusClass = (status) => {
return `status-${status}`
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
excellent: '优秀',
good: '良好',
average: '一般',
attention: '需关注',
poor: '待提升'
}
return statusMap[status] || '未知'
}
onMounted(async () => {
await CenterOverallCenterPerformance()
await CenterTotalGroupCount()
await CenterConversionRate()
await CenterTotalCallCount()
await CenterNewCustomer()
await CenterDepositConversionRate()
await CenterCustomerType()
await CenterUrgentNeedToAddress()
await CenterSeniorManagerList()
await CenterGroupList('all') // 初始化加载全部高级经理数据
})
</script>
<style lang="scss" scoped>
@import '@/assets/styles/main.scss';
.senior-manager-dashboard {
min-height: 100vh;
background-color: #f8fafc;
}
.dashboard-header {
background: white;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e2e8f0;
.header-content {
max-width: 1400px;
margin: 0 auto;
}
.logo-section {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.header-text {
h1 {
font-size: 1.6rem;
font-weight: bold;
color: #1e293b;
margin: 0 0 0.25rem 0;
}
p {
color: #64748b;
margin: 0;
font-size: 0.95rem;
}
}
.stage-info {
display: flex;
align-items: center;
justify-content: center;
.stage-label {
color: #64748b;
font-size: 0.9rem;
font-weight: 500;
}
.stage-value {
color: #3b82f6;
font-size: 0.9rem;
font-weight: 600;
background: #eff6ff;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
margin-left: 0.5rem;
}
}
}
.dashboard-main {
max-width: 1400px;
margin: 0 auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.top-section {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1rem;
}
.bottom-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-top: 1rem;
}
.left-section,
.right-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.right-section {
max-height: 600px;
overflow: auto;
}
.action-items-compact {
height: 380px;
overflow: hidden;
:deep(.action-items) {
height: 100%;
padding: 1rem;
.actions-header {
margin-bottom: 1rem;
h2 {
font-size: 1rem;
}
.header-controls {
gap: 0.5rem;
.priority-filter {
padding: 0.4rem;
font-size: 0.8rem;
}
.add-btn {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
}
}
.actions-summary {
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
.summary-item {
padding: 0.5rem;
.summary-count {
font-size: 1.2rem;
}
.summary-label {
font-size: 0.7rem;
}
}
}
.actions-list {
max-height: 230px;
overflow-y: auto;
.action-item {
padding: 0.75rem;
margin-bottom: 0.5rem;
.action-content {
.action-header {
margin-bottom: 0.25rem;
.action-title {
font-size: 0.9rem;
}
.action-meta {
gap: 0.25rem;
.priority-badge {
padding: 0.2rem 0.4rem;
font-size: 0.7rem;
}
.due-date {
font-size: 0.7rem;
}
}
}
.action-description {
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
.action-details {
margin-bottom: 0.5rem;
.detail-item {
font-size: 0.75rem;
}
}
.action-footer {
.action-buttons {
.btn-edit,
.btn-delete {
padding: 0.25rem 0.5rem;
font-size: 0.7rem;
}
}
}
}
}
}
}
.customer-detail-section {
padding: 1rem;
margin-top: 0.75rem;
}
}
// BB-section 布局
.BB-section {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
margin-top: 1rem;
align-items: stretch;
>* {
height: 100%;
min-height: 400px;
}
}
// 团队成员详情区域
.team-detail-section {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-top: 1rem;
.team-detail-header {
margin-bottom: 2rem;
h2 {
font-size: 1.4rem;
font-weight: 600;
color: #1e293b;
margin: 0 0 1rem 0;
}
.team-summary {
display: flex;
gap: 2rem;
flex-wrap: wrap;
.summary-item {
display: flex;
align-items: center;
gap: 0.5rem;
.label {
font-size: 0.9rem;
color: #64748b;
font-weight: 500;
}
.value {
font-size: 0.9rem;
color: #1e293b;
font-weight: 600;
}
}
}
}
.members-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1rem;
.member-card {
background: #f8fafc;
border-radius: 10px;
padding: 1rem;
border: 1px solid #e2e8f0;
transition: all 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&.status-excellent {
border-left: 4px solid #10b981;
background: linear-gradient(135deg, #ecfdf5 0%, #f0fdf4 100%);
}
&.status-good {
border-left: 4px solid #3b82f6;
background: linear-gradient(135deg, #eff6ff 0%, #f0f9ff 100%);
}
&.status-average {
border-left: 4px solid #f59e0b;
background: linear-gradient(135deg, #fffbeb 0%, #fefce8 100%);
}
&.status-attention {
border-left: 4px solid #f97316;
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
}
&.status-poor {
border-left: 4px solid #ef4444;
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
}
.member-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
.member-info {
flex: 1;
.member-name {
font-size: 1.1rem;
font-weight: 600;
color: #1e293b;
margin: 0 0 0.25rem 0;
}
.member-position {
font-size: 0.85rem;
color: #64748b;
margin: 0 0 0.25rem 0;
}
.member-phone {
font-size: 0.8rem;
color: #94a3b8;
margin: 0;
}
}
.member-status {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
.status-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
&.excellent {
background: #10b981;
color: white;
}
&.good {
background: #3b82f6;
color: white;
}
&.average {
background: #f59e0b;
color: white;
}
&.attention {
background: #f97316;
color: white;
}
&.poor {
background: #ef4444;
color: white;
}
}
.join-date {
font-size: 0.75rem;
color: #94a3b8;
}
}
}
.member-metrics {
.metric-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 0.75rem;
&:last-child {
margin-bottom: 0;
}
.metric-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
.metric-label {
font-size: 0.75rem;
color: #64748b;
font-weight: 500;
}
.metric-value {
font-size: 0.9rem;
color: #1e293b;
font-weight: 600;
}
}
}
}
}
}
}
// 客户详情区域
.customer-detail-section {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-top: 1rem;
}
// 移动端适配
@media (max-width: 768px) {
.dashboard-header {
padding: 1rem;
.header-text {
h1 {
font-size: 1.3rem;
}
p {
font-size: 0.85rem;
}
}
}
.dashboard-main {
padding: 0.5rem;
gap: 0.75rem;
}
.top-section {
grid-template-columns: 1fr;
}
.bottom-section {
grid-template-columns: 1fr;
}
.BB-section {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.right-section {
height: auto;
}
.action-items-compact {
height: 300px;
:deep(.action-items) {
.actions-summary {
grid-template-columns: repeat(4, 1fr);
.summary-item {
padding: 0.4rem;
.summary-count {
font-size: 1rem;
}
.summary-label {
font-size: 0.6rem;
}
}
}
.actions-list {
max-height: 150px;
}
}
}
.team-detail-section {
padding: 1rem;
margin-top: 0.75rem;
.team-detail-header {
margin-bottom: 1.5rem;
h2 {
font-size: 1.2rem;
}
.team-summary {
gap: 1rem;
.summary-item {
.label,
.value {
font-size: 0.8rem;
}
}
}
}
.members-grid {
grid-template-columns: 1fr;
gap: 0.75rem;
.member-card {
padding: 0.75rem;
.member-header {
flex-direction: column;
gap: 0.75rem;
.member-status {
align-items: flex-start;
flex-direction: row;
gap: 0.5rem;
}
}
.member-metrics {
.metric-row {
grid-template-columns: 1fr;
gap: 0.5rem;
margin-bottom: 0.5rem;
.metric-item {
.metric-label {
font-size: 0.7rem;
}
.metric-value {
font-size: 0.8rem;
}
}
}
}
}
}
}
}
</style>