Files
DJKB/my-vue-app/src/views/secondTop/secondTop.vue
chenpanliang 14a536bd1c feat(Calendar): 添加休息天数输入并改进营期设置逻辑
- 在营期设置弹窗中添加休息天数输入字段
- 修改营期结束判断逻辑,不再仅依赖休息日
- 改进用户参数获取逻辑,优先使用路由参数
- 添加测试数据以便在没有营期数据时测试功能
- 优化API请求参数处理,确保总是传递必要参数
2025-08-27 18:13:46 +08:00

1651 lines
44 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 />
</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 :key="CheckType" :overall-data="overallCenterPerformance" @update-check-type="updateCheckType" />
<!-- Action Items (Compact) -->
<div class="action-items-compact">
<ActionItems :selected-group="selectedGroup" />
</div>
</div>
<div class="BB-section">
<!--客户类型占比-->
<CustomerType :customer-data="customerTypeDistribution" @category-change="handleCustomerTypeChange" />
<!-- 优秀录音 -->
<GoodMusic :quality-calls="excellentRecord" />
<!-- 客户问题排行 -->
<ProblemRanking :ranking-data="formattedUrgentNeedData" />
</div>
<!-- Bottom Section -->
<div class="bottom-section">
<!-- Left Section - Group Performance Ranking -->
<div class="left-section">
<GroupRanking :groups="groups" :selected-group="selectedGroup" :conversion-data="conversionRateVsAverage" @select-group="selectGroup" />
</div>
<!-- Right Section - Group Comparison -->
<div class="right-section">
<GroupComparison :groups="groups" :senior-manager-data="seniorManagerList" :group-list="groupList" @select-group="selectGroup" @manager-change="handleManagerChange" />
</div>
</div>
<!-- Team Members Detail Section -->
<div class="team-detail-section" v-if="selectedGroup">
<div class="team-detail-header">
<h2>{{ selectedGroup.name }} - 团队成员详情</h2>
<div class="team-summary">
<div class="summary-item">
<span class="label">组长:</span>
<span class="value">{{ selectedGroup.leader }}</span>
</div>
<div class="summary-item">
<span class="label">成员数:</span>
<span class="value">{{ (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'
// 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')
// 营期调控逻辑
// 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 params = getRequestParams()
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) => b.deals - a.deals) // 根据成交单数从高到低排序
// 更新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() {
const params = getRequestParams()
const params1 = {
user_level:userStore.userInfo.user_level.toString(),
user_name:userStore.userInfo.username
}
const hasParams = params.user_name
const requestParams = hasParams ? {
...params,
} : params1
console.log(188811111,requestParams)
try {
const res = await withCache('CentergetGoodRecord',
() => getExcellentRecordFile(requestParams),
requestParams
)
excellentRecord.value = res.data.excellent_record_list
console.log(111111,res.data.excellent_record_list)
} catch (error) {
console.error("获取优秀录音失败:", error);
}
}
// 缓存管理功能
// 清除所有缓存
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 CenterOverallCenterPerformance()
await CenterTotalGroupCount()
await CenterConversionRate()
await CenterTotalCallCount()
await CenterNewCustomer()
await CenterDepositConversionRate()
await CenterCustomerType()
await CenterUrgentNeedToAddress()
await CenterConversionRateVsAverage()
await CenterSeniorManagerList()
// await CentergetGoodRecord()
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 CenterNewCustomer()
await CenterDepositConversionRate()
await CenterCustomerType()
await CenterUrgentNeedToAddress()
await CenterConversionRateVsAverage()
await CenterSeniorManagerList()
// 获取优秀录音
// await CentergetGoodRecord()
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);
}
}
.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>