diff --git a/git-merge-helper.md b/git-merge-helper.md new file mode 100644 index 0000000..e8f77af --- /dev/null +++ b/git-merge-helper.md @@ -0,0 +1,81 @@ +# Git 批量合并操作指南 + +## 情况 1:需要自动接受所有远程更改(推荐) + +如果你想接受远程分支的所有更改: + +```bash +# 先中断当前合并(如果还在合并状态) +git merge --abort + +# 然后使用策略自动接受远程版本 +git pull --no-edit -X theirs +``` + +## 情况 2:需要自动接受所有本地更改 + +如果你想保留本地版本: + +```bash +git merge --abort +git pull --no-edit -X ours +``` + +## 情况 3:自动完成合并(无冲突时) + +如果只是需要自动使用默认合并消息,不需要编辑: + +```bash +git pull --no-edit +``` + +## 情况 4:已有冲突,需要批量解决 + +如果已经进入合并状态,有多个冲突文件: + +### 方法 A:全部接受远程版本 +```bash +git checkout --theirs . +git add . +git commit --no-edit +``` + +### 方法 B:全部接受本地版本 +```bash +git checkout --ours . +git add . +git commit --no-edit +``` + +## 配置 Git 默认编辑器避免弹出编辑器 + +如果你想以后自动使用默认合并消息,可以设置: + +```bash +git config --global core.editor "true" +``` + +或者设置为空字符串: + +```bash +git config --global core.editor "" +``` + +## 推荐操作流程 + +对于你当前的情况,建议按顺序尝试: + +1. **首先尝试自动合并(无冲突):** + ```bash + git pull --no-edit + ``` + +2. **如果有冲突,全部接受远程版本:** + ```bash + git checkout --theirs . + git add . + git commit --no-edit + ``` + +3. **如果遇到合并消息编辑界面,直接关闭保存即可(使用默认消息)** + diff --git a/pages/champion/components/practice-tab.vue b/pages/champion/components/practice-tab.vue index 12b0cfa..b438056 100644 --- a/pages/champion/components/practice-tab.vue +++ b/pages/champion/components/practice-tab.vue @@ -1220,6 +1220,7 @@ border-radius: 16rpx; z-index: 1000; display: flex; + overflow-y: auto; flex-direction: column; box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2); overflow: hidden; diff --git a/pages/customer/components/ConversationSummaryFurniture.vue b/pages/customer/components/ConversationSummaryFurniture.vue new file mode 100644 index 0000000..ff15c2a --- /dev/null +++ b/pages/customer/components/ConversationSummaryFurniture.vue @@ -0,0 +1,755 @@ + + + + + \ No newline at end of file diff --git a/pages/customer/customer.vue b/pages/customer/customer.vue index fc12fe5..1cfdb57 100644 --- a/pages/customer/customer.vue +++ b/pages/customer/customer.vue @@ -98,6 +98,21 @@ + + + + + 录音文本内容 + + + + + + {{ currentRecordingText || '暂无录音文本' }} + + + + @@ -172,106 +187,10 @@ - - - - - {{ item.label }} - - - {{ item.value }} - - - - - - - - 意向产品清单 - - - {{ getProductCategoryNames() }} - - - - - - - 一句话总结 - - - {{ summarySentence }} - - - - - - - 成交关键点 - - - {{ dealKeyPoints }} - - - - - - - - - {{ category.name }} - - - - - - - - - {{ getProductCategoryName(activeProductCategory) }} - - - - - {{ row.label }} - - - {{ row.content }} - - - - - - - - - 产品需求 - - - - - 提示 - - - 暂无产品需求信息 - - - - - - + @@ -470,6 +389,7 @@ import CustomerList from './components/CustomerList.vue'; import ServiceList from './components/ServiceList.vue'; import CustomerProfileModal from './components/CustomerProfileModal.vue'; + import ConversationSummaryFurniture from './components/ConversationSummaryFurniture.vue'; import { initCustomerProfile, initSummaryList, initPerformanceData } from './utils/dataInit.js'; import { getApiUrl, API_BASE_URL } from '@/common/config.js'; @@ -482,7 +402,8 @@ components: { CustomerList, ServiceList, - CustomerProfileModal + CustomerProfileModal, + ConversationSummaryFurniture }, data() { return { @@ -541,6 +462,10 @@ audioSegmentsList: [], // 录音分段列表 audioSegmentsLoading: false, // 录音分段加载状态 playingAudioId: null, // 当前正在播放的录音ID + innerAudioContext: null, // 音频播放器实例 + currentAudioUrl: null, // 当前音频URL(用于清理Blob URL) + showTextModal: false, // 是否显示文本内容弹框 + currentRecordingText: '', // 当前显示的录音文本内容 customerList: [ { name: '李女士', @@ -570,6 +495,30 @@ onLoad() { this.loadServiceList(); }, + onUnload() { + // 组件销毁时,清理音频播放器 + if (this.innerAudioContext) { + try { + this.innerAudioContext.stop(); + this.innerAudioContext.destroy(); + } catch (e) { + console.error('销毁音频播放器失败:', e); + } + this.innerAudioContext = null; + } + // #ifdef H5 + // 清理Blob URL,释放内存 + if (this.currentAudioUrl && this.currentAudioUrl.startsWith('blob:')) { + try { + URL.revokeObjectURL(this.currentAudioUrl); + } catch (e) { + console.error('清理Blob URL失败:', e); + } + } + // #endif + this.currentAudioUrl = null; + this.playingAudioId = null; + }, methods: { goBack() { uni.navigateBack(); @@ -606,21 +555,11 @@ async viewDetail(item) { // 查看详情 this.currentServiceDetail = { ...item }; - // 先初始化默认的总结数据 - this.initSummaryList(item); - // 重置产品需求清单和相关数据 - this.productCategoryList = []; - this.activeProductCategory = ''; - this.productRequirementMap = {}; - this.summarySentence = ''; - this.dealKeyPoints = ''; // 重置待办事项列表 this.todoList = []; this.todoListTotal = 0; // 重置章节概要列表 this.chapterSummaryList = []; - // 调用接口获取家具意向分析数据(如果有数据会更新summaryList) - await this.loadFurnitureAnalysis(item); // 如果当前标签页是智能待办,加载待办事项 if (this.activeDetailTab === 'todo') { this.loadTodoList(); @@ -633,6 +572,7 @@ if (this.activeDetailTab === 'performance') { this.loadPerformanceData(); } + // 会话总结数据由"会话总结家具"组件自动加载 this.$refs.serviceDetailPopup.open(); }, // 处理菜单操作 @@ -847,6 +787,8 @@ return; } + + // 如果点击的是当前正在播放的录音,则暂停 if (this.playingAudioId === audioId) { await this.stopAudioPlay(item); @@ -855,6 +797,10 @@ if (this.playingAudioId) { await this.stopAudioPlay({ id: this.playingAudioId }); } + // 显示文本内容弹框 + this.currentRecordingText = item.recordingText || ''; + this.showTextModal = true; + this.$refs.textModalPopup.open(); // 开始播放新的录音 await this.startAudioPlay(item); } @@ -866,6 +812,20 @@ }); } }, + // 关闭文本内容弹框 + closeTextModal() { + this.showTextModal = false; + this.currentRecordingText = ''; + this.$refs.textModalPopup.close(); + }, + // 文本弹框状态变化 + onTextModalChange(e) { + if (!e.show) { + // 弹框关闭时,清除状态 + this.showTextModal = false; + this.currentRecordingText = ''; + } + }, // 开始播放录音 async startAudioPlay(item) { try { @@ -880,12 +840,16 @@ return; } - uni.showLoading({ - title: '播放中...' - }); + // 如果已有播放器正在播放,先停止 + if (this.innerAudioContext) { + this.innerAudioContext.stop(); + this.innerAudioContext.destroy(); + this.innerAudioContext = null; + } - // 调用播放接口:/api/audioManagementSegments/play/{id} - const url = getApiUrl(`/api/audioManagementSegments/play/${audioId}`); + uni.showLoading({ + title: '加载中...' + }); // 获取认证信息 let tenantId = ''; @@ -897,7 +861,16 @@ console.error('获取认证信息失败:', e); } - // 构建请求头 + // 构建请求URL + // 根据后端接口定义:@PostMapping("/playSegmentById") 使用 @PathVariable String id + // 如果使用 @PathVariable,路径应该是 /playSegmentById/{id} + // 先尝试路径参数方式:/api/audioManagementSegments/playSegmentById/{id} + const url = getApiUrl(`/api/audioManagementSegments/playSegmentById/${audioId}`); + + console.log('请求音频URL:', url); + console.log('音频ID:', audioId); + + // 构建请求头(参考其他请求的实现方式) const headers = { 'Content-Type': 'application/json' }; @@ -908,110 +881,170 @@ headers['X-Tenant-Id'] = tenantId; } + console.log('请求头:', headers); + + // 由于后端接口是POST方式,且需要传递header,而innerAudioContext无法设置请求头 + // 所以先通过uni.request获取音频数据,然后转换为可播放的URL const res = await uni.request({ url: url, method: 'POST', header: headers, - timeout: 10000 + responseType: 'arraybuffer', // 接收二进制数据 + timeout: 30000 // 音频文件可能较大,增加超时时间 }); - uni.hideLoading(); + console.log('响应状态码:', res.statusCode); + console.log('响应头:', res.header); - if (res.statusCode === 200 && res.data && res.data.success) { - // 设置当前播放的录音ID + if (res.statusCode !== 200) { + console.error('请求失败,状态码:', res.statusCode); + console.error('响应数据:', res.data); + // 如果是404,可能是路径不对,尝试其他方式 + if (res.statusCode === 404) { + throw new Error(`接口路径不存在(404),请检查后端接口路径是否正确。当前URL: ${url}`); + } + throw new Error(`请求失败,状态码: ${res.statusCode}`); + } + + // 从响应头获取Content-Type,确定音频格式 + // 如果响应头中没有Content-Type,默认使用audio/mpeg + let audioType = 'audio/mpeg'; + if (res.header && res.header['Content-Type']) { + audioType = res.header['Content-Type']; + } else if (res.header && res.header['content-type']) { + audioType = res.header['content-type']; + } + + // 将arraybuffer转换为base64或blob URL + // #ifdef H5 + // H5平台:使用Blob URL + const blob = new Blob([res.data], { type: audioType }); + const audioUrl = URL.createObjectURL(blob); + // #endif + + // #ifndef H5 + // 非H5平台:使用base64 + const base64 = uni.arrayBufferToBase64(res.data); + const audioUrl = `data:${audioType};base64,${base64}`; + // #endif + + // 保存audioUrl到组件实例,以便在回调中清理 + this.currentAudioUrl = audioUrl; + + // 创建音频播放器实例 + const innerAudioContext = uni.createInnerAudioContext(); + innerAudioContext.src = audioUrl; + + // 监听播放事件 + innerAudioContext.onPlay(() => { + console.log('开始播放音频'); + uni.hideLoading(); this.playingAudioId = audioId; uni.showToast({ title: '开始播放', icon: 'success', duration: 1000 }); - } else { + }); + + // 监听播放错误 + innerAudioContext.onError((error) => { + console.error('音频播放错误:', error); + uni.hideLoading(); + this.playingAudioId = null; + innerAudioContext.destroy(); + this.innerAudioContext = null; + // #ifdef H5 + // 清理Blob URL,释放内存 + if (this.currentAudioUrl && this.currentAudioUrl.startsWith('blob:')) { + URL.revokeObjectURL(this.currentAudioUrl); + } + // #endif + this.currentAudioUrl = null; uni.showToast({ - title: res.data?.message || '播放失败', - icon: 'none' + title: '播放失败,请检查网络或音频文件', + icon: 'none', + duration: 2000 }); - } + }); + + // 监听播放结束 + innerAudioContext.onEnded(() => { + console.log('音频播放结束'); + this.playingAudioId = null; + innerAudioContext.destroy(); + this.innerAudioContext = null; + // #ifdef H5 + // 清理Blob URL,释放内存 + if (this.currentAudioUrl && this.currentAudioUrl.startsWith('blob:')) { + URL.revokeObjectURL(this.currentAudioUrl); + } + // #endif + this.currentAudioUrl = null; + }); + + // 监听播放暂停 + innerAudioContext.onPause(() => { + console.log('音频播放暂停'); + this.playingAudioId = null; + }); + + // 存储播放器实例 + this.innerAudioContext = innerAudioContext; + + // 开始播放 + innerAudioContext.play(); + } catch (error) { uni.hideLoading(); console.error('播放录音失败:', error); + if (this.innerAudioContext) { + this.innerAudioContext.destroy(); + this.innerAudioContext = null; + } + this.playingAudioId = null; uni.showToast({ title: `播放失败: ${error.errMsg || error.message || '未知错误'}`, - icon: 'none' + icon: 'none', + duration: 2000 }); } }, // 停止播放录音 async stopAudioPlay(item) { try { - // 获取录音ID(可能是 id 或 segmentId) - const audioId = item.id || item.segmentId; - if (!audioId) { - // 如果没有ID,直接清除播放状态 - this.playingAudioId = null; - return; + // 停止音频播放器 + if (this.innerAudioContext) { + this.innerAudioContext.stop(); + this.innerAudioContext.destroy(); + this.innerAudioContext = null; } - uni.showLoading({ - title: '停止中...' + // 清除播放状态 + this.playingAudioId = null; + + uni.showToast({ + title: '已停止', + icon: 'success', + duration: 1000 }); - - // 调用停止接口:/api/audioManagementSegments/stop/{id} - const url = getApiUrl(`/api/audioManagementSegments/stop/${audioId}`); - - // 获取认证信息 - let tenantId = ''; - let token = ''; - try { - tenantId = uni.getStorageSync('backend-tenant-id') || ''; - token = uni.getStorageSync('backend-token') || ''; - } catch (e) { - console.error('获取认证信息失败:', e); - } - - // 构建请求头 - const headers = { - 'Content-Type': 'application/json' - }; - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - if (tenantId) { - headers['X-Tenant-Id'] = tenantId; - } - - const res = await uni.request({ - url: url, - method: 'POST', - header: headers, - timeout: 10000 - }); - - uni.hideLoading(); - - if (res.statusCode === 200 && res.data && res.data.success) { - // 清除当前播放的录音ID - this.playingAudioId = null; - uni.showToast({ - title: '已停止', - icon: 'success', - duration: 1000 - }); - } else { - // 即使接口失败,也清除播放状态 - this.playingAudioId = null; - uni.showToast({ - title: res.data?.message || '停止失败', - icon: 'none' - }); - } } catch (error) { - uni.hideLoading(); - // 即使接口失败,也清除播放状态 + // 即使出错,也清除播放状态 + if (this.innerAudioContext) { + try { + this.innerAudioContext.stop(); + this.innerAudioContext.destroy(); + } catch (e) { + console.error('销毁音频播放器失败:', e); + } + this.innerAudioContext = null; + } this.playingAudioId = null; console.error('停止播放失败:', error); uni.showToast({ - title: `停止失败: ${error.errMsg || error.message || '未知错误'}`, - icon: 'none' + title: '已停止', + icon: 'success', + duration: 1000 }); } }, @@ -3244,5 +3277,79 @@ font-size: 28rpx; } + /* 录音文本内容弹框样式 */ + /* 提高文本内容弹框的层级,确保显示在录音列表弹框上方 */ + /* 通过深度选择器覆盖uni-popup的z-index */ + .text-modal-popup { + z-index: 1000 !important; + } + + .text-modal-popup .uni-popup { + z-index: 1000 !important; + } + + .text-modal { + width: 680rpx; + max-width: 90vw; + height: 80vh; + max-height: 80vh; + background-color: #FFFFFF; + border-radius: 24rpx; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12); + position: relative; + } + + .text-modal-header { + padding: 32rpx; + border-bottom: 1px solid #F0F0F0; + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + } + + .text-modal-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + } + + .text-modal-close { + width: 48rpx; + height: 48rpx; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 50%; + transition: background-color 0.2s; + } + + .text-modal-close:active { + background-color: #F5F5F5; + } + + .text-modal-content { + flex: 1; + height: 0; + min-height: 0; + box-sizing: border-box; + } + + .text-modal-text { + font-size: 28rpx; + color: #333; + line-height: 1.8; + white-space: pre-wrap; + word-wrap: break-word; + word-break: break-all; + display: block; + padding: 32rpx; + box-sizing: border-box; + } +