Compare commits

...

27 Commits

Author SHA1 Message Date
b2ce9d93db fix(导出): 修复客户数据导出功能并增强表单数据处理
- 更新导出API端点以匹配后端接口变更
- 重构表单数据解析逻辑,支持多种数据结构格式
- 动态生成Excel列宽,避免数据截断
- 添加客户姓名字段到导出数据
- 优化重复字段处理,确保导出数据完整性
2026-01-29 19:58:21 +08:00
8260b345dc feat(表单信息展示): 支持多表单数据展示和筛选功能
- 重构表单数据解析逻辑,支持从数组格式中提取多表单数据
- 添加表单筛选按钮,可按表单标题筛选显示内容
- 优化数据结构处理,支持问答列表和旧格式的兼容
- 改进UI布局,添加表单标题和分割线提升可读性
2026-01-23 18:56:46 +08:00
2cd59adfc9 feat(person): 在销售时间轴任务列表中增加已完成用户分组显示
- 将待操作用户列表重构为独立的任务区块,根据选定阶段动态显示标题
- 新增已完成用户区块,根据销售阶段自动计算已完成联系人
- 为不同区块添加视觉区分样式,包括标题颜色和背景
- 添加阶段排序逻辑和表单完成状态检查函数
- 改进联系人数据构建函数以支持多种数据源
2026-01-23 18:07:34 +08:00
baa89001c8 style(manager): 简化按钮文本并调整样式
移除按钮的内边距,统一字体大小,简化切换按钮文本为"原文"和"分析"
2026-01-12 14:55:05 +08:00
9b3c5da105 feat(manager): 添加优秀录音文件获取及展示功能
新增获取优秀录音文件的API接口,并在管理页面添加GoodMusic组件展示录音文件列表
支持录音文件的下载、转文字及分析功能,优化了页面布局和间距
2026-01-12 14:50:30 +08:00
a4c0aca1c2 refactor(CustomerDetail): 重构基础信息分析逻辑为调用API
将原有的本地处理表单、聊天记录和通话记录的逻辑替换为直接调用后端API获取客户基础信息
2025-12-19 11:48:00 +08:00
9f19b8fb66 Merge branch 'Breach' of https://git.yinlihupo.cn/LiuRui/DJKB into Breach 2025-11-25 20:37:27 +08:00
14ddd2839b fix(manager): 修复团队异常预警中可选链操作符的使用
处理原始数据可能为undefined的情况,使用可选链操作符避免潜在的错误
2025-11-25 20:37:26 +08:00
6d2d3bda67 refactor(manager): 移除多余的part_count参数并简化请求参数
简化团队分析请求逻辑,直接使用getRequestParams()获取参数,移除硬编码的part_count字段
2025-11-25 18:49:41 +08:00
b48b7749f2 feat(团队分析): 添加团队整体三阶分析报告功能
- 在api/manager.js中添加获取团队分析报告的接口
- 在TeamReport组件中添加分析按钮并触发事件
- 在manager.vue中实现团队分析弹窗及相关逻辑
- 添加样式美化分析报告展示
2025-11-25 17:53:54 +08:00
2ba88eff08 feat(部门分析): 添加部门整体分析报告功能
实现部门分析弹窗的数据获取与展示功能,新增getTeamEntiretyReport API接口
调整多个组件高度以优化布局
2025-11-25 16:59:43 +08:00
6cf6829334 feat(团队分析): 添加团队和部门分析功能及弹窗
- 在api中添加获取团队各组分析报告的接口
- 在secondTop和seniorManager视图中添加团队分析弹窗组件
- 实现部门分析弹窗功能
- 添加样式和交互逻辑处理团队分析数据展示
2025-11-25 16:11:07 +08:00
9d52b99414 refactor: 移除调试用的console.log语句
清理多个组件和工具文件中用于调试的console.log语句,保持代码整洁
2025-11-25 15:31:40 +08:00
b9f74dc810 fix(统计指标): 修复数据未加载时显示异常问题
修改统计指标组件和父组件的数据处理逻辑,确保在数据未加载或返回null时显示默认值0。同时将props类型从Number改为Object,并添加默认空对象防止访问属性错误。
2025-11-25 15:26:34 +08:00
1647970bed fix(components): 为录音名称添加标题提示和文本溢出处理
添加标题提示以显示完整录音名称,并设置文本溢出样式防止布局问题
2025-11-21 19:36:34 +08:00
e696f768e6 feat(录音报告): 添加一键复制报告内容功能
添加一键复制按钮到录音报告模态框,支持现代Clipboard API和传统复制方法。同时将录音名称显示为客户姓名(如果存在),否则使用默认名称。
2025-11-21 16:35:00 +08:00
6b76d36946 feat: 更新应用图标、标题并调整弹窗尺寸
修改应用图标为NYC_logo,更新页面标题为"暖洋葱家庭教育数据看板"
调整sale.vue中弹窗的最大宽度和高度以适应更多内容
2025-11-19 15:21:36 +08:00
8e5f7335d8 feat(团队管理): 添加团队整体分析按钮并优化布局
在secondTop和seniorManager页面添加团队整体分析按钮
调整seniorManager页面的团队详情头部布局,使其更灵活
为按钮添加悬停和点击效果
2025-11-10 17:40:09 +08:00
5ff42dbbad fix(api): 修正二阶分析报告接口路径错误
refactor(views): 重构阶段分析报告展示逻辑,使用新接口数据格式

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

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

style(views): 优化分析报告样式和布局
2025-11-07 20:53:40 +08:00
b0c2f28d7f feat(录音管理): 添加优秀录音组件并优化API调用
refactor(性能优化): 使用Promise.all并行请求核心KPI接口

style(样式调整): 修改ProblemRanking组件高度和内边距

chore: 移除调试用的console.log语句
2025-10-29 17:11:57 +08:00
51091d097e 5555 2025-10-29 12:13:47 +08:00
86cf54b9de fix(router): 将首页重定向从/sale改为/login
refactor: 移除seniorManager.vue中多余的空白行
2025-10-27 12:11:25 +08:00
10fb9dd4f2 feat(销售时间线): 添加全部录音按钮及处理逻辑
新增全部录音按钮并实现其点击事件处理逻辑,同时将API端点从本地地址改为生产环境地址。处理函数结构与未归属录音类似,但调用不同的API接口获取数据。
2025-10-23 10:47:29 +08:00
2979e7e216 refactor(topOne): 优化优秀录音展示逻辑和样式
- 将excellentRecord和qualityCalls类型从Object改为Array以简化数据处理
- 添加录音分数显示并设置前三名特殊样式
- 调整录音列表的padding和布局
- 移除不必要的overflow-y属性
2025-10-22 18:03:59 +08:00
288a525537 refactor(ui): 调整多个组件的内边距和样式一致性
fix(GoodMusic): 修复录音列表数据结构和显示问题
style: 统一多个组件的头部内边距为10px 20px 10px
chore: 切换API基础路径为生产环境
2025-10-22 18:02:55 +08:00
db6433693a fix(topone): 修复获取优秀记录时返回数据格式不正确的问题
返回数据应从res.data.excellent_record_list改为直接返回res.data,以匹配接口返回格式
2025-10-22 17:38:05 +08:00
99efa8de75 fix(api): 修正获取优秀录音文件的API路径并实现功能
修复top.js和secondTop.js中获取优秀录音文件的API路径错误,将common路径改为正确的level_four和level_five路径
在secondTop.vue中实现获取优秀录音文件的功能,添加参数验证和错误处理
2025-10-22 17:34:34 +08:00
31 changed files with 4375 additions and 603 deletions

View File

@@ -2,9 +2,10 @@
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" href="/NYC_logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
<title>暖洋葱家庭教育数据看板</title>
<script defer src="https://umami.nycjy.cn/script.js" data-website-id="0d851950-9420-4c3e-a12a-c221fcf039b5"></script>
</head>
<body>
<div id="app"></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -84,5 +84,5 @@ export const getCallSuccessRate = (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) => {
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) => {
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
@@ -54,3 +54,12 @@ export const getMemberCallClassify = (params) => {
return https.post('/api/v1/manager/get_member_call_classify', params)
}
// 团队整体三阶分析报告 /api/v1/manager/group_entirety_third_report
export const getGroupEntiretyThirdReport = (params) => {
return https.post('/api/v1/manager/group_entirety_third_report', params)
}
// 获取优秀录音文件 /api/v1/level_five/overview/get_excellent_record_file
export const getExcellentRecordFile = (params) => {
return https.post('/api/v1/level_five/overview/get_excellent_record_file', params)
}

View File

@@ -66,7 +66,7 @@ export const getCampPeriodAdmin = (params) => {
}
// 获取优秀录音文件 /api/v1/level_four/overview/get_excellent_record_file
export const getExcellentRecordFile = (params) => {
return https.post('/api/v1/common/get_excellent_record_file', params)
return https.post('/api/v1/level_four/overview/get_excellent_record_file', params)
}
// 修改营期 /api/v1/level_four/overview/change_camp_period
export const changeCampPeriod = (params) => {
@@ -87,7 +87,7 @@ export const cancelSwitchHistoryCampPeriod = (params) => {
// 一键导出 api/v1/level_four/overview/export_customers
export const exportCustomers = (params) => {
return https.post('/api/v1/level_four/overview/export_customers', params)
return https.post('/api/v1/level_four/overview/export_all_customers_under_sales', params)
}

View File

@@ -79,6 +79,19 @@ export const getTeamManyTarget = (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)
}
// 团队下各组分析报告 /api/v1/level_three/overview/team_every_group_report
export const getTeamEveryGroupReport = (params) => {
return https.post('/api/v1/level_three/overview/team_every_group_report', params)
}
// 部门整体分析报告 /api/v1/level_three/overview/team_entirety_report
export const getTeamEntiretyReport = (params) => {
return https.post('/api/v1/level_three/overview/team_entirety_report', params)
}

View File

@@ -69,8 +69,8 @@ export const getDetailedDataTable = (params) => {
export const getPeriodStage = (params) => {
return https.get('/api/v1/level_five/overview/get_period_stage', params)
}
// 获取优秀录音文件 /api/v1/level_four/overview/get_excellent_record_file
// 获取优秀录音文件 /api/v1/level_five/overview/get_excellent_record_file
export const getExcellentRecordFile = (params) => {
return https.post('/api/v1/common/get_excellent_record_file', params)
return https.post('/api/v1/level_five/overview/get_excellent_record_file', params)
}

View File

@@ -124,12 +124,12 @@ const handleSubmit = async () => {
const token = localStorage.getItem('token') || '';
// 发送 POST 请求到后端接口
const response = await axios.post('https://mldash.nycjy.cn/api/v1/submit_feedback', {project:'mldash',type: formData.type, content: formData.content}, {
const response = await axios.post('https://feedback.api.nycjy.cn/api/v1/feedback/submit_feedback', {project:'mldash',type: formData.type, content: formData.content}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
console.log('响应状态8888:', response.data.message);
// console.log('响应状态8888:', response.data.message);
// 提交成功
submitStatus.value = 'success';
// 触发父组件的事件,并传递数据

View File

@@ -11,7 +11,7 @@ const routes = [
{
path: '/',
name: 'Home',
redirect: '/sale'
redirect: '/login'
},
{
path: '/login',

View File

@@ -5,8 +5,8 @@ import { useUserStore } from '@/stores/user'
// 创建axios实例
const service = axios.create({
// baseURL: 'https://mldash.nycjy.cn/' || '', // API基础路径支持完整URL
baseURL: 'http://192.168.15.121:8890' || '', // API基础路径支持完整URL
baseURL: 'https://mldash.nycjy.cn/' || '', // API基础路径支持完整URL
// baseURL: 'http://192.168.15.121:8890' || '', // API基础路径支持完整URL
timeout: 100000, // 请求超时时间
headers: {
'Content-Type': 'application/json;charset=UTF-8'
@@ -31,7 +31,6 @@ service.interceptors.request.use(
}
// 显示加载状态
if (config.showLoading !== false) {
console.log('显示加载中...')
}
return config
},

File diff suppressed because it is too large Load Diff

View File

@@ -85,11 +85,6 @@
<div class="guidance-section">
<div class="guidance-header" @click="toggleGuidanceCollapse">
<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 }">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4l4 4H4l4-4z"/>
@@ -99,27 +94,17 @@
<div class="guidance-content" v-show="!isGuidanceCollapsed" :class="{ 'collapsing': isGuidanceCollapsed }">
<!-- 分析报告内容 -->
<div class="analysis-report">
<div v-if="isReportLoading" class="loading">正在生成分析报告...</div>
<div v-else class="report-content">{{ analysisReport }}</div>
</div>
<!-- 原有指导建议内容 -->
<div class="guidance-cards">
<div class="guidance-card" v-if="getGuidanceForMember(selectedMember).length > 0">
<div class="guidance-item" v-for="(guidance, index) in getGuidanceForMember(selectedMember)" :key="index">
<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 v-if="isReportLoading" class="loading-message">正在生成分析报告...</div>
<div v-else-if="!analysisReport || (Array.isArray(analysisReport) && analysisReport.length === 0)" class="empty-message">
暂无分析报告数据
</div>
<div v-else-if="Array.isArray(analysisReport)">
<div v-for="(report, index) in analysisReport" :key="index" class="report-section">
<h4 class="report-title">{{ report.name }} ({{ report.start_time }} - {{ report.end_time }})</h4>
<div class="report-content" v-html="report.report.replace(/\n/g, '<br>')"></div>
</div>
</div>
<div v-else class="report-content">{{ analysisReport }}</div>
</div>
</div>
</div>
@@ -224,53 +209,6 @@ const processedCallStats = computed(() => {
})).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 = () => {
isGuidanceCollapsed.value = !isGuidanceCollapsed.value
@@ -293,31 +231,6 @@ const hideTooltip = () => {
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获取数据并更新内部状态
async function updateCallClassificationData() {
if (props.selectedMember && props.selectedMember.user_name) {
@@ -325,6 +238,7 @@ async function updateCallClassificationData() {
const response = await getMemberCallClassify({
user_name: props.selectedMember.user_name
});
console.log('获取通话分类数据:', response.data);
// 将获取到的数据赋值给内部状态
callClassificationData.value = {
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变化
watch(() => props.selectedMember, (newMember) => {
if (newMember) {
// 成员变化时,获取新的通话分类数据
updateCallClassificationData();
// 同时获取新的分析报告
CenterGetSecondOrderAnalysisReport(analysisPeriod.value);
// 获取分析报告数据
fetchAnalysisReport();
// 重置滚动位置
nextTick(() => {
const container = document.querySelector('.member-details')
@@ -656,14 +589,80 @@ watch(() => props.selectedMember, (newMember) => {
border: 1px solid #e2e8f0;
min-height: 100px;
.loading {
.loading-message {
text-align: center;
color: #64748b;
font-style: italic;
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;
line-height: 1.5;
color: #1e293b;

View File

@@ -1,6 +1,9 @@
<template>
<div class="team-report">
<h2>今日团队实时战报</h2>
<div class="header-container">
<h2>今日团队实时战报</h2>
<button class="analysis-button" @click="showTeamAnalysis">团队分析</button>
</div>
<div class="report-grid">
<div class="report-card">
<div class="card-header">
@@ -76,6 +79,9 @@ const props = defineProps({
}
})
// 定义emit
const emit = defineEmits(['show-team-analysis'])
// 监听数据变化,用于调试
watch(() => props.weekTotalData, (newData) => {
console.log('TeamReport 收到的数据:', newData)
@@ -146,6 +152,11 @@ const showTooltip = (metricType, event) => {
const hideTooltip = () => {
tooltip.visible = false
}
// 显示团队分析
const showTeamAnalysis = () => {
emit('show-team-analysis')
}
</script>
<style lang="scss" scoped>
@@ -156,11 +167,33 @@ const hideTooltip = () => {
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
h2 {
font-size: 1.2rem;
font-weight: 600;
color: #1e293b;
margin: 0 0 1.5rem 0;
margin: 0;
}
.analysis-button {
background: #409eff;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 0.9rem;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background: #337ecc;
}
}
.report-grid {

View File

@@ -37,9 +37,11 @@
<!-- Top Section - Team Alerts and Today's Report -->
<div class="top-section">
<!-- Team Alerts -->
<TeamAlerts :abnormalData="groupAbnormalResponse" />
<!-- Today's Team Report -->
<TeamReport :weekTotalData="weekTotalData" />
<!-- <TeamAlerts :abnormalData="groupAbnormalResponse" /> -->
<GoodMusic :quality-calls="excellentRecord"
/>
<!-- Today's Team Report -->
<TeamReport :weekTotalData="weekTotalData" @show-team-analysis="fetchTeamAnalysis" />
</div>
<!-- Sales Funnel Section -->
@@ -65,11 +67,30 @@
</div>
</main>
</div>
<!-- 团队分析弹窗 -->
<div v-if="showTeamAnalysisModal" class="modal-overlay" @click="showTeamAnalysisModal = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>团队整体三阶分析报告</h3>
<button class="close-button" @click="showTeamAnalysisModal = false">×</button>
</div>
<div class="modal-body">
<div v-for="(report, index) in teamAnalysisData" :key="index" class="report-item">
<div class="report-meta">
<span class="time-range">{{ report.start_time }} {{ report.end_time }}</span>
<span class="created-at">生成时间: {{ report.created_at }}</span>
</div>
<div class="report-content" v-html="formatReportContent(report.report)"></div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
import TeamAlerts from "./components/TeamAlerts.vue";
import GoodMusic from "./components/GoodMusic.vue";
import TeamReport from "./components/TeamReport.vue";
import SalesFunnel from "./components/SalesFunnel.vue";
import PerformanceRanking from "./components/PerformanceRanking.vue";
@@ -81,7 +102,7 @@ import CustomerDetail from "../person/components/CustomerDetail.vue";
import { useUserStore } from "@/stores/user";
import { useRouter } from "vue-router";
import {getGroupAbnormalResponse, getWeekTotalCall, getWeekAddCustomerTotal, getWeekAddDealTotal,
getWeekAddFeeTotal, getGroupFunnel,getPayDepositToMoneyRate,getGroupRanking, getGroupCallDuration,getGroupDetail} from "@/api/manager.js";
getWeekAddFeeTotal, getGroupFunnel,getPayDepositToMoneyRate,getGroupRanking, getGroupCallDuration,getGroupDetail, getGroupEntiretyThirdReport,getExcellentRecordFile} from "@/api/manager.js";
// 团队成员数据
const teamMembers = [
@@ -108,9 +129,10 @@ const userStore = useUserStore();
// 获取通用请求参数的函数
const getRequestParams = () => {
const params = {}
// 从路由参数获取
// 从路由参数获取
const routeUserLevel = router.currentRoute.value.query.user_level || router.currentRoute.value.params.user_level
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
// 如果路由有参数,使用路由参数
if (routeUserLevel) {
params.user_level = routeUserLevel.toString()
@@ -119,6 +141,14 @@ const getRequestParams = () => {
params.user_name = routeUserName
}
// 如果没有路由参数,使用当前登录用户的信息
if (!params.user_level && userStore.userInfo?.user_level) {
params.user_level = userStore.userInfo.user_level.toString()
}
if (!params.user_name && userStore.userInfo?.username) {
params.user_name = userStore.userInfo.username
}
return params
}
@@ -146,7 +176,7 @@ const weekTotalData = ref({
pay_deposit_to_money_rate: {},
group_funnel: {},
group_call_duration: {},
});
})
// 团队异常预警
const groupAbnormalResponse = ref({})
async function TeamGetGroupAbnormalResponse() {
@@ -161,9 +191,9 @@ async function TeamGetGroupAbnormalResponse() {
let alertId = 1
// 处理严重超时异常人员
const timeoutPersons = rawData.erious_timeout_rate_abnorma || []
const timeoutPersons = rawData?.serious_timeout_rate_abnorma || []
// 处理表格填写异常人员
const fillingPersons = rawData.table_filling_abnormal || []
const fillingPersons = rawData?.table_filling_abnormal || []
// 为每个异常人员生成独立的预警消息
@@ -234,7 +264,48 @@ async function TeamGetWeekAddDealTotal() {
weekTotalData.value.week_add_deal_total = res.data
}
}
// 月度总业绩
// 优秀录音
// 获取优秀录音
const excellentRecord = ref([]);
// 获取优秀录音文件
async function CentergetGoodRecord() {
console.log('CentergetGoodRecord 开始执行')
try {
const params = getRequestParams()
const params1 = {
user_level: userStore.userInfo?.user_level?.toString() || '',
user_name: userStore.userInfo?.username || ''
}
// 检查参数是否有效
const hasParams = params.user_name && params.user_level
const requestParams = hasParams ? {
...params,
} : params1
console.log('CentergetGoodRecord request params:', requestParams)
// 验证必要参数是否存在
if (!requestParams.user_name || !requestParams.user_level) {
console.error("缺少必要的请求参数:", requestParams);
return;
}
// 直接发送请求,不使用缓存
const res = await getExcellentRecordFile(requestParams)
console.log(972872132,res)
if (res && res.code === 200 && res.data) {
excellentRecord.value = res.data || []
console.log('获取优秀录音成功:', res.data)
} else {
console.error("获取优秀录音失败,响应数据不完整:", res);
excellentRecord.value = []
}
} catch (error) {
console.error("获取优秀录音失败:", error);
excellentRecord.value = []
}
}
// 定金转化
@@ -286,6 +357,10 @@ async function TeamGetGroupRanking() {
const memberDetails = ref({})
// 团队分析数据
const teamAnalysisData = ref([])
const showTeamAnalysisModal = ref(false)
// 当前选中的成员,默认为空
const selectedMember = ref(null);
@@ -318,18 +393,85 @@ week_order_count
}
}
// 获取团队分析数据
const fetchTeamAnalysis = async () => {
try {
showTeamAnalysisModal.value = true
const params = getRequestParams()
const response = await getGroupEntiretyThirdReport(params)
// 根据API响应结构调整数据处理逻辑
if (response.data) {
if (Array.isArray(response.data)) {
// 如果response.data本身就是数组
teamAnalysisData.value = response.data
} else if (response.data.data && Array.isArray(response.data.data)) {
// 如果response.data.data是数组
teamAnalysisData.value = response.data.data
} else {
// 其他情况,可能是单个对象
teamAnalysisData.value = [response.data]
}
}
} catch (error) {
console.error('获取团队分析数据失败:', error)
teamAnalysisData.value = []
}
}
// 格式化报告内容
const formatReportContent = (content) => {
if (!content || content === "None") {
return "<p>暂无分析报告内容</p>";
}
// 处理报告内容,保留换行和基本格式
let formattedContent = content
// 替换连续的换行符
.replace(/\n\s*\n/g, '</p><p>')
// 替换单个换行符为<br>
.replace(/\n/g, '<br>')
// 替换Markdown风格的标题为HTML标签
.replace(/^### (.*?)(<br>|$)/gim, '<h3>$1</h3>')
.replace(/^## (.*?)(<br>|$)/gim, '<h2>$1</h2>')
.replace(/^# (.*?)(<br>|$)/gim, '<h1>$1</h1>')
// 替换粗体
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
// 替换斜体
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// 替换无序列表项
.replace(/^\* (.*?)(<br>|$)/gim, '<li>$1</li>');
// 包装列表项到<ul>标签中
formattedContent = formattedContent.replace(/(<li>.*?<\/li>)+/g, '<ul>$&</ul>');
// 处理段落
if (!formattedContent.startsWith('<p>')) {
formattedContent = '<p>' + formattedContent;
}
if (!formattedContent.endsWith('</p>')) {
formattedContent = formattedContent + '</p>';
}
// 清理多余的<br>标签
formattedContent = formattedContent.replace(/<br><\/p>/g, '</p>');
return formattedContent;
}
// 团队异常预警
onMounted(async () => {
await TeamGetGroupAbnormalResponse()
await TeamGetWeekTotalCall()
await TeamGetGroupCallDuration()
await TeamGetWeekAddCustomerTotal()
await TeamGetWeekAddDealTotal()
await TeamGetWeekAddFeeTotal()
await TeamGetGroupFunnel()
await TeamGetGroupRanking()
CentergetGoodRecord()
TeamGetGroupAbnormalResponse()
TeamGetWeekTotalCall()
TeamGetGroupCallDuration()
TeamGetWeekAddCustomerTotal()
TeamGetWeekAddDealTotal()
TeamGetWeekAddFeeTotal()
TeamGetGroupFunnel()
TeamGetGroupRanking()
})
</script>
@@ -646,12 +788,12 @@ onMounted(async () => {
.top-section {
display: grid;
grid-template-columns: 1fr 3fr;
gap: 1rem;
gap: 0.5rem;
// PC端保持一致布局
@media (min-width: 1024px) {
grid-template-columns: 1fr 3fr;
gap: 1.5rem;
gap: 1rem;
}
// 平板端适配
@@ -676,7 +818,7 @@ onMounted(async () => {
.analytics-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
gap: 0.5rem;
margin-bottom: 1rem;
// PC端保持一致布局
@@ -1850,5 +1992,267 @@ onMounted(async () => {
}
}
}
/* 团队分析弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-width: 90vw;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #999;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
color: #333;
}
.modal-body {
padding: 1.5rem;
overflow-y: auto;
max-height: calc(90vh - 80px);
}
.report-item {
margin-bottom: 2rem;
}
.report-item:last-child {
margin-bottom: 0;
}
.report-meta {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
font-size: 0.9rem;
color: #666;
}
.report-content {
line-height: 1.6;
}
.report-content :deep(h1),
.report-content :deep(h2),
.report-content :deep(h3),
.report-content :deep(h4),
.report-content :deep(h5),
.report-content :deep(h6) {
margin: 1.5rem 0 1rem 0;
font-weight: 600;
}
.report-content :deep(h1) {
font-size: 1.75rem;
border-bottom: 2px solid #eee;
padding-bottom: 0.5rem;
}
.report-content :deep(h2) {
font-size: 1.5rem;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
.report-content :deep(h3) {
font-size: 1.25rem;
}
.report-content :deep(p) {
margin: 0.75rem 0;
}
.report-content :deep(ul),
.report-content :deep(ol) {
margin: 0.75rem 0;
padding-left: 1.5rem;
}
.report-content :deep(li) {
margin: 0.25rem 0;
}
.report-content :deep(strong) {
font-weight: 600;
}
.report-content :deep(em) {
font-style: italic;
}
}
/* 团队分析弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-width: 90vw;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #999;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
color: #333;
}
.modal-body {
padding: 1.5rem;
overflow-y: auto;
max-height: calc(90vh - 80px);
}
.report-item {
margin-bottom: 2rem;
}
.report-item:last-child {
margin-bottom: 0;
}
.report-meta {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
font-size: 0.9rem;
color: #666;
}
.report-content {
line-height: 1.6;
}
.report-content :deep(h1),
.report-content :deep(h2),
.report-content :deep(h3),
.report-content :deep(h4),
.report-content :deep(h5),
.report-content :deep(h6) {
margin: 1.5rem 0 1rem 0;
font-weight: 600;
}
.report-content :deep(h1) {
font-size: 1.75rem;
border-bottom: 2px solid #eee;
padding-bottom: 0.5rem;
}
.report-content :deep(h2) {
font-size: 1.5rem;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
.report-content :deep(h3) {
font-size: 1.25rem;
}
.report-content :deep(p) {
margin: 0.75rem 0;
}
.report-content :deep(ul),
.report-content :deep(ol) {
margin: 0.75rem 0;
padding-left: 1.5rem;
}
.report-content :deep(li) {
margin: 0.25rem 0;
}
.report-content :deep(strong) {
font-weight: 600;
}
.report-content :deep(em) {
font-style: italic;
}
</style>

View File

@@ -186,87 +186,100 @@ watch(() => props.selectedContact, (newContact) => {
}, { immediate: true });
// 基础信息分析
const startBasicAnalysis = async () => {
if (!props.selectedContact) return;
// const startBasicAnalysis = async () => {
// if (!props.selectedContact) return;
isBasicAnalysisLoading.value = true;
basicAnalysisResult.value = '';
// isBasicAnalysisLoading.value = true;
// basicAnalysisResult.value = '';
// 构建表单信息
const formData = props.formInfo || [];
let formInfoText = '暂无表单信息';
// // 构建表单信息
// const formData = props.formInfo || [];
// let formInfoText = '暂无表单信息';
// ** 适配新的 formInfo 数组格式 **
if (Array.isArray(formData) && formData.length > 0) {
const allInfo = [];
// // ** 适配新的 formInfo 数组格式 **
// if (Array.isArray(formData) && formData.length > 0) {
// const allInfo = [];
// 遍历新格式: [{ question_label: "...", answer: "..." }, ...]
formData.forEach(item => {
// 检查字段是否存在且答案有效
if (
item.question_label &&
item.answer &&
item.answer !== '暂无' &&
item.answer !== ''
) {
// 格式化为 "问题标签: 答案"
allInfo.push(`${item.question_label.trim()}: ${item.answer.trim()}`);
}
});
// // 遍历新格式: [{ question_label: "...", answer: "..." }, ...]
// formData.forEach(item => {
// // 检查字段是否存在且答案有效
// if (
// item.question_label &&
// item.answer &&
// item.answer !== '暂无' &&
// item.answer !== ''
// ) {
// // 格式化为 "问题标签: 答案"
// allInfo.push(`${item.question_label.trim()}: ${item.answer.trim()}`);
// }
// });
// 格式化表单信息文本
formInfoText = allInfo.length > 0
? `=== 问卷/表单信息 ===\n${allInfo.join('\n')}`
: '暂无有效问卷/表单信息';
// // 格式化表单信息文本
// formInfoText = allInfo.length > 0
// ? `=== 问卷/表单信息 ===\n${allInfo.join('\n')}`
// : '暂无有效问卷/表单信息';
// }
// // ** 适配结束 **
// // 构建聊天记录信息
// const chatData = props.chatRecords || [];
// const chatInfoText = chatData.messages && 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.salesStage || '未知'}
// === 表单信息 ===
// ${formInfoText}
// === 聊天记录 ===
// ${chatInfoText}
// === 通话记录 ===
// ${callData.length > 0 && callData[0].record_context ? callData[0].record_context : callInfoText}
// 请基于以上客户的表单信息、聊天记录和通话记录,分析客户的基本情况、背景信息和初步画像。`;
// try {
// await chatService_01.sendMessage(
// query,
// (update) => {
// basicAnalysisResult.value = update.content;
// },
// () => {
// isBasicAnalysisLoading.value = false;
// console.log('基础信息分析完成');
// }
// );
// } catch (error) {
// console.error('基础信息分析失败:', error);
// basicAnalysisResult.value = `分析失败: ${error.message}`;
// isBasicAnalysisLoading.value = false;
// }
// };
const startBasicAnalysis=async ()=>{
console.log("客户基础信息:", props.selectedContact);
const res=await https.post('api/v1/sales_timeline/get_customer_basic_info',{
user_name:props.selectedContact.name,
phone:props.selectedContact.phone
})
if(res.data){
basicAnalysisResult.value = res.data;
console.log("客户基础信息分析结果:", res);
}else{
basicAnalysisResult.value = '基础信息暂无数据'
}
// ** 适配结束 **
// 构建聊天记录信息
const chatData = props.chatRecords || [];
const chatInfoText = chatData.messages && 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.salesStage || '未知'}
=== 表单信息 ===
${formInfoText}
=== 聊天记录 ===
${chatInfoText}
=== 通话记录 ===
${callData.length > 0 && callData[0].record_context ? callData[0].record_context : callInfoText}
请基于以上客户的表单信息、聊天记录和通话记录,分析客户的基本情况、背景信息和初步画像。`;
try {
await chatService_01.sendMessage(
query,
(update) => {
basicAnalysisResult.value = update.content;
},
() => {
isBasicAnalysisLoading.value = false;
console.log('基础信息分析完成');
}
);
} catch (error) {
console.error('基础信息分析失败:', error);
basicAnalysisResult.value = `分析失败: ${error.message}`;
isBasicAnalysisLoading.value = false;
}
};
}
// SOP通话分析
const startSopAnalysis = async () => {
if (!props.selectedContact) return;

View File

@@ -111,33 +111,19 @@
<div class="modal-container">
<div class="modal-header">
<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>
</div>
<div class="modal-body">
<div class="analysis-content">
<div v-if="analysisPeriod === 'day'">
<h4>当日分析报告</h4>
<div v-if="analysisReport.day === '数据为空'" class="error-message">数据为空</div>
<div v-else-if="analysisReport.day" v-html="analysisReport.day.replace(/\n/g, '<br>')"></div>
<p v-else>正在生成分析报告...</p>
</div>
<div v-if="analysisPeriod === 'camp'">
<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 v-if="!analysisReport || Object.keys(analysisReport).length === 0" class="loading-message">正在生成分析报告...</div>
<div v-else-if="Array.isArray(analysisReport) && analysisReport.length === 0" class="error-message">数据为空</div>
<div v-else-if="Array.isArray(analysisReport)">
<div v-for="(report, index) in analysisReport" :key="index" class="report-section">
<h4>{{ report.name }} ({{ report.start_time }} {{ report.end_time }})</h4>
<div v-html="report.report.replace(/\n/g, '<br>')"></div>
</div>
</div>
<div v-else class="error-message">数据格式错误</div>
</div>
</div>
</div>
@@ -220,44 +206,12 @@ const props = defineProps({
}
});
async function CenterGetSecondOrderAnalysisReport(time) {
const params = getRequestParams()
const hasParams = {...params,time:time}
const res = await getSecondOrderAnalysisReport(hasParams)
async function CenterGetSecondOrderAnalysisReport() {
const params = getRequestParams()
const res = await getSecondOrderAnalysisReport(params)
if (res.code === 200) {
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)
}
console.log(11111,res.data)
analysisReport.value = res.data
}
}
// Chart.js 实例
@@ -427,17 +381,12 @@ const hideTooltip = () => {
// 阶段分析报告模态框状态
const showAnalysisModal = ref(false);
const analysisPeriod = ref('day'); // 'day', 'camp', 'month'
const analysisReport = ref({
day: '',
camp: '',
month: ''
});
// 阶段分析报告数据
const analysisReport = ref({});
// 显示阶段分析报告模态框
const showSecondOrderAnalysisReport = () => {
showAnalysisModal.value = true;
CenterGetSecondOrderAnalysisReport(analysisPeriod.value)
CenterGetSecondOrderAnalysisReport()
};
// 关闭阶段分析报告模态框
@@ -445,14 +394,6 @@ const closeAnalysisModal = () => {
showAnalysisModal.value = false;
};
// 切换分析周期
const switchAnalysisPeriod = (period) => {
analysisPeriod.value = period;
CenterGetSecondOrderAnalysisReport(period)
};
watch(() => props.contactTimeData, () => {
renderContactTimeChart();
}, { deep: true });
@@ -658,7 +599,7 @@ $white: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 16px;
padding: 10px 20px 10px;
border-bottom: 1px solid #ebeef5;
h3 { margin: 0; color: $slate-900; font-size: 18px; font-weight: 600; }
}
@@ -1021,4 +962,31 @@ $white: #ffffff;
border-radius: 4px;
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>

View File

@@ -14,12 +14,26 @@
</svg>
</div>
<h3 class="card-title">表单信息</h3>
<div class="form-filter">
<button
v-for="option in formFilterOptions"
:key="option"
class="form-filter-btn"
:class="{ active: formFilter === option }"
@click="formFilter = option"
>
{{ option }}
</button>
</div>
</div>
<div class="card-content">
<div class="form-data-list">
<div v-for="(field, index) in formFields" :key="index" class="form-field">
<span class="field-label">{{ field.label }}:</span>
<span class="field-value">{{ field.value }}</span>
<div v-for="(section, sectionIndex) in displayedFormSections" :key="sectionIndex" class="form-section">
<div v-if="section.title" class="form-section-title">{{ section.title }}</div>
<div class="form-data-list">
<div v-for="(field, index) in section.fields" :key="index" class="form-field">
<span class="field-label">{{ field.label }}:</span>
<span class="field-value">{{ field.value }}</span>
</div>
</div>
</div>
</div>
@@ -132,7 +146,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import axios from 'axios'
// Props
@@ -166,79 +180,107 @@ const chatMessages = computed(() => {
return props.chatInfo?.messages || []
})
// 表单字段数据 (已更新)
const formFields = computed(() => {
const formFilter = ref('全部')
const formSections = computed(() => {
const formData = props.formInfo
console.log(8888888,formData)
// --- NEW LOGIC: 处理新的数组格式(问答列表) ---
if (Array.isArray(formData) && formData.length > 0) {
// 检查数组项结构,确保是新的问答格式
if (formData[0].question_label && formData[0].answer) {
// 直接将问答列表映射为 { label: question_label, value: answer } 格式
return formData.map(item => ({
label: item.question_label,
value: item.answer || '暂无回答'
const emptyFields = [
{ label: '姓名', value: '暂无数据' },
{ label: '联系方式', value: '暂无数据' },
{ label: '孩子信息', value: '暂无数据' },
{ label: '地区', value: '暂无数据' }
]
const makeSection = (fields, title = '表单信息') => [{ title, fields }]
const buildFieldsFromAnswers = (answers = []) => {
return answers.map(item => ({
label: item.question_label,
value: item.answer || '暂无回答'
}))
}
let dataArray = null
if (Array.isArray(formData)) {
dataArray = formData
} else if (formData && Array.isArray(formData.data)) {
dataArray = formData.data
}
if (Array.isArray(dataArray) && dataArray.length > 0) {
if (dataArray[0].form_title && Array.isArray(dataArray[0].answers)) {
return dataArray.map(form => ({
title: form.form_title || '表单信息',
fields: buildFieldsFromAnswers(form.answers || [])
}))
}
if (dataArray[0].question_label && Object.prototype.hasOwnProperty.call(dataArray[0], 'answer')) {
return makeSection(buildFieldsFromAnswers(dataArray))
}
}
// --------------------------------------------------
// Fallback: 如果数据为空、null 或旧的空对象格式
if (!formData || (typeof formData === 'object' && Object.keys(formData).length === 0)) {
return [
{ label: '姓名', value: '暂无数据' },
{ label: '联系方式', value: '暂无数据' },
{ label: '孩子信息', value: '暂无数据' },
{ label: '地区', value: '暂无数据' }
return makeSection(emptyFields)
}
let fields = []
if (formData.name || formData.mobile || formData.child_name) {
const customerInfo = [formData.name, formData.mobile, formData.child_relation, formData.occupation].filter(item => item && item !== '暂无').join(' | ')
const childInfo = [formData.child_name, formData.child_gender, formData.child_education].filter(item => item && item !== '暂无').join(' | ')
fields = [
{ label: '客户信息', value: customerInfo || '暂无' },
{ label: '孩子信息', value: childInfo || '暂无' },
{ label: '地区', value: formData.territory || '暂无' }
]
if (formData.additional_info && Array.isArray(formData.additional_info)) {
formData.additional_info.forEach((item) => {
fields.push({
label: item.topic,
value: item.answer
})
})
}
} else {
const customerInfo = [formData.expandTwentyOne, formData.expandOne].filter(item => item && item !== '暂无').join(' | ')
const childInfo = [formData.expandTwentyNine, formData.expandTwentyFive, formData.expandTwo].filter(item => item && item !== '暂无').join(' | ')
fields = [
{ label: '客户信息', value: customerInfo || '暂无' },
{ label: '孩子信息', value: childInfo || '暂无' },
{ label: '学习状态', value: formData.expandFive || '暂无' },
{ label: '沟通情况', value: formData.expandEight || '暂无' },
{ label: '主要问题', value: formData.expandTwentySeven || '暂无' },
{ label: '关注领域', value: formData.expandFifteen || '暂无' },
{ label: '学习成绩', value: formData.expandFourteen || '暂无' },
{ label: '孩子数量', value: formData.expandTwenty || '暂无' },
{ label: '预期时间', value: formData.expandThirty || '暂无' }
]
}
// --- OLD LOGIC: 处理旧的对象格式(保持兼容性) ---
let fields = []
// 检查是否为第一种格式包含name, mobile等字段
if (formData.name || formData.mobile || formData.child_name) {
const customerInfo = [formData.name, formData.mobile, formData.child_relation, formData.occupation].filter(item => item && item !== '暂无').join(' | ')
const childInfo = [formData.child_name, formData.child_gender, formData.child_education].filter(item => item && item !== '暂无').join(' | ')
fields = [
{ label: '客户信息', value: customerInfo || '暂无' },
{ label: '孩子信息', value: childInfo || '暂无' },
{ label: '地区', value: formData.territory || '暂无' }
]
// 如果有additional_info添加所有问题
if (formData.additional_info && Array.isArray(formData.additional_info)) {
formData.additional_info.forEach((item) => {
fields.push({
label: item.topic,
value: item.answer
})
})
}
} else {
// 第二种格式expandXXX字段
const customerInfo = [formData.expandTwentyOne, formData.expandOne].filter(item => item && item !== '暂无').join(' | ')
const childInfo = [formData.expandTwentyNine, formData.expandTwentyFive, formData.expandTwo].filter(item => item && item !== '暂无').join(' | ')
fields = [
{ label: '客户信息', value: customerInfo || '暂无' },
{ label: '孩子信息', value: childInfo || '暂无' },
{ label: '学习状态', value: formData.expandFive || '暂无' },
{ label: '沟通情况', value: formData.expandEight || '暂无' },
{ label: '主要问题', value: formData.expandTwentySeven || '暂无' },
{ label: '关注领域', value: formData.expandFifteen || '暂无' },
{ label: '学习成绩', value: formData.expandFourteen || '暂无' },
{ label: '孩子数量', value: formData.expandTwenty || '暂无' },
{ label: '预期时间', value: formData.expandThirty || '暂无' }
]
}
// 合并表单数据和聊天数据
const allFields = [...fields]
return allFields
return makeSection(fields)
})
const formFilterOptions = computed(() => {
const titles = formSections.value.map(section => section.title).filter(Boolean)
const uniqueTitles = Array.from(new Set(titles))
if (uniqueTitles.length <= 1) return ['全部']
return ['全部', ...uniqueTitles]
})
const displayedFormSections = computed(() => {
if (formFilter.value === '全部') return formSections.value
const filtered = formSections.value.filter(section => section.title === formFilter.value)
return filtered.length > 0 ? filtered : formSections.value
})
watch(formFilterOptions, (options) => {
if (!options.includes(formFilter.value)) {
formFilter.value = options[0] || '全部'
}
}, { immediate: true })
// 聊天数据
const chatData = computed(() => ({
@@ -507,6 +549,58 @@ const formatDateTime = (dateTimeString) => {
flex: 1;
}
.form-filter {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.form-filter-btn {
padding: 6px 12px;
border-radius: 999px;
border: 1px solid #e5e7eb;
background: #ffffff;
font-size: 12px;
font-weight: 600;
color: #6b7280;
cursor: pointer;
transition: all 0.2s ease;
}
.form-filter-btn.active {
border-color: #10b981;
color: #059669;
background: #ecfdf5;
}
.form-filter-btn:hover {
color: #059669;
border-color: #a7f3d0;
}
.form-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-section + .form-section {
margin-top: 16px;
padding-top: 12px;
border-top: 1px dashed #e5e7eb;
}
.form-section-title {
font-size: 14px;
font-weight: 600;
color: #111827;
background: #f0fdf4;
border: 1px solid #dcfce7;
padding: 4px 10px;
border-radius: 999px;
width: fit-content;
}
// 表单字段样式
.form-data-list {
.form-field {

View File

@@ -6,7 +6,8 @@
<span style="font-size: 14px;">客户转化全流程跟踪</span>
</div>
<div>
<button @click="handleUnassignedRecordingsClick" class="unassigned-recordings-btn">尚未归属录音</button>
<button @click="handleUnassignedRecordingsClick" class="unassigned-recordings-btn" style="margin-right: 10px;">尚未归属录音</button>
<button @click="handleAllRecordingsClick" class="unassigned-recordings-btn">全部录音</button>
</div>
</div>
@@ -204,38 +205,84 @@
<div class="task-body">
<div class="actionable-list">
<div class="items-grid">
<div
v-for="item in contacts"
:key="item.id"
:id="`contact-item-${item.id}`"
class="action-item"
:class="[getHealthClass(item.health), { 'active': item.id === selectedContactId }]"
@click="selectContact(item.id)">
<div class="item-content">
<!-- 头像区域 -->
<div class="avatar-section">
<div class="avatar">
<img :src="item.avatar || '/default-avatar.svg'" :alt="item.name" class="avatar-img" />
<div class="task-section">
<div class="task-section-header">
<span class="task-section-title task-section-title-pending">{{ pendingTitle }}</span>
<span class="task-section-count">{{ contacts.length }}</span>
</div>
<div class="items-grid">
<div
v-for="item in contacts"
:key="item.id"
:id="`contact-item-${item.id}`"
class="action-item"
:class="[getHealthClass(item.health), { 'active': item.id === selectedContactId }]"
@click="selectContact(item.id)">
<div class="item-content">
<div class="avatar-section">
<div class="avatar">
<img :src="item.avatar || '/default-avatar.svg'" :alt="item.name" class="avatar-img" />
</div>
</div>
<div class="info-section">
<div class="item-header">
<div class="item-name-group">
<span class="item-name" :title="item.name">{{ item.name }}</span>
</div>
<span class="item-time">{{ item.time }}</span>
</div>
<div class="item-footer">
<div class="profession-education">
<span class="profession">{{ item.profession||'未知' }}</span>
<span class="education">{{ item.education||'未知' }}</span>
<span class="course-attendance">
{{ getAttendedLessons(item.class_situation,item.class_num) }}
</span>
<span class="course-type" v-if="item.type">{{ item.type }}</span>
</div>
</div>
</div>
</div>
<!-- 信息区域 -->
<div class="info-section">
<div class="item-header">
<div class="item-name-group">
<span class="item-name" :title="item.name">{{ item.name }}</span>
</div>
</div>
</div>
<div v-if="completedContacts.length > 0" class="task-section">
<div class="task-section-header">
<span class="task-section-title task-section-title-completed">已完成用户</span>
<span class="task-section-count">{{ completedContacts.length }}</span>
</div>
<div class="items-grid">
<div
v-for="item in completedContacts"
:key="item.id"
:id="`completed-contact-item-${item.id}`"
class="action-item"
:class="[getHealthClass(item.health), { 'active': item.id === selectedContactId }]"
@click="selectContact(item.id)">
<div class="item-content">
<div class="avatar-section">
<div class="avatar">
<img :src="item.avatar || '/default-avatar.svg'" :alt="item.name" class="avatar-img" />
</div>
<span class="item-time">{{ item.time }}</span>
</div>
<div class="item-footer">
<div class="profession-education">
<span class="profession">{{ item.profession||'未知' }}</span>
<span class="education">{{ item.education||'未知' }}</span>
<span class="course-attendance">
{{ getAttendedLessons(item.class_situation,item.class_num) }}
</span>
<span class="course-type" v-if="item.type">{{ item.type }}</span>
<div class="info-section">
<div class="item-header">
<div class="item-name-group">
<span class="item-name" :title="item.name">{{ item.name }}</span>
</div>
<span class="item-time">{{ item.time }}</span>
</div>
<div class="item-footer">
<div class="profession-education">
<span class="profession">{{ item.profession||'未知' }}</span>
<span class="education">{{ item.education||'未知' }}</span>
<span class="course-attendance">
{{ getAttendedLessons(item.class_situation,item.class_num) }}
</span>
<span class="course-type" v-if="item.type">{{ item.type }}</span>
</div>
</div>
</div>
</div>
@@ -335,7 +382,7 @@
<div class="recording-item" v-for="recording in filteredRecordings" :key="recording.id">
<div class="recording-info">
<div class="recording-header">
<span class="recording-name">{{ recording.name }}</span>
<span class="recording-name" :title="recording.name">{{ recording.name }}</span>
<span class="recording-score">分数: {{ recording.score }}</span>
</div>
<span class="recording-time">{{ recording.time }}</span>
@@ -359,6 +406,7 @@
<div class="modal-content report-modal" @click.stop>
<div class="modal-header">
<h3>录音报告</h3>
<button class="report-btn" @click="printReport">一键复制</button>
<button class="close-btn" @click="showReportModal = false">×</button>
</div>
<div class="modal-body">
@@ -432,7 +480,7 @@ const handleUnassignedRecordingsClick = async () => {
// 优先使用路由参数,其次是 Pinia store 中的用户信息,最后是备用值
const user_name = routeParams.user_name || userStore.userInfo?.username || 'example_user';
const response = await axios.post('http://192.168.15.121:8890/api/v1/sales_timeline/get_sale_unassigned_call_info', {
const response = await axios.post('https://mldash.nycjy.cn/api/v1/sales_timeline/get_sale_unassigned_call_info', {
user_name: user_name
});
@@ -482,6 +530,65 @@ const handleUnassignedRecordingsClick = async () => {
}
};
// --- 新增:处理全部录音按钮点击事件 ---
const handleAllRecordingsClick = async () => {
try {
const userStore = useUserStore();
const routeParams = getRequestParams(); // 获取路由参数
// 优先使用路由参数,其次是 Pinia store 中的用户信息,最后是备用值
const user_name = routeParams.user_name || userStore.userInfo?.username || 'example_user';
const response = await axios.post('https://mldash.nycjy.cn/api/v1/sales_timeline/get_sale_all_call_info', {
user_name: user_name
});
console.log('API Response:', response.data.data);
// --- 数据处理逻辑,以支持分类 ---
const apiData = response.data.data;
const tempCategorizedData = {}; // 临时对象
let uniqueId = 0;
for (const category in apiData) {
if (Object.prototype.hasOwnProperty.call(apiData, category)) {
const categoryData = apiData[category];
tempCategorizedData[category] = []; // 为每个分类创建一个空数组
if (categoryData && Array.isArray(categoryData.records)) {
categoryData.records.forEach(record => {
tempCategorizedData[category].push({ // 将记录添加到对应的分类数组中
id: uniqueId++,
name: record.customer_name || `录音 ${uniqueId}`,
time: new Date(record.record_create_time).toLocaleString('zh-CN'),
type: record.record_tag,
downloadUrl: record.record_file_addr,
duration: record.call_duration ? `${record.call_duration} 分钟` : '未知',
score: record.score || '暂无',
details: record.report_content || '暂无详细报告内容。'
});
});
}
}
}
// 更新状态
categorizedRecordings.value = tempCategorizedData;
recordingCategories.value = Object.keys(tempCategorizedData);
// 默认选中第一个Tab
if (recordingCategories.value.length > 0) {
selectedCategory.value = recordingCategories.value[0];
} else {
selectedCategory.value = null;
}
showUnassignedRecordingsModal.value = true;
} catch (error) {
console.error('API请求失败:', error);
alert('获取录音列表失败,请稍后再试。');
}
};
// 查看报告函数 (无需修改)
const viewReport = (recording) => {
reportData.value = {
@@ -491,7 +598,50 @@ const viewReport = (recording) => {
showReportModal.value = true;
};
// ... 您其他的 props, computed, methods 等代码保持不变 ...
const printReport = () => {
const content = reportData.value.details;
if (!content) {
alert('没有可复制的内容');
return;
}
// 尝试使用 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(content).then(() => {
alert('报告内容已复制到剪贴板');
}).catch(err => {
console.error('复制失败:', err);
// 降级到传统方法
fallbackCopyTextToClipboard(content);
});
} else {
// 降级到传统方法
fallbackCopyTextToClipboard(content);
}
};
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed"; //avoid scrolling to bottom
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
alert('报告内容已复制到剪贴板');
} else {
alert('复制失败,请手动复制');
}
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
alert('复制失败,请手动复制');
}
document.body.removeChild(textArea);
}
const props = defineProps({
data: {
@@ -545,6 +695,11 @@ const totalCustomers = computed(() => {
return Math.max(...baseStages, 1);
});
const pendingTitle = computed(() => {
if (props.selectedStage === '全部' || props.selectedStage === 'all') return '全部用户';
return '待操作用户';
});
const getAttendedLessons = (classSituation, classNum) => {
if (classNum && Array.isArray(classNum) && classNum.length > 0) {
const filtered = classNum.filter(n => n !== -1);
@@ -562,6 +717,84 @@ const getAttendedLessons = (classSituation, classNum) => {
return '未到课';
};
const stageOrder = {
'待加微': 1,
'待填表单': 2,
'待入群': 3,
'待联系': 4,
'待到课': 5,
'课1-4': 6,
'点击未支付': 7,
'付定金': 8,
'定金转化': 9,
'成交': 10
};
const getStageIndex = (type) => stageOrder[type] || 0;
const isFormIncomplete = (customer) => {
const occupationMissing = customer.customer_occupation === '未知' || !customer.customer_occupation;
const educationMissing = customer.customer_child_education === '未知' || !customer.customer_child_education;
return occupationMissing || educationMissing;
};
const buildContacts = (customers, stageFallback) => {
return (customers || []).map(customer => ({
id: customer.customer_name || customer.id || customer.name,
name: customer.customer_name || customer.name,
phone: customer.phone,
profession: customer.customer_occupation || customer.profession,
education: customer.customer_child_education || customer.education,
lastMessageTime: customer.latest_message_time || customer.time,
avatarUrl: customer.customer_avatar_url || customer.avatar || customer.weChat_avatar,
avatar: customer.customer_avatar_url || customer.avatar || customer.weChat_avatar || '/default-avatar.svg',
type: customer.type || stageFallback,
classNum: customer.class_num,
class_num: customer.class_num,
salesStage: customer.type || stageFallback,
priority: customer.type === '待联系' ? 'high' : 'normal',
time: customer.latest_message_time || customer.time || '未知',
health: customer.health || 75,
customer_name: customer.customer_name,
customer_occupation: customer.customer_occupation,
customer_child_education: customer.customer_child_education,
scrm_user_main_code: customer.scrm_user_main_code,
weChat_avatar: customer.weChat_avatar,
class_situation: customer.class_situation,
records: customer.records,
time_and_camp_stage: customer.time_and_camp_stage || []
})).filter(customer => customer.id);
};
const completedContacts = computed(() => {
const stage = props.selectedStage;
if (!stage || stage === '全部' || stage === 'all' || stage === '课1-4' || stage === '成交') {
return [];
}
if (stage === '待填表单') {
const baseCustomers = (props.customersList || []).filter(c => c.type !== '待加微');
const completed = baseCustomers.filter(c => !isFormIncomplete(c));
return buildContacts(completed, stage);
}
if (stage === '待到课') {
const baseCustomers = (props.customersList || []).filter(c => c.type !== '待加微');
const completed = baseCustomers.filter(c => getAttendedLessons(c.class_situation, c.class_num) !== '未到课');
return buildContacts(completed, stage);
}
if (['点击未支付', '付定金', '定金转化'].includes(stage)) {
const courseCustomers = props.courseCustomers?.['课1-4'] || [];
let completed = courseCustomers.filter(c => getStageIndex(c.type) > getStageIndex(stage));
if (stage === '定金转化') {
const payList = (props.payMoneyCustomersList || []).map(c => ({ ...c, type: '成交' }));
completed = completed.concat(payList);
}
return buildContacts(completed, stage);
}
const baseCustomers = props.customersList || [];
const completed = baseCustomers.filter(c => getStageIndex(c.type) > getStageIndex(stage));
return buildContacts(completed, stage);
});
// **修改 getStageCount 函数**
const getStageCount = (stageType) => {
@@ -757,7 +990,13 @@ $indigo: #4f46e5;
.stage-content { text-align: center; width: 100%; .stage-title { font-size: 1rem; font-weight: 500; color: $slate-700; margin: 0 0 0.75rem 0; } .stage-stats { display: flex; align-items: baseline; justify-content: center; gap: 0.5rem; margin-bottom: 0.5rem; .stage-count { font-size: 1.5rem; font-weight: 700; color: $slate-600; line-height: 1; } .stage-label { font-size: 0.75rem; color: $slate-500; } } .stage-percentage { font-size: 0.75rem; color: $slate-400; background: $slate-100; padding: 0.25rem 0.75rem; border-radius: 1rem; display: inline-block; } }
.course-details { background: $white; border-radius: 0.75rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); margin-top: 1rem; .course-details-header { background: linear-gradient(135deg, #f8fafc, #e2e8f0); padding: 0.75rem 1rem; border-bottom: 1px solid $slate-200; h4 { margin: 0; font-size: 0.875rem; font-weight: 600; color: $slate-700; } } .course-details-content { padding: 1rem; max-height: 200px; overflow-y: auto; .no-data { text-align: center; color: $slate-500; padding: 1rem 0; } .course-lessons { display: flex; flex-direction: row; gap: 0.75rem; .lesson-item { background: $slate-50; border-radius: 0.5rem; padding: 0.75rem; .lesson-header .lesson-number { font-weight: 600; } .lesson-details { display: flex; flex-wrap: wrap; gap: 1rem; .detail-item .detail-label { color: $slate-500; } .detail-item .detail-value { font-weight: 600; } } } } } }
.task-body { padding: 1rem; max-height: 50vh; background: $white; border-radius: 1rem; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); overflow-y: auto; }
.actionable-list { display: flex; flex-direction: column; }
.actionable-list { display: flex; flex-direction: column; gap: 1rem; }
.task-section { display: flex; flex-direction: column; gap: 0.5rem; }
.task-section-header { display: flex; align-items: center; gap: 0.5rem; }
.task-section-title { font-size: 0.85rem; font-weight: 600; padding: 0.2rem 0.6rem; border-radius: 999px; }
.task-section-title-pending { color: #16a34a; background: #dcfce7; }
.task-section-title-completed { color: #2563eb; background: #dbeafe; }
.task-section-count { font-size: 0.75rem; color: $slate-500; }
.items-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 250px)); gap: 0.45rem; justify-content: center; }
.action-item { background-color: $white; padding: 0.25rem; border-radius: 0.5rem; border: 1px solid $slate-200; cursor: pointer; transition: all 0.2s ease-in-out; display: flex; &:hover { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0,0,0,0.05); } &.active { background-color: #eef2ff; border-color: $indigo; } .item-content { display: flex; align-items: flex-start; gap: 0.75rem; width: 100%; } .avatar-section .avatar { width: 2.5rem; height: 2.5rem; border-radius: 50%; overflow: hidden; background-color: $slate-200; .avatar-img { width: 100%; height: 100%; object-fit: cover; } } .info-section { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.5rem; } .item-header { display: flex; justify-content: space-between; align-items: flex-start; } .item-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .item-time { font-size: 0.75rem; color: $slate-500; flex-shrink: 0; } .profession-education { display: flex; align-items: center; gap: 0.2rem; flex-wrap: wrap; .profession, .education, .course-type, .course-attendance { font-size: 0.75rem; padding: 0.1rem 0.5rem; border-radius: 0.3rem; font-weight: 500; } .profession { background-color: #e0f2fe; color: #0277bd; } .education { background-color: #f3e5f5; color: #7b1fa2; } .course-type { background-color: #e8f5e8; color: #2e7d32; } .course-attendance { background: linear-gradient(135deg, #e3f2fd, #bbdefb); color: #1565c0; border: 1px solid #90caf9; } } }
.unassigned-recordings-btn { background-color: $primary; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; transition: background-color 0.3s ease; &:hover { background-color: darken($primary, 10%); } }
@@ -773,7 +1012,14 @@ $indigo: #4f46e5;
.recording-item { display: flex; flex-direction: column; justify-content: space-between; padding: 12px 16px; background-color: $slate-50; border-radius: 8px; transition: all 0.2s ease; &:hover { background-color: $slate-100; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); } }
.recording-info { display: flex; flex-direction: column; gap: 4px; flex: 1; }
.recording-header { display: flex; justify-content: space-between; align-items: center; }
.recording-name { font-weight: 500; color: $slate-800; }
.recording-name {
font-weight: 500;
color: $slate-800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 80px);
}
.recording-time { font-size: 0.8rem; color: $slate-600; }
.recording-type { font-size: 0.75rem; color: $slate-500; background-color: $slate-200; padding: 2px 8px; border-radius: 12px; display: inline-block; width: fit-content; }
.recording-score { font-size: 0.8rem; color: $warning; font-weight: 600; }
@@ -782,6 +1028,24 @@ $indigo: #4f46e5;
.report-link, .download-link { color: white; border: none; padding: 6px 10px; border-radius: 4px; font-size: 0.85rem; cursor: pointer; transition: background-color 0.2s ease; text-decoration: none; text-align: center; }
.report-link { background-color: $success; &:hover { background-color: darken($success, 10%); } }
.download-link { background-color: $primary; &:hover { background-color: darken($primary, 10%); } }
.report-btn {
background-color: $success;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
margin-left: auto;
&:hover {
background-color: darken($success, 10%);
}
&:active {
background-color: darken($success, 20%);
}
}
.report-modal { max-width: 600px; }
.report-content { display: flex; flex-direction: column; gap: 12px; }
.report-field label {

View File

@@ -366,25 +366,37 @@ async function getCoreKpi() {
const params = getRequestParams()
const hasParams = params.user_name
// 并发请求所有KPI接口
const [
todayCallRes,
conversionRes,
avgCallTimeRes,
callSuccessRateRes
] = await Promise.all([
getTodayCall(hasParams ? params : undefined),
getConversionRateAndAllocatedData(hasParams ? params : undefined),
getAvgCallTime(hasParams ? params : undefined),
getCallSuccessRate(hasParams ? params : undefined)
])
// 今日通话数据
const res = await getTodayCall(hasParams ? params : undefined)
if (res.code === 200) {
kpiDataState.totalCalls = res.data.call_count
if (todayCallRes.code === 200) {
kpiDataState.totalCalls = todayCallRes.data.call_count
}
// 转化率、分配数据量、加微率
const conversionRes = await getConversionRateAndAllocatedData(hasParams ? params : undefined)
if (conversionRes.code === 200) {
kpiDataState.conversionRate = conversionRes.data.conversion_rate || 0
kpiDataState.assignedData = conversionRes.data.all_count || 0
kpiDataState.wechatAddRate = conversionRes.data.plus_v_conversion_rate || 0
}
// 平均通话时长
const avgCallTimeRes = await getAvgCallTime(hasParams ? params : undefined)
if (avgCallTimeRes.code === 200) {
kpiDataState.avgDuration = avgCallTimeRes.data.call_time || 0
}
// 电话接通率
const callSuccessRateRes = await getCallSuccessRate(hasParams ? params : undefined)
if (callSuccessRateRes.code === 200) {
kpiDataState.successRate = callSuccessRateRes.data.call_success_rate || 0
}
@@ -401,26 +413,35 @@ async function getStatisticsData() {
const params = getRequestParams()
const hasParams = params.user_name
// 获取表单填写率
const fillingRateRes = await getTableFillingRate(hasParams ? params : undefined)
// 并发请求所有统计数据
const [
fillingRateRes,
avgResponseRes,
communicationRes,
timeoutRes
] = await Promise.all([
getTableFillingRate(hasParams ? params : undefined),
getAverageResponseTime(hasParams ? params : undefined),
getWeeklyActiveCommunicationRate(hasParams ? params : undefined),
getTimeoutResponseRate(hasParams ? params : undefined)
])
// 处理表单填写率
if (fillingRateRes.code === 200) {
statisticsData.formCompletionRate = fillingRateRes.data.filling_rate
}
// 获取平均响应时间
const avgResponseRes = await getAverageResponseTime(hasParams ? params : undefined)
// 处理平均响应时间
if (avgResponseRes.code === 200) {
statisticsData.averageResponseTime = avgResponseRes.data.average_minutes
}
// 获取客户沟通率
const communicationRes = await getWeeklyActiveCommunicationRate(hasParams ? params : undefined)
// 处理客户沟通率
if (communicationRes.code === 200) {
statisticsData.customerCommunicationRate = communicationRes.data.communication_rate
}
// 获取超时响应率
const timeoutRes = await getTimeoutResponseRate(hasParams ? params : undefined)
// 处理超时响应率
if (timeoutRes.code === 200) {
statisticsData.timeoutResponseRate = timeoutRes.data.overtime_rate_600
statisticsData.severeTimeoutRate = timeoutRes.data.overtime_rate_800
@@ -584,7 +605,7 @@ async function getCustomerForm() {
const res = await getCustomerFormInfo(params)
console.log('获取客户表单数据:', res)
if(res.code === 200) {
formInfo.value = res.data[0].answers || []
formInfo.value = res.data || []
}
} catch (error) {
// 静默处理错误
@@ -956,15 +977,15 @@ async function forceRefreshAllData() {
onMounted(async () => {
try {
isPageLoading.value = true
await getStatisticsData()
await getCoreKpi()
await CenterGetGoldContactTime()
await CenterGetSalesFunnel()
await getCustomerForm()
await getCustomerChat()
await getUrgentProblem()
await getCustomerCall()
await getTimeline()
getStatisticsData()
getCoreKpi()
CenterGetGoldContactTime()
CenterGetSalesFunnel()
getCustomerForm()
getCustomerChat()
getUrgentProblem()
getCustomerCall()
getTimeline()
// 开发环境下暴露数据刷新函数到全局对象,方便调试
if (process.env.NODE_ENV === 'development') {
@@ -1820,10 +1841,10 @@ $primary: #3b82f6;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 600px;
max-width: 1200px;
width: 90%;
// 设置最大高度,防止弹窗超出屏幕
max-height: 35vh;
max-height: 80vh;
// 防止内容溢出容器,配合内部滚动
overflow: hidden;
// 使用 Flexbox 布局,让 .modal-body 可以伸缩

View File

@@ -311,7 +311,7 @@ onBeforeUnmount(() => {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 16px;
padding: 10px 20px 10px;
border-bottom: 1px solid #ebeef5;
h3 { margin: 0; color: #303133; font-size: 18px; font-weight: 600; }
}

View File

@@ -18,6 +18,7 @@
:class="{ active: selectedRecording === index }"
@click="selectRecording(index)"
>
<span class="recording-index">{{ recording.score}}</span>
<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">
@@ -172,8 +173,8 @@ import MarkdownIt from 'markdown-it'
// Props定义
const props = defineProps({
qualityCalls: {
type: Object,
default: () => ({})
type: Array,
default: () => []
}
})
@@ -221,24 +222,22 @@ const recordings = computed(() => {
if (!props.qualityCalls ) {
return staticRecordings.value;
}
const recordingsList = [];
Object.keys(props.qualityCalls).forEach(userName => {
props.qualityCalls[userName].forEach((record, index) => {
props.qualityCalls.forEach((record, index) => {
recordingsList.push({
id: recordingsList.length + 1,
name: record.obj_file_name ? record.obj_file_name.split('/').pop() : `${record.sale_name}-录音-${index + 1}`,
name: record.record_name ? record.record_name : `${record.sale_name}-录音-${index + 1}`,
date: new Date().toISOString().split('T')[0],
url: record.obj_file_name,
transcription: record.context || null,
score: record.score,
sop: record.sop,
sale_name: record.sale_name,
url: record.record_file_addr,
transcription: record.record_context || null,
score: record.record_score,
sop: record.record_report,
sale_name: record.record_name,
size: 2048576, // 默认文件大小 2MB
uploadTime: new Date().toLocaleDateString('zh-CN')
uploadTime: record.created_at,
});
});
});
return recordingsList;
})
@@ -505,14 +504,14 @@ const downloadRecording = (index) => {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
height: 400px;
height: 420px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 0;
padding: 10px 20px 0;
border-bottom: 1px solid #ebeef5;
}
@@ -549,7 +548,7 @@ const downloadRecording = (index) => {
}
.chart-content {
padding: 20px;
padding: 10px;
}
.recording-section {
@@ -562,7 +561,6 @@ const downloadRecording = (index) => {
.recording-list {
margin-bottom: 20px;
max-height: 400px;
overflow-y: auto;
}
.recording-item {
@@ -602,6 +600,39 @@ const downloadRecording = (index) => {
display: inline-block;
}
.recording-index {
/* 基础分数样式 */
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
background-color: #e9ecef;
color: #495057;
margin-right: 10px;
}
/* 第一名样式 */
.recording-item:first-child .recording-index {
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #fff;
box-shadow: 0 2px 4px rgba(255, 215, 0, 0.3);
}
/* 第二名样式 */
.recording-item:nth-child(2) .recording-index {
background: linear-gradient(135deg, #C0C0C0, #A9A9A9);
color: #fff;
box-shadow: 0 2px 4px rgba(192, 192, 192, 0.3);
}
/* 第三名样式 */
.recording-item:nth-child(3) .recording-index {
background: linear-gradient(135deg, #CD7F32, #A0522D);
color: #fff;
box-shadow: 0 2px 4px rgba(205, 127, 50, 0.3);
}
.recording-meta {
display: flex;
gap: 12px;

View File

@@ -90,12 +90,14 @@ async function exportData() {
try {
ElMessage.info('正在导出数据,请稍候...')
console.log('导出参数:', params)
const res = await exportCustomers(params)
const res = await exportCustomers()
if (res.code === 200 && res.data && res.data.length > 0) {
ElMessage.success('数据导出成功')
// 处理数据,将复杂的嵌套对象展平
const exportData = res.data.map(customer => {
const flatData = {
'昵称': customer.nickname || '',
'客户姓名': customer.customer_name || '',
'性别': customer.gender || '',
'跟进人': customer.follow_up_name || '',
'手机号': customer.phone || '',
@@ -103,18 +105,104 @@ async function exportData() {
'用户ID': customer.mantis_user_id || '',
}
// 处理微信表单信息
if (customer.wechat_form) {
flatData['家长姓名'] = customer.wechat_form.name || ''
flatData['孩子姓名'] = customer.wechat_form.child_name || ''
flatData['孩子性别'] = customer.wechat_form.child_gender || ''
flatData['职业'] = customer.wechat_form.occupation || ''
flatData['孩子教育阶段'] = customer.wechat_form.child_education || ''
flatData['与孩子关系'] = customer.wechat_form.child_relation || ''
flatData['联系电话'] = customer.wechat_form.mobile || ''
flatData['地区'] = customer.wechat_form.territory || ''
flatData['创建时间'] = customer.wechat_form.created_at ? new Date(customer.wechat_form.created_at).toLocaleString() : ''
flatData['更新时间'] = customer.wechat_form.updated_at ? new Date(customer.wechat_form.updated_at).toLocaleString() : ''
const parseFormData = (formData) => {
if (typeof formData === 'string') {
try {
return JSON.parse(formData)
} catch (e) {
return null
}
}
return formData || null
}
const normalizeFormArray = (formData) => {
const data = parseFormData(formData)
console.log('解析后的表单数据:', data)
if (Array.isArray(data)) return data
if (data && Array.isArray(data.data)) return data.data
if (data && typeof data === 'object' && !data.answers) {
const numericKeys = Object.keys(data).filter(key => String(Number(key)) === key)
if (numericKeys.length > 0) {
return numericKeys
.sort((a, b) => Number(a) - Number(b))
.map(key => data[key])
.filter(Boolean)
}
}
if (data && data.answers) return [data]
return data ? [data] : []
}
const ensureUniqueKey = (key) => {
if (!flatData[key]) return key
let index = 2
while (flatData[`${key}(${index})`]) {
index += 1
}
return `${key}(${index})`
}
const parsedFormData = parseFormData(customer.wechat_form)
const formArray = normalizeFormArray(parsedFormData)
if (formArray.length > 0) {
console.log('表单数组:', formArray)
const isAnswerList = formArray.every(item => item && item.question_label && Object.prototype.hasOwnProperty.call(item, 'answer'))
const isFormWithAnswers = formArray.every(item => item && Array.isArray(item.answers))
console.log('是否为答案列表:', isAnswerList)
console.log('是否为表单含 answers:', isFormWithAnswers)
if (isAnswerList) {
let formAnswerCount = 0
formArray.forEach((answerItem) => {
const label = String(answerItem.question_label || '').trim()
if (!label) return
const key = ensureUniqueKey(label)
flatData[key] = answerItem.answer ?? ''
formAnswerCount += 1
})
if (formAnswerCount === 0 && formArray.length > 0) {
const fallbackKey = ensureUniqueKey('表单答案')
flatData[fallbackKey] = formArray.map(item => `${item.question_label || ''}: ${item.answer || ''}`).join(' | ')
}
} else if (isFormWithAnswers) {
console.log('表单含 answers:', formArray)
formArray.forEach((formItem) => {
const formTitle = formItem.form_title || '表单'
if (Array.isArray(formItem.answers)) {
let formAnswerCount = 0
formItem.answers.forEach((answerItem) => {
const label = String(answerItem.question_label || '').trim()
if (!label) return
const key = ensureUniqueKey(`${formTitle}-${label}`)
flatData[key] = answerItem.answer ?? ''
formAnswerCount += 1
})
if (formAnswerCount === 0 && formItem.answers.length > 0) {
const fallbackKey = ensureUniqueKey(`${formTitle}-表单答案`)
flatData[fallbackKey] = formItem.answers.map(item => `${item.question_label || ''}: ${item.answer || ''}`).join(' | ')
}
}
if (formItem.created_at) {
const key = ensureUniqueKey(`${formTitle}-创建时间`)
flatData[key] = new Date(formItem.created_at).toLocaleString()
}
if (formItem.updated_at) {
const key = ensureUniqueKey(`${formTitle}-更新时间`)
flatData[key] = new Date(formItem.updated_at).toLocaleString()
}
})
}
} else if (parsedFormData && typeof parsedFormData === 'object') {
flatData['家长姓名'] = parsedFormData.name || ''
flatData['孩子姓名'] = parsedFormData.child_name || ''
flatData['孩子性别'] = parsedFormData.child_gender || ''
flatData['职业'] = parsedFormData.occupation || ''
flatData['孩子教育阶段'] = parsedFormData.child_education || ''
flatData['与孩子关系'] = parsedFormData.child_relation || ''
flatData['联系电话'] = parsedFormData.mobile || ''
flatData['地区'] = parsedFormData.territory || ''
flatData['创建时间'] = parsedFormData.created_at ? new Date(parsedFormData.created_at).toLocaleString() : ''
flatData['更新时间'] = parsedFormData.updated_at ? new Date(parsedFormData.updated_at).toLocaleString() : ''
}
// 处理到课情况
@@ -126,9 +214,12 @@ async function exportData() {
}
// 处理问卷调查信息
if (customer.wechat_form && customer.wechat_form.additional_info) {
customer.wechat_form.additional_info.forEach((item) => {
flatData[item.topic || ''] = item.answer || ''
if (parsedFormData && parsedFormData.additional_info) {
parsedFormData.additional_info.forEach((item) => {
const key = ensureUniqueKey(item.topic || '')
if (key) {
flatData[key] = item.answer || ''
}
})
}
@@ -137,27 +228,18 @@ async function exportData() {
// 创建工作簿
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.json_to_sheet(exportData)
const allKeys = Array.from(new Set(exportData.flatMap(item => Object.keys(item))))
const ws = XLSX.utils.json_to_sheet(exportData, { header: allKeys })
// 设置列宽
const colWidths = [
{ wch: 10 }, // 昵称
{ wch: 6 }, // 性别
{ wch: 12 }, // 跟进人
{ wch: 15 }, // 手机号
{ wch: 10 }, // 是否入群
{ wch: 20 }, // 用户ID
{ wch: 12 }, // 家长姓名
{ wch: 12 }, // 孩子姓名
{ wch: 8 }, // 孩子性别
{ wch: 12 }, // 职业
{ wch: 12 }, // 孩子教育阶段
{ wch: 15 }, // 与孩子关系
{ wch: 15 }, // 联系电话
{ wch: 20 }, // 地区
{ wch: 20 }, // 创建时间
{ wch: 20 }, // 更新时间
]
const colWidths = allKeys.map(key => {
const maxCellLength = exportData.reduce((max, row) => {
const value = row[key]
const length = value === null || value === undefined ? 0 : String(value).length
return Math.max(max, length)
}, 0)
return { wch: Math.min(50, Math.max(10, key.length, maxCellLength)) }
})
ws['!cols'] = colWidths
// 添加工作表到工作簿
@@ -196,7 +278,7 @@ async function exportData() {
}
.chart-header {
padding: 20px 20px 16px;
padding: 10px 20px 10px;
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: space-between;

View File

@@ -112,6 +112,22 @@
<span class="value">{{ selectedGroup.conversionRate }}%</span>
</div>
</div>
<div class="group-performance">
<button @click="showTeamAnalysisModal">团队整体分析</button>
</div>
</div>
<!-- 团队整体分析弹窗 -->
<div v-if="showTeamAnalysis" class="team-analysis-modal" @click.self="closeTeamAnalysisModal">
<div class="modal-content">
<div class="modal-header">
<h3>团队整体分析</h3>
<button class="close-btn" @click="closeTeamAnalysisModal">×</button>
</div>
<div class="modal-body">
<p>这里是团队整体分析的内容</p>
</div>
</div>
</div>
<div class="members-grid">
@@ -178,7 +194,7 @@
</main>
<!-- Loading 组件 -->
<Loading :visible="isLoading" text="数据加载中..." />
<!-- <Loading :visible="isLoading" text="数据加载中..." /> -->
</div>
</template>
@@ -283,6 +299,9 @@ const cardVisibility = ref({
// FeedbackForm 控制变量
const showFeedbackForm = ref(false)
// 团队整体分析弹窗控制变量
const showTeamAnalysis = ref(false)
// 更新卡片显示状态
const updateCardVisibility = (newVisibility) => {
Object.assign(cardVisibility.value, newVisibility)
@@ -297,6 +316,15 @@ const showFeedbackFormModal = () => {
const closeFeedbackFormModal = () => {
showFeedbackForm.value = false
}
// 团队整体分析弹窗控制方法
const showTeamAnalysisModal = () => {
showTeamAnalysis.value = true
}
const closeTeamAnalysisModal = () => {
showTeamAnalysis.value = false
}
// 营期调控逻辑
// This would ideally come from a prop or API call based on the logged-in user
const centerData = ref({
@@ -860,31 +888,46 @@ const conversionRateVsAverage = ref({})
})
}
// 获取优秀录音
const excellentRecord = ref({});
const excellentRecord = ref([]);
// 获取优秀录音文件
// async function CentergetGoodRecord() {
// const params = getRequestParams()
// const params1 = {
// user_level:userStore.userInfo.user_level.toString(),
// user_name:userStore.userInfo.username
// }
// const hasParams = params.user_name
// const requestParams = hasParams ? {
// ...params,
// } : params1
// console.log(188811111,requestParams)
async function CentergetGoodRecord() {
console.log('CentergetGoodRecord 开始执行')
try {
const params = getRequestParams()
const params1 = {
user_level: userStore.userInfo?.user_level?.toString() || '',
user_name: userStore.userInfo?.username || ''
}
// try {
// const res = await withCache('CentergetGoodRecord',
// () => getExcellentRecordFile(requestParams),
// requestParams
// )
// excellentRecord.value = res.data.excellent_record_list
// console.log(111111,res.data.excellent_record_list)
// } catch (error) {
// console.error("获取优秀录音失败:", error);
// }
// }
// 检查参数是否有效
const hasParams = params.user_name && params.user_level
const requestParams = hasParams ? {
...params,
} : params1
console.log('CentergetGoodRecord request params:', requestParams)
// 验证必要参数是否存在
if (!requestParams.user_name || !requestParams.user_level) {
console.error("缺少必要的请求参数:", requestParams);
return;
}
// 直接发送请求,不使用缓存
const res = await getExcellentRecordFile(requestParams)
if (res && res.code === 200 && res.data) {
excellentRecord.value = res.data || []
console.log('获取优秀录音成功:', res.data)
} else {
console.error("获取优秀录音失败,响应数据不完整:", res);
excellentRecord.value = []
}
} catch (error) {
console.error("获取优秀录音失败:", error);
excellentRecord.value = []
}
}
// 缓存管理功能
// 清除所有缓存
@@ -939,20 +982,21 @@ const excellentRecord = ref({});
(currentQuery.user_name && currentQuery.user_level)
if (!isFromRoute) {
await CenterCampPeriodAdmin()
CenterCampPeriodAdmin()
}
CentergetGoodRecord()
CenterOverallCenterPerformance()
CenterTotalGroupCount()
CenterConversionRate()
CenterTotalCallCount()
CenterNewCustomer()
CenterDepositConversionRate()
CenterCustomerType()
CenterUrgentNeedToAddress()
CenterConversionRateVsAverage()
await CenterOverallCenterPerformance()
await CenterTotalGroupCount()
await CenterConversionRate()
await CenterTotalCallCount()
await CenterNewCustomer()
await CenterDepositConversionRate()
await CenterCustomerType()
await CenterUrgentNeedToAddress()
await CenterConversionRateVsAverage()
await CenterSeniorManagerList()
await CenterGroupList('all')
CenterSeniorManagerList()
CenterGroupList('all')
console.log('[强制刷新] 所有数据已重新加载')
} catch (error) {
@@ -980,6 +1024,7 @@ const excellentRecord = ref({});
await CenterTotalGroupCount()
await CenterConversionRate()
await CenterTotalCallCount()
await CentergetGoodRecord()
await CenterNewCustomer()
await CenterDepositConversionRate()
await CenterCustomerType()
@@ -1619,6 +1664,76 @@ const hideTooltip = () => {
}
}
/* 团队分析弹窗样式 */
.team-analysis-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.team-analysis-modal .modal-content {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 80%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.team-analysis-modal .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.team-analysis-modal .modal-header h3 {
margin: 0;
color: #1a202c;
font-size: 1.25rem;
}
.team-analysis-modal .close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #718096;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.team-analysis-modal .close-btn:hover {
color: #1a202c;
}
.team-analysis-modal .modal-body {
padding: 1rem;
overflow-y: auto;
flex: 1;
}
.team-analysis-modal .modal-body p {
margin: 0;
color: #4a5568;
line-height: 1.5;
}
// 路由导航顶栏样式
.route-header {
display: flex;

View File

@@ -135,7 +135,6 @@ const props = defineProps({
})
}
})
console.log(99999,props.overallTeamPerformance)
// 计算属性
const totalPerformance = computed(() => {
return props.overallTeamPerformance.totalPerformance
@@ -158,7 +157,6 @@ const newCustomers = computed(() => {
})
const depositConversions = computed(() => {
console.log(999991111,props.overallTeamPerformance.depositConversions)
return props.overallTeamPerformance.depositConversions
})

File diff suppressed because it is too large Load Diff

View File

@@ -151,7 +151,7 @@ $white: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0px 20px 16px;
padding: 0px 20px 10px;
border-bottom: 1px solid #ebeef5;
h3 {
margin: 0;

View File

@@ -87,7 +87,7 @@ $white: #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
height: 26rem !important;
height: 21.5rem !important;
max-height: 26rem;
// flex: 1;
}
@@ -96,7 +96,7 @@ $white: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 16px;
padding: 10px 20px 10px;
border-bottom: 1px solid #ebeef5;
h3 {

View File

@@ -5,35 +5,35 @@
<div class="stat-icon customer-rate">
<i class="el-icon-chat-dot-round"></i>
</div>
<div class="kpi-value">{{ customerCommunicationRate.active_customer_communication_rate||0 }}</div>
<div class="kpi-value">{{ (customerCommunicationRate && customerCommunicationRate.active_customer_communication_rate) || 0 }}</div>
<p>活跃客户沟通率 <i class="el-icon-warning info-icon" @mouseenter="showTooltip($event, 'customerCommunicationRate')" @mouseleave="hideTooltip"></i></p>
</div>
<div class="kpi-item stat-item">
<div class="stat-icon response-time">
<i class="el-icon-timer"></i>
</div>
<div class="kpi-value">{{ averageResponseTime.average_answer_time||0 }}<span class="kpi-unit">分钟</span></div>
<div class="kpi-value">{{ (averageResponseTime && averageResponseTime.average_answer_time)||0 }}<span class="kpi-unit">分钟</span></div>
<p>平均应答时间 <i class="el-icon-warning info-icon" @mouseenter="showTooltip($event, 'averageResponseTime')" @mouseleave="hideTooltip"></i></p>
</div>
<div class="kpi-item stat-item">
<div class="stat-icon timeout-rate">
<i class="el-icon-warning"></i>
</div>
<div class="kpi-value">{{ timeoutResponseRate.timeout_rate||0 }}</div>
<div class="kpi-value">{{ (timeoutResponseRate && timeoutResponseRate.timeout_rate)||0 }}</div>
<p>超时应答率 <i class="el-icon-warning info-icon" @mouseenter="showTooltip($event, 'timeoutResponseRate')" @mouseleave="hideTooltip"></i></p>
</div>
<div class="kpi-item stat-item">
<div class="stat-icon severe-timeout-rate">
<i class="el-icon-warning-outline"></i>
</div>
<div class="kpi-value">{{ timeoutResponseRate.serious_timeout_rate||0 }}</div>
<div class="kpi-value">{{ (timeoutResponseRate && timeoutResponseRate.serious_timeout_rate)||0 }}</div>
<p>严重超时应答率 <i class="el-icon-warning info-icon" @mouseenter="showTooltip($event, 'severeTimeoutRate')" @mouseleave="hideTooltip"></i></p>
</div>
<div class="kpi-item stat-item">
<div class="stat-icon form-rate">
<i class="el-icon-document"></i>
</div>
<div class="kpi-value">{{ formCompletionRate.table_filling_rate||0 }}</div>
<div class="kpi-value">{{ (formCompletionRate && formCompletionRate.table_filling_rate)||0 }}</div>
<p>表格填写率 <i class="el-icon-warning info-icon" @mouseenter="showTooltip($event, 'formCompletionRate')" @mouseleave="hideTooltip"></i></p>
</div>
</div>
@@ -53,24 +53,24 @@ import Tooltip from '@/components/Tooltip.vue';
defineProps({
customerCommunicationRate: {
type: Number,
default: 0
type: Object,
default: () => ({})
},
averageResponseTime: {
type: Number,
default: 0
type: Object,
default: () => ({})
},
timeoutResponseRate: {
type: Number,
default: 0
type: Object,
default: () => ({})
},
severeTimeoutRate: {
type: Number,
default: 0
type: Object,
default: () => ({})
},
formCompletionRate: {
type: Number,
default: 0
type: Object,
default: () => ({})
}
});

View File

@@ -28,18 +28,42 @@
<div v-if="!isRouteNavigation">
<!-- 用户下拉菜单 -->
<div style="display: flex; align-items: center; gap: 20px;">
<button @click="showDepartmentAnalysisModal" class="feedback-btn">部门分析</button>
<button @click="showFeedbackFormModal" class="feedback-btn">意见反馈</button>
<FeedbackForm
:is-visible="showFeedbackForm"
@close="closeFeedbackFormModal"
@submit-feedback="closeFeedbackFormModal"
/>
<UserDropdown
:card-visibility="cardVisibility"
@update-card-visibility="updateCardVisibility"
/>
<FeedbackForm
:is-visible="showFeedbackForm"
@close="closeFeedbackFormModal"
@submit-feedback="closeFeedbackFormModal"
/>
<!-- 部门分析弹窗 -->
<div v-if="showDepartmentAnalysis" class="department-analysis-modal" @click.self="closeDepartmentAnalysisModal">
<div class="modal-content">
<div class="modal-header">
<h3>部门分析</h3>
<button class="close-btn" @click="closeDepartmentAnalysisModal">×</button>
</div>
<div class="modal-body">
<div v-if="departmentAnalysisData && departmentAnalysisData.length > 0">
<div v-for="(report, index) in departmentAnalysisData" :key="index" class="report-item">
<h4>报告时间: {{ report.start_time }} {{ report.end_time }}</h4>
<div v-if="report.report && report.report !== 'None' && report.report.trim() !== ''" class="report-content" v-html="formatReportContent(report.report)"></div>
<div v-else class="no-report">
<p>暂无分析报告</p>
</div>
</div>
</div>
<div v-else>
<p>暂无部门分析数据</p>
</div>
</div>
</div>
</div>
<UserDropdown
:card-visibility="cardVisibility"
@update-card-visibility="updateCardVisibility"
/>
</div>
</div>
</div>
</div>
</header>
@@ -53,7 +77,12 @@
@update-check-type="updateCheckType"
/>
<div v-if="cardVisibility.teamAlerts" class="action-items-compact">
<TeamAlerts style="height: 300px;" :abnormalData="teamAlerts" />
<!-- <TeamAlerts style="height: 300px;" :abnormalData="teamAlerts" /> -->
<div v-if="cardVisibility.problemRanking" class="problem-ranking">
<!-- 客户迫切解决的问题 -->
<ProblemRanking :problemRanking="problemRanking" />
</div>
</div>
</div>
<StatisticalIndicators
@@ -79,10 +108,7 @@
@select-group="selectGroup"
/>
</div>
<div v-if="cardVisibility.problemRanking" class="problem-ranking">
<!-- 客户迫切解决的问题 -->
<ProblemRanking :problemRanking="problemRanking" />
</div>
<GoodMusic style="height: 300px;" :qualityCalls="excellentRecord" />
<!-- Right Section - Group Comparison -->
<div v-if="cardVisibility.groupComparison" class="right-section">
<GroupComparison
@@ -106,7 +132,8 @@
<!-- 团队详情内容 -->
<div v-else>
<div class="team-detail-header">
<h2>{{ selectedGroup.name }} - 团队成员详情</h2>
<div>
<h2>{{ selectedGroup.name }} - 团队成员详情</h2>
<div class="team-summary">
<div class="summary-item">
<span class="label">组长:</span>
@@ -125,6 +152,35 @@
<span class="value">{{ selectedGroup.conversionRate }}%</span>
</div>
</div>
</div>
<div class="group-performance">
<button @click="showTeamAnalysisModal">团队整体分析</button>
</div>
</div>
<!-- 团队分析弹窗 -->
<div v-if="showTeamAnalysis" class="team-analysis-modal" @click.self="closeTeamAnalysisModal">
<div class="modal-content">
<div class="modal-header">
<h3>团队整体分析</h3>
<button class="close-btn" @click="closeTeamAnalysisModal">×</button>
</div>
<div class="modal-body">
<div v-if="teamAnalysisData && teamAnalysisData.length > 0">
<div v-for="(report, index) in teamAnalysisData" :key="index" class="report-item">
<h4>报告时间: {{ report.start_time }} {{ report.end_time }}</h4>
<div v-if="report.report && report.report !== 'None' && report.report.trim() !== ''" class="report-content" v-html="formatReportContent(report.report)"></div>
<div v-else class="no-report">
<p>暂无分析报告</p>
</div>
</div>
</div>
<div v-else>
<p>暂无团队分析数据</p>
</div>
</div>
</div>
</div>
<div class="members-grid">
@@ -201,6 +257,7 @@ import Tooltip from '@/components/Tooltip.vue'
import CenterOverview from './components/CenterOverview.vue'
import GroupComparison from './components/GroupComparison.vue'
import GroupRanking from './components/GroupRanking.vue'
import GoodMusic from './components/GoodMusic.vue'
import TeamAlerts from '../maneger/components/TeamAlerts.vue'
import ProblemRanking from './components/ProblemRanking.vue'
import StatisticalIndicators from './components/StatisticalIndicators.vue'
@@ -209,8 +266,9 @@ import Loading from '@/components/Loading.vue'
import PerformanceComparison from './components/PerformanceComparison.vue'; // 1. 导入新组件
import { getOverallTeamPerformance,getTotalGroupCount,getConversionRate,getTotalCallCount,
getNewCustomer,getDepositConversionRate,getActiveCustomerCommunicationRate,getAverageAnswerTime,
getTimeoutRate,getTableFillingRate,getUrgentNeedToAddress,getTeamRanking,getTeamRankingInfo,getAbnormalResponseRate,getTeamSalesFunnel } from '@/api/senorManger.js'
getTimeoutRate,getTableFillingRate,getUrgentNeedToAddress,getTeamRanking,getTeamRankingInfo,
getAbnormalResponseRate,getTeamSalesFunnel,getExcellentRecordFile,getTeamEveryGroupReport,
getTeamEntiretyReport } from '@/api/senorManger.js'
import { useUserStore } from '@/stores/user.js'
import FeedbackForm from "@/components/FeedbackForm.vue";
@@ -248,7 +306,6 @@ const withCache = async (functionName, apiCall, params = {}) => {
return cachedData
}
console.log(`调用API获取数据: ${functionName}`, params)
const result = await apiCall()
setCache(cacheKey, result)
return result
@@ -260,6 +317,25 @@ const clearCache = () => {
console.log('所有缓存已清除')
}
// 优秀录音
const excellentRecord = ref([]);
async function CenterExcellentRecord() {
const params={
user_level:userStore.userInfo.user_level.toString(),
user_name:userStore.userInfo.username
}
try {
const cacheKey = getCacheKey('CenterExcellentRecord', params);
const result = await withCache(cacheKey, async () => {
const res = await getExcellentRecordFile(params);
return res.data;
});
excellentRecord.value = result;
} catch (error) {
console.error("获取优秀录音失败:", error);
}
}
const clearSpecificCache = (functionName, params = {}) => {
const cacheKey = getCacheKey(functionName, params)
cache.delete(cacheKey)
@@ -295,19 +371,19 @@ const forceRefreshAllData = async () => {
try {
isLoading.value = true
await fetchOverallTeamPerformance()
await fetchActiveGroups()
await fetchConversionRate()
await fetchTotalCallCount()
await fetchNewCustomers()
await fetchDepositConversions()
await fetchAbnormalResponseRate()
await fetchCustomerCommunicationRate()
await fetchAverageResponseTime()
await fetchTimeoutRate()
await fetchTableFillingRate()
await fetchUrgentNeedToAddress()
await fetchTeamRanking()
fetchOverallTeamPerformance()
fetchActiveGroups()
fetchConversionRate()
fetchTotalCallCount()
fetchNewCustomers()
fetchDepositConversions()
fetchAbnormalResponseRate()
fetchCustomerCommunicationRate()
fetchAverageResponseTime()
fetchTimeoutRate()
fetchTableFillingRate()
fetchUrgentNeedToAddress()
fetchTeamRanking()
console.log('所有数据已强制刷新完成')
} catch (error) {
console.error('强制刷新数据失败:', error)
@@ -324,6 +400,14 @@ const formCompletionRate = ref(90)
const CheckType = ref('month')
// FeedbackForm 控制变量
const showFeedbackForm = ref(false)
// 部门分析弹窗控制变量
const showDepartmentAnalysis = ref(false)
// 团队分析弹窗控制变量
const showTeamAnalysis = ref(false)
// 团队分析数据
const teamAnalysisData = ref([])
// 部门分析数据
const departmentAnalysisData = ref([])
// 更新CheckType的方法
const updateCheckType = async (newValue) => {
@@ -344,6 +428,90 @@ const closeFeedbackFormModal = () => {
showFeedbackForm.value = false
}
// 部门分析弹窗控制方法
const showDepartmentAnalysisModal = async () => {
showDepartmentAnalysis.value = true
// 获取部门分析数据
try {
// 获取当前登录的高级经理信息
const currentUser = userStore.userInfo;
const params = {
user_name: currentUser.username,
user_level: currentUser.user_level.toString(),
part_count: 1 // 默认获取最近1份报告
}
const response = await getTeamEntiretyReport(params)
// 根据API响应结构调整数据处理逻辑
if (response.data) {
if (Array.isArray(response.data)) {
// 如果response.data本身就是数组
departmentAnalysisData.value = response.data
} else if (response.data.data && Array.isArray(response.data.data)) {
// 如果response.data.data是数组
departmentAnalysisData.value = response.data.data
} else {
// 其他情况,可能是单个对象
departmentAnalysisData.value = [response.data]
}
}
} catch (error) {
console.error('获取部门分析数据失败:', error)
departmentAnalysisData.value = []
}
}
const closeDepartmentAnalysisModal = () => {
showDepartmentAnalysis.value = false
}
// 团队分析弹窗控制方法
const showTeamAnalysisModal = async () => {
showTeamAnalysis.value = true
// 获取团队分析数据
try {
const params = {
department_name: selectedGroup.value.name + '-' + selectedGroup.value.leader,
part_count: 1 // 默认获取最近1份报告
}
const response = await getTeamEveryGroupReport(params)
// 根据API响应结构调整数据处理逻辑
if (response.data) {
if (Array.isArray(response.data)) {
// 如果response.data本身就是数组
teamAnalysisData.value = response.data
} else if (response.data.data && Array.isArray(response.data.data)) {
// 如果response.data.data是数组
teamAnalysisData.value = response.data.data
} else {
// 其他情况,可能是单个对象
teamAnalysisData.value = [response.data]
}
}
} catch (error) {
console.error('获取团队分析数据失败:', error)
}
}
const closeTeamAnalysisModal = () => {
showTeamAnalysis.value = false
}
// 格式化报告内容
const formatReportContent = (content) => {
if (!content) return ''
// 将Markdown格式的标题转换为HTML标签
return content
.replace(/### (.*?)(?=\n|$)/g, '<h3>$1</h3>')
.replace(/## (.*?)(?=\n|$)/g, '<h2>$1</h2>')
.replace(/# (.*?)(?=\n|$)/g, '<h1>$1</h1>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
}
// 卡片显示状态
const cardVisibility = ref({
centerOverview: true,
@@ -448,7 +616,6 @@ async function fetchActiveGroups() {
requestParams
)
overallTeamPerformance.value.activeGroups = response.data
console.log('活跃组数:', response.data)
} catch (error) {
console.error('获取活跃组数失败:', error)
}
@@ -529,18 +696,17 @@ async function fetchDepositConversions() {
requestParams
)
overallTeamPerformance.value.depositConversions = response.data
console.log(99888999,response.data)
} catch (error) {
console.error('获取定金转化失败:', error)
}
}
const statisticalIndicators = ref({
customerCommunicationRate: 0,
averageResponseTime: 0,
timeoutResponseRate: 0,
severeTimeoutRate: 0,
formCompletionRate: 0,
customerCommunicationRate: {},
averageResponseTime: {},
timeoutResponseRate: {},
severeTimeoutRate: {},
formCompletionRate: {},
})
// 销售漏斗
@@ -572,17 +738,18 @@ async function fetchAbnormalResponseRate() {
() => getAbnormalResponseRate(hasParams ? params : undefined),
requestParams
)
const rawData = response.data
const rawData = response.data || {} // 添加默认值防止null访问
const processedAlerts = []
const teamData = new Map()
// 添加安全检查防止访问null属性
if (rawData.team_serious_timeout_abnormal_counts_by_group) {
Object.entries(rawData.team_serious_timeout_abnormal_counts_by_group).forEach(([teamName, data]) => {
if (!teamData.has(teamName)) {
teamData.set(teamName, { timeoutCount: 0, fillingCount: 0 })
}
teamData.get(teamName).timeoutCount = data.count
teamData.get(teamName).timeoutCount = data.count || 0
})
}
@@ -591,7 +758,7 @@ async function fetchAbnormalResponseRate() {
if (!teamData.has(teamName)) {
teamData.set(teamName, { timeoutCount: 0, fillingCount: 0 })
}
teamData.get(teamName).fillingCount = data.count
teamData.get(teamName).fillingCount = data.count || 0
})
}
@@ -633,9 +800,12 @@ async function fetchCustomerCommunicationRate() {
() => getActiveCustomerCommunicationRate(hasParams ? params : undefined),
requestParams
)
statisticalIndicators.value.customerCommunicationRate = response.data
// 确保响应数据不为null
statisticalIndicators.value.customerCommunicationRate = response.data || {}
} catch (error) {
console.error('获取活跃客户沟通率失败:', error)
// 出错时设置为空对象
statisticalIndicators.value.customerCommunicationRate = {}
}
}
// 统计指标--平均应答时间
@@ -650,9 +820,12 @@ async function fetchAverageResponseTime() {
() => getAverageAnswerTime(hasParams ? params : undefined),
requestParams
)
statisticalIndicators.value.averageResponseTime = response.data
// 确保响应数据不为null
statisticalIndicators.value.averageResponseTime = response.data || {}
} catch (error) {
console.error('获取平均应答时间失败:', error)
// 出错时设置为空对象
statisticalIndicators.value.averageResponseTime = {}
}
}
// 统计指标--超时应答率、严重超时应答率
@@ -667,9 +840,15 @@ async function fetchTimeoutRate() {
() => getTimeoutRate(hasParams ? params : undefined),
requestParams
)
statisticalIndicators.value.timeoutResponseRate = response.data
// 确保响应数据不为null
statisticalIndicators.value.timeoutResponseRate = response.data || {}
// severeTimeoutRate使用相同的数据源
statisticalIndicators.value.severeTimeoutRate = response.data || {}
} catch (error) {
console.error('获取超时应答率失败:', error)
// 出错时设置为空对象
statisticalIndicators.value.timeoutResponseRate = {}
statisticalIndicators.value.severeTimeoutRate = {}
}
}
// 统计指标--表格填写率
@@ -684,9 +863,12 @@ async function fetchTableFillingRate() {
() => getTableFillingRate(hasParams ? params : undefined),
requestParams
)
statisticalIndicators.value.formCompletionRate = response.data
// 确保响应数据不为null
statisticalIndicators.value.formCompletionRate = response.data || {}
} catch (error) {
console.error('获取表格填写率失败:', error)
// 出错时设置为空对象
statisticalIndicators.value.formCompletionRate = {}
}
}
const problemRanking = ref({})
@@ -772,6 +954,7 @@ onMounted(async ()=>{
await fetchTableFillingRate()
await fetchUrgentNeedToAddress()
await fetchTeamRanking()
await CenterExcellentRecord()
console.log('缓存状态:', getCacheInfo())
@@ -1117,12 +1300,19 @@ const hideTooltip = () => {
.team-detail-header {
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 1rem;
h2 {
font-size: 1.4rem;
font-weight: 600;
color: #1e293b;
margin: 0 0 1rem 0;
margin: 0;
flex: 1;
min-width: 300px;
}
.team-summary {
@@ -1148,6 +1338,32 @@ const hideTooltip = () => {
}
}
}
.group-performance {
button {
background-color: #3b82f6;
color: white;
border: none;
border-radius: 6px;
padding: 0.5rem 1rem;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
height: fit-content;
&:hover {
background-color: #2563eb;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
}
}
}
.members-grid {
@@ -1543,4 +1759,199 @@ const hideTooltip = () => {
.feedback-btn:hover {
background-color: #3182ce;
}
/* 部门分析弹窗样式 */
.department-analysis-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 80%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.modal-header h3 {
margin: 0;
color: #1a202c;
font-size: 1.25rem;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #718096;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #1a202c;
}
.modal-body {
padding: 1rem;
overflow-y: auto;
flex: 1;
}
.modal-body p {
margin: 0;
color: #4a5568;
line-height: 1.5;
}
/* 团队分析弹窗样式 */
.team-analysis-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.team-analysis-modal .modal-content {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 80%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.team-analysis-modal .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.team-analysis-modal .modal-header h3 {
margin: 0;
color: #1a202c;
font-size: 1.25rem;
}
.team-analysis-modal .close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #718096;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.team-analysis-modal .close-btn:hover {
color: #1a202c;
}
.team-analysis-modal .modal-body {
padding: 1rem;
overflow-y: auto;
flex: 1;
}
.team-analysis-modal .modal-body p {
margin: 0;
color: #4a5568;
line-height: 1.5;
}
.team-analysis-modal .report-item {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #e2e8f0;
border-radius: 5px;
background-color: #f8fafc;
}
.team-analysis-modal .report-item h4 {
margin-top: 0;
color: #1a202c;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 5px;
}
.team-analysis-modal .report-content {
margin-top: 10px;
color: #4a5568;
line-height: 1.6;
}
.team-analysis-modal .report-content h1,
.team-analysis-modal .report-content h2,
.team-analysis-modal .report-content h3 {
margin-top: 15px;
margin-bottom: 10px;
color: #1a202c;
}
.team-analysis-modal .report-content h1 {
font-size: 1.5rem;
}
.team-analysis-modal .report-content h2 {
font-size: 1.3rem;
}
.team-analysis-modal .report-content h3 {
font-size: 1.1rem;
}
.team-analysis-modal .report-content strong {
font-weight: bold;
}
.team-analysis-modal .report-content em {
font-style: italic;
}
.team-analysis-modal .no-report {
text-align: center;
color: #718096;
font-style: italic;
}
</style>

View File

@@ -18,6 +18,7 @@
:class="{ active: selectedRecording === index }"
@click="selectRecording(index)"
>
<span class="recording-index">{{ recording.score}}</span>
<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">
@@ -172,8 +173,8 @@ import MarkdownIt from 'markdown-it'
// Props定义
const props = defineProps({
qualityCalls: {
type: Object,
default: () => ({})
type: Array,
default: () => []
}
})
@@ -221,24 +222,22 @@ const recordings = computed(() => {
if (!props.qualityCalls ) {
return staticRecordings.value;
}
const recordingsList = [];
Object.keys(props.qualityCalls).forEach(userName => {
props.qualityCalls[userName].forEach((record, index) => {
props.qualityCalls.forEach((record, index) => {
recordingsList.push({
id: recordingsList.length + 1,
name: record.obj_file_name ? record.obj_file_name.split('/').pop() : `${record.sale_name}-录音-${index + 1}`,
name: record.record_name ? record.record_name : `${record.sale_name}-录音-${index + 1}`,
date: new Date().toISOString().split('T')[0],
url: record.obj_file_name,
transcription: record.context || null,
score: record.score,
sop: record.sop,
sale_name: record.sale_name,
url: record.record_file_addr,
transcription: record.record_context || null,
score: record.record_score,
sop: record.record_report,
sale_name: record.record_name,
size: 2048576, // 默认文件大小 2MB
uploadTime: new Date().toLocaleDateString('zh-CN')
uploadTime: record.created_at,
});
});
});
return recordingsList;
})
@@ -512,7 +511,7 @@ const downloadRecording = (index) => {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 0;
padding: 10px 20px 0;
border-bottom: 1px solid #ebeef5;
}
@@ -549,7 +548,7 @@ const downloadRecording = (index) => {
}
.chart-content {
padding: 20px;
padding: 10px;
}
.recording-section {
@@ -562,7 +561,6 @@ const downloadRecording = (index) => {
.recording-list {
margin-bottom: 20px;
max-height: 400px;
overflow-y: auto;
}
.recording-item {
@@ -602,6 +600,39 @@ const downloadRecording = (index) => {
display: inline-block;
}
.recording-index {
/* 基础分数样式 */
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
background-color: #e9ecef;
color: #495057;
margin-right: 10px;
}
/* 第一名样式 */
.recording-item:first-child .recording-index {
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #fff;
box-shadow: 0 2px 4px rgba(255, 215, 0, 0.3);
}
/* 第二名样式 */
.recording-item:nth-child(2) .recording-index {
background: linear-gradient(135deg, #C0C0C0, #A9A9A9);
color: #fff;
box-shadow: 0 2px 4px rgba(192, 192, 192, 0.3);
}
/* 第三名样式 */
.recording-item:nth-child(3) .recording-index {
background: linear-gradient(135deg, #CD7F32, #A0522D);
color: #fff;
box-shadow: 0 2px 4px rgba(205, 127, 50, 0.3);
}
.recording-meta {
display: flex;
gap: 12px;

View File

@@ -525,7 +525,6 @@ async function getConversionComparison(data) {
const res = await getCompanyConversionRateVsLast(params);
return res.data;
});
console.log(111111,result);
conversionComparison.value = result;
} catch (error) {
console.error("获取转化对比失败:", error);
@@ -665,7 +664,7 @@ const handleFilterChange = (filterParams) => {
getDetailData(filterParams)
}
// 优秀录音
const excellentRecord = ref({});
const excellentRecord = ref([]);
async function CenterExcellentRecord() {
const params={
user_level:userStore.userInfo.user_level.toString(),
@@ -675,10 +674,9 @@ const params={
const cacheKey = getCacheKey('CenterExcellentRecord', params);
const result = await withCache(cacheKey, async () => {
const res = await getExcellentRecordFile(params);
return res.data.excellent_record_list;
return res.data;
});
excellentRecord.value = result;
console.log(111111,result);
} catch (error) {
console.error("获取优秀录音失败:", error);
}
@@ -687,15 +685,15 @@ onMounted(async() => {
// 页面初始化逻辑
console.log('页面初始化,开始加载数据...');
await getRealTimeProgress()
await getTotalDeals()
await getConversionComparison('month')
await getCompanySalesRank('red')
await getCustomerTypeRatio('child_education')
await getCustomerUrgency()
await CusotomGetLevelTree()
await getDetailData()
await CenterExcellentRecord()
getRealTimeProgress()
getTotalDeals()
getConversionComparison('month')
getCompanySalesRank('red')
getCustomerTypeRatio('child_education')
getCustomerUrgency()
CusotomGetLevelTree()
getDetailData()
CenterExcellentRecord()
// 输出缓存状态信息
const cacheInfo = getCacheInfo();