fix(api): 修正二阶分析报告接口路径错误

refactor(views): 重构阶段分析报告展示逻辑,使用新接口数据格式

feat(analytics): 添加umami网站统计脚本

feat(api): 新增优秀录音文件获取接口

style(views): 优化分析报告样式和布局
This commit is contained in:
2025-11-07 20:53:40 +08:00
parent b0c2f28d7f
commit 5ff42dbbad
7 changed files with 157 additions and 185 deletions

View File

@@ -5,6 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title> <title>Vite + Vue</title>
<script defer src="https://umami.nycjy.cn/script.js" data-website-id="0d851950-9420-4c3e-a12a-c221fcf039b5"></script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -84,5 +84,5 @@ export const getCallSuccessRate = (params) => {
// 二阶分析报告 // 二阶分析报告
export const getSecondOrderAnalysisReport = (params) => { export const getSecondOrderAnalysisReport = (params) => {
return https.post('/api/v1/sales/get_call_text', params) return https.post('/api/v1/sales/get_second_analysis_report', params)
} }

View File

@@ -44,9 +44,9 @@ export const getGroupDetail = (params) => {
export const getGroupCallDuration = (params) => { export const getGroupCallDuration = (params) => {
return https.post('/api/v1/manager/group_call_duration', params) return https.post('/api/v1/manager/group_call_duration', params)
} }
// 二阶分析报告 /api/v1/sales/get_call_text // 二阶分析报告 /api/v1/sales/get_call_text
export const GetSecondOrderAnalysisReport = (params) => { export const GetSecondOrderAnalysisReport = (params) => {
return https.post('/api/v1/manager/group_call_text', params) return https.post('/api/v1/manager/group_second_report', params)
} }
// 通话分类数据 /api/v1/manager/get_member_call_classify // 通话分类数据 /api/v1/manager/get_member_call_classify

View File

@@ -79,6 +79,9 @@ export const getTeamManyTarget = (params) => {
return https.post('/api/v1/level_three/overview/get_team_many_target', params) return https.post('/api/v1/level_three/overview/get_team_many_target', params)
} }
// 优秀录音 /api/v1/level_three/overview/get_current_center_excellent_record_file
export const getExcellentRecordFile = (params) => {
return https.post('/api/v1/level_three/overview/get_current_center_excellent_record_file', params)
}

View File

@@ -85,11 +85,6 @@
<div class="guidance-section"> <div class="guidance-section">
<div class="guidance-header" @click="toggleGuidanceCollapse"> <div class="guidance-header" @click="toggleGuidanceCollapse">
<h3>💡 指导建议</h3> <h3>💡 指导建议</h3>
<div class="period-switcher">
<button @click="switchAnalysisPeriod('day')" :class="{ active: analysisPeriod === 'day' }">当日</button>
<button @click="switchAnalysisPeriod('camp')" :class="{ active: analysisPeriod === 'camp' }">当期</button>
<button @click="switchAnalysisPeriod('month')" :class="{ active: analysisPeriod === 'month' }">当月</button>
</div>
<div class="collapse-toggle" :class="{ 'collapsed': isGuidanceCollapsed }"> <div class="collapse-toggle" :class="{ 'collapsed': isGuidanceCollapsed }">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4l4 4H4l4-4z"/> <path d="M8 4l4 4H4l4-4z"/>
@@ -99,27 +94,17 @@
<div class="guidance-content" v-show="!isGuidanceCollapsed" :class="{ 'collapsing': isGuidanceCollapsed }"> <div class="guidance-content" v-show="!isGuidanceCollapsed" :class="{ 'collapsing': isGuidanceCollapsed }">
<!-- 分析报告内容 --> <!-- 分析报告内容 -->
<div class="analysis-report"> <div class="analysis-report">
<div v-if="isReportLoading" class="loading">正在生成分析报告...</div> <div v-if="isReportLoading" class="loading-message">正在生成分析报告...</div>
<div v-else class="report-content">{{ analysisReport }}</div> <div v-else-if="!analysisReport || (Array.isArray(analysisReport) && analysisReport.length === 0)" class="empty-message">
</div> 暂无分析报告数据
</div>
<!-- 原有指导建议内容 --> <div v-else-if="Array.isArray(analysisReport)">
<div class="guidance-cards"> <div v-for="(report, index) in analysisReport" :key="index" class="report-section">
<div class="guidance-card" v-if="getGuidanceForMember(selectedMember).length > 0"> <h4 class="report-title">{{ report.name }} ({{ report.start_time }} - {{ report.end_time }})</h4>
<div class="guidance-item" v-for="(guidance, index) in getGuidanceForMember(selectedMember)" :key="index"> <div class="report-content" v-html="report.report.replace(/\n/g, '<br>')"></div>
<div class="guidance-icon" :class="guidance.type">
{{ guidance.icon }}
</div>
<div class="guidance-content">
<h4 class="guidance-title">{{ guidance.title }}</h4>
<p class="guidance-description">{{ guidance.description }}</p>
<div class="guidance-action" v-if="guidance.action">
<span class="action-label">建议行动:</span>
<span class="action-text">{{ guidance.action }}</span>
</div>
</div>
</div> </div>
</div> </div>
<div v-else class="report-content">{{ analysisReport }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -224,53 +209,6 @@ const processedCallStats = computed(() => {
})).sort((a, b) => b.count - a.count); // 按通话次数降序排列 })).sort((a, b) => b.count - a.count); // 按通话次数降序排列
}); });
// 切换分析周期
const switchAnalysisPeriod = (period) => {
analysisPeriod.value = period
CenterGetSecondOrderAnalysisReport(analysisPeriod.value)
}
// 获取二阶分析报告
async function CenterGetSecondOrderAnalysisReport(time) {
if (!chatService_02.value) {
chatService_02.value = new SimpleChatService('app-MGaBOx5QFblsMZ7dSkxKJDKm')
}
isReportLoading.value = true
analysisReport.value = ''
try {
const params = {
user_name: props.memberDetails?.user_name,
time: time
}
const response = await GetSecondOrderAnalysisReport(params)
if (!response.data.records || response.data.records.length === 0) {
analysisReport.value = '当前周期暂无数据可供分析。'
isReportLoading.value = false
return
}
const records = response.data.records.join('\n')
chatService_02.value.sendMessage(
records,
(update) => {
analysisReport.value = update.content
},
() => {
isReportLoading.value = false
}
)
} catch (error) {
console.error('获取二阶分析报告失败:', error)
analysisReport.value = '获取分析报告失败,请稍后重试。'
isReportLoading.value = false
}
}
// 切换指导建议折叠状态 // 切换指导建议折叠状态
const toggleGuidanceCollapse = () => { const toggleGuidanceCollapse = () => {
isGuidanceCollapsed.value = !isGuidanceCollapsed.value isGuidanceCollapsed.value = !isGuidanceCollapsed.value
@@ -293,31 +231,6 @@ const hideTooltip = () => {
tooltip.visible = false tooltip.visible = false
} }
// 获取成员指导建议
const getGuidanceForMember = (member) => {
const guidance = []
if (!member) return guidance;
// 业绩相关建议
if (member.month_order_count === 0) {
guidance.push({ type: 'urgent', icon: '🚨', title: '业绩突破', description: '当前还未有成交记录,需要重点关注转化技巧和客户跟进。', action: '建议参加销售技巧培训,加强客户需求挖掘' })
} else if (member.month_order_count < 5) {
guidance.push({ type: 'warning', icon: '📈', title: '业绩提升', description: '业绩有提升空间,可以通过优化沟通策略来提高转化率。', action: '分析高业绩同事的沟通技巧,制定个人提升计划' })
}
// 转化率相关建议
if (member.conversion < 3.0) {
guidance.push({ type: 'urgent', icon: '🎯', title: '转化率优化', description: '转化率偏低,需要提升客户沟通和需求挖掘能力。', action: '重点学习客户心理分析和异议处理技巧' })
}
// 通话相关建议
if (member.call_count < 100) {
guidance.push({ type: 'warning', icon: '📞', title: '通话量提升', description: '通话量偏少,增加客户接触频次有助于提升业绩。', action: '制定每日通话计划,确保充足的客户接触量' })
}
return guidance.slice(0, 3);
}
// 【修改】函数现在从API获取数据并更新内部状态 // 【修改】函数现在从API获取数据并更新内部状态
async function updateCallClassificationData() { async function updateCallClassificationData() {
if (props.selectedMember && props.selectedMember.user_name) { if (props.selectedMember && props.selectedMember.user_name) {
@@ -325,6 +238,7 @@ async function updateCallClassificationData() {
const response = await getMemberCallClassify({ const response = await getMemberCallClassify({
user_name: props.selectedMember.user_name user_name: props.selectedMember.user_name
}); });
console.log('获取通话分类数据:', response.data);
// 将获取到的数据赋值给内部状态 // 将获取到的数据赋值给内部状态
callClassificationData.value = { callClassificationData.value = {
call_count_by_tag: response.call_count_by_tag || {}, call_count_by_tag: response.call_count_by_tag || {},
@@ -341,14 +255,33 @@ async function updateCallClassificationData() {
} }
} }
// 获取二阶分析报告数据
async function fetchAnalysisReport() {
if (props.selectedMember && props.selectedMember.user_name) {
isReportLoading.value = true;
try {
const response = await GetSecondOrderAnalysisReport({
user_name: props.selectedMember.user_name
});
console.log('获取分析报告数据:', response.data);
// 将获取到的数据赋值给分析报告变量
analysisReport.value = response.data;
} catch (error) {
console.error('获取分析报告失败:', error);
analysisReport.value = '';
} finally {
isReportLoading.value = false;
}
}
}
// 监听selectedMember变化 // 监听selectedMember变化
watch(() => props.selectedMember, (newMember) => { watch(() => props.selectedMember, (newMember) => {
if (newMember) { if (newMember) {
// 成员变化时,获取新的通话分类数据 // 成员变化时,获取新的通话分类数据
updateCallClassificationData(); updateCallClassificationData();
// 同时获取新的分析报告 // 获取分析报告数据
CenterGetSecondOrderAnalysisReport(analysisPeriod.value); fetchAnalysisReport();
// 重置滚动位置 // 重置滚动位置
nextTick(() => { nextTick(() => {
const container = document.querySelector('.member-details') const container = document.querySelector('.member-details')
@@ -656,14 +589,80 @@ watch(() => props.selectedMember, (newMember) => {
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
min-height: 100px; min-height: 100px;
.loading { .loading-message {
text-align: center; text-align: center;
color: #64748b; color: #64748b;
font-style: italic; font-style: italic;
padding: 1rem; padding: 1rem;
} }
.report-content { .empty-message {
text-align: center;
color: #94a3b8;
padding: 2rem;
font-style: italic;
}
.report-section {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px dashed #e2e8f0;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.report-title {
font-size: 1rem;
font-weight: 600;
color: #1e293b;
margin: 0 0 0.75rem 0;
padding: 0.5rem 0.75rem;
background: #ffffff;
border-left: 3px solid #3b82f6;
border-radius: 2px;
}
.report-content {
white-space: pre-wrap;
line-height: 1.6;
color: #334155;
font-size: 0.9rem;
padding: 0 0.5rem;
h1, h2, h3 {
color: #1e293b;
margin: 1.25rem 0 0.75rem 0;
}
h3 {
font-size: 1.1rem;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 0.25rem;
}
p {
margin: 0.5rem 0;
}
ul, ol {
margin: 0.75rem 0;
padding-left: 1.5rem;
}
li {
margin: 0.25rem 0;
}
strong {
font-weight: 600;
}
}
}
.report-content:not(.report-section .report-content) {
white-space: pre-wrap; white-space: pre-wrap;
line-height: 1.5; line-height: 1.5;
color: #1e293b; color: #1e293b;

View File

@@ -111,33 +111,19 @@
<div class="modal-container"> <div class="modal-container">
<div class="modal-header"> <div class="modal-header">
<h3 class="modal-title">阶段分析报告</h3> <h3 class="modal-title">阶段分析报告</h3>
<div class="period-switcher">
<button @click="switchAnalysisPeriod('day')" :class="{ active: analysisPeriod === 'day' }">当日</button>
<button @click="switchAnalysisPeriod('camp')" :class="{ active: analysisPeriod === 'camp' }">当期</button>
<button @click="switchAnalysisPeriod('month')" :class="{ active: analysisPeriod === 'month' }">当月</button>
</div>
<button class="modal-close-btn" @click="closeAnalysisModal">&times;</button> <button class="modal-close-btn" @click="closeAnalysisModal">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="analysis-content"> <div class="analysis-content">
<div v-if="analysisPeriod === 'day'"> <div v-if="!analysisReport || Object.keys(analysisReport).length === 0" class="loading-message">正在生成分析报告...</div>
<h4>当日分析报告</h4> <div v-else-if="Array.isArray(analysisReport) && analysisReport.length === 0" class="error-message">数据为空</div>
<div v-if="analysisReport.day === '数据为空'" class="error-message">数据为空</div> <div v-else-if="Array.isArray(analysisReport)">
<div v-else-if="analysisReport.day" v-html="analysisReport.day.replace(/\n/g, '<br>')"></div> <div v-for="(report, index) in analysisReport" :key="index" class="report-section">
<p v-else>正在生成分析报告...</p> <h4>{{ report.name }} ({{ report.start_time }} {{ report.end_time }})</h4>
</div> <div v-html="report.report.replace(/\n/g, '<br>')"></div>
<div v-if="analysisPeriod === 'camp'"> </div>
<h4>当期分析报告</h4>
<div v-if="analysisReport.camp === '数据为空'" class="error-message">数据为空</div>
<div v-else-if="analysisReport.camp" v-html="analysisReport.camp.replace(/\n/g, '<br>')"></div>
<p v-else>正在生成分析报告...</p>
</div>
<div v-if="analysisPeriod === 'month'">
<h4>当月分析报告</h4>
<div v-if="analysisReport.month === '数据为空'" class="error-message">数据为空</div>
<div v-else-if="analysisReport.month" v-html="analysisReport.month.replace(/\n/g, '<br>')"></div>
<p v-else>正在生成分析报告...</p>
</div> </div>
<div v-else class="error-message">数据格式错误</div>
</div> </div>
</div> </div>
</div> </div>
@@ -220,44 +206,12 @@ const props = defineProps({
} }
}); });
async function CenterGetSecondOrderAnalysisReport(time) { async function CenterGetSecondOrderAnalysisReport() {
const params = getRequestParams() const params = getRequestParams()
const hasParams = {...params,time:time} const res = await getSecondOrderAnalysisReport(params)
const res = await getSecondOrderAnalysisReport(hasParams) if (res.code === 200) {
if (res.code === 200) { console.log(11111,res.data)
analysisReport.value = res.data
const records = res.data.records.join('\n')
// 检查数据是否为空
if (!records) {
console.error('数据为空')
// 将错误信息存储到对应的响应式变量中
analysisReport.value[time] = '数据为空'
return
}
const prompt = `请分析以下数据:\n${records}\n请提供一个阶段分析报告。`
console.log(prompt)
// 使用sendMessage方法替代chat方法
try {
await chatService_02.sendMessage(
prompt,
(update) => {
// 实时更新回调
if (!update.isStreaming) {
console.log('阶段分析报告:', update.content)
// 将结果存储到对应的响应式变量中
analysisReport.value[time] = update.content
}
},
() => {
// 流结束回调
console.log('阶段分析报告生成完成')
}
)
} catch (error) {
console.error('获取阶段分析报告失败:', error)
}
} }
} }
// Chart.js 实例 // Chart.js 实例
@@ -427,17 +381,12 @@ const hideTooltip = () => {
// 阶段分析报告模态框状态 // 阶段分析报告模态框状态
const showAnalysisModal = ref(false); const showAnalysisModal = ref(false);
const analysisPeriod = ref('day'); // 'day', 'camp', 'month' // 阶段分析报告数据
const analysisReport = ref({ const analysisReport = ref({});
day: '',
camp: '',
month: ''
});
// 显示阶段分析报告模态框 // 显示阶段分析报告模态框
const showSecondOrderAnalysisReport = () => { const showSecondOrderAnalysisReport = () => {
showAnalysisModal.value = true; showAnalysisModal.value = true;
CenterGetSecondOrderAnalysisReport(analysisPeriod.value) CenterGetSecondOrderAnalysisReport()
}; };
// 关闭阶段分析报告模态框 // 关闭阶段分析报告模态框
@@ -445,14 +394,6 @@ const closeAnalysisModal = () => {
showAnalysisModal.value = false; showAnalysisModal.value = false;
}; };
// 切换分析周期
const switchAnalysisPeriod = (period) => {
analysisPeriod.value = period;
CenterGetSecondOrderAnalysisReport(period)
};
watch(() => props.contactTimeData, () => { watch(() => props.contactTimeData, () => {
renderContactTimeChart(); renderContactTimeChart();
}, { deep: true }); }, { deep: true });
@@ -1021,4 +962,31 @@ $white: #ffffff;
border-radius: 4px; border-radius: 4px;
background-color: #fef0f0; background-color: #fef0f0;
} }
.loading-message {
text-align: center;
padding: 20px;
color: #909399;
}
.report-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ebeef5;
border-radius: 4px;
background-color: #f9fafc;
}
.report-section:last-child {
margin-bottom: 0;
}
.report-section h4 {
margin-top: 0;
margin-bottom: 15px;
color: #303133;
font-size: 16px;
border-bottom: 1px solid #ebeef5;
padding-bottom: 8px;
}
</style> </style>

View File

@@ -84,7 +84,7 @@
@select-group="selectGroup" @select-group="selectGroup"
/> />
</div> </div>
<GoodMusic style="height: 300px;" :abnormalData="teamAlerts" /> <GoodMusic style="height: 300px;" :qualityCalls="excellentRecord" />
<!-- Right Section - Group Comparison --> <!-- Right Section - Group Comparison -->
<div v-if="cardVisibility.groupComparison" class="right-section"> <div v-if="cardVisibility.groupComparison" class="right-section">
<GroupComparison <GroupComparison
@@ -212,8 +212,8 @@ import Loading from '@/components/Loading.vue'
import PerformanceComparison from './components/PerformanceComparison.vue'; // 1. 导入新组件 import PerformanceComparison from './components/PerformanceComparison.vue'; // 1. 导入新组件
import { getOverallTeamPerformance,getTotalGroupCount,getConversionRate,getTotalCallCount, import { getOverallTeamPerformance,getTotalGroupCount,getConversionRate,getTotalCallCount,
getNewCustomer,getDepositConversionRate,getActiveCustomerCommunicationRate,getAverageAnswerTime, getNewCustomer,getDepositConversionRate,getActiveCustomerCommunicationRate,getAverageAnswerTime,
getTimeoutRate,getTableFillingRate,getUrgentNeedToAddress,getTeamRanking,getTeamRankingInfo,getAbnormalResponseRate,getTeamSalesFunnel } from '@/api/senorManger.js' getTimeoutRate,getTableFillingRate,getUrgentNeedToAddress,getTeamRanking,getTeamRankingInfo,
import { getExcellentRecordFile } from '@/api/top.js' getAbnormalResponseRate,getTeamSalesFunnel,getExcellentRecordFile } from '@/api/senorManger.js'
import { useUserStore } from '@/stores/user.js' import { useUserStore } from '@/stores/user.js'
import FeedbackForm from "@/components/FeedbackForm.vue"; import FeedbackForm from "@/components/FeedbackForm.vue";
@@ -274,6 +274,7 @@ async function CenterExcellentRecord() {
const cacheKey = getCacheKey('CenterExcellentRecord', params); const cacheKey = getCacheKey('CenterExcellentRecord', params);
const result = await withCache(cacheKey, async () => { const result = await withCache(cacheKey, async () => {
const res = await getExcellentRecordFile(params); const res = await getExcellentRecordFile(params);
console.log(6666666666,res);
return res.data; return res.data;
}); });
excellentRecord.value = result; excellentRecord.value = result;