Files
DJKB/my-vue-app/src/views/senorManger/seniorManager.vue
lbw_9527443 233b7311fe feat: 添加加载组件并优化团队详情页加载体验
- 新增Loading组件用于全局加载状态显示
- 在团队详情页添加加载状态提示
- 优化API请求时的加载状态管理
- 更新axios基础URL配置
2025-08-13 10:47:18 +08:00

1051 lines
26 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>
<UserDropdown class="header-ringht" style="margin-left: auto;" />
</div>
</div>
</header>
<!-- Main Content -->
<main class="dashboard-main">
<div class="top-section">
<CenterOverview
style="height: 330px;"
:overallTeamPerformance="overallTeamPerformance"
/>
<div class="action-items-compact">
<TeamAlerts style="height: 300px;" />
</div>
</div>
<StatisticalIndicators
:customerCommunicationRate="statisticalIndicators.customerCommunicationRate"
:averageResponseTime="statisticalIndicators.averageResponseTime"
:timeoutResponseRate="statisticalIndicators.timeoutResponseRate"
:severeTimeoutRate="statisticalIndicators.severeTimeoutRate"
:formCompletionRate="statisticalIndicators.formCompletionRate"
/>
<!-- 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>
<div class="problem-ranking">
<!-- 客户迫切解决的问题 -->
<ProblemRanking :problemRanking="problemRanking" />
</div>
<!-- Right Section - Group Comparison -->
<div class="right-section">
<GroupComparison
:groups="groups"
:teamRanking="teamRanking"
@select-group="selectGroup"
/>
</div>
</div>
<!-- Team Members Detail Section -->
<div class="team-detail-section" v-if="selectedGroup">
<!-- 团队详情加载状态 -->
<div v-if="isTeamDetailLoading" class="team-loading">
<div class="loading-spinner"></div>
<div class="loading-text">正在加载团队详情...</div>
</div>
<!-- 团队详情内容 -->
<div v-else>
<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">{{ teamPerformanceDetail.group_details?.length || 0 }}</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 teamPerformanceDetail.group_details"
:key="member.id"
class="member-card"
>
<div class="member-header">
<div class="member-info">
<h3 class="member-name">{{ member.name }}</h3>
</div>
</div>
<div class="member-metrics">
<div class="metric-row">
<div class="metric-item">
<span class="metric-label">今日业绩</span>
<span class="metric-value">{{ member.today_performance }}</span>
</div>
<div class="metric-item">
<span class="metric-label">月度业绩</span>
<span class="metric-value">{{ member.monthly_performance }}</span>
</div>
</div>
<div class="metric-row">
<div class="metric-item">
<span class="metric-label">转化率</span>
<span class="metric-value">{{ member.conversion_rate_this_period }}</span>
</div>
<div class="metric-item">
<span class="metric-label">通话次数</span>
<span class="metric-value">{{ member.call_count_this_period }}</span>
</div>
</div>
<div class="metric-row">
<div class="metric-item">
<span class="metric-label">新增客户</span>
<span class="metric-value">{{ member.new_customers_this_period }}</span>
</div>
<div class="metric-item">
<span class="metric-label">成交订单</span>
<span class="metric-value">{{ member.deals_this_period }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
</div>
</main>
<!-- Loading组件 -->
<Loading :visible="isLoading" text="正在加载数据..." />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import CenterOverview from './components/CenterOverview.vue'
import GroupComparison from './components/GroupComparison.vue'
import GroupRanking from './components/GroupRanking.vue'
import TeamAlerts from '../maneger/components/TeamAlerts.vue'
import ProblemRanking from './components/ProblemRanking.vue'
import StatisticalIndicators from './components/StatisticalIndicators.vue'
import UserDropdown from '@/components/UserDropdown.vue'
import Loading from '@/components/Loading.vue'
import { getOverallTeamPerformance,getTotalGroupCount,getConversionRate,getTotalCallCount,
getNewCustomer,getDepositConversionRate,getActiveCustomerCommunicationRate,getAverageAnswerTime,
getTimeoutRate,getTableFillingRate,getUrgentNeedToAddress,getTeamRanking,getTeamRankingInfo } from '@/api/senorManger.js'
import { useUserStore } from '@/stores/user.js'
const customerCommunicationRate = ref(85)
const averageResponseTime = ref(15)
const timeoutResponseRate = ref(5)
const severeTimeoutRate = ref(2)
const formCompletionRate = ref(90)
const userStore = useUserStore()
// Loading状态
const isLoading = ref(false)
// 团队详情加载状态
const isTeamDetailLoading = ref(false)
// 整体概览
const overallTeamPerformance = ref({
totalPerformance: {},
activeGroups: {},
conversionRate: {},
totalCalls: {},
newCustomers: {},
depositConversions: {},
})
// 获取整体概览数据--团队总业绩
async function fetchOverallTeamPerformance() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
// 团队总业绩
try {
const response = await getOverallTeamPerformance(params)
overallTeamPerformance.value.totalPerformance = response.data
} catch (error) {
console.error('获取整体概览数据失败:', error)
}
}
// 获取整体概览数据--活跃组数
async function fetchActiveGroups() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
try {
const response = await getTotalGroupCount(params)
overallTeamPerformance.value.activeGroups = response.data
console.log('活跃组数:', response.data)
} catch (error) {
console.error('获取活跃组数失败:', error)
}
}
// 获取整体概览数据--团队转化率
async function fetchConversionRate() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
try {
const response = await getConversionRate(params)
overallTeamPerformance.value.conversionRate = response.data
} catch (error) {
console.error('获取团队转化率失败:', error)
}
}
// 通话次数
async function fetchTotalCallCount() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
try {
const response = await getTotalCallCount(params)
overallTeamPerformance.value.totalCalls = response.data
} catch (error) {
console.error('获取通话次数失败:', error)
}
}
// 新增客户
async function fetchNewCustomers() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
try {
const response = await getNewCustomer(params)
overallTeamPerformance.value.newCustomers = response.data
} catch (error) {
console.error('获取新增客户失败:', error)
}
}
// 定金转化
async function fetchDepositConversions() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
try {
const response = await getDepositConversionRate(params)
overallTeamPerformance.value.depositConversions = response.data
console.log(99888999,response.data)
} catch (error) {
console.error('获取定金转化失败:', error)
}
}
const statisticalIndicators = ref({
customerCommunicationRate: 0,
averageResponseTime: 0,
timeoutResponseRate: 0,
severeTimeoutRate: 0,
formCompletionRate: 0,
})
// 统计指标--活跃客户沟通率
async function fetchCustomerCommunicationRate() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
try {
const response = await getActiveCustomerCommunicationRate(params)
statisticalIndicators.value.customerCommunicationRate = response.data
} catch (error) {
console.error('获取活跃客户沟通率失败:', error)
}
}
// 统计指标--平均应答时间
async function fetchAverageResponseTime() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
try {
const response = await getAverageAnswerTime(params)
statisticalIndicators.value.averageResponseTime = response.data
} catch (error) {
console.error('获取平均应答时间失败:', error)
}
}
// 统计指标--超时应答率、严重超时应答率
async function fetchTimeoutRate() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
try {
const response = await getTimeoutRate(params)
statisticalIndicators.value.timeoutResponseRate = response.data
} catch (error) {
console.error('获取超时应答率失败:', error)
}
}
// 统计指标--表格填写率
async function fetchTableFillingRate() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
try {
const response = await getTableFillingRate(params)
statisticalIndicators.value.formCompletionRate = response.data
} catch (error) {
console.error('获取表格填写率失败:', error)
}
}
const problemRanking = ref({})
// 客户迫切解决的问题
async function fetchUrgentNeedToAddress() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString()
}
try {
const response = await getUrgentNeedToAddress(params)
problemRanking.value = response.data
/**
* "data": {
"user_name": "陈盼良",
"user_level": 3,
"calculate_urgent_issue_ratio": {
"成绩提升": "0.00%",
"少玩手机": "0.00%",
"回归学校": "0.00%",
"心理健康": "0.00%"
}
}
}
}
}
// 团队详情加载样式
.team-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
.loading-text {
color: #666;
font-size: 0.9rem;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); },
"urgent_issue_consultations": {
"成绩提升": 0,
"少玩手机": 0,
"回归学校": 0,
"心理健康": 0
}
}
*/
// console.log('客户迫切解决的问题:', response.data)
} catch (error) {
console.error('获取客户迫切解决的问题失败:', error)
}
}
//综合表现排名
const teamRanking = ref({})
async function fetchTeamRanking() {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString(),
}
try {
const response = await getTeamRanking(params)
teamRanking.value = response.data
} catch (error) {
console.error('获取团队业绩排名失败:', error)
}
}
// 团队业绩详情
const teamPerformanceDetail = ref({})
async function fetchTeamPerformanceDetail(department) {
const params={
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString(),
department: department
}
try {
teamPerformanceDetail.value = {}
const response = await getTeamRankingInfo(params)
teamPerformanceDetail.value = response.data
} catch (error) {
console.error('获取团队业绩详情失败:', error)
}
}
// 初始化时获取数据
onMounted(async ()=>{
try {
isLoading.value = true
await fetchOverallTeamPerformance()
await fetchActiveGroups()
await fetchConversionRate()
await fetchTotalCallCount()
await fetchNewCustomers()
await fetchDepositConversions()
await fetchCustomerCommunicationRate()
await fetchAverageResponseTime()
await fetchTimeoutRate()
await fetchTableFillingRate()
await fetchUrgentNeedToAddress()
await fetchTeamRanking()
} catch (error) {
console.error('数据加载失败:', error)
} finally {
isLoading.value = false
}
})
// 组别数据
const groups=[]
// 当前选中的组别,默认为第一个
const selectedGroup = ref(groups[0])
// 选择组别函数
const selectGroup = async (group) => {
selectedGroup.value = group
// 获取部门名称并调用团队业绩详情接口
// 从teamRanking数据中查找对应的原始部门名称
let department = group.name
if (teamRanking.value && teamRanking.value.formal_plural) {
// 在formal_plural中查找匹配的部门名称
const departmentKeys = Object.keys(teamRanking.value.formal_plural)
const matchedDepartment = departmentKeys.find(key => {
// 提取部门名称的主要部分进行匹配
const mainName = key.split('-')[0] || key
return group.name.includes(mainName) || mainName.includes(group.name)
})
if (matchedDepartment) {
department = matchedDepartment
}
}
console.log('选中的部门:', group.name, '-> 发送的部门名称:', department)
// 设置团队详情加载状态
isTeamDetailLoading.value = true
try {
await fetchTeamPerformanceDetail(department)
} catch (error) {
console.error('获取团队详情失败:', error)
} finally {
isTeamDetailLoading.value = false
}
}
// 格式化货币
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] || '未知'
}
</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;
}
}
}
.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 1fr;
gap: 1rem;
margin-top: 1rem;
}
.left-section, .right-section{
display: flex;
flex-direction: column;
gap: 1rem;
}
.right-section {
overflow: auto;
}
.action-items-compact {
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: 200px;
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;
}
}
// 团队成员详情区域
.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;
}
// 团队详情加载状态
.team-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #e2e8f0;
border-top: 3px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
.loading-text {
color: #64748b;
font-size: 0.9rem;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// 移动端适配
@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;
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>