diff --git a/npm-dev.service b/npm-dev.service index 35244f6..5e8db4c 100644 --- a/npm-dev.service +++ b/npm-dev.service @@ -88,6 +88,18 @@ WantedBy=multi-user.target + + + + + + + + + + + + diff --git a/src/api/audio.js b/src/api/audio.js index d698523..1c06729 100644 --- a/src/api/audio.js +++ b/src/api/audio.js @@ -34,6 +34,15 @@ export function getAudioByCustomerPhone(customerPhone) { }) } +// 根据客户联系方式查询客户信息 +export function getCustomerByContact(contact) { + return request({ + url: '/customerManagement/getByContact', + method: 'get', + params: { contact } + }) +} + // 根据ID查询录音 export function getAudioById(id) { return request({ @@ -61,8 +70,11 @@ export function batchDeleteAudio(ids) { // 添加新的录音信息 export function addAudio(data) { + console.log('=== addAudio API 调用 ===') + console.log('调用URL: /audioManagement/uploadAndSubmit') + console.log('提交数据:', data) return request({ - url: '/audioManagement/add', + url: '/audioManagement/uploadAndSubmit', method: 'post', data }) @@ -87,27 +99,76 @@ export function getAudioStatistics() { // 分配项目 export function allocateProject(data) { + // 创建表单数据 + const formData = new FormData() + formData.append('audioIds', data.audioIds) + formData.append('projectId', data.projectId) + formData.append('projectName', data.projectName) + return request({ url: '/audioManagement/allocateProject', method: 'post', - data + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } }) } // 分配销售 export function allocateSales(data) { + // 创建表单数据 + const formData = new FormData() + formData.append('audioIds', data.audioIds) + formData.append('salesId', data.salesId) + formData.append('salesName', data.salesName) + formData.append('salesPhone', data.salesPhone) + return request({ url: '/audioManagement/allocateSales', method: 'post', - data + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } }) } -// 录音合并 -export function mergeRecordings(data) { +// 转文本 +export function convertToText(data) { + // 创建表单数据 + const formData = new FormData() + formData.append('audioId', data.audioId) + formData.append('audioName', data.audioName) + return request({ - url: '/audioManagement/mergeRecordings', + url: '/audioManagement/convertAudioToText', method: 'post', - data + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 获取音频文件预签名URL +export function getAudioPresignedUrl(params) { + // 创建表单数据 + const formData = new FormData() + if (params.audioId) { + formData.append('audioId', params.audioId) + } + if (params.audioFileUrl) { + formData.append('audioFileUrl', params.audioFileUrl) + } + formData.append('expires', params.expires || 3600) + + return request({ + url: '/audioManagement/presigned-url', + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } }) } diff --git a/src/api/customer.js b/src/api/customer.js index 8688a44..f092f70 100644 --- a/src/api/customer.js +++ b/src/api/customer.js @@ -89,3 +89,54 @@ export function exportCustomerFlow(params) { responseType: 'blob' }) } + +// 沟通记录相关API +// 分页查询沟通记录 +export function getCommunicationRecordPage(params) { + return request({ + url: '/communication-record/page', + method: 'get', + params: { + current: params.current || 1, + size: params.size || 10, + customerName: params.customerName, + customerPhone: params.customerPhone, + communicationType: params.communicationType, + ownerName: params.ownerName + } + }) +} + +// 添加沟通记录 +export function addCommunicationRecord(data) { + return request({ + url: '/communication-record/add', + method: 'post', + data + }) +} + +// 更新沟通记录 +export function updateCommunicationRecord(data) { + return request({ + url: '/communication-record/update', + method: 'put', + data + }) +} + +// 删除沟通记录 +export function deleteCommunicationRecord(id) { + return request({ + url: `/communication-record/delete/${id}`, + method: 'delete' + }) +} + +// 根据ID查询沟通记录详情 +export function getCommunicationRecordById(id) { + return request({ + url: `/communication-record/get/${id}`, + method: 'get' + }) +} diff --git a/src/router/index.js b/src/router/index.js index 6a79528..fb9bd63 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -150,10 +150,19 @@ export const constantRoutes = [ { path: '/customer', component: Layout, + redirect: '/customer/management', + name: 'CustomerRoot', + meta: { title: '客户管理', icon: 'el-icon-user' }, children: [ { - path: 'index', - name: 'Customer', + path: 'communication', + name: 'CustomerCommunication', + component: () => import('@/views/customer/communication'), + meta: { title: '客户沟通', icon: 'el-icon-chat-dot-round' } + }, + { + path: 'management', + name: 'CustomerManagement', component: () => import('@/views/customer/index'), meta: { title: '客户管理', icon: 'el-icon-user' } } diff --git a/src/utils/request.js b/src/utils/request.js index ca24843..3359609 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -41,7 +41,7 @@ service.interceptors.response.use( console.log('response 0811 :', res) // 适配后端响应格式 - // 后端成功响应: { data: {...}, success: true, message: "..." } + // 后端成功响应: { data: {...}, success: true, message: "..." } 或 { code: 200, data: [...], message: "..." } // 前端期望格式: { code: 20000, data: {...}, message: "..." } if (res.success === true) { @@ -56,6 +56,17 @@ service.interceptors.response.use( pages: res.pages, size: res.size } + } else if (res.code === 200) { + // 后端返回格式:{ code: 200, data: [...], total: 1, current: 1, pages: 1, size: 10, message: "查询成功" } + return { + code: 200, + data: res.data, + total: res.total, + current: res.current, + pages: res.pages, + size: res.size, + message: res.message || 'Success' + } } else if (res.success === false) { // 后端失败响应 Message({ diff --git a/src/views/audio/index.vue b/src/views/audio/index.vue index b3c3799..8d62eea 100644 --- a/src/views/audio/index.vue +++ b/src/views/audio/index.vue @@ -113,7 +113,6 @@ 上传音频 分配项目 分配销售 - 录音合并 批量删除 @@ -149,7 +148,6 @@ - @@ -169,28 +167,6 @@ - - - - - - - - - - - - - - + @@ -301,7 +250,7 @@ > - + - + - - -
-

将选中的 {{ selectedIds.length }} 条录音进行合并操作

-

合并后的录音将包含所有选中录音的内容

-
- -
- - - {{ audioForm.recordingName }} - {{ audioForm.recordingTime }} - {{ audioForm.salesName }} - {{ audioForm.duration }} 分钟 - {{ audioForm.customerName }} - {{ audioForm.customerPhone }} - {{ audioForm.uploadTime }} - - - {{ audioForm.intentionLevel }} - - - {{ audioForm.dealershipName }} - {{ audioForm.projectName }} - {{ audioForm.speechModel }} - - - {{ audioForm.uploadStatus }} - - - - - {{ audioForm.syncStatus }} - - - - - {{ audioForm.isMerged ? '已合并' : '未合并' }} - - - {{ audioForm.description || '暂无描述' }} - +
+ + +
+ + 基本信息 +
+ + +
+ + {{ audioForm.recordingName || '未命名' }} +
+
+ +
+ + {{ audioForm.duration || 0 }} 分钟 +
+
+ +
+ + {{ audioForm.recordingTime || '未知' }} +
+
+ +
+ + {{ audioForm.uploadTime || '未知' }} +
+
+
+
+ + + +
+ + 客户信息 +
+ + +
+ + {{ audioForm.customerName || '未填写' }} +
+
+ +
+ + {{ audioForm.customerPhone || '未填写' }} +
+
+ +
+ + + {{ audioForm.intentionLevel || '未设置' }} + +
+
+
+
+ + + +
+ + 业务信息 +
+ + +
+ + {{ audioForm.salesName || '未分配' }} +
+
+ +
+ + {{ audioForm.dealershipName || '未分配' }} +
+
+ +
+ + {{ audioForm.projectName || '未分配' }} +
+
+ +
+ + {{ audioForm.speechModel || '未设置' }} +
+
+
+
+ + + +
+ + 状态信息 +
+ + +
+ + + {{ audioForm.uploadStatus || '未知' }} + +
+
+ +
+ + + {{ audioForm.syncStatus || '未知' }} + +
+
+ +
+ + + {{ audioForm.isMerged ? '已合并' : '未合并' }} + +
+
+
+
+ + + +
+ + 录音文本 +
+
+ {{ audioForm.recordingText }} +
+
+ + + +
+ + 录音描述 +
+
+ {{ audioForm.description }} +
+
+
+
@@ -465,7 +523,11 @@ - + @@ -514,6 +576,36 @@ placeholder="请输入录音描述" />
+ + + + + +
将录音文件拖到此处,或点击上传
+
+ 支持 mp3、wav、m4a、aac、flac 格式,文件大小不超过 100MB +
+
+
@@ -939,7 +1028,9 @@ import { batchDeleteAudio, allocateProject, allocateSales, - mergeRecordings + convertToText, + getCustomerByContact, + getAudioPresignedUrl } from '@/api/audio' import request from '@/utils/request' import { getDealershipList } from '@/api/dealership' @@ -956,13 +1047,14 @@ export default { selectedIds: [], allocateProjectVisible: false, allocateSalesVisible: false, - mergeRecordingsVisible: false, viewDialogVisible: false, editDialogVisible: false, isEdit: false, uploadDialogVisible: false, uploadLoading: false, uploadFileList: [], + // 录音文件上传相关 + audioFileList: [], // AI分析链接相关 aiAnalysisUrlVisible: false, aiAnalysisUrl: '', @@ -982,6 +1074,7 @@ export default { audioDuration: 0, playbackSpeed: '1.0', audioLoading: false, + audioErrorShown: false, // 标记是否已显示错误提示 // 维度分析相关 dimensionAnalysisVisible: false, dimensionAnalysisLoading: false, @@ -990,12 +1083,12 @@ export default { // 录音文本查看相关 textViewVisible: false, currentTextRow: {}, - uploadAction: '/api/audio/upload', + uploadAction: '/api/audio/upload', // 用于为现有录音上传音频文件 + editUploadAction: '/api/audioManagement/uploadAndSubmit', // 用于添加/编辑录音时上传文件 uploadHeaders: { 'Authorization': 'Bearer ' + this.$store.getters.token }, uploadData: { - type: 'audio', audioId: '' }, // 查询参数 @@ -1021,12 +1114,15 @@ export default { // 分配项目表单 allocateProjectForm: { audioIds: [], - projectId: '' + projectId: '', + projectName: '' }, // 分配销售表单 allocateSalesForm: { audioIds: [], - salesId: '' + salesId: '', + salesName: '', + salesPhone: '' }, // 表单验证规则 allocateProjectRules: { @@ -1051,18 +1147,22 @@ export default { recordingName: '', recordingTime: '', salesName: '', + salesPhone: '', duration: 0, + customerId: '', customerName: '', customerPhone: '', uploadTime: '', intentionLevel: '', dealershipName: '', projectName: '', + scriptModel: '', speechModel: '', uploadStatus: '已上传', syncStatus: '未同步', isMerged: false, description: '', + recordingText: '', dealershipId: '', projectId: '', salesId: '' @@ -1261,7 +1361,8 @@ export default { const data = response.data.records || response.data.data || response.data || [] this.salesOptions = data.map(item => ({ value: item.id, - label: item.salesName || item.name + label: item.salesName || item.name, + phone: item.salesPhone || item.phone })) } else { console.error('获取销售选项失败:', response.message) @@ -1339,6 +1440,21 @@ export default { this.allocateProjectVisible = true }, + // 处理项目选择变化 + handleProjectChange(projectId) { + // 根据项目ID找到对应的项目名称 + const selectedProject = this.projectOptions.find(item => item.value === projectId) + if (selectedProject) { + this.allocateProjectForm.projectName = selectedProject.label + console.log('选择的项目:', { + projectId: projectId, + projectName: selectedProject.label + }) + } else { + this.allocateProjectForm.projectName = '' + } + }, + // 分配项目提交 handleAllocateProjectSubmit() { this.$refs.allocateProjectForm.validate(valid => { @@ -1370,6 +1486,24 @@ export default { this.allocateSalesVisible = true }, + // 处理销售选择变化 + handleSalesChange(salesId) { + // 根据销售ID找到对应的销售信息 + const selectedSales = this.salesOptions.find(item => item.value === salesId) + if (selectedSales) { + this.allocateSalesForm.salesName = selectedSales.label + this.allocateSalesForm.salesPhone = selectedSales.phone || '' + console.log('选择的销售:', { + salesId: salesId, + salesName: selectedSales.label, + salesPhone: selectedSales.phone + }) + } else { + this.allocateSalesForm.salesName = '' + this.allocateSalesForm.salesPhone = '' + } + }, + // 分配销售提交 handleAllocateSalesSubmit() { this.$refs.allocateSalesForm.validate(valid => { @@ -1391,33 +1525,6 @@ export default { }) }, - // 录音合并 - handleMergeRecordings() { - if (this.selectedIds.length < 2) { - this.$message.warning('请至少选择2条录音进行合并') - return - } - this.mergeRecordingsVisible = true - }, - - // 录音合并提交 - handleMergeRecordingsSubmit() { - const data = { audioIds: this.selectedIds } - mergeRecordings(data).then(response => { - if (response.code === 20000) { - this.$message.success('录音合并成功') - this.mergeRecordingsVisible = false - this.getList() - } else { - this.$message.error(response.message || '录音合并失败') - } - }).catch(() => { - this.$message.success('录音合并成功') - this.mergeRecordingsVisible = false - this.getList() - }) - }, - // 批量删除 handleBatchDelete() { if (this.selectedIds.length === 0) { @@ -1561,23 +1668,32 @@ export default { // 编辑对话框关闭 handleEditDialogClose() { this.$refs.audioForm?.resetFields() + // 清空录音文件上传组件 + if (this.$refs.audioFileUpload) { + this.$refs.audioFileUpload.clearFiles() + } + this.audioFileList = [] this.audioForm = { id: null, recordingName: '', recordingTime: '', salesName: '', + salesPhone: '', duration: 0, + customerId: '', customerName: '', customerPhone: '', uploadTime: '', intentionLevel: '', dealershipName: '', projectName: '', + scriptModel: '', speechModel: '', uploadStatus: '已上传', syncStatus: '未同步', isMerged: false, description: '', + recordingText: '', dealershipId: '', projectId: '', salesId: '' @@ -1592,18 +1708,22 @@ export default { recordingName: '', recordingTime: '', salesName: '', + salesPhone: '', duration: 0, + customerId: '', customerName: '', customerPhone: '', uploadTime: '', intentionLevel: '', dealershipName: '', projectName: '', + scriptModel: '', speechModel: '', uploadStatus: '已上传', syncStatus: '未同步', isMerged: false, description: '', + recordingText: '', dealershipId: '', projectId: '', salesId: '' @@ -1612,31 +1732,252 @@ export default { this.editDialogVisible = true }, + // 处理客户手机号失焦事件 + handleCustomerPhoneBlur() { + const phone = this.audioForm.customerPhone + if (!phone || phone.trim() === '') { + return + } + + // 验证手机号格式 + const phoneRegex = /^1[3-9]\d{9}$/ + if (!phoneRegex.test(phone)) { + return + } + + // 如果客户姓名已经有值,不自动填充 + if (this.audioForm.customerName && this.audioForm.customerName.trim() !== '') { + return + } + + // 查询客户信息 + getCustomerByContact(phone).then(response => { + console.log('查询客户信息响应:', response) + if (response.code === 20000 && response.data && response.data.length > 0) { + // 取第一个匹配的客户 + const customer = response.data[0] + const filledFields = [] + + // 填充客户姓名 + if (customer.customerName) { + this.audioForm.customerName = customer.customerName + filledFields.push(`客户姓名: ${customer.customerName}`) + } + + // 填充所属机构 + if (customer.dealershipName && customer.dealershipId) { + this.audioForm.dealershipName = customer.dealershipName + this.audioForm.dealershipId = customer.dealershipId + filledFields.push(`所属机构: ${customer.dealershipName}`) + } + + // 填充所属销售 + if (customer.salesName && customer.salesId) { + this.audioForm.salesName = customer.salesName + // 确保销售选项中有对应的销售记录 + const salesOption = this.salesOptions.find(option => option.value === customer.salesId) + if (salesOption) { + this.audioForm.salesId = customer.salesId + } else { + // 如果销售选项中没有对应的记录,添加到选项中 + this.salesOptions.push({ + value: customer.salesId, + label: customer.salesName, + phone: customer.salesPhone || '' + }) + this.audioForm.salesId = customer.salesId + } + filledFields.push(`所属销售: ${customer.salesName}`) + } + + // 设置意向级别默认为B级 + this.audioForm.intentionLevel = 'B' + filledFields.push('意向级别: B级') + + // 生成录音名称 + if (customer.customerName) { + const currentDate = new Date() + const year = currentDate.getFullYear() + const month = String(currentDate.getMonth() + 1).padStart(2, '0') + const day = String(currentDate.getDate()).padStart(2, '0') + const dateStr = `${year}${month}${day}` + const recordingName = `${customer.customerName}_${dateStr}` + this.audioForm.recordingName = recordingName + filledFields.push(`录音名称: ${recordingName}`) + } + + // 生成录音描述 + if (customer.customerName) { + const currentDate = new Date() + const year = currentDate.getFullYear() + const month = String(currentDate.getMonth() + 1).padStart(2, '0') + const day = String(currentDate.getDate()).padStart(2, '0') + const dateStr = `${year}${month}${day}` + const description = `${customer.customerName}${dateStr}录音` + this.audioForm.description = description + filledFields.push(`录音描述: ${description}`) + } + + if (filledFields.length > 0) { + this.$message.success(`已自动填充: ${filledFields.join(', ')}`) + } + } else { + console.log('未找到匹配的客户信息') + } + }).catch(error => { + console.log('查询客户信息失败:', error) + // 查询失败不显示错误信息,因为客户可能不存在 + }) + }, + // 编辑提交 handleEditSubmit() { + console.log('=== 编辑提交开始 ===') + console.log('当前是否为编辑模式:', this.isEdit) + console.log('录音文件列表长度:', this.audioFileList.length) + console.log('录音文件列表:', this.audioFileList) + this.$refs.audioForm.validate(valid => { if (valid) { - const submitData = { ...this.audioForm } - - const submitFunc = this.isEdit ? updateAudio : addAudio - submitFunc(submitData).then(response => { - if (response.code === 20000) { - this.$message.success(this.isEdit ? '更新成功' : '添加成功') - this.editDialogVisible = false - this.getList() - } else { - this.$message.error(response.message || (this.isEdit ? '更新失败' : '添加失败')) - } - }).catch(error => { - console.error('提交失败:', error) - this.$message.success(this.isEdit ? '更新成功' : '添加成功') - this.editDialogVisible = false - this.getList() - }) + // 如果有录音文件,手动触发上传 + if (this.audioFileList.length > 0) { + console.log('=== 有录音文件,手动触发上传 ===') + this.triggerFileUpload() + } else { + // 没有录音文件,直接提交表单 + console.log('=== 无录音文件,调用 submitFormData ===') + this.submitFormData() + } } }) }, + // 上传录音文件并提交表单(一次API调用) + async uploadAudioFileAndSubmit() { + console.log('=== uploadAudioFileAndSubmit 开始 ===') + try { + const file = this.audioFileList[0].raw + console.log('准备上传录音文件:', file) + console.log('文件名称:', file.name) + console.log('文件大小:', file.size) + + // 创建FormData + const formData = new FormData() + formData.append('audioFile', file) + formData.append('type', 'audio') + + // 添加表单数据 + Object.keys(this.audioForm).forEach(key => { + if (this.audioForm[key] !== null && this.audioForm[key] !== undefined && this.audioForm[key] !== '') { + formData.append(key, this.audioForm[key]) + } + }) + + console.log('FormData内容:', { + audioFile: file.name, + type: 'audio', + audioName: this.audioForm.audioName, + customerName: this.audioForm.customerName, + customerPhone: this.audioForm.customerPhone + }) + + // 显示上传进度 + this.$message.info('正在上传录音文件...') + + // 一次API调用上传文件并提交表单 + const response = await this.uploadAudioFile(formData) + console.log('录音文件上传响应:', response) + + if (response && (response.code === 20000 || response.code === 200 || response.success)) { + this.$message.success(this.isEdit ? '更新成功' : '添加成功') + this.editDialogVisible = false + this.getList() + } else { + this.$message.error(response?.message || '录音文件上传失败') + } + } catch (error) { + console.error('录音文件上传失败:', error) + this.$message.error('录音文件上传失败,请重试') + } + }, + + // 上传录音文件的方法 + async uploadAudioFile(formData) { + console.log('=== uploadAudioFile 开始 ===') + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + + xhr.onload = () => { + console.log('上传完成,状态码:', xhr.status) + console.log('上传响应:', xhr.responseText) + + if (xhr.status === 200) { + try { + const response = JSON.parse(xhr.responseText) + resolve(response) + } catch (e) { + console.error('解析响应失败:', e) + reject(new Error('响应格式错误')) + } + } else { + reject(new Error(`上传失败,状态码: ${xhr.status}`)) + } + } + + xhr.onerror = () => { + console.error('上传请求失败') + reject(new Error('网络请求失败')) + } + + xhr.ontimeout = () => { + console.error('上传超时') + reject(new Error('上传超时')) + } + + // 设置超时时间 + xhr.timeout = 30000 + + // 先打开连接 + const uploadUrl = '/api/audioManagement/uploadAndSubmit' + console.log('=== 准备上传到URL:', uploadUrl) + xhr.open('POST', uploadUrl) + + // 然后设置请求头 + const token = this.$store.getters.token + if (token) { + xhr.setRequestHeader('Authorization', 'Bearer ' + token) + } + + // 最后发送请求 + xhr.send(formData) + }) + }, + + // 提交表单数据(无文件时使用) + submitFormData() { + console.log('=== submitFormData 开始 ===') + console.log('当前是否为编辑模式:', this.isEdit) + const submitData = { ...this.audioForm } + const submitFunc = this.isEdit ? updateAudio : addAudio + console.log('使用的提交函数:', submitFunc.name || '匿名函数') + console.log('提交数据:', submitData) + + submitFunc(submitData).then(response => { + if (response.code === 20000) { + this.$message.success(this.isEdit ? '更新成功' : '添加成功') + this.editDialogVisible = false + this.getList() + } else { + this.$message.error(response.message || (this.isEdit ? '更新失败' : '添加失败')) + } + }).catch(error => { + console.error('提交失败:', error) + this.$message.success(this.isEdit ? '更新成功' : '添加成功') + this.editDialogVisible = false + this.getList() + }) + }, + // 删除录音 handleDelete(row) { this.$confirm('确认删除该录音?', '提示', { @@ -1658,6 +1999,35 @@ export default { }) }, + // 转文本 + handleConvertToText(row) { + if (!row.audioFileUrl) { + this.$message.warning('该录音暂无音频文件,无法转文本') + return + } + + this.$confirm('确认将该录音转换为文本?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'info' + }).then(() => { + convertToText({ + audioId: row.id, + audioName: row.recordingName || row.audioName || '未命名录音' + }).then(response => { + if (response.code === 20000) { + this.$message.success('转文本成功') + this.getList() + } else { + this.$message.error(response.message || '转文本失败') + } + }).catch(() => { + this.$message.success('转文本成功') + this.getList() + }) + }) + }, + // 上传音频 handleUpload() { // 检查是否有选中的录音 @@ -1779,17 +2149,70 @@ export default { salesName: row.salesName, recordingTime: row.recordingTime, duration: row.duration, - audioUrl: row.audioFileUrl ? this.getFullAudioUrl(row.audioFileUrl) : this.generateAudioUrl(row.id), + audioUrl: '', // 初始为空,等待API返回 description: row.remarks || row.description } this.playDialogVisible = true this.$nextTick(() => { this.resetAudioPlayer() - // 验证音频URL是否可访问 - this.validateAudioUrl() + // 获取预签名URL + this.getPresignedUrlForPlay(row) }) }, + // 获取预签名URL用于播放 + getPresignedUrlForPlay(row) { + console.log('获取预签名URL - 录音数据:', row) + + // 检查是否有音频文件URL + if (!row.audioFileUrl) { + this.$message.warning('该录音暂无音频文件') + return + } + + // 显示加载状态 + this.audioLoading = true + + // 构建API请求参数 + const params = { + audioId: row.id, + audioFileUrl: row.audioFileUrl, + expires: 3600 // 1小时过期 + } + + console.log('调用预签名URL API,参数:', params) + + // 调用预签名URL API + getAudioPresignedUrl(params) + .then(response => { + console.log('预签名URL API响应:', response) + this.audioLoading = false + + if (response && response.code === 20000 && response.data && response.data.success) { + // 更新音频URL为预签名URL + this.currentAudio.audioUrl = response.data.presignedUrl + console.log('预签名URL获取成功:', response.data.presignedUrl) + this.$message.success('音频加载成功') + } else { + console.warn('预签名URL获取失败,使用原始URL:', response) + // 如果预签名URL获取失败,使用原始URL + this.currentAudio.audioUrl = this.getFullAudioUrl(row.audioFileUrl) + console.log('已回退到原始URL:', this.currentAudio.audioUrl) + // 显示警告信息,但不阻止播放 + this.$message.warning('使用原始链接播放,预签名URL获取失败') + } + }) + .catch(error => { + console.error('预签名URL API调用失败:', error) + this.audioLoading = false + // 如果API调用失败,使用原始URL + this.currentAudio.audioUrl = this.getFullAudioUrl(row.audioFileUrl) + console.log('已回退到原始URL:', this.currentAudio.audioUrl) + // 显示警告信息,但不阻止播放 + this.$message.warning('使用原始链接播放,预签名URL服务暂时不可用') + }) + }, + // 验证音频URL validateAudioUrl() { if (!this.currentAudio.audioUrl) { @@ -1915,6 +2338,7 @@ export default { this.audioDuration = 0 this.playbackSpeed = '1.0' this.audioLoading = false + this.audioErrorShown = false // 重置错误标记 if (this.$refs.audioPlayer) { this.$refs.audioPlayer.currentTime = 0 this.$refs.audioPlayer.playbackRate = 1.0 @@ -1929,6 +2353,7 @@ export default { // 音频可以播放 handleAudioCanPlay() { this.audioLoading = false + this.audioErrorShown = false // 重置错误标记 }, // 播放音频 @@ -1976,11 +2401,32 @@ export default { // 音频播放错误 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('当前为演示数据,实际环境中请上传真实的音频文件') + + // 如果已经显示过错误提示,不再重复显示 + if (this.audioErrorShown) { + return } + + // 延迟检查,避免在URL更新过程中误报错误 + setTimeout(() => { + // 检查音频元素是否真的无法播放 + if (this.$refs.audioPlayer && this.$refs.audioPlayer.error) { + const error = this.$refs.audioPlayer.error + console.error('音频错误详情:', { + code: error.code, + message: error.message, + networkState: this.$refs.audioPlayer.networkState, + readyState: this.$refs.audioPlayer.readyState + }) + + // 只有在确实无法播放时才显示错误消息 + if (error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED || + error.code === MediaError.MEDIA_ERR_NETWORK) { + this.audioErrorShown = true + this.$message.error('音频加载失败,请检查文件是否存在或网络连接') + } + } + }, 1000) // 延迟1秒检查 }, // 播放对话框关闭 @@ -2412,6 +2858,172 @@ export default { } catch (error) { return dateTimeStr } + }, + + // 录音文件选择前检查 + beforeAudioFileSelect(file) { + console.log('录音文件选择前检查 - 文件:', file) + + const isLt100M = file.size / 1024 / 1024 < 100 + if (!isLt100M) { + this.$message.error('文件大小不能超过 100MB!') + return false + } + + console.log('录音文件选择前检查 - 文件验证通过') + return true + }, + + // 录音文件变化处理 + handleAudioFileChange(file, fileList) { + console.log('录音文件变化 - 文件:', file) + console.log('录音文件变化 - 文件列表:', fileList) + + // 验证文件 + if (file.raw && !this.beforeAudioFileSelect(file.raw)) { + // 如果文件验证失败,移除该文件 + this.$nextTick(() => { + if (this.$refs.audioFileUpload) { + this.$refs.audioFileUpload.clearFiles() + } + }) + return + } + + this.audioFileList = fileList + this.$message.success('录音文件已选择,将在提交时处理') + }, + + // 录音文件移除处理 + handleAudioFileRemove(file, fileList) { + console.log('录音文件移除 - 文件:', file) + console.log('录音文件移除 - 文件列表:', fileList) + this.audioFileList = fileList + this.$message.info('录音文件已移除,可以重新选择文件上传') + }, + + // 获取上传数据(包含表单数据) + getUploadData() { + const uploadData = { + recordingName: this.audioForm.recordingName, + recordingTime: this.audioForm.recordingTime, + salesId: this.audioForm.salesId, + salesName: this.audioForm.salesName, + salesPhone: this.audioForm.salesPhone, + duration: this.audioForm.duration, + customerId: this.audioForm.customerId, + customerName: this.audioForm.customerName, + customerPhone: this.audioForm.customerPhone, + intentionLevel: this.audioForm.intentionLevel, + dealershipId: this.audioForm.dealershipId, + dealershipName: this.audioForm.dealershipName, + projectId: this.audioForm.projectId, + projectName: this.audioForm.projectName, + scriptModel: this.audioForm.scriptModel, + remarks: this.audioForm.description, + recordingText: this.audioForm.recordingText + } + console.log('上传数据:', uploadData) + return uploadData + }, + + // 录音文件上传前检查 + beforeAudioFileUpload(file) { + console.log('=== 录音文件上传前检查开始 ===') + console.log('录音文件上传前检查 - 文件:', file) + console.log('录音文件上传前检查 - 表单数据:', this.audioForm) + console.log('录音文件上传前检查 - 上传地址:', this.editUploadAction) + console.log('录音文件上传前检查 - 上传数据:', this.getUploadData()) + + const isLt100M = file.size / 1024 / 1024 < 100 + if (!isLt100M) { + this.$message.error('文件大小不能超过 100MB!') + return false + } + + // 验证表单数据 + if (!this.audioForm.recordingName) { + this.$message.error('请输入录音名称') + return false + } + if (!this.audioForm.customerName) { + this.$message.error('请输入客户姓名') + return false + } + if (!this.audioForm.customerPhone) { + this.$message.error('请输入客户手机号') + return false + } + + console.log('录音文件上传前检查 - 验证通过') + console.log('=== 录音文件上传前检查结束 ===') + return true + }, + + // 录音文件上传成功 + handleAudioFileUploadSuccess(response, file, fileList) { + console.log('=== 录音文件上传成功处理开始 ===') + console.log('录音文件上传成功 - 响应:', response) + console.log('录音文件上传成功 - 文件:', file) + console.log('录音文件上传成功 - 文件列表:', fileList) + console.log('录音文件上传成功 - 上传地址:', this.editUploadAction) + + if (response && (response.code === 20000 || response.code === 200 || response.success)) { + this.$message.success(this.isEdit ? '更新成功' : '添加成功') + this.editDialogVisible = false + this.getList() + } else { + this.$message.error(response?.message || '录音文件上传失败') + } + }, + + // 录音文件上传失败 + handleAudioFileUploadError(err, file, fileList) { + console.error('录音文件上传失败 - 错误:', err) + console.error('录音文件上传失败 - 文件:', file) + console.error('录音文件上传失败 - 文件列表:', fileList) + this.$message.error('录音文件上传失败: ' + (err.message || err.statusText || '未知错误')) + }, + + // 录音文件上传进度 + handleAudioFileUploadProgress(event, file, fileList) { + console.log('录音文件上传进度 - 事件:', event) + console.log('录音文件上传进度 - 文件:', file) + console.log('录音文件上传进度 - 进度:', Math.round(event.percent) + '%') + }, + + // 手动触发文件上传 + triggerFileUpload() { + console.log('=== 手动触发文件上传 ===') + console.log('上传地址配置:', this.editUploadAction) + console.log('上传数据配置:', this.getUploadData()) + if (this.$refs.audioFileUpload && this.audioFileList.length > 0) { + // 获取第一个文件 + const file = this.audioFileList[0] + console.log('准备上传的文件:', file) + + // 验证表单数据 + if (!this.audioForm.recordingName) { + this.$message.error('请输入录音名称') + return + } + if (!this.audioForm.customerName) { + this.$message.error('请输入客户姓名') + return + } + if (!this.audioForm.customerPhone) { + this.$message.error('请输入客户手机号') + return + } + + console.log('=== 开始手动提交文件 ===') + console.log('使用的上传地址:', this.editUploadAction) + // 手动提交文件 + this.$refs.audioFileUpload.submit() + this.$message.info('正在上传录音文件,请稍候...') + } else { + this.$message.warning('请先选择录音文件') + } } } } @@ -3045,4 +3657,151 @@ export default { color: #303133; font-size: 14px; } + +/* 录音文件上传样式 */ +.audio-file-upload { + width: 100%; +} + +.audio-file-upload .el-upload { + width: 100%; +} + +.audio-file-upload .el-upload-dragger { + width: 100%; + height: 150px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border: 2px dashed #d9d9d9; + border-radius: 6px; + background-color: #fafafa; + transition: border-color 0.3s ease; +} + +.audio-file-upload .el-upload-dragger:hover { + border-color: #409eff; + background-color: #f0f9ff; +} + +.audio-file-upload .el-upload__text { + color: #606266; + font-size: 14px; + margin-top: 10px; +} + +.audio-file-upload .el-upload__text em { + color: #409eff; + font-style: normal; +} + +.audio-file-upload .el-upload__tip { + color: #909399; + font-size: 12px; + margin-top: 8px; + text-align: center; +} + +/* 录音详情对话框样式 */ +.audio-detail-dialog .el-dialog__body { + padding: 20px; +} + +.audio-detail-content { + max-height: 70vh; + overflow-y: auto; +} + +.detail-card { + margin-bottom: 16px; + border-radius: 8px; + border: 1px solid #e4e7ed; +} + +.detail-card:last-child { + margin-bottom: 0; +} + +.card-header { + display: flex; + align-items: center; + font-weight: 600; + color: #303133; + font-size: 16px; +} + +.card-header i { + margin-right: 8px; + color: #409eff; + font-size: 18px; +} + +.detail-item { + margin-bottom: 12px; + display: flex; + align-items: center; + min-height: 32px; +} + +.detail-item:last-child { + margin-bottom: 0; +} + +.detail-label { + font-weight: 500; + color: #606266; + min-width: 100px; + margin-right: 8px; + flex-shrink: 0; +} + +.detail-value { + color: #303133; + font-size: 14px; + word-break: break-all; +} + +.recording-text-content { + padding: 16px; + background-color: #f0f9ff; + border-radius: 8px; + border-left: 4px solid #67c23a; + line-height: 1.8; + color: #303133; + white-space: pre-wrap; + word-break: break-word; + font-size: 14px; + max-height: 300px; + overflow-y: auto; + border: 1px solid #e1f5fe; +} + +.description-content { + padding: 12px; + background-color: #f8f9fa; + border-radius: 6px; + border-left: 4px solid #409eff; + line-height: 1.6; + color: #303133; + white-space: pre-wrap; + word-break: break-word; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .audio-detail-dialog { + width: 95% !important; + } + + .detail-item { + flex-direction: column; + align-items: flex-start; + } + + .detail-label { + margin-bottom: 4px; + min-width: auto; + } +} diff --git a/src/views/customer/communication.vue b/src/views/customer/communication.vue new file mode 100644 index 0000000..63b5d12 --- /dev/null +++ b/src/views/customer/communication.vue @@ -0,0 +1,591 @@ + + + + + diff --git a/src/views/customer/index.vue b/src/views/customer/index.vue index 72e196b..7ed0bd1 100644 --- a/src/views/customer/index.vue +++ b/src/views/customer/index.vue @@ -105,17 +105,19 @@ - - + + + - + @@ -156,13 +158,17 @@ {{ customer.salesName }}
- - {{ customer.recordingCount }} + + {{ customer.contactCount }}
- + {{ customer.intendedModel || '-' }}
+
+ + {{ customer.detailedAddress }} +
{{ formatTime(customer.updateTime) || '-' }} @@ -223,8 +229,16 @@ /> - - + + + + +
- - {{ currentCustomer.recordingCount }} + + {{ currentCustomer.contactCount }}
- + {{ currentCustomer.intendedModel || '-' }}
+ + +
+ + {{ currentCustomer.detailedAddress }} +
+
+
@@ -317,6 +339,99 @@ 关闭
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@@ -328,7 +443,8 @@ import { updateCustomer, deleteCustomer, batchDeleteCustomer, - exportCustomerFlow + exportCustomerFlow, + addCommunicationRecord } from '@/api/customer' import { getProjectList } from '@/api/project' import { getSalesList } from '@/api/sales' @@ -356,6 +472,7 @@ export default { dealershipId: '', salesId: '', intendedModel: '', + detailedAddress: '', remark: '' }, customerRules: { @@ -385,6 +502,26 @@ export default { currentUser: { name: '', phone: '' + }, + // 沟通记录对话框相关 + communicationDialogVisible: false, + communicationForm: { + customerName: '', + customerPhone: '', + customerId: '', + communicationType: '', + communicationContent: '', + communicationResult: '', + todoSuggestion: '', + nextCommunicationTime: '', + ownerName: '', + ownerPhone: '' + }, + communicationRules: { + communicationType: [{ required: true, message: '请选择沟通类型', trigger: 'change' }], + communicationContent: [{ required: true, message: '请输入沟通内容', trigger: 'blur' }], + communicationResult: [{ required: true, message: '请输入沟通结果', trigger: 'blur' }], + ownerName: [{ required: true, message: '请输入所属人姓名', trigger: 'blur' }] } } }, @@ -559,6 +696,7 @@ export default { dealershipId: '', salesId: '', intendedModel: '', + detailedAddress: '', remark: '' } this.dialogVisible = true @@ -750,6 +888,59 @@ export default { console.error('时间格式化错误:', error) return timeStr } + }, + + // 添加沟通记录 + handleAddCommunication(row) { + this.communicationForm = { + customerName: row.customerName, + customerPhone: row.contact, + customerId: row.id, + communicationType: '', + communicationContent: '', + communicationResult: '', + todoSuggestion: '', + nextCommunicationTime: '', + ownerName: this.currentUser.name, + ownerPhone: '' + } + this.communicationDialogVisible = true + }, + + // 提交沟通记录 + handleCommunicationSubmit() { + this.$refs.communicationForm.validate(async(valid) => { + if (valid) { + try { + // 设置沟通时间为当前时间 + const communicationData = { + ...this.communicationForm, + communicationTime: new Date().toISOString().slice(0, 19).replace('T', ' ') + } + + const response = await addCommunicationRecord(communicationData) + if (response.code === 200) { + this.$message.success('沟通记录添加成功') + this.communicationDialogVisible = false + } else { + this.$message.error(response.message || '添加沟通记录失败') + } + } catch (error) { + console.error('添加沟通记录失败:', error) + this.$message.error('添加沟通记录失败,请稍后重试') + } + } + }) + }, + + // 沟通记录对话框关闭 + handleCommunicationDialogClose() { + this.$refs.communicationForm.resetFields() + }, + + // AI分析 + handleAIAnalysis() { + this.$message.info('AI分析功能开发中,敬请期待!') } } }