diff --git a/pages/customer/components/ServiceList.vue b/pages/customer/components/ServiceList.vue index 281a16f..db367a3 100644 --- a/pages/customer/components/ServiceList.vue +++ b/pages/customer/components/ServiceList.vue @@ -32,12 +32,11 @@ class="service-item" v-for="(item, index) in list" :key="index" - @click="$emit('itemClick', item)"> + :class="{ 'service-item--active': selectedServiceItem && selectedServiceItem.id === item.id }"> {{ item.title }} - - 档案 - + + @@ -52,8 +51,36 @@ {{ item.customerName }} + + + + + 删除 + + + + 查看 + + + + AI分析 + + + + 录音 + + + + + @@ -73,7 +100,9 @@ data() { return { salesName: '', - customerName: '' + customerName: '', + selectedServiceItem: null, + showServiceActionMenu: false } }, methods: { @@ -89,6 +118,55 @@ }, onReachBottom() { this.$emit('reachBottom'); + }, + // 菜单按钮点击 + onMenuButtonClick(item) { + // 如果已经选中当前项,则关闭菜单 + if (this.selectedServiceItem && this.selectedServiceItem.id === item.id && this.showServiceActionMenu) { + this.closeServiceActionMenu(); + return; + } + + // 显示菜单 + this.selectedServiceItem = item; + this.showServiceActionMenu = true; + }, + // 关闭菜单 + closeServiceActionMenu() { + this.showServiceActionMenu = false; + this.selectedServiceItem = null; + }, + // 处理删除 + handleDelete(item) { + this.closeServiceActionMenu(); + this.$emit('menuAction', { + action: 'delete', + item: item + }); + }, + // 处理查看 + handleView(item) { + this.closeServiceActionMenu(); + this.$emit('menuAction', { + action: 'view', + item: item + }); + }, + // 处理AI分析 + handleAnalysis(item) { + this.closeServiceActionMenu(); + this.$emit('menuAction', { + action: 'analysis', + item: item + }); + }, + // 处理录音 + handleAudio(item) { + this.closeServiceActionMenu(); + this.$emit('menuAction', { + action: 'audio', + item: item + }); } } } @@ -193,6 +271,7 @@ padding: 32rpx; margin-bottom: 24rpx; box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); + position: relative; } .service-header { @@ -200,6 +279,7 @@ justify-content: space-between; align-items: center; margin-bottom: 16rpx; + position: relative; } .service-title { @@ -208,15 +288,58 @@ color: #333; } - .service-actions { + .service-menu-button { + width: 48rpx; + height: 48rpx; display: flex; align-items: center; + justify-content: center; + cursor: pointer; } - .archive-text { - font-size: 26rpx; - color: #999; - margin-right: 8rpx; + .service-action-menu { + position: absolute; + top: 60rpx; + right: 32rpx; + width: 160rpx; + background-color: #FFFFFF; + border-radius: 12rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); + z-index: 100; + overflow: hidden; + margin-right: 0; + } + + .service-action-menu-item { + padding: 24rpx 32rpx; + font-size: 28rpx; + color: #333; + text-align: center; + background-color: #FFFFFF; + } + + .service-action-menu-item:active { + background-color: #F5F5F5; + } + + .service-action-menu-item--danger { + color: #FF5722; + } + + .service-action-menu-divider { + height: 1rpx; + background-color: #E0E0E0; + margin: 0 16rpx; + } + + .service-action-menu-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: transparent; + z-index: 99; } .service-meta { diff --git a/pages/customer/customer.vue b/pages/customer/customer.vue index dbf11d1..d72f117 100644 --- a/pages/customer/customer.vue +++ b/pages/customer/customer.vue @@ -39,7 +39,8 @@ :total="serviceListTotal" @itemClick="viewDetail" @filterClick="refreshServiceList" - @reachBottom="onServiceListReachBottom" /> + @reachBottom="onServiceListReachBottom" + @menuAction="handleMenuAction" /> + + + + + + + + + 录音列表({{ audioSegmentsList.length }}) + + + + + + + + + + + {{ formatAudioFileName(item.audioFileOriginalName) }} + + + {{ truncateText(item.recordingText, 30) }} + 暂无录音文本 + + + {{ formatDateTime(item.endTime) }} + {{ formatDuration(item.duration) }} + {{ item.deviceNo }} + + + + 暂无录音记录 + + + + + @@ -495,6 +540,9 @@ todoMenuStyle: {}, // 下拉菜单样式(用于fixed定位) currentTodoItem: null, // 当前操作的待办事项 chapterSummaryList: [], // 章节概要列表,包含summary1, summary2, summary3 + audioSegmentsList: [], // 录音分段列表 + audioSegmentsLoading: false, // 录音分段加载状态 + playingAudioId: null, // 当前正在播放的录音ID customerList: [ { name: '李女士', @@ -510,32 +558,7 @@ { text: '客厅,卧室', color: 'blue' } ], phone: '185****3677' - }, - { - name: '彭总', - intent: '高', - serviceStaff: '沈易', - serviceCount: 3, - lastService: '2526-09-16', - tags: [ - { text: '新房装修', color: 'blue' }, - { text: '餐厅,客厅,卧室,定制', color: 'blue' }, - { text: '沙发,床,床垫,茶几,餐桌', color: 'blue' }, - { text: '关注产品颜值', color: 'blue' } - ], - budget: '预算2万-3万' - }, - { - name: '吴女士', - intent: '中', - serviceStaff: '沈易', - serviceCount: 3, - lastService: '2025-08-30', - tags: [ - { text: '新房装修', color: 'blue' }, - { text: '客厅,卧室,定制', color: 'blue' } - ] - } + } ] } }, @@ -614,6 +637,244 @@ } this.$refs.serviceDetailPopup.open(); }, + // 处理菜单操作 + async handleMenuAction({ action, item }) { + console.log('父组件收到菜单操作:', action, item); + switch (action) { + case 'view': + // 查看详情:打开详情弹窗 + break; + case 'analysis': + // AI分析:打开详情弹窗并切换到相关标签页 + break; + case 'audio': + // 录音详情:加载录音分段列表 + await this.loadAudioSegments(item); + break; + case 'delete': + // 删除服务记录 + await this.deleteServiceRecord(item); + break; + default: + console.warn('未知的菜单操作:', action); + } + }, + // 删除服务记录 + async deleteServiceRecord(item) { + console.log('删除服务记录,item:', item); + // 显示确认对话框 + const res = await new Promise((resolve) => { + uni.showModal({ + title: '确认删除', + content: '确定要删除这条服务记录吗?删除后无法恢复。', + confirmText: '删除', + confirmColor: '#FF3B30', + cancelText: '取消', + success: (result) => { + resolve(result.confirm); + }, + fail: () => { + resolve(false); + } + }); + }); + + if (!res) { + return; // 用户取消删除 + } + + try { + // 获取要删除的记录ID + const recordId = item.id || item.rawData?.id; + if (!recordId) { + uni.showToast({ + title: '无法获取记录ID', + icon: 'none' + }); + return; + } + + // 调用删除接口:/api/audioManagement/delete/{id} + const url = getApiUrl(`/api/audioManagement/delete/${recordId}`); + + // 获取认证信息 + 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 deleteRes = await uni.request({ + url: url, + method: 'DELETE', + header: headers, + timeout: 10000 + }); + + if (deleteRes.statusCode === 200 && deleteRes.data && deleteRes.data.success) { + uni.showToast({ + title: '删除成功', + icon: 'success' + }); + // 刷新列表 + this.serviceListPage.current = 1; + this.serviceList = []; + await this.loadServiceList(); + } else { + uni.showToast({ + title: deleteRes.data?.message || '删除失败', + icon: 'none' + }); + } + } catch (error) { + console.error('删除服务记录失败:', error); + uni.showToast({ + title: `删除失败: ${error.errMsg || error.message || '未知错误'}`, + icon: 'none' + }); + } + }, + // 加载录音分段列表 + async loadAudioSegments(item) { + try { + // 获取parentId,从item的rawData中获取id,或者直接使用id + const parentId = item.rawData?.id || item.id; + if (!parentId) { + uni.showToast({ + title: '无法获取录音ID', + icon: 'none' + }); + return; + } + + this.audioSegmentsLoading = true; + this.audioSegmentsList = []; + + uni.showLoading({ + title: '加载中...' + }); + + // 调用接口:/api/audioManagementSegments/listByParentId?parentId={parentId} + const url = `${getApiUrl('/api/audioManagementSegments/listByParentId')}?parentId=${parentId}`; + + // 获取认证信息 + 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: 'GET', + header: headers, + timeout: 10000 + }); + + uni.hideLoading(); + + if (res.statusCode === 200 && res.data && res.data.success) { + const segments = res.data.data || []; + console.log('录音分段列表:', segments); + + // 存储分段数据 + this.audioSegmentsList = segments; + this.audioSegmentsLoading = false; + + // 打开录音列表弹窗 + this.$refs.audioListPopup.open(); + } else { + uni.showToast({ + title: res.data?.message || '获取录音分段失败', + icon: 'none' + }); + } + } catch (error) { + uni.hideLoading(); + this.audioSegmentsLoading = false; + console.error('获取录音分段列表失败:', error); + uni.showToast({ + title: `获取录音分段失败: ${error.errMsg || error.message || '未知错误'}`, + icon: 'none' + }); + } + }, + // 关闭录音列表弹窗 + closeAudioList() { + this.$refs.audioListPopup.close(); + }, + // 格式化时长(秒转换为时分秒) + formatDuration(seconds) { + if (!seconds) return '00:00:00'; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; + }, + // 格式化日期时间 + formatDateTime(dateTime) { + if (!dateTime) return ''; + const date = new Date(dateTime); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + }, + // 格式化录音文件名:只显示第一个中划线之后和最后一个中划线之前的内容 + formatAudioFileName(fileName) { + if (!fileName) return '未命名录音'; + // 找到第一个中划线的位置 + const firstDashIndex = fileName.indexOf('-'); + if (firstDashIndex === -1) { + // 如果没有中划线,直接返回原文件名 + return fileName; + } + // 找到最后一个中划线的位置 + const lastDashIndex = fileName.lastIndexOf('-'); + if (firstDashIndex === lastDashIndex) { + // 如果只有一个中划线,返回中划线之后的内容 + return fileName.substring(firstDashIndex + 1); + } + // 返回第一个中划线之后到最后一个中划线之前的内容 + return fileName.substring(firstDashIndex + 1, lastDashIndex); + }, + // 截断文本,只显示指定长度 + truncateText(text, maxLength) { + if (!text) return ''; + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + '...'; + }, closeDetail() { this.$refs.serviceDetailPopup.close(); }, @@ -2684,5 +2945,101 @@ line-height: 1.8; } + /* 录音列表弹窗样式 */ + .audio-list-modal { + background-color: #FFFFFF; + border-radius: 32rpx 32rpx 0 0; + max-height: 90vh; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .audio-list-content { + flex: 1; + padding: 24rpx 32rpx; + box-sizing: border-box; + background-color: #F5F5F5; + } + + .audio-list-item { + background-color: #FFFFFF; + border-radius: 16rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); + } + + .audio-item-header { + margin-bottom: 16rpx; + display: flex; + align-items: center; + } + + .audio-play-btn { + width: 48rpx; + height: 48rpx; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12rpx; + cursor: pointer; + border-radius: 50%; + transition: background-color 0.2s; + } + + .audio-play-btn:active { + background-color: #F5F5F5; + } + + .audio-item-name { + font-size: 32rpx; + font-weight: 500; + color: #333; + flex: 1; + } + + .audio-item-info { + margin-bottom: 16rpx; + } + + .audio-item-text { + font-size: 28rpx; + color: #666; + line-height: 1.6; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + } + + .audio-item-text--empty { + color: #999; + font-style: italic; + } + + .audio-item-meta { + display: flex; + flex-wrap: nowrap; + gap: 16rpx; + padding-top: 16rpx; + border-top: 1px solid #F0F0F0; + align-items: center; + } + + .audio-meta-item { + font-size: 24rpx; + color: #999; + white-space: nowrap; + flex-shrink: 0; + } + + .audio-list-empty { + padding: 48rpx 0; + text-align: center; + color: #999; + font-size: 28rpx; + } +