Compare commits
2 Commits
570f59f93f
...
af07a1e175
| Author | SHA1 | Date | |
|---|---|---|---|
| af07a1e175 | |||
| 0e0d297da7 |
@@ -4,7 +4,12 @@
|
||||
<div class="overview-grid">
|
||||
<div class="overview-card primary">
|
||||
<div class="card-header">
|
||||
<span class="card-title">中心总业绩</span>
|
||||
<span class="card-title">
|
||||
中心总业绩
|
||||
<i
|
||||
@mouseenter="showTooltip($event, 'centerPerformance')"
|
||||
@mouseleave="hideTooltip">ⓘ</i>
|
||||
</span>
|
||||
<span class="card-trend positive">{{ props.overallData.CenterPerformance?.center_monthly_vs_previous_deals }} vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">{{ props.overallData.CenterPerformance.center_monthly_deal_count || '552,000' }} 单</div>
|
||||
@@ -13,7 +18,12 @@
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">活跃组数</span>
|
||||
<span class="card-title">
|
||||
活跃组数
|
||||
<i
|
||||
@mouseenter="showTooltip($event, 'activeGroups')"
|
||||
@mouseleave="hideTooltip">ⓘ</i>
|
||||
</span>
|
||||
<span class="card-trend stable">{{ props.overallData.TotalGroupCount?.center_total_team_count}}/{{ props.overallData.TotalGroupCount?.center_total_team_count }} 组</span>
|
||||
</div>
|
||||
<div class="card-value">{{ props.overallData.TotalGroupCount?.center_total_team_count || '5' }} 组</div>
|
||||
@@ -22,7 +32,12 @@
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">中心转化率</span>
|
||||
<span class="card-title">
|
||||
中心转化率
|
||||
<i
|
||||
@mouseenter="showTooltip($event, 'conversionRate')"
|
||||
@mouseleave="hideTooltip">ⓘ</i>
|
||||
</span>
|
||||
<span class="card-trend positive">{{ props.overallData.CenterConversionRate?.center_monthly_vs_previous_deals }}vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">{{ props.overallData.CenterConversionRate?.center_conversion_rate || '5.2' }}</div>
|
||||
@@ -31,7 +46,12 @@
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">总通话次数</span>
|
||||
<span class="card-title">
|
||||
总通话次数
|
||||
<i
|
||||
@mouseenter="showTooltip($event, 'totalCalls')"
|
||||
@mouseleave="hideTooltip">ⓘ</i>
|
||||
</span>
|
||||
<span class="card-trend positive">{{ props.overallData.TotalCallCount?.total_call_count_vs_yesterday}} vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">{{ props.overallData.TotalCallCount?.total_call_count || '1,247' }} 次</div>
|
||||
@@ -40,7 +60,12 @@
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">新增客户</span>
|
||||
<span class="card-title">
|
||||
新增客户
|
||||
<i
|
||||
@mouseenter="showTooltip($event, 'newCustomers')"
|
||||
@mouseleave="hideTooltip">ⓘ</i>
|
||||
</span>
|
||||
<span class="card-trend positive">{{ props.overallData.NewCustomer?.center_new_leads_vs_previous_period }} vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">{{ props.overallData.NewCustomer?.center_new_leads_count || '117' }} 人</div>
|
||||
@@ -49,7 +74,12 @@
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">定金转化</span>
|
||||
<span class="card-title">
|
||||
定金转化
|
||||
<i
|
||||
@mouseenter="showTooltip($event, 'depositConversion')"
|
||||
@mouseleave="hideTooltip">ⓘ</i>
|
||||
</span>
|
||||
<span class="card-trend positive">{{ props.overallData.DepositConversionRate?.center_deposit_conversion_vs_previous }} vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">{{ props.overallData.DepositConversionRate?.center_current_deposit_conversion_rate || '0' }} </div>
|
||||
@@ -57,10 +87,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tooltip组件 -->
|
||||
<Tooltip
|
||||
:visible="tooltip.visible"
|
||||
:x="tooltip.x"
|
||||
:y="tooltip.y"
|
||||
:title="tooltip.title"
|
||||
:description="tooltip.description"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
import Tooltip from '@/components/Tooltip.vue'
|
||||
|
||||
// 中心整体概览组件
|
||||
const props = defineProps({
|
||||
overallData: {
|
||||
@@ -75,6 +117,58 @@ const props = defineProps({
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Tooltip状态管理
|
||||
const tooltip = reactive({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
title: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 指标描述信息
|
||||
const metricDescriptions = {
|
||||
centerPerformance: {
|
||||
title: '中心总业绩计算方式',
|
||||
description: '统计本月中心所有销售团队的成交单数总和,与上期同比计算增长率。月目标完成率 = 当月实际成交单数 / 月度目标单数 × 100%'
|
||||
},
|
||||
activeGroups: {
|
||||
title: '活跃组数计算方式',
|
||||
description: '统计当前有业务活动的销售团队数量。活跃标准:本月有通话记录或成交记录的团队。总人数为所有活跃团队的人员总和'
|
||||
},
|
||||
conversionRate: {
|
||||
title: '中心转化率计算方式',
|
||||
description: '中心转化率 = 总成交客户数 / 总接触客户数 × 100%。与上期对比显示增长趋势,行业平均值作为参考基准'
|
||||
},
|
||||
totalCalls: {
|
||||
title: '总通话次数计算方式',
|
||||
description: '统计所有销售人员的通话总次数,包括接听和拨出。有效通话指通话时长超过30秒的通话记录'
|
||||
},
|
||||
newCustomers: {
|
||||
title: '新增客户计算方式',
|
||||
description: '统计本期新录入系统的客户数量。意向客户指经过初步沟通,有明确购买意向的客户数量'
|
||||
},
|
||||
depositConversion: {
|
||||
title: '定金转化计算方式',
|
||||
description: '定金转化率 = 缴纳定金客户数 / 意向客户总数 × 100%。平均定金转化率为本月日均转化率'
|
||||
}
|
||||
}
|
||||
|
||||
// 显示tooltip
|
||||
const showTooltip = (event, metricType) => {
|
||||
const rect = event.target.getBoundingClientRect()
|
||||
tooltip.visible = true
|
||||
tooltip.x = rect.left + rect.width / 2
|
||||
tooltip.y = rect.top - 10
|
||||
tooltip.title = metricDescriptions[metricType].title
|
||||
tooltip.description = metricDescriptions[metricType].description
|
||||
}
|
||||
|
||||
// 隐藏tooltip
|
||||
const hideTooltip = () => {
|
||||
tooltip.visible = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -169,6 +263,39 @@ const props = defineProps({
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #409eff;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
background: #66b3ff;
|
||||
}
|
||||
}
|
||||
|
||||
// 主要卡片中的图标样式
|
||||
&.primary .info-icon {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trend-section {
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
<!-- 录音文件列表 -->
|
||||
<div v-if="!showTranscriptView">
|
||||
<div class="recording-list" v-if="recordings.length > 0">
|
||||
<div class="recording-list" v-if="processedRecordings.length > 0">
|
||||
<div
|
||||
v-for="(recording, index) in recordings"
|
||||
v-for="(recording, index) in processedRecordings"
|
||||
:key="index"
|
||||
class="recording-item"
|
||||
:class="{ active: selectedRecording === index }"
|
||||
@@ -29,8 +29,10 @@
|
||||
<div class="recording-info">
|
||||
<div class="recording-name" :title="recording.name">{{ recording.name.length > 10 ? recording.name.substring(0, 10) + '...' : recording.name }}</div>
|
||||
<div class="recording-meta">
|
||||
<span v-if="recording.sale_name" class="sale-name">{{ recording.sale_name }}</span>
|
||||
<span v-if="recording.score" class="score">评分: {{ recording.score }}</span>
|
||||
<span class="file-size">{{ formatFileSize(recording.size) }}</span>
|
||||
<span class="upload-time">{{ recording.uploadTime }}</span>
|
||||
<span class="upload-time">{{ recording.uploadTime || recording.date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="recording-actions">
|
||||
@@ -180,47 +182,16 @@ import MarkdownIt from 'markdown-it'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'GroupRank',
|
||||
name: 'GoodMusic',
|
||||
props: {
|
||||
recordData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
recordings: [
|
||||
{
|
||||
id: 1,
|
||||
name: '常家硕-张三丰-亮剑二部-20分钟通话-25-07-16_18-23-04-44196-215.mp3',
|
||||
size: 2048576, // 2MB
|
||||
duration: '00:03:45',
|
||||
date: '2024-01-15',
|
||||
url: '/recordings/sample_call_1.mp3',
|
||||
transcription: null
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '常家硕-张三丰-亮剑二部-20分钟通话-25-07-16_18-23-01-439240-599.mp3',
|
||||
size: 3145728, // 3MB
|
||||
duration: '00:05:20',
|
||||
date: '2024-01-14',
|
||||
url: '/recordings/sample_call_2.mp3',
|
||||
transcription: null
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '常家硕-张三丰-亮剑二部-20分钟通话-25-07-16_18-23-02-754615-508.mp3',
|
||||
size: 2048576, // 2MB
|
||||
duration: '00:03:45',
|
||||
date: '2024-01-15',
|
||||
url: '/recordings/sample_call_1.mp3',
|
||||
transcription: null
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '丁传辉-丁传辉-勇士二部-20分钟通话-25-07-10_10-32-54-813815-322.mp3',
|
||||
size: 3145728, // 3MB
|
||||
duration: '00:05:20',
|
||||
date: '2024-01-14',
|
||||
url: '/recordings/sample_call_2.mp3',
|
||||
transcription: null
|
||||
}
|
||||
],
|
||||
recordings: [ ],
|
||||
selectedRecording: null,
|
||||
currentAudio: null,
|
||||
showTranscriptView: false,
|
||||
@@ -252,6 +223,38 @@ export default {
|
||||
formattedAnalysisResult() {
|
||||
if (!this.analysisResult) return ''
|
||||
return this.md.render(this.analysisResult)
|
||||
},
|
||||
// 处理外部传入的录音数据
|
||||
processedRecordings() {
|
||||
let externalRecordings = []
|
||||
|
||||
// 处理外部传入的数据
|
||||
if (this.recordData && this.recordData.excellent_record_list) {
|
||||
const recordList = this.recordData.excellent_record_list
|
||||
|
||||
// 遍历每个销售人员的录音
|
||||
Object.keys(recordList).forEach(saleName => {
|
||||
recordList[saleName].forEach((record, index) => {
|
||||
externalRecordings.push({
|
||||
id: `${saleName}_${index}`,
|
||||
name: record.obj_file_name ? record.obj_file_name.split('/').pop() : `${record.sale_name}的录音`,
|
||||
size: 2048576, // 默认大小
|
||||
duration: '未知',
|
||||
date: new Date().toLocaleDateString(),
|
||||
url: record.obj_file_name,
|
||||
transcription: record.context || null,
|
||||
score: record.score,
|
||||
sale_name: record.sale_name,
|
||||
sop: record.sop,
|
||||
isPlaying: false,
|
||||
isConverting: false
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 合并外部数据和本地数据
|
||||
return [...externalRecordings, ...this.recordings]
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
@@ -274,7 +277,13 @@ export default {
|
||||
isConverting: false,
|
||||
transcript: null
|
||||
}
|
||||
// 如果没有外部数据,则添加到本地recordings数组
|
||||
if (!this.recordData || !this.recordData.excellent_record_list) {
|
||||
this.recordings.push(recording)
|
||||
} else {
|
||||
// 如果有外部数据,可以触发事件通知父组件或其他处理逻辑
|
||||
this.$emit('file-uploaded', recording)
|
||||
}
|
||||
// 清空input以便重复选择同一文件
|
||||
event.target.value = ''
|
||||
}
|
||||
@@ -285,12 +294,12 @@ export default {
|
||||
},
|
||||
// 播放/暂停录音
|
||||
togglePlay(index) {
|
||||
const recording = this.recordings[index]
|
||||
const recording = this.processedRecordings[index]
|
||||
|
||||
// 停止当前播放的音频
|
||||
if (this.currentAudio) {
|
||||
this.currentAudio.pause()
|
||||
this.recordings.forEach(r => r.isPlaying = false)
|
||||
this.processedRecordings.forEach(r => r.isPlaying = false)
|
||||
}
|
||||
|
||||
if (!recording.isPlaying) {
|
||||
@@ -304,47 +313,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
// 转换为文本
|
||||
async convertToText(index) {
|
||||
const recording = this.recordings[index]
|
||||
this.selectedRecording = index
|
||||
this.showTranscriptView = true
|
||||
this.isConverting = true
|
||||
this.currentTranscript = null
|
||||
this.currentViewType = 'transcript'
|
||||
|
||||
try {
|
||||
// 模拟转换过程
|
||||
await axios.post('http://192.168.3.104:8000/api/asr/sync?priority=10', {
|
||||
url: recording.url
|
||||
})
|
||||
|
||||
|
||||
// 这里应该调用实际的语音转文本API
|
||||
// 目前使用模拟数据
|
||||
recording.transcript = `这是 ${recording.name} 的转换文本示例。在实际应用中,这里会显示真实的语音转文本结果。您可以集成百度、阿里云、腾讯云等语音识别服务来实现真正的语音转文本功能。`
|
||||
this.currentTranscript = recording.transcript
|
||||
|
||||
// 转换完成后自动开始录音分析
|
||||
this.startRecordingAnalysis(recording)
|
||||
|
||||
// 添加转换完成的动画效果
|
||||
const resultElement = document.querySelector('.conversion-result')
|
||||
if (resultElement) {
|
||||
resultElement.classList.add('show-result')
|
||||
setTimeout(() => {
|
||||
resultElement.classList.remove('show-result')
|
||||
}, 1000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('转换失败:', error)
|
||||
alert('转换失败,请重试')
|
||||
this.showTranscriptView = false
|
||||
} finally {
|
||||
this.isConverting = false
|
||||
}
|
||||
},
|
||||
|
||||
// 开始通话录音分析
|
||||
async startRecordingAnalysis(recording) {
|
||||
this.isAnalyzing = true
|
||||
@@ -361,7 +329,7 @@ export default {
|
||||
录音信息:
|
||||
文件名:${recording.name}
|
||||
文件大小:${this.formatFileSize(recording.size)}
|
||||
转换文本:${recording.transcript}`
|
||||
转换文本:${recording.transcription}`
|
||||
|
||||
try {
|
||||
await this.chatService_02.sendMessage(
|
||||
@@ -436,18 +404,60 @@ export default {
|
||||
fileInput.click()
|
||||
document.body.removeChild(fileInput)
|
||||
},
|
||||
// 转换为文字
|
||||
convertToText(index) {
|
||||
const recording = this.processedRecordings[index]
|
||||
if (recording.isConverting) return
|
||||
|
||||
// 如果有外部传入的transcription,直接显示
|
||||
if (recording.transcription) {
|
||||
this.currentTranscript = recording.transcription
|
||||
this.showTranscriptView = true
|
||||
this.currentViewType = 'transcript'
|
||||
|
||||
// 开始录音分析
|
||||
this.switchViewType('analysis')
|
||||
this.startRecordingAnalysis(recording)
|
||||
return
|
||||
}
|
||||
|
||||
recording.isConverting = true
|
||||
this.isConverting = true
|
||||
this.showTranscriptView = true
|
||||
this.currentViewType = 'transcript'
|
||||
|
||||
// 模拟转换过程(用于本地上传的文件)
|
||||
setTimeout(() => {
|
||||
const transcriptText = `这是录音 "${recording.name}" 的转录文本内容。这里会显示实际的语音转文字结果。`
|
||||
recording.transcription = transcriptText
|
||||
this.currentTranscript = transcriptText
|
||||
recording.isConverting = false
|
||||
this.isConverting = false
|
||||
|
||||
// 开始录音分析
|
||||
this.startRecordingAnalysis(recording)
|
||||
}, 2000)
|
||||
},
|
||||
downloadRecording(index) {
|
||||
const recording = this.recordings[index]
|
||||
if (recording && recording.url) {
|
||||
const recording = this.processedRecordings[index]
|
||||
|
||||
// 使用obj_file_name字段作为下载链接
|
||||
const downloadUrl = recording.url
|
||||
|
||||
if (!downloadUrl) {
|
||||
alert('录音文件不可用')
|
||||
return
|
||||
}
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = recording.url
|
||||
link.href = downloadUrl
|
||||
link.download = recording.name
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -585,6 +595,16 @@ export default {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.sale-name {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.score {
|
||||
color: #67c23a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recording-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
@@ -797,11 +817,13 @@ export default {
|
||||
}
|
||||
|
||||
.transcript-text {
|
||||
padding: 16px;
|
||||
padding: 8px;
|
||||
line-height: 1.6;
|
||||
color: #303133;
|
||||
background: white;
|
||||
flex: 1;
|
||||
max-height: 200px;
|
||||
|
||||
overflow-y: auto;
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
@@ -819,7 +841,9 @@ export default {
|
||||
font-size: 14px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
padding: 8px;
|
||||
max-height: 200px;
|
||||
|
||||
}
|
||||
|
||||
.analysis-content h1,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="key-metrics">
|
||||
<div class="mini-metric">
|
||||
<span class="mini-label">业绩</span>
|
||||
<span class="mini-value">{{ formatCurrency(group.todayPerformance) }}</span>
|
||||
<span class="mini-value">{{ group.todayPerformance }}</span>
|
||||
</div>
|
||||
<div class="mini-metric">
|
||||
<span class="mini-label">转化</span>
|
||||
@@ -175,22 +175,22 @@ const processedGroups = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// 处理 formal_plural 数据
|
||||
// 处理 formal_plural 数据(业绩数据)
|
||||
if (props.groupList.formal_plural) {
|
||||
console.log('Processing formal_plural:', props.groupList.formal_plural)
|
||||
Object.entries(props.groupList.formal_plural).forEach(([managerName, teamData]) => {
|
||||
if (typeof teamData === 'object' && teamData !== null) {
|
||||
Object.entries(teamData).forEach(([teamName, count]) => {
|
||||
Object.entries(teamData).forEach(([teamName, performance]) => {
|
||||
const existingGroup = groups.find(g => g.id === `${managerName}-${teamName}` || g.id === managerName)
|
||||
if (existingGroup) {
|
||||
existingGroup.newClients = count || 0
|
||||
existingGroup.todayPerformance = performance || 0
|
||||
}
|
||||
})
|
||||
} else if (typeof teamData === 'number') {
|
||||
// 处理直接数值的情况
|
||||
const existingGroup = groups.find(g => g.id === managerName)
|
||||
if (existingGroup) {
|
||||
existingGroup.newClients = teamData || 0
|
||||
existingGroup.todayPerformance = teamData || 0
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17,7 +17,12 @@
|
||||
<!-- 1. 主卡片:中心总业绩 -->
|
||||
<div class="kpi-card primary">
|
||||
<div class="card-header">
|
||||
<span class="card-label">总成交单数</span>
|
||||
<span class="card-label">
|
||||
总成交单数
|
||||
<span class="info-icon"
|
||||
@mouseenter="showTooltip($event, 'totalSales')"
|
||||
@mouseleave="hideTooltip">!</span>
|
||||
</span>
|
||||
<span class="card-trend" :class="getTrendClass(kpiData.totalSales.trend)">
|
||||
{{ formatTrend(kpiData.totalSales.trend) }} vs 上期
|
||||
</span>
|
||||
@@ -34,7 +39,12 @@
|
||||
<!-- 2. 定金转化率 -->
|
||||
<div class="kpi-card">
|
||||
<div class="card-header">
|
||||
<span class="card-label">定金转化率</span>
|
||||
<span class="card-label">
|
||||
定金转化率
|
||||
<span class="info-icon"
|
||||
@mouseenter="showTooltip($event, 'depositConversion')"
|
||||
@mouseleave="hideTooltip">!</span>
|
||||
</span>
|
||||
<span class="card-trend" :class="getTrendClass(kpiData.activeTeams.trend)">
|
||||
{{ formatTrend(kpiData.activeTeams.trend, true) }} vs 上期
|
||||
</span>
|
||||
@@ -51,7 +61,12 @@
|
||||
<!-- 3. 总通话次数 -->
|
||||
<div class="kpi-card">
|
||||
<div class="card-header">
|
||||
<span class="card-label">总通话</span>
|
||||
<span class="card-label">
|
||||
总通话
|
||||
<span class="info-icon"
|
||||
@mouseenter="showTooltip($event, 'totalCalls')"
|
||||
@mouseleave="hideTooltip">!</span>
|
||||
</span>
|
||||
<span class="card-trend" :class="getTrendClass(kpiData.totalCalls.trend)">
|
||||
{{ formatTrend(kpiData.totalCalls.trend) }} vs 上期
|
||||
</span>
|
||||
@@ -68,7 +83,12 @@
|
||||
<!-- 4. 新增客户 -->
|
||||
<div class="kpi-card">
|
||||
<div class="card-header">
|
||||
<span class="card-label">新增客户</span>
|
||||
<span class="card-label">
|
||||
新增客户
|
||||
<span class="info-icon"
|
||||
@mouseenter="showTooltip($event, 'newCustomers')"
|
||||
@mouseleave="hideTooltip">!</span>
|
||||
</span>
|
||||
<span class="card-trend" :class="getTrendClass(kpiData.newCustomers.trend)">
|
||||
{{ formatTrend(kpiData.newCustomers.trend) }} vs 上期
|
||||
</span>
|
||||
@@ -85,7 +105,12 @@
|
||||
<!-- 5. 中心转化率 -->
|
||||
<div class="kpi-card">
|
||||
<div class="card-header">
|
||||
<span class="card-label">转化率</span>
|
||||
<span class="card-label">
|
||||
转化率
|
||||
<span class="info-icon"
|
||||
@mouseenter="showTooltip($event, 'conversionRate')"
|
||||
@mouseleave="hideTooltip">!</span>
|
||||
</span>
|
||||
<span class="card-trend" :class="getTrendClass(kpiData.conversionRate.trend)">
|
||||
{{ formatTrend(kpiData.conversionRate.trend, true) }} vs 上期
|
||||
</span>
|
||||
@@ -96,11 +121,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tooltip组件 -->
|
||||
<Tooltip
|
||||
:visible="tooltip.visible"
|
||||
:x="tooltip.x"
|
||||
:y="tooltip.y"
|
||||
:title="tooltip.title"
|
||||
:description="tooltip.description"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ref, computed, watch, reactive } from 'vue';
|
||||
import Tooltip from '@/components/Tooltip.vue';
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
@@ -117,6 +152,54 @@ const props = defineProps({
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
// Tooltip状态管理
|
||||
const tooltip = reactive({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
title: '',
|
||||
description: ''
|
||||
});
|
||||
|
||||
// 指标描述
|
||||
const metricDescriptions = {
|
||||
totalSales: {
|
||||
title: '总成交单数计算方式',
|
||||
description: '统计公司在选定时间范围内所有已完成的成交订单总数,包括各个中心、各个团队的成交业绩汇总。'
|
||||
},
|
||||
depositConversion: {
|
||||
title: '定金转化率计算方式',
|
||||
description: '定金转化率 = (支付定金客户数 / 意向客户总数) × 100%,反映从意向客户到付费客户的转化效果。'
|
||||
},
|
||||
totalCalls: {
|
||||
title: '总通话次数计算方式',
|
||||
description: '统计公司所有销售人员在选定时间范围内的外呼和接听通话总次数,包括有效通话和无效通话。'
|
||||
},
|
||||
newCustomers: {
|
||||
title: '新增客户计算方式',
|
||||
description: '统计在选定时间范围内新建档的客户数量,不包括重复录入的客户,按首次录入时间计算。'
|
||||
},
|
||||
conversionRate: {
|
||||
title: '中心转化率计算方式',
|
||||
description: '中心转化率 = (成交客户数 / 总客户数) × 100%,反映整体销售转化效果和业务质量。'
|
||||
}
|
||||
};
|
||||
|
||||
// 显示tooltip
|
||||
function showTooltip(event, metricType) {
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
tooltip.visible = true;
|
||||
tooltip.x = rect.left + rect.width / 2;
|
||||
tooltip.y = rect.top - 10;
|
||||
tooltip.title = metricDescriptions[metricType].title;
|
||||
tooltip.description = metricDescriptions[metricType].description;
|
||||
}
|
||||
|
||||
// 隐藏tooltip
|
||||
function hideTooltip() {
|
||||
tooltip.visible = false;
|
||||
}
|
||||
|
||||
// 计算属性:将API数据转换为组件需要的格式
|
||||
const kpiData = computed(() => {
|
||||
const data = props.kpiData;
|
||||
@@ -355,4 +438,39 @@ function formatTrend(trend, isPercentagePoint = false) {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 感叹号图标样式 */
|
||||
.info-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.info-icon:hover {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 非主卡片中的图标样式 */
|
||||
.kpi-card:not(.primary) .info-icon {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.kpi-card:not(.primary) .info-icon:hover {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user