feat(api): 新增销售漏斗和黄金联络时段API接口
feat(views): 添加销售漏斗和黄金联络时段数据展示功能 refactor(views): 优化客户详情组件的数据处理逻辑 fix(views): 修复业绩数据显示字段不一致问题 style(views): 调整路由导航顶栏样式
This commit is contained in:
@@ -15,17 +15,17 @@
|
||||
<button
|
||||
@click="startSopAnalysis"
|
||||
class="analysis-button sop-button"
|
||||
:disabled="isSopAnalysisLoading"
|
||||
:disabled="true"
|
||||
>
|
||||
{{ isSopAnalysisLoading ? 'SOP分析中...' : 'SOP通话分析' }}
|
||||
</button>
|
||||
<button
|
||||
<!-- <button
|
||||
@click="startDemandAnalysis"
|
||||
class="analysis-button demand-button"
|
||||
:disabled="isDemandAnalysisLoading"
|
||||
>
|
||||
{{ isDemandAnalysisLoading ? '诉求分析中...' : '客户诉求分析' }}
|
||||
</button>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,8 +65,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 下方整行区域 -->
|
||||
<div class="bottom-row">
|
||||
<!-- 客户诉求分析 -->
|
||||
<!-- <div class="bottom-row">
|
||||
<div class="analysis-section demand-analysis">
|
||||
<div class="section-header">
|
||||
<h4>客户诉求分析</h4>
|
||||
@@ -80,7 +79,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,6 +100,18 @@ const props = defineProps({
|
||||
selectedContact: {
|
||||
type: Object,
|
||||
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); // 诉求分析加载状态
|
||||
|
||||
// Dify API配置
|
||||
const DIFY_API_KEY_01 = 'app-wbR1P1j6kvdBK8Q1qXzdswzP';
|
||||
const DIFY_API_KEY = 'app-37VXHRieOnq17BSury9ONavG';
|
||||
const DIFY_API_KEY_01 = 'app-h4uBo5kOGoiYhjuBF1AHZi8b'; //基础信息分析
|
||||
const DIFY_API_KEY = 'app-ZIJSFWbcdZLufkwCp9RrvpUR';
|
||||
// 初始化ChatService
|
||||
const chatService_01 = new SimpleChatService(DIFY_API_KEY_01);
|
||||
const chatService = new SimpleChatService(DIFY_API_KEY);
|
||||
@@ -174,17 +185,82 @@ const startBasicAnalysis = async () => {
|
||||
isBasicAnalysisLoading.value = true;
|
||||
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 = `请对客户进行基础信息分析:
|
||||
客户姓名:${props.selectedContact.name}
|
||||
联系电话:${props.selectedContact.phone || '未提供'}
|
||||
邮箱:${props.selectedContact.email || '未提供'}
|
||||
公司:${props.selectedContact.company || '未提供'}
|
||||
职位:${props.selectedContact.position || '未提供'}
|
||||
销售阶段:${props.selectedContact.salesStage || '未知'}
|
||||
健康度:${props.selectedContact.health || '未知'}%
|
||||
|
||||
请分析客户的基本情况、背景信息和初步画像。`;
|
||||
|
||||
=== 表单信息 ===
|
||||
${formInfoText}
|
||||
|
||||
=== 聊天记录 ===
|
||||
${chatInfoText}
|
||||
|
||||
=== 通话记录 ===
|
||||
${callInfoText}
|
||||
|
||||
请基于以上客户的表单信息、聊天记录和通话记录,分析客户的基本情况、背景信息和初步画像。`;
|
||||
try {
|
||||
await chatService_01.sendMessage(
|
||||
query,
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
</template>
|
||||
|
||||
<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 * as echarts from 'echarts';
|
||||
import Chart from 'chart.js/auto';
|
||||
@@ -130,10 +130,7 @@ const props = defineProps({
|
||||
},
|
||||
contactTimeData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
labels: ['9-10点', '10-11点', '11-12点', '14-15点', '15-16点', '16-17点'],
|
||||
data: [65, 85, 80, 92, 75, 60]
|
||||
})
|
||||
default: () => ({})
|
||||
},
|
||||
statisticsData: {
|
||||
type: Object,
|
||||
@@ -176,12 +173,6 @@ const totalProblemCount = computed(() => {
|
||||
return props.urgentProblemData.reduce((sum, item) => sum + item.value, 0);
|
||||
});
|
||||
|
||||
// --- 方法 ---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Chart.js: 创建或更新图表
|
||||
const createOrUpdateChart = (chartId, canvasRef, config) => {
|
||||
if (chartInstances[chartId]) {
|
||||
@@ -198,10 +189,10 @@ const renderPersonalFunnelChart = () => {
|
||||
const config = {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: funnelData.labels,
|
||||
labels: funnelData.value.labels,
|
||||
datasets: [{
|
||||
label: '数量', data: funnelData.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)'],
|
||||
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)', 'rgba(168, 85, 247, 0.8)'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
@@ -219,12 +210,19 @@ const renderPersonalFunnelChart = () => {
|
||||
|
||||
// Chart.js: 渲染黄金联络时段图
|
||||
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 = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: contactTimeData.labels,
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: '成功率', data: contactTimeData.data,
|
||||
label: '成功率', data: data,
|
||||
borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
borderWidth: 3, tension: 0.4, fill: true, pointRadius: 4,
|
||||
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(() => {
|
||||
|
||||
@@ -93,7 +93,16 @@
|
||||
<span class="call-duration">{{ call.duration }}</span>
|
||||
<span class="call-time">{{ call.time }}</span>
|
||||
</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>
|
||||
@@ -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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -481,10 +527,56 @@ const callRecords = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.call-summary {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
line-height: 1.5;
|
||||
.call-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
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: '👁';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user