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 @@
+
+
+
+
+ 加载中...
+
+
+
+
+ 暂无会话总结数据
+
+
+
+
+
+ {{ item.label }}
+
+
+ {{ item.value }}
+
+
+
+
+
+
+
+ 意向产品清单
+
+
+ {{ getProductCategoryNames() }}
+
+
+
+
+
+
+ 一句话总结
+
+
+ {{ summarySentence }}
+
+
+
+
+
+
+ 成交关键点
+
+
+ {{ dealKeyPoints }}
+
+
+
+
+
+
+
+
+ {{ category.name }}
+
+
+
+
+
+
+
+
+ {{ getProductCategoryName(activeProductCategory) }}
+
+
+
+
+ {{ row.label }}
+
+
+ {{ row.content }}
+
+
+
+
+
+
+
+
+ 产品需求
+
+
+
+
+ 提示
+
+
+ 暂无产品需求信息
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
@@ -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;
+ }
+