feat(api): 新增销售漏斗和黄金联络时段API接口
feat(views): 添加销售漏斗和黄金联络时段数据展示功能 refactor(views): 优化客户详情组件的数据处理逻辑 fix(views): 修复业绩数据显示字段不一致问题 style(views): 调整路由导航顶栏样式
This commit is contained in:
@@ -66,4 +66,14 @@ export const getCustomerCallInfo = (params) => {
|
|||||||
return https.post('/api/v1/sales_timeline/get_customer_call_info', params)
|
return https.post('/api/v1/sales_timeline/get_customer_call_info', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 销售漏斗 /api/v1/sales/sales_funnel
|
||||||
|
export const getSalesFunnel = (params) => {
|
||||||
|
return https.post('/api/v1/sales/sales_funnel', params)
|
||||||
|
}
|
||||||
|
// 黄金联络 /api/v1/sales/get_gold_contact_time
|
||||||
|
export const getGoldContactTime = (params) => {
|
||||||
|
return https.post('/api/v1/sales/get_gold_contact_time', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -143,67 +143,7 @@ const teamMembers = [
|
|||||||
newClients: 1,
|
newClients: 1,
|
||||||
deals: 0,
|
deals: 0,
|
||||||
avgDealValue: 0,
|
avgDealValue: 0,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: "陈雨",
|
|
||||||
rank: 6,
|
|
||||||
performance: 45000,
|
|
||||||
conversion: 3.2,
|
|
||||||
calls: 98,
|
|
||||||
callTime: 4.1,
|
|
||||||
newClients: 4,
|
|
||||||
deals: 1,
|
|
||||||
avgDealValue: 45000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
name: "周杰",
|
|
||||||
rank: 7,
|
|
||||||
performance: 38000,
|
|
||||||
conversion: 2.8,
|
|
||||||
calls: 115,
|
|
||||||
callTime: 5.3,
|
|
||||||
newClients: 5,
|
|
||||||
deals: 1,
|
|
||||||
avgDealValue: 38000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: "吴梅",
|
|
||||||
rank: 8,
|
|
||||||
performance: 22000,
|
|
||||||
conversion: 1.9,
|
|
||||||
calls: 87,
|
|
||||||
callTime: 3.7,
|
|
||||||
newClients: 3,
|
|
||||||
deals: 1,
|
|
||||||
avgDealValue: 22000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
name: "孙涛",
|
|
||||||
rank: 9,
|
|
||||||
performance: 15000,
|
|
||||||
conversion: 1.2,
|
|
||||||
calls: 92,
|
|
||||||
callTime: 4.0,
|
|
||||||
newClients: 2,
|
|
||||||
deals: 1,
|
|
||||||
avgDealValue: 15000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
name: "马丽",
|
|
||||||
rank: 10,
|
|
||||||
performance: 8000,
|
|
||||||
conversion: 0.8,
|
|
||||||
calls: 68,
|
|
||||||
callTime: 2.5,
|
|
||||||
newClients: 1,
|
|
||||||
deals: 1,
|
|
||||||
avgDealValue: 8000,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 路由实例
|
// 路由实例
|
||||||
|
|||||||
@@ -15,17 +15,17 @@
|
|||||||
<button
|
<button
|
||||||
@click="startSopAnalysis"
|
@click="startSopAnalysis"
|
||||||
class="analysis-button sop-button"
|
class="analysis-button sop-button"
|
||||||
:disabled="isSopAnalysisLoading"
|
:disabled="true"
|
||||||
>
|
>
|
||||||
{{ isSopAnalysisLoading ? 'SOP分析中...' : 'SOP通话分析' }}
|
{{ isSopAnalysisLoading ? 'SOP分析中...' : 'SOP通话分析' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<!-- <button
|
||||||
@click="startDemandAnalysis"
|
@click="startDemandAnalysis"
|
||||||
class="analysis-button demand-button"
|
class="analysis-button demand-button"
|
||||||
:disabled="isDemandAnalysisLoading"
|
:disabled="isDemandAnalysisLoading"
|
||||||
>
|
>
|
||||||
{{ isDemandAnalysisLoading ? '诉求分析中...' : '客户诉求分析' }}
|
{{ isDemandAnalysisLoading ? '诉求分析中...' : '客户诉求分析' }}
|
||||||
</button>
|
</button> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -65,8 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 下方整行区域 -->
|
<!-- 下方整行区域 -->
|
||||||
<div class="bottom-row">
|
<!-- <div class="bottom-row">
|
||||||
<!-- 客户诉求分析 -->
|
|
||||||
<div class="analysis-section demand-analysis">
|
<div class="analysis-section demand-analysis">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h4>客户诉求分析</h4>
|
<h4>客户诉求分析</h4>
|
||||||
@@ -80,7 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -101,6 +100,18 @@ const props = defineProps({
|
|||||||
selectedContact: {
|
selectedContact: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
formInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
chatRecords: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
callRecords: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,8 +126,8 @@ const isSopAnalysisLoading = ref(false); // SOP分析加载状态
|
|||||||
const isDemandAnalysisLoading = ref(false); // 诉求分析加载状态
|
const isDemandAnalysisLoading = ref(false); // 诉求分析加载状态
|
||||||
|
|
||||||
// Dify API配置
|
// Dify API配置
|
||||||
const DIFY_API_KEY_01 = 'app-wbR1P1j6kvdBK8Q1qXzdswzP';
|
const DIFY_API_KEY_01 = 'app-h4uBo5kOGoiYhjuBF1AHZi8b'; //基础信息分析
|
||||||
const DIFY_API_KEY = 'app-37VXHRieOnq17BSury9ONavG';
|
const DIFY_API_KEY = 'app-ZIJSFWbcdZLufkwCp9RrvpUR';
|
||||||
// 初始化ChatService
|
// 初始化ChatService
|
||||||
const chatService_01 = new SimpleChatService(DIFY_API_KEY_01);
|
const chatService_01 = new SimpleChatService(DIFY_API_KEY_01);
|
||||||
const chatService = new SimpleChatService(DIFY_API_KEY);
|
const chatService = new SimpleChatService(DIFY_API_KEY);
|
||||||
@@ -174,17 +185,82 @@ const startBasicAnalysis = async () => {
|
|||||||
isBasicAnalysisLoading.value = true;
|
isBasicAnalysisLoading.value = true;
|
||||||
basicAnalysisResult.value = '';
|
basicAnalysisResult.value = '';
|
||||||
|
|
||||||
|
// 构建表单信息
|
||||||
|
const formData = props.formInfo || {};
|
||||||
|
let formInfoText = '暂无表单信息';
|
||||||
|
|
||||||
|
if (Object.keys(formData).length > 0) {
|
||||||
|
const basicInfo = [];
|
||||||
|
const additionalInfo = [];
|
||||||
|
|
||||||
|
// 处理基础信息字段
|
||||||
|
const basicFields = {
|
||||||
|
name: '姓名',
|
||||||
|
mobile: '手机号',
|
||||||
|
occupation: '职业',
|
||||||
|
territory: '地区',
|
||||||
|
child_name: '孩子姓名',
|
||||||
|
child_gender: '孩子性别',
|
||||||
|
child_education: '孩子教育阶段',
|
||||||
|
child_relation: '与孩子关系'
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(basicFields).forEach(([key, label]) => {
|
||||||
|
if (formData[key] && formData[key] !== '暂无' && formData[key] !== '') {
|
||||||
|
basicInfo.push(`${label}: ${formData[key]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理 additional_info 数组
|
||||||
|
if (formData.additional_info && Array.isArray(formData.additional_info)) {
|
||||||
|
formData.additional_info.forEach(item => {
|
||||||
|
if (item.topic && item.answer) {
|
||||||
|
additionalInfo.push(`${item.topic}\n答案: ${item.answer}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组合所有信息
|
||||||
|
const allInfo = [];
|
||||||
|
if (basicInfo.length > 0) {
|
||||||
|
allInfo.push('=== 基础信息 ===');
|
||||||
|
allInfo.push(...basicInfo);
|
||||||
|
}
|
||||||
|
if (additionalInfo.length > 0) {
|
||||||
|
allInfo.push('\n=== 问卷信息 ===');
|
||||||
|
allInfo.push(...additionalInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
formInfoText = allInfo.length > 0 ? allInfo.join('\n') : '暂无表单信息';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建聊天记录信息
|
||||||
|
const chatData = props.chatRecords || [];
|
||||||
|
const chatInfoText = chatData.messages.length > 0 ?
|
||||||
|
`聊天记录数量: ${chatData.messages.length}条\n最近聊天内容: ${JSON.stringify(chatData.messages.slice(-3), null, 2)}` :
|
||||||
|
'暂无聊天记录';
|
||||||
|
|
||||||
|
// 构建通话记录信息
|
||||||
|
const callData = props.callRecords || [];
|
||||||
|
const callInfoText = callData.length > 0 ?
|
||||||
|
`通话记录数量: ${callData.length}次\n通话记录详情: ${JSON.stringify(callData, null, 2)}` :
|
||||||
|
'暂无通话记录';
|
||||||
|
|
||||||
const query = `请对客户进行基础信息分析:
|
const query = `请对客户进行基础信息分析:
|
||||||
客户姓名:${props.selectedContact.name}
|
客户姓名:${props.selectedContact.name}
|
||||||
联系电话:${props.selectedContact.phone || '未提供'}
|
联系电话:${props.selectedContact.phone || '未提供'}
|
||||||
邮箱:${props.selectedContact.email || '未提供'}
|
|
||||||
公司:${props.selectedContact.company || '未提供'}
|
|
||||||
职位:${props.selectedContact.position || '未提供'}
|
|
||||||
销售阶段:${props.selectedContact.salesStage || '未知'}
|
销售阶段:${props.selectedContact.salesStage || '未知'}
|
||||||
健康度:${props.selectedContact.health || '未知'}%
|
|
||||||
|
|
||||||
请分析客户的基本情况、背景信息和初步画像。`;
|
=== 表单信息 ===
|
||||||
|
${formInfoText}
|
||||||
|
|
||||||
|
=== 聊天记录 ===
|
||||||
|
${chatInfoText}
|
||||||
|
|
||||||
|
=== 通话记录 ===
|
||||||
|
${callInfoText}
|
||||||
|
|
||||||
|
请基于以上客户的表单信息、聊天记录和通话记录,分析客户的基本情况、背景信息和初步画像。`;
|
||||||
try {
|
try {
|
||||||
await chatService_01.sendMessage(
|
await chatService_01.sendMessage(
|
||||||
query,
|
query,
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, onBeforeUnmount, computed } 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';
|
||||||
import Chart from 'chart.js/auto';
|
import Chart from 'chart.js/auto';
|
||||||
@@ -130,10 +130,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
contactTimeData: {
|
contactTimeData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({})
|
||||||
labels: ['9-10点', '10-11点', '11-12点', '14-15点', '15-16点', '16-17点'],
|
|
||||||
data: [65, 85, 80, 92, 75, 60]
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
statisticsData: {
|
statisticsData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -176,12 +173,6 @@ const totalProblemCount = computed(() => {
|
|||||||
return props.urgentProblemData.reduce((sum, item) => sum + item.value, 0);
|
return props.urgentProblemData.reduce((sum, item) => sum + item.value, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 方法 ---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Chart.js: 创建或更新图表
|
// Chart.js: 创建或更新图表
|
||||||
const createOrUpdateChart = (chartId, canvasRef, config) => {
|
const createOrUpdateChart = (chartId, canvasRef, config) => {
|
||||||
if (chartInstances[chartId]) {
|
if (chartInstances[chartId]) {
|
||||||
@@ -198,10 +189,10 @@ const renderPersonalFunnelChart = () => {
|
|||||||
const config = {
|
const config = {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
labels: funnelData.labels,
|
labels: funnelData.value.labels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: '数量', data: funnelData.data,
|
label: '数量', data: funnelData.value.data,
|
||||||
backgroundColor: ['rgba(59, 130, 246, 0.8)', 'rgba(16, 185, 129, 0.8)', 'rgba(245, 158, 11, 0.8)', 'rgba(239, 68, 68, 0.8)'],
|
backgroundColor: ['rgba(59, 130, 246, 0.8)', 'rgba(16, 185, 129, 0.8)', 'rgba(245, 158, 11, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(168, 85, 247, 0.8)'],
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
@@ -219,12 +210,19 @@ const renderPersonalFunnelChart = () => {
|
|||||||
|
|
||||||
// Chart.js: 渲染黄金联络时段图
|
// Chart.js: 渲染黄金联络时段图
|
||||||
const renderContactTimeChart = () => {
|
const renderContactTimeChart = () => {
|
||||||
|
if (!props.contactTimeData || !props.contactTimeData.gold_contact_success_rate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = Object.keys(props.contactTimeData.gold_contact_success_rate);
|
||||||
|
const data = Object.values(props.contactTimeData.gold_contact_success_rate).map(rate => parseFloat(rate));
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: contactTimeData.labels,
|
labels: labels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: '成功率', data: contactTimeData.data,
|
label: '成功率', data: data,
|
||||||
borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||||
borderWidth: 3, tension: 0.4, fill: true, pointRadius: 4,
|
borderWidth: 3, tension: 0.4, fill: true, pointRadius: 4,
|
||||||
pointBackgroundColor: '#10b981', pointBorderColor: '#ffffff', pointBorderWidth: 2
|
pointBackgroundColor: '#10b981', pointBorderColor: '#ffffff', pointBorderWidth: 2
|
||||||
@@ -254,6 +252,10 @@ const getRankBadgeClass = (index) => ({ 'badge-gold': index === 0, 'badge-silver
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
watch(() => props.contactTimeData, () => {
|
||||||
|
renderContactTimeChart();
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
// --- 生命周期钩子 ---
|
// --- 生命周期钩子 ---
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -93,7 +93,16 @@
|
|||||||
<span class="call-duration">{{ call.duration }}</span>
|
<span class="call-duration">{{ call.duration }}</span>
|
||||||
<span class="call-time">{{ call.time }}</span>
|
<span class="call-time">{{ call.time }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="call-summary">{{ call.summary }}</div>
|
<div class="call-actions">
|
||||||
|
<button class="action-btn download-btn" @click="downloadRecording(call)">
|
||||||
|
<i class="icon-download"></i>
|
||||||
|
录音下载
|
||||||
|
</button>
|
||||||
|
<button class="action-btn view-btn" @click="viewTranscript(call)">
|
||||||
|
<i class="icon-view"></i>
|
||||||
|
查看原文
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,6 +239,43 @@ const callRecords = computed(() => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 录音下载方法
|
||||||
|
const downloadRecording = (call) => {
|
||||||
|
console.log('下载录音:', call)
|
||||||
|
|
||||||
|
// 检查是否有录音文件地址
|
||||||
|
if (call.record_file_addr_list && call.record_file_addr_list.length > 0) {
|
||||||
|
const recordingUrl = call.record_file_addr_list[0]
|
||||||
|
|
||||||
|
// 从URL中提取文件名
|
||||||
|
const urlParts = recordingUrl.split('/')
|
||||||
|
const fileName = urlParts[urlParts.length - 1]
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = recordingUrl
|
||||||
|
link.download = fileName
|
||||||
|
link.target = '_blank'
|
||||||
|
|
||||||
|
// 触发下载
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
|
||||||
|
console.log(`正在下载录音文件: ${fileName}`)
|
||||||
|
} else {
|
||||||
|
alert('该通话记录暂无录音文件')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看原文方法
|
||||||
|
const viewTranscript = (call) => {
|
||||||
|
// 这里可以根据实际需求实现查看原文逻辑
|
||||||
|
console.log('查看原文:', call)
|
||||||
|
// 示例:打开模态框显示通话原文
|
||||||
|
alert(`查看 ${call.time} 的通话原文:\n\n${call.summary}`)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -481,10 +527,56 @@ const callRecords = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-summary {
|
.call-actions {
|
||||||
font-size: 14px;
|
display: flex;
|
||||||
color: #374151;
|
gap: 12px;
|
||||||
line-height: 1.5;
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.download-btn {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #3b82f6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bfdbfe;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.view-btn {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #059669;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #a7f3d0;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
|
||||||
|
&.icon-download::before {
|
||||||
|
content: '⬇';
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon-view::before {
|
||||||
|
content: '👁';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sales-dashboard">
|
<div class="sales-dashboard">
|
||||||
<!-- 页面加载状态 -->
|
<!-- 页面加载状态 -->
|
||||||
<!-- <Loading :visible="isPageLoading" text="正在加载数据..." /> -->
|
<Loading :visible="isPageLoading" text="正在加载数据..." />
|
||||||
<!-- 顶部导航栏 -->
|
<!-- 顶部导航栏 -->
|
||||||
<!-- 销售时间线区域 -->
|
<!-- 销售时间线区域 -->
|
||||||
<section class="timeline-section">
|
<section class="timeline-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<!-- 动态顶栏:根据是否有路由参数显示不同内容 -->
|
<!-- 动态顶栏:根据是否有路由参数显示不同内容 -->
|
||||||
<!-- 路由跳转时的顶栏:面包屑 + 姓名 -->
|
<!-- 路由跳转时的顶栏:面包屑 + 姓名 -->
|
||||||
<div v-if="isRouteNavigation" class="route-header">
|
<div v-if="isRouteNavigation" class="route-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb" style="display: flex; flex-direction: column;">
|
||||||
<span class="breadcrumb-item" @click="goBack">团队管理</span>
|
<span class="breadcrumb-item" @click="goBack">团队管理 >{{ routeUserName }}</span>
|
||||||
<span class="breadcrumb-separator">></span>
|
<span class="breadcrumb-item current"> 数据驾驶舱</span>
|
||||||
<span class="breadcrumb-item current"> {{ routeUserName }}数据驾驶舱</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="user-name">
|
<div class="user-name">
|
||||||
{{ routeUserName }}
|
{{ routeUserName }}
|
||||||
@@ -67,6 +66,7 @@
|
|||||||
:selected-contact="selectedContact"
|
:selected-contact="selectedContact"
|
||||||
:form-info="formInfo"
|
:form-info="formInfo"
|
||||||
:chat-info="chatRecords"
|
:chat-info="chatRecords"
|
||||||
|
:call-info="callRecords"
|
||||||
@view-form-data="handleViewFormData"
|
@view-form-data="handleViewFormData"
|
||||||
@view-chat-data="handleViewChatData"
|
@view-chat-data="handleViewChatData"
|
||||||
@view-call-data="handleViewCallData" />
|
@view-call-data="handleViewCallData" />
|
||||||
@@ -83,7 +83,11 @@
|
|||||||
<h2>客户详情</h2>
|
<h2>客户详情</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
<CustomerDetail :selected-contact="selectedContact" />
|
<CustomerDetail
|
||||||
|
:selected-contact="selectedContact"
|
||||||
|
:form-info="formInfo"
|
||||||
|
:chat-records="chatRecords"
|
||||||
|
:call-records="callRecords" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -101,13 +105,12 @@
|
|||||||
v-else
|
v-else
|
||||||
:kpi-data="kpiData"
|
:kpi-data="kpiData"
|
||||||
:funnel-data="funnelData"
|
:funnel-data="funnelData"
|
||||||
:contact-time-data="contactTimeData"
|
:contact-time-data="goldContactTime"
|
||||||
:statistics-data="statisticsData"
|
:statistics-data="statisticsData"
|
||||||
:urgent-problem-data="urgentProblemData"
|
:urgent-problem-data="urgentProblemData"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -123,7 +126,7 @@ import UserDropdown from "@/components/UserDropdown.vue";
|
|||||||
import Loading from "@/components/Loading.vue";
|
import Loading from "@/components/Loading.vue";
|
||||||
import {getCustomerAttendance,getTodayCall,getProblemDistribution,getTableFillingRate,getAverageResponseTime,
|
import {getCustomerAttendance,getTodayCall,getProblemDistribution,getTableFillingRate,getAverageResponseTime,
|
||||||
getWeeklyActiveCommunicationRate,getTimeoutResponseRate,getCustomerCallInfo,getCustomerChatInfo,getCustomerFormInfo,
|
getWeeklyActiveCommunicationRate,getTimeoutResponseRate,getCustomerCallInfo,getCustomerChatInfo,getCustomerFormInfo,
|
||||||
getConversionRateAndAllocatedData,getCustomerAttendanceAfterClass4,getPayMoneyCustomers} from "@/api/api.js"
|
getConversionRateAndAllocatedData,getCustomerAttendanceAfterClass4,getPayMoneyCustomers,getSalesFunnel,getGoldContactTime} from "@/api/api.js"
|
||||||
|
|
||||||
// 路由实例
|
// 路由实例
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -429,10 +432,7 @@ async function getTimeline() {
|
|||||||
weChat_avatar: customer.weChat_avatar,
|
weChat_avatar: customer.weChat_avatar,
|
||||||
pay_status: customer.pay_status
|
pay_status: customer.pay_status
|
||||||
})
|
})
|
||||||
|
|
||||||
// 后三个阶段的客户数据已存储在courseCustomers['课1-4']中,不需要合并到customersList
|
// 后三个阶段的客户数据已存储在courseCustomers['课1-4']中,不需要合并到customersList
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 成交阶段
|
// 成交阶段
|
||||||
@@ -513,7 +513,15 @@ async function getCustomerCall() {
|
|||||||
const res = await getCustomerCallInfo(params)
|
const res = await getCustomerCallInfo(params)
|
||||||
if(res.code === 200) {
|
if(res.code === 200) {
|
||||||
callRecords.value = res.data
|
callRecords.value = res.data
|
||||||
|
/**
|
||||||
|
* "data": {
|
||||||
|
"user_name": "常琳",
|
||||||
|
"customer_name": "191桐桐爸爸高一男(婧)",
|
||||||
|
"record_file_addr_list": [
|
||||||
|
"http://192.168.3.112:5000/api/record/download/杨振彦-20分钟通话-25-08-19_07-23-37-744009-835.mp3"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 静默处理错误
|
// 静默处理错误
|
||||||
@@ -531,10 +539,26 @@ const selectedContact = computed(() => {
|
|||||||
return MOCK_DATA.contacts.find((c) => c.id === selectedContactId.value) || null;
|
return MOCK_DATA.contacts.find((c) => c.id === selectedContactId.value) || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const funnelData = computed(() => ({
|
const funnelData = computed(() => {
|
||||||
labels: ["线索", "沟通", "意向", "预约", "成交"],
|
if (!SalesFunnel.value || !SalesFunnel.value.sale_funnel) {
|
||||||
data: MOCK_DATA.personalFunnel,
|
return {
|
||||||
}));
|
labels: ["线索总数", "有效沟通", "到课数据", "预付定金", "成功签单"],
|
||||||
|
data: [0, 0, 0, 0, 0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const funnel = SalesFunnel.value.sale_funnel;
|
||||||
|
return {
|
||||||
|
labels: ["线索总数", "有效沟通", "到课数据", "预付定金", "成功签单"],
|
||||||
|
data: [
|
||||||
|
funnel.线索总数 || 0,
|
||||||
|
funnel.有效沟通 || 0,
|
||||||
|
funnel.到课数据 || 0,
|
||||||
|
funnel.预付定金 || 0,
|
||||||
|
funnel.成功签单 || 0
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const contactTimeData = computed(() => ({
|
const contactTimeData = computed(() => ({
|
||||||
labels: MOCK_DATA.contactTimeAnalysis.labels,
|
labels: MOCK_DATA.contactTimeAnalysis.labels,
|
||||||
@@ -682,7 +706,6 @@ const handleStageSelect = (stage, extraData = null) => {
|
|||||||
currentFilteredCustomers.value = [];
|
currentFilteredCustomers.value = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewFormData = async (contact) => {
|
const handleViewFormData = async (contact) => {
|
||||||
// 获取客户表单数据
|
// 获取客户表单数据
|
||||||
await getCustomerForm();
|
await getCustomerForm();
|
||||||
@@ -700,18 +723,54 @@ const handleViewChatData = async (contact) => {
|
|||||||
const handleViewCallData = (contact) => {
|
const handleViewCallData = (contact) => {
|
||||||
// TODO: 实现通话录音查看逻辑
|
// TODO: 实现通话录音查看逻辑
|
||||||
};
|
};
|
||||||
|
// 销售漏斗
|
||||||
|
const SalesFunnel = ref([])
|
||||||
|
async function CenterGetSalesFunnel() {
|
||||||
|
const params = getRequestParams()
|
||||||
|
const hasParams = params.user_name
|
||||||
|
const res = await getSalesFunnel(hasParams?params:undefined)
|
||||||
|
if(res.code === 200){
|
||||||
|
SalesFunnel.value = res.data
|
||||||
|
/**
|
||||||
|
* "data": {
|
||||||
|
"user_name": "常琳",
|
||||||
|
"user_level": 1,
|
||||||
|
"sale_funnel": {
|
||||||
|
"线索总数": 11,
|
||||||
|
"有效沟通": 9,
|
||||||
|
"到课数据": 8,
|
||||||
|
"预付定金": 0,
|
||||||
|
"成功签单": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 黄金联络时间段
|
||||||
|
const goldContactTime = ref([])
|
||||||
|
async function CenterGetGoldContactTime() {
|
||||||
|
const params = getRequestParams()
|
||||||
|
const hasParams = params.user_name
|
||||||
|
const res = await getGoldContactTime(hasParams?params:undefined)
|
||||||
|
if(res.code === 200){
|
||||||
|
goldContactTime.value = res.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LIFECYCLE HOOKS
|
// LIFECYCLE HOOKS
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
isPageLoading.value = true
|
isPageLoading.value = true
|
||||||
await getCoreKpi()
|
await getCoreKpi()
|
||||||
|
await CenterGetGoldContactTime()
|
||||||
|
await CenterGetSalesFunnel()
|
||||||
await getCustomerForm()
|
await getCustomerForm()
|
||||||
await getCustomerChat()
|
await getCustomerChat()
|
||||||
await getUrgentProblem()
|
await getUrgentProblem()
|
||||||
await getCustomerCall()
|
await getCustomerCall()
|
||||||
await getTimeline()
|
await getTimeline()
|
||||||
await getCustomerPayMoney()
|
await getCustomerPayMoney()
|
||||||
|
|
||||||
// 等待数据加载完成后选择默认客户
|
// 等待数据加载完成后选择默认客户
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
@@ -1323,10 +1382,8 @@ $primary: #3b82f6;
|
|||||||
|
|
||||||
// 路由导航顶栏样式
|
// 路由导航顶栏样式
|
||||||
.route-header {
|
.route-header {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 0 2rem;
|
||||||
|
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -112,11 +112,11 @@
|
|||||||
<div class="metric-row">
|
<div class="metric-row">
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<span class="metric-label">今日业绩</span>
|
<span class="metric-label">今日业绩</span>
|
||||||
<span class="metric-value">{{ formatCurrency(member.todayPerformance) }}</span>
|
<span class="metric-value">{{ member.todayPerformance}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<span class="metric-label">月度业绩</span>
|
<span class="metric-label">月度业绩</span>
|
||||||
<span class="metric-value">{{ formatCurrency(member.monthlyPerformance) }}</span>
|
<span class="metric-value">{{ member.monthlyPerformance }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,6 +147,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Loading 组件 -->
|
||||||
|
<Loading :visible="isLoading" text="数据加载中..." />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -163,16 +166,18 @@
|
|||||||
import ProblemRanking from './components/ProblemRanking.vue'
|
import ProblemRanking from './components/ProblemRanking.vue'
|
||||||
import seniorManager from './components/seniorManager.vue'
|
import seniorManager from './components/seniorManager.vue'
|
||||||
import UserDropdown from '@/components/UserDropdown.vue'
|
import UserDropdown from '@/components/UserDropdown.vue'
|
||||||
|
import Loading from '@/components/Loading.vue'
|
||||||
import {
|
import {
|
||||||
getOverallCenterPerformance, getTotalGroupCount, getCenterConversionRate, getTotalCallCount, getNewCustomer
|
getOverallCenterPerformance, getTotalGroupCount, getCenterConversionRate, getTotalCallCount, getNewCustomer
|
||||||
, getDepositConversionRate, getCustomerTypeDistribution, getUrgentNeedToAddress, getCenterAdvancedManagerList, getTeamRanking,
|
, getDepositConversionRate, getCustomerTypeDistribution, getUrgentNeedToAddress, getCenterAdvancedManagerList, getTeamRanking,
|
||||||
getTeamRankingInfo, getConversionRateVsAverage
|
getTeamRankingInfo, getConversionRateVsAverage
|
||||||
|
|
||||||
} from '@/api/secondTop.js'
|
} from '@/api/secondTop.js'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user.js'
|
import { useUserStore } from '@/stores/user.js'
|
||||||
// 组别数据
|
// 组别数据
|
||||||
const groups = ref([])
|
const groups = ref([])
|
||||||
|
// loading 状态
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
// 路由实例
|
// 路由实例
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -455,8 +460,8 @@ const conversionRateVsAverage = ref({})
|
|||||||
phone: '***-****-****', // 隐藏手机号
|
phone: '***-****-****', // 隐藏手机号
|
||||||
status: member.rank === 1 ? 'excellent' : member.rank === 2 ? 'good' : 'average',
|
status: member.rank === 1 ? 'excellent' : member.rank === 2 ? 'good' : 'average',
|
||||||
joinDate: '2023-01-01', // 默认入职日期
|
joinDate: '2023-01-01', // 默认入职日期
|
||||||
todayPerformance: member.today_performance || 0,
|
todayPerformance: member.today_deals,
|
||||||
monthlyPerformance: member.monthly_performance || 0,
|
monthlyPerformance: member.monthly_deals,
|
||||||
conversionRate: parseFloat(member.conversion_rate_this_period) || 0,
|
conversionRate: parseFloat(member.conversion_rate_this_period) || 0,
|
||||||
callCount: member.call_count_this_period || 0,
|
callCount: member.call_count_this_period || 0,
|
||||||
newClients: member.new_customers_this_period || 0,
|
newClients: member.new_customers_this_period || 0,
|
||||||
@@ -506,6 +511,8 @@ const conversionRateVsAverage = ref({})
|
|||||||
return statusMap[status] || '未知'
|
return statusMap[status] || '未知'
|
||||||
}
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
await CenterOverallCenterPerformance()
|
await CenterOverallCenterPerformance()
|
||||||
await CenterTotalGroupCount()
|
await CenterTotalGroupCount()
|
||||||
await CenterConversionRate()
|
await CenterConversionRate()
|
||||||
@@ -517,6 +524,11 @@ const conversionRateVsAverage = ref({})
|
|||||||
await CenterConversionRateVsAverage()
|
await CenterConversionRateVsAverage()
|
||||||
await CenterSeniorManagerList()
|
await CenterSeniorManagerList()
|
||||||
await CenterGroupList('all') // 初始化加载全部高级经理数据
|
await CenterGroupList('all') // 初始化加载全部高级经理数据
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据加载失败:', error)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<span class="label">今日业绩:</span>
|
<span class="label">今日业绩:</span>
|
||||||
<span class="value">{{ formatCurrency(selectedGroup.todayPerformance) }}</span>
|
<span class="value">{{ selectedGroup.todayPerformance }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<span class="label">转化率:</span>
|
<span class="label">转化率:</span>
|
||||||
@@ -100,12 +100,12 @@
|
|||||||
<div class="metric-row">
|
<div class="metric-row">
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<span class="metric-label">今日业绩</span>
|
<span class="metric-label">今日业绩</span>
|
||||||
<span class="metric-value">{{ member.today_performance }}</span>
|
<span class="metric-value">{{ member.today_deals }}</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<span class="metric-label">月度业绩</span>
|
<span class="metric-label">月度业绩</span>
|
||||||
<span class="metric-value">{{ member.monthly_performance }}</span>
|
<span class="metric-value">{{ member.monthly_deals}}</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,7 +201,6 @@ const getRequestParams = () => {
|
|||||||
if (routeUserName) {
|
if (routeUserName) {
|
||||||
params.user_name = routeUserName
|
params.user_name = routeUserName
|
||||||
}
|
}
|
||||||
|
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user