feat(销售管理): 优化团队成员详情展示和录音下载功能
- 在团队成员详情组件中添加memberDetails属性,展示更详细的数据统计 - 改进录音下载功能,处理HTTPS页面下载HTTP资源的情况并优化文件名获取 - 新增下载专用弹窗组件,防止与普通弹窗冲突 - 修复销售时间线中"点击未支付"阶段的显示文本 - 增强模态框的滚动控制和样式一致性
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="member-details">
|
||||
<div class="details-header" @click="toggleDetailsCollapse">
|
||||
<h2>{{ selectedMember?.user_name || selectedMember?.name||'张三' }} 的详细数据</h2>
|
||||
<h2>{{ memberDetails?.user_name || selectedMember?.user_name || selectedMember?.name||'张三' }} 的详细数据</h2>
|
||||
<div class="collapse-toggle" :class="{ 'collapsed': isDetailsCollapsed }">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 4l4 4H4l4-4z"/>
|
||||
@@ -15,28 +15,28 @@
|
||||
总通话次数
|
||||
<span class="info-icon" @mouseenter="showTooltip($event, 'totalCalls')" @mouseleave="hideTooltip">ⓘ</span>
|
||||
</div>
|
||||
<div class="detail-value">{{ selectedMember?.calls || 0 }} 次</div>
|
||||
<div class="detail-value">{{ memberDetails?.call_count || 0 }} 次</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-label">
|
||||
通话时长
|
||||
<span class="info-icon" @mouseenter="showTooltip($event, 'callTime')" @mouseleave="hideTooltip">ⓘ</span>
|
||||
</div>
|
||||
<div class="detail-value">{{ selectedMember?.callTime || 0 }} 小时</div>
|
||||
<div class="detail-value">{{ memberDetails?.total_call_duration_hour || 0 }} 小时</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-label">
|
||||
新增客户
|
||||
<span class="info-icon" @mouseenter="showTooltip($event, 'newClients')" @mouseleave="hideTooltip">ⓘ</span>
|
||||
</div>
|
||||
<div class="detail-value">{{ selectedMember?.newClients || 0 }} 人</div>
|
||||
<div class="detail-value">{{ memberDetails?.add_customer_count || 0 }} 人</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-label">
|
||||
成交单数
|
||||
<span class="info-icon" @mouseenter="showTooltip($event, 'deals')" @mouseleave="hideTooltip">ⓘ</span>
|
||||
</div>
|
||||
<div class="detail-value">{{ selectedMember?.deals || 0 }} 单</div>
|
||||
<div class="detail-value">{{ memberDetails?.month_order_count || 0 }} 单</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-label">
|
||||
@@ -59,8 +59,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="guidance-cards" v-show="!isGuidanceCollapsed" :class="{ 'collapsing': isGuidanceCollapsed }">
|
||||
<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-card" v-if="getGuidanceForMember(memberDetails).length > 0">
|
||||
<div class="guidance-item" v-for="(guidance, index) in getGuidanceForMember(memberDetails)" :key="index">
|
||||
<div class="guidance-icon" :class="guidance.type">
|
||||
{{ guidance.icon }}
|
||||
</div>
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="no-guidance" v-else>
|
||||
<div class="celebration-icon">🎉</div>
|
||||
<h4>表现优秀!</h4>
|
||||
<p>{{ selectedMember?.user_name || selectedMember?.name }} 的各项指标都很不错,继续保持这种状态!</p>
|
||||
<p>{{ memberDetails?.user_name || selectedMember?.user_name || selectedMember?.name }} 的各项指标都很不错,继续保持这种状态!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,6 +140,10 @@ const props = defineProps({
|
||||
selectedMember: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
memberDetails: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -231,7 +235,7 @@ const getRecordingsForMember = (member) => {
|
||||
const recordings = []
|
||||
|
||||
// 根据成员ID返回对应的录音,这里简化处理
|
||||
return recordings.slice(0, Math.min(3, member?.calls || 0))
|
||||
return recordings.slice(0, Math.min(3, member?.call_count || 0))
|
||||
}
|
||||
|
||||
// 下载录音文件
|
||||
@@ -257,7 +261,7 @@ const getGuidanceForMember = (member) => {
|
||||
}
|
||||
|
||||
// 业绩相关建议
|
||||
if (member.performance === 0) {
|
||||
if (member.month_order_count === 0) {
|
||||
guidance.push({
|
||||
type: 'urgent',
|
||||
icon: '🚨',
|
||||
@@ -265,7 +269,7 @@ const getGuidanceForMember = (member) => {
|
||||
description: '当前还未有成交记录,需要重点关注转化技巧和客户跟进。',
|
||||
action: '建议参加销售技巧培训,加强客户需求挖掘'
|
||||
})
|
||||
} else if (member.performance < 80000) {
|
||||
} else if (member.month_order_count < 5) {
|
||||
guidance.push({
|
||||
type: 'warning',
|
||||
icon: '📈',
|
||||
@@ -295,7 +299,7 @@ const getGuidanceForMember = (member) => {
|
||||
}
|
||||
|
||||
// 通话相关建议
|
||||
if (member.calls < 100) {
|
||||
if (member.call_count < 100) {
|
||||
guidance.push({
|
||||
type: 'warning',
|
||||
icon: '📞',
|
||||
@@ -306,7 +310,7 @@ const getGuidanceForMember = (member) => {
|
||||
}
|
||||
|
||||
// 客户开发建议
|
||||
if (member.newClients < 5) {
|
||||
if (member.add_customer_count < 5) {
|
||||
guidance.push({
|
||||
type: 'info',
|
||||
icon: '👥',
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<!-- Right Section -->
|
||||
<div class="right-section">
|
||||
<!-- Member Details -->
|
||||
<MemberDetails :selected-member="selectedMember" />
|
||||
<MemberDetails :selected-member="selectedMember" :memberDetails="memberDetails" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -81,7 +81,7 @@ import CustomerDetail from "../person/components/CustomerDetail.vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useRouter } from "vue-router";
|
||||
import {getGroupAbnormalResponse, getWeekTotalCall, getWeekAddCustomerTotal, getWeekAddDealTotal,
|
||||
getWeekAddFeeTotal, getGroupFunnel,getPayDepositToMoneyRate,getGroupRanking, getGroupCallDuration} from "@/api/manager.js";
|
||||
getWeekAddFeeTotal, getGroupFunnel,getPayDepositToMoneyRate,getGroupRanking, getGroupCallDuration,getGroupDetail} from "@/api/manager.js";
|
||||
|
||||
// 团队成员数据
|
||||
const teamMembers = [
|
||||
@@ -292,8 +292,31 @@ const selectedMember = ref(null);
|
||||
// 选择成员函数
|
||||
const selectMember = (member) => {
|
||||
selectedMember.value = member;
|
||||
console.log(122331,member)
|
||||
TeamGetGroupDetail(member.user_name)
|
||||
};
|
||||
|
||||
// 成员详细数据
|
||||
async function TeamGetGroupDetail(member) {
|
||||
const res = await getGroupDetail({user_name:member})
|
||||
console.log(res)
|
||||
if (res.code === 200) {
|
||||
memberDetails.value = res.data
|
||||
/**
|
||||
* add_customer_count:32
|
||||
call_count:96
|
||||
month_order_count:5
|
||||
total_call_duration_hour
|
||||
:
|
||||
1.92
|
||||
user_name
|
||||
:
|
||||
"李晓雪"
|
||||
week_order_count
|
||||
:
|
||||
2
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 团队异常预警
|
||||
|
||||
@@ -258,55 +258,94 @@ const downloadRecording = async (call) => {
|
||||
|
||||
// 检查是否有录音文件地址
|
||||
if (call.record_file_addr) {
|
||||
const recordingUrl = call.record_file_addr
|
||||
|
||||
try {
|
||||
// 显示下载开始提示
|
||||
emit('show-modal', '下载提示', '正在下载录音文件,请稍候...')
|
||||
emit('show-download-modal', '下载提示', '正在下载录音文件,请稍候...')
|
||||
|
||||
const recordingUrl = call.record_file_addr
|
||||
// 若为 HTTPS 页面请求 HTTP 资源,浏览器会拦截,回退为在新标签页打开
|
||||
if (window.location.protocol === 'https:' && recordingUrl.startsWith('http://')) {
|
||||
const parts = recordingUrl.split('/')
|
||||
const fallbackName = parts[parts.length - 1] || 'recording'
|
||||
const link = document.createElement('a')
|
||||
link.href = recordingUrl
|
||||
link.target = '_blank'
|
||||
link.rel = 'noopener'
|
||||
link.download = fallbackName
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
emit('show-download-modal', '提示', '目标为不安全的HTTP资源,已在新标签页打开下载链接。')
|
||||
return
|
||||
}
|
||||
|
||||
// 从URL中提取文件名
|
||||
const urlParts = recordingUrl.split('/')
|
||||
const fileName = urlParts[urlParts.length - 1]
|
||||
|
||||
// 使用fetch获取文件
|
||||
// 通过请求的方式下载
|
||||
const response = await fetch(recordingUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
redirect: 'follow',
|
||||
referrerPolicy: 'no-referrer'
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
throw new Error(`下载失败,状态码: ${response.status}`)
|
||||
}
|
||||
|
||||
// 从响应头或URL中提取文件名
|
||||
let fileName = 'recording'
|
||||
const disposition = response.headers.get('content-disposition')
|
||||
if (disposition) {
|
||||
const match = disposition.match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i)
|
||||
if (match) fileName = decodeURIComponent(match[1] || match[2])
|
||||
}
|
||||
if (!fileName || fileName === 'recording') {
|
||||
try {
|
||||
const urlObj = new URL(recordingUrl, window.location.href)
|
||||
const segments = urlObj.pathname.split('/')
|
||||
fileName = segments[segments.length - 1] || 'recording'
|
||||
} catch (e) {
|
||||
const parts = recordingUrl.split('/')
|
||||
fileName = parts[parts.length - 1] || 'recording'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件blob
|
||||
const blob = await response.blob()
|
||||
const objectUrl = URL.createObjectURL(blob)
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = fileName
|
||||
link.style.display = 'none'
|
||||
|
||||
// 触发下载
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
// 清理
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
const a = document.createElement('a')
|
||||
a.href = objectUrl
|
||||
a.download = fileName
|
||||
a.style.display = 'none'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000)
|
||||
|
||||
// 下载成功提示
|
||||
emit('show-modal', '下载成功', '录音文件下载完成!')
|
||||
|
||||
emit('show-download-modal', '下载成功', '录音文件下载完成!')
|
||||
} catch (error) {
|
||||
console.error('下载录音文件失败:', error)
|
||||
emit('show-modal', '下载失败', '下载录音文件失败,请检查网络连接或文件是否存在')
|
||||
|
||||
// 回退:尝试在新标签页直接打开原始链接(适用于CORS或其他限制)
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = recordingUrl
|
||||
link.target = '_blank'
|
||||
link.rel = 'noopener'
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
emit('show-download-modal', '提示', '无法直接下载,已在新标签页打开录音链接。')
|
||||
} catch (e) {
|
||||
emit('show-download-modal', '下载失败', '下载录音文件失败,请检查网络连接或文件是否存在')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emit('show-modal', '提示', '该通话记录暂无录音文件')
|
||||
emit('show-download-modal', '提示', '该通话记录暂无录音文件')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '点击未支付') > 0 }" @click="selectCourseStage(2, '点击未支付')">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">点击支付</span>
|
||||
<span class="mini-title">点击未付</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(2, '点击未支付') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
@view-chat-data="handleViewChatData"
|
||||
@view-call-data="handleViewCallData"
|
||||
@analyze-sop="handleAnalyzeSop"
|
||||
@show-modal="handleShowModal" />
|
||||
@show-modal="handleShowModal"
|
||||
@show-download-modal="handleShowDownloadModal" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -129,8 +130,8 @@
|
||||
</section>
|
||||
|
||||
<!-- 自定义弹框 -->
|
||||
<div v-if="showModal" class="modal-overlay" @click="closeModal">
|
||||
<div class="modal-container" @click.stop>
|
||||
<div v-if="showModal" class="modal-overlay" @click="closeModal" @wheel.prevent @touchmove.prevent>
|
||||
<div class="modal-container" @click.stop @wheel.stop @touchmove.stop>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{ modalTitle }}</h3>
|
||||
<button class="modal-close-btn" @click="closeModal">
|
||||
@@ -147,6 +148,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下载弹框 -->
|
||||
<div v-if="showDownloadModal" class="modal-overlay" @click="closeDownloadModal" @wheel.prevent @touchmove.prevent>
|
||||
<div class="modal-container" @click.stop @wheel.stop @touchmove.stop>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{ downloadModalTitle }}</h3>
|
||||
<button class="modal-close-btn" @click="closeDownloadModal">
|
||||
<i class="icon-close">×</i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-content">
|
||||
{{ downloadModalContent }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-btn modal-btn-primary" @click="closeDownloadModal">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -273,6 +294,11 @@ const showModal = ref(false)
|
||||
const modalContent = ref('')
|
||||
const modalTitle = ref('')
|
||||
|
||||
// 下载弹框状态
|
||||
const showDownloadModal = ref(false)
|
||||
const downloadModalContent = ref('')
|
||||
const downloadModalTitle = ref('')
|
||||
|
||||
// 时间线数据
|
||||
const timelineData = ref({});
|
||||
|
||||
@@ -838,8 +864,9 @@ const handleViewCallData = (contact) => {
|
||||
|
||||
// 处理弹框显示事件
|
||||
const handleShowModal = (title, content) => {
|
||||
modalTitle.value = title
|
||||
modalContent.value = content
|
||||
console.log('handleShowModal0000', title)
|
||||
modalTitle.value = title.title
|
||||
modalContent.value = title.content
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
@@ -850,6 +877,20 @@ const closeModal = () => {
|
||||
modalTitle.value = ''
|
||||
}
|
||||
|
||||
// 处理下载弹框显示
|
||||
const handleShowDownloadModal = (title, content) => {
|
||||
downloadModalTitle.value = title
|
||||
downloadModalContent.value = content
|
||||
showDownloadModal.value = true
|
||||
}
|
||||
|
||||
// 关闭下载弹框
|
||||
const closeDownloadModal = () => {
|
||||
showDownloadModal.value = false
|
||||
downloadModalContent.value = ''
|
||||
downloadModalTitle.value = ''
|
||||
}
|
||||
|
||||
// // 处理SOP分析事件
|
||||
// const handleAnalyzeSop = (analyzeData) => {
|
||||
// console.log('handleAnalyzeSop', analyzeData)
|
||||
@@ -1767,6 +1808,7 @@ $primary: #3b82f6;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
// 使用 Flexbox 实现垂直和水平居中
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1781,8 +1823,13 @@ $primary: #3b82f6;
|
||||
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;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
// 设置最大高度,防止弹窗超出屏幕
|
||||
max-height: 35vh;
|
||||
// 防止内容溢出容器,配合内部滚动
|
||||
overflow: hidden;
|
||||
// 使用 Flexbox 布局,让 .modal-body 可以伸缩
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@@ -1792,7 +1839,6 @@ $primary: #3b82f6;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@@ -1827,21 +1873,25 @@ $primary: #3b82f6;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 5px;
|
||||
max-height: 60vh;
|
||||
// 关键:让内容区域占据所有剩余空间
|
||||
flex: 1;
|
||||
// 关键:当内容超出时,只在垂直方向显示滚动条
|
||||
overflow-y: auto;
|
||||
// 防止滚动链传递到页面,仅在弹框内滚动
|
||||
overscroll-behavior: contain;
|
||||
// 为内容提供统一内边距
|
||||
padding: 24px;
|
||||
// 配合 flex: 1 使用,防止 flex item 在某些浏览器中无法正确收缩
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
// 支持长文本和换行
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
padding: 16px;
|
||||
margin-left: 30px;
|
||||
border-radius: 8px;
|
||||
// border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
@@ -1850,7 +1900,7 @@ $primary: #3b82f6;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
// flex-shrink: 0; // 确保 footer 不会被压缩
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
@@ -1911,17 +1961,14 @@ $primary: #3b82f6;
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
max-height: 55vh;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
font-size: 13px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user