fix(GoodMusic): 修复录音列表数据结构和显示问题 style: 统一多个组件的头部内边距为10px 20px 10px chore: 切换API基础路径为生产环境
1740 lines
47 KiB
Vue
1740 lines
47 KiB
Vue
<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">
|
||
<!-- 用户下拉菜单 -->
|
||
<div style="display: flex; align-items: center; gap: 20px;">
|
||
<button @click="showFeedbackFormModal" class="feedback-btn">意见反馈</button>
|
||
<FeedbackForm
|
||
:is-visible="showFeedbackForm"
|
||
@close="closeFeedbackFormModal"
|
||
@submit-feedback="closeFeedbackFormModal"
|
||
/>
|
||
<UserDropdown
|
||
:card-visibility="cardVisibility"
|
||
@update-card-visibility="updateCardVisibility"
|
||
/>
|
||
</div>
|
||
</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
|
||
v-if="cardVisibility.centerOverview"
|
||
:key="CheckType"
|
||
:overall-data="overallCenterPerformance"
|
||
@update-check-type="updateCheckType"
|
||
/>
|
||
|
||
<!-- Action Items (Compact) -->
|
||
<div v-if="cardVisibility.actionItems" class="action-items-compact">
|
||
<ActionItems :selected-group="selectedGroup" />
|
||
</div>
|
||
</div>
|
||
<div class="BB-section">
|
||
<!--客户类型占比-->
|
||
<CustomerType
|
||
v-if="cardVisibility.customerType"
|
||
:customer-data="customerTypeDistribution"
|
||
@category-change="handleCustomerTypeChange"
|
||
/>
|
||
<!-- 优秀录音 -->
|
||
<GoodMusic
|
||
v-if="cardVisibility.goodMusic"
|
||
:quality-calls="excellentRecord"
|
||
/>
|
||
<!-- 客户问题排行 -->
|
||
<ProblemRanking
|
||
v-if="cardVisibility.problemRanking"
|
||
:ranking-data="formattedUrgentNeedData"
|
||
/>
|
||
</div>
|
||
<!-- Bottom Section -->
|
||
<div class="bottom-section">
|
||
<!-- Left Section - Group Performance Ranking -->
|
||
<div v-if="cardVisibility.groupRanking" class="left-section">
|
||
<GroupRanking :groups="groups" :selected-group="selectedGroup" :conversion-data="conversionRateVsAverage" @select-group="selectGroup" />
|
||
</div>
|
||
|
||
<!-- Right Section - Group Comparison -->
|
||
<div v-if="cardVisibility.groupComparison" 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 && cardVisibility.teamDetail">
|
||
<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">{{ (groupPerformance && groupPerformance.group_details) ? groupPerformance.group_details.length : 0 }}人</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)" @dblclick="navigateToSale(member.name)">
|
||
<div class="member-header">
|
||
<div class="member-info">
|
||
<h3 class="member-name">{{ member.name }}</h3>
|
||
<p class="member-position">{{ member.position }}</p>
|
||
</div>
|
||
<!-- 本人排名 -->
|
||
<div class="member-ranking">
|
||
<span class="ranking-label">排名:</span>
|
||
<span class="ranking-value">{{ member.rank }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="member-metrics">
|
||
<div class="metric-row">
|
||
<div class="metric-item">
|
||
<span class="metric-label">今日业绩</span>
|
||
<span class="metric-value">{{ member.todayPerformance}}</span>
|
||
</div>
|
||
<div class="metric-item">
|
||
<span class="metric-label">月度业绩</span>
|
||
<span class="metric-value">{{ member.monthlyPerformance }}</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.conversionRate }}%</span>
|
||
</div>
|
||
<div class="metric-item">
|
||
<span class="metric-label">通话次数</span>
|
||
<span class="metric-value">{{ member.callCount }}</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.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>
|
||
|
||
<!-- Loading 组件 -->
|
||
<!-- <Loading :visible="isLoading" text="数据加载中..." /> -->
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, computed,reactive } from 'vue'
|
||
import FeedbackForm from "@/components/FeedbackForm.vue";
|
||
// 30分钟数据缓存系统
|
||
const cache = new Map()
|
||
const CACHE_DURATION = 30 * 60 * 1000 // 30分钟
|
||
|
||
// 生成缓存键
|
||
const getCacheKey = (functionName, params = {}) => {
|
||
const sortedParams = Object.keys(params).sort().reduce((result, key) => {
|
||
result[key] = params[key]
|
||
return result
|
||
}, {})
|
||
return `${functionName}_${JSON.stringify(sortedParams)}`
|
||
}
|
||
|
||
// 检查缓存是否有效
|
||
const isValidCache = (cacheData) => {
|
||
return cacheData && (Date.now() - cacheData.timestamp) < CACHE_DURATION
|
||
}
|
||
|
||
// 设置缓存
|
||
const setCache = (key, data) => {
|
||
cache.set(key, {
|
||
data,
|
||
timestamp: Date.now()
|
||
})
|
||
}
|
||
|
||
// 获取缓存
|
||
const getCache = (key) => {
|
||
const cacheData = cache.get(key)
|
||
if (isValidCache(cacheData)) {
|
||
return cacheData.data
|
||
}
|
||
cache.delete(key) // 删除过期缓存
|
||
return null
|
||
}
|
||
|
||
// 带缓存的API调用包装器
|
||
const withCache = async (functionName, apiCall, params = {}) => {
|
||
const cacheKey = getCacheKey(functionName, params)
|
||
const cachedData = getCache(cacheKey)
|
||
|
||
if (cachedData) {
|
||
console.log(`[缓存命中] ${functionName}:`, cachedData)
|
||
return cachedData
|
||
}
|
||
|
||
try {
|
||
const result = await apiCall()
|
||
if (result && result.code === 200) {
|
||
setCache(cacheKey, result)
|
||
console.log(`[缓存设置] ${functionName}:`, result)
|
||
}
|
||
return result
|
||
} catch (error) {
|
||
console.error(`[API调用失败] ${functionName}:`, error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
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 Loading from '@/components/Loading.vue'
|
||
import Tooltip from '@/components/Tooltip.vue'
|
||
import {
|
||
getOverallCenterPerformance, getTotalGroupCount, getCenterConversionRate, getTotalCallCount, getNewCustomer
|
||
, getDepositConversionRate, getCustomerTypeDistribution, getUrgentNeedToAddress, getCenterAdvancedManagerList, getTeamRanking,
|
||
getTeamRankingInfo, getConversionRateVsAverage,getCampPeriodAdmin ,getExcellentRecordFile } from '@/api/secondTop.js'
|
||
import { useRouter } from 'vue-router'
|
||
import { useUserStore } from '@/stores/user.js'
|
||
// 路由实例
|
||
const router = useRouter();
|
||
// 用户store实例
|
||
const userStore = useUserStore();
|
||
const CheckType = ref('month')
|
||
|
||
// 卡片显示状态
|
||
const cardVisibility = ref({
|
||
centerOverview: true,
|
||
actionItems: true,
|
||
customerType: true,
|
||
goodMusic: true,
|
||
problemRanking: true,
|
||
groupRanking: true,
|
||
groupComparison: true,
|
||
teamDetail: true
|
||
})
|
||
|
||
// FeedbackForm 控制变量
|
||
const showFeedbackForm = ref(false)
|
||
|
||
// 更新卡片显示状态
|
||
const updateCardVisibility = (newVisibility) => {
|
||
Object.assign(cardVisibility.value, newVisibility)
|
||
console.log('卡片显示状态已更新:', cardVisibility.value)
|
||
}
|
||
|
||
// FeedbackForm 控制方法
|
||
const showFeedbackFormModal = () => {
|
||
showFeedbackForm.value = true
|
||
}
|
||
|
||
const closeFeedbackFormModal = () => {
|
||
showFeedbackForm.value = false
|
||
}
|
||
// 营期调控逻辑
|
||
// This would ideally come from a prop or API call based on the logged-in user
|
||
const centerData = ref({
|
||
id: 1,
|
||
name: '一中心',
|
||
startDate: '2025-08-18',
|
||
stages: [
|
||
{ name: '接数据', days: 3, color: '#ffc107', startDate: '', endDate: '' },
|
||
{ name: '课一', days: 1, color: '#0dcaf0', startDate: '', endDate: '' },
|
||
{ name: '课二', days: 1, color: '#0d6efd', startDate: '', endDate: '' },
|
||
{ name: '课三', days: 1, color: '#6f42c1', startDate: '', endDate: '' },
|
||
{ name: '课四', days: 1, color: '#d63384', startDate: '', endDate: '' }
|
||
]
|
||
});
|
||
const getCurrentStage = (center) => {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
const startDate = new Date(center.startDate);
|
||
startDate.setHours(0, 0, 0, 0);
|
||
|
||
if (today < startDate) {
|
||
return '未开始';
|
||
}
|
||
|
||
const diffTime = Math.abs(today - startDate);
|
||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // 当天算第一天
|
||
|
||
let cumulativeDays = 0;
|
||
for (let i = 0; i < center.stages.length; i++) {
|
||
const stage = center.stages[i];
|
||
cumulativeDays += stage.days;
|
||
if (diffDays <= cumulativeDays) {
|
||
return stage.name;
|
||
}
|
||
}
|
||
|
||
return '已结束';
|
||
};
|
||
|
||
const currentStage = computed(() => getCurrentStage(centerData.value));
|
||
|
||
const isDataReceivingStage = computed(() => currentStage.value === '接数据');
|
||
|
||
// The '接数据' stage object
|
||
const dataReceivingStage = computed(() => centerData.value.stages.find(s => s.name === '接数据'));
|
||
|
||
const recalculateStageDates = () => {
|
||
const startDate = new Date(centerData.value.startDate);
|
||
let cumulativeDays = 0;
|
||
centerData.value.stages.forEach(stage => {
|
||
const stageStartDate = new Date(startDate);
|
||
stageStartDate.setDate(startDate.getDate() + cumulativeDays);
|
||
stage.startDate = stageStartDate.toISOString().split('T')[0];
|
||
|
||
cumulativeDays += stage.days;
|
||
|
||
const stageEndDate = new Date(startDate);
|
||
stageEndDate.setDate(startDate.getDate() + cumulativeDays - 1);
|
||
stage.endDate = stageEndDate.toISOString().split('T')[0];
|
||
});
|
||
};
|
||
// 保存营期
|
||
const saveCampSettings = async () => {
|
||
recalculateStageDates();
|
||
// 准备API请求参数
|
||
const params = {
|
||
...getRequestParams(),
|
||
receipt_data_time: dataReceivingStage.value.days.toString()
|
||
};
|
||
|
||
try {
|
||
const res = await getCampPeriodAdmin(params);
|
||
if (res.code === 200) {
|
||
console.log('营期设置保存成功:', res.data);
|
||
alert('营期设置已保存!');
|
||
} else {
|
||
alert('保存失败,请重试');
|
||
}
|
||
} catch (error) {
|
||
console.error('保存营期设置失败:', error);
|
||
alert('保存失败,请重试');
|
||
}
|
||
};
|
||
// console.log('currentStage', userStore.userInfo)
|
||
|
||
// 获取,修改当前营期
|
||
const campPeriodAdmin = ref({})
|
||
async function CenterCampPeriodAdmin() {
|
||
const params = {
|
||
user_name: userStore.userInfo.username
|
||
,user_level: userStore.userInfo.user_level.toString()
|
||
}
|
||
|
||
const res = await getCampPeriodAdmin(params)
|
||
if (res.code === 200) {
|
||
campPeriodAdmin.value = res.data
|
||
|
||
// 根据API返回的数据更新centerData
|
||
if (res.data.camp_period) {
|
||
const campPeriod = res.data.camp_period
|
||
|
||
// 解析接数据阶段的开始日期作为营期开始日期
|
||
if (campPeriod.receipt_data_time) {
|
||
const startDate = campPeriod.receipt_data_time.split(' to ')[0].split(' ')[0]
|
||
centerData.value.startDate = startDate
|
||
}
|
||
|
||
// 计算各阶段天数
|
||
const calculateDays = (timeRange) => {
|
||
if (!timeRange) return 1
|
||
const [start, end] = timeRange.split(' to ')
|
||
const startDate = new Date(start.split(' ')[0])
|
||
const endDate = new Date(end.split(' ')[0])
|
||
return Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1
|
||
}
|
||
|
||
// 更新各阶段天数
|
||
centerData.value.stages.forEach(stage => {
|
||
switch(stage.name) {
|
||
case '接数据':
|
||
stage.days = calculateDays(campPeriod.receipt_data_time)
|
||
break
|
||
case '课一':
|
||
stage.days = calculateDays(campPeriod.class_one)
|
||
break
|
||
case '课二':
|
||
stage.days = calculateDays(campPeriod.class_two)
|
||
break
|
||
case '课三':
|
||
stage.days = calculateDays(campPeriod.class_three)
|
||
break
|
||
case '课四':
|
||
stage.days = calculateDays(campPeriod.class_four)
|
||
break
|
||
}
|
||
})
|
||
|
||
// 重新计算阶段日期
|
||
recalculateStageDates()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 组别数据
|
||
const groups = ref([])
|
||
// loading 状态
|
||
const isLoading = ref(false)
|
||
|
||
// 获取通用请求参数的函数
|
||
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 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)
|
||
}
|
||
// 中心整体概览
|
||
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
|
||
const requestParams = hasParams ? {
|
||
...params,
|
||
check_type: CheckType.value
|
||
} : {check_type: CheckType.value}
|
||
|
||
try {
|
||
const res = await withCache('CenterOverallCenterPerformance',
|
||
() => getOverallCenterPerformance(requestParams),
|
||
requestParams
|
||
)
|
||
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
|
||
const requestParams = hasParams ? {
|
||
...params,
|
||
check_type: CheckType.value
|
||
} : {check_type: CheckType.value}
|
||
|
||
try {
|
||
const res = await withCache('CenterTotalGroupCount',
|
||
() => getTotalGroupCount(requestParams),
|
||
requestParams
|
||
)
|
||
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
|
||
const requestParams = hasParams ? {
|
||
...params,
|
||
check_type: CheckType.value
|
||
} : {check_type: CheckType.value}
|
||
|
||
try {
|
||
const res = await withCache('CenterConversionRate',
|
||
() => getCenterConversionRate(requestParams),
|
||
requestParams
|
||
)
|
||
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
|
||
const requestParams = hasParams ? {
|
||
...params,
|
||
check_type: CheckType.value
|
||
} : {check_type: CheckType.value}
|
||
|
||
try {
|
||
const res = await withCache('CenterTotalCallCount',
|
||
() => getTotalCallCount(requestParams),
|
||
requestParams
|
||
)
|
||
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
|
||
const requestParams = hasParams ? {
|
||
...params,
|
||
check_type: CheckType.value
|
||
} : {check_type: CheckType.value}
|
||
|
||
try {
|
||
const res = await withCache('CenterNewCustomer',
|
||
() => getNewCustomer(requestParams),
|
||
requestParams
|
||
)
|
||
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
|
||
const requestParams = hasParams ? {
|
||
...params,
|
||
check_type: CheckType.value
|
||
} : {check_type: CheckType.value}
|
||
|
||
try {
|
||
const res = await withCache('CenterDepositConversionRate',
|
||
() => getDepositConversionRate(requestParams),
|
||
requestParams
|
||
)
|
||
if (res.code === 200) {
|
||
overallCenterPerformance.value.DepositConversionRate = res.data
|
||
}
|
||
} catch (error) {
|
||
console.error('获取中心整体概览失败:', error)
|
||
}
|
||
}
|
||
// 客户类型
|
||
async function CenterCustomerType(distributionType = 'child_education') {
|
||
const params = getRequestParams()
|
||
const hasParams = params.user_name
|
||
// 添加distribution_type参数
|
||
const requestParams = hasParams ? { ...params, distribution_type: distributionType } : { distribution_type: distributionType }
|
||
|
||
try {
|
||
const res = await withCache('CenterCustomerType',
|
||
() => getCustomerTypeDistribution(requestParams),
|
||
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
|
||
const requestParams = hasParams ? params : {}
|
||
|
||
try {
|
||
const res = await withCache('CenterUrgentNeedToAddress',
|
||
() => getUrgentNeedToAddress(hasParams ? params : undefined),
|
||
requestParams
|
||
)
|
||
if (res.code === 200) {
|
||
urgentNeedToAddress.value = res.data
|
||
}
|
||
} catch (error) {
|
||
console.error('获取中心整体概览失败:', error)
|
||
}
|
||
}
|
||
// 各阶段转化率
|
||
const conversionRateVsAverage = ref({})
|
||
async function CenterConversionRateVsAverage() {
|
||
const params = getRequestParams()
|
||
const hasParams = params.user_name
|
||
const requestParams = hasParams ? params : {}
|
||
|
||
try {
|
||
const res = await withCache('CenterConversionRateVsAverage',
|
||
() => getConversionRateVsAverage(hasParams ? params : undefined),
|
||
requestParams
|
||
)
|
||
if (res.code === 200) {
|
||
conversionRateVsAverage.value = res.data
|
||
}
|
||
} catch (error) {
|
||
console.error('获取中心整体概览失败:', error)
|
||
}
|
||
}
|
||
|
||
// 综合排名---高级经理列表
|
||
const seniorManagerList = ref([])
|
||
async function CenterSeniorManagerList() {
|
||
const params = getRequestParams()
|
||
const hasParams = params.user_name
|
||
const requestParams = hasParams ? params : {}
|
||
|
||
try {
|
||
const res = await withCache('CenterSeniorManagerList',
|
||
() => getCenterAdvancedManagerList(hasParams ? params : undefined),
|
||
requestParams
|
||
)
|
||
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 withCache('CenterGroupList',
|
||
() => getTeamRanking(requestParams),
|
||
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 groupPerformance = ref({})
|
||
|
||
// 根据传来的组名字来获取组业绩详情
|
||
async function CenterGroupPerformance(groupName) {
|
||
const routeParams = getRequestParams()
|
||
const params = routeParams.user_name
|
||
? routeParams
|
||
: {user_name: userStore.userInfo.username, user_level: userStore.userInfo.user_level.toString()}
|
||
const hasParams = params.user_name
|
||
const requestParams = hasParams ? {
|
||
...params,
|
||
department: groupName
|
||
} : {
|
||
department: groupName
|
||
}
|
||
|
||
try {
|
||
const res = await withCache('CenterGroupPerformance',
|
||
() => getTeamRankingInfo(requestParams),
|
||
requestParams
|
||
)
|
||
|
||
if (res.code === 200) {
|
||
groupPerformance.value = res.data
|
||
}
|
||
} catch (error) {
|
||
console.error('获取团队排名失败:', error)
|
||
}
|
||
}
|
||
|
||
|
||
// 当前选中的组别,默认为第一个
|
||
const selectedGroup = ref(groups[0])
|
||
|
||
// 选择组别函数
|
||
const selectGroup = async (group) => {
|
||
selectedGroup.value = group
|
||
console.log('选中的组别111:', group)
|
||
const departmentName = group.name+'-'+group.leader
|
||
|
||
try {
|
||
await CenterGroupPerformance(departmentName)
|
||
|
||
// 将API返回的数据整理后更新到selectedGroup的members字段
|
||
if (groupPerformance.value && groupPerformance.value.group_details) {
|
||
const formattedMembers = groupPerformance.value.group_details
|
||
.map((member, index) => ({
|
||
id: index + 1,
|
||
name: member.name,
|
||
position: '销售顾问', // 默认职位
|
||
phone: '***-****-****', // 隐藏手机号
|
||
status: member.rank === 1 ? 'excellent' : member.rank === 2 ? 'good' : 'average',
|
||
joinDate: '2023-01-01', // 默认入职日期
|
||
todayPerformance: member.today_deals,
|
||
monthlyPerformance: member.monthly_deals,
|
||
conversionRate: parseFloat(member.conversion_rate_this_period) || 0,
|
||
callCount: member.call_count_this_period || 0,
|
||
newClients: member.new_customers_this_period || 0,
|
||
deals: member.deals_this_period || 0,
|
||
rank: member.rank || 0
|
||
}))
|
||
.sort((a, b) => a.rank - b.rank) // 根据排名降序排列(排名数字越大越靠后)
|
||
|
||
// 更新selectedGroup的members数据
|
||
selectedGroup.value = {
|
||
...selectedGroup.value,
|
||
members: formattedMembers
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取团队详情失败:', error)
|
||
}
|
||
}
|
||
|
||
// 处理高级经理选择变化
|
||
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] || '未知'
|
||
}
|
||
|
||
// 双击成员卡片跳转到销售页面
|
||
const navigateToSale = (userName) => {
|
||
router.push({
|
||
path: '/sale',
|
||
query: {
|
||
user_name: userName,
|
||
user_level: '1'
|
||
}
|
||
})
|
||
}
|
||
// 获取优秀录音
|
||
const excellentRecord = ref([]);
|
||
// 获取优秀录音文件
|
||
async function CentergetGoodRecord() {
|
||
console.log('CentergetGoodRecord 开始执行')
|
||
try {
|
||
const params = getRequestParams()
|
||
const params1 = {
|
||
user_level: userStore.userInfo?.user_level?.toString() || '',
|
||
user_name: userStore.userInfo?.username || ''
|
||
}
|
||
|
||
// 检查参数是否有效
|
||
const hasParams = params.user_name && params.user_level
|
||
const requestParams = hasParams ? {
|
||
...params,
|
||
} : params1
|
||
|
||
console.log('CentergetGoodRecord request params:', requestParams)
|
||
|
||
// 验证必要参数是否存在
|
||
if (!requestParams.user_name || !requestParams.user_level) {
|
||
console.error("缺少必要的请求参数:", requestParams);
|
||
return;
|
||
}
|
||
|
||
// 直接发送请求,不使用缓存
|
||
const res = await getExcellentRecordFile(requestParams)
|
||
|
||
if (res && res.code === 200 && res.data) {
|
||
excellentRecord.value = res.data || []
|
||
console.log('获取优秀录音成功:', res.data)
|
||
} else {
|
||
console.error("获取优秀录音失败,响应数据不完整:", res);
|
||
excellentRecord.value = []
|
||
}
|
||
} catch (error) {
|
||
console.error("获取优秀录音失败:", error);
|
||
excellentRecord.value = []
|
||
}
|
||
}
|
||
|
||
// 缓存管理功能
|
||
// 清除所有缓存
|
||
const clearCache = () => {
|
||
cache.clear()
|
||
console.log('[缓存清除] 所有缓存已清除')
|
||
}
|
||
|
||
// 清除特定缓存
|
||
const clearSpecificCache = (functionName, params = {}) => {
|
||
const cacheKey = getCacheKey(functionName, params)
|
||
cache.delete(cacheKey)
|
||
console.log(`[缓存清除] ${functionName} 缓存已清除`)
|
||
}
|
||
|
||
// 获取缓存信息
|
||
const getCacheInfo = () => {
|
||
const cacheInfo = {
|
||
totalCount: cache.size,
|
||
validCount: 0,
|
||
expiredCount: 0,
|
||
cacheKeys: []
|
||
}
|
||
|
||
cache.forEach((value, key) => {
|
||
if (isValidCache(value)) {
|
||
cacheInfo.validCount++
|
||
cacheInfo.cacheKeys.push({
|
||
key,
|
||
timestamp: value.timestamp,
|
||
remainingTime: CACHE_DURATION - (Date.now() - value.timestamp)
|
||
})
|
||
} else {
|
||
cacheInfo.expiredCount++
|
||
cache.delete(key) // 清除过期缓存
|
||
}
|
||
})
|
||
|
||
console.log('[缓存信息]', cacheInfo)
|
||
return cacheInfo
|
||
}
|
||
|
||
// 强制刷新所有数据(清除缓存并重新调用API)
|
||
const forceRefreshAllData = async () => {
|
||
clearCache()
|
||
isLoading.value = true
|
||
|
||
try {
|
||
const currentQuery = router.currentRoute.value.query
|
||
const isFromRoute = currentQuery.fromRoute ||
|
||
sessionStorage.getItem('fromRoute') ||
|
||
(currentQuery.user_name && currentQuery.user_level)
|
||
|
||
if (!isFromRoute) {
|
||
await CenterCampPeriodAdmin()
|
||
}
|
||
await CentergetGoodRecord()
|
||
await CenterOverallCenterPerformance()
|
||
await CenterTotalGroupCount()
|
||
await CenterConversionRate()
|
||
await CenterTotalCallCount()
|
||
await CenterNewCustomer()
|
||
await CenterDepositConversionRate()
|
||
await CenterCustomerType()
|
||
await CenterUrgentNeedToAddress()
|
||
await CenterConversionRateVsAverage()
|
||
|
||
await CenterSeniorManagerList()
|
||
await CenterGroupList('all')
|
||
|
||
console.log('[强制刷新] 所有数据已重新加载')
|
||
} catch (error) {
|
||
console.error('[强制刷新] 数据加载失败:', error)
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
isLoading.value = true
|
||
const currentQuery = router.currentRoute.value.query
|
||
const isFromRoute = currentQuery.fromRoute ||
|
||
sessionStorage.getItem('fromRoute') ||
|
||
(currentQuery.user_name && currentQuery.user_level)
|
||
|
||
if (!isFromRoute) {
|
||
// 直接登录进入页面时才调用CenterCampPeriodAdmin
|
||
await CenterCampPeriodAdmin()
|
||
}
|
||
// CenterCampPeriodAdmin中已经调用了recalculateStageDates,这里不需要重复调用
|
||
await CenterOverallCenterPerformance()
|
||
await CenterTotalGroupCount()
|
||
await CenterConversionRate()
|
||
await CenterTotalCallCount()
|
||
await CentergetGoodRecord()
|
||
await CenterNewCustomer()
|
||
await CenterDepositConversionRate()
|
||
await CenterCustomerType()
|
||
await CenterUrgentNeedToAddress()
|
||
await CenterConversionRateVsAverage()
|
||
await CenterSeniorManagerList()
|
||
await CenterGroupList('all') // 初始化加载全部高级经理数据
|
||
|
||
// 输出缓存信息
|
||
getCacheInfo()
|
||
|
||
// 开发环境下暴露缓存管理函数到全局
|
||
if (process.env.NODE_ENV === 'development') {
|
||
window.secondTopCache = {
|
||
clearCache,
|
||
clearSpecificCache,
|
||
getCacheInfo,
|
||
forceRefreshAllData,
|
||
cache
|
||
}
|
||
console.log('[开发模式] 缓存管理函数已暴露到 window.secondTopCache')
|
||
}
|
||
} catch (error) {
|
||
console.error('数据加载失败:', error)
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
})
|
||
|
||
// 更新CheckType并重新获取数据
|
||
const updateCheckType = async (newValue) => {
|
||
CheckType.value = newValue
|
||
console.log('CheckType已更新为:', newValue)
|
||
|
||
// 使用强制刷新功能重新获取数据
|
||
await forceRefreshAllData()
|
||
}
|
||
|
||
// 工具提示状态
|
||
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;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.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;
|
||
}
|
||
|
||
// 移动端适配
|
||
@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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 路由导航顶栏样式
|
||
.route-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
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);
|
||
}
|
||
|
||
/* 意见反馈按钮样式 */
|
||
.feedback-btn {
|
||
background-color: #4299e1;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 0.5rem 1rem;
|
||
font-size: 0.9rem;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
|
||
&:hover {
|
||
background-color: #3182ce;
|
||
}
|
||
}
|
||
}
|
||
.stage-control {
|
||
margin-left: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.control-label {
|
||
margin-right: 10px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
}
|
||
|
||
.days-input {
|
||
width: 60px;
|
||
text-align: center;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.save-button {
|
||
padding: 5px 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.finish-camp-button {
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
background-color: #dc3545;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
|
||
&:hover {
|
||
background-color: #c82333;
|
||
}
|
||
}
|
||
</style> |