Files
DJKB/my-vue-app/src/views/senorManger/seniorManager.vue
lbw_9527443 d7c8a9e173 fix(metrics): 更新多个视图中的指标计算方式描述
refactor(header): 重构高级经理指挥台的头部组件,支持路由导航显示
2025-08-26 12:00:02 +08:00

1268 lines
33 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 v-if="isRouteNavigation" class="route-header">
<div class="breadcrumb">
<span class="breadcrumb-item" @click="goBack">团队管理</span>
<span class="breadcrumb-separator">></span>
<span class="breadcrumb-item current">{{ routeUserName }}高级经理指挥台</span>
</div>
<div class="user-name">
{{ routeUserName }}
</div>
</div>
<!-- 自己登录时的顶栏原有样式 -->
<template v-else>
<div class="header-text">
<h1>高级经理指挥台</h1>
<p>统筹多组运营优化资源配置驱动业绩增长实现团队协同发展</p>
</div>
</template>
<div v-if="!isRouteNavigation">
<!-- 用户下拉菜单 -->
<UserDropdown class="header-ringht" style="margin-left: auto;" />
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="dashboard-main">
<div class="top-section">
<CenterOverview
style="height: 330px;"
:overallTeamPerformance="overallTeamPerformance"
@update-check-type="updateCheckType"
/>
<div class="action-items-compact">
<TeamAlerts style="height: 300px;" :abnormalData="teamAlerts" />
</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">{{ 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"
@dblclick="handleMemberDoubleClick(member)"
>
<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_deals }}</span>
</div>
<div class="metric-item">
<span class="metric-label">月度业绩</span>
<span class="metric-value">{{ member.monthly_deals}}</span>
</div>
</div>
<div class="metric-row">
<div class="metric-item">
<span class="metric-label">转化率<i class="info-icon" @mouseenter="showTooltip($event, 'teamPerformance')" @mouseleave="hideTooltip"></i></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>
<!-- Tooltip 组件 -->
<Tooltip
:visible="tooltip.visible"
:x="tooltip.x"
:y="tooltip.y"
:title="tooltip.title"
:description="tooltip.description"
/>
</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>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { computed, reactive } from 'vue'
import Tooltip from '@/components/Tooltip.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,getAbnormalResponseRate } 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 CheckType = ref('month')
// 更新CheckType的方法
const updateCheckType = async (newValue) => {
CheckType.value = newValue
console.log('CheckType已更新为:', newValue)
// 重新获取所有使用CheckType的数据
try {
isLoading.value = true
await fetchOverallTeamPerformance()
await fetchActiveGroups()
await fetchConversionRate()
await fetchTotalCallCount()
await fetchNewCustomers()
await fetchDepositConversions()
console.log('数据已根据新的统计模式重新加载')
} catch (error) {
console.error('重新加载数据失败:', error)
} finally {
isLoading.value = false
}
}
const userStore = useUserStore()
// 路由实例
const router = useRouter()
// 判断是否为路由导航(有路由参数)
const isRouteNavigation = computed(() => {
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
return !!routeUserName
})
// 获取路由传递的用户名
const routeUserName = computed(() => {
return router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name || ''
})
// 返回上一页
const goBack = () => {
router.go(-1)
}
// Loading状态
const isLoading = ref(false)
// 团队详情加载状态
const isTeamDetailLoading = ref(false)
// 整体概览
const overallTeamPerformance = ref({
totalPerformance: {},
activeGroups: {},
conversionRate: {},
totalCalls: {},
newCustomers: {},
depositConversions: {},
})
// 获取通用请求参数的函数
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
}
// 获取整体概览数据--团队总业绩
async function fetchOverallTeamPerformance() {
const params = getRequestParams()
const hasParams = params.user_name
// 团队总业绩
try {
const response = await getOverallTeamPerformance(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
overallTeamPerformance.value.totalPerformance = response.data
} catch (error) {
console.error('获取整体概览数据失败:', error)
}
}
// 获取整体概览数据--活跃组数
async function fetchActiveGroups() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getTotalGroupCount(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
overallTeamPerformance.value.activeGroups = response.data
console.log('活跃组数:', response.data)
} catch (error) {
console.error('获取活跃组数失败:', error)
}
}
// 获取整体概览数据--团队转化率
async function fetchConversionRate() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getConversionRate(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
overallTeamPerformance.value.conversionRate = response.data
} catch (error) {
console.error('获取团队转化率失败:', error)
}
}
// 通话次数
async function fetchTotalCallCount() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getTotalCallCount(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
overallTeamPerformance.value.totalCalls = response.data
} catch (error) {
console.error('获取通话次数失败:', error)
}
}
// 新增客户
async function fetchNewCustomers() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getNewCustomer(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
overallTeamPerformance.value.newCustomers = response.data
} catch (error) {
console.error('获取新增客户失败:', error)
}
}
// 定金转化
async function fetchDepositConversions() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getDepositConversionRate(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
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,
})
// 团队异常
const teamAlerts = ref({})
// 异常预警
async function fetchAbnormalResponseRate() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getAbnormalResponseRate(hasParams ? params : undefined)
const rawData = response.data
// 转换数据格式,按团队分组生成预警消息
const processedAlerts = []
const teamData = new Map()
// 收集严重超时异常数据
if (rawData.team_serious_timeout_abnormal_counts_by_group) {
Object.entries(rawData.team_serious_timeout_abnormal_counts_by_group).forEach(([teamName, data]) => {
if (!teamData.has(teamName)) {
teamData.set(teamName, { timeoutCount: 0, fillingCount: 0 })
}
teamData.get(teamName).timeoutCount = data.count
})
}
// 收集表格填写异常数据
if (rawData.team_table_filling_abnormal_counts_by_group) {
Object.entries(rawData.team_table_filling_abnormal_counts_by_group).forEach(([teamName, data]) => {
if (!teamData.has(teamName)) {
teamData.set(teamName, { timeoutCount: 0, fillingCount: 0 })
}
teamData.get(teamName).fillingCount = data.count
})
}
// 生成按团队分组的预警消息
let alertId = 1
teamData.forEach((counts, teamName) => {
const messages = []
if (counts.timeoutCount > 0) {
messages.push(`${counts.timeoutCount}人严重超时率过高`)
}
if (counts.fillingCount > 0) {
messages.push(`${counts.fillingCount}人表格填写率过低`)
}
if (messages.length > 0) {
processedAlerts.push({
id: alertId++,
type: messages.length > 1 ? 'danger' : 'warning',
icon: messages.length > 1 ? '🔺' : '⚠',
message: `${teamName}团队${messages.join('')}`
})
}
})
// 设置处理后的数据
teamAlerts.value = { processedAlerts }
} catch (error) {
console.error('获取异常预警失败:', error)
}
}
// 统计指标--活跃客户沟通率
async function fetchCustomerCommunicationRate() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getActiveCustomerCommunicationRate(hasParams ? params : undefined)
statisticalIndicators.value.customerCommunicationRate = response.data
} catch (error) {
console.error('获取活跃客户沟通率失败:', error)
}
}
// 统计指标--平均应答时间
async function fetchAverageResponseTime() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getAverageAnswerTime(hasParams ? params : undefined)
statisticalIndicators.value.averageResponseTime = response.data
} catch (error) {
console.error('获取平均应答时间失败:', error)
}
}
// 统计指标--超时应答率、严重超时应答率
async function fetchTimeoutRate() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getTimeoutRate(hasParams ? params : undefined)
statisticalIndicators.value.timeoutResponseRate = response.data
} catch (error) {
console.error('获取超时应答率失败:', error)
}
}
// 统计指标--表格填写率
async function fetchTableFillingRate() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getTableFillingRate(hasParams ? params : undefined)
statisticalIndicators.value.formCompletionRate = response.data
} catch (error) {
console.error('获取表格填写率失败:', error)
}
}
const problemRanking = ref({})
// 客户迫切解决的问题
async function fetchUrgentNeedToAddress() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getUrgentNeedToAddress(hasParams ? params : undefined)
problemRanking.value = response.data
} catch (error) {
console.error('获取客户迫切解决的问题失败:', error)
}
}
//综合表现排名
const teamRanking = ref({})
async function fetchTeamRanking() {
const params = getRequestParams()
const hasParams = params.user_name
try {
const response = await getTeamRanking(hasParams ? params : undefined)
teamRanking.value = response.data
} catch (error) {
console.error('获取团队业绩排名失败:', error)
}
}
// 团队业绩详情
const teamPerformanceDetail = ref({})
async function fetchTeamPerformanceDetail(department) {
const params = getRequestParams()
const hasParams = params.user_name
const requestParams = hasParams ? {
...params,
department: department
} : {
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString(),
department: department
}
try {
teamPerformanceDetail.value = {}
const response = await getTeamRankingInfo(requestParams)
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 fetchAbnormalResponseRate()
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 handleMemberDoubleClick = (member) => {
console.log('双击事件触发,成员数据:', member)
// 将成员等级写死为1所有成员都可以跳转
const memberLevel = 1
console.log('等级设置为1准备跳转到Sale页面')
// 跳转到Sale页面携带成员姓名和等级
router.push({
name: 'Sale',
query: {
user_name: member.name,
user_level: memberLevel
}
})
}
// 格式化货币
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] || '未知'
}
// 工具提示状态
const tooltip = reactive({
visible: false,
x: 0,
y: 0,
title: '',
description: ''
})
// 指标描述
const metricDescriptions = {
teamPerformance: {
title: '转化率',
description: '本期最终成交/本期客户总数'
}
}
// 显示工具提示
const showTooltip = (event, metricType) => {
const metric = metricDescriptions[metricType]
if (metric) {
tooltip.title = metric.title
tooltip.description = metric.description
tooltip.x = event.clientX
tooltip.y = event.clientY
tooltip.visible = true
}
}
// 隐藏工具提示
const hideTooltip = () => {
tooltip.visible = false
}
</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;
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
&.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;
}
}
}
}
}
}
}
.info-icon {
color: #94a3b8;
font-size: 0.75rem;
cursor: pointer;
opacity: 0.7;
transition: all 0.2s ease;
margin-left: 0.25rem;
&:hover {
opacity: 1;
color: #3b82f6;
transform: scale(1.1);
}
}
// 客户详情区域
.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;
}
}
}
}
}
}
}
}
// 路由导航样式
.route-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
.breadcrumb-item {
color: #64748b;
font-size: 0.875rem;
font-weight: 500;
&:not(.current) {
cursor: pointer;
transition: color 0.2s;
&:hover {
color: #3b82f6;
}
}
&.current {
color: #1e293b;
font-weight: 600;
}
}
.breadcrumb-separator {
color: #94a3b8;
font-size: 0.875rem;
}
}
.user-name {
color: #1e293b;
font-size: 1.125rem;
font-weight: 600;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.8);
border-radius: 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.1);
}
}
</style>