Files
smartDriveEEUniApp/pages/furniture_customer/components/ServiceList.vue
2026-01-11 08:10:32 +08:00

553 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="service-list-container">
<view class="service-status-toolbar">
<text class="toolbar-total">{{ total || list.length }}</text>
<input
class="toolbar-input"
v-model="salesName"
placeholder="所属销售姓名"
placeholder-style="color: #b0b0b0"
confirm-type="search"
@confirm="handleRefresh"
@input="onInputChange"
/>
<input
class="toolbar-input"
v-model="customerName"
placeholder="客户姓名"
placeholder-style="color: #b0b0b0"
confirm-type="search"
@confirm="handleRefresh"
@input="onInputChange"
/>
<view class="toolbar-actions">
<view class="toolbar-btn toolbar-btn--refresh" @click="handleRefresh">
<uni-icons type="refresh" size="18" color="#2A68FF"></uni-icons>
<text>刷新</text>
</view>
</view>
</view>
<scroll-view class="service-list" scroll-y @scrolltolower="onReachBottom">
<view
class="service-item"
v-for="(item, index) in list"
:key="index"
:class="{ 'service-item--active': selectedServiceItem && selectedServiceItem.id === item.id }"
@click="handleItemClick(item)">
<view class="service-header">
<text class="service-title">{{ item.title }}</text>
<view class="service-menu-button" @click.stop="onMenuButtonClick(item)">
<uni-icons type="more-filled" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="service-meta">
<text class="service-date">{{ item.date }}</text>
<text class="service-duration" v-if="item.duration != null && item.duration > 0">录音时长{{ formatDuration(item.duration) }}</text>
</view>
<view class="service-recording-name" v-if="item.recordingName">
<text class="recording-name-label">记录名称</text>
<text class="recording-name-value">{{ item.recordingName }}</text>
</view>
<view class="service-content">
<text class="service-desc">{{ item.description }}</text>
</view>
<view class="service-customer">
<view class="customer-avatar">
<text class="avatar-text">{{ item.customerName.charAt(0) }}</text>
</view>
<text class="customer-name">{{ item.customerName }}</text>
</view>
<!-- 操作菜单 -->
<view
class="service-action-menu"
v-if="selectedServiceItem && selectedServiceItem.id === item.id && showServiceActionMenu"
@click.stop>
<view class="service-action-menu-item service-action-menu-item--danger" @click="handleDelete(item)">
<text>删除</text>
</view>
<view class="service-action-menu-divider"></view>
<view class="service-action-menu-item" @click="handleView(item)">
<text>查看</text>
</view>
<view class="service-action-menu-divider"></view>
<view class="service-action-menu-item" @click="handleAnalysis(item)">
<text>AI分析</text>
</view>
<view class="service-action-menu-divider"></view>
<view class="service-action-menu-item" @click="handleAudio(item)">
<text>录音</text>
</view>
</view>
</view>
</scroll-view>
<!-- 遮罩层点击关闭菜单 -->
<view
class="service-action-menu-mask"
v-if="showServiceActionMenu"
@click="closeServiceActionMenu">
</view>
</view>
</template>
<script>
import { getApiUrl } from '@/common/config.js';
export default {
name: 'ServiceList',
props: {
list: {
type: Array,
default: () => []
},
total: {
type: Number,
default: 0
}
},
data() {
return {
salesName: '',
customerName: '',
selectedServiceItem: null,
showServiceActionMenu: false
}
},
methods: {
onInputChange() {
// 输入框变化时,可以在这里添加实时搜索逻辑(如果需要)
},
handleRefresh() {
// 触发刷新事件,并传递查询参数
this.$emit('filterClick', {
salesName: this.salesName.trim(),
customerName: this.customerName.trim()
});
},
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分析
async handleAnalysis(item) {
this.closeServiceActionMenu();
try {
// 获取录音记录ID
const recordId = item.id || item.rawData?.id;
if (!recordId) {
uni.showToast({
title: '无法获取记录ID',
icon: 'none'
});
return;
}
// 获取完整的 AudioManagement 对象,优先使用 rawData如果没有则使用 item 的字段构建
const audioManagement = item.rawData || {
id: recordId
};
uni.showLoading({
title: 'AI分析中...'
});
// 调用AI分析接口/api/audioManagement/AIAnlyzById
const url = getApiUrl('/api/audioManagement/AIAnlyzById');
// 获取认证信息
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',
data: audioManagement,
header: headers,
timeout: 60000 // AI分析可能需要较长时间设置60秒超时
});
uni.hideLoading();
if (res.statusCode === 200 && res.data && res.data.success) {
uni.showToast({
title: res.data.message || 'AI分析请求已提交',
icon: 'success',
duration: 2000
});
// 如果返回了更新后的数据,通知父组件更新列表项
if (res.data.data) {
// 构建更新后的item数据
const updatedItem = {
...item,
rawData: res.data.data,
description: res.data.data.summary || item.description
};
// 通知父组件更新该项
this.$emit('updateItem', {
recordId: recordId,
data: updatedItem
});
// 触发详情查看事件,使用更新后的数据打开详情页
this.$emit('itemClick', updatedItem);
} else {
// 如果没有返回数据,仍然打开详情页
this.$emit('itemClick', item);
}
} else {
uni.showToast({
title: res.data?.message || 'AI分析失败',
icon: 'none',
duration: 2000
});
}
} catch (error) {
uni.hideLoading();
console.error('AI分析失败:', error);
uni.showToast({
title: `AI分析失败: ${error.errMsg || error.message || '未知错误'}`,
icon: 'none',
duration: 2000
});
}
},
// 处理录音
handleAudio(item) {
this.closeServiceActionMenu();
this.$emit('menuAction', {
action: 'audio',
item: item
});
},
// 处理列表项点击
handleItemClick(item) {
// 如果菜单正在显示,不触发列表项点击
if (this.showServiceActionMenu) {
return;
}
// 触发列表项点击事件,打开详情页面
this.$emit('itemClick', item);
},
// 格式化时长(分钟转换为时分秒格式)
formatDuration(minutes) {
// 确保是数字类型,后端返回的单位是分钟
const numMinutes = Number(minutes);
if (isNaN(numMinutes) || numMinutes < 0) return '00:00:00';
// 将分钟转换为总秒数,然后格式化为时分秒
const totalSeconds = Math.floor(numMinutes * 60); // 将分钟转为秒,向下取整
const hours = Math.floor(totalSeconds / 3600);
const mins = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
}
}
</script>
<style scoped>
.service-list-container {
/* 使用绝对定位从tab页底部开始到底部导航栏结束 */
position: absolute;
top: 160rpx; /* 导航栏(88rpx) + tab页(72rpx) = 160rpx */
left: 0;
right: 0;
bottom: 96rpx; /* 底部导航栏高度 */
display: flex;
flex-direction: column;
background-color: #F5F5F5;
padding: 24rpx 16rpx;
box-sizing: border-box;
}
.service-status-toolbar {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 8rpx; /* 增加间距,避免元素被挤压 */
padding: 12rpx 8rpx; /* 增加内边距,给搜索框更多空间 */
margin-bottom: 24rpx; /* 与列表项间距保持一致24rpx */
background-color: #F8F8FA;
border-radius: 8rpx;
border: 1px solid #EFEFF2;
overflow-x: auto;
min-height: 64rpx; /* 设置最小高度,确保搜索区域不被挤压 */
box-sizing: border-box;
flex-shrink: 0; /* 防止被压缩 */
}
.toolbar-total {
font-size: 24rpx;
color: #666;
white-space: nowrap;
flex-shrink: 0;
margin-right: 8rpx; /* 增加右边距,与搜索框保持距离 */
line-height: 1.2;
padding: 0 4rpx; /* 增加左右内边距 */
}
.toolbar-input {
flex: 1;
min-width: 120rpx; /* 增加最小宽度,避免被挤压变形 */
max-width: 200rpx; /* 设置最大宽度,保持合理比例 */
height: 56rpx; /* 稍微增加高度,提升可用性 */
line-height: 56rpx;
background-color: #F5F6FA;
border-radius: 8rpx;
padding: 0 12rpx; /* 增加左右内边距 */
font-size: 24rpx;
border: 1px solid transparent;
box-sizing: border-box;
flex-shrink: 1; /* 允许适当收缩,但保持最小宽度 */
}
.toolbar-actions {
display: flex;
align-items: center;
margin-left: auto;
flex-shrink: 0;
gap: 8rpx; /* 增加按钮之间的间距 */
}
.toolbar-btn {
padding: 0 12rpx; /* 增加左右内边距 */
height: 56rpx; /* 与输入框高度保持一致 */
min-width: 56rpx;
color: #2A68FF;
display: flex;
align-items: center;
justify-content: center;
gap: 4rpx; /* 增加图标和文字之间的间距 */
font-size: 24rpx;
font-weight: 400;
line-height: 1;
box-sizing: border-box;
}
.toolbar-btn text {
color: #2A68FF;
}
.service-list {
/* 使用flex填充剩余空间 */
flex: 1;
background-color: transparent;
box-sizing: border-box;
overflow-y: auto;
}
.service-item {
width: 100%;
box-sizing: border-box;
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
position: relative;
}
.service-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
position: relative;
}
.service-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.service-menu-button {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.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 {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 16rpx;
gap: 16rpx;
}
.service-date {
font-size: 24rpx;
color: #999;
}
.service-duration {
font-size: 24rpx;
color: #999;
}
.service-recording-name {
display: flex;
align-items: center;
margin-bottom: 16rpx;
font-size: 24rpx;
color: #666;
}
.recording-name-label {
color: #999;
margin-right: 8rpx;
}
.recording-name-value {
color: #333;
}
.service-content {
margin-bottom: 24rpx;
}
.service-desc {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.service-customer {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.customer-avatar {
width: 64rpx;
height: 64rpx;
background-color: #2196F3;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.avatar-text {
font-size: 32rpx;
color: #FFFFFF;
font-weight: 500;
}
.customer-name {
font-size: 28rpx;
color: #333;
}
.ai-tag {
padding-top: 16rpx;
border-top: 1px solid #F0F0F0;
}
.ai-tag text {
font-size: 24rpx;
color: #999;
}
</style>