总结代码优化,录音播放功能

This commit is contained in:
zhonghua1
2026-01-03 20:58:40 +08:00
parent 248dfd2d60
commit a122dfba7d
4 changed files with 1132 additions and 188 deletions

81
git-merge-helper.md Normal file
View File

@@ -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. **如果遇到合并消息编辑界面,直接关闭保存即可(使用默认消息)**

View File

@@ -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;

View File

@@ -0,0 +1,755 @@
<template>
<scroll-view class="detail-content" scroll-y>
<!-- 加载状态 -->
<view class="loading-container" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
<!-- 空状态 -->
<view class="empty-container" v-else-if="!loading && summaryList.length === 0 && !summarySentence && !dealKeyPoints && productCategoryList.length === 0">
<text class="empty-text">暂无会话总结数据</text>
</view>
<!-- 客户类型家庭结构等总结信息在产品清单上方 -->
<view
class="summary-item"
v-for="(item, index) in summaryList"
:key="index"
@click="viewSummaryDetail(item)">
<view class="summary-label">
<text>{{ item.label }}</text>
</view>
<view class="summary-value">
<text>{{ item.value }}</text>
</view>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
<!-- 意向产品清单显示产品类别名称列表 -->
<view class="summary-item" v-if="productCategoryList.length > 0">
<view class="summary-label">
<text>意向产品清单</text>
</view>
<view class="summary-value">
<text>{{ getProductCategoryNames() }}</text>
</view>
</view>
<!-- 一句话总结 -->
<view class="summary-item" v-if="summarySentence">
<view class="summary-label">
<text>一句话总结</text>
</view>
<view class="summary-value">
<text>{{ summarySentence }}</text>
</view>
</view>
<!-- 成交关键点 -->
<view class="summary-item" v-if="dealKeyPoints">
<view class="summary-label">
<text>成交关键点</text>
</view>
<view class="summary-value">
<text>{{ dealKeyPoints }}</text>
</view>
</view>
<!-- 产品类别标签页 -->
<view class="product-category-tabs" v-if="productCategoryList.length > 0">
<scroll-view class="category-tabs-scroll" scroll-x>
<view class="category-tabs-wrapper">
<view
class="category-tab-item"
:class="{ active: activeProductCategory === category.key }"
v-for="(category, index) in productCategoryList"
:key="index"
@click="switchProductCategory(category.key)">
<text>{{ category.name }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 产品需求清单表格 -->
<view class="product-requirement-table" v-if="activeProductCategory && currentProductRequirement">
<view class="requirement-title">
<text>{{ getProductCategoryName(activeProductCategory) }}</text>
</view>
<view class="requirement-table">
<view
class="requirement-row"
v-for="(row, index) in currentProductRequirement.rows"
:key="index">
<view class="requirement-label">
<text>{{ row.label }}</text>
</view>
<view class="requirement-content">
<text>{{ row.content }}</text>
</view>
</view>
</view>
</view>
<!-- 如果没有产品类别数据显示提示确保categoryList已初始化后显示 -->
<view class="product-requirement-table" v-else-if="productCategoryList.length === 0 && furnitureAnalysisList.length > 0">
<view class="requirement-title">
<text>产品需求</text>
</view>
<view class="requirement-table">
<view class="requirement-row">
<view class="requirement-label">
<text>提示</text>
</view>
<view class="requirement-content">
<text>暂无产品需求信息</text>
</view>
</view>
</view>
</view>
</scroll-view>
</template>
<script>
import { getApiUrl } from '@/common/config.js';
import { initSummaryList } from '../utils/dataInit.js';
export default {
name: 'ConversationSummaryFurniture',
props: {
// 服务记录项
serviceItem: {
type: Object,
default: () => ({})
}
},
data() {
return {
summaryList: [],
furnitureAnalysisList: [], // 家具意向分析数据
productCategoryList: [], // 产品类别列表
activeProductCategory: '', // 当前选中的产品类别
productRequirementMap: {}, // 产品需求映射 {categoryKey: {rows: [...]}}
summarySentence: '', // 一句话总结
dealKeyPoints: '', // 成交关键点
loading: false // 加载状态
}
},
computed: {
// 当前选中的产品需求
currentProductRequirement() {
if (!this.activeProductCategory) return null;
return this.productRequirementMap[this.activeProductCategory] || null;
}
},
watch: {
serviceItem: {
handler(newVal, oldVal) {
// 如果新值和旧值相同,不重复处理
if (newVal === oldVal) {
return;
}
// 检查新值是否有效
if (newVal && typeof newVal === 'object') {
const newId = newVal.id || newVal.rawData?.id;
const oldId = oldVal?.id || oldVal?.rawData?.id;
// 如果ID相同不重复加载
if (newId && newId === oldId) {
return;
}
if (newId) {
this.initData();
this.loadFurnitureAnalysis(newVal);
} else {
// 如果没有ID重置所有状态
this.initData();
}
} else {
// 如果新值无效,重置所有状态
this.initData();
}
},
immediate: true,
deep: true
}
},
mounted() {
// 组件挂载时如果已有serviceItem初始化数据
if (this.serviceItem && (this.serviceItem.id || this.serviceItem.rawData?.id)) {
this.initData();
this.loadFurnitureAnalysis(this.serviceItem);
}
},
methods: {
// 初始化数据
initData() {
try {
// 只有当serviceItem有效时才初始化summaryList
if (this.serviceItem && (this.serviceItem.id || this.serviceItem.rawData?.id)) {
this.summaryList = initSummaryList(this.serviceItem);
} else {
this.summaryList = [];
}
} catch (error) {
console.error('初始化总结数据失败:', error);
this.summaryList = [];
}
// 重置产品需求清单和相关数据
this.productCategoryList = [];
this.activeProductCategory = '';
this.productRequirementMap = {};
this.summarySentence = '';
this.dealKeyPoints = '';
this.furnitureAnalysisList = [];
this.loading = false;
},
// 加载家具意向分析数据
async loadFurnitureAnalysis(item) {
if (!item) {
return;
}
try {
this.loading = true;
// 获取parentId从rawData中获取id
const parentId = item.rawData?.id || item.id;
if (!parentId) {
console.warn('无法获取parentId');
this.furnitureAnalysisList = [];
this.loading = false;
return;
}
// 统一使用 getApiUrl根据 env.js 配置自动切换环境
const url = `${getApiUrl('/api/audioTextAnalysisFurniture/byParentId')}?parentId=${parentId}`;
const res = await uni.request({
url: url,
method: 'GET',
timeout: 10000
});
if (res.statusCode === 200 && res.data && res.data.success) {
this.furnitureAnalysisList = res.data.data || [];
// 如果有数据更新summaryList显示家具意向分析
if (this.furnitureAnalysisList.length > 0) {
this.updateSummaryWithFurnitureData();
}
} else {
console.warn('获取家具意向分析失败:', res.data?.message || '未知错误');
this.furnitureAnalysisList = [];
}
} catch (error) {
console.error('获取家具意向分析失败:', error);
this.furnitureAnalysisList = [];
} finally {
this.loading = false;
}
},
// 用家具意向分析数据更新会话总结
updateSummaryWithFurnitureData() {
if (this.furnitureAnalysisList.length === 0) return;
// 从第一个家具意向分析数据中提取关键信息
const firstItem = this.furnitureAnalysisList[0];
// 构建会话总结列表,显示客户类型、家庭结构、一句话总结
const summaryItems = [];
// 客户类型
if (firstItem.customerType) {
summaryItems.push({
label: '客户类型',
value: firstItem.customerType,
rawData: firstItem
});
}
// 家庭结构
if (firstItem.familyStructure) {
summaryItems.push({
label: '家庭结构',
value: firstItem.familyStructure,
rawData: firstItem
});
}
// 一句话总结单独存储不在summaryList中
if (firstItem.summarySentence) {
this.summarySentence = firstItem.summarySentence;
}
// 成交关键点(可以从数据中提取或生成)
if (firstItem.dealKeyPoints) {
this.dealKeyPoints = firstItem.dealKeyPoints;
} else if (firstItem.summarySentence) {
// 如果数据中没有成交关键点,可以从一句话总结中提取关键信息
// 提取关键词:活动优惠、搭配、环保、安全、交付、退换、服务等
const keyWords = [];
const summary = firstItem.summarySentence;
if (summary.includes('优惠') || summary.includes('活动')) {
keyWords.push('活动优惠');
}
if (summary.includes('搭配')) {
keyWords.push('整套搭配');
}
if (summary.includes('环保')) {
keyWords.push('环保');
}
if (summary.includes('安全')) {
keyWords.push('安全');
}
if (summary.includes('交付') || summary.includes('周期')) {
keyWords.push('快速交付');
}
if (summary.includes('退换') || summary.includes('服务')) {
keyWords.push('无忧退换服务');
}
if (keyWords.length > 0) {
this.dealKeyPoints = keyWords.join('');
}
}
// 如果提取到了数据更新summaryList
if (summaryItems.length > 0) {
this.summaryList = summaryItems;
}
// 构建产品需求清单
this.buildProductRequirementList(firstItem);
},
// 构建产品需求清单(固定显示所有产品类别)
buildProductRequirementList(data) {
const categories = [
{ key: 'sofa', name: '沙发', field: 'sofa' },
{ key: 'bed', name: '床', field: 'bedAndMattress' },
{ key: 'mattress', name: '床垫', field: 'bedAndMattress' },
{ key: 'dining_table', name: '餐桌椅', field: 'diningTable' },
{ key: 'tea_table', name: '茶几', field: 'teaTable' },
{ key: 'tv_cabinet', name: '电视柜', field: 'tvCabinet' },
{ key: 'study_desk', name: '学习桌', field: 'studyDesk' },
{ key: 'cabinet', name: '橱柜', field: 'cabinet' },
{ key: 'wine_cabinet', name: '酒柜', field: 'wineCabinet' },
{ key: 'master_bedroom_cabinet', name: '主卧衣柜', field: 'masterBedroomCabinet' },
{ key: 'secondary_bedroom_cabinet', name: '次卧衣柜', field: 'secondaryBedroomCabinet' },
{ key: 'shoe_cabinet', name: '鞋柜', field: 'shoeCabinet' },
{ key: 'custom', name: '定制家居', field: null }
];
const requirementMap = {};
const categoryList = [];
categories.forEach(category => {
// 检查是否有该产品的数据
let hasData = false;
let productData = null;
if (category.key === 'bed' || category.key === 'mattress') {
// 床和床垫共用bedAndMattress字段
const fieldValue = data.bedAndMattress || data.bed_and_mattress || '';
if (fieldValue && fieldValue.trim()) {
hasData = true;
productData = this.parseProductJson(fieldValue);
}
} else if (category.key === 'custom') {
// 定制家居:暂时跳过,因为各个定制产品已有单独类别
// 如果需要显示定制家居汇总,可以在这里添加逻辑
hasData = false;
} else {
// 尝试驼峰命名和蛇形命名
const fieldValue = data[category.field] || data[this.toSnakeCase(category.field)] || '';
if (fieldValue && fieldValue.trim()) {
hasData = true;
productData = this.parseProductJson(fieldValue);
}
}
// 只有当有数据时才添加到列表中
if (hasData) {
categoryList.push({ key: category.key, name: category.name });
// 构建产品需求行
const rows = this.buildProductDimensionRows(productData, data, category.key);
requirementMap[category.key] = { rows };
}
});
this.productRequirementMap = requirementMap;
this.productCategoryList = categoryList;
if (categoryList.length > 0) {
this.activeProductCategory = categoryList[0].key;
}
},
// 解析产品JSON字符串
parseProductJson(jsonString) {
if (!jsonString || typeof jsonString !== 'string') {
return null;
}
try {
const parsed = JSON.parse(jsonString);
return parsed;
} catch (error) {
console.warn('解析产品JSON失败:', error, jsonString);
return null;
}
},
// 驼峰转蛇形命名
toSnakeCase(str) {
if (!str) return '';
// 在大写字母前加下划线,然后转小写,最后去掉开头的下划线
return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
},
// 切换产品类别
switchProductCategory(categoryKey) {
this.activeProductCategory = categoryKey;
},
// 获取产品类别名称
getProductCategoryName(categoryKey) {
const category = this.productCategoryList.find(item => item.key === categoryKey);
return category ? category.name : '';
},
// 获取所有产品类别名称(用于意向产品清单显示)
getProductCategoryNames() {
return this.productCategoryList.map(item => item.name).join('');
},
parseProductAttributes(productKey, requirementText, data) {
const attributes = {};
const text = (requirementText || '').trim();
if (!text) return attributes;
const matchFirst = (pattern) => {
const result = text.match(pattern);
return result ? result[0] : '';
};
switch (productKey) {
case 'sofa':
attributes.type = matchFirst(/(L型|L形|一字型|U型|U形|带贵妃榻|贵妃榻|三人位|四人位|转角|组合)/);
attributes.material = matchFirst(/(真皮|科技布|布艺|布|皮质|皮革|实木|板式|棉麻|绒布)/);
attributes.size = matchFirst(/(\d+\.?\d*)\s*(米|m|平米|平方米)/);
attributes.color = matchFirst(/(米白|白色|灰色|黑色|棕色|蓝色|绿色|暖色调|冷色调|灰白色系)/);
attributes.style = matchFirst(/(现代简约|简约现代|现代|北欧|中式|美式|欧式|工业风|原木风|简约|极简)/);
if (text.includes('储物') || text.includes('收纳')) attributes.feature = '储物';
if (text.includes('好打理')) attributes.remark = '要求好打理';
break;
case 'bed':
attributes.size = matchFirst(/(\d+\.?\d*)\s*(米|m)/);
attributes.material = matchFirst(/(实木|板式|软包|皮质|布艺)/);
attributes.style = data.decorationStyle || '';
if (text.includes('主卧')) attributes.remark = '主卧';
break;
case 'mattress':
attributes.size = matchFirst(/(\d+\.?\d*)\s*(米|m)/);
attributes.material = matchFirst(/(弹簧|记忆棉|乳胶|椰棕|海绵)/);
if (text.includes('分区支撑')) attributes.softness = '分区支撑(兼顾软硬需求)';
else if (text.includes('偏软') || text.includes('软')) attributes.softness = '偏软';
else if (text.includes('偏硬') || text.includes('硬')) attributes.softness = '偏硬';
break;
case 'dining_table':
if (text.includes('伸缩')) attributes.type = '可伸缩';
const rangeMatch = text.match(/(\d+\.?\d*)\s*[-~到]\s*(\d+\.?\d*)\s*米/);
if (rangeMatch) attributes.size = `${rangeMatch[1]}-${rangeMatch[2]}`;
else attributes.size = matchFirst(/(\d+\.?\d*)\s*米/);
attributes.chairCount = matchFirst(/(\d+)\s*把/);
if (text.includes('舒适')) attributes.feature = '舒适型';
attributes.style = data.decorationStyle || '';
break;
case 'tea_table':
if (text.includes('抽屉')) attributes.type = '带抽屉';
if (text.includes('储物') || text.includes('收纳')) attributes.feature = '储物';
attributes.style = data.decorationStyle || '';
break;
case 'tv_cabinet':
if (text.includes('组合')) attributes.type = '组合式';
if (text.includes('储物')) attributes.feature = '储物';
attributes.style = data.decorationStyle || '';
break;
case 'study_desk':
if (text.includes('可调节') || text.includes('调节高度')) attributes.type = '可调节高度';
if (text.includes('成长')) attributes.feature = '适应孩子成长';
break;
case 'wine_cabinet':
if (text.includes('恒温')) attributes.feature = '恒温';
const capacity = text.match(/(\d+)[-~到](\d+)\s*瓶|(\d+)\s*瓶/);
if (capacity) {
if (capacity[1] && capacity[2]) attributes.capacity = `${capacity[1]}-${capacity[2]}`;
else if (capacity[3]) attributes.capacity = `${capacity[3]}`;
}
break;
case 'master_bedroom_cabinet':
case 'secondary_bedroom_cabinet':
if (text.includes('定制') || text.includes('整墙')) attributes.type = text.includes('整墙') ? '整墙定制' : '定制';
attributes.size = matchFirst(/(\d+\.?\d*)\s*(米|m)/);
if (text.includes('衣帽间')) attributes.feature = '带衣帽间';
if (text.includes('主卧')) attributes.remark = '主卧';
if (text.includes('次卧')) attributes.remark = '次卧';
break;
case 'shoe_cabinet':
if (text.includes('窄款') || text.includes('高型')) attributes.type = '窄款高型';
attributes.size = matchFirst(/(\d+\.?\d*)\s*(米|m)/);
if (text.includes('带座') || text.includes('可坐')) attributes.feature = '带座功能';
if (text.includes('收纳空间')) attributes.remark = '大容量收纳';
break;
default:
attributes.description = text;
break;
}
if (!attributes.style && data && data.decorationStyle) attributes.style = data.decorationStyle;
if (!attributes.color && data && data.decorationStyle) {
const color = data.decorationStyle.match(/(米白|白色|灰色|黑色|棕色|蓝色|暖色调|冷色调|灰白色系|原木色)/);
if (color) attributes.color = color[0];
}
if (!attributes.description && text) attributes.description = text;
return attributes;
},
buildProductDimensionRows(productData = null, data = {}, categoryKey = '') {
const dimensionLabels = [
{ key: 'styleColor', label: '偏好风格与颜色', field: 'preferenceStyleAndColor' },
{ key: 'material', label: '材质偏好', field: 'materialPreference' },
{ key: 'focus', label: '客户重点关注', field: 'customerFocusAreas' },
{ key: 'price', label: '价格敏感度', field: 'priceSensitivity' },
{ key: 'campaign', label: '活动参与度', field: 'activityParticipation' },
{ key: 'ordered', label: '客户疑虑点', field: 'customerConcerns' },
{ key: 'service', label: '交付服务', field: 'deliveryService' },
{ key: 'competitor', label: '竞品反馈', field: 'competitorFeedback' },
{ key: 'delivery', label: '交付周期', field: 'deliveryCycle' },
{ key: 'appearance', label: '产品颜值', field: 'productAppearance' }
];
// 判断productData是否是解析后的JSON对象
// 通过检查是否包含后端返回的JSON字段来判断
const isParsedJson = productData &&
typeof productData === 'object' &&
!Array.isArray(productData) &&
productData !== null &&
('preferenceStyleAndColor' in productData ||
'materialPreference' in productData ||
'customerConcerns' in productData);
const getValue = (field) => {
if (isParsedJson) {
// 从解析后的JSON对象中获取字段
const value = productData[field];
// 如果值是字符串,去除首尾空格;如果是其他类型,转为字符串
if (value === null || value === undefined || value === '') {
return '暂无信息';
}
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed || '暂无信息';
}
return String(value) || '暂无信息';
} else {
// 向后兼容从data对象中获取或使用默认值
// 这里productData可能是旧的attributes对象或null
const attributes = productData || {};
switch (field) {
case 'preferenceStyleAndColor':
const parts = [];
if (attributes.style) parts.push(attributes.style);
if (attributes.color) parts.push(attributes.color);
if (parts.length === 0 && data.decorationStyle) parts.push(data.decorationStyle);
return parts.join(' / ') || '暂无信息';
case 'materialPreference':
return attributes.material || '暂无信息';
case 'customerFocusAreas':
return attributes.feature || attributes.remark || '暂无信息';
case 'priceSensitivity':
return data.priceSensitivity || '暂无信息';
case 'activityParticipation':
return data.campaignPreference || data.activityParticipation || '暂无信息';
case 'customerConcerns':
return attributes.description || '暂无信息';
case 'deliveryService':
return data.serviceExpectation || '暂无信息';
case 'competitorFeedback':
return data.competitorFeedback || '暂无信息';
case 'deliveryCycle':
return data.deliveryCycle || '暂无信息';
case 'productAppearance':
return data.appearancePreference || attributes.style || '暂无信息';
default:
return '暂无信息';
}
}
};
return dimensionLabels.map(item => ({
label: item.label,
content: getValue(item.field) || '暂无信息'
}));
},
viewSummaryDetail(item) {
// 查看总结详情
console.log('查看总结详情', item);
}
}
}
</script>
<style scoped>
/* 详情内容 */
.detail-content {
flex: 1;
padding: 24rpx 32rpx;
background-color: #FFFFFF;
overflow-y: auto;
min-height: 0;
}
.summary-item {
padding: 24rpx 0;
border-bottom: 1px solid #F0F0F0;
display: flex;
flex-direction: column;
position: relative;
padding-right: 40rpx;
}
.summary-item:last-child {
border-bottom: none;
}
.summary-label {
margin-bottom: 12rpx;
}
.summary-label text {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
.summary-value {
flex: 1;
}
.summary-value text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
.summary-item uni-icons {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
/* 产品类别标签页 */
.product-category-tabs {
background-color: #FFFFFF;
border-bottom: 1px solid #E0E0E0;
margin-bottom: 24rpx;
padding: 0;
}
.category-tabs-scroll {
white-space: nowrap;
width: 100%;
}
.category-tabs-wrapper {
display: inline-flex;
padding: 0 32rpx;
}
.category-tab-item {
padding: 24rpx 16rpx;
position: relative;
margin-right: 32rpx;
white-space: nowrap;
}
.category-tab-item text {
font-size: 28rpx;
color: #666;
}
.category-tab-item.active text {
color: #333;
font-weight: 500;
}
.category-tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 16rpx;
right: 16rpx;
height: 4rpx;
background-color: #007AFF;
border-radius: 2rpx;
}
/* 产品需求清单表格 */
.product-requirement-table {
background-color: #FFFFFF;
padding: 0;
}
.requirement-title {
padding: 24rpx 32rpx;
font-size: 32rpx;
font-weight: 600;
color: #333;
border-bottom: 1px solid #F0F0F0;
}
.requirement-table {
padding: 0 32rpx 24rpx;
}
.requirement-row {
display: flex;
padding: 24rpx 0;
border-bottom: 1px solid #F0F0F0;
}
.requirement-row:last-child {
border-bottom: none;
}
.requirement-label {
min-width: 200rpx;
width: 200rpx;
padding-right: 24rpx;
}
.requirement-label text {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
.requirement-content {
flex: 1;
}
.requirement-content text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
/* 加载和空状态 */
.loading-container,
.empty-container {
padding: 80rpx 0;
text-align: center;
}
.loading-text,
.empty-text {
font-size: 28rpx;
color: #999;
}
</style>

View File

@@ -98,6 +98,21 @@
</view>
</uni-popup>
<!-- 录音文本内容弹框放在录音列表弹框之后确保层级更高 -->
<uni-popup ref="textModalPopup" type="center" :mask-click="true" @change="onTextModalChange" class="text-modal-popup">
<view class="text-modal">
<view class="text-modal-header">
<text class="text-modal-title">录音文本内容</text>
<view class="text-modal-close" @click="closeTextModal">
<uni-icons type="close" size="24" color="#333"></uni-icons>
</view>
</view>
<scroll-view class="text-modal-content" scroll-y>
<text class="text-modal-text">{{ currentRecordingText || '暂无录音文本' }}</text>
</scroll-view>
</view>
</uni-popup>
<!-- 服务记录详情弹窗 -->
<uni-popup ref="serviceDetailPopup" type="bottom" :mask-click="false">
<view class="service-detail-modal">
@@ -172,106 +187,10 @@
</view>
<!-- 会话总结内容 -->
<scroll-view class="detail-content" scroll-y v-if="activeDetailTab === 'summary'">
<!-- 客户类型家庭结构等总结信息在产品清单上方 -->
<view
class="summary-item"
v-for="(item, index) in summaryList"
:key="index"
@click="viewSummaryDetail(item)">
<view class="summary-label">
<text>{{ item.label }}</text>
</view>
<view class="summary-value">
<text>{{ item.value }}</text>
</view>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
<!-- 意向产品清单显示产品类别名称列表 -->
<view class="summary-item" v-if="productCategoryList.length > 0">
<view class="summary-label">
<text>意向产品清单</text>
</view>
<view class="summary-value">
<text>{{ getProductCategoryNames() }}</text>
</view>
</view>
<!-- 一句话总结 -->
<view class="summary-item" v-if="summarySentence">
<view class="summary-label">
<text>一句话总结</text>
</view>
<view class="summary-value">
<text>{{ summarySentence }}</text>
</view>
</view>
<!-- 成交关键点 -->
<view class="summary-item" v-if="dealKeyPoints">
<view class="summary-label">
<text>成交关键点</text>
</view>
<view class="summary-value">
<text>{{ dealKeyPoints }}</text>
</view>
</view>
<!-- 产品类别标签页 -->
<view class="product-category-tabs" v-if="productCategoryList.length > 0">
<scroll-view class="category-tabs-scroll" scroll-x>
<view class="category-tabs-wrapper">
<view
class="category-tab-item"
:class="{ active: activeProductCategory === category.key }"
v-for="(category, index) in productCategoryList"
:key="index"
@click="switchProductCategory(category.key)">
<text>{{ category.name }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 产品需求清单表格 -->
<view class="product-requirement-table" v-if="activeProductCategory && currentProductRequirement">
<view class="requirement-title">
<text>{{ getProductCategoryName(activeProductCategory) }}</text>
</view>
<view class="requirement-table">
<view
class="requirement-row"
v-for="(row, index) in currentProductRequirement.rows"
:key="index">
<view class="requirement-label">
<text>{{ row.label }}</text>
</view>
<view class="requirement-content">
<text>{{ row.content }}</text>
</view>
</view>
</view>
</view>
<!-- 如果没有产品类别数据显示提示确保categoryList已初始化后显示 -->
<view class="product-requirement-table" v-else-if="productCategoryList.length === 0 && furnitureAnalysisList.length > 0">
<view class="requirement-title">
<text>产品需求</text>
</view>
<view class="requirement-table">
<view class="requirement-row">
<view class="requirement-label">
<text>提示</text>
</view>
<view class="requirement-content">
<text>暂无产品需求信息</text>
</view>
</view>
</view>
</view>
</scroll-view>
<ConversationSummaryFurniture
v-if="activeDetailTab === 'summary'"
:serviceItem="currentServiceDetail"
:key="`summary-${currentServiceDetail.id || currentServiceDetail.rawData?.id || Date.now()}`" />
<!-- 服务表现内容 -->
<scroll-view class="detail-content" scroll-y v-if="activeDetailTab === 'performance'">
@@ -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;
}
</style>