feat: 添加加载组件并优化团队详情页加载体验

- 新增Loading组件用于全局加载状态显示
- 在团队详情页添加加载状态提示
- 优化API请求时的加载状态管理
- 更新axios基础URL配置
This commit is contained in:
2025-08-13 10:47:18 +08:00
parent d6a4ff3843
commit 233b7311fe
3 changed files with 277 additions and 103 deletions

View File

@@ -0,0 +1,107 @@
<template>
<div class="loading-overlay" v-if="visible">
<div class="loading-container">
<div class="loading-spinner">
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
</div>
<div class="loading-text">{{ text }}</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
visible: {
type: Boolean,
default: false
},
text: {
type: String,
default: '加载中...'
}
})
</script>
<style lang="scss" scoped>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(2px);
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.loading-spinner {
position: relative;
width: 60px;
height: 60px;
}
.spinner-ring {
position: absolute;
width: 100%;
height: 100%;
border: 3px solid transparent;
border-top: 3px solid #3b82f6;
border-radius: 50%;
animation: spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
.spinner-ring:nth-child(1) {
animation-delay: -0.45s;
}
.spinner-ring:nth-child(2) {
animation-delay: -0.3s;
}
.spinner-ring:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 1rem;
color: #64748b;
font-weight: 500;
text-align: center;
}
/* 响应式设计 */
@media (max-width: 768px) {
.loading-spinner {
width: 50px;
height: 50px;
}
.loading-text {
font-size: 0.9rem;
}
}
</style>

View File

@@ -5,7 +5,7 @@ import { useUserStore } from '@/stores/user'
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
baseURL: 'http://192.168.15.53:8890' || '', // API基础路径支持完整URL baseURL: 'http://192.168.15.51:8890' || '', // API基础路径支持完整URL
timeout: 30000, // 请求超时时间 timeout: 30000, // 请求超时时间
headers: { headers: {
'Content-Type': 'application/json;charset=UTF-8' 'Content-Type': 'application/json;charset=UTF-8'

View File

@@ -55,72 +55,81 @@
</div> </div>
<!-- Team Members Detail Section --> <!-- Team Members Detail Section -->
<div class="team-detail-section" v-if="selectedGroup"> <div class="team-detail-section" v-if="selectedGroup">
<div class="team-detail-header"> <!-- 团队详情加载状态 -->
<h2>{{ selectedGroup.name }} - 团队成员详情</h2> <div v-if="isTeamDetailLoading" class="team-loading">
<div class="team-summary"> <div class="loading-spinner"></div>
<div class="summary-item"> <div class="loading-text">正在加载团队详情...</div>
<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 }}</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>
<div class="members-grid"> <!-- 团队详情内容 -->
<div <div v-else>
v-for="member in teamPerformanceDetail.group_details" <div class="team-detail-header">
:key="member.id" <h2>{{ selectedGroup.name }} - 团队成员详情</h2>
class="member-card" <div class="team-summary">
> <div class="summary-item">
<div class="member-header"> <span class="label">组长:</span>
<div class="member-info"> <span class="value">{{ selectedGroup.leader }}</span>
<h3 class="member-name">{{ member.name }}</h3>
</div> </div>
</div> <div class="summary-item">
<div class="member-metrics"> <span class="label">成员数:</span>
<div class="metric-row"> <span class="value">{{ teamPerformanceDetail.group_details?.length || 0 }}</span>
<div class="metric-item"> </div>
<span class="metric-label">今日业绩</span> <div class="summary-item">
<span class="metric-value">{{ member.today_performance }}</span> <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>
<div class="metric-item"> <div class="metric-item">
<span class="metric-label">月度业绩</span> <span class="metric-label">月度业绩</span>
<span class="metric-value">{{ member.monthly_performance }}</span> <span class="metric-value">{{ member.monthly_performance }}</span>
</div>
</div> </div>
</div>
<div class="metric-row">
<div class="metric-row"> <div class="metric-item">
<div class="metric-item"> <span class="metric-label">转化率</span>
<span class="metric-label">转化率</span> <span class="metric-value">{{ member.conversion_rate_this_period }}</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>
<div class="metric-item">
<span class="metric-label">通话次数</span> <div class="metric-row">
<span class="metric-value">{{ member.call_count_this_period }}</span> <div class="metric-item">
</div> <span class="metric-label">新增客户</span>
</div> <span class="metric-value">{{ member.new_customers_this_period }}</span>
</div>
<div class="metric-row"> <div class="metric-item">
<div class="metric-item"> <span class="metric-label">成交订单</span>
<span class="metric-label">新增客户</span> <span class="metric-value">{{ member.deals_this_period }}</span>
<span class="metric-value">{{ member.new_customers_this_period }}</span> </div>
</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>
@@ -131,6 +140,9 @@
</div> </div>
</main> </main>
<!-- Loading组件 -->
<Loading :visible="isLoading" text="正在加载数据..." />
</div> </div>
</template> </template>
@@ -144,6 +156,7 @@ import TeamAlerts from '../maneger/components/TeamAlerts.vue'
import ProblemRanking from './components/ProblemRanking.vue' import ProblemRanking from './components/ProblemRanking.vue'
import StatisticalIndicators from './components/StatisticalIndicators.vue' import StatisticalIndicators from './components/StatisticalIndicators.vue'
import UserDropdown from '@/components/UserDropdown.vue' import UserDropdown from '@/components/UserDropdown.vue'
import Loading from '@/components/Loading.vue'
import { getOverallTeamPerformance,getTotalGroupCount,getConversionRate,getTotalCallCount, import { getOverallTeamPerformance,getTotalGroupCount,getConversionRate,getTotalCallCount,
getNewCustomer,getDepositConversionRate,getActiveCustomerCommunicationRate,getAverageAnswerTime, getNewCustomer,getDepositConversionRate,getActiveCustomerCommunicationRate,getAverageAnswerTime,
getTimeoutRate,getTableFillingRate,getUrgentNeedToAddress,getTeamRanking,getTeamRankingInfo } from '@/api/senorManger.js' getTimeoutRate,getTableFillingRate,getUrgentNeedToAddress,getTeamRanking,getTeamRankingInfo } from '@/api/senorManger.js'
@@ -158,6 +171,11 @@ const formCompletionRate = ref(90)
const userStore = useUserStore() const userStore = useUserStore()
// Loading状态
const isLoading = ref(false)
// 团队详情加载状态
const isTeamDetailLoading = ref(false)
// 整体概览 // 整体概览
const overallTeamPerformance = ref({ const overallTeamPerformance = ref({
totalPerformance: {}, totalPerformance: {},
@@ -331,7 +349,39 @@ async function fetchUrgentNeedToAddress() {
"少玩手机": "0.00%", "少玩手机": "0.00%",
"回归学校": "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": { "urgent_issue_consultations": {
"成绩提升": 0, "成绩提升": 0,
"少玩手机": 0, "少玩手机": 0,
@@ -349,7 +399,6 @@ async function fetchUrgentNeedToAddress() {
const teamRanking = ref({}) const teamRanking = ref({})
async function fetchTeamRanking() { async function fetchTeamRanking() {
console.log(555555,userStore.userInfo)
const params={ const params={
user_name: userStore.userInfo.username, user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString(), user_level: userStore.userInfo.user_level.toString(),
@@ -357,31 +406,6 @@ async function fetchTeamRanking() {
try { try {
const response = await getTeamRanking(params) const response = await getTeamRanking(params)
teamRanking.value = response.data teamRanking.value = response.data
/**
* "data": {
"user_name": "陈盼良",
"user_level": 3,
"formal_plural": {
"巅峰五部-时永帅": 2,
"技术部": 0,
"星火一部--张瑾": 4,
"美团业务支持部": 0,
"星耀三部-周毅": 3,
"亮剑二部-田贵星": 2,
"巅峰三部--刘东洋": 0
},
"composition_transformation": {
"巅峰五部-时永帅": "0.00%",
"技术部": "0.00%",
"星火一部--张瑾": "11.43%",
"美团业务支持部": "0.00%",
"星耀三部-周毅": "15.00%",
"亮剑二部-田贵星": "5.71%",
"巅峰三部--刘东洋": "0.00%"
}
}
*/
console.log('团队业绩排名:', response.data)
} catch (error) { } catch (error) {
console.error('获取团队业绩排名失败:', error) console.error('获取团队业绩排名失败:', error)
} }
@@ -399,26 +423,31 @@ async function fetchTeamPerformanceDetail(department) {
teamPerformanceDetail.value = {} teamPerformanceDetail.value = {}
const response = await getTeamRankingInfo(params) const response = await getTeamRankingInfo(params)
teamPerformanceDetail.value = response.data teamPerformanceDetail.value = response.data
console.log('团队业绩详情:', response.data)
} catch (error) { } catch (error) {
console.error('获取团队业绩详情失败:', error) console.error('获取团队业绩详情失败:', error)
} }
} }
// 初始化时获取数据 // 初始化时获取数据
onMounted(async ()=>{ onMounted(async ()=>{
// await fetchOverallTeamPerformance() try {
// await fetchActiveGroups() isLoading.value = true
// await fetchConversionRate() await fetchOverallTeamPerformance()
// await fetchTotalCallCount() await fetchActiveGroups()
// await fetchNewCustomers() await fetchConversionRate()
// await fetchDepositConversions() await fetchTotalCallCount()
// await fetchCustomerCommunicationRate() await fetchNewCustomers()
// await fetchAverageResponseTime() await fetchDepositConversions()
// await fetchTimeoutRate() await fetchCustomerCommunicationRate()
// await fetchTableFillingRate() await fetchAverageResponseTime()
await fetchUrgentNeedToAddress() await fetchTimeoutRate()
await fetchTeamRanking() await fetchTableFillingRate()
await fetchUrgentNeedToAddress()
await fetchTeamRanking()
} catch (error) {
console.error('数据加载失败:', error)
} finally {
isLoading.value = false
}
}) })
// 组别数据 // 组别数据
@@ -427,7 +456,7 @@ const groups=[]
const selectedGroup = ref(groups[0]) const selectedGroup = ref(groups[0])
// 选择组别函数 // 选择组别函数
const selectGroup = (group) => { const selectGroup = async (group) => {
selectedGroup.value = group selectedGroup.value = group
// 获取部门名称并调用团队业绩详情接口 // 获取部门名称并调用团队业绩详情接口
// 从teamRanking数据中查找对应的原始部门名称 // 从teamRanking数据中查找对应的原始部门名称
@@ -445,7 +474,16 @@ const selectGroup = (group) => {
} }
} }
console.log('选中的部门:', group.name, '-> 发送的部门名称:', department) console.log('选中的部门:', group.name, '-> 发送的部门名称:', department)
fetchTeamPerformanceDetail(department)
// 设置团队详情加载状态
isTeamDetailLoading.value = true
try {
await fetchTeamPerformanceDetail(department)
} catch (error) {
console.error('获取团队详情失败:', error)
} finally {
isTeamDetailLoading.value = false
}
} }
// 格式化货币 // 格式化货币
@@ -860,6 +898,35 @@ const getStatusText = (status) => {
margin-top: 1rem; 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) { @media (max-width: 768px) {
.dashboard-header { .dashboard-header {