@@ -4,14 +4,6 @@
< div class = "chart-container" >
< div class = "chart-header" >
< 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 class = "chart-content" >
< div class = "recording-section" >
@@ -29,7 +21,6 @@
< div class = "recording-info" >
< div class = "recording-name" :title = "recording.name" > { { recording . name . length > 10 ? recording . name . substring ( 0 , 10 ) + '...' : recording . name } } < / div >
< div class = "recording-meta" >
< span class = "file-size" > { { formatFileSize ( recording . size ) } } < / span >
< span class = "upload-time" > { { recording . uploadTime } } < / span >
< / div >
< / div >
@@ -67,9 +58,8 @@
< div class = "result-header" >
< button class = "back-btn" @click ="backToRecordings" >
< i class = "el-icon-arrow-left" > < / i >
返回录音列表
返回
< / button >
< h4 > { { isConverting ? '正在转换...' : ( currentViewType === 'transcript' ? '转换文本' : '录音分析' ) } } < / h4 >
< div class = "header-actions" >
<!-- 视图切换按钮 -- >
< div class = "view-toggle" v-if = "currentTranscript && !isConverting" >
@@ -90,7 +80,7 @@
< / div >
< button class = "expand-btn" @click ="showExpandDialog" v-if = "(currentTranscript && currentViewType === 'transcript') || (analysisResult && currentViewType === 'analysis')" >
< i class = "el-icon-full-screen" > < / i >
展开查看
展开
< / button >
< button class = "copy-btn" @click ="copyText" v-if = "currentTranscript && currentViewType === 'transcript'" >
< i class = "el-icon-document-copy" > < / i >
@@ -174,21 +164,21 @@
< / div >
< / template >
< script >
< script setup >
import { ref , reactive , computed , onMounted , onBeforeUnmount } from 'vue'
import { SimpleChatService } from '@/utils/ChatService.js'
import MarkdownIt from 'markdown-it'
export default {
name : 'QualityCalls' ,
props : {
// Props定义
const props = defineProps ( {
qualityCalls : {
type : Object ,
default : ( ) => ( { } )
}
} ,
data ( ) {
return {
staticRecordings : [
} )
// 响应式数据
const staticRecordings = ref ( [
{
id : 1 ,
name : '常家硕-张三丰-亮剑二部-20分钟通话-25-07-16_18-23-04-44196-215.mp3' ,
@@ -197,103 +187,77 @@ export default {
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 ( {
] )
const selectedRecording = ref ( null )
const currentAudio = ref ( null )
const showTranscriptView = ref ( false )
const isConverting = ref ( false )
const currentTranscript = ref ( null )
const showDialog = ref ( false )
// 录音分析相关
const showAnalysisView = ref ( 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
} )
} ,
computed : {
// 处理传入的录音数据
recordings ( ) {
if ( ! this . qualityCalls || ! this . qualityCalls . excellent _record _list ) {
return this . staticRecordings ;
} )
// 计算属性
// 处理传入的录音数据
const recordings = computed ( ( ) => {
if ( ! props . qualityCalls ) {
return staticRecordings . value ;
}
const recordings = [ ] ;
Object . keys ( thi s. qualityCalls . excellent _record _list ). forEach ( userName => {
thi s . qualityCalls . excellent _record _list [userName ] . forEach ( ( record , index ) => {
recordings . push ( {
id : recordings . length + 1 ,
const recordingsList = [ ] ;
Object . keys ( prop s. qualityCalls ) . forEach ( userName => {
prop s. 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 } ` ,
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
sale _name : record . sale _name ,
size : 2048576 , // 默认文件大小 2MB
uploadTime : new Date ( ) . toLocaleDateString ( 'zh-CN' )
} ) ;
} ) ;
} ) ;
return recordings ;
} ,
// 格式化分析结果
formattedAnalysisResult ( ) {
if ( ! this . analysisResult ) return ''
return this . md . render ( this . analysisResult )
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
}
} ,
beforeUnmount ( ) {
if ( this . currentAudio ) {
this . currentAudio . pause ( )
this . currentAudio = null
}
} ,
methods : {
// 录音文件选择
handleFileSelect ( event ) {
} )
// 方法定义
// 录音文件选择
const handleFileSelect = ( event ) => {
const file = event . target . files [ 0 ]
if ( file ) {
const recording = {
@@ -305,56 +269,60 @@ export default {
isConverting : false ,
transcript : null
}
this . recordings . push ( recording )
staticRecordings . value . push ( recording )
// 清空input以便重复选择同一文件
event . target . value = ''
}
} ,
// 选择录音
selectRecording ( index ) {
this . selected Recording = index
} ,
// 播放/暂停录音
togglePlay ( index ) {
const recording = this . recordings [ index ]
}
// 选择录音
const selectRecording = ( index ) => {
selectedRecording . value = index
}
// 播放/暂停录音
const togglePlay = ( index ) => {
const recording = recordings . value [ index ]
// 停止当前播放的音频
if ( this . currentAudio ) {
this . currentAudio . pause ( )
this . recordings . forEach ( r => r . isPlaying = false )
if ( currentAudio . value ) {
currentAudio . value . pause( )
recordings . value . forEach( r => r . isPlaying = false )
}
if ( ! recording . isPlaying ) {
this . currentAudio = new Audio ( recording . url )
this . currentAudio . play ( )
currentAudio . value = new Audio ( recording . url )
currentAudio . value . play( )
recording . isPlaying = true
this . currentAudio . onended = ( ) => {
currentAudio . value . onended = ( ) => {
recording . isPlaying = false
this . currentAudio = null
currentAudio . value = null
}
}
} ,
// 转换为文本
async convertToText ( index ) {
const recording = this . recordings [ index ]
this . selectedRecording = index
this . showTranscriptView = true
this . isConverting = true
this . currentTranscript = null
this . currentViewType = 'transcript'
}
// 转换为文字
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 , 2 000) )
await new Promise ( resolve => setTimeout ( resolve , 1 000) )
// 这里应该调用实际的语音转文本API
// 目前使用模拟数据
recording . transcript = ` 这是 ${ recording . name } 的转换文本示例。在实际应用中,这里会显示真实的语音转文本结果。您可以集成百度、阿里云、腾讯云等语音识别服务来实现真正的语音转文本功能。 `
this . currentTranscript = recording . transcript
// 转换完成后自动开始录音分析
this . startRecordingAnalysis ( recording )
// 使用从API获取的transcription数据
if ( recording . transcription ) {
recording . transcript = recording . transcription
currentTranscript . value = recording . transcription
} else {
// 如果没有transcription数据, 显示提示信息
recording . transcript = '暂无转换文本数据'
currentTranscript . value = '暂无转换文本数据'
}
// 添加转换完成的动画效果
const resultElement = document . querySelector ( '.conversion-result' )
@@ -367,105 +335,135 @@ export default {
} catch ( error ) {
console . error ( '转换失败:' , error )
alert ( '转换失败,请重试' )
this . showTranscriptView = false
showTranscriptView . value = false
} finally {
this . isConverting = false
isConverting . value = false
}
} ,
}
// 开始通话录音分析
async startRecordingAnalysis ( recording ) {
this . isAnalyzing = true
this . analysisResult = ''
// 构建通话录音分析查询
const recordingQuery = ` 请对录音文件 ${ recording . name } 进行通话录音分析,包括:
1. 通话质量评估
2. 客户情绪分析
3. 沟通效果评价
4. 关键信息提取
5. 改进建议
录音信息:
文件名: ${ recording . name }
文件大小: ${ this . formatFileSize ( recording . size ) }
转换文本: ${ recording . transcript } `
// 开始通话录音分析
const startRecordingAnalysis = async ( recording ) => {
isAnalyzing . value = true
try {
await this . chatService _02 . sendMessage (
recordingQuery ,
( update ) => {
// 实时更新通话录音分析结果
this . analysisResult = update . content
} ,
( ) => {
// 流结束回调
console . log ( '通话录音分析完成' )
this . isAnalyzing = false
// 使用从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 )
this . analysisResult = '通话 录音分析失败,请重试。'
this . isAnalyzing = false
console . error ( '录音分析失败:' , error )
analysisResult . value = '录音分析失败,请重试。'
} finally {
isAnalyzing . value = false
}
} ,
}
// 切换视图类型
switchViewType ( type ) {
this . currentViewType = type
} ,
// 切换视图类型
const switchViewType = ( type ) => {
currentViewType . value = type
// 返回录音列表
backTo Recordings ( ) {
this . showTranscriptView = false
th is . currentTranscript = null
this . analysisResult = ''
this . currentViewType = 'transcript'
this . isAnalyzing = false
} ,
// 复制文本
copyText ( ) {
if ( this . currentTranscript ) {
navigator . clipboard . writeText ( this . currentTranscript )
// 如果切换到录音分析视图,且还没有分析结果,则开始分析
if ( type === 'analysis' && ! analysisResult . value && selected Recording. value !== null ) {
const recording = recordings . value [ selectedRecording . value ]
startRecordingAnalys is( 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 ( '文本已复制到剪贴板' )
}
} ,
// 复制分析结果
copyAnalysisText ( ) {
if ( this . analysisResult ) {
navigator . clipboard . writeText ( this . analysisResult )
} catch ( error ) {
console . error ( '复制失败:' , error )
alert ( '复制失败,请手动复制' )
}
}
}
// 复制分析结果
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 ( '分析结果已复制到剪贴板' )
}
} ,
// 显示展开弹框
showExpandDialog ( ) {
this . showDialog = true
} ,
// 关闭弹框
closeDialog ( ) {
this . showDialog = false
} ,
// 格式化文件大小
formatFileSize ( bytes ) {
} catch ( error ) {
console . error ( '复制失败:' , error )
alert ( '复制失败,请手动复制' )
}
}
}
// 显示展开弹框
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 ]
} ,
triggerFileUpload ( ) {
}
const triggerFileUpload = ( ) => {
const fileInput = document . createElement ( 'input' )
fileInput . type = 'file'
fileInput . accept = 'audio/*'
fileInput . style . display = 'none'
fileInput . addEventListener ( 'change' , this . handleFileSelect )
fileInput . addEventListener ( 'change' , handleFileSelect )
document . body . appendChild ( fileInput )
fileInput . click ( )
document . body . removeChild ( fileInput )
} ,
downloadRecording ( index ) {
const r ecording = this . recordings [ index ]
}
const downloadR ecording = ( index ) => {
const recording = recordings . value [ index ]
if ( recording && recording . url ) {
const link = document . createElement ( 'a' )
link . href = recording . url
@@ -474,8 +472,6 @@ export default {
link . click ( )
document . body . removeChild ( link )
}
}
}
}
< / script >
@@ -558,8 +554,8 @@ export default {
. recording - section {
width : 100 % ;
min - height : 3 00px ;
max - height : 5 00px ;
min - height : 2 00px ;
max - height : 3 00px ;
overflow - y : auto ;
}