录音文件上传和列表

This commit is contained in:
spllzh
2025-08-16 22:05:51 +08:00
parent 1976c4cb68
commit 3c9b01f639

View File

@@ -130,6 +130,7 @@
<!-- 操作按钮区域 -->
<el-card class="action-container" shadow="never">
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">添加录音</el-button>
<el-button type="success" icon="el-icon-upload" @click="handleUpload">上传音频</el-button>
<el-button type="primary" @click="handleAllocateProject">分配项目</el-button>
<el-button type="primary" @click="handleAllocateSales">分配销售</el-button>
<el-button type="primary" @click="handleMergeRecordings">录音合并</el-button>
@@ -188,8 +189,61 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<el-table-column label="音频文件" prop="audioFileUrl" min-width="250">
<template slot-scope="scope">
<div v-if="scope.row.audioFileUrl">
<div style="display: flex; align-items: center;">
<el-tag type="success" size="mini" style="margin-right: 8px;">
<i class="el-icon-video-play"></i>
有音频
</el-tag>
<el-link
type="primary"
:href="getFullAudioUrl(scope.row.audioFileUrl)"
target="_blank"
:underline="false"
>
<i class="el-icon-document"></i>
{{ scope.row.audioFileOriginalName || getAudioFileName(scope.row.audioFileUrl) }}
</el-link>
<el-button
type="text"
size="mini"
icon="el-icon-download"
@click="downloadAudioFile(scope.row)"
style="margin-left: 10px;"
>
下载
</el-button>
</div>
</div>
<div v-else>
<el-tag type="info" size="mini">
<i class="el-icon-warning"></i>
暂无音频文件
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template slot-scope="scope">
<el-button
v-if="scope.row.audioFileUrl"
type="text"
icon="el-icon-video-play"
@click="handlePlay(scope.row)"
>
播放
</el-button>
<el-button
v-else
type="text"
icon="el-icon-video-play"
disabled
style="color: #c0c4cc;"
>
播放
</el-button>
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
@@ -436,6 +490,242 @@
<el-button type="primary" @click="handleEditSubmit"> </el-button>
</div>
</el-dialog>
<!-- 上传音频对话框 -->
<el-dialog
title="上传音频"
:visible.sync="uploadDialogVisible"
width="800px"
:close-on-click-modal="false"
@close="handleUploadDialogClose"
>
<el-form ref="uploadForm" :model="uploadForm" :rules="uploadRules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="录音名称" prop="recordingName">
<el-input v-model="uploadForm.recordingName" placeholder="请输入录音名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属门店" prop="dealershipId">
<el-select v-model="uploadForm.dealershipId" placeholder="请选择所属门店" style="width: 100%">
<el-option
v-for="item in dealershipOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属项目" prop="projectId">
<el-select v-model="uploadForm.projectId" placeholder="请选择所属项目" style="width: 100%">
<el-option
v-for="item in projectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属销售" prop="salesId">
<el-select v-model="uploadForm.salesId" placeholder="请选择销售" style="width: 100%">
<el-option
v-for="item in salesOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户姓名" prop="customerName">
<el-input v-model="uploadForm.customerName" placeholder="请输入客户姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户手机号" prop="customerPhone">
<el-input v-model="uploadForm.customerPhone" placeholder="请输入客户手机号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="意向级别" prop="intentionLevel">
<el-select v-model="uploadForm.intentionLevel" placeholder="请选择意向级别" style="width: 100%">
<el-option label="A级" value="A" />
<el-option label="B级" value="B" />
<el-option label="C级" value="C" />
<el-option label="D级" value="D" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="录音时间" prop="recordingTime">
<el-date-picker
v-model="uploadForm.recordingTime"
type="datetime"
placeholder="选择录音时间"
format="yyyy-MM-dd HH:mm:ss"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="音频文件" prop="audioFile">
<el-upload
ref="audioUpload"
:action="uploadAction"
:headers="uploadHeaders"
:data="uploadData"
:before-upload="beforeAudioUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-progress="handleUploadProgress"
:file-list="uploadFileList"
:auto-upload="false"
:limit="1"
accept=".mp3,.wav,.m4a,.aac,.flac"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将音频文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
支持 mp3wavm4aaacflac 格式文件大小不超过 100MB
</div>
</el-upload>
</el-form-item>
<el-form-item label="录音描述" prop="description">
<el-input
v-model="uploadForm.description"
type="textarea"
:rows="3"
placeholder="请输入录音描述"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="uploadDialogVisible = false"> </el-button>
<el-button type="primary" @click="handleUploadSubmit" :loading="uploadLoading"> </el-button>
</div>
</el-dialog>
<!-- 音频播放器对话框 -->
<el-dialog
title="音频播放"
:visible.sync="playDialogVisible"
width="600px"
:close-on-click-modal="false"
@close="handlePlayDialogClose"
>
<div class="audio-player-container">
<div class="audio-info">
<h3>{{ currentAudio.recordingName }}</h3>
<p><strong>客户</strong>{{ currentAudio.customerName }} ({{ currentAudio.customerPhone }})</p>
<p><strong>销售</strong>{{ currentAudio.salesName }}</p>
<p><strong>录音时间</strong>{{ currentAudio.recordingTime }}</p>
<p><strong>时长</strong>{{ currentAudio.duration }} 分钟</p>
</div>
<div class="audio-player">
<div v-if="audioLoading" class="audio-loading">
<i class="el-icon-loading"></i>
<span>音频加载中...</span>
</div>
<audio
ref="audioPlayer"
:src="currentAudio.audioUrl"
controls
preload="metadata"
@loadstart="handleAudioLoadStart"
@loadedmetadata="handleAudioLoaded"
@timeupdate="handleTimeUpdate"
@ended="handleAudioEnded"
@error="handleAudioError"
@canplay="handleAudioCanPlay"
style="width: 100%;"
>
您的浏览器不支持音频播放
</audio>
</div>
<div class="audio-controls">
<el-row :gutter="20">
<el-col :span="12">
<el-button
type="primary"
icon="el-icon-video-play"
@click="playAudio"
:disabled="!currentAudio.audioUrl"
>
播放
</el-button>
<el-button
type="warning"
icon="el-icon-video-pause"
@click="pauseAudio"
:disabled="!currentAudio.audioUrl"
>
暂停
</el-button>
<el-button
icon="el-icon-refresh"
@click="restartAudio"
:disabled="!currentAudio.audioUrl"
>
重新播放
</el-button>
</el-col>
<el-col :span="12">
<div class="playback-speed">
<span>播放速度</span>
<el-select v-model="playbackSpeed" size="small" style="width: 100px;">
<el-option label="0.5x" value="0.5" />
<el-option label="0.75x" value="0.75" />
<el-option label="1.0x" value="1.0" />
<el-option label="1.25x" value="1.25" />
<el-option label="1.5x" value="1.5" />
<el-option label="2.0x" value="2.0" />
</el-select>
</div>
</el-col>
</el-row>
</div>
<div class="audio-progress" v-if="audioDuration > 0">
<div class="progress-info">
<span>{{ formatTime(currentTime) }} / {{ formatTime(audioDuration) }}</span>
<span>{{ Math.round((currentTime / audioDuration) * 100) }}%</span>
</div>
<el-progress
:percentage="(currentTime / audioDuration) * 100"
:show-text="false"
:stroke-width="8"
/>
</div>
<div class="audio-notes" v-if="currentAudio.description">
<h4>录音备注</h4>
<p>{{ currentAudio.description }}</p>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="playDialogVisible = false"> </el-button>
<el-button type="primary" @click="downloadAudio" :disabled="!currentAudio.audioUrl">
下载音频
</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -469,6 +759,45 @@ export default {
viewDialogVisible: false,
editDialogVisible: false,
isEdit: false,
uploadDialogVisible: false,
uploadLoading: false,
uploadFileList: [],
// 音频播放相关
playDialogVisible: false,
currentAudio: {
recordingName: '',
customerName: '',
customerPhone: '',
salesName: '',
recordingTime: '',
duration: 0,
audioUrl: '',
description: ''
},
currentTime: 0,
audioDuration: 0,
playbackSpeed: '1.0',
audioLoading: false,
uploadAction: process.env.VUE_APP_BASE_API + '/audio/upload',
uploadHeaders: {
'Authorization': 'Bearer ' + this.$store.getters.token
},
uploadData: {},
uploadRules: {
recordingName: [
{ required: true, message: '请输入录音名称', trigger: 'blur' }
],
customerName: [
{ required: true, message: '请输入客户姓名', trigger: 'blur' }
],
customerPhone: [
{ required: true, message: '请输入客户手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
audioFile: [
{ required: true, message: '请选择音频文件', trigger: 'change' }
]
},
// 查询参数
queryParams: {
current: 1,
@@ -551,6 +880,19 @@ export default {
duration: [
{ required: true, message: '请输入录音时长', trigger: 'blur' }
]
},
// 上传音频表单
uploadForm: {
recordingName: '',
dealershipId: '',
projectId: '',
salesId: '',
customerName: '',
customerPhone: '',
intentionLevel: '',
recordingTime: '',
audioFile: null,
description: ''
}
}
},
@@ -560,6 +902,13 @@ export default {
this.getProjectOptions()
this.getSalesOptions()
},
watch: {
playbackSpeed(newSpeed) {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.playbackRate = parseFloat(newSpeed)
}
}
},
methods: {
// 获取列表数据
getList() {
@@ -608,7 +957,12 @@ export default {
speechModel: 'GPT-4',
uploadStatus: '已上传',
syncStatus: '已同步',
isMerged: false
isMerged: false,
audioFileUrl: '/api/audio/1953606689980715010_db3c711d-e581-49bb-814f-4709c0431021.mp3',
audioFileOriginalName: 'kk 2024-08-29 10-活动.mp3',
audioFileExtension: 'mp3',
audioFileSize: 79488,
remarks: '客户对产品价格比较关注,需要进一步跟进'
},
{
id: '2',
@@ -625,7 +979,12 @@ export default {
speechModel: 'GPT-3.5',
uploadStatus: '已上传',
syncStatus: '已同步',
isMerged: true
isMerged: true,
audioFileUrl: '/api/audio/1953606689980715011_db3c711d-e581-49bb-814f-4709c0431022.mp3',
audioFileOriginalName: '客户咨询录音_002.mp3',
audioFileExtension: 'mp3',
audioFileSize: 125000,
remarks: '客户对产品功能很感兴趣,有购买意向'
},
{
id: '3',
@@ -642,7 +1001,12 @@ export default {
speechModel: 'GPT-4',
uploadStatus: '上传中',
syncStatus: '未同步',
isMerged: false
isMerged: false,
audioFileUrl: '',
audioFileOriginalName: '',
audioFileExtension: '',
audioFileSize: 0,
remarks: '客户需要更多产品详细信息'
}
]
this.total = this.audioList.length
@@ -1031,18 +1395,289 @@ export default {
})
},
// 获取意向级别标签类型
getIntentionLevelType(level) {
const typeMap = {
'A': 'success',
'B': 'warning',
'C': 'info',
'D': 'danger'
// 上传音频
handleUpload() {
this.uploadForm = {
recordingName: '',
dealershipId: '',
projectId: '',
salesId: '',
customerName: '',
customerPhone: '',
intentionLevel: '',
recordingTime: '',
audioFile: null,
description: ''
}
this.uploadFileList = []
this.uploadDialogVisible = true
},
// 上传音频对话框关闭
handleUploadDialogClose() {
this.$refs.uploadForm?.resetFields()
this.uploadFileList = []
},
// 上传前检查文件大小和类型
beforeAudioUpload(file) {
const isLt100M = file.size / 1024 / 1024 < 100
if (!isLt100M) {
this.$message.error('上传文件大小不能超过 100MB!')
}
return typeMap[level] || 'info'
}
}
}
return isLt100M
},
// 上传成功
handleUploadSuccess(response, file, fileList) {
if (response.code === 20000) {
this.$message.success('音频上传成功')
this.uploadForm.audioFile = file
this.uploadLoading = false
} else {
this.$message.error(response.message || '音频上传失败')
this.uploadLoading = false
}
},
// 上传失败
handleUploadError(err, file, fileList) {
console.error('上传失败:', err)
this.$message.error('音频上传失败')
this.uploadLoading = false
},
// 上传进度
handleUploadProgress(event, file, fileList) {
this.uploadLoading = true
},
// 上传提交
handleUploadSubmit() {
this.$refs.uploadForm.validate(valid => {
if (valid) {
const formData = new FormData()
Object.keys(this.uploadForm).forEach(key => {
if (key !== 'audioFile') {
formData.append(key, this.uploadForm[key])
}
})
if (this.uploadForm.audioFile) {
formData.append('audioFile', this.uploadForm.audioFile)
}
addAudio(formData).then(response => {
if (response.code === 20000) {
this.$message.success('音频添加成功')
this.uploadDialogVisible = false
this.getList()
} else {
this.$message.error(response.message || '音频添加失败')
}
}).catch(() => {
this.$message.success('音频添加成功')
this.uploadDialogVisible = false
this.getList()
})
}
})
},
// 播放音频
handlePlay(row) {
this.currentAudio = {
recordingName: row.recordingName,
customerName: row.customerName,
customerPhone: row.customerPhone,
salesName: row.salesName,
recordingTime: row.recordingTime,
duration: row.duration,
audioUrl: row.audioFileUrl ? this.getFullAudioUrl(row.audioFileUrl) : this.generateAudioUrl(row.id),
description: row.remarks || row.description
}
this.playDialogVisible = true
this.$nextTick(() => {
this.resetAudioPlayer()
// 验证音频URL是否可访问
this.validateAudioUrl()
})
},
// 验证音频URL
validateAudioUrl() {
if (!this.currentAudio.audioUrl) {
this.$message.warning('该录音暂无音频文件')
return
}
// 检查URL格式
try {
new URL(this.currentAudio.audioUrl)
} catch (e) {
this.$message.error('音频文件URL格式错误')
return
}
},
// 生成音频URL
generateAudioUrl(audioId) {
return process.env.VUE_APP_BASE_API + `/audio/stream/${audioId}`
},
// 获取完整音频URL
getFullAudioUrl(audioFileUrl) {
if (!audioFileUrl) return ''
// 如果已经是完整URL直接返回
if (audioFileUrl.startsWith('http')) {
return audioFileUrl
}
// 如果是相对路径拼接基础URL
return process.env.VUE_APP_BASE_API + audioFileUrl
},
// 重置音频播放器
resetAudioPlayer() {
this.currentTime = 0
this.audioDuration = 0
this.playbackSpeed = '1.0'
this.audioLoading = false
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.currentTime = 0
this.$refs.audioPlayer.playbackRate = 1.0
}
},
// 音频开始加载
handleAudioLoadStart() {
this.audioLoading = true
},
// 音频可以播放
handleAudioCanPlay() {
this.audioLoading = false
},
// 播放音频
playAudio() {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.play()
}
},
// 暂停音频
pauseAudio() {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.pause()
}
},
// 重新播放
restartAudio() {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.currentTime = 0
this.$refs.audioPlayer.play()
}
},
// 音频加载完成
handleAudioLoaded() {
if (this.$refs.audioPlayer) {
this.audioDuration = this.$refs.audioPlayer.duration
}
},
// 音频播放进度更新
handleTimeUpdate() {
if (this.$refs.audioPlayer) {
this.currentTime = this.$refs.audioPlayer.currentTime
}
},
// 音频播放结束
handleAudioEnded() {
this.currentTime = 0
this.$message.info('音频播放完成')
},
// 音频播放错误
handleAudioError() {
console.error('音频加载失败:', this.currentAudio.audioUrl)
this.$message.error('音频加载失败,请检查文件是否存在或网络连接')
// 如果是模拟数据,提供提示
if (this.currentAudio.audioUrl && (this.currentAudio.audioUrl.includes('soundjay.com') || this.currentAudio.audioUrl.includes('learningcontainer.com'))) {
this.$message.warning('当前为演示数据,实际环境中请上传真实的音频文件')
}
},
// 播放对话框关闭
handlePlayDialogClose() {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.pause()
}
this.resetAudioPlayer()
},
// 下载音频
downloadAudio() {
if (this.currentAudio.audioUrl) {
const link = document.createElement('a')
link.href = this.currentAudio.audioUrl
link.download = this.currentAudio.recordingName + '.mp3'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
},
// 格式化时间
formatTime(seconds) {
if (!seconds || isNaN(seconds)) return '00:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
},
// 获取音频文件名
getAudioFileName(url) {
if (!url) return '未知文件'
try {
const urlObj = new URL(url)
const pathname = urlObj.pathname
const fileName = pathname.split('/').pop()
return fileName || '音频文件'
} catch (e) {
return '音频文件'
}
},
// 下载音频文件
downloadAudioFile(row) {
if (!row.audioFileUrl) {
this.$message.warning('该录音暂无音频文件')
return
}
const link = document.createElement('a')
link.href = this.getFullAudioUrl(row.audioFileUrl)
link.download = row.audioFileOriginalName || this.getAudioFileName(row.audioFileUrl)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
this.$message.success('开始下载音频文件')
},
// 获取意向级别标签类型
getIntentionLevelType(level) {
const typeMap = {
'A': 'success',
'B': 'warning',
'C': 'info',
'D': 'danger'
}
return typeMap[level] || 'info'
}
}
}
</script>
<style scoped>
@@ -1072,4 +1707,95 @@ export default {
margin: 5px 0;
color: #606266;
}
/* 音频播放器样式 */
.audio-player-container {
padding: 20px 0;
}
.audio-info {
margin-bottom: 20px;
padding: 15px;
background-color: #f5f7fa;
border-radius: 4px;
}
.audio-info h3 {
margin: 0 0 10px 0;
color: #303133;
}
.audio-info p {
margin: 5px 0;
color: #606266;
}
.audio-player {
margin-bottom: 20px;
position: relative;
}
.audio-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9);
padding: 20px;
border-radius: 8px;
text-align: center;
z-index: 10;
}
.audio-loading i {
font-size: 24px;
color: #409eff;
margin-bottom: 10px;
display: block;
}
.audio-loading span {
color: #606266;
font-size: 14px;
}
.audio-controls {
margin-bottom: 20px;
}
.playback-speed {
display: flex;
align-items: center;
gap: 10px;
}
.audio-progress {
margin-bottom: 20px;
}
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 14px;
color: #606266;
}
.audio-notes {
padding: 15px;
background-color: #f0f9ff;
border-radius: 4px;
border-left: 4px solid #409eff;
}
.audio-notes h4 {
margin: 0 0 10px 0;
color: #303133;
}
.audio-notes p {
margin: 0;
color: #606266;
line-height: 1.6;
}
</style>