refactor(QualityCalls): 重构组件为Composition API并简化UI

fix(https): 将API基础路径从http改为https
fix(topone): 修正优秀录音数据处理逻辑
fix(CenterOverview): 修改默认显示值为0
This commit is contained in:
2025-08-26 22:10:43 +08:00
parent abadcf2494
commit d0b8159274
4 changed files with 305 additions and 321 deletions

View File

@@ -5,7 +5,7 @@ import { useUserStore } from '@/stores/user'
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
baseURL: 'http://mldash.nycjy.cn/' || '', // API基础路径支持完整URL baseURL: 'https://mldash.nycjy.cn/' || '', // API基础路径支持完整URL
timeout: 100000, // 请求超时时间 timeout: 100000, // 请求超时时间
headers: { headers: {
'Content-Type': 'application/json;charset=UTF-8' 'Content-Type': 'application/json;charset=UTF-8'

View File

@@ -67,8 +67,8 @@
</span> </span>
<span class="card-trend positive">{{ props.overallData.TotalCallCount?.total_call_count_vs_yesterday}} vs 上期</span> <span class="card-trend positive">{{ props.overallData.TotalCallCount?.total_call_count_vs_yesterday}} vs 上期</span>
</div> </div>
<div class="card-value">{{ props.overallData.TotalCallCount?.total_call_count || '1,247' }} </div> <div class="card-value">{{ props.overallData.TotalCallCount?.total_call_count || '0' }} </div>
<div class="card-subtitle">有效通话: {{ props.overallData.TotalCallCount?.center_effective_call_count || '892' }}</div> <div class="card-subtitle">有效通话: {{ props.overallData.TotalCallCount?.center_effective_call_count || '0' }}</div>
</div> </div>
<div class="overview-card"> <div class="overview-card">

View File

@@ -4,14 +4,6 @@
<div class="chart-container"> <div class="chart-container">
<div class="chart-header"> <div class="chart-header">
<h3>优秀录音</h3> <h3>优秀录音</h3>
<button class="upload-icon-btn" @click="triggerFileUpload" title="上传录音">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.89 22 5.99 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="18" x2="12" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="9,15 12,12 15,15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div> </div>
<div class="chart-content"> <div class="chart-content">
<div class="recording-section"> <div class="recording-section">
@@ -29,7 +21,6 @@
<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">
<span class="file-size">{{ formatFileSize(recording.size) }}</span>
<span class="upload-time">{{ recording.uploadTime }}</span> <span class="upload-time">{{ recording.uploadTime }}</span>
</div> </div>
</div> </div>
@@ -67,9 +58,8 @@
<div class="result-header"> <div class="result-header">
<button class="back-btn" @click="backToRecordings"> <button class="back-btn" @click="backToRecordings">
<i class="el-icon-arrow-left"></i> <i class="el-icon-arrow-left"></i>
返回录音列表 返回
</button> </button>
<h4>{{ isConverting ? '正在转换...' : (currentViewType === 'transcript' ? '转换文本' : '录音分析') }}</h4>
<div class="header-actions"> <div class="header-actions">
<!-- 视图切换按钮 --> <!-- 视图切换按钮 -->
<div class="view-toggle" v-if="currentTranscript && !isConverting"> <div class="view-toggle" v-if="currentTranscript && !isConverting">
@@ -90,7 +80,7 @@
</div> </div>
<button class="expand-btn" @click="showExpandDialog" v-if="(currentTranscript && currentViewType === 'transcript') || (analysisResult && currentViewType === 'analysis')"> <button class="expand-btn" @click="showExpandDialog" v-if="(currentTranscript && currentViewType === 'transcript') || (analysisResult && currentViewType === 'analysis')">
<i class="el-icon-full-screen"></i> <i class="el-icon-full-screen"></i>
展开查看 展开
</button> </button>
<button class="copy-btn" @click="copyText" v-if="currentTranscript && currentViewType === 'transcript'"> <button class="copy-btn" @click="copyText" v-if="currentTranscript && currentViewType === 'transcript'">
<i class="el-icon-document-copy"></i> <i class="el-icon-document-copy"></i>
@@ -174,309 +164,315 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
import { SimpleChatService } from '@/utils/ChatService.js' import { SimpleChatService } from '@/utils/ChatService.js'
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
export default { // Props定义
name: 'QualityCalls', const props = defineProps({
props: { qualityCalls: {
qualityCalls: { type: Object,
type: Object, default: () => ({})
default: () => ({}) }
} })
},
data() {
return {
staticRecordings: [
{
id: 1,
name: '常家硕-张三丰-亮剑二部-20分钟通话-25-07-16_18-23-04-44196-215.mp3',
size: 2048576, // 2MB
duration: '00:03:45',
date: '2024-01-15',
url: '/recordings/sample_call_1.mp3',
transcription: null
},
{
id: 2,
name: '常家硕-张三丰-亮剑二部-20分钟通话-25-07-16_18-23-01-439240-599.mp3',
size: 3145728, // 3MB
duration: '00:05:20',
date: '2024-01-14',
url: '/recordings/sample_call_2.mp3',
transcription: null
},
{
id: 3,
name: '常家硕-张三丰-亮剑二部-20分钟通话-25-07-16_18-23-02-754615-508.mp3',
size: 2048576, // 2MB
duration: '00:03:45',
date: '2024-01-15',
url: '/recordings/sample_call_1.mp3',
transcription: null
},
{
id: 4,
name: '丁传辉-丁传辉-勇士二部-20分钟通话-25-07-10_10-32-54-813815-322.mp3',
size: 3145728, // 3MB
duration: '00:05:20',
date: '2024-01-14',
url: '/recordings/sample_call_2.mp3',
transcription: null
}
],
selectedRecording: null,
currentAudio: null,
showTranscriptView: false,
isConverting: false,
currentTranscript: null,
showDialog: false,
// 录音分析相关
showAnalysisView: false,
isAnalyzing: false,
analysisResult: '',
currentViewType: 'transcript', // 'transcript' 或 'analysis'
// Dify API配置
DIFY_API_KEY_02: 'app-h4uBo5kOGoiYhjuBF1AHZi8b', // 通话录音分析
chatService_02: null,
md: null
}
},
created() {
// 初始化服务
this.chatService_02 = new SimpleChatService(this.DIFY_API_KEY_02)
this.md = new MarkdownIt({
html: true,
linkify: true,
typographer: true
})
},
computed: {
// 处理传入的录音数据
recordings() {
if (!this.qualityCalls || !this.qualityCalls.excellent_record_list) {
return this.staticRecordings;
}
const recordings = [];
Object.keys(this.qualityCalls.excellent_record_list).forEach(userName => {
this.qualityCalls.excellent_record_list[userName].forEach((record, index) => {
recordings.push({
id: recordings.length + 1,
name: record.obj_file_name ? record.obj_file_name.split('/').pop() : `${record.sale_name}-录音-${index + 1}`,
size: 2048576, // 默认大小
duration: '00:03:45', // 默认时长
date: new Date().toISOString().split('T')[0],
url: record.obj_file_name,
transcription: record.context || null,
score: record.score,
sop: record.sop,
sale_name: record.sale_name
});
});
});
return recordings;
},
// 格式化分析结果
formattedAnalysisResult() {
if (!this.analysisResult) return ''
return this.md.render(this.analysisResult)
}
},
beforeUnmount() {
if (this.currentAudio) {
this.currentAudio.pause()
this.currentAudio = null
}
},
methods: {
// 录音文件选择
handleFileSelect(event) {
const file = event.target.files[0]
if (file) {
const recording = {
name: file.name,
size: file.size,
uploadTime: new Date().toLocaleString(),
url: URL.createObjectURL(file),
isPlaying: false,
isConverting: false,
transcript: null
}
this.recordings.push(recording)
// 清空input以便重复选择同一文件
event.target.value = ''
}
},
// 选择录音
selectRecording(index) {
this.selectedRecording = index
},
// 播放/暂停录音
togglePlay(index) {
const recording = this.recordings[index]
// 停止当前播放的音频
if (this.currentAudio) {
this.currentAudio.pause()
this.recordings.forEach(r => r.isPlaying = false)
}
if (!recording.isPlaying) {
this.currentAudio = new Audio(recording.url)
this.currentAudio.play()
recording.isPlaying = true
this.currentAudio.onended = () => {
recording.isPlaying = false
this.currentAudio = null
}
}
},
// 转换为文本
async convertToText(index) {
const recording = this.recordings[index]
this.selectedRecording = index
this.showTranscriptView = true
this.isConverting = true
this.currentTranscript = null
this.currentViewType = 'transcript'
try {
// 模拟转换过程
await new Promise(resolve => setTimeout(resolve, 2000))
// 这里应该调用实际的语音转文本API
// 目前使用模拟数据
recording.transcript = `这是 ${recording.name} 的转换文本示例。在实际应用中,这里会显示真实的语音转文本结果。您可以集成百度、阿里云、腾讯云等语音识别服务来实现真正的语音转文本功能。`
this.currentTranscript = recording.transcript
// 转换完成后自动开始录音分析
this.startRecordingAnalysis(recording)
// 添加转换完成的动画效果
const resultElement = document.querySelector('.conversion-result')
if (resultElement) {
resultElement.classList.add('show-result')
setTimeout(() => {
resultElement.classList.remove('show-result')
}, 1000)
}
} catch (error) {
console.error('转换失败:', error)
alert('转换失败,请重试')
this.showTranscriptView = false
} finally {
this.isConverting = false
}
},
// 开始通话录音分析
async startRecordingAnalysis(recording) {
this.isAnalyzing = true
this.analysisResult = ''
// 构建通话录音分析查询
const recordingQuery = `请对录音文件 ${recording.name} 进行通话录音分析,包括:
1. 通话质量评估
2. 客户情绪分析
3. 沟通效果评价
4. 关键信息提取
5. 改进建议
录音信息: // 响应式数据
文件名:${recording.name} const staticRecordings = ref([
文件大小:${this.formatFileSize(recording.size)} {
转换文本:${recording.transcript}` id: 1,
name: '常家硕-张三丰-亮剑二部-20分钟通话-25-07-16_18-23-04-44196-215.mp3',
try { size: 2048576, // 2MB
await this.chatService_02.sendMessage( duration: '00:03:45',
recordingQuery, date: '2024-01-15',
(update) => { url: '/recordings/sample_call_1.mp3',
// 实时更新通话录音分析结果 transcription: null
this.analysisResult = update.content }
}, ])
() => {
// 流结束回调 const selectedRecording = ref(null)
console.log('通话录音分析完成') const currentAudio = ref(null)
this.isAnalyzing = false const showTranscriptView = ref(false)
} const isConverting = ref(false)
) const currentTranscript = ref(null)
} catch (error) { const showDialog = ref(false)
console.error('通话录音分析失败:', error) // 录音分析相关
this.analysisResult = '通话录音分析失败,请重试。' const showAnalysisView = ref(false)
this.isAnalyzing = false const isAnalyzing = ref(false)
} const analysisResult = ref('')
}, const currentViewType = ref('transcript') // 'transcript' 或 'analysis'
// Dify API配置
const DIFY_API_KEY_02 = 'app-h4uBo5kOGoiYhjuBF1AHZi8b' // 通话录音分析
const chatService_02 = ref(null)
const md = ref(null)
// 初始化服务
onMounted(() => {
chatService_02.value = new SimpleChatService(DIFY_API_KEY_02)
md.value = new MarkdownIt({
html: true,
linkify: true,
typographer: true
})
})
// 计算属性
// 处理传入的录音数据
const recordings = computed(() => {
if (!props.qualityCalls ) {
return staticRecordings.value;
}
const recordingsList = [];
Object.keys(props.qualityCalls).forEach(userName => {
props.qualityCalls[userName].forEach((record, index) => {
recordingsList.push({
id: recordingsList.length + 1,
name: record.obj_file_name ? record.obj_file_name.split('/').pop() : `${record.sale_name}-录音-${index + 1}`,
date: new Date().toISOString().split('T')[0],
url: record.obj_file_name,
transcription: record.context || null,
score: record.score,
sop: record.sop,
sale_name: record.sale_name,
size: 2048576, // 默认文件大小 2MB
uploadTime: new Date().toLocaleDateString('zh-CN')
});
});
});
return recordingsList;
})
// 格式化分析结果
const formattedAnalysisResult = computed(() => {
if (!analysisResult.value) return ''
return md.value.render(analysisResult.value)
})
// 生命周期钩子
onBeforeUnmount(() => {
if (currentAudio.value) {
currentAudio.value.pause()
currentAudio.value = null
}
})
// 方法定义
// 录音文件选择
const handleFileSelect = (event) => {
const file = event.target.files[0]
if (file) {
const recording = {
name: file.name,
size: file.size,
uploadTime: new Date().toLocaleString(),
url: URL.createObjectURL(file),
isPlaying: false,
isConverting: false,
transcript: null
}
staticRecordings.value.push(recording)
// 清空input以便重复选择同一文件
event.target.value = ''
}
}
// 选择录音
const selectRecording = (index) => {
selectedRecording.value = index
}
// 播放/暂停录音
const togglePlay = (index) => {
const recording = recordings.value[index]
// 停止当前播放的音频
if (currentAudio.value) {
currentAudio.value.pause()
recordings.value.forEach(r => r.isPlaying = false)
}
if (!recording.isPlaying) {
currentAudio.value = new Audio(recording.url)
currentAudio.value.play()
recording.isPlaying = true
// 切换视图类型 currentAudio.value.onended = () => {
switchViewType(type) { recording.isPlaying = false
this.currentViewType = type currentAudio.value = null
}, }
}
}
// 转换为文字
const convertToText = async (index) => {
const recording = recordings.value[index]
selectedRecording.value = index
showTranscriptView.value = true
isConverting.value = true
currentTranscript.value = null
currentViewType.value = 'transcript'
try {
// 模拟转换过程
await new Promise(resolve => setTimeout(resolve, 1000))
// 返回录音列表 // 使用从API获取的transcription数据
backToRecordings() { if (recording.transcription) {
this.showTranscriptView = false recording.transcript = recording.transcription
this.currentTranscript = null currentTranscript.value = recording.transcription
this.analysisResult = '' } else {
this.currentViewType = 'transcript' // 如果没有transcription数据显示提示信息
this.isAnalyzing = false recording.transcript = '暂无转换文本数据'
}, currentTranscript.value = '暂无转换文本数据'
// 复制文本 }
copyText() {
if (this.currentTranscript) { // 添加转换完成的动画效果
navigator.clipboard.writeText(this.currentTranscript) const resultElement = document.querySelector('.conversion-result')
if (resultElement) {
resultElement.classList.add('show-result')
setTimeout(() => {
resultElement.classList.remove('show-result')
}, 1000)
}
} catch (error) {
console.error('转换失败:', error)
alert('转换失败,请重试')
showTranscriptView.value = false
} finally {
isConverting.value = false
}
}
// 开始通话录音分析
const startRecordingAnalysis = async (recording) => {
isAnalyzing.value = true
try {
// 使用从API获取的sop数据作为录音分析结果
if (recording.sop) {
analysisResult.value = recording.sop
} else {
analysisResult.value = '暂无录音分析数据'
}
// 模拟分析过程
await new Promise(resolve => setTimeout(resolve, 500))
console.log('录音分析完成')
} catch (error) {
console.error('录音分析失败:', error)
analysisResult.value = '录音分析失败,请重试。'
} finally {
isAnalyzing.value = false
}
}
// 切换视图类型
const switchViewType = (type) => {
currentViewType.value = type
// 如果切换到录音分析视图,且还没有分析结果,则开始分析
if (type === 'analysis' && !analysisResult.value && selectedRecording.value !== null) {
const recording = recordings.value[selectedRecording.value]
startRecordingAnalysis(recording)
}
}
// 返回录音列表
const backToRecordings = () => {
showTranscriptView.value = false
currentTranscript.value = null
analysisResult.value = ''
currentViewType.value = 'transcript'
isAnalyzing.value = false
}
// 复制文本
const copyText = async () => {
if (currentTranscript.value) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(currentTranscript.value)
alert('文本已复制到剪贴板')
} else {
// 降级方案:使用传统的复制方法
const textArea = document.createElement('textarea')
textArea.value = currentTranscript.value
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
alert('文本已复制到剪贴板') alert('文本已复制到剪贴板')
} }
}, } catch (error) {
// 复制分析结果 console.error('复制失败:', error)
copyAnalysisText() { alert('复制失败,请手动复制')
if (this.analysisResult) { }
navigator.clipboard.writeText(this.analysisResult) }
}
// 复制分析结果
const copyAnalysisText = async () => {
if (analysisResult.value) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(analysisResult.value)
alert('分析结果已复制到剪贴板')
} else {
// 降级方案:使用传统的复制方法
const textArea = document.createElement('textarea')
textArea.value = analysisResult.value
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
alert('分析结果已复制到剪贴板') alert('分析结果已复制到剪贴板')
} }
}, } catch (error) {
// 显示展开弹框 console.error('复制失败:', error)
showExpandDialog() { alert('复制失败,请手动复制')
this.showDialog = true
},
// 关闭弹框
closeDialog() {
this.showDialog = false
},
// 格式化文件大小
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
},
triggerFileUpload() {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = 'audio/*'
fileInput.style.display = 'none'
fileInput.addEventListener('change', this.handleFileSelect)
document.body.appendChild(fileInput)
fileInput.click()
document.body.removeChild(fileInput)
},
downloadRecording(index) {
const recording = this.recordings[index]
if (recording && recording.url) {
const link = document.createElement('a')
link.href = recording.url
link.download = recording.name
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
} }
} }
} }
// 显示展开弹框
const showExpandDialog = () => {
showDialog.value = true
}
// 关闭弹框
const closeDialog = () => {
showDialog.value = false
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const triggerFileUpload = () => {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = 'audio/*'
fileInput.style.display = 'none'
fileInput.addEventListener('change', handleFileSelect)
document.body.appendChild(fileInput)
fileInput.click()
document.body.removeChild(fileInput)
}
const downloadRecording = (index) => {
const recording = recordings.value[index]
if (recording && recording.url) {
const link = document.createElement('a')
link.href = recording.url
link.download = recording.name
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
}
</script> </script>
<style scoped> <style scoped>
@@ -558,8 +554,8 @@ export default {
.recording-section { .recording-section {
width: 100%; width: 100%;
min-height: 300px; min-height: 200px;
max-height: 500px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
} }

View File

@@ -495,20 +495,8 @@ const params={
} }
try { try {
const res = await getExcellentRecordFile(params) const res = await getExcellentRecordFile(params)
excellentRecord.value = res.data excellentRecord.value = res.data.excellent_record_list
/** console.log(111111,res.data.excellent_record_list)
* "user_name": "赵世敬",
"user_level": 5,
"excellent_record_list": {
"马然": [
{
"sale_name": "马然",
"sop": ...,
"context": "...",
"obj_file_name": "http://192.168.3.112:5000/api/record/download/马然-20分钟通话-25-08-20_20-24-43-653520-759.mp3",
"score": 55.0
},]}
*/
} catch (error) { } catch (error) {
console.error("获取优秀录音失败:", error); console.error("获取优秀录音失败:", error);
} }