feat(组件): 添加指标说明工具提示功能

在个人仪表盘、团队报表和统计指标组件中添加工具提示功能,当用户悬停在指标信息图标上时显示详细说明
创建新的Tooltip组件用于显示指标说明
更新API基础路径配置
This commit is contained in:
2025-08-22 18:13:42 +08:00
parent 8b63ab6123
commit 9b61620b86
5 changed files with 363 additions and 58 deletions

View File

@@ -0,0 +1,53 @@
<template>
<Teleport to="body">
<div v-if="visible" class="stat-tooltip" :style="{ left: x + 'px', top: y + 'px' }">
<div class="tooltip-title">
{{ title }}
</div>
<div class="tooltip-description">
<p>{{ description }}</p>
</div>
</div>
</Teleport>
</template>
<script setup>
defineProps({
visible: Boolean,
x: Number,
y: Number,
title: String,
description: String
});
</script>
<style scoped>
.stat-tooltip {
position: fixed;
z-index: 9999;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
max-width: 300px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
pointer-events: none;
}
.tooltip-title {
font-weight: 600;
margin-bottom: 4px;
color: #fff;
}
.tooltip-description {
font-size: 13px;
color: #e0e0e0;
line-height: 1.4;
}
.tooltip-description p {
margin: 0;
}
</style>

View File

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

View File

@@ -4,52 +4,62 @@
<div class="report-grid"> <div class="report-grid">
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
<span class="card-title">团队总通话</span> <span class="card-title">团队总通话 <i class="info-icon" @mouseenter="showTooltip('totalCalls', $event)" @mouseleave="hideTooltip"></i></span>
<span class="card-trend positive">{{ weekTotalData.week_total_call?.team_data?.current_rate_last_current || '0%' }} vs 上期</span> <span class="card-trend positive">{{ weekTotalData.week_total_call?.team_data?.current_rate_last_current || '0%' }} vs 上期</span>
</div> </div>
<div class="card-value">{{ weekTotalData.week_total_call?.team_data?.total_current_week_call || 0 }} </div> <div class="card-value">{{ weekTotalData.week_total_call?.team_data?.total_current_week_call || 0 }} </div>
</div> </div>
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
<span class="card-title">有效通话时长</span> <span class="card-title">有效通话时长 <i class="info-icon" @mouseenter="showTooltip('callDuration', $event)" @mouseleave="hideTooltip"></i></span>
<span class="card-trend negative">{{ weekTotalData.week_total_call?.team_data?.current_rate_last_current || '0%' }} vs 上期</span> <span class="card-trend negative">{{ weekTotalData.week_total_call?.team_data?.current_rate_last_current || '0%' }} vs 上期</span>
</div> </div>
<div class="card-value">{{ formatDuration(weekTotalData.week_total_call?.team_data?.total_call_duration)||0 }} 小时</div> <div class="card-value">{{ formatDuration(weekTotalData.week_total_call?.team_data?.total_call_duration)||0 }} 小时</div>
</div> </div>
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
<span class="card-title">新增意向客户</span> <span class="card-title">新增意向客户 <i class="info-icon" @mouseenter="showTooltip('newCustomers', $event)" @mouseleave="hideTooltip"></i></span>
<span class="card-trend positive">{{ weekTotalData.week_add_customer_total?.team_data?.week_rate_last_week || '0%' }} vs 上期</span> <span class="card-trend positive">{{ weekTotalData.week_add_customer_total?.team_data?.week_rate_last_week || '0%' }} vs 上期</span>
</div> </div>
<div class="card-value">{{ weekTotalData.week_add_customer_total?.team_data?.total_week_add_customer || 0 }} </div> <div class="card-value">{{ weekTotalData.week_add_customer_total?.team_data?.total_week_add_customer || 0 }} </div>
</div> </div>
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
<span class="card-title">新增成交</span> <span class="card-title">新增成交 <i class="info-icon" @mouseenter="showTooltip('newDeals', $event)" @mouseleave="hideTooltip"></i></span>
<span class="card-trend positive">{{ weekTotalData.week_add_deal_total?.team_data?.week_rate_last_week || '0%' }} vs 上期</span> <span class="card-trend positive">{{ weekTotalData.week_add_deal_total?.team_data?.week_rate_last_week || '0%' }} vs 上期</span>
</div> </div>
<div class="card-value">{{ weekTotalData.week_add_deal_total?.team_data?.total_week_add_deal || 0 }} </div> <div class="card-value">{{ weekTotalData.week_add_deal_total?.team_data?.total_week_add_deal || 0 }} </div>
</div> </div>
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
<span class="card-title">月度总业绩</span> <span class="card-title">月度总业绩 <i class="info-icon" @mouseenter="showTooltip('monthlyRevenue', $event)" @mouseleave="hideTooltip"></i></span>
<span class="card-trend positive">+8% vs 上月</span> <span class="card-trend positive">+8% vs 上月</span>
</div> </div>
<div class="card-value">{{ formatCurrency(weekTotalData.week_add_fee_total?.total_add_fee || 0) }} </div> <div class="card-value">{{ formatCurrency(weekTotalData.week_add_fee_total?.total_add_fee || 0) }} </div>
</div> </div>
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
<span class="card-title">定金转化率</span> <span class="card-title">定金转化率 <i class="info-icon" @mouseenter="showTooltip('conversionRate', $event)" @mouseleave="hideTooltip"></i></span>
<span class="card-trend positive">{{ weekTotalData.pay_deposit_to_money_rate?.team_data?.week_vs_last_week || '0%' }} vs 上期</span> <span class="card-trend positive">{{ weekTotalData.pay_deposit_to_money_rate?.team_data?.week_vs_last_week || '0%' }} vs 上期</span>
</div> </div>
<div class="card-value">{{ weekTotalData.pay_deposit_to_money_rate?.team_data?.week_pay_deposit_to_money_rate || '0%' }} </div> <div class="card-value">{{ weekTotalData.pay_deposit_to_money_rate?.team_data?.week_pay_deposit_to_money_rate || '0%' }} </div>
</div> </div>
</div> </div>
<!-- Tooltip 组件 -->
<Tooltip
:visible="tooltip.visible"
:x="tooltip.x"
:y="tooltip.y"
:title="tooltip.title"
:description="tooltip.description"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { watch } from 'vue' import { watch, reactive } from 'vue'
import Tooltip from '@/components/Tooltip.vue'
// 定义props // 定义props
const props = defineProps({ const props = defineProps({
@@ -82,6 +92,59 @@ const formatCurrency = (amount) => {
if (!amount) return '0' if (!amount) return '0'
return amount.toLocaleString() return amount.toLocaleString()
} }
// Tooltip 相关数据
const tooltip = reactive({
visible: false,
x: 0,
y: 0,
title: '',
description: ''
})
// 指标说明配置
const metricDescriptions = {
totalCalls: {
title: '团队总通话',
description: '团队所有成员在本期内的通话总次数,包括拨出和接听的所有电话。'
},
callDuration: {
title: '有效通话时长',
description: '团队所有成员有效通话的总时长,不包括未接通和短时间通话。'
},
newCustomers: {
title: '新增意向客户',
description: '本期新增的有明确购买意向的客户数量,通过沟通确认有需求的客户。'
},
newDeals: {
title: '新增成交',
description: '本期新增的成交订单数量,已确认付款或签约的客户订单。'
},
monthlyRevenue: {
title: '月度总业绩',
description: '本月团队累计完成的销售业绩总额,包括所有已确认的订单金额。'
},
conversionRate: {
title: '定金转化率',
description: '支付定金的客户数 ÷ 意向客户总数,反映客户从意向到付费的转化效果。'
}
}
// Tooltip 相关方法
const showTooltip = (metricType, event) => {
const description = metricDescriptions[metricType]
if (description) {
tooltip.title = description.title
tooltip.description = description.description
tooltip.x = event.clientX + 10
tooltip.y = event.clientY - 10
tooltip.visible = true
}
}
const hideTooltip = () => {
tooltip.visible = false
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -188,4 +251,30 @@ const formatCurrency = (amount) => {
border-radius: 8px; border-radius: 8px;
} }
} }
// 感叹号图标样式
.info-icon {
font-style: normal;
color: #409eff;
font-size: 12px;
margin-left: 4px;
opacity: 0.7;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
opacity: 1;
color: #007bff;
transform: scale(1.2);
}
}
.card-title:hover .info-icon {
opacity: 1;
}
.report-card {
position: relative;
transition: all 0.2s ease;
}
</style> </style>

View File

@@ -13,27 +13,27 @@
<div class="kpi-grid"> <div class="kpi-grid">
<div class="kpi-item"> <div class="kpi-item">
<div class="kpi-value">{{ props.kpiData.totalCalls }}</div> <div class="kpi-value">{{ props.kpiData.totalCalls }}</div>
<p>今日通话</p> <p>今日通话 <i class="info-icon" @mouseenter="showTooltip('totalCalls', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
<div class="kpi-item"> <div class="kpi-item">
<div class="kpi-value">{{ props.kpiData.successRate }}%</div> <div class="kpi-value">{{ props.kpiData.successRate }}%</div>
<p>电话接通率</p> <p>电话接通率 <i class="info-icon" @mouseenter="showTooltip('successRate', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
<div class="kpi-item"> <div class="kpi-item">
<div class="kpi-value">{{ props.kpiData.avgDuration }}<span class="kpi-unit">min</span></div> <div class="kpi-value">{{ props.kpiData.avgDuration }}<span class="kpi-unit">min</span></div>
<p>平均通话时长</p> <p>平均通话时长 <i class="info-icon" @mouseenter="showTooltip('avgDuration', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
<div class="kpi-item"> <div class="kpi-item">
<div class="kpi-value">{{ props.kpiData.conversionRate }}</div> <div class="kpi-value">{{ props.kpiData.conversionRate }}</div>
<p>成交转化率</p> <p>成交转化率 <i class="info-icon" @mouseenter="showTooltip('conversionRate', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
<div class="kpi-item"> <div class="kpi-item">
<div class="kpi-value">{{ props.kpiData.assignedData }}</div> <div class="kpi-value">{{ props.kpiData.assignedData }}</div>
<p>本期分配数据</p> <p>本期分配数据 <i class="info-icon" @mouseenter="showTooltip('assignedData', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
<div class="kpi-item"> <div class="kpi-item">
<div class="kpi-value">{{ props.kpiData.wechatAddRate }}</div> <div class="kpi-value">{{ props.kpiData.wechatAddRate }}</div>
<p>加微率</p> <p>加微率 <i class="info-icon" @mouseenter="showTooltip('wechatAddRate', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
</div> </div>
</div> </div>
@@ -95,10 +95,21 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 指标说明 Tooltip -->
<Tooltip
:visible="tooltip.visible"
:x="tooltip.x"
:y="tooltip.y"
:title="tooltip.title"
:description="tooltip.description"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import Tooltip from '@/components/Tooltip.vue';
import { ref, reactive, onMounted, onBeforeUnmount, computed, watch } from 'vue'; import { ref, reactive, onMounted, onBeforeUnmount, computed, watch } from 'vue';
import StatisticData from './StatisticData.vue'; import StatisticData from './StatisticData.vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
@@ -155,6 +166,35 @@ const chartInstances = {};
const personalFunnelChartCanvas = ref(null); const personalFunnelChartCanvas = ref(null);
const contactTimeChartCanvas = ref(null); const contactTimeChartCanvas = ref(null);
// Tooltip 相关数据
const tooltip = reactive({
visible: false,
x: 0,
y: 0,
title: '',
description: ''
});
// 指标说明配置
const kpiDescriptions = {
successRate: {
title: '电话接通率',
description: '拨通电话 ÷ 拨打的电话'
},
avgDuration: {
title: '平均通话时长',
description: '所有通话总时长 ÷ 拨打电话次数。'
},
conversionRate: {
title: '成交转化率',
description: '成交人数 ÷ 本期总数据。'
},
wechatAddRate: {
title: '加微率',
description: '加微人数 ÷ 本期数据总人数'
}
};
// Chart.js 数据 - 使用props传递的数据 // Chart.js 数据 - 使用props传递的数据
const funnelData = computed(() => props.funnelData); const funnelData = computed(() => props.funnelData);
const contactTimeData = computed(() => props.contactTimeData); const contactTimeData = computed(() => props.contactTimeData);
@@ -250,6 +290,22 @@ const getPercentage = (value) => {
const getRankingClass = (index) => ({ 'rank-first': index === 0, 'rank-second': index === 1, 'rank-third': index === 2, 'rank-other': index > 2 }); const getRankingClass = (index) => ({ 'rank-first': index === 0, 'rank-second': index === 1, 'rank-third': index === 2, 'rank-other': index > 2 });
const getRankBadgeClass = (index) => ({ 'badge-gold': index === 0, 'badge-silver': index === 1, 'badge-bronze': index === 2, 'badge-default': index > 2 }); const getRankBadgeClass = (index) => ({ 'badge-gold': index === 0, 'badge-silver': index === 1, 'badge-bronze': index === 2, 'badge-default': index > 2 });
// Tooltip 相关方法
const showTooltip = (kpiType, event) => {
const description = kpiDescriptions[kpiType];
if (description) {
tooltip.title = description.title;
tooltip.description = description.description;
tooltip.x = event.clientX + 10;
tooltip.y = event.clientY - 10;
tooltip.visible = true;
}
};
const hideTooltip = () => {
tooltip.visible = false;
};
watch(() => props.contactTimeData, () => { watch(() => props.contactTimeData, () => {
@@ -433,6 +489,8 @@ $white: #ffffff;
} }
// --- 图表区域 --- // --- 图表区域 ---
.charts-section { .charts-section {
display: grid; display: grid;
@@ -649,4 +707,32 @@ $white: #ffffff;
} }
.info-icon {
font-style: normal;
color: $blue;
font-size: 12px;
margin-left: 4px;
opacity: 0.7;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
opacity: 1;
color: #007bff;
transform: scale(1.2);
}
}
.kpi-item:hover .info-icon {
opacity: 1;
}
.kpi-item {
position: relative;
transition: all 0.2s ease;
}
</style> </style>

View File

@@ -2,72 +2,96 @@
<div class="stat-card kpi-card"> <div class="stat-card kpi-card">
<h3 class="card-title">统计指标</h3> <h3 class="card-title">统计指标</h3>
<div class="kpi-grid stats-grid-inner"> <div class="kpi-grid stats-grid-inner">
<div class="kpi-item stat-item"> <!-- 所有 .kpi-item 保持不变 -->
<div class="stat-icon customer-rate"> <div class="kpi-item stat-item" >
<i class="el-icon-chat-dot-round"></i> <div class="stat-icon customer-rate"><i class="el-icon-chat-dot-round"></i></div>
</div>
<div class="kpi-value">{{ customerCommunicationRate }}</div> <div class="kpi-value">{{ customerCommunicationRate }}</div>
<p>活跃客户沟通率</p> <p>活跃客户沟通率 <i class="info-icon" @mouseenter="showTooltip('customerCommunicationRate', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
<div class="kpi-item stat-item"> <div class="kpi-item stat-item" >
<div class="stat-icon response-time"> <div class="stat-icon response-time"><i class="el-icon-timer"></i></div>
<i class="el-icon-timer"></i>
</div>
<div class="kpi-value">{{ averageResponseTime }}<span class="kpi-unit">分钟</span></div> <div class="kpi-value">{{ averageResponseTime }}<span class="kpi-unit">分钟</span></div>
<p>平均应答时间</p> <p>平均应答时间 <i class="info-icon" @mouseenter="showTooltip('averageResponseTime', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
<div class="kpi-item stat-item"> <div class="kpi-item stat-item" >
<div class="stat-icon timeout-rate"> <div class="stat-icon timeout-rate"><i class="el-icon-warning"></i></div>
<i class="el-icon-warning"></i>
</div>
<div class="kpi-value">{{ timeoutResponseRate }}</div> <div class="kpi-value">{{ timeoutResponseRate }}</div>
<p>超时应答率</p> <p>超时应答率 <i class="info-icon" @mouseenter="showTooltip('timeoutResponseRate', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
<div class="kpi-item stat-item"> <div class="kpi-item stat-item">
<div class="stat-icon severe-timeout-rate"> <div class="stat-icon severe-timeout-rate"><i class="el-icon-warning-outline"></i></div>
<i class="el-icon-warning-outline"></i>
</div>
<div class="kpi-value">{{ severeTimeoutRate }}</div> <div class="kpi-value">{{ severeTimeoutRate }}</div>
<p>严重超时应答率</p> <p>严重超时应答率 <i class="info-icon" @mouseenter="showTooltip('severeTimeoutRate', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
<div class="kpi-item stat-item"> <div class="kpi-item stat-item">
<div class="stat-icon form-rate"> <div class="stat-icon form-rate"><i class="el-icon-document"></i></div>
<i class="el-icon-document"></i>
</div>
<div class="kpi-value">{{ formCompletionRate }}</div> <div class="kpi-value">{{ formCompletionRate }}</div>
<p>表格填写率</p> <p>表格填写率 <i class="info-icon" @mouseenter="showTooltip('formCompletionRate', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
</div> </div>
<!-- 指标说明 Tooltip -->
<Tooltip
:visible="tooltip.visible"
:x="tooltip.x"
:y="tooltip.y"
:title="tooltip.title"
:description="tooltip.description"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { defineProps } from 'vue'; import { defineProps, reactive } from 'vue';
import Tooltip from '@/components/Tooltip.vue';
defineProps({ defineProps({
customerCommunicationRate: { customerCommunicationRate: { type: Number, default: 0 },
type: Number, averageResponseTime: { type: Number, default: 0 },
default: 0 timeoutResponseRate: { type: Number, default: 0 },
}, severeTimeoutRate: { type: Number, default: 0 },
averageResponseTime: { formCompletionRate: { type: Number, default: 0 }
type: Number,
default: 0
},
timeoutResponseRate: {
type: Number,
default: 0
},
severeTimeoutRate: {
type: Number,
default: 0
},
formCompletionRate: {
type: Number,
default: 0
}
}); });
const tooltip = reactive({
visible: false, // 确保初始值为 false
x: 0,
y: 0,
title: '',
description: ''
});
const statDescriptions = {
customerCommunicationRate: { title: '活跃客户沟通率', description: '活跃沟通客户数 ÷ 总客户数' },
averageResponseTime: { title: '平均应答时间', description: '总应答时间 ÷ 应答次数' },
timeoutResponseRate: { title: '超时应答率', description: '超时应答次数 ÷ 总应答次数' },
severeTimeoutRate: { title: '严重超时应答率', description: '严重超时应答次数 ÷ 总应答次数' },
formCompletionRate: { title: '表格填写率', description: '已填写表格数 ÷ 总表格数' }
};
const showTooltip = (statKey, event) => {
console.log('Mouse entered! Firing showTooltip for:', statKey);
const description = statDescriptions[statKey];
if (description) {
tooltip.visible = true;
tooltip.x = event.clientX + 10;
tooltip.y = event.clientY + 15;
tooltip.title = description.title;
tooltip.description = description.description;
console.log('Tooltip state updated:', JSON.parse(JSON.stringify(tooltip))); // 使用 JSON 序列化来查看快照
}
};
const hideTooltip = () => {
console.log('Mouse left! Hiding tooltip.');
tooltip.visible = false;
};
</script> </script>
<style lang="scss" scoped>
/* ... 您的样式代码不变 ... */
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
// --- 颜色和变量定义 --- // --- 颜色和变量定义 ---
$slate-50: #f8fafc; $slate-50: #f8fafc;
@@ -157,6 +181,59 @@ $white: #ffffff;
} }
} }
// Info 图标样式
.info-icon {
font-size: 12px;
margin-left: 4px;
opacity: 0.7;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
opacity: 1;
color: #007bff;
transform: scale(1.2);
}
}
// Tooltip 样式
.stat-tooltip {
position: fixed;
z-index: 9999;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
max-width: 300px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
pointer-events: none;
.tooltip-title {
font-weight: 600;
margin-bottom: 4px;
color: #fff;
}
.tooltip-description {
font-size: 13px;
color: #e0e0e0;
line-height: 1.4;
}
}
// 统计项悬停效果
.stat-item {
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
// --- 响应式设计 --- // --- 响应式设计 ---
@media (max-width: 768px) { @media (max-width: 768px) {
.stats-grid-inner { .stats-grid-inner {