Files
DJKB/my-vue-app/src/views/secondTop/secondTop.vue
lbw_9527443 b7d46c3dde refactor(secondTop): 优化营期设置和结束逻辑
使用getRequestParams统一处理请求参数
将硬编码的字符串值改为变量引用
2025-08-26 13:12:35 +08:00

1499 lines
49 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 :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 :record-data="goodRecord" />
<!-- 客户问题排行 -->
<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'
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('保存失败,请重试');
}
};
const padding111 = ref('11')
// 结束营期
const finishCamp = async () => {
try {
const params = {
...getRequestParams(),
is_camp_finish: padding111.value.toString()
};
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
try {
const res = await getOverallCenterPerformance(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
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
try {
const res = await getTotalGroupCount(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
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
try {
const res = await getCenterConversionRate(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
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
try {
const res = await getTotalCallCount(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
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
try {
const res = await getNewCustomer(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
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
try {
const res = await getDepositConversionRate(hasParams ? {
...params,
check_type: CheckType.value
} : {check_type: CheckType.value})
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 getCustomerTypeDistribution(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
try {
const res = await getUrgentNeedToAddress(hasParams ? params : undefined)
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
try {
const res = await getConversionRateVsAverage(hasParams ? params : undefined)
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
try {
const res = await getCenterAdvancedManagerList(hasParams ? params : undefined)
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 getTeamRanking(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 getTeamRankingInfo(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 goodRecord = ref([])
// 获取优秀录音文件
async function getGoodRecord() {
const params = getRequestParams()
const hasParams = params.user_name
const requestParams = hasParams ? {
...params,
} : {
}
try {
const res = await getExcellentRecordFile(requestParams)
if (res.code === 200) {
goodRecord.value = res.data
/**
* "data": {
"user_name": "张三丰",
"user_level": 4,
"excellent_record_list": {
"马然": [
{
"sale_name": "马然",
"sop": null,
"context": "[4.61s - 324.55s] Speaker 1: 哎,星宇吗?能看到常老师吧。嗯嗯,好,老师也把摄像头翻转过来,带您看一下咱们平台。嗯,你看能看到吧啊,可以看到嗯,咱们规模够大吧啊,我们是全国也是全互联网规模最大的解决青少年心理问题的平台。所以为什么一开始就跟您说孩子问题不用担心,不用害怕。对于您来说,现在不知道该怎么去跟孩子沟通了,我们也能够感受到孩子也很痛苦,他也想改变,他也想让自己变优秀,但是到底怎么引导着孩子,让他成为一个优秀的自己呢?我们每天去处理的都是这样的事情。因为我刚刚跟您说的很多的各种阶段的、初中的、高中的,甚至说进入大学的孩子,也是本来也是一一个很优秀的孩子,真的就是面对这些问题,面对这些困难,他不知道怎么去处理我们家长到底如何给予孩子帮助?你看这个这是央视的品牌,中国,咱们是重点推荐品牌,这是央视的力量栏目组。咱们是受邀专访企业,这是咱们赵院长去年去参加腾讯的育儿盛典,给他们颁发的综合实力。家庭教育品牌这边呢是咱们前台暖洋葱,家庭教育能看到吧啊,看到了嗯,你看咱们的规模都是非常大的。我们的愿景就是让咱们孩子开心快乐的去主动学习自己,能够把精力放在学习上,我们也不用再去天天担心孩子了,孩子也能够有个更好的发展。咱们的使命就是改善我们的家庭教育,影响幸福代人孩只是孩子一一个成绩,有一良好的发展,更多多也孩孩子一个良好的品良良好心心态,够够自己己人生生展展,是咱们这几天上课的,也是说赵世杰、赵院长还记得吧?中科院的高级心理咨询师,也是我们暖阳村教研中心的主任课就在他那里买的。对对,然后最擅长的就是解决提升孩子这些学习力内驱力,让他主动学习,提升成绩,解决这些各种原因,厌学休学,不想上学,不想学习依赖手机沉迷游戏的问题。可以说,赵院孩子问题,赵院长眼中中等程度算不上,非常好解决,又不是说真的去割动脉了。那孩子也有这样一个行为,但是划破皮是不是?但是我们也需要重视视,毕竟现在是发的就是这个对,也是有这样一个感觉自己心理上的痛苦,当心理上的痛苦,他无法承受的时候,他就只能通过伤害自身身体来缓解内心的这种压力了。这边呢是咱们一个发展历程。你看我们是零九年的时候建立的,当时在北京、北京十七个家里面,九位老师都是如何高无一例外,孩子学习能力是有的,包括甚至小学的时候都非常听话,也非常开朗。但是你会发现,一旦进入初中,一旦进入高中,各种各样的情况,各种各样的问题就开始出现了。比如说跟咱们孩子一样,学校里面老师批评了孩子自尊心受到打击了,都甚至同学矛盾、矛盾,甚至到校园霸凌的程度。好,有的也是爸爸妈妈长期在家里面吵架,甚至说长期的对于孩子缺乏陪伴,孩子没有安全感,是不是那孩子的这个性格方面就肯定爸爸爸。嗯嗯,这一点咱们对于孩子做的还是很好的,但孩子面对的问题是各种各样的,从来没从来没挑战。嗯,有一个幸福的家庭,那这样已经也是超过大部分的来来解决的这些家长了。那关键就是孩子在面对其他问题的时候,我们到底能够给予到孩子一个什么样的帮助。由于孩子他没有一个解决的问题,他孩子就是孩子他不知道该怎么办,他的想法都是非常幼稚的,非常单纯的。我们家长到底如何引导着孩子给到他力量温暖和引导,让他去解决这些问题,对不对?你们这一开始咱们就是洋葱互助会的形式成立的。零九年到一三年,咱们互助会就累计帮助了数万人了。到一五年,我们正式成立了暖洋葱家庭教育。一七年两年的时间,咱们迅速发展,当时已经是非常大的规模了,能看到吧?很多人,你看你看现在国家也开始重视青少年的心理健康了,要求学校里面搭建什么校园心理工作室?我们想让孩子长大之后成为一个优秀的人。首先,他得能够心理健康的长大吧,对不对?那他们需要找到专业的平台呀,对,那就找到咱们咱们去帮助全国多所学校搭建校园经理工作室。二二年,全国多家媒体登门进行独家的专访。二三年抖音平台家庭教育板块给咱们颁发新锐领学官的证书,什么意思呢?孩子妈妈家庭教育这个板块,我们要说自己是第二的话,那就没人有资格说自己是第一了。领学官领学官领了在家学习的人才叫领学官。这个是二四年央视的两个栏目,对我们进行独家专访的原因。我们也成功的帮助,数万组家庭走向幸福。孩子们现在对我们洋葱有一个大概的了解了吧?啊嗯所以说专业的事情交给专业的人。咱们这些问题呢,我会反馈咱们赵院长让他去进行针对性的讲课。我稍后也整理一下咱们机构的资料,包括跟您孩子类似的他们的一些成长改变发给咱们,咱们可以了解一下课程。开始之前我也会提醒咱们有问题了,随时联系我,好吧,可以可以可以。那那老师今天就咱们沟通到这里,他的妈妈再见。好的好的好的嗯,\n[324.71s - 325.87s] Speaker 0: 好的,好,谢谢啊。\n",
"obj_file_name": "http://192.168.3.112:5000/api/record/download/马然-20分钟通话-25-08-20_20-17-36-347230-987.mp3",
"score": 25.0
},
{
"sale_name": "马然",
"sop": null,
"context": "[8.79s - 22.35s] Speaker 0: 我刚在给你打电话接话啊,我可以给初二给人打电话,给喂我的妈妈刚刚玩一下就行。现是进入直播间学习上班哦,\n[27.25s - 34.91s] Speaker 0: 但是这个哎新宇妈妈一个月开始十分钟吗?\n[34.91s - 36.13s] Speaker 1: 啊,能听到吗?\n[36.13s - 37.81s] Speaker 0: 稍等一下哈哦哦,\n[38.15s - 197.56s] Speaker 1: 等一下他在忙是吧,不能不能听得到。你说嗯信号怎么好哦,行行行,是咱们第三节直播已经开始十多分钟了,是来提醒了们上课的,看到没有?我刚才没听没有没有嗯嗯,老师今天晚上都忙,没时间吗?这几天赶货比较忙哦,赶货干啊,刚吃饭厂里上班的哦哦哦,厂里上班咱们就是刚休息是吗?一会儿是还要去吗?对,刚吃饭回来。对,哎呦,虽然我上课这两天上课都是边上班边听的。嗯嗯嗯,对,咱们可以戴个耳机边上班边听。嗯啊对对对,孩子这两天咱们跟孩子沟通情况怎么样啊,孩子没回来了,还在学校。嗯,嗯,哦咱们这两天也没给他发消息是吧?呃,手机没说,反正嗯手机没有嗯对手机收了。嗯,那郑院长昨天也提到了咱们的一对一指导,帮助孩子去解决这样一个问题。不管是心理方面了,还是对于学习这些的,咱们是呃怎么考虑的呢?我刚今天又听了我今天的那个课听差不多了。嗯,嗯,那个有点贵呀。嗯,好的,妈妈两个月的一对一指导啊。咱们如果说了解的话,平时都是一万以上的,也只有咱们这一天赵院长给我们的一个优惠名额是六千八。咱们也而且还是你抢到这个名额优惠名额有三百抵三千。今天晚上价值五千九百八的证件训练,也是截止赠送了三个六千八。说实话,孩孩子这样一个情况,我们这种情况,我们这种家庭嗯嗯非常啊对我非常理解咱们这样一个心情,对不对?但是如果说你孩子这样一个问题,你想两个月的老师这样一堆指导,你要是我我给你找一个一千块的让你报名,你想的是啥样的,老师给他们指导啊,对不对?哎,对于孩子来说,家家庭太困难了,我们哎,太难了,反正先写在外面的事。没事没事,咱们先嗯今天晚上依然该听课听课。好吧,有很多家长都是刚刚还没进去,没没去,刚刚吃饭有点忙。嗯,行行,刚刚回来,我就是稍后把链接发给咱们。然后晚上的时候,我也我我嗯整理跟咱们家庭情况比较相似的,都可以发给咱们,咱们可以看一看了解一下,好吧,我都还没没来得及看。你昨天给我发了好多,我都没没仔细看。哎呦啊,那都是时间太忙,工作太忙。那行,咱们先进直播间签到打卡吧,行吧,进直播间签到打卡,结果回放。嗯,好谢行吧,再见。好好好好好再见嗯,拜拜。\n",
"obj_file_name": "http://192.168.3.112:5000/api/record/download/马然-20分钟通话-25-08-20_20-24-43-653520-759.mp3",
"score": 55.0
}
]
}
}
*/
}
} catch (error) {
console.error('获取优秀录音失败:', error)
}
}
onMounted(async () => {
try {
isLoading.value = true
// 判断页面进入方式如果是通过路由跳转进入URL中有user_name和user_level参数则不发送CenterCampPeriodAdmin请求
// 如果是直接登录进入页面,则发送请求
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 getGoodRecord()
await CenterGroupList('all') // 初始化加载全部高级经理数据
} catch (error) {
console.error('数据加载失败:', error)
} finally {
isLoading.value = false
}
})
// 更新CheckType并重新获取数据
const updateCheckType = async (newValue) => {
CheckType.value = newValue
console.log('CheckType已更新为:', newValue)
// 重新调用相关API函数
isLoading.value = true
try {
await CenterOverallCenterPerformance()
await CenterTotalGroupCount()
await CenterConversionRate()
await CenterTotalCallCount()
await CenterNewCustomer()
await CenterDepositConversionRate()
} catch (error) {
console.error('重新获取数据失败:', error)
} finally {
isLoading.value = false
}
}
// 工具提示状态
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>