feat(销售管理): 优化团队成员详情展示和录音下载功能

- 在团队成员详情组件中添加memberDetails属性,展示更详细的数据统计
- 改进录音下载功能,处理HTTPS页面下载HTTP资源的情况并优化文件名获取
- 新增下载专用弹窗组件,防止与普通弹窗冲突
- 修复销售时间线中"点击未支付"阶段的显示文本
- 增强模态框的滚动控制和样式一致性
This commit is contained in:
2025-09-17 10:56:11 +08:00
parent 3033326def
commit 4885674f23
5 changed files with 185 additions and 72 deletions

View File

@@ -255,58 +255,97 @@ const callRecords = computed(() => {
// 录音下载方法
const downloadRecording = async (call) => {
console.log('下载录音:', call)
// 检查是否有录音文件地址
if (call.record_file_addr) {
const recordingUrl = call.record_file_addr
try {
// 显示下载开始提示
emit('show-modal', '下载提示', '正在下载录音文件,请稍候...')
const recordingUrl = call.record_file_addr
// 从URL中提取文件名
const urlParts = recordingUrl.split('/')
const fileName = urlParts[urlParts.length - 1]
// 使用fetch获取文件
emit('show-download-modal', '下载提示', '正在下载录音文件,请稍候...')
// 若为 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
}
// 通过请求的方式下载
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}`)
}
// 获取文件blob
// 从响应头或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'
}
}
const blob = await response.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 objectUrl = URL.createObjectURL(blob)
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', '提示', '该通话记录暂无录音文件')
}
}

View File

@@ -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>

View File

@@ -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>
</style>