Compare commits

..

33 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
d75fd9beb8 fix: 修复表单数据处理逻辑并更新UI文本显示
修复表单数据从对象到数组的格式兼容处理,确保新旧数据格式都能正确显示
更新多处UI文本描述,包括"今日通话"改为"本期通话"、"月度总业绩"改为"本月成交单数"
调整客户阶段筛选逻辑,排除'待加微'客户对'待填表单'和'待到课'的影响
修复未分配通话记录API端点错误
2025-10-22 16:22:51 +08:00
094f655634 fix: 修复表单数据处理和API端点问题
修复RawDataCards和CustomerDetail组件中表单数据的默认值和类型定义
更新getTodayCall API端点路径为current_camp_call
调整sale.vue中表单数据处理逻辑以适应新API响应格式
优化PerformanceComparison组件的数据处理逻辑
修改开发环境API基础路径为本地测试地址
2025-10-22 14:57:28 +08:00
10c8c7b796 refactor(components): 重构通话记录卡片布局和样式
- 移除调试用的console.log语句
- 重新组织通话记录卡片的结构,将用户信息、标签和分数整合到更清晰的布局中
- 新增通话时长格式化方法和分数样式分类方法
- 优化移动端布局和交互效果
- 改进标签和按钮的视觉样式,增加悬停效果
2025-10-16 11:44:33 +08:00
ea32a16e5d fix: 将客户查询参数从name改为phone
修改客户聊天和通话记录的查询参数,从使用客户姓名改为使用电话号码,以匹配后端接口的变更需求
2025-10-15 21:38:19 +08:00
57be345996 feat(录音管理): 实现无归属录音API集成及报告查看功能
- 添加API请求获取真实录音数据并处理展示
- 实现录音报告弹窗展示详细信息
- 优化录音列表UI布局和响应式设计
2025-10-15 18:56:11 +08:00
3ed490d6dc fix: 将客户表单请求参数从name改为phone
客户表单接口现在需要使用手机号而非姓名作为查询参数,以更准确地识别客户
2025-10-15 17:48:31 +08:00
33 changed files with 5104 additions and 2397 deletions

View File

@@ -2,9 +2,10 @@
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8" /> <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" /> <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> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -4664,7 +4664,7 @@
}, },
"node_modules/markdown-it": { "node_modules/markdown-it": {
"version": "14.1.0", "version": "14.1.0",
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -7,7 +7,7 @@ export const getProblemDistribution = (params) => {
// 今日通话 /api/v1/more_level_screening/today_call // 今日通话 /api/v1/more_level_screening/today_call
export const getTodayCall = (params) => { export const getTodayCall = (params) => {
return https.post('/api/v1/sales/today_call', params) return https.post('/api/v1/sales/current_camp_call', params)
} }
// 表格填写率 /api/v1/more_level_screening/table_filling_rate // 表格填写率 /api/v1/more_level_screening/table_filling_rate
@@ -84,5 +84,5 @@ export const getCallSuccessRate = (params) => {
// 二阶分析报告 // 二阶分析报告
export const getSecondOrderAnalysisReport = (params) => { export const getSecondOrderAnalysisReport = (params) => {
return https.post('/api/v1/sales/get_call_text', params) return https.post('/api/v1/sales/get_second_analysis_report', params)
} }

View File

@@ -44,9 +44,9 @@ export const getGroupDetail = (params) => {
export const getGroupCallDuration = (params) => { export const getGroupCallDuration = (params) => {
return https.post('/api/v1/manager/group_call_duration', params) return https.post('/api/v1/manager/group_call_duration', params)
} }
// 二阶分析报告 /api/v1/sales/get_call_text // 二阶分析报告 /api/v1/sales/get_call_text
export const GetSecondOrderAnalysisReport = (params) => { export const GetSecondOrderAnalysisReport = (params) => {
return https.post('/api/v1/manager/group_call_text', params) return https.post('/api/v1/manager/group_second_report', params)
} }
// 通话分类数据 /api/v1/manager/get_member_call_classify // 通话分类数据 /api/v1/manager/get_member_call_classify
@@ -54,3 +54,12 @@ export const getMemberCallClassify = (params) => {
return https.post('/api/v1/manager/get_member_call_classify', 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 // 获取优秀录音文件 /api/v1/level_four/overview/get_excellent_record_file
export const getExcellentRecordFile = (params) => { 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 // 修改营期 /api/v1/level_four/overview/change_camp_period
export const changeCampPeriod = (params) => { export const changeCampPeriod = (params) => {
@@ -87,7 +87,7 @@ export const cancelSwitchHistoryCampPeriod = (params) => {
// 一键导出 api/v1/level_four/overview/export_customers // 一键导出 api/v1/level_four/overview/export_customers
export const exportCustomers = (params) => { 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

@@ -74,8 +74,24 @@ export const getAbnormalResponseRate = (params) => {
export const getHistoryCamps = (params) => { export const getHistoryCamps = (params) => {
return https.post('/api/v1/level_three/overview/get_history_camps', params) return https.post('/api/v1/level_three/overview/get_history_camps', params)
} }
// 数据对比 /api/v1/level_three/overview/get_team_many_target
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) => { export const getPeriodStage = (params) => {
return https.get('/api/v1/level_five/overview/get_period_stage', 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) => { 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') || ''; const token = localStorage.getItem('token') || '';
// 发送 POST 请求到后端接口 // 发送 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: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
}); });
console.log('响应状态8888:', response.data.message); // console.log('响应状态8888:', response.data.message);
// 提交成功 // 提交成功
submitStatus.value = 'success'; submitStatus.value = 'success';
// 触发父组件的事件,并传递数据 // 触发父组件的事件,并传递数据

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
<template> <template>
<div class="team-report"> <div class="team-report">
<div class="header-container">
<h2>今日团队实时战报</h2> <h2>今日团队实时战报</h2>
<button class="analysis-button" @click="showTeamAnalysis">团队分析</button>
</div>
<div class="report-grid"> <div class="report-grid">
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
@@ -32,7 +35,7 @@
</div> </div>
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
<span class="card-title">月度总业绩 <i class="info-icon" @mouseenter="showTooltip('monthlyRevenue', $event)" @mouseleave="hideTooltip"></i></span> <span class="card-title">本月成交单数 <i class="info-icon" @mouseenter="showTooltip('monthlyRevenue', $event)" @mouseleave="hideTooltip"></i></span>
<span class="card-trend positive">+8% vs 上月</span> <span class="card-trend positive">+8% vs 上月</span>
</div> </div>
<div class="card-value">{{ formatCurrency(weekTotalData.week_add_fee_total?.total_add_fee || 0) }} </div> <div class="card-value">{{ formatCurrency(weekTotalData.week_add_fee_total?.total_add_fee || 0) }} </div>
@@ -76,6 +79,9 @@ const props = defineProps({
} }
}) })
// 定义emit
const emit = defineEmits(['show-team-analysis'])
// 监听数据变化,用于调试 // 监听数据变化,用于调试
watch(() => props.weekTotalData, (newData) => { watch(() => props.weekTotalData, (newData) => {
console.log('TeamReport 收到的数据:', newData) console.log('TeamReport 收到的数据:', newData)
@@ -122,8 +128,8 @@ const metricDescriptions = {
description: '本期新增的成交订单数量,已确认付款或签约的客户订单。' description: '本期新增的成交订单数量,已确认付款或签约的客户订单。'
}, },
monthlyRevenue: { monthlyRevenue: {
title: '月度总业绩', title: '本月成交单数',
description: '本月团队累计完成的销售业绩总额,包括所有已确认的订单金额。' description: '本月团队累计完成的销售订单数量,包括所有已确认的订单。'
}, },
conversionRate: { conversionRate: {
title: '定金转化率', title: '定金转化率',
@@ -146,6 +152,11 @@ const showTooltip = (metricType, event) => {
const hideTooltip = () => { const hideTooltip = () => {
tooltip.visible = false tooltip.visible = false
} }
// 显示团队分析
const showTeamAnalysis = () => {
emit('show-team-analysis')
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -156,11 +167,33 @@ const hideTooltip = () => {
padding: 1.5rem; padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 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 { h2 {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 600; font-weight: 600;
color: #1e293b; 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 { .report-grid {

View File

@@ -37,9 +37,11 @@
<!-- Top Section - Team Alerts and Today's Report --> <!-- Top Section - Team Alerts and Today's Report -->
<div class="top-section"> <div class="top-section">
<!-- Team Alerts --> <!-- Team Alerts -->
<TeamAlerts :abnormalData="groupAbnormalResponse" /> <!-- <TeamAlerts :abnormalData="groupAbnormalResponse" /> -->
<GoodMusic :quality-calls="excellentRecord"
/>
<!-- Today's Team Report --> <!-- Today's Team Report -->
<TeamReport :weekTotalData="weekTotalData" /> <TeamReport :weekTotalData="weekTotalData" @show-team-analysis="fetchTeamAnalysis" />
</div> </div>
<!-- Sales Funnel Section --> <!-- Sales Funnel Section -->
@@ -65,11 +67,30 @@
</div> </div>
</main> </main>
</div> </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> </template>
<script setup> <script setup>
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed } from "vue";
import TeamAlerts from "./components/TeamAlerts.vue"; import TeamAlerts from "./components/TeamAlerts.vue";
import GoodMusic from "./components/GoodMusic.vue";
import TeamReport from "./components/TeamReport.vue"; import TeamReport from "./components/TeamReport.vue";
import SalesFunnel from "./components/SalesFunnel.vue"; import SalesFunnel from "./components/SalesFunnel.vue";
import PerformanceRanking from "./components/PerformanceRanking.vue"; import PerformanceRanking from "./components/PerformanceRanking.vue";
@@ -81,7 +102,7 @@ import CustomerDetail from "../person/components/CustomerDetail.vue";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import {getGroupAbnormalResponse, getWeekTotalCall, getWeekAddCustomerTotal, getWeekAddDealTotal, 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 = [ const teamMembers = [
@@ -108,9 +129,10 @@ const userStore = useUserStore();
// 获取通用请求参数的函数 // 获取通用请求参数的函数
const getRequestParams = () => { const getRequestParams = () => {
const params = {} const params = {}
// 从路由参数获取 // 从路由参数获取
const routeUserLevel = router.currentRoute.value.query.user_level || router.currentRoute.value.params.user_level 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 const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
// 如果路由有参数,使用路由参数 // 如果路由有参数,使用路由参数
if (routeUserLevel) { if (routeUserLevel) {
params.user_level = routeUserLevel.toString() params.user_level = routeUserLevel.toString()
@@ -119,6 +141,14 @@ const getRequestParams = () => {
params.user_name = routeUserName 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 return params
} }
@@ -146,7 +176,7 @@ const weekTotalData = ref({
pay_deposit_to_money_rate: {}, pay_deposit_to_money_rate: {},
group_funnel: {}, group_funnel: {},
group_call_duration: {}, group_call_duration: {},
}); })
// 团队异常预警 // 团队异常预警
const groupAbnormalResponse = ref({}) const groupAbnormalResponse = ref({})
async function TeamGetGroupAbnormalResponse() { async function TeamGetGroupAbnormalResponse() {
@@ -161,9 +191,9 @@ async function TeamGetGroupAbnormalResponse() {
let alertId = 1 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 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 memberDetails = ref({})
// 团队分析数据
const teamAnalysisData = ref([])
const showTeamAnalysisModal = ref(false)
// 当前选中的成员,默认为空 // 当前选中的成员,默认为空
const selectedMember = ref(null); 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 () => { onMounted(async () => {
await TeamGetGroupAbnormalResponse() CentergetGoodRecord()
await TeamGetWeekTotalCall() TeamGetGroupAbnormalResponse()
await TeamGetGroupCallDuration() TeamGetWeekTotalCall()
await TeamGetWeekAddCustomerTotal() TeamGetGroupCallDuration()
await TeamGetWeekAddDealTotal() TeamGetWeekAddCustomerTotal()
await TeamGetWeekAddFeeTotal() TeamGetWeekAddDealTotal()
await TeamGetGroupFunnel() TeamGetWeekAddFeeTotal()
await TeamGetGroupRanking() TeamGetGroupFunnel()
TeamGetGroupRanking()
}) })
</script> </script>
@@ -646,12 +788,12 @@ onMounted(async () => {
.top-section { .top-section {
display: grid; display: grid;
grid-template-columns: 1fr 3fr; grid-template-columns: 1fr 3fr;
gap: 1rem; gap: 0.5rem;
// PC端保持一致布局 // PC端保持一致布局
@media (min-width: 1024px) { @media (min-width: 1024px) {
grid-template-columns: 1fr 3fr; grid-template-columns: 1fr 3fr;
gap: 1.5rem; gap: 1rem;
} }
// 平板端适配 // 平板端适配
@@ -676,7 +818,7 @@ onMounted(async () => {
.analytics-section { .analytics-section {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 1rem; gap: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
// PC端保持一致布局 // 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> </style>

View File

@@ -104,8 +104,8 @@ const props = defineProps({
default: null default: null
}, },
formInfo: { formInfo: {
type: Object, type: Array,
default: () => ({}) default: () => []
}, },
chatRecords: { chatRecords: {
type: Array, type: Array,
@@ -186,142 +186,100 @@ watch(() => props.selectedContact, (newContact) => {
}, { immediate: true }); }, { immediate: true });
// 基础信息分析 // 基础信息分析
// const startBasicAnalysis = async () => {
// if (!props.selectedContact) return;
// isBasicAnalysisLoading.value = true;
// basicAnalysisResult.value = '';
// // 构建表单信息
// const formData = props.formInfo || [];
// let formInfoText = '暂无表单信息';
// // ** 适配新的 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()}`);
// }
// });
// // 格式化表单信息文本
// 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 ()=>{ const startBasicAnalysis=async ()=>{
if (!props.selectedContact) return; console.log("客户基础信息:", props.selectedContact);
const res=await https.post('api/v1/sales_timeline/get_customer_basic_info',{
isBasicAnalysisLoading.value = true; user_name:props.selectedContact.name,
basicAnalysisResult.value = ''; phone:props.selectedContact.phone
})
// 构建表单信息 if(res.data){
const formData = props.formInfo || {}; basicAnalysisResult.value = res.data;
let formInfoText = '暂无表单信息'; console.log("客户基础信息分析结果:", res);
}else{
if (Object.keys(formData).length > 0) { basicAnalysisResult.value = '基础信息暂无数据'
const allInfo = [];
// 处理第一种格式基础信息和additional_info
if (formData.name || formData.mobile || formData.additional_info) {
const basicInfo = [];
const additionalInfo = [];
const basicFields = {
name: '姓名',
mobile: '手机号',
occupation: '职业',
territory: '地区',
child_name: '孩子姓名',
child_gender: '孩子性别',
child_education: '孩子教育阶段',
child_relation: '与孩子关系'
};
Object.entries(basicFields).forEach(([key, label]) => {
if (formData[key] && formData[key] !== '暂无' && formData[key] !== '') {
basicInfo.push(`${label}: ${formData[key]}`);
}
});
if (formData.additional_info && Array.isArray(formData.additional_info)) {
formData.additional_info.forEach(item => {
if (item.topic && item.answer) {
additionalInfo.push(`${item.topic}\n答案: ${item.answer}`);
}
});
} }
if (basicInfo.length > 0) {
allInfo.push('=== 基础信息 ===');
allInfo.push(...basicInfo);
} }
if (additionalInfo.length > 0) {
allInfo.push('\n=== 问卷信息 ===');
allInfo.push(...additionalInfo);
}
}
// 处理第二种格式customerExpandFieldMap
if (formData.customerExpandFieldMap) {
const expandInfo = [];
const map = formData.customerExpandFieldMap;
Object.entries(map).forEach(([key, value]) => {
if (!map.hasOwnProperty(key)) return;
if (value && typeof value === 'object' && value.key) {
const question = value.key;
let answer = '';
if (value.typeCode === 'SINGLE_SELECT' || value.typeCode === 'MULTIPLE_SELECT') {
if (value.expandValueList && value.expandValueList.length > 0) {
answer = value.expandValueList.map(item => item.itemName).join('、');
}
} else if (value.typeCode === 'TEXT' || value.typeCode === 'TEXTAREA' || value.typeCode === 'NUMBER') {
answer = formData[key] || '';
}
if (answer && answer !== '暂无' && answer !== '') {
expandInfo.push(`${question}\n答案: ${answer}`);
}
}
});
if (expandInfo.length > 0) {
if (allInfo.length > 0) allInfo.push('\n');
allInfo.push('=== 问卷详细信息 ===');
allInfo.push(...expandInfo);
}
}
formInfoText = allInfo.length > 0 ? 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;
}
};
// SOP通话分析 // SOP通话分析
const startSopAnalysis = async () => { const startSopAnalysis = async () => {
if (!props.selectedContact) return; if (!props.selectedContact) return;

View File

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

View File

@@ -14,16 +14,30 @@
</svg> </svg>
</div> </div>
<h3 class="card-title">表单信息</h3> <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>
<div class="card-content"> <div class="card-content">
<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 class="form-data-list">
<div v-for="(field, index) in formFields" :key="index" class="form-field"> <div v-for="(field, index) in section.fields" :key="index" class="form-field">
<span class="field-label">{{ field.label }}:</span> <span class="field-label">{{ field.label }}:</span>
<span class="field-value">{{ field.value }}</span> <span class="field-value">{{ field.value }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- 聊天记录和通话录音卡片 --> <!-- 聊天记录和通话录音卡片 -->
<div class="data-card communication-card"> <div class="data-card communication-card">
@@ -70,7 +84,7 @@
<span class="content-time">最新: {{ chatData.lastMessage }}</span> <span class="content-time">最新: {{ chatData.lastMessage }}</span>
</div> </div>
<div class="message-list"> <div class="message-list">
<div v-for="(message, index) in props.chatInfo.messages" :key="index" class="message-item"> <div v-for="(message, index) in props.chatInfo?.messages" :key="index" class="message-item">
<div class="message-header"> <div class="message-header">
<span class="message-sender">{{ message.format_direction }}</span> <span class="message-sender">{{ message.format_direction }}</span>
<span class="message-time">{{ message.format_add_time }}</span> <span class="message-time">{{ message.format_add_time }}</span>
@@ -89,13 +103,23 @@
<div class="call-list"> <div class="call-list">
<div v-for="(call, index) in callRecords" :key="index" class="call-item"> <div v-for="(call, index) in callRecords" :key="index" class="call-item">
<div class="call-header"> <div class="call-header">
<div class="header-main" style="display: flex; flex-direction: row;">
<div class="user-info">
<span class="call-type">用户: {{ call.user_name }}</span> <span class="call-type">用户: {{ call.user_name }}</span>
<span class="call-duration">客户: {{ call.customer_name }}</span> </div>
<div class="header-tags">
<span class="call-tag" :class="{ <span class="call-tag" :class="{
'tag-20min': call.record_tag === '20分钟通话', 'tag-20min': call.record_tag === '20分钟通话',
'tag-other': call.record_tag === '其他' 'tag-invalid': call.record_tag === '无效通话',
'tag-other': call.record_tag !== '20分钟通话' && call.record_tag !== '无效通话'
}" v-if="call.record_tag">{{ call.record_tag }}</span> }" v-if="call.record_tag">{{ call.record_tag }}</span>
<!-- 分数移到类型标签后面 -->
<span class="stat-value" :class="getScoreClass(call.score)">{{ call.score }}</span>
</div> </div>
</div>
<div class="call-time">{{ formatDateTime(call.record_create_time) }}</div>
</div>
<div class="call-actions"> <div class="call-actions">
<div class="action-buttons"> <div class="action-buttons">
<button class="action-btn download-btn" @click="downloadRecording(call)"> <button class="action-btn download-btn" @click="downloadRecording(call)">
@@ -106,9 +130,8 @@
<i class="icon-view"></i> <i class="icon-view"></i>
查看原文 查看原文
</button> </button>
</div> <!-- 时长移到操作按钮后面 -->
<div class="call-time-info"> <span class="call-duration">{{ formatCallDuration(call.call_duration) }}</span>
<span class="call-duration">{{ formatDateTime(call.record_create_time) }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -120,12 +143,10 @@
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed, watch } from 'vue'
import axios from 'axios' import axios from 'axios'
// Props // Props
@@ -135,8 +156,8 @@ const props = defineProps({
default: () => ({}) default: () => ({})
}, },
formInfo: { formInfo: {
type: Object, type: Array,
default: () => ({}) default: () => []
}, },
chatInfo: { chatInfo: {
type: Object, type: Object,
@@ -149,33 +170,60 @@ const props = defineProps({
}) })
// Emits // Emits
const emit = defineEmits(['analyze-sop', 'show-modal']) const emit = defineEmits(['analyze-sop', 'show-modal', 'show-download-modal'])
// 当前激活的tab // 当前激活的tab
const activeTab = ref('chat') const activeTab = ref('chat')
// 聊天消息列表 // 聊天消息列表
const chatMessages = computed(() => { const chatMessages = computed(() => {
return props.chatInfo?.messages || [] return props.chatInfo?.messages || []
}) })
// 表单字段数据 const formFilter = ref('全部')
const formFields = computed(() => {
const formSections = computed(() => {
const formData = props.formInfo const formData = props.formInfo
if (!formData || Object.keys(formData).length === 0) { const emptyFields = [
return [
{ label: '姓名', value: '暂无数据' }, { label: '姓名', value: '暂无数据' },
{ label: '联系方式', value: '暂无数据' }, { label: '联系方式', value: '暂无数据' },
{ 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))
}
}
if (!formData || (typeof formData === 'object' && Object.keys(formData).length === 0)) {
return makeSection(emptyFields)
} }
let fields = [] let fields = []
// 检查是否为第一种格式包含name, mobile等字段
if (formData.name || formData.mobile || formData.child_name) { 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 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(' | ') const childInfo = [formData.child_name, formData.child_gender, formData.child_education].filter(item => item && item !== '暂无').join(' | ')
@@ -186,7 +234,6 @@ const formFields = computed(() => {
{ label: '地区', value: formData.territory || '暂无' } { label: '地区', value: formData.territory || '暂无' }
] ]
// 如果有additional_info添加所有问题
if (formData.additional_info && Array.isArray(formData.additional_info)) { if (formData.additional_info && Array.isArray(formData.additional_info)) {
formData.additional_info.forEach((item) => { formData.additional_info.forEach((item) => {
fields.push({ fields.push({
@@ -196,7 +243,6 @@ const formFields = computed(() => {
}) })
} }
} else { } else {
// 第二种格式expandXXX字段
const customerInfo = [formData.expandTwentyOne, formData.expandOne].filter(item => item && item !== '暂无').join(' | ') const customerInfo = [formData.expandTwentyOne, formData.expandOne].filter(item => item && item !== '暂无').join(' | ')
const childInfo = [formData.expandTwentyNine, formData.expandTwentyFive, formData.expandTwo].filter(item => item && item !== '暂无').join(' | ') const childInfo = [formData.expandTwentyNine, formData.expandTwentyFive, formData.expandTwo].filter(item => item && item !== '暂无').join(' | ')
@@ -212,12 +258,30 @@ const formFields = computed(() => {
{ label: '预期时间', value: formData.expandThirty || '暂无' } { 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(() => ({ const chatData = computed(() => ({
count: props.chatInfo?.messages?.length || 0, count: props.chatInfo?.messages?.length || 0,
@@ -232,7 +296,6 @@ const callData = computed(() => ({
// 通话记录列表 // 通话记录列表
const callRecords = computed(() => { const callRecords = computed(() => {
// 从 props.callInfo 中获取真实的通话记录数据 // 从 props.callInfo 中获取真实的通话记录数据
if (props.callInfo && Array.isArray(props.callInfo)) { if (props.callInfo && Array.isArray(props.callInfo)) {
console.log('通话记录:', props.callInfo) console.log('通话记录:', props.callInfo)
@@ -252,6 +315,28 @@ const callRecords = computed(() => {
return [] return []
}) })
// 新增:格式化通话时长的方法
const formatCallDuration = (durationInMinutes) => {
if (typeof durationInMinutes !== 'number' || durationInMinutes < 0) {
return '暂无';
}
const totalSeconds = Math.round(durationInMinutes * 60);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}${seconds}`;
};
// 新增根据分数获取CSS类的方法
const getScoreClass = (score) => {
if (score >= 80) {
return 'score-high';
} else if (score >= 60) {
return 'score-medium';
} else {
return 'score-low';
}
};
// 录音下载方法 // 录音下载方法
const downloadRecording = async (call) => { const downloadRecording = async (call) => {
console.log('下载录音:', call) console.log('下载录音:', call)
@@ -464,6 +549,58 @@ const formatDateTime = (dateTimeString) => {
flex: 1; 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-data-list {
.form-field { .form-field {
@@ -595,42 +732,61 @@ const formatDateTime = (dateTimeString) => {
.call-list { .call-list {
.call-item { .call-item {
margin-bottom: 16px; margin-bottom: 16px;
padding: 16px; padding: 20px;
border-radius: 8px; border-radius: 12px;
background: #f9fafb; background: #ffffff;
border-left: 4px solid #3b82f6; border: 1px solid #e5e7eb;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
&:hover {
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.call-header { .call-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
margin-bottom: 8px; .header-main {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
.user-info {
display: flex;
gap: 12px;
flex-wrap: wrap;
.call-type { .call-type {
font-size: 12px; font-size: 13px;
font-weight: 600; font-weight: 600;
padding: 4px 8px; padding: 5px 10px;
border-radius: 4px; border-radius: 20px;
background: #dbeafe; background: #dbeafe;
color: #3b82f6; color: #3b82f6;
} }
.call-duration { .call-customer {
font-size: 12px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: #6b7280; color: #6b7280;
align-self: center;
}
} }
.call-time { .header-tags {
font-size: 12px; display: flex;
color: #9ca3af; gap: 8px;
} align-items: center;
flex-wrap: wrap;
.call-tag { .call-tag {
font-size: 11px; font-size: 12px;
font-weight: 600; font-weight: 600;
padding: 3px 8px; padding: 4px 10px;
border-radius: 12px; border-radius: 20px;
&.tag-20min { &.tag-20min {
background: #dcfce7; background: #dcfce7;
@@ -638,12 +794,52 @@ const formatDateTime = (dateTimeString) => {
border: 1px solid #bbf7d0; border: 1px solid #bbf7d0;
} }
&.tag-invalid {
background: #fee2e2;
color: #dc2626;
border: 1px solid #fecaca;
}
&.tag-other { &.tag-other {
background: #fef3c7; background: #fef3c7;
color: #d97706; color: #d97706;
border: 1px solid #fed7aa; border: 1px solid #fed7aa;
} }
} }
// 分数样式
.stat-value {
font-size: 14px;
font-weight: 700;
padding: 4px 10px;
border-radius: 20px;
&.score-high {
color: #16a34a; // 绿色
background-color: #dcfce7;
}
&.score-medium {
color: #d97706; // 橙色
background-color: #fef3c7;
}
&.score-low {
color: #dc2626; // 红色
background-color: #fee2e2;
}
}
}
}
.call-time {
font-size: 12px;
color: #9ca3af;
font-weight: 500;
white-space: nowrap;
padding: 4px 8px;
background: #f9fafb;
border-radius: 4px;
align-self: flex-start;
}
} }
.call-actions { .call-actions {
@@ -651,25 +847,24 @@ const formatDateTime = (dateTimeString) => {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-top: 12px; margin-top: 12px;
padding-top: 8px;
border-top: 1px solid #f3f4f6;
.action-buttons { .action-buttons {
display: flex; display: flex;
gap: 8px; gap: 12px;
} align-items: center;
.action-btn { .action-btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 6px 10px; padding: 8px 14px;
border: none; border: none;
border-radius: 6px; border-radius: 8px;
font-size: 11px; font-size: 13px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&.download-btn { &.download-btn {
background: #dbeafe; background: #dbeafe;
@@ -677,8 +872,12 @@ const formatDateTime = (dateTimeString) => {
&:hover { &:hover {
background: #bfdbfe; background: #bfdbfe;
transform: translateY(-1px); transform: translateY(-2px);
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2); box-shadow: 0 4px 6px rgba(59, 130, 246, 0.2);
}
&:active {
transform: translateY(0);
} }
} }
@@ -688,14 +887,18 @@ const formatDateTime = (dateTimeString) => {
&:hover { &:hover {
background: #a7f3d0; background: #a7f3d0;
transform: translateY(-1px); transform: translateY(-2px);
box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2); box-shadow: 0 4px 6px rgba(5, 150, 105, 0.2);
}
&:active {
transform: translateY(0);
} }
} }
i { i {
width: 12px; font-style: normal;
height: 12px; font-size: 14px;
&.icon-download::before { &.icon-download::before {
content: '⬇'; content: '⬇';
@@ -707,14 +910,14 @@ const formatDateTime = (dateTimeString) => {
} }
} }
.call-time-info { // 通话时长样式
.call-duration { .call-duration {
font-size: 11px; font-size: 13px;
color: #6b7280; color: #6b7280;
font-weight: 500; font-weight: 500;
background: #f9fafb; background: #f9fafb;
padding: 4px 8px; padding: 6px 12px;
border-radius: 4px; border-radius: 20px;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
} }
} }
@@ -722,77 +925,6 @@ const formatDateTime = (dateTimeString) => {
} }
} }
.card-content {
margin-bottom: 20px;
}
.card-description {
color: #6b7280;
font-size: 14px;
margin: 0 0 16px 0;
line-height: 1.5;
}
.card-stats {
display: flex;
gap: 20px;
@media (max-width: 480px) {
flex-direction: column;
gap: 12px;
}
}
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-label {
font-size: 12px;
color: #9ca3af;
font-weight: 500;
}
.stat-value {
font-size: 14px;
color: #111827;
font-weight: 600;
}
.card-action {
border-top: 1px solid #f3f4f6;
padding-top: 16px;
margin-top: 16px;
}
.view-btn {
display: flex;
align-items: center;
gap: 8px;
background: none;
border: none;
color: #6b7280;
font-size: 14px;
font-weight: 500;
cursor: pointer;
padding: 8px 0;
transition: color 0.2s ease;
&:hover {
color: #111827;
}
svg {
transition: transform 0.2s ease;
}
&:hover svg {
transform: translateX(2px);
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.raw-data-cards { .raw-data-cards {
margin: 20px 0; margin: 20px 0;
@@ -840,11 +972,10 @@ const formatDateTime = (dateTimeString) => {
font-size: 14px; font-size: 14px;
} }
.card-description { .call-actions {
font-size: 13px; flex-direction: column;
align-items: flex-start;
gap: 10px;
} }
} }
// 弹框样式
</style> </style>

View File

@@ -323,7 +323,7 @@ const payMoneyCustomersList = ref([]);
const payMoneyCustomersCount = ref(0); const payMoneyCustomersCount = ref(0);
// 表单信息 // 表单信息
const formInfo = ref({}); const formInfo = ref([]);
// 通话记录 // 通话记录
const callRecords = ref([]); const callRecords = ref([]);
// 聊天记录 // 聊天记录
@@ -366,25 +366,37 @@ async function getCoreKpi() {
const params = getRequestParams() const params = getRequestParams()
const hasParams = params.user_name 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 (todayCallRes.code === 200) {
if (res.code === 200) { kpiDataState.totalCalls = todayCallRes.data.call_count
kpiDataState.totalCalls = res.data.today_call
} }
// 转化率、分配数据量、加微率 // 转化率、分配数据量、加微率
const conversionRes = await getConversionRateAndAllocatedData(hasParams ? params : undefined)
if (conversionRes.code === 200) { if (conversionRes.code === 200) {
kpiDataState.conversionRate = conversionRes.data.conversion_rate || 0 kpiDataState.conversionRate = conversionRes.data.conversion_rate || 0
kpiDataState.assignedData = conversionRes.data.all_count || 0 kpiDataState.assignedData = conversionRes.data.all_count || 0
kpiDataState.wechatAddRate = conversionRes.data.plus_v_conversion_rate || 0 kpiDataState.wechatAddRate = conversionRes.data.plus_v_conversion_rate || 0
} }
// 平均通话时长 // 平均通话时长
const avgCallTimeRes = await getAvgCallTime(hasParams ? params : undefined)
if (avgCallTimeRes.code === 200) { if (avgCallTimeRes.code === 200) {
kpiDataState.avgDuration = avgCallTimeRes.data.call_time || 0 kpiDataState.avgDuration = avgCallTimeRes.data.call_time || 0
} }
// 电话接通率 // 电话接通率
const callSuccessRateRes = await getCallSuccessRate(hasParams ? params : undefined)
if (callSuccessRateRes.code === 200) { if (callSuccessRateRes.code === 200) {
kpiDataState.successRate = callSuccessRateRes.data.call_success_rate || 0 kpiDataState.successRate = callSuccessRateRes.data.call_success_rate || 0
} }
@@ -401,26 +413,35 @@ async function getStatisticsData() {
const params = getRequestParams() const params = getRequestParams()
const hasParams = params.user_name 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) { if (fillingRateRes.code === 200) {
statisticsData.formCompletionRate = fillingRateRes.data.filling_rate statisticsData.formCompletionRate = fillingRateRes.data.filling_rate
} }
// 获取平均响应时间 // 处理平均响应时间
const avgResponseRes = await getAverageResponseTime(hasParams ? params : undefined)
if (avgResponseRes.code === 200) { if (avgResponseRes.code === 200) {
statisticsData.averageResponseTime = avgResponseRes.data.average_minutes statisticsData.averageResponseTime = avgResponseRes.data.average_minutes
} }
// 获取客户沟通率 // 处理客户沟通率
const communicationRes = await getWeeklyActiveCommunicationRate(hasParams ? params : undefined)
if (communicationRes.code === 200) { if (communicationRes.code === 200) {
statisticsData.customerCommunicationRate = communicationRes.data.communication_rate statisticsData.customerCommunicationRate = communicationRes.data.communication_rate
} }
// 获取超时响应率 // 处理超时响应率
const timeoutRes = await getTimeoutResponseRate(hasParams ? params : undefined)
if (timeoutRes.code === 200) { if (timeoutRes.code === 200) {
statisticsData.timeoutResponseRate = timeoutRes.data.overtime_rate_600 statisticsData.timeoutResponseRate = timeoutRes.data.overtime_rate_600
statisticsData.severeTimeoutRate = timeoutRes.data.overtime_rate_800 statisticsData.severeTimeoutRate = timeoutRes.data.overtime_rate_800
@@ -578,12 +599,13 @@ async function getCustomerForm() {
const routeParams = getRequestParams() const routeParams = getRequestParams()
const params = { const params = {
user_name: routeParams.user_name || userStore.userInfo.username, user_name: routeParams.user_name || userStore.userInfo.username,
customer_name: selectedContact.value.name, phone: selectedContact.value.phone,
} }
try { try {
const res = await getCustomerFormInfo(params) const res = await getCustomerFormInfo(params)
console.log('获取客户表单数据:', res)
if(res.code === 200) { if(res.code === 200) {
formInfo.value = res.data formInfo.value = res.data || []
} }
} catch (error) { } catch (error) {
// 静默处理错误 // 静默处理错误
@@ -597,7 +619,7 @@ async function getCustomerChat() {
const routeParams = getRequestParams() const routeParams = getRequestParams()
const params = { const params = {
user_name: routeParams.user_name || userStore.userInfo.username, user_name: routeParams.user_name || userStore.userInfo.username,
customer_name: selectedContact.value.name, phone: selectedContact.value.phone,
} }
try { try {
const res = await getCustomerChatInfo(params) const res = await getCustomerChatInfo(params)
@@ -618,14 +640,12 @@ async function getCustomerCall() {
const routeParams = getRequestParams() const routeParams = getRequestParams()
const params = { const params = {
user_name: routeParams.user_name || userStore.userInfo.username, user_name: routeParams.user_name || userStore.userInfo.username,
customer_name: selectedContact.value.name, phone: selectedContact.value.phone,
} }
try { try {
const res = await getCustomerCallInfo(params) const res = await getCustomerCallInfo(params)
if(res.code === 200) { if(res.code === 200) {
callRecords.value = res.data callRecords.value = res.data
console.log('Call Records Data from API:', res.data)
console.log('callRecords.value after assignment:', callRecords.value)
} }
} catch (error) { } catch (error) {
// 静默处理错误 // 静默处理错误
@@ -957,15 +977,15 @@ async function forceRefreshAllData() {
onMounted(async () => { onMounted(async () => {
try { try {
isPageLoading.value = true isPageLoading.value = true
await getStatisticsData() getStatisticsData()
await getCoreKpi() getCoreKpi()
await CenterGetGoldContactTime() CenterGetGoldContactTime()
await CenterGetSalesFunnel() CenterGetSalesFunnel()
await getCustomerForm() getCustomerForm()
await getCustomerChat() getCustomerChat()
await getUrgentProblem() getUrgentProblem()
await getCustomerCall() getCustomerCall()
await getTimeline() getTimeline()
// 开发环境下暴露数据刷新函数到全局对象,方便调试 // 开发环境下暴露数据刷新函数到全局对象,方便调试
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
@@ -1821,10 +1841,10 @@ $primary: #3b82f6;
background: #ffffff; background: #ffffff;
border-radius: 12px; 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); 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%; width: 90%;
// 设置最大高度,防止弹窗超出屏幕 // 设置最大高度,防止弹窗超出屏幕
max-height: 35vh; max-height: 80vh;
// 防止内容溢出容器,配合内部滚动 // 防止内容溢出容器,配合内部滚动
overflow: hidden; overflow: hidden;
// 使用 Flexbox 布局,让 .modal-body 可以伸缩 // 使用 Flexbox 布局,让 .modal-body 可以伸缩

View File

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

View File

@@ -18,6 +18,7 @@
:class="{ active: selectedRecording === index }" :class="{ active: selectedRecording === index }"
@click="selectRecording(index)" @click="selectRecording(index)"
> >
<span class="recording-index">{{ recording.score}}</span>
<div class="recording-info"> <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-name" :title="recording.name">{{ recording.name.length > 10 ? recording.name.substring(0, 10) + '...' : recording.name }}</div>
<div class="recording-meta"> <div class="recording-meta">
@@ -172,8 +173,8 @@ import MarkdownIt from 'markdown-it'
// Props定义 // Props定义
const props = defineProps({ const props = defineProps({
qualityCalls: { qualityCalls: {
type: Object, type: Array,
default: () => ({}) default: () => []
} }
}) })
@@ -221,25 +222,23 @@ const recordings = computed(() => {
if (!props.qualityCalls ) { if (!props.qualityCalls ) {
return staticRecordings.value; return staticRecordings.value;
} }
const recordingsList = []; const recordingsList = [];
Object.keys(props.qualityCalls).forEach(userName => { props.qualityCalls.forEach((record, index) => {
props.qualityCalls[userName].forEach((record, index) => {
recordingsList.push({ recordingsList.push({
id: recordingsList.length + 1, 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], date: new Date().toISOString().split('T')[0],
url: record.obj_file_name, url: record.record_file_addr,
transcription: record.context || null, transcription: record.record_context || null,
score: record.score, score: record.record_score,
sop: record.sop, sop: record.record_report,
sale_name: record.sale_name, sale_name: record.record_name,
size: 2048576, // 默认文件大小 2MB size: 2048576, // 默认文件大小 2MB
uploadTime: new Date().toLocaleDateString('zh-CN') uploadTime: record.created_at,
});
}); });
}); });
return recordingsList; return recordingsList;
}) })
@@ -505,14 +504,14 @@ const downloadRecording = (index) => {
background: white; background: white;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
height: 400px; height: 420px;
} }
.chart-header { .chart-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 20px 0; padding: 10px 20px 0;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
} }
@@ -549,7 +548,7 @@ const downloadRecording = (index) => {
} }
.chart-content { .chart-content {
padding: 20px; padding: 10px;
} }
.recording-section { .recording-section {
@@ -562,7 +561,6 @@ const downloadRecording = (index) => {
.recording-list { .recording-list {
margin-bottom: 20px; margin-bottom: 20px;
max-height: 400px; max-height: 400px;
overflow-y: auto;
} }
.recording-item { .recording-item {
@@ -602,6 +600,39 @@ const downloadRecording = (index) => {
display: inline-block; 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 { .recording-meta {
display: flex; display: flex;
gap: 12px; gap: 12px;

View File

@@ -90,12 +90,14 @@ async function exportData() {
try { try {
ElMessage.info('正在导出数据,请稍候...') ElMessage.info('正在导出数据,请稍候...')
console.log('导出参数:', params) console.log('导出参数:', params)
const res = await exportCustomers(params) const res = await exportCustomers()
if (res.code === 200 && res.data && res.data.length > 0) { if (res.code === 200 && res.data && res.data.length > 0) {
ElMessage.success('数据导出成功')
// 处理数据,将复杂的嵌套对象展平 // 处理数据,将复杂的嵌套对象展平
const exportData = res.data.map(customer => { const exportData = res.data.map(customer => {
const flatData = { const flatData = {
'昵称': customer.nickname || '', '昵称': customer.nickname || '',
'客户姓名': customer.customer_name || '',
'性别': customer.gender || '', '性别': customer.gender || '',
'跟进人': customer.follow_up_name || '', '跟进人': customer.follow_up_name || '',
'手机号': customer.phone || '', '手机号': customer.phone || '',
@@ -103,18 +105,104 @@ async function exportData() {
'用户ID': customer.mantis_user_id || '', '用户ID': customer.mantis_user_id || '',
} }
// 处理微信表单信息 const parseFormData = (formData) => {
if (customer.wechat_form) { if (typeof formData === 'string') {
flatData['家长姓名'] = customer.wechat_form.name || '' try {
flatData['孩子姓名'] = customer.wechat_form.child_name || '' return JSON.parse(formData)
flatData['孩子性别'] = customer.wechat_form.child_gender || '' } catch (e) {
flatData['职业'] = customer.wechat_form.occupation || '' return null
flatData['孩子教育阶段'] = customer.wechat_form.child_education || '' }
flatData['与孩子关系'] = customer.wechat_form.child_relation || '' }
flatData['联系电话'] = customer.wechat_form.mobile || '' return formData || null
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 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) { if (parsedFormData && parsedFormData.additional_info) {
customer.wechat_form.additional_info.forEach((item) => { parsedFormData.additional_info.forEach((item) => {
flatData[item.topic || ''] = item.answer || '' 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 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 = [ const colWidths = allKeys.map(key => {
{ wch: 10 }, // 昵称 const maxCellLength = exportData.reduce((max, row) => {
{ wch: 6 }, // 性别 const value = row[key]
{ wch: 12 }, // 跟进人 const length = value === null || value === undefined ? 0 : String(value).length
{ wch: 15 }, // 手机号 return Math.max(max, length)
{ wch: 10 }, // 是否入群 }, 0)
{ wch: 20 }, // 用户ID return { wch: Math.min(50, Math.max(10, key.length, maxCellLength)) }
{ wch: 12 }, // 家长姓名 })
{ wch: 12 }, // 孩子姓名
{ wch: 8 }, // 孩子性别
{ wch: 12 }, // 职业
{ wch: 12 }, // 孩子教育阶段
{ wch: 15 }, // 与孩子关系
{ wch: 15 }, // 联系电话
{ wch: 20 }, // 地区
{ wch: 20 }, // 创建时间
{ wch: 20 }, // 更新时间
]
ws['!cols'] = colWidths ws['!cols'] = colWidths
// 添加工作表到工作簿 // 添加工作表到工作簿
@@ -196,7 +278,7 @@ async function exportData() {
} }
.chart-header { .chart-header {
padding: 20px 20px 16px; padding: 10px 20px 10px;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@@ -112,6 +112,22 @@
<span class="value">{{ selectedGroup.conversionRate }}%</span> <span class="value">{{ selectedGroup.conversionRate }}%</span>
</div> </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">
<p>这里是团队整体分析的内容</p>
</div>
</div>
</div> </div>
<div class="members-grid"> <div class="members-grid">
@@ -178,7 +194,7 @@
</main> </main>
<!-- Loading 组件 --> <!-- Loading 组件 -->
<Loading :visible="isLoading" text="数据加载中..." /> <!-- <Loading :visible="isLoading" text="数据加载中..." /> -->
</div> </div>
</template> </template>
@@ -283,6 +299,9 @@ const cardVisibility = ref({
// FeedbackForm 控制变量 // FeedbackForm 控制变量
const showFeedbackForm = ref(false) const showFeedbackForm = ref(false)
// 团队整体分析弹窗控制变量
const showTeamAnalysis = ref(false)
// 更新卡片显示状态 // 更新卡片显示状态
const updateCardVisibility = (newVisibility) => { const updateCardVisibility = (newVisibility) => {
Object.assign(cardVisibility.value, newVisibility) Object.assign(cardVisibility.value, newVisibility)
@@ -297,6 +316,15 @@ const showFeedbackFormModal = () => {
const closeFeedbackFormModal = () => { const closeFeedbackFormModal = () => {
showFeedbackForm.value = false 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 // This would ideally come from a prop or API call based on the logged-in user
const centerData = ref({ const centerData = ref({
@@ -860,31 +888,46 @@ const conversionRateVsAverage = ref({})
}) })
} }
// 获取优秀录音 // 获取优秀录音
const excellentRecord = ref({}); const excellentRecord = ref([]);
// 获取优秀录音文件 // 获取优秀录音文件
// async function CentergetGoodRecord() { async function CentergetGoodRecord() {
// const params = getRequestParams() console.log('CentergetGoodRecord 开始执行')
// const params1 = { try {
// user_level:userStore.userInfo.user_level.toString(), const params = getRequestParams()
// user_name:userStore.userInfo.username const params1 = {
// } user_level: userStore.userInfo?.user_level?.toString() || '',
// const hasParams = params.user_name user_name: userStore.userInfo?.username || ''
// const requestParams = hasParams ? { }
// ...params,
// } : params1
// console.log(188811111,requestParams)
// try { // 检查参数是否有效
// const res = await withCache('CentergetGoodRecord', const hasParams = params.user_name && params.user_level
// () => getExcellentRecordFile(requestParams), const requestParams = hasParams ? {
// requestParams ...params,
// ) } : params1
// excellentRecord.value = res.data.excellent_record_list
// console.log(111111,res.data.excellent_record_list) console.log('CentergetGoodRecord request params:', requestParams)
// } catch (error) {
// console.error("获取优秀录音失败:", error); // 验证必要参数是否存在
// } 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) (currentQuery.user_name && currentQuery.user_level)
if (!isFromRoute) { if (!isFromRoute) {
await CenterCampPeriodAdmin() CenterCampPeriodAdmin()
} }
CentergetGoodRecord()
CenterOverallCenterPerformance()
CenterTotalGroupCount()
CenterConversionRate()
CenterTotalCallCount()
CenterNewCustomer()
CenterDepositConversionRate()
CenterCustomerType()
CenterUrgentNeedToAddress()
CenterConversionRateVsAverage()
await CenterOverallCenterPerformance() CenterSeniorManagerList()
await CenterTotalGroupCount() CenterGroupList('all')
await CenterConversionRate()
await CenterTotalCallCount()
await CenterNewCustomer()
await CenterDepositConversionRate()
await CenterCustomerType()
await CenterUrgentNeedToAddress()
await CenterConversionRateVsAverage()
await CenterSeniorManagerList()
await CenterGroupList('all')
console.log('[强制刷新] 所有数据已重新加载') console.log('[强制刷新] 所有数据已重新加载')
} catch (error) { } catch (error) {
@@ -980,6 +1024,7 @@ const excellentRecord = ref({});
await CenterTotalGroupCount() await CenterTotalGroupCount()
await CenterConversionRate() await CenterConversionRate()
await CenterTotalCallCount() await CenterTotalCallCount()
await CentergetGoodRecord()
await CenterNewCustomer() await CenterNewCustomer()
await CenterDepositConversionRate() await CenterDepositConversionRate()
await CenterCustomerType() 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 { .route-header {
display: flex; display: flex;

View File

@@ -24,10 +24,10 @@
团队总业绩 团队总业绩
<span class="info-icon" @mouseenter="showTooltip($event, 'teamPerformance')" @mouseleave="hideTooltip"></span> <span class="info-icon" @mouseenter="showTooltip($event, 'teamPerformance')" @mouseleave="hideTooltip"></span>
</span> </span>
<span class="card-trend positive">{{ totalPerformance.team_current_vs_previous_deals }} vs 上期</span> <span class="card-trend positive">{{ totalPerformance.team_current_vs_previous_period_deals_comparison }} vs 上期</span>
</div> </div>
<div class="card-value">{{ totalPerformance.current_team_odd_numbers||0 }}</div> <div class="card-value">{{ totalPerformance.current_team_odd_numbers||0 }}</div>
<div class="card-subtitle">月目标完成率: {{ totalPerformance.team_monthly_performance }}</div> <div class="card-subtitle">月目标完成率: {{ totalPerformance.team_monthly_target_completion_rate }}</div>
</div> </div>
<div class="overview-card"> <div class="overview-card">
@@ -48,7 +48,7 @@
团队转化率 团队转化率
<span class="info-icon" @mouseenter="showTooltip($event, 'conversionRate')" @mouseleave="hideTooltip"></span> <span class="info-icon" @mouseenter="showTooltip($event, 'conversionRate')" @mouseleave="hideTooltip"></span>
</span> </span>
<span class="card-trend positive">{{ conversionRate.team_current_vs_previous_deals }} vs 上期</span> <span class="card-trend positive">{{ conversionRate.team_current_vs_previous_conversion_rate }} vs 上期</span>
</div> </div>
<div class="card-value">{{ conversionRate.center_conversion_rate }}</div> <div class="card-value">{{ conversionRate.center_conversion_rate }}</div>
<div class="card-subtitle">团队平均转化率: {{ conversionRate.average_conversion_rate }}</div> <div class="card-subtitle">团队平均转化率: {{ conversionRate.average_conversion_rate }}</div>
@@ -135,7 +135,6 @@ const props = defineProps({
}) })
} }
}) })
console.log(99999,props.overallTeamPerformance)
// 计算属性 // 计算属性
const totalPerformance = computed(() => { const totalPerformance = computed(() => {
return props.overallTeamPerformance.totalPerformance return props.overallTeamPerformance.totalPerformance
@@ -158,7 +157,6 @@ const newCustomers = computed(() => {
}) })
const depositConversions = computed(() => { const depositConversions = computed(() => {
console.log(999991111,props.overallTeamPerformance.depositConversions)
return 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; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0px 20px 16px; padding: 0px 20px 10px;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
h3 { h3 {
margin: 0; margin: 0;

View File

@@ -60,42 +60,41 @@ import { ref, computed, onMounted, watch } from 'vue';
// 假设你有一个API服务来获取对比数据 // 假设你有一个API服务来获取对比数据
// import { getPerformanceComparisonData } from '@/api/senorManger.js'; // import { getPerformanceComparisonData } from '@/api/senorManger.js';
// 模拟API调用 // **MODIFIED**: 更新模拟API以返回新的数据结构
const getPerformanceComparisonData = async (params) => { const getPerformanceComparisonData = async (params) => {
console.log('模拟API请求:', params); console.log('模拟API请求 (新数据结构):', params);
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { setTimeout(() => {
// 模拟不同周期返回不同数据
let mockData; let mockData;
if (params.period === 'last_week') { if (params.period === 'last_week') {
mockData = { mockData = {
assignedLeads: 480, "allocated_data_volume": 650,
wechatAdds: 390, "wechat_added_volume": 410,
calls: 1450, "call_volume_after_classification": { "加微通话": 10, "20分钟通话": 50, "未分类": 150, "无效通话": 100, "促到课": 40 },
callDuration: 11500, "call_avg_duration_after_classification": { "加微通话": 4.5, "20分钟通话": 15.0, "未分类": 1.5, "无效通话": 1.0, "促到课": 2.0 },
deposits: 38, "deposit_volume": 40,
deals: 50, "transaction_volume": 52,
conversionRate: 10.4, "conversion_rate": "8.00%"
}; };
} else if (params.period === 'last_month') { } else if (params.period === 'last_month') {
mockData = { mockData = {
assignedLeads: 2000, "allocated_data_volume": 2800,
wechatAdds: 1500, "wechat_added_volume": 1800,
calls: 5800, "call_volume_after_classification": { "加微通话": 40, "20分钟通话": 220, "未分类": 600, "视频通话": 50, "无效通话": 450, "促到课": 180 },
callDuration: 48000, "call_avg_duration_after_classification": { "加微通话": 5.0, "20分钟通话": 16.0, "未分类": 1.8, "视频通话": 6.0, "无效通话": 1.1, "促到课": 1.5 },
deposits: 150, "deposit_volume": 180,
deals: 210, "transaction_volume": 230,
conversionRate: 10.5, "conversion_rate": "8.21%"
}; };
} else { } else { // last_quarter
mockData = { mockData = {
assignedLeads: 6500, "allocated_data_volume": 8500,
wechatAdds: 5200, "wechat_added_volume": 5500,
calls: 18000, "call_volume_after_classification": { "加微通话": 120, "20分钟通话": 650, "未分类": 1800, "视频通话": 150, "无效通话": 1200, "促到课": 500 },
callDuration: 150000, "call_avg_duration_after_classification": { "加微通话": 4.8, "20分钟通话": 16.5, "未分类": 1.7, "视频通话": 5.5, "无效通话": 1.0, "促到课": 1.8 },
deposits: 450, "deposit_volume": 550,
deals: 600, "transaction_volume": 700,
conversionRate: 9.2, "conversion_rate": "8.24%"
}; };
} }
resolve({ code: 200, data: mockData }); resolve({ code: 200, data: mockData });
@@ -125,12 +124,11 @@ const selectedPeriodLabel = computed(() => periodLabels[selectedPeriod.value]);
const fetchComparisonData = async () => { const fetchComparisonData = async () => {
isLoading.value = true; isLoading.value = true;
try { try {
// 真实场景中这里应该调用API
const res = await getPerformanceComparisonData({ period: selectedPeriod.value }); const res = await getPerformanceComparisonData({ period: selectedPeriod.value });
if (res.code === 200) { if (res.code === 200) {
previousPeriodData.value = res.data; previousPeriodData.value = res.data;
} else { } else {
previousPeriodData.value = null; // API出错或无数据 previousPeriodData.value = null;
} }
} catch (error) { } catch (error) {
console.error('获取对比数据失败:', error); console.error('获取对比数据失败:', error);
@@ -144,14 +142,54 @@ onMounted(() => {
fetchComparisonData(); fetchComparisonData();
}); });
// 如果本期数据可能变化可以监听props来刷新
watch(() => props.currentPeriodData, () => { watch(() => props.currentPeriodData, () => {
fetchComparisonData(); fetchComparisonData();
}, { deep: true }); }, { deep: true });
const comparedMetrics = computed(() => { // **NEW**: 新增一个工具函数用于处理API返回的复杂数据结构
if (!props.currentPeriodData || !previousPeriodData.value) return []; // 并将其转换为表格渲染所需的扁平化结构
const processRawData = (rawData) => {
if (!rawData) return null;
// 1. 计算通话总量
const totalCalls = Object.values(rawData.call_volume_after_classification || {})
.reduce((sum, count) => sum + count, 0);
// 2. 计算通话总时长 (分钟)
// 逻辑: (分类通话数 * 分类平均时长) 的总和
const callVolumes = rawData.call_volume_after_classification || {};
const avgDurations = rawData.call_avg_duration_after_classification || {};
const totalDurationInMinutes = Object.keys(callVolumes).reduce((sum, key) => {
const volume = callVolumes[key] || 0;
const avgDuration = avgDurations[key] || 0;
return sum + (volume * avgDuration);
}, 0);
// 3. 将转化率字符串 "7.98%" 转为数字 7.98
const conversionRateValue = parseFloat(rawData.conversion_rate) || 0;
// 4. 返回一个与旧版组件兼容的对象结构
return {
assignedLeads: rawData.allocated_data_volume,
wechatAdds: rawData.wechat_added_volume,
calls: totalCalls,
// 组件内部格式化时会除以60因此这里需要乘以60将分钟转为秒
callDuration: totalDurationInMinutes * 60,
deposits: rawData.deposit_volume,
deals: rawData.transaction_volume,
conversionRate: conversionRateValue,
};
};
const comparedMetrics = computed(() => {
// **MODIFIED**: 在计算前,先使用 processRawData 对数据进行处理
const processedCurrentData = processRawData(props.currentPeriodData);
const processedPreviousData = processRawData(previousPeriodData.value);
if (!processedCurrentData || !processedPreviousData) return [];
// 指标配置保持不变,因为数据已经被处理成它期望的格式
const metricsConfig = [ const metricsConfig = [
{ key: 'assignedLeads', label: '分配数据量', unit: '个' }, { key: 'assignedLeads', label: '分配数据量', unit: '个' },
{ key: 'wechatAdds', label: '加微量', unit: '个' }, { key: 'wechatAdds', label: '加微量', unit: '个' },
@@ -163,8 +201,8 @@ const comparedMetrics = computed(() => {
]; ];
return metricsConfig.map(metric => { return metricsConfig.map(metric => {
const current = props.currentPeriodData[metric.key]; const current = processedCurrentData[metric.key];
const previous = previousPeriodData.value[metric.key]; const previous = processedPreviousData[metric.key];
return { return {
...metric, ...metric,
current, current,
@@ -198,6 +236,7 @@ const calculateChange = (current, previous) => {
const formatValue = (value, unit) => { const formatValue = (value, unit) => {
if (value === null || value === undefined) return '-'; if (value === null || value === undefined) return '-';
if (unit === '分钟') { if (unit === '分钟') {
// 假设传入的value是秒转换为分钟显示
return `${Math.round(value / 60)} 分钟`; return `${Math.round(value / 60)} 分钟`;
} }
if (unit === '%') { if (unit === '%') {
@@ -212,6 +251,7 @@ const formatChange = (diff, unit) => {
const absDiff = Math.abs(diff); const absDiff = Math.abs(diff);
if (unit === '分钟') { if (unit === '分钟') {
// 假设diff是秒转换为分钟显示
return `${prefix}${Math.round(absDiff / 60)} 分钟`; return `${prefix}${Math.round(absDiff / 60)} 分钟`;
} }
if (unit === '%') { if (unit === '%') {
@@ -229,12 +269,12 @@ const getChangeClass = (trend) => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 样式部分无需改动,因此省略以保持简洁 */
.performance-comparison { .performance-comparison {
background: #ffffff; background: #ffffff;
border-radius: 12px; border-radius: 12px;
padding: 2rem; padding: 2rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
margin-top: 1.5rem;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
} }
@@ -242,7 +282,6 @@ const getChangeClass = (trend) => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem; padding-bottom: 1rem;
border-bottom: 1px solid #f1f5f9; border-bottom: 1px solid #f1f5f9;

View File

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

View File

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

View File

@@ -28,12 +28,36 @@
<div v-if="!isRouteNavigation"> <div v-if="!isRouteNavigation">
<!-- 用户下拉菜单 --> <!-- 用户下拉菜单 -->
<div style="display: flex; align-items: center; gap: 20px;"> <div style="display: flex; align-items: center; gap: 20px;">
<button @click="showDepartmentAnalysisModal" class="feedback-btn">部门分析</button>
<button @click="showFeedbackFormModal" class="feedback-btn">意见反馈</button> <button @click="showFeedbackFormModal" class="feedback-btn">意见反馈</button>
<FeedbackForm <FeedbackForm
:is-visible="showFeedbackForm" :is-visible="showFeedbackForm"
@close="closeFeedbackFormModal" @close="closeFeedbackFormModal"
@submit-feedback="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 <UserDropdown
:card-visibility="cardVisibility" :card-visibility="cardVisibility"
@update-card-visibility="updateCardVisibility" @update-card-visibility="updateCardVisibility"
@@ -53,7 +77,12 @@
@update-check-type="updateCheckType" @update-check-type="updateCheckType"
/> />
<div v-if="cardVisibility.teamAlerts" class="action-items-compact"> <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>
</div> </div>
<StatisticalIndicators <StatisticalIndicators
@@ -64,6 +93,10 @@
:severeTimeoutRate="statisticalIndicators.severeTimeoutRate" :severeTimeoutRate="statisticalIndicators.severeTimeoutRate"
:formCompletionRate="statisticalIndicators.formCompletionRate" :formCompletionRate="statisticalIndicators.formCompletionRate"
/> />
<!-- 新增业绩周期对比组件 -->
<div v-if="false" class="performance-comparison-section">
<PerformanceComparison :current-period-data="currentPeriodMetrics" />
</div>
<!-- Bottom Section --> <!-- Bottom Section -->
<div class="bottom-section"> <div class="bottom-section">
<!-- Left Section - Group Performance Ranking --> <!-- Left Section - Group Performance Ranking -->
@@ -75,10 +108,7 @@
@select-group="selectGroup" @select-group="selectGroup"
/> />
</div> </div>
<div v-if="cardVisibility.problemRanking" class="problem-ranking"> <GoodMusic style="height: 300px;" :qualityCalls="excellentRecord" />
<!-- 客户迫切解决的问题 -->
<ProblemRanking :problemRanking="problemRanking" />
</div>
<!-- Right Section - Group Comparison --> <!-- Right Section - Group Comparison -->
<div v-if="cardVisibility.groupComparison" class="right-section"> <div v-if="cardVisibility.groupComparison" class="right-section">
<GroupComparison <GroupComparison
@@ -90,10 +120,7 @@
</div> </div>
</div> </div>
<!-- 新增业绩周期对比组件 -->
<div v-if="cardVisibility.performanceComparison" class="performance-comparison-section">
<PerformanceComparison :current-period-data="currentPeriodMetrics" />
</div>
<!-- Team Members Detail Section --> <!-- Team Members Detail Section -->
<div class="team-detail-section" v-if="selectedGroup && cardVisibility.teamDetail"> <div class="team-detail-section" v-if="selectedGroup && cardVisibility.teamDetail">
@@ -105,6 +132,7 @@
<!-- 团队详情内容 --> <!-- 团队详情内容 -->
<div v-else> <div v-else>
<div class="team-detail-header"> <div class="team-detail-header">
<div>
<h2>{{ selectedGroup.name }} - 团队成员详情</h2> <h2>{{ selectedGroup.name }} - 团队成员详情</h2>
<div class="team-summary"> <div class="team-summary">
<div class="summary-item"> <div class="summary-item">
@@ -126,6 +154,35 @@
</div> </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"> <div class="members-grid">
<div <div
v-for="member in teamPerformanceDetail.group_details" v-for="member in teamPerformanceDetail.group_details"
@@ -200,6 +257,7 @@ import Tooltip from '@/components/Tooltip.vue'
import CenterOverview from './components/CenterOverview.vue' import CenterOverview from './components/CenterOverview.vue'
import GroupComparison from './components/GroupComparison.vue' import GroupComparison from './components/GroupComparison.vue'
import GroupRanking from './components/GroupRanking.vue' import GroupRanking from './components/GroupRanking.vue'
import GoodMusic from './components/GoodMusic.vue'
import TeamAlerts from '../maneger/components/TeamAlerts.vue' import TeamAlerts from '../maneger/components/TeamAlerts.vue'
import ProblemRanking from './components/ProblemRanking.vue' import ProblemRanking from './components/ProblemRanking.vue'
import StatisticalIndicators from './components/StatisticalIndicators.vue' import StatisticalIndicators from './components/StatisticalIndicators.vue'
@@ -208,8 +266,9 @@ import Loading from '@/components/Loading.vue'
import PerformanceComparison from './components/PerformanceComparison.vue'; // 1. 导入新组件 import PerformanceComparison from './components/PerformanceComparison.vue'; // 1. 导入新组件
import { getOverallTeamPerformance,getTotalGroupCount,getConversionRate,getTotalCallCount, import { getOverallTeamPerformance,getTotalGroupCount,getConversionRate,getTotalCallCount,
getNewCustomer,getDepositConversionRate,getActiveCustomerCommunicationRate,getAverageAnswerTime, getNewCustomer,getDepositConversionRate,getActiveCustomerCommunicationRate,getAverageAnswerTime,
getTimeoutRate,getTableFillingRate,getUrgentNeedToAddress,getTeamRanking,getTeamRankingInfo,getAbnormalResponseRate,getTeamSalesFunnel } from '@/api/senorManger.js' getTimeoutRate,getTableFillingRate,getUrgentNeedToAddress,getTeamRanking,getTeamRankingInfo,
getAbnormalResponseRate,getTeamSalesFunnel,getExcellentRecordFile,getTeamEveryGroupReport,
getTeamEntiretyReport } from '@/api/senorManger.js'
import { useUserStore } from '@/stores/user.js' import { useUserStore } from '@/stores/user.js'
import FeedbackForm from "@/components/FeedbackForm.vue"; import FeedbackForm from "@/components/FeedbackForm.vue";
@@ -247,7 +306,6 @@ const withCache = async (functionName, apiCall, params = {}) => {
return cachedData return cachedData
} }
console.log(`调用API获取数据: ${functionName}`, params)
const result = await apiCall() const result = await apiCall()
setCache(cacheKey, result) setCache(cacheKey, result)
return result return result
@@ -259,6 +317,25 @@ const clearCache = () => {
console.log('所有缓存已清除') 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 clearSpecificCache = (functionName, params = {}) => {
const cacheKey = getCacheKey(functionName, params) const cacheKey = getCacheKey(functionName, params)
cache.delete(cacheKey) cache.delete(cacheKey)
@@ -294,19 +371,19 @@ const forceRefreshAllData = async () => {
try { try {
isLoading.value = true isLoading.value = true
await fetchOverallTeamPerformance() fetchOverallTeamPerformance()
await fetchActiveGroups() fetchActiveGroups()
await fetchConversionRate() fetchConversionRate()
await fetchTotalCallCount() fetchTotalCallCount()
await fetchNewCustomers() fetchNewCustomers()
await fetchDepositConversions() fetchDepositConversions()
await fetchAbnormalResponseRate() fetchAbnormalResponseRate()
await fetchCustomerCommunicationRate() fetchCustomerCommunicationRate()
await fetchAverageResponseTime() fetchAverageResponseTime()
await fetchTimeoutRate() fetchTimeoutRate()
await fetchTableFillingRate() fetchTableFillingRate()
await fetchUrgentNeedToAddress() fetchUrgentNeedToAddress()
await fetchTeamRanking() fetchTeamRanking()
console.log('所有数据已强制刷新完成') console.log('所有数据已强制刷新完成')
} catch (error) { } catch (error) {
console.error('强制刷新数据失败:', error) console.error('强制刷新数据失败:', error)
@@ -323,6 +400,14 @@ const formCompletionRate = ref(90)
const CheckType = ref('month') const CheckType = ref('month')
// FeedbackForm 控制变量 // FeedbackForm 控制变量
const showFeedbackForm = ref(false) const showFeedbackForm = ref(false)
// 部门分析弹窗控制变量
const showDepartmentAnalysis = ref(false)
// 团队分析弹窗控制变量
const showTeamAnalysis = ref(false)
// 团队分析数据
const teamAnalysisData = ref([])
// 部门分析数据
const departmentAnalysisData = ref([])
// 更新CheckType的方法 // 更新CheckType的方法
const updateCheckType = async (newValue) => { const updateCheckType = async (newValue) => {
@@ -343,6 +428,90 @@ const closeFeedbackFormModal = () => {
showFeedbackForm.value = false 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({ const cardVisibility = ref({
centerOverview: true, centerOverview: true,
@@ -447,7 +616,6 @@ async function fetchActiveGroups() {
requestParams requestParams
) )
overallTeamPerformance.value.activeGroups = response.data overallTeamPerformance.value.activeGroups = response.data
console.log('活跃组数:', response.data)
} catch (error) { } catch (error) {
console.error('获取活跃组数失败:', error) console.error('获取活跃组数失败:', error)
} }
@@ -528,18 +696,17 @@ async function fetchDepositConversions() {
requestParams requestParams
) )
overallTeamPerformance.value.depositConversions = response.data overallTeamPerformance.value.depositConversions = response.data
console.log(99888999,response.data)
} catch (error) { } catch (error) {
console.error('获取定金转化失败:', error) console.error('获取定金转化失败:', error)
} }
} }
const statisticalIndicators = ref({ const statisticalIndicators = ref({
customerCommunicationRate: 0, customerCommunicationRate: {},
averageResponseTime: 0, averageResponseTime: {},
timeoutResponseRate: 0, timeoutResponseRate: {},
severeTimeoutRate: 0, severeTimeoutRate: {},
formCompletionRate: 0, formCompletionRate: {},
}) })
// 销售漏斗 // 销售漏斗
@@ -571,17 +738,18 @@ async function fetchAbnormalResponseRate() {
() => getAbnormalResponseRate(hasParams ? params : undefined), () => getAbnormalResponseRate(hasParams ? params : undefined),
requestParams requestParams
) )
const rawData = response.data const rawData = response.data || {} // 添加默认值防止null访问
const processedAlerts = [] const processedAlerts = []
const teamData = new Map() const teamData = new Map()
// 添加安全检查防止访问null属性
if (rawData.team_serious_timeout_abnormal_counts_by_group) { if (rawData.team_serious_timeout_abnormal_counts_by_group) {
Object.entries(rawData.team_serious_timeout_abnormal_counts_by_group).forEach(([teamName, data]) => { Object.entries(rawData.team_serious_timeout_abnormal_counts_by_group).forEach(([teamName, data]) => {
if (!teamData.has(teamName)) { if (!teamData.has(teamName)) {
teamData.set(teamName, { timeoutCount: 0, fillingCount: 0 }) teamData.set(teamName, { timeoutCount: 0, fillingCount: 0 })
} }
teamData.get(teamName).timeoutCount = data.count teamData.get(teamName).timeoutCount = data.count || 0
}) })
} }
@@ -590,7 +758,7 @@ async function fetchAbnormalResponseRate() {
if (!teamData.has(teamName)) { if (!teamData.has(teamName)) {
teamData.set(teamName, { timeoutCount: 0, fillingCount: 0 }) teamData.set(teamName, { timeoutCount: 0, fillingCount: 0 })
} }
teamData.get(teamName).fillingCount = data.count teamData.get(teamName).fillingCount = data.count || 0
}) })
} }
@@ -632,9 +800,12 @@ async function fetchCustomerCommunicationRate() {
() => getActiveCustomerCommunicationRate(hasParams ? params : undefined), () => getActiveCustomerCommunicationRate(hasParams ? params : undefined),
requestParams requestParams
) )
statisticalIndicators.value.customerCommunicationRate = response.data // 确保响应数据不为null
statisticalIndicators.value.customerCommunicationRate = response.data || {}
} catch (error) { } catch (error) {
console.error('获取活跃客户沟通率失败:', error) console.error('获取活跃客户沟通率失败:', error)
// 出错时设置为空对象
statisticalIndicators.value.customerCommunicationRate = {}
} }
} }
// 统计指标--平均应答时间 // 统计指标--平均应答时间
@@ -649,9 +820,12 @@ async function fetchAverageResponseTime() {
() => getAverageAnswerTime(hasParams ? params : undefined), () => getAverageAnswerTime(hasParams ? params : undefined),
requestParams requestParams
) )
statisticalIndicators.value.averageResponseTime = response.data // 确保响应数据不为null
statisticalIndicators.value.averageResponseTime = response.data || {}
} catch (error) { } catch (error) {
console.error('获取平均应答时间失败:', error) console.error('获取平均应答时间失败:', error)
// 出错时设置为空对象
statisticalIndicators.value.averageResponseTime = {}
} }
} }
// 统计指标--超时应答率、严重超时应答率 // 统计指标--超时应答率、严重超时应答率
@@ -666,9 +840,15 @@ async function fetchTimeoutRate() {
() => getTimeoutRate(hasParams ? params : undefined), () => getTimeoutRate(hasParams ? params : undefined),
requestParams requestParams
) )
statisticalIndicators.value.timeoutResponseRate = response.data // 确保响应数据不为null
statisticalIndicators.value.timeoutResponseRate = response.data || {}
// severeTimeoutRate使用相同的数据源
statisticalIndicators.value.severeTimeoutRate = response.data || {}
} catch (error) { } catch (error) {
console.error('获取超时应答率失败:', error) console.error('获取超时应答率失败:', error)
// 出错时设置为空对象
statisticalIndicators.value.timeoutResponseRate = {}
statisticalIndicators.value.severeTimeoutRate = {}
} }
} }
// 统计指标--表格填写率 // 统计指标--表格填写率
@@ -683,9 +863,12 @@ async function fetchTableFillingRate() {
() => getTableFillingRate(hasParams ? params : undefined), () => getTableFillingRate(hasParams ? params : undefined),
requestParams requestParams
) )
statisticalIndicators.value.formCompletionRate = response.data // 确保响应数据不为null
statisticalIndicators.value.formCompletionRate = response.data || {}
} catch (error) { } catch (error) {
console.error('获取表格填写率失败:', error) console.error('获取表格填写率失败:', error)
// 出错时设置为空对象
statisticalIndicators.value.formCompletionRate = {}
} }
} }
const problemRanking = ref({}) const problemRanking = ref({})
@@ -771,6 +954,7 @@ onMounted(async ()=>{
await fetchTableFillingRate() await fetchTableFillingRate()
await fetchUrgentNeedToAddress() await fetchUrgentNeedToAddress()
await fetchTeamRanking() await fetchTeamRanking()
await CenterExcellentRecord()
console.log('缓存状态:', getCacheInfo()) console.log('缓存状态:', getCacheInfo())
@@ -996,10 +1180,6 @@ const hideTooltip = () => {
overflow: auto; overflow: auto;
} }
/* 新增 */
.performance-comparison-section {
margin-top: 1rem;
}
.action-items-compact { .action-items-compact {
overflow: hidden; overflow: hidden;
@@ -1120,12 +1300,19 @@ const hideTooltip = () => {
.team-detail-header { .team-detail-header {
margin-bottom: 2rem; margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 1rem;
h2 { h2 {
font-size: 1.4rem; font-size: 1.4rem;
font-weight: 600; font-weight: 600;
color: #1e293b; color: #1e293b;
margin: 0 0 1rem 0; margin: 0;
flex: 1;
min-width: 300px;
} }
.team-summary { .team-summary {
@@ -1151,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 { .members-grid {
@@ -1546,4 +1759,199 @@ const hideTooltip = () => {
.feedback-btn:hover { .feedback-btn:hover {
background-color: #3182ce; 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> </style>

View File

@@ -18,6 +18,7 @@
:class="{ active: selectedRecording === index }" :class="{ active: selectedRecording === index }"
@click="selectRecording(index)" @click="selectRecording(index)"
> >
<span class="recording-index">{{ recording.score}}</span>
<div class="recording-info"> <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-name" :title="recording.name">{{ recording.name.length > 10 ? recording.name.substring(0, 10) + '...' : recording.name }}</div>
<div class="recording-meta"> <div class="recording-meta">
@@ -172,8 +173,8 @@ import MarkdownIt from 'markdown-it'
// Props定义 // Props定义
const props = defineProps({ const props = defineProps({
qualityCalls: { qualityCalls: {
type: Object, type: Array,
default: () => ({}) default: () => []
} }
}) })
@@ -221,25 +222,23 @@ const recordings = computed(() => {
if (!props.qualityCalls ) { if (!props.qualityCalls ) {
return staticRecordings.value; return staticRecordings.value;
} }
const recordingsList = []; const recordingsList = [];
Object.keys(props.qualityCalls).forEach(userName => { props.qualityCalls.forEach((record, index) => {
props.qualityCalls[userName].forEach((record, index) => {
recordingsList.push({ recordingsList.push({
id: recordingsList.length + 1, 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], date: new Date().toISOString().split('T')[0],
url: record.obj_file_name, url: record.record_file_addr,
transcription: record.context || null, transcription: record.record_context || null,
score: record.score, score: record.record_score,
sop: record.sop, sop: record.record_report,
sale_name: record.sale_name, sale_name: record.record_name,
size: 2048576, // 默认文件大小 2MB size: 2048576, // 默认文件大小 2MB
uploadTime: new Date().toLocaleDateString('zh-CN') uploadTime: record.created_at,
});
}); });
}); });
return recordingsList; return recordingsList;
}) })
@@ -512,7 +511,7 @@ const downloadRecording = (index) => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 20px 0; padding: 10px 20px 0;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
} }
@@ -549,7 +548,7 @@ const downloadRecording = (index) => {
} }
.chart-content { .chart-content {
padding: 20px; padding: 10px;
} }
.recording-section { .recording-section {
@@ -562,7 +561,6 @@ const downloadRecording = (index) => {
.recording-list { .recording-list {
margin-bottom: 20px; margin-bottom: 20px;
max-height: 400px; max-height: 400px;
overflow-y: auto;
} }
.recording-item { .recording-item {
@@ -602,6 +600,39 @@ const downloadRecording = (index) => {
display: inline-block; 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 { .recording-meta {
display: flex; display: flex;
gap: 12px; gap: 12px;

View File

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